spam

package
v1.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 28, 2026 License: Apache-2.0 Imports: 22 Imported by: 0

Documentation

Overview

Package spam holds the site-wide spam protection settings and the checks the public form handlers run on every submission.

Index

Constants

View Source
const (
	ReasonLinks  = "too many links"
	ReasonWord   = "blocked word"
	ReasonScript = "blocked script"
)

Reasons EvaluateRules reports. They name the rule only, never user content.

View Source
const (
	ProviderNone      = "none"
	ProviderTurnstile = "turnstile"
	ProviderRecaptcha = "recaptcha"
	ProviderHCaptcha  = "hcaptcha"
)

Captcha providers.

View Source
const (
	ActionSpam   = "spam"
	ActionRefuse = "refuse"
)

What happens when a content rule trips.

View Source
const (
	MaxMinSeconds   = 60
	MaxMaxLinks     = 50
	MaxBlockedWords = 200
	MaxWordLen      = 100
	MaxSiteKeyLen   = 200
)

Limits on the numeric and list settings.

View Source
const (
	RecaptchaAction = "hm_form"
)
View Source
const (

	// TokenMaxAge is how long an issued timing token stays usable.
	TokenMaxAge = 24 * time.Hour
)

Variables

View Source
var DefaultEndpoints = map[string]string{
	ProviderTurnstile: "https://challenges.cloudflare.com/turnstile/v0/siteverify",
	ProviderRecaptcha: "https://www.google.com/recaptcha/api/siteverify",
	ProviderHCaptcha:  "https://api.hcaptcha.com/siteverify",
}

DefaultEndpoints are the provider siteverify URLs.

Providers lists the valid provider values.

View Source
var Scripts = []string{"cyrillic", "cjk", "arabic", "hebrew", "thai", "devanagari"}

Scripts lists the writing systems a site can block.

Functions

func CheckToken

func CheckToken(key []byte, slug, tok string, now time.Time) (issued time.Time, ok bool)

CheckToken returns when tok was issued. ok is false for a malformed, forged, future-dated or expired token; callers then treat the timing as missing.

func EvaluateRules

func EvaluateRules(s Settings, data map[string]interface{}) (reason string, tripped bool)

EvaluateRules checks the submitted values against the content rules and reports the first rule that trips. Control fields must already be stripped.

func IssueToken

func IssueToken(key []byte, slug string, now time.Time) string

IssueToken returns a stateless token proving when a visitor loaded the form with this slug: v1.<unix seconds>.<base64url HMAC>.

func NewClient

func NewClient() *http.Client

NewClient returns the HTTP client verification uses: SSRF-safe, 5s timeout, no redirects.

func SealSecret

func SealSecret(key []byte, secret string) ([]byte, error)

SealSecret returns the sealed form of a captcha secret for Settings.SecretSealed.

func SecretKey

func SecretKey(serverSecret string) []byte

SecretKey derives the key that seals the captcha secret from the server secret.

func TimingKey

func TimingKey(serverSecret string) []byte

TimingKey derives the timing token key from the server secret.

Types

type HTTPVerifier

type HTTPVerifier struct {
	// contains filtered or unexported fields
}

HTTPVerifier verifies tokens over HTTPS.

func NewHTTPVerifier

func NewHTTPVerifier(c *http.Client, endpoints map[string]string) *HTTPVerifier

NewHTTPVerifier returns a verifier using c. endpoints overrides the provider URLs (tests); nil means DefaultEndpoints.

func (*HTTPVerifier) TestSecret

func (v *HTTPVerifier) TestSecret(ctx context.Context, provider, secret, siteKey string) (valid bool, detail string, err error)

TestSecret asks the provider about a dummy token, which tells a wrong secret (rejected as such) from a right one (only the token is rejected). For hCaptcha the site key is sent too, so a key and secret from different accounts show up. valid is false with a plain-sentence detail when the secret is wrong or the provider's reply cannot tell.

func (*HTTPVerifier) Verify

func (v *HTTPVerifier) Verify(ctx context.Context, provider, secret, siteKey, token, remoteIP string) (Result, error)

Verify posts the token to the provider. A transport error, a non-200 reply or an unreadable body is an error; a reply that rejects the token is a Result with OK false. Neither the secret nor the token is ever included in an error.

type Input

type Input struct {
	Slug            string
	CaptchaRequired bool // the form's "Require the spam check" switch
	Data            map[string]interface{}
	Timing          string // the hm_ts token the page fetched at load
	Captcha         string // the provider's response token
	IP              string
}

Input is one submission as the pipeline sees it. Data holds the validated field values only: control fields must already be stripped.

type Pipeline

type Pipeline struct {
	Settings  SettingsReader
	Verifier  Verifier
	SecretKey []byte // opens the sealed captcha secret
	TimingKey []byte // signs and checks timing tokens
	Now       func() time.Time
}

Pipeline runs the checks every public form submission passes through.

func (*Pipeline) CaptchaUsable

func (p *Pipeline) CaptchaUsable(s Settings) bool

CaptchaUsable reports whether s names a provider whose secret can be read.

func (*Pipeline) Check

func (p *Pipeline) Check(ctx context.Context, in Input) (v Verdict, reason string)

Check runs, in order: the captcha (only when the form requires it and a provider with a readable secret is set up), the minimum time to submit, and the content rules. reason names the rule that tripped, never user content.

func (*Pipeline) CheckToken

func (p *Pipeline) CheckToken(slug, tok string) bool

CheckToken reports whether tok is a valid, unexpired timing token for slug.

func (*Pipeline) IssueToken

func (p *Pipeline) IssueToken(slug string) string

IssueToken returns a timing token for the form with this slug.

type Querier

type Querier interface {
	QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
	Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
}

Querier is the part of a pgx pool the Store needs.

type Result

type Result struct {
	OK    bool
	Codes []string // provider error codes, safe to show an admin
	Score float64  // reCAPTCHA v3 score, 0 for other providers
}

Result is a provider's verdict on one token.

type Settings

type Settings struct {
	Provider       string
	SiteKey        string
	SecretSealed   []byte // sealed captcha secret; nil when none is saved
	MinSeconds     int
	MaxLinks       int
	BlockedWords   []string
	BlockedScripts []string
	Action         string
}

Settings is the spam protection configuration.

func Defaults

func Defaults() Settings

Defaults is what a site gets before anyone saves the page.

func (Settings) Secret

func (s Settings) Secret(key []byte) (secret string, ok bool)

Secret opens the stored captcha secret. ok is false when none is saved or it cannot be read (the server's secret changed); callers treat both as "no key".

func (Settings) Validate

func (s Settings) Validate() error

Validate checks every field against its allowed values.

type SettingsReader

type SettingsReader interface {
	Get(ctx context.Context) (Settings, error)
}

SettingsReader supplies the current settings; *Store satisfies it.

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store reads and writes the single hatamoto_spam_settings row, with a short cache because every public page render and form submit reads it.

func NewStore

func NewStore(db Querier) *Store

NewStore returns a Store over db (a *pgxpool.Pool fits).

func (*Store) Get

func (s *Store) Get(ctx context.Context) (Settings, error)

Get returns the stored settings, or Defaults when no row exists. On a read error it serves the last good copy if it has one.

func (*Store) Invalidate

func (s *Store) Invalidate()

Invalidate drops the cached copy so the next Get reads the database.

func (*Store) Save

func (s *Store) Save(ctx context.Context, set Settings) error

Save validates set and stores it, then drops the cache.

type Verdict

type Verdict int

Verdict is the outcome of the spam pipeline for one submission.

const (
	Allow       Verdict = iota // store as a normal submission
	Spam                       // store with status spam, send nothing
	Refuse                     // reject the submission
	CaptchaFail                // the captcha was missing, wrong or could not be checked
)

Pipeline outcomes.

type Verifier

type Verifier interface {
	Verify(ctx context.Context, provider, secret, siteKey, token, remoteIP string) (Result, error)
}

Verifier checks a captcha token with the provider.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL