github.com/psiphon-labs/psiphon-tunnel-core@v2.0.28+incompatible/psiphon/server/psinet/psinet_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 psinet 21 22 import ( 23 "bytes" 24 "fmt" 25 "io/ioutil" 26 "os" 27 "path/filepath" 28 "strconv" 29 "testing" 30 "time" 31 ) 32 33 func TestDatabase(t *testing.T) { 34 35 testDataDirName, err := ioutil.TempDir("", "psinet-test") 36 if err != nil { 37 t.Fatalf("TempDir failed: %s\n", err) 38 } 39 defer os.RemoveAll(testDataDirName) 40 41 databaseJSON := ` 42 { 43 "sponsors" : { 44 "SPONSOR-ID" : { 45 "id" : "SPONSOR-ID", 46 "home_pages" : { 47 "CLIENT-REGION" : [{ 48 "region" : "CLIENT-REGION", 49 "url" : "HOME-PAGE-URL?client_region=XX" 50 }], 51 "None" : [{ 52 "region" : "None", 53 "url" : "DEFAULT-HOME-PAGE-URL?client_region=XX" 54 }] 55 }, 56 "mobile_home_pages": { 57 "CLIENT-REGION" : [{ 58 "region" : "CLIENT-REGION", 59 "url" : "MOBILE-HOME-PAGE-URL?client_region=XX&client_asn=XX" 60 }], 61 "None" : [{ 62 "region" : "None", 63 "url" : "DEFAULT-MOBILE-HOME-PAGE-URL?client_region=XX&client_asn=XX" 64 }] 65 }, 66 "alert_action_urls" : { 67 "ALERT-REASON-1" : ["SPONSOR-ALERT-1-ACTION-URL?client_region=XX"] 68 }, 69 "https_request_regexes" : [{ 70 "regex" : "REGEX-VALUE", 71 "replace" : "REPLACE-VALUE" 72 }] 73 } 74 }, 75 76 "client_versions" : { 77 "CLIENT-PLATFORM" : [ 78 {"version" : "1"}, 79 {"version" : "2"} 80 ] 81 }, 82 83 "default_sponsor_id" : "SPONSOR-ID", 84 85 "default_alert_action_urls" : { 86 "ALERT-REASON-1" : ["DEFAULT-ALERT-1-ACTION-URL?client_region=XX"], 87 "ALERT-REASON-2" : ["DEFAULT-ALERT-2-ACTION-URL?client_region=XX"] 88 }, 89 90 "valid_server_entry_tags" : { 91 "SERVER-ENTRY-TAG" : true 92 }, 93 94 "discovery_servers" : [ 95 {"discovery_date_range" : ["1900-01-01T00:00:00Z", "2000-01-01T00:00:00Z"], "encoded_server_entry" : "0"}, 96 {"discovery_date_range" : ["1900-01-01T00:00:00Z", "2000-01-01T00:00:00Z"], "encoded_server_entry" : "0"}, 97 {"discovery_date_range" : ["1900-01-01T00:00:00Z", "2000-01-01T00:00:00Z"], "encoded_server_entry" : "0"}, 98 {"discovery_date_range" : ["1900-01-01T00:00:00Z", "2000-01-01T00:00:00Z"], "encoded_server_entry" : "0"}, 99 {"discovery_date_range" : ["2000-01-01T00:00:00Z", "2100-01-01T00:00:00Z"], "encoded_server_entry" : "1"}, 100 {"discovery_date_range" : ["2000-01-01T00:00:00Z", "2100-01-01T00:00:00Z"], "encoded_server_entry" : "1"}, 101 {"discovery_date_range" : ["2000-01-01T00:00:00Z", "2100-01-01T00:00:00Z"], "encoded_server_entry" : "1"}, 102 {"discovery_date_range" : ["2000-01-01T00:00:00Z", "2100-01-01T00:00:00Z"], "encoded_server_entry" : "1"} 103 ] 104 }` 105 106 filename := filepath.Join(testDataDirName, "psinet.json") 107 108 err = ioutil.WriteFile(filename, []byte(databaseJSON), 0600) 109 if err != nil { 110 t.Fatalf("WriteFile failed: %s", err) 111 } 112 113 db, err := NewDatabase(filename) 114 if err != nil { 115 t.Fatalf("NewDatabase failed: %s", err) 116 } 117 118 homePageTestCases := []struct { 119 sponsorID string 120 clientRegion string 121 clientASN string 122 isMobile bool 123 expectedURL string 124 }{ 125 {"SPONSOR-ID", "CLIENT-REGION", "65535", false, "HOME-PAGE-URL?client_region=CLIENT-REGION"}, 126 {"SPONSOR-ID", "UNCONFIGURED-CLIENT-REGION", "65535", false, "DEFAULT-HOME-PAGE-URL?client_region=UNCONFIGURED-CLIENT-REGION"}, 127 {"SPONSOR-ID", "CLIENT-REGION", "65535", true, "MOBILE-HOME-PAGE-URL?client_region=CLIENT-REGION&client_asn=65535"}, 128 {"SPONSOR-ID", "UNCONFIGURED-CLIENT-REGION", "65535", true, "DEFAULT-MOBILE-HOME-PAGE-URL?client_region=UNCONFIGURED-CLIENT-REGION&client_asn=65535"}, 129 {"UNCONFIGURED-SPONSOR-ID", "CLIENT-REGION", "65535", false, "HOME-PAGE-URL?client_region=CLIENT-REGION"}, 130 {"UNCONFIGURED-SPONSOR-ID", "UNCONFIGURED-CLIENT-REGION", "65535", false, "DEFAULT-HOME-PAGE-URL?client_region=UNCONFIGURED-CLIENT-REGION"}, 131 {"UNCONFIGURED-SPONSOR-ID", "CLIENT-REGION", "65535", true, "MOBILE-HOME-PAGE-URL?client_region=CLIENT-REGION&client_asn=65535"}, 132 {"UNCONFIGURED-SPONSOR-ID", "UNCONFIGURED-CLIENT-REGION", "65535", true, "DEFAULT-MOBILE-HOME-PAGE-URL?client_region=UNCONFIGURED-CLIENT-REGION&client_asn=65535"}, 133 } 134 135 for _, testCase := range homePageTestCases { 136 t.Run(fmt.Sprintf("%+v", testCase), func(t *testing.T) { 137 homepages := db.GetHomepages(testCase.sponsorID, testCase.clientRegion, testCase.clientASN, testCase.isMobile) 138 if len(homepages) != 1 || homepages[0] != testCase.expectedURL { 139 t.Fatalf("unexpected home page: %+v", homepages) 140 } 141 }) 142 } 143 144 alertActionURLTestCases := []struct { 145 alertReason string 146 sponsorID string 147 expectedURLCount int 148 expectedURL string 149 }{ 150 {"ALERT-REASON-1", "SPONSOR-ID", 1, "SPONSOR-ALERT-1-ACTION-URL?client_region=CLIENT-REGION"}, 151 {"ALERT-REASON-1", "UNCONFIGURED-SPONSOR-ID", 1, "DEFAULT-ALERT-1-ACTION-URL?client_region=CLIENT-REGION"}, 152 {"ALERT-REASON-2", "SPONSOR-ID", 1, "DEFAULT-ALERT-2-ACTION-URL?client_region=CLIENT-REGION"}, 153 {"ALERT-REASON-2", "UNCONFIGURED-SPONSOR-ID", 1, "DEFAULT-ALERT-2-ACTION-URL?client_region=CLIENT-REGION"}, 154 {"UNCONFIGURED-ALERT-REASON", "SPONSOR-ID", 0, ""}, 155 } 156 157 for _, testCase := range alertActionURLTestCases { 158 t.Run(fmt.Sprintf("%+v", testCase), func(t *testing.T) { 159 URLs := db.GetAlertActionURLs(testCase.alertReason, testCase.sponsorID, "CLIENT-REGION", "") 160 if len(URLs) != testCase.expectedURLCount || (len(URLs) > 0 && URLs[0] != testCase.expectedURL) { 161 t.Fatalf("unexpected URLs: %d %+v, %+v", testCase.expectedURLCount, testCase.expectedURL, URLs) 162 } 163 }) 164 } 165 166 versionTestCases := []struct { 167 currentClientVersion string 168 clientPlatform string 169 expectedUpgradeClientVersion string 170 }{ 171 {"0", "CLIENT-PLATFORM", "2"}, 172 {"1", "CLIENT-PLATFORM", "2"}, 173 {"2", "CLIENT-PLATFORM", ""}, 174 {"3", "CLIENT-PLATFORM", ""}, 175 {"2", "UNCONFIGURED-CLIENT-PLATFORM", ""}, 176 } 177 178 for _, testCase := range versionTestCases { 179 t.Run(fmt.Sprintf("%+v", testCase), func(t *testing.T) { 180 upgradeVersion := db.GetUpgradeClientVersion(testCase.currentClientVersion, testCase.clientPlatform) 181 if upgradeVersion != testCase.expectedUpgradeClientVersion { 182 t.Fatalf("unexpected upgrade version: %s", upgradeVersion) 183 } 184 }) 185 } 186 187 httpsRegexTestCases := []struct { 188 sponsorID string 189 expectedRegexValue string 190 expectedReplaceValue string 191 }{ 192 {"SPONSOR-ID", "REGEX-VALUE", "REPLACE-VALUE"}, 193 {"UNCONFIGURED-SPONSOR-ID", "REGEX-VALUE", "REPLACE-VALUE"}, 194 } 195 196 for _, testCase := range httpsRegexTestCases { 197 t.Run(fmt.Sprintf("%+v", testCase), func(t *testing.T) { 198 regexes, checksum := db.GetHttpsRequestRegexes(testCase.sponsorID) 199 if !bytes.Equal(checksum, db.GetDomainBytesChecksum(testCase.sponsorID)) { 200 t.Fatalf("unexpected checksum: %+v", checksum) 201 } 202 var regexValue, replaceValue string 203 ok := false 204 if len(regexes) == 1 && len(regexes[0]) == 2 { 205 regexValue, ok = regexes[0]["regex"] 206 if ok { 207 replaceValue, ok = regexes[0]["replace"] 208 } 209 } 210 if !ok || regexValue != testCase.expectedRegexValue || replaceValue != testCase.expectedReplaceValue { 211 t.Fatalf("unexpected regexes: %+v", regexes) 212 } 213 }) 214 } 215 216 for i := 0; i < 1000; i++ { 217 encodedServerEntries := db.DiscoverServers(i) 218 if len(encodedServerEntries) != 1 || encodedServerEntries[0] != "1" { 219 t.Fatalf("unexpected discovery server list: %+v", encodedServerEntries) 220 } 221 } 222 223 if !db.IsValidServerEntryTag("SERVER-ENTRY-TAG") { 224 t.Fatalf("unexpected invalid server entry tag") 225 } 226 227 if db.IsValidServerEntryTag("INVALID-SERVER-ENTRY-TAG") { 228 t.Fatalf("unexpected valid server entry tag") 229 } 230 } 231 232 func TestDiscoveryBuckets(t *testing.T) { 233 234 checkBuckets := func(buckets [][]*DiscoveryServer, expectedServerEntries [][]int) { 235 if len(buckets) != len(expectedServerEntries) { 236 t.Errorf( 237 "unexpected bucket count: got %d expected %d", 238 len(buckets), len(expectedServerEntries)) 239 return 240 } 241 for i := 0; i < len(buckets); i++ { 242 if len(buckets[i]) != len(expectedServerEntries[i]) { 243 t.Errorf( 244 "unexpected bucket %d size: got %d expected %d", 245 i, len(buckets[i]), len(expectedServerEntries[i])) 246 return 247 } 248 for j := 0; j < len(buckets[i]); j++ { 249 expectedServerEntry := strconv.Itoa(expectedServerEntries[i][j]) 250 if buckets[i][j].EncodedServerEntry != expectedServerEntry { 251 t.Errorf( 252 "unexpected bucket %d item %d: got %s expected %s", 253 i, j, buckets[i][j].EncodedServerEntry, expectedServerEntry) 254 return 255 } 256 } 257 } 258 } 259 260 // Partition test cases from: 261 // http://stackoverflow.com/questions/2659900/python-slicing-a-list-into-n-nearly-equal-length-partitions 262 263 servers := make([]*DiscoveryServer, 0) 264 for i := 0; i < 105; i++ { 265 servers = append(servers, &DiscoveryServer{EncodedServerEntry: strconv.Itoa(i)}) 266 } 267 268 t.Run("5 servers, 5 buckets", func(t *testing.T) { 269 checkBuckets( 270 bucketizeServerList(servers[0:5], 5), 271 [][]int{{0}, {1}, {2}, {3}, {4}}) 272 }) 273 274 t.Run("5 servers, 2 buckets", func(t *testing.T) { 275 checkBuckets( 276 bucketizeServerList(servers[0:5], 2), 277 [][]int{{0, 1, 2}, {3, 4}}) 278 }) 279 280 t.Run("5 servers, 3 buckets", func(t *testing.T) { 281 checkBuckets( 282 bucketizeServerList(servers[0:5], 3), 283 [][]int{{0, 1}, {2}, {3, 4}}) 284 }) 285 286 t.Run("105 servers, 10 buckets", func(t *testing.T) { 287 checkBuckets( 288 bucketizeServerList(servers, 10), 289 [][]int{ 290 {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, 291 {11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, 292 {21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}, 293 {32, 33, 34, 35, 36, 37, 38, 39, 40, 41}, 294 {42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52}, 295 {53, 54, 55, 56, 57, 58, 59, 60, 61, 62}, 296 {63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73}, 297 {74, 75, 76, 77, 78, 79, 80, 81, 82, 83}, 298 {84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94}, 299 {95, 96, 97, 98, 99, 100, 101, 102, 103, 104}, 300 }) 301 }) 302 303 t.Run("repeatedly discover with fixed IP address", func(t *testing.T) { 304 305 // For a IP address values, only one bucket should be used; with enough 306 // iterations, all and only the items in a single bucket should be discovered. 307 308 discoveredServers := make(map[string]bool) 309 310 // discoveryValue is derived from the client's IP address and indexes the bucket; 311 // a value of 0 always maps to the first bucket. 312 discoveryValue := 0 313 314 for i := 0; i < 1000; i++ { 315 for _, server := range selectServers(servers, i*int(time.Hour/time.Second), discoveryValue) { 316 discoveredServers[server.EncodedServerEntry] = true 317 } 318 } 319 320 bucketCount := calculateBucketCount(len(servers)) 321 322 buckets := bucketizeServerList(servers, bucketCount) 323 324 if len(buckets[0]) != len(discoveredServers) { 325 t.Errorf( 326 "unexpected discovered server count: got %d expected %d", 327 len(discoveredServers), len(buckets[0])) 328 return 329 } 330 331 for _, bucketServer := range buckets[0] { 332 if _, ok := discoveredServers[bucketServer.EncodedServerEntry]; !ok { 333 t.Errorf("unexpected missing discovery server: %s", bucketServer.EncodedServerEntry) 334 return 335 } 336 } 337 }) 338 339 }