github.com/astaguna/popon-core@v0.0.0-20231019235610-96e42d76a5ff/psiphon/serverApi.go (about)

     1  /*
     2   * Copyright (c) 2015, Psiphon Inc.
     3   * All rights reserved.
     4   *
     5   * This program is free software: you can redistribute it and/or modify
     6   * it under the terms of the GNU General Public License as published by
     7   * the Free Software Foundation, either version 3 of the License, or
     8   * (at your option) any later version.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package psiphon
    21  
    22  import (
    23  	"bytes"
    24  	"context"
    25  	"encoding/base64"
    26  	"encoding/hex"
    27  	"encoding/json"
    28  	"fmt"
    29  	"io"
    30  	"io/ioutil"
    31  	"net"
    32  	"net/http"
    33  	"net/url"
    34  	"strconv"
    35  	"strings"
    36  	"time"
    37  
    38  	"github.com/astaguna/popon-core/psiphon/common"
    39  	"github.com/astaguna/popon-core/psiphon/common/buildinfo"
    40  	"github.com/astaguna/popon-core/psiphon/common/errors"
    41  	"github.com/astaguna/popon-core/psiphon/common/fragmentor"
    42  	"github.com/astaguna/popon-core/psiphon/common/parameters"
    43  	"github.com/astaguna/popon-core/psiphon/common/prng"
    44  	"github.com/astaguna/popon-core/psiphon/common/protocol"
    45  	"github.com/astaguna/popon-core/psiphon/common/tactics"
    46  	"github.com/astaguna/popon-core/psiphon/transferstats"
    47  )
    48  
    49  // ServerContext is a utility struct which holds all of the data associated
    50  // with a Psiphon server connection. In addition to the established tunnel, this
    51  // includes data and transport mechanisms for Psiphon API requests. Legacy servers
    52  // offer the Psiphon API through a web service; newer servers offer the Psiphon
    53  // API through SSH requests made directly through the tunnel's SSH client.
    54  type ServerContext struct {
    55  	tunnel                   *Tunnel
    56  	psiphonHttpsClient       *http.Client
    57  	statsRegexps             *transferstats.Regexps
    58  	clientUpgradeVersion     string
    59  	serverHandshakeTimestamp string
    60  	paddingPRNG              *prng.PRNG
    61  }
    62  
    63  // MakeSessionId creates a new session ID. The same session ID is used across
    64  // multi-tunnel controller runs, where each tunnel has its own ServerContext
    65  // instance.
    66  // In server-side stats, we now consider a "session" to be the lifetime of the
    67  // Controller (e.g., the user's commanded start and stop) and we measure this
    68  // duration as well as the duration of each tunnel within the session.
    69  func MakeSessionId() (string, error) {
    70  	randomId, err := common.MakeSecureRandomBytes(protocol.PSIPHON_API_CLIENT_SESSION_ID_LENGTH)
    71  	if err != nil {
    72  		return "", errors.Trace(err)
    73  	}
    74  	return hex.EncodeToString(randomId), nil
    75  }
    76  
    77  // NewServerContext makes the tunneled handshake request to the Psiphon server
    78  // and returns a ServerContext struct for use with subsequent Psiphon server API
    79  // requests (e.g., periodic connected and status requests).
    80  func NewServerContext(tunnel *Tunnel) (*ServerContext, error) {
    81  
    82  	// For legacy servers, set up psiphonHttpsClient for
    83  	// accessing the Psiphon API via the web service.
    84  	var psiphonHttpsClient *http.Client
    85  	if !tunnel.dialParams.ServerEntry.SupportsSSHAPIRequests() ||
    86  		tunnel.config.TargetApiProtocol == protocol.PSIPHON_WEB_API_PROTOCOL {
    87  
    88  		var err error
    89  		psiphonHttpsClient, err = makePsiphonHttpsClient(tunnel)
    90  		if err != nil {
    91  			return nil, errors.Trace(err)
    92  		}
    93  	}
    94  
    95  	serverContext := &ServerContext{
    96  		tunnel:             tunnel,
    97  		psiphonHttpsClient: psiphonHttpsClient,
    98  		paddingPRNG:        prng.NewPRNGWithSeed(tunnel.dialParams.APIRequestPaddingSeed),
    99  	}
   100  
   101  	ignoreRegexps := tunnel.config.GetParameters().Get().Bool(
   102  		parameters.IgnoreHandshakeStatsRegexps)
   103  
   104  	err := serverContext.doHandshakeRequest(ignoreRegexps)
   105  	if err != nil {
   106  		return nil, errors.Trace(err)
   107  	}
   108  
   109  	return serverContext, nil
   110  }
   111  
   112  // doHandshakeRequest performs the "handshake" API request. The handshake
   113  // returns upgrade info, newly discovered server entries -- which are
   114  // stored -- and sponsor info (home pages, stat regexes).
   115  func (serverContext *ServerContext) doHandshakeRequest(
   116  	ignoreStatsRegexps bool) error {
   117  
   118  	params := serverContext.getBaseAPIParameters(baseParametersAll)
   119  
   120  	// The server will return a signed copy of its own server entry when the
   121  	// client specifies this 'missing_server_entry_signature' parameter.
   122  	//
   123  	// The purpose of this mechanism is to rapidly upgrade client local storage
   124  	// from unsigned to signed server entries, and to ensure that the client has
   125  	// a signed server entry for its currently connected server as required for
   126  	// the client-to-client exchange feature.
   127  	//
   128  	// The server entry will be included in handshakeResponse.EncodedServerList,
   129  	// along side discovery servers.
   130  	requestedMissingSignature := false
   131  	if !serverContext.tunnel.dialParams.ServerEntry.HasSignature() {
   132  		requestedMissingSignature = true
   133  		params["missing_server_entry_signature"] =
   134  			serverContext.tunnel.dialParams.ServerEntry.Tag
   135  	}
   136  
   137  	doTactics := !serverContext.tunnel.config.DisableTactics
   138  
   139  	networkID := ""
   140  	if doTactics {
   141  
   142  		// Limitation: it is assumed that the network ID obtained here is the
   143  		// one that is active when the handshake request is received by the
   144  		// server. However, it is remotely possible to switch networks
   145  		// immediately after invoking the GetNetworkID callback and initiating
   146  		// the handshake, if the tunnel protocol is meek.
   147  		//
   148  		// The response handling code below calls GetNetworkID again and ignores
   149  		// any tactics payload if the network ID is not the same. While this
   150  		// doesn't detect all cases of changing networks, it reduces the already
   151  		// narrow window.
   152  
   153  		networkID = serverContext.tunnel.config.GetNetworkID()
   154  
   155  		err := tactics.SetTacticsAPIParameters(
   156  			GetTacticsStorer(serverContext.tunnel.config),
   157  			networkID,
   158  			params)
   159  		if err != nil {
   160  			return errors.Trace(err)
   161  		}
   162  	}
   163  
   164  	// When split tunnel mode is enabled, indicate this to the server. When
   165  	// indicated, the server will perform split tunnel classifications on TCP
   166  	// port forwards and reject, with a distinct response, port forwards which
   167  	// the client should connect to directly, untunneled.
   168  	if serverContext.tunnel.config.SplitTunnelOwnRegion {
   169  		params["split_tunnel"] = "1"
   170  	}
   171  
   172  	// While regular split tunnel mode makes untunneled connections to
   173  	// destinations in the client's own country, selected split tunnel mode
   174  	// allows the client to specify a list of untunneled countries. Either or
   175  	// both modes may be enabled.
   176  	if len(serverContext.tunnel.config.SplitTunnelRegions) > 0 {
   177  		params["split_tunnel_regions"] = serverContext.tunnel.config.SplitTunnelRegions
   178  	}
   179  
   180  	var response []byte
   181  	if serverContext.psiphonHttpsClient == nil {
   182  
   183  		params[protocol.PSIPHON_API_HANDSHAKE_AUTHORIZATIONS] =
   184  			serverContext.tunnel.config.GetAuthorizations()
   185  
   186  		request, err := serverContext.makeSSHAPIRequestPayload(params)
   187  		if err != nil {
   188  			return errors.Trace(err)
   189  		}
   190  
   191  		response, err = serverContext.tunnel.SendAPIRequest(
   192  			protocol.PSIPHON_API_HANDSHAKE_REQUEST_NAME, request)
   193  		if err != nil {
   194  			return errors.Trace(err)
   195  		}
   196  
   197  	} else {
   198  
   199  		// Legacy web service API request
   200  
   201  		responseBody, err := serverContext.doGetRequest(
   202  			makeRequestUrl(serverContext.tunnel, "", "handshake", params))
   203  		if err != nil {
   204  			return errors.Trace(err)
   205  		}
   206  		// Skip legacy format lines and just parse the JSON config line
   207  		configLinePrefix := []byte("Config: ")
   208  		for _, line := range bytes.Split(responseBody, []byte("\n")) {
   209  			if bytes.HasPrefix(line, configLinePrefix) {
   210  				response = line[len(configLinePrefix):]
   211  				break
   212  			}
   213  		}
   214  		if len(response) == 0 {
   215  			return errors.TraceNew("no config line found")
   216  		}
   217  	}
   218  
   219  	// Legacy fields:
   220  	// - 'preemptive_reconnect_lifetime_milliseconds' is unused and ignored
   221  	// - 'ssh_session_id' is ignored; client session ID is used instead
   222  
   223  	var handshakeResponse protocol.HandshakeResponse
   224  
   225  	// Initialize these fields to distinguish between psiphond omitting values in
   226  	// the response and the zero value, which means unlimited rate.
   227  	handshakeResponse.UpstreamBytesPerSecond = -1
   228  	handshakeResponse.DownstreamBytesPerSecond = -1
   229  
   230  	err := json.Unmarshal(response, &handshakeResponse)
   231  	if err != nil {
   232  		return errors.Trace(err)
   233  	}
   234  
   235  	if serverContext.tunnel.config.EmitClientAddress {
   236  		NoticeClientAddress(handshakeResponse.ClientAddress)
   237  	}
   238  
   239  	NoticeClientRegion(handshakeResponse.ClientRegion)
   240  
   241  	// Emit a SplitTunnelRegions notice indicating active split tunnel region.
   242  	// For SplitTunnelOwnRegion, the handshake ClientRegion is the split
   243  	// tunnel region and this region is always listed first.
   244  
   245  	splitTunnelRegions := []string{}
   246  	if serverContext.tunnel.config.SplitTunnelOwnRegion {
   247  		splitTunnelRegions = []string{handshakeResponse.ClientRegion}
   248  	}
   249  	for _, region := range serverContext.tunnel.config.SplitTunnelRegions {
   250  		if !serverContext.tunnel.config.SplitTunnelOwnRegion ||
   251  			region != handshakeResponse.ClientRegion {
   252  
   253  			splitTunnelRegions = append(splitTunnelRegions, region)
   254  		}
   255  	}
   256  	if len(splitTunnelRegions) > 0 {
   257  		NoticeSplitTunnelRegions(splitTunnelRegions)
   258  	}
   259  
   260  	var serverEntries []protocol.ServerEntryFields
   261  
   262  	// Store discovered server entries
   263  	// We use the server's time, as it's available here, for the server entry
   264  	// timestamp since this is more reliable than the client time.
   265  	for _, encodedServerEntry := range handshakeResponse.EncodedServerList {
   266  
   267  		serverEntryFields, err := protocol.DecodeServerEntryFields(
   268  			encodedServerEntry,
   269  			common.TruncateTimestampToHour(handshakeResponse.ServerTimestamp),
   270  			protocol.SERVER_ENTRY_SOURCE_DISCOVERY)
   271  		if err != nil {
   272  			return errors.Trace(err)
   273  		}
   274  
   275  		// Retain the original timestamp and source in the requestedMissingSignature
   276  		// case, as this server entry was not discovered here.
   277  		//
   278  		// Limitation: there is a transient edge case where
   279  		// requestedMissingSignature will be set for a discovery server entry that
   280  		// _is_ also discovered here.
   281  		if requestedMissingSignature &&
   282  			serverEntryFields.GetIPAddress() == serverContext.tunnel.dialParams.ServerEntry.IpAddress {
   283  
   284  			serverEntryFields.SetLocalTimestamp(serverContext.tunnel.dialParams.ServerEntry.LocalTimestamp)
   285  			serverEntryFields.SetLocalSource(serverContext.tunnel.dialParams.ServerEntry.LocalSource)
   286  		}
   287  
   288  		err = protocol.ValidateServerEntryFields(serverEntryFields)
   289  		if err != nil {
   290  			// Skip this entry and continue with the next one
   291  			NoticeWarning("invalid handshake server entry: %s", err)
   292  			continue
   293  		}
   294  
   295  		serverEntries = append(serverEntries, serverEntryFields)
   296  	}
   297  
   298  	err = StoreServerEntries(
   299  		serverContext.tunnel.config,
   300  		serverEntries,
   301  		true)
   302  	if err != nil {
   303  		return errors.Trace(err)
   304  	}
   305  
   306  	NoticeHomepages(handshakeResponse.Homepages)
   307  
   308  	serverContext.clientUpgradeVersion = handshakeResponse.UpgradeClientVersion
   309  	if handshakeResponse.UpgradeClientVersion != "" {
   310  		NoticeClientUpgradeAvailable(handshakeResponse.UpgradeClientVersion)
   311  	} else {
   312  		NoticeClientIsLatestVersion("")
   313  	}
   314  
   315  	if !ignoreStatsRegexps {
   316  
   317  		// The handshake returns page_view_regexes and https_request_regexes.
   318  		// page_view_regexes is obsolete and not used. https_request_regexes, which
   319  		// are actually host/domain name regexes, are used for host/domain name
   320  		// bytes transferred metrics: tunneled traffic TLS SNI server names and HTTP
   321  		// Host header host names are matched against these regexes to select flows
   322  		// for bytes transferred counting.
   323  
   324  		var regexpsNotices []string
   325  		serverContext.statsRegexps, regexpsNotices = transferstats.MakeRegexps(
   326  			handshakeResponse.HttpsRequestRegexes)
   327  
   328  		for _, notice := range regexpsNotices {
   329  			NoticeWarning(notice)
   330  		}
   331  	}
   332  
   333  	diagnosticID := serverContext.tunnel.dialParams.ServerEntry.GetDiagnosticID()
   334  
   335  	serverContext.serverHandshakeTimestamp = handshakeResponse.ServerTimestamp
   336  	NoticeServerTimestamp(diagnosticID, serverContext.serverHandshakeTimestamp)
   337  
   338  	NoticeActiveAuthorizationIDs(diagnosticID, handshakeResponse.ActiveAuthorizationIDs)
   339  
   340  	NoticeTrafficRateLimits(
   341  		diagnosticID,
   342  		handshakeResponse.UpstreamBytesPerSecond,
   343  		handshakeResponse.DownstreamBytesPerSecond)
   344  
   345  	if doTactics && handshakeResponse.TacticsPayload != nil &&
   346  		networkID == serverContext.tunnel.config.GetNetworkID() {
   347  
   348  		var payload *tactics.Payload
   349  		err := json.Unmarshal(handshakeResponse.TacticsPayload, &payload)
   350  		if err != nil {
   351  			return errors.Trace(err)
   352  		}
   353  
   354  		// handshakeResponse.TacticsPayload may be "null", and payload
   355  		// will successfully unmarshal as nil. As a result, the previous
   356  		// handshakeResponse.TacticsPayload != nil test is insufficient.
   357  		if payload != nil {
   358  
   359  			tacticsRecord, err := tactics.HandleTacticsPayload(
   360  				GetTacticsStorer(serverContext.tunnel.config),
   361  				networkID,
   362  				payload)
   363  			if err != nil {
   364  				return errors.Trace(err)
   365  			}
   366  
   367  			if tacticsRecord != nil &&
   368  				prng.FlipWeightedCoin(tacticsRecord.Tactics.Probability) {
   369  
   370  				err := serverContext.tunnel.config.SetParameters(
   371  					tacticsRecord.Tag, true, tacticsRecord.Tactics.Parameters)
   372  				if err != nil {
   373  					NoticeInfo("apply handshake tactics failed: %s", err)
   374  				}
   375  				// The error will be due to invalid tactics values
   376  				// from the server. When SetParameters fails, all
   377  				// previous tactics values are left in place.
   378  			}
   379  		}
   380  	}
   381  
   382  	return nil
   383  }
   384  
   385  // DoConnectedRequest performs the "connected" API request. This request is
   386  // used for statistics, including unique user counting; reporting the full
   387  // tunnel establishment duration including the handshake request; and updated
   388  // fragmentor metrics.
   389  //
   390  // Users are not assigned identifiers. Instead, daily unique users are
   391  // calculated by having clients submit their last connected timestamp
   392  // (truncated to an hour, as a privacy measure). As client clocks are
   393  // unreliable, the server returns new last_connected values for the client to
   394  // store and send next time it connects.
   395  func (serverContext *ServerContext) DoConnectedRequest() error {
   396  
   397  	// Limitation: as currently implemented, the last_connected exchange isn't a
   398  	// distributed, atomic operation. When clients send the connected request,
   399  	// the server may receive the request, count a unique user based on the
   400  	// client's last_connected, and then the tunnel fails before the client
   401  	// receives the response, so the client will not update its last_connected
   402  	// value and submit the same one again, resulting in an inflated unique user
   403  	// count.
   404  	//
   405  	// The SetInFlightConnectedRequest mechanism mitigates one class of connected
   406  	// request interruption, a commanded shutdown in the middle of a connected
   407  	// request, by allowing some time for the request to complete before
   408  	// terminating the tunnel.
   409  	//
   410  	// TODO: consider extending the connected request protocol with additional
   411  	// "acknowledgment" messages so that the server does not commit its unique
   412  	// user count until after the client has acknowledged receipt and durable
   413  	// storage of the new last_connected value.
   414  
   415  	requestDone := make(chan struct{})
   416  	defer close(requestDone)
   417  
   418  	if !serverContext.tunnel.SetInFlightConnectedRequest(requestDone) {
   419  		return errors.TraceNew("tunnel is closing")
   420  	}
   421  	defer serverContext.tunnel.SetInFlightConnectedRequest(nil)
   422  
   423  	params := serverContext.getBaseAPIParameters(
   424  		baseParametersOnlyUpstreamFragmentorDialParameters)
   425  
   426  	lastConnected, err := getLastConnected()
   427  	if err != nil {
   428  		return errors.Trace(err)
   429  	}
   430  
   431  	params["last_connected"] = lastConnected
   432  
   433  	// serverContext.tunnel.establishDuration is nanoseconds; report milliseconds
   434  	params["establishment_duration"] =
   435  		fmt.Sprintf("%d", serverContext.tunnel.establishDuration/time.Millisecond)
   436  
   437  	var response []byte
   438  	if serverContext.psiphonHttpsClient == nil {
   439  
   440  		request, err := serverContext.makeSSHAPIRequestPayload(params)
   441  		if err != nil {
   442  			return errors.Trace(err)
   443  		}
   444  
   445  		response, err = serverContext.tunnel.SendAPIRequest(
   446  			protocol.PSIPHON_API_CONNECTED_REQUEST_NAME, request)
   447  		if err != nil {
   448  			return errors.Trace(err)
   449  		}
   450  
   451  	} else {
   452  
   453  		// Legacy web service API request
   454  
   455  		response, err = serverContext.doGetRequest(
   456  			makeRequestUrl(serverContext.tunnel, "", "connected", params))
   457  		if err != nil {
   458  			return errors.Trace(err)
   459  		}
   460  	}
   461  
   462  	var connectedResponse protocol.ConnectedResponse
   463  	err = json.Unmarshal(response, &connectedResponse)
   464  	if err != nil {
   465  		return errors.Trace(err)
   466  	}
   467  
   468  	err = SetKeyValue(
   469  		datastoreLastConnectedKey, connectedResponse.ConnectedTimestamp)
   470  	if err != nil {
   471  		return errors.Trace(err)
   472  	}
   473  
   474  	return nil
   475  }
   476  
   477  func getLastConnected() (string, error) {
   478  	lastConnected, err := GetKeyValue(datastoreLastConnectedKey)
   479  	if err != nil {
   480  		return "", errors.Trace(err)
   481  	}
   482  	if lastConnected == "" {
   483  		lastConnected = "None"
   484  	}
   485  	return lastConnected, nil
   486  }
   487  
   488  // StatsRegexps gets the Regexps used for the statistics for this tunnel.
   489  func (serverContext *ServerContext) StatsRegexps() *transferstats.Regexps {
   490  	return serverContext.statsRegexps
   491  }
   492  
   493  // DoStatusRequest makes a "status" API request to the server, sending session stats.
   494  func (serverContext *ServerContext) DoStatusRequest(tunnel *Tunnel) error {
   495  
   496  	params := serverContext.getBaseAPIParameters(baseParametersNoDialParameters)
   497  
   498  	// Note: ensure putBackStatusRequestPayload is called, to replace
   499  	// payload for future attempt, in all failure cases.
   500  
   501  	statusPayload, statusPayloadInfo, err := makeStatusRequestPayload(
   502  		serverContext.tunnel.config,
   503  		tunnel.dialParams.ServerEntry.IpAddress)
   504  	if err != nil {
   505  		return errors.Trace(err)
   506  	}
   507  
   508  	// Skip the request when there's no payload to send.
   509  
   510  	if len(statusPayload) == 0 {
   511  		return nil
   512  	}
   513  
   514  	var response []byte
   515  
   516  	if serverContext.psiphonHttpsClient == nil {
   517  
   518  		rawMessage := json.RawMessage(statusPayload)
   519  		params["statusData"] = &rawMessage
   520  
   521  		var request []byte
   522  		request, err = serverContext.makeSSHAPIRequestPayload(params)
   523  
   524  		if err == nil {
   525  			response, err = serverContext.tunnel.SendAPIRequest(
   526  				protocol.PSIPHON_API_STATUS_REQUEST_NAME, request)
   527  		}
   528  
   529  	} else {
   530  
   531  		// Legacy web service API request
   532  		response, err = serverContext.doPostRequest(
   533  			makeRequestUrl(serverContext.tunnel, "", "status", params),
   534  			"application/json",
   535  			bytes.NewReader(statusPayload))
   536  	}
   537  
   538  	if err != nil {
   539  
   540  		// Resend the transfer stats and tunnel stats later
   541  		// Note: potential duplicate reports if the server received and processed
   542  		// the request but the client failed to receive the response.
   543  		putBackStatusRequestPayload(statusPayloadInfo)
   544  
   545  		return errors.Trace(err)
   546  	}
   547  
   548  	confirmStatusRequestPayload(statusPayloadInfo)
   549  
   550  	var statusResponse protocol.StatusResponse
   551  	err = json.Unmarshal(response, &statusResponse)
   552  	if err != nil {
   553  		return errors.Trace(err)
   554  	}
   555  
   556  	for _, serverEntryTag := range statusResponse.InvalidServerEntryTags {
   557  		PruneServerEntry(serverContext.tunnel.config, serverEntryTag)
   558  	}
   559  
   560  	return nil
   561  }
   562  
   563  // statusRequestPayloadInfo is a temporary structure for data used to
   564  // either "clear" or "put back" status request payload data depending
   565  // on whether or not the request succeeded.
   566  type statusRequestPayloadInfo struct {
   567  	serverId        string
   568  	transferStats   *transferstats.AccumulatedStats
   569  	persistentStats map[string][][]byte
   570  }
   571  
   572  func makeStatusRequestPayload(
   573  	config *Config,
   574  	serverId string) ([]byte, *statusRequestPayloadInfo, error) {
   575  
   576  	transferStats := transferstats.TakeOutStatsForServer(serverId)
   577  	hostBytes := transferStats.GetStatsForStatusRequest()
   578  
   579  	persistentStats, err := TakeOutUnreportedPersistentStats(config)
   580  	if err != nil {
   581  		NoticeWarning(
   582  			"TakeOutUnreportedPersistentStats failed: %s", errors.Trace(err))
   583  		persistentStats = nil
   584  		// Proceed with transferStats only
   585  	}
   586  
   587  	if len(hostBytes) == 0 && len(persistentStats) == 0 {
   588  		// There is no payload to send.
   589  		return nil, nil, nil
   590  	}
   591  
   592  	payloadInfo := &statusRequestPayloadInfo{
   593  		serverId, transferStats, persistentStats}
   594  
   595  	payload := make(map[string]interface{})
   596  
   597  	payload["host_bytes"] = hostBytes
   598  
   599  	// We're not recording these fields, but legacy servers require them.
   600  	payload["bytes_transferred"] = 0
   601  	payload["page_views"] = make([]string, 0)
   602  	payload["https_requests"] = make([]string, 0)
   603  
   604  	persistentStatPayloadNames := make(map[string]string)
   605  	persistentStatPayloadNames[datastorePersistentStatTypeRemoteServerList] = "remote_server_list_stats"
   606  	persistentStatPayloadNames[datastorePersistentStatTypeFailedTunnel] = "failed_tunnel_stats"
   607  
   608  	for statType, stats := range persistentStats {
   609  
   610  		// Persistent stats records are already in JSON format
   611  		jsonStats := make([]json.RawMessage, len(stats))
   612  		for i, stat := range stats {
   613  			jsonStats[i] = json.RawMessage(stat)
   614  		}
   615  		payload[persistentStatPayloadNames[statType]] = jsonStats
   616  	}
   617  
   618  	jsonPayload, err := json.Marshal(payload)
   619  	if err != nil {
   620  
   621  		// Send the transfer stats and tunnel stats later
   622  		putBackStatusRequestPayload(payloadInfo)
   623  
   624  		return nil, nil, errors.Trace(err)
   625  	}
   626  
   627  	return jsonPayload, payloadInfo, nil
   628  }
   629  
   630  func putBackStatusRequestPayload(payloadInfo *statusRequestPayloadInfo) {
   631  	transferstats.PutBackStatsForServer(
   632  		payloadInfo.serverId, payloadInfo.transferStats)
   633  	err := PutBackUnreportedPersistentStats(payloadInfo.persistentStats)
   634  	if err != nil {
   635  		// These persistent stats records won't be resent until after a
   636  		// datastore re-initialization.
   637  		NoticeWarning(
   638  			"PutBackUnreportedPersistentStats failed: %s", errors.Trace(err))
   639  	}
   640  }
   641  
   642  func confirmStatusRequestPayload(payloadInfo *statusRequestPayloadInfo) {
   643  	err := ClearReportedPersistentStats(payloadInfo.persistentStats)
   644  	if err != nil {
   645  		// These persistent stats records may be resent.
   646  		NoticeWarning(
   647  			"ClearReportedPersistentStats failed: %s", errors.Trace(err))
   648  	}
   649  }
   650  
   651  // RecordRemoteServerListStat records a completed common or OSL remote server
   652  // list resource download.
   653  //
   654  // The RSL download event could occur when the client is unable to immediately
   655  // send a status request to a server, so these records are stored in the
   656  // persistent datastore and reported via subsequent status requests sent to
   657  // any Psiphon server.
   658  //
   659  // Note that some common event field values may change between the stat
   660  // recording and reporting, including client geolocation and host_id.
   661  //
   662  // The bytes/duration fields reflect the size and download time for the _last
   663  // chunk only_ in the case of a resumed download. The purpose of these fields
   664  // is to calculate rough data transfer rates. Both bytes and duration are
   665  // included in the log, to allow for filtering out of small transfers which
   666  // may not produce accurate rate numbers.
   667  //
   668  // Multiple "status" requests may be in flight at once (due to multi-tunnel,
   669  // asynchronous final status retry, and aggressive status requests for
   670  // pre-registered tunnels), To avoid duplicate reporting, persistent stats
   671  // records are "taken-out" by a status request and then "put back" in case the
   672  // request fails.
   673  //
   674  // Duplicate reporting may also occur when a server receives and processes a
   675  // status request but the client fails to receive the response.
   676  func RecordRemoteServerListStat(
   677  	config *Config,
   678  	tunneled bool,
   679  	url string,
   680  	etag string,
   681  	bytes int64,
   682  	duration time.Duration,
   683  	authenticated bool,
   684  	additionalParameters common.APIParameters) error {
   685  
   686  	if !config.GetParameters().Get().WeightedCoinFlip(
   687  		parameters.RecordRemoteServerListPersistentStatsProbability) {
   688  		return nil
   689  	}
   690  
   691  	params := make(common.APIParameters)
   692  
   693  	params["session_id"] = config.SessionID
   694  	params["propagation_channel_id"] = config.PropagationChannelId
   695  	params["sponsor_id"] = config.GetSponsorID()
   696  	params["client_version"] = config.ClientVersion
   697  	params["client_platform"] = config.ClientPlatform
   698  	params["client_build_rev"] = buildinfo.GetBuildInfo().BuildRev
   699  	if config.DeviceRegion != "" {
   700  		params["device_region"] = config.DeviceRegion
   701  	}
   702  
   703  	params["client_download_timestamp"] = common.TruncateTimestampToHour(common.GetCurrentTimestamp())
   704  	tunneledStr := "0"
   705  	if tunneled {
   706  		tunneledStr = "1"
   707  	}
   708  	params["tunneled"] = tunneledStr
   709  	params["url"] = url
   710  	params["etag"] = etag
   711  	params["bytes"] = fmt.Sprintf("%d", bytes)
   712  
   713  	// duration is nanoseconds; report milliseconds
   714  	params["duration"] = fmt.Sprintf("%d", duration/time.Millisecond)
   715  
   716  	authenticatedStr := "0"
   717  	if authenticated {
   718  		authenticatedStr = "1"
   719  	}
   720  	params["authenticated"] = authenticatedStr
   721  
   722  	for k, v := range additionalParameters {
   723  		params[k] = v
   724  	}
   725  
   726  	remoteServerListStatJson, err := json.Marshal(params)
   727  	if err != nil {
   728  		return errors.Trace(err)
   729  	}
   730  
   731  	return StorePersistentStat(
   732  		config, datastorePersistentStatTypeRemoteServerList, remoteServerListStatJson)
   733  }
   734  
   735  // RecordFailedTunnelStat records metrics for a failed tunnel dial, including
   736  // dial parameters and error condition (tunnelErr). No record is created when
   737  // tunnelErr is nil.
   738  //
   739  // This uses the same reporting facility, with the same caveats, as
   740  // RecordRemoteServerListStat.
   741  func RecordFailedTunnelStat(
   742  	config *Config,
   743  	dialParams *DialParameters,
   744  	livenessTestMetrics *livenessTestMetrics,
   745  	bytesUp int64,
   746  	bytesDown int64,
   747  	tunnelErr error) error {
   748  
   749  	probability := config.GetParameters().Get().Float(
   750  		parameters.RecordFailedTunnelPersistentStatsProbability)
   751  
   752  	if !prng.FlipWeightedCoin(probability) {
   753  		return nil
   754  	}
   755  
   756  	// Callers should not call RecordFailedTunnelStat with a nil tunnelErr, as
   757  	// this is not a useful stat and it results in a nil pointer dereference.
   758  	// This check catches potential bug cases. An example edge case, now
   759  	// fixed, is deferred error handlers, such as the ones in in
   760  	// dialTunnel/tunnel.Activate, which may be invoked in the case of a
   761  	// panic, which can occur before any error value is returned.
   762  	if tunnelErr == nil {
   763  		return errors.TraceNew("no error")
   764  	}
   765  
   766  	lastConnected, err := getLastConnected()
   767  	if err != nil {
   768  		return errors.Trace(err)
   769  	}
   770  
   771  	params := getBaseAPIParameters(baseParametersAll, config, dialParams)
   772  
   773  	delete(params, "server_secret")
   774  	params["server_entry_tag"] = dialParams.ServerEntry.Tag
   775  	params["last_connected"] = lastConnected
   776  	params["client_failed_timestamp"] = common.TruncateTimestampToHour(common.GetCurrentTimestamp())
   777  	if livenessTestMetrics != nil {
   778  		params["liveness_test_upstream_bytes"] = strconv.Itoa(livenessTestMetrics.UpstreamBytes)
   779  		params["liveness_test_sent_upstream_bytes"] = strconv.Itoa(livenessTestMetrics.SentUpstreamBytes)
   780  		params["liveness_test_downstream_bytes"] = strconv.Itoa(livenessTestMetrics.DownstreamBytes)
   781  		params["liveness_test_received_downstream_bytes"] = strconv.Itoa(livenessTestMetrics.ReceivedDownstreamBytes)
   782  	}
   783  	if bytesUp >= 0 {
   784  		params["bytes_up"] = fmt.Sprintf("%d", bytesUp)
   785  	}
   786  	if bytesDown >= 0 {
   787  		params["bytes_down"] = fmt.Sprintf("%d", bytesDown)
   788  	}
   789  
   790  	// Log RecordFailedTunnelPersistentStatsProbability to indicate the
   791  	// proportion of failed tunnel events being recorded at the time of
   792  	// this log event.
   793  	params["record_probability"] = fmt.Sprintf("%f", probability)
   794  
   795  	// Ensure direct server IPs are not exposed in logs. The "net" package, and
   796  	// possibly other 3rd party packages, will include destination addresses in
   797  	// I/O error messages.
   798  	tunnelError := common.RedactIPAddressesString(tunnelErr.Error())
   799  	params["tunnel_error"] = tunnelError
   800  
   801  	failedTunnelStatJson, err := json.Marshal(params)
   802  	if err != nil {
   803  		return errors.Trace(err)
   804  	}
   805  
   806  	return StorePersistentStat(
   807  		config, datastorePersistentStatTypeFailedTunnel, failedTunnelStatJson)
   808  }
   809  
   810  // doGetRequest makes a tunneled HTTPS request and returns the response body.
   811  func (serverContext *ServerContext) doGetRequest(
   812  	requestUrl string) (responseBody []byte, err error) {
   813  
   814  	request, err := http.NewRequest("GET", requestUrl, nil)
   815  	if err != nil {
   816  		return nil, errors.Trace(err)
   817  	}
   818  
   819  	request.Header.Set("User-Agent", MakePsiphonUserAgent(serverContext.tunnel.config))
   820  
   821  	response, err := serverContext.psiphonHttpsClient.Do(request)
   822  	if err == nil && response.StatusCode != http.StatusOK {
   823  		response.Body.Close()
   824  		err = fmt.Errorf("HTTP GET request failed with response code: %d", response.StatusCode)
   825  	}
   826  	if err != nil {
   827  		// Trim this error since it may include long URLs
   828  		return nil, errors.Trace(TrimError(err))
   829  	}
   830  	defer response.Body.Close()
   831  	body, err := ioutil.ReadAll(response.Body)
   832  	if err != nil {
   833  		return nil, errors.Trace(err)
   834  	}
   835  	return body, nil
   836  }
   837  
   838  // doPostRequest makes a tunneled HTTPS POST request.
   839  func (serverContext *ServerContext) doPostRequest(
   840  	requestUrl string, bodyType string, body io.Reader) (responseBody []byte, err error) {
   841  
   842  	request, err := http.NewRequest("POST", requestUrl, body)
   843  	if err != nil {
   844  		return nil, errors.Trace(err)
   845  	}
   846  
   847  	request.Header.Set("User-Agent", MakePsiphonUserAgent(serverContext.tunnel.config))
   848  	request.Header.Set("Content-Type", bodyType)
   849  
   850  	response, err := serverContext.psiphonHttpsClient.Do(request)
   851  	if err == nil && response.StatusCode != http.StatusOK {
   852  		response.Body.Close()
   853  		err = fmt.Errorf("HTTP POST request failed with response code: %d", response.StatusCode)
   854  	}
   855  	if err != nil {
   856  		// Trim this error since it may include long URLs
   857  		return nil, errors.Trace(TrimError(err))
   858  	}
   859  	defer response.Body.Close()
   860  	responseBody, err = ioutil.ReadAll(response.Body)
   861  	if err != nil {
   862  		return nil, errors.Trace(err)
   863  	}
   864  	return responseBody, nil
   865  }
   866  
   867  // makeSSHAPIRequestPayload makes a JSON payload for an SSH API request.
   868  func (serverContext *ServerContext) makeSSHAPIRequestPayload(
   869  	params common.APIParameters) ([]byte, error) {
   870  	jsonPayload, err := json.Marshal(params)
   871  	if err != nil {
   872  		return nil, errors.Trace(err)
   873  	}
   874  	return jsonPayload, nil
   875  }
   876  
   877  type baseParametersFilter int
   878  
   879  const (
   880  	baseParametersAll baseParametersFilter = iota
   881  	baseParametersOnlyUpstreamFragmentorDialParameters
   882  	baseParametersNoDialParameters
   883  )
   884  
   885  func (serverContext *ServerContext) getBaseAPIParameters(
   886  	filter baseParametersFilter) common.APIParameters {
   887  
   888  	params := getBaseAPIParameters(
   889  		filter,
   890  		serverContext.tunnel.config,
   891  		serverContext.tunnel.dialParams)
   892  
   893  	// Add a random amount of padding to defend against API call traffic size
   894  	// fingerprints. The "pad_response" field instructs the server to pad its
   895  	// response accordingly.
   896  
   897  	p := serverContext.tunnel.config.GetParameters().Get()
   898  	minUpstreamPadding := p.Int(parameters.APIRequestUpstreamPaddingMinBytes)
   899  	maxUpstreamPadding := p.Int(parameters.APIRequestUpstreamPaddingMaxBytes)
   900  	minDownstreamPadding := p.Int(parameters.APIRequestDownstreamPaddingMinBytes)
   901  	maxDownstreamPadding := p.Int(parameters.APIRequestDownstreamPaddingMaxBytes)
   902  
   903  	if maxUpstreamPadding > 0 {
   904  		size := serverContext.paddingPRNG.Range(minUpstreamPadding, maxUpstreamPadding)
   905  		params["padding"] = strings.Repeat(" ", size)
   906  	}
   907  
   908  	if maxDownstreamPadding > 0 {
   909  		size := serverContext.paddingPRNG.Range(minDownstreamPadding, maxDownstreamPadding)
   910  		params["pad_response"] = strconv.Itoa(size)
   911  	}
   912  
   913  	return params
   914  }
   915  
   916  // getBaseAPIParameters returns all the common API parameters that are
   917  // included with each Psiphon API request. These common parameters are used
   918  // for metrics.
   919  func getBaseAPIParameters(
   920  	filter baseParametersFilter,
   921  	config *Config,
   922  	dialParams *DialParameters) common.APIParameters {
   923  
   924  	params := make(common.APIParameters)
   925  
   926  	params["session_id"] = config.SessionID
   927  	params["client_session_id"] = config.SessionID
   928  	params["server_secret"] = dialParams.ServerEntry.WebServerSecret
   929  	params["propagation_channel_id"] = config.PropagationChannelId
   930  	params["sponsor_id"] = config.GetSponsorID()
   931  	params["client_version"] = config.ClientVersion
   932  	params["client_platform"] = config.ClientPlatform
   933  	params["client_features"] = config.clientFeatures
   934  	params["client_build_rev"] = buildinfo.GetBuildInfo().BuildRev
   935  
   936  	// Blank parameters must be omitted.
   937  
   938  	if config.DeviceRegion != "" {
   939  		params["device_region"] = config.DeviceRegion
   940  	}
   941  
   942  	if filter == baseParametersAll {
   943  
   944  		params["relay_protocol"] = dialParams.TunnelProtocol
   945  		params["network_type"] = dialParams.GetNetworkType()
   946  
   947  		if dialParams.BPFProgramName != "" {
   948  			params["client_bpf"] = dialParams.BPFProgramName
   949  		}
   950  
   951  		if dialParams.SelectedSSHClientVersion {
   952  			params["ssh_client_version"] = dialParams.SSHClientVersion
   953  		}
   954  
   955  		if dialParams.UpstreamProxyType != "" {
   956  			params["upstream_proxy_type"] = dialParams.UpstreamProxyType
   957  		}
   958  
   959  		if dialParams.UpstreamProxyCustomHeaderNames != nil {
   960  			params["upstream_proxy_custom_header_names"] = dialParams.UpstreamProxyCustomHeaderNames
   961  		}
   962  
   963  		if dialParams.FrontingProviderID != "" {
   964  			params["fronting_provider_id"] = dialParams.FrontingProviderID
   965  		}
   966  
   967  		if dialParams.MeekDialAddress != "" {
   968  			params["meek_dial_address"] = dialParams.MeekDialAddress
   969  		}
   970  
   971  		if protocol.TunnelProtocolUsesFrontedMeek(dialParams.TunnelProtocol) {
   972  			meekResolvedIPAddress := dialParams.MeekResolvedIPAddress.Load().(string)
   973  			if meekResolvedIPAddress != "" {
   974  				params["meek_resolved_ip_address"] = meekResolvedIPAddress
   975  			}
   976  		}
   977  
   978  		if dialParams.MeekSNIServerName != "" {
   979  			params["meek_sni_server_name"] = dialParams.MeekSNIServerName
   980  		}
   981  
   982  		if dialParams.MeekHostHeader != "" {
   983  			params["meek_host_header"] = dialParams.MeekHostHeader
   984  		}
   985  
   986  		// MeekTransformedHostName is meaningful when meek is used, which is when
   987  		// MeekDialAddress != ""
   988  		if dialParams.MeekDialAddress != "" {
   989  			transformedHostName := "0"
   990  			if dialParams.MeekTransformedHostName {
   991  				transformedHostName = "1"
   992  			}
   993  			params["meek_transformed_host_name"] = transformedHostName
   994  		}
   995  
   996  		if dialParams.TLSOSSHSNIServerName != "" {
   997  			params["tls_ossh_sni_server_name"] = dialParams.TLSOSSHSNIServerName
   998  		}
   999  
  1000  		if dialParams.TLSOSSHTransformedSNIServerName {
  1001  			params["tls_ossh_transformed_host_name"] = "1"
  1002  		}
  1003  
  1004  		if dialParams.SelectedUserAgent {
  1005  			params["user_agent"] = dialParams.UserAgent
  1006  		}
  1007  
  1008  		if dialParams.SelectedTLSProfile {
  1009  			params["tls_profile"] = dialParams.TLSProfile
  1010  			params["tls_version"] = dialParams.GetTLSVersionForMetrics()
  1011  		}
  1012  
  1013  		if dialParams.ServerEntry.Region != "" {
  1014  			params["server_entry_region"] = dialParams.ServerEntry.Region
  1015  		}
  1016  
  1017  		if dialParams.ServerEntry.LocalSource != "" {
  1018  			params["server_entry_source"] = dialParams.ServerEntry.LocalSource
  1019  		}
  1020  
  1021  		// As with last_connected, this timestamp stat, which may be a precise
  1022  		// handshake request server timestamp, is truncated to hour granularity to
  1023  		// avoid introducing a reconstructable cross-session user trace into server
  1024  		// logs.
  1025  		localServerEntryTimestamp := common.TruncateTimestampToHour(
  1026  			dialParams.ServerEntry.LocalTimestamp)
  1027  		if localServerEntryTimestamp != "" {
  1028  			params["server_entry_timestamp"] = localServerEntryTimestamp
  1029  		}
  1030  
  1031  		params[tactics.APPLIED_TACTICS_TAG_PARAMETER_NAME] =
  1032  			config.GetParameters().Get().Tag()
  1033  
  1034  		if dialParams.DialPortNumber != "" {
  1035  			params["dial_port_number"] = dialParams.DialPortNumber
  1036  		}
  1037  
  1038  		if dialParams.QUICVersion != "" {
  1039  			params["quic_version"] = dialParams.QUICVersion
  1040  		}
  1041  
  1042  		if dialParams.QUICDialSNIAddress != "" {
  1043  			params["quic_dial_sni_address"] = dialParams.QUICDialSNIAddress
  1044  		}
  1045  
  1046  		if dialParams.QUICDisablePathMTUDiscovery {
  1047  			params["quic_disable_client_path_mtu_discovery"] = "1"
  1048  		}
  1049  
  1050  		isReplay := "0"
  1051  		if dialParams.IsReplay {
  1052  			isReplay = "1"
  1053  		}
  1054  		params["is_replay"] = isReplay
  1055  
  1056  		if config.EgressRegion != "" {
  1057  			params["egress_region"] = config.EgressRegion
  1058  		}
  1059  
  1060  		// dialParams.DialDuration is nanoseconds; report milliseconds
  1061  		params["dial_duration"] = fmt.Sprintf("%d", dialParams.DialDuration/time.Millisecond)
  1062  
  1063  		params["candidate_number"] = strconv.Itoa(dialParams.CandidateNumber)
  1064  
  1065  		params["established_tunnels_count"] = strconv.Itoa(dialParams.EstablishedTunnelsCount)
  1066  
  1067  		if dialParams.NetworkLatencyMultiplier != 0.0 {
  1068  			params["network_latency_multiplier"] =
  1069  				fmt.Sprintf("%f", dialParams.NetworkLatencyMultiplier)
  1070  		}
  1071  
  1072  		if dialParams.ConjureTransport != "" {
  1073  			params["conjure_transport"] = dialParams.ConjureTransport
  1074  		}
  1075  
  1076  		if dialParams.ResolveParameters != nil {
  1077  
  1078  			if dialParams.ResolveParameters.PreresolvedIPAddress != "" {
  1079  				params["dns_preresolved"] = dialParams.ResolveParameters.PreresolvedIPAddress
  1080  
  1081  			} else {
  1082  
  1083  				// Log enough information to distinguish several successful or
  1084  				// failed circumvention cases of interest, including preferring
  1085  				// alternate servers and/or using DNS protocol transforms, and
  1086  				// appropriate for both handshake and failed_tunnel logging:
  1087  				//
  1088  				// - The initial attempt made by Resolver.ResolveIP,
  1089  				//   preferring an alternate DNS server and/or using a
  1090  				//   protocol transform succeeds (dns_result = 0, the initial
  1091  				//   attempt, 0, got the first result).
  1092  				//
  1093  				// - A second attempt may be used, still preferring an
  1094  				//   alternate DNS server but no longer using the protocol
  1095  				//   transform, which presumably failed (dns_result = 1, the
  1096  				//   second attempt, 1, got the first result).
  1097  				//
  1098  				// - Subsequent attempts will use the system DNS server and no
  1099  				//   protocol transforms (dns_result > 2).
  1100  				//
  1101  				// Due to the design of Resolver.ResolveIP, the notion
  1102  				// of "success" is approximate; for example a successful
  1103  				// response may arrive after a subsequent attempt succeeds,
  1104  				// simply due to slow network conditions. It's also possible
  1105  				// that, for a given attemp, only one of the two concurrent
  1106  				// requests (A and AAAA) succeeded.
  1107  				//
  1108  				// Note that ResolveParameters.GetFirstAttemptWithAnswer
  1109  				// semantics assume that dialParams.ResolveParameters wasn't
  1110  				// used by or modified by any other dial.
  1111  
  1112  				if dialParams.ResolveParameters.PreferAlternateDNSServer {
  1113  					params["dns_preferred"] = dialParams.ResolveParameters.AlternateDNSServer
  1114  				}
  1115  
  1116  				if dialParams.ResolveParameters.ProtocolTransformName != "" {
  1117  					params["dns_transform"] = dialParams.ResolveParameters.ProtocolTransformName
  1118  				}
  1119  
  1120  				params["dns_attempt"] = strconv.Itoa(
  1121  					dialParams.ResolveParameters.GetFirstAttemptWithAnswer())
  1122  			}
  1123  		}
  1124  
  1125  		if dialParams.HTTPTransformerParameters != nil {
  1126  			if dialParams.HTTPTransformerParameters.ProtocolTransformSpec != nil {
  1127  				params["http_transform"] = dialParams.HTTPTransformerParameters.ProtocolTransformName
  1128  			}
  1129  		}
  1130  
  1131  		if dialParams.OSSHObfuscatorSeedTransformerParameters != nil {
  1132  			if dialParams.OSSHObfuscatorSeedTransformerParameters.TransformSpec != nil {
  1133  				params["seed_transform"] = dialParams.OSSHObfuscatorSeedTransformerParameters.TransformName
  1134  			}
  1135  		}
  1136  
  1137  		if dialParams.ObfuscatedQUICNonceTransformerParameters != nil {
  1138  			if dialParams.ObfuscatedQUICNonceTransformerParameters.TransformSpec != nil {
  1139  				params["seed_transform"] = dialParams.ObfuscatedQUICNonceTransformerParameters.TransformName
  1140  			}
  1141  		}
  1142  
  1143  		if dialParams.OSSHPrefixSpec != nil {
  1144  			if dialParams.OSSHPrefixSpec.Spec != nil {
  1145  				params["ossh_prefix"] = dialParams.OSSHPrefixSpec.Name
  1146  			}
  1147  		}
  1148  
  1149  		if dialParams.DialConnMetrics != nil {
  1150  			metrics := dialParams.DialConnMetrics.GetMetrics()
  1151  			for name, value := range metrics {
  1152  				params[name] = fmt.Sprintf("%v", value)
  1153  			}
  1154  		}
  1155  
  1156  		if dialParams.ObfuscatedSSHConnMetrics != nil {
  1157  			metrics := dialParams.ObfuscatedSSHConnMetrics.GetMetrics()
  1158  			for name, value := range metrics {
  1159  				params[name] = fmt.Sprintf("%v", value)
  1160  			}
  1161  		}
  1162  
  1163  	} else if filter == baseParametersOnlyUpstreamFragmentorDialParameters {
  1164  
  1165  		if dialParams.DialConnMetrics != nil {
  1166  			names := fragmentor.GetUpstreamMetricsNames()
  1167  			metrics := dialParams.DialConnMetrics.GetMetrics()
  1168  			for name, value := range metrics {
  1169  				if common.Contains(names, name) {
  1170  					params[name] = fmt.Sprintf("%v", value)
  1171  				}
  1172  			}
  1173  		}
  1174  	}
  1175  
  1176  	return params
  1177  }
  1178  
  1179  // makeRequestUrl makes a URL for a web service API request.
  1180  func makeRequestUrl(tunnel *Tunnel, port, path string, params common.APIParameters) string {
  1181  	var requestUrl bytes.Buffer
  1182  
  1183  	if port == "" {
  1184  		port = tunnel.dialParams.ServerEntry.WebServerPort
  1185  	}
  1186  
  1187  	requestUrl.WriteString("https://")
  1188  	requestUrl.WriteString(tunnel.dialParams.ServerEntry.IpAddress)
  1189  	requestUrl.WriteString(":")
  1190  	requestUrl.WriteString(port)
  1191  	requestUrl.WriteString("/")
  1192  	requestUrl.WriteString(path)
  1193  
  1194  	if len(params) > 0 {
  1195  
  1196  		queryParams := url.Values{}
  1197  
  1198  		for name, value := range params {
  1199  
  1200  			// Note: this logic skips the tactics.SPEED_TEST_SAMPLES_PARAMETER_NAME
  1201  			// parameter, which has a different type. This parameter is not recognized
  1202  			// by legacy servers.
  1203  
  1204  			switch v := value.(type) {
  1205  			case string:
  1206  				queryParams.Set(name, v)
  1207  			case []string:
  1208  				// String array param encoded as JSON
  1209  				jsonValue, err := json.Marshal(v)
  1210  				if err != nil {
  1211  					break
  1212  				}
  1213  				queryParams.Set(name, string(jsonValue))
  1214  			}
  1215  		}
  1216  
  1217  		requestUrl.WriteString("?")
  1218  		requestUrl.WriteString(queryParams.Encode())
  1219  	}
  1220  
  1221  	return requestUrl.String()
  1222  }
  1223  
  1224  // makePsiphonHttpsClient creates a Psiphon HTTPS client that tunnels web service API
  1225  // requests and which validates the web server using the Psiphon server entry web server
  1226  // certificate.
  1227  func makePsiphonHttpsClient(tunnel *Tunnel) (httpsClient *http.Client, err error) {
  1228  
  1229  	certificate, err := DecodeCertificate(
  1230  		tunnel.dialParams.ServerEntry.WebServerCertificate)
  1231  	if err != nil {
  1232  		return nil, errors.Trace(err)
  1233  	}
  1234  
  1235  	tunneledDialer := func(_ context.Context, _, addr string) (net.Conn, error) {
  1236  		// This case bypasses tunnel.Dial, to avoid its check that the tunnel is
  1237  		// already active (it won't be pre-handshake). This bypass won't handle the
  1238  		// server rejecting the port forward due to split tunnel classification, but
  1239  		// we know that the server won't classify the web API destination as
  1240  		// untunneled.
  1241  		return tunnel.sshClient.Dial("tcp", addr)
  1242  	}
  1243  
  1244  	// Note: as with SSH API requests, there no dial context here. SSH port forward dials
  1245  	// cannot be interrupted directly. Closing the tunnel will interrupt both the dial and
  1246  	// the request. While it's possible to add a timeout here, we leave it with no explicit
  1247  	// timeout which is the same as SSH API requests: if the tunnel has stalled then SSH keep
  1248  	// alives will cause the tunnel to close.
  1249  
  1250  	dialer := NewCustomTLSDialer(
  1251  		&CustomTLSConfig{
  1252  			Parameters:              tunnel.config.GetParameters(),
  1253  			Dial:                    tunneledDialer,
  1254  			VerifyLegacyCertificate: certificate,
  1255  		})
  1256  
  1257  	transport := &http.Transport{
  1258  		DialTLS: func(network, addr string) (net.Conn, error) {
  1259  			return dialer(context.Background(), network, addr)
  1260  		},
  1261  		Dial: func(network, addr string) (net.Conn, error) {
  1262  			return nil, errors.TraceNew("HTTP not supported")
  1263  		},
  1264  	}
  1265  
  1266  	return &http.Client{
  1267  		Transport: transport,
  1268  	}, nil
  1269  }
  1270  
  1271  func HandleServerRequest(
  1272  	tunnelOwner TunnelOwner, tunnel *Tunnel, name string, payload []byte) error {
  1273  
  1274  	switch name {
  1275  	case protocol.PSIPHON_API_OSL_REQUEST_NAME:
  1276  		return HandleOSLRequest(tunnelOwner, tunnel, payload)
  1277  	case protocol.PSIPHON_API_ALERT_REQUEST_NAME:
  1278  		return HandleAlertRequest(tunnelOwner, tunnel, payload)
  1279  	}
  1280  
  1281  	return errors.Tracef("invalid request name: %s", name)
  1282  }
  1283  
  1284  func HandleOSLRequest(
  1285  	tunnelOwner TunnelOwner, tunnel *Tunnel, payload []byte) error {
  1286  
  1287  	var oslRequest protocol.OSLRequest
  1288  	err := json.Unmarshal(payload, &oslRequest)
  1289  	if err != nil {
  1290  		return errors.Trace(err)
  1291  	}
  1292  
  1293  	if oslRequest.ClearLocalSLOKs {
  1294  		DeleteSLOKs()
  1295  	}
  1296  
  1297  	seededNewSLOK := false
  1298  
  1299  	for _, slok := range oslRequest.SeedPayload.SLOKs {
  1300  		duplicate, err := SetSLOK(slok.ID, slok.Key)
  1301  		if err != nil {
  1302  			// TODO: return error to trigger retry?
  1303  			NoticeWarning("SetSLOK failed: %s", errors.Trace(err))
  1304  		} else if !duplicate {
  1305  			seededNewSLOK = true
  1306  		}
  1307  
  1308  		if tunnel.config.EmitSLOKs {
  1309  			NoticeSLOKSeeded(base64.StdEncoding.EncodeToString(slok.ID), duplicate)
  1310  		}
  1311  	}
  1312  
  1313  	if seededNewSLOK {
  1314  		tunnelOwner.SignalSeededNewSLOK()
  1315  	}
  1316  
  1317  	return nil
  1318  }
  1319  
  1320  func HandleAlertRequest(
  1321  	tunnelOwner TunnelOwner, tunnel *Tunnel, payload []byte) error {
  1322  
  1323  	var alertRequest protocol.AlertRequest
  1324  	err := json.Unmarshal(payload, &alertRequest)
  1325  	if err != nil {
  1326  		return errors.Trace(err)
  1327  	}
  1328  
  1329  	if tunnel.config.EmitServerAlerts {
  1330  		NoticeServerAlert(alertRequest)
  1331  	}
  1332  
  1333  	return nil
  1334  }