Explorar o código

Merge pull request #577 from mirokuratczyk/feedback-upload

Add feedback upload mode to ConsoleClient
Rod Hynes %!s(int64=5) %!d(string=hai) anos
pai
achega
51227d533f
Modificáronse 1 ficheiros con 173 adicións e 63 borrados
  1. 173 63
      ConsoleClient/main.go

+ 173 - 63
ConsoleClient/main.go

@@ -64,6 +64,20 @@ func main() {
 	flag.BoolVar(&versionDetails, "version", false, "print build information and exit")
 	flag.BoolVar(&versionDetails, "v", false, "print build information and exit")
 
+	var feedbackUpload bool
+	flag.BoolVar(&feedbackUpload, "feedbackUpload", false,
+		"Run in feedback upload mode to send a feedback package to Psiphon Inc.\n"+
+			"The feedback package will be read as a UTF-8 encoded string from stdin.\n"+
+			"Informational notices will be written to stdout. If the upload succeeds,\n"+
+			"the process will exit with status code 0; otherwise, the process will\n"+
+			"exit with status code 1. A feedback compatible config must be specified\n"+
+			"with the \"-config\" flag. Config must be provided by Psiphon Inc.")
+
+	var feedbackUploadPath string
+	flag.StringVar(&feedbackUploadPath, "feedbackUploadPath", "",
+		"The path at which to upload the feedback package when the \"-feedbackUpload\"\n"+
+			"flag is provided. Must be provided by Psiphon Inc.")
+
 	var tunDevice, tunBindInterface, tunPrimaryDNS, tunSecondaryDNS string
 	if tun.IsSupported() {
 
@@ -221,79 +235,44 @@ func main() {
 
 	psiphon.NoticeBuildInfo()
 
-	// Initialize data store
+	var worker Worker
 
-	err = psiphon.OpenDataStore(config)
-	if err != nil {
-		psiphon.NoticeError("error initializing datastore: %s", err)
-		os.Exit(1)
-	}
-	defer psiphon.CloseDataStore()
-
-	// Handle optional embedded server list file parameter
-	// If specified, the embedded server list is loaded and stored. When there
-	// are no server candidates at all, we wait for this import to complete
-	// before starting the Psiphon controller. Otherwise, we import while
-	// concurrently starting the controller to minimize delay before attempting
-	// to connect to existing candidate servers.
-	// If the import fails, an error notice is emitted, but the controller is
-	// still started: either existing candidate servers may suffice, or the
-	// remote server list fetch may obtain candidate servers.
-	if embeddedServerEntryListFilename != "" {
-		embeddedServerListWaitGroup := new(sync.WaitGroup)
-		embeddedServerListWaitGroup.Add(1)
-		go func() {
-			defer embeddedServerListWaitGroup.Done()
-			serverEntryList, err := ioutil.ReadFile(embeddedServerEntryListFilename)
-			if err != nil {
-				psiphon.NoticeError("error loading embedded server entry list file: %s", err)
-				return
-			}
-			// TODO: stream embedded server list data? also, the cast makes an unnecessary copy of a large buffer?
-			serverEntries, err := protocol.DecodeServerEntryList(
-				string(serverEntryList),
-				common.GetCurrentTimestamp(),
-				protocol.SERVER_ENTRY_SOURCE_EMBEDDED)
-			if err != nil {
-				psiphon.NoticeError("error decoding embedded server entry list file: %s", err)
-				return
-			}
-			// Since embedded server list entries may become stale, they will not
-			// overwrite existing stored entries for the same server.
-			err = psiphon.StoreServerEntries(config, serverEntries, false)
-			if err != nil {
-				psiphon.NoticeError("error storing embedded server entry list data: %s", err)
-				return
-			}
-		}()
-
-		if psiphon.CountServerEntries() == 0 {
-			embeddedServerListWaitGroup.Wait()
-		} else {
-			defer embeddedServerListWaitGroup.Wait()
+	if feedbackUpload {
+		// Feedback upload mode
+		worker = &FeedbackWorker{
+			feedbackUploadPath: feedbackUploadPath,
+		}
+	} else {
+		// Tunnel mode
+		worker = &TunnelWorker{
+			embeddedServerEntryListFilename: embeddedServerEntryListFilename,
 		}
 	}
 
-	// Run Psiphon
-
-	controller, err := psiphon.NewController(config)
+	err = worker.Init(config)
 	if err != nil {
-		psiphon.NoticeError("error creating controller: %s", err)
+		psiphon.NoticeError("error in init: %s", err)
 		os.Exit(1)
 	}
 
-	controllerCtx, stopController := context.WithCancel(context.Background())
-	defer stopController()
+	workCtx, stopWork := context.WithCancel(context.Background())
+	defer stopWork()
 
-	controllerWaitGroup := new(sync.WaitGroup)
-	controllerWaitGroup.Add(1)
+	workWaitGroup := new(sync.WaitGroup)
+	workWaitGroup.Add(1)
 	go func() {
-		defer controllerWaitGroup.Done()
-		controller.Run(controllerCtx)
+		defer workWaitGroup.Done()
+
+		err := worker.Run(workCtx)
+		if err != nil {
+			psiphon.NoticeError("%s", err)
+			stopWork()
+			os.Exit(1)
+		}
 
 		// Signal the <-controllerCtx.Done() case below. If the <-systemStopSignal
 		// case already called stopController, this is a noop.
-		stopController()
+		stopWork()
 	}()
 
 	systemStopSignal := make(chan os.Signal, 1)
@@ -317,10 +296,10 @@ func main() {
 				profileSampleDurationSeconds)
 		case <-systemStopSignal:
 			psiphon.NoticeInfo("shutdown by system")
-			stopController()
-			controllerWaitGroup.Wait()
+			stopWork()
+			workWaitGroup.Wait()
 			exit = true
-		case <-controllerCtx.Done():
+		case <-workCtx.Done():
 			psiphon.NoticeInfo("shutdown by controller")
 			exit = true
 		}
@@ -369,3 +348,134 @@ func (p *tunProvider) GetPrimaryDnsServer() string {
 func (p *tunProvider) GetSecondaryDnsServer() string {
 	return p.secondaryDNS
 }
+
+// Worker creates a protocol around the different run modes provided by the
+// compiled executable.
+type Worker interface {
+	// Init is called once for the worker to perform any initialization.
+	Init(config *psiphon.Config) error
+	// Run is called once, after Init(..), for the worker to perform its
+	// work. The provided context should control the lifetime of the work
+	// being performed.
+	Run(ctx context.Context) error
+}
+
+// TunnelWorker is the Worker protocol implementation used for tunnel mode.
+type TunnelWorker struct {
+	embeddedServerEntryListFilename string
+	controller                      *psiphon.Controller
+}
+
+// Init implements the Worker interface.
+func (w *TunnelWorker) Init(config *psiphon.Config) error {
+
+	// Initialize data store
+
+	err := psiphon.OpenDataStore(config)
+	if err != nil {
+		psiphon.NoticeError("error initializing datastore: %s", err)
+		os.Exit(1)
+	}
+
+	// Handle optional embedded server list file parameter
+	// If specified, the embedded server list is loaded and stored. When there
+	// are no server candidates at all, we wait for this import to complete
+	// before starting the Psiphon controller. Otherwise, we import while
+	// concurrently starting the controller to minimize delay before attempting
+	// to connect to existing candidate servers.
+	// If the import fails, an error notice is emitted, but the controller is
+	// still started: either existing candidate servers may suffice, or the
+	// remote server list fetch may obtain candidate servers.
+	if w.embeddedServerEntryListFilename != "" {
+		embeddedServerListWaitGroup := new(sync.WaitGroup)
+		embeddedServerListWaitGroup.Add(1)
+		go func() {
+			defer embeddedServerListWaitGroup.Done()
+			serverEntryList, err := ioutil.ReadFile(w.embeddedServerEntryListFilename)
+			if err != nil {
+				psiphon.NoticeError("error loading embedded server entry list file: %s", err)
+				return
+			}
+			// TODO: stream embedded server list data? also, the cast makes an unnecessary copy of a large buffer?
+			serverEntries, err := protocol.DecodeServerEntryList(
+				string(serverEntryList),
+				common.GetCurrentTimestamp(),
+				protocol.SERVER_ENTRY_SOURCE_EMBEDDED)
+			if err != nil {
+				psiphon.NoticeError("error decoding embedded server entry list file: %s", err)
+				return
+			}
+			// Since embedded server list entries may become stale, they will not
+			// overwrite existing stored entries for the same server.
+			err = psiphon.StoreServerEntries(config, serverEntries, false)
+			if err != nil {
+				psiphon.NoticeError("error storing embedded server entry list data: %s", err)
+				return
+			}
+		}()
+
+		if psiphon.CountServerEntries() == 0 {
+			embeddedServerListWaitGroup.Wait()
+		} else {
+			defer embeddedServerListWaitGroup.Wait()
+		}
+	}
+
+	controller, err := psiphon.NewController(config)
+	if err != nil {
+		psiphon.NoticeError("error creating controller: %s", err)
+		return errors.Trace(err)
+	}
+	w.controller = controller
+
+	return nil
+}
+
+// Run implements the Worker interface.
+func (w *TunnelWorker) Run(ctx context.Context) error {
+	defer psiphon.CloseDataStore()
+
+	w.controller.Run(ctx)
+	return nil
+}
+
+// FeedbackWorker is the Worker protocol implementation used for feedback
+// upload mode.
+type FeedbackWorker struct {
+	config             *psiphon.Config
+	feedbackUploadPath string
+}
+
+// Init implements the Worker interface.
+func (f *FeedbackWorker) Init(config *psiphon.Config) error {
+
+	// The datastore is not opened here, with psiphon.OpenDatastore,
+	// because it is opened/closed transiently in the psiphon.SendFeedback
+	// operation. We do not want to contest database access incase another
+	// process needs to use the database. E.g. a process running in tunnel
+	// mode, which will fail if it cannot aquire a lock on the database
+	// within a short period of time.
+
+	f.config = config
+
+	return nil
+}
+
+// Run implements the Worker interface.
+func (f *FeedbackWorker) Run(ctx context.Context) error {
+
+	// TODO: cancel blocking read when worker context cancelled?
+	diagnostics, err := ioutil.ReadAll(os.Stdin)
+	if err != nil {
+		return errors.Trace(err)
+	}
+
+	err = psiphon.SendFeedback(ctx, f.config, string(diagnostics), f.feedbackUploadPath)
+	if err != nil {
+		return errors.TraceMsg(err, "FeedbackUpload: upload failed")
+	}
+
+	psiphon.NoticeInfo("FeedbackUpload: upload succeeded")
+
+	return nil
+}