github.com/Psiphon-Labs/psiphon-tunnel-core@v2.0.28+incompatible/psiphon/server/api.go (about)

     1  /*
     2   * Copyright (c) 2016, 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 server
    21  
    22  import (
    23  	"crypto/hmac"
    24  	"crypto/sha256"
    25  	"crypto/subtle"
    26  	"encoding/base64"
    27  	"encoding/json"
    28  	std_errors "errors"
    29  	"net"
    30  	"regexp"
    31  	"strconv"
    32  	"strings"
    33  	"time"
    34  	"unicode"
    35  
    36  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
    37  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
    38  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/fragmentor"
    39  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
    40  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/tactics"
    41  )
    42  
    43  const (
    44  	MAX_API_PARAMS_SIZE = 256 * 1024 // 256KB
    45  	PADDING_MAX_BYTES   = 16 * 1024
    46  
    47  	CLIENT_PLATFORM_ANDROID = "Android"
    48  	CLIENT_PLATFORM_WINDOWS = "Windows"
    49  	CLIENT_PLATFORM_IOS     = "iOS"
    50  )
    51  
    52  // sshAPIRequestHandler routes Psiphon API requests transported as
    53  // JSON objects via the SSH request mechanism.
    54  //
    55  // The API request handlers, handshakeAPIRequestHandler, etc., are
    56  // reused by webServer which offers the Psiphon API via web transport.
    57  //
    58  // The API request parameters and event log values follow the legacy
    59  // psi_web protocol and naming conventions. The API is compatible with
    60  // all tunnel-core clients but are not backwards compatible with all
    61  // legacy clients.
    62  //
    63  func sshAPIRequestHandler(
    64  	support *SupportServices,
    65  	clientAddr string,
    66  	geoIPData GeoIPData,
    67  	authorizedAccessTypes []string,
    68  	name string,
    69  	requestPayload []byte) ([]byte, error) {
    70  
    71  	// Notes:
    72  	//
    73  	// - For SSH requests, MAX_API_PARAMS_SIZE is implicitly enforced
    74  	//   by max SSH request packet size.
    75  	//
    76  	// - The param protocol.PSIPHON_API_HANDSHAKE_AUTHORIZATIONS is an
    77  	//   array of base64-encoded strings; the base64 representation should
    78  	//   not be decoded to []byte values. The default behavior of
    79  	//   https://golang.org/pkg/encoding/json/#Unmarshal for a target of
    80  	//   type map[string]interface{} will unmarshal a base64-encoded string
    81  	//   to a string, not a decoded []byte, as required.
    82  
    83  	var params common.APIParameters
    84  	err := json.Unmarshal(requestPayload, &params)
    85  	if err != nil {
    86  		return nil, errors.Tracef(
    87  			"invalid payload for request name: %s: %s", name, err)
    88  	}
    89  
    90  	return dispatchAPIRequestHandler(
    91  		support,
    92  		protocol.PSIPHON_SSH_API_PROTOCOL,
    93  		clientAddr,
    94  		geoIPData,
    95  		authorizedAccessTypes,
    96  		name,
    97  		params)
    98  }
    99  
   100  // dispatchAPIRequestHandler is the common dispatch point for both
   101  // web and SSH API requests.
   102  func dispatchAPIRequestHandler(
   103  	support *SupportServices,
   104  	apiProtocol string,
   105  	clientAddr string,
   106  	geoIPData GeoIPData,
   107  	authorizedAccessTypes []string,
   108  	name string,
   109  	params common.APIParameters) (response []byte, reterr error) {
   110  
   111  	// Before invoking the handlers, enforce some preconditions:
   112  	//
   113  	// - A handshake request must precede any other requests.
   114  	// - When the handshake results in a traffic rules state where
   115  	//   the client is immediately exhausted, no requests
   116  	//   may succeed. This case ensures that blocked clients do
   117  	//   not log "connected", etc.
   118  	//
   119  	// Only one handshake request may be made. There is no check here
   120  	// to enforce that handshakeAPIRequestHandler will be called at
   121  	// most once. The SetHandshakeState call in handshakeAPIRequestHandler
   122  	// enforces that only a single handshake is made; enforcing that there
   123  	// ensures no race condition even if concurrent requests are
   124  	// in flight.
   125  
   126  	if name != protocol.PSIPHON_API_HANDSHAKE_REQUEST_NAME {
   127  
   128  		// TODO: same session-ID-lookup TODO in handshakeAPIRequestHandler
   129  		// applies here.
   130  		sessionID, err := getStringRequestParam(params, "client_session_id")
   131  		if err == nil {
   132  			// Note: follows/duplicates baseParams validation
   133  			if !isHexDigits(support.Config, sessionID) {
   134  				err = std_errors.New("invalid param: client_session_id")
   135  			}
   136  		}
   137  		if err != nil {
   138  			return nil, errors.Trace(err)
   139  		}
   140  
   141  		completed, exhausted, err := support.TunnelServer.GetClientHandshaked(sessionID)
   142  		if err != nil {
   143  			return nil, errors.Trace(err)
   144  		}
   145  		if !completed {
   146  			return nil, errors.TraceNew("handshake not completed")
   147  		}
   148  		if exhausted {
   149  			return nil, errors.TraceNew("exhausted after handshake")
   150  		}
   151  	}
   152  
   153  	switch name {
   154  
   155  	case protocol.PSIPHON_API_HANDSHAKE_REQUEST_NAME:
   156  		return handshakeAPIRequestHandler(
   157  			support, apiProtocol, clientAddr, geoIPData, params)
   158  
   159  	case protocol.PSIPHON_API_CONNECTED_REQUEST_NAME:
   160  		return connectedAPIRequestHandler(
   161  			support, clientAddr, geoIPData, authorizedAccessTypes, params)
   162  
   163  	case protocol.PSIPHON_API_STATUS_REQUEST_NAME:
   164  		return statusAPIRequestHandler(
   165  			support, clientAddr, geoIPData, authorizedAccessTypes, params)
   166  
   167  	case protocol.PSIPHON_API_CLIENT_VERIFICATION_REQUEST_NAME:
   168  		return clientVerificationAPIRequestHandler(
   169  			support, clientAddr, geoIPData, authorizedAccessTypes, params)
   170  	}
   171  
   172  	return nil, errors.Tracef("invalid request name: %s", name)
   173  }
   174  
   175  var handshakeRequestParams = append(
   176  	append(
   177  		append(
   178  			[]requestParamSpec{
   179  				// Legacy clients may not send "session_id" in handshake
   180  				{"session_id", isHexDigits, requestParamOptional},
   181  				{"missing_server_entry_signature", isBase64String, requestParamOptional}},
   182  			baseParams...),
   183  		baseDialParams...),
   184  	tacticsParams...)
   185  
   186  // handshakeAPIRequestHandler implements the "handshake" API request.
   187  // Clients make the handshake immediately after establishing a tunnel
   188  // connection; the response tells the client what homepage to open, what
   189  // stats to record, etc.
   190  func handshakeAPIRequestHandler(
   191  	support *SupportServices,
   192  	apiProtocol string,
   193  	clientAddr string,
   194  	geoIPData GeoIPData,
   195  	params common.APIParameters) ([]byte, error) {
   196  
   197  	// Note: ignoring legacy "known_servers" params
   198  
   199  	err := validateRequestParams(support.Config, params, handshakeRequestParams)
   200  	if err != nil {
   201  		return nil, errors.Trace(err)
   202  	}
   203  
   204  	sessionID, _ := getStringRequestParam(params, "client_session_id")
   205  	sponsorID, _ := getStringRequestParam(params, "sponsor_id")
   206  	clientVersion, _ := getStringRequestParam(params, "client_version")
   207  	clientPlatform, _ := getStringRequestParam(params, "client_platform")
   208  	isMobile := isMobileClientPlatform(clientPlatform)
   209  	normalizedPlatform := normalizeClientPlatform(clientPlatform)
   210  
   211  	// establishedTunnelsCount is used in traffic rule selection. When omitted by
   212  	// the client, a value of 0 will be used.
   213  	establishedTunnelsCount, _ := getIntStringRequestParam(params, "established_tunnels_count")
   214  
   215  	// splitTunnelOwnRegion indicates if the client is requesting split tunnel
   216  	// mode to be applied to the client's own country. When omitted by the
   217  	// client, the value will be false.
   218  	//
   219  	// When split_tunnel_regions is non-empty, split tunnel mode will be
   220  	// applied for the specified country codes. When omitted by the client,
   221  	// the value will be an empty slice.
   222  	splitTunnelOwnRegion, _ := getBoolStringRequestParam(params, "split_tunnel")
   223  	splitTunnelOtherRegions, _ := getStringArrayRequestParam(params, "split_tunnel_regions")
   224  
   225  	ownRegion := ""
   226  	if splitTunnelOwnRegion {
   227  		ownRegion = geoIPData.Country
   228  	}
   229  	var splitTunnelLookup *splitTunnelLookup
   230  	if ownRegion != "" || len(splitTunnelOtherRegions) > 0 {
   231  		splitTunnelLookup, err = newSplitTunnelLookup(ownRegion, splitTunnelOtherRegions)
   232  		if err != nil {
   233  			return nil, errors.Trace(err)
   234  		}
   235  	}
   236  
   237  	var authorizations []string
   238  	if params[protocol.PSIPHON_API_HANDSHAKE_AUTHORIZATIONS] != nil {
   239  		authorizations, err = getStringArrayRequestParam(params, protocol.PSIPHON_API_HANDSHAKE_AUTHORIZATIONS)
   240  		if err != nil {
   241  			return nil, errors.Trace(err)
   242  		}
   243  	}
   244  
   245  	// Note: no guarantee that PsinetDatabase won't reload between database calls
   246  	db := support.PsinetDatabase
   247  
   248  	httpsRequestRegexes, domainBytesChecksum := db.GetHttpsRequestRegexes(sponsorID)
   249  
   250  	// Flag the SSH client as having completed its handshake. This
   251  	// may reselect traffic rules and starts allowing port forwards.
   252  
   253  	// TODO: in the case of SSH API requests, the actual sshClient could
   254  	// be passed in and used here. The session ID lookup is only strictly
   255  	// necessary to support web API requests.
   256  	handshakeStateInfo, err := support.TunnelServer.SetClientHandshakeState(
   257  		sessionID,
   258  		handshakeState{
   259  			completed:               true,
   260  			apiProtocol:             apiProtocol,
   261  			apiParams:               copyBaseSessionAndDialParams(params),
   262  			domainBytesChecksum:     domainBytesChecksum,
   263  			establishedTunnelsCount: establishedTunnelsCount,
   264  			splitTunnelLookup:       splitTunnelLookup,
   265  		},
   266  		authorizations)
   267  	if err != nil {
   268  		return nil, errors.Trace(err)
   269  	}
   270  
   271  	tacticsPayload, err := support.TacticsServer.GetTacticsPayload(
   272  		common.GeoIPData(geoIPData), params)
   273  	if err != nil {
   274  		return nil, errors.Trace(err)
   275  	}
   276  
   277  	var marshaledTacticsPayload []byte
   278  
   279  	if tacticsPayload != nil {
   280  
   281  		marshaledTacticsPayload, err = json.Marshal(tacticsPayload)
   282  		if err != nil {
   283  			return nil, errors.Trace(err)
   284  		}
   285  
   286  		// Log a metric when new tactics are issued. Logging here indicates that
   287  		// the handshake tactics mechanism is active; but logging for every
   288  		// handshake creates unneccesary log data.
   289  
   290  		if len(tacticsPayload.Tactics) > 0 {
   291  
   292  			logFields := getRequestLogFields(
   293  				tactics.TACTICS_METRIC_EVENT_NAME,
   294  				geoIPData,
   295  				handshakeStateInfo.authorizedAccessTypes,
   296  				params,
   297  				handshakeRequestParams)
   298  
   299  			logFields[tactics.NEW_TACTICS_TAG_LOG_FIELD_NAME] = tacticsPayload.Tag
   300  			logFields[tactics.IS_TACTICS_REQUEST_LOG_FIELD_NAME] = false
   301  
   302  			log.LogRawFieldsWithTimestamp(logFields)
   303  		}
   304  	}
   305  
   306  	// The log comes _after_ SetClientHandshakeState, in case that call rejects
   307  	// the state change (for example, if a second handshake is performed)
   308  	//
   309  	// The handshake event is no longer shipped to log consumers, so this is
   310  	// simply a diagnostic log. Since the "server_tunnel" event includes all
   311  	// common API parameters and "handshake_completed" flag, this handshake
   312  	// log is mostly redundant and set to debug level.
   313  
   314  	log.WithTraceFields(
   315  		getRequestLogFields(
   316  			"",
   317  			geoIPData,
   318  			handshakeStateInfo.authorizedAccessTypes,
   319  			params,
   320  			handshakeRequestParams)).Debug("handshake")
   321  
   322  	pad_response, _ := getPaddingSizeRequestParam(params, "pad_response")
   323  
   324  	// Discover new servers
   325  
   326  	disableDiscovery, err := support.TunnelServer.GetClientDisableDiscovery(sessionID)
   327  	if err != nil {
   328  		return nil, errors.Trace(err)
   329  	}
   330  
   331  	var encodedServerList []string
   332  
   333  	if !disableDiscovery {
   334  
   335  		host, _, err := net.SplitHostPort(clientAddr)
   336  		if err != nil {
   337  			return nil, errors.Trace(err)
   338  		}
   339  
   340  		clientIP := net.ParseIP(host)
   341  		if clientIP == nil {
   342  			return nil, errors.TraceNew("missing client IP")
   343  		}
   344  
   345  		encodedServerList = db.DiscoverServers(
   346  			calculateDiscoveryValue(support.Config.DiscoveryValueHMACKey, clientIP))
   347  	}
   348  
   349  	// When the client indicates that it used an unsigned server entry for this
   350  	// connection, return a signed copy of the server entry for the client to
   351  	// upgrade to. See also: comment in psiphon.doHandshakeRequest.
   352  	//
   353  	// The missing_server_entry_signature parameter value is a server entry tag,
   354  	// which is used to select the correct server entry for servers with multiple
   355  	// entries. Identifying the server entries tags instead of server IPs prevents
   356  	// an enumeration attack, where a malicious client can abuse this facilty to
   357  	// check if an arbitrary IP address is a Psiphon server.
   358  	serverEntryTag, ok := getOptionalStringRequestParam(
   359  		params, "missing_server_entry_signature")
   360  	if ok {
   361  		ownServerEntry, ok := support.Config.GetOwnEncodedServerEntry(serverEntryTag)
   362  		if ok {
   363  			encodedServerList = append(encodedServerList, ownServerEntry)
   364  		}
   365  	}
   366  
   367  	// PageViewRegexes is obsolete and not used by any tunnel-core clients. In
   368  	// the JSON response, return an empty array instead of null for legacy
   369  	// clients.
   370  
   371  	handshakeResponse := protocol.HandshakeResponse{
   372  		SSHSessionID:             sessionID,
   373  		Homepages:                db.GetRandomizedHomepages(sponsorID, geoIPData.Country, geoIPData.ASN, isMobile),
   374  		UpgradeClientVersion:     db.GetUpgradeClientVersion(clientVersion, normalizedPlatform),
   375  		PageViewRegexes:          make([]map[string]string, 0),
   376  		HttpsRequestRegexes:      httpsRequestRegexes,
   377  		EncodedServerList:        encodedServerList,
   378  		ClientRegion:             geoIPData.Country,
   379  		ClientAddress:            clientAddr,
   380  		ServerTimestamp:          common.GetCurrentTimestamp(),
   381  		ActiveAuthorizationIDs:   handshakeStateInfo.activeAuthorizationIDs,
   382  		TacticsPayload:           marshaledTacticsPayload,
   383  		UpstreamBytesPerSecond:   handshakeStateInfo.upstreamBytesPerSecond,
   384  		DownstreamBytesPerSecond: handshakeStateInfo.downstreamBytesPerSecond,
   385  		Padding:                  strings.Repeat(" ", pad_response),
   386  	}
   387  
   388  	responsePayload, err := json.Marshal(handshakeResponse)
   389  	if err != nil {
   390  		return nil, errors.Trace(err)
   391  	}
   392  
   393  	return responsePayload, nil
   394  }
   395  
   396  // calculateDiscoveryValue derives a value from the client IP address to be
   397  // used as input in the server discovery algorithm.
   398  // See https://github.com/Psiphon-Inc/psiphon-automation/tree/master/Automation/psi_ops_discovery.py
   399  // for full details.
   400  func calculateDiscoveryValue(discoveryValueHMACKey string, ipAddress net.IP) int {
   401  	// From: psi_ops_discovery.calculate_ip_address_strategy_value:
   402  	//     # Mix bits from all octets of the client IP address to determine the
   403  	//     # bucket. An HMAC is used to prevent pre-calculation of buckets for IPs.
   404  	//     return ord(hmac.new(HMAC_KEY, ip_address, hashlib.sha256).digest()[0])
   405  	// TODO: use 3-octet algorithm?
   406  	hash := hmac.New(sha256.New, []byte(discoveryValueHMACKey))
   407  	hash.Write([]byte(ipAddress.String()))
   408  	return int(hash.Sum(nil)[0])
   409  }
   410  
   411  // uniqueUserParams are the connected request parameters which are logged for
   412  // unique_user events.
   413  var uniqueUserParams = append(
   414  	[]requestParamSpec{
   415  		{"last_connected", isLastConnected, 0}},
   416  	baseSessionParams...)
   417  
   418  var connectedRequestParams = append(
   419  	[]requestParamSpec{
   420  		{"establishment_duration", isIntString, requestParamOptional | requestParamLogStringAsInt}},
   421  	uniqueUserParams...)
   422  
   423  // updateOnConnectedParamNames are connected request parameters which are
   424  // copied to update data logged with server_tunnel: these fields either only
   425  // ship with or ship newer data with connected requests.
   426  var updateOnConnectedParamNames = append(
   427  	[]string{
   428  		"last_connected",
   429  		"establishment_duration",
   430  	},
   431  	fragmentor.GetUpstreamMetricsNames()...)
   432  
   433  // connectedAPIRequestHandler implements the "connected" API request. Clients
   434  // make the connected request once a tunnel connection has been established
   435  // and at least once per 24h for long-running tunnels. The last_connected
   436  // input value, which should be a connected_timestamp output from a previous
   437  // connected response, is used to calculate unique user stats.
   438  // connected_timestamp is truncated as a privacy measure.
   439  func connectedAPIRequestHandler(
   440  	support *SupportServices,
   441  	clientAddr string,
   442  	geoIPData GeoIPData,
   443  	authorizedAccessTypes []string,
   444  	params common.APIParameters) ([]byte, error) {
   445  
   446  	err := validateRequestParams(support.Config, params, connectedRequestParams)
   447  	if err != nil {
   448  		return nil, errors.Trace(err)
   449  	}
   450  
   451  	sessionID, _ := getStringRequestParam(params, "client_session_id")
   452  	lastConnected, _ := getStringRequestParam(params, "last_connected")
   453  
   454  	// Update, for server_tunnel logging, upstream fragmentor metrics, as the
   455  	// client may have performed more upstream fragmentation since the previous
   456  	// metrics reported by the handshake request. Also, additional fields that
   457  	// are reported only in the connected request are added to server_tunnel
   458  	// here.
   459  
   460  	// TODO: same session-ID-lookup TODO in handshakeAPIRequestHandler
   461  	// applies here.
   462  	err = support.TunnelServer.UpdateClientAPIParameters(
   463  		sessionID, copyUpdateOnConnectedParams(params))
   464  	if err != nil {
   465  		return nil, errors.Trace(err)
   466  	}
   467  
   468  	connectedTimestamp := common.TruncateTimestampToHour(common.GetCurrentTimestamp())
   469  
   470  	// The finest required granularity for unique users is daily. To save space,
   471  	// only record a "unique_user" log event when the client's last_connected is
   472  	// in the previous day relative to the new connected_timestamp.
   473  
   474  	logUniqueUser := false
   475  	if lastConnected == "None" {
   476  		logUniqueUser = true
   477  	} else {
   478  
   479  		t1, _ := time.Parse(time.RFC3339, lastConnected)
   480  		year, month, day := t1.Date()
   481  		d1 := time.Date(year, month, day, 0, 0, 0, 0, time.UTC)
   482  
   483  		t2, _ := time.Parse(time.RFC3339, connectedTimestamp)
   484  		year, month, day = t2.Date()
   485  		d2 := time.Date(year, month, day, 0, 0, 0, 0, time.UTC)
   486  
   487  		if t1.Before(t2) && d1 != d2 {
   488  			logUniqueUser = true
   489  		}
   490  	}
   491  
   492  	if logUniqueUser {
   493  		log.LogRawFieldsWithTimestamp(
   494  			getRequestLogFields(
   495  				"unique_user",
   496  				geoIPData,
   497  				authorizedAccessTypes,
   498  				params,
   499  				uniqueUserParams))
   500  	}
   501  
   502  	pad_response, _ := getPaddingSizeRequestParam(params, "pad_response")
   503  
   504  	connectedResponse := protocol.ConnectedResponse{
   505  		ConnectedTimestamp: connectedTimestamp,
   506  		Padding:            strings.Repeat(" ", pad_response),
   507  	}
   508  
   509  	responsePayload, err := json.Marshal(connectedResponse)
   510  	if err != nil {
   511  		return nil, errors.Trace(err)
   512  	}
   513  
   514  	return responsePayload, nil
   515  }
   516  
   517  var statusRequestParams = baseSessionParams
   518  
   519  var remoteServerListStatParams = append(
   520  	[]requestParamSpec{
   521  		{"client_download_timestamp", isISO8601Date, 0},
   522  		{"tunneled", isBooleanFlag, requestParamOptional | requestParamLogFlagAsBool},
   523  		{"url", isAnyString, 0},
   524  		{"etag", isAnyString, 0},
   525  		{"bytes", isIntString, requestParamOptional | requestParamLogStringAsInt},
   526  		{"duration", isIntString, requestParamOptional | requestParamLogStringAsInt},
   527  		{"authenticated", isBooleanFlag, requestParamOptional | requestParamLogFlagAsBool}},
   528  	baseSessionParams...)
   529  
   530  // Backwards compatibility case: legacy clients do not include these fields in
   531  // the remote_server_list_stats entries. Use the values from the outer status
   532  // request as an approximation (these values reflect the client at persistent
   533  // stat shipping time, which may differ from the client at persistent stat
   534  // recording time). Note that all but client_build_rev and device_region are
   535  // required fields.
   536  var remoteServerListStatBackwardsCompatibilityParamNames = []string{
   537  	"session_id",
   538  	"propagation_channel_id",
   539  	"sponsor_id",
   540  	"client_version",
   541  	"client_platform",
   542  	"client_build_rev",
   543  	"device_region",
   544  }
   545  
   546  var failedTunnelStatParams = append(
   547  	[]requestParamSpec{
   548  		{"server_entry_tag", isAnyString, requestParamOptional},
   549  		{"session_id", isHexDigits, 0},
   550  		{"last_connected", isLastConnected, 0},
   551  		{"client_failed_timestamp", isISO8601Date, 0},
   552  		{"liveness_test_upstream_bytes", isIntString, requestParamOptional | requestParamLogStringAsInt},
   553  		{"liveness_test_sent_upstream_bytes", isIntString, requestParamOptional | requestParamLogStringAsInt},
   554  		{"liveness_test_downstream_bytes", isIntString, requestParamOptional | requestParamLogStringAsInt},
   555  		{"liveness_test_received_downstream_bytes", isIntString, requestParamOptional | requestParamLogStringAsInt},
   556  		{"bytes_up", isIntString, requestParamOptional | requestParamLogStringAsInt},
   557  		{"bytes_down", isIntString, requestParamOptional | requestParamLogStringAsInt},
   558  		{"tunnel_error", isAnyString, 0}},
   559  	baseSessionAndDialParams...)
   560  
   561  // statusAPIRequestHandler implements the "status" API request.
   562  // Clients make periodic status requests which deliver client-side
   563  // recorded data transfer and tunnel duration stats.
   564  // Note from psi_web implementation: no input validation on domains;
   565  // any string is accepted (regex transform may result in arbitrary
   566  // string). Stats processor must handle this input with care.
   567  func statusAPIRequestHandler(
   568  	support *SupportServices,
   569  	clientAddr string,
   570  	geoIPData GeoIPData,
   571  	authorizedAccessTypes []string,
   572  	params common.APIParameters) ([]byte, error) {
   573  
   574  	err := validateRequestParams(support.Config, params, statusRequestParams)
   575  	if err != nil {
   576  		return nil, errors.Trace(err)
   577  	}
   578  
   579  	sessionID, _ := getStringRequestParam(params, "client_session_id")
   580  
   581  	statusData, err := getJSONObjectRequestParam(params, "statusData")
   582  	if err != nil {
   583  		return nil, errors.Trace(err)
   584  	}
   585  
   586  	// Logs are queued until the input is fully validated. Otherwise, stats
   587  	// could be double counted if the client has a bug in its request
   588  	// formatting: partial stats would be logged (counted), the request would
   589  	// fail, and clients would then resend all the same stats again.
   590  
   591  	logQueue := make([]LogFields, 0)
   592  
   593  	// Domain bytes transferred stats
   594  	// Older clients may not submit this data
   595  
   596  	// Clients are expected to send host_bytes/domain_bytes stats only when
   597  	// configured to do so in the handshake reponse. Legacy clients may still
   598  	// report "(OTHER)" host_bytes when no regexes are set. Drop those stats.
   599  
   600  	acceptDomainBytes, err := support.TunnelServer.AcceptClientDomainBytes(sessionID)
   601  	if err != nil {
   602  		return nil, errors.Trace(err)
   603  	}
   604  
   605  	if acceptDomainBytes && statusData["host_bytes"] != nil {
   606  
   607  		hostBytes, err := getMapStringInt64RequestParam(statusData, "host_bytes")
   608  		if err != nil {
   609  			return nil, errors.Trace(err)
   610  		}
   611  		for domain, bytes := range hostBytes {
   612  
   613  			domainBytesFields := getRequestLogFields(
   614  				"domain_bytes",
   615  				geoIPData,
   616  				authorizedAccessTypes,
   617  				params,
   618  				statusRequestParams)
   619  
   620  			domainBytesFields["domain"] = domain
   621  			domainBytesFields["bytes"] = bytes
   622  
   623  			logQueue = append(logQueue, domainBytesFields)
   624  		}
   625  	}
   626  
   627  	// Limitation: for "persistent" stats, host_id and geolocation is time-of-sending
   628  	// not time-of-recording.
   629  
   630  	// Remote server list download persistent stats.
   631  	// Older clients may not submit this data.
   632  
   633  	if statusData["remote_server_list_stats"] != nil {
   634  
   635  		remoteServerListStats, err := getJSONObjectArrayRequestParam(statusData, "remote_server_list_stats")
   636  		if err != nil {
   637  			return nil, errors.Trace(err)
   638  		}
   639  		for _, remoteServerListStat := range remoteServerListStats {
   640  
   641  			for _, name := range remoteServerListStatBackwardsCompatibilityParamNames {
   642  				if _, ok := remoteServerListStat[name]; !ok {
   643  					if field, ok := params[name]; ok {
   644  						remoteServerListStat[name] = field
   645  					}
   646  				}
   647  			}
   648  
   649  			// For validation, copy expected fields from the outer
   650  			// statusRequestParams.
   651  			remoteServerListStat["server_secret"] = params["server_secret"]
   652  			remoteServerListStat["client_session_id"] = params["client_session_id"]
   653  
   654  			err := validateRequestParams(support.Config, remoteServerListStat, remoteServerListStatParams)
   655  			if err != nil {
   656  				// Occasionally, clients may send corrupt persistent stat data. Do not
   657  				// fail the status request, as this will lead to endless retries.
   658  				log.WithTraceFields(LogFields{"error": err}).Warning("remote_server_list_stats entry dropped")
   659  				continue
   660  			}
   661  
   662  			remoteServerListFields := getRequestLogFields(
   663  				"remote_server_list",
   664  				geoIPData,
   665  				authorizedAccessTypes,
   666  				remoteServerListStat,
   667  				remoteServerListStatParams)
   668  
   669  			logQueue = append(logQueue, remoteServerListFields)
   670  		}
   671  	}
   672  
   673  	// Failed tunnel persistent stats.
   674  	// Older clients may not submit this data.
   675  
   676  	var invalidServerEntryTags map[string]bool
   677  
   678  	if statusData["failed_tunnel_stats"] != nil {
   679  
   680  		// Note: no guarantee that PsinetDatabase won't reload between database calls
   681  		db := support.PsinetDatabase
   682  
   683  		invalidServerEntryTags = make(map[string]bool)
   684  
   685  		failedTunnelStats, err := getJSONObjectArrayRequestParam(statusData, "failed_tunnel_stats")
   686  		if err != nil {
   687  			return nil, errors.Trace(err)
   688  		}
   689  		for _, failedTunnelStat := range failedTunnelStats {
   690  
   691  			// failed_tunnel supplies a full set of base params, but the server secret
   692  			// must use the correct value from the outer statusRequestParams.
   693  			failedTunnelStat["server_secret"] = params["server_secret"]
   694  
   695  			err := validateRequestParams(support.Config, failedTunnelStat, failedTunnelStatParams)
   696  			if err != nil {
   697  				// Occasionally, clients may send corrupt persistent stat data. Do not
   698  				// fail the status request, as this will lead to endless retries.
   699  				//
   700  				// TODO: trigger pruning if the data corruption indicates corrupt server
   701  				// entry storage?
   702  				log.WithTraceFields(LogFields{"error": err}).Warning("failed_tunnel_stats entry dropped")
   703  				continue
   704  			}
   705  
   706  			failedTunnelFields := getRequestLogFields(
   707  				"failed_tunnel",
   708  				geoIPData,
   709  				authorizedAccessTypes,
   710  				failedTunnelStat,
   711  				failedTunnelStatParams)
   712  
   713  			// Return a list of servers, identified by server entry tag, that are
   714  			// invalid and presumed to be deleted. This information is used by clients
   715  			// to prune deleted servers from their local datastores and stop attempting
   716  			// connections to servers that no longer exist.
   717  			//
   718  			// This mechanism uses tags instead of server IPs: (a) to prevent an
   719  			// enumeration attack, where a malicious client can query the entire IPv4
   720  			// range and build a map of the Psiphon network; (b) to deal with recyling
   721  			// cases where a server deleted and its IP is reused for a new server with
   722  			// a distinct server entry.
   723  			//
   724  			// IsValidServerEntryTag ensures that the local copy of psinet is not stale
   725  			// before returning a negative result, to mitigate accidental pruning.
   726  			//
   727  			// In addition, when the reported dial port number is 0, flag the server
   728  			// entry as invalid to trigger client pruning. This covers a class of
   729  			// invalid/semi-functional server entries, found in practice to be stored
   730  			// by clients, where some protocol port number has been omitted -- due to
   731  			// historical bugs in various server entry handling implementations. When
   732  			// missing from a server entry loaded by a client, the port number
   733  			// evaluates to 0, the zero value, which is not a valid port number even if
   734  			// were not missing.
   735  
   736  			serverEntryTag, ok := getOptionalStringRequestParam(failedTunnelStat, "server_entry_tag")
   737  
   738  			if ok {
   739  				serverEntryValid := db.IsValidServerEntryTag(serverEntryTag)
   740  
   741  				if serverEntryValid {
   742  					dialPortNumber, err := getIntStringRequestParam(failedTunnelStat, "dial_port_number")
   743  					if err == nil && dialPortNumber == 0 {
   744  						serverEntryValid = false
   745  					}
   746  				}
   747  
   748  				if !serverEntryValid {
   749  					invalidServerEntryTags[serverEntryTag] = true
   750  				}
   751  
   752  				// Add a field to the failed_tunnel log indicating if the server entry is
   753  				// valid.
   754  				failedTunnelFields["server_entry_valid"] = serverEntryValid
   755  			}
   756  
   757  			// Log failed_tunnel.
   758  
   759  			logQueue = append(logQueue, failedTunnelFields)
   760  		}
   761  	}
   762  
   763  	for _, logItem := range logQueue {
   764  		log.LogRawFieldsWithTimestamp(logItem)
   765  	}
   766  
   767  	pad_response, _ := getPaddingSizeRequestParam(params, "pad_response")
   768  
   769  	statusResponse := protocol.StatusResponse{
   770  		Padding: strings.Repeat(" ", pad_response),
   771  	}
   772  
   773  	if len(invalidServerEntryTags) > 0 {
   774  		statusResponse.InvalidServerEntryTags = make([]string, len(invalidServerEntryTags))
   775  		i := 0
   776  		for tag := range invalidServerEntryTags {
   777  			statusResponse.InvalidServerEntryTags[i] = tag
   778  			i++
   779  		}
   780  	}
   781  
   782  	responsePayload, err := json.Marshal(statusResponse)
   783  	if err != nil {
   784  		return nil, errors.Trace(err)
   785  	}
   786  
   787  	return responsePayload, nil
   788  }
   789  
   790  // clientVerificationAPIRequestHandler is just a compliance stub
   791  // for older Android clients that still send verification requests
   792  func clientVerificationAPIRequestHandler(
   793  	support *SupportServices,
   794  	clientAddr string,
   795  	geoIPData GeoIPData,
   796  	authorizedAccessTypes []string,
   797  	params common.APIParameters) ([]byte, error) {
   798  	return make([]byte, 0), nil
   799  }
   800  
   801  var tacticsParams = []requestParamSpec{
   802  	{tactics.STORED_TACTICS_TAG_PARAMETER_NAME, isAnyString, requestParamOptional},
   803  	{tactics.SPEED_TEST_SAMPLES_PARAMETER_NAME, nil, requestParamOptional | requestParamJSON},
   804  }
   805  
   806  var tacticsRequestParams = append(
   807  	append([]requestParamSpec(nil), tacticsParams...),
   808  	baseSessionAndDialParams...)
   809  
   810  func getTacticsAPIParameterValidator(config *Config) common.APIParameterValidator {
   811  	return func(params common.APIParameters) error {
   812  		return validateRequestParams(config, params, tacticsRequestParams)
   813  	}
   814  }
   815  
   816  func getTacticsAPIParameterLogFieldFormatter() common.APIParameterLogFieldFormatter {
   817  
   818  	return func(geoIPData common.GeoIPData, params common.APIParameters) common.LogFields {
   819  
   820  		logFields := getRequestLogFields(
   821  			tactics.TACTICS_METRIC_EVENT_NAME,
   822  			GeoIPData(geoIPData),
   823  			nil, // authorizedAccessTypes are not known yet
   824  			params,
   825  			tacticsRequestParams)
   826  
   827  		return common.LogFields(logFields)
   828  	}
   829  }
   830  
   831  // requestParamSpec defines a request parameter. Each param is expected to be
   832  // a string, unless requestParamArray is specified, in which case an array of
   833  // strings is expected.
   834  type requestParamSpec struct {
   835  	name      string
   836  	validator func(*Config, string) bool
   837  	flags     uint32
   838  }
   839  
   840  const (
   841  	requestParamOptional                                      = 1
   842  	requestParamNotLogged                                     = 1 << 1
   843  	requestParamArray                                         = 1 << 2
   844  	requestParamJSON                                          = 1 << 3
   845  	requestParamLogStringAsInt                                = 1 << 4
   846  	requestParamLogStringAsFloat                              = 1 << 5
   847  	requestParamLogStringLengthAsInt                          = 1 << 6
   848  	requestParamLogFlagAsBool                                 = 1 << 7
   849  	requestParamLogOnlyForFrontedMeekOrConjure                = 1 << 8
   850  	requestParamNotLoggedForUnfrontedMeekNonTransformedHeader = 1 << 9
   851  )
   852  
   853  // baseParams are the basic request parameters that are expected for all API
   854  // requests and log events.
   855  var baseParams = []requestParamSpec{
   856  	{"server_secret", isServerSecret, requestParamNotLogged},
   857  	{"client_session_id", isHexDigits, requestParamNotLogged},
   858  	{"propagation_channel_id", isHexDigits, 0},
   859  	{"sponsor_id", isHexDigits, 0},
   860  	{"client_version", isIntString, requestParamLogStringAsInt},
   861  	{"client_platform", isClientPlatform, 0},
   862  	{"client_features", isAnyString, requestParamOptional | requestParamArray},
   863  	{"client_build_rev", isHexDigits, requestParamOptional},
   864  	{"device_region", isAnyString, requestParamOptional},
   865  }
   866  
   867  // baseSessionParams adds to baseParams the required session_id parameter. For
   868  // all requests except handshake, all existing clients are expected to send
   869  // session_id. Legacy clients may not send "session_id" in handshake.
   870  var baseSessionParams = append(
   871  	[]requestParamSpec{
   872  		{"session_id", isHexDigits, 0}},
   873  	baseParams...)
   874  
   875  // baseDialParams are the dial parameters, per-tunnel network protocol and
   876  // obfuscation metrics which are logged with server_tunnel, failed_tunnel, and
   877  // tactics.
   878  var baseDialParams = []requestParamSpec{
   879  	{"relay_protocol", isRelayProtocol, 0},
   880  	{"ssh_client_version", isAnyString, requestParamOptional},
   881  	{"upstream_proxy_type", isUpstreamProxyType, requestParamOptional},
   882  	{"upstream_proxy_custom_header_names", isAnyString, requestParamOptional | requestParamArray},
   883  	{"fronting_provider_id", isAnyString, requestParamOptional},
   884  	{"meek_dial_address", isDialAddress, requestParamOptional | requestParamLogOnlyForFrontedMeekOrConjure},
   885  	{"meek_resolved_ip_address", isIPAddress, requestParamOptional | requestParamLogOnlyForFrontedMeekOrConjure},
   886  	{"meek_sni_server_name", isDomain, requestParamOptional},
   887  	{"meek_host_header", isHostHeader, requestParamOptional | requestParamNotLoggedForUnfrontedMeekNonTransformedHeader},
   888  	{"meek_transformed_host_name", isBooleanFlag, requestParamOptional | requestParamLogFlagAsBool},
   889  	{"user_agent", isAnyString, requestParamOptional},
   890  	{"tls_profile", isAnyString, requestParamOptional},
   891  	{"tls_version", isAnyString, requestParamOptional},
   892  	{"server_entry_region", isRegionCode, requestParamOptional},
   893  	{"server_entry_source", isServerEntrySource, requestParamOptional},
   894  	{"server_entry_timestamp", isISO8601Date, requestParamOptional},
   895  	{tactics.APPLIED_TACTICS_TAG_PARAMETER_NAME, isAnyString, requestParamOptional},
   896  	{"dial_port_number", isIntString, requestParamOptional | requestParamLogStringAsInt},
   897  	{"quic_version", isAnyString, requestParamOptional},
   898  	{"quic_dial_sni_address", isAnyString, requestParamOptional},
   899  	{"quic_disable_client_path_mtu_discovery", isBooleanFlag, requestParamOptional | requestParamLogFlagAsBool},
   900  	{"upstream_bytes_fragmented", isIntString, requestParamOptional | requestParamLogStringAsInt},
   901  	{"upstream_min_bytes_written", isIntString, requestParamOptional | requestParamLogStringAsInt},
   902  	{"upstream_max_bytes_written", isIntString, requestParamOptional | requestParamLogStringAsInt},
   903  	{"upstream_min_delayed", isIntString, requestParamOptional | requestParamLogStringAsInt},
   904  	{"upstream_max_delayed", isIntString, requestParamOptional | requestParamLogStringAsInt},
   905  	{"padding", isAnyString, requestParamOptional | requestParamLogStringLengthAsInt},
   906  	{"pad_response", isIntString, requestParamOptional | requestParamLogStringAsInt},
   907  	{"is_replay", isBooleanFlag, requestParamOptional | requestParamLogFlagAsBool},
   908  	{"egress_region", isRegionCode, requestParamOptional},
   909  	{"dial_duration", isIntString, requestParamOptional | requestParamLogStringAsInt},
   910  	{"candidate_number", isIntString, requestParamOptional | requestParamLogStringAsInt},
   911  	{"established_tunnels_count", isIntString, requestParamOptional | requestParamLogStringAsInt},
   912  	{"upstream_ossh_padding", isIntString, requestParamOptional | requestParamLogStringAsInt},
   913  	{"meek_cookie_size", isIntString, requestParamOptional | requestParamLogStringAsInt},
   914  	{"meek_limit_request", isIntString, requestParamOptional | requestParamLogStringAsInt},
   915  	{"meek_tls_padding", isIntString, requestParamOptional | requestParamLogStringAsInt},
   916  	{"network_latency_multiplier", isFloatString, requestParamOptional | requestParamLogStringAsFloat},
   917  	{"client_bpf", isAnyString, requestParamOptional},
   918  	{"network_type", isAnyString, requestParamOptional},
   919  	{"conjure_cached", isBooleanFlag, requestParamOptional | requestParamLogFlagAsBool},
   920  	{"conjure_delay", isIntString, requestParamOptional | requestParamLogStringAsInt},
   921  	{"conjure_transport", isAnyString, requestParamOptional},
   922  	{"split_tunnel", isBooleanFlag, requestParamOptional | requestParamLogFlagAsBool},
   923  	{"split_tunnel_regions", isRegionCode, requestParamOptional | requestParamArray},
   924  	{"dns_preresolved", isAnyString, requestParamOptional},
   925  	{"dns_preferred", isAnyString, requestParamOptional},
   926  	{"dns_transform", isAnyString, requestParamOptional},
   927  	{"dns_attempt", isIntString, requestParamOptional | requestParamLogStringAsInt},
   928  }
   929  
   930  // baseSessionAndDialParams adds baseDialParams to baseSessionParams.
   931  var baseSessionAndDialParams = append(
   932  	append(
   933  		[]requestParamSpec{},
   934  		baseSessionParams...),
   935  	baseDialParams...)
   936  
   937  func validateRequestParams(
   938  	config *Config,
   939  	params common.APIParameters,
   940  	expectedParams []requestParamSpec) error {
   941  
   942  	for _, expectedParam := range expectedParams {
   943  		value := params[expectedParam.name]
   944  		if value == nil {
   945  			if expectedParam.flags&requestParamOptional != 0 {
   946  				continue
   947  			}
   948  			return errors.Tracef("missing param: %s", expectedParam.name)
   949  		}
   950  		var err error
   951  		switch {
   952  		case expectedParam.flags&requestParamArray != 0:
   953  			err = validateStringArrayRequestParam(config, expectedParam, value)
   954  		case expectedParam.flags&requestParamJSON != 0:
   955  			// No validation: the JSON already unmarshalled; the parameter
   956  			// user will validate that the JSON contains the expected
   957  			// objects/data.
   958  
   959  			// TODO: without validation, any valid JSON will be logged
   960  			// by getRequestLogFields, even if the parameter user validates
   961  			// and rejects the parameter.
   962  
   963  		default:
   964  			err = validateStringRequestParam(config, expectedParam, value)
   965  		}
   966  		if err != nil {
   967  			return errors.Trace(err)
   968  		}
   969  	}
   970  
   971  	return nil
   972  }
   973  
   974  // copyBaseSessionAndDialParams makes a copy of the params which includes only
   975  // the baseSessionAndDialParams.
   976  func copyBaseSessionAndDialParams(params common.APIParameters) common.APIParameters {
   977  
   978  	// Note: not a deep copy; assumes baseSessionAndDialParams values are all
   979  	// scalar types (int, string, etc.)
   980  	paramsCopy := make(common.APIParameters)
   981  	for _, baseParam := range baseSessionAndDialParams {
   982  		value := params[baseParam.name]
   983  		if value == nil {
   984  			continue
   985  		}
   986  		paramsCopy[baseParam.name] = value
   987  	}
   988  	return paramsCopy
   989  }
   990  
   991  func copyUpdateOnConnectedParams(params common.APIParameters) common.APIParameters {
   992  
   993  	// Note: not a deep copy
   994  	paramsCopy := make(common.APIParameters)
   995  	for _, name := range updateOnConnectedParamNames {
   996  		value := params[name]
   997  		if value == nil {
   998  			continue
   999  		}
  1000  		paramsCopy[name] = value
  1001  	}
  1002  	return paramsCopy
  1003  }
  1004  
  1005  func validateStringRequestParam(
  1006  	config *Config,
  1007  	expectedParam requestParamSpec,
  1008  	value interface{}) error {
  1009  
  1010  	strValue, ok := value.(string)
  1011  	if !ok {
  1012  		return errors.Tracef("unexpected string param type: %s", expectedParam.name)
  1013  	}
  1014  	if !expectedParam.validator(config, strValue) {
  1015  		return errors.Tracef("invalid param: %s: %s", expectedParam.name, strValue)
  1016  	}
  1017  	return nil
  1018  }
  1019  
  1020  func validateStringArrayRequestParam(
  1021  	config *Config,
  1022  	expectedParam requestParamSpec,
  1023  	value interface{}) error {
  1024  
  1025  	arrayValue, ok := value.([]interface{})
  1026  	if !ok {
  1027  		return errors.Tracef("unexpected array param type: %s", expectedParam.name)
  1028  	}
  1029  	for _, value := range arrayValue {
  1030  		err := validateStringRequestParam(config, expectedParam, value)
  1031  		if err != nil {
  1032  			return errors.Trace(err)
  1033  		}
  1034  	}
  1035  	return nil
  1036  }
  1037  
  1038  // getRequestLogFields makes LogFields to log the API event following
  1039  // the legacy psi_web and current ELK naming conventions.
  1040  func getRequestLogFields(
  1041  	eventName string,
  1042  	geoIPData GeoIPData,
  1043  	authorizedAccessTypes []string,
  1044  	params common.APIParameters,
  1045  	expectedParams []requestParamSpec) LogFields {
  1046  
  1047  	logFields := make(LogFields)
  1048  
  1049  	if eventName != "" {
  1050  		logFields["event_name"] = eventName
  1051  	}
  1052  
  1053  	geoIPData.SetLogFields(logFields)
  1054  
  1055  	if len(authorizedAccessTypes) > 0 {
  1056  		logFields["authorized_access_types"] = authorizedAccessTypes
  1057  	}
  1058  
  1059  	if params == nil {
  1060  		return logFields
  1061  	}
  1062  
  1063  	for _, expectedParam := range expectedParams {
  1064  
  1065  		if expectedParam.flags&requestParamNotLogged != 0 {
  1066  			continue
  1067  		}
  1068  
  1069  		var tunnelProtocol string
  1070  		if value, ok := params["relay_protocol"]; ok {
  1071  			tunnelProtocol, _ = value.(string)
  1072  		}
  1073  
  1074  		if expectedParam.flags&requestParamLogOnlyForFrontedMeekOrConjure != 0 &&
  1075  			!protocol.TunnelProtocolUsesFrontedMeek(tunnelProtocol) &&
  1076  			!protocol.TunnelProtocolUsesConjure(tunnelProtocol) {
  1077  			continue
  1078  		}
  1079  
  1080  		if expectedParam.flags&requestParamNotLoggedForUnfrontedMeekNonTransformedHeader != 0 &&
  1081  			protocol.TunnelProtocolUsesMeek(tunnelProtocol) &&
  1082  			!protocol.TunnelProtocolUsesFrontedMeek(tunnelProtocol) {
  1083  
  1084  			// Non-HTTP unfronted meek protocols never tranform the host header.
  1085  			if protocol.TunnelProtocolUsesMeekHTTPS(tunnelProtocol) {
  1086  				continue
  1087  			}
  1088  
  1089  			var transformedHostName string
  1090  			if value, ok := params["meek_transformed_host_name"]; ok {
  1091  				transformedHostName, _ = value.(string)
  1092  			}
  1093  			if transformedHostName != "1" {
  1094  				continue
  1095  			}
  1096  		}
  1097  
  1098  		value := params[expectedParam.name]
  1099  		if value == nil {
  1100  
  1101  			// Special case: older clients don't send this value,
  1102  			// so log a default.
  1103  			if expectedParam.name == "tunnel_whole_device" {
  1104  				value = "0"
  1105  			} else {
  1106  				// Skip omitted, optional params
  1107  				continue
  1108  			}
  1109  		}
  1110  
  1111  		switch v := value.(type) {
  1112  		case string:
  1113  			strValue := v
  1114  
  1115  			// Special cases:
  1116  			// - Number fields are encoded as integer types.
  1117  			// - For ELK performance we record certain domain-or-IP
  1118  			//   fields as one of two different values based on type;
  1119  			//   we also omit port from these host:port fields for now.
  1120  			// - Boolean fields that come into the api as "1"/"0"
  1121  			//   must be logged as actual boolean values
  1122  			switch expectedParam.name {
  1123  
  1124  			case "meek_dial_address":
  1125  				host, _, _ := net.SplitHostPort(strValue)
  1126  				if isIPAddress(nil, host) {
  1127  					logFields["meek_dial_ip_address"] = host
  1128  				} else {
  1129  					logFields["meek_dial_domain"] = host
  1130  				}
  1131  
  1132  			case "upstream_proxy_type":
  1133  				// Submitted value could be e.g., "SOCKS5" or "socks5"; log lowercase
  1134  				logFields[expectedParam.name] = strings.ToLower(strValue)
  1135  
  1136  			case tactics.SPEED_TEST_SAMPLES_PARAMETER_NAME:
  1137  				// Due to a client bug, clients may deliever an incorrect ""
  1138  				// value for speed_test_samples via the web API protocol. Omit
  1139  				// the field in this case.
  1140  
  1141  			case "tunnel_error":
  1142  				// net/url.Error, returned from net/url.Parse, contains the original input
  1143  				// URL, which may contain PII. New clients strip this out by using
  1144  				// common.SafeParseURL. Legacy clients will still send the full error
  1145  				// message, so strip it out here. The target substring should be unique to
  1146  				// legacy clients.
  1147  				target := "upstreamproxy error: proxyURI url.Parse: parse "
  1148  				index := strings.Index(strValue, target)
  1149  				if index != -1 {
  1150  					strValue = strValue[:index+len(target)] + "<redacted>"
  1151  				}
  1152  				logFields[expectedParam.name] = strValue
  1153  
  1154  			default:
  1155  				if expectedParam.flags&requestParamLogStringAsInt != 0 {
  1156  					intValue, _ := strconv.Atoi(strValue)
  1157  					logFields[expectedParam.name] = intValue
  1158  
  1159  				} else if expectedParam.flags&requestParamLogStringAsFloat != 0 {
  1160  					floatValue, _ := strconv.ParseFloat(strValue, 64)
  1161  					logFields[expectedParam.name] = floatValue
  1162  
  1163  				} else if expectedParam.flags&requestParamLogStringLengthAsInt != 0 {
  1164  					logFields[expectedParam.name] = len(strValue)
  1165  
  1166  				} else if expectedParam.flags&requestParamLogFlagAsBool != 0 {
  1167  					// Submitted value could be "0" or "1"
  1168  					// "0" and non "0"/"1" values should be transformed to false
  1169  					// "1" should be transformed to true
  1170  					if strValue == "1" {
  1171  						logFields[expectedParam.name] = true
  1172  					} else {
  1173  						logFields[expectedParam.name] = false
  1174  					}
  1175  
  1176  				} else {
  1177  					logFields[expectedParam.name] = strValue
  1178  				}
  1179  			}
  1180  
  1181  		case []interface{}:
  1182  			if expectedParam.name == tactics.SPEED_TEST_SAMPLES_PARAMETER_NAME {
  1183  				logFields[expectedParam.name] = makeSpeedTestSamplesLogField(v)
  1184  			} else {
  1185  				logFields[expectedParam.name] = v
  1186  			}
  1187  
  1188  		default:
  1189  			logFields[expectedParam.name] = v
  1190  		}
  1191  	}
  1192  
  1193  	return logFields
  1194  }
  1195  
  1196  // makeSpeedTestSamplesLogField renames the tactics.SpeedTestSample json tag
  1197  // fields to more verbose names for metrics.
  1198  func makeSpeedTestSamplesLogField(samples []interface{}) []interface{} {
  1199  	// TODO: use reflection and add additional tags, e.g.,
  1200  	// `json:"s" log:"timestamp"` to remove hard-coded
  1201  	// tag value dependency?
  1202  	logSamples := make([]interface{}, len(samples))
  1203  	for i, sample := range samples {
  1204  		logSample := make(map[string]interface{})
  1205  		if m, ok := sample.(map[string]interface{}); ok {
  1206  			for k, v := range m {
  1207  				logK := k
  1208  				switch k {
  1209  				case "s":
  1210  					logK = "timestamp"
  1211  				case "r":
  1212  					logK = "server_region"
  1213  				case "p":
  1214  					logK = "relay_protocol"
  1215  				case "t":
  1216  					logK = "round_trip_time_ms"
  1217  				case "u":
  1218  					logK = "bytes_up"
  1219  				case "d":
  1220  					logK = "bytes_down"
  1221  				}
  1222  				logSample[logK] = v
  1223  			}
  1224  		}
  1225  		logSamples[i] = logSample
  1226  	}
  1227  	return logSamples
  1228  }
  1229  
  1230  func getOptionalStringRequestParam(params common.APIParameters, name string) (string, bool) {
  1231  	if params[name] == nil {
  1232  		return "", false
  1233  	}
  1234  	value, ok := params[name].(string)
  1235  	if !ok {
  1236  		return "", false
  1237  	}
  1238  	return value, true
  1239  }
  1240  
  1241  func getStringRequestParam(params common.APIParameters, name string) (string, error) {
  1242  	if params[name] == nil {
  1243  		return "", errors.Tracef("missing param: %s", name)
  1244  	}
  1245  	value, ok := params[name].(string)
  1246  	if !ok {
  1247  		return "", errors.Tracef("invalid param: %s", name)
  1248  	}
  1249  	return value, nil
  1250  }
  1251  
  1252  func getIntStringRequestParam(params common.APIParameters, name string) (int, error) {
  1253  	if params[name] == nil {
  1254  		return 0, errors.Tracef("missing param: %s", name)
  1255  	}
  1256  	valueStr, ok := params[name].(string)
  1257  	if !ok {
  1258  		return 0, errors.Tracef("invalid param: %s", name)
  1259  	}
  1260  	value, err := strconv.Atoi(valueStr)
  1261  	if !ok {
  1262  		return 0, errors.Trace(err)
  1263  	}
  1264  	return value, nil
  1265  }
  1266  
  1267  func getBoolStringRequestParam(params common.APIParameters, name string) (bool, error) {
  1268  	if params[name] == nil {
  1269  		return false, errors.Tracef("missing param: %s", name)
  1270  	}
  1271  	valueStr, ok := params[name].(string)
  1272  	if !ok {
  1273  		return false, errors.Tracef("invalid param: %s", name)
  1274  	}
  1275  	if valueStr == "1" {
  1276  		return true, nil
  1277  	}
  1278  	return false, nil
  1279  }
  1280  
  1281  func getPaddingSizeRequestParam(params common.APIParameters, name string) (int, error) {
  1282  	value, err := getIntStringRequestParam(params, name)
  1283  	if err != nil {
  1284  		return 0, errors.Trace(err)
  1285  	}
  1286  	if value < 0 {
  1287  		value = 0
  1288  	}
  1289  	if value > PADDING_MAX_BYTES {
  1290  		value = PADDING_MAX_BYTES
  1291  	}
  1292  	return int(value), nil
  1293  }
  1294  
  1295  func getJSONObjectRequestParam(params common.APIParameters, name string) (common.APIParameters, error) {
  1296  	if params[name] == nil {
  1297  		return nil, errors.Tracef("missing param: %s", name)
  1298  	}
  1299  	// Note: generic unmarshal of JSON produces map[string]interface{}, not common.APIParameters
  1300  	value, ok := params[name].(map[string]interface{})
  1301  	if !ok {
  1302  		return nil, errors.Tracef("invalid param: %s", name)
  1303  	}
  1304  	return common.APIParameters(value), nil
  1305  }
  1306  
  1307  func getJSONObjectArrayRequestParam(params common.APIParameters, name string) ([]common.APIParameters, error) {
  1308  	if params[name] == nil {
  1309  		return nil, errors.Tracef("missing param: %s", name)
  1310  	}
  1311  	value, ok := params[name].([]interface{})
  1312  	if !ok {
  1313  		return nil, errors.Tracef("invalid param: %s", name)
  1314  	}
  1315  
  1316  	result := make([]common.APIParameters, len(value))
  1317  	for i, item := range value {
  1318  		// Note: generic unmarshal of JSON produces map[string]interface{}, not common.APIParameters
  1319  		resultItem, ok := item.(map[string]interface{})
  1320  		if !ok {
  1321  			return nil, errors.Tracef("invalid param: %s", name)
  1322  		}
  1323  		result[i] = common.APIParameters(resultItem)
  1324  	}
  1325  
  1326  	return result, nil
  1327  }
  1328  
  1329  func getMapStringInt64RequestParam(params common.APIParameters, name string) (map[string]int64, error) {
  1330  	if params[name] == nil {
  1331  		return nil, errors.Tracef("missing param: %s", name)
  1332  	}
  1333  	// TODO: can't use common.APIParameters type?
  1334  	value, ok := params[name].(map[string]interface{})
  1335  	if !ok {
  1336  		return nil, errors.Tracef("invalid param: %s", name)
  1337  	}
  1338  
  1339  	result := make(map[string]int64)
  1340  	for k, v := range value {
  1341  		numValue, ok := v.(float64)
  1342  		if !ok {
  1343  			return nil, errors.Tracef("invalid param: %s", name)
  1344  		}
  1345  		result[k] = int64(numValue)
  1346  	}
  1347  
  1348  	return result, nil
  1349  }
  1350  
  1351  func getStringArrayRequestParam(params common.APIParameters, name string) ([]string, error) {
  1352  	if params[name] == nil {
  1353  		return nil, errors.Tracef("missing param: %s", name)
  1354  	}
  1355  	value, ok := params[name].([]interface{})
  1356  	if !ok {
  1357  		return nil, errors.Tracef("invalid param: %s", name)
  1358  	}
  1359  
  1360  	result := make([]string, len(value))
  1361  	for i, v := range value {
  1362  		strValue, ok := v.(string)
  1363  		if !ok {
  1364  			return nil, errors.Tracef("invalid param: %s", name)
  1365  		}
  1366  		result[i] = strValue
  1367  	}
  1368  
  1369  	return result, nil
  1370  }
  1371  
  1372  // Normalize reported client platform. Android clients, for example, report
  1373  // OS version, rooted status, and Google Play build status in the clientPlatform
  1374  // string along with "Android".
  1375  func normalizeClientPlatform(clientPlatform string) string {
  1376  
  1377  	if strings.Contains(strings.ToLower(clientPlatform), strings.ToLower(CLIENT_PLATFORM_ANDROID)) {
  1378  		return CLIENT_PLATFORM_ANDROID
  1379  	} else if strings.HasPrefix(clientPlatform, CLIENT_PLATFORM_IOS) {
  1380  		return CLIENT_PLATFORM_IOS
  1381  	}
  1382  
  1383  	return CLIENT_PLATFORM_WINDOWS
  1384  }
  1385  
  1386  func isAnyString(config *Config, value string) bool {
  1387  	return true
  1388  }
  1389  
  1390  func isMobileClientPlatform(clientPlatform string) bool {
  1391  	normalizedClientPlatform := normalizeClientPlatform(clientPlatform)
  1392  	return normalizedClientPlatform == CLIENT_PLATFORM_ANDROID ||
  1393  		normalizedClientPlatform == CLIENT_PLATFORM_IOS
  1394  }
  1395  
  1396  // Input validators follow the legacy validations rules in psi_web.
  1397  
  1398  func isServerSecret(config *Config, value string) bool {
  1399  	return subtle.ConstantTimeCompare(
  1400  		[]byte(value),
  1401  		[]byte(config.WebServerSecret)) == 1
  1402  }
  1403  
  1404  func isHexDigits(_ *Config, value string) bool {
  1405  	// Allows both uppercase in addition to lowercase, for legacy support.
  1406  	return -1 == strings.IndexFunc(value, func(c rune) bool {
  1407  		return !unicode.Is(unicode.ASCII_Hex_Digit, c)
  1408  	})
  1409  }
  1410  
  1411  func isBase64String(_ *Config, value string) bool {
  1412  	_, err := base64.StdEncoding.DecodeString(value)
  1413  	return err == nil
  1414  }
  1415  
  1416  func isDigits(_ *Config, value string) bool {
  1417  	return -1 == strings.IndexFunc(value, func(c rune) bool {
  1418  		return c < '0' || c > '9'
  1419  	})
  1420  }
  1421  
  1422  func isIntString(_ *Config, value string) bool {
  1423  	_, err := strconv.Atoi(value)
  1424  	return err == nil
  1425  }
  1426  
  1427  func isFloatString(_ *Config, value string) bool {
  1428  	_, err := strconv.ParseFloat(value, 64)
  1429  	return err == nil
  1430  }
  1431  
  1432  func isClientPlatform(_ *Config, value string) bool {
  1433  	return -1 == strings.IndexFunc(value, func(c rune) bool {
  1434  		// Note: stricter than psi_web's Python string.whitespace
  1435  		return unicode.Is(unicode.White_Space, c)
  1436  	})
  1437  }
  1438  
  1439  func isRelayProtocol(_ *Config, value string) bool {
  1440  	return common.Contains(protocol.SupportedTunnelProtocols, value)
  1441  }
  1442  
  1443  func isBooleanFlag(_ *Config, value string) bool {
  1444  	return value == "0" || value == "1"
  1445  }
  1446  
  1447  func isUpstreamProxyType(_ *Config, value string) bool {
  1448  	value = strings.ToLower(value)
  1449  	return value == "http" || value == "socks5" || value == "socks4a"
  1450  }
  1451  
  1452  func isRegionCode(_ *Config, value string) bool {
  1453  	if len(value) != 2 {
  1454  		return false
  1455  	}
  1456  	return -1 == strings.IndexFunc(value, func(c rune) bool {
  1457  		return c < 'A' || c > 'Z'
  1458  	})
  1459  }
  1460  
  1461  func isDialAddress(_ *Config, value string) bool {
  1462  	// "<host>:<port>", where <host> is a domain or IP address
  1463  	parts := strings.Split(value, ":")
  1464  	if len(parts) != 2 {
  1465  		return false
  1466  	}
  1467  	if !isIPAddress(nil, parts[0]) && !isDomain(nil, parts[0]) {
  1468  		return false
  1469  	}
  1470  	if !isDigits(nil, parts[1]) {
  1471  		return false
  1472  	}
  1473  	_, err := strconv.Atoi(parts[1])
  1474  	if err != nil {
  1475  		return false
  1476  	}
  1477  	// Allow port numbers outside [0,65535] to accommodate failed_tunnel cases.
  1478  	return true
  1479  }
  1480  
  1481  func isIPAddress(_ *Config, value string) bool {
  1482  	return net.ParseIP(value) != nil
  1483  }
  1484  
  1485  var isDomainRegex = regexp.MustCompile(`[a-zA-Z\d-]{1,63}$`)
  1486  
  1487  func isDomain(_ *Config, value string) bool {
  1488  
  1489  	// From: http://stackoverflow.com/questions/2532053/validate-a-hostname-string
  1490  	//
  1491  	// "ensures that each segment
  1492  	//    * contains at least one character and a maximum of 63 characters
  1493  	//    * consists only of allowed characters
  1494  	//    * doesn't begin or end with a hyphen"
  1495  	//
  1496  
  1497  	if len(value) > 255 {
  1498  		return false
  1499  	}
  1500  	value = strings.TrimSuffix(value, ".")
  1501  	for _, part := range strings.Split(value, ".") {
  1502  		// Note: regexp doesn't support the following Perl expression which
  1503  		// would check for '-' prefix/suffix: "(?!-)[a-zA-Z\\d-]{1,63}(?<!-)$"
  1504  		if strings.HasPrefix(part, "-") || strings.HasSuffix(part, "-") {
  1505  			return false
  1506  		}
  1507  		if !isDomainRegex.Match([]byte(part)) {
  1508  			return false
  1509  		}
  1510  	}
  1511  	return true
  1512  }
  1513  
  1514  func isHostHeader(_ *Config, value string) bool {
  1515  	// "<host>:<port>", where <host> is a domain or IP address and ":<port>" is optional
  1516  	if strings.Contains(value, ":") {
  1517  		return isDialAddress(nil, value)
  1518  	}
  1519  	return isIPAddress(nil, value) || isDomain(nil, value)
  1520  }
  1521  
  1522  func isServerEntrySource(_ *Config, value string) bool {
  1523  	return common.Contains(protocol.SupportedServerEntrySources, value)
  1524  }
  1525  
  1526  var isISO8601DateRegex = regexp.MustCompile(
  1527  	`(?P<year>[0-9]{4})-(?P<month>[0-9]{1,2})-(?P<day>[0-9]{1,2})T(?P<hour>[0-9]{2}):(?P<minute>[0-9]{2}):(?P<second>[0-9]{2})(\.(?P<fraction>[0-9]+))?(?P<timezone>Z|(([-+])([0-9]{2}):([0-9]{2})))`)
  1528  
  1529  func isISO8601Date(_ *Config, value string) bool {
  1530  	return isISO8601DateRegex.Match([]byte(value))
  1531  }
  1532  
  1533  func isLastConnected(_ *Config, value string) bool {
  1534  	return value == "None" || isISO8601Date(nil, value)
  1535  }