This commit is contained in:
yusing
2026-02-16 08:59:01 +08:00
parent 15b9635ee1
commit e4e6f6b3e8
242 changed files with 3953 additions and 3502 deletions

View File

@@ -1,10 +1,10 @@
# Entrypoint
The entrypoint package provides the main HTTP entry point for GoDoxy, handling domain-based routing, middleware application, short link matching, and access logging.
The entrypoint package provides the main HTTP entry point for GoDoxy, handling domain-based routing, middleware application, short link matching, access logging, and HTTP server lifecycle management.
## Overview
The entrypoint package implements the primary HTTP handler that receives all incoming requests, determines the target route based on hostname, applies middleware, and forwards requests to the appropriate route handler.
The entrypoint package implements the primary HTTP handler that receives all incoming requests, manages the lifecycle of HTTP servers, determines the target route based on hostname, applies middleware, and forwards requests to the appropriate route handler.
### Key Features
@@ -14,103 +14,350 @@ The entrypoint package implements the primary HTTP handler that receives all inc
- Access logging for all requests
- Configurable not-found handling
- Per-domain route resolution
- HTTP server management (HTTP/HTTPS)
- Route pool abstractions via [`PoolLike`](internal/entrypoint/types/entrypoint.go:27) and [`RWPoolLike`](internal/entrypoint/types/entrypoint.go:33) interfaces
## Architecture
### Primary Consumers
```mermaid
graph TD
A[HTTP Request] --> B[Entrypoint Handler]
B --> C{Access Logger?}
C -->|Yes| D[Wrap Response Recorder]
C -->|No| E[Skip Logging]
- **HTTP servers**: Per-listen-addr servers dispatch requests to routes
- **Route providers**: Register routes via [`StartAddRoute`](internal/entrypoint/routes.go:48)
- **Configuration layer**: Validates and applies middleware/access-logging config
D --> F[Find Route by Host]
E --> F
### Non-goals
F --> G{Route Found?}
G -->|Yes| H{Middleware?}
G -->|No| I{Short Link?}
I -->|Yes| J[Short Link Handler]
I -->|No| K{Not Found Handler?}
K -->|Yes| L[Not Found Handler]
K -->|No| M[Serve 404]
- Does not implement route discovery (delegates to providers)
- Does not handle TLS certificate management (delegates to autocert)
- Does not implement health checks (delegates to `internal/health/monitor`)
- Does not manage TCP/UDP listeners directly (only HTTP/HTTPS via `goutils/server`)
H -->|Yes| N[Apply Middleware]
H -->|No| O[Direct Route]
N --> O
### Stability
O --> P[Route ServeHTTP]
P --> Q[Response]
L --> R[404 Response]
J --> Q
M --> R
```
## Core Components
### Entrypoint Structure
```go
type Entrypoint struct {
middleware *middleware.Middleware
notFoundHandler http.Handler
accessLogger accesslog.AccessLogger
findRouteFunc func(host string) types.HTTPRoute
shortLinkTree *ShortLinkMatcher
}
```
### Active Config
```go
var ActiveConfig atomic.Pointer[entrypoint.Config]
```
Internal package with stable core interfaces. The [`Entrypoint`](internal/entrypoint/types/entrypoint.go:7) interface is the public contract.
## Public API
### Creation
### Entrypoint Interface
```go
// NewEntrypoint creates a new entrypoint instance.
func NewEntrypoint() Entrypoint
type Entrypoint interface {
// Server capabilities
SupportProxyProtocol() bool
DisablePoolsLog(v bool)
// Route registry access
GetRoute(alias string) (types.Route, bool)
StartAddRoute(r types.Route) error
IterRoutes(yield func(r types.Route) bool)
NumRoutes() int
RoutesByProvider() map[string][]types.Route
// Route pool accessors
HTTPRoutes() PoolLike[types.HTTPRoute]
StreamRoutes() PoolLike[types.StreamRoute]
ExcludedRoutes() RWPoolLike[types.Route]
// Health info queries
GetHealthInfo() map[string]types.HealthInfo
GetHealthInfoWithoutDetail() map[string]types.HealthInfoWithoutDetail
GetHealthInfoSimple() map[string]types.HealthStatus
// Configuration
SetFindRouteDomains(domains []string)
SetMiddlewares(mws []map[string]any) error
SetNotFoundRules(rules rules.Rules)
SetAccessLogger(parent task.Parent, cfg *accesslog.RequestLoggerConfig) error
// Context integration
ShortLinkMatcher() *ShortLinkMatcher
}
```
### Pool Interfaces
```go
type PoolLike[Route types.Route] interface {
Get(alias string) (Route, bool)
Iter(yield func(alias string, r Route) bool)
Size() int
}
type RWPoolLike[Route types.Route] interface {
PoolLike[Route]
Add(r Route)
Del(r Route)
}
```
### Configuration
```go
// SetFindRouteDomains configures domain-based route lookup.
func (ep *Entrypoint) SetFindRouteDomains(domains []string)
// SetMiddlewares loads and configures middleware chain.
func (ep *Entrypoint) SetMiddlewares(mws []map[string]any) error
// SetNotFoundRules configures the not-found handler.
func (ep *Entrypoint) SetNotFoundRules(rules rules.Rules)
// SetAccessLogger initializes access logging.
func (ep *Entrypoint) SetAccessLogger(parent task.Parent, cfg *accesslog.RequestLoggerConfig) error
// ShortLinkMatcher returns the short link matcher.
func (ep *Entrypoint) ShortLinkMatcher() *ShortLinkMatcher
type Config struct {
SupportProxyProtocol bool `json:"support_proxy_protocol"`
Rules struct {
NotFound rules.Rules `json:"not_found"`
} `json:"rules"`
Middlewares []map[string]any `json:"middlewares"`
AccessLog *accesslog.RequestLoggerConfig `json:"access_log" validate:"omitempty"`
}
```
### Request Handling
### Context Functions
```go
// ServeHTTP is the main HTTP handler.
func (ep *Entrypoint) ServeHTTP(w http.ResponseWriter, r *http.Request)
// FindRoute looks up a route by hostname.
func (ep *Entrypoint) FindRoute(s string) types.HTTPRoute
func SetCtx(ctx interface{ SetValue(any, any) }, ep Entrypoint)
func FromCtx(ctx context.Context) Entrypoint
```
## Usage
## Architecture
### Core Components
```mermaid
classDiagram
class Entrypoint {
+task *task.new_task
+cfg *Config
+middleware *middleware.Middleware
+notFoundHandler http.Handler
+accessLogger AccessLogger
+findRouteFunc findRouteFunc
+shortLinkMatcher *ShortLinkMatcher
+streamRoutes *pool.Pool[types.StreamRoute]
+excludedRoutes *pool.Pool[types.Route]
+servers *xsync.Map[string, *httpServer]
+SupportProxyProtocol() bool
+StartAddRoute(r) error
+IterRoutes(yield)
+HTTPRoutes() PoolLike
}
class httpServer {
+routes *pool.Pool[types.HTTPRoute]
+ServeHTTP(w, r)
+AddRoute(route)
+DelRoute(route)
+FindRoute(s) types.HTTPRoute
}
class PoolLike {
<<interface>>
+Get(alias) (Route, bool)
+Iter(yield) bool
+Size() int
}
class RWPoolLike {
<<interface>>
+PoolLike
+Add(r Route)
+Del(r Route)
}
class ShortLinkMatcher {
+fqdnRoutes *xsync.Map[string, string]
+subdomainRoutes *xsync.Map[string, struct{}]
+ServeHTTP(w, r)
+AddRoute(alias)
+DelRoute(alias)
+SetDefaultDomainSuffix(suffix)
}
Entrypoint --> httpServer : manages
Entrypoint --> ShortLinkMatcher : owns
Entrypoint --> PoolLike : HTTPRoutes()
Entrypoint --> RWPoolLike : ExcludedRoutes()
httpServer --> PoolLike : routes pool
```
### Request Processing Pipeline
```mermaid
flowchart TD
A[HTTP Request] --> B[Find Route by Host]
B --> C{Route Found?}
C -->|Yes| D{Middleware?}
C -->|No| E{Short Link?}
E -->|Yes| F[Short Link Handler]
E -->|No| G{Not Found Handler?}
G -->|Yes| H[Not Found Handler]
G -->|No| I[Serve 404]
D -->|Yes| J[Apply Middleware Chain]
D -->|No| K[Direct Route Handler]
J --> K
K --> L[Route ServeHTTP]
L --> M[Response]
F --> M
H --> N[404 Response]
I --> N
```
### Server Lifecycle
```mermaid
stateDiagram-v2
[*] --> Empty: NewEntrypoint()
Empty --> Listening: StartAddRoute()
Listening --> Listening: StartAddRoute()
Listening --> Listening: delHTTPRoute()
Listening --> [*]: Cancel()
Listening --> AddingServer: addHTTPRoute()
AddingServer --> Listening: Server starts
note right of Listening
servers map: addr -> httpServer
For HTTPS, routes are added to ProxyHTTPSAddr
Default routes added to both HTTP and HTTPS
end note
```
## Data Flow
```mermaid
sequenceDiagram
participant Client
participant httpServer
participant Entrypoint
participant Middleware
participant Route
Client->>httpServer: GET /path
httpServer->>Entrypoint: FindRoute(host)
alt Route Found
Entrypoint-->>httpServer: HTTPRoute
httpServer->>Middleware: ServeHTTP(routeHandler)
alt Has Middleware
Middleware->>Middleware: Process Chain
end
Middleware->>Route: Forward Request
Route-->>Middleware: Response
Middleware-->>httpServer: Response
else Short Link (go.example.com/alias)
httpServer->>ShortLinkMatcher: Match short code
ShortLinkMatcher-->>httpServer: Redirect
else Not Found
httpServer->>NotFoundHandler: Serve 404
NotFoundHandler-->>httpServer: 404 Page
end
httpServer-->>Client: Response
```
## Route Registry
Routes are managed per-entrypoint:
```go
// Adding a route (main entry point for providers)
if err := ep.StartAddRoute(route); err != nil {
return err
}
// Iterating all routes including excluded
ep.IterRoutes(func(r types.Route) bool {
log.Info().Str("alias", r.Name()).Msg("route")
return true // continue iteration
})
// Querying by alias
route, ok := ep.GetRoute("myapp")
// Grouping by provider
byProvider := ep.RoutesByProvider()
```
## Configuration Surface
### Config Source
Environment variables and YAML config file:
```yaml
entrypoint:
support_proxy_protocol: true
middlewares:
- rate_limit:
requests_per_second: 100
rules:
not_found:
# not-found rules configuration
access_log:
path: /var/log/godoxy/access.log
```
### Environment Variables
| Variable | Description |
| ------------------------------ | ----------------------------- |
| `PROXY_SUPPORT_PROXY_PROTOCOL` | Enable PROXY protocol support |
## Dependency and Integration Map
| Dependency | Purpose |
| ---------------------------------- | --------------------------- |
| `internal/route` | Route types and handlers |
| `internal/route/rules` | Not-found rules processing |
| `internal/logging/accesslog` | Request logging |
| `internal/net/gphttp/middleware` | Middleware chain |
| `internal/types` | Route and health types |
| `github.com/puzpuzpuz/xsync/v4` | Concurrent server map |
| `github.com/yusing/goutils/pool` | Route pool implementations |
| `github.com/yusing/goutils/task` | Lifecycle management |
| `github.com/yusing/goutils/server` | HTTP/HTTPS server lifecycle |
## Observability
### Logs
| Level | Context | Description |
| ------- | --------------------- | ----------------------- |
| `DEBUG` | `route`, `listen_url` | Route addition/removal |
| `DEBUG` | `addr`, `proto` | Server lifecycle |
| `ERROR` | `route`, `listen_url` | Server startup failures |
### Metrics
Route metrics exposed via [`GetHealthInfo`](internal/entrypoint/query.go:10) methods:
```go
// Health info for all routes
healthMap := ep.GetHealthInfo()
// {
// "myapp": {Status: "healthy", Uptime: 3600, Latency: 5ms},
// "excluded-route": {Status: "unknown", Detail: "n/a"},
// }
```
## Security Considerations
- Route lookup is read-only from route pools
- Middleware chain is applied per-request
- Proxy protocol support must be explicitly enabled
- Access logger captures request metadata before processing
- Short link matching is limited to configured domains
## Failure Modes and Recovery
| Failure | Behavior | Recovery |
| --------------------- | ------------------------------- | ---------------------------- |
| Server bind fails | Error returned, route not added | Fix port/address conflict |
| Route start fails | Route excluded, error logged | Fix route configuration |
| Middleware load fails | SetMiddlewares returns error | Fix middleware configuration |
| Context cancelled | All servers stopped gracefully | Restart entrypoint |
## Usage Examples
### Basic Setup
```go
ep := entrypoint.NewEntrypoint()
ep := entrypoint.NewEntrypoint(parent, &entrypoint.Config{
SupportProxyProtocol: false,
})
// Configure domain matching
ep.SetFindRouteDomains([]string{".example.com", "example.com"})
@@ -120,7 +367,7 @@ err := ep.SetMiddlewares([]map[string]any{
{"rate_limit": map[string]any{"requests_per_second": 100}},
})
if err != nil {
log.Fatal(err)
return err
}
// Configure access logging
@@ -128,181 +375,58 @@ err = ep.SetAccessLogger(parent, &accesslog.RequestLoggerConfig{
Path: "/var/log/godoxy/access.log",
})
if err != nil {
log.Fatal(err)
return err
}
// Start server
http.ListenAndServe(":80", &ep)
```
### Route Lookup Logic
The entrypoint uses multiple strategies to find routes:
1. **Subdomain Matching**: For `sub.domain.com`, looks for `sub`
1. **Exact Match**: Looks for the full hostname
1. **Port Stripping**: Strips port from host if present
### Route Querying
```go
func findRouteAnyDomain(host string) types.HTTPRoute {
// Try subdomain (everything before first dot)
idx := strings.IndexByte(host, '.')
if idx != -1 {
target := host[:idx]
if r, ok := routes.HTTP.Get(target); ok {
return r
}
}
// Iterate all routes including excluded
ep.IterRoutes(func(r types.Route) bool {
log.Info().
Str("alias", r.Name()).
Str("provider", r.ProviderName()).
Bool("excluded", r.ShouldExclude()).
Msg("route")
return true // continue iteration
})
// Try exact match
if r, ok := routes.HTTP.Get(host); ok {
return r
}
// Try stripping port
if before, _, ok := strings.Cut(host, ":"); ok {
if r, ok := routes.HTTP.Get(before); ok {
return r
}
}
return nil
// Get health info for all routes
healthMap := ep.GetHealthInfoSimple()
for alias, status := range healthMap {
log.Info().Str("alias", alias).Str("status", string(status)).Msg("health")
}
```
### Short Links
### Route Addition
Short links use a special `.short` domain:
Routes are typically added by providers via `StartAddRoute`:
```go
// Request to: https://abc.short.example.com
// Looks for route with alias "abc"
if strings.EqualFold(host, common.ShortLinkPrefix) {
// Handle short link
ep.shortLinkTree.ServeHTTP(w, r)
// StartAddRoute handles route registration and server creation
if err := ep.StartAddRoute(route); err != nil {
return err
}
```
## Data Flow
### Context Integration
```mermaid
sequenceDiagram
participant Client
participant Entrypoint
participant Middleware
participant Route
participant Logger
Client->>Entrypoint: GET /path
Entrypoint->>Entrypoint: FindRoute(host)
alt Route Found
Entrypoint->>Logger: Get ResponseRecorder
Logger-->>Entrypoint: Recorder
Entrypoint->>Middleware: ServeHTTP(routeHandler)
alt Has Middleware
Middleware->>Middleware: Process Chain
end
Middleware->>Route: Forward Request
Route-->>Middleware: Response
Middleware-->>Entrypoint: Response
else Short Link
Entrypoint->>ShortLinkTree: Match short code
ShortLinkTree-->>Entrypoint: Redirect
else Not Found
Entrypoint->>NotFoundHandler: Serve 404
NotFoundHandler-->>Entrypoint: 404 Page
end
Entrypoint->>Logger: Log Request
Logger-->>Entrypoint: Complete
Entrypoint-->>Client: Response
```
## Not-Found Handling
When no route is found, the entrypoint:
1. Attempts to serve a static error page file
1. Logs the 404 request
1. Falls back to the configured error page
1. Returns 404 status code
Routes can access the entrypoint from request context:
```go
func (ep *Entrypoint) serveNotFound(w http.ResponseWriter, r *http.Request) {
if served := middleware.ServeStaticErrorPageFile(w, r); !served {
log.Error().
Str("method", r.Method).
Str("url", r.URL.String()).
Str("remote", r.RemoteAddr).
Msgf("not found: %s", r.Host)
// Set entrypoint in context (typically during initialization)
entrypoint.SetCtx(task, ep)
errorPage, ok := errorpage.GetErrorPageByStatus(http.StatusNotFound)
if ok {
w.WriteHeader(http.StatusNotFound)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write(errorPage)
} else {
http.NotFound(w, r)
}
}
// Get entrypoint from context
if ep := entrypoint.FromCtx(r.Context()); ep != nil {
route, ok := ep.GetRoute("alias")
}
```
## Configuration Structure
## Testing Notes
```go
type Config struct {
Middlewares []map[string]any `json:"middlewares"`
Rules rules.Rules `json:"rules"`
AccessLog *accesslog.RequestLoggerConfig `json:"access_log"`
}
```
## Middleware Integration
The entrypoint supports middleware chains configured via YAML:
```yaml
entrypoint:
middlewares:
- use: rate_limit
average: 100
burst: 200
bypass:
- remote 192.168.1.0/24
- use: redirect_http
```
## Access Logging
Access logging wraps the response recorder to capture:
- Request method and URL
- Response status code
- Response size
- Request duration
- Client IP address
```go
func (ep *Entrypoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if ep.accessLogger != nil {
rec := accesslog.GetResponseRecorder(w)
w = rec
defer func() {
ep.accessLogger.Log(r, rec.Response())
accesslog.PutResponseRecorder(rec)
}()
}
// ... handle request
}
```
## Integration Points
The entrypoint integrates with:
- **Route Registry**: HTTP route lookup
- **Middleware**: Request processing chain
- **AccessLog**: Request logging
- **ErrorPage**: 404 error pages
- **ShortLink**: Short link handling
- Benchmark tests in [`entrypoint_benchmark_test.go`](internal/entrypoint/entrypoint_benchmark_test.go)
- Integration tests in [`entrypoint_test.go`](internal/entrypoint/entrypoint_test.go)
- Mock route pools for unit testing
- Short link tests in [`shortlink_test.go`](internal/entrypoint/shortlink_test.go)