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)