mirror of
https://github.com/caddyserver/caddy.git
synced 2025-12-08 06:09:53 +00:00
Merge 2500b122fa into 31960dc998
This commit is contained in:
commit
39d7d01b99
15 changed files with 699 additions and 468 deletions
4
admin.go
4
admin.go
|
|
@ -222,7 +222,7 @@ func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool, _ Co
|
||||||
muxWrap.remoteControl = admin.Remote
|
muxWrap.remoteControl = admin.Remote
|
||||||
} else {
|
} else {
|
||||||
// see comment in allowedOrigins() as to why we disable the host check for unix/fd networks
|
// see comment in allowedOrigins() as to why we disable the host check for unix/fd networks
|
||||||
muxWrap.enforceHost = !addr.isWildcardInterface() && !addr.IsUnixNetwork() && !addr.IsFdNetwork()
|
muxWrap.enforceHost = !addr.isWildcardInterface() && !addr.IsUnixNetwork() && !addr.IsFDNetwork()
|
||||||
muxWrap.allowedOrigins = admin.allowedOrigins(addr)
|
muxWrap.allowedOrigins = admin.allowedOrigins(addr)
|
||||||
muxWrap.enforceOrigin = admin.EnforceOrigin
|
muxWrap.enforceOrigin = admin.EnforceOrigin
|
||||||
}
|
}
|
||||||
|
|
@ -342,7 +342,7 @@ func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []*url.URL {
|
||||||
// and a false sense of security.
|
// and a false sense of security.
|
||||||
//
|
//
|
||||||
// See also the discussion in #6832.
|
// See also the discussion in #6832.
|
||||||
if admin.Origins == nil && !addr.IsUnixNetwork() && !addr.IsFdNetwork() {
|
if admin.Origins == nil && !addr.IsUnixNetwork() && !addr.IsFDNetwork() {
|
||||||
if addr.isLoopback() {
|
if addr.isLoopback() {
|
||||||
uniqueOrigins[net.JoinHostPort("localhost", addr.port())] = struct{}{}
|
uniqueOrigins[net.JoinHostPort("localhost", addr.port())] = struct{}{}
|
||||||
uniqueOrigins[net.JoinHostPort("::1", addr.port())] = struct{}{}
|
uniqueOrigins[net.JoinHostPort("::1", addr.port())] = struct{}{}
|
||||||
|
|
|
||||||
|
|
@ -729,7 +729,7 @@ func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
parsedAddr.Host = addr
|
parsedAddr.Host = addr
|
||||||
} else if parsedAddr.IsFdNetwork() {
|
} else if parsedAddr.IsFDNetwork() {
|
||||||
origin = "http://127.0.0.1"
|
origin = "http://127.0.0.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -738,7 +738,7 @@ func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("making request: %v", err)
|
return nil, fmt.Errorf("making request: %v", err)
|
||||||
}
|
}
|
||||||
if parsedAddr.IsUnixNetwork() || parsedAddr.IsFdNetwork() {
|
if parsedAddr.IsUnixNetwork() || parsedAddr.IsFDNetwork() {
|
||||||
// We used to conform to RFC 2616 Section 14.26 which requires
|
// We used to conform to RFC 2616 Section 14.26 which requires
|
||||||
// an empty host header when there is no host, as is the case
|
// an empty host header when there is no host, as is the case
|
||||||
// with unix sockets and socket fds. However, Go required a
|
// with unix sockets and socket fds. However, Go required a
|
||||||
|
|
|
||||||
206
listeners.go
206
listeners.go
|
|
@ -38,10 +38,6 @@ import (
|
||||||
"github.com/caddyserver/caddy/v2/internal"
|
"github.com/caddyserver/caddy/v2/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
// listenFdsStart is the first file descriptor number for systemd socket activation.
|
|
||||||
// File descriptors 0, 1, 2 are reserved for stdin, stdout, stderr.
|
|
||||||
const listenFdsStart = 3
|
|
||||||
|
|
||||||
// NetworkAddress represents one or more network addresses.
|
// NetworkAddress represents one or more network addresses.
|
||||||
// It contains the individual components for a parsed network
|
// It contains the individual components for a parsed network
|
||||||
// address of the form accepted by ParseNetworkAddress().
|
// address of the form accepted by ParseNetworkAddress().
|
||||||
|
|
@ -137,42 +133,45 @@ func (na NetworkAddress) ListenAll(ctx context.Context, config net.ListenConfig)
|
||||||
// Listen synchronizes binds to unix domain sockets to avoid race conditions
|
// Listen synchronizes binds to unix domain sockets to avoid race conditions
|
||||||
// while an existing socket is unlinked.
|
// while an existing socket is unlinked.
|
||||||
func (na NetworkAddress) Listen(ctx context.Context, portOffset uint, config net.ListenConfig) (any, error) {
|
func (na NetworkAddress) Listen(ctx context.Context, portOffset uint, config net.ListenConfig) (any, error) {
|
||||||
if na.IsUnixNetwork() {
|
var (
|
||||||
unixSocketsMu.Lock()
|
ln any
|
||||||
defer unixSocketsMu.Unlock()
|
err error
|
||||||
}
|
)
|
||||||
|
|
||||||
// check to see if plugin provides listener
|
// check to see if plugin provides a listener
|
||||||
if ln, err := getListenerFromPlugin(ctx, na.Network, na.Host, na.port(), portOffset, config); ln != nil || err != nil {
|
if ln, err = getListenerFromPlugin(ctx, na.Network, na.Host, na.port(), portOffset, config); ln != nil || err != nil {
|
||||||
return ln, err
|
return ln, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// create (or reuse) the listener ourselves
|
// create (or reuse) the listener ourselves
|
||||||
return na.listen(ctx, portOffset, config)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (na NetworkAddress) listen(ctx context.Context, portOffset uint, config net.ListenConfig) (any, error) {
|
|
||||||
var (
|
var (
|
||||||
ln any
|
|
||||||
err error
|
|
||||||
address string
|
address string
|
||||||
unixFileMode fs.FileMode
|
unixFileMode fs.FileMode
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// lock other unix sockets from being bound and
|
||||||
// split unix socket addr early so lnKey
|
// split unix socket addr early so lnKey
|
||||||
// is independent of permissions bits
|
// is independent of permissions bits
|
||||||
if na.IsUnixNetwork() {
|
if na.IsUnixNetwork() {
|
||||||
|
unixSocketsMu.Lock()
|
||||||
|
defer unixSocketsMu.Unlock()
|
||||||
|
|
||||||
address, unixFileMode, err = internal.SplitUnixSocketPermissionsBits(na.Host)
|
address, unixFileMode, err = internal.SplitUnixSocketPermissionsBits(na.Host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
} else if na.IsFdNetwork() {
|
} else if na.IsFDNetwork() {
|
||||||
address = na.Host
|
socketFd, err := strconv.ParseUint(na.Host, 0, strconv.IntSize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid file descriptor: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
address = strconv.FormatUint(uint64(uint(socketFd)+portOffset), 10)
|
||||||
} else {
|
} else {
|
||||||
address = na.JoinHostPort(portOffset)
|
address = na.JoinHostPort(portOffset)
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(na.Network, "ip") {
|
if na.IsIPNetwork() {
|
||||||
ln, err = config.ListenPacket(ctx, na.Network, address)
|
ln, err = config.ListenPacket(ctx, na.Network, address)
|
||||||
} else {
|
} else {
|
||||||
if na.IsUnixNetwork() {
|
if na.IsUnixNetwork() {
|
||||||
|
|
@ -209,21 +208,39 @@ func (na NetworkAddress) listen(ctx context.Context, portOffset uint, config net
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsUnixNetwork returns true if na.Network is
|
// IsUnixNetwork returns true if na.Network is
|
||||||
// unix, unixgram, or unixpacket.
|
// unix, unixgram, unixpacket, or unix+h2c.
|
||||||
func (na NetworkAddress) IsUnixNetwork() bool {
|
func (na NetworkAddress) IsUnixNetwork() bool {
|
||||||
return IsUnixNetwork(na.Network)
|
return IsUnixNetwork(na.Network)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsFdNetwork returns true if na.Network is
|
// IsTCPNetwork returns true if na.Network is
|
||||||
|
// tcp, tcp4, or tcp6.
|
||||||
|
func (na NetworkAddress) IsTCPNetwork() bool {
|
||||||
|
return IsTCPNetwork(na.Network)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsUDPNetwork returns true if na.Network is
|
||||||
|
// udp, udp4, or udp6.
|
||||||
|
func (na NetworkAddress) IsUDPNetwork() bool {
|
||||||
|
return IsUDPNetwork(na.Network)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsIPNetwork returns true if na.Network starts with
|
||||||
|
// ip: ip4: or ip6:
|
||||||
|
func (na NetworkAddress) IsIPNetwork() bool {
|
||||||
|
return IsIPNetwork(na.Network)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsFDNetwork returns true if na.Network is
|
||||||
// fd or fdgram.
|
// fd or fdgram.
|
||||||
func (na NetworkAddress) IsFdNetwork() bool {
|
func (na NetworkAddress) IsFDNetwork() bool {
|
||||||
return IsFdNetwork(na.Network)
|
return IsFDNetwork(na.Network)
|
||||||
}
|
}
|
||||||
|
|
||||||
// JoinHostPort is like net.JoinHostPort, but where the port
|
// JoinHostPort is like net.JoinHostPort, but where the port
|
||||||
// is StartPort + offset.
|
// is StartPort + offset.
|
||||||
func (na NetworkAddress) JoinHostPort(offset uint) string {
|
func (na NetworkAddress) JoinHostPort(offset uint) string {
|
||||||
if na.IsUnixNetwork() || na.IsFdNetwork() {
|
if na.IsUnixNetwork() || na.IsFDNetwork() {
|
||||||
return na.Host
|
return na.Host
|
||||||
}
|
}
|
||||||
return net.JoinHostPort(na.Host, strconv.FormatUint(uint64(na.StartPort+offset), 10))
|
return net.JoinHostPort(na.Host, strconv.FormatUint(uint64(na.StartPort+offset), 10))
|
||||||
|
|
@ -260,7 +277,7 @@ func (na NetworkAddress) PortRangeSize() uint {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (na NetworkAddress) isLoopback() bool {
|
func (na NetworkAddress) isLoopback() bool {
|
||||||
if na.IsUnixNetwork() || na.IsFdNetwork() {
|
if na.IsUnixNetwork() || na.IsFDNetwork() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if na.Host == "localhost" {
|
if na.Host == "localhost" {
|
||||||
|
|
@ -293,80 +310,12 @@ func (na NetworkAddress) port() string {
|
||||||
// The output can be parsed by ParseNetworkAddress(). If the
|
// The output can be parsed by ParseNetworkAddress(). If the
|
||||||
// address is a unix socket, any non-zero port will be dropped.
|
// address is a unix socket, any non-zero port will be dropped.
|
||||||
func (na NetworkAddress) String() string {
|
func (na NetworkAddress) String() string {
|
||||||
if na.Network == "tcp" && (na.Host != "" || na.port() != "") {
|
if na.Network == TCP && (na.Host != "" || na.port() != "") {
|
||||||
na.Network = "" // omit default network value for brevity
|
na.Network = "" // omit default network value for brevity
|
||||||
}
|
}
|
||||||
return JoinNetworkAddress(na.Network, na.Host, na.port())
|
return JoinNetworkAddress(na.Network, na.Host, na.port())
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsUnixNetwork returns true if the netw is a unix network.
|
|
||||||
func IsUnixNetwork(netw string) bool {
|
|
||||||
return strings.HasPrefix(netw, "unix")
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsFdNetwork returns true if the netw is a fd network.
|
|
||||||
func IsFdNetwork(netw string) bool {
|
|
||||||
return strings.HasPrefix(netw, "fd")
|
|
||||||
}
|
|
||||||
|
|
||||||
// getFdByName returns the file descriptor number for the given
|
|
||||||
// socket name from systemd's LISTEN_FDNAMES environment variable.
|
|
||||||
// Socket names are provided by systemd via socket activation.
|
|
||||||
//
|
|
||||||
// The name can optionally include an index to handle multiple sockets
|
|
||||||
// with the same name: "web:0" for first, "web:1" for second, etc.
|
|
||||||
// If no index is specified, defaults to index 0 (first occurrence).
|
|
||||||
func getFdByName(nameWithIndex string) (int, error) {
|
|
||||||
if nameWithIndex == "" {
|
|
||||||
return 0, fmt.Errorf("socket name cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
fdNamesStr := os.Getenv("LISTEN_FDNAMES")
|
|
||||||
if fdNamesStr == "" {
|
|
||||||
return 0, fmt.Errorf("LISTEN_FDNAMES environment variable not set")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse name and optional index
|
|
||||||
parts := strings.Split(nameWithIndex, ":")
|
|
||||||
if len(parts) > 2 {
|
|
||||||
return 0, fmt.Errorf("invalid socket name format '%s': too many colons", nameWithIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
name := parts[0]
|
|
||||||
targetIndex := 0
|
|
||||||
|
|
||||||
if len(parts) > 1 {
|
|
||||||
var err error
|
|
||||||
targetIndex, err = strconv.Atoi(parts[1])
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("invalid socket index '%s': %v", parts[1], err)
|
|
||||||
}
|
|
||||||
if targetIndex < 0 {
|
|
||||||
return 0, fmt.Errorf("socket index cannot be negative: %d", targetIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the socket names
|
|
||||||
names := strings.Split(fdNamesStr, ":")
|
|
||||||
|
|
||||||
// Find the Nth occurrence of the requested name
|
|
||||||
matchCount := 0
|
|
||||||
for i, fdName := range names {
|
|
||||||
if fdName == name {
|
|
||||||
if matchCount == targetIndex {
|
|
||||||
return listenFdsStart + i, nil
|
|
||||||
}
|
|
||||||
matchCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if matchCount == 0 {
|
|
||||||
return 0, fmt.Errorf("socket name '%s' not found in LISTEN_FDNAMES", name)
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0, fmt.Errorf("socket name '%s' found %d times, but index %d requested", name, matchCount, targetIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseNetworkAddress parses addr into its individual
|
// ParseNetworkAddress parses addr into its individual
|
||||||
// components. The input string is expected to be of
|
// components. The input string is expected to be of
|
||||||
// the form "network/host:port-range" where any part is
|
// the form "network/host:port-range" where any part is
|
||||||
|
|
@ -397,28 +346,10 @@ func ParseNetworkAddressWithDefaults(addr, defaultNetwork string, defaultPort ui
|
||||||
Host: host,
|
Host: host,
|
||||||
}, err
|
}, err
|
||||||
}
|
}
|
||||||
if IsFdNetwork(network) {
|
if IsFDNetwork(network) {
|
||||||
fdAddr := host
|
|
||||||
|
|
||||||
// Handle named socket activation (fdname/name, fdgramname/name)
|
|
||||||
if strings.HasPrefix(network, "fdname") || strings.HasPrefix(network, "fdgramname") {
|
|
||||||
fdNum, err := getFdByName(host)
|
|
||||||
if err != nil {
|
|
||||||
return NetworkAddress{}, fmt.Errorf("named socket activation: %v", err)
|
|
||||||
}
|
|
||||||
fdAddr = strconv.Itoa(fdNum)
|
|
||||||
|
|
||||||
// Normalize network to standard fd/fdgram
|
|
||||||
if strings.HasPrefix(network, "fdname") {
|
|
||||||
network = "fd"
|
|
||||||
} else {
|
|
||||||
network = "fdgram"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return NetworkAddress{
|
return NetworkAddress{
|
||||||
Network: network,
|
Network: network,
|
||||||
Host: fdAddr,
|
Host: host,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
var start, end uint64
|
var start, end uint64
|
||||||
|
|
@ -460,7 +391,7 @@ func SplitNetworkAddress(a string) (network, host, port string, err error) {
|
||||||
if slashFound {
|
if slashFound {
|
||||||
network = strings.ToLower(strings.TrimSpace(beforeSlash))
|
network = strings.ToLower(strings.TrimSpace(beforeSlash))
|
||||||
a = afterSlash
|
a = afterSlash
|
||||||
if IsUnixNetwork(network) || IsFdNetwork(network) {
|
if IsUnixNetwork(network) || IsFDNetwork(network) {
|
||||||
host = a
|
host = a
|
||||||
return network, host, port, err
|
return network, host, port, err
|
||||||
}
|
}
|
||||||
|
|
@ -495,7 +426,7 @@ func JoinNetworkAddress(network, host, port string) string {
|
||||||
if network != "" {
|
if network != "" {
|
||||||
a = network + "/"
|
a = network + "/"
|
||||||
}
|
}
|
||||||
if (host != "" && port == "") || IsUnixNetwork(network) || IsFdNetwork(network) {
|
if (host != "" && port == "") || IsUnixNetwork(network) || IsFDNetwork(network) {
|
||||||
a += host
|
a += host
|
||||||
} else if port != "" {
|
} else if port != "" {
|
||||||
a += net.JoinHostPort(host, port)
|
a += net.JoinHostPort(host, port)
|
||||||
|
|
@ -720,55 +651,12 @@ func (fcql *fakeCloseQuicListener) Close() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterNetwork registers a network type with Caddy so that if a listener is
|
|
||||||
// created for that network type, getListener will be invoked to get the listener.
|
|
||||||
// This should be called during init() and will panic if the network type is standard
|
|
||||||
// or reserved, or if it is already registered. EXPERIMENTAL and subject to change.
|
|
||||||
func RegisterNetwork(network string, getListener ListenerFunc) {
|
|
||||||
network = strings.TrimSpace(strings.ToLower(network))
|
|
||||||
|
|
||||||
if network == "tcp" || network == "tcp4" || network == "tcp6" ||
|
|
||||||
network == "udp" || network == "udp4" || network == "udp6" ||
|
|
||||||
network == "unix" || network == "unixpacket" || network == "unixgram" ||
|
|
||||||
strings.HasPrefix(network, "ip:") || strings.HasPrefix(network, "ip4:") || strings.HasPrefix(network, "ip6:") ||
|
|
||||||
network == "fd" || network == "fdgram" {
|
|
||||||
panic("network type " + network + " is reserved")
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := networkTypes[strings.ToLower(network)]; ok {
|
|
||||||
panic("network type " + network + " is already registered")
|
|
||||||
}
|
|
||||||
|
|
||||||
networkTypes[network] = getListener
|
|
||||||
}
|
|
||||||
|
|
||||||
var unixSocketsMu sync.Mutex
|
var unixSocketsMu sync.Mutex
|
||||||
|
|
||||||
// getListenerFromPlugin returns a listener on the given network and address
|
|
||||||
// if a plugin has registered the network name. It may return (nil, nil) if
|
|
||||||
// no plugin can provide a listener.
|
|
||||||
func getListenerFromPlugin(ctx context.Context, network, host, port string, portOffset uint, config net.ListenConfig) (any, error) {
|
|
||||||
// get listener from plugin if network type is registered
|
|
||||||
if getListener, ok := networkTypes[network]; ok {
|
|
||||||
Log().Debug("getting listener from plugin", zap.String("network", network))
|
|
||||||
return getListener(ctx, network, host, port, portOffset, config)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func listenerKey(network, addr string) string {
|
func listenerKey(network, addr string) string {
|
||||||
return network + "/" + addr
|
return network + "/" + addr
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListenerFunc is a function that can return a listener given a network and address.
|
|
||||||
// The listeners must be capable of overlapping: with Caddy, new configs are loaded
|
|
||||||
// before old ones are unloaded, so listeners may overlap briefly if the configs
|
|
||||||
// both need the same listener. EXPERIMENTAL and subject to change.
|
|
||||||
type ListenerFunc func(ctx context.Context, network, host, portRange string, portOffset uint, cfg net.ListenConfig) (any, error)
|
|
||||||
|
|
||||||
var networkTypes = map[string]ListenerFunc{}
|
|
||||||
|
|
||||||
// ListenerWrapper is a type that wraps a listener
|
// ListenerWrapper is a type that wraps a listener
|
||||||
// so it can modify the input listener's methods.
|
// so it can modify the input listener's methods.
|
||||||
// Modules that implement this interface are found
|
// Modules that implement this interface are found
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@
|
||||||
package caddy
|
package caddy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
|
@ -653,286 +652,3 @@ func TestSplitUnixSocketPermissionsBits(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestGetFdByName tests the getFdByName function for systemd socket activation.
|
|
||||||
func TestGetFdByName(t *testing.T) {
|
|
||||||
// Save original environment
|
|
||||||
originalFdNames := os.Getenv("LISTEN_FDNAMES")
|
|
||||||
|
|
||||||
// Restore environment after test
|
|
||||||
defer func() {
|
|
||||||
if originalFdNames != "" {
|
|
||||||
os.Setenv("LISTEN_FDNAMES", originalFdNames)
|
|
||||||
} else {
|
|
||||||
os.Unsetenv("LISTEN_FDNAMES")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fdNames string
|
|
||||||
socketName string
|
|
||||||
expectedFd int
|
|
||||||
expectError bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "simple http socket",
|
|
||||||
fdNames: "http",
|
|
||||||
socketName: "http",
|
|
||||||
expectedFd: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multiple different sockets - first",
|
|
||||||
fdNames: "http:https:dns",
|
|
||||||
socketName: "http",
|
|
||||||
expectedFd: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multiple different sockets - second",
|
|
||||||
fdNames: "http:https:dns",
|
|
||||||
socketName: "https",
|
|
||||||
expectedFd: 4,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multiple different sockets - third",
|
|
||||||
fdNames: "http:https:dns",
|
|
||||||
socketName: "dns",
|
|
||||||
expectedFd: 5,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "duplicate names - first occurrence (no index)",
|
|
||||||
fdNames: "web:web:api",
|
|
||||||
socketName: "web",
|
|
||||||
expectedFd: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "duplicate names - first occurrence (explicit index 0)",
|
|
||||||
fdNames: "web:web:api",
|
|
||||||
socketName: "web:0",
|
|
||||||
expectedFd: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "duplicate names - second occurrence (index 1)",
|
|
||||||
fdNames: "web:web:api",
|
|
||||||
socketName: "web:1",
|
|
||||||
expectedFd: 4,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "complex duplicates - first api",
|
|
||||||
fdNames: "web:api:web:api:dns",
|
|
||||||
socketName: "api:0",
|
|
||||||
expectedFd: 4,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "complex duplicates - second api",
|
|
||||||
fdNames: "web:api:web:api:dns",
|
|
||||||
socketName: "api:1",
|
|
||||||
expectedFd: 6,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "complex duplicates - first web",
|
|
||||||
fdNames: "web:api:web:api:dns",
|
|
||||||
socketName: "web:0",
|
|
||||||
expectedFd: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "complex duplicates - second web",
|
|
||||||
fdNames: "web:api:web:api:dns",
|
|
||||||
socketName: "web:1",
|
|
||||||
expectedFd: 5,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "socket not found",
|
|
||||||
fdNames: "http:https",
|
|
||||||
socketName: "missing",
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty socket name",
|
|
||||||
fdNames: "http",
|
|
||||||
socketName: "",
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing LISTEN_FDNAMES",
|
|
||||||
fdNames: "",
|
|
||||||
socketName: "http",
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "index out of range",
|
|
||||||
fdNames: "web:web",
|
|
||||||
socketName: "web:2",
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "negative index",
|
|
||||||
fdNames: "web",
|
|
||||||
socketName: "web:-1",
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid index format",
|
|
||||||
fdNames: "web",
|
|
||||||
socketName: "web:abc",
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "too many colons",
|
|
||||||
fdNames: "web",
|
|
||||||
socketName: "web:0:extra",
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
// Set up environment
|
|
||||||
if tc.fdNames != "" {
|
|
||||||
os.Setenv("LISTEN_FDNAMES", tc.fdNames)
|
|
||||||
} else {
|
|
||||||
os.Unsetenv("LISTEN_FDNAMES")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test the function
|
|
||||||
fd, err := getFdByName(tc.socketName)
|
|
||||||
|
|
||||||
if tc.expectError {
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("Expected error but got none")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Expected no error but got: %v", err)
|
|
||||||
}
|
|
||||||
if fd != tc.expectedFd {
|
|
||||||
t.Errorf("Expected FD %d but got %d", tc.expectedFd, fd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestParseNetworkAddressFdName tests parsing of fdname and fdgramname addresses.
|
|
||||||
func TestParseNetworkAddressFdName(t *testing.T) {
|
|
||||||
// Save and restore environment
|
|
||||||
originalFdNames := os.Getenv("LISTEN_FDNAMES")
|
|
||||||
defer func() {
|
|
||||||
if originalFdNames != "" {
|
|
||||||
os.Setenv("LISTEN_FDNAMES", originalFdNames)
|
|
||||||
} else {
|
|
||||||
os.Unsetenv("LISTEN_FDNAMES")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Set up test environment
|
|
||||||
os.Setenv("LISTEN_FDNAMES", "http:https:dns")
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
input string
|
|
||||||
expectAddr NetworkAddress
|
|
||||||
expectErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
input: "fdname/http",
|
|
||||||
expectAddr: NetworkAddress{
|
|
||||||
Network: "fd",
|
|
||||||
Host: "3",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "fdname/https",
|
|
||||||
expectAddr: NetworkAddress{
|
|
||||||
Network: "fd",
|
|
||||||
Host: "4",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "fdname/dns",
|
|
||||||
expectAddr: NetworkAddress{
|
|
||||||
Network: "fd",
|
|
||||||
Host: "5",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "fdname/http:0",
|
|
||||||
expectAddr: NetworkAddress{
|
|
||||||
Network: "fd",
|
|
||||||
Host: "3",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "fdname/https:0",
|
|
||||||
expectAddr: NetworkAddress{
|
|
||||||
Network: "fd",
|
|
||||||
Host: "4",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "fdgramname/http",
|
|
||||||
expectAddr: NetworkAddress{
|
|
||||||
Network: "fdgram",
|
|
||||||
Host: "3",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "fdgramname/https",
|
|
||||||
expectAddr: NetworkAddress{
|
|
||||||
Network: "fdgram",
|
|
||||||
Host: "4",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "fdgramname/http:0",
|
|
||||||
expectAddr: NetworkAddress{
|
|
||||||
Network: "fdgram",
|
|
||||||
Host: "3",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "fdname/nonexistent",
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "fdgramname/nonexistent",
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "fdname/http:99",
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "fdname/invalid:abc",
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
// Test that old fd/N syntax still works
|
|
||||||
{
|
|
||||||
input: "fd/7",
|
|
||||||
expectAddr: NetworkAddress{
|
|
||||||
Network: "fd",
|
|
||||||
Host: "7",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "fdgram/8",
|
|
||||||
expectAddr: NetworkAddress{
|
|
||||||
Network: "fdgram",
|
|
||||||
Host: "8",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, tc := range tests {
|
|
||||||
actualAddr, err := ParseNetworkAddress(tc.input)
|
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
|
||||||
t.Errorf("Test %d (%s): Expected error but got none", i, tc.input)
|
|
||||||
}
|
|
||||||
if !tc.expectErr && err != nil {
|
|
||||||
t.Errorf("Test %d (%s): Expected no error but got: %v", i, tc.input, err)
|
|
||||||
}
|
|
||||||
if !tc.expectErr && !reflect.DeepEqual(tc.expectAddr, actualAddr) {
|
|
||||||
t.Errorf("Test %d (%s): Expected %+v but got %+v", i, tc.input, tc.expectAddr, actualAddr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -604,7 +604,7 @@ func (app *App) Start() error {
|
||||||
|
|
||||||
// if binding to port 0, the OS chooses a port for us;
|
// if binding to port 0, the OS chooses a port for us;
|
||||||
// but the user won't know the port unless we print it
|
// but the user won't know the port unless we print it
|
||||||
if !listenAddr.IsUnixNetwork() && !listenAddr.IsFdNetwork() && listenAddr.StartPort == 0 && listenAddr.EndPort == 0 {
|
if !listenAddr.IsUnixNetwork() && !listenAddr.IsFDNetwork() && listenAddr.StartPort == 0 && listenAddr.EndPort == 0 {
|
||||||
app.logger.Info("port 0 listener",
|
app.logger.Info("port 0 listener",
|
||||||
zap.String("input_address", lnAddr),
|
zap.String("input_address", lnAddr),
|
||||||
zap.String("actual_address", ln.Addr().String()))
|
zap.String("actual_address", ln.Addr().String()))
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@ func (pp *ListenerWrapper) Provision(ctx caddy.Context) error {
|
||||||
|
|
||||||
pp.policy = func(options goproxy.ConnPolicyOptions) (goproxy.Policy, error) {
|
pp.policy = func(options goproxy.ConnPolicyOptions) (goproxy.Policy, error) {
|
||||||
// trust unix sockets
|
// trust unix sockets
|
||||||
if network := options.Upstream.Network(); caddy.IsUnixNetwork(network) || caddy.IsFdNetwork(network) {
|
if network := options.Upstream.Network(); caddy.IsUnixNetwork(network) || caddy.IsFDNetwork(network) {
|
||||||
return goproxy.USE, nil
|
return goproxy.USE, nil
|
||||||
}
|
}
|
||||||
ret := pp.FallbackPolicy
|
ret := pp.FallbackPolicy
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,7 @@ func parseUpstreamDialAddress(upstreamAddr string) (parsedAddr, error) {
|
||||||
}
|
}
|
||||||
// we can assume a port if only a hostname is specified, but use of a
|
// we can assume a port if only a hostname is specified, but use of a
|
||||||
// placeholder without a port likely means a port will be filled in
|
// placeholder without a port likely means a port will be filled in
|
||||||
if port == "" && !strings.Contains(host, "{") && !caddy.IsUnixNetwork(network) && !caddy.IsFdNetwork(network) {
|
if port == "" && !strings.Contains(host, "{") && !caddy.IsUnixNetwork(network) && !caddy.IsFDNetwork(network) {
|
||||||
port = "80"
|
port = "80"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -331,7 +331,7 @@ func (h *Handler) doActiveHealthCheckForAllHosts() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if hcp := uint(upstream.activeHealthCheckPort); hcp != 0 {
|
if hcp := uint(upstream.activeHealthCheckPort); hcp != 0 {
|
||||||
if addr.IsUnixNetwork() || addr.IsFdNetwork() {
|
if addr.IsUnixNetwork() || addr.IsFDNetwork() {
|
||||||
addr.Network = "tcp" // I guess we just assume TCP since we are using a port??
|
addr.Network = "tcp" // I guess we just assume TCP since we are using a port??
|
||||||
}
|
}
|
||||||
addr.StartPort, addr.EndPort = hcp, hcp
|
addr.StartPort, addr.EndPort = hcp, hcp
|
||||||
|
|
@ -345,7 +345,7 @@ func (h *Handler) doActiveHealthCheckForAllHosts() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
hostAddr := addr.JoinHostPort(0)
|
hostAddr := addr.JoinHostPort(0)
|
||||||
if addr.IsUnixNetwork() || addr.IsFdNetwork() {
|
if addr.IsUnixNetwork() || addr.IsFDNetwork() {
|
||||||
// this will be used as the Host portion of a http.Request URL, and
|
// this will be used as the Host portion of a http.Request URL, and
|
||||||
// paths to socket files would produce an error when creating URL,
|
// paths to socket files would produce an error when creating URL,
|
||||||
// so use a fake Host value instead; unix sockets are usually local
|
// so use a fake Host value instead; unix sockets are usually local
|
||||||
|
|
|
||||||
|
|
@ -382,7 +382,7 @@ func cmdRespond(fl caddycmd.Flags) (int, error) {
|
||||||
return caddy.ExitCodeFailedStartup, err
|
return caddy.ExitCodeFailedStartup, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !listenAddr.IsUnixNetwork() && !listenAddr.IsFdNetwork() {
|
if !listenAddr.IsUnixNetwork() && !listenAddr.IsFDNetwork() {
|
||||||
listenAddrs := make([]string, 0, listenAddr.PortRangeSize())
|
listenAddrs := make([]string, 0, listenAddr.PortRangeSize())
|
||||||
for offset := uint(0); offset < listenAddr.PortRangeSize(); offset++ {
|
for offset := uint(0); offset < listenAddr.PortRangeSize(); offset++ {
|
||||||
listenAddrs = append(listenAddrs, listenAddr.JoinHostPort(offset))
|
listenAddrs = append(listenAddrs, listenAddr.JoinHostPort(offset))
|
||||||
|
|
|
||||||
129
networks.go
Normal file
129
networks.go
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package caddy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
UNIX = "unix"
|
||||||
|
UNIX_H2C = "unix+h2c"
|
||||||
|
UNIXGRAM = "unixgram"
|
||||||
|
UNIXPACKET = "unixpacket"
|
||||||
|
TCP = "tcp"
|
||||||
|
TCP4 = "tcp4"
|
||||||
|
TCP6 = "tcp6"
|
||||||
|
UDP = "udp"
|
||||||
|
UDP4 = "udp4"
|
||||||
|
UDP6 = "udp6"
|
||||||
|
IP_ = "ip:"
|
||||||
|
IP4_ = "ip4:"
|
||||||
|
IP6_ = "ip6:"
|
||||||
|
FD = "fd"
|
||||||
|
FDGRAM = "fdgram"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsUnixNetwork returns true if the netw is a unix network.
|
||||||
|
func IsUnixNetwork(netw string) bool {
|
||||||
|
return netw == UNIX || netw == UNIX_H2C || netw == UNIXGRAM || netw == UNIXPACKET
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsUnixNetwork returns true if the netw is a TCP network.
|
||||||
|
func IsTCPNetwork(netw string) bool {
|
||||||
|
return netw == TCP || netw == TCP4 || netw == TCP6
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsUnixNetwork returns true if the netw is a UDP network.
|
||||||
|
func IsUDPNetwork(netw string) bool {
|
||||||
|
return netw == UDP || netw == UDP4 || netw == UDP6
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsIPNetwork returns true if the netw is an ip network.
|
||||||
|
func IsIPNetwork(netw string) bool {
|
||||||
|
return strings.HasPrefix(netw, IP_) || strings.HasPrefix(netw, IP4_) || strings.HasPrefix(netw, IP6_)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsFDNetwork returns true if the netw is a fd network.
|
||||||
|
func IsFDNetwork(netw string) bool {
|
||||||
|
return netw == FD || netw == FDGRAM
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsReservedNetwork(network string) bool {
|
||||||
|
return IsUnixNetwork(network) ||
|
||||||
|
IsTCPNetwork(network) ||
|
||||||
|
IsUDPNetwork(network) ||
|
||||||
|
IsIPNetwork(network) ||
|
||||||
|
IsFDNetwork(network)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsIPv4Network(netw string) bool {
|
||||||
|
return netw == TCP || netw == TCP4 || netw == UDP || netw == UDP4 || strings.HasPrefix(netw, IP_) || strings.HasPrefix(netw, IP4_)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsIPv6Network(netw string) bool {
|
||||||
|
return netw == TCP || netw == TCP6 || netw == UDP || netw == UDP6 || strings.HasPrefix(netw, IP_) || strings.HasPrefix(netw, IP6_)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsStreamNetwork(netw string) bool {
|
||||||
|
return netw == UNIX || netw == UNIX_H2C || netw == UNIXPACKET || IsTCPNetwork(netw) || netw == FD
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsPacketNetwork(netw string) bool {
|
||||||
|
return netw == UNIXGRAM || IsUDPNetwork(netw) || IsIPNetwork(netw) || netw == FDGRAM
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenerFunc is a function that can return a listener given a network and address.
|
||||||
|
// The listeners must be capable of overlapping: with Caddy, new configs are loaded
|
||||||
|
// before old ones are unloaded, so listeners may overlap briefly if the configs
|
||||||
|
// both need the same listener. EXPERIMENTAL and subject to change.
|
||||||
|
type ListenerFunc func(ctx context.Context, network, host, portRange string, portOffset uint, cfg net.ListenConfig) (any, error)
|
||||||
|
|
||||||
|
var networkPlugins = map[string]ListenerFunc{}
|
||||||
|
|
||||||
|
// RegisterNetwork registers a network plugin with Caddy so that if a listener is
|
||||||
|
// created for that network plugin, getListener will be invoked to get the listener.
|
||||||
|
// This should be called during init() and will panic if the network type is standard
|
||||||
|
// or reserved, or if it is already registered. EXPERIMENTAL and subject to change.
|
||||||
|
func RegisterNetwork(network string, getListener ListenerFunc) {
|
||||||
|
network = strings.TrimSpace(strings.ToLower(network))
|
||||||
|
|
||||||
|
if IsReservedNetwork(network) {
|
||||||
|
panic("network type " + network + " is reserved")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := networkPlugins[strings.ToLower(network)]; ok {
|
||||||
|
panic("network type " + network + " is already registered")
|
||||||
|
}
|
||||||
|
|
||||||
|
networkPlugins[network] = getListener
|
||||||
|
}
|
||||||
|
|
||||||
|
// getListenerFromPlugin returns a listener on the given network and address
|
||||||
|
// if a plugin has registered the network name. It may return (nil, nil) if
|
||||||
|
// no plugin can provide a listener.
|
||||||
|
func getListenerFromPlugin(ctx context.Context, network, host, port string, portOffset uint, config net.ListenConfig) (any, error) {
|
||||||
|
// get listener from plugin if network is registered
|
||||||
|
if getListener, ok := networkPlugins[network]; ok {
|
||||||
|
Log().Debug("getting listener from plugin", zap.String("network", network))
|
||||||
|
return getListener(ctx, network, host, port, portOffset, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
17
replacer.go
17
replacer.go
|
|
@ -36,16 +36,12 @@ func NewReplacer() *Replacer {
|
||||||
static: make(map[string]any),
|
static: make(map[string]any),
|
||||||
mapMutex: &sync.RWMutex{},
|
mapMutex: &sync.RWMutex{},
|
||||||
}
|
}
|
||||||
rep.providers = []replacementProvider{
|
rep.providers = append(globalReplacementProviders, ReplacerFunc(rep.fromStatic))
|
||||||
globalDefaultReplacementProvider{},
|
|
||||||
fileReplacementProvider{},
|
|
||||||
ReplacerFunc(rep.fromStatic),
|
|
||||||
}
|
|
||||||
return rep
|
return rep
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewEmptyReplacer returns a new Replacer,
|
// NewEmptyReplacer returns a new Replacer,
|
||||||
// without the global default replacements.
|
// without the global replacements.
|
||||||
func NewEmptyReplacer() *Replacer {
|
func NewEmptyReplacer() *Replacer {
|
||||||
rep := &Replacer{
|
rep := &Replacer{
|
||||||
static: make(map[string]any),
|
static: make(map[string]any),
|
||||||
|
|
@ -360,12 +356,11 @@ func (f fileReplacementProvider) replace(key string) (any, bool) {
|
||||||
return string(body), true
|
return string(body), true
|
||||||
}
|
}
|
||||||
|
|
||||||
// globalDefaultReplacementProvider handles replacements
|
// defaultReplacementProvider handles replacements
|
||||||
// that can be used in any context, such as system variables,
|
// such as system variables, time, or environment variables.
|
||||||
// time, or environment variables.
|
type defaultReplacementProvider struct{}
|
||||||
type globalDefaultReplacementProvider struct{}
|
|
||||||
|
|
||||||
func (f globalDefaultReplacementProvider) replace(key string) (any, bool) {
|
func (f defaultReplacementProvider) replace(key string) (any, bool) {
|
||||||
// check environment variable
|
// check environment variable
|
||||||
const envPrefix = "env."
|
const envPrefix = "env."
|
||||||
if strings.HasPrefix(key, envPrefix) {
|
if strings.HasPrefix(key, envPrefix) {
|
||||||
|
|
|
||||||
8
replacer_nosystemd.go
Normal file
8
replacer_nosystemd.go
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
//go:build !linux || nosystemd
|
||||||
|
|
||||||
|
package caddy
|
||||||
|
|
||||||
|
var globalReplacementProviders = []replacementProvider{
|
||||||
|
defaultReplacementProvider{},
|
||||||
|
fileReplacementProvider{},
|
||||||
|
}
|
||||||
123
replacer_systemd.go
Normal file
123
replacer_systemd.go
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
//go:build linux && !nosystemd
|
||||||
|
|
||||||
|
package caddy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func sdListenFds() (int, error) {
|
||||||
|
lnPid, ok := os.LookupEnv("LISTEN_PID")
|
||||||
|
if !ok {
|
||||||
|
return 0, errors.New("LISTEN_PID is unset")
|
||||||
|
}
|
||||||
|
|
||||||
|
pid, err := strconv.Atoi(lnPid)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if pid != os.Getpid() {
|
||||||
|
return 0, fmt.Errorf("LISTEN_PID does not match pid: %d != %d", pid, os.Getpid())
|
||||||
|
}
|
||||||
|
|
||||||
|
lnFds, ok := os.LookupEnv("LISTEN_FDS")
|
||||||
|
if !ok {
|
||||||
|
return 0, errors.New("LISTEN_FDS is unset")
|
||||||
|
}
|
||||||
|
|
||||||
|
fds, err := strconv.Atoi(lnFds)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sdListenFdsWithNames() (map[string][]uint, error) {
|
||||||
|
const lnFdsStart = 3
|
||||||
|
|
||||||
|
fds, err := sdListenFds()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
lnFdnames, ok := os.LookupEnv("LISTEN_FDNAMES")
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("LISTEN_FDNAMES is unset")
|
||||||
|
}
|
||||||
|
|
||||||
|
fdNames := strings.Split(lnFdnames, ":")
|
||||||
|
if fds != len(fdNames) {
|
||||||
|
return nil, fmt.Errorf("LISTEN_FDS does not match LISTEN_FDNAMES length: %d != %d", fds, len(fdNames))
|
||||||
|
}
|
||||||
|
|
||||||
|
nameToFiles := make(map[string][]uint, len(fdNames))
|
||||||
|
for index, name := range fdNames {
|
||||||
|
nameToFiles[name] = append(nameToFiles[name], lnFdsStart+uint(index))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nameToFiles, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSdListenFd(nameToFiles map[string][]uint, nameOffset string) (uint, error) {
|
||||||
|
index := uint(0)
|
||||||
|
|
||||||
|
name, offset, found := strings.Cut(nameOffset, ":")
|
||||||
|
if found {
|
||||||
|
off, err := strconv.ParseUint(offset, 0, strconv.IntSize)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
index += uint(off)
|
||||||
|
}
|
||||||
|
|
||||||
|
files, ok := nameToFiles[name]
|
||||||
|
if !ok {
|
||||||
|
return 0, fmt.Errorf("invalid listen fd name: %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if uint(len(files)) <= index {
|
||||||
|
return 0, fmt.Errorf("invalid listen fd index: %d", index)
|
||||||
|
}
|
||||||
|
|
||||||
|
return files[index], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var initNameToFiles, initNameToFilesErr = sdListenFdsWithNames()
|
||||||
|
|
||||||
|
// systemdReplacementProvider handles {systemd.*} replacements
|
||||||
|
type systemdReplacementProvider struct{}
|
||||||
|
|
||||||
|
func (f systemdReplacementProvider) replace(key string) (any, bool) {
|
||||||
|
// check environment variable
|
||||||
|
const systemdListenPrefix = "systemd.listen."
|
||||||
|
if strings.HasPrefix(key, systemdListenPrefix) {
|
||||||
|
if initNameToFilesErr != nil {
|
||||||
|
Log().Error("unable to read LISTEN_FDNAMES", zap.Error(initNameToFilesErr))
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
fd, err := getSdListenFd(initNameToFiles, key[len(systemdListenPrefix):])
|
||||||
|
if err != nil {
|
||||||
|
Log().Error("unable to process {"+key+"}", zap.Error(err))
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return fd, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO const systemdCredsPrefix = "systemd.creds."
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
var globalReplacementProviders = []replacementProvider{
|
||||||
|
defaultReplacementProvider{},
|
||||||
|
fileReplacementProvider{},
|
||||||
|
systemdReplacementProvider{},
|
||||||
|
}
|
||||||
|
|
@ -374,10 +374,6 @@ func TestReplacerMap(t *testing.T) {
|
||||||
func TestReplacerNew(t *testing.T) {
|
func TestReplacerNew(t *testing.T) {
|
||||||
repl := NewReplacer()
|
repl := NewReplacer()
|
||||||
|
|
||||||
if len(repl.providers) != 3 {
|
|
||||||
t.Errorf("Expected providers length '%v' got length '%v'", 3, len(repl.providers))
|
|
||||||
}
|
|
||||||
|
|
||||||
// test if default global replacements are added as the first provider
|
// test if default global replacements are added as the first provider
|
||||||
hostname, _ := os.Hostname()
|
hostname, _ := os.Hostname()
|
||||||
wd, _ := os.Getwd()
|
wd, _ := os.Getwd()
|
||||||
|
|
|
||||||
376
replacer_test_systemd.go
Normal file
376
replacer_test_systemd.go
Normal file
|
|
@ -0,0 +1,376 @@
|
||||||
|
//go:build linux && !nosystemd
|
||||||
|
|
||||||
|
package caddy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestGetSdListenFd tests the getSdListenFd function for systemd socket activation.
|
||||||
|
func TestGetSdListenFd(t *testing.T) {
|
||||||
|
// Save original environment
|
||||||
|
originalFdNames := os.Getenv("LISTEN_FDNAMES")
|
||||||
|
originalFds := os.Getenv("LISTEN_FDS")
|
||||||
|
originalPid := os.Getenv("LISTEN_PID")
|
||||||
|
|
||||||
|
// Restore environment after test
|
||||||
|
defer func() {
|
||||||
|
if originalFdNames != "" {
|
||||||
|
os.Setenv("LISTEN_FDNAMES", originalFdNames)
|
||||||
|
} else {
|
||||||
|
os.Unsetenv("LISTEN_FDNAMES")
|
||||||
|
}
|
||||||
|
if originalFds != "" {
|
||||||
|
os.Setenv("LISTEN_FDS", originalFds)
|
||||||
|
} else {
|
||||||
|
os.Unsetenv("LISTEN_FDS")
|
||||||
|
}
|
||||||
|
if originalPid != "" {
|
||||||
|
os.Setenv("LISTEN_PID", originalPid)
|
||||||
|
} else {
|
||||||
|
os.Unsetenv("LISTEN_PID")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fdNames string
|
||||||
|
fds string
|
||||||
|
socketName string
|
||||||
|
expectedFd uint
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "simple http socket",
|
||||||
|
fdNames: "http",
|
||||||
|
fds: "1",
|
||||||
|
socketName: "http",
|
||||||
|
expectedFd: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple different sockets - first",
|
||||||
|
fdNames: "http:https:dns",
|
||||||
|
fds: "3",
|
||||||
|
socketName: "http",
|
||||||
|
expectedFd: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple different sockets - second",
|
||||||
|
fdNames: "http:https:dns",
|
||||||
|
fds: "3",
|
||||||
|
socketName: "https",
|
||||||
|
expectedFd: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple different sockets - third",
|
||||||
|
fdNames: "http:https:dns",
|
||||||
|
fds: "3",
|
||||||
|
socketName: "dns",
|
||||||
|
expectedFd: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "duplicate names - first occurrence (no index)",
|
||||||
|
fdNames: "web:web:api",
|
||||||
|
fds: "3",
|
||||||
|
socketName: "web",
|
||||||
|
expectedFd: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "duplicate names - first occurrence (explicit index 0)",
|
||||||
|
fdNames: "web:web:api",
|
||||||
|
fds: "3",
|
||||||
|
socketName: "web:0",
|
||||||
|
expectedFd: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "duplicate names - second occurrence (index 1)",
|
||||||
|
fdNames: "web:web:api",
|
||||||
|
fds: "3",
|
||||||
|
socketName: "web:1",
|
||||||
|
expectedFd: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "complex duplicates - first api",
|
||||||
|
fdNames: "web:api:web:api:dns",
|
||||||
|
fds: "5",
|
||||||
|
socketName: "api:0",
|
||||||
|
expectedFd: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "complex duplicates - second api",
|
||||||
|
fdNames: "web:api:web:api:dns",
|
||||||
|
fds: "5",
|
||||||
|
socketName: "api:1",
|
||||||
|
expectedFd: 6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "complex duplicates - first web",
|
||||||
|
fdNames: "web:api:web:api:dns",
|
||||||
|
fds: "5",
|
||||||
|
socketName: "web:0",
|
||||||
|
expectedFd: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "complex duplicates - second web",
|
||||||
|
fdNames: "web:api:web:api:dns",
|
||||||
|
fds: "5",
|
||||||
|
socketName: "web:1",
|
||||||
|
expectedFd: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "socket not found",
|
||||||
|
fdNames: "http:https",
|
||||||
|
fds: "2",
|
||||||
|
socketName: "missing",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty socket name",
|
||||||
|
fdNames: "http",
|
||||||
|
fds: "1",
|
||||||
|
socketName: "",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing LISTEN_FDNAMES",
|
||||||
|
fdNames: "",
|
||||||
|
fds: "",
|
||||||
|
socketName: "http",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "index out of range",
|
||||||
|
fdNames: "web:web",
|
||||||
|
fds: "2",
|
||||||
|
socketName: "web:2",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "negative index",
|
||||||
|
fdNames: "web",
|
||||||
|
fds: "1",
|
||||||
|
socketName: "web:-1",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid index format",
|
||||||
|
fdNames: "web",
|
||||||
|
fds: "1",
|
||||||
|
socketName: "web:abc",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "too many colons",
|
||||||
|
fdNames: "web",
|
||||||
|
fds: "1",
|
||||||
|
socketName: "web:0:extra",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
// Set up environment
|
||||||
|
if tc.fdNames != "" {
|
||||||
|
os.Setenv("LISTEN_FDNAMES", tc.fdNames)
|
||||||
|
} else {
|
||||||
|
os.Unsetenv("LISTEN_FDNAMES")
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.fds != "" {
|
||||||
|
os.Setenv("LISTEN_FDS", tc.fds)
|
||||||
|
} else {
|
||||||
|
os.Unsetenv("LISTEN_FDS")
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Setenv("LISTEN_PID", strconv.Itoa(os.Getpid()))
|
||||||
|
|
||||||
|
// Test the function
|
||||||
|
var (
|
||||||
|
listenFdsWithNames map[string][]uint
|
||||||
|
err error
|
||||||
|
fd uint
|
||||||
|
)
|
||||||
|
listenFdsWithNames, err = sdListenFdsWithNames()
|
||||||
|
if err == nil {
|
||||||
|
fd, err = getSdListenFd(listenFdsWithNames, tc.socketName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectError {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected error but got none")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
if fd != tc.expectedFd {
|
||||||
|
t.Errorf("Expected FD %d but got %d", tc.expectedFd, fd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestParseSystemdListenPlaceholder tests parsing of {systemd.listen.name} placeholders.
|
||||||
|
func TestParseSystemdListenPlaceholder(t *testing.T) {
|
||||||
|
// Save and restore environment
|
||||||
|
originalFdNames := os.Getenv("LISTEN_FDNAMES")
|
||||||
|
originalFds := os.Getenv("LISTEN_FDS")
|
||||||
|
originalPid := os.Getenv("LISTEN_PID")
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if originalFdNames != "" {
|
||||||
|
os.Setenv("LISTEN_FDNAMES", originalFdNames)
|
||||||
|
} else {
|
||||||
|
os.Unsetenv("LISTEN_FDNAMES")
|
||||||
|
}
|
||||||
|
if originalFds != "" {
|
||||||
|
os.Setenv("LISTEN_FDS", originalFds)
|
||||||
|
} else {
|
||||||
|
os.Unsetenv("LISTEN_FDS")
|
||||||
|
}
|
||||||
|
if originalPid != "" {
|
||||||
|
os.Setenv("LISTEN_PID", originalPid)
|
||||||
|
} else {
|
||||||
|
os.Unsetenv("LISTEN_PID")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Set up test environment
|
||||||
|
os.Setenv("LISTEN_FDNAMES", "http:https:dns")
|
||||||
|
os.Setenv("LISTEN_FDS", "3")
|
||||||
|
os.Setenv("LISTEN_PID", strconv.Itoa(os.Getpid()))
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
expectedAddr NetworkAddress
|
||||||
|
expectedFd uint
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
input: "fd/{systemd.listen.http}",
|
||||||
|
expectedAddr: NetworkAddress{
|
||||||
|
Network: "fd",
|
||||||
|
Host: "{systemd.listen.http}",
|
||||||
|
},
|
||||||
|
expectedFd: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "fd/{systemd.listen.https}",
|
||||||
|
expectedAddr: NetworkAddress{
|
||||||
|
Network: "fd",
|
||||||
|
Host: "{systemd.listen.https}",
|
||||||
|
},
|
||||||
|
expectedFd: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "fd/{systemd.listen.dns}",
|
||||||
|
expectedAddr: NetworkAddress{
|
||||||
|
Network: "fd",
|
||||||
|
Host: "{systemd.listen.dns}",
|
||||||
|
},
|
||||||
|
expectedFd: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "fd/{systemd.listen.http:0}",
|
||||||
|
expectedAddr: NetworkAddress{
|
||||||
|
Network: "fd",
|
||||||
|
Host: "{systemd.listen.http:0}",
|
||||||
|
},
|
||||||
|
expectedFd: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "fd/{systemd.listen.https:0}",
|
||||||
|
expectedAddr: NetworkAddress{
|
||||||
|
Network: "fd",
|
||||||
|
Host: "{systemd.listen.https:0}",
|
||||||
|
},
|
||||||
|
expectedFd: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "fdgram/{systemd.listen.http}",
|
||||||
|
expectedAddr: NetworkAddress{
|
||||||
|
Network: "fdgram",
|
||||||
|
Host: "{systemd.listen.http}",
|
||||||
|
},
|
||||||
|
expectedFd: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "fdgram/{systemd.listen.https}",
|
||||||
|
expectedAddr: NetworkAddress{
|
||||||
|
Network: "fdgram",
|
||||||
|
Host: "{systemd.listen.https}",
|
||||||
|
},
|
||||||
|
expectedFd: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "fdgram/{systemd.listen.http:0}",
|
||||||
|
expectedAddr: NetworkAddress{
|
||||||
|
Network: "fdgram",
|
||||||
|
Host: "http:0",
|
||||||
|
},
|
||||||
|
expectedFd: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "fd/{systemd.listen.nonexistent}",
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "fdgram/{systemd.listen.nonexistent}",
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "fd/{systemd.listen.http:99}",
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "fd/{systemd.listen.invalid:abc}",
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
// Test that old fd/N syntax still works
|
||||||
|
{
|
||||||
|
input: "fd/7",
|
||||||
|
expectedAddr: NetworkAddress{
|
||||||
|
Network: "fd",
|
||||||
|
Host: "7",
|
||||||
|
},
|
||||||
|
expectedFd: 7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "fdgram/8",
|
||||||
|
expectedAddr: NetworkAddress{
|
||||||
|
Network: "fdgram",
|
||||||
|
Host: "8",
|
||||||
|
},
|
||||||
|
expectedFd: 8,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tc := range tests {
|
||||||
|
actualAddr, err := ParseNetworkAddress(tc.input)
|
||||||
|
if err == nil {
|
||||||
|
var fd uint
|
||||||
|
fdWide, err := strconv.ParseUint(actualAddr.Host, 0, strconv.IntSize)
|
||||||
|
if err == nil {
|
||||||
|
fd = uint(fdWide)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectErr && err == nil {
|
||||||
|
t.Errorf("Test %d (%s): Expected error but got none", i, tc.input)
|
||||||
|
}
|
||||||
|
if !tc.expectErr && err != nil {
|
||||||
|
t.Errorf("Test %d (%s): Expected no error but got: %v", i, tc.input, err)
|
||||||
|
}
|
||||||
|
if !tc.expectErr && !reflect.DeepEqual(tc.expectedAddr, actualAddr) {
|
||||||
|
t.Errorf("Test %d (%s): Expected %+v but got %+v", i, tc.input, tc.expectedAddr, actualAddr)
|
||||||
|
}
|
||||||
|
if !tc.expectErr && fd != tc.expectedFd {
|
||||||
|
t.Errorf("Expected FD %d but got %d", tc.expectedFd, fd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue