mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-26 18:28:30 +02:00
docs: add per package README for implementation details (AI generated with human review)
This commit is contained in:
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