mirror of
https://github.com/TecharoHQ/anubis.git
synced 2026-04-10 10:38:45 +00:00
Compare commits
9 Commits
Xe/contain
...
Xe/actorif
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63d557b96e | ||
|
|
d718792881 | ||
|
|
b28840f1a9 | ||
|
|
63591866aa | ||
|
|
f79d36d21e | ||
|
|
f5b5243b5e | ||
|
|
2011b83a44 | ||
|
|
8ed89a6c6e | ||
|
|
9430d0e6a5 |
6
.github/actions/spelling/expect.txt
vendored
6
.github/actions/spelling/expect.txt
vendored
@@ -1,4 +1,7 @@
|
|||||||
acs
|
acs
|
||||||
|
Actorified
|
||||||
|
actorifiedstore
|
||||||
|
actorify
|
||||||
Aibrew
|
Aibrew
|
||||||
alibaba
|
alibaba
|
||||||
alrest
|
alrest
|
||||||
@@ -157,6 +160,7 @@ ifm
|
|||||||
Imagesift
|
Imagesift
|
||||||
imgproxy
|
imgproxy
|
||||||
impressum
|
impressum
|
||||||
|
inbox
|
||||||
inp
|
inp
|
||||||
internets
|
internets
|
||||||
IPTo
|
IPTo
|
||||||
@@ -214,6 +218,7 @@ nicksnyder
|
|||||||
nobots
|
nobots
|
||||||
NONINFRINGEMENT
|
NONINFRINGEMENT
|
||||||
nosleep
|
nosleep
|
||||||
|
nullglob
|
||||||
OCOB
|
OCOB
|
||||||
ogtag
|
ogtag
|
||||||
oklch
|
oklch
|
||||||
@@ -278,6 +283,7 @@ Seo
|
|||||||
setsebool
|
setsebool
|
||||||
shellcheck
|
shellcheck
|
||||||
shirou
|
shirou
|
||||||
|
shopt
|
||||||
Sidetrade
|
Sidetrade
|
||||||
simprint
|
simprint
|
||||||
sitemap
|
sitemap
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ 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")
|
||||||
@@ -433,6 +434,7 @@ 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)
|
||||||
|
|||||||
@@ -14,6 +14,12 @@ 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 {
|
||||||
@@ -21,30 +27,38 @@ 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] {
|
||||||
return &Impl[K, V]{
|
m := &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 {
|
||||||
m.lock.RLock()
|
// Use a single write lock to avoid RUnlock->Lock convoy.
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,19 +67,14 @@ 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 {
|
||||||
m.lock.RLock()
|
// Use a single write lock to avoid RUnlock->Lock convoy.
|
||||||
_, ok := m.data[key]
|
|
||||||
m.lock.RUnlock()
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
m.lock.Lock()
|
m.lock.Lock()
|
||||||
delete(m.data, key)
|
defer m.lock.Unlock()
|
||||||
m.lock.Unlock()
|
_, ok := m.data[key]
|
||||||
|
if ok {
|
||||||
return true
|
delete(m.data, key)
|
||||||
|
}
|
||||||
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get gets a value from the DecayMap by key.
|
// Get gets a value from the DecayMap by key.
|
||||||
@@ -81,13 +90,12 @@ func (m *Impl[K, V]) Get(key K) (V, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if time.Now().After(value.expiry) {
|
if time.Now().After(value.expiry) {
|
||||||
m.lock.Lock()
|
// Defer decay deletion to the background worker to avoid convoy.
|
||||||
// Since previously reading m.data[key], the value may have been updated.
|
select {
|
||||||
// Delete the entry only if the expiry time is still the same.
|
case m.deleteCh <- deleteReq[K]{key: key, expiry: value.expiry}:
|
||||||
if m.data[key].expiry.Equal(value.expiry) {
|
default:
|
||||||
delete(m.data, key)
|
// Channel full: drop request; a future Cleanup() or Get will retry.
|
||||||
}
|
}
|
||||||
m.lock.Unlock()
|
|
||||||
|
|
||||||
return Zilch[V](), false
|
return Zilch[V](), false
|
||||||
}
|
}
|
||||||
@@ -125,3 +133,64 @@ 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()
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ 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)
|
||||||
|
|
||||||
@@ -28,10 +29,24 @@ 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)
|
||||||
|
|||||||
@@ -13,11 +13,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
<!-- This changes the project to: -->
|
<!-- This changes the project to: -->
|
||||||
|
|
||||||
- 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 decaymap ([#1103](https://github.com/TecharoHQ/anubis/issues/1103)).
|
||||||
- Add validation warning when persistent storage is used without setting signing keys
|
- Fix lock convoy problem in bbolt by implementing the actor pattern ([#1103](https://github.com/TecharoHQ/anubis/issues/1103)).
|
||||||
- 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))
|
- 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)).
|
||||||
|
- 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
|
||||||
|
|
||||||
|
|||||||
@@ -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,6 +70,7 @@ 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`. |
|
||||||
@@ -100,14 +101,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>
|
||||||
|
|
||||||
|
|||||||
107
internal/actorify/actorify.go
Normal file
107
internal/actorify/actorify.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -501,6 +501,12 @@ 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.")
|
||||||
@@ -508,22 +514,13 @@ 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 {
|
||||||
tokenString, err = s.signJWT(jwt.MapClaims{
|
claims["restriction"] = internal.SHA256sum(r.Header.Get(s.opts.JWTRestrictionHeader))
|
||||||
"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)
|
||||||
|
|||||||
4
lib/challenge/metarefresh/metarefresh_templ.go
generated
4
lib/challenge/metarefresh/metarefresh_templ.go
generated
@@ -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, redir))
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d; url=%s", difficulty+1, redir))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 16, Col: 83}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 16, Col: 85}
|
||||||
}
|
}
|
||||||
_, 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 {
|
||||||
|
|||||||
@@ -40,9 +40,9 @@ for the JavaScript code in this page.
|
|||||||
|
|
||||||
mkdir -p static/js
|
mkdir -p static/js
|
||||||
|
|
||||||
for file in js/*.jsx; do
|
for file in js/*.tsx; do
|
||||||
filename="${file##*/}" # Extracts "app.jsx" from "./js/app.jsx"
|
filename="${file##*/}" # Extracts "app.jsx" from "./js/app.jsx"
|
||||||
output="${filename%.jsx}.js" # Changes "app.jsx" to "app.js"
|
output="${filename%.tsx}.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}"
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
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"));
|
|
||||||
87
lib/challenge/preact/js/app.tsx
Normal file
87
lib/challenge/preact/js/app.tsx
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
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"));
|
||||||
@@ -46,6 +46,7 @@ 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) {
|
||||||
|
|||||||
@@ -62,5 +62,6 @@
|
|||||||
"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ų"
|
||||||
}
|
}
|
||||||
|
|||||||
82
lib/store/actorifiedstore.go
Normal file
82
lib/store/actorifiedstore.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -48,7 +48,7 @@ func (Factory) Build(ctx context.Context, data json.RawMessage) (store.Interface
|
|||||||
|
|
||||||
go result.cleanupThread(ctx)
|
go result.cleanupThread(ctx)
|
||||||
|
|
||||||
return result, nil
|
return store.NewActorifiedStore(result), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Valid parses and validates the bbolt store Config or returns
|
// Valid parses and validates the bbolt store Config or returns
|
||||||
|
|||||||
19
web/build.sh
19
web/build.sh
@@ -39,9 +39,18 @@ 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/
|
||||||
|
|
||||||
for file in js/*.mjs js/worker/*.mjs; do
|
shopt -s nullglob globstar
|
||||||
esbuild "${file}" --sourcemap --bundle --minify --outfile=static/"${file}" --banner:js="${LICENSE}"
|
|
||||||
gzip -f -k -n static/${file}
|
for file in js/**/*.ts js/**/*.mjs; do
|
||||||
zstd -f -k --ultra -22 static/${file}
|
out="static/${file}"
|
||||||
brotli -fZk static/${file}
|
if [[ "$file" == *.ts ]]; then
|
||||||
|
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
|
||||||
|
|||||||
@@ -1,11 +1,21 @@
|
|||||||
|
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(
|
||||||
{ basePrefix, version },
|
options: ProcessOptions,
|
||||||
data,
|
data: string,
|
||||||
difficulty = 5,
|
difficulty: number = 5,
|
||||||
signal = null,
|
signal: AbortSignal | null = null,
|
||||||
progressCallback = null,
|
progressCallback?: ProgressCallback,
|
||||||
threads = Math.trunc(Math.max(navigator.hardwareConcurrency / 2, 1)),
|
threads: number = Math.trunc(Math.max(getHardwareConcurrency() / 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";
|
||||||
@@ -16,13 +26,17 @@ export default function process(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let webWorkerURL = `${basePrefix}/.within.website/x/cmd/anubis/static/js/worker/sha256-${workerMethod}.mjs?cacheBuster=${version}`;
|
let webWorkerURL = `${options.basePrefix}/.within.website/x/cmd/anubis/static/js/worker/sha256-${workerMethod}.mjs?cacheBuster=${options.version}`;
|
||||||
|
|
||||||
console.log(webWorkerURL);
|
const workers: Worker[] = [];
|
||||||
|
|
||||||
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;
|
||||||
@@ -34,12 +48,6 @@ 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();
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import fast from "./fast.mjs";
|
import fast from "./fast";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
fast: fast,
|
fast: fast,
|
||||||
@@ -1,20 +1,24 @@
|
|||||||
import algorithms from "./algorithms/index.mjs";
|
import algorithms from "./algorithms";
|
||||||
|
|
||||||
const defaultDifficulty = 4;
|
const defaultDifficulty = 4;
|
||||||
|
|
||||||
const status = document.getElementById("status");
|
const status: HTMLParagraphElement = document.getElementById("status") as HTMLParagraphElement;
|
||||||
const difficultyInput = document.getElementById("difficulty-input");
|
const difficultyInput: HTMLInputElement = document.getElementById("difficulty-input") as HTMLInputElement;
|
||||||
const algorithmSelect = document.getElementById("algorithm-select");
|
const algorithmSelect: HTMLSelectElement = document.getElementById("algorithm-select") as HTMLSelectElement;
|
||||||
const compareSelect = document.getElementById("compare-select");
|
const compareSelect: HTMLSelectElement = document.getElementById("compare-select") as HTMLSelectElement;
|
||||||
const header = document.getElementById("table-header");
|
const header: HTMLTableRowElement = document.getElementById("table-header") as HTMLTableRowElement;
|
||||||
const headerCompare = document.getElementById("table-header-compare");
|
const headerCompare: HTMLTableSectionElement = document.getElementById("table-header-compare") as HTMLTableSectionElement;
|
||||||
const results = document.getElementById("results");
|
const results: HTMLTableRowElement = document.getElementById("results") as HTMLTableRowElement;
|
||||||
|
|
||||||
const setupControls = () => {
|
const setupControls = () => {
|
||||||
difficultyInput.value = defaultDifficulty;
|
if (defaultDifficulty == null) {
|
||||||
|
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;
|
||||||
@@ -116,13 +120,13 @@ const benchmarkLoop = async (controller) => {
|
|||||||
await benchmarkLoop(controller);
|
await benchmarkLoop(controller);
|
||||||
};
|
};
|
||||||
|
|
||||||
let controller = null;
|
let controller: AbortController | null = 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;
|
const table = results.parentElement as HTMLElement;
|
||||||
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";
|
||||||
@@ -1,12 +1,21 @@
|
|||||||
import algorithms from "./algorithms/index.mjs";
|
import algorithms from "./algorithms";
|
||||||
|
|
||||||
// from Xeact
|
// from Xeact
|
||||||
const u = (url = "", params = {}) => {
|
const u = (url: string = "", params: Record<string, any> = {}) => {
|
||||||
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,
|
||||||
@@ -14,9 +23,10 @@ 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 = JSON.parse(
|
const basePrefix = j("anubis_base_prefix");
|
||||||
document.getElementById("anubis_base_prefix").textContent,
|
if (basePrefix === null) {
|
||||||
);
|
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`);
|
||||||
@@ -38,9 +48,11 @@ const getBrowserLanguage = async () =>
|
|||||||
|
|
||||||
// Load translations from JSON files
|
// Load translations from JSON files
|
||||||
const loadTranslations = async (lang) => {
|
const loadTranslations = async (lang) => {
|
||||||
const basePrefix = JSON.parse(
|
const basePrefix = j("anubis_base_prefix");
|
||||||
document.getElementById("anubis_base_prefix").textContent,
|
if (basePrefix === null) {
|
||||||
);
|
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();
|
||||||
@@ -54,9 +66,10 @@ const loadTranslations = async (lang) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getRedirectUrl = () => {
|
const getRedirectUrl = () => {
|
||||||
const publicUrl = JSON.parse(
|
const publicUrl = j("anubis_public_url");
|
||||||
document.getElementById("anubis_public_url").textContent,
|
if (publicUrl === null) {
|
||||||
);
|
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');
|
||||||
@@ -91,16 +104,14 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key;
|
|||||||
value: navigator.cookieEnabled,
|
value: navigator.cookieEnabled,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const status = document.getElementById("status");
|
|
||||||
const image = document.getElementById("image");
|
const status: HTMLParagraphElement = document.getElementById("status") as HTMLParagraphElement;
|
||||||
const title = document.getElementById("title");
|
const image: HTMLImageElement = document.getElementById("image") as HTMLImageElement;
|
||||||
const progress = document.getElementById("progress");
|
const title: HTMLHeadingElement = document.getElementById("title") as HTMLHeadingElement;
|
||||||
const anubisVersion = JSON.parse(
|
const progress: HTMLDivElement = document.getElementById("progress") as HTMLDivElement;
|
||||||
document.getElementById("anubis_version").textContent,
|
|
||||||
);
|
const anubisVersion = j("anubis_version");
|
||||||
const basePrefix = JSON.parse(
|
const basePrefix = j("anubis_base_prefix");
|
||||||
document.getElementById("anubis_base_prefix").textContent,
|
|
||||||
);
|
|
||||||
const details = document.querySelector("details");
|
const details = document.querySelector("details");
|
||||||
let userReadDetails = false;
|
let userReadDetails = false;
|
||||||
|
|
||||||
@@ -132,9 +143,7 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { challenge, rules } = JSON.parse(
|
const { challenge, rules } = j("anubis_challenge");
|
||||||
document.getElementById("anubis_challenge").textContent,
|
|
||||||
);
|
|
||||||
|
|
||||||
const process = algorithms[rules.algorithm];
|
const process = algorithms[rules.algorithm];
|
||||||
if (!process) {
|
if (!process) {
|
||||||
@@ -182,7 +191,9 @@ 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;
|
||||||
progress.firstElementChild.style.width = `${distance}%`;
|
if (progress.firstElementChild !== null) {
|
||||||
|
(progress.firstElementChild as HTMLElement).style.width = `${distance}%`;
|
||||||
|
}
|
||||||
|
|
||||||
if (probability < 0.1 && !showingApology) {
|
if (probability < 0.1 && !showingApology) {
|
||||||
status.append(
|
status.append(
|
||||||
@@ -197,7 +208,7 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key;
|
|||||||
console.log({ hash, nonce });
|
console.log({ hash, nonce });
|
||||||
|
|
||||||
if (userReadDetails) {
|
if (userReadDetails) {
|
||||||
const container = document.getElementById("progress");
|
const container: HTMLDivElement = document.getElementById("progress") as HTMLDivElement;
|
||||||
|
|
||||||
// Style progress bar as a continue button
|
// Style progress bar as a continue button
|
||||||
container.style.display = "flex";
|
container.style.display = "flex";
|
||||||
@@ -6,7 +6,7 @@ const calculateSHA256 = (text) => {
|
|||||||
return hash.digest();
|
return hash.digest();
|
||||||
};
|
};
|
||||||
|
|
||||||
function toHexString(arr) {
|
function toHexString(arr: Uint8Array): string {
|
||||||
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("");
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
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) => {
|
const toHexString = (byteArray: Uint8Array) => {
|
||||||
return byteArray.reduce((str, byte) => str + byte.toString(16).padStart(2, "0"), "");
|
return byteArray.reduce((str, byte) => str + byte.toString(16).padStart(2, "0"), "");
|
||||||
};
|
};
|
||||||
|
|
||||||
Reference in New Issue
Block a user