···231 return rp, nil
232}
233234-func startDecayMapCleanup(ctx context.Context, s *libanubis.Server) {
235- ticker := time.NewTicker(1 * time.Hour)
236- defer ticker.Stop()
237-238- for {
239- select {
240- case <-ticker.C:
241- s.CleanupDecayMap()
242- case <-ctx.Done():
243- return
244- }
245- }
246-}
247-248func main() {
249 flagenv.Parse()
250 flag.Parse()
···421 wg.Add(1)
422 go metricsServer(ctx, wg.Done)
423 }
424- go startDecayMapCleanup(ctx, s)
425426 var h http.Handler
427 h = s
···231 return rp, nil
232}
23300000000000000234func main() {
235 flagenv.Parse()
236 flag.Parse()
···407 wg.Add(1)
408 go metricsServer(ctx, wg.Done)
409 }
0410411 var h http.Handler
412 h = s
+1
docs/docs/CHANGELOG.md
···24- Remove the "Success" interstitial after a proof of work challenge is concluded.
25- 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.
26- 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.
027- Add option for forcing a specific language ([#742](https://github.com/TecharoHQ/anubis/pull/742))
28- Add translation for Turkish language ([#751](https://github.com/TecharoHQ/anubis/pull/751))
29- Allow [Common Crawl](https://commoncrawl.org/) by default so scrapers have less incentive to scrape
···24- Remove the "Success" interstitial after a proof of work challenge is concluded.
25- 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.
26- 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.
27+- 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.
28- Add option for forcing a specific language ([#742](https://github.com/TecharoHQ/anubis/pull/742))
29- Add translation for Turkish language ([#751](https://github.com/TecharoHQ/anubis/pull/751))
30- Allow [Common Crawl](https://commoncrawl.org/) by default so scrapers have less incentive to scrape
+7-6
internal/ogtags/cache.go
···1package ogtags
23import (
04 "errors"
5 "log/slog"
6 "net/url"
···8)
910// GetOGTags is the main function that retrieves Open Graph tags for a URL
11-func (c *OGTagCache) GetOGTags(url *url.URL, originalHost string) (map[string]string, error) {
12 if url == nil {
13 return nil, errors.New("nil URL provided, cannot fetch OG tags")
14 }
···21 cacheKey := c.generateCacheKey(target, originalHost)
2223 // Check cache first
24- if cachedTags := c.checkCache(cacheKey); cachedTags != nil {
25 return cachedTags, nil
26 }
2728 // Fetch HTML content, passing the original host
29- doc, err := c.fetchHTMLDocumentWithCache(target, originalHost, cacheKey)
30 if errors.Is(err, syscall.ECONNREFUSED) {
31 slog.Debug("Connection refused, returning empty tags")
32 return nil, nil
···42 ogTags := c.extractOGTags(doc)
4344 // Store in cache
45- c.cache.Set(cacheKey, ogTags, c.ogTimeToLive)
4647 return ogTags, nil
48}
···59}
6061// checkCache checks if we have the tags cached and returns them if so
62-func (c *OGTagCache) checkCache(cacheKey string) map[string]string {
63- if cachedTags, ok := c.cache.Get(cacheKey); ok {
64 slog.Debug("cache hit", "tags", cachedTags)
65 return cachedTags
66 }
···1package ogtags
23import (
4+ "context"
5 "errors"
6 "log/slog"
7 "net/url"
···9)
1011// GetOGTags is the main function that retrieves Open Graph tags for a URL
12+func (c *OGTagCache) GetOGTags(ctx context.Context, url *url.URL, originalHost string) (map[string]string, error) {
13 if url == nil {
14 return nil, errors.New("nil URL provided, cannot fetch OG tags")
15 }
···22 cacheKey := c.generateCacheKey(target, originalHost)
2324 // Check cache first
25+ if cachedTags := c.checkCache(ctx, cacheKey); cachedTags != nil {
26 return cachedTags, nil
27 }
2829 // Fetch HTML content, passing the original host
30+ doc, err := c.fetchHTMLDocumentWithCache(ctx, target, originalHost, cacheKey)
31 if errors.Is(err, syscall.ECONNREFUSED) {
32 slog.Debug("Connection refused, returning empty tags")
33 return nil, nil
···43 ogTags := c.extractOGTags(doc)
4445 // Store in cache
46+ c.cache.Set(ctx, cacheKey, ogTags, c.ogTimeToLive)
4748 return ogTags, nil
49}
···60}
6162// checkCache checks if we have the tags cached and returns them if so
63+func (c *OGTagCache) checkCache(ctx context.Context, cacheKey string) map[string]string {
64+ if cachedTags, err := c.cache.Get(ctx, cacheKey); err == nil {
65 slog.Debug("cache hit", "tags", cachedTags)
66 return cachedTags
67 }
+13-12
internal/ogtags/cache_test.go
···9 "time"
1011 "github.com/TecharoHQ/anubis/lib/policy/config"
012)
1314func TestCacheReturnsDefault(t *testing.T) {
···21 TimeToLive: time.Minute,
22 ConsiderHost: false,
23 Override: want,
24- })
2526 u, err := url.Parse("https://anubis.techaro.lol")
27 if err != nil {
28 t.Fatal(err)
29 }
3031- result, err := cache.GetOGTags(u, "anubis.techaro.lol")
32 if err != nil {
33 t.Fatal(err)
34 }
···49 Enabled: true,
50 TimeToLive: time.Minute,
51 ConsiderHost: false,
52- })
5354 // Set up test data
55 urlStr := "http://example.com/page"
···60 cacheKey := cache.generateCacheKey(urlStr, "example.com")
6162 // Test cache miss
63- tags := cache.checkCache(cacheKey)
64 if tags != nil {
65 t.Errorf("expected nil tags on cache miss, got %v", tags)
66 }
6768 // Manually add to cache
69- cache.cache.Set(cacheKey, expectedTags, time.Minute)
7071 // Test cache hit
72- tags = cache.checkCache(cacheKey)
73 if tags == nil {
74 t.Fatal("expected non-nil tags on cache hit, got nil")
75 }
···112 Enabled: true,
113 TimeToLive: time.Minute,
114 ConsiderHost: false,
115- })
116117 // Parse the test server URL
118 parsedURL, err := url.Parse(ts.URL)
···122123 // Test fetching OG tags from the test server
124 // Pass the host from the parsed test server URL
125- ogTags, err := cache.GetOGTags(parsedURL, parsedURL.Host)
126 if err != nil {
127 t.Fatalf("failed to get OG tags: %v", err)
128 }
···142143 // Test fetching OG tags from the cache
144 // Pass the host from the parsed test server URL
145- ogTags, err = cache.GetOGTags(parsedURL, parsedURL.Host)
146 if err != nil {
147 t.Fatalf("failed to get OG tags from cache: %v", err)
148 }
149150 // Test fetching OG tags from the cache (3rd time)
151 // Pass the host from the parsed test server URL
152- newOgTags, err := cache.GetOGTags(parsedURL, parsedURL.Host)
153 if err != nil {
154 t.Fatalf("failed to get OG tags from cache: %v", err)
155 }
···263 Enabled: true,
264 TimeToLive: time.Minute,
265 ConsiderHost: tc.ogCacheConsiderHost,
266- })
267268 for i, req := range tc.requests {
269- ogTags, err := cache.GetOGTags(parsedURL, req.host)
270 if err != nil {
271 t.Errorf("Request %d (host: %s): unexpected error: %v", i+1, req.host, err)
272 continue // Skip further checks for this request if error occurred
···9 "time"
1011 "github.com/TecharoHQ/anubis/lib/policy/config"
12+ "github.com/TecharoHQ/anubis/lib/store/memory"
13)
1415func TestCacheReturnsDefault(t *testing.T) {
···22 TimeToLive: time.Minute,
23 ConsiderHost: false,
24 Override: want,
25+ }, memory.New(t.Context()))
2627 u, err := url.Parse("https://anubis.techaro.lol")
28 if err != nil {
29 t.Fatal(err)
30 }
3132+ result, err := cache.GetOGTags(t.Context(), u, "anubis.techaro.lol")
33 if err != nil {
34 t.Fatal(err)
35 }
···50 Enabled: true,
51 TimeToLive: time.Minute,
52 ConsiderHost: false,
53+ }, memory.New(t.Context()))
5455 // Set up test data
56 urlStr := "http://example.com/page"
···61 cacheKey := cache.generateCacheKey(urlStr, "example.com")
6263 // Test cache miss
64+ tags := cache.checkCache(t.Context(), cacheKey)
65 if tags != nil {
66 t.Errorf("expected nil tags on cache miss, got %v", tags)
67 }
6869 // Manually add to cache
70+ cache.cache.Set(t.Context(), cacheKey, expectedTags, time.Minute)
7172 // Test cache hit
73+ tags = cache.checkCache(t.Context(), cacheKey)
74 if tags == nil {
75 t.Fatal("expected non-nil tags on cache hit, got nil")
76 }
···113 Enabled: true,
114 TimeToLive: time.Minute,
115 ConsiderHost: false,
116+ }, memory.New(t.Context()))
117118 // Parse the test server URL
119 parsedURL, err := url.Parse(ts.URL)
···123124 // Test fetching OG tags from the test server
125 // Pass the host from the parsed test server URL
126+ ogTags, err := cache.GetOGTags(t.Context(), parsedURL, parsedURL.Host)
127 if err != nil {
128 t.Fatalf("failed to get OG tags: %v", err)
129 }
···143144 // Test fetching OG tags from the cache
145 // Pass the host from the parsed test server URL
146+ ogTags, err = cache.GetOGTags(t.Context(), parsedURL, parsedURL.Host)
147 if err != nil {
148 t.Fatalf("failed to get OG tags from cache: %v", err)
149 }
150151 // Test fetching OG tags from the cache (3rd time)
152 // Pass the host from the parsed test server URL
153+ newOgTags, err := cache.GetOGTags(t.Context(), parsedURL, parsedURL.Host)
154 if err != nil {
155 t.Fatalf("failed to get OG tags from cache: %v", err)
156 }
···264 Enabled: true,
265 TimeToLive: time.Minute,
266 ConsiderHost: tc.ogCacheConsiderHost,
267+ }, memory.New(t.Context()))
268269 for i, req := range tc.requests {
270+ ogTags, err := cache.GetOGTags(t.Context(), parsedURL, req.host)
271 if err != nil {
272 t.Errorf("Request %d (host: %s): unexpected error: %v", i+1, req.host, err)
273 continue // Skip further checks for this request if error occurred
+4-4
internal/ogtags/fetch.go
···2021// fetchHTMLDocumentWithCache fetches the HTML document from the given URL string,
22// preserving the original host header.
23-func (c *OGTagCache) fetchHTMLDocumentWithCache(urlStr string, originalHost string, cacheKey string) (*html.Node, error) {
24- req, err := http.NewRequestWithContext(context.Background(), "GET", urlStr, nil)
25 if err != nil {
26 return nil, fmt.Errorf("failed to create http request: %w", err)
27 }
···41 var netErr net.Error
42 if errors.As(err, &netErr) && netErr.Timeout() {
43 slog.Debug("og: request timed out", "url", urlStr)
44- c.cache.Set(cacheKey, emptyMap, c.ogTimeToLive/2) // Cache empty result for half the TTL to not spam the server
45 }
46 return nil, fmt.Errorf("http get failed: %w", err)
47 }
···5657 if resp.StatusCode != http.StatusOK {
58 slog.Debug("og: received non-OK status code", "url", urlStr, "status", resp.StatusCode)
59- c.cache.Set(cacheKey, emptyMap, c.ogTimeToLive) // Cache empty result for non-successful status codes
60 return nil, fmt.Errorf("%w: page not found", ErrOgHandled)
61 }
62
···2021// fetchHTMLDocumentWithCache fetches the HTML document from the given URL string,
22// preserving the original host header.
23+func (c *OGTagCache) fetchHTMLDocumentWithCache(ctx context.Context, urlStr string, originalHost string, cacheKey string) (*html.Node, error) {
24+ req, err := http.NewRequestWithContext(ctx, "GET", urlStr, nil)
25 if err != nil {
26 return nil, fmt.Errorf("failed to create http request: %w", err)
27 }
···41 var netErr net.Error
42 if errors.As(err, &netErr) && netErr.Timeout() {
43 slog.Debug("og: request timed out", "url", urlStr)
44+ c.cache.Set(ctx, cacheKey, emptyMap, c.ogTimeToLive/2) // Cache empty result for half the TTL to not spam the server
45 }
46 return nil, fmt.Errorf("http get failed: %w", err)
47 }
···5657 if resp.StatusCode != http.StatusOK {
58 slog.Debug("og: received non-OK status code", "url", urlStr, "status", resp.StatusCode)
59+ c.cache.Set(ctx, cacheKey, emptyMap, c.ogTimeToLive) // Cache empty result for non-successful status codes
60 return nil, fmt.Errorf("%w: page not found", ErrOgHandled)
61 }
62