init
This commit is contained in:
8
internal/server/doc.go
Normal file
8
internal/server/doc.go
Normal file
@@ -0,0 +1,8 @@
|
||||
// Package server hosts the Fiber HTTP service, request middleware chain, and
|
||||
// hub registry glue that wires Host/port resolution into proxy handlers.
|
||||
// Phase 1 focuses on a single binary that bootstraps Fiber, attaches logging
|
||||
// and error middlewares, injects the HubRegistry built from config, and exposes
|
||||
// router constructors that other packages (cmd/any-hub, proxy) can reuse.
|
||||
// Future phases may extend this package with TLS, metrics endpoints, or admin
|
||||
// surfaces, so keep exports narrow and accept explicit dependencies.
|
||||
package server
|
||||
77
internal/server/http_client.go
Normal file
77
internal/server/http_client.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"time"
|
||||
|
||||
"github.com/any-hub/any-hub/internal/config"
|
||||
)
|
||||
|
||||
// Shared HTTP transport tunings,复用长连接并集中配置超时。
|
||||
var defaultTransport = &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
MaxIdleConns: 100,
|
||||
MaxIdleConnsPerHost: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
ForceAttemptHTTP2: true,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).DialContext,
|
||||
}
|
||||
|
||||
// NewUpstreamClient 返回共享 http.Client,用于所有上游请求。
|
||||
func NewUpstreamClient(cfg *config.Config) *http.Client {
|
||||
timeout := 30 * time.Second
|
||||
if cfg != nil && cfg.Global.UpstreamTimeout.DurationValue() > 0 {
|
||||
timeout = cfg.Global.UpstreamTimeout.DurationValue()
|
||||
}
|
||||
|
||||
return &http.Client{
|
||||
Timeout: timeout,
|
||||
Transport: defaultTransport.Clone(),
|
||||
}
|
||||
}
|
||||
|
||||
// hopByHopHeaders 定义 RFC 7230 中禁止代理转发的头部。
|
||||
var hopByHopHeaders = map[string]struct{}{
|
||||
"Connection": {},
|
||||
"Keep-Alive": {},
|
||||
"Proxy-Authenticate": {},
|
||||
"Proxy-Authorization": {},
|
||||
"Te": {},
|
||||
"Trailer": {},
|
||||
"Transfer-Encoding": {},
|
||||
"Upgrade": {},
|
||||
"Proxy-Connection": {}, // 非标准字段,但部分代理仍使用
|
||||
}
|
||||
|
||||
// CopyHeaders 将 src 中允许透传的头复制到 dst,自动忽略 hop-by-hop 字段。
|
||||
func CopyHeaders(dst, src http.Header) {
|
||||
for key, values := range src {
|
||||
if isHopByHopHeader(key) {
|
||||
continue
|
||||
}
|
||||
for _, value := range values {
|
||||
dst.Add(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isHopByHopHeader(key string) bool {
|
||||
canonical := textproto.CanonicalMIMEHeaderKey(key)
|
||||
if _, ok := hopByHopHeaders[canonical]; ok {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// IsHopByHopHeader reports whether the header should be stripped by proxies.
|
||||
func IsHopByHopHeader(key string) bool {
|
||||
return isHopByHopHeader(key)
|
||||
}
|
||||
45
internal/server/http_client_test.go
Normal file
45
internal/server/http_client_test.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/any-hub/any-hub/internal/config"
|
||||
)
|
||||
|
||||
func TestNewUpstreamClientUsesConfigTimeout(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Global: config.GlobalConfig{
|
||||
UpstreamTimeout: config.Duration(45 * time.Second),
|
||||
},
|
||||
}
|
||||
|
||||
client := NewUpstreamClient(cfg)
|
||||
if client.Timeout != 45*time.Second {
|
||||
t.Fatalf("expected timeout 45s, got %s", client.Timeout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyHeadersSkipsHopByHop(t *testing.T) {
|
||||
src := http.Header{}
|
||||
src.Add("Connection", "keep-alive")
|
||||
src.Add("Keep-Alive", "timeout=5")
|
||||
src.Add("X-Test-Header", "1")
|
||||
src.Add("x-test-header", "2")
|
||||
|
||||
dst := http.Header{}
|
||||
CopyHeaders(dst, src)
|
||||
|
||||
if _, exists := dst["Connection"]; exists {
|
||||
t.Fatalf("connection header should not be copied")
|
||||
}
|
||||
if _, exists := dst["Keep-Alive"]; exists {
|
||||
t.Fatalf("keep-alive header should not be copied")
|
||||
}
|
||||
|
||||
got := dst.Values("X-Test-Header")
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("expected 2 values, got %v", got)
|
||||
}
|
||||
}
|
||||
152
internal/server/hub_registry.go
Normal file
152
internal/server/hub_registry.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/any-hub/any-hub/internal/config"
|
||||
)
|
||||
|
||||
// HubRoute 将 Hub 配置与派生属性(如缓存 TTL、解析后的 Upstream/Proxy URL)
|
||||
// 聚合在一起,供路由/代理层直接复用,避免重复解析配置。
|
||||
type HubRoute struct {
|
||||
// Config 是用户在 config.toml 中声明的 Hub 字段副本,避免外部修改。
|
||||
Config config.HubConfig
|
||||
// ListenPort 记录当前 CLI 监听端口,方便日志/转发头输出。
|
||||
ListenPort int
|
||||
// CacheTTL 是对当前 Hub 生效的 TTL,若 Hub 未覆盖则等于全局值。
|
||||
CacheTTL time.Duration
|
||||
// UpstreamURL/ProxyURL 在构造 Registry 时提前解析完成,便于后续请求快速复用。
|
||||
UpstreamURL *url.URL
|
||||
ProxyURL *url.URL
|
||||
}
|
||||
|
||||
// HubRegistry 提供 Host/Host:port 到 HubRoute 的查询能力,所有 Hub 共享同一个监听端口。
|
||||
type HubRegistry struct {
|
||||
routes map[string]*HubRoute
|
||||
ordered []*HubRoute
|
||||
}
|
||||
|
||||
// NewHubRegistry 根据配置构建 Host/端口映射。调用方应在启动阶段创建一次并复用。
|
||||
func NewHubRegistry(cfg *config.Config) (*HubRegistry, error) {
|
||||
if cfg == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
registry := &HubRegistry{
|
||||
routes: make(map[string]*HubRoute, len(cfg.Hubs)),
|
||||
}
|
||||
|
||||
if len(cfg.Hubs) == 0 {
|
||||
return registry, nil
|
||||
}
|
||||
|
||||
for _, hub := range cfg.Hubs {
|
||||
normalizedHost := normalizeDomain(hub.Domain)
|
||||
if normalizedHost == "" {
|
||||
return nil, fmt.Errorf("invalid domain for hub %s", hub.Name)
|
||||
}
|
||||
if _, exists := registry.routes[normalizedHost]; exists {
|
||||
return nil, fmt.Errorf("duplicate domain mapping detected for %s", normalizedHost)
|
||||
}
|
||||
|
||||
route, err := buildHubRoute(cfg, hub)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
registry.routes[normalizedHost] = route
|
||||
registry.ordered = append(registry.ordered, route)
|
||||
}
|
||||
|
||||
return registry, nil
|
||||
}
|
||||
|
||||
// Lookup 根据 Host 或 Host:port 查找 HubRoute。
|
||||
func (r *HubRegistry) Lookup(host string) (*HubRoute, bool) {
|
||||
if r == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
normalizedHost, _ := normalizeHost(host)
|
||||
if normalizedHost == "" {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
route, ok := r.routes[normalizedHost]
|
||||
return route, ok
|
||||
}
|
||||
|
||||
// List 返回当前注册的 HubRoute 列表(按配置定义的顺序),用于调试或 /status 输出。
|
||||
func (r *HubRegistry) List() []HubRoute {
|
||||
if r == nil || len(r.ordered) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]HubRoute, len(r.ordered))
|
||||
for i, route := range r.ordered {
|
||||
result[i] = *route
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func buildHubRoute(cfg *config.Config, hub config.HubConfig) (*HubRoute, error) {
|
||||
upstreamURL, err := url.Parse(hub.Upstream)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid upstream for hub %s: %w", hub.Name, err)
|
||||
}
|
||||
|
||||
var proxyURL *url.URL
|
||||
if hub.Proxy != "" {
|
||||
proxyURL, err = url.Parse(hub.Proxy)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid proxy for hub %s: %w", hub.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return &HubRoute{
|
||||
Config: hub,
|
||||
ListenPort: cfg.Global.ListenPort,
|
||||
CacheTTL: cfg.EffectiveCacheTTL(hub),
|
||||
UpstreamURL: upstreamURL,
|
||||
ProxyURL: proxyURL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func normalizeDomain(domain string) string {
|
||||
host, _ := normalizeHost(domain)
|
||||
return host
|
||||
}
|
||||
|
||||
func normalizeHost(raw string) (string, int) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return "", 0
|
||||
}
|
||||
|
||||
host := raw
|
||||
port := 0
|
||||
|
||||
if strings.Contains(raw, ":") {
|
||||
if h, p, err := net.SplitHostPort(raw); err == nil {
|
||||
host = h
|
||||
if parsedPort, err := strconv.Atoi(p); err == nil {
|
||||
port = parsedPort
|
||||
}
|
||||
} else if idx := strings.LastIndex(raw, ":"); idx > -1 && strings.Count(raw[idx+1:], ":") == 0 {
|
||||
if parsedPort, err := strconv.Atoi(raw[idx+1:]); err == nil {
|
||||
host = raw[:idx]
|
||||
port = parsedPort
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
host = strings.TrimSuffix(host, ".")
|
||||
host = strings.ToLower(host)
|
||||
return host, port
|
||||
}
|
||||
120
internal/server/hub_registry_test.go
Normal file
120
internal/server/hub_registry_test.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/any-hub/any-hub/internal/config"
|
||||
)
|
||||
|
||||
func TestHubRegistryLookupByHost(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Global: config.GlobalConfig{
|
||||
ListenPort: 5000,
|
||||
CacheTTL: config.Duration(2 * time.Hour),
|
||||
},
|
||||
Hubs: []config.HubConfig{
|
||||
{
|
||||
Name: "docker",
|
||||
Domain: "docker.hub.local",
|
||||
Type: "docker",
|
||||
Upstream: "https://registry-1.docker.io",
|
||||
EnableHeadCheck: true,
|
||||
},
|
||||
{
|
||||
Name: "npm",
|
||||
Domain: "npm.hub.local",
|
||||
Type: "npm",
|
||||
Upstream: "https://registry.npmjs.org",
|
||||
CacheTTL: config.Duration(30 * time.Minute),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
registry, err := NewHubRegistry(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
route, ok := registry.Lookup("docker.hub.local")
|
||||
if !ok {
|
||||
t.Fatalf("expected docker route")
|
||||
}
|
||||
|
||||
if route.Config.Name != "docker" {
|
||||
t.Errorf("wrong hub returned: %s", route.Config.Name)
|
||||
}
|
||||
|
||||
if route.CacheTTL != cfg.EffectiveCacheTTL(route.Config) {
|
||||
t.Errorf("cache ttl mismatch: got %s", route.CacheTTL)
|
||||
}
|
||||
|
||||
if route.UpstreamURL.String() != "https://registry-1.docker.io" {
|
||||
t.Errorf("unexpected upstream URL: %s", route.UpstreamURL)
|
||||
}
|
||||
|
||||
if route.ProxyURL != nil {
|
||||
t.Errorf("expected nil proxy")
|
||||
}
|
||||
|
||||
if route.ListenPort != cfg.Global.ListenPort {
|
||||
t.Fatalf("route listen port mismatch: %d", route.ListenPort)
|
||||
}
|
||||
|
||||
if got := len(registry.List()); got != 2 {
|
||||
t.Fatalf("expected 2 routes in list, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHubRegistryParsesHostHeaderPort(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Global: config.GlobalConfig{
|
||||
ListenPort: 5000,
|
||||
CacheTTL: config.Duration(time.Hour),
|
||||
},
|
||||
Hubs: []config.HubConfig{
|
||||
{
|
||||
Name: "docker",
|
||||
Domain: "docker.hub.local",
|
||||
Type: "docker",
|
||||
Upstream: "https://registry-1.docker.io",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
registry, err := NewHubRegistry(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if _, ok := registry.Lookup("docker.hub.local:6000"); !ok {
|
||||
t.Fatalf("expected lookup to ignore host header port")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHubRegistryRejectsDuplicateDomains(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Global: config.GlobalConfig{
|
||||
ListenPort: 5000,
|
||||
CacheTTL: config.Duration(time.Hour),
|
||||
},
|
||||
Hubs: []config.HubConfig{
|
||||
{
|
||||
Name: "docker",
|
||||
Domain: "docker.hub.local",
|
||||
Type: "docker",
|
||||
Upstream: "https://registry-1.docker.io",
|
||||
},
|
||||
{
|
||||
Name: "docker-alt",
|
||||
Domain: "docker.hub.local",
|
||||
Type: "docker",
|
||||
Upstream: "https://mirror.registry-1.docker.io",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if _, err := NewHubRegistry(cfg); err == nil {
|
||||
t.Fatalf("expected duplicate domain error")
|
||||
}
|
||||
}
|
||||
163
internal/server/router.go
Normal file
163
internal/server/router.go
Normal 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",
|
||||
})
|
||||
}
|
||||
118
internal/server/router_test.go
Normal file
118
internal/server/router_test.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/any-hub/any-hub/internal/config"
|
||||
)
|
||||
|
||||
func TestRouterRoutesRequestWhenHostMatches(t *testing.T) {
|
||||
app := newTestApp(t, 5000)
|
||||
|
||||
req := httptest.NewRequest("GET", "http://docker.hub.local/v2/", nil)
|
||||
req.Host = "docker.hub.local"
|
||||
req.Header.Set("Host", "docker.hub.local")
|
||||
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test failed: %v", err)
|
||||
}
|
||||
if resp.StatusCode != fiber.StatusNoContent {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
t.Fatalf("expected 204 status, got %d (body=%s, hostHeader=%s)", resp.StatusCode, string(body), resp.Header.Get("X-Any-Hub-Host"))
|
||||
}
|
||||
|
||||
if app.storage.routeName != "docker" {
|
||||
t.Fatalf("expected docker route, got %s", app.storage.routeName)
|
||||
}
|
||||
|
||||
if reqID := resp.Header.Get("X-Request-ID"); reqID == "" {
|
||||
t.Fatalf("expected X-Request-ID header to be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouterReturns404WhenHostUnknown(t *testing.T) {
|
||||
app := newTestApp(t, 5000)
|
||||
|
||||
req := httptest.NewRequest("GET", "http://unknown.local/v2/", nil)
|
||||
req.Host = "unknown.local"
|
||||
req.Header.Set("Host", "unknown.local")
|
||||
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test failed: %v", err)
|
||||
}
|
||||
if resp.StatusCode != fiber.StatusNotFound {
|
||||
t.Fatalf("expected 404 status, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if !bytes.Contains(body, []byte(`"host_unmapped"`)) {
|
||||
t.Fatalf("expected host_unmapped error, got %s", string(body))
|
||||
}
|
||||
}
|
||||
|
||||
type testApp struct {
|
||||
*fiber.App
|
||||
storage *proxyRecorder
|
||||
}
|
||||
|
||||
func newTestApp(t *testing.T, port int) *testApp {
|
||||
t.Helper()
|
||||
|
||||
cfg := &config.Config{
|
||||
Global: config.GlobalConfig{
|
||||
ListenPort: port,
|
||||
CacheTTL: config.Duration(3600),
|
||||
},
|
||||
Hubs: []config.HubConfig{
|
||||
{
|
||||
Name: "docker",
|
||||
Domain: "docker.hub.local",
|
||||
Type: "docker",
|
||||
Upstream: "https://registry-1.docker.io",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
registry, err := NewHubRegistry(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create registry: %v", err)
|
||||
}
|
||||
if _, ok := registry.Lookup("docker.hub.local"); !ok {
|
||||
t.Fatalf("registry lookup failed for docker")
|
||||
}
|
||||
|
||||
logger := logrus.New()
|
||||
logger.SetOutput(io.Discard)
|
||||
|
||||
recorder := &proxyRecorder{}
|
||||
app, err := NewApp(AppOptions{
|
||||
Logger: logger,
|
||||
Registry: registry,
|
||||
Proxy: recorder,
|
||||
ListenPort: port,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create app: %v", err)
|
||||
}
|
||||
|
||||
return &testApp{App: app, storage: recorder}
|
||||
}
|
||||
|
||||
type proxyRecorder struct {
|
||||
lastRoute *HubRoute
|
||||
routeName string
|
||||
}
|
||||
|
||||
func (p *proxyRecorder) Handle(c fiber.Ctx, route *HubRoute) error {
|
||||
p.lastRoute = route
|
||||
p.routeName = route.Config.Name
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
}
|
||||
Reference in New Issue
Block a user