Files
fiber/domain.go

689 lines
24 KiB
Go

// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️
// 🤖 GitHub Repository: https://github.com/gofiber/fiber
// 📌 API Documentation: https://docs.gofiber.io
package fiber
import (
"fmt"
"reflect"
"strings"
"github.com/gofiber/utils/v2"
utilsstrings "github.com/gofiber/utils/v2/strings"
)
// domainLocalsKeyType is an unexported type used as the Locals key for domain
// parameters, preventing collisions with user or middleware keys.
type domainLocalsKeyType struct{}
// domainLocalsKey is the typed key used in c.Locals() to store domain parameter values.
var domainLocalsKey = domainLocalsKeyType{}
// domainParams stores domain parameter names and their values for a request.
type domainParams struct {
names []string
values []string
}
// domainCheckResult caches a domain match result for a single request.
// It stores the matched domain params (if any) alongside the match status
// to avoid allocating a new domainParams struct for every handler invocation.
type domainCheckResult struct {
params *domainParams // pre-built params (nil if no params or no match)
matched bool
}
// domainMatcher holds the parsed domain pattern for matching against request hostnames.
type domainMatcher struct {
parts []string // domain parts split by "."
paramIdx []int // indices of parameter parts
paramNames []string // parameter names (without ":")
numParts int // total number of parts
}
// maxDomainParts defines the maximum number of domain labels allowed (e.g., sub.domain.example.com = 4 parts).
// This prevents DoS attacks from patterns or hostnames with excessive label counts.
// RFC 1035 suggests 127 labels max, but we use a more conservative limit to prevent memory exhaustion.
const maxDomainParts = 16
// parseDomainPattern parses a domain pattern like ":subdomain.example.com"
// into a domainMatcher. Parameter parts start with ":".
// Constant labels are lowercased per RFC 4343 (domain names are case-insensitive),
// but parameter names are preserved as-is so that DomainParam lookups work with
// the exact names the caller used (e.g., ":User" → param name "User").
func parseDomainPattern(pattern string) domainMatcher {
pattern = utils.TrimSpace(pattern)
// Trim trailing dot of a fully-qualified domain name (RFC 3986),
// consistent with Fiber's own host normalization in Subdomains().
pattern = utils.TrimRight(pattern, '.')
// Validate pattern is not empty after trimming
if pattern == "" {
panic("Domain pattern cannot be empty")
}
// Enforce RFC 1035 total length limit on patterns
if len(pattern) > 253 {
panic(fmt.Sprintf("Domain pattern '%s' exceeds RFC 1035 maximum of 253 characters (%d chars)",
pattern, len(pattern)))
}
parts := strings.Split(pattern, ".")
// Prevent DoS from patterns with excessive label counts
if len(parts) > maxDomainParts {
panic(fmt.Sprintf("Domain pattern '%s' has %d parts, which exceeds the maximum of %d",
pattern, len(parts), maxDomainParts))
}
m := domainMatcher{
parts: make([]string, len(parts)),
numParts: len(parts),
}
for i, part := range parts {
// Validate no empty labels (e.g., "example..com" is invalid)
if part == "" {
panic(fmt.Sprintf("Domain pattern '%s' contains empty label at position %d", pattern, i))
}
if part[0] == ':' {
// Validate parameter name is not empty
if len(part) == 1 {
panic(fmt.Sprintf("Domain pattern '%s' contains empty parameter name at position %d", pattern, i))
}
paramName := part[1:]
// Validate parameter name contains only ASCII-safe characters (a-z, A-Z, 0-9, underscore, hyphen).
// Using explicit ASCII ranges rather than unicode.IsLetter/IsDigit to reject non-ASCII
// characters that are invalid in DNS names.
for _, ch := range paramName {
if !isASCIIAlphanumeric(ch) && ch != '_' && ch != '-' {
panic(fmt.Sprintf("Domain pattern '%s' contains invalid parameter name '%s' with character '%c'", pattern, paramName, ch))
}
}
m.paramIdx = append(m.paramIdx, i)
m.paramNames = append(m.paramNames, paramName) // preserve original case
m.parts[i] = part // keep ":param" marker for matching
} else {
// Only lowercase constant labels (RFC 4343)
// Enforce RFC 1035 per-label length limit (63 characters)
if len(part) > 63 {
panic(fmt.Sprintf("Domain pattern '%s' has label '%s' exceeding RFC 1035 limit of 63 characters (%d chars)",
pattern, part, len(part)))
}
// Validate label contains only valid ASCII domain characters (a-z, 0-9, hyphen).
normalized := utilsstrings.ToLower(part)
for _, ch := range normalized {
if !isASCIIAlphanumeric(ch) && ch != '-' {
panic(fmt.Sprintf("Domain pattern '%s' contains invalid character '%c' in label '%s'", pattern, ch, part))
}
}
m.parts[i] = normalized
}
}
// Check if the domain pattern has too many parameters
if len(m.paramNames) > maxParams {
panic(fmt.Sprintf("Domain pattern '%s' has %d parameters, which exceeds the maximum of %d",
pattern, len(m.paramNames), maxParams))
}
return m
}
// match checks if a hostname matches the domain pattern.
// It returns true if matched and a slice of parameter values (parallel to paramNames).
// Uses a stack-allocated buffer to avoid heap allocation for typical domain names.
// Validates hostname to prevent DoS attacks from malicious input.
func (m *domainMatcher) match(hostname string) (bool, []string) { //nolint:gocritic // unnamedResult: named returns conflict with nonamedreturns linter
// Trim trailing dot of a fully-qualified domain name (RFC 3986),
// consistent with Fiber's own host normalization in Subdomains().
hostname = utils.TrimRight(hostname, '.')
// Validate hostname is not empty and not excessively long (DoS protection)
// RFC 1035 limits domain names to 253 characters
if hostname == "" || len(hostname) > 253 {
return false, nil
}
// Domain names are case-insensitive per RFC 4343; lowercase after cheap validation
hostname = utilsstrings.ToLower(hostname)
// Use stack-allocated array for typical domain names (up to 16 labels).
// This avoids heap allocation for most common cases, consistent with
// the Subdomains() implementation in req.go.
// The buffer size matches maxDomainParts to prevent overflow.
var partsBuf [maxDomainParts]string
parts := partsBuf[:0]
labelCount := 0
for part := range strings.SplitSeq(hostname, ".") {
labelCount++
// DoS protection: reject hostnames with too many labels
if labelCount > maxDomainParts {
return false, nil
}
// DoS protection: reject empty labels or excessively long labels
// RFC 1035 limits each label to 63 characters
if part == "" || len(part) > 63 {
return false, nil
}
// Validate label contains only safe ASCII domain characters (basic sanitization)
// This prevents injection attacks via malicious hostnames
for _, ch := range part {
if ch != '-' && !isASCIIAlphanumeric(ch) {
return false, nil
}
}
parts = append(parts, part)
}
if len(parts) != m.numParts {
return false, nil
}
// First pass: validate all constant labels without allocating paramValues.
for i, patternPart := range m.parts {
if patternPart != "" && patternPart[0] == ':' {
// Parameter segment; skip in this pass.
continue
}
if patternPart != parts[i] {
return false, nil
}
}
// No parameters to capture; avoid allocating an empty slice.
if len(m.paramIdx) == 0 {
return true, nil
}
// Second pass: now that constants are confirmed, allocate and fill paramValues.
paramValues := make([]string, len(m.paramIdx))
paramIter := 0
for i, patternPart := range m.parts {
if patternPart != "" && patternPart[0] == ':' {
paramValues[paramIter] = parts[i]
paramIter++
}
}
return true, paramValues
}
// isASCIIAlphanumeric returns true if the rune is an ASCII letter (a-z, A-Z) or digit (0-9).
// This is used instead of unicode.IsLetter/unicode.IsDigit to ensure only ASCII characters
// are accepted in domain patterns and hostnames, as DNS names are ASCII-only.
func isASCIIAlphanumeric(ch rune) bool {
return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9')
}
// DomainParam returns the value of a domain parameter from the context.
// Domain parameters are set when a route registered via [App.Domain] or [Group.Domain]
// matches the incoming request hostname.
//
// app.Domain("example.com").Get("/", func(c fiber.Ctx) error {
// return c.SendString("Welcome!")
// })
//
// app.Domain(":user.example.com").Get("/", func(c fiber.Ctx) error {
// user := fiber.DomainParam(c, "user")
// return c.SendString("Hello, " + user)
// })
func DomainParam(c Ctx, key string, defaultValue ...string) string {
if params, ok := c.Locals(domainLocalsKey).(*domainParams); ok && params != nil {
for i, name := range params.names {
if name == key {
return params.values[i]
}
}
}
if len(defaultValue) > 0 {
return defaultValue[0]
}
return ""
}
// domainRouter implements [Router] for domain-filtered routing.
// It wraps an underlying [App] or [Group] and checks the request hostname
// against the domain pattern before executing handlers.
//
// Routes registered through a domainRouter have zero impact on routing
// performance for requests that don't use domain-based routing.
//
// Known limitation: because domain filtering is applied at handler-execution
// time (not at route-matching time), Fiber's 405 Method Not Allowed logic
// may advertise methods for domain-scoped routes even when the requesting
// host does not match the domain pattern. Fixing this would require core
// router changes; for now callers should be aware that 405 responses may
// include methods from domain-scoped routes whose host did not match.
type domainRouter struct {
app *App
group *Group // non-nil when created from a Group
matcher domainMatcher
}
// Verify domainRouter implements Router at compile time.
var _ Router = (*domainRouter)(nil)
// wrapHandlers wraps every handler in the slice with domain checking.
// The hostname match is computed once per request per domain-router and cached
// so that subsequent handlers in the same route avoid redundant parsing.
// Each handler independently checks the cached result, ensuring that Fiber's
// route-merging behavior (combining handlers from multiple registrations into
// one route) cannot cause a non-domain handler to be skipped.
// domainCheckResult objects are cached per-request in c.Locals() to avoid redundant hostname parsing.
func (d *domainRouter) wrapHandlers(handlers []Handler) []Handler {
if len(handlers) == 0 {
return handlers
}
// Use the domainRouter pointer as cache key to avoid cross-matcher collisions.
// Each domainRouter instance gets its own cache slot.
cacheKey := d
result := make([]Handler, len(handlers))
for i, h := range handlers {
origHandler := h
result[i] = func(c Ctx) error {
// Check if we already matched this domain on this request.
var check *domainCheckResult
if cached, ok := c.Locals(cacheKey).(*domainCheckResult); ok {
check = cached
} else {
hostname := c.Hostname()
matched, values := d.matcher.match(hostname)
check = &domainCheckResult{matched: matched}
if matched && len(values) > 0 {
// Store values directly — match() returns a fresh slice each time.
// Build domainParams once and cache it alongside the match result
// so subsequent handlers reuse the same struct.
check.params = &domainParams{
names: d.matcher.paramNames,
values: values,
}
}
c.Locals(cacheKey, check)
}
if !check.matched {
return c.Next()
}
// Reuse the cached domainParams (or nil to clear stale values)
// instead of allocating a new struct per handler invocation.
c.Locals(domainLocalsKey, check.params)
return origHandler(c)
}
}
return result
}
// registerPath returns the full path for registration, taking group prefix into account.
func (d *domainRouter) registerPath(path string) string {
if d.group != nil {
return getGroupPath(d.group.Prefix, path)
}
return path
}
// registerGroup returns the group to associate with routes, if any.
func (d *domainRouter) registerGroup() *Group {
return d.group
}
// Use registers a middleware route that will match requests
// with the provided prefix (which is optional and defaults to "/").
//
// The middleware only executes when the request hostname matches the domain pattern.
//
// api := app.Domain("api.example.com")
// api.Use(func(c fiber.Ctx) error {
// // Only runs for api.example.com requests
// return c.Next()
// })
func (d *domainRouter) Use(args ...any) Router {
var subApp *App
var prefix string
var prefixes []string
var handlers []Handler
for i := range args {
switch arg := args[i].(type) {
case string:
prefix = arg
case []string:
prefixes = arg
case *App:
subApp = arg
default:
handler, ok := toFiberHandler(arg)
if !ok {
panic(fmt.Sprintf("use: invalid handler %v", reflect.TypeOf(arg)))
}
handlers = append(handlers, handler)
}
}
if len(prefixes) == 0 {
prefixes = append(prefixes, prefix)
}
for _, prefix := range prefixes {
if subApp != nil {
return d.mount(prefix, subApp)
}
wrapped := d.wrapHandlers(handlers)
d.app.register([]string{methodUse}, d.registerPath(prefix), d.registerGroup(), wrapped...)
}
// Mark the underlying group so Name() can distinguish between
// group-name-prefix calls (before routes) and route-name calls (after routes).
if d.group != nil && !d.group.anyRouteDefined {
d.group.anyRouteDefined = true
}
return d
}
// mount attaches a sub-app instance to the domain router at the specified prefix.
// All routes from the sub-app will only be accessible when the request hostname
// matches the domain pattern.
//
// The sub-app is not modified: routes are cloned into a dedicated wrapper app
// with domain-filtered handlers, so the same sub-app can safely be mounted on
// multiple domains without double-wrapping. Routes added to the sub-app after
// mounting will not inherit domain filtering.
func (d *domainRouter) mount(prefix string, subApp *App) Router {
// Determine the full mount path by combining the domain router's path with the prefix
var mountPath string
if d.group != nil {
mountPath = getGroupPath(d.group.Prefix, prefix)
} else {
mountPath = prefix
}
// Normalize the mount path
mountPath = utils.TrimRight(mountPath, '/')
if mountPath == "" {
mountPath = "/"
}
// Create a wrapper app so that the original sub-app is not mutated.
// This allows the same sub-app to be reused (e.g., mounted on multiple
// domains) without double-wrapping handlers.
wrapperApp := New(Config{
CaseSensitive: subApp.config.CaseSensitive,
StrictRouting: subApp.config.StrictRouting,
})
// Clone routes from the sub-app with domain-wrapped handlers.
// Lock the sub-app while reading to prevent data races with concurrent
// route registration.
subApp.mutex.Lock()
defer subApp.mutex.Unlock()
for m := range subApp.stack {
for _, route := range subApp.stack[m] {
clonedRoute := subApp.copyRoute(route)
if len(clonedRoute.Handlers) > 0 {
clonedRoute.Handlers = d.wrapHandlers(clonedRoute.Handlers)
}
wrapperApp.stack[m] = append(wrapperApp.stack[m], clonedRoute)
}
}
d.app.mutex.Lock()
// Support for configs of mounted-apps and sub-mounted-apps
for mountedPrefixes, subAppInstance := range subApp.mountFields.appList {
path := getGroupPath(mountPath, mountedPrefixes)
subAppInstance.mountFields.mountPath = path
d.app.mountFields.appList[path] = subAppInstance
}
d.app.mutex.Unlock()
// Create a mount group referencing the wrapper app (not the original).
// During route expansion (processSubAppsRoutes), Fiber reads routes from
// route.group.app.stack — so using the wrapper ensures expanded routes
// carry domain-filtered handlers.
mountGroup := &Group{Prefix: mountPath, app: wrapperApp}
// Register the mount point - the routes will be expanded during startup
d.app.register([]string{methodUse}, mountPath, mountGroup)
// Execute onMount hooks
if err := subApp.hooks.executeOnMountHooks(d.app); err != nil {
panic(err)
}
// Mark the underlying group so Name() can distinguish between
// group-name-prefix calls and route-name calls
if d.group != nil && !d.group.anyRouteDefined {
d.group.anyRouteDefined = true
}
return d
}
// Get registers a route for GET methods.
// The handler only executes when the request hostname matches the domain pattern.
func (d *domainRouter) Get(path string, handler any, handlers ...any) Router {
return d.Add([]string{MethodGet}, path, handler, handlers...)
}
// Head registers a route for HEAD methods.
// The handler only executes when the request hostname matches the domain pattern.
func (d *domainRouter) Head(path string, handler any, handlers ...any) Router {
return d.Add([]string{MethodHead}, path, handler, handlers...)
}
// Post registers a route for POST methods.
// The handler only executes when the request hostname matches the domain pattern.
func (d *domainRouter) Post(path string, handler any, handlers ...any) Router {
return d.Add([]string{MethodPost}, path, handler, handlers...)
}
// Put registers a route for PUT methods.
// The handler only executes when the request hostname matches the domain pattern.
func (d *domainRouter) Put(path string, handler any, handlers ...any) Router {
return d.Add([]string{MethodPut}, path, handler, handlers...)
}
// Delete registers a route for DELETE methods.
// The handler only executes when the request hostname matches the domain pattern.
func (d *domainRouter) Delete(path string, handler any, handlers ...any) Router {
return d.Add([]string{MethodDelete}, path, handler, handlers...)
}
// Connect registers a route for CONNECT methods.
// The handler only executes when the request hostname matches the domain pattern.
func (d *domainRouter) Connect(path string, handler any, handlers ...any) Router {
return d.Add([]string{MethodConnect}, path, handler, handlers...)
}
// Options registers a route for OPTIONS methods.
// The handler only executes when the request hostname matches the domain pattern.
func (d *domainRouter) Options(path string, handler any, handlers ...any) Router {
return d.Add([]string{MethodOptions}, path, handler, handlers...)
}
// Trace registers a route for TRACE methods.
// The handler only executes when the request hostname matches the domain pattern.
func (d *domainRouter) Trace(path string, handler any, handlers ...any) Router {
return d.Add([]string{MethodTrace}, path, handler, handlers...)
}
// Patch registers a route for PATCH methods.
// The handler only executes when the request hostname matches the domain pattern.
func (d *domainRouter) Patch(path string, handler any, handlers ...any) Router {
return d.Add([]string{MethodPatch}, path, handler, handlers...)
}
// Add allows you to specify multiple HTTP methods to register a route.
// The handler only executes when the request hostname matches the domain pattern.
func (d *domainRouter) Add(methods []string, path string, handler any, handlers ...any) Router {
converted := collectHandlers("domain", append([]any{handler}, handlers...)...)
wrapped := d.wrapHandlers(converted)
d.app.register(methods, d.registerPath(path), d.registerGroup(), wrapped...)
// Mark the underlying group so Name() can distinguish between
// group-name-prefix calls (before routes) and route-name calls (after routes).
if d.group != nil && !d.group.anyRouteDefined {
d.group.anyRouteDefined = true
}
return d
}
// All registers the handler on all HTTP methods.
// The handler only executes when the request hostname matches the domain pattern.
func (d *domainRouter) All(path string, handler any, handlers ...any) Router {
return d.Add(d.app.config.RequestMethods, path, handler, handlers...)
}
// Group creates a new sub-router with a common prefix, scoped to the domain pattern.
// Routes registered through the returned Router also inherit the domain filter.
func (d *domainRouter) Group(prefix string, handlers ...any) Router {
fullPrefix := d.registerPath(prefix)
if len(handlers) > 0 {
converted := collectHandlers("domain", handlers...)
wrapped := d.wrapHandlers(converted)
d.app.register([]string{methodUse}, fullPrefix, d.registerGroup(), wrapped...)
}
// Create a new group on the app
newGrp := &Group{Prefix: fullPrefix, app: d.app, parentGroup: d.group}
if err := d.app.hooks.executeOnGroupHooks(*newGrp); err != nil {
panic(err)
}
return &domainRouter{
app: d.app,
group: newGrp,
matcher: d.matcher,
}
}
// RouteChain creates a Registering instance for the domain router.
func (d *domainRouter) RouteChain(path string) Register {
return &domainRegistering{
domain: d,
path: d.registerPath(path),
}
}
// Route defines routes with a common prefix inside the supplied function,
// scoped to the domain pattern.
func (d *domainRouter) Route(prefix string, fn func(router Router), name ...string) Router {
if fn == nil {
panic("route handler 'fn' cannot be nil")
}
group := d.Group(prefix)
if len(name) > 0 {
group.Name(name[0])
}
fn(group)
return group
}
// Name assigns a name to the most recently registered route.
// When the domain router was created from a Group, this delegates to the
// group's Name method so that group name prefixes are applied correctly.
func (d *domainRouter) Name(name string) Router {
if d.group != nil {
d.group.Name(name)
} else {
d.app.Name(name)
}
return d
}
// Domain creates a new domain router that inherits this domain router's
// group (if any) but uses a different hostname pattern.
func (d *domainRouter) Domain(host string) Router {
return &domainRouter{
app: d.app,
group: d.group,
matcher: parseDomainPattern(host),
}
}
// domainRegistering provides route registration helpers for a specific path
// on a domain router, implementing the [Register] interface.
type domainRegistering struct {
domain *domainRouter
path string
}
// Verify domainRegistering implements Register at compile time.
var _ Register = (*domainRegistering)(nil)
func (r *domainRegistering) All(handler any, handlers ...any) Register {
converted := collectHandlers("domain", append([]any{handler}, handlers...)...)
wrapped := r.domain.wrapHandlers(converted)
r.domain.app.register([]string{methodUse}, r.path, r.domain.registerGroup(), wrapped...)
return r
}
func (r *domainRegistering) Get(handler any, handlers ...any) Register {
return r.Add([]string{MethodGet}, handler, handlers...)
}
func (r *domainRegistering) Head(handler any, handlers ...any) Register {
return r.Add([]string{MethodHead}, handler, handlers...)
}
func (r *domainRegistering) Post(handler any, handlers ...any) Register {
return r.Add([]string{MethodPost}, handler, handlers...)
}
func (r *domainRegistering) Put(handler any, handlers ...any) Register {
return r.Add([]string{MethodPut}, handler, handlers...)
}
func (r *domainRegistering) Delete(handler any, handlers ...any) Register {
return r.Add([]string{MethodDelete}, handler, handlers...)
}
func (r *domainRegistering) Connect(handler any, handlers ...any) Register {
return r.Add([]string{MethodConnect}, handler, handlers...)
}
func (r *domainRegistering) Options(handler any, handlers ...any) Register {
return r.Add([]string{MethodOptions}, handler, handlers...)
}
func (r *domainRegistering) Trace(handler any, handlers ...any) Register {
return r.Add([]string{MethodTrace}, handler, handlers...)
}
func (r *domainRegistering) Patch(handler any, handlers ...any) Register {
return r.Add([]string{MethodPatch}, handler, handlers...)
}
func (r *domainRegistering) Add(methods []string, handler any, handlers ...any) Register {
converted := collectHandlers("domain", append([]any{handler}, handlers...)...)
wrapped := r.domain.wrapHandlers(converted)
r.domain.app.register(methods, r.path, r.domain.registerGroup(), wrapped...)
return r
}
func (r *domainRegistering) RouteChain(path string) Register {
return &domainRegistering{
domain: r.domain,
path: getGroupPath(r.path, path),
}
}