feedback_test.go 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  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. package psiphon
  20. import (
  21. "context"
  22. "crypto/rand"
  23. "crypto/rsa"
  24. "crypto/tls"
  25. "crypto/x509"
  26. "encoding/base64"
  27. "encoding/json"
  28. "errors"
  29. "fmt"
  30. "io"
  31. "io/ioutil"
  32. "net/http"
  33. "os"
  34. "os/exec"
  35. "strings"
  36. "testing"
  37. "time"
  38. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
  39. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
  40. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
  41. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/values"
  42. )
  43. type Diagnostics struct {
  44. Feedback struct {
  45. Message struct {
  46. Text string `json:"text"`
  47. }
  48. Email string `json:"email"`
  49. }
  50. Metadata struct {
  51. Id string `json:"id"`
  52. Platform string `json:"platform"`
  53. Version int `json:"version"`
  54. }
  55. }
  56. func TestFeedbackUploadRemote(t *testing.T) {
  57. configFileContents, err := ioutil.ReadFile("controller_test.config")
  58. if err != nil {
  59. // Skip, don't fail, if config file is not present
  60. t.Skipf("error loading configuration file: %s", err)
  61. }
  62. // Load config, configure data root directory and commit it
  63. config, err := LoadConfig(configFileContents)
  64. if err != nil {
  65. t.Fatalf("error loading configuration file: %s", err)
  66. }
  67. if !config.EnableFeedbackUpload {
  68. config.EnableFeedbackUpload = true
  69. }
  70. if config.ClientPlatform == "" {
  71. config.ClientPlatform = testClientPlatform
  72. }
  73. testDataDirName, err := ioutil.TempDir("", "psiphon-feedback-test")
  74. if err != nil {
  75. t.Fatalf("TempDir failed: %s", err)
  76. }
  77. config.DataRootDirectory = testDataDirName
  78. err = config.Commit(true)
  79. if err != nil {
  80. t.Fatalf("error committing configuration file: %s", err)
  81. }
  82. shortRevHash, err := exec.Command("git", "rev-parse", "--short", "HEAD").Output()
  83. if err != nil {
  84. // Log, don't fail, if git rev is not available
  85. t.Logf("error loading git revision file: %s", err)
  86. shortRevHash = []byte("unknown")
  87. }
  88. // Construct feedback data which can be verified later
  89. diagnostics := Diagnostics{}
  90. diagnostics.Feedback.Message.Text = "Test feedback from feedback_test.go, revision: " + string(shortRevHash)
  91. diagnostics.Metadata.Id = "0000000000000000"
  92. diagnostics.Metadata.Platform = "android"
  93. diagnostics.Metadata.Version = 4
  94. diagnosticData, err := json.Marshal(diagnostics)
  95. if err != nil {
  96. t.Fatalf("Marshal failed: %s", err)
  97. }
  98. err = SendFeedback(context.Background(), config, string(diagnosticData), "")
  99. if err != nil {
  100. t.Fatalf("SendFeedback failed: %s", err)
  101. }
  102. }
  103. func TestFeedbackUploadLocal(t *testing.T) {
  104. t.Run("without fronting spec", func(t *testing.T) {
  105. runTestFeedbackUploadLocal(t, false)
  106. })
  107. t.Run("with fronting spec", func(t *testing.T) {
  108. runTestFeedbackUploadLocal(t, true)
  109. })
  110. }
  111. func runTestFeedbackUploadLocal(t *testing.T, useFrontingSpecs bool) {
  112. // Generate server keys
  113. sk, err := rsa.GenerateKey(rand.Reader, 2048)
  114. if err != nil {
  115. t.Fatalf("error generating key: %s", err)
  116. }
  117. pubKeyBytes, err := x509.MarshalPKIXPublicKey(&sk.PublicKey)
  118. if err != nil {
  119. t.Fatalf("error marshaling public key: %s", err)
  120. }
  121. // Start local server that will receive feedback upload
  122. mux := http.NewServeMux()
  123. mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  124. _, err := io.ReadAll(r.Body)
  125. if err != nil {
  126. w.WriteHeader(http.StatusInternalServerError)
  127. }
  128. // TODO: verify HMAC and decrypt feedback
  129. })
  130. host := values.GetHostName()
  131. certificate, privateKey, _, err := common.GenerateWebServerCertificate(host)
  132. if err != nil {
  133. t.Fatalf("common.GenerateWebServerCertificate failed: %v", err)
  134. }
  135. tlsCertificate, err := tls.X509KeyPair([]byte(certificate), []byte(privateKey))
  136. if err != nil {
  137. t.Fatalf("tls.X509KeyPair failed: %v", err)
  138. }
  139. serverConfig := &tls.Config{
  140. Certificates: []tls.Certificate{tlsCertificate},
  141. NextProtos: []string{"http/1.1"},
  142. MinVersion: tls.VersionTLS10,
  143. }
  144. listener, err := tls.Listen("tcp", "127.0.0.1:0", serverConfig)
  145. if err != nil {
  146. t.Fatalf("net.Listen failed %v", err)
  147. }
  148. defer listener.Close()
  149. s := &http.Server{
  150. Addr: listener.Addr().String(),
  151. Handler: mux,
  152. }
  153. serverErrors := make(chan error)
  154. defer func() {
  155. err := s.Shutdown(context.Background())
  156. if err != nil {
  157. t.Fatalf("error shutting down server: %s", err)
  158. }
  159. err = <-serverErrors
  160. if err != nil {
  161. t.Fatalf("error running server: %s", err)
  162. }
  163. }()
  164. go func() {
  165. err := s.Serve(listener)
  166. if !errors.Is(err, http.ErrServerClosed) {
  167. serverErrors <- err
  168. }
  169. close(serverErrors)
  170. }()
  171. // Setup client
  172. networkID := fmt.Sprintf("WIFI-%s", time.Now().String())
  173. clientConfigJSON := fmt.Sprintf(`
  174. {
  175. "ClientPlatform" : "Android_10_com.test.app",
  176. "ClientVersion" : "0",
  177. "SponsorId" : "0000000000000000",
  178. "PropagationChannelId" : "0000000000000000",
  179. "DeviceLocation" : "gzzzz",
  180. "DeviceRegion" : "US",
  181. "DisableRemoteServerListFetcher" : true,
  182. "EnableFeedbackUpload" : true,
  183. "DisableTactics" : true,
  184. "FeedbackEncryptionPublicKey" : "%s",
  185. "NetworkID" : "%s"
  186. }`,
  187. base64.StdEncoding.EncodeToString(pubKeyBytes),
  188. networkID)
  189. config, err := LoadConfig([]byte(clientConfigJSON))
  190. if err != nil {
  191. t.Fatalf("error processing configuration file: %s", err)
  192. }
  193. testDataDirName, err := os.MkdirTemp("", "psiphon-feedback-test")
  194. if err != nil {
  195. t.Fatalf("TempDir failed: %s", err)
  196. }
  197. defer os.RemoveAll(testDataDirName)
  198. config.DataRootDirectory = testDataDirName
  199. address := listener.Addr().String()
  200. addressRegex := strings.ReplaceAll(address, ".", "\\.")
  201. url := fmt.Sprintf("https://%s", address)
  202. var frontingSpecs parameters.FrontingSpecs
  203. if useFrontingSpecs {
  204. frontingSpecs = parameters.FrontingSpecs{
  205. {
  206. FrontingProviderID: prng.HexString(8),
  207. Addresses: []string{addressRegex},
  208. DisableSNI: prng.FlipCoin(),
  209. SkipVerify: true,
  210. Host: host,
  211. },
  212. }
  213. }
  214. config.FeedbackUploadURLs = parameters.TransferURLs{
  215. {
  216. URL: base64.StdEncoding.EncodeToString([]byte(url)),
  217. SkipVerify: true,
  218. OnlyAfterAttempts: 0,
  219. B64EncodedPublicKey: base64.StdEncoding.EncodeToString(pubKeyBytes),
  220. RequestHeaders: map[string]string{},
  221. FrontingSpecs: frontingSpecs,
  222. },
  223. }
  224. err = config.Commit(true)
  225. if err != nil {
  226. t.Fatalf("error committing configuration file: %s", err)
  227. }
  228. err = OpenDataStore(config)
  229. if err != nil {
  230. t.Fatalf("OpenDataStore failed: %s", err)
  231. }
  232. defer CloseDataStore()
  233. // Construct feedback data
  234. diagnostics := Diagnostics{}
  235. diagnostics.Feedback.Message.Text = "Test feedback from feedback_test.go"
  236. diagnostics.Metadata.Id = "0000000000000000"
  237. diagnostics.Metadata.Platform = "android"
  238. diagnostics.Metadata.Version = 4
  239. diagnosticData, err := json.Marshal(diagnostics)
  240. if err != nil {
  241. t.Fatalf("Marshal failed: %s", err)
  242. }
  243. // Upload feedback
  244. err = SendFeedback(context.Background(), config, string(diagnosticData), "/upload_path")
  245. if err != nil {
  246. t.Fatalf("SendFeedback failed: %s", err)
  247. }
  248. // Upload feedback again to exercise replay
  249. err = SendFeedback(context.Background(), config, string(diagnosticData), "/upload_path")
  250. if err != nil {
  251. t.Fatalf("SendFeedback failed: %s", err)
  252. }
  253. }