mirror of
https://github.com/yusing/godoxy.git
synced 2026-03-26 11:01:07 +01:00
This is a large-scale refactoring across the codebase that replaces the custom `gperr.Error` type with Go's standard `error` interface. The changes include: - Replacing `gperr.Error` return types with `error` in function signatures - Using `errors.New()` and `fmt.Errorf()` instead of `gperr.New()` and `gperr.Errorf()` - Using `%w` format verb for error wrapping instead of `.With()` method - Replacing `gperr.Subject()` calls with `gperr.PrependSubject()` - Converting error logging from `gperr.Log*()` functions to zerolog's `.Err().Msg()` pattern - Update NewLogger to handle multiline error message - Updating `goutils` submodule to latest commit This refactoring aligns with Go idioms and removes the dependency on custom error handling abstractions in favor of standard library patterns.
265 lines
6.5 KiB
Markdown
265 lines
6.5 KiB
Markdown
# Captcha Middleware
|
|
|
|
CAPTCHA verification middleware package providing session-based captcha challenge and verification.
|
|
|
|
## Overview
|
|
|
|
This package implements CAPTCHA verification middleware that protects routes by requiring users to complete a CAPTCHA challenge before accessing the protected resource. It supports pluggable providers (currently hCAPTCHA) and uses encrypted sessions for verification state.
|
|
|
|
## Architecture
|
|
|
|
```mermaid
|
|
graph TD
|
|
A[Client Request] --> B{Captcha Session?}
|
|
B -->|Valid| C[Proceed to Backend]
|
|
B -->|Invalid| D[Show CAPTCHA Page]
|
|
|
|
D --> E{POST with Token?}
|
|
E -->|Valid| F[Create Session<br/>Set Cookie]
|
|
E -->|Invalid| G[Show Error]
|
|
F --> C
|
|
|
|
subgraph Captcha Provider
|
|
H[hCAPTCHA API]
|
|
D -->|Script/Form HTML| H
|
|
F -->|Verify Token| H
|
|
end
|
|
|
|
subgraph Session Store
|
|
I[CaptchaSessions<br/>jsonstore]
|
|
end
|
|
|
|
F --> I
|
|
I -.->|Session Check| B
|
|
```
|
|
|
|
## Captcha Flow
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant C as Client
|
|
participant M as Middleware
|
|
participant P as Provider
|
|
participant S as Session Store
|
|
participant B as Backend
|
|
|
|
C->>M: Request (no session)
|
|
M->>M: Check cookie
|
|
M->>M: Session not found/expired
|
|
M->>C: Send CAPTCHA Page
|
|
|
|
C->>M: POST with captcha response
|
|
M->>P: Verify token
|
|
P-->>M: Verification result
|
|
|
|
alt Verification successful
|
|
M->>S: Store session
|
|
M->>C: Set session cookie<br/>Redirect to protected path
|
|
C->>M: Request (with session cookie)
|
|
M->>S: Validate session
|
|
M->>B: Forward request
|
|
else Verification failed
|
|
M->>C: Error: verification failed
|
|
end
|
|
```
|
|
|
|
## Core Components
|
|
|
|
### Provider Interface
|
|
|
|
```go
|
|
type Provider interface {
|
|
// CSP directives for the captcha provider
|
|
CSPDirectives() []string
|
|
// CSP sources for the captcha provider
|
|
CSPSources() []string
|
|
// Verify the captcha response from the request
|
|
Verify(r *http.Request) error
|
|
// Session expiry duration after successful verification
|
|
SessionExpiry() time.Duration
|
|
// Script HTML to include in the page
|
|
ScriptHTML() string
|
|
// Form HTML to render the captcha widget
|
|
FormHTML() string
|
|
}
|
|
```
|
|
|
|
### ProviderBase
|
|
|
|
```go
|
|
type ProviderBase struct {
|
|
Expiry time.Duration `json:"session_expiry"` // Default: 24 hours
|
|
}
|
|
|
|
func (p *ProviderBase) SessionExpiry() time.Duration
|
|
```
|
|
|
|
### hCAPTCHA Provider
|
|
|
|
```go
|
|
type HcaptchaProvider struct {
|
|
ProviderBase
|
|
SiteKey string `json:"site_key" validate:"required"`
|
|
Secret string `json:"secret" validate:"required"`
|
|
}
|
|
|
|
// CSP Directives: script-src, frame-src, style-src, connect-src
|
|
// CSP Sources: https://hcaptcha.com, https://*.hcaptcha.com
|
|
```
|
|
|
|
### Captcha Session
|
|
|
|
```go
|
|
type CaptchaSession struct {
|
|
ID string `json:"id"`
|
|
Expiry time.Time `json:"expiry"`
|
|
}
|
|
|
|
var CaptchaSessions = jsonstore.Store[*CaptchaSession]("captcha_sessions")
|
|
|
|
func newCaptchaSession(p Provider) *CaptchaSession
|
|
func (s *CaptchaSession) expired() bool
|
|
```
|
|
|
|
## Middleware Integration
|
|
|
|
```go
|
|
type hCaptcha struct {
|
|
captcha.HcaptchaProvider
|
|
}
|
|
|
|
func (h *hCaptcha) before(w http.ResponseWriter, r *http.Request) bool {
|
|
return captcha.PreRequest(h, w, r)
|
|
}
|
|
|
|
var HCaptcha = NewMiddleware[hCaptcha]()
|
|
```
|
|
|
|
### PreRequest Handler
|
|
|
|
```go
|
|
func PreRequest(p Provider, w http.ResponseWriter, r *http.Request) (proceed bool)
|
|
```
|
|
|
|
This function:
|
|
|
|
1. Checks for valid session cookie
|
|
1. Validates session expiry
|
|
1. Returns true if session is valid
|
|
1. For non-HTML requests, returns 403 Forbidden
|
|
1. For POST requests, verifies the captcha token
|
|
1. For GET requests, renders the CAPTCHA challenge page
|
|
|
|
## Configuration
|
|
|
|
### hCAPTCHA Configuration
|
|
|
|
```yaml
|
|
middleware:
|
|
my-captcha:
|
|
use: hcaptcha
|
|
site_key: "YOUR_SITE_KEY"
|
|
secret: "YOUR_SECRET"
|
|
session_expiry: 24h # optional, default 24h
|
|
```
|
|
|
|
### Route Configuration
|
|
|
|
```yaml
|
|
routes:
|
|
- host: example.com
|
|
path: /admin
|
|
middlewares:
|
|
- my-captcha
|
|
```
|
|
|
|
## Usage Examples
|
|
|
|
### Basic Setup
|
|
|
|
```go
|
|
import "github.com/yusing/godoxy/internal/net/gphttp/middleware"
|
|
|
|
hcaptchaMiddleware := middleware.HCaptcha.New(middleware.OptionsRaw{
|
|
"site_key": "your-site-key",
|
|
"secret": "your-secret",
|
|
})
|
|
```
|
|
|
|
### Using in Middleware Chain
|
|
|
|
```yaml
|
|
# config/middlewares/admin-protection.yml
|
|
- use: captcha
|
|
site_key: "${HCAPTCHA_SITE_KEY}"
|
|
secret: "${HCAPTCHA_SECRET}"
|
|
bypass:
|
|
- type: CIDR
|
|
value: 10.0.0.0/8
|
|
```
|
|
|
|
## Session Management
|
|
|
|
Sessions are stored in a JSON-based store with the following properties:
|
|
|
|
- **Session ID**: 32-byte CRNG (`crypto/rand.Read`) random hex string
|
|
- **Expiry**: Configurable duration (default 24 hours)
|
|
- **Cookie**: `godoxy_captcha_session` with HttpOnly flag
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
A[Session Created] --> B[Cookie Set]
|
|
B --> C[Client Sends Cookie]
|
|
C --> D{Session Valid?}
|
|
D -->|Yes| E[Proceed]
|
|
D -->|No| F{HTML Request?}
|
|
F -->|Yes| G[Show CAPTCHA]
|
|
F -->|No| H[403 Forbidden]
|
|
```
|
|
|
|
## CSP Integration
|
|
|
|
The CAPTCHA provider supplies CSP directives that should be added to the response:
|
|
|
|
```go
|
|
// hCAPTCHA CSP Directives
|
|
CSPDirectives() []string
|
|
// Returns: ["script-src", "frame-src", "style-src", "connect-src"]
|
|
|
|
CSPSources() []string
|
|
// Returns: ["https://hcaptcha.com", "https://*.hcaptcha.com"]
|
|
```
|
|
|
|
## HTML Template
|
|
|
|
The package includes an embedded HTML template (`captcha.html`) that renders the CAPTCHA challenge page with:
|
|
|
|
- Provider script (`<script src="https://js.hcaptcha.com/1/api.js">`)
|
|
- Provider form (`<div class="h-captcha" data-sitekey="...">`)
|
|
- Auto-submit callback on successful verification
|
|
|
|
## Security Considerations
|
|
|
|
1. **Session Cookie**: Uses HttpOnly flag to prevent JavaScript access
|
|
1. **Token Verification**: Tokens are verified server-side with the CAPTCHA provider
|
|
1. **Remote IP**: Client IP is included in verification request to prevent token reuse
|
|
1. **Session Expiry**: Sessions expire after configurable duration
|
|
1. **Non-HTML Fallback**: Non-HTML requests receive 403 without challenge page
|
|
|
|
## Error Handling
|
|
|
|
```go
|
|
var ErrCaptchaVerificationFailed = errors.New("captcha verification failed")
|
|
|
|
// Verification errors are logged with request details
|
|
log.Warn().Err(err).Str("url", r.URL.String()).Str("remote_addr", r.RemoteAddr).Msg("failed to verify captcha")
|
|
```
|
|
|
|
## Integration with GoDoxy
|
|
|
|
The captcha middleware integrates with GoDoxy's:
|
|
|
|
- **Authentication**: Sessions are managed via `auth.SetTokenCookie`
|
|
- **Session Store**: Uses `jsonstore` for persistent session storage
|
|
- **Middleware Framework**: Implements `RequestModifier` interface
|