mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-18 06:29:42 +02:00
docs: add per package README for implementation details (AI generated with human review)
This commit is contained in:
144
internal/net/README.md
Normal file
144
internal/net/README.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# Network Utilities
|
||||
|
||||
The net package provides network utility functions for GoDoxy, including TCP connection testing and network-related helpers.
|
||||
|
||||
## Overview
|
||||
|
||||
The net package implements network utility functions that are used throughout GoDoxy for connectivity testing, TCP operations, and network-related utilities.
|
||||
|
||||
### Key Features
|
||||
|
||||
- TCP connection testing (ping)
|
||||
- Connection utilities
|
||||
|
||||
## Core Functions
|
||||
|
||||
### TCP Ping
|
||||
|
||||
```go
|
||||
// PingTCP pings a TCP endpoint by attempting a connection.
|
||||
func PingTCP(ctx context.Context, ip net.IP, port int) error
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```go
|
||||
import "github.com/yusing/godoxy/internal/net"
|
||||
|
||||
func checkService(ctx context.Context, ip string, port int) error {
|
||||
addr := net.ParseIP(ip)
|
||||
if addr == nil {
|
||||
return fmt.Errorf("invalid IP: %s", ip)
|
||||
}
|
||||
|
||||
err := net.PingTCP(ctx, addr, port)
|
||||
if err != nil {
|
||||
return fmt.Errorf("service %s:%d unreachable: %w", ip, port, err)
|
||||
}
|
||||
|
||||
fmt.Printf("Service %s:%d is reachable\n", ip, port)
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### Timeout Usage
|
||||
|
||||
```go
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
ip := net.ParseIP("192.168.1.100")
|
||||
err := net.PingTCP(ctx, ip, 8080)
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
log.Println("Connection timed out")
|
||||
} else {
|
||||
log.Printf("Connection failed: %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation
|
||||
|
||||
```go
|
||||
func PingTCP(ctx context.Context, ip net.IP, port int) error {
|
||||
var dialer net.Dialer
|
||||
conn, err := dialer.DialContext(ctx, "tcp", fmt.Sprintf("%s:%d", ip, port))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conn.Close()
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Caller
|
||||
participant Dialer
|
||||
participant TCPEndpoint
|
||||
participant Connection
|
||||
|
||||
Caller->>Dialer: DialContext("tcp", "ip:port")
|
||||
Dialer->>TCPEndpoint: SYN
|
||||
TCPEndpoint-->>Dialer: SYN-ACK
|
||||
Dialer->>Connection: Create connection
|
||||
Connection-->>Dialer: Connection
|
||||
Dialer-->>Caller: nil error
|
||||
|
||||
Note over Caller,Connection: Connection immediately closed
|
||||
Connection->>TCPEndpoint: FIN
|
||||
TCPEndpoint-->>Connection: FIN-ACK
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Service Health Check
|
||||
|
||||
```go
|
||||
func checkServices(ctx context.Context, services []Service) error {
|
||||
for _, svc := range services {
|
||||
ip := net.ParseIP(svc.IP)
|
||||
if ip == nil {
|
||||
return fmt.Errorf("invalid IP for %s: %s", svc.Name, svc.IP)
|
||||
}
|
||||
|
||||
if err := net.PingTCP(ctx, ip, svc.Port); err != nil {
|
||||
return fmt.Errorf("service %s (%s:%d) unreachable: %w",
|
||||
svc.Name, svc.IP, svc.Port, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### Proxmox Container Reachability
|
||||
|
||||
```go
|
||||
// Check if a Proxmox container is reachable on its proxy port
|
||||
func checkContainerReachability(ctx context.Context, node *proxmox.Node, vmid int, port int) error {
|
||||
ips, err := node.LXCGetIPs(ctx, vmid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, ip := range ips {
|
||||
if err := net.PingTCP(ctx, ip, port); err == nil {
|
||||
return nil // Found reachable IP
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("no reachable IP found for container %d", vmid)
|
||||
}
|
||||
```
|
||||
|
||||
## Related Packages
|
||||
|
||||
- **Route**: Uses TCP ping for load balancing health checks
|
||||
- **Proxmox**: Uses TCP ping to verify container reachability
|
||||
- **Idlewatcher**: Uses TCP ping to check idle status
|
||||
146
internal/net/gphttp/README.md
Normal file
146
internal/net/gphttp/README.md
Normal file
@@ -0,0 +1,146 @@
|
||||
# gphttp
|
||||
|
||||
HTTP utilities package providing transport configuration, default HTTP client, and a wrapper around `http.ServeMux` with panic recovery.
|
||||
|
||||
## Overview
|
||||
|
||||
This package provides shared HTTP utilities used throughout GoDoxy:
|
||||
|
||||
- **Default HTTP Client**: Pre-configured `http.Client` with secure settings
|
||||
- **Transport Factory**: Functions to create optimized `http.Transport` configurations
|
||||
- **ServeMux Wrapper**: Extended `http.ServeMux` with panic recovery for handler registration
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[HTTP Request] --> B[gphttp.Client]
|
||||
B --> C[Transport]
|
||||
C --> D[Network Connection]
|
||||
|
||||
E[Server Setup] --> F[gphttp.ServeMux]
|
||||
F --> G[http.ServeMux]
|
||||
G --> H[HTTP Handlers]
|
||||
```
|
||||
|
||||
## Core Components
|
||||
|
||||
### HTTP Client
|
||||
|
||||
The package exports a pre-configured `http.Client` with secure defaults:
|
||||
|
||||
```go
|
||||
var (
|
||||
httpClient = &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
DisableKeepAlives: true,
|
||||
ForceAttemptHTTP2: false,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 3 * time.Second,
|
||||
KeepAlive: 60 * time.Second,
|
||||
}).DialContext,
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
|
||||
Get = httpClient.Get
|
||||
Post = httpClient.Post
|
||||
Head = httpClient.Head
|
||||
Do = httpClient.Do
|
||||
)
|
||||
```
|
||||
|
||||
### Transport Factory
|
||||
|
||||
Functions for creating optimized HTTP transports:
|
||||
|
||||
```go
|
||||
// NewTransport creates an http.Transport with proxy support and optimized settings
|
||||
func NewTransport() *http.Transport
|
||||
|
||||
// NewTransportWithTLSConfig creates an http.Transport with custom TLS configuration
|
||||
func NewTransportWithTLSConfig(tlsConfig *tls.Config) *http.Transport
|
||||
```
|
||||
|
||||
Default transport settings:
|
||||
|
||||
- `MaxIdleConnsPerHost`: 1000
|
||||
- `IdleConnTimeout`: 90 seconds
|
||||
- `TLSHandshakeTimeout`: 10 seconds
|
||||
- `ResponseHeaderTimeout`: 60 seconds
|
||||
- `WriteBufferSize` / `ReadBufferSize`: 16KB
|
||||
|
||||
### ServeMux Wrapper
|
||||
|
||||
Extended `http.ServeMux` with panic recovery:
|
||||
|
||||
```go
|
||||
type ServeMux struct {
|
||||
*http.ServeMux
|
||||
}
|
||||
|
||||
func NewServeMux() ServeMux
|
||||
func (mux ServeMux) Handle(pattern string, handler http.Handler) (err error)
|
||||
func (mux ServeMux) HandleFunc(pattern string, handler http.HandlerFunc) (err error)
|
||||
```
|
||||
|
||||
The `Handle` and `HandleFunc` methods recover from panics and return them as errors, preventing one bad handler from crashing the entire server.
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Using the Default Client
|
||||
|
||||
```go
|
||||
import "github.com/yusing/godoxy/internal/net/gphttp"
|
||||
|
||||
// Simple GET request
|
||||
resp, err := gphttp.Get("https://example.com")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// POST request
|
||||
resp, err := gphttp.Post("https://example.com", "application/json", body)
|
||||
```
|
||||
|
||||
### Creating Custom Transports
|
||||
|
||||
```go
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"github.com/yusing/godoxy/internal/net/gphttp"
|
||||
)
|
||||
|
||||
// Default transport with environment proxy
|
||||
transport := gphttp.NewTransport()
|
||||
|
||||
// Custom TLS configuration
|
||||
tlsConfig := &tls.Config{
|
||||
ServerName: "example.com",
|
||||
}
|
||||
transport := gphttp.NewTransportWithTLSConfig(tlsConfig)
|
||||
```
|
||||
|
||||
### Using ServeMux with Panic Recovery
|
||||
|
||||
```go
|
||||
mux := gphttp.NewServeMux()
|
||||
|
||||
// Register handlers - panics are converted to errors
|
||||
if err := mux.HandleFunc("/api", apiHandler); err != nil {
|
||||
log.Printf("handler registration failed: %v", err)
|
||||
}
|
||||
```
|
||||
|
||||
## Integration Points
|
||||
|
||||
- Used by `internal/net/gphttp/middleware` for HTTP request/response processing
|
||||
- Used by `internal/net/gphttp/loadbalancer` for backend connections
|
||||
- Used throughout the route handling system
|
||||
|
||||
## Configuration
|
||||
|
||||
The default client disables HTTP/2 (`ForceAttemptHTTP2: false`) and keep-alives (`DisableKeepAlives: true`) for security and compatibility reasons. The transport uses environment proxy settings via `http.ProxyFromEnvironment`.
|
||||
304
internal/net/gphttp/loadbalancer/README.md
Normal file
304
internal/net/gphttp/loadbalancer/README.md
Normal file
@@ -0,0 +1,304 @@
|
||||
# Load Balancer
|
||||
|
||||
Load balancing package providing multiple distribution algorithms, sticky sessions, and server health management.
|
||||
|
||||
## Overview
|
||||
|
||||
This package implements a flexible load balancer for distributing HTTP requests across multiple backend servers. It supports multiple balancing algorithms and integrates with GoDoxy's task management and health monitoring systems.
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[HTTP Request] --> B[LoadBalancer]
|
||||
B --> C{Algorithm}
|
||||
C -->|Round Robin| D[RoundRobin]
|
||||
C -->|Least Connections| E[LeastConn]
|
||||
C -->|IP Hash| F[IPHash]
|
||||
|
||||
D --> G[Available Servers]
|
||||
E --> G
|
||||
F --> G
|
||||
|
||||
G --> H[Server Selection]
|
||||
H --> I{Sticky Session?}
|
||||
I -->|Yes| J[Set Cookie]
|
||||
I -->|No| K[Continue]
|
||||
|
||||
J --> L[ServeHTTP]
|
||||
K --> L
|
||||
```
|
||||
|
||||
## Algorithms
|
||||
|
||||
### Round Robin
|
||||
|
||||
Distributes requests evenly across all available servers in sequence.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Client
|
||||
participant LB as LoadBalancer
|
||||
participant S1 as Server 1
|
||||
participant S2 as Server 2
|
||||
participant S3 as Server 3
|
||||
|
||||
C->>LB: Request 1
|
||||
LB->>S1: Route to Server 1
|
||||
C->>LB: Request 2
|
||||
LB->>S2: Route to Server 2
|
||||
C->>LB: Request 3
|
||||
LB->>S3: Route to Server 3
|
||||
C->>LB: Request 4
|
||||
LB->>S1: Route to Server 1
|
||||
```
|
||||
|
||||
### Least Connections
|
||||
|
||||
Routes requests to the server with the fewest active connections.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph LB["Load Balancer"]
|
||||
direction TB
|
||||
A["Server A<br/>3 connections"]
|
||||
B["Server B<br/>1 connection"]
|
||||
C["Server C<br/>5 connections"]
|
||||
end
|
||||
|
||||
New["New Request"] --> B
|
||||
```
|
||||
|
||||
### IP Hash
|
||||
|
||||
Consistently routes requests from the same client IP to the same server using hash-based distribution.
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
Client1["Client IP: 192.168.1.10"] -->|Hash| ServerA
|
||||
Client2["Client IP: 192.168.1.20"] -->|Hash| ServerB
|
||||
Client3["Client IP: 192.168.1.30"] -->|Hash| ServerA
|
||||
```
|
||||
|
||||
## Core Components
|
||||
|
||||
### LoadBalancer
|
||||
|
||||
```go
|
||||
type LoadBalancer struct {
|
||||
*types.LoadBalancerConfig
|
||||
task *task.Task
|
||||
pool pool.Pool[types.LoadBalancerServer]
|
||||
poolMu sync.Mutex
|
||||
sumWeight int
|
||||
startTime time.Time
|
||||
}
|
||||
```
|
||||
|
||||
**Key Methods:**
|
||||
|
||||
```go
|
||||
// Create a new load balancer from configuration
|
||||
func New(cfg *types.LoadBalancerConfig) *LoadBalancer
|
||||
|
||||
// Start the load balancer as a background task
|
||||
func (lb *LoadBalancer) Start(parent task.Parent) gperr.Error
|
||||
|
||||
// Update configuration dynamically
|
||||
func (lb *LoadBalancer) UpdateConfigIfNeeded(cfg *types.LoadBalancerConfig)
|
||||
|
||||
// Add a backend server
|
||||
func (lb *LoadBalancer) AddServer(srv types.LoadBalancerServer)
|
||||
|
||||
// Remove a backend server
|
||||
func (lb *LoadBalancer) RemoveServer(srv types.LoadBalancerServer)
|
||||
|
||||
// ServeHTTP implements http.Handler
|
||||
func (lb *LoadBalancer) ServeHTTP(rw http.ResponseWriter, r *http.Request)
|
||||
```
|
||||
|
||||
### Server
|
||||
|
||||
```go
|
||||
type server struct {
|
||||
name string
|
||||
url *nettypes.URL
|
||||
weight int
|
||||
http.Handler
|
||||
types.HealthMonitor
|
||||
}
|
||||
|
||||
// Create a new backend server
|
||||
func NewServer(name string, url *nettypes.URL, weight int, handler http.Handler, healthMon types.HealthMonitor) types.LoadBalancerServer
|
||||
```
|
||||
|
||||
**Server Interface:**
|
||||
|
||||
```go
|
||||
type LoadBalancerServer interface {
|
||||
Name() string
|
||||
URL() *nettypes.URL
|
||||
Key() string
|
||||
Weight() int
|
||||
SetWeight(weight int)
|
||||
Status() types.HealthStatus
|
||||
Latency() time.Duration
|
||||
ServeHTTP(rw http.ResponseWriter, r *http.Request)
|
||||
TryWake() error
|
||||
}
|
||||
```
|
||||
|
||||
### Sticky Sessions
|
||||
|
||||
The load balancer supports sticky sessions via cookies:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Client Request] --> B{Cookie exists?}
|
||||
B -->|No| C[Select Server]
|
||||
B -->|Yes| D[Extract Server Hash]
|
||||
D --> E[Find Matching Server]
|
||||
C --> F[Set Cookie<br/>godoxy_lb_sticky]
|
||||
E --> G[Route to Server]
|
||||
F --> G
|
||||
```
|
||||
|
||||
```go
|
||||
// Cookie settings
|
||||
Name: "godoxy_lb_sticky"
|
||||
MaxAge: Configurable (default: 24 hours)
|
||||
HttpOnly: true
|
||||
SameSite: Lax
|
||||
Secure: Based on TLS/Forwarded-Proto
|
||||
```
|
||||
|
||||
## Balancing Modes
|
||||
|
||||
```go
|
||||
const (
|
||||
LoadbalanceModeUnset = ""
|
||||
LoadbalanceModeRoundRobin = "round_robin"
|
||||
LoadbalanceModeLeastConn = "least_conn"
|
||||
LoadbalanceModeIPHash = "ip_hash"
|
||||
)
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
```go
|
||||
type LoadBalancerConfig struct {
|
||||
Link string // Link name
|
||||
Mode LoadbalanceMode // Balancing algorithm
|
||||
Sticky bool // Enable sticky sessions
|
||||
StickyMaxAge time.Duration // Cookie max age
|
||||
Options map[string]any // Algorithm-specific options
|
||||
}
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Round Robin Load Balancer
|
||||
|
||||
```go
|
||||
config := &types.LoadBalancerConfig{
|
||||
Link: "my-service",
|
||||
Mode: types.LoadbalanceModeRoundRobin,
|
||||
}
|
||||
|
||||
lb := loadbalancer.New(config)
|
||||
lb.Start(parentTask)
|
||||
|
||||
// Add backend servers
|
||||
lb.AddServer(loadbalancer.NewServer("backend-1", url1, 10, handler1, health1))
|
||||
lb.AddServer(loadbalancer.NewServer("backend-2", url2, 10, handler2, health2))
|
||||
|
||||
// Use as HTTP handler
|
||||
http.Handle("/", lb)
|
||||
```
|
||||
|
||||
### Least Connections with Sticky Sessions
|
||||
|
||||
```go
|
||||
config := &types.LoadBalancerConfig{
|
||||
Link: "api-service",
|
||||
Mode: types.LoadbalanceModeLeastConn,
|
||||
Sticky: true,
|
||||
StickyMaxAge: 1 * time.Hour,
|
||||
}
|
||||
|
||||
lb := loadbalancer.New(config)
|
||||
lb.Start(parentTask)
|
||||
|
||||
for _, srv := range backends {
|
||||
lb.AddServer(srv)
|
||||
}
|
||||
```
|
||||
|
||||
### IP Hash Load Balancer with Real IP
|
||||
|
||||
```go
|
||||
config := &types.LoadBalancerConfig{
|
||||
Link: "user-service",
|
||||
Mode: types.LoadbalanceModeIPHash,
|
||||
Options: map[string]any{
|
||||
"header": "X-Real-IP",
|
||||
"from": []string{"10.0.0.0/8", "172.16.0.0/12"},
|
||||
"recursive": true,
|
||||
},
|
||||
}
|
||||
|
||||
lb := loadbalancer.New(config)
|
||||
```
|
||||
|
||||
### Server Weight Management
|
||||
|
||||
```go
|
||||
// Servers are balanced based on weight (max total: 100)
|
||||
lb.AddServer(NewServer("server1", url1, 30, handler, health))
|
||||
lb.AddServer(NewServer("server2", url2, 50, handler, health))
|
||||
lb.AddServer(NewServer("server3", url3, 20, handler, health))
|
||||
|
||||
// Weights are auto-rebalanced if total != 100
|
||||
```
|
||||
|
||||
## Idlewatcher Integration
|
||||
|
||||
The load balancer integrates with the idlewatcher system:
|
||||
|
||||
- Wake events path (`/api/wake`): Wakes all idle servers
|
||||
- Favicon and loading page paths: Bypassed for sticky session handling
|
||||
- Server wake support via `TryWake()` interface
|
||||
|
||||
## Health Monitoring
|
||||
|
||||
The load balancer implements `types.HealthMonitor`:
|
||||
|
||||
```go
|
||||
func (lb *LoadBalancer) Status() types.HealthStatus
|
||||
func (lb *LoadBalancer) Detail() string
|
||||
func (lb *LoadBalancer) Uptime() time.Duration
|
||||
func (lb *LoadBalancer) Latency() time.Duration
|
||||
```
|
||||
|
||||
Health JSON representation:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-service",
|
||||
"status": "healthy",
|
||||
"detail": "3/3 servers are healthy",
|
||||
"started": "2024-01-01T00:00:00Z",
|
||||
"uptime": "1h2m3s",
|
||||
"latency": "10ms",
|
||||
"extra": {
|
||||
"config": {...},
|
||||
"pool": {...}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Thread Safety
|
||||
|
||||
- Server pool operations are protected by `poolMu` mutex
|
||||
- Algorithm-specific state uses atomic operations or dedicated synchronization
|
||||
- Least connections uses `xsync.Map` for thread-safe connection counting
|
||||
336
internal/net/gphttp/middleware/README.md
Normal file
336
internal/net/gphttp/middleware/README.md
Normal file
@@ -0,0 +1,336 @@
|
||||
# Middleware
|
||||
|
||||
HTTP middleware framework providing request/response processing, middleware chaining, and composition from YAML files.
|
||||
|
||||
## Overview
|
||||
|
||||
This package implements a flexible HTTP middleware system for GoDoxy. Middleware can modify requests before they reach the backend and modify responses before they return to the client. The system supports:
|
||||
|
||||
- **Request Modifiers**: Process requests before forwarding
|
||||
- **Response Modifiers**: Modify responses before returning to client
|
||||
- **Middleware Chaining**: Compose multiple middleware in priority order
|
||||
- **YAML Composition**: Define middleware chains in configuration files
|
||||
- **Bypass Rules**: Skip middleware based on request properties
|
||||
- **Dynamic Loading**: Load middleware definitions from files at runtime
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[HTTP Request] --> B[Middleware Chain]
|
||||
|
||||
subgraph Chain [Middleware Pipeline]
|
||||
direction LR
|
||||
B1[RedirectHTTP] --> B2[RealIP]
|
||||
B2 --> B3[RateLimit]
|
||||
B3 --> B4[OIDC]
|
||||
B4 --> B5[CustomErrorPage]
|
||||
end
|
||||
|
||||
Chain --> C[Backend Handler]
|
||||
C --> D[Response Modifier]
|
||||
|
||||
subgraph ResponseChain [Response Pipeline]
|
||||
direction LR
|
||||
D1[CustomErrorPage] --> D2[ModifyResponse]
|
||||
D2 --> D3[ModifyHTML]
|
||||
end
|
||||
|
||||
ResponseChain --> E[HTTP Response]
|
||||
```
|
||||
|
||||
## Middleware Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Client
|
||||
participant M as Middleware Chain
|
||||
participant B as Backend
|
||||
participant R as Response Chain
|
||||
participant C2 as Client
|
||||
|
||||
C->>M: HTTP Request
|
||||
M->>M: before() - RequestModifier
|
||||
M->>M: Check Bypass Rules
|
||||
M->>M: Sort by Priority
|
||||
|
||||
par Request Modifiers
|
||||
M->>M: Middleware 1 (before)
|
||||
M->>M: Middleware 2 (before)
|
||||
end
|
||||
|
||||
M->>B: Forward Request
|
||||
|
||||
B-->>M: HTTP Response
|
||||
|
||||
par Response Modifiers
|
||||
M->>R: ResponseModifier 1
|
||||
M->>R: ResponseModifier 2
|
||||
end
|
||||
|
||||
R-->>C2: Modified Response
|
||||
```
|
||||
|
||||
## Core Components
|
||||
|
||||
### Middleware
|
||||
|
||||
```go
|
||||
type Middleware struct {
|
||||
name string
|
||||
construct ImplNewFunc
|
||||
impl any
|
||||
commonOptions
|
||||
}
|
||||
|
||||
type commonOptions struct {
|
||||
Priority int `json:"priority"` // Default: 10, 0 is highest
|
||||
Bypass Bypass `json:"bypass"`
|
||||
}
|
||||
```
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
```go
|
||||
// RequestModifier - modify or filter requests
|
||||
type RequestModifier interface {
|
||||
before(w http.ResponseWriter, r *http.Request) (proceed bool)
|
||||
}
|
||||
|
||||
// ResponseModifier - modify responses
|
||||
type ResponseModifier interface {
|
||||
modifyResponse(r *http.Response) error
|
||||
}
|
||||
|
||||
// MiddlewareWithSetup - one-time setup after construction
|
||||
type MiddlewareWithSetup interface {
|
||||
setup()
|
||||
}
|
||||
|
||||
// MiddlewareFinalizer - finalize after options applied
|
||||
type MiddlewareFinalizer interface {
|
||||
finalize()
|
||||
}
|
||||
|
||||
// MiddlewareFinalizerWithError - finalize with error handling
|
||||
type MiddlewareFinalizerWithError interface {
|
||||
finalize() error
|
||||
}
|
||||
```
|
||||
|
||||
### Middleware Chain
|
||||
|
||||
```go
|
||||
type middlewareChain struct {
|
||||
beforess []RequestModifier
|
||||
modResps []ResponseModifier
|
||||
}
|
||||
|
||||
func NewMiddlewareChain(name string, chain []*Middleware) *Middleware
|
||||
```
|
||||
|
||||
### Bypass Rules
|
||||
|
||||
```go
|
||||
type Bypass []rules.RuleOn
|
||||
|
||||
// ShouldBypass checks if request should skip middleware
|
||||
func (b Bypass) ShouldBypass(w http.ResponseWriter, r *http.Request) bool
|
||||
```
|
||||
|
||||
## Available Middleware
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------------------------------- | -------- | ------------------------------------------ |
|
||||
| `redirecthttp` | Request | Redirect HTTP to HTTPS |
|
||||
| `oidc` | Request | OIDC authentication |
|
||||
| `forwardauth` | Request | Forward authentication to external service |
|
||||
| `modifyrequest` / `request` | Request | Modify request headers and path |
|
||||
| `modifyresponse` / `response` | Response | Modify response headers |
|
||||
| `setxforwarded` | Request | Set X-Forwarded headers |
|
||||
| `hidexforwarded` | Request | Remove X-Forwarded headers |
|
||||
| `modifyhtml` | Response | Inject HTML into responses |
|
||||
| `themed` | Response | Apply theming to HTML |
|
||||
| `errorpage` / `customerrorpage` | Response | Serve custom error pages |
|
||||
| `realip` | Request | Extract real client IP from headers |
|
||||
| `cloudflarerealip` | Request | Cloudflare-specific real IP extraction |
|
||||
| `cidrwhitelist` | Request | Allow only specific IP ranges |
|
||||
| `ratelimit` | Request | Rate limiting by IP |
|
||||
| `hcaptcha` | Request | hCAPTCHA verification |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Creating a Middleware
|
||||
|
||||
```go
|
||||
import "github.com/yusing/godoxy/internal/net/gphttp/middleware"
|
||||
|
||||
type myMiddleware struct {
|
||||
SomeOption string `json:"some_option"`
|
||||
}
|
||||
|
||||
func (m *myMiddleware) before(w http.ResponseWriter, r *http.Request) bool {
|
||||
// Process request
|
||||
r.Header.Set("X-Custom", m.SomeOption)
|
||||
return true // false would block the request
|
||||
}
|
||||
|
||||
var MyMiddleware = middleware.NewMiddleware[myMiddleware]()
|
||||
```
|
||||
|
||||
### Building Middleware from Map
|
||||
|
||||
```go
|
||||
middlewaresMap := map[string]middleware.OptionsRaw{
|
||||
"realip": {
|
||||
"priority": 5,
|
||||
"header": "X-Real-IP",
|
||||
"from": []string{"10.0.0.0/8"},
|
||||
},
|
||||
"ratelimit": {
|
||||
"priority": 10,
|
||||
"average": 10,
|
||||
"burst": 20,
|
||||
},
|
||||
}
|
||||
|
||||
mid, err := middleware.BuildMiddlewareFromMap("my-chain", middlewaresMap)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
### YAML Composition
|
||||
|
||||
```yaml
|
||||
# config/middlewares/my-chain.yml
|
||||
- use: realip
|
||||
header: X-Real-IP
|
||||
from:
|
||||
- 10.0.0.0/8
|
||||
- 172.16.0.0/12
|
||||
bypass:
|
||||
- path glob("/public/*")
|
||||
|
||||
- use: ratelimit
|
||||
average: 100
|
||||
burst: 200
|
||||
|
||||
- use: oidc
|
||||
allowed_users:
|
||||
- user@example.com
|
||||
```
|
||||
|
||||
```go
|
||||
// Load from file
|
||||
eb := &gperr.Builder{}
|
||||
middlewares := middleware.BuildMiddlewaresFromComposeFile(
|
||||
"config/middlewares/my-chain.yml",
|
||||
eb,
|
||||
)
|
||||
```
|
||||
|
||||
### Applying Middleware to Reverse Proxy
|
||||
|
||||
```go
|
||||
import "github.com/yusing/goutils/http/reverseproxy"
|
||||
|
||||
rp := &reverseproxy.ReverseProxy{
|
||||
Target: backendURL,
|
||||
}
|
||||
|
||||
err := middleware.PatchReverseProxy(rp, middlewaresMap)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
### Bypass Rules
|
||||
|
||||
```go
|
||||
bypassRules := middleware.Bypass{
|
||||
{
|
||||
Type: rules.RuleOnTypePathPrefix,
|
||||
Value: "/public",
|
||||
},
|
||||
{
|
||||
Type: rules.RuleOnTypePath,
|
||||
Value: "/health",
|
||||
},
|
||||
}
|
||||
|
||||
mid, _ := middleware.RateLimiter.New(middleware.OptionsRaw{
|
||||
"bypass": bypassRules,
|
||||
"average": 10,
|
||||
"burst": 20,
|
||||
})
|
||||
```
|
||||
|
||||
## Priority
|
||||
|
||||
Middleware are executed in priority order (lower number = higher priority):
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[Priority 0] --> B[Priority 5]
|
||||
B --> C[Priority 10]
|
||||
C --> D[Priority 20]
|
||||
|
||||
style A fill:#14532d,stroke:#fff,color:#fff
|
||||
style B fill:#14532d,stroke:#fff,color:#fff
|
||||
style C fill:#44403c,stroke:#fff,color:#fff
|
||||
style D fill:#44403c,stroke:#fff,color:#fff
|
||||
```
|
||||
|
||||
## Request Processing
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Request] --> B{Has Bypass Rules?}
|
||||
B -->|Yes| C{Match Bypass?}
|
||||
B -->|No| D[Execute before#40;#41;]
|
||||
|
||||
C -->|Match| E[Skip Middleware<br/>Proceed to Next]
|
||||
C -->|No Match| D
|
||||
|
||||
D --> F{before#40;#41; Returns?}
|
||||
F -->|true| G[Continue to Next]
|
||||
F -->|false| H[Stop Pipeline]
|
||||
|
||||
G --> I[Backend Handler]
|
||||
I --> J[Response]
|
||||
J --> K{Has Response Modifier?}
|
||||
K -->|Yes| L[Execute modifyResponse]
|
||||
K -->|No| M[Return Response]
|
||||
L --> M
|
||||
```
|
||||
|
||||
## Integration Points
|
||||
|
||||
- **Error Pages**: Uses `errorpage` package for custom error responses
|
||||
- **Authentication**: Integrates with `internal/auth` for OIDC
|
||||
- **Rate Limiting**: Uses `golang.org/x/time/rate`
|
||||
- **IP Processing**: Uses `internal/net/types` for CIDR handling
|
||||
|
||||
## Error Handling
|
||||
|
||||
Errors during middleware construction are collected and reported:
|
||||
|
||||
```go
|
||||
var errs gperr.Builder
|
||||
for name, opts := range middlewaresMap {
|
||||
m, err := middleware.Get(name)
|
||||
if err != nil {
|
||||
errs.Add(err)
|
||||
continue
|
||||
}
|
||||
mid, err := m.New(opts)
|
||||
if err != nil {
|
||||
errs.AddSubjectf(err, "middlewares.%s", name)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if errs.HasError() {
|
||||
log.Error().Err(errs.Error()).Msg("middleware compilation failed")
|
||||
}
|
||||
```
|
||||
264
internal/net/gphttp/middleware/captcha/README.md
Normal file
264
internal/net/gphttp/middleware/captcha/README.md
Normal file
@@ -0,0 +1,264 @@
|
||||
# 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 = gperr.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
|
||||
301
internal/net/gphttp/middleware/errorpage/README.md
Normal file
301
internal/net/gphttp/middleware/errorpage/README.md
Normal file
@@ -0,0 +1,301 @@
|
||||
# Error Page Middleware
|
||||
|
||||
Custom error page serving middleware that replaces default HTTP error responses with styled custom pages.
|
||||
|
||||
## Overview
|
||||
|
||||
This package provides two components:
|
||||
|
||||
1. **errorpage package**: Manages error page file loading, caching, and hot-reloading from disk
|
||||
1. **CustomErrorPage middleware**: Intercepts error responses and replaces them with custom error pages
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[HTTP Error Response] --> B{CustomErrorPage Middleware}
|
||||
B --> C{Status Code & Content Type}
|
||||
C -->|HTML/Plain| D[Look Up Error Page]
|
||||
C -->|Other| E[Pass Through]
|
||||
|
||||
D --> F{Page Found?}
|
||||
F -->|Yes| G[Replace Body<br/>Set Content-Type]
|
||||
F -->|No| H[Log Error<br/>Pass Through]
|
||||
|
||||
G --> I[Custom Error Page Response]
|
||||
|
||||
subgraph Error Page Management
|
||||
J[Error Pages Directory]
|
||||
K[File Watcher]
|
||||
L[Content Cache]
|
||||
M[HTTP Handler]
|
||||
end
|
||||
|
||||
J -->|Read| L
|
||||
J -->|Watch Changes| K
|
||||
K -->|Notify| L
|
||||
L --> M
|
||||
```
|
||||
|
||||
## Error Page Lookup Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Error Status: 503] --> B{Look for 503.html?}
|
||||
B -->|Found| C[Return 503.html]
|
||||
B -->|Not Found| D{Look for 404.html?}
|
||||
D -->|Found| E[Return 404.html]
|
||||
D -->|Not Found| F[Return Default Error]
|
||||
```
|
||||
|
||||
## Core Components
|
||||
|
||||
### Error Page Package
|
||||
|
||||
```go
|
||||
var (
|
||||
setupOnce sync.Once
|
||||
dirWatcher watcher.Watcher
|
||||
fileContentMap = xsync.NewMap[string, []byte]()
|
||||
)
|
||||
|
||||
func setup() {
|
||||
t := task.RootTask("error_page", false)
|
||||
dirWatcher = watcher.NewDirectoryWatcher(t, errPagesBasePath)
|
||||
loadContent()
|
||||
go watchDir()
|
||||
}
|
||||
|
||||
// GetStaticFile retrieves an error page file by filename
|
||||
func GetStaticFile(filename string) ([]byte, bool)
|
||||
|
||||
// GetErrorPageByStatus retrieves the error page for a given status code
|
||||
func GetErrorPageByStatus(statusCode int) (content []byte, ok bool)
|
||||
```
|
||||
|
||||
### File Watcher
|
||||
|
||||
The package watches the error pages directory for changes:
|
||||
|
||||
```go
|
||||
func watchDir() {
|
||||
eventCh, errCh := dirWatcher.Events(task.RootContext())
|
||||
for {
|
||||
select {
|
||||
case event := <-eventCh:
|
||||
filename := event.ActorName
|
||||
switch event.Action {
|
||||
case events.ActionFileWritten:
|
||||
fileContentMap.Delete(filename)
|
||||
loadContent()
|
||||
case events.ActionFileDeleted:
|
||||
fileContentMap.Delete(filename)
|
||||
case events.ActionFileRenamed:
|
||||
fileContentMap.Delete(filename)
|
||||
loadContent()
|
||||
}
|
||||
case err := <-errCh:
|
||||
gperr.LogError("error watching error page directory", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Error Page Middleware
|
||||
|
||||
```go
|
||||
type customErrorPage struct{}
|
||||
|
||||
var CustomErrorPage = NewMiddleware[customErrorPage]()
|
||||
|
||||
const StaticFilePathPrefix = "/$gperrorpage/"
|
||||
```
|
||||
|
||||
### Request Modifier
|
||||
|
||||
```go
|
||||
func (customErrorPage) before(w http.ResponseWriter, r *http.Request) bool {
|
||||
return !ServeStaticErrorPageFile(w, r)
|
||||
}
|
||||
```
|
||||
|
||||
### Response Modifier
|
||||
|
||||
```go
|
||||
func (customErrorPage) modifyResponse(resp *http.Response) error {
|
||||
// Only handles:
|
||||
// - Non-success status codes (4xx, 5xx)
|
||||
// - HTML or Plain Text content types
|
||||
contentType := httputils.GetContentType(resp.Header)
|
||||
if !httputils.IsSuccess(resp.StatusCode) && (contentType.IsHTML() || contentType.IsPlainText()) {
|
||||
errorPage, ok := errorpage.GetErrorPageByStatus(resp.StatusCode)
|
||||
if ok {
|
||||
// Replace response body with error page
|
||||
resp.Body = io.NopCloser(bytes.NewReader(errorPage))
|
||||
resp.ContentLength = int64(len(errorPage))
|
||||
resp.Header.Set(httpheaders.HeaderContentLength, strconv.Itoa(len(errorPage)))
|
||||
resp.Header.Set(httpheaders.HeaderContentType, "text/html; charset=utf-8")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## Static File Serving
|
||||
|
||||
The middleware also serves static error page assets:
|
||||
|
||||
```go
|
||||
func ServeStaticErrorPageFile(w http.ResponseWriter, r *http.Request) bool {
|
||||
if strings.HasPrefix(path, StaticFilePathPrefix) {
|
||||
filename := path[len(StaticFilePathPrefix):]
|
||||
file, ok := errorpage.GetStaticFile(filename)
|
||||
if ok {
|
||||
// Set content type based on extension
|
||||
switch ext := filepath.Ext(filename); ext {
|
||||
case ".html":
|
||||
w.Header().Set(httpheaders.HeaderContentType, "text/html; charset=utf-8")
|
||||
case ".js":
|
||||
w.Header().Set(httpheaders.HeaderContentType, "application/javascript; charset=utf-8")
|
||||
case ".css":
|
||||
w.Header().Set(httpheaders.HeaderContentType, "text/css; charset=utf-8")
|
||||
}
|
||||
w.Write(file)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Error Pages Directory
|
||||
|
||||
Default path: `config/error_pages/`
|
||||
|
||||
### Supported Files
|
||||
|
||||
| File Pattern | Description |
|
||||
| ------------------- | -------------------------------------- |
|
||||
| `{statusCode}.html` | Specific error page (e.g., `503.html`) |
|
||||
| `404.html` | Fallback for missing specific pages |
|
||||
| `*.css` | Stylesheets |
|
||||
| `*.js` | JavaScript files |
|
||||
| `*.{png,jpg,svg}` | Images and assets |
|
||||
|
||||
### Example Structure
|
||||
|
||||
```
|
||||
config/error_pages/
|
||||
├── 403.html
|
||||
├── 404.html
|
||||
├── 500.html
|
||||
├── 502.html
|
||||
├── 503.html
|
||||
├── style.css
|
||||
└── logo.png
|
||||
```
|
||||
|
||||
### Middleware Configuration
|
||||
|
||||
```yaml
|
||||
# In route middleware configuration
|
||||
- use: errorpage
|
||||
# Optional: bypass rules
|
||||
bypass:
|
||||
- type: PathPrefix
|
||||
value: /api
|
||||
```
|
||||
|
||||
## Response Processing
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Backend Response] --> B{Status Code >= 400?}
|
||||
B -->|No| C[Pass Through]
|
||||
B -->|Yes| D{Content Type HTML/Plain?}
|
||||
D -->|No| C
|
||||
D -->|Yes| E{Look Up Error Page}
|
||||
E -->|Found| F[Replace Body]
|
||||
E -->|Not Found| G[Log Error]
|
||||
G --> C
|
||||
F --> H[Set Content-Type: text/html]
|
||||
H --> I[Return Custom Error Page]
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Creating Custom Error Pages
|
||||
|
||||
**503.html**:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Service Unavailable</title>
|
||||
<link rel="stylesheet" href="/$gperrorpage/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="error-container">
|
||||
<h1>503 - Service Unavailable</h1>
|
||||
<p>The service is temporarily unavailable. Please try again later.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### Using in Middleware Chain
|
||||
|
||||
```yaml
|
||||
# config/middlewares/error-pages.yml
|
||||
- use: errorpage
|
||||
bypass:
|
||||
- type: PathPrefix
|
||||
value: /api/health
|
||||
```
|
||||
|
||||
### Programmatic Usage
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
|
||||
"github.com/yusing/godoxy/internal/net/gphttp/middleware/errorpage"
|
||||
)
|
||||
|
||||
// Check if error page exists
|
||||
content, ok := errorpage.GetErrorPageByStatus(503)
|
||||
if ok {
|
||||
// Use error page content
|
||||
}
|
||||
|
||||
// Serve static asset
|
||||
ServeStaticErrorPageFile(w, r)
|
||||
```
|
||||
|
||||
## Integration with GoDoxy
|
||||
|
||||
The error page middleware integrates with:
|
||||
|
||||
- **File Watching**: Uses `internal/watcher` for hot-reloading
|
||||
- **Task Management**: Uses `internal/task` for lifetime management
|
||||
- **Content Caching**: Uses `xsync.Map` for thread-safe caching
|
||||
- **HTTP Headers**: Uses `goutils/http/httpheaders` for content type handling
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- Error page content is cached in memory after first load
|
||||
- File watcher notifies on changes for cache invalidation
|
||||
- Static files are served directly from cache
|
||||
- Concurrent access protected by `xsync.Map`
|
||||
|
||||
## Error Handling
|
||||
|
||||
```go
|
||||
// Logging on error page not found
|
||||
log.Error().Msgf("unable to load error page for status %d", resp.StatusCode)
|
||||
|
||||
// Logging on static file not found
|
||||
log.Error().Msg("unable to load resource " + filename)
|
||||
```
|
||||
Reference in New Issue
Block a user