Skip to content

Commit fa50e42

Browse files
committed
internal/test: run tests iff npx exists and DONT_USE_NETWORK is not set
Signed-off-by: Xe Iaso <me@xeiaso.net>
1 parent b78d0da commit fa50e42

5 files changed

Lines changed: 277 additions & 136 deletions

File tree

cmd/anubis/main.go

Lines changed: 47 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"log/slog"
99
"net"
1010
"net/http"
11+
"net/http/httputil"
12+
"net/url"
1113
"os"
1214
"os/signal"
1315
"strconv"
@@ -18,11 +20,9 @@ import (
1820

1921
"github.com/TecharoHQ/anubis"
2022
"github.com/TecharoHQ/anubis/internal"
21-
"github.com/TecharoHQ/anubis/lib"
23+
libanubis "github.com/TecharoHQ/anubis/lib"
2224
"github.com/TecharoHQ/anubis/lib/policy/config"
2325
"github.com/TecharoHQ/anubis/web"
24-
"github.com/TecharoHQ/anubis/xess"
25-
"github.com/a-h/templ"
2626
"github.com/facebookgo/flagenv"
2727
"github.com/prometheus/client_golang/prometheus/promhttp"
2828
)
@@ -90,6 +90,34 @@ func setupListener(network string, address string) (net.Listener, string) {
9090
return listener, formattedAddress
9191
}
9292

93+
func makeReverseProxy(target string) (http.Handler, error) {
94+
u, err := url.Parse(target)
95+
if err != nil {
96+
return nil, fmt.Errorf("failed to parse target URL: %w", err)
97+
}
98+
99+
transport := http.DefaultTransport.(*http.Transport).Clone()
100+
101+
// https://github.com/oauth2-proxy/oauth2-proxy/blob/4e2100a2879ef06aea1411790327019c1a09217c/pkg/upstream/http.go#L124
102+
if u.Scheme == "unix" {
103+
// clean path up so we don't use the socket path in proxied requests
104+
addr := u.Path
105+
u.Path = ""
106+
// tell transport how to dial unix sockets
107+
transport.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) {
108+
dialer := net.Dialer{}
109+
return dialer.DialContext(ctx, "unix", addr)
110+
}
111+
// tell transport how to handle the unix url scheme
112+
transport.RegisterProtocol("unix", libanubis.UnixRoundTripper{Transport: transport})
113+
}
114+
115+
rp := httputil.NewSingleHostReverseProxy(u)
116+
rp.Transport = transport
117+
118+
return rp, nil
119+
}
120+
93121
func main() {
94122
flagenv.Parse()
95123
flag.Parse()
@@ -103,13 +131,18 @@ func main() {
103131
return
104132
}
105133

106-
s, err := lib.New(*target, *policyFname, *challengeDifficulty)
134+
rp, err := makeReverseProxy(*target)
107135
if err != nil {
108-
log.Fatal(err)
136+
log.Fatalf("can't make reverse proxy: %v", err)
137+
}
138+
139+
policy, err := libanubis.LoadPoliciesOrDefault(*policyFname, *challengeDifficulty)
140+
if err != nil {
141+
log.Fatalf("can't parse policy file: %v", err)
109142
}
110143

111144
fmt.Println("Rule error IDs:")
112-
for _, rule := range s.Policy.Bots {
145+
for _, rule := range policy.Bots {
113146
if rule.Action != config.RuleDeny {
114147
continue
115148
}
@@ -123,25 +156,13 @@ func main() {
123156
}
124157
fmt.Println()
125158

126-
mux := http.NewServeMux()
127-
xess.Mount(mux)
128-
129-
mux.Handle(anubis.StaticPath, internal.UnchangingCache(http.StripPrefix(anubis.StaticPath, http.FileServerFS(web.Static))))
130-
131-
// mux.HandleFunc("GET /.within.website/x/cmd/anubis/static/js/main.mjs", serveMainJSWithBestEncoding)
132-
133-
mux.HandleFunc("POST /.within.website/x/cmd/anubis/api/make-challenge", s.MakeChallenge)
134-
mux.HandleFunc("GET /.within.website/x/cmd/anubis/api/pass-challenge", s.PassChallenge)
135-
mux.HandleFunc("GET /.within.website/x/cmd/anubis/api/test-error", s.TestError)
136-
137-
if *robotsTxt {
138-
mux.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) {
139-
http.ServeFileFS(w, r, web.Static, "static/robots.txt")
140-
})
141-
142-
mux.HandleFunc("/.well-known/robots.txt", func(w http.ResponseWriter, r *http.Request) {
143-
http.ServeFileFS(w, r, web.Static, "static/robots.txt")
144-
})
159+
s, err := libanubis.New(libanubis.Options{
160+
Next: rp,
161+
Policy: policy,
162+
ServeRobotsTXT: *robotsTxt,
163+
})
164+
if err != nil {
165+
log.Fatalf("can't construct libanubis.Server: %v", err)
145166
}
146167

147168
wg := new(sync.WaitGroup)
@@ -154,10 +175,8 @@ func main() {
154175
go metricsServer(ctx, wg.Done)
155176
}
156177

157-
mux.HandleFunc("/", s.MaybeReverseProxy)
158-
159178
var h http.Handler
160-
h = mux
179+
h = s
161180
h = internal.DefaultXRealIP(*debugXRealIPDefault, h)
162181
h = internal.XForwardedForToXRealIP(h)
163182

@@ -212,11 +231,6 @@ func metricsServer(ctx context.Context, done func()) {
212231
}
213232
}
214233

215-
func ohNoes(w http.ResponseWriter, r *http.Request, err error) {
216-
slog.Error("super fatal error", "err", err)
217-
templ.Handler(web.Base("Oh noes!", web.ErrorPage("An internal server error happened")), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
218-
}
219-
220234
func serveMainJSWithBestEncoding(w http.ResponseWriter, r *http.Request) {
221235
priorityList := []string{"zstd", "br", "gzip"}
222236
enc2ext := map[string]string{

internal/test/playwright_test.go

Lines changed: 140 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
//go:build integration
2-
31
// Integration tests for Anubis, using Playwright.
42
//
53
// These tests require an already running Anubis and Playwright server.
@@ -16,31 +14,60 @@
1614
package test
1715

1816
import (
19-
"context"
2017
"flag"
2118
"fmt"
2219
"net/http"
20+
"net/http/httptest"
2321
"net/url"
2422
"os"
23+
"os/exec"
2524
"testing"
2625
"time"
2726

27+
"github.com/TecharoHQ/anubis"
28+
libanubis "github.com/TecharoHQ/anubis/lib"
2829
"github.com/playwright-community/playwright-go"
2930
)
3031

3132
var (
32-
anubisServer = flag.String("anubis", "http://localhost:8923", "Anubis server URL")
3333
serverBindAddr = flag.String("bind", "localhost:3923", "test server bind address")
34+
playwrightPort = flag.Int("playwright-port", 3000, "Playwright port")
3435
playwrightServer = flag.String("playwright", "ws://localhost:3000", "Playwright server URL")
3536
playwrightMaxTime = flag.Duration("playwright-max-time", 5*time.Second, "maximum time for Playwright requests")
3637
playwrightMaxHardTime = flag.Duration("playwright-max-hard-time", 5*time.Minute, "maximum time for hard Playwright requests")
3738

3839
testCases = []testCase{
39-
{name: "firefox", action: actionChallenge, realIP: placeholderIP, userAgent: "Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0"},
40-
{name: "headlessChrome", action: actionDeny, realIP: placeholderIP, userAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/120.0.6099.28 Safari/537.36"},
41-
{name: "kagiBadIP", action: actionChallenge, isHard: true, realIP: placeholderIP, userAgent: "Mozilla/5.0 (compatible; Kagibot/1.0; +https://kagi.com/bot)"},
42-
{name: "kagiGoodIP", action: actionAllow, realIP: "216.18.205.234", userAgent: "Mozilla/5.0 (compatible; Kagibot/1.0; +https://kagi.com/bot)"},
43-
{name: "unknownAgent", action: actionAllow, realIP: placeholderIP, userAgent: "AnubisTest/0"},
40+
{
41+
name: "firefox",
42+
action: actionChallenge,
43+
realIP: placeholderIP,
44+
userAgent: "Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0",
45+
},
46+
{
47+
name: "headlessChrome",
48+
action: actionDeny,
49+
realIP: placeholderIP,
50+
userAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/120.0.6099.28 Safari/537.36",
51+
},
52+
{
53+
name: "kagiBadIP",
54+
action: actionChallenge,
55+
isHard: true,
56+
realIP: placeholderIP,
57+
userAgent: "Mozilla/5.0 (compatible; Kagibot/1.0; +https://kagi.com/bot)",
58+
},
59+
{
60+
name: "kagiGoodIP",
61+
action: actionAllow,
62+
realIP: "216.18.205.234",
63+
userAgent: "Mozilla/5.0 (compatible; Kagibot/1.0; +https://kagi.com/bot)",
64+
},
65+
{
66+
name: "unknownAgent",
67+
action: actionAllow,
68+
realIP: placeholderIP,
69+
userAgent: "AnubisTest/0",
70+
},
4471
}
4572
)
4673

@@ -49,7 +76,8 @@ const (
4976
actionDeny action = "DENY"
5077
actionChallenge action = "CHALLENGE"
5178

52-
placeholderIP = "fd11:5ee:bad:c0de::"
79+
placeholderIP = "fd11:5ee:bad:c0de::"
80+
playwrightVersion = "1.50.1"
5381
)
5482

5583
type action string
@@ -61,9 +89,86 @@ type testCase struct {
6189
realIP, userAgent string
6290
}
6391

92+
func doesNPXExist(t *testing.T) {
93+
t.Helper()
94+
95+
if _, err := exec.LookPath("npx"); err != nil {
96+
t.Skipf("npx not found in PATH, skipping integration smoke testing: %v", err)
97+
}
98+
}
99+
100+
func run(t *testing.T, command string) string {
101+
t.Helper()
102+
103+
shPath, err := exec.LookPath("sh")
104+
if err != nil {
105+
t.Fatalf("[unexpected] %v", err)
106+
}
107+
108+
t.Logf("running command: %s", command)
109+
110+
cmd := exec.Command(shPath, "-c", command)
111+
cmd.Stdin = nil
112+
cmd.Stderr = os.Stderr
113+
output, err := cmd.Output()
114+
if err != nil {
115+
t.Fatalf("can't run command: %v", err)
116+
}
117+
118+
return string(output)
119+
}
120+
121+
func daemonize(t *testing.T, command string) {
122+
t.Helper()
123+
124+
shPath, err := exec.LookPath("sh")
125+
if err != nil {
126+
t.Fatalf("[unexpected] %v", err)
127+
}
128+
129+
t.Logf("daemonizing command: %s", command)
130+
131+
cmd := exec.Command(shPath, "-c", command)
132+
cmd.Stdin = nil
133+
cmd.Stderr = os.Stderr
134+
cmd.Stdout = os.Stdout
135+
136+
if err := cmd.Start(); err != nil {
137+
t.Fatalf("can't daemonize command: %v", err)
138+
}
139+
140+
t.Cleanup(func() {
141+
cmd.Process.Kill()
142+
})
143+
}
144+
145+
func startPlaywright(t *testing.T) {
146+
t.Helper()
147+
148+
run(t, fmt.Sprintf("npx --yes playwright@%s install", playwrightVersion))
149+
daemonize(t, fmt.Sprintf("npx --yes playwright@%s run-server --port %d", playwrightVersion, *playwrightPort))
150+
151+
for true {
152+
if _, err := http.Get(fmt.Sprintf("http://localhost:%d", *playwrightPort)); err != nil {
153+
time.Sleep(250 * time.Millisecond)
154+
continue
155+
}
156+
break
157+
}
158+
}
159+
64160
func TestPlaywrightBrowser(t *testing.T) {
161+
if os.Getenv("DONT_USE_NETWORK") != "" {
162+
t.Skip("test requires network egress")
163+
return
164+
}
165+
166+
doesNPXExist(t)
167+
startPlaywright(t)
168+
65169
pw := setupPlaywright(t)
66-
spawnTestServer(t)
170+
anubisURL := spawnAnubis(t)
171+
67172
browsers := []playwright.BrowserType{pw.Chromium, pw.Firefox, pw.WebKit}
68173

69174
for _, typ := range browsers {
@@ -75,7 +180,7 @@ func TestPlaywrightBrowser(t *testing.T) {
75180
t.Skip("skipping hard challenge with deadline")
76181
}
77182

78-
perfomedAction := executeTestCase(t, tc, typ)
183+
perfomedAction := executeTestCase(t, tc, typ, anubisURL)
79184

80185
if perfomedAction != tc.action {
81186
t.Errorf("unexpected test result, expected %s, got %s", tc.action, perfomedAction)
@@ -97,7 +202,7 @@ func buildBrowserConnect(name string) string {
97202
return u.String()
98203
}
99204

100-
func executeTestCase(t *testing.T, tc testCase, typ playwright.BrowserType) action {
205+
func executeTestCase(t *testing.T, tc testCase, typ playwright.BrowserType, anubisURL string) action {
101206
deadline, _ := t.Deadline()
102207

103208
browser, err := typ.Connect(buildBrowserConnect(typ.Name()), playwright.BrowserTypeConnectOptions{
@@ -129,7 +234,7 @@ func executeTestCase(t *testing.T, tc testCase, typ playwright.BrowserType) acti
129234
// Attempt challenge.
130235

131236
start := time.Now()
132-
_, err = page.Goto(*anubisServer, playwright.PageGotoOptions{
237+
_, err = page.Goto(anubisURL, playwright.PageGotoOptions{
133238
Timeout: pwTimeout(tc, deadline),
134239
})
135240
if err != nil {
@@ -252,25 +357,34 @@ func setupPlaywright(t *testing.T) *playwright.Playwright {
252357
return pw
253358
}
254359

255-
func spawnTestServer(t *testing.T) {
360+
func spawnAnubis(t *testing.T) string {
256361
t.Helper()
257362

258-
s := new(http.Server)
259-
s.Addr = *serverBindAddr
260-
s.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
363+
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
261364
w.Header().Add("Content-Type", "text/html")
262365
fmt.Fprintf(w, "<html><body><span id=anubis-test>%d</span></body></html>", time.Now().Unix())
263366
})
264367

265-
go func() {
266-
if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed {
267-
t.Logf("test HTTP server terminated unexpectedly: %v", err)
268-
}
269-
}()
368+
policy, err := libanubis.LoadPoliciesOrDefault("", anubis.DefaultDifficulty)
369+
if err != nil {
370+
t.Fatal(err)
371+
}
372+
373+
s, err := libanubis.New(libanubis.Options{
374+
Next: h,
375+
Policy: policy,
376+
ServeRobotsTXT: true,
377+
})
378+
if err != nil {
379+
t.Fatalf("can't construct libanubis.Server: %v", err)
380+
}
381+
382+
ts := httptest.NewServer(s)
383+
t.Log(ts.URL)
270384

271385
t.Cleanup(func() {
272-
if err := s.Shutdown(context.Background()); err != nil {
273-
t.Fatalf("could not shutdown test server: %v", err)
274-
}
386+
ts.Close()
275387
})
388+
389+
return ts.URL
276390
}

0 commit comments

Comments
 (0)