diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/dnsbl/dnsbl.go | 95 | ||||
| -rw-r--r-- | internal/dnsbl/dnsbl_test.go | 61 | ||||
| -rw-r--r-- | internal/dnsbl/droneblresponse_string.go | 54 | ||||
| -rw-r--r-- | internal/hash.go | 12 | ||||
| -rw-r--r-- | internal/test/playwright_test.go | 213 |
5 files changed, 408 insertions, 27 deletions
diff --git a/internal/dnsbl/dnsbl.go b/internal/dnsbl/dnsbl.go new file mode 100644 index 0000000..60edd5c --- /dev/null +++ b/internal/dnsbl/dnsbl.go @@ -0,0 +1,95 @@ +package dnsbl + +import ( + "errors" + "fmt" + "net" + "strings" +) + +//go:generate go tool golang.org/x/tools/cmd/stringer -type=DroneBLResponse + +type DroneBLResponse byte + +const ( + AllGood DroneBLResponse = 0 + IRCDrone DroneBLResponse = 3 + Bottler DroneBLResponse = 5 + UnknownSpambotOrDrone DroneBLResponse = 6 + DDOSDrone DroneBLResponse = 7 + SOCKSProxy DroneBLResponse = 8 + HTTPProxy DroneBLResponse = 9 + ProxyChain DroneBLResponse = 10 + OpenProxy DroneBLResponse = 11 + OpenDNSResolver DroneBLResponse = 12 + BruteForceAttackers DroneBLResponse = 13 + OpenWingateProxy DroneBLResponse = 14 + CompromisedRouter DroneBLResponse = 15 + AutoRootingWorms DroneBLResponse = 16 + AutoDetectedBotIP DroneBLResponse = 17 + Unknown DroneBLResponse = 255 +) + +func Reverse(ip net.IP) string { + if ip.To4() != nil { + return reverse4(ip) + } + + return reverse6(ip) +} + +func reverse4(ip net.IP) string { + splitAddress := strings.Split(ip.String(), ".") + + // swap first and last octet + splitAddress[0], splitAddress[3] = splitAddress[3], splitAddress[0] + // swap middle octets + splitAddress[1], splitAddress[2] = splitAddress[2], splitAddress[1] + + return strings.Join(splitAddress, ".") +} + +func reverse6(ip net.IP) string { + ipBytes := []byte(ip) + var sb strings.Builder + + for i := len(ipBytes) - 1; i >= 0; i-- { + // Split the byte into two nibbles + highNibble := ipBytes[i] >> 4 + lowNibble := ipBytes[i] & 0x0F + + // Append the nibbles in reversed order + sb.WriteString(fmt.Sprintf("%x.%x.", lowNibble, highNibble)) + } + + return sb.String()[:len(sb.String())-1] +} + +func Lookup(ipStr string) (DroneBLResponse, error) { + ip := net.ParseIP(ipStr) + if ip == nil { + return Unknown, errors.New("dnsbl: input is not an IP address") + } + + revIP := Reverse(ip) + ".dnsbl.dronebl.org" + + ips, err := net.LookupIP(revIP) + if err != nil { + var dnserr *net.DNSError + if errors.As(err, &dnserr) { + if dnserr.IsNotFound { + return AllGood, nil + } + } + + return Unknown, err + } + + if len(ips) != 0 { + for _, ip := range ips { + return DroneBLResponse(ip.To4()[3]), nil + } + } + + return UnknownSpambotOrDrone, nil +} diff --git a/internal/dnsbl/dnsbl_test.go b/internal/dnsbl/dnsbl_test.go new file mode 100644 index 0000000..ccadd64 --- /dev/null +++ b/internal/dnsbl/dnsbl_test.go @@ -0,0 +1,61 @@ +package dnsbl + +import ( + "fmt" + "net" + "os" + "testing" +) + +func TestReverse4(t *testing.T) { + cases := []struct { + inp, out string + }{ + {"1.2.3.4", "4.3.2.1"}, + } + + for _, cs := range cases { + t.Run(fmt.Sprintf("%s->%s", cs.inp, cs.out), func(t *testing.T) { + out := reverse4(net.ParseIP(cs.inp)) + + if out != cs.out { + t.Errorf("wanted %s\ngot: %s", cs.out, out) + } + }) + } +} + +func TestReverse6(t *testing.T) { + cases := []struct { + inp, out string + }{ + { + inp: "1234:5678:9ABC:DEF0:1234:5678:9ABC:DEF0", + out: "0.f.e.d.c.b.a.9.8.7.6.5.4.3.2.1.0.f.e.d.c.b.a.9.8.7.6.5.4.3.2.1", + }, + } + + for _, cs := range cases { + t.Run(fmt.Sprintf("%s->%s", cs.inp, cs.out), func(t *testing.T) { + out := reverse6(net.ParseIP(cs.inp)) + + if out != cs.out { + t.Errorf("wanted %s, got: %s", cs.out, out) + } + }) + } +} + +func TestLookup(t *testing.T) { + if os.Getenv("DONT_USE_NETWORK") != "" { + t.Skip("test requires network egress") + return + } + + resp, err := Lookup("27.65.243.194") + if err != nil { + t.Fatalf("it broked: %v", err) + } + + t.Logf("response: %d", resp) +} diff --git a/internal/dnsbl/droneblresponse_string.go b/internal/dnsbl/droneblresponse_string.go new file mode 100644 index 0000000..5104dda --- /dev/null +++ b/internal/dnsbl/droneblresponse_string.go @@ -0,0 +1,54 @@ +// Code generated by "stringer -type=DroneBLResponse"; DO NOT EDIT. + +package dnsbl + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[AllGood-0] + _ = x[IRCDrone-3] + _ = x[Bottler-5] + _ = x[UnknownSpambotOrDrone-6] + _ = x[DDOSDrone-7] + _ = x[SOCKSProxy-8] + _ = x[HTTPProxy-9] + _ = x[ProxyChain-10] + _ = x[OpenProxy-11] + _ = x[OpenDNSResolver-12] + _ = x[BruteForceAttackers-13] + _ = x[OpenWingateProxy-14] + _ = x[CompromisedRouter-15] + _ = x[AutoRootingWorms-16] + _ = x[AutoDetectedBotIP-17] + _ = x[Unknown-255] +} + +const ( + _DroneBLResponse_name_0 = "AllGood" + _DroneBLResponse_name_1 = "IRCDrone" + _DroneBLResponse_name_2 = "BottlerUnknownSpambotOrDroneDDOSDroneSOCKSProxyHTTPProxyProxyChainOpenProxyOpenDNSResolverBruteForceAttackersOpenWingateProxyCompromisedRouterAutoRootingWormsAutoDetectedBotIP" + _DroneBLResponse_name_3 = "Unknown" +) + +var ( + _DroneBLResponse_index_2 = [...]uint8{0, 7, 28, 37, 47, 56, 66, 75, 90, 109, 125, 142, 158, 175} +) + +func (i DroneBLResponse) String() string { + switch { + case i == 0: + return _DroneBLResponse_name_0 + case i == 3: + return _DroneBLResponse_name_1 + case 5 <= i && i <= 17: + i -= 5 + return _DroneBLResponse_name_2[_DroneBLResponse_index_2[i]:_DroneBLResponse_index_2[i+1]] + case i == 255: + return _DroneBLResponse_name_3 + default: + return "DroneBLResponse(" + strconv.FormatInt(int64(i), 10) + ")" + } +} diff --git a/internal/hash.go b/internal/hash.go new file mode 100644 index 0000000..818ad55 --- /dev/null +++ b/internal/hash.go @@ -0,0 +1,12 @@ +package internal + +import ( + "crypto/sha256" + "encoding/hex" +) + +func SHA256sum(text string) string { + hash := sha256.New() + hash.Write([]byte(text)) + return hex.EncodeToString(hash.Sum(nil)) +} diff --git a/internal/test/playwright_test.go b/internal/test/playwright_test.go index 156d7df..b8b6cac 100644 --- a/internal/test/playwright_test.go +++ b/internal/test/playwright_test.go @@ -1,5 +1,3 @@ -//go:build integration - // Integration tests for Anubis, using Playwright. // // These tests require an already running Anubis and Playwright server. @@ -16,31 +14,60 @@ package test import ( - "context" "flag" "fmt" "net/http" + "net/http/httptest" "net/url" "os" + "os/exec" "testing" "time" + "github.com/TecharoHQ/anubis" + libanubis "github.com/TecharoHQ/anubis/lib" "github.com/playwright-community/playwright-go" ) var ( - anubisServer = flag.String("anubis", "http://localhost:8923", "Anubis server URL") serverBindAddr = flag.String("bind", "localhost:3923", "test server bind address") + playwrightPort = flag.Int("playwright-port", 3000, "Playwright port") playwrightServer = flag.String("playwright", "ws://localhost:3000", "Playwright server URL") playwrightMaxTime = flag.Duration("playwright-max-time", 5*time.Second, "maximum time for Playwright requests") playwrightMaxHardTime = flag.Duration("playwright-max-hard-time", 5*time.Minute, "maximum time for hard Playwright requests") testCases = []testCase{ - {name: "firefox", action: actionChallenge, realIP: placeholderIP, userAgent: "Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0"}, - {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"}, - {name: "kagiBadIP", action: actionChallenge, isHard: true, realIP: placeholderIP, userAgent: "Mozilla/5.0 (compatible; Kagibot/1.0; +https://kagi.com/bot)"}, - {name: "kagiGoodIP", action: actionAllow, realIP: "216.18.205.234", userAgent: "Mozilla/5.0 (compatible; Kagibot/1.0; +https://kagi.com/bot)"}, - {name: "unknownAgent", action: actionAllow, realIP: placeholderIP, userAgent: "AnubisTest/0"}, + { + name: "firefox", + action: actionChallenge, + realIP: placeholderIP, + userAgent: "Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0", + }, + { + 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", + }, + { + name: "kagiBadIP", + action: actionChallenge, + isHard: true, + realIP: placeholderIP, + userAgent: "Mozilla/5.0 (compatible; Kagibot/1.0; +https://kagi.com/bot)", + }, + { + name: "kagiGoodIP", + action: actionAllow, + realIP: "216.18.205.234", + userAgent: "Mozilla/5.0 (compatible; Kagibot/1.0; +https://kagi.com/bot)", + }, + { + name: "unknownAgent", + action: actionAllow, + realIP: placeholderIP, + userAgent: "AnubisTest/0", + }, } ) @@ -49,7 +76,8 @@ const ( actionDeny action = "DENY" actionChallenge action = "CHALLENGE" - placeholderIP = "fd11:5ee:bad:c0de::" + placeholderIP = "fd11:5ee:bad:c0de::" + playwrightVersion = "1.50.1" ) type action string @@ -61,21 +89,143 @@ type testCase struct { realIP, userAgent string } +func doesNPXExist(t *testing.T) { + t.Helper() + + if _, err := exec.LookPath("npx"); err != nil { + t.Skipf("npx not found in PATH, skipping integration smoke testing: %v", err) + } +} + +func run(t *testing.T, command string) string { + t.Helper() + + shPath, err := exec.LookPath("sh") + if err != nil { + t.Fatalf("[unexpected] %v", err) + } + + t.Logf("running command: %s", command) + + cmd := exec.Command(shPath, "-c", command) + cmd.Stdin = nil + cmd.Stderr = os.Stderr + output, err := cmd.Output() + if err != nil { + t.Fatalf("can't run command: %v", err) + } + + return string(output) +} + +func daemonize(t *testing.T, command string) { + t.Helper() + + shPath, err := exec.LookPath("sh") + if err != nil { + t.Fatalf("[unexpected] %v", err) + } + + t.Logf("daemonizing command: %s", command) + + cmd := exec.Command(shPath, "-c", command) + cmd.Stdin = nil + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + + if err := cmd.Start(); err != nil { + t.Fatalf("can't daemonize command: %v", err) + } + + t.Cleanup(func() { + cmd.Process.Kill() + }) +} + +func startPlaywright(t *testing.T) { + t.Helper() + + if os.Getenv("CI") == "true" { + run(t, fmt.Sprintf("npx --yes playwright@%s install --with-deps", playwrightVersion)) + } else { + run(t, fmt.Sprintf("npx --yes playwright@%s install", playwrightVersion)) + } + + daemonize(t, fmt.Sprintf("npx --yes playwright@%s run-server --port %d", playwrightVersion, *playwrightPort)) + + for true { + if _, err := http.Get(fmt.Sprintf("http://localhost:%d", *playwrightPort)); err != nil { + time.Sleep(500 * time.Millisecond) + continue + } + break + } + + //nosleep:bypass XXX(Xe): Playwright doesn't have a good way to signal readiness. This is a HACK that will just let the tests pass. + time.Sleep(2 * time.Second) +} + func TestPlaywrightBrowser(t *testing.T) { + if os.Getenv("CI") == "true" { + t.Skip("XXX(Xe): This is broken in CI, will fix later") + } + + if os.Getenv("DONT_USE_NETWORK") != "" { + t.Skip("test requires network egress") + return + } + + doesNPXExist(t) + startPlaywright(t) + pw := setupPlaywright(t) - spawnTestServer(t) + anubisURL := spawnAnubis(t) + browsers := []playwright.BrowserType{pw.Chromium, pw.Firefox, pw.WebKit} for _, typ := range browsers { + t.Run(typ.Name()+"/warmup", func(t *testing.T) { + browser, err := typ.Connect(buildBrowserConnect(typ.Name()), playwright.BrowserTypeConnectOptions{ + ExposeNetwork: playwright.String("<loopback>"), + }) + if err != nil { + t.Fatalf("could not connect to remote browser: %v", err) + } + defer browser.Close() + + ctx, err := browser.NewContext(playwright.BrowserNewContextOptions{ + AcceptDownloads: playwright.Bool(false), + ExtraHttpHeaders: map[string]string{ + "X-Real-Ip": "127.0.0.1", + }, + UserAgent: playwright.String("Sephiroth"), + }) + if err != nil { + t.Fatalf("could not create context: %v", err) + } + defer ctx.Close() + + page, err := ctx.NewPage() + if err != nil { + t.Fatalf("could not create page: %v", err) + } + defer page.Close() + + timeout := 2.0 + page.Goto(anubisURL, playwright.PageGotoOptions{ + Timeout: &timeout, + }) + }) + for _, tc := range testCases { - name := fmt.Sprintf("%s@%s", tc.name, typ.Name()) + name := fmt.Sprintf("%s/%s", typ.Name(), tc.name) t.Run(name, func(t *testing.T) { _, hasDeadline := t.Deadline() if tc.isHard && hasDeadline { t.Skip("skipping hard challenge with deadline") } - perfomedAction := executeTestCase(t, tc, typ) + perfomedAction := executeTestCase(t, tc, typ, anubisURL) if perfomedAction != tc.action { t.Errorf("unexpected test result, expected %s, got %s", tc.action, perfomedAction) @@ -97,7 +247,7 @@ func buildBrowserConnect(name string) string { return u.String() } -func executeTestCase(t *testing.T, tc testCase, typ playwright.BrowserType) action { +func executeTestCase(t *testing.T, tc testCase, typ playwright.BrowserType, anubisURL string) action { deadline, _ := t.Deadline() browser, err := typ.Connect(buildBrowserConnect(typ.Name()), playwright.BrowserTypeConnectOptions{ @@ -129,7 +279,7 @@ func executeTestCase(t *testing.T, tc testCase, typ playwright.BrowserType) acti // Attempt challenge. start := time.Now() - _, err = page.Goto(*anubisServer, playwright.PageGotoOptions{ + _, err = page.Goto(anubisURL, playwright.PageGotoOptions{ Timeout: pwTimeout(tc, deadline), }) if err != nil { @@ -252,25 +402,34 @@ func setupPlaywright(t *testing.T) *playwright.Playwright { return pw } -func spawnTestServer(t *testing.T) { +func spawnAnubis(t *testing.T) string { t.Helper() - s := new(http.Server) - s.Addr = *serverBindAddr - s.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Add("Content-Type", "text/html") fmt.Fprintf(w, "<html><body><span id=anubis-test>%d</span></body></html>", time.Now().Unix()) }) - go func() { - if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed { - t.Logf("test HTTP server terminated unexpectedly: %v", err) - } - }() + policy, err := libanubis.LoadPoliciesOrDefault("", anubis.DefaultDifficulty) + if err != nil { + t.Fatal(err) + } + + s, err := libanubis.New(libanubis.Options{ + Next: h, + Policy: policy, + ServeRobotsTXT: true, + }) + if err != nil { + t.Fatalf("can't construct libanubis.Server: %v", err) + } + + ts := httptest.NewServer(s) + t.Log(ts.URL) t.Cleanup(func() { - if err := s.Shutdown(context.Background()); err != nil { - t.Fatalf("could not shutdown test server: %v", err) - } + ts.Close() }) + + return ts.URL } |
