diff --git a/internal/config/README.md b/internal/config/README.md index 5ad73378..3fe95604 100644 --- a/internal/config/README.md +++ b/internal/config/README.md @@ -54,7 +54,7 @@ type State interface { Task() *task.Task Context() context.Context Value() *Config - EntrypointHandler() http.Handler + Entrypoint() entrypoint.Entrypoint ShortLinkMatcher() config.ShortLinkMatcher AutoCertProvider() server.CertProvider LoadOrStoreProvider(key string, value types.RouteProvider) (actual types.RouteProvider, loaded bool) @@ -62,6 +62,12 @@ type State interface { IterProviders() iter.Seq2[string, types.RouteProvider] StartProviders() error 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/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/maxmind` - GeoIP configuration - `internal/notif` - Notification providers - `internal/proxmox` - LXC container management - `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 ### External dependencies @@ -312,5 +321,8 @@ for name, provider := range config.GetState().IterProviders() { ```go state := config.GetState() -http.Handle("/", state.EntrypointHandler()) +// Get entrypoint interface for route management +ep := state.Entrypoint() +// Add routes directly to entrypoint +ep.AddRoute(route) ``` diff --git a/internal/entrypoint/README.md b/internal/entrypoint/README.md index 2f9b256b..c83159b9 100644 --- a/internal/entrypoint/README.md +++ b/internal/entrypoint/README.md @@ -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/TCP/UDP 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/TCP/UDP servers, determines the target route based on hostname, applies middleware, and forwards requests to the appropriate route handler. ### Key Features @@ -14,103 +14,310 @@ 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 +- 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 -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 [`AddRoute`](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`) - 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) + 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 ```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"` +} ``` -### 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 { + <> + +Get(alias) (Route, bool) + +Iter(yield) bool + +Size() int + } + + class RWPoolLike { + <> + +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 -// ServeHTTP is the main HTTP handler. -func (ep *Entrypoint) ServeHTTP(w http.ResponseWriter, r *http.Request) +// Adding a route +ep.AddRoute(route) -// FindRoute looks up a route by hostname. -func (ep *Entrypoint) FindRoute(s string) types.HTTPRoute +// Iterating all routes +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 ```go -ep := entrypoint.NewEntrypoint() +ep := entrypoint.NewEntrypoint(parent, &entrypoint.Config{ + SupportProxyProtocol: false, +}) // Configure domain matching 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}}, }) if err != nil { - log.Fatal(err) + return err } // Configure access logging @@ -128,181 +335,59 @@ 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 +for r := range ep.IterRoutes { + log.Info(). + Str("alias", r.Name()). + Str("provider", r.ProviderName()). + Bool("excluded", r.ShouldExclude()). + Msg("route") +} - // 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 - -Short links use a special `.short` domain: +### Route Addition ```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) +route := &route.Route{ + Alias: "myapp", + Scheme: route.SchemeHTTP, + Host: "myapp", + Port: route.Port{Proxy: 80, Target: 3000}, } + +ep.AddRoute(route) ``` -## 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 +entrypoint.SetCtx(r.Context(), 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) diff --git a/internal/route/README.md b/internal/route/README.md index acf7b83a..260343e9 100644 --- a/internal/route/README.md +++ b/internal/route/README.md @@ -30,9 +30,11 @@ Internal package with stable core types. Route configuration schema is versioned type Route struct { Alias string // Unique route identifier Scheme Scheme // http, https, h2c, tcp, udp, fileserver - Host string // Virtual host + Host string // Virtual host / target address Port Port // Listen and target ports + Bind string // Bind address for listening (IP address, optional) + // File serving Root string // Document root SPA bool // Single-page app mode @@ -196,6 +198,7 @@ type Route struct { Alias string `json:"alias"` Scheme Scheme `json:"scheme"` Host string `json:"host,omitempty"` + Bind string `json:"bind,omitempty"` // Listen bind address Port Port `json:"port"` Root string `json:"root,omitempty"` SPA bool `json:"spa,omitempty"` @@ -218,23 +221,28 @@ labels: routes: myapp: scheme: http - root: /var/www/myapp - spa: true + host: myapp.local + 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 | Purpose | -| -------------------------------- | -------------------------------- | -| `internal/route/routes` | Route registry and lookup | -| `internal/route/rules` | Request/response rule processing | -| `internal/route/stream` | TCP/UDP stream proxying | -| `internal/route/provider` | Route discovery and loading | -| `internal/health/monitor` | Health checking | -| `internal/idlewatcher` | Idle container management | -| `internal/logging/accesslog` | Request logging | -| `internal/homepage` | Dashboard integration | -| `github.com/yusing/goutils/errs` | Error handling | +| Dependency | Purpose | +| ---------------------------------- | --------------------------------- | +| `internal/route/routes/context.go` | Route context helpers (only file) | +| `internal/route/rules` | Request/response rule processing | +| `internal/route/stream` | TCP/UDP stream proxying | +| `internal/route/provider` | Route discovery and loading | +| `internal/health/monitor` | Health checking | +| `internal/idlewatcher` | Idle container management | +| `internal/logging/accesslog` | Request logging | +| `internal/homepage` | Dashboard integration | +| `github.com/yusing/goutils/errs` | Error handling | ## 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 ```go