15
package reverseproxy
16
17
import (
18
+ "net"
19
"net/http"
20
+ "net/url"
21
+ "reflect"
22
"strconv"
23
"strings"
24
"time"
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) {
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":
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
}
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())
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