Просмотр исходного кода

Merge pull request #471 from mirokuratczyk/measurement-library

Measurement library
Rod Hynes 7 лет назад
Родитель
Сommit
05ee6298a7

+ 1 - 0
.gitignore

@@ -16,6 +16,7 @@ AndroidLibrary/psi.aar
 *.o
 *.a
 *.so
+*.dylib
 
 # Folders
 _obj

+ 8 - 0
MeasurementLibrary/Makefile

@@ -0,0 +1,8 @@
+shared:
+	go build -buildmode=c-shared -o PsiphonTunnel.dylib PsiphonTunnel.go
+.PHONY: shared
+
+static:
+	go build -buildmode=c-archive -o PsiphonTunnel.a PsiphonTunnel.go
+
+.PHONY: static

+ 268 - 0
MeasurementLibrary/PsiphonTunnel.go

@@ -0,0 +1,268 @@
+package main
+
+import "C"
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"sync"
+	"time"
+
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon"
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
+)
+
+type StartResultCode int
+
+const (
+	StartResultCodeSuccess StartResultCode = iota
+	StartResultCodeTimeout
+	StartResultCodeOtherError
+)
+
+type NoticeEvent struct {
+	Data       map[string]interface{} `json:"data"`
+	NoticeType string                 `json:"noticeType"`
+}
+
+type StartResult struct {
+	Code           StartResultCode `json:"result_code"`
+	BootstrapTime  float64         `json:"bootstrap_time,omitempty"`
+	ErrorString    string          `json:"error,omitempty"`
+	HttpProxyPort  int             `json:"http_proxy_port,omitempty"`
+	SocksProxyPort int             `json:"socks_proxy_port,omitempty"`
+}
+
+type MeasurementTest struct {
+	controllerWaitGroup sync.WaitGroup
+	controllerCtx       context.Context
+	stopController      context.CancelFunc
+	httpProxyPort       int
+	socksProxyPort      int
+}
+
+var measurementTest MeasurementTest
+
+//export Start
+// Start starts the controller and returns once either of the following has occured: an active tunnel has been
+// established, the timeout has elapsed before an active tunnel could be established or an error has occured.
+//
+// Start returns a StartResult object serialized as a JSON string in the form of a null-terminated buffer of C chars.
+// Start will return,
+// On success:
+//   {
+//     "result_code": 0,
+//     "bootstrap_time": <time_to_establish_tunnel>,
+//     "http_proxy_port": <http_proxy_port_num>,
+//     "socks_proxy_port": <socks_proxy_port_num>
+//   }
+//
+// On timeout:
+//  {
+//    "result_code": 1,
+//    "error": <error message>
+//  }
+//
+// On other error:
+//   {
+//     "result_code": 2,
+//     "error": <error message>
+//   }
+//
+// networkID should be not be blank and should follow the format specified by
+// https://godoc.org/github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon#NetworkIDGetter.
+func Start(configJSON, embeddedServerEntryList, networkID string, timeout int64) *C.char {
+
+	// Load provided config
+
+	config, err := psiphon.LoadConfig([]byte(configJSON))
+	if err != nil {
+		return startErrorJson(err)
+	}
+
+	// Set network ID
+
+	if networkID != "" {
+		config.NetworkID = networkID
+	}
+
+	// All config fields should be set before calling commit
+
+	err = config.Commit()
+	if err != nil {
+		return startErrorJson(err)
+	}
+
+	// Setup signals
+
+	connected := make(chan bool)
+
+	testError := make(chan error)
+
+	// Set up notice handling
+
+	psiphon.SetNoticeWriter(psiphon.NewNoticeReceiver(
+		func(notice []byte) {
+
+			var event NoticeEvent
+
+			err := json.Unmarshal(notice, &event)
+			if err != nil {
+				err = errors.New(fmt.Sprintf("Failed to unmarshal json: %s", err.Error()))
+				select {
+				case testError <- err:
+				default:
+				}
+			}
+
+			if event.NoticeType == "ListeningHttpProxyPort" {
+				port := event.Data["port"].(float64)
+				measurementTest.httpProxyPort = int(port)
+			} else if event.NoticeType == "ListeningSocksProxyPort" {
+				port := event.Data["port"].(float64)
+				measurementTest.socksProxyPort = int(port)
+			} else if event.NoticeType == "Tunnels" {
+				count := event.Data["count"].(float64)
+				if count > 0 {
+					select {
+					case connected <- true:
+					default:
+					}
+				}
+			}
+		}))
+
+	// Initialize data store
+
+	err = psiphon.InitDataStore(config)
+	if err != nil {
+		return startErrorJson(err)
+	}
+
+	// Store embedded server entries
+
+	serverEntries, err := protocol.DecodeServerEntryList(
+		embeddedServerEntryList,
+		common.GetCurrentTimestamp(),
+		protocol.SERVER_ENTRY_SOURCE_EMBEDDED)
+	if err != nil {
+		return startErrorJson(err)
+	}
+
+	err = psiphon.StoreServerEntries(config, serverEntries, false)
+	if err != nil {
+		return startErrorJson(err)
+	}
+
+	// Run Psiphon
+
+	controller, err := psiphon.NewController(config)
+	if err != nil {
+		return startErrorJson(err)
+	}
+
+	measurementTest.controllerCtx, measurementTest.stopController = context.WithCancel(context.Background())
+
+	// Set start time
+
+	startTime := time.Now()
+
+	// Setup timeout signal
+
+	runtimeTimeout := time.Duration(timeout) * time.Second
+
+	timeoutSignal, cancelTimeout := context.WithTimeout(context.Background(), runtimeTimeout)
+	defer cancelTimeout()
+
+	// Run test
+
+	var result StartResult
+
+	measurementTest.controllerWaitGroup.Add(1)
+	go func() {
+		defer measurementTest.controllerWaitGroup.Done()
+		controller.Run(measurementTest.controllerCtx)
+
+		select {
+		case testError <- errors.New("controller.Run exited unexpectedly"):
+		default:
+		}
+	}()
+
+	// Wait for an active tunnel, timeout or error
+
+	select {
+	case <-connected:
+		result.Code = StartResultCodeSuccess
+		result.BootstrapTime = secondsBeforeNow(startTime)
+		result.HttpProxyPort = measurementTest.httpProxyPort
+		result.SocksProxyPort = measurementTest.socksProxyPort
+	case <-timeoutSignal.Done():
+		result.Code = StartResultCodeTimeout
+		err = timeoutSignal.Err()
+		if err != nil {
+			result.ErrorString = fmt.Sprintf("Timeout occured before Psiphon connected: %s", err.Error())
+		}
+		measurementTest.stopController()
+	case err := <-testError:
+		result.Code = StartResultCodeOtherError
+		result.ErrorString = err.Error()
+		measurementTest.stopController()
+	}
+
+	// Return result
+
+	return marshalStartResult(result)
+}
+
+//export Stop
+// Stop stops the controller if it is running and waits for it to clean up and exit.
+//
+// Stop should always be called after a successful call to Start to ensure the
+// controller is not left running.
+func Stop() {
+	if measurementTest.stopController != nil {
+		measurementTest.stopController()
+	}
+	measurementTest.controllerWaitGroup.Wait()
+}
+
+// secondsBeforeNow returns the delta seconds of the current time subtract startTime.
+func secondsBeforeNow(startTime time.Time) float64 {
+	delta := time.Now().Sub(startTime)
+	return delta.Seconds()
+}
+
+// marshalStartResult serializes a StartResult object as a JSON string in the form
+// of a null-terminated buffer of C chars.
+func marshalStartResult(result StartResult) *C.char {
+	resultJSON, err := json.Marshal(result)
+	if err != nil {
+		return C.CString(fmt.Sprintf("{\"result_code\":%d, \"error\": \"%s\"}", StartResultCodeOtherError, err.Error()))
+	}
+
+	return C.CString(string(resultJSON))
+}
+
+// startErrorJson returns a StartResult object serialized as a JSON string in the form
+// of a null-terminated buffer of C chars. The object's return result code will be set to
+// StartResultCodeOtherError (2) and its error string set to the error string of the provided error.
+//
+// The JSON will be in the form of:
+// {
+//   "result_code": 2,
+//   "error": <error message>
+// }
+func startErrorJson(err error) *C.char {
+	var result StartResult
+	result.Code = StartResultCodeOtherError
+	result.ErrorString = err.Error()
+
+	return marshalStartResult(result)
+}
+
+// main is a stub required by cgo.
+func main() {}

+ 12 - 0
MeasurementLibrary/README.md

@@ -0,0 +1,12 @@
+Psiphon Measurement Library README
+================================================================================
+
+Overview
+--------------------------------------------------------------------------------
+
+The Psiphon Measurement Library is specifically intended for use by network measurement tools, such as [OONI](https://ooni.io/), and is not intended for general use and distribution.
+
+Usage
+--------------------------------------------------------------------------------
+
+If you are using the Library in your app, please read the [USAGE.md](USAGE.md) instructions.

+ 16 - 0
MeasurementLibrary/USAGE.md

@@ -0,0 +1,16 @@
+# Using the Psiphon Measurement Library
+
+## Overview
+
+The Psiphon Measurement Library enables you to easily embed a test of Psiphon in your app.
+
+## Using the Psiphon network
+
+In order to use the Psiphon Measurement Library for testing the Psiphon network, you need to contact Psiphon to obtain connection parameters to use with your application. Please email us at [info@psiphon.ca](mailto:info@psiphon.ca).
+
+## Using the Library in your App
+
+**First step:** Review the sample code, located under `example`.
+This code provides an example of how to correctly use the measurement library.
+
+**Second step:** Review the comments for `Start` and `Stop` in [`PsiphonTunnel.go`](PsiphonTunnel.go). These functions make up the testing interface.

+ 1 - 0
MeasurementLibrary/example/.gitignore

@@ -0,0 +1 @@
+main

+ 12 - 0
MeasurementLibrary/example/Makefile

@@ -0,0 +1,12 @@
+main: PsiphonTunnel.dylib main.o
+	gcc PsiphonTunnel.dylib -o main main.o
+
+main.o: main.c
+	gcc -I.. -c main.c
+
+PsiphonTunnel.dylib: ../PsiphonTunnel.go
+	go build -buildmode=c-shared -o PsiphonTunnel.dylib ../PsiphonTunnel.go
+
+clean:
+	rm PsiphonTunnel.dylib PsiphonTunnel.h main main.o
+

+ 68 - 0
MeasurementLibrary/example/main.c

@@ -0,0 +1,68 @@
+#include "PsiphonTunnel.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+char *read_file(char *filename) {
+    char *buffer = NULL;
+    size_t size = 0;
+
+    FILE *fp = fopen(filename, "r");
+
+    if (!fp) {
+        return NULL;
+    }
+
+    fseek(fp, 0, SEEK_END);
+    size = ftell(fp); 
+
+    rewind(fp);
+    buffer = malloc((size + 1) * sizeof(*buffer));
+
+    fread(buffer, size, 1, fp);
+    buffer[size] = '\0';
+
+    return buffer;
+}
+
+int main(int argc, char *argv[]) {
+    
+    // load config
+    char * const default_config = "psiphon_config";
+
+    char * config = argv[1];
+
+    if (!config) {
+        config = default_config;
+        printf("Using default config file: %s\n", default_config);
+    }
+
+    char *file_contents = read_file(config);
+    if (!file_contents) {
+        printf("Could not find config file: %s\n", config);
+        return 1;
+    }
+
+    GoString psiphon_config = {file_contents, strlen(file_contents)};
+
+    // set server list
+    GoString serverList = {};
+
+    // set timout
+    long long timeout = 60;
+
+    // set network ID
+    char * const test_network_id = "TEST";
+    GoString network_id = {test_network_id, strlen(test_network_id)};
+
+    // start will return once Psiphon connects or does not connect for timeout seconds
+    char *result = Start(psiphon_config, serverList, network_id, timeout);
+    Stop();
+
+    // print results
+    printf("Result: %s\n", result);
+
+    // cleanup
+    free(result);
+}
+