206 lines
6.0 KiB
Go
206 lines
6.0 KiB
Go
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(`<script>addEventListener("load",()=>setTimeout(()=>fetch("https://api.douyin.com/probe"),300))</script>`))
|
|
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")
|
|
}
|
|
}
|