diff options
Diffstat (limited to 'modules/caddyhttp/reverseproxy')
-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 |
4 files changed, 162 insertions, 19 deletions
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 |