summaryrefslogtreecommitdiff
path: root/modules/caddyhttp/reverseproxy/caddyfile.go
diff options
context:
space:
mode:
authorMatthew Holt <mholt@users.noreply.github.com>2020-02-27 20:56:24 -0700
committerMatthew Holt <mholt@users.noreply.github.com>2020-02-27 21:04:28 -0700
commit260982b2dfc2cbf30c9b6e3a06f54e589344fc41 (patch)
tree06bdf23e20f274d2753f6398cab266fa3da674a9 /modules/caddyhttp/reverseproxy/caddyfile.go
parent0130b699df7a3e669a60732b465079cdd7a4addb (diff)
reverse_proxy: Allow use of URL to specify scheme
This makes it more convenient to configure quick proxies that use HTTPS but also introduces a lot of logical complexity. We have to do a lot of verification for consistency and errors. Path and query string is not supported (i.e. no rewriting). Scheme and port can be inferred from each other if HTTP(S)/80/443. If omitted, defaults to HTTP. Any explicit transport config must be consistent with the upstream schemes, and the upstream schemes must all match too. But, this change allows a config that used to require this: reverse_proxy example.com:443 { transport http { tls } } to be reduced to this: reverse_proxy https://example.com which is really nice syntactic sugar (and is reminiscent of Caddy 1).
Diffstat (limited to 'modules/caddyhttp/reverseproxy/caddyfile.go')
-rw-r--r--modules/caddyhttp/reverseproxy/caddyfile.go135
1 files changed, 130 insertions, 5 deletions
diff --git a/modules/caddyhttp/reverseproxy/caddyfile.go b/modules/caddyhttp/reverseproxy/caddyfile.go
index d08e7f1..e9962dc 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,95 @@ 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) {
+ // slight hack, to ensure a non-URL parses correctly (simplifies our code paths)
+ const undefinedScheme = "undefined"
+ if !strings.Contains(upstreamAddr, "://") {
+ upstreamAddr = undefinedScheme + "://" + upstreamAddr
+ }
+
+ // convenient way to get desired scheme, host, and port
+ toURL, err := url.Parse(upstreamAddr)
+ if err != nil {
+ return "", d.Errf("parsing upstream address: %v", err)
+ }
+ if toURL.Scheme == undefinedScheme {
+ toURL.Scheme = ""
+ }
+
+ // 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)")
+ }
+
+ // dial addresses always need a port, so if no port was
+ // specified, assume the default ports for HTTP(S)
+ if urlPort == "" {
+ var toPort string
+ if toURL.Scheme == "" {
+ // if no port or scheme is specified, we assume HTTP
+ toPort = "80"
+ } else if toURL.Scheme == "https" {
+ toPort = "443"
+ }
+ toURL.Host = net.JoinHostPort(toURL.Host, toPort)
+ }
+
+ // if port is known and scheme is not, set the scheme
+ if toURL.Scheme == "" {
+ if urlPort == "80" {
+ toURL.Scheme = "http"
+ } else if urlPort == "443" {
+ toURL.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 != "" && toURL.Scheme != commonScheme {
+ return "", d.Errf("for now, all proxy upstreams must use the same scheme (transport protocol); expecting '%s://' but got '%s://'",
+ commonScheme, toURL.Scheme)
+ }
+ commonScheme = toURL.Scheme
+
+ return toURL.Host, 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 +183,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 +484,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 +501,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 +509,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
}