@marcel91200 / caddy Mirror · read-only / commits / 260982b2
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 14, 05:15 UTC

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).

Matthew Holt committed Feb 27, 2020 at 20:56 UTC 260982b2dfc2cbf30c9b6e3a06f54e589344fc41
1 file changed +130 -5
modules/caddyhttp/reverseproxy/caddyfile.go
+130 -5
@@ -15,7 +15,10 @@
15 package reverseproxy
16
17 import (
18 + "net"
19 "net/http"
20 + "net/url"
21 + "reflect"
22 "strconv"
23 "strings"
24 "time"
@@ -81,10 +84,95 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
84 // }
85 // }
86 //
87 +// Proxy upstream addresses should be network dial addresses such
88 +// as `host:port`, or a URL such as `scheme://host:port`. Scheme
89 +// and port may be inferred from other parts of the address/URL; if
90 +// either are missing, defaults to HTTP.
91 func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
92 + // currently, all backends must use the same scheme/protocol (the
93 + // underlying JSON does not yet support per-backend transports)
94 + var commonScheme string
95 +
96 + // we'll wait until the very end of parsing before
97 + // validating and encoding the transport
98 + var transport http.RoundTripper
99 + var transportModuleName string
100 +
101 + // TODO: the logic in this function is kind of sensitive, we need
102 + // to write tests before making any more changes to it
103 + upstreamDialAddress := func(upstreamAddr string) (string, error) {
104 + // slight hack, to ensure a non-URL parses correctly (simplifies our code paths)
105 + const undefinedScheme = "undefined"
106 + if !strings.Contains(upstreamAddr, "://") {
107 + upstreamAddr = undefinedScheme + "://" + upstreamAddr
108 + }
109 +
110 + // convenient way to get desired scheme, host, and port
111 + toURL, err := url.Parse(upstreamAddr)
112 + if err != nil {
113 + return "", d.Errf("parsing upstream address: %v", err)
114 + }
115 + if toURL.Scheme == undefinedScheme {
116 + toURL.Scheme = ""
117 + }
118 +
119 + // there is currently no way to perform a URL rewrite between choosing
120 + // a backend and proxying to it, so we cannot allow extra components
121 + // in backend URLs
122 + if toURL.Path != "" || toURL.RawQuery != "" || toURL.Fragment != "" {
123 + return "", d.Err("for now, URLs for proxy upstreams only support scheme, host, and port components")
124 + }
125 +
126 + // ensure the port and scheme aren't in conflict
127 + urlPort := toURL.Port()
128 + if toURL.Scheme == "http" && urlPort == "443" {
129 + return "", d.Err("upstream address has conflicting scheme (http://) and port (:443, the HTTPS port)")
130 + }
131 + if toURL.Scheme == "https" && urlPort == "80" {
132 + return "", d.Err("upstream address has conflicting scheme (https://) and port (:80, the HTTP port)")
133 + }
134 +
135 + // dial addresses always need a port, so if no port was
136 + // specified, assume the default ports for HTTP(S)
137 + if urlPort == "" {
138 + var toPort string
139 + if toURL.Scheme == "" {
140 + // if no port or scheme is specified, we assume HTTP
141 + toPort = "80"
142 + } else if toURL.Scheme == "https" {
143 + toPort = "443"
144 + }
145 + toURL.Host = net.JoinHostPort(toURL.Host, toPort)
146 + }
147 +
148 + // if port is known and scheme is not, set the scheme
149 + if toURL.Scheme == "" {
150 + if urlPort == "80" {
151 + toURL.Scheme = "http"
152 + } else if urlPort == "443" {
153 + toURL.Scheme = "https"
154 + }
155 + }
156 +
157 + // the underlying JSON does not yet support different
158 + // transports (protocols or schemes) to each backend,
159 + // so we remember the last one we see and compare them
160 + if commonScheme != "" && toURL.Scheme != commonScheme {
161 + return "", d.Errf("for now, all proxy upstreams must use the same scheme (transport protocol); expecting '%s://' but got '%s://'",
162 + commonScheme, toURL.Scheme)
163 + }
164 + commonScheme = toURL.Scheme
165 +
166 + return toURL.Host, nil
167 + }
168 +
169 for d.Next() {
170 for _, up := range d.RemainingArgs() {
87 - h.Upstreams = append(h.Upstreams, &Upstream{Dial: up})
171 + dialAddr, err := upstreamDialAddress(up)
172 + if err != nil {
173 + return err
174 + }
175 + h.Upstreams = append(h.Upstreams, &Upstream{Dial: dialAddr})
176 }
177
178 for d.NextBlock(0) {
@@ -95,7 +183,11 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
183 return d.ArgErr()
184 }
185 for _, up := range args {
98 - h.Upstreams = append(h.Upstreams, &Upstream{Dial: up})
186 + dialAddr, err := upstreamDialAddress(up)
187 + if err != nil {
188 + return err
189 + }
190 + h.Upstreams = append(h.Upstreams, &Upstream{Dial: dialAddr})
191 }
192
193 case "lb_policy":
@@ -392,8 +484,8 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
484 if h.TransportRaw != nil {
485 return d.Err("transport already specified")
486 }
395 - name := d.Val()
396 - mod, err := caddy.GetModule("http.reverse_proxy.transport." + name)
487 + transportModuleName = d.Val()
488 + mod, err := caddy.GetModule("http.reverse_proxy.transport." + transportModuleName)
489 if err != nil {
490 return d.Errf("getting transport module '%s': %v", mod, err)
491 }
@@ -409,7 +501,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
501 if !ok {
502 return d.Errf("module %s is not a RoundTripper", mod)
503 }
412 - h.TransportRaw = caddyconfig.JSONModuleObject(rt, "protocol", name, nil)
504 + transport = rt
505
506 default:
507 return d.Errf("unrecognized subdirective %s", d.Val())
@@ -417,6 +509,39 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
509 }
510 }
511
512 + // if the scheme inferred from the backends' addresses is
513 + // HTTPS, we will need a non-nil transport to enable TLS
514 + if commonScheme == "https" && transport == nil {
515 + transport = new(HTTPTransport)
516 + transportModuleName = "http"
517 + }
518 +
519 + // verify transport configuration, and finally encode it
520 + if transport != nil {
521 + // TODO: these two cases are identical, but I don't know how to reuse the code
522 + switch ht := transport.(type) {
523 + case *HTTPTransport:
524 + if commonScheme == "https" && ht.TLS == nil {
525 + ht.TLS = new(TLSConfig)
526 + }
527 + if ht.TLS != nil && commonScheme == "http" {
528 + return d.Errf("upstream address scheme is HTTP but transport is configured for HTTP+TLS (HTTPS)")
529 + }
530 +
531 + case *NTLMTransport:
532 + if commonScheme == "https" && ht.TLS == nil {
533 + ht.TLS = new(TLSConfig)
534 + }
535 + if ht.TLS != nil && commonScheme == "http" {
536 + return d.Errf("upstream address scheme is HTTP but transport is configured for HTTP+TLS (HTTPS)")
537 + }
538 + }
539 +
540 + if !reflect.DeepEqual(transport, new(HTTPTransport)) {
541 + h.TransportRaw = caddyconfig.JSONModuleObject(transport, "protocol", transportModuleName, nil)
542 + }
543 + }
544 +
545 return nil
546 }
547