serverApi.go 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732
  1. /*
  2. * Copyright (c) 2015, Psiphon Inc.
  3. * All rights reserved.
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation, either version 3 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License
  16. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  17. *
  18. */
  19. package psiphon
  20. import (
  21. "bytes"
  22. "encoding/base64"
  23. "encoding/hex"
  24. "encoding/json"
  25. "errors"
  26. "fmt"
  27. "io"
  28. "io/ioutil"
  29. "net"
  30. "net/http"
  31. "strconv"
  32. "sync/atomic"
  33. "time"
  34. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/transferstats"
  35. )
  36. // ServerContext is a utility struct which holds all of the data associated
  37. // with a Psiphon server connection. In addition to the established tunnel, this
  38. // includes data associated with Psiphon API requests and a persistent http
  39. // client configured to make tunneled Psiphon API requests.
  40. type ServerContext struct {
  41. sessionId string
  42. tunnelNumber int64
  43. baseRequestUrl string
  44. psiphonHttpsClient *http.Client
  45. statsRegexps *transferstats.Regexps
  46. clientRegion string
  47. clientUpgradeVersion string
  48. serverHandshakeTimestamp string
  49. }
  50. // MeekStats holds extra stats that are only gathered for meek tunnels.
  51. type MeekStats struct {
  52. DialAddress string
  53. ResolvedIPAddress string
  54. SNIServerName string
  55. HostHeader string
  56. TransformedHostName bool
  57. }
  58. // nextTunnelNumber is a monotonically increasing number assigned to each
  59. // successive tunnel connection. The sessionId and tunnelNumber together
  60. // form a globally unique identifier for tunnels, which is used for
  61. // stats. Note that the number is increasing but not necessarily
  62. // consecutive for each active tunnel in session.
  63. var nextTunnelNumber int64
  64. // MakeSessionId creates a new session ID. The same session ID is used across
  65. // multi-tunnel controller runs, where each tunnel has its own ServerContext
  66. // instance.
  67. // In server-side stats, we now consider a "session" to be the lifetime of the
  68. // Controller (e.g., the user's commanded start and stop) and we measure this
  69. // duration as well as the duration of each tunnel within the session.
  70. func MakeSessionId() (sessionId string, err error) {
  71. randomId, err := MakeSecureRandomBytes(PSIPHON_API_CLIENT_SESSION_ID_LENGTH)
  72. if err != nil {
  73. return "", ContextError(err)
  74. }
  75. return hex.EncodeToString(randomId), nil
  76. }
  77. // NewServerContext makes the tunnelled handshake request to the Psiphon server
  78. // and returns a ServerContext struct for use with subsequent Psiphon server API
  79. // requests (e.g., periodic connected and status requests).
  80. func NewServerContext(tunnel *Tunnel, sessionId string) (*ServerContext, error) {
  81. psiphonHttpsClient, err := makePsiphonHttpsClient(tunnel)
  82. if err != nil {
  83. return nil, ContextError(err)
  84. }
  85. serverContext := &ServerContext{
  86. sessionId: sessionId,
  87. tunnelNumber: atomic.AddInt64(&nextTunnelNumber, 1),
  88. baseRequestUrl: makeBaseRequestUrl(tunnel, "", sessionId),
  89. psiphonHttpsClient: psiphonHttpsClient,
  90. }
  91. err = serverContext.doHandshakeRequest()
  92. if err != nil {
  93. return nil, ContextError(err)
  94. }
  95. return serverContext, nil
  96. }
  97. // doHandshakeRequest performs the handshake API request. The handshake
  98. // returns upgrade info, newly discovered server entries -- which are
  99. // stored -- and sponsor info (home pages, stat regexes).
  100. func (serverContext *ServerContext) doHandshakeRequest() error {
  101. extraParams := make([]*ExtraParam, 0)
  102. serverEntryIpAddresses, err := GetServerEntryIpAddresses()
  103. if err != nil {
  104. return ContextError(err)
  105. }
  106. // Submit a list of known servers -- this will be used for
  107. // discovery statistics.
  108. for _, ipAddress := range serverEntryIpAddresses {
  109. extraParams = append(extraParams, &ExtraParam{"known_server", ipAddress})
  110. }
  111. url := buildRequestUrl(serverContext.baseRequestUrl, "handshake", extraParams...)
  112. responseBody, err := serverContext.doGetRequest(url)
  113. if err != nil {
  114. return ContextError(err)
  115. }
  116. // Skip legacy format lines and just parse the JSON config line
  117. configLinePrefix := []byte("Config: ")
  118. var configLine []byte
  119. for _, line := range bytes.Split(responseBody, []byte("\n")) {
  120. if bytes.HasPrefix(line, configLinePrefix) {
  121. configLine = line[len(configLinePrefix):]
  122. break
  123. }
  124. }
  125. if len(configLine) == 0 {
  126. return ContextError(errors.New("no config line found"))
  127. }
  128. // Note:
  129. // - 'preemptive_reconnect_lifetime_milliseconds' is currently unused
  130. // - 'ssh_session_id' is ignored; client session ID is used instead
  131. var handshakeConfig struct {
  132. Homepages []string `json:"homepages"`
  133. UpgradeClientVersion string `json:"upgrade_client_version"`
  134. PageViewRegexes []map[string]string `json:"page_view_regexes"`
  135. HttpsRequestRegexes []map[string]string `json:"https_request_regexes"`
  136. EncodedServerList []string `json:"encoded_server_list"`
  137. ClientRegion string `json:"client_region"`
  138. ServerTimestamp string `json:"server_timestamp"`
  139. }
  140. err = json.Unmarshal(configLine, &handshakeConfig)
  141. if err != nil {
  142. return ContextError(err)
  143. }
  144. serverContext.clientRegion = handshakeConfig.ClientRegion
  145. NoticeClientRegion(serverContext.clientRegion)
  146. var decodedServerEntries []*ServerEntry
  147. // Store discovered server entries
  148. // We use the server's time, as it's available here, for the server entry
  149. // timestamp since this is more reliable than the client time.
  150. for _, encodedServerEntry := range handshakeConfig.EncodedServerList {
  151. serverEntry, err := DecodeServerEntry(
  152. encodedServerEntry,
  153. TruncateTimestampToHour(handshakeConfig.ServerTimestamp),
  154. SERVER_ENTRY_SOURCE_DISCOVERY)
  155. if err != nil {
  156. return ContextError(err)
  157. }
  158. err = ValidateServerEntry(serverEntry)
  159. if err != nil {
  160. // Skip this entry and continue with the next one
  161. continue
  162. }
  163. decodedServerEntries = append(decodedServerEntries, serverEntry)
  164. }
  165. // The reason we are storing the entire array of server entries at once rather
  166. // than one at a time is that some desirable side-effects get triggered by
  167. // StoreServerEntries that don't get triggered by StoreServerEntry.
  168. err = StoreServerEntries(decodedServerEntries, true)
  169. if err != nil {
  170. return ContextError(err)
  171. }
  172. // TODO: formally communicate the sponsor and upgrade info to an
  173. // outer client via some control interface.
  174. for _, homepage := range handshakeConfig.Homepages {
  175. NoticeHomepage(homepage)
  176. }
  177. serverContext.clientUpgradeVersion = handshakeConfig.UpgradeClientVersion
  178. if handshakeConfig.UpgradeClientVersion != "" {
  179. NoticeClientUpgradeAvailable(handshakeConfig.UpgradeClientVersion)
  180. } else {
  181. NoticeClientIsLatestVersion("")
  182. }
  183. var regexpsNotices []string
  184. serverContext.statsRegexps, regexpsNotices = transferstats.MakeRegexps(
  185. handshakeConfig.PageViewRegexes,
  186. handshakeConfig.HttpsRequestRegexes)
  187. for _, notice := range regexpsNotices {
  188. NoticeAlert(notice)
  189. }
  190. serverContext.serverHandshakeTimestamp = handshakeConfig.ServerTimestamp
  191. return nil
  192. }
  193. // DoConnectedRequest performs the connected API request. This request is
  194. // used for statistics. The server returns a last_connected token for
  195. // the client to store and send next time it connects. This token is
  196. // a timestamp (using the server clock, and should be rounded to the
  197. // nearest hour) which is used to determine when a connection represents
  198. // a unique user for a time period.
  199. func (serverContext *ServerContext) DoConnectedRequest() error {
  200. const DATA_STORE_LAST_CONNECTED_KEY = "lastConnected"
  201. lastConnected, err := GetKeyValue(DATA_STORE_LAST_CONNECTED_KEY)
  202. if err != nil {
  203. return ContextError(err)
  204. }
  205. if lastConnected == "" {
  206. lastConnected = "None"
  207. }
  208. url := buildRequestUrl(
  209. serverContext.baseRequestUrl,
  210. "connected",
  211. &ExtraParam{"session_id", serverContext.sessionId},
  212. &ExtraParam{"last_connected", lastConnected})
  213. responseBody, err := serverContext.doGetRequest(url)
  214. if err != nil {
  215. return ContextError(err)
  216. }
  217. var response struct {
  218. ConnectedTimestamp string `json:"connected_timestamp"`
  219. }
  220. err = json.Unmarshal(responseBody, &response)
  221. if err != nil {
  222. return ContextError(err)
  223. }
  224. err = SetKeyValue(DATA_STORE_LAST_CONNECTED_KEY, response.ConnectedTimestamp)
  225. if err != nil {
  226. return ContextError(err)
  227. }
  228. return nil
  229. }
  230. // StatsRegexps gets the Regexps used for the statistics for this tunnel.
  231. func (serverContext *ServerContext) StatsRegexps() *transferstats.Regexps {
  232. return serverContext.statsRegexps
  233. }
  234. // DoStatusRequest makes a /status request to the server, sending session stats.
  235. func (serverContext *ServerContext) DoStatusRequest(tunnel *Tunnel) error {
  236. url := makeStatusRequestUrl(serverContext.sessionId, serverContext.baseRequestUrl, true)
  237. payload, payloadInfo, err := makeStatusRequestPayload(tunnel.serverEntry.IpAddress)
  238. if err != nil {
  239. return ContextError(err)
  240. }
  241. err = serverContext.doPostRequest(url, "application/json", bytes.NewReader(payload))
  242. if err != nil {
  243. // Resend the transfer stats and tunnel stats later
  244. // Note: potential duplicate reports if the server received and processed
  245. // the request but the client failed to receive the response.
  246. putBackStatusRequestPayload(payloadInfo)
  247. return ContextError(err)
  248. }
  249. confirmStatusRequestPayload(payloadInfo)
  250. return nil
  251. }
  252. func makeStatusRequestUrl(sessionId, baseRequestUrl string, isTunneled bool) string {
  253. // Add a random amount of padding to help prevent stats updates from being
  254. // a predictable size (which often happens when the connection is quiet).
  255. padding := MakeSecureRandomPadding(0, PSIPHON_API_STATUS_REQUEST_PADDING_MAX_BYTES)
  256. // Legacy clients set "connected" to "0" when disconnecting, and this value
  257. // is used to calculate session duration estimates. This is now superseded
  258. // by explicit tunnel stats duration reporting.
  259. // The legacy method of reconstructing session durations is not compatible
  260. // with this client's connected request retries and asynchronous final
  261. // status request attempts. So we simply set this "connected" flag to reflect
  262. // whether the request is sent tunneled or not.
  263. connected := "1"
  264. if !isTunneled {
  265. connected = "0"
  266. }
  267. return buildRequestUrl(
  268. baseRequestUrl,
  269. "status",
  270. &ExtraParam{"session_id", sessionId},
  271. &ExtraParam{"connected", connected},
  272. // TODO: base64 encoding of padding means the padding
  273. // size is not exactly [0, PADDING_MAX_BYTES]
  274. &ExtraParam{"padding", base64.StdEncoding.EncodeToString(padding)})
  275. }
  276. // statusRequestPayloadInfo is a temporary structure for data used to
  277. // either "clear" or "put back" status request payload data depending
  278. // on whether or not the request succeeded.
  279. type statusRequestPayloadInfo struct {
  280. serverId string
  281. transferStats *transferstats.AccumulatedStats
  282. tunnelStats [][]byte
  283. }
  284. func makeStatusRequestPayload(
  285. serverId string) ([]byte, *statusRequestPayloadInfo, error) {
  286. transferStats := transferstats.TakeOutStatsForServer(serverId)
  287. tunnelStats, err := TakeOutUnreportedTunnelStats(
  288. PSIPHON_API_TUNNEL_STATS_MAX_COUNT)
  289. if err != nil {
  290. NoticeAlert(
  291. "TakeOutUnreportedTunnelStats failed: %s", ContextError(err))
  292. tunnelStats = nil
  293. // Proceed with transferStats only
  294. }
  295. payloadInfo := &statusRequestPayloadInfo{
  296. serverId, transferStats, tunnelStats}
  297. payload := make(map[string]interface{})
  298. hostBytes, bytesTransferred := transferStats.GetStatsForStatusRequest()
  299. payload["host_bytes"] = hostBytes
  300. payload["bytes_transferred"] = bytesTransferred
  301. // We're not recording these fields, but the server requires them.
  302. payload["page_views"] = make([]string, 0)
  303. payload["https_requests"] = make([]string, 0)
  304. // Tunnel stats records are already in JSON format
  305. jsonTunnelStats := make([]json.RawMessage, len(tunnelStats))
  306. for i, tunnelStatsRecord := range tunnelStats {
  307. jsonTunnelStats[i] = json.RawMessage(tunnelStatsRecord)
  308. }
  309. payload["tunnel_stats"] = jsonTunnelStats
  310. jsonPayload, err := json.Marshal(payload)
  311. if err != nil {
  312. // Send the transfer stats and tunnel stats later
  313. putBackStatusRequestPayload(payloadInfo)
  314. return nil, nil, ContextError(err)
  315. }
  316. return jsonPayload, payloadInfo, nil
  317. }
  318. func putBackStatusRequestPayload(payloadInfo *statusRequestPayloadInfo) {
  319. transferstats.PutBackStatsForServer(
  320. payloadInfo.serverId, payloadInfo.transferStats)
  321. err := PutBackUnreportedTunnelStats(payloadInfo.tunnelStats)
  322. if err != nil {
  323. // These tunnel stats records won't be resent under after a
  324. // datastore re-initialization.
  325. NoticeAlert(
  326. "PutBackUnreportedTunnelStats failed: %s", ContextError(err))
  327. }
  328. }
  329. func confirmStatusRequestPayload(payloadInfo *statusRequestPayloadInfo) {
  330. err := ClearReportedTunnelStats(payloadInfo.tunnelStats)
  331. if err != nil {
  332. // These tunnel stats records may be resent.
  333. NoticeAlert(
  334. "ClearReportedTunnelStats failed: %s", ContextError(err))
  335. }
  336. }
  337. // TryUntunneledStatusRequest makes direct connections to the specified
  338. // server (if supported) in an attempt to send useful bytes transferred
  339. // and tunnel duration stats after a tunnel has alreay failed.
  340. // The tunnel is assumed to be closed, but its config, protocol, and
  341. // context values must still be valid.
  342. // TryUntunneledStatusRequest emits notices detailing failed attempts.
  343. func TryUntunneledStatusRequest(tunnel *Tunnel, isShutdown bool) error {
  344. for _, port := range tunnel.serverEntry.GetDirectWebRequestPorts() {
  345. err := doUntunneledStatusRequest(tunnel, port, isShutdown)
  346. if err == nil {
  347. return nil
  348. }
  349. NoticeAlert("doUntunneledStatusRequest failed for %s:%s: %s",
  350. tunnel.serverEntry.IpAddress, port, err)
  351. }
  352. return errors.New("all attempts failed")
  353. }
  354. // doUntunneledStatusRequest attempts an untunneled stratus request.
  355. func doUntunneledStatusRequest(
  356. tunnel *Tunnel, port string, isShutdown bool) error {
  357. url := makeStatusRequestUrl(
  358. tunnel.serverContext.sessionId,
  359. makeBaseRequestUrl(tunnel, port, tunnel.serverContext.sessionId),
  360. false)
  361. certificate, err := DecodeCertificate(tunnel.serverEntry.WebServerCertificate)
  362. if err != nil {
  363. return ContextError(err)
  364. }
  365. timeout := time.Duration(*tunnel.config.PsiphonApiServerTimeoutSeconds) * time.Second
  366. dialConfig := tunnel.untunneledDialConfig
  367. if isShutdown {
  368. timeout = PSIPHON_API_SHUTDOWN_SERVER_TIMEOUT
  369. // Use a copy of DialConfig without pendingConns. This ensures
  370. // this request isn't interrupted/canceled. This measure should
  371. // be used only with the very short PSIPHON_API_SHUTDOWN_SERVER_TIMEOUT.
  372. dialConfig = new(DialConfig)
  373. *dialConfig = *tunnel.untunneledDialConfig
  374. }
  375. httpClient, requestUrl, err := MakeUntunneledHttpsClient(
  376. dialConfig,
  377. certificate,
  378. url,
  379. timeout)
  380. if err != nil {
  381. return ContextError(err)
  382. }
  383. payload, payloadInfo, err := makeStatusRequestPayload(tunnel.serverEntry.IpAddress)
  384. if err != nil {
  385. return ContextError(err)
  386. }
  387. bodyType := "application/json"
  388. body := bytes.NewReader(payload)
  389. response, err := httpClient.Post(requestUrl, bodyType, body)
  390. if err == nil && response.StatusCode != http.StatusOK {
  391. response.Body.Close()
  392. err = fmt.Errorf("HTTP POST request failed with response code: %d", response.StatusCode)
  393. }
  394. if err != nil {
  395. // Resend the transfer stats and tunnel stats later
  396. // Note: potential duplicate reports if the server received and processed
  397. // the request but the client failed to receive the response.
  398. putBackStatusRequestPayload(payloadInfo)
  399. // Trim this error since it may include long URLs
  400. return ContextError(TrimError(err))
  401. }
  402. confirmStatusRequestPayload(payloadInfo)
  403. response.Body.Close()
  404. return nil
  405. }
  406. // RecordTunnelStats records a tunnel duration and bytes
  407. // sent and received for subsequent reporting and quality
  408. // analysis.
  409. //
  410. // Tunnel durations are precisely measured client-side
  411. // and reported in status requests. As the duration is
  412. // not determined until the tunnel is closed, tunnel
  413. // stats records are stored in the persistent datastore
  414. // and reported via subsequent status requests sent to any
  415. // Psiphon server.
  416. //
  417. // Since the status request that reports a tunnel stats
  418. // record is not necessarily handled by the same server, the
  419. // tunnel stats records include the original server ID.
  420. //
  421. // Other fields that may change between tunnel stats recording
  422. // and reporting include client geo data, propagation channel,
  423. // sponsor ID, client version. These are not stored in the
  424. // datastore (client region, in particular, since that would
  425. // create an on-disk record of user location).
  426. // TODO: the server could encrypt, with a nonce and key unknown to
  427. // the client, a blob containing this data; return it in the
  428. // handshake response; and the client could store and later report
  429. // this blob with its tunnel stats records.
  430. //
  431. // Multiple "status" requests may be in flight at once (due
  432. // to multi-tunnel, asynchronous final status retry, and
  433. // aggressive status requests for pre-registered tunnels),
  434. // To avoid duplicate reporting, tunnel stats records are
  435. // "taken-out" by a status request and then "put back" in
  436. // case the request fails.
  437. //
  438. // Note: since tunnel stats records have a globally unique
  439. // identifier (sessionId + tunnelNumber), we could tolerate
  440. // duplicate reporting and filter our duplicates on the
  441. // server-side. Permitting duplicate reporting could increase
  442. // the velocity of reporting (for example, both the asynchronous
  443. // untunneled final status requests and the post-connected
  444. // immediate startus requests could try to report the same tunnel
  445. // stats).
  446. // Duplicate reporting may also occur when a server receives and
  447. // processes a status request but the client fails to receive
  448. // the response.
  449. func RecordTunnelStats(
  450. sessionId string,
  451. tunnelNumber int64,
  452. tunnelServerIpAddress string,
  453. serverHandshakeTimestamp, duration string,
  454. totalBytesSent, totalBytesReceived int64) error {
  455. tunnelStats := struct {
  456. SessionId string `json:"session_id"`
  457. TunnelNumber int64 `json:"tunnel_number"`
  458. TunnelServerIpAddress string `json:"tunnel_server_ip_address"`
  459. ServerHandshakeTimestamp string `json:"server_handshake_timestamp"`
  460. Duration string `json:"duration"`
  461. TotalBytesSent int64 `json:"total_bytes_sent"`
  462. TotalBytesReceived int64 `json:"total_bytes_received"`
  463. }{
  464. sessionId,
  465. tunnelNumber,
  466. tunnelServerIpAddress,
  467. serverHandshakeTimestamp,
  468. duration,
  469. totalBytesSent,
  470. totalBytesReceived,
  471. }
  472. tunnelStatsJson, err := json.Marshal(tunnelStats)
  473. if err != nil {
  474. return ContextError(err)
  475. }
  476. return StoreTunnelStats(tunnelStatsJson)
  477. }
  478. // doGetRequest makes a tunneled HTTPS request and returns the response body.
  479. func (serverContext *ServerContext) doGetRequest(
  480. requestUrl string) (responseBody []byte, err error) {
  481. response, err := serverContext.psiphonHttpsClient.Get(requestUrl)
  482. if err == nil && response.StatusCode != http.StatusOK {
  483. response.Body.Close()
  484. err = fmt.Errorf("HTTP GET request failed with response code: %d", response.StatusCode)
  485. }
  486. if err != nil {
  487. // Trim this error since it may include long URLs
  488. return nil, ContextError(TrimError(err))
  489. }
  490. defer response.Body.Close()
  491. body, err := ioutil.ReadAll(response.Body)
  492. if err != nil {
  493. return nil, ContextError(err)
  494. }
  495. return body, nil
  496. }
  497. // doPostRequest makes a tunneled HTTPS POST request.
  498. func (serverContext *ServerContext) doPostRequest(
  499. requestUrl string, bodyType string, body io.Reader) (err error) {
  500. response, err := serverContext.psiphonHttpsClient.Post(requestUrl, bodyType, body)
  501. if err == nil && response.StatusCode != http.StatusOK {
  502. response.Body.Close()
  503. err = fmt.Errorf("HTTP POST request failed with response code: %d", response.StatusCode)
  504. }
  505. if err != nil {
  506. // Trim this error since it may include long URLs
  507. return ContextError(TrimError(err))
  508. }
  509. response.Body.Close()
  510. return nil
  511. }
  512. // makeBaseRequestUrl makes a URL containing all the common parameters
  513. // that are included with Psiphon API requests. These common parameters
  514. // are used for statistics.
  515. func makeBaseRequestUrl(tunnel *Tunnel, port, sessionId string) string {
  516. var requestUrl bytes.Buffer
  517. if port == "" {
  518. port = tunnel.serverEntry.WebServerPort
  519. }
  520. // Note: don't prefix with HTTPS scheme, see comment in doGetRequest.
  521. // e.g., don't do this: requestUrl.WriteString("https://")
  522. requestUrl.WriteString("http://")
  523. requestUrl.WriteString(tunnel.serverEntry.IpAddress)
  524. requestUrl.WriteString(":")
  525. requestUrl.WriteString(port)
  526. requestUrl.WriteString("/")
  527. // Placeholder for the path component of a request
  528. requestUrl.WriteString("%s")
  529. requestUrl.WriteString("?client_session_id=")
  530. requestUrl.WriteString(sessionId)
  531. requestUrl.WriteString("&server_secret=")
  532. requestUrl.WriteString(tunnel.serverEntry.WebServerSecret)
  533. requestUrl.WriteString("&propagation_channel_id=")
  534. requestUrl.WriteString(tunnel.config.PropagationChannelId)
  535. requestUrl.WriteString("&sponsor_id=")
  536. requestUrl.WriteString(tunnel.config.SponsorId)
  537. requestUrl.WriteString("&client_version=")
  538. requestUrl.WriteString(tunnel.config.ClientVersion)
  539. // TODO: client_tunnel_core_version
  540. requestUrl.WriteString("&relay_protocol=")
  541. requestUrl.WriteString(tunnel.protocol)
  542. requestUrl.WriteString("&client_platform=")
  543. requestUrl.WriteString(tunnel.config.ClientPlatform)
  544. requestUrl.WriteString("&tunnel_whole_device=")
  545. requestUrl.WriteString(strconv.Itoa(tunnel.config.TunnelWholeDevice))
  546. // The following parameters may be blank and must
  547. // not be sent to the server if blank.
  548. if tunnel.config.DeviceRegion != "" {
  549. requestUrl.WriteString("&device_region=")
  550. requestUrl.WriteString(tunnel.config.DeviceRegion)
  551. }
  552. if tunnel.meekStats != nil {
  553. if tunnel.meekStats.DialAddress != "" {
  554. requestUrl.WriteString("&meek_dial_address=")
  555. requestUrl.WriteString(tunnel.meekStats.DialAddress)
  556. }
  557. if tunnel.meekStats.ResolvedIPAddress != "" {
  558. requestUrl.WriteString("&meek_resolved_ip_address=")
  559. requestUrl.WriteString(tunnel.meekStats.ResolvedIPAddress)
  560. }
  561. if tunnel.meekStats.SNIServerName != "" {
  562. requestUrl.WriteString("&meek_sni_server_name=")
  563. requestUrl.WriteString(tunnel.meekStats.SNIServerName)
  564. }
  565. if tunnel.meekStats.HostHeader != "" {
  566. requestUrl.WriteString("&meek_host_header=")
  567. requestUrl.WriteString(tunnel.meekStats.HostHeader)
  568. }
  569. requestUrl.WriteString("&meek_transformed_host_name=")
  570. if tunnel.meekStats.TransformedHostName {
  571. requestUrl.WriteString("1")
  572. } else {
  573. requestUrl.WriteString("0")
  574. }
  575. }
  576. if tunnel.serverEntry.Region != "" {
  577. requestUrl.WriteString("&server_entry_region=")
  578. requestUrl.WriteString(tunnel.serverEntry.Region)
  579. }
  580. if tunnel.serverEntry.LocalSource != "" {
  581. requestUrl.WriteString("&server_entry_source=")
  582. requestUrl.WriteString(tunnel.serverEntry.LocalSource)
  583. }
  584. // As with last_connected, this timestamp stat, which may be
  585. // a precise handshake request server timestamp, is truncated
  586. // to hour granularity to avoid introducing a reconstructable
  587. // cross-session user trace into server logs.
  588. localServerEntryTimestamp := TruncateTimestampToHour(tunnel.serverEntry.LocalTimestamp)
  589. if localServerEntryTimestamp != "" {
  590. requestUrl.WriteString("&server_entry_timestamp=")
  591. requestUrl.WriteString(localServerEntryTimestamp)
  592. }
  593. return requestUrl.String()
  594. }
  595. type ExtraParam struct{ name, value string }
  596. // buildRequestUrl makes a URL for an API request. The URL includes the
  597. // base request URL and any extra parameters for the specific request.
  598. func buildRequestUrl(baseRequestUrl, path string, extraParams ...*ExtraParam) string {
  599. var requestUrl bytes.Buffer
  600. requestUrl.WriteString(fmt.Sprintf(baseRequestUrl, path))
  601. for _, extraParam := range extraParams {
  602. requestUrl.WriteString("&")
  603. requestUrl.WriteString(extraParam.name)
  604. requestUrl.WriteString("=")
  605. requestUrl.WriteString(extraParam.value)
  606. }
  607. return requestUrl.String()
  608. }
  609. // makePsiphonHttpsClient creates a Psiphon HTTPS client that tunnels requests and which validates
  610. // the web server using the Psiphon server entry web server certificate.
  611. // This is not a general purpose HTTPS client.
  612. // As the custom dialer makes an explicit TLS connection, URLs submitted to the returned
  613. // http.Client should use the "http://" scheme. Otherwise http.Transport will try to do another TLS
  614. // handshake inside the explicit TLS session.
  615. func makePsiphonHttpsClient(tunnel *Tunnel) (httpsClient *http.Client, err error) {
  616. certificate, err := DecodeCertificate(tunnel.serverEntry.WebServerCertificate)
  617. if err != nil {
  618. return nil, ContextError(err)
  619. }
  620. tunneledDialer := func(_, addr string) (conn net.Conn, err error) {
  621. // TODO: check tunnel.isClosed, and apply TUNNEL_PORT_FORWARD_DIAL_TIMEOUT as in Tunnel.Dial?
  622. return tunnel.sshClient.Dial("tcp", addr)
  623. }
  624. timeout := time.Duration(*tunnel.config.PsiphonApiServerTimeoutSeconds) * time.Second
  625. dialer := NewCustomTLSDialer(
  626. &CustomTLSConfig{
  627. Dial: tunneledDialer,
  628. Timeout: timeout,
  629. VerifyLegacyCertificate: certificate,
  630. })
  631. transport := &http.Transport{
  632. Dial: dialer,
  633. ResponseHeaderTimeout: timeout,
  634. }
  635. return &http.Client{
  636. Transport: transport,
  637. Timeout: timeout,
  638. }, nil
  639. }