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
reverseproxy: Fix dial placeholders, SRV, active health checks (#3780)
* reverseproxy: Fix dial placeholders, SRV, active health checks Supercedes #3776 Partially reverts or updates #3756, #3693, and #3695 * reverseproxy: add integration tests Co-authored-by: Mohammed Al Sahaf <msaa1990@gmail.com>
Matt Holt committed
Oct 13, 2020 at 10:35 UTC
c7efb0307d425eb533885e314518c370a06763da
5 files changed
+270
-33
caddytest/integration/reverseproxy_test.go
+238
@@ -79,6 +79,244 @@ func TestSRVWithDial(t *testing.T) {
79
`, "json", `upstream: specifying dial address is incompatible with lookup_srv: 0: {\"dial\": \"tcp/address.to.upstream:80\", \"lookup_srv\": \"srv.host.service.consul\"}`)
80
}
81
82
+func TestDialWithPlaceholderUnix(t *testing.T) {
83
+
84
+ if runtime.GOOS == "windows" {
85
+ t.SkipNow()
86
+ }
87
+
88
+ f, err := ioutil.TempFile("", "*.sock")
89
+ if err != nil {
90
+ t.Errorf("failed to create TempFile: %s", err)
91
+ return
92
+ }
93
+ // a hack to get a file name within a valid path to use as socket
94
+ socketName := f.Name()
95
+ os.Remove(f.Name())
96
+
97
+ server := http.Server{
98
+ Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
99
+ w.Write([]byte("Hello, World!"))
100
+ }),
101
+ }
102
+
103
+ unixListener, err := net.Listen("unix", socketName)
104
+ if err != nil {
105
+ t.Errorf("failed to listen on the socket: %s", err)
106
+ return
107
+ }
108
+ go server.Serve(unixListener)
109
+ t.Cleanup(func() {
110
+ server.Close()
111
+ })
112
+ runtime.Gosched() // Allow other goroutines to run
113
+
114
+ tester := caddytest.NewTester(t)
115
+ tester.InitServer(`
116
+ {
117
+ "apps": {
118
+ "http": {
119
+ "servers": {
120
+ "srv0": {
121
+ "listen": [
122
+ ":8080"
123
+ ],
124
+ "routes": [
125
+ {
126
+ "handle": [
127
+ {
128
+ "handler": "reverse_proxy",
129
+ "upstreams": [
130
+ {
131
+ "dial": "unix/{http.request.header.X-Caddy-Upstream-Dial}"
132
+ }
133
+ ]
134
+ }
135
+ ]
136
+ }
137
+ ]
138
+ }
139
+ }
140
+ }
141
+ }
142
+ }
143
+ `, "json")
144
+
145
+ req, err := http.NewRequest(http.MethodGet, "http://localhost:8080", nil)
146
+ if err != nil {
147
+ t.Fail()
148
+ return
149
+ }
150
+ req.Header.Set("X-Caddy-Upstream-Dial", socketName)
151
+ tester.AssertResponse(req, 200, "Hello, World!")
152
+}
153
+
154
+func TestReverseProxyWithPlaceholderDialAddress(t *testing.T) {
155
+ tester := caddytest.NewTester(t)
156
+ tester.InitServer(`
157
+ {
158
+ "apps": {
159
+ "http": {
160
+ "servers": {
161
+ "srv0": {
162
+ "listen": [
163
+ ":8080"
164
+ ],
165
+ "routes": [
166
+ {
167
+ "match": [
168
+ {
169
+ "host": [
170
+ "localhost"
171
+ ]
172
+ }
173
+ ],
174
+ "handle": [
175
+ {
176
+ "handler": "static_response",
177
+ "body": "Hello, World!"
178
+ }
179
+ ],
180
+ "terminal": true
181
+ }
182
+ ],
183
+ "automatic_https": {
184
+ "skip": [
185
+ "localhost"
186
+ ]
187
+ }
188
+ },
189
+ "srv1": {
190
+ "listen": [
191
+ ":9080"
192
+ ],
193
+ "routes": [
194
+ {
195
+ "match": [
196
+ {
197
+ "host": [
198
+ "localhost"
199
+ ]
200
+ }
201
+ ],
202
+ "handle": [
203
+ {
204
+
205
+ "handler": "reverse_proxy",
206
+ "upstreams": [
207
+ {
208
+ "dial": "{http.request.header.X-Caddy-Upstream-Dial}"
209
+ }
210
+ ]
211
+ }
212
+ ],
213
+ "terminal": true
214
+ }
215
+ ],
216
+ "automatic_https": {
217
+ "skip": [
218
+ "localhost"
219
+ ]
220
+ }
221
+ }
222
+ }
223
+ }
224
+ }
225
+ }
226
+ `, "json")
227
+
228
+ req, err := http.NewRequest(http.MethodGet, "http://localhost:9080", nil)
229
+ if err != nil {
230
+ t.Fail()
231
+ return
232
+ }
233
+ req.Header.Set("X-Caddy-Upstream-Dial", "localhost:8080")
234
+ tester.AssertResponse(req, 200, "Hello, World!")
235
+}
236
+
237
+func TestReverseProxyWithPlaceholderTCPDialAddress(t *testing.T) {
238
+ tester := caddytest.NewTester(t)
239
+ tester.InitServer(`
240
+ {
241
+ "apps": {
242
+ "http": {
243
+ "servers": {
244
+ "srv0": {
245
+ "listen": [
246
+ ":8080"
247
+ ],
248
+ "routes": [
249
+ {
250
+ "match": [
251
+ {
252
+ "host": [
253
+ "localhost"
254
+ ]
255
+ }
256
+ ],
257
+ "handle": [
258
+ {
259
+ "handler": "static_response",
260
+ "body": "Hello, World!"
261
+ }
262
+ ],
263
+ "terminal": true
264
+ }
265
+ ],
266
+ "automatic_https": {
267
+ "skip": [
268
+ "localhost"
269
+ ]
270
+ }
271
+ },
272
+ "srv1": {
273
+ "listen": [
274
+ ":9080"
275
+ ],
276
+ "routes": [
277
+ {
278
+ "match": [
279
+ {
280
+ "host": [
281
+ "localhost"
282
+ ]
283
+ }
284
+ ],
285
+ "handle": [
286
+ {
287
+
288
+ "handler": "reverse_proxy",
289
+ "upstreams": [
290
+ {
291
+ "dial": "tcp/{http.request.header.X-Caddy-Upstream-Dial}:8080"
292
+ }
293
+ ]
294
+ }
295
+ ],
296
+ "terminal": true
297
+ }
298
+ ],
299
+ "automatic_https": {
300
+ "skip": [
301
+ "localhost"
302
+ ]
303
+ }
304
+ }
305
+ }
306
+ }
307
+ }
308
+ }
309
+ `, "json")
310
+
311
+ req, err := http.NewRequest(http.MethodGet, "http://localhost:9080", nil)
312
+ if err != nil {
313
+ t.Fail()
314
+ return
315
+ }
316
+ req.Header.Set("X-Caddy-Upstream-Dial", "localhost")
317
+ tester.AssertResponse(req, 200, "Hello, World!")
318
+}
319
+
320
func TestSRVWithActiveHealthcheck(t *testing.T) {
321
caddytest.AssertLoadError(t, `
322
{
modules/caddyhttp/reverseproxy/healthchecks.go
+31
-12
@@ -154,23 +154,42 @@ func (h *Handler) doActiveHealthCheckForAllHosts() {
154
}
155
}()
156
157
- portStr := strconv.Itoa(upstream.activeHealthCheckPort)
158
- hostAddr := net.JoinHostPort(upstream.networkAddress.Host, portStr)
159
- if upstream.networkAddress.IsUnixNetwork() {
157
+ networkAddr, err := caddy.NewReplacer().ReplaceOrErr(upstream.Dial, true, true)
158
+ if err != nil {
159
+ h.HealthChecks.Active.logger.Error("invalid use of placeholders in dial address for active health checks",
160
+ zap.String("address", networkAddr),
161
+ zap.Error(err),
162
+ )
163
+ return
164
+ }
165
+ addr, err := caddy.ParseNetworkAddress(networkAddr)
166
+ if err != nil {
167
+ h.HealthChecks.Active.logger.Error("bad network address",
168
+ zap.String("address", networkAddr),
169
+ zap.Error(err),
170
+ )
171
+ return
172
+ }
173
+ if hcp := uint(upstream.activeHealthCheckPort); hcp != 0 {
174
+ if addr.IsUnixNetwork() {
175
+ addr.Network = "tcp" // I guess we just assume TCP since we are using a port??
176
+ }
177
+ addr.StartPort, addr.EndPort = hcp, hcp
178
+ }
179
+ if upstream.LookupSRV == "" && addr.PortRangeSize() != 1 {
180
+ h.HealthChecks.Active.logger.Error("multiple addresses (upstream must map to only one address)",
181
+ zap.String("address", networkAddr),
182
+ )
183
+ return
184
+ }
185
+ hostAddr := addr.JoinHostPort(0)
186
+ if addr.IsUnixNetwork() {
187
// this will be used as the Host portion of a http.Request URL, and
188
// paths to socket files would produce an error when creating URL,
189
// so use a fake Host value instead; unix sockets are usually local
190
hostAddr = "localhost"
191
}
165
-
166
- dialInfo := DialInfo{
167
- Upstream: upstream,
168
- Network: upstream.networkAddress.Network,
169
- Host: upstream.networkAddress.Host,
170
- Port: portStr,
171
- Address: hostAddr,
172
- }
173
- err := h.doActiveHealthCheck(dialInfo, hostAddr, upstream.Host)
192
+ err = h.doActiveHealthCheck(DialInfo{Network: addr.Network, Address: hostAddr}, hostAddr, upstream.Host)
193
if err != nil {
194
h.HealthChecks.Active.logger.Error("active health check failed",
195
zap.String("address", hostAddr),
modules/caddyhttp/reverseproxy/hosts.go
-1
@@ -92,7 +92,6 @@ type Upstream struct {
92
// HeaderAffinity string
93
// IPAffinity string
94
95
- networkAddress caddy.NetworkAddress
95
activeHealthCheckPort int
96
healthCheckPolicy *PassiveHealthChecks
97
cb CircuitBreaker
modules/caddyhttp/reverseproxy/httptransport.go
-3
@@ -182,9 +182,6 @@ func (h *HTTPTransport) NewTransport(ctx caddy.Context) (*http.Transport, error)
182
if dialInfo, ok := GetDialInfo(ctx); ok {
183
network = dialInfo.Network
184
address = dialInfo.Address
185
- if dialInfo.Upstream.networkAddress.IsUnixNetwork() {
186
- address = dialInfo.Host
187
- }
185
}
186
conn, err := dialer.DialContext(ctx, network, address)
187
if err != nil {
modules/caddyhttp/reverseproxy/reverseproxy.go
+1
-17
@@ -129,10 +129,8 @@ func (h *Handler) Provision(ctx caddy.Context) error {
129
h.ctx = ctx
130
h.logger = ctx.Logger(h)
131
132
- // get validation out of the way
132
+ // verify SRV compatibility
133
for i, v := range h.Upstreams {
134
- // Having LookupSRV non-empty conflicts with 2 other config parameters: active health checks, and upstream dial address.
135
- // Therefore if LookupSRV is empty, then there are no immediately apparent config conflicts, and the iteration can be skipped.
134
if v.LookupSRV == "" {
135
continue
136
}
@@ -219,18 +217,6 @@ func (h *Handler) Provision(ctx caddy.Context) error {
217
218
// set up upstreams
219
for _, upstream := range h.Upstreams {
222
- if upstream.LookupSRV == "" {
223
- addr, err := caddy.ParseNetworkAddress(upstream.Dial)
224
- if err != nil {
225
- return err
226
- }
227
-
228
- if addr.PortRangeSize() != 1 {
229
- return fmt.Errorf("multiple addresses (upstream must map to only one address): %v", addr)
230
- }
231
-
232
- upstream.networkAddress = addr
233
- }
220
// create or get the host representation for this upstream
221
var host Host = new(upstreamHost)
222
existingHost, loaded := hosts.LoadOrStore(upstream.String(), host)
@@ -292,8 +278,6 @@ func (h *Handler) Provision(ctx caddy.Context) error {
278
// then use it, otherwise use the port of upstream.
279
if h.HealthChecks.Active.Port != 0 {
280
upstream.activeHealthCheckPort = h.HealthChecks.Active.Port
295
- } else {
296
- upstream.activeHealthCheckPort = int(upstream.networkAddress.StartPort)
281
}
282
}
283