// 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 }
// 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) }
// getBaseParams returns all the common API parameters that are included // with each Psiphon API request. These common parameters are used for // statistics. func (serverContext *ServerContext) getBaseParams() requestJSONObject { params := make(requestJSONObject) tunnel := serverContext.tunnel params["session_id"] = serverContext.sessionId params["client_session_id"] = serverContext.sessionId params["server_secret"] = tunnel.serverEntry.WebServerSecret params["propagation_channel_id"] = tunnel.config.PropagationChannelId params["sponsor_id"] = tunnel.config.SponsorId params["client_version"] = tunnel.config.ClientVersion // TODO: client_tunnel_core_version? params["relay_protocol"] = tunnel.protocol params["client_platform"] = tunnel.config.ClientPlatform params["tunnel_whole_device"] = strconv.Itoa(tunnel.config.TunnelWholeDevice) // The following parameters may be blank and must // not be sent to the server if blank. if tunnel.config.DeviceRegion != "" { params["device_region"] = tunnel.config.DeviceRegion } if tunnel.dialStats != nil { if tunnel.dialStats.UpstreamProxyType != "" { params["upstream_proxy_type"] = tunnel.dialStats.UpstreamProxyType } if tunnel.dialStats.UpstreamProxyCustomHeaderNames != nil { params["upstream_proxy_custom_header_names"] = tunnel.dialStats.UpstreamProxyCustomHeaderNames } if tunnel.dialStats.MeekDialAddress != "" { params["meek_dial_address"] = tunnel.dialStats.MeekDialAddress } if tunnel.dialStats.MeekResolvedIPAddress != "" { params["meek_resolved_ip_address"] = tunnel.dialStats.MeekResolvedIPAddress } if tunnel.dialStats.MeekSNIServerName != "" { params["meek_sni_server_name"] = tunnel.dialStats.MeekSNIServerName } if tunnel.dialStats.MeekHostHeader != "" { params["meek_host_header"] = tunnel.dialStats.MeekHostHeader } transformedHostName := "0" if tunnel.dialStats.MeekTransformedHostName { transformedHostName = "1" } params["meek_transformed_host_name"] = transformedHostName } if tunnel.serverEntry.Region != "" { params["server_entry_region"] = tunnel.serverEntry.Region } if tunnel.serverEntry.LocalSource != "" { params["server_entry_source"] = tunnel.serverEntry.LocalSource } // As with last_connected, this timestamp stat, which may be // a precise handshake request server timestamp, is truncated // to hour granularity to avoid introducing a reconstructable // cross-session user trace into server logs. localServerEntryTimestamp := common.TruncateTimestampToHour(tunnel.serverEntry.LocalTimestamp) if localServerEntryTimestamp != "" { params["server_entry_timestamp"] = localServerEntryTimestamp } return params }
// doHandshakeRequest performs the "handshake" API request. The handshake // returns upgrade info, newly discovered server entries -- which are // stored -- and sponsor info (home pages, stat regexes). func (serverContext *ServerContext) doHandshakeRequest() error { params := serverContext.getBaseParams() // *TODO*: this is obsolete? /* serverEntryIpAddresses, err := GetServerEntryIpAddresses() if err != nil { return common.ContextError(err) } // Submit a list of known servers -- this will be used for // discovery statistics. for _, ipAddress := range serverEntryIpAddresses { params = append(params, requestParam{"known_server", ipAddress}) } */ var response []byte if serverContext.psiphonHttpsClient == nil { request, err := makeSSHAPIRequestPayload(params) if err != nil { return common.ContextError(err) } response, err = serverContext.tunnel.SendAPIRequest( common.PSIPHON_API_HANDSHAKE_REQUEST_NAME, request) if err != nil { return common.ContextError(err) } } else { // Legacy web service API request responseBody, err := serverContext.doGetRequest( makeRequestUrl(serverContext.tunnel, "", "handshake", params)) if err != nil { return common.ContextError(err) } // Skip legacy format lines and just parse the JSON config line configLinePrefix := []byte("Config: ") for _, line := range bytes.Split(responseBody, []byte("\n")) { if bytes.HasPrefix(line, configLinePrefix) { response = line[len(configLinePrefix):] break } } if len(response) == 0 { return common.ContextError(errors.New("no config line found")) } } // Legacy fields: // - 'preemptive_reconnect_lifetime_milliseconds' is unused and ignored // - 'ssh_session_id' is ignored; client session ID is used instead var handshakeResponse common.HandshakeResponse err := json.Unmarshal(response, &handshakeResponse) if err != nil { return common.ContextError(err) } serverContext.clientRegion = handshakeResponse.ClientRegion NoticeClientRegion(serverContext.clientRegion) var decodedServerEntries []*ServerEntry // Store discovered server entries // We use the server's time, as it's available here, for the server entry // timestamp since this is more reliable than the client time. for _, encodedServerEntry := range handshakeResponse.EncodedServerList { serverEntry, err := DecodeServerEntry( encodedServerEntry, common.TruncateTimestampToHour(handshakeResponse.ServerTimestamp), common.SERVER_ENTRY_SOURCE_DISCOVERY) if err != nil { return common.ContextError(err) } err = ValidateServerEntry(serverEntry) if err != nil { // Skip this entry and continue with the next one continue } decodedServerEntries = append(decodedServerEntries, serverEntry) } // The reason we are storing the entire array of server entries at once rather // than one at a time is that some desirable side-effects get triggered by // StoreServerEntries that don't get triggered by StoreServerEntry. err = StoreServerEntries(decodedServerEntries, true) if err != nil { return common.ContextError(err) } // TODO: formally communicate the sponsor and upgrade info to an // outer client via some control interface. for _, homepage := range handshakeResponse.Homepages { NoticeHomepage(homepage) } serverContext.clientUpgradeVersion = handshakeResponse.UpgradeClientVersion if handshakeResponse.UpgradeClientVersion != "" { NoticeClientUpgradeAvailable(handshakeResponse.UpgradeClientVersion) } else { NoticeClientIsLatestVersion("") } var regexpsNotices []string serverContext.statsRegexps, regexpsNotices = transferstats.MakeRegexps( handshakeResponse.PageViewRegexes, handshakeResponse.HttpsRequestRegexes) for _, notice := range regexpsNotices { NoticeAlert(notice) } serverContext.serverHandshakeTimestamp = handshakeResponse.ServerTimestamp return nil }