@marcel91200 / caddy Mirror · read-only / commits / 0830fbad
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

Reconcile upstream dial addresses and request host/URL information

My goodness that was complicated Blessed be request.Context Sort of

Matthew Holt committed Sep 5, 2019 at 13:14 UTC 0830fbad0347ead1dbea60e664556b263e44653f
9 files changed +237 -183
listeners.go
+9 -9
@@ -165,19 +165,19 @@ var (
165 listenersMu sync.Mutex
166 )
167
168 -// ParseListenAddr parses addr, a string of the form "network/host:port"
168 +// ParseNetworkAddress parses addr, a string of the form "network/host:port"
169 // (with any part optional) into its component parts. Because a port can
170 // also be a port range, there may be multiple addresses returned.
171 -func ParseListenAddr(addr string) (network string, addrs []string, err error) {
171 +func ParseNetworkAddress(addr string) (network string, addrs []string, err error) {
172 var host, port string
173 - network, host, port, err = SplitListenAddr(addr)
173 + network, host, port, err = SplitNetworkAddress(addr)
174 if network == "" {
175 network = "tcp"
176 }
177 if err != nil {
178 return
179 }
180 - if network == "unix" {
180 + if network == "unix" || network == "unixgram" || network == "unixpacket" {
181 addrs = []string{host}
182 return
183 }
@@ -204,14 +204,14 @@ func ParseListenAddr(addr string) (network string, addrs []string, err error) {
204 return
205 }
206
207 -// SplitListenAddr splits a into its network, host, and port components.
207 +// SplitNetworkAddress splits a into its network, host, and port components.
208 // Note that port may be a port range, or omitted for unix sockets.
209 -func SplitListenAddr(a string) (network, host, port string, err error) {
209 +func SplitNetworkAddress(a string) (network, host, port string, err error) {
210 if idx := strings.Index(a, "/"); idx >= 0 {
211 network = strings.ToLower(strings.TrimSpace(a[:idx]))
212 a = a[idx+1:]
213 }
214 - if network == "unix" {
214 + if network == "unix" || network == "unixgram" || network == "unixpacket" {
215 host = a
216 return
217 }
@@ -219,11 +219,11 @@ func SplitListenAddr(a string) (network, host, port string, err error) {
219 return
220 }
221
222 -// JoinListenAddr combines network, host, and port into a single
222 +// JoinNetworkAddress combines network, host, and port into a single
223 // address string of the form "network/host:port". Port may be a
224 // port range. For unix sockets, the network should be "unix" and
225 // the path to the socket should be given in the host argument.
226 -func JoinListenAddr(network, host, port string) string {
226 +func JoinNetworkAddress(network, host, port string) string {
227 var a string
228 if network != "" {
229 a = network + "/"
listeners_test.go
+16 -6
@@ -19,7 +19,7 @@ import (
19 "testing"
20 )
21
22 -func TestSplitListenerAddr(t *testing.T) {
22 +func TestSplitNetworkAddress(t *testing.T) {
23 for i, tc := range []struct {
24 input string
25 expectNetwork string
@@ -67,8 +67,18 @@ func TestSplitListenerAddr(t *testing.T) {
67 expectNetwork: "unix",
68 expectHost: "/foo/bar",
69 },
70 + {
71 + input: "unixgram//foo/bar",
72 + expectNetwork: "unixgram",
73 + expectHost: "/foo/bar",
74 + },
75 + {
76 + input: "unixpacket//foo/bar",
77 + expectNetwork: "unixpacket",
78 + expectHost: "/foo/bar",
79 + },
80 } {
71 - actualNetwork, actualHost, actualPort, err := SplitListenAddr(tc.input)
81 + actualNetwork, actualHost, actualPort, err := SplitNetworkAddress(tc.input)
82 if tc.expectErr && err == nil {
83 t.Errorf("Test %d: Expected error but got: %v", i, err)
84 }
@@ -87,7 +97,7 @@ func TestSplitListenerAddr(t *testing.T) {
97 }
98 }
99
90 -func TestJoinListenerAddr(t *testing.T) {
100 +func TestJoinNetworkAddress(t *testing.T) {
101 for i, tc := range []struct {
102 network, host, port string
103 expect string
@@ -129,14 +139,14 @@ func TestJoinListenerAddr(t *testing.T) {
139 expect: "unix//foo/bar",
140 },
141 } {
132 - actual := JoinListenAddr(tc.network, tc.host, tc.port)
142 + actual := JoinNetworkAddress(tc.network, tc.host, tc.port)
143 if actual != tc.expect {
144 t.Errorf("Test %d: Expected '%s' but got '%s'", i, tc.expect, actual)
145 }
146 }
147 }
148
139 -func TestParseListenerAddr(t *testing.T) {
149 +func TestParseNetworkAddress(t *testing.T) {
150 for i, tc := range []struct {
151 input string
152 expectNetwork string
@@ -194,7 +204,7 @@ func TestParseListenerAddr(t *testing.T) {
204 expectAddrs: []string{"localhost:0"},
205 },
206 } {
197 - actualNetwork, actualAddrs, err := ParseListenAddr(tc.input)
207 + actualNetwork, actualAddrs, err := ParseNetworkAddress(tc.input)
208 if tc.expectErr && err == nil {
209 t.Errorf("Test %d: Expected error but got: %v", i, err)
210 }
modules/caddyhttp/caddyhttp.go
+6 -6
@@ -108,7 +108,7 @@ func (app *App) Validate() error {
108 lnAddrs := make(map[string]string)
109 for srvName, srv := range app.Servers {
110 for _, addr := range srv.Listen {
111 - netw, expanded, err := caddy.ParseListenAddr(addr)
111 + netw, expanded, err := caddy.ParseNetworkAddress(addr)
112 if err != nil {
113 return fmt.Errorf("invalid listener address '%s': %v", addr, err)
114 }
@@ -149,7 +149,7 @@ func (app *App) Start() error {
149 }
150
151 for _, lnAddr := range srv.Listen {
152 - network, addrs, err := caddy.ParseListenAddr(lnAddr)
152 + network, addrs, err := caddy.ParseNetworkAddress(lnAddr)
153 if err != nil {
154 return fmt.Errorf("%s: parsing listen address '%s': %v", srvName, lnAddr, err)
155 }
@@ -309,7 +309,7 @@ func (app *App) automaticHTTPS() error {
309
310 // create HTTP->HTTPS redirects
311 for _, addr := range srv.Listen {
312 - netw, host, port, err := caddy.SplitListenAddr(addr)
312 + netw, host, port, err := caddy.SplitNetworkAddress(addr)
313 if err != nil {
314 return fmt.Errorf("%s: invalid listener address: %v", srvName, addr)
315 }
@@ -318,7 +318,7 @@ func (app *App) automaticHTTPS() error {
318 if httpPort == 0 {
319 httpPort = DefaultHTTPPort
320 }
321 - httpRedirLnAddr := caddy.JoinListenAddr(netw, host, strconv.Itoa(httpPort))
321 + httpRedirLnAddr := caddy.JoinNetworkAddress(netw, host, strconv.Itoa(httpPort))
322 lnAddrMap[httpRedirLnAddr] = struct{}{}
323
324 if parts := strings.SplitN(port, "-", 2); len(parts) == 2 {
@@ -361,7 +361,7 @@ func (app *App) automaticHTTPS() error {
361 var lnAddrs []string
362 mapLoop:
363 for addr := range lnAddrMap {
364 - netw, addrs, err := caddy.ParseListenAddr(addr)
364 + netw, addrs, err := caddy.ParseNetworkAddress(addr)
365 if err != nil {
366 continue
367 }
@@ -386,7 +386,7 @@ func (app *App) automaticHTTPS() error {
386 func (app *App) listenerTaken(network, address string) bool {
387 for _, srv := range app.Servers {
388 for _, addr := range srv.Listen {
389 - netw, addrs, err := caddy.ParseListenAddr(addr)
389 + netw, addrs, err := caddy.ParseNetworkAddress(addr)
390 if err != nil || netw != network {
391 continue
392 }
modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
+32 -70
@@ -25,6 +25,7 @@ import (
25 "strings"
26 "time"
27
28 + "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
29 "github.com/caddyserver/caddy/v2/modules/caddytls"
30
31 "github.com/caddyserver/caddy/v2"
@@ -34,6 +35,7 @@ func init() {
35 caddy.RegisterModule(Transport{})
36 }
37
38 +// Transport facilitates FastCGI communication.
39 type Transport struct {
40 //////////////////////////////
41 // TODO: taken from v1 Handler type
@@ -57,32 +59,32 @@ type Transport struct {
59
60 // Use this directory as the fastcgi root directory. Defaults to the root
61 // directory of the parent virtual host.
60 - Root string
62 + Root string `json:"root,omitempty"`
63
64 // The path in the URL will be split into two, with the first piece ending
65 // with the value of SplitPath. The first piece will be assumed as the
66 // actual resource (CGI script) name, and the second piece will be set to
67 // PATH_INFO for the CGI script to use.
66 - SplitPath string
68 + SplitPath string `json:"split_path,omitempty"`
69
70 // If the URL ends with '/' (which indicates a directory), these index
71 // files will be tried instead.
70 - IndexFiles []string
72 + // IndexFiles []string
73
74 // Environment Variables
73 - EnvVars [][2]string
75 + EnvVars [][2]string `json:"env,omitempty"`
76
77 // Ignored paths
76 - IgnoredSubPaths []string
78 + // IgnoredSubPaths []string
79
80 // The duration used to set a deadline when connecting to an upstream.
79 - DialTimeout time.Duration
81 + DialTimeout caddy.Duration `json:"dial_timeout,omitempty"`
82
83 // The duration used to set a deadline when reading from the FastCGI server.
82 - ReadTimeout time.Duration
84 + ReadTimeout caddy.Duration `json:"read_timeout,omitempty"`
85
86 // The duration used to set a deadline when sending to the FastCGI server.
85 - WriteTimeout time.Duration
87 + WriteTimeout caddy.Duration `json:"write_timeout,omitempty"`
88 }
89
90 // CaddyModule returns the Caddy module information.
@@ -93,102 +95,62 @@ func (Transport) CaddyModule() caddy.ModuleInfo {
95 }
96 }
97
98 +// RoundTrip implements http.RoundTripper.
99 func (t Transport) RoundTrip(r *http.Request) (*http.Response, error) {
97 - // Create environment for CGI script
100 env, err := t.buildEnv(r)
101 if err != nil {
102 return nil, fmt.Errorf("building environment: %v", err)
103 }
104
103 - // TODO:
104 - // Connect to FastCGI gateway
105 - // address, err := f.Address()
106 - // if err != nil {
107 - // return http.StatusBadGateway, err
108 - // }
109 - // network, address := parseAddress(address)
110 - network, address := "tcp", r.URL.Host // TODO:
111 -
105 + // TODO: doesn't dialer have a Timeout field?
106 ctx := context.Background()
107 if t.DialTimeout > 0 {
108 var cancel context.CancelFunc
115 - ctx, cancel = context.WithTimeout(ctx, t.DialTimeout)
109 + ctx, cancel = context.WithTimeout(ctx, time.Duration(t.DialTimeout))
110 defer cancel()
111 }
112
113 + // extract dial information from request (this
114 + // should embedded by the reverse proxy)
115 + network, address := "tcp", r.URL.Host
116 + if dialInfoVal := ctx.Value(reverseproxy.DialInfoCtxKey); dialInfoVal != nil {
117 + dialInfo := dialInfoVal.(reverseproxy.DialInfo)
118 + network = dialInfo.Network
119 + address = dialInfo.Address
120 + }
121 +
122 fcgiBackend, err := DialContext(ctx, network, address)
123 if err != nil {
124 return nil, fmt.Errorf("dialing backend: %v", err)
125 }
123 - // fcgiBackend is closed when response body is closed (see clientCloser)
126 + // fcgiBackend gets closed when response body is closed (see clientCloser)
127
128 // read/write timeouts
126 - if err := fcgiBackend.SetReadTimeout(t.ReadTimeout); err != nil {
129 + if err := fcgiBackend.SetReadTimeout(time.Duration(t.ReadTimeout)); err != nil {
130 return nil, fmt.Errorf("setting read timeout: %v", err)
131 }
129 - if err := fcgiBackend.SetWriteTimeout(t.WriteTimeout); err != nil {
132 + if err := fcgiBackend.SetWriteTimeout(time.Duration(t.WriteTimeout)); err != nil {
133 return nil, fmt.Errorf("setting write timeout: %v", err)
134 }
135
133 - var resp *http.Response
134 -
135 - var contentLength int64
136 - // if ContentLength is already set
137 - if r.ContentLength > 0 {
138 - contentLength = r.ContentLength
139 - } else {
136 + contentLength := r.ContentLength
137 + if contentLength == 0 {
138 contentLength, _ = strconv.ParseInt(r.Header.Get("Content-Length"), 10, 64)
139 }
140 +
141 + var resp *http.Response
142 switch r.Method {
143 - case "HEAD":
143 + case http.MethodHead:
144 resp, err = fcgiBackend.Head(env)
145 - case "GET":
145 + case http.MethodGet:
146 resp, err = fcgiBackend.Get(env, r.Body, contentLength)
147 - case "OPTIONS":
147 + case http.MethodOptions:
148 resp, err = fcgiBackend.Options(env)
149 default:
150 resp, err = fcgiBackend.Post(env, r.Method, r.Header.Get("Content-Type"), r.Body, contentLength)
151 }
152
153 - // TODO:
153 return resp, err
155 -
156 - // Stuff brought over from v1 that might not be necessary here:
157 -
158 - // if resp != nil && resp.Body != nil {
159 - // defer resp.Body.Close()
160 - // }
161 -
162 - // if err != nil {
163 - // if err, ok := err.(net.Error); ok && err.Timeout() {
164 - // return http.StatusGatewayTimeout, err
165 - // } else if err != io.EOF {
166 - // return http.StatusBadGateway, err
167 - // }
168 - // }
169 -
170 - // // Write response header
171 - // writeHeader(w, resp)
172 -
173 - // // Write the response body
174 - // _, err = io.Copy(w, resp.Body)
175 - // if err != nil {
176 - // return http.StatusBadGateway, err
177 - // }
178 -
179 - // // Log any stderr output from upstream
180 - // if fcgiBackend.stderr.Len() != 0 {
181 - // // Remove trailing newline, error logger already does this.
182 - // err = LogError(strings.TrimSuffix(fcgiBackend.stderr.String(), "\n"))
183 - // }
184 -
185 - // // Normally we would return the status code if it is an error status (>= 400),
186 - // // however, upstream FastCGI apps don't know about our contract and have
187 - // // probably already written an error page. So we just return 0, indicating
188 - // // that the response body is already written. However, we do return any
189 - // // error value so it can be logged.
190 - // // Note that the proxy middleware works the same way, returning status=0.
191 - // return 0, err
154 }
155
156 // buildEnv returns a set of CGI environment variables for the request.
modules/caddyhttp/reverseproxy/healthchecks.go
+49 -19
@@ -15,6 +15,7 @@
15 package reverseproxy
16
17 import (
18 + "context"
19 "fmt"
20 "io"
21 "io/ioutil"
@@ -93,15 +94,31 @@ func (h *Handler) activeHealthChecker() {
94 // health checks for all hosts in the global repository.
95 func (h *Handler) doActiveHealthChecksForAllHosts() {
96 hosts.Range(func(key, value interface{}) bool {
96 - addr := key.(string)
97 + networkAddr := key.(string)
98 host := value.(Host)
99
99 - go func(addr string, host Host) {
100 - err := h.doActiveHealthCheck(addr, host)
100 + go func(networkAddr string, host Host) {
101 + network, addrs, err := caddy.ParseNetworkAddress(networkAddr)
102 if err != nil {
102 - log.Printf("[ERROR] reverse_proxy: active health check for host %s: %v", addr, err)
103 + log.Printf("[ERROR] reverse_proxy: active health check for host %s: bad network address: %v", networkAddr, err)
104 + return
105 }
104 - }(addr, host)
106 + if len(addrs) != 1 {
107 + log.Printf("[ERROR] reverse_proxy: active health check for host %s: multiple addresses (upstream must map to only one address)", networkAddr)
108 + return
109 + }
110 + hostAddr := addrs[0]
111 + if network == "unix" || network == "unixgram" || network == "unixpacket" {
112 + // this will be used as the Host portion of a http.Request URL, and
113 + // paths to socket files would produce an error when creating URL,
114 + // so use a fake Host value instead
115 + hostAddr = network
116 + }
117 + err = h.doActiveHealthCheck(DialInfo{network, addrs[0]}, hostAddr, host)
118 + if err != nil {
119 + log.Printf("[ERROR] reverse_proxy: active health check for host %s: %v", networkAddr, err)
120 + }
121 + }(networkAddr, host)
122
123 // continue to iterate all hosts
124 return true
@@ -115,26 +132,39 @@ func (h *Handler) doActiveHealthChecksForAllHosts() {
132 // according to whether it passes the health check. An error is
133 // returned only if the health check fails to occur or if marking
134 // the host's health status fails.
118 -func (h *Handler) doActiveHealthCheck(hostAddr string, host Host) error {
119 - // create the URL for the health check
120 - u, err := url.Parse(hostAddr)
121 - if err != nil {
122 - return err
135 +func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, host Host) error {
136 + // create the URL for the request that acts as a health check
137 + scheme := "http"
138 + if ht, ok := h.Transport.(*http.Transport); ok && ht.TLSClientConfig != nil {
139 + // this is kind of a hacky way to know if we should use HTTPS, but whatever
140 + scheme = "https"
141 }
124 - if h.HealthChecks.Active.Path != "" {
125 - u.Path = h.HealthChecks.Active.Path
142 + u := &url.URL{
143 + Scheme: scheme,
144 + Host: hostAddr,
145 + Path: h.HealthChecks.Active.Path,
146 }
147 +
148 + // adjust the port, if configured to be different
149 if h.HealthChecks.Active.Port != 0 {
150 portStr := strconv.Itoa(h.HealthChecks.Active.Port)
129 - u.Host = net.JoinHostPort(u.Hostname(), portStr)
151 + host, _, err := net.SplitHostPort(hostAddr)
152 + if err != nil {
153 + host = hostAddr
154 + }
155 + u.Host = net.JoinHostPort(host, portStr)
156 }
157
132 - req, err := http.NewRequest(http.MethodGet, u.String(), nil)
158 + // attach dialing information to this request
159 + ctx := context.Background()
160 + ctx = context.WithValue(ctx, caddy.ReplacerCtxKey, caddy.NewReplacer())
161 + ctx = context.WithValue(ctx, DialInfoCtxKey, dialInfo)
162 + req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
163 if err != nil {
134 - return err
164 + return fmt.Errorf("making request: %v", err)
165 }
166
137 - // do the request, careful to tame the response body
167 + // do the request, being careful to tame the response body
168 resp, err := h.HealthChecks.Active.httpClient.Do(req)
169 if err != nil {
170 log.Printf("[INFO] reverse_proxy: active health check: %s is down (HTTP request failed: %v)", hostAddr, err)
@@ -149,7 +179,7 @@ func (h *Handler) doActiveHealthCheck(hostAddr string, host Host) error {
179 body = io.LimitReader(body, h.HealthChecks.Active.MaxSize)
180 }
181 defer func() {
152 - // drain any remaining body so connection can be re-used
182 + // drain any remaining body so connection could be re-used
183 io.Copy(ioutil.Discard, body)
184 resp.Body.Close()
185 }()
@@ -225,7 +255,7 @@ func (h *Handler) countFailure(upstream *Upstream) {
255 err := upstream.Host.CountFail(1)
256 if err != nil {
257 log.Printf("[ERROR] proxy: upstream %s: counting failure: %v",
228 - upstream.hostURL, err)
258 + upstream.dialInfo, err)
259 }
260
261 // forget it later
@@ -234,7 +264,7 @@ func (h *Handler) countFailure(upstream *Upstream) {
264 err := host.CountFail(-1)
265 if err != nil {
266 log.Printf("[ERROR] proxy: upstream %s: expiring failure: %v",
237 - upstream.hostURL, err)
267 + upstream.dialInfo, err)
268 }
269 }(upstream.Host, failDuration)
270 }
modules/caddyhttp/reverseproxy/hosts.go
+30 -8
@@ -16,7 +16,6 @@ package reverseproxy
16
17 import (
18 "fmt"
19 - "net/url"
19 "sync/atomic"
20
21 "github.com/caddyserver/caddy/v2"
@@ -59,7 +58,7 @@ type UpstreamPool []*Upstream
58 type Upstream struct {
59 Host `json:"-"`
60
62 - Address string `json:"address,omitempty"`
61 + Dial string `json:"dial,omitempty"`
62 MaxRequests int `json:"max_requests,omitempty"`
63
64 // TODO: This could be really useful, to bind requests
@@ -68,8 +67,8 @@ type Upstream struct {
67 // IPAffinity string
68
69 healthCheckPolicy *PassiveHealthChecks
71 - hostURL *url.URL
70 cb CircuitBreaker
71 + dialInfo DialInfo
72 }
73
74 // Available returns true if the remote host
@@ -101,11 +100,6 @@ func (u *Upstream) Full() bool {
100 return u.MaxRequests > 0 && u.Host.NumRequests() >= u.MaxRequests
101 }
102
104 -// URL returns the upstream host's endpoint URL.
105 -func (u *Upstream) URL() *url.URL {
106 - return u.hostURL
107 -}
108 -
103 // upstreamHost is the basic, in-memory representation
104 // of the state of a remote host. It implements the
105 // Host interface.
@@ -162,6 +156,34 @@ func (uh *upstreamHost) SetHealthy(healthy bool) (bool, error) {
156 return swapped, nil
157 }
158
159 +// DialInfo contains information needed to dial a
160 +// connection to an upstream host. This information
161 +// may be different than that which is represented
162 +// in a URL (for example, unix sockets don't have
163 +// a host that can be represented in a URL, but
164 +// they certainly have a network name and address).
165 +type DialInfo struct {
166 + // The network to use. This should be one of the
167 + // values that is accepted by net.Dial:
168 + // https://golang.org/pkg/net/#Dial
169 + Network string
170 +
171 + // The address to dial. Follows the same
172 + // semantics and rules as net.Dial.
173 + Address string
174 +}
175 +
176 +// String returns the Caddy network address form
177 +// by joining the network and address with a
178 +// forward slash.
179 +func (di DialInfo) String() string {
180 + return di.Network + "/" + di.Address
181 +}
182 +
183 +// DialInfoCtxKey is used to store a DialInfo
184 +// in a context.Context.
185 +const DialInfoCtxKey = caddy.CtxKey("dial_info")
186 +
187 // hosts is the global repository for hosts that are
188 // currently in use by active configuration(s). This
189 // allows the state of remote hosts to be preserved
modules/caddyhttp/reverseproxy/httptransport.go
+12 -16
@@ -15,6 +15,7 @@
15 package reverseproxy
16
17 import (
18 + "context"
19 "crypto/tls"
20 "crypto/x509"
21 "encoding/base64"
@@ -63,14 +64,23 @@ func (HTTPTransport) CaddyModule() caddy.ModuleInfo {
64
65 // Provision sets up h.RoundTripper with a http.Transport
66 // that is ready to use.
66 -func (h *HTTPTransport) Provision(ctx caddy.Context) error {
67 +func (h *HTTPTransport) Provision(_ caddy.Context) error {
68 dialer := &net.Dialer{
69 Timeout: time.Duration(h.DialTimeout),
70 FallbackDelay: time.Duration(h.FallbackDelay),
71 // TODO: Resolver
72 }
73 +
74 rt := &http.Transport{
73 - DialContext: dialer.DialContext,
75 + DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
76 + // the proper dialing information should be embedded into the request's context
77 + if dialInfoVal := ctx.Value(DialInfoCtxKey); dialInfoVal != nil {
78 + dialInfo := dialInfoVal.(DialInfo)
79 + network = dialInfo.Network
80 + address = dialInfo.Address
81 + }
82 + return dialer.DialContext(ctx, network, address)
83 + },
84 MaxConnsPerHost: h.MaxConnsPerHost,
85 ResponseHeaderTimeout: time.Duration(h.ResponseHeaderTimeout),
86 ExpectContinueTimeout: time.Duration(h.ExpectContinueTimeout),
@@ -91,7 +101,6 @@ func (h *HTTPTransport) Provision(ctx caddy.Context) error {
101
102 if h.KeepAlive != nil {
103 dialer.KeepAlive = time.Duration(h.KeepAlive.ProbeInterval)
94 -
104 if enabled := h.KeepAlive.Enabled; enabled != nil {
105 rt.DisableKeepAlives = !*enabled
106 }
@@ -191,16 +200,3 @@ type KeepAlive struct {
200 MaxIdleConnsPerHost int `json:"max_idle_conns_per_host,omitempty"`
201 IdleConnTimeout caddy.Duration `json:"idle_timeout,omitempty"` // how long should connections be kept alive when idle
202 }
194 -
195 -var (
196 - defaultDialer = net.Dialer{
197 - Timeout: 10 * time.Second,
198 - KeepAlive: 30 * time.Second,
199 - }
200 -
201 - defaultTransport = &http.Transport{
202 - DialContext: defaultDialer.DialContext,
203 - TLSHandshakeTimeout: 5 * time.Second,
204 - IdleConnTimeout: 2 * time.Minute,
205 - }
206 -)
modules/caddyhttp/reverseproxy/reverseproxy.go
+82 -48
@@ -20,7 +20,6 @@ import (
20 "fmt"
21 "net"
22 "net/http"
23 - "net/url"
23 "regexp"
24 "strings"
25 "time"
@@ -86,7 +85,18 @@ func (h *Handler) Provision(ctx caddy.Context) error {
85 }
86
87 if h.Transport == nil {
89 - h.Transport = defaultTransport
88 + t := &HTTPTransport{
89 + KeepAlive: &KeepAlive{
90 + ProbeInterval: caddy.Duration(30 * time.Second),
91 + IdleConnTimeout: caddy.Duration(2 * time.Minute),
92 + },
93 + DialTimeout: caddy.Duration(10 * time.Second),
94 + }
95 + err := t.Provision(ctx)
96 + if err != nil {
97 + return fmt.Errorf("provisioning default transport: %v", err)
98 + }
99 + h.Transport = t
100 }
101
102 if h.LoadBalancing == nil {
@@ -133,51 +143,65 @@ func (h *Handler) Provision(ctx caddy.Context) error {
143 go h.activeHealthChecker()
144 }
145
146 + var allUpstreams []*Upstream
147 for _, upstream := range h.Upstreams {
137 - upstream.cb = h.CB
138 -
139 - // url parser requires a scheme
140 - if !strings.Contains(upstream.Address, "://") {
141 - upstream.Address = "http://" + upstream.Address
142 - }
143 - u, err := url.Parse(upstream.Address)
148 + // upstreams are allowed to map to only a single host,
149 + // but an upstream's address may semantically represent
150 + // multiple addresses, so make sure to handle each
151 + // one in turn based on this one upstream config
152 + network, addresses, err := caddy.ParseNetworkAddress(upstream.Dial)
153 if err != nil {
145 - return fmt.Errorf("invalid upstream address %s: %v", upstream.Address, err)
146 - }
147 - upstream.hostURL = u
148 -
149 - // if host already exists from a current config,
150 - // use that instead; otherwise, add it
151 - // TODO: make hosts modular, so that their state can be distributed in enterprise for example
152 - // TODO: If distributed, the pool should be stored in storage...
153 - var host Host = new(upstreamHost)
154 - activeHost, loaded := hosts.LoadOrStore(u.String(), host)
155 - if loaded {
156 - host = activeHost.(Host)
157 - }
158 - upstream.Host = host
159 -
160 - // if the passive health checker has a non-zero "unhealthy
161 - // request count" but the upstream has no MaxRequests set
162 - // (they are the same thing, but one is a default value for
163 - // for upstreams with a zero MaxRequests), copy the default
164 - // value into this upstream, since the value in the upstream
165 - // is what is used during availability checks
166 - if h.HealthChecks != nil &&
167 - h.HealthChecks.Passive != nil &&
168 - h.HealthChecks.Passive.UnhealthyRequestCount > 0 &&
169 - upstream.MaxRequests == 0 {
170 - upstream.MaxRequests = h.HealthChecks.Passive.UnhealthyRequestCount
154 + return fmt.Errorf("parsing dial address: %v", err)
155 }
156
173 - if h.HealthChecks != nil {
157 + for _, addr := range addresses {
158 + // make a new upstream based on the original
159 + // that has a singular dial address
160 + upstreamCopy := *upstream
161 + upstreamCopy.dialInfo = DialInfo{network, addr}
162 + upstreamCopy.Dial = upstreamCopy.dialInfo.String()
163 + upstreamCopy.cb = h.CB
164 +
165 + // if host already exists from a current config,
166 + // use that instead; otherwise, add it
167 + // TODO: make hosts modular, so that their state can be distributed in enterprise for example
168 + // TODO: If distributed, the pool should be stored in storage...
169 + var host Host = new(upstreamHost)
170 + activeHost, loaded := hosts.LoadOrStore(upstreamCopy.Dial, host)
171 + if loaded {
172 + host = activeHost.(Host)
173 + }
174 + upstreamCopy.Host = host
175 +
176 + // if the passive health checker has a non-zero "unhealthy
177 + // request count" but the upstream has no MaxRequests set
178 + // (they are the same thing, but one is a default value for
179 + // for upstreams with a zero MaxRequests), copy the default
180 + // value into this upstream, since the value in the upstream
181 + // (MaxRequests) is what is used during availability checks
182 + if h.HealthChecks != nil &&
183 + h.HealthChecks.Passive != nil &&
184 + h.HealthChecks.Passive.UnhealthyRequestCount > 0 &&
185 + upstreamCopy.MaxRequests == 0 {
186 + upstreamCopy.MaxRequests = h.HealthChecks.Passive.UnhealthyRequestCount
187 + }
188 +
189 // upstreams need independent access to the passive
175 - // health check policy so they can, you know, passively
176 - // do health checks
177 - upstream.healthCheckPolicy = h.HealthChecks.Passive
190 + // health check policy because they run outside of the
191 + // scope of a request handler
192 + if h.HealthChecks != nil {
193 + upstreamCopy.healthCheckPolicy = h.HealthChecks.Passive
194 + }
195 +
196 + allUpstreams = append(allUpstreams, &upstreamCopy)
197 }
198 }
199
200 + // replace the unmarshaled upstreams (possible 1:many
201 + // address mapping) with our list, which is mapped 1:1,
202 + // thus may have expanded the original list
203 + h.Upstreams = allUpstreams
204 +
205 return nil
206 }
207
@@ -192,7 +216,7 @@ func (h *Handler) Cleanup() error {
216
217 // remove hosts from our config from the pool
218 for _, upstream := range h.Upstreams {
195 - hosts.Delete(upstream.hostURL.String())
219 + hosts.Delete(upstream.dialInfo.String())
220 }
221
222 return nil
@@ -222,6 +246,12 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
246 continue
247 }
248
249 + // attach to the request information about how to dial the upstream;
250 + // this is necessary because the information cannot be sufficiently
251 + // or satisfactorily represented in a URL
252 + ctx := context.WithValue(r.Context(), DialInfoCtxKey, upstream.dialInfo)
253 + r = r.WithContext(ctx)
254 +
255 // proxy the request to that upstream
256 proxyErr = h.reverseProxy(w, r, upstream)
257 if proxyErr == nil || proxyErr == context.Canceled {
@@ -249,6 +279,16 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
279 // This assumes that no mutations of the request are performed
280 // by h during or after proxying.
281 func (h Handler) prepareRequest(req *http.Request) error {
282 + // as a special (but very common) case, if the transport
283 + // is HTTP, then ensure the request has the proper scheme
284 + // because incoming requests by default are lacking it
285 + if req.URL.Scheme == "" {
286 + req.URL.Scheme = "http"
287 + if ht, ok := h.Transport.(*HTTPTransport); ok && ht.TLS != nil {
288 + req.URL.Scheme = "https"
289 + }
290 + }
291 +
292 if req.ContentLength == 0 {
293 req.Body = nil // Issue golang/go#16036: nil Body for http.Transport retries
294 }
@@ -433,14 +473,8 @@ func (h Handler) tryAgain(start time.Time, proxyErr error) bool {
473 // directRequest modifies only req.URL so that it points to the
474 // given upstream host. It must modify ONLY the request URL.
475 func (h Handler) directRequest(req *http.Request, upstream *Upstream) {
436 - target := upstream.hostURL
437 - req.URL.Scheme = target.Scheme
438 - req.URL.Host = target.Host
439 - req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path) // TODO: This might be a bug (if any part of the path was augmented from a previously-tried upstream; need to start from clean original path of request, same for query string!)
440 - if target.RawQuery == "" || req.URL.RawQuery == "" {
441 - req.URL.RawQuery = target.RawQuery + req.URL.RawQuery
442 - } else {
443 - req.URL.RawQuery = target.RawQuery + "&" + req.URL.RawQuery
476 + if req.URL.Host == "" {
477 + req.URL.Host = upstream.dialInfo.Address
478 }
479 }
480
modules/caddyhttp/server.go
+1 -1
@@ -168,7 +168,7 @@ func (s *Server) enforcementHandler(w http.ResponseWriter, r *http.Request, next
168 // listeners in s that use a port which is not otherPort.
169 func (s *Server) listenersUseAnyPortOtherThan(otherPort int) bool {
170 for _, lnAddr := range s.Listen {
171 - _, addrs, err := caddy.ParseListenAddr(lnAddr)
171 + _, addrs, err := caddy.ParseNetworkAddress(lnAddr)
172 if err == nil {
173 for _, a := range addrs {
174 _, port, err := net.SplitHostPort(a)