feat: add default OpenGraph tags to configuration file (#694)

* feat(config): opengraph passthrough configuration

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

* chore(ogtags): use config.OpenGraph for configuration

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

* chore: wire up ogtags config in most of the app

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

* feat(ogtags): return default tags if they are supplied

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

* chore: make OpenGraph legal so we have some sanity in reviewing

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

* chore: spelling

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

* fix(lib): use OpenGraph.Enabled

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

* test(lib): load default config file if one is not specified in spawnAnubis

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

* chore(config): fix ST1005

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

* docs: document open graph defaults and its new home in the policy file

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

* docs(installation): point to weight threshold new home

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

* chore: rename default to override

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

* chore(default-config): add off-by-default opengraph settings to bot policy file

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

* fix(anubis): make build

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

* test(lib): fix build

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

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
This commit is contained in:
Xe Iaso 2025-06-19 18:00:44 -04:00 committed by GitHub
parent 7aa732c700
commit 4948036f39
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 416 additions and 78 deletions

View file

@ -44,6 +44,10 @@ func loadPolicies(t *testing.T, fname string, difficulty int) *policy.ParsedConf
func spawnAnubis(t *testing.T, opts Options) *Server {
t.Helper()
if opts.Policy == nil {
opts.Policy = loadPolicies(t, "", 4)
}
s, err := New(opts)
if err != nil {
t.Fatalf("can't construct libanubis.Server: %v", err)

View file

@ -21,27 +21,26 @@ import (
"github.com/TecharoHQ/anubis/internal/ogtags"
"github.com/TecharoHQ/anubis/lib/challenge"
"github.com/TecharoHQ/anubis/lib/policy"
"github.com/TecharoHQ/anubis/lib/policy/config"
"github.com/TecharoHQ/anubis/web"
"github.com/TecharoHQ/anubis/xess"
)
type Options struct {
Next http.Handler
Policy *policy.ParsedConfig
Target string
CookieDomain string
CookieName string
BasePrefix string
WebmasterEmail string
RedirectDomains []string
PrivateKey ed25519.PrivateKey
CookieExpiration time.Duration
OGTimeToLive time.Duration
StripBasePrefix bool
OGCacheConsidersHost bool
OGPassthrough bool
CookiePartitioned bool
ServeRobotsTXT bool
Next http.Handler
Policy *policy.ParsedConfig
Target string
CookieDomain string
CookieName string
BasePrefix string
WebmasterEmail string
RedirectDomains []string
PrivateKey ed25519.PrivateKey
CookieExpiration time.Duration
StripBasePrefix bool
OpenGraph config.OpenGraph
CookiePartitioned bool
ServeRobotsTXT bool
}
func LoadPoliciesOrDefault(ctx context.Context, fname string, defaultDifficulty int) (*policy.ParsedConfig, error) {
@ -112,7 +111,7 @@ func New(opts Options) (*Server, error) {
policy: opts.Policy,
opts: opts,
DNSBLCache: decaymap.New[string, dnsbl.DroneBLResponse](),
OGTags: ogtags.NewOGTagCache(opts.Target, opts.OGPassthrough, opts.OGTimeToLive, opts.OGCacheConsidersHost),
OGTags: ogtags.NewOGTagCache(opts.Target, opts.Policy.OpenGraph),
cookieName: cookieName,
}

View file

@ -80,7 +80,7 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *polic
challengeStr := s.challengeFor(r, rule.Challenge.Difficulty)
var ogTags map[string]string = nil
if s.opts.OGPassthrough {
if s.opts.OpenGraph.Enabled {
var err error
ogTags, err = s.OGTags.GetOGTags(r.URL, r.Host)
if err != nil {

View file

@ -10,6 +10,7 @@ import (
"os"
"regexp"
"strings"
"time"
"github.com/TecharoHQ/anubis/data"
"k8s.io/apimachinery/pkg/util/yaml"
@ -323,10 +324,11 @@ func (sc StatusCodes) Valid() error {
}
type fileConfig struct {
Bots []BotOrImport `json:"bots"`
DNSBL bool `json:"dnsbl"`
StatusCodes StatusCodes `json:"status_codes"`
Thresholds []Threshold `json:"thresholds"`
Bots []BotOrImport `json:"bots"`
DNSBL bool `json:"dnsbl"`
OpenGraph openGraphFileConfig `json:"openGraph,omitempty"`
StatusCodes StatusCodes `json:"status_codes"`
Thresholds []Threshold `json:"thresholds"`
}
func (c *fileConfig) Valid() error {
@ -342,6 +344,12 @@ func (c *fileConfig) Valid() error {
}
}
if c.OpenGraph.Enabled {
if err := c.OpenGraph.Valid(); err != nil {
errs = append(errs, err)
}
}
if err := c.StatusCodes.Valid(); err != nil {
errs = append(errs, err)
}
@ -376,10 +384,21 @@ func Load(fin io.Reader, fname string) (*Config, error) {
}
result := &Config{
DNSBL: c.DNSBL,
DNSBL: c.DNSBL,
OpenGraph: OpenGraph{
Enabled: c.OpenGraph.Enabled,
ConsiderHost: c.OpenGraph.ConsiderHost,
Override: c.OpenGraph.Override,
},
StatusCodes: c.StatusCodes,
}
if c.OpenGraph.TimeToLive != "" {
// XXX(Xe): already validated in Valid()
ogTTL, _ := time.ParseDuration(c.OpenGraph.TimeToLive)
result.OpenGraph.TimeToLive = ogTTL
}
var validationErrs []error
for _, boi := range c.Bots {
@ -426,6 +445,7 @@ type Config struct {
Bots []BotConfig
Thresholds []Threshold
DNSBL bool
OpenGraph OpenGraph
StatusCodes StatusCodes
}

View file

@ -0,0 +1,51 @@
package config
import (
"errors"
"fmt"
"time"
)
var (
ErrInvalidOpenGraphConfig = errors.New("config.OpenGraph: invalid OpenGraph configuration")
ErrOpenGraphTTLDoesNotParse = errors.New("config.OpenGraph: ttl does not parse as a Duration, see https://pkg.go.dev/time#ParseDuration (formatted like 5m -> 5 minutes, 2h -> 2 hours, etc)")
ErrOpenGraphMissingProperty = errors.New("config.OpenGraph: default opengraph tags missing a property")
)
type openGraphFileConfig struct {
Enabled bool `json:"enabled" yaml:"enabled"`
ConsiderHost bool `json:"considerHost" yaml:"enabled"`
TimeToLive string `json:"ttl" yaml:"ttl"`
Override map[string]string `json:"override,omitempty" yaml:"override,omitempty"`
}
type OpenGraph struct {
Enabled bool `json:"enabled" yaml:"enabled"`
ConsiderHost bool `json:"considerHost" yaml:"enabled"`
Override map[string]string `json:"override,omitempty" yaml:"override,omitempty"`
TimeToLive time.Duration `json:"ttl" yaml:"ttl"`
}
func (og *openGraphFileConfig) Valid() error {
var errs []error
if _, err := time.ParseDuration(og.TimeToLive); err != nil {
errs = append(errs, fmt.Errorf("%w: ParseDuration(%q) returned: %w", ErrOpenGraphTTLDoesNotParse, og.TimeToLive, err))
}
if len(og.Override) != 0 {
for _, tag := range []string{
"og:title",
} {
if _, ok := og.Override[tag]; !ok {
errs = append(errs, fmt.Errorf("%w: %s", ErrOpenGraphMissingProperty, tag))
}
}
}
if len(errs) != 0 {
return errors.Join(ErrInvalidOpenGraphConfig, errors.Join(errs...))
}
return nil
}

View file

@ -0,0 +1,67 @@
package config
import (
"errors"
"testing"
)
func TestOpenGraphFileConfigValid(t *testing.T) {
for _, tt := range []struct {
name string
input *openGraphFileConfig
err error
}{
{
name: "basic happy path",
input: &openGraphFileConfig{
Enabled: true,
ConsiderHost: false,
TimeToLive: "1h",
Override: map[string]string{},
},
err: nil,
},
{
name: "basic happy path with default",
input: &openGraphFileConfig{
Enabled: true,
ConsiderHost: false,
TimeToLive: "1h",
Override: map[string]string{
"og:title": "foobar",
},
},
err: nil,
},
{
name: "invalid time duration",
input: &openGraphFileConfig{
Enabled: true,
ConsiderHost: false,
TimeToLive: "taco",
Override: map[string]string{},
},
err: ErrOpenGraphTTLDoesNotParse,
},
{
name: "missing og:title in defaults",
input: &openGraphFileConfig{
Enabled: true,
ConsiderHost: false,
TimeToLive: "1h",
Override: map[string]string{
"description": "foobar",
},
},
err: ErrOpenGraphMissingProperty,
},
} {
t.Run(tt.name, func(t *testing.T) {
if err := tt.input.Valid(); !errors.Is(err, tt.err) {
t.Logf("wanted error: %v", tt.err)
t.Logf("got error: %v", err)
t.Error("validation failed")
}
})
}
}

View file

@ -0,0 +1,12 @@
bots:
- name: everything
user_agent_regex: .*
action: DENY
openGraph:
enabled: true
considerHost: false
ttl: taco
default:
"og:title": "Xe's magic land of fun"
"og:description": "We're no strangers to love, you know the rules and so do I"

View file

@ -0,0 +1,12 @@
bots:
- name: everything
user_agent_regex: .*
action: DENY
openGraph:
enabled: true
considerHost: false
ttl: 1h
default:
"og:title": "Xe's magic land of fun"
"og:description": "We're no strangers to love, you know the rules and so do I"

View file

@ -31,6 +31,7 @@ type ParsedConfig struct {
Bots []Bot
Thresholds []*Threshold
DNSBL bool
OpenGraph config.OpenGraph
DefaultDifficulty int
StatusCodes config.StatusCodes
}
@ -38,6 +39,7 @@ type ParsedConfig struct {
func NewParsedConfig(orig *config.Config) *ParsedConfig {
return &ParsedConfig{
orig: orig,
OpenGraph: orig.OpenGraph,
StatusCodes: orig.StatusCodes,
}
}