Example #1
0
// connectedAPIRequestHandler implements the "connected" API request.
// Clients make the connected request once a tunnel connection has been
// established and at least once per day. The last_connected input value,
// which should be a connected_timestamp output from a previous connected
// response, is used to calculate unique user stats.
func connectedAPIRequestHandler(
	support *SupportServices,
	geoIPData GeoIPData,
	params requestJSONObject) ([]byte, error) {

	err := validateRequestParams(support, params, connectedRequestParams)
	if err != nil {
		return nil, common.ContextError(err)
	}

	log.LogRawFieldsWithTimestamp(
		getRequestLogFields(
			support,
			"connected",
			geoIPData,
			params,
			connectedRequestParams))

	connectedResponse := common.ConnectedResponse{
		ConnectedTimestamp: common.TruncateTimestampToHour(common.GetCurrentTimestamp()),
	}

	responsePayload, err := json.Marshal(connectedResponse)
	if err != nil {
		return nil, common.ContextError(err)
	}

	return responsePayload, nil
}
// Directly call DecodeServerEntry and ValidateServerEntry with invalid inputs
func TestInvalidServerEntries(t *testing.T) {

	testCases := [2]string{_INVALID_WINDOWS_REGISTRY_LEGACY_SERVER_ENTRY, _INVALID_MALFORMED_IP_ADDRESS_SERVER_ENTRY}

	for _, testCase := range testCases {
		encodedServerEntry := hex.EncodeToString([]byte(testCase))
		serverEntry, err := DecodeServerEntry(
			encodedServerEntry, common.GetCurrentTimestamp(), SERVER_ENTRY_SOURCE_EMBEDDED)
		if err != nil {
			t.Error(err.Error())
		}
		err = ValidateServerEntry(serverEntry)
		if err == nil {
			t.Error("server entry should not validate: %s", testCase)
		}
	}
}
func storeServerEntries(serverList string) error {

	serverEntries, err := protocol.DecodeAndValidateServerEntryList(
		serverList,
		common.GetCurrentTimestamp(),
		protocol.SERVER_ENTRY_SOURCE_REMOTE)
	if err != nil {
		return common.ContextError(err)
	}

	// TODO: record stats for newly discovered servers

	err = StoreServerEntries(serverEntries, true)
	if err != nil {
		return common.ContextError(err)
	}

	return nil
}
// RecordRemoteServerListStat records a completed common or OSL
// remote server list resource download. These stats use the same
// persist-until-reported mechanism described in RecordTunnelStats.
func RecordRemoteServerListStat(
	url, etag string) error {

	remoteServerListStat := struct {
		ClientDownloadTimestamp string `json:"client_download_timestamp"`
		URL                     string `json:"url"`
		ETag                    string `json:"etag"`
	}{
		common.TruncateTimestampToHour(common.GetCurrentTimestamp()),
		url,
		etag,
	}

	remoteServerListStatJson, err := json.Marshal(remoteServerListStat)
	if err != nil {
		return common.ContextError(err)
	}

	return StorePersistentStat(
		PERSISTENT_STAT_TYPE_REMOTE_SERVER_LIST, remoteServerListStatJson)
}
// DecodeAndValidateServerEntryList should return 2 valid decoded entries from the input list of 4
func TestDecodeAndValidateServerEntryList(t *testing.T) {

	testEncodedServerEntryList := hex.EncodeToString([]byte(_VALID_NORMAL_SERVER_ENTRY)) + "\n" +
		hex.EncodeToString([]byte(_VALID_BLANK_LEGACY_SERVER_ENTRY)) + "\n" +
		hex.EncodeToString([]byte(_INVALID_WINDOWS_REGISTRY_LEGACY_SERVER_ENTRY)) + "\n" +
		hex.EncodeToString([]byte(_INVALID_MALFORMED_IP_ADDRESS_SERVER_ENTRY))

	serverEntries, err := DecodeAndValidateServerEntryList(
		testEncodedServerEntryList, common.GetCurrentTimestamp(), SERVER_ENTRY_SOURCE_EMBEDDED)
	if err != nil {
		t.Error(err.Error())
		t.FailNow()
	}
	if len(serverEntries) != 2 {
		t.Error("unexpected number of valid server entries")
	}
	for _, serverEntry := range serverEntries {
		if serverEntry.IpAddress != _EXPECTED_IP_ADDRESS {
			t.Error("unexpected IP address in decoded server entry: %s", serverEntry.IpAddress)
		}
	}
}
// newTargetServerEntryIterator is a helper for initializing the TargetServerEntry case
func newTargetServerEntryIterator(config *Config) (iterator *ServerEntryIterator, err error) {
	serverEntry, err := protocol.DecodeServerEntry(
		config.TargetServerEntry, common.GetCurrentTimestamp(), protocol.SERVER_ENTRY_SOURCE_TARGET)
	if err != nil {
		return nil, err
	}
	if config.EgressRegion != "" && serverEntry.Region != config.EgressRegion {
		return nil, errors.New("TargetServerEntry does not support EgressRegion")
	}
	if config.TunnelProtocol != "" {
		// Note: same capability/protocol mapping as in StoreServerEntry
		requiredCapability := strings.TrimSuffix(config.TunnelProtocol, "-OSSH")
		if !common.Contains(serverEntry.Capabilities, requiredCapability) {
			return nil, errors.New("TargetServerEntry does not support TunnelProtocol")
		}
	}
	iterator = &ServerEntryIterator{
		isTargetServerEntryIterator: true,
		hasNextTargetServerEntry:    true,
		targetServerEntry:           serverEntry,
	}
	NoticeInfo("using TargetServerEntry: %s", serverEntry.IpAddress)
	return iterator, nil
}
Example #7
0
func Start(
	configJson, embeddedServerEntryList string,
	provider PsiphonProvider,
	useDeviceBinder bool) error {

	controllerMutex.Lock()
	defer controllerMutex.Unlock()

	if controller != nil {
		return fmt.Errorf("already started")
	}

	config, err := psiphon.LoadConfig([]byte(configJson))
	if err != nil {
		return fmt.Errorf("error loading configuration file: %s", err)
	}
	config.NetworkConnectivityChecker = provider

	if useDeviceBinder {
		config.DeviceBinder = provider
		config.DnsServerGetter = provider
	}

	psiphon.SetNoticeOutput(psiphon.NewNoticeReceiver(
		func(notice []byte) {
			provider.Notice(string(notice))
		}))

	psiphon.NoticeBuildInfo()

	// TODO: should following errors be Notices?

	err = psiphon.InitDataStore(config)
	if err != nil {
		return fmt.Errorf("error initializing datastore: %s", err)
	}

	serverEntries, err := protocol.DecodeAndValidateServerEntryList(
		embeddedServerEntryList,
		common.GetCurrentTimestamp(),
		protocol.SERVER_ENTRY_SOURCE_EMBEDDED)
	if err != nil {
		return fmt.Errorf("error decoding embedded server entry list: %s", err)
	}
	err = psiphon.StoreServerEntries(serverEntries, false)
	if err != nil {
		return fmt.Errorf("error storing embedded server entry list: %s", err)
	}

	controller, err = psiphon.NewController(config)
	if err != nil {
		return fmt.Errorf("error initializing controller: %s", err)
	}

	shutdownBroadcast = make(chan struct{})
	controllerWaitGroup = new(sync.WaitGroup)
	controllerWaitGroup.Add(1)
	go func() {
		defer controllerWaitGroup.Done()
		controller.Run(shutdownBroadcast)
	}()

	return nil
}
Example #8
0
func main() {

	// Define command-line parameters

	var configFilename string
	flag.StringVar(&configFilename, "config", "", "configuration input file")

	var embeddedServerEntryListFilename string
	flag.StringVar(&embeddedServerEntryListFilename, "serverList", "", "embedded server entry list input file")

	var formatNotices bool
	flag.BoolVar(&formatNotices, "formatNotices", false, "emit notices in human-readable format")

	var profileFilename string
	flag.StringVar(&profileFilename, "profile", "", "CPU profile output file")

	var interfaceName string
	flag.StringVar(&interfaceName, "listenInterface", "", "Interface Name")

	flag.Parse()

	// Initialize default Notice output (stderr)

	var noticeWriter io.Writer
	noticeWriter = os.Stderr
	if formatNotices {
		noticeWriter = psiphon.NewNoticeConsoleRewriter(noticeWriter)
	}
	psiphon.SetNoticeOutput(noticeWriter)

	psiphon.NoticeBuildInfo()

	// Handle required config file parameter

	if configFilename == "" {
		psiphon.NoticeError("configuration file is required")
		os.Exit(1)
	}
	configFileContents, err := ioutil.ReadFile(configFilename)
	if err != nil {
		psiphon.NoticeError("error loading configuration file: %s", err)
		os.Exit(1)
	}
	config, err := psiphon.LoadConfig(configFileContents)
	if err != nil {
		psiphon.NoticeError("error processing configuration file: %s", err)
		os.Exit(1)
	}

	// When a logfile is configured, reinitialize Notice output

	if config.LogFilename != "" {
		logFile, err := os.OpenFile(config.LogFilename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
		if err != nil {
			psiphon.NoticeError("error opening log file: %s", err)
			os.Exit(1)
		}
		defer logFile.Close()
		var noticeWriter io.Writer
		noticeWriter = logFile
		if formatNotices {
			noticeWriter = psiphon.NewNoticeConsoleRewriter(noticeWriter)
		}
		psiphon.SetNoticeOutput(noticeWriter)
	}

	// Handle optional profiling parameter

	if profileFilename != "" {
		profileFile, err := os.Create(profileFilename)
		if err != nil {
			psiphon.NoticeError("error opening profile file: %s", err)
			os.Exit(1)
		}
		pprof.StartCPUProfile(profileFile)
		defer pprof.StopCPUProfile()
	}

	// Initialize data store

	err = psiphon.InitDataStore(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 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 := psiphon.DecodeAndValidateServerEntryList(
				string(serverEntryList),
				common.GetCurrentTimestamp(),
				common.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(serverEntries, false)
			if err != nil {
				psiphon.NoticeError("error storing embedded server entry list data: %s", err)
				return
			}
		}()

		if psiphon.CountServerEntries(config.EgressRegion, config.TunnelProtocol) == 0 {
			embeddedServerListWaitGroup.Wait()
		} else {
			defer embeddedServerListWaitGroup.Wait()
		}
	}

	if interfaceName != "" {
		config.ListenInterface = interfaceName
	}

	// Run Psiphon

	controller, err := psiphon.NewController(config)
	if err != nil {
		psiphon.NoticeError("error creating controller: %s", err)
		os.Exit(1)
	}

	controllerStopSignal := make(chan struct{}, 1)
	shutdownBroadcast := make(chan struct{})
	controllerWaitGroup := new(sync.WaitGroup)
	controllerWaitGroup.Add(1)
	go func() {
		defer controllerWaitGroup.Done()
		controller.Run(shutdownBroadcast)
		controllerStopSignal <- *new(struct{})
	}()

	// Wait for an OS signal or a Run stop signal, then stop Psiphon and exit

	systemStopSignal := make(chan os.Signal, 1)
	signal.Notify(systemStopSignal, os.Interrupt, os.Kill)
	select {
	case <-systemStopSignal:
		psiphon.NoticeInfo("shutdown by system")
		close(shutdownBroadcast)
		controllerWaitGroup.Wait()
	case <-controllerStopSignal:
		psiphon.NoticeInfo("shutdown by controller")
	}
}
Example #9
0
// handshakeAPIRequestHandler implements the "handshake" API request.
// Clients make the handshake immediately after establishing a tunnel
// connection; the response tells the client what homepage to open, what
// stats to record, etc.
func handshakeAPIRequestHandler(
	support *SupportServices,
	apiProtocol string,
	geoIPData GeoIPData,
	params requestJSONObject) ([]byte, error) {

	// Note: ignoring "known_servers" params

	err := validateRequestParams(support, params, baseRequestParams)
	if err != nil {
		return nil, common.ContextError(err)
	}

	log.LogRawFieldsWithTimestamp(
		getRequestLogFields(
			support,
			"handshake",
			geoIPData,
			params,
			baseRequestParams))

	// Note: ignoring param format errors as params have been validated

	sessionID, _ := getStringRequestParam(params, "client_session_id")
	sponsorID, _ := getStringRequestParam(params, "sponsor_id")
	clientVersion, _ := getStringRequestParam(params, "client_version")
	clientPlatform, _ := getStringRequestParam(params, "client_platform")
	isMobile := isMobileClientPlatform(clientPlatform)
	normalizedPlatform := normalizeClientPlatform(clientPlatform)

	// Flag the SSH client as having completed its handshake. This
	// may reselect traffic rules and starts allowing port forwards.

	// TODO: in the case of SSH API requests, the actual sshClient could
	// be passed in and used here. The session ID lookup is only strictly
	// necessary to support web API requests.
	err = support.TunnelServer.SetClientHandshakeState(
		sessionID,
		handshakeState{
			completed:   true,
			apiProtocol: apiProtocol,
			apiParams:   copyBaseRequestParams(params),
		})
	if err != nil {
		return nil, common.ContextError(err)
	}

	// Note: no guarantee that PsinetDatabase won't reload between database calls
	db := support.PsinetDatabase
	handshakeResponse := common.HandshakeResponse{
		SSHSessionID:         sessionID,
		Homepages:            db.GetRandomHomepage(sponsorID, geoIPData.Country, isMobile),
		UpgradeClientVersion: db.GetUpgradeClientVersion(clientVersion, normalizedPlatform),
		PageViewRegexes:      make([]map[string]string, 0),
		HttpsRequestRegexes:  db.GetHttpsRequestRegexes(sponsorID),
		EncodedServerList:    db.DiscoverServers(geoIPData.DiscoveryValue),
		ClientRegion:         geoIPData.Country,
		ServerTimestamp:      common.GetCurrentTimestamp(),
	}

	responsePayload, err := json.Marshal(handshakeResponse)
	if err != nil {
		return nil, common.ContextError(err)
	}

	return responsePayload, nil
}
// FetchRemoteServerList downloads a remote server list JSON record from
// config.RemoteServerListUrl; validates its digital signature using the
// public key config.RemoteServerListSignaturePublicKey; and parses the
// data field into ServerEntry records.
func FetchRemoteServerList(
	config *Config,
	tunnel *Tunnel,
	untunneledDialConfig *DialConfig) error {

	NoticeInfo("fetching remote server list")

	// Select tunneled or untunneled configuration

	httpClient, requestUrl, err := MakeDownloadHttpClient(
		config,
		tunnel,
		untunneledDialConfig,
		config.RemoteServerListUrl,
		time.Duration(*config.FetchRemoteServerListTimeoutSeconds)*time.Second)
	if err != nil {
		return common.ContextError(err)
	}

	// Proceed with download

	downloadFilename := config.RemoteServerListDownloadFilename
	if downloadFilename == "" {
		splitPath := strings.Split(config.RemoteServerListUrl, "/")
		downloadFilename = splitPath[len(splitPath)-1]
	}

	lastETag, err := GetUrlETag(config.RemoteServerListUrl)
	if err != nil {
		return common.ContextError(err)
	}

	n, responseETag, err := ResumeDownload(
		httpClient, requestUrl, downloadFilename, lastETag)

	NoticeRemoteServerListDownloadedBytes(n)

	if err != nil {
		return common.ContextError(err)
	}

	if responseETag == lastETag {
		// The remote server list is unchanged and no data was downloaded
		return nil
	}

	NoticeRemoteServerListDownloaded(downloadFilename)

	// The downloaded content is a zlib compressed authenticated
	// data package containing a list of encoded server entries.

	downloadContent, err := os.Open(downloadFilename)
	if err != nil {
		return common.ContextError(err)
	}
	defer downloadContent.Close()

	zlibReader, err := zlib.NewReader(downloadContent)
	if err != nil {
		return common.ContextError(err)
	}

	dataPackage, err := ioutil.ReadAll(zlibReader)
	zlibReader.Close()
	if err != nil {
		return common.ContextError(err)
	}

	remoteServerList, err := ReadAuthenticatedDataPackage(
		dataPackage, config.RemoteServerListSignaturePublicKey)
	if err != nil {
		return common.ContextError(err)
	}

	serverEntries, err := DecodeAndValidateServerEntryList(
		remoteServerList,
		common.GetCurrentTimestamp(),
		common.SERVER_ENTRY_SOURCE_REMOTE)
	if err != nil {
		return common.ContextError(err)
	}

	err = StoreServerEntries(serverEntries, true)
	if err != nil {
		return common.ContextError(err)
	}

	// Now that the server entries are successfully imported, store the response
	// ETag so we won't re-download this same data again.

	if responseETag != "" {
		err := SetUrlETag(config.RemoteServerListUrl, responseETag)
		if err != nil {
			NoticeAlert("failed to set remote server list ETag: %s", common.ContextError(err))
			// This fetch is still reported as a success, even if we can't store the etag
		}
	}

	return nil
}