Compare commits

...

9 Commits

Author SHA1 Message Date
Xe Iaso
be42c5accf fix(lib/store/bbolt): run cleanup every hour instead of every 5 minutes
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-07-06 01:15:11 +00:00
Xe Iaso
7d0c58d1a8 fix: make ogtags and dnsbl use the Store instead of memory (#760)
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-07-05 16:17:46 -04:00
Xe Iaso
e870ede120 docs(known-instances): add git.aya.so
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-07-05 16:41:36 +00:00
Xe Iaso
592d1e3dfc docs(known-instances): add Pluralpedia
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-07-05 16:40:32 +00:00
Xe Iaso
f6254b4b98 docs(installation): clarify BASE_PREFIX matches the /.within.website endpoints
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-07-05 13:47:02 +00:00
Lothar Serra Mari
d19026d693 docs(known-instances): Add Duke University, coinhoards.org (and myself) to known instances (#757)
* docs(known-instances): add Duke University to known instances

Signed-off-by: Lothar Serra Mari <mail@serra.me>

* docs(known-instances): add fabulous.systems to known instances

Signed-off-by: Lothar Serra Mari <mail@serra.me>

* docs(known-instances): add coinhoards.org to known instances

Signed-off-by: Lothar Serra Mari <mail@serra.me>

* chore(spelling): exempt the known instances page

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Lothar Serra Mari <mail@serra.me>
Signed-off-by: Xe Iaso <me@xeiaso.net>
Co-authored-by: Xe Iaso <me@xeiaso.net>
2025-07-05 08:29:44 -04:00
Xe Iaso
7b72c790ab chore: spelling
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-07-05 12:29:19 +00:00
Xe Iaso
719a1409ca test(lib/store/bbolt): disable this test case for now
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-07-05 04:56:19 +00:00
Xe Iaso
890f21bf47 chore(devcontainer): move playwright to its own devcontainer service (#756)
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-07-05 00:53:45 -04:00
27 changed files with 161 additions and 163 deletions

View File

@@ -3,9 +3,7 @@ FROM ghcr.io/xe/devcontainer-base/pre/go
WORKDIR /app WORKDIR /app
COPY go.mod go.sum package.json package-lock.json ./ COPY go.mod go.sum package.json package-lock.json ./
RUN go install github.com/a-h/templ/cmd/templ \ RUN apt-get update \
&& npx --yes playwright@1.52.0 install --with-deps\
&& apt-get update \
&& apt-get -y install zstd brotli redis \ && apt-get -y install zstd brotli redis \
&& mkdir -p /home/vscode/.local/share/fish \ && mkdir -p /home/vscode/.local/share/fish \
&& chown -R vscode:vscode /home/vscode/.local/share/fish \ && chown -R vscode:vscode /home/vscode/.local/share/fish \

View File

@@ -2,14 +2,6 @@
// README at: https://github.com/devcontainers/templates/tree/main/src/debian // README at: https://github.com/devcontainers/templates/tree/main/src/debian
{ {
"name": "Dev", "name": "Dev",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
// "build": {
// "dockerfile": "./Dockerfile",
// "context": "..",
// "cacheFrom": [
// "type=registry,ref=ghcr.io/techarohq/anubis/devcontainer"
// ]
// },
"dockerComposeFile": ["./docker-compose.yaml"], "dockerComposeFile": ["./docker-compose.yaml"],
"service": "workspace", "service": "workspace",
"workspaceFolder": "/workspace/anubis", "workspaceFolder": "/workspace/anubis",

View File

@@ -1,4 +1,13 @@
services: services:
playwright:
image: mcr.microsoft.com/playwright:v1.52.0-noble
init: true
network_mode: service:workspace
command:
- /bin/sh
- -c
- npx -y playwright@1.52.0 run-server --port 9001 --host 0.0.0.0
valkey: valkey:
image: valkey/valkey:8 image: valkey/valkey:8
pull_policy: always pull_policy: always
@@ -9,8 +18,6 @@ services:
build: build:
context: .. context: ..
dockerfile: .devcontainer/Dockerfile dockerfile: .devcontainer/Dockerfile
cache_from:
- "type=registry,ref=ghcr.io/techarohq/anubis/devcontainer"
volumes: volumes:
- ../:/workspace/anubis:cached - ../:/workspace/anubis:cached
environment: environment:

View File

@@ -84,6 +84,7 @@
^\Q.github/workflows/spelling.yml\E$ ^\Q.github/workflows/spelling.yml\E$
^data/crawlers/ ^data/crawlers/
^docs/blog/tags\.yml$ ^docs/blog/tags\.yml$
^docs/docs/user/known-instances.md$
^docs/manifest/.*$ ^docs/manifest/.*$
^docs/static/\.nojekyll$ ^docs/static/\.nojekyll$
^lib/policy/config/testdata/bad/unparseable\.json$ ^lib/policy/config/testdata/bad/unparseable\.json$

View File

@@ -20,7 +20,7 @@ bbolt
bdba bdba
berr berr
bingbot bingbot
bitcoin Bitcoin
bitrate bitrate
blogging blogging
Bluesky Bluesky
@@ -130,7 +130,6 @@ Hashcash
hashrate hashrate
headermap headermap
healthcheck healthcheck
hebis
hec hec
hmc hmc
hostable hostable
@@ -250,7 +249,6 @@ RUnlock
runtimedir runtimedir
sas sas
sasl sasl
Scumm
searchbot searchbot
searx searx
sebest sebest
@@ -263,10 +261,8 @@ shellcheck
Sidetrade Sidetrade
simprint simprint
sitemap sitemap
skopeo
sls sls
sni sni
Sourceware
Spambot Spambot
sparkline sparkline
spyderbot spyderbot
@@ -289,7 +285,6 @@ techarohq
templ templ
templruntime templruntime
testarea testarea
testdb
Thancred Thancred
thoth thoth
thothmock thothmock

View File

@@ -1,47 +0,0 @@
name: Dev container prebuild
on:
push:
branches: ["main"]
tags: ["v*.*.*"]
jobs:
devcontainer:
runs-on: ubuntu-24.04
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-tags: true
fetch-depth: 0
persist-credentials: false
- name: Set up QEMU
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: latest
- run: |
sudo apt-get update
sudo apt-get -y install skopeo
- name: Log into registry
if: github.event_name != 'pull_request'
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
with:
registry: ghcr.io
username: techarohq
password: ${{ secrets.GITHUB_TOKEN }}
- name: Pre-build dev container image
uses: devcontainers/ci@8bf61b26e9c3a98f69cb6ce2f88d24ff59b785c6 # v0.3.1900000417
with:
imageName: ghcr.io/techarohq/anubis/devcontainer
cacheFrom: ghcr.io/techarohq/anubis/devcontainer
push: always
platform: linux/amd64,linux/arm64

View File

@@ -231,20 +231,6 @@ func makeReverseProxy(target string, targetSNI string, targetHost string, insecu
return rp, nil return rp, nil
} }
func startDecayMapCleanup(ctx context.Context, s *libanubis.Server) {
ticker := time.NewTicker(1 * time.Hour)
defer ticker.Stop()
for {
select {
case <-ticker.C:
s.CleanupDecayMap()
case <-ctx.Done():
return
}
}
}
func main() { func main() {
flagenv.Parse() flagenv.Parse()
flag.Parse() flag.Parse()
@@ -421,7 +407,6 @@ func main() {
wg.Add(1) wg.Add(1)
go metricsServer(ctx, wg.Done) go metricsServer(ctx, wg.Done)
} }
go startDecayMapCleanup(ctx, s)
var h http.Handler var h http.Handler
h = s h = s

View File

@@ -24,9 +24,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Remove the "Success" interstitial after a proof of work challenge is concluded. - Remove the "Success" interstitial after a proof of work challenge is concluded.
- Anubis now has the concept of [storage backends](./admin/policies.mdx#storage-backends). These allow you to change how Anubis stores temporary data (in memory, on the disk, or in Valkey). If you run Anubis in an environment where you have a low amount of memory available for Anubis (eg: less than 64 megabytes), be sure to configure the [`bbolt`](./admin/policies.mdx#bbolt) storage backend. - Anubis now has the concept of [storage backends](./admin/policies.mdx#storage-backends). These allow you to change how Anubis stores temporary data (in memory, on the disk, or in Valkey). If you run Anubis in an environment where you have a low amount of memory available for Anubis (eg: less than 64 megabytes), be sure to configure the [`bbolt`](./admin/policies.mdx#bbolt) storage backend.
- The challenge issuance and validation process has been rewritten from scratch. Instead of generating challenge strings from request metadata (under the assumption that the values being compared against are stable), Anubis now generates random data for each challenge. This data is stored in the active [storage backend](./admin/policies.mdx#storage-backends) for up to 30 minutes. Fixes [#564](https://github.com/TecharoHQ/anubis/issues/564), [#746](https://github.com/TecharoHQ/anubis/issues/746), and other similar instances of this issue. - The challenge issuance and validation process has been rewritten from scratch. Instead of generating challenge strings from request metadata (under the assumption that the values being compared against are stable), Anubis now generates random data for each challenge. This data is stored in the active [storage backend](./admin/policies.mdx#storage-backends) for up to 30 minutes. Fixes [#564](https://github.com/TecharoHQ/anubis/issues/564), [#746](https://github.com/TecharoHQ/anubis/issues/746), and other similar instances of this issue.
- Make the [Open Graph](./admin/configuration/open-graph.mdx) subsystem and DNSBL subsystem use [storage backends](./admin/policies.mdx#storage-backends) instead of storing everything in memory by default.
- Add option for forcing a specific language ([#742](https://github.com/TecharoHQ/anubis/pull/742)) - Add option for forcing a specific language ([#742](https://github.com/TecharoHQ/anubis/pull/742))
- Add translation for Turkish language ([#751](https://github.com/TecharoHQ/anubis/pull/751)) - Add translation for Turkish language ([#751](https://github.com/TecharoHQ/anubis/pull/751))
- Allow [Common Crawl](https://commoncrawl.org/) by default so scrapers have less incentive to scrape - Allow [Common Crawl](https://commoncrawl.org/) by default so scrapers have less incentive to scrape
- The [bbolt storage backend](./admin/policies.mdx#bbolt) now runs its cleanup every hour instead of every five minutes.
### Potentially breaking changes ### Potentially breaking changes

View File

@@ -60,14 +60,14 @@ 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. 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. |
| `COOKIE_DOMAIN` | unset | The domain the Anubis challenge pass cookie should be set to. This should be set to the domain you bought from your registrar (EG: `techaro.lol` if your webapp is running on `anubis.techaro.lol`). See this [stackoverflow explanation of cookies](https://stackoverflow.com/a/1063760) for more information.<br/><br/>Note that unlike `REDIRECT_DOMAINS`, you should never include a port number in this variable. | | `COOKIE_DOMAIN` | unset | The domain the Anubis challenge pass cookie should be set to. This should be set to the domain you bought from your registrar (EG: `techaro.lol` if your webapp is running on `anubis.techaro.lol`). See this [stackoverflow explanation of cookies](https://stackoverflow.com/a/1063760) for more information.<br/><br/>Note that unlike `REDIRECT_DOMAINS`, you should never include a port number in this variable. |
| `COOKIE_DYNAMIC_DOMAIN` | false | If set to true, automatically set cookie domain fields based on the hostname of the request. EG: if you are making a request to `anubis.techaro.lol`, the Anubis cookie will be valid for any subdomain of `techaro.lol`. | | `COOKIE_DYNAMIC_DOMAIN` | false | If set to true, automatically set cookie domain fields based on the hostname of the request. EG: if you are making a request to `anubis.techaro.lol`, the Anubis cookie will be valid for any subdomain of `techaro.lol`. |
| `COOKIE_EXPIRATION_TIME` | `168h` | The amount of time the authorization cookie is valid for. | | `COOKIE_EXPIRATION_TIME` | `168h` | The amount of time the authorization cookie is valid for. |
| `COOKIE_PARTITIONED` | `false` | If set to `true`, enables the [partitioned (CHIPS) flag](https://developers.google.com/privacy-sandbox/cookies/chips), meaning that Anubis inside an iframe has a different set of cookies than the domain hosting the iframe. | | `COOKIE_PARTITIONED` | `false` | If set to `true`, enables the [partitioned (CHIPS) flag](https://developers.google.com/privacy-sandbox/cookies/chips), meaning that Anubis inside an iframe has a different set of cookies than the domain hosting the iframe. |
| `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. |
| `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. 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. 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. | | `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. |

View File

@@ -46,6 +46,10 @@ This page contains a non-exhaustive list with all websites using Anubis.
- https://wiki.koha-community.org/ - https://wiki.koha-community.org/
- https://extensions.typo3.org/ - https://extensions.typo3.org/
- https://ebird.org/ - https://ebird.org/
- https://fabulous.systems/
- https://coinhoards.org/
- https://pluralpedia.org/
- https://git.aya.so/
- <details> - <details>
<summary>FreeCAD</summary> <summary>FreeCAD</summary>
- https://forum.freecad.org/ - https://forum.freecad.org/
@@ -83,3 +87,10 @@ This page contains a non-exhaustive list with all websites using Anubis.
- https://karla.hds.hebis.de/ - https://karla.hds.hebis.de/
- and many more (see https://www.hebis.de/dienste/hebis-discovery-system/) - and many more (see https://www.hebis.de/dienste/hebis-discovery-system/)
</details> </details>
- <details>
<summary>Duke University</summary>
- https://repository.duke.edu/
- https://archives.lib.duke.edu/
- https://find.library.duke.edu/
- https://nicholas.duke.edu/
</details>

View File

@@ -1,6 +1,7 @@
package ogtags package ogtags
import ( import (
"context"
"errors" "errors"
"log/slog" "log/slog"
"net/url" "net/url"
@@ -8,7 +9,7 @@ import (
) )
// GetOGTags is the main function that retrieves Open Graph tags for a URL // GetOGTags is the main function that retrieves Open Graph tags for a URL
func (c *OGTagCache) GetOGTags(url *url.URL, originalHost string) (map[string]string, error) { func (c *OGTagCache) GetOGTags(ctx context.Context, url *url.URL, originalHost string) (map[string]string, error) {
if url == nil { if url == nil {
return nil, errors.New("nil URL provided, cannot fetch OG tags") return nil, errors.New("nil URL provided, cannot fetch OG tags")
} }
@@ -21,12 +22,12 @@ func (c *OGTagCache) GetOGTags(url *url.URL, originalHost string) (map[string]st
cacheKey := c.generateCacheKey(target, originalHost) cacheKey := c.generateCacheKey(target, originalHost)
// Check cache first // Check cache first
if cachedTags := c.checkCache(cacheKey); cachedTags != nil { if cachedTags := c.checkCache(ctx, cacheKey); cachedTags != nil {
return cachedTags, nil return cachedTags, nil
} }
// Fetch HTML content, passing the original host // Fetch HTML content, passing the original host
doc, err := c.fetchHTMLDocumentWithCache(target, originalHost, cacheKey) doc, err := c.fetchHTMLDocumentWithCache(ctx, target, originalHost, cacheKey)
if errors.Is(err, syscall.ECONNREFUSED) { if errors.Is(err, syscall.ECONNREFUSED) {
slog.Debug("Connection refused, returning empty tags") slog.Debug("Connection refused, returning empty tags")
return nil, nil return nil, nil
@@ -42,7 +43,7 @@ func (c *OGTagCache) GetOGTags(url *url.URL, originalHost string) (map[string]st
ogTags := c.extractOGTags(doc) ogTags := c.extractOGTags(doc)
// Store in cache // Store in cache
c.cache.Set(cacheKey, ogTags, c.ogTimeToLive) c.cache.Set(ctx, cacheKey, ogTags, c.ogTimeToLive)
return ogTags, nil return ogTags, nil
} }
@@ -59,8 +60,8 @@ func (c *OGTagCache) generateCacheKey(target string, originalHost string) string
} }
// checkCache checks if we have the tags cached and returns them if so // checkCache checks if we have the tags cached and returns them if so
func (c *OGTagCache) checkCache(cacheKey string) map[string]string { func (c *OGTagCache) checkCache(ctx context.Context, cacheKey string) map[string]string {
if cachedTags, ok := c.cache.Get(cacheKey); ok { if cachedTags, err := c.cache.Get(ctx, cacheKey); err == nil {
slog.Debug("cache hit", "tags", cachedTags) slog.Debug("cache hit", "tags", cachedTags)
return cachedTags return cachedTags
} }

View File

@@ -9,6 +9,7 @@ import (
"time" "time"
"github.com/TecharoHQ/anubis/lib/policy/config" "github.com/TecharoHQ/anubis/lib/policy/config"
"github.com/TecharoHQ/anubis/lib/store/memory"
) )
func TestCacheReturnsDefault(t *testing.T) { func TestCacheReturnsDefault(t *testing.T) {
@@ -21,14 +22,14 @@ func TestCacheReturnsDefault(t *testing.T) {
TimeToLive: time.Minute, TimeToLive: time.Minute,
ConsiderHost: false, ConsiderHost: false,
Override: want, Override: want,
}) }, memory.New(t.Context()))
u, err := url.Parse("https://anubis.techaro.lol") u, err := url.Parse("https://anubis.techaro.lol")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
result, err := cache.GetOGTags(u, "anubis.techaro.lol") result, err := cache.GetOGTags(t.Context(), u, "anubis.techaro.lol")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -49,7 +50,7 @@ func TestCheckCache(t *testing.T) {
Enabled: true, Enabled: true,
TimeToLive: time.Minute, TimeToLive: time.Minute,
ConsiderHost: false, ConsiderHost: false,
}) }, memory.New(t.Context()))
// Set up test data // Set up test data
urlStr := "http://example.com/page" urlStr := "http://example.com/page"
@@ -60,16 +61,16 @@ func TestCheckCache(t *testing.T) {
cacheKey := cache.generateCacheKey(urlStr, "example.com") cacheKey := cache.generateCacheKey(urlStr, "example.com")
// Test cache miss // Test cache miss
tags := cache.checkCache(cacheKey) tags := cache.checkCache(t.Context(), cacheKey)
if tags != nil { if tags != nil {
t.Errorf("expected nil tags on cache miss, got %v", tags) t.Errorf("expected nil tags on cache miss, got %v", tags)
} }
// Manually add to cache // Manually add to cache
cache.cache.Set(cacheKey, expectedTags, time.Minute) cache.cache.Set(t.Context(), cacheKey, expectedTags, time.Minute)
// Test cache hit // Test cache hit
tags = cache.checkCache(cacheKey) tags = cache.checkCache(t.Context(), cacheKey)
if tags == nil { if tags == nil {
t.Fatal("expected non-nil tags on cache hit, got nil") t.Fatal("expected non-nil tags on cache hit, got nil")
} }
@@ -112,7 +113,7 @@ func TestGetOGTags(t *testing.T) {
Enabled: true, Enabled: true,
TimeToLive: time.Minute, TimeToLive: time.Minute,
ConsiderHost: false, ConsiderHost: false,
}) }, memory.New(t.Context()))
// Parse the test server URL // Parse the test server URL
parsedURL, err := url.Parse(ts.URL) parsedURL, err := url.Parse(ts.URL)
@@ -122,7 +123,7 @@ func TestGetOGTags(t *testing.T) {
// Test fetching OG tags from the test server // Test fetching OG tags from the test server
// Pass the host from the parsed test server URL // Pass the host from the parsed test server URL
ogTags, err := cache.GetOGTags(parsedURL, parsedURL.Host) ogTags, err := cache.GetOGTags(t.Context(), parsedURL, parsedURL.Host)
if err != nil { if err != nil {
t.Fatalf("failed to get OG tags: %v", err) t.Fatalf("failed to get OG tags: %v", err)
} }
@@ -142,14 +143,14 @@ func TestGetOGTags(t *testing.T) {
// Test fetching OG tags from the cache // Test fetching OG tags from the cache
// Pass the host from the parsed test server URL // Pass the host from the parsed test server URL
ogTags, err = cache.GetOGTags(parsedURL, parsedURL.Host) ogTags, err = cache.GetOGTags(t.Context(), parsedURL, parsedURL.Host)
if err != nil { if err != nil {
t.Fatalf("failed to get OG tags from cache: %v", err) t.Fatalf("failed to get OG tags from cache: %v", err)
} }
// Test fetching OG tags from the cache (3rd time) // Test fetching OG tags from the cache (3rd time)
// Pass the host from the parsed test server URL // Pass the host from the parsed test server URL
newOgTags, err := cache.GetOGTags(parsedURL, parsedURL.Host) newOgTags, err := cache.GetOGTags(t.Context(), parsedURL, parsedURL.Host)
if err != nil { if err != nil {
t.Fatalf("failed to get OG tags from cache: %v", err) t.Fatalf("failed to get OG tags from cache: %v", err)
} }
@@ -263,10 +264,10 @@ func TestGetOGTagsWithHostConsideration(t *testing.T) {
Enabled: true, Enabled: true,
TimeToLive: time.Minute, TimeToLive: time.Minute,
ConsiderHost: tc.ogCacheConsiderHost, ConsiderHost: tc.ogCacheConsiderHost,
}) }, memory.New(t.Context()))
for i, req := range tc.requests { for i, req := range tc.requests {
ogTags, err := cache.GetOGTags(parsedURL, req.host) ogTags, err := cache.GetOGTags(t.Context(), parsedURL, req.host)
if err != nil { if err != nil {
t.Errorf("Request %d (host: %s): unexpected error: %v", i+1, req.host, err) t.Errorf("Request %d (host: %s): unexpected error: %v", i+1, req.host, err)
continue // Skip further checks for this request if error occurred continue // Skip further checks for this request if error occurred

View File

@@ -20,8 +20,8 @@ var (
// fetchHTMLDocumentWithCache fetches the HTML document from the given URL string, // fetchHTMLDocumentWithCache fetches the HTML document from the given URL string,
// preserving the original host header. // preserving the original host header.
func (c *OGTagCache) fetchHTMLDocumentWithCache(urlStr string, originalHost string, cacheKey string) (*html.Node, error) { func (c *OGTagCache) fetchHTMLDocumentWithCache(ctx context.Context, urlStr string, originalHost string, cacheKey string) (*html.Node, error) {
req, err := http.NewRequestWithContext(context.Background(), "GET", urlStr, nil) req, err := http.NewRequestWithContext(ctx, "GET", urlStr, nil)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create http request: %w", err) return nil, fmt.Errorf("failed to create http request: %w", err)
} }
@@ -41,7 +41,7 @@ func (c *OGTagCache) fetchHTMLDocumentWithCache(urlStr string, originalHost stri
var netErr net.Error var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() { if errors.As(err, &netErr) && netErr.Timeout() {
slog.Debug("og: request timed out", "url", urlStr) slog.Debug("og: request timed out", "url", urlStr)
c.cache.Set(cacheKey, emptyMap, c.ogTimeToLive/2) // Cache empty result for half the TTL to not spam the server c.cache.Set(ctx, cacheKey, emptyMap, c.ogTimeToLive/2) // Cache empty result for half the TTL to not spam the server
} }
return nil, fmt.Errorf("http get failed: %w", err) return nil, fmt.Errorf("http get failed: %w", err)
} }
@@ -56,7 +56,7 @@ func (c *OGTagCache) fetchHTMLDocumentWithCache(urlStr string, originalHost stri
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
slog.Debug("og: received non-OK status code", "url", urlStr, "status", resp.StatusCode) slog.Debug("og: received non-OK status code", "url", urlStr, "status", resp.StatusCode)
c.cache.Set(cacheKey, emptyMap, c.ogTimeToLive) // Cache empty result for non-successful status codes c.cache.Set(ctx, cacheKey, emptyMap, c.ogTimeToLive) // Cache empty result for non-successful status codes
return nil, fmt.Errorf("%w: page not found", ErrOgHandled) return nil, fmt.Errorf("%w: page not found", ErrOgHandled)
} }

View File

@@ -1,6 +1,7 @@
package ogtags package ogtags
import ( import (
"context"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@@ -11,6 +12,7 @@ import (
"time" "time"
"github.com/TecharoHQ/anubis/lib/policy/config" "github.com/TecharoHQ/anubis/lib/policy/config"
"github.com/TecharoHQ/anubis/lib/store/memory"
"golang.org/x/net/html" "golang.org/x/net/html"
) )
@@ -85,8 +87,8 @@ func TestFetchHTMLDocument(t *testing.T) {
Enabled: true, Enabled: true,
TimeToLive: time.Minute, TimeToLive: time.Minute,
ConsiderHost: false, ConsiderHost: false,
}) }, memory.New(t.Context()))
doc, err := cache.fetchHTMLDocument(ts.URL, "anything") doc, err := cache.fetchHTMLDocument(t.Context(), ts.URL, "anything")
if tt.expectError { if tt.expectError {
if err == nil { if err == nil {
@@ -116,9 +118,9 @@ func TestFetchHTMLDocumentInvalidURL(t *testing.T) {
Enabled: true, Enabled: true,
TimeToLive: time.Minute, TimeToLive: time.Minute,
ConsiderHost: false, ConsiderHost: false,
}) }, memory.New(t.Context()))
doc, err := cache.fetchHTMLDocument("http://invalid.url.that.doesnt.exist.example", "anything") doc, err := cache.fetchHTMLDocument(t.Context(), "http://invalid.url.that.doesnt.exist.example", "anything")
if err == nil { if err == nil {
t.Error("expected error for invalid URL, got nil") t.Error("expected error for invalid URL, got nil")
@@ -130,7 +132,7 @@ func TestFetchHTMLDocumentInvalidURL(t *testing.T) {
} }
// fetchHTMLDocument allows you to call fetchHTMLDocumentWithCache without a duplicate generateCacheKey call // fetchHTMLDocument allows you to call fetchHTMLDocumentWithCache without a duplicate generateCacheKey call
func (c *OGTagCache) fetchHTMLDocument(urlStr string, originalHost string) (*html.Node, error) { func (c *OGTagCache) fetchHTMLDocument(ctx context.Context, urlStr string, originalHost string) (*html.Node, error) {
cacheKey := c.generateCacheKey(urlStr, originalHost) cacheKey := c.generateCacheKey(urlStr, originalHost)
return c.fetchHTMLDocumentWithCache(urlStr, originalHost, cacheKey) return c.fetchHTMLDocumentWithCache(ctx, urlStr, originalHost, cacheKey)
} }

View File

@@ -8,6 +8,7 @@ import (
"time" "time"
"github.com/TecharoHQ/anubis/lib/policy/config" "github.com/TecharoHQ/anubis/lib/policy/config"
"github.com/TecharoHQ/anubis/lib/store/memory"
) )
func TestIntegrationGetOGTags(t *testing.T) { func TestIntegrationGetOGTags(t *testing.T) {
@@ -110,7 +111,7 @@ func TestIntegrationGetOGTags(t *testing.T) {
Enabled: true, Enabled: true,
TimeToLive: time.Minute, TimeToLive: time.Minute,
ConsiderHost: false, ConsiderHost: false,
}) }, memory.New(t.Context()))
// Create URL for test // Create URL for test
testURL, _ := url.Parse(ts.URL) testURL, _ := url.Parse(ts.URL)
@@ -119,7 +120,7 @@ func TestIntegrationGetOGTags(t *testing.T) {
// Get OG tags // Get OG tags
// Pass the host from the test URL // Pass the host from the test URL
ogTags, err := cache.GetOGTags(testURL, testURL.Host) ogTags, err := cache.GetOGTags(t.Context(), testURL, testURL.Host)
// Check error expectation // Check error expectation
if tc.expectError { if tc.expectError {
@@ -147,7 +148,7 @@ func TestIntegrationGetOGTags(t *testing.T) {
// Test cache retrieval // Test cache retrieval
// Pass the host from the test URL // Pass the host from the test URL
cachedOGTags, err := cache.GetOGTags(testURL, testURL.Host) cachedOGTags, err := cache.GetOGTags(t.Context(), testURL, testURL.Host)
if err != nil { if err != nil {
t.Fatalf("failed to get OG tags from cache: %v", err) t.Fatalf("failed to get OG tags from cache: %v", err)
} }

View File

@@ -7,6 +7,7 @@ import (
"testing" "testing"
"github.com/TecharoHQ/anubis/lib/policy/config" "github.com/TecharoHQ/anubis/lib/policy/config"
"github.com/TecharoHQ/anubis/lib/store/memory"
"golang.org/x/net/html" "golang.org/x/net/html"
) )
@@ -30,7 +31,7 @@ func BenchmarkGetTarget(b *testing.B) {
for _, tt := range tests { for _, tt := range tests {
b.Run(tt.name, func(b *testing.B) { b.Run(tt.name, func(b *testing.B) {
cache := NewOGTagCache(tt.target, config.OpenGraph{}) cache := NewOGTagCache(tt.target, config.OpenGraph{}, memory.New(b.Context()))
urls := make([]*url.URL, len(tt.paths)) urls := make([]*url.URL, len(tt.paths))
for i, path := range tt.paths { for i, path := range tt.paths {
u, _ := url.Parse(path) u, _ := url.Parse(path)
@@ -66,7 +67,7 @@ func BenchmarkExtractOGTags(b *testing.B) {
</head><body><div><p>Content</p></div></body></html>`, </head><body><div><p>Content</p></div></body></html>`,
} }
cache := NewOGTagCache("http://example.com", config.OpenGraph{}) cache := NewOGTagCache("http://example.com", config.OpenGraph{}, memory.New(b.Context()))
docs := make([]*html.Node, len(htmlSamples)) docs := make([]*html.Node, len(htmlSamples))
for i, sample := range htmlSamples { for i, sample := range htmlSamples {
@@ -84,7 +85,7 @@ func BenchmarkExtractOGTags(b *testing.B) {
// Memory usage test // Memory usage test
func TestMemoryUsage(t *testing.T) { func TestMemoryUsage(t *testing.T) {
cache := NewOGTagCache("http://example.com", config.OpenGraph{}) cache := NewOGTagCache("http://example.com", config.OpenGraph{}, memory.New(t.Context()))
// Force GC and wait for it to complete // Force GC and wait for it to complete
runtime.GC() runtime.GC()

View File

@@ -9,8 +9,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/TecharoHQ/anubis/decaymap"
"github.com/TecharoHQ/anubis/lib/policy/config" "github.com/TecharoHQ/anubis/lib/policy/config"
"github.com/TecharoHQ/anubis/lib/store"
) )
const ( const (
@@ -22,7 +22,7 @@ const (
) )
type OGTagCache struct { type OGTagCache struct {
cache *decaymap.Impl[string, map[string]string] cache store.JSON[map[string]string]
targetURL *url.URL targetURL *url.URL
client *http.Client client *http.Client
@@ -36,7 +36,7 @@ type OGTagCache struct {
ogOverride map[string]string ogOverride map[string]string
} }
func NewOGTagCache(target string, conf config.OpenGraph) *OGTagCache { func NewOGTagCache(target string, conf config.OpenGraph, backend store.Interface) *OGTagCache {
// Predefined approved tags and prefixes // Predefined approved tags and prefixes
defaultApprovedTags := []string{"description", "keywords", "author"} defaultApprovedTags := []string{"description", "keywords", "author"}
defaultApprovedPrefixes := []string{"og:", "twitter:", "fediverse:"} defaultApprovedPrefixes := []string{"og:", "twitter:", "fediverse:"}
@@ -77,7 +77,10 @@ func NewOGTagCache(target string, conf config.OpenGraph) *OGTagCache {
} }
return &OGTagCache{ return &OGTagCache{
cache: decaymap.New[string, map[string]string](), cache: store.JSON[map[string]string]{
Underlying: backend,
Prefix: "ogtags:",
},
targetURL: parsedTargetURL, targetURL: parsedTargetURL,
ogPassthrough: conf.Enabled, ogPassthrough: conf.Enabled,
ogTimeToLive: conf.TimeToLive, ogTimeToLive: conf.TimeToLive,
@@ -124,9 +127,3 @@ func (c *OGTagCache) getTarget(u *url.URL) string {
return sb.String() return sb.String()
} }
func (c *OGTagCache) Cleanup() {
if c.cache != nil {
c.cache.Cleanup()
}
}

View File

@@ -1,12 +1,14 @@
package ogtags package ogtags
import ( import (
"context"
"net/url" "net/url"
"strings" "strings"
"testing" "testing"
"unicode/utf8" "unicode/utf8"
"github.com/TecharoHQ/anubis/lib/policy/config" "github.com/TecharoHQ/anubis/lib/policy/config"
"github.com/TecharoHQ/anubis/lib/store/memory"
"golang.org/x/net/html" "golang.org/x/net/html"
) )
@@ -46,7 +48,7 @@ func FuzzGetTarget(f *testing.F) {
} }
// Create cache - should not panic // Create cache - should not panic
cache := NewOGTagCache(target, config.OpenGraph{}) cache := NewOGTagCache(target, config.OpenGraph{}, memory.New(context.Background()))
// Create URL // Create URL
u := &url.URL{ u := &url.URL{
@@ -130,7 +132,7 @@ func FuzzExtractOGTags(f *testing.F) {
return return
} }
cache := NewOGTagCache("http://example.com", config.OpenGraph{}) cache := NewOGTagCache("http://example.com", config.OpenGraph{}, memory.New(context.Background()))
// Should not panic // Should not panic
tags := cache.extractOGTags(doc) tags := cache.extractOGTags(doc)
@@ -186,7 +188,7 @@ func FuzzGetTargetRoundTrip(f *testing.F) {
t.Skip() t.Skip()
} }
cache := NewOGTagCache(target, config.OpenGraph{}) cache := NewOGTagCache(target, config.OpenGraph{}, memory.New(context.Background()))
u := &url.URL{Path: path, RawQuery: query} u := &url.URL{Path: path, RawQuery: query}
result := cache.getTarget(u) result := cache.getTarget(u)
@@ -243,7 +245,7 @@ func FuzzExtractMetaTagInfo(f *testing.F) {
}, },
} }
cache := NewOGTagCache("http://example.com", config.OpenGraph{}) cache := NewOGTagCache("http://example.com", config.OpenGraph{}, memory.New(context.Background()))
// Should not panic // Should not panic
property, content := cache.extractMetaTagInfo(node) property, content := cache.extractMetaTagInfo(node)
@@ -296,7 +298,7 @@ func BenchmarkFuzzedGetTarget(b *testing.B) {
for _, input := range inputs { for _, input := range inputs {
b.Run(input.name, func(b *testing.B) { b.Run(input.name, func(b *testing.B) {
cache := NewOGTagCache(input.target, config.OpenGraph{}) cache := NewOGTagCache(input.target, config.OpenGraph{}, memory.New(context.Background()))
u := &url.URL{Path: input.path, RawQuery: input.query} u := &url.URL{Path: input.path, RawQuery: input.query}
b.ResetTimer() b.ResetTimer()

View File

@@ -15,6 +15,7 @@ import (
"time" "time"
"github.com/TecharoHQ/anubis/lib/policy/config" "github.com/TecharoHQ/anubis/lib/policy/config"
"github.com/TecharoHQ/anubis/lib/store/memory"
) )
func TestNewOGTagCache(t *testing.T) { func TestNewOGTagCache(t *testing.T) {
@@ -44,7 +45,7 @@ func TestNewOGTagCache(t *testing.T) {
Enabled: tt.ogPassthrough, Enabled: tt.ogPassthrough,
TimeToLive: tt.ogTimeToLive, TimeToLive: tt.ogTimeToLive,
ConsiderHost: false, ConsiderHost: false,
}) }, memory.New(t.Context()))
if cache == nil { if cache == nil {
t.Fatal("expected non-nil cache, got nil") t.Fatal("expected non-nil cache, got nil")
@@ -84,7 +85,7 @@ func TestNewOGTagCache_UnixSocket(t *testing.T) {
Enabled: true, Enabled: true,
TimeToLive: 5 * time.Minute, TimeToLive: 5 * time.Minute,
ConsiderHost: false, ConsiderHost: false,
}) }, memory.New(t.Context()))
if cache == nil { if cache == nil {
t.Fatal("expected non-nil cache, got nil") t.Fatal("expected non-nil cache, got nil")
@@ -169,7 +170,7 @@ func TestGetTarget(t *testing.T) {
Enabled: true, Enabled: true,
TimeToLive: time.Minute, TimeToLive: time.Minute,
ConsiderHost: false, ConsiderHost: false,
}) }, memory.New(t.Context()))
u := &url.URL{ u := &url.URL{
Path: tt.path, Path: tt.path,
@@ -242,14 +243,14 @@ func TestIntegrationGetOGTags_UnixSocket(t *testing.T) {
Enabled: true, Enabled: true,
TimeToLive: time.Minute, TimeToLive: time.Minute,
ConsiderHost: false, ConsiderHost: false,
}) }, memory.New(t.Context()))
// Create a dummy URL for the request (path and query matter) // Create a dummy URL for the request (path and query matter)
testReqURL, _ := url.Parse("/some/page?query=1") testReqURL, _ := url.Parse("/some/page?query=1")
// Get OG tags // Get OG tags
// Pass an empty string for host, as it's irrelevant for unix sockets // Pass an empty string for host, as it's irrelevant for unix sockets
ogTags, err := cache.GetOGTags(testReqURL, "") ogTags, err := cache.GetOGTags(t.Context(), testReqURL, "")
if err != nil { if err != nil {
t.Fatalf("GetOGTags failed for unix socket: %v", err) t.Fatalf("GetOGTags failed for unix socket: %v", err)
@@ -265,7 +266,7 @@ func TestIntegrationGetOGTags_UnixSocket(t *testing.T) {
// Test cache retrieval (should hit cache) // Test cache retrieval (should hit cache)
// Pass an empty string for host // Pass an empty string for host
cachedTags, err := cache.GetOGTags(testReqURL, "") cachedTags, err := cache.GetOGTags(t.Context(), testReqURL, "")
if err != nil { if err != nil {
t.Fatalf("GetOGTags (cache hit) failed for unix socket: %v", err) t.Fatalf("GetOGTags (cache hit) failed for unix socket: %v", err)
} }

View File

@@ -7,6 +7,7 @@ import (
"time" "time"
"github.com/TecharoHQ/anubis/lib/policy/config" "github.com/TecharoHQ/anubis/lib/policy/config"
"github.com/TecharoHQ/anubis/lib/store/memory"
"golang.org/x/net/html" "golang.org/x/net/html"
) )
@@ -17,7 +18,7 @@ func TestExtractOGTags(t *testing.T) {
Enabled: false, Enabled: false,
ConsiderHost: false, ConsiderHost: false,
TimeToLive: time.Minute, TimeToLive: time.Minute,
}) }, memory.New(t.Context()))
// Manually set approved tags/prefixes based on the user request for clarity // Manually set approved tags/prefixes based on the user request for clarity
testCache.approvedTags = []string{"description"} testCache.approvedTags = []string{"description"}
testCache.approvedPrefixes = []string{"og:"} testCache.approvedPrefixes = []string{"og:"}
@@ -198,7 +199,7 @@ func TestExtractMetaTagInfo(t *testing.T) {
Enabled: false, Enabled: false,
ConsiderHost: false, ConsiderHost: false,
TimeToLive: time.Minute, TimeToLive: time.Minute,
}) }, memory.New(t.Context()))
testCache.approvedTags = []string{"description"} testCache.approvedTags = []string{"description"}
testCache.approvedPrefixes = []string{"og:"} testCache.approvedPrefixes = []string{"og:"}

View File

@@ -70,7 +70,6 @@ type Server struct {
next http.Handler next http.Handler
mux *http.ServeMux mux *http.ServeMux
policy *policy.ParsedConfig policy *policy.ParsedConfig
DNSBLCache *decaymap.Impl[string, dnsbl.DroneBLResponse]
OGTags *ogtags.OGTagCache OGTags *ogtags.OGTagCache
ed25519Priv ed25519.PrivateKey ed25519Priv ed25519.PrivateKey
hs512Secret []byte hs512Secret []byte
@@ -279,15 +278,16 @@ func (s *Server) checkRules(w http.ResponseWriter, r *http.Request, cr policy.Ch
} }
func (s *Server) handleDNSBL(w http.ResponseWriter, r *http.Request, ip string, lg *slog.Logger) bool { func (s *Server) handleDNSBL(w http.ResponseWriter, r *http.Request, ip string, lg *slog.Logger) bool {
db := &store.JSON[dnsbl.DroneBLResponse]{Underlying: s.store, Prefix: "dronebl:"}
if s.policy.DNSBL && ip != "" { if s.policy.DNSBL && ip != "" {
resp, ok := s.DNSBLCache.Get(ip) resp, err := db.Get(r.Context(), ip)
if !ok { if err != nil {
lg.Debug("looking up ip in dnsbl") lg.Debug("looking up ip in dnsbl")
resp, err := dnsbl.Lookup(ip) resp, err := dnsbl.Lookup(ip)
if err != nil { if err != nil {
lg.Error("can't look up ip in dnsbl", "err", err) lg.Error("can't look up ip in dnsbl", "err", err)
} }
s.DNSBLCache.Set(ip, resp, 24*time.Hour) db.Set(r.Context(), ip, resp, 24*time.Hour)
droneBLHits.WithLabelValues(resp.String()).Inc() droneBLHits.WithLabelValues(resp.String()).Inc()
} }
@@ -551,8 +551,3 @@ func (s *Server) check(r *http.Request) (policy.CheckResult, *policy.Bot, error)
Rules: &checker.List{}, Rules: &checker.List{},
}, nil }, nil
} }
func (s *Server) CleanupDecayMap() {
s.DNSBLCache.Cleanup()
s.OGTags.Cleanup()
}

View File

@@ -15,9 +15,7 @@ import (
"github.com/TecharoHQ/anubis" "github.com/TecharoHQ/anubis"
"github.com/TecharoHQ/anubis/data" "github.com/TecharoHQ/anubis/data"
"github.com/TecharoHQ/anubis/decaymap"
"github.com/TecharoHQ/anubis/internal" "github.com/TecharoHQ/anubis/internal"
"github.com/TecharoHQ/anubis/internal/dnsbl"
"github.com/TecharoHQ/anubis/internal/ogtags" "github.com/TecharoHQ/anubis/internal/ogtags"
"github.com/TecharoHQ/anubis/lib/challenge" "github.com/TecharoHQ/anubis/lib/challenge"
"github.com/TecharoHQ/anubis/lib/localization" "github.com/TecharoHQ/anubis/lib/localization"
@@ -108,8 +106,7 @@ func New(opts Options) (*Server, error) {
hs512Secret: opts.HS512Secret, hs512Secret: opts.HS512Secret,
policy: opts.Policy, policy: opts.Policy,
opts: opts, opts: opts,
DNSBLCache: decaymap.New[string, dnsbl.DroneBLResponse](), OGTags: ogtags.NewOGTagCache(opts.Target, opts.Policy.OpenGraph, opts.Policy.Store),
OGTags: ogtags.NewOGTagCache(opts.Target, opts.Policy.OpenGraph),
store: opts.Policy.Store, store: opts.Policy.Store,
} }

View File

@@ -138,7 +138,7 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *polic
var ogTags map[string]string = nil var ogTags map[string]string = nil
if s.opts.OpenGraph.Enabled { if s.opts.OpenGraph.Enabled {
var err error var err error
ogTags, err = s.OGTags.GetOGTags(r.URL, r.Host) ogTags, err = s.OGTags.GetOGTags(r.Context(), r.URL, r.Host)
if err != nil { if err != nil {
lg.Error("failed to get OG tags", "err", err) lg.Error("failed to get OG tags", "err", err)
} }

View File

@@ -126,7 +126,7 @@ func (s *Store) cleanup(ctx context.Context) error {
} }
func (s *Store) cleanupThread(ctx context.Context) { func (s *Store) cleanupThread(ctx context.Context) {
t := time.NewTicker(5 * time.Minute) t := time.NewTicker(time.Hour)
defer t.Stop() defer t.Stop()
for { for {

View File

@@ -3,7 +3,6 @@ package bbolt
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"path/filepath"
"testing" "testing"
) )
@@ -27,13 +26,6 @@ func TestFactoryValid(t *testing.T) {
cfg: Config{}, cfg: Config{},
err: ErrMissingPath, err: ErrMissingPath,
}, },
{
name: "unwritable folder",
cfg: Config{
Path: filepath.Join("/", "testdb"),
},
err: ErrCantWriteToPath,
},
} { } {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
data, err := json.Marshal(tt.cfg) data, err := json.Marshal(tt.cfg)

View File

@@ -43,13 +43,22 @@ func z[T any]() T { return *new(T) }
type JSON[T any] struct { type JSON[T any] struct {
Underlying Interface Underlying Interface
Prefix string
} }
func (j *JSON[T]) Delete(ctx context.Context, key string) error { func (j *JSON[T]) Delete(ctx context.Context, key string) error {
if j.Prefix != "" {
key = j.Prefix + key
}
return j.Underlying.Delete(ctx, key) return j.Underlying.Delete(ctx, key)
} }
func (j *JSON[T]) Get(ctx context.Context, key string) (T, error) { func (j *JSON[T]) Get(ctx context.Context, key string) (T, error) {
if j.Prefix != "" {
key = j.Prefix + key
}
data, err := j.Underlying.Get(ctx, key) data, err := j.Underlying.Get(ctx, key)
if err != nil { if err != nil {
return z[T](), err return z[T](), err
@@ -64,6 +73,10 @@ func (j *JSON[T]) Get(ctx context.Context, key string) (T, error) {
} }
func (j *JSON[T]) Set(ctx context.Context, key string, value T, expiry time.Duration) error { func (j *JSON[T]) Set(ctx context.Context, key string, value T, expiry time.Duration) error {
if j.Prefix != "" {
key = j.Prefix + key
}
data, err := json.Marshal(value) data, err := json.Marshal(value)
if err != nil { if err != nil {
return fmt.Errorf("%w: %w", ErrCantEncode, err) return fmt.Errorf("%w: %w", ErrCantEncode, err)

50
lib/store/json_test.go Normal file
View File

@@ -0,0 +1,50 @@
package store_test
import (
"testing"
"time"
"github.com/TecharoHQ/anubis/lib/store"
"github.com/TecharoHQ/anubis/lib/store/memory"
)
func TestJSON(t *testing.T) {
type data struct {
ID string `json:"id"`
}
st := memory.New(t.Context())
db := store.JSON[data]{
Underlying: st,
Prefix: "foo:",
}
if err := db.Set(t.Context(), "test", data{ID: t.Name()}, time.Minute); err != nil {
t.Fatal(err)
}
got, err := db.Get(t.Context(), "test")
if err != nil {
t.Fatal(err)
}
if got.ID != t.Name() {
t.Fatalf("got wrong data for key \"test\", wanted %q but got: %q", t.Name(), got.ID)
}
if err := db.Delete(t.Context(), "test"); err != nil {
t.Fatal(err)
}
if _, err := db.Get(t.Context(), "test"); err == nil {
t.Fatal("wanted invalid get to fail, it did not")
}
if err := st.Set(t.Context(), "foo:test", []byte("}"), time.Minute); err != nil {
t.Fatal(err)
}
if _, err := db.Get(t.Context(), "test"); err == nil {
t.Fatal("wanted invalid get to fail, it did not")
}
}