package main import ( "context" "io" "net" "net/http" "net/http/httptest" "os" "os/exec" "path/filepath" "strings" "sync" "syscall" "testing" "time" "git.ipao.vip/rogee/creator-hub/internal/douyin" ) func TestCDPBrowserRealNavigationIsolation(t *testing.T) { if os.Getenv("CREATORHUB_REAL_CDP_TEST") != "1" { t.Skip("set CREATORHUB_REAL_CDP_TEST=1 to run against local Chrome") } chrome, err := exec.LookPath("google-chrome") if err != nil { t.Skip("google-chrome is unavailable") } var mu sync.Mutex phase := "redirect" release := make(chan struct{}) mainRequests := make(chan string, 4) loginRequests := make(chan string, 1) subdomainRequests := make(chan string, 1) server := httptest.NewTLSServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { host := request.Host if name, _, splitErr := net.SplitHostPort(host); splitErr == nil { host = name } switch host { case "login.douyin.com": loginRequests <- request.Header.Get("Cookie") _, _ = response.Write([]byte("redirected")) case "api.douyin.com": subdomainRequests <- request.Header.Get("Cookie") _, _ = response.Write([]byte("ok")) case "www.douyin.com": if request.URL.Path == "/" { mainRequests <- request.Header.Get("Cookie") } mu.Lock() currentPhase, currentRelease := phase, release mu.Unlock() if currentPhase == "redirect" { http.Redirect(response, request, "https://login.douyin.com/landing", http.StatusFound) return } if currentPhase == "blocked" { <-currentRelease } response.Header().Set("Content-Type", "text/html") _, _ = response.Write([]byte(``)) default: response.WriteHeader(http.StatusNotFound) } })) t.Cleanup(server.Close) allowedHosts := map[string]bool{"www.douyin.com": true, "login.douyin.com": true, "api.douyin.com": true} proxy := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { host := request.Host if name, _, splitErr := net.SplitHostPort(host); splitErr == nil { host = name } if request.Method != http.MethodConnect || !allowedHosts[host] { response.WriteHeader(http.StatusForbidden) return } upstream, dialErr := net.Dial("tcp", server.Listener.Addr().String()) if dialErr != nil { response.WriteHeader(http.StatusBadGateway) return } client, _, hijackErr := response.(http.Hijacker).Hijack() if hijackErr != nil { upstream.Close() return } _, _ = client.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n")) go func() { _, _ = io.Copy(upstream, client) _ = upstream.Close() }() _, _ = io.Copy(client, upstream) _ = client.Close() })) t.Cleanup(proxy.Close) profile := t.TempDir() command := exec.Command(chrome, "--headless=new", "--no-sandbox", "--disable-gpu", "--disable-background-networking", "--disable-quic", "--disable-dev-shm-usage", "--no-first-run", "--no-default-browser-check", "--password-store=basic", "--use-mock-keychain", "--ignore-certificate-errors", "--proxy-server="+proxy.URL, "--remote-debugging-address=127.0.0.1", "--remote-debugging-port=0", "--remote-allow-origins=*", "--user-data-dir="+profile, "about:blank") command.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} if err := command.Start(); err != nil { t.Fatal(err) } t.Cleanup(func() { _ = syscall.Kill(-command.Process.Pid, syscall.SIGKILL) _ = command.Wait() }) var debugPort string for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(25 * time.Millisecond) { content, readErr := os.ReadFile(filepath.Join(profile, "DevToolsActivePort")) if readErr == nil { debugPort = strings.SplitN(string(content), "\n", 2)[0] break } } if debugPort == "" { t.Fatal("Chrome did not expose a DevTools port") } time.Sleep(250 * time.Millisecond) browser := cdpBrowser{endpoint: func(string) string { return "http://127.0.0.1:" + debugPort }} cookie := []douyin.Cookie{{Name: "sessionid", Value: "fake-secret", Domain: ".douyin.com", Path: "/", Secure: true}} ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) redirectErr := browser.SetCookies(ctx, "account-a", cookie) if redirectErr == nil { cancel() t.Fatal("accepted a redirected navigation") } cancel() select { case got := <-loginRequests: if strings.Contains(got, "fake-secret") { t.Fatal("cookie leaked during redirected navigation") } case <-time.After(3 * time.Second): t.Fatalf("redirect target was not reached: %v", redirectErr) } mu.Lock() phase = "success" mu.Unlock() ctx, cancel = context.WithTimeout(context.Background(), 15*time.Second) if err := browser.SetCookies(ctx, "account-a", cookie); err != nil { cancel() t.Fatal(err) } cancel() select { case <-mainRequests: case <-time.After(3 * time.Second): t.Fatal("successful navigation was not observed") } mu.Lock() phase = "blocked" release = make(chan struct{}) currentRelease := release mu.Unlock() defer func() { select { case <-currentRelease: default: close(currentRelease) } }() done := make(chan error, 1) ctx, cancel = context.WithTimeout(context.Background(), 15*time.Second) go func() { done <- browser.SetCookies(ctx, "account-a", cookie) }() select { case got := <-mainRequests: if strings.Contains(got, "fake-secret") { cancel() t.Fatal("cookie leaked during navigation") } case <-time.After(3 * time.Second): cancel() t.Fatal("blocked navigation was not observed") } select { case err := <-done: cancel() t.Fatalf("navigation completed before its loader: %v", err) case <-time.After(100 * time.Millisecond): } close(currentRelease) if err := <-done; err != nil { cancel() t.Fatal(err) } cancel() select { case got := <-subdomainRequests: if strings.Contains(got, "fake-secret") { t.Fatal("host-only cookie leaked to a subdomain") } case <-time.After(3 * time.Second): t.Fatal("subdomain probe did not run") } }