This commit is contained in:
2025-11-14 12:11:44 +08:00
commit 39ebf61572
88 changed files with 9999 additions and 0 deletions

163
internal/server/router.go Normal file
View File

@@ -0,0 +1,163 @@
package server
import (
"errors"
"fmt"
"strings"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/recover"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
)
// ProxyHandler describes the component responsible for proxying requests to
// the upstream Hub. It allows injecting fake handlers during tests.
type ProxyHandler interface {
Handle(fiber.Ctx, *HubRoute) error
}
// ProxyHandlerFunc adapts a function to the ProxyHandler interface.
type ProxyHandlerFunc func(fiber.Ctx, *HubRoute) error
// Handle makes ProxyHandlerFunc satisfy ProxyHandler.
func (f ProxyHandlerFunc) Handle(c fiber.Ctx, route *HubRoute) error {
return f(c, route)
}
// AppOptions controls how the Fiber application should behave on a specific port.
type AppOptions struct {
Logger *logrus.Logger
Registry *HubRegistry
Proxy ProxyHandler
ListenPort int
}
const (
contextKeyRoute = "_anyhub_route"
contextKeyRequestID = "_anyhub_request_id"
)
// NewApp builds a Fiber application with Host/port routing middleware and
// structured error handling.
func NewApp(opts AppOptions) (*fiber.App, error) {
if opts.Logger == nil {
return nil, errors.New("logger is required")
}
if opts.Registry == nil {
return nil, errors.New("hub registry is required")
}
if opts.Proxy == nil {
return nil, errors.New("proxy handler is required")
}
if opts.ListenPort <= 0 {
return nil, fmt.Errorf("invalid listen port: %d", opts.ListenPort)
}
app := fiber.New(fiber.Config{
CaseSensitive: true,
})
app.Use(recover.New())
app.Use(requestContextMiddleware(opts))
app.All("/*", func(c fiber.Ctx) error {
route, _ := getRouteFromContext(c)
if route == nil {
return renderHostUnmapped(c, opts.Logger, "", opts.ListenPort)
}
return opts.Proxy.Handle(c, route)
})
return app, nil
}
// requestContextMiddleware 负责生成请求 ID并基于 Host/Host:port 查找 HubRoute。
func requestContextMiddleware(opts AppOptions) fiber.Handler {
return func(c fiber.Ctx) error {
reqID := uuid.NewString()
c.Locals(contextKeyRequestID, reqID)
c.Set("X-Request-ID", reqID)
rawHost := strings.TrimSpace(getHostHeader(c))
route, ok := opts.Registry.Lookup(rawHost)
if !ok {
return renderHostUnmapped(c, opts.Logger, rawHost, opts.ListenPort)
}
if err := ensureRouterHubType(route); err != nil {
return renderTypeUnsupported(c, opts.Logger, route, err)
}
c.Locals(contextKeyRoute, route)
return c.Next()
}
}
func renderHostUnmapped(c fiber.Ctx, logger *logrus.Logger, host string, port int) error {
fields := logrus.Fields{
"action": "host_lookup",
"host": host,
"port": port,
}
logger.WithFields(fields).Warn("host unmapped")
if host != "" {
c.Set("X-Any-Hub-Host", host)
}
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"error": "host_unmapped",
})
}
func getHostHeader(c fiber.Ctx) string {
if raw := c.Request().Header.Peek(fiber.HeaderHost); len(raw) > 0 {
return string(raw)
}
return c.Hostname()
}
func getRouteFromContext(c fiber.Ctx) (*HubRoute, bool) {
if value := c.Locals(contextKeyRoute); value != nil {
if route, ok := value.(*HubRoute); ok {
return route, true
}
}
return nil, false
}
// RequestID returns the request identifier stored by the router middleware.
func RequestID(c fiber.Ctx) string {
if value := c.Locals(contextKeyRequestID); value != nil {
if reqID, ok := value.(string); ok {
return reqID
}
}
return ""
}
func ensureRouterHubType(route *HubRoute) error {
switch route.Config.Type {
case "docker":
return nil
case "npm":
return nil
case "go":
return nil
default:
return fmt.Errorf("unsupported hub type: %s", route.Config.Type)
}
}
func renderTypeUnsupported(c fiber.Ctx, logger *logrus.Logger, route *HubRoute, err error) error {
fields := logrus.Fields{
"action": "hub_type_check",
"hub": route.Config.Name,
"hub_type": route.Config.Type,
"error": "hub_type_unsupported",
}
logger.WithFields(fields).Error(err.Error())
return c.Status(fiber.StatusNotImplemented).JSON(fiber.Map{
"error": "hub_type_unsupported",
})
}