mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-25 10:18:59 +02:00
chore(docs): update package docs
This commit is contained in:
@@ -54,7 +54,7 @@ type State interface {
|
|||||||
Task() *task.Task
|
Task() *task.Task
|
||||||
Context() context.Context
|
Context() context.Context
|
||||||
Value() *Config
|
Value() *Config
|
||||||
EntrypointHandler() http.Handler
|
Entrypoint() entrypoint.Entrypoint
|
||||||
ShortLinkMatcher() config.ShortLinkMatcher
|
ShortLinkMatcher() config.ShortLinkMatcher
|
||||||
AutoCertProvider() server.CertProvider
|
AutoCertProvider() server.CertProvider
|
||||||
LoadOrStoreProvider(key string, value types.RouteProvider) (actual types.RouteProvider, loaded bool)
|
LoadOrStoreProvider(key string, value types.RouteProvider) (actual types.RouteProvider, loaded bool)
|
||||||
@@ -62,6 +62,12 @@ type State interface {
|
|||||||
IterProviders() iter.Seq2[string, types.RouteProvider]
|
IterProviders() iter.Seq2[string, types.RouteProvider]
|
||||||
StartProviders() error
|
StartProviders() error
|
||||||
NumProviders() int
|
NumProviders() int
|
||||||
|
|
||||||
|
// Lifecycle management
|
||||||
|
StartAPIServers()
|
||||||
|
StartMetrics()
|
||||||
|
|
||||||
|
FlushTmpLog()
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -214,12 +220,15 @@ Configuration supports hot-reloading via editing `config/config.yml`.
|
|||||||
|
|
||||||
- `internal/acl` - Access control configuration
|
- `internal/acl` - Access control configuration
|
||||||
- `internal/autocert` - SSL certificate management
|
- `internal/autocert` - SSL certificate management
|
||||||
- `internal/entrypoint` - HTTP entrypoint setup
|
- `internal/entrypoint` - HTTP entrypoint setup (now via interface)
|
||||||
- `internal/route/provider` - Route providers (Docker, file, agent)
|
- `internal/route/provider` - Route providers (Docker, file, agent)
|
||||||
- `internal/maxmind` - GeoIP configuration
|
- `internal/maxmind` - GeoIP configuration
|
||||||
- `internal/notif` - Notification providers
|
- `internal/notif` - Notification providers
|
||||||
- `internal/proxmox` - LXC container management
|
- `internal/proxmox` - LXC container management
|
||||||
- `internal/homepage/types` - Dashboard configuration
|
- `internal/homepage/types` - Dashboard configuration
|
||||||
|
- `internal/api` - REST API servers
|
||||||
|
- `internal/metrics/systeminfo` - System metrics polling
|
||||||
|
- `internal/metrics/uptime` - Uptime tracking
|
||||||
- `github.com/yusing/goutils/task` - Object lifecycle management
|
- `github.com/yusing/goutils/task` - Object lifecycle management
|
||||||
|
|
||||||
### External dependencies
|
### External dependencies
|
||||||
@@ -312,5 +321,8 @@ for name, provider := range config.GetState().IterProviders() {
|
|||||||
|
|
||||||
```go
|
```go
|
||||||
state := config.GetState()
|
state := config.GetState()
|
||||||
http.Handle("/", state.EntrypointHandler())
|
// Get entrypoint interface for route management
|
||||||
|
ep := state.Entrypoint()
|
||||||
|
// Add routes directly to entrypoint
|
||||||
|
ep.AddRoute(route)
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
# Entrypoint
|
# 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/TCP/UDP server lifecycle management.
|
||||||
|
|
||||||
## Overview
|
## 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/TCP/UDP servers, determines the target route based on hostname, applies middleware, and forwards requests to the appropriate route handler.
|
||||||
|
|
||||||
### Key Features
|
### Key Features
|
||||||
|
|
||||||
@@ -14,103 +14,310 @@ The entrypoint package implements the primary HTTP handler that receives all inc
|
|||||||
- Access logging for all requests
|
- Access logging for all requests
|
||||||
- Configurable not-found handling
|
- Configurable not-found handling
|
||||||
- Per-domain route resolution
|
- Per-domain route resolution
|
||||||
|
- Multi-protocol server management (HTTP/HTTPS/TCP/UDP)
|
||||||
|
- Route pool abstractions via [`PoolLike`](internal/entrypoint/types/entrypoint.go:27) and [`RWPoolLike`](internal/entrypoint/types/entrypoint.go:33) interfaces
|
||||||
|
|
||||||
## Architecture
|
### Primary Consumers
|
||||||
|
|
||||||
```mermaid
|
- **HTTP servers**: Per-listen-addr servers dispatch requests to routes
|
||||||
graph TD
|
- **Route providers**: Register routes via [`AddRoute`](internal/entrypoint/routes.go:48)
|
||||||
A[HTTP Request] --> B[Entrypoint Handler]
|
- **Configuration layer**: Validates and applies middleware/access-logging config
|
||||||
B --> C{Access Logger?}
|
|
||||||
C -->|Yes| D[Wrap Response Recorder]
|
|
||||||
C -->|No| E[Skip Logging]
|
|
||||||
|
|
||||||
D --> F[Find Route by Host]
|
### Non-goals
|
||||||
E --> F
|
|
||||||
|
|
||||||
F --> G{Route Found?}
|
- Does not implement route discovery (delegates to providers)
|
||||||
G -->|Yes| H{Middleware?}
|
- Does not handle TLS certificate management (delegates to autocert)
|
||||||
G -->|No| I{Short Link?}
|
- Does not implement health checks (delegates to `internal/health/monitor`)
|
||||||
I -->|Yes| J[Short Link Handler]
|
|
||||||
I -->|No| K{Not Found Handler?}
|
|
||||||
K -->|Yes| L[Not Found Handler]
|
|
||||||
K -->|No| M[Serve 404]
|
|
||||||
|
|
||||||
H -->|Yes| N[Apply Middleware]
|
### Stability
|
||||||
H -->|No| O[Direct Route]
|
|
||||||
N --> O
|
|
||||||
|
|
||||||
O --> P[Route ServeHTTP]
|
Internal package with stable core interfaces. The [`Entrypoint`](internal/entrypoint/types/entrypoint.go:7) interface is the public contract.
|
||||||
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]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Public API
|
## Public API
|
||||||
|
|
||||||
### Creation
|
### Entrypoint Interface
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// NewEntrypoint creates a new entrypoint instance.
|
type Entrypoint interface {
|
||||||
func NewEntrypoint() Entrypoint
|
// Server capabilities
|
||||||
|
SupportProxyProtocol() bool
|
||||||
|
DisablePoolsLog(v bool)
|
||||||
|
|
||||||
|
// Route registry access
|
||||||
|
GetRoute(alias string) (types.Route, bool)
|
||||||
|
AddRoute(r types.Route)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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
|
### Configuration
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// SetFindRouteDomains configures domain-based route lookup.
|
type Config struct {
|
||||||
func (ep *Entrypoint) SetFindRouteDomains(domains []string)
|
SupportProxyProtocol bool `json:"support_proxy_protocol"`
|
||||||
|
}
|
||||||
// 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Request Handling
|
## Architecture
|
||||||
|
|
||||||
|
### Core Components
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class Entrypoint {
|
||||||
|
+task *task.Task
|
||||||
|
+cfg *Config
|
||||||
|
+middleware *middleware.Middleware
|
||||||
|
+shortLinkMatcher *ShortLinkMatcher
|
||||||
|
+streamRoutes *pool.Pool[types.StreamRoute]
|
||||||
|
+excludedRoutes *pool.Pool[types.Route]
|
||||||
|
+servers *xsync.Map[string, *httpServer]
|
||||||
|
+tcpListeners *xsync.Map[string, net.Listener]
|
||||||
|
+udpListeners *xsync.Map[string, net.PacketConn]
|
||||||
|
+SupportProxyProtocol() bool
|
||||||
|
+AddRoute(r)
|
||||||
|
+IterRoutes(yield)
|
||||||
|
+HTTPRoutes() PoolLike
|
||||||
|
}
|
||||||
|
|
||||||
|
class httpServer {
|
||||||
|
+routes *routePool
|
||||||
|
+ServeHTTP(w, r)
|
||||||
|
+AddRoute(route)
|
||||||
|
+DelRoute(route)
|
||||||
|
}
|
||||||
|
|
||||||
|
class routePool {
|
||||||
|
+Get(alias) (HTTPRoute, bool)
|
||||||
|
+AddRoute(route)
|
||||||
|
+DelRoute(route)
|
||||||
|
}
|
||||||
|
|
||||||
|
class PoolLike {
|
||||||
|
<<interface>>
|
||||||
|
+Get(alias) (Route, bool)
|
||||||
|
+Iter(yield) bool
|
||||||
|
+Size() int
|
||||||
|
}
|
||||||
|
|
||||||
|
class RWPoolLike {
|
||||||
|
<<interface>>
|
||||||
|
+PoolLike
|
||||||
|
+Add(r Route)
|
||||||
|
+Del(r Route)
|
||||||
|
}
|
||||||
|
|
||||||
|
Entrypoint --> httpServer : manages
|
||||||
|
Entrypoint --> routePool : HTTPRoutes()
|
||||||
|
Entrypoint --> PoolLike : returns
|
||||||
|
Entrypoint --> RWPoolLike : ExcludedRoutes()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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: AddRoute()
|
||||||
|
Listening --> Listening: AddRoute()
|
||||||
|
Listening --> Listening: delHTTPRoute()
|
||||||
|
Listening --> [*]: Cancel()
|
||||||
|
|
||||||
|
Listening --> AddingServer: addHTTPRoute()
|
||||||
|
AddingServer --> Listening: Server starts
|
||||||
|
|
||||||
|
note right of Listening
|
||||||
|
servers map: addr -> httpServer
|
||||||
|
tcpListeners map: addr -> Listener
|
||||||
|
udpListeners map: addr -> PacketConn
|
||||||
|
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
|
||||||
|
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 now managed per-entrypoint instead of global registry:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// ServeHTTP is the main HTTP handler.
|
// Adding a route
|
||||||
func (ep *Entrypoint) ServeHTTP(w http.ResponseWriter, r *http.Request)
|
ep.AddRoute(route)
|
||||||
|
|
||||||
// FindRoute looks up a route by hostname.
|
// Iterating all routes
|
||||||
func (ep *Entrypoint) FindRoute(s string) types.HTTPRoute
|
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()
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Configuration Surface
|
||||||
|
|
||||||
|
### Config Source
|
||||||
|
|
||||||
|
Environment variables and YAML config file:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
entrypoint:
|
||||||
|
support_proxy_protocol: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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 |
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
## Failure Modes and Recovery
|
||||||
|
|
||||||
|
| Failure | Behavior | Recovery |
|
||||||
|
| --------------------- | ------------------------------ | ---------------------------- |
|
||||||
|
| Server bind fails | Error logged, route not added | Fix port/address conflict |
|
||||||
|
| Route start fails | Route excluded, error logged | Fix route configuration |
|
||||||
|
| Middleware load fails | AddRoute returns error | Fix middleware configuration |
|
||||||
|
| Context cancelled | All servers stopped gracefully | Restart entrypoint |
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
### Basic Setup
|
### Basic Setup
|
||||||
|
|
||||||
```go
|
```go
|
||||||
ep := entrypoint.NewEntrypoint()
|
ep := entrypoint.NewEntrypoint(parent, &entrypoint.Config{
|
||||||
|
SupportProxyProtocol: false,
|
||||||
|
})
|
||||||
|
|
||||||
// Configure domain matching
|
// Configure domain matching
|
||||||
ep.SetFindRouteDomains([]string{".example.com", "example.com"})
|
ep.SetFindRouteDomains([]string{".example.com", "example.com"})
|
||||||
@@ -120,7 +327,7 @@ err := ep.SetMiddlewares([]map[string]any{
|
|||||||
{"rate_limit": map[string]any{"requests_per_second": 100}},
|
{"rate_limit": map[string]any{"requests_per_second": 100}},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure access logging
|
// Configure access logging
|
||||||
@@ -128,181 +335,59 @@ err = ep.SetAccessLogger(parent, &accesslog.RequestLoggerConfig{
|
|||||||
Path: "/var/log/godoxy/access.log",
|
Path: "/var/log/godoxy/access.log",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start server
|
|
||||||
http.ListenAndServe(":80", &ep)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Route Lookup Logic
|
### Route Querying
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func findRouteAnyDomain(host string) types.HTTPRoute {
|
// Iterate all routes including excluded
|
||||||
// Try subdomain (everything before first dot)
|
for r := range ep.IterRoutes {
|
||||||
idx := strings.IndexByte(host, '.')
|
log.Info().
|
||||||
if idx != -1 {
|
Str("alias", r.Name()).
|
||||||
target := host[:idx]
|
Str("provider", r.ProviderName()).
|
||||||
if r, ok := routes.HTTP.Get(target); ok {
|
Bool("excluded", r.ShouldExclude()).
|
||||||
return r
|
Msg("route")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Try exact match
|
// Get health info for all routes
|
||||||
if r, ok := routes.HTTP.Get(host); ok {
|
healthMap := ep.GetHealthInfoSimple()
|
||||||
return r
|
for alias, status := range healthMap {
|
||||||
}
|
log.Info().Str("alias", alias).Str("status", string(status)).Msg("health")
|
||||||
|
|
||||||
// Try stripping port
|
|
||||||
if before, _, ok := strings.Cut(host, ":"); ok {
|
|
||||||
if r, ok := routes.HTTP.Get(before); ok {
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Short Links
|
### Route Addition
|
||||||
|
|
||||||
Short links use a special `.short` domain:
|
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// Request to: https://abc.short.example.com
|
route := &route.Route{
|
||||||
// Looks for route with alias "abc"
|
Alias: "myapp",
|
||||||
if strings.EqualFold(host, common.ShortLinkPrefix) {
|
Scheme: route.SchemeHTTP,
|
||||||
// Handle short link
|
Host: "myapp",
|
||||||
ep.shortLinkTree.ServeHTTP(w, r)
|
Port: route.Port{Proxy: 80, Target: 3000},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ep.AddRoute(route)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Data Flow
|
## Context Integration
|
||||||
|
|
||||||
```mermaid
|
Routes can access the entrypoint from request context:
|
||||||
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
|
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func (ep *Entrypoint) serveNotFound(w http.ResponseWriter, r *http.Request) {
|
// Set entrypoint in context
|
||||||
if served := middleware.ServeStaticErrorPageFile(w, r); !served {
|
entrypoint.SetCtx(r.Context(), ep)
|
||||||
log.Error().
|
|
||||||
Str("method", r.Method).
|
|
||||||
Str("url", r.URL.String()).
|
|
||||||
Str("remote", r.RemoteAddr).
|
|
||||||
Msgf("not found: %s", r.Host)
|
|
||||||
|
|
||||||
errorPage, ok := errorpage.GetErrorPageByStatus(http.StatusNotFound)
|
// Get entrypoint from context
|
||||||
if ok {
|
if ep := entrypoint.FromCtx(r.Context()); ep != nil {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
route, ok := ep.GetRoute("alias")
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
||||||
w.Write(errorPage)
|
|
||||||
} else {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration Structure
|
## Testing Notes
|
||||||
|
|
||||||
```go
|
- Benchmark tests in [`entrypoint_benchmark_test.go`](internal/entrypoint/entrypoint_benchmark_test.go)
|
||||||
type Config struct {
|
- Integration tests in [`entrypoint_test.go`](internal/entrypoint/entrypoint_test.go)
|
||||||
Middlewares []map[string]any `json:"middlewares"`
|
- Mock route pools for unit testing
|
||||||
Rules rules.Rules `json:"rules"`
|
- Short link tests in [`shortlink_test.go`](internal/entrypoint/shortlink_test.go)
|
||||||
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
|
|
||||||
|
|||||||
@@ -30,9 +30,11 @@ Internal package with stable core types. Route configuration schema is versioned
|
|||||||
type Route struct {
|
type Route struct {
|
||||||
Alias string // Unique route identifier
|
Alias string // Unique route identifier
|
||||||
Scheme Scheme // http, https, h2c, tcp, udp, fileserver
|
Scheme Scheme // http, https, h2c, tcp, udp, fileserver
|
||||||
Host string // Virtual host
|
Host string // Virtual host / target address
|
||||||
Port Port // Listen and target ports
|
Port Port // Listen and target ports
|
||||||
|
|
||||||
|
Bind string // Bind address for listening (IP address, optional)
|
||||||
|
|
||||||
// File serving
|
// File serving
|
||||||
Root string // Document root
|
Root string // Document root
|
||||||
SPA bool // Single-page app mode
|
SPA bool // Single-page app mode
|
||||||
@@ -196,6 +198,7 @@ type Route struct {
|
|||||||
Alias string `json:"alias"`
|
Alias string `json:"alias"`
|
||||||
Scheme Scheme `json:"scheme"`
|
Scheme Scheme `json:"scheme"`
|
||||||
Host string `json:"host,omitempty"`
|
Host string `json:"host,omitempty"`
|
||||||
|
Bind string `json:"bind,omitempty"` // Listen bind address
|
||||||
Port Port `json:"port"`
|
Port Port `json:"port"`
|
||||||
Root string `json:"root,omitempty"`
|
Root string `json:"root,omitempty"`
|
||||||
SPA bool `json:"spa,omitempty"`
|
SPA bool `json:"spa,omitempty"`
|
||||||
@@ -218,23 +221,28 @@ labels:
|
|||||||
routes:
|
routes:
|
||||||
myapp:
|
myapp:
|
||||||
scheme: http
|
scheme: http
|
||||||
root: /var/www/myapp
|
host: myapp.local
|
||||||
spa: true
|
bind: 192.168.1.100 # Optional: bind to specific address
|
||||||
|
port:
|
||||||
|
proxy: 80
|
||||||
|
target: 3000
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Route with Custom Bind Address
|
||||||
|
|
||||||
## Dependency and Integration Map
|
## Dependency and Integration Map
|
||||||
|
|
||||||
| Dependency | Purpose |
|
| Dependency | Purpose |
|
||||||
| -------------------------------- | -------------------------------- |
|
| ---------------------------------- | --------------------------------- |
|
||||||
| `internal/route/routes` | Route registry and lookup |
|
| `internal/route/routes/context.go` | Route context helpers (only file) |
|
||||||
| `internal/route/rules` | Request/response rule processing |
|
| `internal/route/rules` | Request/response rule processing |
|
||||||
| `internal/route/stream` | TCP/UDP stream proxying |
|
| `internal/route/stream` | TCP/UDP stream proxying |
|
||||||
| `internal/route/provider` | Route discovery and loading |
|
| `internal/route/provider` | Route discovery and loading |
|
||||||
| `internal/health/monitor` | Health checking |
|
| `internal/health/monitor` | Health checking |
|
||||||
| `internal/idlewatcher` | Idle container management |
|
| `internal/idlewatcher` | Idle container management |
|
||||||
| `internal/logging/accesslog` | Request logging |
|
| `internal/logging/accesslog` | Request logging |
|
||||||
| `internal/homepage` | Dashboard integration |
|
| `internal/homepage` | Dashboard integration |
|
||||||
| `github.com/yusing/goutils/errs` | Error handling |
|
| `github.com/yusing/goutils/errs` | Error handling |
|
||||||
|
|
||||||
## Observability
|
## Observability
|
||||||
|
|
||||||
@@ -305,6 +313,18 @@ route := &route.Route{
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Route with Custom Bind Address
|
||||||
|
|
||||||
|
```go
|
||||||
|
route := &route.Route{
|
||||||
|
Alias: "myapp",
|
||||||
|
Scheme: route.SchemeHTTP,
|
||||||
|
Host: "myapp.local",
|
||||||
|
Bind: "192.168.1.100", // Bind to specific interface
|
||||||
|
Port: route.Port{Proxy: 80, Target: 3000, Listening: 8443},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### File Server Route
|
### File Server Route
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
|||||||
Reference in New Issue
Block a user