mirror of
https://github.com/karust/openserp.git
synced 2026-08-05 16:53:54 +08:00
355 lines
9.8 KiB
Go
355 lines
9.8 KiB
Go
//go:build integration
|
|
// +build integration
|
|
|
|
package core
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/karust/openserp/core/fpcheck"
|
|
"github.com/karust/openserp/core/fpcheck/detectors"
|
|
"github.com/karust/openserp/testutil"
|
|
)
|
|
|
|
const botFingerprintTestsEnv = "OPENSERP_BOT_TESTS"
|
|
const botFingerprintArtifactDir = "testdata"
|
|
|
|
func TestCreateBrowser(t *testing.T) {
|
|
testutil.RequireIntegration(t)
|
|
|
|
opts := BrowserOpts{IsHeadless: true, IsLeakless: false}
|
|
browser, err := NewBrowser(opts)
|
|
if err != nil {
|
|
t.Fatalf("Error failed initializing browser: %s", err)
|
|
}
|
|
|
|
page, err := browser.Navigate(context.Background(), "about:blank")
|
|
if err != nil {
|
|
t.Fatalf("navigate about:blank: %v", err)
|
|
}
|
|
defer closeTestBrowser(t, browser)
|
|
defer func() {
|
|
if err := ClosePageWithTimeout(context.Background(), page, time.Second); err != nil {
|
|
t.Logf("close page: %v", err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
func TestNavigateUsesIsolatedBrowserContext(t *testing.T) {
|
|
testutil.RequireIntegration(t)
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/cookies/set":
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: "openserp_session",
|
|
Value: "request-a",
|
|
Path: "/",
|
|
})
|
|
_, _ = w.Write([]byte("cookie-set"))
|
|
case "/cookies":
|
|
_, _ = w.Write([]byte(r.Header.Get("Cookie")))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
opts := BrowserOpts{IsHeadless: true, IsLeakless: false, Timeout: 15 * time.Second}
|
|
browser, err := NewBrowser(opts)
|
|
if err != nil {
|
|
t.Fatalf("failed initializing browser: %s", err)
|
|
}
|
|
defer closeTestBrowser(t, browser)
|
|
|
|
pageA, err := browser.Navigate(context.Background(), srv.URL+"/cookies/set")
|
|
if err != nil {
|
|
t.Fatalf("navigate cookie setter: %v", err)
|
|
}
|
|
if err := ClosePageWithTimeout(context.Background(), pageA, time.Second); err != nil {
|
|
t.Fatalf("close setter page: %v", err)
|
|
}
|
|
|
|
pageB, err := browser.Navigate(context.Background(), srv.URL+"/cookies")
|
|
if err != nil {
|
|
t.Fatalf("navigate cookie reader: %v", err)
|
|
}
|
|
defer func() {
|
|
if err := ClosePageWithTimeout(context.Background(), pageB, time.Second); err != nil {
|
|
t.Logf("close reader page: %v", err)
|
|
}
|
|
}()
|
|
|
|
body, err := pageB.Timeout(5 * time.Second).Element("body")
|
|
if err != nil {
|
|
t.Fatalf("read response body: %v", err)
|
|
}
|
|
cookieHeader, err := body.Text()
|
|
if err != nil {
|
|
t.Fatalf("extract response text: %v", err)
|
|
}
|
|
|
|
if strings.Contains(cookieHeader, "openserp_session=request-a") {
|
|
t.Fatalf("cookie leaked between requests; got header %q", cookieHeader)
|
|
}
|
|
}
|
|
|
|
func TestNavigateReusesCookiesForSameProxyLane(t *testing.T) {
|
|
testutil.RequireIntegration(t)
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/cookies/set":
|
|
http.SetCookie(w, &http.Cookie{Name: "openserp_lane", Value: "same-lane", Path: "/"})
|
|
_, _ = w.Write([]byte("cookie-set"))
|
|
case "/cookies":
|
|
_, _ = w.Write([]byte(r.Header.Get("Cookie")))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
browser, err := NewBrowser(BrowserOpts{
|
|
IsHeadless: true,
|
|
IsLeakless: false,
|
|
Timeout: 15 * time.Second,
|
|
ProxyLaneStore: NewLaneStore(10),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("failed initializing browser: %s", err)
|
|
}
|
|
defer closeTestBrowser(t, browser)
|
|
|
|
laneCtx := WithProxyLaneKey(WithEngine(context.Background(), "google"), ProxyLaneKey{Engine: "google", SessionID: "sid-a"})
|
|
pageA, err := browser.Navigate(laneCtx, srv.URL+"/cookies/set")
|
|
if err != nil {
|
|
t.Fatalf("navigate cookie setter: %v", err)
|
|
}
|
|
if err := ClosePageWithTimeout(context.Background(), pageA, time.Second); err != nil {
|
|
t.Fatalf("close setter page: %v", err)
|
|
}
|
|
|
|
pageB, err := browser.Navigate(laneCtx, srv.URL+"/cookies")
|
|
if err != nil {
|
|
t.Fatalf("navigate cookie reader: %v", err)
|
|
}
|
|
defer func() {
|
|
if err := ClosePageWithTimeout(context.Background(), pageB, time.Second); err != nil {
|
|
t.Logf("close reader page: %v", err)
|
|
}
|
|
}()
|
|
|
|
body, err := pageB.Timeout(5 * time.Second).Element("body")
|
|
if err != nil {
|
|
t.Fatalf("read response body: %v", err)
|
|
}
|
|
cookieHeader, err := body.Text()
|
|
if err != nil {
|
|
t.Fatalf("extract response text: %v", err)
|
|
}
|
|
if !strings.Contains(cookieHeader, "openserp_lane=same-lane") {
|
|
t.Fatalf("expected same proxy lane to restore cookie, got %q", cookieHeader)
|
|
}
|
|
}
|
|
|
|
func TestNavigateDoesNotShareCookiesAcrossProxyLanes(t *testing.T) {
|
|
testutil.RequireIntegration(t)
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/cookies/set":
|
|
http.SetCookie(w, &http.Cookie{Name: "openserp_lane", Value: "lane-a", Path: "/"})
|
|
_, _ = w.Write([]byte("cookie-set"))
|
|
case "/cookies":
|
|
_, _ = w.Write([]byte(r.Header.Get("Cookie")))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
browser, err := NewBrowser(BrowserOpts{
|
|
IsHeadless: true,
|
|
IsLeakless: false,
|
|
Timeout: 15 * time.Second,
|
|
ProxyLaneStore: NewLaneStore(10),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("failed initializing browser: %s", err)
|
|
}
|
|
defer closeTestBrowser(t, browser)
|
|
|
|
laneA := WithProxyLaneKey(WithEngine(context.Background(), "google"), ProxyLaneKey{Engine: "google", SessionID: "sid-a"})
|
|
laneB := WithProxyLaneKey(WithEngine(context.Background(), "google"), ProxyLaneKey{Engine: "google", SessionID: "sid-b"})
|
|
pageA, err := browser.Navigate(laneA, srv.URL+"/cookies/set")
|
|
if err != nil {
|
|
t.Fatalf("navigate cookie setter: %v", err)
|
|
}
|
|
if err := ClosePageWithTimeout(context.Background(), pageA, time.Second); err != nil {
|
|
t.Fatalf("close setter page: %v", err)
|
|
}
|
|
|
|
pageB, err := browser.Navigate(laneB, srv.URL+"/cookies")
|
|
if err != nil {
|
|
t.Fatalf("navigate cookie reader: %v", err)
|
|
}
|
|
defer func() {
|
|
if err := ClosePageWithTimeout(context.Background(), pageB, time.Second); err != nil {
|
|
t.Logf("close reader page: %v", err)
|
|
}
|
|
}()
|
|
|
|
body, err := pageB.Timeout(5 * time.Second).Element("body")
|
|
if err != nil {
|
|
t.Fatalf("read response body: %v", err)
|
|
}
|
|
cookieHeader, err := body.Text()
|
|
if err != nil {
|
|
t.Fatalf("extract response text: %v", err)
|
|
}
|
|
if strings.Contains(cookieHeader, "openserp_lane=lane-a") {
|
|
t.Fatalf("cookie leaked across proxy lanes; got header %q", cookieHeader)
|
|
}
|
|
}
|
|
|
|
func TestNavigateClassifiesMainDocumentStatus(t *testing.T) {
|
|
testutil.RequireIntegration(t)
|
|
|
|
tests := []struct {
|
|
name string
|
|
status int
|
|
want error
|
|
}{
|
|
{name: "blocked", status: http.StatusForbidden, want: ErrBlocked},
|
|
{name: "rate limited", status: http.StatusTooManyRequests, want: ErrRateLimited},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(tt.status)
|
|
_, _ = w.Write([]byte("classified"))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
browser, err := NewBrowser(BrowserOpts{IsHeadless: true, IsLeakless: false, Timeout: 15 * time.Second})
|
|
if err != nil {
|
|
t.Fatalf("failed initializing browser: %s", err)
|
|
}
|
|
defer closeTestBrowser(t, browser)
|
|
|
|
page, err := browser.Navigate(context.Background(), srv.URL)
|
|
if page != nil {
|
|
_ = ClosePageWithTimeout(context.Background(), page, time.Second)
|
|
}
|
|
if !errors.Is(err, tt.want) {
|
|
t.Fatalf("expected %v, got %v", tt.want, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFingerprintDetectors(t *testing.T) {
|
|
testutil.RequireIntegration(t)
|
|
if strings.TrimSpace(os.Getenv(botFingerprintTestsEnv)) != "1" {
|
|
t.Skipf("set %s=1 to run fingerprint tests", botFingerprintTestsEnv)
|
|
}
|
|
|
|
reports := make(map[string]fpcheck.Report)
|
|
criticalFailures := make([]string, 0)
|
|
|
|
for _, detector := range detectors.All() {
|
|
report := runFingerprintDetector(t, detector)
|
|
key := detector.Name()
|
|
reports[key] = report
|
|
|
|
if len(report.Summary.Critical) > 0 {
|
|
for _, critical := range report.Summary.Critical {
|
|
criticalFailures = append(criticalFailures, fmt.Sprintf("%s:%s", key, critical))
|
|
}
|
|
}
|
|
}
|
|
|
|
reportPath := filepath.Join(botFingerprintArtifactDir, "fingerprint_report.json")
|
|
if err := writeFingerprintReport(reportPath, reports); err != nil {
|
|
t.Fatalf("write fingerprint report: %v", err)
|
|
}
|
|
absReportPath, err := filepath.Abs(reportPath)
|
|
if err == nil {
|
|
t.Logf("Fingerprint report artifact: %s", absReportPath)
|
|
} else {
|
|
t.Logf("Fingerprint report artifact: core/%s", filepath.ToSlash(reportPath))
|
|
}
|
|
|
|
if len(criticalFailures) > 0 {
|
|
t.Fatalf("critical fingerprint detections found: %s", strings.Join(criticalFailures, ", "))
|
|
}
|
|
}
|
|
|
|
func runFingerprintDetector(t *testing.T, detector fpcheck.Detector) fpcheck.Report {
|
|
t.Helper()
|
|
|
|
opts := BrowserOpts{
|
|
IsHeadless: true,
|
|
IsLeakless: false,
|
|
Timeout: 20 * time.Second,
|
|
}
|
|
browser, err := NewBrowser(opts)
|
|
if err != nil {
|
|
t.Fatalf("create browser: %v", err)
|
|
}
|
|
defer closeTestBrowser(t, browser)
|
|
|
|
report, err := fpcheck.Run(context.Background(), browser, detector, botFingerprintArtifactDir)
|
|
if err != nil {
|
|
t.Fatalf("run detector %s: %v", detector.Name(), err)
|
|
}
|
|
|
|
t.Logf(
|
|
"Fingerprint %s: passed=%d failed=%d critical=%d",
|
|
detector.Name(),
|
|
report.Summary.Passed,
|
|
report.Summary.Failed,
|
|
len(report.Summary.Critical),
|
|
)
|
|
return report
|
|
}
|
|
|
|
func writeFingerprintReport(path string, reports map[string]fpcheck.Report) error {
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
return fmt.Errorf("create artifact directory: %w", err)
|
|
}
|
|
|
|
data, err := json.MarshalIndent(reports, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("marshal report: %w", err)
|
|
}
|
|
data = append(data, '\n')
|
|
|
|
if err := os.WriteFile(path, data, 0o644); err != nil {
|
|
return fmt.Errorf("write report file %s: %w", path, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func closeTestBrowser(t *testing.T, browser *Browser) {
|
|
t.Helper()
|
|
if browser == nil {
|
|
return
|
|
}
|
|
if err := browser.Close(); err != nil {
|
|
t.Logf("close browser: %v", err)
|
|
}
|
|
}
|