github.com/Psiphon-Labs/psiphon-tunnel-core@v2.0.28+incompatible/psiphon/userAgent_test.go (about) 1 /* 2 * Copyright (c) 2017, Psiphon Inc. 3 * All rights reserved. 4 * 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package psiphon 21 22 import ( 23 "context" 24 "fmt" 25 "io/ioutil" 26 "net/http" 27 "os" 28 "sync" 29 "testing" 30 "time" 31 32 "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common" 33 "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/values" 34 "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/server" 35 "github.com/elazarl/goproxy" 36 ) 37 38 // TODO: test that server receives and records correct user_agent value 39 40 func TestOSSHUserAgent(t *testing.T) { 41 attemptConnectionsWithUserAgent(t, "OSSH", true) 42 } 43 44 func TestUnfrontedMeekUserAgent(t *testing.T) { 45 attemptConnectionsWithUserAgent(t, "UNFRONTED-MEEK-OSSH", false) 46 } 47 48 func TestUnfrontedMeekHTTPSUserAgent(t *testing.T) { 49 attemptConnectionsWithUserAgent(t, "UNFRONTED-MEEK-HTTPS-OSSH", true) 50 } 51 52 var mockUserAgents = []string{"UserAgentA", "UserAgentB"} 53 var userAgentCountsMutex sync.Mutex 54 var userAgentCounts map[string]int 55 var initUserAgentCounter sync.Once 56 57 func initMockUserAgents() { 58 values.SetUserAgentsSpec(values.NewPickOneSpec(mockUserAgents)) 59 } 60 61 func resetUserAgentCounts() { 62 userAgentCountsMutex.Lock() 63 defer userAgentCountsMutex.Unlock() 64 userAgentCounts = make(map[string]int) 65 } 66 67 func countHTTPUserAgent(headers http.Header, isCONNECT bool) { 68 userAgentCountsMutex.Lock() 69 defer userAgentCountsMutex.Unlock() 70 if _, ok := headers["User-Agent"]; !ok { 71 userAgentCounts["BLANK"]++ 72 } else if isCONNECT { 73 userAgentCounts["CONNECT-"+headers.Get("User-Agent")]++ 74 } else { 75 userAgentCounts[headers.Get("User-Agent")]++ 76 } 77 } 78 79 func countNoticeUserAgent(userAgent string) { 80 userAgentCountsMutex.Lock() 81 defer userAgentCountsMutex.Unlock() 82 userAgentCounts["NOTICE-"+userAgent]++ 83 } 84 85 func checkUserAgentCounts(t *testing.T, isCONNECT bool) { 86 userAgentCountsMutex.Lock() 87 defer userAgentCountsMutex.Unlock() 88 89 for _, userAgent := range mockUserAgents { 90 91 if isCONNECT { 92 if userAgentCounts["CONNECT-"+userAgent] == 0 { 93 t.Fatalf("unexpected CONNECT user agent count of 0: %+v", userAgentCounts) 94 return 95 } 96 } else { 97 98 if userAgentCounts[userAgent] == 0 { 99 t.Fatalf("unexpected non-CONNECT user agent count of 0: %+v", userAgentCounts) 100 return 101 } 102 } 103 104 if userAgentCounts["NOTICE-"+userAgent] == 0 { 105 t.Fatalf("unexpected NOTICE user agent count of 0: %+v", userAgentCounts) 106 return 107 } 108 } 109 110 if userAgentCounts["BLANK"] == 0 { 111 t.Fatalf("unexpected BLANK user agent count of 0: %+v", userAgentCounts) 112 return 113 } 114 115 // TODO: check proportions 116 t.Logf("%+v", userAgentCounts) 117 } 118 119 func initUserAgentCounterUpstreamProxy() { 120 initUserAgentCounter.Do(func() { 121 go func() { 122 proxy := goproxy.NewProxyHttpServer() 123 124 proxy.OnRequest().DoFunc( 125 func(r *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { 126 countHTTPUserAgent(r.Header, false) 127 return nil, goproxy.NewResponse(r, goproxy.ContentTypeText, http.StatusUnauthorized, "") 128 }) 129 130 proxy.OnRequest().HandleConnectFunc( 131 func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) { 132 countHTTPUserAgent(ctx.Req.Header, true) 133 return goproxy.RejectConnect, host 134 }) 135 136 err := http.ListenAndServe("127.0.0.1:2163", proxy) 137 if err != nil { 138 fmt.Printf("upstream proxy failed: %s\n", err) 139 } 140 }() 141 142 // TODO: more robust wait-until-listening 143 time.Sleep(1 * time.Second) 144 }) 145 } 146 147 func attemptConnectionsWithUserAgent( 148 t *testing.T, tunnelProtocol string, isCONNECT bool) { 149 150 testDataDirName, err := ioutil.TempDir("", "psiphon-user-agent-test") 151 if err != nil { 152 t.Fatalf("TempDir failed: %s\n", err) 153 } 154 defer os.RemoveAll(testDataDirName) 155 156 initMockUserAgents() 157 initUserAgentCounterUpstreamProxy() 158 resetUserAgentCounts() 159 160 // create a server entry 161 162 serverIPv4Address, serverIPv6Address, err := common.GetRoutableInterfaceIPAddresses() 163 if err != nil { 164 t.Fatalf("error getting server IP address: %s", err) 165 } 166 serverIPAddress := "" 167 if serverIPv4Address != nil { 168 serverIPAddress = serverIPv4Address.String() 169 } else { 170 serverIPAddress = serverIPv6Address.String() 171 } 172 173 _, _, _, _, encodedServerEntry, err := server.GenerateConfig( 174 &server.GenerateConfigParams{ 175 ServerIPAddress: serverIPAddress, 176 EnableSSHAPIRequests: true, 177 WebServerPort: 8000, 178 TunnelProtocolPorts: map[string]int{tunnelProtocol: 4000}, 179 }) 180 if err != nil { 181 t.Fatalf("error generating server config: %s", err) 182 } 183 184 // attempt connections with client 185 186 // Connections are made through a mock upstream proxy that 187 // counts user agents. No server is running, and the upstream 188 // proxy rejects connections after counting the user agent. 189 190 // Note: calling LoadConfig ensures all *int config fields are initialized 191 clientConfigJSON := ` 192 { 193 "ClientPlatform" : "Windows", 194 "ClientVersion" : "0", 195 "SponsorId" : "0", 196 "PropagationChannelId" : "0", 197 "ConnectionWorkerPoolSize" : 1, 198 "EstablishTunnelPausePeriodSeconds" : 1, 199 "DisableRemoteServerListFetcher" : true, 200 "TransformHostNameProbability" : 0.0, 201 "UpstreamProxyUrl" : "http://127.0.0.1:2163", 202 "UpstreamProxyAllowAllServerEntrySources" : true 203 }` 204 clientConfig, err := LoadConfig([]byte(clientConfigJSON)) 205 if err != nil { 206 t.Fatalf("error processing configuration file: %s", err) 207 } 208 209 clientConfig.TargetServerEntry = string(encodedServerEntry) 210 clientConfig.TunnelProtocol = tunnelProtocol 211 clientConfig.DataRootDirectory = testDataDirName 212 213 err = clientConfig.Commit(false) 214 if err != nil { 215 t.Fatalf("error committing configuration file: %s", err) 216 } 217 218 err = OpenDataStore(clientConfig) 219 if err != nil { 220 t.Fatalf("error initializing client datastore: %s", err) 221 } 222 defer CloseDataStore() 223 224 SetNoticeWriter(NewNoticeReceiver( 225 func(notice []byte) { 226 noticeType, payload, err := GetNotice(notice) 227 if err != nil { 228 return 229 } 230 if noticeType == "ConnectingServer" { 231 userAgent, ok := payload["userAgent"] 232 if ok { 233 countNoticeUserAgent(userAgent.(string)) 234 } 235 } 236 })) 237 238 controller, err := NewController(clientConfig) 239 if err != nil { 240 t.Fatalf("error creating client controller: %s", err) 241 } 242 243 ctx, cancelFunc := context.WithCancel(context.Background()) 244 245 controllerWaitGroup := new(sync.WaitGroup) 246 247 controllerWaitGroup.Add(1) 248 go func() { 249 defer controllerWaitGroup.Done() 250 controller.Run(ctx) 251 }() 252 253 // repeat attempts for long enough to select each user agent 254 255 time.Sleep(30 * time.Second) 256 257 cancelFunc() 258 259 controllerWaitGroup.Wait() 260 261 checkUserAgentCounts(t, isCONNECT) 262 }