diff options
Diffstat (limited to 'modules/caddyhttp')
-rw-r--r-- | modules/caddyhttp/caddyhttp.go | 9 | ||||
-rw-r--r-- | modules/caddyhttp/encode/encode.go | 2 | ||||
-rw-r--r-- | modules/caddyhttp/fileserver/matcher.go | 2 | ||||
-rw-r--r-- | modules/caddyhttp/matchers.go | 2 | ||||
-rw-r--r-- | modules/caddyhttp/reverseproxy/caddyfile.go | 146 | ||||
-rw-r--r-- | modules/caddyhttp/reverseproxy/command.go | 21 | ||||
-rw-r--r-- | modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go | 12 | ||||
-rw-r--r-- | modules/caddyhttp/reverseproxy/hosts.go | 2 | ||||
-rw-r--r-- | modules/caddyhttp/server.go | 4 | ||||
-rw-r--r-- | modules/caddyhttp/templates/templates.go | 2 |
10 files changed, 177 insertions, 25 deletions
diff --git a/modules/caddyhttp/caddyhttp.go b/modules/caddyhttp/caddyhttp.go index 94b2eee..6ad70f5 100644 --- a/modules/caddyhttp/caddyhttp.go +++ b/modules/caddyhttp/caddyhttp.go @@ -29,6 +29,7 @@ import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddytls" + "github.com/caddyserver/certmagic" "github.com/lucas-clemente/quic-go/http3" "go.uber.org/zap" ) @@ -112,6 +113,10 @@ type App struct { // affect functionality. Servers map[string]*Server `json:"servers,omitempty"` + // DefaultSNI if set configures all certificate lookups to fallback to use + // this SNI name if a more specific certificate could not be found + DefaultSNI string `json:"default_sni,omitempty"` + servers []*http.Server h3servers []*http3.Server h3listeners []net.PacketConn @@ -145,8 +150,10 @@ func (app *App) Provision(ctx caddy.Context) error { repl := caddy.NewReplacer() + certmagic.Default.DefaultServerName = app.DefaultSNI + // this provisions the matchers for each route, - // and prepares auto HTTP->HTTP redirects, and + // and prepares auto HTTP->HTTPS redirects, and // is required before we provision each server err = app.automaticHTTPSPhase1(ctx, repl) if err != nil { diff --git a/modules/caddyhttp/encode/encode.go b/modules/caddyhttp/encode/encode.go index c3a1c23..52205aa 100644 --- a/modules/caddyhttp/encode/encode.go +++ b/modules/caddyhttp/encode/encode.go @@ -282,7 +282,7 @@ func acceptedEncodings(r *http.Request) []string { } // encodings with q-factor of 0 are not accepted; - // use a small theshold to account for float precision + // use a small threshold to account for float precision if qFactor < 0.00001 { continue } diff --git a/modules/caddyhttp/fileserver/matcher.go b/modules/caddyhttp/fileserver/matcher.go index ed5c102..1915fb7 100644 --- a/modules/caddyhttp/fileserver/matcher.go +++ b/modules/caddyhttp/fileserver/matcher.go @@ -51,7 +51,7 @@ type MatchFile struct { Root string `json:"root,omitempty"` // The list of files to try. Each path here is - // considered relatice to Root. If nil, the request + // considered related to Root. If nil, the request // URL's path will be assumed. Files and // directories are treated distinctly, so to match // a directory, the filepath MUST end in a forward diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go index 3c357c6..6b57ead 100644 --- a/modules/caddyhttp/matchers.go +++ b/modules/caddyhttp/matchers.go @@ -680,7 +680,7 @@ func (m MatchRemoteIP) Match(r *http.Request) bool { return false } -// MatchRegexp is an embeddable type for matching +// MatchRegexp is an embedable type for matching // using regular expressions. It adds placeholders // to the request's replacer. type MatchRegexp struct { diff --git a/modules/caddyhttp/reverseproxy/caddyfile.go b/modules/caddyhttp/reverseproxy/caddyfile.go index d08e7f1..9ff9dce 100644 --- a/modules/caddyhttp/reverseproxy/caddyfile.go +++ b/modules/caddyhttp/reverseproxy/caddyfile.go @@ -15,7 +15,10 @@ package reverseproxy import ( + "net" "net/http" + "net/url" + "reflect" "strconv" "strings" "time" @@ -81,10 +84,106 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) // } // } // +// Proxy upstream addresses should be network dial addresses such +// as `host:port`, or a URL such as `scheme://host:port`. Scheme +// and port may be inferred from other parts of the address/URL; if +// either are missing, defaults to HTTP. func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + // currently, all backends must use the same scheme/protocol (the + // underlying JSON does not yet support per-backend transports) + var commonScheme string + + // we'll wait until the very end of parsing before + // validating and encoding the transport + var transport http.RoundTripper + var transportModuleName string + + // TODO: the logic in this function is kind of sensitive, we need + // to write tests before making any more changes to it + upstreamDialAddress := func(upstreamAddr string) (string, error) { + var network, scheme, host, port string + + if strings.Contains(upstreamAddr, "://") { + toURL, err := url.Parse(upstreamAddr) + if err != nil { + return "", d.Errf("parsing upstream URL: %v", err) + } + + // there is currently no way to perform a URL rewrite between choosing + // a backend and proxying to it, so we cannot allow extra components + // in backend URLs + if toURL.Path != "" || toURL.RawQuery != "" || toURL.Fragment != "" { + return "", d.Err("for now, URLs for proxy upstreams only support scheme, host, and port components") + } + + // ensure the port and scheme aren't in conflict + urlPort := toURL.Port() + if toURL.Scheme == "http" && urlPort == "443" { + return "", d.Err("upstream address has conflicting scheme (http://) and port (:443, the HTTPS port)") + } + if toURL.Scheme == "https" && urlPort == "80" { + return "", d.Err("upstream address has conflicting scheme (https://) and port (:80, the HTTP port)") + } + + // if port is missing, attempt to infer from scheme + if toURL.Port() == "" { + var toPort string + switch toURL.Scheme { + case "", "http": + toPort = "80" + case "https": + toPort = "443" + } + toURL.Host = net.JoinHostPort(toURL.Hostname(), toPort) + } + + scheme, host, port = toURL.Scheme, toURL.Hostname(), toURL.Port() + } else { + // extract network manually, since caddy.ParseNetworkAddress() will always add one + if idx := strings.Index(upstreamAddr, "/"); idx >= 0 { + network = strings.ToLower(strings.TrimSpace(upstreamAddr[:idx])) + upstreamAddr = upstreamAddr[idx+1:] + } + var err error + host, port, err = net.SplitHostPort(upstreamAddr) + if err != nil { + host = upstreamAddr + } + } + + // if scheme is not set, we may be able to infer it from a known port + if scheme == "" { + if port == "80" { + scheme = "http" + } else if port == "443" { + scheme = "https" + } + } + + // the underlying JSON does not yet support different + // transports (protocols or schemes) to each backend, + // so we remember the last one we see and compare them + if commonScheme != "" && scheme != commonScheme { + return "", d.Errf("for now, all proxy upstreams must use the same scheme (transport protocol); expecting '%s://' but got '%s://'", + commonScheme, scheme) + } + commonScheme = scheme + + // for simplest possible config, we only need to include + // the network portion if the user specified one + if network != "" { + return caddy.JoinNetworkAddress(network, host, port), nil + } + return net.JoinHostPort(host, port), nil + } + for d.Next() { for _, up := range d.RemainingArgs() { - h.Upstreams = append(h.Upstreams, &Upstream{Dial: up}) + dialAddr, err := upstreamDialAddress(up) + if err != nil { + return err + } + h.Upstreams = append(h.Upstreams, &Upstream{Dial: dialAddr}) } for d.NextBlock(0) { @@ -95,7 +194,11 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return d.ArgErr() } for _, up := range args { - h.Upstreams = append(h.Upstreams, &Upstream{Dial: up}) + dialAddr, err := upstreamDialAddress(up) + if err != nil { + return err + } + h.Upstreams = append(h.Upstreams, &Upstream{Dial: dialAddr}) } case "lb_policy": @@ -392,8 +495,8 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if h.TransportRaw != nil { return d.Err("transport already specified") } - name := d.Val() - mod, err := caddy.GetModule("http.reverse_proxy.transport." + name) + transportModuleName = d.Val() + mod, err := caddy.GetModule("http.reverse_proxy.transport." + transportModuleName) if err != nil { return d.Errf("getting transport module '%s': %v", mod, err) } @@ -409,7 +512,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if !ok { return d.Errf("module %s is not a RoundTripper", mod) } - h.TransportRaw = caddyconfig.JSONModuleObject(rt, "protocol", name, nil) + transport = rt default: return d.Errf("unrecognized subdirective %s", d.Val()) @@ -417,6 +520,39 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { } } + // if the scheme inferred from the backends' addresses is + // HTTPS, we will need a non-nil transport to enable TLS + if commonScheme == "https" && transport == nil { + transport = new(HTTPTransport) + transportModuleName = "http" + } + + // verify transport configuration, and finally encode it + if transport != nil { + // TODO: these two cases are identical, but I don't know how to reuse the code + switch ht := transport.(type) { + case *HTTPTransport: + if commonScheme == "https" && ht.TLS == nil { + ht.TLS = new(TLSConfig) + } + if ht.TLS != nil && commonScheme == "http" { + return d.Errf("upstream address scheme is HTTP but transport is configured for HTTP+TLS (HTTPS)") + } + + case *NTLMTransport: + if commonScheme == "https" && ht.TLS == nil { + ht.TLS = new(TLSConfig) + } + if ht.TLS != nil && commonScheme == "http" { + return d.Errf("upstream address scheme is HTTP but transport is configured for HTTP+TLS (HTTPS)") + } + } + + if !reflect.DeepEqual(transport, new(HTTPTransport)) { + h.TransportRaw = caddyconfig.JSONModuleObject(transport, "protocol", transportModuleName, nil) + } + } + return nil } diff --git a/modules/caddyhttp/reverseproxy/command.go b/modules/caddyhttp/reverseproxy/command.go index 462be1b..6f70d14 100644 --- a/modules/caddyhttp/reverseproxy/command.go +++ b/modules/caddyhttp/reverseproxy/command.go @@ -36,7 +36,7 @@ func init() { caddycmd.RegisterCommand(caddycmd.Command{ Name: "reverse-proxy", Func: cmdReverseProxy, - Usage: "[--from <addr>] [--to <addr>]", + Usage: "[--from <addr>] [--to <addr>] [--change-host-header]", Short: "A quick and production-ready reverse proxy", Long: ` A simple but production-ready reverse proxy. Useful for quick deployments, @@ -46,11 +46,16 @@ Simply shuttles HTTP traffic from the --from address to the --to address. If the --from address has a domain name, Caddy will attempt to serve the proxy over HTTPS with a certificate. + +If --change-host-header is set, the Host header on the request will be modified +from its original incoming value to the address of the upstream. (Otherwise, by +default, all incoming headers are passed through unmodified.) `, Flags: func() *flag.FlagSet { fs := flag.NewFlagSet("file-server", flag.ExitOnError) - fs.String("from", "", "Address to receive traffic on") - fs.String("to", "", "Upstream address to proxy traffic to") + fs.String("from", "", "Address on which to receive traffic") + fs.String("to", "", "Upstream address to which to to proxy traffic") + fs.Bool("change-host-header", false, "Set upstream Host header to address of upstream") return fs }(), }) @@ -59,6 +64,7 @@ proxy over HTTPS with a certificate. func cmdReverseProxy(fs caddycmd.Flags) (int, error) { from := fs.String("from") to := fs.String("to") + changeHost := fs.Bool("change-host-header") if from == "" { from = "localhost:" + httpcaddyfile.DefaultPort @@ -97,13 +103,16 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) { handler := Handler{ TransportRaw: caddyconfig.JSONModuleObject(ht, "protocol", "http", nil), Upstreams: UpstreamPool{{Dial: toURL.Host}}, - Headers: &headers.Handler{ + } + + if changeHost { + handler.Headers = &headers.Handler{ Request: &headers.HeaderOps{ Set: http.Header{ - "Host": []string{"{http.reverse_proxy.upstream.host}"}, + "Host": []string{"{http.reverse_proxy.upstream.hostport}"}, }, }, - }, + } } route := caddyhttp.Route{ diff --git a/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go b/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go index 8c9fd38..81fd48e 100644 --- a/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go +++ b/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go @@ -105,7 +105,7 @@ func (t *Transport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { // } // // Thus, this directive produces multiple handlers, each with a different -// matcher because multiple consecutive hgandlers are necessary to support +// matcher because multiple consecutive handlers are necessary to support // the common PHP use case. If this "common" config is not compatible // with a user's PHP requirements, they can use a manual approach based // on the example above to configure it precisely as they need. @@ -167,14 +167,10 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error // either way, strip the matcher token and pass // the remaining tokens to the unmarshaler so that // we can gain the rest of the reverse_proxy syntax - userMatcherSet, hasUserMatcher, err := h.MatcherToken() + userMatcherSet, err := h.ExtractMatcherSet() if err != nil { return nil, err } - if hasUserMatcher { - h.Dispenser.Delete() // strip matcher token - } - h.Dispenser.Reset() // pretend this lookahead never happened // set up the transport for FastCGI, and specifically PHP fcgiTransport := Transport{SplitPath: ".php"} @@ -186,6 +182,8 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error // the rest of the config is specified by the user // using the reverse_proxy directive syntax + // TODO: this can overwrite our fcgiTransport that we encoded and + // set on the rpHandler... even with a non-fastcgi transport! err = rpHandler.UnmarshalCaddyfile(h.Dispenser) if err != nil { return nil, err @@ -204,7 +202,7 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error // the user's matcher is a prerequisite for ours, so // wrap ours in a subroute and return that - if hasUserMatcher { + if userMatcherSet != nil { return []httpcaddyfile.ConfigValue{ { Class: "route", diff --git a/modules/caddyhttp/reverseproxy/hosts.go b/modules/caddyhttp/reverseproxy/hosts.go index 54de5a8..602aab2 100644 --- a/modules/caddyhttp/reverseproxy/hosts.go +++ b/modules/caddyhttp/reverseproxy/hosts.go @@ -27,7 +27,7 @@ import ( // Host represents a remote host which can be proxied to. // Its methods must be safe for concurrent use. type Host interface { - // NumRequests returns the numnber of requests + // NumRequests returns the number of requests // currently in process with the host. NumRequests() int diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index 580449b..1e22790 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -94,7 +94,9 @@ type Server struct { // client authentication. StrictSNIHost *bool `json:"strict_sni_host,omitempty"` - // Logs customizes how access logs are handled in this server. + // Customizes how access logs are handled in this server. To + // minimally enable access logs, simply set this to a non-null, + // empty struct. Logs *ServerLogConfig `json:"logs,omitempty"` // Enable experimental HTTP/3 support. Note that HTTP/3 is not a diff --git a/modules/caddyhttp/templates/templates.go b/modules/caddyhttp/templates/templates.go index 94764bf..cf0908d 100644 --- a/modules/caddyhttp/templates/templates.go +++ b/modules/caddyhttp/templates/templates.go @@ -254,7 +254,7 @@ func (t *Templates) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy rec.Header().Del("Accept-Ranges") // we don't know ranges for dynamically-created content rec.Header().Del("Last-Modified") // useless for dynamic content since it's always changing - // we don't know a way to guickly generate etag for dynamic content, + // we don't know a way to quickly generate etag for dynamic content, // and weak etags still cause browsers to rely on it even after a // refresh, so disable them until we find a better way to do this rec.Header().Del("Etag") |