github.com/psiphon-labs/psiphon-tunnel-core@v2.0.28+incompatible/psiphon/transferstats/transferstats_test.go (about) 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 20 package transferstats 21 22 import ( 23 "errors" 24 "fmt" 25 "net" 26 "net/http" 27 "regexp" 28 "testing" 29 30 mapset "github.com/deckarep/golang-set" 31 "github.com/stretchr/testify/suite" 32 ) 33 34 const ( 35 _SERVER_ID = "myserverid" 36 ) 37 38 var nextServerID = 0 39 40 type StatsTestSuite struct { 41 suite.Suite 42 serverID string 43 httpClient *http.Client 44 } 45 46 func TestStatsTestSuite(t *testing.T) { 47 suite.Run(t, new(StatsTestSuite)) 48 } 49 50 func (suite *StatsTestSuite) SetupTest() { 51 52 suite.serverID = fmt.Sprintf("%s-%d", _SERVER_ID, nextServerID) 53 nextServerID++ 54 suite.httpClient = &http.Client{ 55 Transport: &http.Transport{ 56 Dial: makeStatsDialer(suite.serverID, &Regexps{}), 57 }, 58 } 59 } 60 61 func (suite *StatsTestSuite) TearDownTest() { 62 suite.httpClient = nil 63 } 64 65 func makeStatsDialer(serverID string, regexps *Regexps) func(network, addr string) (conn net.Conn, err error) { 66 return func(network, addr string) (conn net.Conn, err error) { 67 var subConn net.Conn 68 69 switch network { 70 case "tcp", "tcp4", "tcp6": 71 tcpAddr, err := net.ResolveTCPAddr(network, addr) 72 if err != nil { 73 return nil, err 74 } 75 subConn, err = net.DialTCP(network, nil, tcpAddr) 76 if err != nil { 77 return nil, err 78 } 79 default: 80 err = errors.New("using an unsupported testing network type") 81 return 82 } 83 84 conn = NewConn(subConn, serverID, regexps) 85 err = nil 86 return 87 } 88 } 89 90 func (suite *StatsTestSuite) Test_StatsConn() { 91 resp, err := suite.httpClient.Get("http://example.com/index.html") 92 suite.Nil(err, "basic HTTP requests should succeed") 93 resp.Body.Close() 94 95 resp, err = suite.httpClient.Get("https://example.org/index.html") 96 suite.Nil(err, "basic HTTPS requests should succeed") 97 resp.Body.Close() 98 } 99 100 func (suite *StatsTestSuite) Test_TakeOutStatsForServer() { 101 102 zeroPayload := &AccumulatedStats{hostnameToStats: make(map[string]*hostStats)} 103 104 payload := TakeOutStatsForServer(suite.serverID) 105 suite.Equal(payload, zeroPayload, "should get zero stats before any traffic") 106 107 resp, err := suite.httpClient.Get("http://example.com/index.html") 108 suite.Nil(err, "need successful http to proceed with tests") 109 resp.Body.Close() 110 111 payload = TakeOutStatsForServer(suite.serverID) 112 suite.NotNil(payload, "should receive valid payload for valid server ID") 113 114 // After we retrieve the stats for a server, they should be cleared out of the tracked stats 115 payload = TakeOutStatsForServer(suite.serverID) 116 suite.Equal(payload, zeroPayload, "after retrieving stats for a server, there should be zero stats (until more data goes through)") 117 } 118 119 func (suite *StatsTestSuite) Test_PutBackStatsForServer() { 120 121 // Set a regexp for the httpClient to ensure it at least records "(OTHER)" domain bytes; 122 // The regex is set to "nomatch.com" so that it _will_ exercise the "(OTHER)" case. 123 regexp, _ := regexp.Compile(`^[a-z0-9\.]*\.(nomatch\.com)$`) 124 replace := "$1" 125 regexps := &Regexps{regexpReplace{regexp: regexp, replace: replace}} 126 suite.httpClient = &http.Client{ 127 Transport: &http.Transport{ 128 Dial: makeStatsDialer(suite.serverID, regexps), 129 }, 130 } 131 132 resp, err := suite.httpClient.Get("http://example.com/index.html") 133 suite.Nil(err, "need successful http to proceed with tests") 134 resp.Body.Close() 135 136 payloadToPutBack := TakeOutStatsForServer(suite.serverID) 137 suite.NotNil(payloadToPutBack, "should receive valid payload for valid server ID") 138 139 zeroPayload := &AccumulatedStats{hostnameToStats: make(map[string]*hostStats)} 140 141 payload := TakeOutStatsForServer(suite.serverID) 142 suite.Equal(payload, zeroPayload, "should be zero stats after getting them") 143 144 PutBackStatsForServer(suite.serverID, payloadToPutBack) 145 146 payload = TakeOutStatsForServer(suite.serverID) 147 suite.NotEqual(payload, zeroPayload, "stats should be re-added after putting back") 148 suite.Equal(payload, payloadToPutBack, "stats should be the same as after the first retrieval") 149 } 150 151 func (suite *StatsTestSuite) Test_NoRegexes() { 152 153 // Set no regexps for the httpClient 154 suite.httpClient = &http.Client{ 155 Transport: &http.Transport{ 156 Dial: makeStatsDialer(suite.serverID, &Regexps{}), 157 }, 158 } 159 160 // Ensure there are no stats before making the no-regex request 161 _ = TakeOutStatsForServer(suite.serverID) 162 163 resp, err := suite.httpClient.Get("http://example.com/index.html") 164 suite.Nil(err, "need successful http to proceed with tests") 165 resp.Body.Close() 166 167 zeroPayload := &AccumulatedStats{hostnameToStats: make(map[string]*hostStats)} 168 169 payload := TakeOutStatsForServer(suite.serverID) 170 suite.Equal(payload, zeroPayload, "should be zero stats after getting them") 171 } 172 173 func (suite *StatsTestSuite) Test_MakeRegexps() { 174 hostnameRegexes := []map[string]string{make(map[string]string), make(map[string]string)} 175 hostnameRegexes[0]["regex"] = `^[a-z0-9\.]*\.(example\.com)$` 176 hostnameRegexes[0]["replace"] = "$1" 177 hostnameRegexes[1]["regex"] = `^.*example\.org$` 178 hostnameRegexes[1]["replace"] = "replacement" 179 180 regexps, notices := MakeRegexps(hostnameRegexes) 181 suite.NotNil(regexps, "should return a valid object") 182 suite.Len(*regexps, 2, "should only have processed hostnameRegexes") 183 suite.Len(notices, 0, "should return no notices") 184 185 // 186 // Test some bad regexps 187 // 188 189 hostnameRegexes[0]["regex"] = "" 190 hostnameRegexes[0]["replace"] = "$1" 191 regexps, notices = MakeRegexps(hostnameRegexes) 192 suite.NotNil(regexps, "should return a valid object") 193 suite.Len(*regexps, 1, "should have discarded one regexp") 194 suite.Len(notices, 1, "should have returned one notice") 195 196 hostnameRegexes[0]["regex"] = `^[a-z0-9\.]*\.(example\.com)$` 197 hostnameRegexes[0]["replace"] = "" 198 regexps, notices = MakeRegexps(hostnameRegexes) 199 suite.NotNil(regexps, "should return a valid object") 200 suite.Len(*regexps, 1, "should have discarded one regexp") 201 suite.Len(notices, 1, "should have returned one notice") 202 203 hostnameRegexes[0]["regex"] = `^[a-z0-9\.]*\.(example\.com$` // missing closing paren 204 hostnameRegexes[0]["replace"] = "$1" 205 regexps, notices = MakeRegexps(hostnameRegexes) 206 suite.NotNil(regexps, "should return a valid object") 207 suite.Len(*regexps, 1, "should have discarded one regexp") 208 suite.Len(notices, 1, "should have returned one notice") 209 } 210 211 func (suite *StatsTestSuite) Test_Regex() { 212 // We'll make a new client with actual regexps. 213 hostnameRegexes := []map[string]string{make(map[string]string), make(map[string]string)} 214 hostnameRegexes[0]["regex"] = `^[a-z0-9\.]*\.(example\.com)$` 215 hostnameRegexes[0]["replace"] = "$1" 216 hostnameRegexes[1]["regex"] = `^.*example\.org$` 217 hostnameRegexes[1]["replace"] = "replacement" 218 regexps, _ := MakeRegexps(hostnameRegexes) 219 220 suite.httpClient = &http.Client{ 221 Transport: &http.Transport{ 222 Dial: makeStatsDialer(suite.serverID, regexps), 223 }, 224 } 225 226 // Using both HTTP and HTTPS will help us to exercise both methods of hostname parsing 227 for _, scheme := range []string{"http", "https"} { 228 // No subdomain, so won't match regex 229 url := fmt.Sprintf("%s://example.com/index.html", scheme) 230 resp, err := suite.httpClient.Get(url) 231 suite.Nil(err) 232 resp.Body.Close() 233 234 // Will match the first regex 235 url = fmt.Sprintf("%s://www.example.com/index.html", scheme) 236 resp, err = suite.httpClient.Get(url) 237 suite.Nil(err) 238 resp.Body.Close() 239 240 // Will match the second regex 241 url = fmt.Sprintf("%s://example.org/index.html", scheme) 242 resp, err = suite.httpClient.Get(url) 243 suite.Nil(err) 244 resp.Body.Close() 245 246 payload := TakeOutStatsForServer(suite.serverID) 247 suite.NotNil(payload, "should get stats because we made HTTP reqs; %s", scheme) 248 249 expectedHostnames := mapset.NewSet() 250 expectedHostnames.Add("(OTHER)") 251 expectedHostnames.Add("example.com") 252 expectedHostnames.Add("replacement") 253 254 hostnames := make([]interface{}, 0) 255 for hostname := range payload.hostnameToStats { 256 hostnames = append(hostnames, hostname) 257 } 258 259 actualHostnames := mapset.NewSetFromSlice(hostnames) 260 261 suite.Equal(expectedHostnames, actualHostnames, "post-regex hostnames should be processed as expecteds; %s", scheme) 262 } 263 } 264 265 func (suite *StatsTestSuite) Test_getTLSHostname() { 266 // TODO: Create a more robust/antagonistic set of negative tests. 267 // We can write raw TCP to simulate any arbitrary degree of "almost looks 268 // like a TLS handshake". 269 // These tests are basically just checking for crashes. 270 // 271 // An easier way to construct valid client-hello messages (but not malicious ones) 272 // would be to use the clientHelloMsg struct and marshal function from: 273 // https://github.com/golang/go/blob/master/src/crypto/tls/handshake_messages.go 274 275 // TODO: Talk to a local TCP server instead of spamming example.com 276 277 dialer := makeStatsDialer(suite.serverID, nil) 278 279 // Data too short 280 conn, err := dialer("tcp", "example.com:80") 281 suite.Nil(err) 282 b := []byte(`my bytes`) 283 n, err := conn.Write(b) 284 suite.Nil(err) 285 suite.Equal(len(b), n) 286 err = conn.Close() 287 suite.Nil(err) 288 289 // Data long enough, but wrong first byte 290 conn, err = dialer("tcp", "example.com:80") 291 suite.Nil(err) 292 b = []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 293 n, err = conn.Write(b) 294 suite.Nil(err) 295 suite.Equal(len(b), n) 296 err = conn.Close() 297 suite.Nil(err) 298 299 // Data long enough, correct first byte 300 conn, err = dialer("tcp", "example.com:80") 301 suite.Nil(err) 302 b = []byte{22, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 303 n, err = conn.Write(b) 304 suite.Nil(err) 305 suite.Equal(len(b), n) 306 err = conn.Close() 307 suite.Nil(err) 308 309 // Correct until after SSL version 310 conn, err = dialer("tcp", "example.com:80") 311 suite.Nil(err) 312 b = []byte{22, 3, 1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 313 n, err = conn.Write(b) 314 suite.Nil(err) 315 suite.Equal(len(b), n) 316 err = conn.Close() 317 suite.Nil(err) 318 319 plaintextLen := byte(70) 320 321 // Correct until after plaintext length 322 conn, err = dialer("tcp", "example.com:80") 323 suite.Nil(err) 324 b = []byte{22, 3, 1, 0, plaintextLen, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 325 n, err = conn.Write(b) 326 suite.Nil(err) 327 suite.Equal(len(b), n) 328 err = conn.Close() 329 suite.Nil(err) 330 331 // Correct until after handshake type 332 conn, err = dialer("tcp", "example.com:80") 333 suite.Nil(err) 334 b = []byte{22, 3, 1, 0, plaintextLen, 1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 335 n, err = conn.Write(b) 336 suite.Nil(err) 337 suite.Equal(len(b), n) 338 err = conn.Close() 339 suite.Nil(err) 340 341 // Correct until after handshake length 342 conn, err = dialer("tcp", "example.com:80") 343 suite.Nil(err) 344 b = []byte{22, 3, 1, 0, plaintextLen, 1, 0, 0, plaintextLen - 4, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 345 n, err = conn.Write(b) 346 suite.Nil(err) 347 suite.Equal(len(b), n) 348 err = conn.Close() 349 suite.Nil(err) 350 351 // Correct until after protocol version 352 conn, err = dialer("tcp", "example.com:80") 353 suite.Nil(err) 354 b = []byte{22, 3, 1, 0, plaintextLen, 1, 0, 0, plaintextLen - 4, 3, 3, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 355 n, err = conn.Write(b) 356 suite.Nil(err) 357 suite.Equal(len(b), n) 358 err = conn.Close() 359 suite.Nil(err) 360 }