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, ¶ms) 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 }