Apply bits of the cookie settings PR one by one (#140)
Enables uses to change the cookie domain and partitioned flags. Signed-off-by: Xe Iaso <me@xeiaso.net>
This commit is contained in:
parent
d1d63d9c18
commit
7d4be0dcec
9 changed files with 168 additions and 56 deletions
7
.github/workflows/go.yml
vendored
7
.github/workflows/go.yml
vendored
|
@ -64,8 +64,13 @@ jobs:
|
||||||
~/.cache/ms-playwright
|
~/.cache/ms-playwright
|
||||||
key: ${{ runner.os }}-playwright-${{ hashFiles('**/go.sum') }}
|
key: ${{ runner.os }}-playwright-${{ hashFiles('**/go.sum') }}
|
||||||
|
|
||||||
|
- name: install playwright browsers
|
||||||
|
run: |
|
||||||
|
npx --yes playwright@1.50.1 install --with-deps
|
||||||
|
npx --yes playwright@1.50.1 run-server --port 3000 &
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: go build ./...
|
run: go build ./...
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: go test ./...
|
run: go test -v ./...
|
||||||
|
|
|
@ -34,6 +34,8 @@ var (
|
||||||
bind = flag.String("bind", ":8923", "network address to bind HTTP to")
|
bind = flag.String("bind", ":8923", "network address to bind HTTP to")
|
||||||
bindNetwork = flag.String("bind-network", "tcp", "network family to bind HTTP to, e.g. unix, tcp")
|
bindNetwork = flag.String("bind-network", "tcp", "network family to bind HTTP to, e.g. unix, tcp")
|
||||||
challengeDifficulty = flag.Int("difficulty", anubis.DefaultDifficulty, "difficulty of the challenge")
|
challengeDifficulty = flag.Int("difficulty", anubis.DefaultDifficulty, "difficulty of the challenge")
|
||||||
|
cookieDomain = flag.String("cookie-domain", "", "if set, the top-level domain that the Anubis cookie will be valid for")
|
||||||
|
cookiePartitioned = flag.Bool("cookie-partitioned", false, "if true, sets the partitioned flag on Anubis cookies, enabling CHIPS support")
|
||||||
ed25519PrivateKeyHex = flag.String("ed25519-private-key-hex", "", "private key used to sign JWTs, if not set a random one will be assigned")
|
ed25519PrivateKeyHex = flag.String("ed25519-private-key-hex", "", "private key used to sign JWTs, if not set a random one will be assigned")
|
||||||
metricsBind = flag.String("metrics-bind", ":9090", "network address to bind metrics to")
|
metricsBind = flag.String("metrics-bind", ":9090", "network address to bind metrics to")
|
||||||
metricsBindNetwork = flag.String("metrics-bind-network", "tcp", "network family for the metrics server to bind to")
|
metricsBindNetwork = flag.String("metrics-bind-network", "tcp", "network family for the metrics server to bind to")
|
||||||
|
@ -189,10 +191,12 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
s, err := libanubis.New(libanubis.Options{
|
s, err := libanubis.New(libanubis.Options{
|
||||||
Next: rp,
|
Next: rp,
|
||||||
Policy: policy,
|
Policy: policy,
|
||||||
ServeRobotsTXT: *robotsTxt,
|
ServeRobotsTXT: *robotsTxt,
|
||||||
PrivateKey: priv,
|
PrivateKey: priv,
|
||||||
|
CookieDomain: *cookieDomain,
|
||||||
|
CookiePartitioned: *cookiePartitioned,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("can't construct libanubis.Server: %v", err)
|
log.Fatalf("can't construct libanubis.Server: %v", err)
|
||||||
|
|
|
@ -19,6 +19,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- Fix default difficulty setting that was broken in a refactor
|
- Fix default difficulty setting that was broken in a refactor
|
||||||
- Linting fixes
|
- Linting fixes
|
||||||
- Make dark mode diff lines readable in the documentation
|
- Make dark mode diff lines readable in the documentation
|
||||||
|
- Add the ability to set the cookie domain with the envvar `COOKIE_DOMAIN=techaro.lol` for all domains under `techaro.lol`
|
||||||
|
- Add the ability to set the cookie partitioned flag with the envvar `COOKIE_PARTITIONED=true`
|
||||||
|
- Fix CI based browser smoke test
|
||||||
|
|
||||||
## v1.14.2
|
## v1.14.2
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,8 @@ Anubis uses these environment variables for configuration:
|
||||||
| :------------------------ | :---------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| :------------------------ | :---------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `BIND` | `:8923` | The network address that Anubis listens on. For `unix`, set this to a path: `/run/anubis/instance.sock` |
|
| `BIND` | `:8923` | The network address that Anubis listens on. For `unix`, set this to a path: `/run/anubis/instance.sock` |
|
||||||
| `BIND_NETWORK` | `tcp` | The address family that Anubis listens on. Accepts `tcp`, `unix` and anything Go's [`net.Listen`](https://pkg.go.dev/net#Listen) supports. |
|
| `BIND_NETWORK` | `tcp` | The address family that Anubis listens on. Accepts `tcp`, `unix` and anything Go's [`net.Listen`](https://pkg.go.dev/net#Listen) supports. |
|
||||||
|
| `COOKIE_DOMAIN` | unset | The domain the Anubis challenge pass cookie should be set to. This should be set to the domain you bought from your registrar (EG: `techaro.lol` if your webapp is running on `anubis.techaro.lol`). See [here](https://stackoverflow.com/a/1063760) for more information. |
|
||||||
|
| `COOKIE_PARTITIONED` | `false` | If set to `true`, enables the [partitioned (CHIPS) flag](https://developers.google.com/privacy-sandbox/cookies/chips), meaning that Anubis inside an iframe has a different set of cookies than the domain hosting the iframe. |
|
||||||
| `DIFFICULTY` | `5` | The difficulty of the challenge, or the number of leading zeroes that must be in successful responses. |
|
| `DIFFICULTY` | `5` | The difficulty of the challenge, or the number of leading zeroes that must be in successful responses. |
|
||||||
| `ED25519_PRIVATE_KEY_HEX` | | The hex-encoded ed25519 private key used to sign Anubis responses. If this is not set, Anubis will generate one for you. This should be exactly 64 characters long. See below for details. |
|
| `ED25519_PRIVATE_KEY_HEX` | | The hex-encoded ed25519 private key used to sign Anubis responses. If this is not set, Anubis will generate one for you. This should be exactly 64 characters long. See below for details. |
|
||||||
| `METRICS_BIND` | `:9090` | The network address that Anubis serves Prometheus metrics on. See `BIND` for more information. |
|
| `METRICS_BIND` | `:9090` | The network address that Anubis serves Prometheus metrics on. See `BIND` for more information. |
|
||||||
|
|
|
@ -166,10 +166,6 @@ func startPlaywright(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPlaywrightBrowser(t *testing.T) {
|
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") != "" {
|
if os.Getenv("DONT_USE_NETWORK") != "" {
|
||||||
t.Skip("test requires network egress")
|
t.Skip("test requires network egress")
|
||||||
return
|
return
|
||||||
|
@ -225,12 +221,20 @@ func TestPlaywrightBrowser(t *testing.T) {
|
||||||
t.Skip("skipping hard challenge with deadline")
|
t.Skip("skipping hard challenge with deadline")
|
||||||
}
|
}
|
||||||
|
|
||||||
perfomedAction := executeTestCase(t, tc, typ, anubisURL)
|
var perfomedAction action
|
||||||
|
var err error
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
perfomedAction, err = executeTestCase(t, tc, typ, anubisURL)
|
||||||
|
if perfomedAction == tc.action {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(time.Duration(i+1) * 250 * time.Millisecond)
|
||||||
|
}
|
||||||
if perfomedAction != tc.action {
|
if perfomedAction != tc.action {
|
||||||
t.Errorf("unexpected test result, expected %s, got %s", tc.action, perfomedAction)
|
t.Errorf("unexpected test result, expected %s, got %s", tc.action, perfomedAction)
|
||||||
} else {
|
}
|
||||||
t.Logf("test passed")
|
if err != nil {
|
||||||
|
t.Fatalf("test error: %v", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -247,14 +251,14 @@ func buildBrowserConnect(name string) string {
|
||||||
return u.String()
|
return u.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func executeTestCase(t *testing.T, tc testCase, typ playwright.BrowserType, anubisURL string) action {
|
func executeTestCase(t *testing.T, tc testCase, typ playwright.BrowserType, anubisURL string) (action, error) {
|
||||||
deadline, _ := t.Deadline()
|
deadline, _ := t.Deadline()
|
||||||
|
|
||||||
browser, err := typ.Connect(buildBrowserConnect(typ.Name()), playwright.BrowserTypeConnectOptions{
|
browser, err := typ.Connect(buildBrowserConnect(typ.Name()), playwright.BrowserTypeConnectOptions{
|
||||||
ExposeNetwork: playwright.String("<loopback>"),
|
ExposeNetwork: playwright.String("<loopback>"),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("could not connect to remote browser: %v", err)
|
return "", fmt.Errorf("could not connect to remote browser: %w", err)
|
||||||
}
|
}
|
||||||
defer browser.Close()
|
defer browser.Close()
|
||||||
|
|
||||||
|
@ -266,13 +270,13 @@ func executeTestCase(t *testing.T, tc testCase, typ playwright.BrowserType, anub
|
||||||
UserAgent: playwright.String(tc.userAgent),
|
UserAgent: playwright.String(tc.userAgent),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("could not create context: %v", err)
|
return "", fmt.Errorf("could not create context: %w", err)
|
||||||
}
|
}
|
||||||
defer ctx.Close()
|
defer ctx.Close()
|
||||||
|
|
||||||
page, err := ctx.NewPage()
|
page, err := ctx.NewPage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("could not create page: %v", err)
|
return "", fmt.Errorf("could not create page: %w", err)
|
||||||
}
|
}
|
||||||
defer page.Close()
|
defer page.Close()
|
||||||
|
|
||||||
|
@ -283,7 +287,7 @@ func executeTestCase(t *testing.T, tc testCase, typ playwright.BrowserType, anub
|
||||||
Timeout: pwTimeout(tc, deadline),
|
Timeout: pwTimeout(tc, deadline),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pwFail(t, page, "could not navigate to test server: %v", err)
|
return "", pwFail(t, page, "could not navigate to test server: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
hadChallenge := false
|
hadChallenge := false
|
||||||
|
@ -294,7 +298,7 @@ func executeTestCase(t *testing.T, tc testCase, typ playwright.BrowserType, anub
|
||||||
hadChallenge = true
|
hadChallenge = true
|
||||||
case actionDeny:
|
case actionDeny:
|
||||||
checkImage(t, tc, deadline, page, "#image[src*=sad]")
|
checkImage(t, tc, deadline, page, "#image[src*=sad]")
|
||||||
return actionDeny
|
return actionDeny, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure protected resource was provided.
|
// Ensure protected resource was provided.
|
||||||
|
@ -317,9 +321,9 @@ func executeTestCase(t *testing.T, tc testCase, typ playwright.BrowserType, anub
|
||||||
}
|
}
|
||||||
|
|
||||||
if hadChallenge {
|
if hadChallenge {
|
||||||
return actionChallenge
|
return actionChallenge, nil
|
||||||
} else {
|
} else {
|
||||||
return actionAllow
|
return actionAllow, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -342,11 +346,11 @@ func checkImage(t *testing.T, tc testCase, deadline time.Time, page playwright.P
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func pwFail(t *testing.T, page playwright.Page, format string, args ...any) {
|
func pwFail(t *testing.T, page playwright.Page, format string, args ...any) error {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
saveScreenshot(t, page)
|
saveScreenshot(t, page)
|
||||||
t.Fatalf(format, args...)
|
return fmt.Errorf(format, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func pwTimeout(tc testCase, deadline time.Time) *float64 {
|
func pwTimeout(tc testCase, deadline time.Time) *float64 {
|
||||||
|
|
3
internal/test/var/.gitignore
vendored
Normal file
3
internal/test/var/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
*.png
|
||||||
|
*.txt
|
||||||
|
*.html
|
|
@ -67,6 +67,10 @@ type Options struct {
|
||||||
Policy *policy.ParsedConfig
|
Policy *policy.ParsedConfig
|
||||||
ServeRobotsTXT bool
|
ServeRobotsTXT bool
|
||||||
PrivateKey ed25519.PrivateKey
|
PrivateKey ed25519.PrivateKey
|
||||||
|
|
||||||
|
CookieDomain string
|
||||||
|
CookieName string
|
||||||
|
CookiePartitioned bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadPoliciesOrDefault(fname string, defaultDifficulty int) (*policy.ParsedConfig, error) {
|
func LoadPoliciesOrDefault(fname string, defaultDifficulty int) (*policy.ParsedConfig, error) {
|
||||||
|
@ -108,6 +112,7 @@ func New(opts Options) (*Server, error) {
|
||||||
priv: opts.PrivateKey,
|
priv: opts.PrivateKey,
|
||||||
pub: opts.PrivateKey.Public().(ed25519.PublicKey),
|
pub: opts.PrivateKey.Public().(ed25519.PublicKey),
|
||||||
policy: opts.Policy,
|
policy: opts.Policy,
|
||||||
|
opts: opts,
|
||||||
DNSBLCache: decaymap.New[string, dnsbl.DroneBLResponse](),
|
DNSBLCache: decaymap.New[string, dnsbl.DroneBLResponse](),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,6 +150,7 @@ type Server struct {
|
||||||
priv ed25519.PrivateKey
|
priv ed25519.PrivateKey
|
||||||
pub ed25519.PublicKey
|
pub ed25519.PublicKey
|
||||||
policy *policy.ParsedConfig
|
policy *policy.ParsedConfig
|
||||||
|
opts Options
|
||||||
DNSBLCache *decaymap.Impl[string, dnsbl.DroneBLResponse]
|
DNSBLCache *decaymap.Impl[string, dnsbl.DroneBLResponse]
|
||||||
ChallengeDifficulty int
|
ChallengeDifficulty int
|
||||||
}
|
}
|
||||||
|
@ -217,7 +223,7 @@ func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) {
|
||||||
s.next.ServeHTTP(w, r)
|
s.next.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
case config.RuleDeny:
|
case config.RuleDeny:
|
||||||
ClearCookie(w)
|
s.ClearCookie(w)
|
||||||
lg.Info("explicit deny")
|
lg.Info("explicit deny")
|
||||||
if rule == nil {
|
if rule == nil {
|
||||||
lg.Error("rule is nil, cannot calculate checksum")
|
lg.Error("rule is nil, cannot calculate checksum")
|
||||||
|
@ -236,7 +242,7 @@ func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) {
|
||||||
case config.RuleChallenge:
|
case config.RuleChallenge:
|
||||||
lg.Debug("challenge requested")
|
lg.Debug("challenge requested")
|
||||||
default:
|
default:
|
||||||
ClearCookie(w)
|
s.ClearCookie(w)
|
||||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("Other internal server error (contact the admin)")), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
templ.Handler(web.Base("Oh noes!", web.ErrorPage("Other internal server error (contact the admin)")), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -244,21 +250,21 @@ func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) {
|
||||||
ckie, err := r.Cookie(anubis.CookieName)
|
ckie, err := r.Cookie(anubis.CookieName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lg.Debug("cookie not found", "path", r.URL.Path)
|
lg.Debug("cookie not found", "path", r.URL.Path)
|
||||||
ClearCookie(w)
|
s.ClearCookie(w)
|
||||||
s.RenderIndex(w, r)
|
s.RenderIndex(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ckie.Valid(); err != nil {
|
if err := ckie.Valid(); err != nil {
|
||||||
lg.Debug("cookie is invalid", "err", err)
|
lg.Debug("cookie is invalid", "err", err)
|
||||||
ClearCookie(w)
|
s.ClearCookie(w)
|
||||||
s.RenderIndex(w, r)
|
s.RenderIndex(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if time.Now().After(ckie.Expires) && !ckie.Expires.IsZero() {
|
if time.Now().After(ckie.Expires) && !ckie.Expires.IsZero() {
|
||||||
lg.Debug("cookie expired", "path", r.URL.Path)
|
lg.Debug("cookie expired", "path", r.URL.Path)
|
||||||
ClearCookie(w)
|
s.ClearCookie(w)
|
||||||
s.RenderIndex(w, r)
|
s.RenderIndex(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -269,7 +275,7 @@ func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
if err != nil || !token.Valid {
|
if err != nil || !token.Valid {
|
||||||
lg.Debug("invalid token", "path", r.URL.Path, "err", err)
|
lg.Debug("invalid token", "path", r.URL.Path, "err", err)
|
||||||
ClearCookie(w)
|
s.ClearCookie(w)
|
||||||
s.RenderIndex(w, r)
|
s.RenderIndex(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -284,7 +290,7 @@ func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) {
|
||||||
claims, ok := token.Claims.(jwt.MapClaims)
|
claims, ok := token.Claims.(jwt.MapClaims)
|
||||||
if !ok {
|
if !ok {
|
||||||
lg.Debug("invalid token claims type", "path", r.URL.Path)
|
lg.Debug("invalid token claims type", "path", r.URL.Path)
|
||||||
ClearCookie(w)
|
s.ClearCookie(w)
|
||||||
s.RenderIndex(w, r)
|
s.RenderIndex(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -292,7 +298,7 @@ func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
if claims["challenge"] != challenge {
|
if claims["challenge"] != challenge {
|
||||||
lg.Debug("invalid challenge", "path", r.URL.Path)
|
lg.Debug("invalid challenge", "path", r.URL.Path)
|
||||||
ClearCookie(w)
|
s.ClearCookie(w)
|
||||||
s.RenderIndex(w, r)
|
s.RenderIndex(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -309,7 +315,7 @@ func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) {
|
||||||
if subtle.ConstantTimeCompare([]byte(claims["response"].(string)), []byte(calculated)) != 1 {
|
if subtle.ConstantTimeCompare([]byte(claims["response"].(string)), []byte(calculated)) != 1 {
|
||||||
lg.Debug("invalid response", "path", r.URL.Path)
|
lg.Debug("invalid response", "path", r.URL.Path)
|
||||||
failedValidations.Inc()
|
failedValidations.Inc()
|
||||||
ClearCookie(w)
|
s.ClearCookie(w)
|
||||||
s.RenderIndex(w, r)
|
s.RenderIndex(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -372,7 +378,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
nonceStr := r.FormValue("nonce")
|
nonceStr := r.FormValue("nonce")
|
||||||
if nonceStr == "" {
|
if nonceStr == "" {
|
||||||
ClearCookie(w)
|
s.ClearCookie(w)
|
||||||
lg.Debug("no nonce")
|
lg.Debug("no nonce")
|
||||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("missing nonce")), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
templ.Handler(web.Base("Oh noes!", web.ErrorPage("missing nonce")), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
|
@ -380,7 +386,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
elapsedTimeStr := r.FormValue("elapsedTime")
|
elapsedTimeStr := r.FormValue("elapsedTime")
|
||||||
if elapsedTimeStr == "" {
|
if elapsedTimeStr == "" {
|
||||||
ClearCookie(w)
|
s.ClearCookie(w)
|
||||||
lg.Debug("no elapsedTime")
|
lg.Debug("no elapsedTime")
|
||||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("missing elapsedTime")), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
templ.Handler(web.Base("Oh noes!", web.ErrorPage("missing elapsedTime")), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
|
@ -388,7 +394,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
elapsedTime, err := strconv.ParseFloat(elapsedTimeStr, 64)
|
elapsedTime, err := strconv.ParseFloat(elapsedTimeStr, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ClearCookie(w)
|
s.ClearCookie(w)
|
||||||
lg.Debug("elapsedTime doesn't parse", "err", err)
|
lg.Debug("elapsedTime doesn't parse", "err", err)
|
||||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("invalid elapsedTime")), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
templ.Handler(web.Base("Oh noes!", web.ErrorPage("invalid elapsedTime")), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
|
@ -404,7 +410,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
nonce, err := strconv.Atoi(nonceStr)
|
nonce, err := strconv.Atoi(nonceStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ClearCookie(w)
|
s.ClearCookie(w)
|
||||||
lg.Debug("nonce doesn't parse", "err", err)
|
lg.Debug("nonce doesn't parse", "err", err)
|
||||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("invalid nonce")), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
templ.Handler(web.Base("Oh noes!", web.ErrorPage("invalid nonce")), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
|
@ -414,7 +420,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||||
calculated := internal.SHA256sum(calcString)
|
calculated := internal.SHA256sum(calcString)
|
||||||
|
|
||||||
if subtle.ConstantTimeCompare([]byte(response), []byte(calculated)) != 1 {
|
if subtle.ConstantTimeCompare([]byte(response), []byte(calculated)) != 1 {
|
||||||
ClearCookie(w)
|
s.ClearCookie(w)
|
||||||
lg.Debug("hash does not match", "got", response, "want", calculated)
|
lg.Debug("hash does not match", "got", response, "want", calculated)
|
||||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("invalid response")), templ.WithStatus(http.StatusForbidden)).ServeHTTP(w, r)
|
templ.Handler(web.Base("Oh noes!", web.ErrorPage("invalid response")), templ.WithStatus(http.StatusForbidden)).ServeHTTP(w, r)
|
||||||
failedValidations.Inc()
|
failedValidations.Inc()
|
||||||
|
@ -423,7 +429,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
// compare the leading zeroes
|
// compare the leading zeroes
|
||||||
if !strings.HasPrefix(response, strings.Repeat("0", s.ChallengeDifficulty)) {
|
if !strings.HasPrefix(response, strings.Repeat("0", s.ChallengeDifficulty)) {
|
||||||
ClearCookie(w)
|
s.ClearCookie(w)
|
||||||
lg.Debug("difficulty check failed", "response", response, "difficulty", s.ChallengeDifficulty)
|
lg.Debug("difficulty check failed", "response", response, "difficulty", s.ChallengeDifficulty)
|
||||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("invalid response")), templ.WithStatus(http.StatusForbidden)).ServeHTTP(w, r)
|
templ.Handler(web.Base("Oh noes!", web.ErrorPage("invalid response")), templ.WithStatus(http.StatusForbidden)).ServeHTTP(w, r)
|
||||||
failedValidations.Inc()
|
failedValidations.Inc()
|
||||||
|
@ -442,17 +448,19 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||||
tokenString, err := token.SignedString(s.priv)
|
tokenString, err := token.SignedString(s.priv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lg.Error("failed to sign JWT", "err", err)
|
lg.Error("failed to sign JWT", "err", err)
|
||||||
ClearCookie(w)
|
s.ClearCookie(w)
|
||||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("failed to sign JWT")), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
templ.Handler(web.Base("Oh noes!", web.ErrorPage("failed to sign JWT")), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
http.SetCookie(w, &http.Cookie{
|
||||||
Name: anubis.CookieName,
|
Name: anubis.CookieName,
|
||||||
Value: tokenString,
|
Value: tokenString,
|
||||||
Expires: time.Now().Add(24 * 7 * time.Hour),
|
Expires: time.Now().Add(24 * 7 * time.Hour),
|
||||||
SameSite: http.SameSiteLaxMode,
|
SameSite: http.SameSiteLaxMode,
|
||||||
Path: "/",
|
Domain: s.opts.CookieDomain,
|
||||||
|
Partitioned: s.opts.CookiePartitioned,
|
||||||
|
Path: "/",
|
||||||
})
|
})
|
||||||
|
|
||||||
challengesValidated.Inc()
|
challengesValidated.Inc()
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
package lib
|
package lib
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/TecharoHQ/anubis"
|
"github.com/TecharoHQ/anubis"
|
||||||
|
"github.com/TecharoHQ/anubis/internal"
|
||||||
|
"github.com/TecharoHQ/anubis/lib/policy"
|
||||||
)
|
)
|
||||||
|
|
||||||
func spawnAnubis(t *testing.T, h http.Handler) string {
|
func loadPolicies(t *testing.T, fname string) *policy.ParsedConfig {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
policy, err := LoadPoliciesOrDefault("", anubis.DefaultDifficulty)
|
policy, err := LoadPoliciesOrDefault("", anubis.DefaultDifficulty)
|
||||||
|
@ -17,23 +20,102 @@ func spawnAnubis(t *testing.T, h http.Handler) string {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s, err := New(Options{
|
return policy
|
||||||
Next: h,
|
}
|
||||||
Policy: policy,
|
|
||||||
ServeRobotsTXT: true,
|
func spawnAnubis(t *testing.T, opts Options) *Server {
|
||||||
})
|
t.Helper()
|
||||||
|
|
||||||
|
s, err := New(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("can't construct libanubis.Server: %v", err)
|
t.Fatalf("can't construct libanubis.Server: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ts := httptest.NewServer(s)
|
return s
|
||||||
t.Log(ts.URL)
|
}
|
||||||
|
|
||||||
t.Cleanup(func() {
|
func TestCookieSettings(t *testing.T) {
|
||||||
ts.Close()
|
pol := loadPolicies(t, "")
|
||||||
|
pol.DefaultDifficulty = 0
|
||||||
|
|
||||||
|
srv := spawnAnubis(t, Options{
|
||||||
|
Next: http.NewServeMux(),
|
||||||
|
Policy: pol,
|
||||||
|
|
||||||
|
CookieDomain: "local.cetacean.club",
|
||||||
|
CookiePartitioned: true,
|
||||||
|
CookieName: t.Name(),
|
||||||
})
|
})
|
||||||
|
|
||||||
return ts.URL
|
ts := httptest.NewServer(internal.DefaultXRealIP("127.0.0.1", srv))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
cli := &http.Client{
|
||||||
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||||
|
return http.ErrUseLastResponse
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := cli.Post(ts.URL+"/.within.website/x/cmd/anubis/api/make-challenge", "", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("can't request challenge: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var chall = struct {
|
||||||
|
Challenge string `json:"challenge"`
|
||||||
|
}{}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&chall); err != nil {
|
||||||
|
t.Fatalf("can't read challenge response body: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce := 0
|
||||||
|
elapsedTime := 420
|
||||||
|
redir := "/"
|
||||||
|
calcString := fmt.Sprintf("%s%d", chall.Challenge, nonce)
|
||||||
|
calculated := internal.SHA256sum(calcString)
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, ts.URL+"/.within.website/x/cmd/anubis/api/pass-challenge", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("can't make request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
q := req.URL.Query()
|
||||||
|
q.Set("response", calculated)
|
||||||
|
q.Set("nonce", fmt.Sprint(nonce))
|
||||||
|
q.Set("redir", redir)
|
||||||
|
q.Set("elapsedTime", fmt.Sprint(elapsedTime))
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
resp, err = cli.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("can't do challenge passing")
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusFound {
|
||||||
|
t.Errorf("wanted %d, got: %d", http.StatusFound, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ckie *http.Cookie
|
||||||
|
for _, cookie := range resp.Cookies() {
|
||||||
|
t.Logf("%#v", cookie)
|
||||||
|
if cookie.Name == anubis.CookieName {
|
||||||
|
ckie = cookie
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ckie.Domain != "local.cetacean.club" {
|
||||||
|
t.Errorf("cookie domain is wrong, wanted local.cetacean.club, got: %s", ckie.Domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ckie.Partitioned != srv.opts.CookiePartitioned {
|
||||||
|
t.Errorf("wanted partitioned flag %v, got: %v", srv.opts.CookiePartitioned, ckie.Partitioned)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ckie == nil {
|
||||||
|
t.Errorf("Cookie %q not found", anubis.CookieName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCheckDefaultDifficultyMatchesPolicy(t *testing.T) {
|
func TestCheckDefaultDifficultyMatchesPolicy(t *testing.T) {
|
||||||
|
|
|
@ -7,13 +7,14 @@ import (
|
||||||
"github.com/TecharoHQ/anubis"
|
"github.com/TecharoHQ/anubis"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ClearCookie(w http.ResponseWriter) {
|
func (s *Server) ClearCookie(w http.ResponseWriter) {
|
||||||
http.SetCookie(w, &http.Cookie{
|
http.SetCookie(w, &http.Cookie{
|
||||||
Name: anubis.CookieName,
|
Name: anubis.CookieName,
|
||||||
Value: "",
|
Value: "",
|
||||||
Expires: time.Now().Add(-1 * time.Hour),
|
Expires: time.Now().Add(-1 * time.Hour),
|
||||||
MaxAge: -1,
|
MaxAge: -1,
|
||||||
SameSite: http.SameSiteLaxMode,
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
Domain: s.opts.CookieDomain,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue