Files
netbird/proxy/internal/proxy/servicemapping.go

124 lines
3.3 KiB
Go

package proxy
import (
"net"
"net/http"
"net/url"
"sort"
"strings"
"time"
"github.com/netbirdio/netbird/proxy/internal/types"
)
// PathRewriteMode controls how the request path is rewritten before forwarding.
type PathRewriteMode int
const (
// PathRewriteDefault strips the matched prefix and joins with the target path.
PathRewriteDefault PathRewriteMode = iota
// PathRewritePreserve keeps the full original request path as-is.
PathRewritePreserve
)
// PathTarget holds a backend URL and per-target behavioral options.
type PathTarget struct {
URL *url.URL
SkipTLSVerify bool
RequestTimeout time.Duration
PathRewrite PathRewriteMode
CustomHeaders map[string]string
}
// Mapping describes how a domain is routed by the HTTP reverse proxy.
type Mapping struct {
ID types.ServiceID
AccountID types.AccountID
Host string
Paths map[string]*PathTarget
PassHostHeader bool
RewriteRedirects bool
// StripAuthHeaders are header names used for header-based auth.
// These headers are stripped from requests before forwarding.
StripAuthHeaders []string
// sortedPaths caches the paths sorted by length (longest first).
sortedPaths []string
}
type targetResult struct {
target *PathTarget
matchedPath string
serviceID types.ServiceID
accountID types.AccountID
passHostHeader bool
rewriteRedirects bool
stripAuthHeaders []string
}
func (p *ReverseProxy) findTargetForRequest(req *http.Request) (targetResult, bool) {
p.mappingsMux.RLock()
defer p.mappingsMux.RUnlock()
// Strip port from host if present (e.g., "external.test:8443" -> "external.test")
host := req.Host
if h, _, err := net.SplitHostPort(host); err == nil {
host = h
}
m, exists := p.mappings[host]
if !exists {
p.logger.Debugf("no mapping found for host: %s", host)
return targetResult{}, false
}
for _, path := range m.sortedPaths {
if strings.HasPrefix(req.URL.Path, path) {
pt := m.Paths[path]
if pt == nil || pt.URL == nil {
p.logger.Warnf("invalid mapping for host: %s, path: %s (nil target)", host, path)
continue
}
p.logger.Debugf("matched host: %s, path: %s -> %s", host, path, pt.URL)
return targetResult{
target: pt,
matchedPath: path,
serviceID: m.ID,
accountID: m.AccountID,
passHostHeader: m.PassHostHeader,
rewriteRedirects: m.RewriteRedirects,
stripAuthHeaders: m.StripAuthHeaders,
}, true
}
}
p.logger.Debugf("no path match for host: %s, path: %s", host, req.URL.Path)
return targetResult{}, false
}
// AddMapping registers a host-to-backend mapping for the reverse proxy.
func (p *ReverseProxy) AddMapping(m Mapping) {
// Sort paths longest-first to match the most specific route first.
paths := make([]string, 0, len(m.Paths))
for path := range m.Paths {
paths = append(paths, path)
}
sort.Slice(paths, func(i, j int) bool {
return len(paths[i]) > len(paths[j])
})
m.sortedPaths = paths
p.mappingsMux.Lock()
defer p.mappingsMux.Unlock()
p.mappings[m.Host] = m
}
// RemoveMapping removes the mapping for the given host and reports whether it existed.
func (p *ReverseProxy) RemoveMapping(m Mapping) bool {
p.mappingsMux.Lock()
defer p.mappingsMux.Unlock()
if _, ok := p.mappings[m.Host]; !ok {
return false
}
delete(p.mappings, m.Host)
return true
}