1package engine 2 3import ( 4 "context" 5 "errors" 6 "io/ioutil" 7 "net/http" 8 "net/http/httptest" 9 "net/url" 10 "os" 11 "syscall" 12 "testing" 13 "time" 14 15 "github.com/apex/log" 16 "github.com/google/go-cmp/cmp" 17 "github.com/ooni/probe-engine/geolocate" 18 "github.com/ooni/probe-engine/model" 19 "github.com/ooni/probe-engine/netx" 20 "github.com/ooni/probe-engine/probeservices" 21 "github.com/ooni/probe-engine/version" 22) 23 24func TestNewSessionBuilderChecks(t *testing.T) { 25 if testing.Short() { 26 t.Skip("skip test in short mode") 27 } 28 t.Run("with no settings", func(t *testing.T) { 29 newSessionMustFail(t, SessionConfig{}) 30 }) 31 t.Run("with only assets dir", func(t *testing.T) { 32 newSessionMustFail(t, SessionConfig{ 33 AssetsDir: "testdata", 34 }) 35 }) 36 t.Run("with also logger", func(t *testing.T) { 37 newSessionMustFail(t, SessionConfig{ 38 AssetsDir: "testdata", 39 Logger: model.DiscardLogger, 40 }) 41 }) 42 t.Run("with also software name", func(t *testing.T) { 43 newSessionMustFail(t, SessionConfig{ 44 AssetsDir: "testdata", 45 Logger: model.DiscardLogger, 46 SoftwareName: "ooniprobe-engine", 47 }) 48 }) 49 t.Run("with software version and wrong tempdir", func(t *testing.T) { 50 newSessionMustFail(t, SessionConfig{ 51 AssetsDir: "testdata", 52 Logger: model.DiscardLogger, 53 SoftwareName: "ooniprobe-engine", 54 SoftwareVersion: "0.0.1", 55 TempDir: "./nonexistent", 56 }) 57 }) 58} 59 60func TestNewSessionBuilderGood(t *testing.T) { 61 if testing.Short() { 62 t.Skip("skip test in short mode") 63 } 64 newSessionForTesting(t) 65} 66 67func newSessionMustFail(t *testing.T, config SessionConfig) { 68 sess, err := NewSession(config) 69 if err == nil { 70 t.Fatal("expected an error here") 71 } 72 if sess != nil { 73 t.Fatal("expected nil session here") 74 } 75} 76 77func TestSessionTorArgsTorBinary(t *testing.T) { 78 if testing.Short() { 79 t.Skip("skip test in short mode") 80 } 81 sess, err := NewSession(SessionConfig{ 82 AssetsDir: "testdata", 83 AvailableProbeServices: []model.Service{{ 84 Address: "https://ams-pg-test.ooni.org", 85 Type: "https", 86 }}, 87 Logger: model.DiscardLogger, 88 SoftwareName: "ooniprobe-engine", 89 SoftwareVersion: "0.0.1", 90 TorArgs: []string{"antani1", "antani2", "antani3"}, 91 TorBinary: "mascetti", 92 }) 93 if err != nil { 94 t.Fatal(err) 95 } 96 if sess.TorBinary() != "mascetti" { 97 t.Fatal("not the TorBinary we expected") 98 } 99 if len(sess.TorArgs()) != 3 { 100 t.Fatal("not the TorArgs length we expected") 101 } 102 if sess.TorArgs()[0] != "antani1" { 103 t.Fatal("not the TorArgs[0] we expected") 104 } 105 if sess.TorArgs()[1] != "antani2" { 106 t.Fatal("not the TorArgs[1] we expected") 107 } 108 if sess.TorArgs()[2] != "antani3" { 109 t.Fatal("not the TorArgs[2] we expected") 110 } 111} 112 113func newSessionForTestingNoLookupsWithProxyURL(t *testing.T, URL *url.URL) *Session { 114 sess, err := NewSession(SessionConfig{ 115 AssetsDir: "testdata", 116 AvailableProbeServices: []model.Service{{ 117 Address: "https://ams-pg-test.ooni.org", 118 Type: "https", 119 }}, 120 Logger: model.DiscardLogger, 121 ProxyURL: URL, 122 SoftwareName: "ooniprobe-engine", 123 SoftwareVersion: "0.0.1", 124 }) 125 if err != nil { 126 t.Fatal(err) 127 } 128 return sess 129} 130 131func newSessionForTestingNoLookups(t *testing.T) *Session { 132 return newSessionForTestingNoLookupsWithProxyURL(t, nil) 133} 134 135func newSessionForTestingNoBackendsLookup(t *testing.T) *Session { 136 sess := newSessionForTestingNoLookups(t) 137 if err := sess.MaybeLookupLocation(); err != nil { 138 t.Fatal(err) 139 } 140 log.Infof("Platform: %s", sess.Platform()) 141 log.Infof("ProbeASN: %d", sess.ProbeASN()) 142 log.Infof("ProbeASNString: %s", sess.ProbeASNString()) 143 log.Infof("ProbeCC: %s", sess.ProbeCC()) 144 log.Infof("ProbeIP: %s", sess.ProbeIP()) 145 log.Infof("ProbeNetworkName: %s", sess.ProbeNetworkName()) 146 log.Infof("ResolverASN: %d", sess.ResolverASN()) 147 log.Infof("ResolverASNString: %s", sess.ResolverASNString()) 148 log.Infof("ResolverIP: %s", sess.ResolverIP()) 149 log.Infof("ResolverNetworkName: %s", sess.ResolverNetworkName()) 150 return sess 151} 152 153func newSessionForTesting(t *testing.T) *Session { 154 sess := newSessionForTestingNoBackendsLookup(t) 155 if err := sess.MaybeLookupBackends(); err != nil { 156 t.Fatal(err) 157 } 158 return sess 159} 160 161func TestNewOrchestraClient(t *testing.T) { 162 if testing.Short() { 163 t.Skip("skip test in short mode") 164 } 165 sess := newSessionForTestingNoLookups(t) 166 defer sess.Close() 167 clnt, err := sess.NewOrchestraClient(context.Background()) 168 if err != nil { 169 t.Fatal(err) 170 } 171 if clnt == nil { 172 t.Fatal("expected non nil client here") 173 } 174} 175 176func TestInitOrchestraClientMaybeRegisterError(t *testing.T) { 177 if testing.Short() { 178 t.Skip("skip test in short mode") 179 } 180 ctx, cancel := context.WithCancel(context.Background()) 181 cancel() // so we fail immediately 182 sess := newSessionForTestingNoLookups(t) 183 defer sess.Close() 184 clnt, err := probeservices.NewClient(sess, model.Service{ 185 Address: "https://ams-pg-test.ooni.org/", 186 Type: "https", 187 }) 188 if err != nil { 189 t.Fatal(err) 190 } 191 outclnt, err := sess.initOrchestraClient( 192 ctx, clnt, clnt.MaybeLogin, 193 ) 194 if !errors.Is(err, context.Canceled) { 195 t.Fatal("not the error we expected") 196 } 197 if outclnt != nil { 198 t.Fatal("expected a nil client here") 199 } 200} 201 202func TestInitOrchestraClientMaybeLoginError(t *testing.T) { 203 if testing.Short() { 204 t.Skip("skip test in short mode") 205 } 206 ctx := context.Background() 207 sess := newSessionForTestingNoLookups(t) 208 defer sess.Close() 209 clnt, err := probeservices.NewClient(sess, model.Service{ 210 Address: "https://ams-pg-test.ooni.org/", 211 Type: "https", 212 }) 213 if err != nil { 214 t.Fatal(err) 215 } 216 expected := errors.New("mocked error") 217 outclnt, err := sess.initOrchestraClient( 218 ctx, clnt, func(context.Context) error { 219 return expected 220 }, 221 ) 222 if !errors.Is(err, expected) { 223 t.Fatal("not the error we expected") 224 } 225 if outclnt != nil { 226 t.Fatal("expected a nil client here") 227 } 228} 229 230func TestBouncerError(t *testing.T) { 231 if testing.Short() { 232 t.Skip("skip test in short mode") 233 } 234 // Combine proxy testing with a broken proxy with errors 235 // in reaching out to the bouncer. 236 server := httptest.NewServer(http.HandlerFunc( 237 func(w http.ResponseWriter, r *http.Request) { 238 w.WriteHeader(500) 239 }, 240 )) 241 defer server.Close() 242 URL, err := url.Parse(server.URL) 243 if err != nil { 244 t.Fatal(err) 245 } 246 sess := newSessionForTestingNoLookupsWithProxyURL(t, URL) 247 defer sess.Close() 248 if sess.ProxyURL() == nil { 249 t.Fatal("expected to see explicit proxy here") 250 } 251 if err := sess.MaybeLookupBackends(); err == nil { 252 t.Fatal("expected an error here") 253 } 254} 255 256func TestMaybeLookupBackendsNewClientError(t *testing.T) { 257 if testing.Short() { 258 t.Skip("skip test in short mode") 259 } 260 sess := newSessionForTestingNoLookups(t) 261 sess.availableProbeServices = []model.Service{{ 262 Type: "onion", 263 Address: "httpo://jehhrikjjqrlpufu.onion", 264 }} 265 defer sess.Close() 266 err := sess.MaybeLookupBackends() 267 if !errors.Is(err, ErrAllProbeServicesFailed) { 268 t.Fatal("not the error we expected") 269 } 270} 271 272func TestSessionLocationLookup(t *testing.T) { 273 if testing.Short() { 274 t.Skip("skip test in short mode") 275 } 276 sess := newSessionForTestingNoLookups(t) 277 defer sess.Close() 278 if err := sess.MaybeLookupLocation(); err != nil { 279 t.Fatal(err) 280 } 281 if sess.ProbeASNString() == geolocate.DefaultProbeASNString { 282 t.Fatal("unexpected ProbeASNString") 283 } 284 if sess.ProbeASN() == geolocate.DefaultProbeASN { 285 t.Fatal("unexpected ProbeASN") 286 } 287 if sess.ProbeCC() == geolocate.DefaultProbeCC { 288 t.Fatal("unexpected ProbeCC") 289 } 290 if sess.ProbeIP() == geolocate.DefaultProbeIP { 291 t.Fatal("unexpected ProbeIP") 292 } 293 if sess.ProbeNetworkName() == geolocate.DefaultProbeNetworkName { 294 t.Fatal("unexpected ProbeNetworkName") 295 } 296 if sess.ResolverASN() == geolocate.DefaultResolverASN { 297 t.Fatal("unexpected ResolverASN") 298 } 299 if sess.ResolverASNString() == geolocate.DefaultResolverASNString { 300 t.Fatal("unexpected ResolverASNString") 301 } 302 if sess.ResolverIP() == geolocate.DefaultResolverIP { 303 t.Fatal("unexpected ResolverIP") 304 } 305 if sess.ResolverNetworkName() == geolocate.DefaultResolverNetworkName { 306 t.Fatal("unexpected ResolverNetworkName") 307 } 308 if sess.KibiBytesSent() <= 0 { 309 t.Fatal("unexpected KibiBytesSent") 310 } 311 if sess.KibiBytesReceived() <= 0 { 312 t.Fatal("unexpected KibiBytesReceived") 313 } 314} 315 316func TestSessionCloseCancelsTempDir(t *testing.T) { 317 if testing.Short() { 318 t.Skip("skip test in short mode") 319 } 320 sess := newSessionForTestingNoLookups(t) 321 tempDir := sess.TempDir() 322 if _, err := os.Stat(tempDir); err != nil { 323 t.Fatal(err) 324 } 325 if err := sess.Close(); err != nil { 326 t.Fatal(err) 327 } 328 if _, err := os.Stat(tempDir); !errors.Is(err, syscall.ENOENT) { 329 t.Fatal("not the error we expected") 330 } 331} 332 333func TestSessionDownloadResources(t *testing.T) { 334 if testing.Short() { 335 t.Skip("skip test in short mode") 336 } 337 tmpdir, err := ioutil.TempDir("", "test-download-resources-idempotent") 338 if err != nil { 339 t.Fatal(err) 340 } 341 ctx := context.Background() 342 sess := newSessionForTestingNoLookups(t) 343 defer sess.Close() 344 sess.SetAssetsDir(tmpdir) 345 err = sess.MaybeUpdateResources(ctx) 346 if err != nil { 347 t.Fatal(err) 348 } 349 readfile := func(path string) (err error) { 350 _, err = ioutil.ReadFile(path) 351 return 352 } 353 if err := readfile(sess.ASNDatabasePath()); err != nil { 354 t.Fatal(err) 355 } 356 if err := readfile(sess.CountryDatabasePath()); err != nil { 357 t.Fatal(err) 358 } 359} 360 361func TestGetAvailableProbeServices(t *testing.T) { 362 if testing.Short() { 363 t.Skip("skip test in short mode") 364 } 365 sess, err := NewSession(SessionConfig{ 366 AssetsDir: "testdata", 367 Logger: model.DiscardLogger, 368 SoftwareName: "ooniprobe-engine", 369 SoftwareVersion: "0.0.1", 370 }) 371 if err != nil { 372 t.Fatal(err) 373 } 374 defer sess.Close() 375 all := sess.GetAvailableProbeServices() 376 diff := cmp.Diff(all, probeservices.Default()) 377 if diff != "" { 378 t.Fatal(diff) 379 } 380} 381 382func TestMaybeLookupBackendsFailure(t *testing.T) { 383 if testing.Short() { 384 t.Skip("skip test in short mode") 385 } 386 sess, err := NewSession(SessionConfig{ 387 AssetsDir: "testdata", 388 Logger: model.DiscardLogger, 389 SoftwareName: "ooniprobe-engine", 390 SoftwareVersion: "0.0.1", 391 }) 392 if err != nil { 393 t.Fatal(err) 394 } 395 defer sess.Close() 396 ctx, cancel := context.WithCancel(context.Background()) 397 cancel() // so we fail immediately 398 err = sess.MaybeLookupBackendsContext(ctx) 399 if !errors.Is(err, ErrAllProbeServicesFailed) { 400 t.Fatal("unexpected error") 401 } 402} 403 404func TestMaybeLookupTestHelpersIdempotent(t *testing.T) { 405 if testing.Short() { 406 t.Skip("skip test in short mode") 407 } 408 sess, err := NewSession(SessionConfig{ 409 AssetsDir: "testdata", 410 Logger: model.DiscardLogger, 411 SoftwareName: "ooniprobe-engine", 412 SoftwareVersion: "0.0.1", 413 }) 414 if err != nil { 415 t.Fatal(err) 416 } 417 defer sess.Close() 418 ctx := context.Background() 419 if err = sess.MaybeLookupBackendsContext(ctx); err != nil { 420 t.Fatal(err) 421 } 422 if err = sess.MaybeLookupBackendsContext(ctx); err != nil { 423 t.Fatal(err) 424 } 425 if sess.QueryProbeServicesCount() != 1 { 426 t.Fatal("unexpected number of queries sent to the bouncer") 427 } 428} 429 430func TestAllProbeServicesUnsupported(t *testing.T) { 431 if testing.Short() { 432 t.Skip("skip test in short mode") 433 } 434 sess, err := NewSession(SessionConfig{ 435 AssetsDir: "testdata", 436 Logger: model.DiscardLogger, 437 SoftwareName: "ooniprobe-engine", 438 SoftwareVersion: "0.0.1", 439 }) 440 if err != nil { 441 t.Fatal(err) 442 } 443 defer sess.Close() 444 sess.AppendAvailableProbeService(model.Service{ 445 Address: "mascetti", 446 Type: "antani", 447 }) 448 err = sess.MaybeLookupBackends() 449 if !errors.Is(err, ErrAllProbeServicesFailed) { 450 t.Fatal("unexpected error") 451 } 452} 453 454func TestStartTunnelGood(t *testing.T) { 455 if testing.Short() { 456 t.Skip("skip test in short mode") 457 } 458 sess := newSessionForTestingNoLookups(t) 459 defer sess.Close() 460 ctx := context.Background() 461 if err := sess.MaybeStartTunnel(ctx, "psiphon"); err != nil { 462 t.Fatal(err) 463 } 464 if err := sess.MaybeStartTunnel(ctx, "psiphon"); err != nil { 465 t.Fatal(err) // check twice, must be idempotent 466 } 467 if sess.ProxyURL() == nil { 468 t.Fatal("expected non-nil ProxyURL") 469 } 470} 471 472func TestStartTunnelNonexistent(t *testing.T) { 473 if testing.Short() { 474 t.Skip("skip test in short mode") 475 } 476 sess := newSessionForTestingNoLookups(t) 477 defer sess.Close() 478 ctx := context.Background() 479 if err := sess.MaybeStartTunnel(ctx, "antani"); err.Error() != "unsupported tunnel" { 480 t.Fatal("not the error we expected") 481 } 482 if sess.ProxyURL() != nil { 483 t.Fatal("expected nil ProxyURL") 484 } 485} 486 487func TestStartTunnelEmptyString(t *testing.T) { 488 if testing.Short() { 489 t.Skip("skip test in short mode") 490 } 491 sess := newSessionForTestingNoLookups(t) 492 defer sess.Close() 493 ctx := context.Background() 494 if sess.MaybeStartTunnel(ctx, "") != nil { 495 t.Fatal("expected no error here") 496 } 497 if sess.ProxyURL() != nil { 498 t.Fatal("expected nil ProxyURL") 499 } 500} 501 502func TestStartTunnelEmptyStringWithProxy(t *testing.T) { 503 if testing.Short() { 504 t.Skip("skip test in short mode") 505 } 506 proxyURL := &url.URL{Scheme: "socks5", Host: "127.0.0.1:9050"} 507 sess := newSessionForTestingNoLookups(t) 508 sess.proxyURL = proxyURL 509 defer sess.Close() 510 ctx := context.Background() 511 if sess.MaybeStartTunnel(ctx, "") != nil { 512 t.Fatal("expected no error here") 513 } 514 diff := cmp.Diff(proxyURL, sess.ProxyURL()) 515 if diff != "" { 516 t.Fatal(diff) 517 } 518} 519 520func TestStartTunnelWithAlreadyExistingTunnel(t *testing.T) { 521 if testing.Short() { 522 t.Skip("skip test in short mode") 523 } 524 sess := newSessionForTestingNoLookups(t) 525 defer sess.Close() 526 ctx := context.Background() 527 if sess.MaybeStartTunnel(ctx, "psiphon") != nil { 528 t.Fatal("expected no error here") 529 } 530 prev := sess.ProxyURL() 531 err := sess.MaybeStartTunnel(ctx, "tor") 532 if !errors.Is(err, ErrAlreadyUsingProxy) { 533 t.Fatal("expected another error here") 534 } 535 cur := sess.ProxyURL() 536 diff := cmp.Diff(prev, cur) 537 if diff != "" { 538 t.Fatal(diff) 539 } 540} 541 542func TestStartTunnelWithAlreadyExistingProxy(t *testing.T) { 543 if testing.Short() { 544 t.Skip("skip test in short mode") 545 } 546 sess := newSessionForTestingNoLookups(t) 547 defer sess.Close() 548 ctx := context.Background() 549 orig := &url.URL{Scheme: "socks5", Host: "[::1]:9050"} 550 sess.proxyURL = orig 551 err := sess.MaybeStartTunnel(ctx, "psiphon") 552 if !errors.Is(err, ErrAlreadyUsingProxy) { 553 t.Fatal("expected another error here") 554 } 555 cur := sess.ProxyURL() 556 diff := cmp.Diff(orig, cur) 557 if diff != "" { 558 t.Fatal(diff) 559 } 560} 561 562func TestStartTunnelCanceledContext(t *testing.T) { 563 if testing.Short() { 564 t.Skip("skip test in short mode") 565 } 566 sess := newSessionForTestingNoLookups(t) 567 defer sess.Close() 568 ctx, cancel := context.WithCancel(context.Background()) 569 cancel() // immediately cancel 570 err := sess.MaybeStartTunnel(ctx, "psiphon") 571 if !errors.Is(err, context.Canceled) { 572 t.Fatal("not the error we expected") 573 } 574} 575 576func TestUserAgentNoProxy(t *testing.T) { 577 if testing.Short() { 578 t.Skip("skip test in short mode") 579 } 580 expect := "ooniprobe-engine/0.0.1 ooniprobe-engine/" + version.Version 581 sess := newSessionForTestingNoLookups(t) 582 ua := sess.UserAgent() 583 diff := cmp.Diff(expect, ua) 584 if diff != "" { 585 t.Fatal(diff) 586 } 587} 588 589func TestNewOrchestraClientMaybeLookupBackendsFailure(t *testing.T) { 590 if testing.Short() { 591 t.Skip("skip test in short mode") 592 } 593 sess := newSessionForTestingNoLookups(t) 594 ctx, cancel := context.WithCancel(context.Background()) 595 cancel() // fail immediately 596 client, err := sess.NewOrchestraClient(ctx) 597 if !errors.Is(err, ErrAllProbeServicesFailed) { 598 t.Fatal("not the error we expected") 599 } 600 if client != nil { 601 t.Fatal("expected nil client here") 602 } 603} 604 605type httpTransportThatSleeps struct { 606 txp netx.HTTPRoundTripper 607 st time.Duration 608} 609 610func (txp httpTransportThatSleeps) RoundTrip(req *http.Request) (*http.Response, error) { 611 resp, err := txp.txp.RoundTrip(req) 612 time.Sleep(txp.st) 613 return resp, err 614} 615 616func (txp httpTransportThatSleeps) CloseIdleConnections() { 617 txp.txp.CloseIdleConnections() 618} 619 620func TestNewOrchestraClientMaybeLookupLocationFailure(t *testing.T) { 621 if testing.Short() { 622 t.Skip("skip test in short mode") 623 } 624 sess := newSessionForTestingNoLookups(t) 625 sess.httpDefaultTransport = httpTransportThatSleeps{ 626 txp: sess.httpDefaultTransport, 627 st: 5 * time.Second, 628 } 629 // The transport sleeps for five seconds, so the context should be expired by 630 // the time in which we attempt at looking up the location. Because the 631 // implementation performs the round-trip and _then_ sleeps, it means we'll 632 // see the context expired error when performing the location lookup. 633 ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second) 634 defer cancel() 635 client, err := sess.NewOrchestraClient(ctx) 636 if !errors.Is(err, geolocate.ErrAllIPLookuppersFailed) { 637 t.Fatalf("not the error we expected: %+v", err) 638 } 639 if client != nil { 640 t.Fatal("expected nil client here") 641 } 642} 643 644func TestNewOrchestraClientProbeServicesNewClientFailure(t *testing.T) { 645 if testing.Short() { 646 t.Skip("skip test in short mode") 647 } 648 sess := newSessionForTestingNoLookups(t) 649 sess.selectedProbeServiceHook = func(svc *model.Service) { 650 svc.Type = "antani" // should really not be supported for a long time 651 } 652 client, err := sess.NewOrchestraClient(context.Background()) 653 if !errors.Is(err, probeservices.ErrUnsupportedEndpoint) { 654 t.Fatal("not the error we expected") 655 } 656 if client != nil { 657 t.Fatal("expected nil client here") 658 } 659} 660