2019-08-09 21:05:47 +03:00
// 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 httpcaddyfile
import (
"encoding/json"
"fmt"
2023-10-17 08:57:03 +03:00
"net"
2019-08-09 21:05:47 +03:00
"reflect"
2024-04-05 01:27:52 +03:00
"slices"
2019-08-21 19:46:35 +03:00
"sort"
2020-03-31 03:39:21 +03:00
"strconv"
2019-08-09 21:05:47 +03:00
"strings"
2023-08-14 18:41:15 +03:00
"go.uber.org/zap"
2019-08-09 21:05:47 +03:00
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
2021-02-03 03:23:52 +03:00
"github.com/caddyserver/caddy/v2/modules/caddypki"
2019-08-09 21:05:47 +03:00
"github.com/caddyserver/caddy/v2/modules/caddytls"
)
func init ( ) {
caddyconfig . RegisterAdapter ( "caddyfile" , caddyfile . Adapter { ServerType : ServerType { } } )
}
2021-02-16 06:10:27 +03:00
// App represents the configuration for a non-standard
// Caddy app module (e.g. third-party plugin) which was
// parsed from a global options block.
type App struct {
// The JSON key for the app being configured
Name string
// The raw app config as JSON
Value json . RawMessage
}
2019-08-09 21:05:47 +03:00
// ServerType can set up a config from an HTTP Caddyfile.
2023-08-07 22:40:31 +03:00
type ServerType struct { }
2019-08-09 21:05:47 +03:00
// Setup makes a config from the tokens.
2023-05-16 18:27:52 +03:00
func ( st ServerType ) Setup (
inputServerBlocks [ ] caddyfile . ServerBlock ,
options map [ string ] any ,
) ( * caddy . Config , [ ] caddyconfig . Warning , error ) {
2019-08-09 21:05:47 +03:00
var warnings [ ] caddyconfig . Warning
2020-01-17 03:08:52 +03:00
gc := counter { new ( int ) }
2022-08-02 23:39:09 +03:00
state := make ( map [ string ] any )
2019-08-09 21:05:47 +03:00
2022-07-26 02:28:20 +03:00
// load all the server blocks and associate them with a "pile" of config values
2020-04-28 17:32:04 +03:00
originalServerBlocks := make ( [ ] serverBlock , 0 , len ( inputServerBlocks ) )
2022-07-26 02:28:20 +03:00
for _ , sblock := range inputServerBlocks {
2020-02-17 01:28:27 +03:00
for j , k := range sblock . Keys {
2024-02-19 03:22:48 +03:00
if j == 0 && strings . HasPrefix ( k . Text , "@" ) {
return nil , warnings , fmt . Errorf ( "%s:%d: cannot define a matcher outside of a site block: '%s'" , k . File , k . Line , k . Text )
}
if _ , ok := registeredDirectives [ k . Text ] ; ok {
return nil , warnings , fmt . Errorf ( "%s:%d: parsed '%s' as a site address, but it is a known directive; directives must appear in a site block" , k . File , k . Line , k . Text )
2020-05-20 19:37:48 +03:00
}
2020-02-17 01:28:27 +03:00
}
2020-04-28 17:32:04 +03:00
originalServerBlocks = append ( originalServerBlocks , serverBlock {
2019-08-21 19:46:35 +03:00
block : sblock ,
pile : make ( map [ string ] [ ] ConfigValue ) ,
} )
}
2020-02-17 01:28:27 +03:00
// apply any global options
var err error
2020-04-28 17:32:04 +03:00
originalServerBlocks , err = st . evaluateGlobalOptionsBlock ( originalServerBlocks , options )
2020-02-17 01:28:27 +03:00
if err != nil {
return nil , warnings , err
2019-08-22 22:38:37 +03:00
}
2023-09-08 21:38:44 +03:00
// this will replace both static and user-defined placeholder shorthands
// with actual identifiers used by Caddy
replacer := NewShorthandReplacer ( )
originalServerBlocks , err = st . extractNamedRoutes ( originalServerBlocks , options , & warnings , replacer )
2023-05-16 18:27:52 +03:00
if err != nil {
return nil , warnings , err
}
2020-04-28 17:32:04 +03:00
for _ , sb := range originalServerBlocks {
2023-09-08 21:38:44 +03:00
for i := range sb . block . Segments {
replacer . ApplyToSegment ( & sb . block . Segments [ i ] )
2019-08-21 20:03:50 +03:00
}
2019-08-22 22:38:37 +03:00
if len ( sb . block . Keys ) == 0 {
return nil , warnings , fmt . Errorf ( "server block without any key is global configuration, and if used, it must be first" )
}
2019-08-21 19:46:35 +03:00
// extract matcher definitions
2020-01-10 00:00:32 +03:00
matcherDefs := make ( map [ string ] caddy . ModuleMap )
for _ , segment := range sb . block . Segments {
if dir := segment . Directive ( ) ; strings . HasPrefix ( dir , matcherPrefix ) {
d := sb . block . DispenseDirective ( dir )
err := parseMatcherDefinitions ( d , matcherDefs )
if err != nil {
return nil , warnings , err
}
}
2019-08-21 19:46:35 +03:00
}
2020-02-17 01:28:27 +03:00
// evaluate each directive ("segment") in this block
2019-08-21 19:46:35 +03:00
for _ , segment := range sb . block . Segments {
dir := segment . Directive ( )
2020-01-10 00:00:32 +03:00
if strings . HasPrefix ( dir , matcherPrefix ) {
// matcher definitions were pre-processed
2019-08-21 19:46:35 +03:00
continue
}
2020-01-10 00:00:32 +03:00
dirFunc , ok := registeredDirectives [ dir ]
if ! ok {
2019-08-21 20:03:50 +03:00
tkn := segment [ 0 ]
2021-08-23 20:53:27 +03:00
message := "%s:%d: unrecognized directive: %s"
if ! sb . block . HasBraces {
message += "\nDid you mean to define a second site? If so, you must use curly braces around each site to separate their configurations."
}
return nil , warnings , fmt . Errorf ( message , tkn . File , tkn . Line , dir )
2019-08-21 19:46:35 +03:00
}
2020-01-10 00:00:32 +03:00
2020-03-04 19:58:49 +03:00
h := Helper {
2020-01-16 21:29:20 +03:00
Dispenser : caddyfile . NewDispenser ( segment ) ,
options : options ,
warnings : & warnings ,
matcherDefs : matcherDefs ,
parentBlock : sb . block ,
2020-01-17 03:08:52 +03:00
groupCounter : gc ,
2020-03-04 19:58:49 +03:00
State : state ,
}
results , err := dirFunc ( h )
2020-01-10 00:00:32 +03:00
if err != nil {
return nil , warnings , fmt . Errorf ( "parsing caddyfile tokens for '%s': %v" , dir , err )
}
2020-09-17 05:01:22 +03:00
2021-12-13 21:42:08 +03:00
dir = normalizeDirectiveName ( dir )
2020-09-17 05:01:22 +03:00
2020-01-10 00:00:32 +03:00
for _ , result := range results {
result . directive = dir
sb . pile [ result . Class ] = append ( sb . pile [ result . Class ] , result )
}
2023-05-16 18:27:52 +03:00
// specially handle named routes that were pulled out from
// the invoke directive, which could be nested anywhere within
// some subroutes in this directive; we add them to the pile
// for this server block
if state [ namedRouteKey ] != nil {
for name := range state [ namedRouteKey ] . ( map [ string ] struct { } ) {
result := ConfigValue { Class : namedRouteKey , Value : name }
sb . pile [ result . Class ] = append ( sb . pile [ result . Class ] , result )
}
state [ namedRouteKey ] = nil
}
2019-08-21 19:46:35 +03:00
}
}
2019-08-09 21:05:47 +03:00
// map
2024-09-30 19:55:03 +03:00
sbmap , err := st . mapAddressToProtocolToServerBlocks ( originalServerBlocks , options )
2019-08-09 21:05:47 +03:00
if err != nil {
return nil , warnings , err
}
// reduce
pairings := st . consolidateAddrMappings ( sbmap )
// each pairing of listener addresses to list of server
// blocks is basically a server definition
2020-01-17 03:08:52 +03:00
servers , err := st . serversFromPairings ( pairings , options , & warnings , gc )
2019-08-09 21:05:47 +03:00
if err != nil {
return nil , warnings , err
}
// now that each server is configured, make the HTTP app
httpApp := caddyhttp . App {
2022-08-03 20:04:51 +03:00
HTTPPort : tryInt ( options [ "http_port" ] , & warnings ) ,
HTTPSPort : tryInt ( options [ "https_port" ] , & warnings ) ,
GracePeriod : tryDuration ( options [ "grace_period" ] , & warnings ) ,
ShutdownDelay : tryDuration ( options [ "shutdown_delay" ] , & warnings ) ,
Servers : servers ,
2019-08-09 21:05:47 +03:00
}
2020-03-18 06:00:45 +03:00
// then make the TLS app
tlsApp , warnings , err := st . buildTLSApp ( pairings , options , warnings )
if err != nil {
return nil , warnings , err
2019-09-30 18:11:30 +03:00
}
2019-08-09 21:05:47 +03:00
2021-02-03 03:23:52 +03:00
// then make the PKI app
pkiApp , warnings , err := st . buildPKIApp ( pairings , options , warnings )
if err != nil {
return nil , warnings , err
}
2020-02-26 08:00:33 +03:00
// extract any custom logs, and enforce configured levels
var customLogs [ ] namedCustomLog
var hasDefaultLog bool
2021-03-12 23:00:02 +03:00
addCustomLog := func ( ncl namedCustomLog ) {
if ncl . name == "" {
return
}
2022-10-05 21:14:13 +03:00
if ncl . name == caddy . DefaultLoggerName {
2021-03-12 23:00:02 +03:00
hasDefaultLog = true
}
2023-08-02 10:13:46 +03:00
if _ , ok := options [ "debug" ] ; ok && ncl . log != nil && ncl . log . Level == "" {
2022-09-16 08:10:16 +03:00
ncl . log . Level = zap . DebugLevel . CapitalString ( )
2021-03-12 23:00:02 +03:00
}
customLogs = append ( customLogs , ncl )
}
2022-04-28 19:16:25 +03:00
2021-03-12 23:00:02 +03:00
// Apply global log options, when set
if options [ "log" ] != nil {
for _ , logValue := range options [ "log" ] . ( [ ] ConfigValue ) {
addCustomLog ( logValue . Value . ( namedCustomLog ) )
}
}
2020-04-28 17:32:04 +03:00
2020-02-26 08:00:33 +03:00
if ! hasDefaultLog {
// if the default log was not customized, ensure we
// configure it with any applicable options
if _ , ok := options [ "debug" ] ; ok {
customLogs = append ( customLogs , namedCustomLog {
2022-10-05 21:14:13 +03:00
name : caddy . DefaultLoggerName ,
2023-03-28 00:41:24 +03:00
log : & caddy . CustomLog {
BaseLog : caddy . BaseLog { Level : zap . DebugLevel . CapitalString ( ) } ,
} ,
2020-02-26 08:00:33 +03:00
} )
}
}
2022-04-28 19:16:25 +03:00
// Apply server-specific log options
for _ , p := range pairings {
for _ , sb := range p . serverBlocks {
for _ , clVal := range sb . pile [ "custom_log" ] {
addCustomLog ( clVal . Value . ( namedCustomLog ) )
}
}
}
2019-08-09 21:05:47 +03:00
// annnd the top-level config, then we're done!
2019-12-10 23:36:46 +03:00
cfg := & caddy . Config { AppsRaw : make ( caddy . ModuleMap ) }
2021-02-16 06:10:27 +03:00
// loop through the configured options, and if any of
// them are an httpcaddyfile App, then we insert them
// into the config as raw Caddy apps
for _ , opt := range options {
if app , ok := opt . ( App ) ; ok {
cfg . AppsRaw [ app . Name ] = app . Value
}
}
// insert the standard Caddy apps into the config
2020-03-18 06:00:45 +03:00
if len ( httpApp . Servers ) > 0 {
2019-08-09 21:05:47 +03:00
cfg . AppsRaw [ "http" ] = caddyconfig . JSON ( httpApp , & warnings )
}
2020-03-18 06:00:45 +03:00
if ! reflect . DeepEqual ( tlsApp , & caddytls . TLS { CertificatesRaw : make ( caddy . ModuleMap ) } ) {
2019-08-09 21:05:47 +03:00
cfg . AppsRaw [ "tls" ] = caddyconfig . JSON ( tlsApp , & warnings )
}
2021-02-03 03:23:52 +03:00
if ! reflect . DeepEqual ( pkiApp , & caddypki . PKI { CAs : make ( map [ string ] * caddypki . CA ) } ) {
cfg . AppsRaw [ "pki" ] = caddyconfig . JSON ( pkiApp , & warnings )
}
2024-01-13 23:12:43 +03:00
if filesystems , ok := options [ "filesystem" ] . ( caddy . Module ) ; ok {
cfg . AppsRaw [ "caddy.filesystems" ] = caddyconfig . JSON (
filesystems ,
& warnings )
}
2019-09-19 21:42:36 +03:00
if storageCvtr , ok := options [ "storage" ] . ( caddy . StorageConverter ) ; ok {
2019-09-27 03:06:15 +03:00
cfg . StorageRaw = caddyconfig . JSONModuleObject ( storageCvtr ,
"module" ,
2019-12-10 23:36:46 +03:00
storageCvtr . ( caddy . Module ) . CaddyModule ( ) . ID . Name ( ) ,
2019-09-27 03:06:15 +03:00
& warnings )
2019-09-19 21:42:36 +03:00
}
2020-08-03 22:44:38 +03:00
if adminConfig , ok := options [ "admin" ] . ( * caddy . AdminConfig ) ; ok && adminConfig != nil {
cfg . Admin = adminConfig
2019-10-31 00:12:42 +03:00
}
2023-01-28 07:31:37 +03:00
if pc , ok := options [ "persist_config" ] . ( string ) ; ok && pc == "off" {
if cfg . Admin == nil {
cfg . Admin = new ( caddy . AdminConfig )
}
if cfg . Admin . Config == nil {
cfg . Admin . Config = new ( caddy . ConfigSettings )
}
cfg . Admin . Config . Persist = new ( bool )
}
2020-02-26 08:00:33 +03:00
if len ( customLogs ) > 0 {
if cfg . Logging == nil {
cfg . Logging = & caddy . Logging {
Logs : make ( map [ string ] * caddy . CustomLog ) ,
}
}
2023-08-02 10:13:46 +03:00
// Add the default log first if defined, so that it doesn't
// accidentally get re-created below due to the Exclude logic
for _ , ncl := range customLogs {
if ncl . name == caddy . DefaultLoggerName && ncl . log != nil {
cfg . Logging . Logs [ caddy . DefaultLoggerName ] = ncl . log
break
}
}
// Add the rest of the custom logs
2020-02-26 08:00:33 +03:00
for _ , ncl := range customLogs {
2023-08-02 10:13:46 +03:00
if ncl . log == nil || ncl . name == caddy . DefaultLoggerName {
continue
}
2020-02-26 08:00:33 +03:00
if ncl . name != "" {
cfg . Logging . Logs [ ncl . name ] = ncl . log
}
2020-04-28 17:32:04 +03:00
// most users seem to prefer not writing access logs
// to the default log when they are directed to a
// file or have any other special customization
2022-10-05 21:14:13 +03:00
if ncl . name != caddy . DefaultLoggerName && len ( ncl . log . Include ) > 0 {
defaultLog , ok := cfg . Logging . Logs [ caddy . DefaultLoggerName ]
2020-04-28 17:32:04 +03:00
if ! ok {
defaultLog = new ( caddy . CustomLog )
2022-10-05 21:14:13 +03:00
cfg . Logging . Logs [ caddy . DefaultLoggerName ] = defaultLog
2020-04-28 17:32:04 +03:00
}
defaultLog . Exclude = append ( defaultLog . Exclude , ncl . log . Include ... )
2023-08-02 10:13:46 +03:00
// avoid duplicates by sorting + compacting
2023-08-03 03:41:37 +03:00
sort . Strings ( defaultLog . Exclude )
2023-08-02 10:13:46 +03:00
defaultLog . Exclude = slices . Compact [ [ ] string , string ] ( defaultLog . Exclude )
2020-04-28 17:32:04 +03:00
}
2020-02-26 08:00:33 +03:00
}
2023-08-02 10:13:46 +03:00
// we may have not actually added anything, so remove if empty
if len ( cfg . Logging . Logs ) == 0 {
cfg . Logging = nil
}
2020-02-26 08:00:33 +03:00
}
2019-08-09 21:05:47 +03:00
return cfg , warnings , nil
}
2020-02-17 01:28:27 +03:00
// evaluateGlobalOptionsBlock evaluates the global options block,
// which is expected to be the first server block if it has zero
// keys. It returns the updated list of server blocks with the
// global options block removed, and updates options accordingly.
2022-08-02 23:39:09 +03:00
func ( ServerType ) evaluateGlobalOptionsBlock ( serverBlocks [ ] serverBlock , options map [ string ] any ) ( [ ] serverBlock , error ) {
2020-02-17 01:28:27 +03:00
if len ( serverBlocks ) == 0 || len ( serverBlocks [ 0 ] . block . Keys ) > 0 {
return serverBlocks , nil
}
for _ , segment := range serverBlocks [ 0 ] . block . Segments {
2020-11-23 22:46:50 +03:00
opt := segment . Directive ( )
2022-08-02 23:39:09 +03:00
var val any
2020-02-17 01:28:27 +03:00
var err error
disp := caddyfile . NewDispenser ( segment )
2020-05-12 00:00:35 +03:00
2020-11-23 22:46:50 +03:00
optFunc , ok := registeredGlobalOptions [ opt ]
2020-05-12 00:00:35 +03:00
if ! ok {
tkn := segment [ 0 ]
2020-11-23 22:46:50 +03:00
return nil , fmt . Errorf ( "%s:%d: unrecognized global option: %s" , tkn . File , tkn . Line , opt )
2020-02-17 01:28:27 +03:00
}
2020-05-12 00:00:35 +03:00
2021-01-07 21:01:58 +03:00
val , err = optFunc ( disp , options [ opt ] )
2020-02-17 01:28:27 +03:00
if err != nil {
2020-11-23 22:46:50 +03:00
return nil , fmt . Errorf ( "parsing caddyfile tokens for '%s': %v" , opt , err )
}
// As a special case, fold multiple "servers" options together
// in an array instead of overwriting a possible existing value
if opt == "servers" {
existingOpts , ok := options [ opt ] . ( [ ] serverOptions )
if ! ok {
existingOpts = [ ] serverOptions { }
}
serverOpts , ok := val . ( serverOptions )
if ! ok {
2021-03-12 23:00:02 +03:00
return nil , fmt . Errorf ( "unexpected type from 'servers' global options: %T" , val )
2020-11-23 22:46:50 +03:00
}
options [ opt ] = append ( existingOpts , serverOpts )
continue
2020-02-17 01:28:27 +03:00
}
2021-03-12 23:00:02 +03:00
// Additionally, fold multiple "log" options together into an
// array so that multiple loggers can be configured.
if opt == "log" {
existingOpts , ok := options [ opt ] . ( [ ] ConfigValue )
if ! ok {
existingOpts = [ ] ConfigValue { }
}
logOpts , ok := val . ( [ ] ConfigValue )
if ! ok {
return nil , fmt . Errorf ( "unexpected type from 'log' global options: %T" , val )
}
options [ opt ] = append ( existingOpts , logOpts ... )
continue
}
2024-09-30 19:55:03 +03:00
// Also fold multiple "default_bind" options together into an
// array so that server blocks can have multiple binds by default.
if opt == "default_bind" {
existingOpts , ok := options [ opt ] . ( [ ] ConfigValue )
if ! ok {
existingOpts = [ ] ConfigValue { }
}
defaultBindOpts , ok := val . ( [ ] ConfigValue )
if ! ok {
return nil , fmt . Errorf ( "unexpected type from 'default_bind' global options: %T" , val )
}
options [ opt ] = append ( existingOpts , defaultBindOpts ... )
continue
}
2020-05-12 00:00:35 +03:00
2020-11-23 22:46:50 +03:00
options [ opt ] = val
}
// If we got "servers" options, we'll sort them by their listener address
if serverOpts , ok := options [ "servers" ] . ( [ ] serverOptions ) ; ok {
sort . Slice ( serverOpts , func ( i , j int ) bool {
return len ( serverOpts [ i ] . ListenerAddress ) > len ( serverOpts [ j ] . ListenerAddress )
} )
// Reject the config if there are duplicate listener address
seen := make ( map [ string ] bool )
for _ , entry := range serverOpts {
if _ , alreadySeen := seen [ entry . ListenerAddress ] ; alreadySeen {
return nil , fmt . Errorf ( "cannot have 'servers' global options with duplicate listener addresses: %s" , entry . ListenerAddress )
}
seen [ entry . ListenerAddress ] = true
}
2020-02-17 01:28:27 +03:00
}
return serverBlocks [ 1 : ] , nil
}
2023-05-16 18:27:52 +03:00
// extractNamedRoutes pulls out any named route server blocks
// so they don't get parsed as sites, and stores them in options
// for later.
func ( ServerType ) extractNamedRoutes (
serverBlocks [ ] serverBlock ,
options map [ string ] any ,
warnings * [ ] caddyconfig . Warning ,
2023-09-08 21:38:44 +03:00
replacer ShorthandReplacer ,
2023-05-16 18:27:52 +03:00
) ( [ ] serverBlock , error ) {
namedRoutes := map [ string ] * caddyhttp . Route { }
gc := counter { new ( int ) }
state := make ( map [ string ] any )
// copy the server blocks so we can
// splice out the named route ones
filtered := append ( [ ] serverBlock { } , serverBlocks ... )
index := - 1
for _ , sb := range serverBlocks {
index ++
if ! sb . block . IsNamedRoute {
continue
}
// splice out this block, because we know it's not a real server
filtered = append ( filtered [ : index ] , filtered [ index + 1 : ] ... )
index --
if len ( sb . block . Segments ) == 0 {
continue
}
wholeSegment := caddyfile . Segment { }
2023-09-08 21:38:44 +03:00
for i := range sb . block . Segments {
// replace user-defined placeholder shorthands in extracted named routes
replacer . ApplyToSegment ( & sb . block . Segments [ i ] )
// zip up all the segments since ParseSegmentAsSubroute
// was designed to take a directive+
wholeSegment = append ( wholeSegment , sb . block . Segments [ i ] ... )
2023-05-16 18:27:52 +03:00
}
h := Helper {
Dispenser : caddyfile . NewDispenser ( wholeSegment ) ,
options : options ,
warnings : warnings ,
matcherDefs : nil ,
parentBlock : sb . block ,
groupCounter : gc ,
State : state ,
}
handler , err := ParseSegmentAsSubroute ( h )
if err != nil {
return nil , err
}
subroute := handler . ( * caddyhttp . Subroute )
route := caddyhttp . Route { }
if len ( subroute . Routes ) == 1 && len ( subroute . Routes [ 0 ] . MatcherSetsRaw ) == 0 {
// if there's only one route with no matcher, then we can simplify
route . HandlersRaw = append ( route . HandlersRaw , subroute . Routes [ 0 ] . HandlersRaw [ 0 ] )
} else {
// otherwise we need the whole subroute
route . HandlersRaw = [ ] json . RawMessage { caddyconfig . JSONModuleObject ( handler , "handler" , subroute . CaddyModule ( ) . ID . Name ( ) , h . warnings ) }
}
2024-02-19 03:22:48 +03:00
namedRoutes [ sb . block . GetKeysText ( ) [ 0 ] ] = & route
2023-05-16 18:27:52 +03:00
}
options [ "named_routes" ] = namedRoutes
return filtered , nil
}
2019-08-09 21:05:47 +03:00
// serversFromPairings creates the servers for each pairing of addresses
// to server blocks. Each pairing is essentially a server definition.
2019-08-22 23:26:33 +03:00
func ( st * ServerType ) serversFromPairings (
pairings [ ] sbAddrAssociation ,
2022-08-02 23:39:09 +03:00
options map [ string ] any ,
2019-08-22 23:26:33 +03:00
warnings * [ ] caddyconfig . Warning ,
2020-01-17 03:08:52 +03:00
groupCounter counter ,
2019-08-22 23:26:33 +03:00
) ( map [ string ] * caddyhttp . Server , error ) {
2019-08-09 21:05:47 +03:00
servers := make ( map [ string ] * caddyhttp . Server )
2020-03-13 20:32:53 +03:00
defaultSNI := tryString ( options [ "default_sni" ] , warnings )
2023-05-10 23:29:29 +03:00
fallbackSNI := tryString ( options [ "fallback_sni" ] , warnings )
2020-03-13 20:32:53 +03:00
2020-04-09 21:39:05 +03:00
httpPort := strconv . Itoa ( caddyhttp . DefaultHTTPPort )
if hp , ok := options [ "http_port" ] . ( int ) ; ok {
httpPort = strconv . Itoa ( hp )
}
2020-04-25 03:58:28 +03:00
httpsPort := strconv . Itoa ( caddyhttp . DefaultHTTPSPort )
if hsp , ok := options [ "https_port" ] . ( int ) ; ok {
httpsPort = strconv . Itoa ( hsp )
}
2020-05-20 01:59:51 +03:00
autoHTTPS := "on"
if ah , ok := options [ "auto_https" ] . ( string ) ; ok {
autoHTTPS = ah
}
2020-04-09 21:39:05 +03:00
2019-08-09 21:05:47 +03:00
for i , p := range pairings {
2022-07-26 02:28:20 +03:00
// detect ambiguous site definitions: server blocks which
// have the same host bound to the same interface (listener
// address), otherwise their routes will improperly be added
// to the same server (see issue #4635)
for j , sblock1 := range p . serverBlocks {
2024-02-19 03:22:48 +03:00
for _ , key := range sblock1 . block . GetKeysText ( ) {
2022-07-26 02:28:20 +03:00
for k , sblock2 := range p . serverBlocks {
if k == j {
continue
}
2024-09-25 23:30:56 +03:00
if slices . Contains ( sblock2 . block . GetKeysText ( ) , key ) {
2022-07-26 02:28:20 +03:00
return nil , fmt . Errorf ( "ambiguous site definition: %s" , key )
}
}
}
}
2024-09-30 19:55:03 +03:00
var (
addresses [ ] string
protocols [ ] [ ] string
)
for _ , addressWithProtocols := range p . addressesWithProtocols {
addresses = append ( addresses , addressWithProtocols . address )
protocols = append ( protocols , addressWithProtocols . protocols )
}
2019-08-09 21:05:47 +03:00
srv := & caddyhttp . Server {
2024-09-30 19:55:03 +03:00
Listen : addresses ,
ListenProtocols : protocols ,
}
// remove srv.ListenProtocols[j] if it only contains the default protocols
for j , lnProtocols := range srv . ListenProtocols {
srv . ListenProtocols [ j ] = nil
for _ , lnProtocol := range lnProtocols {
if lnProtocol != "" {
srv . ListenProtocols [ j ] = lnProtocols
break
}
}
}
// remove srv.ListenProtocols if it only contains the default protocols for all listen addresses
listenProtocols := srv . ListenProtocols
srv . ListenProtocols = nil
for _ , lnProtocols := range listenProtocols {
if lnProtocols != nil {
srv . ListenProtocols = listenProtocols
break
}
2019-08-09 21:05:47 +03:00
}
2020-05-20 01:59:51 +03:00
// handle the auto_https global option
if autoHTTPS != "on" {
srv . AutoHTTPS = new ( caddyhttp . AutoHTTPSConfig )
2022-02-18 01:40:34 +03:00
switch autoHTTPS {
case "off" :
2020-05-20 01:59:51 +03:00
srv . AutoHTTPS . Disabled = true
2022-02-18 01:40:34 +03:00
case "disable_redirects" :
2020-05-20 01:59:51 +03:00
srv . AutoHTTPS . DisableRedir = true
2022-02-18 01:40:34 +03:00
case "disable_certs" :
srv . AutoHTTPS . DisableCerts = true
case "ignore_loaded_certs" :
2021-05-02 21:11:27 +03:00
srv . AutoHTTPS . IgnoreLoadedCerts = true
}
2020-05-20 01:59:51 +03:00
}
2022-04-25 19:12:10 +03:00
// Using paths in site addresses is deprecated
// See ParseAddress() where parsing should later reject paths
// See https://github.com/caddyserver/caddy/pull/4728 for a full explanation
for _ , sblock := range p . serverBlocks {
2024-09-30 19:55:03 +03:00
for _ , addr := range sblock . parsedKeys {
2022-04-25 19:12:10 +03:00
if addr . Path != "" {
caddy . Log ( ) . Named ( "caddyfile" ) . Warn ( "Using a path in a site address is deprecated; please use the 'handle' directive instead" , zap . String ( "address" , addr . String ( ) ) )
}
}
}
2020-01-15 23:51:12 +03:00
// sort server blocks by their keys; this is important because
// only the first matching site should be evaluated, and we should
// attempt to match most specific site first (host and path), in
// case their matchers overlap; we do this somewhat naively by
// descending sort by length of host then path
sort . SliceStable ( p . serverBlocks , func ( i , j int ) bool {
2020-01-17 03:08:52 +03:00
// TODO: we could pre-process the specificities for efficiency,
2020-04-02 23:20:30 +03:00
// but I don't expect many blocks will have THAT many keys...
2020-01-15 23:51:12 +03:00
var iLongestPath , jLongestPath string
var iLongestHost , jLongestHost string
2020-06-03 18:35:13 +03:00
var iWildcardHost , jWildcardHost bool
2024-09-30 19:55:03 +03:00
for _ , addr := range p . serverBlocks [ i ] . parsedKeys {
2021-02-22 21:14:59 +03:00
if strings . Contains ( addr . Host , "*" ) || addr . Host == "" {
2020-06-03 18:35:13 +03:00
iWildcardHost = true
}
2020-01-17 03:08:52 +03:00
if specificity ( addr . Host ) > specificity ( iLongestHost ) {
2020-01-15 23:51:12 +03:00
iLongestHost = addr . Host
}
2020-01-17 03:08:52 +03:00
if specificity ( addr . Path ) > specificity ( iLongestPath ) {
2020-01-15 23:51:12 +03:00
iLongestPath = addr . Path
}
}
2024-09-30 19:55:03 +03:00
for _ , addr := range p . serverBlocks [ j ] . parsedKeys {
2021-02-22 21:14:59 +03:00
if strings . Contains ( addr . Host , "*" ) || addr . Host == "" {
2020-06-03 18:35:13 +03:00
jWildcardHost = true
}
2020-01-17 03:08:52 +03:00
if specificity ( addr . Host ) > specificity ( jLongestHost ) {
2020-01-15 23:51:12 +03:00
jLongestHost = addr . Host
}
2020-01-17 03:08:52 +03:00
if specificity ( addr . Path ) > specificity ( jLongestPath ) {
2020-01-15 23:51:12 +03:00
jLongestPath = addr . Path
}
}
2021-02-01 21:45:28 +03:00
// catch-all blocks (blocks with no hostname) should always go
// last, even after blocks with wildcard hosts
if specificity ( iLongestHost ) == 0 {
return false
}
2020-06-16 19:02:06 +03:00
if specificity ( jLongestHost ) == 0 {
return true
}
2020-06-03 18:35:13 +03:00
if iWildcardHost != jWildcardHost {
// site blocks that have a key with a wildcard in the hostname
// must always be less specific than blocks without one; see
// https://github.com/caddyserver/caddy/issues/3410
return jWildcardHost && ! iWildcardHost
}
2020-01-17 03:08:52 +03:00
if specificity ( iLongestHost ) == specificity ( jLongestHost ) {
2020-01-15 23:51:12 +03:00
return len ( iLongestPath ) > len ( jLongestPath )
}
2020-01-17 03:08:52 +03:00
return specificity ( iLongestHost ) > specificity ( jLongestHost )
2020-01-15 23:51:12 +03:00
} )
2020-04-25 03:58:28 +03:00
var hasCatchAllTLSConnPolicy , addressQualifiesForTLS bool
2020-05-20 01:59:51 +03:00
autoHTTPSWillAddConnPolicy := autoHTTPS != "off"
2020-02-20 10:15:11 +03:00
2021-08-02 23:15:27 +03:00
// if needed, the ServerLogConfig is initialized beforehand so
// that all server blocks can populate it with data, even when not
// coming with a log directive
for _ , sblock := range p . serverBlocks {
if len ( sblock . pile [ "custom_log" ] ) != 0 {
srv . Logs = new ( caddyhttp . ServerLogConfig )
break
}
}
2023-05-16 18:27:52 +03:00
// add named routes to the server if 'invoke' was used inside of it
configuredNamedRoutes := options [ "named_routes" ] . ( map [ string ] * caddyhttp . Route )
for _ , sblock := range p . serverBlocks {
if len ( sblock . pile [ namedRouteKey ] ) == 0 {
continue
}
for _ , value := range sblock . pile [ namedRouteKey ] {
if srv . NamedRoutes == nil {
srv . NamedRoutes = map [ string ] * caddyhttp . Route { }
}
name := value . Value . ( string )
if configuredNamedRoutes [ name ] == nil {
return nil , fmt . Errorf ( "cannot invoke named route '%s', which was not defined" , name )
}
srv . NamedRoutes [ name ] = configuredNamedRoutes [ name ]
}
}
2020-01-15 23:51:12 +03:00
// create a subroute for each site in the server block
2019-08-09 21:05:47 +03:00
for _ , sblock := range p . serverBlocks {
2020-04-02 23:20:30 +03:00
matcherSetsEnc , err := st . compileEncodedMatcherSets ( sblock )
2019-08-09 21:05:47 +03:00
if err != nil {
2019-08-21 19:46:35 +03:00
return nil , fmt . Errorf ( "server block %v: compiling matcher sets: %v" , sblock . block . Keys , err )
2019-08-09 21:05:47 +03:00
}
httpcaddyfile, caddytls: Multiple edge case fixes; add tests
- Create two default automation policies; if the TLS app is used in
isolation with the 'automate' certificate loader, it will now use
an internal issuer for internal-only names, and an ACME issuer for
all other names by default.
- If the HTTP Caddyfile adds an 'automate' loader, it now also adds an
automation policy for any names in that loader that do not qualify
for public certificates so that they will be issued internally. (It
might be nice if this wasn't necessary, but the alternative is to
either make auto-HTTPS logic way more complex by scanning the names in
the 'automate' loader, or to have an automation policy without an
issuer switch between default issuer based on the name being issued
a certificate - I think I like the latter option better, right now we
do something kind of like that but at a level above each individual
automation policies, we do that switch only when no automation
policies match, rather than when a policy without an issuer does
match.)
- Set the default LoggerName rather than a LoggerNames with an empty
host value, which is now taken literally rather than as a catch-all.
- hostsFromKeys, the function that gets a list of hosts from server
block keys, no longer returns an empty string in its resulting slice,
ever.
2020-04-08 23:46:44 +03:00
hosts := sblock . hostsFromKeys ( false )
2020-03-07 09:15:25 +03:00
2021-02-02 03:02:01 +03:00
// emit warnings if user put unspecified IP addresses; they probably want the bind directive
for _ , h := range hosts {
if h == "0.0.0.0" || h == "::" {
2022-04-25 19:12:10 +03:00
caddy . Log ( ) . Named ( "caddyfile" ) . Warn ( "Site block has an unspecified IP address which only matches requests having that Host header; you probably want the 'bind' directive to configure the socket" , zap . String ( "address" , h ) )
2021-02-02 03:02:01 +03:00
}
}
2020-03-18 06:00:45 +03:00
// tls: connection policies
if cpVals , ok := sblock . pile [ "tls.connection_policy" ] ; ok {
// tls connection policies
2019-08-21 19:46:35 +03:00
for _ , cpVal := range cpVals {
cp := cpVal . Value . ( * caddytls . ConnectionPolicy )
2019-08-09 21:05:47 +03:00
2019-12-14 02:32:27 +03:00
// make sure the policy covers all hostnames from the block
2020-03-07 09:15:25 +03:00
for _ , h := range hosts {
if h == defaultSNI {
hosts = append ( hosts , "" )
cp . DefaultSNI = defaultSNI
break
}
2023-05-10 23:29:29 +03:00
if h == fallbackSNI {
hosts = append ( hosts , "" )
cp . FallbackSNI = fallbackSNI
break
}
2020-03-07 09:15:25 +03:00
}
2019-12-14 02:32:27 +03:00
if len ( hosts ) > 0 {
2023-10-11 18:47:07 +03:00
slices . Sort ( hosts ) // for deterministic JSON output
2019-12-10 23:36:46 +03:00
cp . MatchersRaw = caddy . ModuleMap {
2019-08-21 19:46:35 +03:00
"sni" : caddyconfig . JSON ( hosts , warnings ) , // make sure to match all hosts, not just auto-HTTPS-qualified ones
2019-08-09 21:05:47 +03:00
}
2020-02-06 23:00:41 +03:00
} else {
2020-03-17 21:39:01 +03:00
cp . DefaultSNI = defaultSNI
2023-05-10 23:29:29 +03:00
cp . FallbackSNI = fallbackSNI
2019-08-09 21:05:47 +03:00
}
2020-02-06 23:00:41 +03:00
2020-05-05 21:37:52 +03:00
// only append this policy if it actually changes something
if ! cp . SettingsEmpty ( ) {
srv . TLSConnPolicies = append ( srv . TLSConnPolicies , cp )
2020-09-11 22:45:21 +03:00
hasCatchAllTLSConnPolicy = len ( hosts ) == 0
2020-05-05 21:37:52 +03:00
}
2019-08-09 21:05:47 +03:00
}
}
2024-09-30 19:55:03 +03:00
for _ , addr := range sblock . parsedKeys {
2022-03-25 07:54:03 +03:00
// if server only uses HTTP port, auto-HTTPS will not apply
2021-01-20 00:16:06 +03:00
if listenersUseAnyPortOtherThan ( srv . Listen , httpPort ) {
// exclude any hosts that were defined explicitly with "http://"
// in the key from automated cert management (issue #2998)
if addr . Scheme == "http" && addr . Host != "" {
if srv . AutoHTTPS == nil {
srv . AutoHTTPS = new ( caddyhttp . AutoHTTPSConfig )
}
2024-09-25 23:30:56 +03:00
if ! slices . Contains ( srv . AutoHTTPS . Skip , addr . Host ) {
2021-01-20 00:16:06 +03:00
srv . AutoHTTPS . Skip = append ( srv . AutoHTTPS . Skip , addr . Host )
}
2020-01-23 23:17:16 +03:00
}
}
2021-01-20 00:16:06 +03:00
2023-10-11 00:46:39 +03:00
// If TLS is specified as directive, it will also result in 1 or more connection policy being created
// Thus, catch-all address with non-standard port, e.g. :8443, can have TLS enabled without
// specifying prefix "https://"
// Second part of the condition is to allow creating TLS conn policy even though `auto_https` has been disabled
// ensuring compatibility with behavior described in below link
// https://caddy.community/t/making-sense-of-auto-https-and-why-disabling-it-still-serves-https-instead-of-http/9761
createdTLSConnPolicies , ok := sblock . pile [ "tls.connection_policy" ]
hasTLSEnabled := ( ok && len ( createdTLSConnPolicies ) > 0 ) ||
2024-09-25 23:30:56 +03:00
( addr . Host != "" && srv . AutoHTTPS != nil && ! slices . Contains ( srv . AutoHTTPS . Skip , addr . Host ) )
2023-10-11 00:46:39 +03:00
2020-04-25 03:58:28 +03:00
// we'll need to remember if the address qualifies for auto-HTTPS, so we
// can add a TLS conn policy if necessary
if addr . Scheme == "https" ||
2023-10-11 00:46:39 +03:00
( addr . Scheme != "http" && addr . Port != httpPort && hasTLSEnabled ) {
2020-04-25 03:58:28 +03:00
addressQualifiesForTLS = true
2020-04-09 21:39:05 +03:00
}
2020-04-25 03:58:28 +03:00
// predict whether auto-HTTPS will add the conn policy for us; if so, we
// may not need to add one for this server
autoHTTPSWillAddConnPolicy = autoHTTPSWillAddConnPolicy &&
( addr . Port == httpsPort || ( addr . Port != httpPort && addr . Host != "" ) )
2020-01-23 23:17:16 +03:00
}
2020-06-01 18:50:00 +03:00
// Look for any config values that provide listener wrappers on the server block
for _ , listenerConfig := range sblock . pile [ "listener_wrapper" ] {
listenerWrapper , ok := listenerConfig . Value . ( caddy . ListenerWrapper )
if ! ok {
return nil , fmt . Errorf ( "config for a listener wrapper did not provide a value that implements caddy.ListenerWrapper" )
}
jsonListenerWrapper := caddyconfig . JSONModuleObject (
listenerWrapper ,
"wrapper" ,
listenerWrapper . ( caddy . Module ) . CaddyModule ( ) . ID . Name ( ) ,
warnings )
srv . ListenerWrappersRaw = append ( srv . ListenerWrappersRaw , jsonListenerWrapper )
}
2020-01-16 22:09:54 +03:00
// set up each handler directive, making sure to honor directive order
2019-08-21 19:46:35 +03:00
dirRoutes := sblock . pile [ "route" ]
2023-01-19 00:04:41 +03:00
siteSubroute , err := buildSubroute ( dirRoutes , groupCounter , true )
2020-01-17 03:08:52 +03:00
if err != nil {
return nil , err
2019-08-09 21:05:47 +03:00
}
2020-02-17 08:24:20 +03:00
// add the site block's route(s) to the server
srv . Routes = appendSubrouteToRouteList ( srv . Routes , siteSubroute , matcherSetsEnc , p , warnings )
// if error routes are defined, add those too
if errorSubrouteVals , ok := sblock . pile [ "error_route" ] ; ok {
if srv . Errors == nil {
srv . Errors = new ( caddyhttp . HTTPErrorConfig )
}
2024-01-16 09:24:17 +03:00
sort . SliceStable ( errorSubrouteVals , func ( i , j int ) bool {
sri , srj := errorSubrouteVals [ i ] . Value . ( * caddyhttp . Subroute ) , errorSubrouteVals [ j ] . Value . ( * caddyhttp . Subroute )
if len ( sri . Routes [ 0 ] . MatcherSetsRaw ) == 0 && len ( srj . Routes [ 0 ] . MatcherSetsRaw ) != 0 {
return false
}
return true
} )
errorsSubroute := & caddyhttp . Subroute { }
2020-02-17 08:24:20 +03:00
for _ , val := range errorSubrouteVals {
sr := val . Value . ( * caddyhttp . Subroute )
2024-01-16 09:24:17 +03:00
errorsSubroute . Routes = append ( errorsSubroute . Routes , sr . Routes ... )
2020-02-17 08:24:20 +03:00
}
2024-01-16 09:24:17 +03:00
srv . Errors . Routes = appendSubrouteToRouteList ( srv . Errors . Routes , errorsSubroute , matcherSetsEnc , p , warnings )
2020-01-15 23:51:12 +03:00
}
2020-02-26 08:00:33 +03:00
// add log associations
2020-04-28 17:32:04 +03:00
// see https://github.com/caddyserver/caddy/issues/3310
sblockLogHosts := sblock . hostsFromKeys ( true )
2020-02-26 08:00:33 +03:00
for _ , cval := range sblock . pile [ "custom_log" ] {
ncl := cval . Value . ( namedCustomLog )
2024-05-11 16:31:44 +03:00
// if `no_hostname` is set, then this logger will not
// be associated with any of the site block's hostnames,
// and only be usable via the `log_name` directive
// or the `access_logger_names` variable
if ncl . noHostname {
continue
}
2023-08-02 10:13:46 +03:00
if sblock . hasHostCatchAllKey ( ) && len ( ncl . hostnames ) == 0 {
2020-04-28 17:32:04 +03:00
// all requests for hosts not able to be listed should use
// this log because it's a catch-all-hosts server block
srv . Logs . DefaultLoggerName = ncl . name
2023-08-02 10:13:46 +03:00
} else if len ( ncl . hostnames ) > 0 {
// if the logger overrides the hostnames, map that to the logger name
for _ , h := range ncl . hostnames {
if srv . Logs . LoggerNames == nil {
2024-04-17 01:26:18 +03:00
srv . Logs . LoggerNames = make ( map [ string ] caddyhttp . StringArray )
2023-08-02 10:13:46 +03:00
}
2024-04-17 01:26:18 +03:00
srv . Logs . LoggerNames [ h ] = append ( srv . Logs . LoggerNames [ h ] , ncl . name )
2023-08-02 10:13:46 +03:00
}
httpcaddyfile, caddytls: Multiple edge case fixes; add tests
- Create two default automation policies; if the TLS app is used in
isolation with the 'automate' certificate loader, it will now use
an internal issuer for internal-only names, and an ACME issuer for
all other names by default.
- If the HTTP Caddyfile adds an 'automate' loader, it now also adds an
automation policy for any names in that loader that do not qualify
for public certificates so that they will be issued internally. (It
might be nice if this wasn't necessary, but the alternative is to
either make auto-HTTPS logic way more complex by scanning the names in
the 'automate' loader, or to have an automation policy without an
issuer switch between default issuer based on the name being issued
a certificate - I think I like the latter option better, right now we
do something kind of like that but at a level above each individual
automation policies, we do that switch only when no automation
policies match, rather than when a policy without an issuer does
match.)
- Set the default LoggerName rather than a LoggerNames with an empty
host value, which is now taken literally rather than as a catch-all.
- hostsFromKeys, the function that gets a list of hosts from server
block keys, no longer returns an empty string in its resulting slice,
ever.
2020-04-08 23:46:44 +03:00
} else {
2023-08-02 10:13:46 +03:00
// otherwise, map each host to the logger name
2020-04-28 17:32:04 +03:00
for _ , h := range sblockLogHosts {
2023-10-17 08:57:03 +03:00
// strip the port from the host, if any
host , _ , err := net . SplitHostPort ( h )
if err != nil {
host = h
}
2024-04-17 01:26:18 +03:00
if srv . Logs . LoggerNames == nil {
srv . Logs . LoggerNames = make ( map [ string ] caddyhttp . StringArray )
}
srv . Logs . LoggerNames [ host ] = append ( srv . Logs . LoggerNames [ host ] , ncl . name )
2020-04-04 05:19:46 +03:00
}
2020-02-26 08:00:33 +03:00
}
}
2020-04-28 17:32:04 +03:00
if srv . Logs != nil && len ( sblock . pile [ "custom_log" ] ) == 0 {
// server has access logs enabled, but this server block does not
// enable access logs; therefore, all hosts of this server block
// should not be access-logged
if len ( hosts ) == 0 {
// if the server block has a catch-all-hosts key, then we should
// not log reqs to any host unless it appears in the map
srv . Logs . SkipUnmappedHosts = true
}
srv . Logs . SkipHosts = append ( srv . Logs . SkipHosts , sblockLogHosts ... )
}
2019-08-09 21:05:47 +03:00
}
2023-12-18 22:54:52 +03:00
// sort for deterministic JSON output
if srv . Logs != nil {
slices . Sort ( srv . Logs . SkipHosts )
}
2020-04-02 23:20:30 +03:00
// a server cannot (natively) serve both HTTP and HTTPS at the
// same time, so make sure the configuration isn't in conflict
err := detectConflictingSchemes ( srv , p . serverBlocks , options )
if err != nil {
return nil , err
}
2020-02-20 10:15:11 +03:00
// a catch-all TLS conn policy is necessary to ensure TLS can
// be offered to all hostnames of the server; even though only
// one policy is needed to enable TLS for the server, that
// policy might apply to only certain TLS handshakes; but when
// using the Caddyfile, user would expect all handshakes to at
// least have a matching connection policy, so here we append a
// catch-all/default policy if there isn't one already (it's
// important that it goes at the end) - see issue #3004:
// https://github.com/caddyserver/caddy/issues/3004
2020-03-18 06:00:45 +03:00
// TODO: maybe a smarter way to handle this might be to just make the
// auto-HTTPS logic at provision-time detect if there is any connection
// policy missing for any HTTPS-enabled hosts, if so, add it... maybe?
2020-04-25 03:58:28 +03:00
if addressQualifiesForTLS &&
2020-04-09 21:39:05 +03:00
! hasCatchAllTLSConnPolicy &&
2023-05-10 23:29:29 +03:00
( len ( srv . TLSConnPolicies ) > 0 || ! autoHTTPSWillAddConnPolicy || defaultSNI != "" || fallbackSNI != "" ) {
srv . TLSConnPolicies = append ( srv . TLSConnPolicies , & caddytls . ConnectionPolicy { DefaultSNI : defaultSNI , FallbackSNI : fallbackSNI } )
2020-02-20 10:15:11 +03:00
}
2020-03-18 06:00:45 +03:00
// tidy things up a bit
caddytls: Refactor certificate selection policies (close #1575)
Certificate selection used to be a module, but this seems unnecessary,
especially since the built-in CustomSelectionPolicy allows quite complex
selection logic on a number of fields in certs. If we need to extend
that logic, we can, but I don't think there are SO many possibilities
that we need modules.
This update also allows certificate selection to choose between multiple
matching certs based on client compatibility and makes a number of other
improvements in the default cert selection logic, both here and in the
latest CertMagic.
The hardest part of this was the conn policy consolidation logic
(Caddyfile only, of course). We have to merge connection policies that
we can easily combine, because if two certs are manually loaded in a
Caddyfile site block, that produces two connection policies, and each
cert is tagged with a different tag, meaning only the first would ever
be selected. So given the same matchers, we can merge the two, but this
required improving the Tag selection logic to support multiple tags to
choose from, hence "tags" changed to "any_tag" or "all_tags" (but we
use any_tag in our Caddyfile logic).
Combining conn policies with conflicting settings is impossible, so
that should return an error if two policies with the exact same matchers
have non-empty settings that are not the same (the one exception being
any_tag which we can merge because the logic for them is to OR them).
It was a bit complicated. It seems to work in numerous tests I've
conducted, but we'll see how it pans out in the release candidates.
2020-04-02 05:49:35 +03:00
srv . TLSConnPolicies , err = consolidateConnPolicies ( srv . TLSConnPolicies )
if err != nil {
return nil , fmt . Errorf ( "consolidating TLS connection policies for server %d: %v" , i , err )
}
2019-08-09 21:05:47 +03:00
srv . Routes = consolidateRoutes ( srv . Routes )
servers [ fmt . Sprintf ( "srv%d" , i ) ] = srv
}
2024-09-30 19:55:03 +03:00
if err := applyServerOptions ( servers , options , warnings ) ; err != nil {
2023-01-27 22:56:39 +03:00
return nil , fmt . Errorf ( "applying global server options: %v" , err )
2020-11-23 22:46:50 +03:00
}
2019-08-09 21:05:47 +03:00
return servers , nil
}
2022-08-02 23:39:09 +03:00
func detectConflictingSchemes ( srv * caddyhttp . Server , serverBlocks [ ] serverBlock , options map [ string ] any ) error {
2020-04-02 23:20:30 +03:00
httpPort := strconv . Itoa ( caddyhttp . DefaultHTTPPort )
if hp , ok := options [ "http_port" ] . ( int ) ; ok {
httpPort = strconv . Itoa ( hp )
}
httpsPort := strconv . Itoa ( caddyhttp . DefaultHTTPSPort )
if hsp , ok := options [ "https_port" ] . ( int ) ; ok {
httpsPort = strconv . Itoa ( hsp )
}
var httpOrHTTPS string
checkAndSetHTTP := func ( addr Address ) error {
if httpOrHTTPS == "HTTPS" {
errMsg := fmt . Errorf ( "server listening on %v is configured for HTTPS and cannot natively multiplex HTTP and HTTPS: %s" ,
srv . Listen , addr . Original )
if addr . Scheme == "" && addr . Host == "" {
errMsg = fmt . Errorf ( "%s (try specifying https:// in the address)" , errMsg )
}
return errMsg
}
if len ( srv . TLSConnPolicies ) > 0 {
// any connection policies created for an HTTP server
// is a logical conflict, as it would enable HTTPS
return fmt . Errorf ( "server listening on %v is HTTP, but attempts to configure TLS connection policies" , srv . Listen )
}
httpOrHTTPS = "HTTP"
return nil
}
checkAndSetHTTPS := func ( addr Address ) error {
if httpOrHTTPS == "HTTP" {
return fmt . Errorf ( "server listening on %v is configured for HTTP and cannot natively multiplex HTTP and HTTPS: %s" ,
srv . Listen , addr . Original )
}
httpOrHTTPS = "HTTPS"
return nil
}
for _ , sblock := range serverBlocks {
2024-09-30 19:55:03 +03:00
for _ , addr := range sblock . parsedKeys {
2020-04-02 23:20:30 +03:00
if addr . Scheme == "http" || addr . Port == httpPort {
if err := checkAndSetHTTP ( addr ) ; err != nil {
return err
}
2020-04-25 03:58:28 +03:00
} else if addr . Scheme == "https" || addr . Port == httpsPort || len ( srv . TLSConnPolicies ) > 0 {
2020-04-02 23:20:30 +03:00
if err := checkAndSetHTTPS ( addr ) ; err != nil {
return err
}
} else if addr . Host == "" {
if err := checkAndSetHTTP ( addr ) ; err != nil {
return err
}
}
}
}
return nil
}
2020-09-11 22:45:21 +03:00
// consolidateConnPolicies sorts any catch-all policy to the end, removes empty TLS connection
// policies, and combines equivalent ones for a cleaner overall output.
caddytls: Refactor certificate selection policies (close #1575)
Certificate selection used to be a module, but this seems unnecessary,
especially since the built-in CustomSelectionPolicy allows quite complex
selection logic on a number of fields in certs. If we need to extend
that logic, we can, but I don't think there are SO many possibilities
that we need modules.
This update also allows certificate selection to choose between multiple
matching certs based on client compatibility and makes a number of other
improvements in the default cert selection logic, both here and in the
latest CertMagic.
The hardest part of this was the conn policy consolidation logic
(Caddyfile only, of course). We have to merge connection policies that
we can easily combine, because if two certs are manually loaded in a
Caddyfile site block, that produces two connection policies, and each
cert is tagged with a different tag, meaning only the first would ever
be selected. So given the same matchers, we can merge the two, but this
required improving the Tag selection logic to support multiple tags to
choose from, hence "tags" changed to "any_tag" or "all_tags" (but we
use any_tag in our Caddyfile logic).
Combining conn policies with conflicting settings is impossible, so
that should return an error if two policies with the exact same matchers
have non-empty settings that are not the same (the one exception being
any_tag which we can merge because the logic for them is to OR them).
It was a bit complicated. It seems to work in numerous tests I've
conducted, but we'll see how it pans out in the release candidates.
2020-04-02 05:49:35 +03:00
func consolidateConnPolicies ( cps caddytls . ConnectionPolicies ) ( caddytls . ConnectionPolicies , error ) {
2020-09-11 22:45:21 +03:00
// catch-all policies (those without any matcher) should be at the
// end, otherwise it nullifies any more specific policies
sort . SliceStable ( cps , func ( i , j int ) bool {
return cps [ j ] . MatchersRaw == nil && cps [ i ] . MatchersRaw != nil
} )
2020-03-18 06:00:45 +03:00
for i := 0 ; i < len ( cps ) ; i ++ {
2020-04-04 05:19:46 +03:00
// compare it to the others
2020-03-18 06:00:45 +03:00
for j := 0 ; j < len ( cps ) ; j ++ {
if j == i {
continue
}
// if they're exactly equal in every way, just keep one of them
if reflect . DeepEqual ( cps [ i ] , cps [ j ] ) {
cps = append ( cps [ : j ] , cps [ j + 1 : ] ... )
i --
break
}
caddytls: Refactor certificate selection policies (close #1575)
Certificate selection used to be a module, but this seems unnecessary,
especially since the built-in CustomSelectionPolicy allows quite complex
selection logic on a number of fields in certs. If we need to extend
that logic, we can, but I don't think there are SO many possibilities
that we need modules.
This update also allows certificate selection to choose between multiple
matching certs based on client compatibility and makes a number of other
improvements in the default cert selection logic, both here and in the
latest CertMagic.
The hardest part of this was the conn policy consolidation logic
(Caddyfile only, of course). We have to merge connection policies that
we can easily combine, because if two certs are manually loaded in a
Caddyfile site block, that produces two connection policies, and each
cert is tagged with a different tag, meaning only the first would ever
be selected. So given the same matchers, we can merge the two, but this
required improving the Tag selection logic to support multiple tags to
choose from, hence "tags" changed to "any_tag" or "all_tags" (but we
use any_tag in our Caddyfile logic).
Combining conn policies with conflicting settings is impossible, so
that should return an error if two policies with the exact same matchers
have non-empty settings that are not the same (the one exception being
any_tag which we can merge because the logic for them is to OR them).
It was a bit complicated. It seems to work in numerous tests I've
conducted, but we'll see how it pans out in the release candidates.
2020-04-02 05:49:35 +03:00
// if they have the same matcher, try to reconcile each field: either they must
// be identical, or we have to be able to combine them safely
if reflect . DeepEqual ( cps [ i ] . MatchersRaw , cps [ j ] . MatchersRaw ) {
if len ( cps [ i ] . ALPN ) > 0 &&
len ( cps [ j ] . ALPN ) > 0 &&
! reflect . DeepEqual ( cps [ i ] . ALPN , cps [ j ] . ALPN ) {
return nil , fmt . Errorf ( "two policies with same match criteria have conflicting ALPN: %v vs. %v" ,
cps [ i ] . ALPN , cps [ j ] . ALPN )
}
if len ( cps [ i ] . CipherSuites ) > 0 &&
len ( cps [ j ] . CipherSuites ) > 0 &&
! reflect . DeepEqual ( cps [ i ] . CipherSuites , cps [ j ] . CipherSuites ) {
return nil , fmt . Errorf ( "two policies with same match criteria have conflicting cipher suites: %v vs. %v" ,
cps [ i ] . CipherSuites , cps [ j ] . CipherSuites )
}
if cps [ i ] . ClientAuthentication == nil &&
cps [ j ] . ClientAuthentication != nil &&
! reflect . DeepEqual ( cps [ i ] . ClientAuthentication , cps [ j ] . ClientAuthentication ) {
return nil , fmt . Errorf ( "two policies with same match criteria have conflicting client auth configuration: %+v vs. %+v" ,
cps [ i ] . ClientAuthentication , cps [ j ] . ClientAuthentication )
}
if len ( cps [ i ] . Curves ) > 0 &&
len ( cps [ j ] . Curves ) > 0 &&
! reflect . DeepEqual ( cps [ i ] . Curves , cps [ j ] . Curves ) {
return nil , fmt . Errorf ( "two policies with same match criteria have conflicting curves: %v vs. %v" ,
cps [ i ] . Curves , cps [ j ] . Curves )
}
if cps [ i ] . DefaultSNI != "" &&
cps [ j ] . DefaultSNI != "" &&
cps [ i ] . DefaultSNI != cps [ j ] . DefaultSNI {
return nil , fmt . Errorf ( "two policies with same match criteria have conflicting default SNI: %s vs. %s" ,
cps [ i ] . DefaultSNI , cps [ j ] . DefaultSNI )
}
if cps [ i ] . ProtocolMin != "" &&
cps [ j ] . ProtocolMin != "" &&
cps [ i ] . ProtocolMin != cps [ j ] . ProtocolMin {
return nil , fmt . Errorf ( "two policies with same match criteria have conflicting min protocol: %s vs. %s" ,
cps [ i ] . ProtocolMin , cps [ j ] . ProtocolMin )
}
if cps [ i ] . ProtocolMax != "" &&
cps [ j ] . ProtocolMax != "" &&
cps [ i ] . ProtocolMax != cps [ j ] . ProtocolMax {
return nil , fmt . Errorf ( "two policies with same match criteria have conflicting max protocol: %s vs. %s" ,
cps [ i ] . ProtocolMax , cps [ j ] . ProtocolMax )
}
if cps [ i ] . CertSelection != nil && cps [ j ] . CertSelection != nil {
// merging fields other than AnyTag is not implemented
if ! reflect . DeepEqual ( cps [ i ] . CertSelection . SerialNumber , cps [ j ] . CertSelection . SerialNumber ) ||
! reflect . DeepEqual ( cps [ i ] . CertSelection . SubjectOrganization , cps [ j ] . CertSelection . SubjectOrganization ) ||
cps [ i ] . CertSelection . PublicKeyAlgorithm != cps [ j ] . CertSelection . PublicKeyAlgorithm ||
! reflect . DeepEqual ( cps [ i ] . CertSelection . AllTags , cps [ j ] . CertSelection . AllTags ) {
return nil , fmt . Errorf ( "two policies with same match criteria have conflicting cert selections: %+v vs. %+v" ,
cps [ i ] . CertSelection , cps [ j ] . CertSelection )
}
}
// by now we've decided that we can merge the two -- we'll keep i and drop j
if len ( cps [ i ] . ALPN ) == 0 && len ( cps [ j ] . ALPN ) > 0 {
cps [ i ] . ALPN = cps [ j ] . ALPN
}
if len ( cps [ i ] . CipherSuites ) == 0 && len ( cps [ j ] . CipherSuites ) > 0 {
cps [ i ] . CipherSuites = cps [ j ] . CipherSuites
}
if cps [ i ] . ClientAuthentication == nil && cps [ j ] . ClientAuthentication != nil {
cps [ i ] . ClientAuthentication = cps [ j ] . ClientAuthentication
}
if len ( cps [ i ] . Curves ) == 0 && len ( cps [ j ] . Curves ) > 0 {
cps [ i ] . Curves = cps [ j ] . Curves
}
if cps [ i ] . DefaultSNI == "" && cps [ j ] . DefaultSNI != "" {
cps [ i ] . DefaultSNI = cps [ j ] . DefaultSNI
}
if cps [ i ] . ProtocolMin == "" && cps [ j ] . ProtocolMin != "" {
cps [ i ] . ProtocolMin = cps [ j ] . ProtocolMin
}
if cps [ i ] . ProtocolMax == "" && cps [ j ] . ProtocolMax != "" {
cps [ i ] . ProtocolMax = cps [ j ] . ProtocolMax
}
if cps [ i ] . CertSelection == nil && cps [ j ] . CertSelection != nil {
// if j is the only one with a policy, move it over to i
cps [ i ] . CertSelection = cps [ j ] . CertSelection
} else if cps [ i ] . CertSelection != nil && cps [ j ] . CertSelection != nil {
// if both have one, then combine AnyTag
for _ , tag := range cps [ j ] . CertSelection . AnyTag {
2024-09-25 23:30:56 +03:00
if ! slices . Contains ( cps [ i ] . CertSelection . AnyTag , tag ) {
caddytls: Refactor certificate selection policies (close #1575)
Certificate selection used to be a module, but this seems unnecessary,
especially since the built-in CustomSelectionPolicy allows quite complex
selection logic on a number of fields in certs. If we need to extend
that logic, we can, but I don't think there are SO many possibilities
that we need modules.
This update also allows certificate selection to choose between multiple
matching certs based on client compatibility and makes a number of other
improvements in the default cert selection logic, both here and in the
latest CertMagic.
The hardest part of this was the conn policy consolidation logic
(Caddyfile only, of course). We have to merge connection policies that
we can easily combine, because if two certs are manually loaded in a
Caddyfile site block, that produces two connection policies, and each
cert is tagged with a different tag, meaning only the first would ever
be selected. So given the same matchers, we can merge the two, but this
required improving the Tag selection logic to support multiple tags to
choose from, hence "tags" changed to "any_tag" or "all_tags" (but we
use any_tag in our Caddyfile logic).
Combining conn policies with conflicting settings is impossible, so
that should return an error if two policies with the exact same matchers
have non-empty settings that are not the same (the one exception being
any_tag which we can merge because the logic for them is to OR them).
It was a bit complicated. It seems to work in numerous tests I've
conducted, but we'll see how it pans out in the release candidates.
2020-04-02 05:49:35 +03:00
cps [ i ] . CertSelection . AnyTag = append ( cps [ i ] . CertSelection . AnyTag , tag )
}
}
}
cps = append ( cps [ : j ] , cps [ j + 1 : ] ... )
i --
break
}
2020-03-18 06:00:45 +03:00
}
}
caddytls: Refactor certificate selection policies (close #1575)
Certificate selection used to be a module, but this seems unnecessary,
especially since the built-in CustomSelectionPolicy allows quite complex
selection logic on a number of fields in certs. If we need to extend
that logic, we can, but I don't think there are SO many possibilities
that we need modules.
This update also allows certificate selection to choose between multiple
matching certs based on client compatibility and makes a number of other
improvements in the default cert selection logic, both here and in the
latest CertMagic.
The hardest part of this was the conn policy consolidation logic
(Caddyfile only, of course). We have to merge connection policies that
we can easily combine, because if two certs are manually loaded in a
Caddyfile site block, that produces two connection policies, and each
cert is tagged with a different tag, meaning only the first would ever
be selected. So given the same matchers, we can merge the two, but this
required improving the Tag selection logic to support multiple tags to
choose from, hence "tags" changed to "any_tag" or "all_tags" (but we
use any_tag in our Caddyfile logic).
Combining conn policies with conflicting settings is impossible, so
that should return an error if two policies with the exact same matchers
have non-empty settings that are not the same (the one exception being
any_tag which we can merge because the logic for them is to OR them).
It was a bit complicated. It seems to work in numerous tests I've
conducted, but we'll see how it pans out in the release candidates.
2020-04-02 05:49:35 +03:00
return cps , nil
2020-03-18 06:00:45 +03:00
}
2020-02-17 08:24:20 +03:00
// appendSubrouteToRouteList appends the routes in subroute
// to the routeList, optionally qualified by matchers.
func appendSubrouteToRouteList ( routeList caddyhttp . RouteList ,
subroute * caddyhttp . Subroute ,
matcherSetsEnc [ ] caddy . ModuleMap ,
p sbAddrAssociation ,
2023-08-07 22:40:31 +03:00
warnings * [ ] caddyconfig . Warning ,
) caddyhttp . RouteList {
2020-04-02 23:20:30 +03:00
// nothing to do if... there's nothing to do
if len ( matcherSetsEnc ) == 0 && len ( subroute . Routes ) == 0 && subroute . Errors == nil {
return routeList
}
2022-10-12 18:27:08 +03:00
// No need to wrap the handlers in a subroute if this is the only server block
// and there is no matcher for it (doing so would produce unnecessarily nested
// JSON), *unless* there is a host matcher within this site block; if so, then
// we still need to wrap in a subroute because otherwise the host matcher from
// the inside of the site block would be a top-level host matcher, which is
// subject to auto-HTTPS (cert management), and using a host matcher within
// a site block is a valid, common pattern for excluding domains from cert
// management, leading to unexpected behavior; see issue #5124.
wrapInSubroute := true
2020-02-17 08:24:20 +03:00
if len ( matcherSetsEnc ) == 0 && len ( p . serverBlocks ) == 1 {
2022-10-12 18:27:08 +03:00
var hasHostMatcher bool
outer :
for _ , route := range subroute . Routes {
for _ , ms := range route . MatcherSetsRaw {
for matcherName := range ms {
if matcherName == "host" {
hasHostMatcher = true
break outer
}
}
}
}
wrapInSubroute = hasHostMatcher
}
if wrapInSubroute {
2020-04-02 23:20:30 +03:00
route := caddyhttp . Route {
// the semantics of a site block in the Caddyfile dictate
// that only the first matching one is evaluated, since
// site blocks do not cascade nor inherit
Terminal : true ,
}
if len ( matcherSetsEnc ) > 0 {
route . MatcherSetsRaw = matcherSetsEnc
}
if len ( subroute . Routes ) > 0 || subroute . Errors != nil {
route . HandlersRaw = [ ] json . RawMessage {
2020-02-17 08:24:20 +03:00
caddyconfig . JSONModuleObject ( subroute , "handler" , "subroute" , warnings ) ,
2020-04-02 23:20:30 +03:00
}
}
if len ( route . MatcherSetsRaw ) > 0 || len ( route . HandlersRaw ) > 0 {
routeList = append ( routeList , route )
}
2022-10-12 18:27:08 +03:00
} else {
routeList = append ( routeList , subroute . Routes ... )
2020-02-17 08:24:20 +03:00
}
2022-10-12 18:27:08 +03:00
2020-02-17 08:24:20 +03:00
return routeList
}
// buildSubroute turns the config values, which are expected to be routes
// into a clean and orderly subroute that has all the routes within it.
2023-01-19 00:04:41 +03:00
func buildSubroute ( routes [ ] ConfigValue , groupCounter counter , needsSorting bool ) ( * caddyhttp . Subroute , error ) {
if needsSorting {
for _ , val := range routes {
2024-09-25 23:30:56 +03:00
if ! slices . Contains ( directiveOrder , val . directive ) {
2023-05-13 17:04:42 +03:00
return nil , fmt . Errorf ( "directive '%s' is not an ordered HTTP handler, so it cannot be used here - try placing within a route block or using the order global option" , val . directive )
2023-01-19 00:04:41 +03:00
}
2020-01-17 03:08:52 +03:00
}
2023-01-19 00:04:41 +03:00
sortRoutes ( routes )
}
2020-01-17 03:08:52 +03:00
subroute := new ( caddyhttp . Subroute )
2020-02-04 23:04:34 +03:00
// some directives are mutually exclusive (only first matching
// instance should be evaluated); this is done by putting their
// routes in the same group
mutuallyExclusiveDirs := map [ string ] * struct {
count int
groupName string
} {
2020-01-17 03:08:52 +03:00
// as a special case, group rewrite directives so that they are mutually exclusive;
// this means that only the first matching rewrite will be evaluated, and that's
// probably a good thing, since there should never be a need to do more than one
// rewrite (I think?), and cascading rewrites smell bad... imagine these rewrites:
// rewrite /docs/json/* /docs/json/index.html
// rewrite /docs/* /docs/index.html
// (We use this on the Caddy website, or at least we did once.) The first rewrite's
// result is also matched by the second rewrite, making the first rewrite pointless.
// See issue #2959.
2020-02-04 23:04:34 +03:00
"rewrite" : { } ,
2020-01-17 03:08:52 +03:00
// handle blocks are also mutually exclusive by definition
2020-02-04 23:04:34 +03:00
"handle" : { } ,
// root just sets a variable, so if it was not mutually exclusive, intersecting
// root directives would overwrite previously-matched ones; they should not cascade
"root" : { } ,
}
2020-09-17 05:01:22 +03:00
// we need to deterministically loop over each of these directives
// in order to keep the group numbers consistent
keys := make ( [ ] string , 0 , len ( mutuallyExclusiveDirs ) )
for k := range mutuallyExclusiveDirs {
keys = append ( keys , k )
}
sort . Strings ( keys )
for _ , meDir := range keys {
info := mutuallyExclusiveDirs [ meDir ]
2020-02-04 23:04:34 +03:00
// see how many instances of the directive there are
for _ , r := range routes {
if r . directive == meDir {
info . count ++
if info . count > 1 {
break
}
}
}
// if there is more than one, put them in a group
2020-03-18 21:18:10 +03:00
// (special case: "rewrite" directive must always be in
// its own group--even if there is only one--because we
// do not want a rewrite to be consolidated into other
// adjacent routes that happen to have the same matcher,
// see caddyserver/caddy#3108 - because the implied
// intent of rewrite is to do an internal redirect,
// we can't assume that the request will continue to
// match the same matcher; anyway, giving a route a
// unique group name should keep it from consolidating)
if info . count > 1 || meDir == "rewrite" {
2020-02-04 23:04:34 +03:00
info . groupName = groupCounter . nextGroup ( )
}
}
// add all the routes piled in from directives
for _ , r := range routes {
// put this route into a group if it is mutually exclusive
if info , ok := mutuallyExclusiveDirs [ r . directive ] ; ok {
2020-01-17 03:08:52 +03:00
route := r . Value . ( caddyhttp . Route )
2020-02-04 23:04:34 +03:00
route . Group = info . groupName
2020-01-17 03:08:52 +03:00
r . Value = route
}
switch route := r . Value . ( type ) {
case caddyhttp . Subroute :
// if a route-class config value is actually a Subroute handler
// with nothing but a list of routes, then it is the intention
// of the directive to keep these handlers together and in this
// same order, but not necessarily in a subroute (if it wanted
// to keep them in a subroute, the directive would have returned
// a route with a Subroute as its handler); this is useful to
// keep multiple handlers/routes together and in the same order
// so that the sorting procedure we did above doesn't reorder them
if route . Errors != nil {
// if error handlers are also set, this is confusing; it's
// probably supposed to be wrapped in a Route and encoded
// as a regular handler route... programmer error.
panic ( "found subroute with more than just routes; perhaps it should have been wrapped in a route?" )
}
subroute . Routes = append ( subroute . Routes , route . Routes ... )
case caddyhttp . Route :
subroute . Routes = append ( subroute . Routes , route )
}
}
subroute . Routes = consolidateRoutes ( subroute . Routes )
return subroute , nil
}
2021-12-13 21:42:08 +03:00
// normalizeDirectiveName ensures directives that should be sorted
// at the same level are named the same before sorting happens.
func normalizeDirectiveName ( directive string ) string {
// As a special case, we want "handle_path" to be sorted
// at the same level as "handle", so we force them to use
// the same directive name after their parsing is complete.
// See https://github.com/caddyserver/caddy/issues/3675#issuecomment-678042377
if directive == "handle_path" {
directive = "handle"
}
return directive
}
2019-08-09 21:05:47 +03:00
// consolidateRoutes combines routes with the same properties
// (same matchers, same Terminal and Group settings) for a
// cleaner overall output.
func consolidateRoutes ( routes caddyhttp . RouteList ) caddyhttp . RouteList {
for i := 0 ; i < len ( routes ) - 1 ; i ++ {
2019-08-21 19:46:35 +03:00
if reflect . DeepEqual ( routes [ i ] . MatcherSetsRaw , routes [ i + 1 ] . MatcherSetsRaw ) &&
2019-08-09 21:05:47 +03:00
routes [ i ] . Terminal == routes [ i + 1 ] . Terminal &&
routes [ i ] . Group == routes [ i + 1 ] . Group {
// keep the handlers in the same order, then splice out repetitive route
2019-08-21 19:46:35 +03:00
routes [ i ] . HandlersRaw = append ( routes [ i ] . HandlersRaw , routes [ i + 1 ] . HandlersRaw ... )
2019-08-09 21:05:47 +03:00
routes = append ( routes [ : i + 1 ] , routes [ i + 2 : ] ... )
i --
}
}
return routes
}
2019-08-21 19:46:35 +03:00
func matcherSetFromMatcherToken (
2019-08-09 21:05:47 +03:00
tkn caddyfile . Token ,
2019-12-10 23:36:46 +03:00
matcherDefs map [ string ] caddy . ModuleMap ,
2019-08-09 21:05:47 +03:00
warnings * [ ] caddyconfig . Warning ,
2019-12-10 23:36:46 +03:00
) ( caddy . ModuleMap , bool , error ) {
2019-08-09 21:05:47 +03:00
// matcher tokens can be wildcards, simple path matchers,
// or refer to a pre-defined matcher by some name
if tkn . Text == "*" {
// match all requests == no matchers, so nothing to do
return nil , true , nil
2024-05-01 14:43:05 +03:00
}
// convenient way to specify a single path match
if strings . HasPrefix ( tkn . Text , "/" ) {
2019-12-10 23:36:46 +03:00
return caddy . ModuleMap {
2019-08-09 21:05:47 +03:00
"path" : caddyconfig . JSON ( caddyhttp . MatchPath { tkn . Text } , warnings ) ,
} , true , nil
2024-05-01 14:43:05 +03:00
}
// pre-defined matcher
if strings . HasPrefix ( tkn . Text , matcherPrefix ) {
2020-01-10 00:00:32 +03:00
m , ok := matcherDefs [ tkn . Text ]
2019-08-09 21:05:47 +03:00
if ! ok {
2020-01-10 00:00:32 +03:00
return nil , false , fmt . Errorf ( "unrecognized matcher name: %+v" , tkn . Text )
2019-08-09 21:05:47 +03:00
}
return m , true , nil
}
2024-05-01 14:43:05 +03:00
2019-08-09 21:05:47 +03:00
return nil , false , nil
}
2020-04-02 23:20:30 +03:00
func ( st * ServerType ) compileEncodedMatcherSets ( sblock serverBlock ) ( [ ] caddy . ModuleMap , error ) {
2019-08-09 21:05:47 +03:00
type hostPathPair struct {
hostm caddyhttp . MatchHost
pathm caddyhttp . MatchPath
}
// keep routes with common host and path matchers together
var matcherPairs [ ] * hostPathPair
2020-03-18 06:00:45 +03:00
var catchAllHosts bool
2024-09-30 19:55:03 +03:00
for _ , addr := range sblock . parsedKeys {
2019-08-09 21:05:47 +03:00
// choose a matcher pair that should be shared by this
// server block; if none exists yet, create one
var chosenMatcherPair * hostPathPair
for _ , mp := range matcherPairs {
if ( len ( mp . pathm ) == 0 && addr . Path == "" ) ||
( len ( mp . pathm ) == 1 && mp . pathm [ 0 ] == addr . Path ) {
chosenMatcherPair = mp
break
}
}
if chosenMatcherPair == nil {
chosenMatcherPair = new ( hostPathPair )
if addr . Path != "" {
chosenMatcherPair . pathm = [ ] string { addr . Path }
}
matcherPairs = append ( matcherPairs , chosenMatcherPair )
}
2020-03-18 06:00:45 +03:00
// if one of the keys has no host (i.e. is a catch-all for
// any hostname), then we need to null out the host matcher
// entirely so that it matches all hosts
if addr . Host == "" && ! catchAllHosts {
chosenMatcherPair . hostm = nil
catchAllHosts = true
}
if catchAllHosts {
continue
}
2019-08-09 21:05:47 +03:00
// add this server block's keys to the matcher
// pair if it doesn't already exist
2024-09-25 23:30:56 +03:00
if addr . Host != "" && ! slices . Contains ( chosenMatcherPair . hostm , addr . Host ) {
chosenMatcherPair . hostm = append ( chosenMatcherPair . hostm , addr . Host )
2019-08-09 21:05:47 +03:00
}
}
// iterate each pairing of host and path matchers and
// put them into a map for JSON encoding
var matcherSets [ ] map [ string ] caddyhttp . RequestMatcher
for _ , mp := range matcherPairs {
matcherSet := make ( map [ string ] caddyhttp . RequestMatcher )
if len ( mp . hostm ) > 0 {
matcherSet [ "host" ] = mp . hostm
}
if len ( mp . pathm ) > 0 {
matcherSet [ "path" ] = mp . pathm
}
if len ( matcherSet ) > 0 {
matcherSets = append ( matcherSets , matcherSet )
}
}
// finally, encode each of the matcher sets
2020-04-09 00:31:51 +03:00
matcherSetsEnc := make ( [ ] caddy . ModuleMap , 0 , len ( matcherSets ) )
2019-08-09 21:05:47 +03:00
for _ , ms := range matcherSets {
msEncoded , err := encodeMatcherSet ( ms )
if err != nil {
2020-04-02 23:20:30 +03:00
return nil , fmt . Errorf ( "server block %v: %v" , sblock . block . Keys , err )
2019-08-09 21:05:47 +03:00
}
matcherSetsEnc = append ( matcherSetsEnc , msEncoded )
}
return matcherSetsEnc , nil
}
2020-01-10 00:00:32 +03:00
func parseMatcherDefinitions ( d * caddyfile . Dispenser , matchers map [ string ] caddy . ModuleMap ) error {
2024-01-24 03:36:59 +03:00
d . Next ( ) // advance to the first token
2020-01-10 00:00:32 +03:00
2024-01-24 03:36:59 +03:00
// this is the "name" for "named matchers"
definitionName := d . Val ( )
if _ , ok := matchers [ definitionName ] ; ok {
return fmt . Errorf ( "matcher is defined more than once: %s" , definitionName )
}
matchers [ definitionName ] = make ( caddy . ModuleMap )
// given a matcher name and the tokens following it, parse
// the tokens as a matcher module and record it
makeMatcher := func ( matcherName string , tokens [ ] caddyfile . Token ) error {
2024-04-17 21:19:14 +03:00
// create a new dispenser from the tokens
dispenser := caddyfile . NewDispenser ( tokens )
// set the matcher name (without @) in the dispenser context so
// that matcher modules can access it to use it as their name
// (e.g. regexp matchers which use the name for capture groups)
dispenser . SetContext ( caddyfile . MatcherNameCtxKey , definitionName [ 1 : ] )
2024-01-24 03:36:59 +03:00
mod , err := caddy . GetModule ( "http.matchers." + matcherName )
if err != nil {
return fmt . Errorf ( "getting matcher module '%s': %v" , matcherName , err )
2020-01-10 00:00:32 +03:00
}
2024-01-24 03:36:59 +03:00
unm , ok := mod . New ( ) . ( caddyfile . Unmarshaler )
if ! ok {
return fmt . Errorf ( "matcher module '%s' is not a Caddyfile unmarshaler" , matcherName )
}
2024-04-17 21:19:14 +03:00
err = unm . UnmarshalCaddyfile ( dispenser )
2024-01-24 03:36:59 +03:00
if err != nil {
return err
}
rm , ok := unm . ( caddyhttp . RequestMatcher )
if ! ok {
return fmt . Errorf ( "matcher module '%s' is not a request matcher" , matcherName )
}
matchers [ definitionName ] [ matcherName ] = caddyconfig . JSON ( rm , nil )
return nil
}
2020-01-10 00:00:32 +03:00
2024-01-24 03:36:59 +03:00
// if the next token is quoted, we can assume it's not a matcher name
// and that it's probably an 'expression' matcher
if d . NextArg ( ) {
if d . Token ( ) . Quoted ( ) {
// since it was missing the matcher name, we insert a token
2024-05-01 14:43:05 +03:00
// in front of the expression token itself; we use Clone() to
// make the new token to keep the same the import location as
// the next token, if this is within a snippet or imported file.
// see https://github.com/caddyserver/caddy/issues/6287
expressionToken := d . Token ( ) . Clone ( )
expressionToken . Text = "expression"
err := makeMatcher ( "expression" , [ ] caddyfile . Token { expressionToken , d . Token ( ) } )
2019-09-30 18:11:30 +03:00
if err != nil {
2020-01-10 00:00:32 +03:00
return err
2019-09-30 18:11:30 +03:00
}
2022-09-02 06:12:37 +03:00
return nil
}
2024-01-24 03:36:59 +03:00
// if it wasn't quoted, then we need to rewind after calling
// d.NextArg() so the below properly grabs the matcher name
d . Prev ( )
}
2022-09-02 06:12:37 +03:00
2024-01-24 03:36:59 +03:00
// in case there are multiple instances of the same matcher, concatenate
// their tokens (we expect that UnmarshalCaddyfile should be able to
// handle more than one segment); otherwise, we'd overwrite other
// instances of the matcher in this set
tokensByMatcherName := make ( map [ string ] [ ] caddyfile . Token )
for nesting := d . Nesting ( ) ; d . NextArg ( ) || d . NextBlock ( nesting ) ; {
matcherName := d . Val ( )
tokensByMatcherName [ matcherName ] = append ( tokensByMatcherName [ matcherName ] , d . NextSegment ( ) ... )
}
for matcherName , tokens := range tokensByMatcherName {
err := makeMatcher ( matcherName , tokens )
if err != nil {
return err
2019-09-30 18:11:30 +03:00
}
}
2020-01-10 00:00:32 +03:00
return nil
2019-09-30 18:11:30 +03:00
}
2019-12-10 23:36:46 +03:00
func encodeMatcherSet ( matchers map [ string ] caddyhttp . RequestMatcher ) ( caddy . ModuleMap , error ) {
msEncoded := make ( caddy . ModuleMap )
2019-08-09 21:05:47 +03:00
for matcherName , val := range matchers {
jsonBytes , err := json . Marshal ( val )
if err != nil {
return nil , fmt . Errorf ( "marshaling matcher set %#v: %v" , matchers , err )
}
msEncoded [ matcherName ] = jsonBytes
}
return msEncoded , nil
}
2022-05-06 19:25:31 +03:00
// WasReplacedPlaceholderShorthand checks if a token string was
// likely a replaced shorthand of the known Caddyfile placeholder
// replacement outputs. Useful to prevent some user-defined map
// output destinations from overlapping with one of the
// predefined shorthands.
func WasReplacedPlaceholderShorthand ( token string ) string {
prev := ""
for i , item := range placeholderShorthands ( ) {
// only look at every 2nd item, which is the replacement
if i % 2 == 0 {
prev = item
continue
}
if strings . Trim ( token , "{}" ) == strings . Trim ( item , "{}" ) {
// we return the original shorthand so it
// can be used for an error message
return prev
}
}
return ""
}
2019-08-22 22:38:37 +03:00
// tryInt tries to convert val to an integer. If it fails,
// it downgrades the error to a warning and returns 0.
2022-08-02 23:39:09 +03:00
func tryInt ( val any , warnings * [ ] caddyconfig . Warning ) int {
2019-08-22 22:38:37 +03:00
intVal , ok := val . ( int )
if val != nil && ! ok && warnings != nil {
* warnings = append ( * warnings , caddyconfig . Warning { Message : "not an integer type" } )
2019-08-09 21:05:47 +03:00
}
2019-08-22 22:38:37 +03:00
return intVal
2019-08-09 21:05:47 +03:00
}
2022-08-02 23:39:09 +03:00
func tryString ( val any , warnings * [ ] caddyconfig . Warning ) string {
2020-02-27 02:01:47 +03:00
stringVal , ok := val . ( string )
if val != nil && ! ok && warnings != nil {
* warnings = append ( * warnings , caddyconfig . Warning { Message : "not a string type" } )
}
return stringVal
2021-05-08 01:18:17 +03:00
}
2022-08-02 23:39:09 +03:00
func tryDuration ( val any , warnings * [ ] caddyconfig . Warning ) caddy . Duration {
2021-05-08 01:18:17 +03:00
durationVal , ok := val . ( caddy . Duration )
if val != nil && ! ok && warnings != nil {
* warnings = append ( * warnings , caddyconfig . Warning { Message : "not a duration type" } )
}
return durationVal
2020-02-27 02:01:47 +03:00
}
2021-01-20 00:16:06 +03:00
// listenersUseAnyPortOtherThan returns true if there are any
// listeners in addresses that use a port which is not otherPort.
// Mostly borrowed from unexported method in caddyhttp package.
func listenersUseAnyPortOtherThan ( addresses [ ] string , otherPort string ) bool {
otherPortInt , err := strconv . Atoi ( otherPort )
if err != nil {
return false
}
for _ , lnAddr := range addresses {
laddrs , err := caddy . ParseNetworkAddress ( lnAddr )
if err != nil {
continue
}
if uint ( otherPortInt ) > laddrs . EndPort || uint ( otherPortInt ) < laddrs . StartPort {
return true
}
}
return false
}
2020-02-28 05:30:48 +03:00
// specificity returns len(s) minus any wildcards (*) and
2020-01-17 03:08:52 +03:00
// placeholders ({...}). Basically, it's a length count
// that penalizes the use of wildcards and placeholders.
// This is useful for comparing hostnames and paths.
// However, wildcards in paths are not a sure answer to
2020-02-28 05:30:48 +03:00
// the question of specificity. For example,
2020-01-17 03:08:52 +03:00
// '*.example.com' is clearly less specific than
// 'a.example.com', but is '/a' more or less specific
// than '/a*'?
func specificity ( s string ) int {
l := len ( s ) - strings . Count ( s , "*" )
for len ( s ) > 0 {
start := strings . Index ( s , "{" )
if start < 0 {
return l
}
end := strings . Index ( s [ start : ] , "}" ) + start + 1
if end <= start {
return l
}
l -= end - start
s = s [ end : ]
}
return l
}
type counter struct {
n * int
}
func ( c counter ) nextGroup ( ) string {
name := fmt . Sprintf ( "group%d" , * c . n )
* c . n ++
return name
2020-01-15 23:51:12 +03:00
}
2020-02-26 08:00:33 +03:00
type namedCustomLog struct {
2024-05-11 16:31:44 +03:00
name string
hostnames [ ] string
log * caddy . CustomLog
noHostname bool
2020-02-26 08:00:33 +03:00
}
2024-09-30 19:55:03 +03:00
// addressWithProtocols associates a listen address with
// the protocols to serve it with
type addressWithProtocols struct {
address string
protocols [ ] string
}
2020-02-28 05:30:48 +03:00
// sbAddrAssociation is a mapping from a list of
2024-09-30 19:55:03 +03:00
// addresses with protocols, and a list of server
// blocks that are served on those addresses.
2019-08-09 21:05:47 +03:00
type sbAddrAssociation struct {
2024-09-30 19:55:03 +03:00
addressesWithProtocols [ ] addressWithProtocols
serverBlocks [ ] serverBlock
2019-08-09 21:05:47 +03:00
}
2023-08-07 22:40:31 +03:00
const (
matcherPrefix = "@"
namedRouteKey = "named_route"
)
2020-01-10 00:00:32 +03:00
2019-08-09 21:05:47 +03:00
// Interface guard
var _ caddyfile . ServerType = ( * ServerType ) ( nil )