|
|
@@ -5,10 +5,14 @@ import "C"
|
|
|
import (
|
|
|
"context"
|
|
|
"encoding/json"
|
|
|
+ "errors"
|
|
|
"fmt"
|
|
|
+ "sync"
|
|
|
"time"
|
|
|
|
|
|
- "github.com/Psiphon-Labs/psiphon-tunnel-core/MobileLibrary/psi"
|
|
|
+ "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 NoticeEvent struct {
|
|
|
@@ -16,135 +20,188 @@ type NoticeEvent struct {
|
|
|
NoticeType string `json:"noticeType"`
|
|
|
}
|
|
|
|
|
|
-type PsiphonProvider struct {
|
|
|
- connected chan bool
|
|
|
- err chan error
|
|
|
- stopped chan bool
|
|
|
- networkID string
|
|
|
+type TestResult struct {
|
|
|
+ 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"`
|
|
|
}
|
|
|
|
|
|
-func (pp PsiphonProvider) Notice(noticeJSON string) {
|
|
|
- var event NoticeEvent
|
|
|
+type MeasurementTest struct {
|
|
|
+ controllerWaitGroup sync.WaitGroup
|
|
|
+ controllerCtx context.Context
|
|
|
+ stopController context.CancelFunc
|
|
|
+ httpProxyPort int
|
|
|
+ socksProxyPort int
|
|
|
+}
|
|
|
+
|
|
|
+var measurementTest MeasurementTest
|
|
|
+
|
|
|
+//export Start
|
|
|
+func Start(configJSON, embeddedServerEntryList, networkID string, timeout int64) *C.char {
|
|
|
|
|
|
- err := json.Unmarshal([]byte(noticeJSON), &event)
|
|
|
+ // Load provided config
|
|
|
+
|
|
|
+ config, err := psiphon.LoadConfig([]byte(configJSON))
|
|
|
if err != nil {
|
|
|
- select {
|
|
|
- case pp.err <- err:
|
|
|
- default:
|
|
|
- }
|
|
|
- return
|
|
|
+ return errorJSONForC(err)
|
|
|
}
|
|
|
|
|
|
- if event.NoticeType == "Tunnels" {
|
|
|
- count := event.Data["count"].(float64)
|
|
|
- if count > 0 {
|
|
|
- select {
|
|
|
- case pp.connected <- true:
|
|
|
- default:
|
|
|
- }
|
|
|
- }
|
|
|
+ // Set network ID
|
|
|
+
|
|
|
+ if networkID != "" {
|
|
|
+ config.NetworkID = networkID
|
|
|
}
|
|
|
-}
|
|
|
|
|
|
-func (pp PsiphonProvider) HasNetworkConnectivity() int {
|
|
|
- return 1
|
|
|
-}
|
|
|
+ // All config fields should be set before calling commit
|
|
|
|
|
|
-func (pp PsiphonProvider) BindToDevice(fileDescriptor int) (string, error) {
|
|
|
- return "", nil
|
|
|
-}
|
|
|
+ err = config.Commit()
|
|
|
+ if err != nil {
|
|
|
+ return errorJSONForC(err)
|
|
|
+ }
|
|
|
|
|
|
-func (pp PsiphonProvider) IPv6Synthesize(IPv4Addr string) string {
|
|
|
- return "::1"
|
|
|
-}
|
|
|
+ // Setup signals
|
|
|
|
|
|
-func (pp PsiphonProvider) GetPrimaryDnsServer() string {
|
|
|
- return "8.8.8.8"
|
|
|
-}
|
|
|
+ connected := make(chan bool)
|
|
|
|
|
|
-func (pp PsiphonProvider) GetSecondaryDnsServer() string {
|
|
|
- return "8.8.8.8"
|
|
|
-}
|
|
|
+ testError := make(chan error)
|
|
|
|
|
|
-func (pp PsiphonProvider) GetNetworkID() string {
|
|
|
- return pp.networkID
|
|
|
-}
|
|
|
+ // Set up notice handling
|
|
|
|
|
|
-var provider PsiphonProvider
|
|
|
+ psiphon.SetNoticeWriter(psiphon.NewNoticeReceiver(
|
|
|
+ func(notice []byte) {
|
|
|
|
|
|
-type StartResult struct {
|
|
|
- BootstrapTime float64 `json:"bootstrap_time"`
|
|
|
- ErrorString string `json:"error,omitempty"`
|
|
|
-}
|
|
|
+ var event NoticeEvent
|
|
|
|
|
|
-//export Start
|
|
|
-func Start(configJSON,
|
|
|
- embeddedServerEntryList, networkID string, timeout int64) *C.char {
|
|
|
+ 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:
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- var result StartResult
|
|
|
+ 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:
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }))
|
|
|
|
|
|
- provider.networkID = networkID
|
|
|
- provider.connected = make(chan bool)
|
|
|
- provider.stopped = make(chan bool)
|
|
|
- provider.err = make(chan error)
|
|
|
+ // Initialize data store
|
|
|
|
|
|
- runtimeTimeout := time.Duration(timeout) * time.Second
|
|
|
- startTime := time.Now().UTC()
|
|
|
+ err = psiphon.InitDataStore(config)
|
|
|
+ if err != nil {
|
|
|
+ return errorJSONForC(err)
|
|
|
+ }
|
|
|
|
|
|
- connectedCtx, cancel := context.WithTimeout(context.Background(), runtimeTimeout)
|
|
|
- defer cancel()
|
|
|
+ // Store embedded server entries
|
|
|
|
|
|
- err := psi.Start(configJSON, embeddedServerEntryList, "", provider, false, false)
|
|
|
+ serverEntries, err := protocol.DecodeServerEntryList(
|
|
|
+ embeddedServerEntryList,
|
|
|
+ common.GetCurrentTimestamp(),
|
|
|
+ protocol.SERVER_ENTRY_SOURCE_EMBEDDED)
|
|
|
if err != nil {
|
|
|
- return errorJsonForC(err)
|
|
|
+ return errorJSONForC(err)
|
|
|
}
|
|
|
|
|
|
+ err = psiphon.StoreServerEntries(config, serverEntries, false)
|
|
|
+ if err != nil {
|
|
|
+ return errorJSONForC(err)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Run Psiphon
|
|
|
+
|
|
|
+ controller, err := psiphon.NewController(config)
|
|
|
+ if err != nil {
|
|
|
+ return errorJSONForC(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 TestResult
|
|
|
+
|
|
|
+ measurementTest.controllerWaitGroup.Add(1)
|
|
|
+ go func() {
|
|
|
+ defer measurementTest.controllerWaitGroup.Done()
|
|
|
+ controller.Run(measurementTest.controllerCtx)
|
|
|
+
|
|
|
+ select {
|
|
|
+ case testError <- errors.New("controller.Run exited unexpectedly"):
|
|
|
+ default:
|
|
|
+ }
|
|
|
+
|
|
|
+ // This is a noop if stopController was already called
|
|
|
+ measurementTest.stopController()
|
|
|
+ }()
|
|
|
+
|
|
|
+ // Wait for a stop signal, then stop Psiphon and exit
|
|
|
+
|
|
|
select {
|
|
|
- case <-connectedCtx.Done():
|
|
|
- result.BootstrapTime = bootstrapTime(startTime)
|
|
|
- err = connectedCtx.Err()
|
|
|
+ case <-connected:
|
|
|
+ result.BootstrapTime = secondsBeforeNow(startTime)
|
|
|
+ result.HttpProxyPort = measurementTest.httpProxyPort
|
|
|
+ result.SocksProxyPort = measurementTest.socksProxyPort
|
|
|
+ case <-timeoutSignal.Done():
|
|
|
+ err = timeoutSignal.Err()
|
|
|
if err != nil {
|
|
|
- result.ErrorString = err.Error()
|
|
|
+ result.ErrorString = fmt.Sprintf("Timeout occured before Psiphon connected: %s", err.Error())
|
|
|
+ } else {
|
|
|
+ result.ErrorString = "Timeout cancelled before Psiphon connected"
|
|
|
}
|
|
|
- case <-provider.connected:
|
|
|
- result.BootstrapTime = bootstrapTime(startTime)
|
|
|
- cancel()
|
|
|
- case <-provider.stopped:
|
|
|
- result.BootstrapTime = bootstrapTime(startTime)
|
|
|
- result.ErrorString = "stop signalled before client connected"
|
|
|
- cancel()
|
|
|
- case err := <-provider.err:
|
|
|
- result.BootstrapTime = bootstrapTime(startTime)
|
|
|
+ case err := <-testError:
|
|
|
result.ErrorString = err.Error()
|
|
|
- cancel()
|
|
|
}
|
|
|
|
|
|
+ // Return result
|
|
|
+
|
|
|
resultJSON, err := json.Marshal(result)
|
|
|
if err != nil {
|
|
|
- return errorJsonForC(err)
|
|
|
+ return errorJSONForC(err)
|
|
|
}
|
|
|
|
|
|
return C.CString(string(resultJSON))
|
|
|
}
|
|
|
|
|
|
-func bootstrapTime(startTime time.Time) float64 {
|
|
|
- delta := time.Now().UTC().Sub(startTime)
|
|
|
- return delta.Seconds()
|
|
|
+//export Stop
|
|
|
+func Stop() {
|
|
|
+ if measurementTest.stopController != nil {
|
|
|
+ measurementTest.stopController()
|
|
|
+ }
|
|
|
+ measurementTest.controllerWaitGroup.Wait()
|
|
|
}
|
|
|
|
|
|
-func errorJsonForC(err error) *C.char {
|
|
|
- return C.CString(fmt.Sprintf("{\"error\": \"%s\"}", err.Error()))
|
|
|
+func secondsBeforeNow(startTime time.Time) float64 {
|
|
|
+ delta := time.Now().Sub(startTime)
|
|
|
+ return delta.Seconds()
|
|
|
}
|
|
|
|
|
|
-//export Stop
|
|
|
-func Stop() bool {
|
|
|
- psi.Stop()
|
|
|
- select {
|
|
|
- case provider.stopped <- true:
|
|
|
- default:
|
|
|
- }
|
|
|
-
|
|
|
- return true
|
|
|
+func errorJSONForC(err error) *C.char {
|
|
|
+ return C.CString(fmt.Sprintf("{\"error\": \"%s\"}", err.Error()))
|
|
|
}
|
|
|
|
|
|
func main() {} // stub required by cgo
|