Compare commits

..

1 Commits

Author SHA1 Message Date
Xe Iaso
cf0b5a331a fix(cmd/containerbuild): support commas in --docker-tags
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-09-08 22:13:08 +00:00
23 changed files with 183 additions and 527 deletions

View File

@@ -1,7 +1,4 @@
acs acs
Actorified
actorifiedstore
actorify
Aibrew Aibrew
alibaba alibaba
alrest alrest
@@ -160,7 +157,6 @@ ifm
Imagesift Imagesift
imgproxy imgproxy
impressum impressum
inbox
inp inp
internets internets
IPTo IPTo
@@ -218,7 +214,6 @@ nicksnyder
nobots nobots
NONINFRINGEMENT NONINFRINGEMENT
nosleep nosleep
nullglob
OCOB OCOB
ogtag ogtag
oklch oklch
@@ -283,7 +278,6 @@ Seo
setsebool setsebool
shellcheck shellcheck
shirou shirou
shopt
Sidetrade Sidetrade
simprint simprint
sitemap sitemap

View File

@@ -51,7 +51,6 @@ var (
cookieExpiration = flag.Duration("cookie-expiration-time", anubis.CookieDefaultExpirationTime, "The amount of time the authorization cookie is valid for") cookieExpiration = flag.Duration("cookie-expiration-time", anubis.CookieDefaultExpirationTime, "The amount of time the authorization cookie is valid for")
cookiePrefix = flag.String("cookie-prefix", anubis.CookieName, "prefix for browser cookies created by Anubis") cookiePrefix = flag.String("cookie-prefix", anubis.CookieName, "prefix for browser cookies created by Anubis")
cookiePartitioned = flag.Bool("cookie-partitioned", false, "if true, sets the partitioned flag on Anubis cookies, enabling CHIPS support") cookiePartitioned = flag.Bool("cookie-partitioned", false, "if true, sets the partitioned flag on Anubis cookies, enabling CHIPS support")
difficultyInJWT = flag.Bool("difficulty-in-jwt", false, "if true, adds a difficulty field in the JWT claims")
useSimplifiedExplanation = flag.Bool("use-simplified-explanation", false, "if true, replaces the text when clicking \"Why am I seeing this?\" with a more simplified text for a non-tech-savvy audience.") useSimplifiedExplanation = flag.Bool("use-simplified-explanation", false, "if true, replaces the text when clicking \"Why am I seeing this?\" with a more simplified text for a non-tech-savvy audience.")
forcedLanguage = flag.String("forced-language", "", "if set, this language is being used instead of the one from the request's Accept-Language header") forcedLanguage = flag.String("forced-language", "", "if set, this language is being used instead of the one from the request's Accept-Language header")
hs512Secret = flag.String("hs512-secret", "", "secret used to sign JWTs, uses ed25519 if not set") hs512Secret = flag.String("hs512-secret", "", "secret used to sign JWTs, uses ed25519 if not set")
@@ -434,7 +433,6 @@ func main() {
CookieSecure: *cookieSecure, CookieSecure: *cookieSecure,
PublicUrl: *publicUrl, PublicUrl: *publicUrl,
JWTRestrictionHeader: *jwtRestrictionHeader, JWTRestrictionHeader: *jwtRestrictionHeader,
DifficultyInJWT: *difficultyInJWT,
}) })
if err != nil { if err != nil {
log.Fatalf("can't construct libanubis.Server: %v", err) log.Fatalf("can't construct libanubis.Server: %v", err)

View File

@@ -14,12 +14,6 @@ func Zilch[T any]() T {
type Impl[K comparable, V any] struct { type Impl[K comparable, V any] struct {
data map[K]decayMapEntry[V] data map[K]decayMapEntry[V]
lock sync.RWMutex lock sync.RWMutex
// deleteCh receives decay-deletion requests from readers.
deleteCh chan deleteReq[K]
// stopCh stops the background cleanup worker.
stopCh chan struct{}
wg sync.WaitGroup
} }
type decayMapEntry[V any] struct { type decayMapEntry[V any] struct {
@@ -27,38 +21,30 @@ type decayMapEntry[V any] struct {
expiry time.Time expiry time.Time
} }
// deleteReq is a request to remove a key if its expiry timestamp still matches
// the observed one. This prevents racing with concurrent Set updates.
type deleteReq[K comparable] struct {
key K
expiry time.Time
}
// New creates a new DecayMap of key type K and value type V. // New creates a new DecayMap of key type K and value type V.
// //
// Key types must be comparable to work with maps. // Key types must be comparable to work with maps.
func New[K comparable, V any]() *Impl[K, V] { func New[K comparable, V any]() *Impl[K, V] {
m := &Impl[K, V]{ return &Impl[K, V]{
data: make(map[K]decayMapEntry[V]), data: make(map[K]decayMapEntry[V]),
deleteCh: make(chan deleteReq[K], 1024),
stopCh: make(chan struct{}),
} }
m.wg.Add(1)
go m.cleanupWorker()
return m
} }
// expire forcibly expires a key by setting its time-to-live one second in the past. // expire forcibly expires a key by setting its time-to-live one second in the past.
func (m *Impl[K, V]) expire(key K) bool { func (m *Impl[K, V]) expire(key K) bool {
// Use a single write lock to avoid RUnlock->Lock convoy. m.lock.RLock()
m.lock.Lock()
defer m.lock.Unlock()
val, ok := m.data[key] val, ok := m.data[key]
m.lock.RUnlock()
if !ok { if !ok {
return false return false
} }
m.lock.Lock()
val.expiry = time.Now().Add(-1 * time.Second) val.expiry = time.Now().Add(-1 * time.Second)
m.data[key] = val m.data[key] = val
m.lock.Unlock()
return true return true
} }
@@ -67,14 +53,19 @@ func (m *Impl[K, V]) expire(key K) bool {
// If the value does not exist, return false. Return true after // If the value does not exist, return false. Return true after
// deletion. // deletion.
func (m *Impl[K, V]) Delete(key K) bool { func (m *Impl[K, V]) Delete(key K) bool {
// Use a single write lock to avoid RUnlock->Lock convoy. m.lock.RLock()
m.lock.Lock()
defer m.lock.Unlock()
_, ok := m.data[key] _, ok := m.data[key]
if ok { m.lock.RUnlock()
delete(m.data, key)
if !ok {
return false
} }
return ok
m.lock.Lock()
delete(m.data, key)
m.lock.Unlock()
return true
} }
// Get gets a value from the DecayMap by key. // Get gets a value from the DecayMap by key.
@@ -90,12 +81,13 @@ func (m *Impl[K, V]) Get(key K) (V, bool) {
} }
if time.Now().After(value.expiry) { if time.Now().After(value.expiry) {
// Defer decay deletion to the background worker to avoid convoy. m.lock.Lock()
select { // Since previously reading m.data[key], the value may have been updated.
case m.deleteCh <- deleteReq[K]{key: key, expiry: value.expiry}: // Delete the entry only if the expiry time is still the same.
default: if m.data[key].expiry.Equal(value.expiry) {
// Channel full: drop request; a future Cleanup() or Get will retry. delete(m.data, key)
} }
m.lock.Unlock()
return Zilch[V](), false return Zilch[V](), false
} }
@@ -133,64 +125,3 @@ func (m *Impl[K, V]) Len() int {
defer m.lock.RUnlock() defer m.lock.RUnlock()
return len(m.data) return len(m.data)
} }
// Close stops the background cleanup worker. It's optional to call; maps live
// for the process lifetime in many cases. Call in tests or when you know you no
// longer need the map to avoid goroutine leaks.
func (m *Impl[K, V]) Close() {
close(m.stopCh)
m.wg.Wait()
}
// cleanupWorker batches decay deletions to minimize lock contention.
func (m *Impl[K, V]) cleanupWorker() {
defer m.wg.Done()
batch := make([]deleteReq[K], 0, 64)
ticker := time.NewTicker(10 * time.Millisecond)
defer ticker.Stop()
flush := func() {
if len(batch) == 0 {
return
}
m.applyDeletes(batch)
// reset batch without reallocating
batch = batch[:0]
}
for {
select {
case req := <-m.deleteCh:
batch = append(batch, req)
case <-ticker.C:
flush()
case <-m.stopCh:
// Drain any remaining requests then exit
for {
select {
case req := <-m.deleteCh:
batch = append(batch, req)
default:
flush()
return
}
}
}
}
}
func (m *Impl[K, V]) applyDeletes(batch []deleteReq[K]) {
now := time.Now()
m.lock.Lock()
for _, req := range batch {
entry, ok := m.data[req.key]
if !ok {
continue
}
// Only delete if the expiry is unchanged and already past.
if entry.expiry.Equal(req.expiry) && now.After(entry.expiry) {
delete(m.data, req.key)
}
}
m.lock.Unlock()
}

View File

@@ -7,7 +7,6 @@ import (
func TestImpl(t *testing.T) { func TestImpl(t *testing.T) {
dm := New[string, string]() dm := New[string, string]()
t.Cleanup(dm.Close)
dm.Set("test", "hi", 5*time.Minute) dm.Set("test", "hi", 5*time.Minute)
@@ -29,24 +28,10 @@ func TestImpl(t *testing.T) {
if ok { if ok {
t.Error("got value even though it was supposed to be expired") t.Error("got value even though it was supposed to be expired")
} }
// Deletion of expired entries after Get is deferred to a background worker.
// Assert it eventually disappears from the map.
deadline := time.Now().Add(200 * time.Millisecond)
for time.Now().Before(deadline) {
if dm.Len() == 0 {
break
}
time.Sleep(5 * time.Millisecond)
}
if dm.Len() != 0 {
t.Fatalf("expected background cleanup to remove expired key; len=%d", dm.Len())
}
} }
func TestCleanup(t *testing.T) { func TestCleanup(t *testing.T) {
dm := New[string, string]() dm := New[string, string]()
t.Cleanup(dm.Close)
dm.Set("test1", "hi1", 1*time.Second) dm.Set("test1", "hi1", 1*time.Second)
dm.Set("test2", "hi2", 2*time.Second) dm.Set("test2", "hi2", 2*time.Second)

View File

@@ -13,16 +13,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
<!-- This changes the project to: --> <!-- This changes the project to: -->
- Fix lock convoy problem in decaymap ([#1103](https://github.com/TecharoHQ/anubis/issues/1103)). - Document missing environment variables in installation guide: `SLOG_LEVEL`, `COOKIE_PREFIX`, `FORCED_LANGUAGE`, and `TARGET_DISABLE_KEEPALIVE` ([#1086](https://github.com/TecharoHQ/anubis/pull/1086))
- Fix lock convoy problem in bbolt by implementing the actor pattern ([#1103](https://github.com/TecharoHQ/anubis/issues/1103)). - Add validation warning when persistent storage is used without setting signing keys
- Document missing environment variables in installation guide: `SLOG_LEVEL`, `COOKIE_PREFIX`, `FORCED_LANGUAGE`, and `TARGET_DISABLE_KEEPALIVE` ([#1086](https://github.com/TecharoHQ/anubis/pull/1086)). - Fixed `robots2policy` to properly group consecutive user agents into `any:` instead of only processing the last one ([#925](https://github.com/TecharoHQ/anubis/pull/925))
- Add validation warning when persistent storage is used without setting signing keys.
- Fixed `robots2policy` to properly group consecutive user agents into `any:` instead of only processing the last one ([#925](https://github.com/TecharoHQ/anubis/pull/925)).
- Add the [`s3api` storage backend](./admin/policies.mdx#s3api) to allow Anubis to use S3 API compatible object storage as its storage backend. - Add the [`s3api` storage backend](./admin/policies.mdx#s3api) to allow Anubis to use S3 API compatible object storage as its storage backend.
- Make `cmd/containerbuild` support commas for separating elements of the `--docker-tags` argument as well as newlines. - Make `cmd/containerbuild` support commas for separating elements of the `--docker-tags` argument as well as newlines.
- Add the `DIFFICULTY_IN_JWT` option, which allows one to add the `difficulty` field in the JWT claims which indicates the difficulty of the token ([#1063](https://github.com/TecharoHQ/anubis/pull/1063)).
- Ported the client-side JS to TypeScript to avoid egregious errors in the future.
- Fixes concurrency problems with very old browsers ([#1082](https://github.com/TecharoHQ/anubis/issues/1082)).
### Bug Fixes ### Bug Fixes

View File

@@ -59,7 +59,7 @@ Currently the following settings are configurable via the policy file:
Anubis uses these environment variables for configuration: Anubis uses these environment variables for configuration:
| Environment Variable | Default value | Explanation | | Environment Variable | Default value | Explanation |
| :----------------------------- | :---------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | |:-------------------------------|:------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `BASE_PREFIX` | unset | If set, adds a global prefix to all Anubis endpoints (everything starting with `/.within.website/x/anubis/`). For example, setting this to `/myapp` would make Anubis accessible at `/myapp/` instead of `/`. This is useful when running Anubis behind a reverse proxy that routes based on path prefixes. | | `BASE_PREFIX` | unset | If set, adds a global prefix to all Anubis endpoints (everything starting with `/.within.website/x/anubis/`). For example, setting this to `/myapp` would make Anubis accessible at `/myapp/` instead of `/`. This is useful when running Anubis behind a reverse proxy that routes based on path prefixes. |
| `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. |
@@ -70,7 +70,6 @@ Anubis uses these environment variables for configuration:
| `COOKIE_PREFIX` | `anubis-cookie` | The prefix used for browser cookies created by Anubis. Useful for customization or avoiding conflicts with other applications. | | `COOKIE_PREFIX` | `anubis-cookie` | The prefix used for browser cookies created by Anubis. Useful for customization or avoiding conflicts with other applications. |
| `COOKIE_SECURE` | `true` | If set to `true`, enables the [Secure flag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#block_access_to_your_cookies), meaning that the cookies will only be transmitted over HTTPS. If Anubis is used in an unsecure context (plain HTTP), this will be need to be set to false | | `COOKIE_SECURE` | `true` | If set to `true`, enables the [Secure flag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#block_access_to_your_cookies), meaning that the cookies will only be transmitted over HTTPS. If Anubis is used in an unsecure context (plain HTTP), this will be need to be set to false |
| `DIFFICULTY` | `4` | The difficulty of the challenge, or the number of leading zeroes that must be in successful responses. | | `DIFFICULTY` | `4` | The difficulty of the challenge, or the number of leading zeroes that must be in successful responses. |
| `DIFFICULTY_IN_JWT` | `false` | If set to `true`, adds the `difficulty` field into JWT claims, which indicates the difficulty the token has been generated. This may be useful for statistics and debugging. |
| `ED25519_PRIVATE_KEY_HEX` | unset | 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. **Required when using persistent storage backends** (like bbolt) to ensure challenges survive service restarts. When running multiple instances on the same base domain, the key must be the same across all instances. See below for details. | | `ED25519_PRIVATE_KEY_HEX` | unset | 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. **Required when using persistent storage backends** (like bbolt) to ensure challenges survive service restarts. When running multiple instances on the same base domain, the key must be the same across all instances. See below for details. |
| `ED25519_PRIVATE_KEY_HEX_FILE` | unset | Path to a file containing the hex-encoded ed25519 private key. Only one of this or its sister option may be set. **Required when using persistent storage backends** (like bbolt) to ensure challenges survive service restarts. When running multiple instances on the same base domain, the key must be the same across all instances. | | `ED25519_PRIVATE_KEY_HEX_FILE` | unset | Path to a file containing the hex-encoded ed25519 private key. Only one of this or its sister option may be set. **Required when using persistent storage backends** (like bbolt) to ensure challenges survive service restarts. When running multiple instances on the same base domain, the key must be the same across all instances. |
| `JWT_RESTRICTION_HEADER` | `X-Real-IP` | If set, the JWT is only valid if the current value of this header matches the value when the JWT was created. You can use it e.g. to restrict a JWT to the source IP of the user using `X-Real-IP`. | | `JWT_RESTRICTION_HEADER` | `X-Real-IP` | If set, the JWT is only valid if the current value of this header matches the value when the JWT was created. You can use it e.g. to restrict a JWT to the source IP of the user using `X-Real-IP`. |
@@ -101,14 +100,14 @@ If you don't know or understand what these settings mean, ignore them. These are
::: :::
| Environment Variable | Default value | Explanation | | Environment Variable | Default value | Explanation |
| :---------------------------- | :------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | :---------------------------- | :------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `FORCED_LANGUAGE` | unset | If set, forces Anubis to display challenge pages in the specified language instead of using the browser's Accept-Language header. Use ISO 639-1 language codes (e.g., `de` for German, `fr` for French). | | `FORCED_LANGUAGE` | unset | If set, forces Anubis to display challenge pages in the specified language instead of using the browser's Accept-Language header. Use ISO 639-1 language codes (e.g., `de` for German, `fr` for French). |
| `HS512_SECRET` | unset | Secret string for JWT HS512 algorithm. If this is not set, Anubis will use ED25519 as defined via the variables above. The longer the better; 128 chars should suffice. **Required when using persistent storage backends** (like bbolt) to ensure challenges survive service restarts. When running multiple instances on the same base domain, the key must be the same across all instances. | | `HS512_SECRET` | unset | Secret string for JWT HS512 algorithm. If this is not set, Anubis will use ED25519 as defined via the variables above. The longer the better; 128 chars should suffice. **Required when using persistent storage backends** (like bbolt) to ensure challenges survive service restarts. When running multiple instances on the same base domain, the key must be the same across all instances. |
| `TARGET_DISABLE_KEEPALIVE` | `false` | If `true`, disables HTTP keep-alive for connections to the target backend. Useful for backends that don't handle keep-alive properly. | | `TARGET_DISABLE_KEEPALIVE` | `false` | If `true`, disables HTTP keep-alive for connections to the target backend. Useful for backends that don't handle keep-alive properly. |
| `TARGET_HOST` | unset | If set, overrides the Host header in requests forwarded to `TARGET`. | | `TARGET_HOST` | unset | If set, overrides the Host header in requests forwarded to `TARGET`. |
| `TARGET_INSECURE_SKIP_VERIFY` | `false` | If `true`, skip TLS certificate validation for targets that listen over `https`. If your backend does not listen over `https`, ignore this setting. | | `TARGET_INSECURE_SKIP_VERIFY` | `false` | If `true`, skip TLS certificate validation for targets that listen over `https`. If your backend does not listen over `https`, ignore this setting. |
| `TARGET_SNI` | unset | If set, overrides the TLS handshake hostname in requests forwarded to `TARGET`. | | `TARGET_SNI` | unset | If set, overrides the TLS handshake hostname in requests forwarded to `TARGET`. |
</details> </details>

View File

@@ -1,107 +0,0 @@
// Package actorify lets you transform a parallel operation into a serialized
// operation via the Actor pattern[1].
//
// [1]: https://en.wikipedia.org/wiki/Actor_model
package actorify
import (
"context"
"errors"
)
func z[Z any]() Z {
var z Z
return z
}
var (
// ErrActorDied is returned when the actor inbox or reply channel was closed.
ErrActorDied = errors.New("actorify: the actor inbox or reply channel was closed")
)
// Handler is a function alias for the underlying logic the Actor should call.
type Handler[Input, Output any] func(ctx context.Context, input Input) (Output, error)
// Actor is a serializing wrapper that runs a function in a background goroutine.
// Whenever the Call method is invoked, a message is sent to the actor's inbox and then
// the callee waits for a response. Depending on how busy the actor is, this may take
// a moment.
type Actor[Input, Output any] struct {
handler Handler[Input, Output]
inbox chan *message[Input, Output]
}
type message[Input, Output any] struct {
ctx context.Context
arg Input
reply chan reply[Output]
}
type reply[Output any] struct {
output Output
err error
}
// New constructs a new Actor and starts its background thread. Cancel the context and you cancel
// the Actor.
func New[Input, Output any](ctx context.Context, handler Handler[Input, Output]) *Actor[Input, Output] {
result := &Actor[Input, Output]{
handler: handler,
inbox: make(chan *message[Input, Output], 32),
}
go result.handle(ctx)
return result
}
func (a *Actor[Input, Output]) handle(ctx context.Context) {
for {
select {
case <-ctx.Done():
close(a.inbox)
return
case msg, ok := <-a.inbox:
if !ok {
if msg.reply != nil {
close(msg.reply)
}
return
}
result, err := a.handler(msg.ctx, msg.arg)
reply := reply[Output]{
output: result,
err: err,
}
msg.reply <- reply
}
}
}
// Call calls the Actor with a given Input and returns the handler's Output.
//
// This only works with unary functions by design. If you need to have more inputs, define
// a struct type to use as a container.
func (a *Actor[Input, Output]) Call(ctx context.Context, input Input) (Output, error) {
replyCh := make(chan reply[Output])
a.inbox <- &message[Input, Output]{
arg: input,
reply: replyCh,
}
select {
case reply, ok := <-replyCh:
if !ok {
return z[Output](), ErrActorDied
}
return reply.output, reply.err
case <-ctx.Done():
return z[Output](), context.Cause(ctx)
}
}

View File

@@ -501,12 +501,6 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
var tokenString string var tokenString string
// check if JWTRestrictionHeader is set and header is in request // check if JWTRestrictionHeader is set and header is in request
claims := jwt.MapClaims{
"challenge": chall.ID,
"method": rule.Challenge.Algorithm,
"policyRule": rule.Hash(),
"action": string(cr.Rule),
}
if s.opts.JWTRestrictionHeader != "" { if s.opts.JWTRestrictionHeader != "" {
if r.Header.Get(s.opts.JWTRestrictionHeader) == "" { if r.Header.Get(s.opts.JWTRestrictionHeader) == "" {
lg.Error("JWTRestrictionHeader is set in config but not found in request, please check your reverse proxy config.") lg.Error("JWTRestrictionHeader is set in config but not found in request, please check your reverse proxy config.")
@@ -514,13 +508,22 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
s.respondWithError(w, r, "failed to sign JWT") s.respondWithError(w, r, "failed to sign JWT")
return return
} else { } else {
claims["restriction"] = internal.SHA256sum(r.Header.Get(s.opts.JWTRestrictionHeader)) tokenString, err = s.signJWT(jwt.MapClaims{
"challenge": chall.ID,
"method": rule.Challenge.Algorithm,
"policyRule": rule.Hash(),
"action": string(cr.Rule),
"restriction": internal.SHA256sum(r.Header.Get(s.opts.JWTRestrictionHeader)),
})
} }
} else {
tokenString, err = s.signJWT(jwt.MapClaims{
"challenge": chall.ID,
"method": rule.Challenge.Algorithm,
"policyRule": rule.Hash(),
"action": string(cr.Rule),
})
} }
if s.opts.DifficultyInJWT {
claims["difficulty"] = rule.Challenge.Difficulty
}
tokenString, err = s.signJWT(claims)
if err != nil { if err != nil {
lg.Error("failed to sign JWT", "err", err) lg.Error("failed to sign JWT", "err", err)

View File

@@ -93,9 +93,9 @@ func page(redir string, difficulty int, loc *localization.SimpleLocalizer) templ
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var6 string var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d; url=%s", difficulty+1, redir)) templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d; url=%s", difficulty, redir))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 16, Col: 85} return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 16, Col: 83}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {

View File

@@ -40,9 +40,9 @@ for the JavaScript code in this page.
mkdir -p static/js mkdir -p static/js
for file in js/*.tsx; do for file in js/*.jsx; do
filename="${file##*/}" # Extracts "app.jsx" from "./js/app.jsx" filename="${file##*/}" # Extracts "app.jsx" from "./js/app.jsx"
output="${filename%.tsx}.js" # Changes "app.jsx" to "app.js" output="${filename%.jsx}.js" # Changes "app.jsx" to "app.js"
echo $output echo $output
esbuild "${file}" --minify --bundle --outfile=static/"${output}" --banner:js="${LICENSE}" esbuild "${file}" --minify --bundle --outfile=static/"${output}" --banner:js="${LICENSE}"

View File

@@ -0,0 +1,62 @@
import { render, h, Fragment } from 'preact';
import { useState, useEffect } from 'preact/hooks';
import { g, j, u, x } from "./xeact.js";
import { Sha256 } from '@aws-crypto/sha256-js';
/** @jsx h */
/** @jsxFrag Fragment */
function toHexString(arr) {
return Array.from(arr)
.map((c) => c.toString(16).padStart(2, "0"))
.join("");
}
const App = () => {
const [state, setState] = useState(null);
const [imageURL, setImageURL] = useState(null);
const [passed, setPassed] = useState(false);
const [challenge, setChallenge] = useState(null);
useEffect(() => {
setState(j("preact_info"));
});
useEffect(() => {
setImageURL(state.pensive_url);
const hash = new Sha256('');
hash.update(state.challenge);
setChallenge(toHexString(hash.digestSync()));
}, [state]);
useEffect(() => {
const timer = setTimeout(() => {
setPassed(true);
}, state.difficulty * 125);
return () => clearTimeout(timer);
}, [challenge]);
useEffect(() => {
window.location.href = u(state.redir, {
result: challenge,
});
}, [passed]);
return (
<>
{imageURL !== null && (
<img src={imageURL} style="width:100%;max-width:256px;" />
)}
{state !== null && (
<>
<p id="status">{state.loading_message}</p>
<p>{state.connection_security_message}</p>
</>
)}
</>
);
};
x(g("app"));
render(<App />, g("app"));

View File

@@ -1,87 +0,0 @@
import { render, h, Fragment } from "preact";
import { useState, useEffect } from "preact/hooks";
import { g, j, r, u, x } from "./xeact.js";
import { Sha256 } from "@aws-crypto/sha256-js";
/** @jsx h */
/** @jsxFrag Fragment */
function toHexString(arr: Uint8Array) {
return Array.from(arr)
.map((c) => c.toString(16).padStart(2, "0"))
.join("");
}
interface PreactInfo {
redir: string;
challenge: string;
difficulty: number;
connection_security_message: string;
loading_message: string;
pensive_url: string;
}
const App = () => {
const [state, setState] = useState<PreactInfo>();
const [imageURL, setImageURL] = useState<string | null>(null);
const [passed, setPassed] = useState<boolean>(false);
const [challenge, setChallenge] = useState<string | null>(null);
useEffect(() => {
setState(j("preact_info"));
});
useEffect(() => {
if (state === undefined) {
return;
}
setImageURL(state?.pensive_url);
const hash = new Sha256("");
hash.update(state.challenge);
setChallenge(toHexString(hash.digestSync()));
}, [state]);
useEffect(() => {
if (state === undefined) {
return;
}
const timer = setTimeout(() => {
setPassed(true);
}, state?.difficulty * 125);
return () => clearTimeout(timer);
}, [challenge]);
useEffect(() => {
if (state === undefined) {
return;
}
if (challenge === null) {
return;
}
window.location.href = u(state.redir, {
result: challenge,
});
}, [passed]);
return (
<>
{imageURL !== null && (
<img src={imageURL} style={{ width: "100%", maxWidth: "256px" }} />
)}
{state !== undefined && (
<>
<p id="status">{state.loading_message}</p>
<p>{state.connection_security_message}</p>
</>
)}
</>
);
};
x(g("app"));
render(<App />, g("app"));

View File

@@ -46,7 +46,6 @@ type Options struct {
Logger *slog.Logger Logger *slog.Logger
PublicUrl string PublicUrl string
JWTRestrictionHeader string JWTRestrictionHeader string
DifficultyInJWT bool
} }
func LoadPoliciesOrDefault(ctx context.Context, fname string, defaultDifficulty int) (*policy.ParsedConfig, error) { func LoadPoliciesOrDefault(ctx context.Context, fname string, defaultDifficulty int) (*policy.ParsedConfig, error) {

View File

@@ -62,6 +62,5 @@
"js_iterations": "iteracijų", "js_iterations": "iteracijų",
"js_finished_reading": "Viską perskaičiau, tęskime →", "js_finished_reading": "Viską perskaičiau, tęskime →",
"js_calculation_error": "Skaičiavimo klaida!", "js_calculation_error": "Skaičiavimo klaida!",
"js_calculation_error_msg": "Nepavyko įveikti iššūkio:", "js_calculation_error_msg": "Nepavyko įveikti iššūkio:"
"missing_required_forwarded_headers": "Trūksta privalomų X-Forwarded-* antraščių"
} }

View File

@@ -1,82 +0,0 @@
package store
import (
"context"
"time"
"github.com/TecharoHQ/anubis/internal/actorify"
)
type unit struct{}
type ActorifiedStore struct {
Interface
deleteActor *actorify.Actor[string, unit]
getActor *actorify.Actor[string, []byte]
setActor *actorify.Actor[*actorSetReq, unit]
cancel context.CancelFunc
}
type actorSetReq struct {
key string
value []byte
expiry time.Duration
}
func NewActorifiedStore(backend Interface) *ActorifiedStore {
ctx, cancel := context.WithCancel(context.Background())
result := &ActorifiedStore{
Interface: backend,
cancel: cancel,
}
result.deleteActor = actorify.New(ctx, result.actorDelete)
result.getActor = actorify.New(ctx, backend.Get)
result.setActor = actorify.New(ctx, result.actorSet)
return result
}
func (a *ActorifiedStore) Close() { a.cancel() }
func (a *ActorifiedStore) Delete(ctx context.Context, key string) error {
if _, err := a.deleteActor.Call(ctx, key); err != nil {
return err
}
return nil
}
func (a *ActorifiedStore) Get(ctx context.Context, key string) ([]byte, error) {
return a.getActor.Call(ctx, key)
}
func (a *ActorifiedStore) Set(ctx context.Context, key string, value []byte, expiry time.Duration) error {
if _, err := a.setActor.Call(ctx, &actorSetReq{
key: key,
value: value,
expiry: expiry,
}); err != nil {
return err
}
return nil
}
func (a *ActorifiedStore) actorDelete(ctx context.Context, key string) (unit, error) {
if err := a.Interface.Delete(ctx, key); err != nil {
return unit{}, err
}
return unit{}, nil
}
func (a *ActorifiedStore) actorSet(ctx context.Context, req *actorSetReq) (unit, error) {
if err := a.Interface.Set(ctx, req.key, req.value, req.expiry); err != nil {
return unit{}, err
}
return unit{}, nil
}

View File

@@ -48,7 +48,7 @@ func (Factory) Build(ctx context.Context, data json.RawMessage) (store.Interface
go result.cleanupThread(ctx) go result.cleanupThread(ctx)
return store.NewActorifiedStore(result), nil return result, nil
} }
// Valid parses and validates the bbolt store Config or returns // Valid parses and validates the bbolt store Config or returns

View File

@@ -39,18 +39,9 @@ for the JavaScript code in this page.
mkdir -p static/locales mkdir -p static/locales
cp ../lib/localization/locales/*.json static/locales/ cp ../lib/localization/locales/*.json static/locales/
shopt -s nullglob globstar for file in js/*.mjs js/worker/*.mjs; do
esbuild "${file}" --sourcemap --bundle --minify --outfile=static/"${file}" --banner:js="${LICENSE}"
for file in js/**/*.ts js/**/*.mjs; do gzip -f -k -n static/${file}
out="static/${file}" zstd -f -k --ultra -22 static/${file}
if [[ "$file" == *.ts ]]; then brotli -fZk static/${file}
out="static/${file%.ts}.mjs"
fi
mkdir -p "$(dirname "$out")"
esbuild "$file" --sourcemap --bundle --minify --outfile="$out" --banner:js="$LICENSE"
gzip -f -k -n "$out"
zstd -f -k --ultra -22 "$out"
brotli -fZk "$out"
done done

View File

@@ -1,21 +1,11 @@
type ProgressCallback = (nonce: number) => void;
interface ProcessOptions {
basePrefix: string;
version: string;
}
const getHardwareConcurrency = () =>
navigator.hardwareConcurrency !== undefined ? navigator.hardwareConcurrency : 1;
export default function process( export default function process(
options: ProcessOptions, { basePrefix, version },
data: string, data,
difficulty: number = 5, difficulty = 5,
signal: AbortSignal | null = null, signal = null,
progressCallback?: ProgressCallback, progressCallback = null,
threads: number = Math.trunc(Math.max(getHardwareConcurrency() / 2, 1)), threads = Math.trunc(Math.max(navigator.hardwareConcurrency / 2, 1)),
): Promise<string> { ) {
console.debug("fast algo"); console.debug("fast algo");
let workerMethod = window.crypto !== undefined ? "webcrypto" : "purejs"; let workerMethod = window.crypto !== undefined ? "webcrypto" : "purejs";
@@ -26,17 +16,13 @@ export default function process(
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let webWorkerURL = `${options.basePrefix}/.within.website/x/cmd/anubis/static/js/worker/sha256-${workerMethod}.mjs?cacheBuster=${options.version}`; let webWorkerURL = `${basePrefix}/.within.website/x/cmd/anubis/static/js/worker/sha256-${workerMethod}.mjs?cacheBuster=${version}`;
const workers: Worker[] = []; console.log(webWorkerURL);
const workers = [];
let settled = false; let settled = false;
const onAbort = () => {
console.log("PoW aborted");
cleanup();
reject(new DOMException("Aborted", "AbortError"));
};
const cleanup = () => { const cleanup = () => {
if (settled) { if (settled) {
return; return;
@@ -48,6 +34,12 @@ export default function process(
} }
}; };
const onAbort = () => {
console.log("PoW aborted");
cleanup();
reject(new DOMException("Aborted", "AbortError"));
};
if (signal != null) { if (signal != null) {
if (signal.aborted) { if (signal.aborted) {
return onAbort(); return onAbort();

View File

@@ -1,4 +1,4 @@
import fast from "./fast"; import fast from "./fast.mjs";
export default { export default {
fast: fast, fast: fast,

View File

@@ -1,24 +1,20 @@
import algorithms from "./algorithms"; import algorithms from "./algorithms/index.mjs";
const defaultDifficulty = 4; const defaultDifficulty = 4;
const status: HTMLParagraphElement = document.getElementById("status") as HTMLParagraphElement; const status = document.getElementById("status");
const difficultyInput: HTMLInputElement = document.getElementById("difficulty-input") as HTMLInputElement; const difficultyInput = document.getElementById("difficulty-input");
const algorithmSelect: HTMLSelectElement = document.getElementById("algorithm-select") as HTMLSelectElement; const algorithmSelect = document.getElementById("algorithm-select");
const compareSelect: HTMLSelectElement = document.getElementById("compare-select") as HTMLSelectElement; const compareSelect = document.getElementById("compare-select");
const header: HTMLTableRowElement = document.getElementById("table-header") as HTMLTableRowElement; const header = document.getElementById("table-header");
const headerCompare: HTMLTableSectionElement = document.getElementById("table-header-compare") as HTMLTableSectionElement; const headerCompare = document.getElementById("table-header-compare");
const results: HTMLTableRowElement = document.getElementById("results") as HTMLTableRowElement; const results = document.getElementById("results");
const setupControls = () => { const setupControls = () => {
if (defaultDifficulty == null) { difficultyInput.value = defaultDifficulty;
return;
}
difficultyInput.value = defaultDifficulty.toString();
for (const alg of Object.keys(algorithms)) { for (const alg of Object.keys(algorithms)) {
const option1 = document.createElement("option"); const option1 = document.createElement("option");
algorithmSelect?.append(option1); algorithmSelect.append(option1);
const option2 = document.createElement("option"); const option2 = document.createElement("option");
compareSelect.append(option2); compareSelect.append(option2);
option1.value = option1.innerText = option2.value = option2.innerText = alg; option1.value = option1.innerText = option2.value = option2.innerText = alg;
@@ -120,13 +116,13 @@ const benchmarkLoop = async (controller) => {
await benchmarkLoop(controller); await benchmarkLoop(controller);
}; };
let controller: AbortController | null = null; let controller = null;
const reset = () => { const reset = () => {
stats.time = stats.iters = 0; stats.time = stats.iters = 0;
comparison.time = comparison.iters = 0; comparison.time = comparison.iters = 0;
results.innerHTML = status.innerText = ""; results.innerHTML = status.innerText = "";
const table = results.parentElement as HTMLElement; const table = results.parentElement;
if (compareSelect.value !== "NONE") { if (compareSelect.value !== "NONE") {
table.style.gridTemplateColumns = "repeat(4,auto)"; table.style.gridTemplateColumns = "repeat(4,auto)";
header.style.display = "none"; header.style.display = "none";

View File

@@ -1,21 +1,12 @@
import algorithms from "./algorithms"; import algorithms from "./algorithms/index.mjs";
// from Xeact // from Xeact
const u = (url: string = "", params: Record<string, any> = {}) => { const u = (url = "", params = {}) => {
let result = new URL(url, window.location.href); let result = new URL(url, window.location.href);
Object.entries(params).forEach(([k, v]) => result.searchParams.set(k, v)); Object.entries(params).forEach(([k, v]) => result.searchParams.set(k, v));
return result.toString(); return result.toString();
}; };
const j = (id: string): any | null => {
const elem = document.getElementById(id);
if (elem === null) {
return null;
}
return JSON.parse(elem.textContent);
};
const imageURL = (mood, cacheBuster, basePrefix) => const imageURL = (mood, cacheBuster, basePrefix) =>
u(`${basePrefix}/.within.website/x/cmd/anubis/static/img/${mood}.webp`, { u(`${basePrefix}/.within.website/x/cmd/anubis/static/img/${mood}.webp`, {
cacheBuster, cacheBuster,
@@ -23,10 +14,9 @@ const imageURL = (mood, cacheBuster, basePrefix) =>
// Detect available languages by loading the manifest // Detect available languages by loading the manifest
const getAvailableLanguages = async () => { const getAvailableLanguages = async () => {
const basePrefix = j("anubis_base_prefix"); const basePrefix = JSON.parse(
if (basePrefix === null) { document.getElementById("anubis_base_prefix").textContent,
return; );
}
try { try {
const response = await fetch(`${basePrefix}/.within.website/x/cmd/anubis/static/locales/manifest.json`); const response = await fetch(`${basePrefix}/.within.website/x/cmd/anubis/static/locales/manifest.json`);
@@ -48,11 +38,9 @@ const getBrowserLanguage = async () =>
// Load translations from JSON files // Load translations from JSON files
const loadTranslations = async (lang) => { const loadTranslations = async (lang) => {
const basePrefix = j("anubis_base_prefix"); const basePrefix = JSON.parse(
if (basePrefix === null) { document.getElementById("anubis_base_prefix").textContent,
return; );
}
try { try {
const response = await fetch(`${basePrefix}/.within.website/x/cmd/anubis/static/locales/${lang}.json`); const response = await fetch(`${basePrefix}/.within.website/x/cmd/anubis/static/locales/${lang}.json`);
return await response.json(); return await response.json();
@@ -66,10 +54,9 @@ const loadTranslations = async (lang) => {
}; };
const getRedirectUrl = () => { const getRedirectUrl = () => {
const publicUrl = j("anubis_public_url"); const publicUrl = JSON.parse(
if (publicUrl === null) { document.getElementById("anubis_public_url").textContent,
return; );
}
if (publicUrl && window.location.href.startsWith(publicUrl)) { if (publicUrl && window.location.href.startsWith(publicUrl)) {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
return urlParams.get('redir'); return urlParams.get('redir');
@@ -104,14 +91,16 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key;
value: navigator.cookieEnabled, value: navigator.cookieEnabled,
}, },
]; ];
const status = document.getElementById("status");
const status: HTMLParagraphElement = document.getElementById("status") as HTMLParagraphElement; const image = document.getElementById("image");
const image: HTMLImageElement = document.getElementById("image") as HTMLImageElement; const title = document.getElementById("title");
const title: HTMLHeadingElement = document.getElementById("title") as HTMLHeadingElement; const progress = document.getElementById("progress");
const progress: HTMLDivElement = document.getElementById("progress") as HTMLDivElement; const anubisVersion = JSON.parse(
document.getElementById("anubis_version").textContent,
const anubisVersion = j("anubis_version"); );
const basePrefix = j("anubis_base_prefix"); const basePrefix = JSON.parse(
document.getElementById("anubis_base_prefix").textContent,
);
const details = document.querySelector("details"); const details = document.querySelector("details");
let userReadDetails = false; let userReadDetails = false;
@@ -143,7 +132,9 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key;
} }
} }
const { challenge, rules } = j("anubis_challenge"); const { challenge, rules } = JSON.parse(
document.getElementById("anubis_challenge").textContent,
);
const process = algorithms[rules.algorithm]; const process = algorithms[rules.algorithm];
if (!process) { if (!process) {
@@ -191,9 +182,7 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key;
const probability = Math.pow(1 - likelihood, iters); const probability = Math.pow(1 - likelihood, iters);
const distance = (1 - Math.pow(probability, 2)) * 100; const distance = (1 - Math.pow(probability, 2)) * 100;
progress["aria-valuenow"] = distance; progress["aria-valuenow"] = distance;
if (progress.firstElementChild !== null) { progress.firstElementChild.style.width = `${distance}%`;
(progress.firstElementChild as HTMLElement).style.width = `${distance}%`;
}
if (probability < 0.1 && !showingApology) { if (probability < 0.1 && !showingApology) {
status.append( status.append(
@@ -208,7 +197,7 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key;
console.log({ hash, nonce }); console.log({ hash, nonce });
if (userReadDetails) { if (userReadDetails) {
const container: HTMLDivElement = document.getElementById("progress") as HTMLDivElement; const container = document.getElementById("progress");
// Style progress bar as a continue button // Style progress bar as a continue button
container.style.display = "flex"; container.style.display = "flex";

View File

@@ -6,7 +6,7 @@ const calculateSHA256 = (text) => {
return hash.digest(); return hash.digest();
}; };
function toHexString(arr: Uint8Array): string { function toHexString(arr) {
return Array.from(arr) return Array.from(arr)
.map((c) => c.toString(16).padStart(2, "0")) .map((c) => c.toString(16).padStart(2, "0"))
.join(""); .join("");

View File

@@ -1,11 +1,10 @@
const encoder = new TextEncoder(); const encoder = new TextEncoder();
const calculateSHA256 = async (input) => {
const calculateSHA256 = async (input: string) => {
const data = encoder.encode(input); const data = encoder.encode(input);
return await crypto.subtle.digest("SHA-256", data); return await crypto.subtle.digest("SHA-256", data);
}; };
const toHexString = (byteArray: Uint8Array) => { const toHexString = (byteArray) => {
return byteArray.reduce((str, byte) => str + byte.toString(16).padStart(2, "0"), ""); return byteArray.reduce((str, byte) => str + byte.toString(16).padStart(2, "0"), "");
}; };