captcha

package module
v0.2.3 Latest Latest
Warning

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

Go to latest
Published: Aug 18, 2026 License: MIT Imports: 19 Imported by: 0

README

captcha

A unified CAPTCHA package with multiple challenge types. All comments and documentation are in English. The package follows the same multi-module architecture as cache/ and lock/ in ling-base.

Supported challenge types

Type Constant Description User input
Image TypeImage Distorted text rendered as a PNG Text code (case-insensitive)
Click TypeClick Click target characters in order Ordered list of (x, y) points
Slider TypeSlider Drag slider to the end of the track X offset (int)
Math TypeMath Arithmetic problem ("3 + 5 = ?") Numeric answer (int)
Jigsaw TypeJigsaw Drag puzzle piece to fit the cutout X offset (int)
Rotate TypeRotate Rotate image back to upright Rotation angle (degrees)

Quick start

package main

import (
    "context"
    "fmt"
    "time"
    "github.com/LingByte/ling-base/captcha"
)

func main() {
    // Initialize the global manager with defaults.
    captcha.InitGlobalManager(captcha.DefaultConfig())

    // Generate a random challenge (picks one of the six types).
    result, err := captcha.EnsureGlobalManager().GenerateRandom()
    if err != nil {
        panic(err)
    }
    fmt.Printf("Captcha ID: %s, Type: %s\n", result.ID, result.Type)

    // Verify the user's answer (consumes the captcha on success).
    ok, err := captcha.VerifyPayload(captcha.Payload{
        ID:    result.ID,
        Type:  result.Type,
        Value: /* user's answer */,
    })
    fmt.Println("Valid:", ok, "Error:", err)
}

Individual challenge types

Image captcha

Renders a random alphanumeric code (excluding easily confused characters like 0/O/I/1/l) onto a PNG with interference lines and dots. The user types the code they see.

ic := captcha.NewImageCaptcha(200, 60, 4, 5*time.Minute, nil)
result, _ := ic.Generate()
// result.Data["image"] is a base64 PNG data URL.
// result.Data["length"] is the code length (4).

ok, _ := ic.Verify(result.ID, "ABCD")  // case-insensitive
Click captcha

Displays a set of characters on a background image. The user must click the target characters in the specified order. Includes decoy characters to increase difficulty.

cc := captcha.NewClickCaptcha(300, 200, 3, 20, 5*time.Minute, nil)
result, _ := cc.Generate()
// result.Data["targets"] is the ordered list of target characters.
// result.Data["chars"] is all character positions (targets + decoys).
// result.Data["background"] is a base64 PNG data URL.
// result.Data["tolerance"] is the click tolerance in pixels.

// User clicks in order; each click is a Point{X, Y}.
ok, _ := cc.Verify(result.ID, []captcha.Point{
    {X: 50, Y: 80},
    {X: 120, Y: 90},
    {X: 200, Y: 75},
})
Slider captcha

The user drags a slider to the end of a track. Verification passes when the drag distance exceeds passRatio * trackWidth and does not exceed trackWidth.

sc := captcha.NewSliderCaptcha(300, 0.92, 5*time.Minute, nil)
result, _ := sc.Generate()
// result.Data["trackWidth"] is the track width in pixels.

ok, _ := sc.Verify(result.ID, 290)  // must be >= 0.92 * 300 = 276
Math captcha

Generates a random arithmetic problem using addition, subtraction, or multiplication. Results are always non-negative. The user submits the numeric answer.

mc := captcha.NewMathCaptcha(5*time.Minute, nil)
result, _ := mc.Generate()
// result.Data["question"] is e.g. "3 + 5 = ?"

ok, _ := mc.Verify(result.ID, 8)
// or: ok, _ := mc.VerifyString(result.ID, "8")
Jigsaw captcha

A slider puzzle: a piece is cut from the background image and displayed separately. The user drags the piece horizontally to fill the gap. The server stores the target X position and checks it within a tolerance margin.

jc := captcha.NewJigsawCaptcha(300, 150, 40, 5, 5*time.Minute, nil)
result, _ := jc.Generate()
// result.Data["background"] is the background with the cutout masked.
// result.Data["piece"] is the puzzle piece image.
// result.Data["pieceSize"] is the piece size in pixels.
// result.Data["tolerance"] is the X tolerance in pixels.

ok, _ := jc.Verify(result.ID, 180)  // user's drag X position
Rotate captcha

An image is rotated by a random angle. The user must rotate it back to upright. The server stores the rotation angle and checks that the residual (storedAngle - userAngle) mod 360 is within tolerance of 0 degrees.

rc := captcha.NewRotateCaptcha(200, 15, 5*time.Minute, nil)
result, _ := rc.Generate()
// result.Data["image"] is the rotated image as a base64 PNG.
// result.Data["size"] is the image dimensions (square).
// result.Data["tolerance"] is the angular tolerance in degrees.

ok, _ := rc.Verify(result.ID, 90)  // user's rotation angle

Configuration

The Config struct controls all challenge types at once:

cfg := &captcha.Config{
    ImageWidth:       200,
    ImageHeight:      60,
    ImageLength:      4,
    ClickWidth:       300,
    ClickHeight:      200,
    ClickCount:       3,
    ClickTolerance:   30,
    SliderTrackWidth: 300,
    SliderPassRatio:  0.92,
    JigsawWidth:      300,
    JigsawHeight:     150,
    JigsawPieceSize:  40,
    JigsawTolerance:  5,
    RotateSize:       200,
    RotateTolerance:  15,
    Expiration:       5 * time.Minute,
    Store:            captcha.NewMemoryStore(),
}
m := captcha.NewManager(cfg)

Use captcha.DefaultConfig() for sensible defaults.

Store interface

All challenge types store their state through the Store interface:

type Store interface {
    Set(id string, data interface{}, expires time.Time) error
    Get(id string) (interface{}, error)
    Delete(id string) error
    VerifyWithFunc(id string, input interface{}, compareFunc func(stored, input interface{}) bool) (bool, error)
    VerifyWithFuncWithoutDelete(id string, input interface{}, compareFunc func(stored, input interface{}) bool) (bool, error)
}

MemoryStore is the default in-process implementation. For distributed deployments, implement Store with Redis or another shared backend.

VerifyWithFunc consumes the captcha on success (one-shot verification). VerifyWithFuncWithoutDelete checks without removing (for pre-verification flows where the captcha should remain valid).

Manager API

// Generate a specific type.
result, err := m.Generate(captcha.TypeImage)

// Generate a random type.
result, err := m.GenerateRandom()

// Verify a payload (consumes on success).
ok, err := m.Verify(captcha.Payload{ID: id, Type: captcha.TypeMath, Value: 42})

// Validate via the global manager (convenience for HTTP handlers).
err := captcha.ValidatePayload(id, typeStr, value)

Implementation notes

Image captcha
  • Uses golang.org/x/image/font/gofont/goregular for font rendering via freetype.
  • Background is filled with a light color, then 5 interference lines and 50 interference dots are drawn.
  • Each character is rendered with a random dark color and slight position jitter.
  • The code charset excludes 0, O, I, 1, l to avoid ambiguity.
  • Verification is case-insensitive.
Click captcha
  • Generates count + decoys unique characters from a mixed pool of words and alphanumeric characters.
  • Characters are placed at non-overlapping random positions on a gradient background.
  • The first count characters (before shuffle) are the targets; the user must click them in order.
  • Verification uses Euclidean distance with a squared tolerance check.
Slider captcha
  • Stores the track width and checks that the user's drag X is within [passRatio * trackWidth, trackWidth].
  • passRatio defaults to 0.92 (the slider must reach at least 92% of the track).
Math captcha
  • Randomly selects +, -, or x operations.
  • Operands are in [1, 20] for addition/subtraction.
  • Subtraction results are always non-negative (operands are swapped if needed).
  • Multiplication operands are capped at 10 to keep answers manageable.
  • VerifyString parses the input as an integer and delegates to Verify.
Jigsaw captcha
  • Generates a gradient background with interference lines.
  • A square piece is cut from the right half of the image at a random X position.
  • The cutout area in the background is darkened to show the gap.
  • The piece image has a white border for visibility.
  • Verification checks abs(userX - targetX) <= tolerance.
Rotate captcha
  • Generates a circular gradient image with a red arrow at the top so the user can identify the correct orientation.
  • The image is rotated by a random angle (0-359 degrees) using inverse-pixel rotation.
  • Verification computes residual = (storedAngle - userAngle) mod 360 and accepts if residual <= tolerance or residual >= 360 - tolerance (to handle wrap-around at 0/360).

Testing

go test ./... -v
go test ./... -cover

Current test coverage: 95.7% of statements. Tests cover all challenge types, error paths, store operations, type mismatches, and edge cases (tolerance boundaries, wrap-around, default values).

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrPayloadRequired = errors.New("captcha: id and type are required")
	ErrPayloadInvalid  = errors.New("captcha: verification failed")
)

Sentinel errors returned by ValidatePayload so callers can map to i18n responses.

View Source
var LoginCaptchaTypes = []Type{TypeImage, TypeMath}

LoginCaptchaTypes are the kinds randomly issued for auth flows. Slider, click, jigsaw, and rotate are excluded: poor UX on mobile and low-quality visuals.

Functions

func InitGlobalManager

func InitGlobalManager(config *Config)

InitGlobalManager initializes GlobalManager once.

func ValidatePayload

func ValidatePayload(id, typ string, value interface{}) error

ValidatePayload trims and validates a captcha proof without touching any HTTP context. Returns nil on success, or ErrPayloadRequired / ErrPayloadInvalid so the caller can decide how to surface the error.

func VerifyPayload

func VerifyPayload(p Payload) (bool, error)

VerifyPayload validates using GlobalManager.

Types

type CaptchaFields

type CaptchaFields struct {
	CaptchaID    string      `json:"captchaId"`
	CaptchaType  string      `json:"captchaType"`
	CaptchaValue interface{} `json:"captchaValue"`
}

CaptchaFields is embedded in public auth requests that require human verification.

type CharMarker

type CharMarker struct {
	Char string `json:"char"`
	X    int    `json:"x"`
	Y    int    `json:"y"`
}

CharMarker is one character rendered on the click-captcha canvas.

type ClickCaptcha

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

ClickCaptcha is an ordered click challenge: the user must click on the target characters in the displayed order.

func NewClickCaptcha

func NewClickCaptcha(width, height, count, tolerance int, expiration time.Duration, store Store) *ClickCaptcha

NewClickCaptcha creates a click captcha generator.

func (*ClickCaptcha) Generate

func (cc *ClickCaptcha) Generate() (*Result, error)

Generate creates a click challenge. The client renders the characters; the user must click the targets in the specified order.

func (*ClickCaptcha) Verify

func (cc *ClickCaptcha) Verify(id string, userPositions []Point) (bool, error)

Verify validates that the user clicked the targets in the correct order within the tolerance radius.

type Config

type Config struct {
	ImageWidth  int
	ImageHeight int
	ImageLength int

	ClickWidth     int
	ClickHeight    int
	ClickCount     int
	ClickTolerance int

	SliderTrackWidth int
	SliderPassRatio  float64

	JigsawWidth     int
	JigsawHeight    int
	JigsawPieceSize int
	JigsawTolerance int

	RotateSize      int
	RotateTolerance int

	Expiration time.Duration
	Store      Store
}

Config holds captcha settings.

func DefaultConfig

func DefaultConfig() *Config

DefaultConfig returns sensible defaults.

type ImageCaptcha

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

ImageCaptcha generates distorted-text image challenges.

func NewImageCaptcha

func NewImageCaptcha(width, height, length int, expiration time.Duration, store Store) *ImageCaptcha

NewImageCaptcha creates an image captcha generator.

func (*ImageCaptcha) Generate

func (ic *ImageCaptcha) Generate() (*Result, error)

Generate produces a new image captcha challenge.

func (*ImageCaptcha) Verify

func (ic *ImageCaptcha) Verify(id, code string) (bool, error)

Verify checks the user's answer against the stored code (case-insensitive).

func (*ImageCaptcha) VerifyWithoutDelete

func (ic *ImageCaptcha) VerifyWithoutDelete(id, code string) (bool, error)

VerifyWithoutDelete checks without consuming the captcha (for pre-verification).

type JigsawCaptcha

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

JigsawCaptcha is a slider puzzle challenge: a piece is cut from the image and the user must drag it back to the correct horizontal position.

func NewJigsawCaptcha

func NewJigsawCaptcha(width, height, pieceSize, tolerance int, expiration time.Duration, store Store) *JigsawCaptcha

NewJigsawCaptcha creates a jigsaw captcha generator.

func (*JigsawCaptcha) Generate

func (jc *JigsawCaptcha) Generate() (*Result, error)

Generate creates a jigsaw challenge. The response includes the background image (with the piece area masked) and the puzzle piece image, both as PNG data URLs. The user drags the piece horizontally to fill the gap.

func (*JigsawCaptcha) Verify

func (jc *JigsawCaptcha) Verify(id string, userX int) (bool, error)

Verify checks that the user dragged the piece to the correct X position within the tolerance margin.

type Manager

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

Manager is the unified captcha manager.

var GlobalManager *Manager

GlobalManager is the process-wide captcha manager.

func EnsureGlobalManager

func EnsureGlobalManager() *Manager

EnsureGlobalManager lazily initializes GlobalManager.

func NewManager

func NewManager(config *Config) *Manager

NewManager creates a captcha manager.

func (*Manager) Generate

func (m *Manager) Generate(captchaType Type) (*Result, error)

Generate creates a captcha of the given type.

func (*Manager) GenerateRandom

func (m *Manager) GenerateRandom() (*Result, error)

GenerateRandom creates a captcha using RandomType.

func (*Manager) Verify

func (m *Manager) Verify(p Payload) (bool, error)

Verify validates a captcha proof and consumes it on success.

type MathCaptcha

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

MathCaptcha generates arithmetic problems (e.g., "3 + 5 = ?"). The user must solve the problem and submit the numeric answer.

func NewMathCaptcha

func NewMathCaptcha(expiration time.Duration, store Store) *MathCaptcha

NewMathCaptcha creates a math captcha generator.

func (*MathCaptcha) Generate

func (mc *MathCaptcha) Generate() (*Result, error)

Generate produces a new arithmetic challenge.

func (*MathCaptcha) Verify

func (mc *MathCaptcha) Verify(id string, answer int) (bool, error)

Verify checks the user's numeric answer.

func (*MathCaptcha) VerifyString

func (mc *MathCaptcha) VerifyString(id, answer string) (bool, error)

VerifyString checks a string answer (parsed as integer).

type MemoryStore

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

MemoryStore is an in-process implementation of Store backed by a map.

func NewMemoryStore

func NewMemoryStore() *MemoryStore

NewMemoryStore creates a new in-memory store.

func (*MemoryStore) Delete

func (s *MemoryStore) Delete(id string) error

func (*MemoryStore) Get

func (s *MemoryStore) Get(id string) (interface{}, error)

func (*MemoryStore) Set

func (s *MemoryStore) Set(id string, data interface{}, expires time.Time) error

func (*MemoryStore) VerifyWithFunc

func (s *MemoryStore) VerifyWithFunc(id string, input interface{}, compareFunc func(stored, input interface{}) bool) (bool, error)

VerifyWithFunc checks the captcha and deletes it on success.

func (*MemoryStore) VerifyWithFuncWithoutDelete

func (s *MemoryStore) VerifyWithFuncWithoutDelete(id string, input interface{}, compareFunc func(stored, input interface{}) bool) (bool, error)

VerifyWithFuncWithoutDelete checks the captcha without removing it (for pre-verification).

type Payload

type Payload struct {
	ID    string      `json:"captchaId"`
	Type  Type        `json:"captchaType"`
	Value interface{} `json:"captchaValue"`
}

Payload is the client proof submitted with protected actions.

type Point

type Point struct {
	X int `json:"x"`
	Y int `json:"y"`
}

Point is a click coordinate in logical pixels.

type Result

type Result struct {
	ID      string                 `json:"id"`
	Type    Type                   `json:"type"`
	Data    map[string]interface{} `json:"data"`
	Expires time.Time              `json:"expires"`
}

Result is returned when a captcha challenge is created.

type RotateCaptcha

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

RotateCaptcha is a rotation challenge: an image is rotated by a random angle and the user must rotate it back to upright (0 degrees). The server stores the angle the image was rotated by; the user submits the angle they rotated it back. A pass requires the residual to be within tolerance of 0 (mod 360).

func NewRotateCaptcha

func NewRotateCaptcha(size, tolerance int, expiration time.Duration, store Store) *RotateCaptcha

NewRotateCaptcha creates a rotate captcha generator.

func (*RotateCaptcha) Generate

func (rc *RotateCaptcha) Generate() (*Result, error)

Generate creates a rotate challenge. The response includes a circular image rotated by a random angle; the user must rotate it back to upright.

func (*RotateCaptcha) Verify

func (rc *RotateCaptcha) Verify(id string, userAngle int) (bool, error)

Verify checks that the user's rotation angle brings the image within tolerance of upright. The user submits the angle they rotated; the residual is (storedAngle - userAngle) mod 360, which must be within tolerance of 0.

type SliderCaptcha

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

SliderCaptcha is a drag-to-end slider challenge.

func NewSliderCaptcha

func NewSliderCaptcha(trackWidth int, passRatio float64, expiration time.Duration, store Store) *SliderCaptcha

NewSliderCaptcha creates a slider captcha manager.

func (*SliderCaptcha) Generate

func (sc *SliderCaptcha) Generate() (*Result, error)

Generate creates a slider challenge.

func (*SliderCaptcha) Verify

func (sc *SliderCaptcha) Verify(id string, x int) (bool, error)

Verify checks that the slider was dragged near the right edge.

type Store

type Store interface {
	Set(id string, data interface{}, expires time.Time) error
	Get(id string) (interface{}, error)
	Delete(id string) error
	VerifyWithFunc(id string, input interface{}, compareFunc func(stored, input interface{}) bool) (bool, error)
	VerifyWithFuncWithoutDelete(id string, input interface{}, compareFunc func(stored, input interface{}) bool) (bool, error)
}

Store is the interface for captcha storage backends.

type Type

type Type string

Type is the captcha challenge kind.

const (
	TypeImage  Type = "image"  // distorted text image
	TypeClick  Type = "click"  // ordered click on characters
	TypeSlider Type = "slider" // drag slider to the end
	TypeMath   Type = "math"   // arithmetic problem
	TypeJigsaw Type = "jigsaw" // drag puzzle piece to fit
	TypeRotate Type = "rotate" // rotate image to upright position
	TypeRandom Type = "random"
)

func RandomType

func RandomType() Type

RandomType picks one captcha kind uniformly at random.

Jump to

Keyboard shortcuts

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