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: WIP refactor and support for FastCGI
Matthew Holt committed
Sep 2, 2019 at 22:01 UTC
026df7c5cb33331d223afc6a9599274e8c89dfd9
17 files changed
+2741
-888
cmd/caddy/main.go
+1
@@ -29,6 +29,7 @@ import (
29
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/markdown"
30
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/requestbody"
31
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
32
+ _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy/fastcgi"
33
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
34
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/templates"
35
_ "github.com/caddyserver/caddy/v2/modules/caddytls"
go.mod
+1
-4
@@ -8,7 +8,6 @@ require (
8
github.com/Masterminds/semver v1.4.2 // indirect
9
github.com/Masterminds/sprig v2.20.0+incompatible
10
github.com/andybalholm/brotli v0.0.0-20190704151324-71eb68cc467c
11
- github.com/davecgh/go-spew v1.1.1 // indirect
11
github.com/dustin/go-humanize v1.0.0
12
github.com/go-acme/lego v2.6.0+incompatible
13
github.com/google/go-cmp v0.3.1 // indirect
@@ -17,7 +16,6 @@ require (
16
github.com/imdario/mergo v0.3.7 // indirect
17
github.com/klauspost/compress v1.7.1-0.20190613161414-0b31f265a57b
18
github.com/klauspost/cpuid v1.2.1
20
- github.com/kr/pretty v0.1.0 // indirect
19
github.com/mholt/certmagic v0.6.2
20
github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936
21
github.com/rs/cors v1.6.0
@@ -26,8 +24,7 @@ require (
24
github.com/starlight-go/starlight v0.0.0-20181207205707-b06f321544f3
25
go.starlark.net v0.0.0-20190604130855-6ddc71c0ba77
26
golang.org/x/net v0.0.0-20190603091049-60506f45cf65
29
- golang.org/x/sys v0.0.0-20190422165155-953cdadca894 // indirect
27
+ golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e // indirect
28
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4
31
- gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
29
gopkg.in/yaml.v2 v2.2.2 // indirect
30
)
go.sum
+2
-11
@@ -12,8 +12,6 @@ github.com/cenkalti/backoff v2.1.1+incompatible h1:tKJnvO2kl0zmb/jA5UKAt4VoEVw1q
12
github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
13
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
14
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
15
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
16
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
15
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
16
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
17
github.com/go-acme/lego v2.5.0+incompatible/go.mod h1:yzMNe9CasVUhkquNvti5nAtPmG94USbYxYrZfTkIn0M=
@@ -34,11 +32,6 @@ github.com/klauspost/compress v1.7.1-0.20190613161414-0b31f265a57b/go.mod h1:RyI
32
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
33
github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w=
34
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
37
-github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
38
-github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
39
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
40
-github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
41
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
35
github.com/mholt/certmagic v0.6.2 h1:yy9cKm3rtxdh12SW4E51lzG3Eo6N59LEOfBQ0CTnMms=
36
github.com/mholt/certmagic v0.6.2/go.mod h1:g4cOPxcjV0oFq3qwpjSA30LReKD8AoIfwAY9VvG35NY=
37
github.com/miekg/dns v1.1.3 h1:1g0r1IvskvgL8rR+AcHzUA+oFmGcQlaIm4IqakufeMM=
@@ -71,16 +64,14 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
64
golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
65
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
66
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
74
-golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
75
-golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
67
+golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e h1:ZytStCyV048ZqDsWHiYDdoI2Vd4msMcrDECFxS+tL9c=
68
+golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
69
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
70
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
71
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
72
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
73
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
74
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
82
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
83
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
75
gopkg.in/square/go-jose.v2 v2.2.2 h1:orlkJ3myw8CN1nVQHBFfloD+L3egixIa4FvUP6RosSA=
76
gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
77
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
listeners.go
+2
@@ -24,6 +24,8 @@ import (
24
"time"
25
)
26
27
+// TODO: Can we use the new UsagePool type?
28
+
29
// Listen returns a listener suitable for use in a Caddy module.
30
// Always be sure to close listeners when you are done with them.
31
func Listen(network, addr string) (net.Listener, error) {
modules/caddyhttp/caddyhttp.go
+14
@@ -518,6 +518,20 @@ func (ws WeakString) String() string {
518
return string(ws)
519
}
520
521
+// StatusCodeMatches returns true if a real HTTP status code matches
522
+// the configured status code, which may be either a real HTTP status
523
+// code or an integer representing a class of codes (e.g. 4 for all
524
+// 4xx statuses).
525
+func StatusCodeMatches(actual, configured int) bool {
526
+ if actual == configured {
527
+ return true
528
+ }
529
+ if configured < 100 && actual >= configured*100 && actual < (configured+1)*100 {
530
+ return true
531
+ }
532
+ return false
533
+}
534
+
535
const (
536
// DefaultHTTPPort is the default port for HTTP.
537
DefaultHTTPPort = 80
modules/caddyhttp/matchers.go
+1
-4
@@ -616,10 +616,7 @@ func (rm ResponseMatcher) matchStatusCode(statusCode int) bool {
616
return true
617
}
618
for _, code := range rm.StatusCode {
619
- if statusCode == code {
620
- return true
621
- }
622
- if code < 100 && statusCode >= code*100 && statusCode < (code+1)*100 {
619
+ if StatusCodeMatches(statusCode, code) {
620
return true
621
}
622
}
modules/caddyhttp/reverseproxy/fastcgi/client.go
new
+578
@@ -0,0 +1,578 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
2
+//
3
+// Licensed under the Apache License, Version 2.0 (the "License");
4
+// you may not use this file except in compliance with the License.
5
+// You may obtain a copy of the License at
6
+//
7
+// http://www.apache.org/licenses/LICENSE-2.0
8
+//
9
+// Unless required by applicable law or agreed to in writing, software
10
+// distributed under the License is distributed on an "AS IS" BASIS,
11
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+// See the License for the specific language governing permissions and
13
+// limitations under the License.
14
+
15
+// Forked Jan. 2015 from http://bitbucket.org/PinIdea/fcgi_client
16
+// (which is forked from https://code.google.com/p/go-fastcgi-client/).
17
+// This fork contains several fixes and improvements by Matt Holt and
18
+// other contributors to the Caddy project.
19
+
20
+// Copyright 2012 Junqing Tan <ivan@mysqlab.net> and The Go Authors
21
+// Use of this source code is governed by a BSD-style
22
+// Part of source code is from Go fcgi package
23
+
24
+package fastcgi
25
+
26
+import (
27
+ "bufio"
28
+ "bytes"
29
+ "context"
30
+ "encoding/binary"
31
+ "errors"
32
+ "io"
33
+ "io/ioutil"
34
+ "mime/multipart"
35
+ "net"
36
+ "net/http"
37
+ "net/http/httputil"
38
+ "net/textproto"
39
+ "net/url"
40
+ "os"
41
+ "path/filepath"
42
+ "strconv"
43
+ "strings"
44
+ "sync"
45
+ "time"
46
+)
47
+
48
+// FCGIListenSockFileno describes listen socket file number.
49
+const FCGIListenSockFileno uint8 = 0
50
+
51
+// FCGIHeaderLen describes header length.
52
+const FCGIHeaderLen uint8 = 8
53
+
54
+// Version1 describes the version.
55
+const Version1 uint8 = 1
56
+
57
+// FCGINullRequestID describes the null request ID.
58
+const FCGINullRequestID uint8 = 0
59
+
60
+// FCGIKeepConn describes keep connection mode.
61
+const FCGIKeepConn uint8 = 1
62
+
63
+const (
64
+ // BeginRequest is the begin request flag.
65
+ BeginRequest uint8 = iota + 1
66
+ // AbortRequest is the abort request flag.
67
+ AbortRequest
68
+ // EndRequest is the end request flag.
69
+ EndRequest
70
+ // Params is the parameters flag.
71
+ Params
72
+ // Stdin is the standard input flag.
73
+ Stdin
74
+ // Stdout is the standard output flag.
75
+ Stdout
76
+ // Stderr is the standard error flag.
77
+ Stderr
78
+ // Data is the data flag.
79
+ Data
80
+ // GetValues is the get values flag.
81
+ GetValues
82
+ // GetValuesResult is the get values result flag.
83
+ GetValuesResult
84
+ // UnknownType is the unknown type flag.
85
+ UnknownType
86
+ // MaxType is the maximum type flag.
87
+ MaxType = UnknownType
88
+)
89
+
90
+const (
91
+ // Responder is the responder flag.
92
+ Responder uint8 = iota + 1
93
+ // Authorizer is the authorizer flag.
94
+ Authorizer
95
+ // Filter is the filter flag.
96
+ Filter
97
+)
98
+
99
+const (
100
+ // RequestComplete is the completed request flag.
101
+ RequestComplete uint8 = iota
102
+ // CantMultiplexConns is the multiplexed connections flag.
103
+ CantMultiplexConns
104
+ // Overloaded is the overloaded flag.
105
+ Overloaded
106
+ // UnknownRole is the unknown role flag.
107
+ UnknownRole
108
+)
109
+
110
+const (
111
+ // MaxConns is the maximum connections flag.
112
+ MaxConns string = "MAX_CONNS"
113
+ // MaxRequests is the maximum requests flag.
114
+ MaxRequests string = "MAX_REQS"
115
+ // MultiplexConns is the multiplex connections flag.
116
+ MultiplexConns string = "MPXS_CONNS"
117
+)
118
+
119
+const (
120
+ maxWrite = 65500 // 65530 may work, but for compatibility
121
+ maxPad = 255
122
+)
123
+
124
+type header struct {
125
+ Version uint8
126
+ Type uint8
127
+ ID uint16
128
+ ContentLength uint16
129
+ PaddingLength uint8
130
+ Reserved uint8
131
+}
132
+
133
+// for padding so we don't have to allocate all the time
134
+// not synchronized because we don't care what the contents are
135
+var pad [maxPad]byte
136
+
137
+func (h *header) init(recType uint8, reqID uint16, contentLength int) {
138
+ h.Version = 1
139
+ h.Type = recType
140
+ h.ID = reqID
141
+ h.ContentLength = uint16(contentLength)
142
+ h.PaddingLength = uint8(-contentLength & 7)
143
+}
144
+
145
+type record struct {
146
+ h header
147
+ rbuf []byte
148
+}
149
+
150
+func (rec *record) read(r io.Reader) (buf []byte, err error) {
151
+ if err = binary.Read(r, binary.BigEndian, &rec.h); err != nil {
152
+ return
153
+ }
154
+ if rec.h.Version != 1 {
155
+ err = errors.New("fcgi: invalid header version")
156
+ return
157
+ }
158
+ if rec.h.Type == EndRequest {
159
+ err = io.EOF
160
+ return
161
+ }
162
+ n := int(rec.h.ContentLength) + int(rec.h.PaddingLength)
163
+ if len(rec.rbuf) < n {
164
+ rec.rbuf = make([]byte, n)
165
+ }
166
+ if _, err = io.ReadFull(r, rec.rbuf[:n]); err != nil {
167
+ return
168
+ }
169
+ buf = rec.rbuf[:int(rec.h.ContentLength)]
170
+
171
+ return
172
+}
173
+
174
+// FCGIClient implements a FastCGI client, which is a standard for
175
+// interfacing external applications with Web servers.
176
+type FCGIClient struct {
177
+ mutex sync.Mutex
178
+ rwc io.ReadWriteCloser
179
+ h header
180
+ buf bytes.Buffer
181
+ stderr bytes.Buffer
182
+ keepAlive bool
183
+ reqID uint16
184
+}
185
+
186
+// DialWithDialerContext connects to the fcgi responder at the specified network address, using custom net.Dialer
187
+// and a context.
188
+// See func net.Dial for a description of the network and address parameters.
189
+func DialWithDialerContext(ctx context.Context, network, address string, dialer net.Dialer) (fcgi *FCGIClient, err error) {
190
+ var conn net.Conn
191
+ conn, err = dialer.DialContext(ctx, network, address)
192
+ if err != nil {
193
+ return
194
+ }
195
+
196
+ fcgi = &FCGIClient{
197
+ rwc: conn,
198
+ keepAlive: false,
199
+ reqID: 1,
200
+ }
201
+
202
+ return
203
+}
204
+
205
+// DialContext is like Dial but passes ctx to dialer.Dial.
206
+func DialContext(ctx context.Context, network, address string) (fcgi *FCGIClient, err error) {
207
+ // TODO: why not set timeout here?
208
+ return DialWithDialerContext(ctx, network, address, net.Dialer{})
209
+}
210
+
211
+// Dial connects to the fcgi responder at the specified network address, using default net.Dialer.
212
+// See func net.Dial for a description of the network and address parameters.
213
+func Dial(network, address string) (fcgi *FCGIClient, err error) {
214
+ return DialContext(context.Background(), network, address)
215
+}
216
+
217
+// Close closes fcgi connection
218
+func (c *FCGIClient) Close() {
219
+ c.rwc.Close()
220
+}
221
+
222
+func (c *FCGIClient) writeRecord(recType uint8, content []byte) (err error) {
223
+ c.mutex.Lock()
224
+ defer c.mutex.Unlock()
225
+ c.buf.Reset()
226
+ c.h.init(recType, c.reqID, len(content))
227
+ if err := binary.Write(&c.buf, binary.BigEndian, c.h); err != nil {
228
+ return err
229
+ }
230
+ if _, err := c.buf.Write(content); err != nil {
231
+ return err
232
+ }
233
+ if _, err := c.buf.Write(pad[:c.h.PaddingLength]); err != nil {
234
+ return err
235
+ }
236
+ _, err = c.rwc.Write(c.buf.Bytes())
237
+ return err
238
+}
239
+
240
+func (c *FCGIClient) writeBeginRequest(role uint16, flags uint8) error {
241
+ b := [8]byte{byte(role >> 8), byte(role), flags}
242
+ return c.writeRecord(BeginRequest, b[:])
243
+}
244
+
245
+func (c *FCGIClient) writeEndRequest(appStatus int, protocolStatus uint8) error {
246
+ b := make([]byte, 8)
247
+ binary.BigEndian.PutUint32(b, uint32(appStatus))
248
+ b[4] = protocolStatus
249
+ return c.writeRecord(EndRequest, b)
250
+}
251
+
252
+func (c *FCGIClient) writePairs(recType uint8, pairs map[string]string) error {
253
+ w := newWriter(c, recType)
254
+ b := make([]byte, 8)
255
+ nn := 0
256
+ for k, v := range pairs {
257
+ m := 8 + len(k) + len(v)
258
+ if m > maxWrite {
259
+ // param data size exceed 65535 bytes"
260
+ vl := maxWrite - 8 - len(k)
261
+ v = v[:vl]
262
+ }
263
+ n := encodeSize(b, uint32(len(k)))
264
+ n += encodeSize(b[n:], uint32(len(v)))
265
+ m = n + len(k) + len(v)
266
+ if (nn + m) > maxWrite {
267
+ w.Flush()
268
+ nn = 0
269
+ }
270
+ nn += m
271
+ if _, err := w.Write(b[:n]); err != nil {
272
+ return err
273
+ }
274
+ if _, err := w.WriteString(k); err != nil {
275
+ return err
276
+ }
277
+ if _, err := w.WriteString(v); err != nil {
278
+ return err
279
+ }
280
+ }
281
+ w.Close()
282
+ return nil
283
+}
284
+
285
+func encodeSize(b []byte, size uint32) int {
286
+ if size > 127 {
287
+ size |= 1 << 31
288
+ binary.BigEndian.PutUint32(b, size)
289
+ return 4
290
+ }
291
+ b[0] = byte(size)
292
+ return 1
293
+}
294
+
295
+// bufWriter encapsulates bufio.Writer but also closes the underlying stream when
296
+// Closed.
297
+type bufWriter struct {
298
+ closer io.Closer
299
+ *bufio.Writer
300
+}
301
+
302
+func (w *bufWriter) Close() error {
303
+ if err := w.Writer.Flush(); err != nil {
304
+ w.closer.Close()
305
+ return err
306
+ }
307
+ return w.closer.Close()
308
+}
309
+
310
+func newWriter(c *FCGIClient, recType uint8) *bufWriter {
311
+ s := &streamWriter{c: c, recType: recType}
312
+ w := bufio.NewWriterSize(s, maxWrite)
313
+ return &bufWriter{s, w}
314
+}
315
+
316
+// streamWriter abstracts out the separation of a stream into discrete records.
317
+// It only writes maxWrite bytes at a time.
318
+type streamWriter struct {
319
+ c *FCGIClient
320
+ recType uint8
321
+}
322
+
323
+func (w *streamWriter) Write(p []byte) (int, error) {
324
+ nn := 0
325
+ for len(p) > 0 {
326
+ n := len(p)
327
+ if n > maxWrite {
328
+ n = maxWrite
329
+ }
330
+ if err := w.c.writeRecord(w.recType, p[:n]); err != nil {
331
+ return nn, err
332
+ }
333
+ nn += n
334
+ p = p[n:]
335
+ }
336
+ return nn, nil
337
+}
338
+
339
+func (w *streamWriter) Close() error {
340
+ // send empty record to close the stream
341
+ return w.c.writeRecord(w.recType, nil)
342
+}
343
+
344
+type streamReader struct {
345
+ c *FCGIClient
346
+ buf []byte
347
+}
348
+
349
+func (w *streamReader) Read(p []byte) (n int, err error) {
350
+
351
+ if len(p) > 0 {
352
+ if len(w.buf) == 0 {
353
+
354
+ // filter outputs for error log
355
+ for {
356
+ rec := &record{}
357
+ var buf []byte
358
+ buf, err = rec.read(w.c.rwc)
359
+ if err != nil {
360
+ return
361
+ }
362
+ // standard error output
363
+ if rec.h.Type == Stderr {
364
+ w.c.stderr.Write(buf)
365
+ continue
366
+ }
367
+ w.buf = buf
368
+ break
369
+ }
370
+ }
371
+
372
+ n = len(p)
373
+ if n > len(w.buf) {
374
+ n = len(w.buf)
375
+ }
376
+ copy(p, w.buf[:n])
377
+ w.buf = w.buf[n:]
378
+ }
379
+
380
+ return
381
+}
382
+
383
+// Do made the request and returns a io.Reader that translates the data read
384
+// from fcgi responder out of fcgi packet before returning it.
385
+func (c *FCGIClient) Do(p map[string]string, req io.Reader) (r io.Reader, err error) {
386
+ err = c.writeBeginRequest(uint16(Responder), 0)
387
+ if err != nil {
388
+ return
389
+ }
390
+
391
+ err = c.writePairs(Params, p)
392
+ if err != nil {
393
+ return
394
+ }
395
+
396
+ body := newWriter(c, Stdin)
397
+ if req != nil {
398
+ _, _ = io.Copy(body, req)
399
+ }
400
+ body.Close()
401
+
402
+ r = &streamReader{c: c}
403
+ return
404
+}
405
+
406
+// clientCloser is a io.ReadCloser. It wraps a io.Reader with a Closer
407
+// that closes FCGIClient connection.
408
+type clientCloser struct {
409
+ *FCGIClient
410
+ io.Reader
411
+}
412
+
413
+func (f clientCloser) Close() error { return f.rwc.Close() }
414
+
415
+// Request returns a HTTP Response with Header and Body
416
+// from fcgi responder
417
+func (c *FCGIClient) Request(p map[string]string, req io.Reader) (resp *http.Response, err error) {
418
+ r, err := c.Do(p, req)
419
+ if err != nil {
420
+ return
421
+ }
422
+
423
+ rb := bufio.NewReader(r)
424
+ tp := textproto.NewReader(rb)
425
+ resp = new(http.Response)
426
+
427
+ // Parse the response headers.
428
+ mimeHeader, err := tp.ReadMIMEHeader()
429
+ if err != nil && err != io.EOF {
430
+ return
431
+ }
432
+ resp.Header = http.Header(mimeHeader)
433
+
434
+ if resp.Header.Get("Status") != "" {
435
+ statusParts := strings.SplitN(resp.Header.Get("Status"), " ", 2)
436
+ resp.StatusCode, err = strconv.Atoi(statusParts[0])
437
+ if err != nil {
438
+ return
439
+ }
440
+ if len(statusParts) > 1 {
441
+ resp.Status = statusParts[1]
442
+ }
443
+
444
+ } else {
445
+ resp.StatusCode = http.StatusOK
446
+ }
447
+
448
+ // TODO: fixTransferEncoding ?
449
+ resp.TransferEncoding = resp.Header["Transfer-Encoding"]
450
+ resp.ContentLength, _ = strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
451
+
452
+ if chunked(resp.TransferEncoding) {
453
+ resp.Body = clientCloser{c, httputil.NewChunkedReader(rb)}
454
+ } else {
455
+ resp.Body = clientCloser{c, ioutil.NopCloser(rb)}
456
+ }
457
+ return
458
+}
459
+
460
+// Get issues a GET request to the fcgi responder.
461
+func (c *FCGIClient) Get(p map[string]string, body io.Reader, l int64) (resp *http.Response, err error) {
462
+
463
+ p["REQUEST_METHOD"] = "GET"
464
+ p["CONTENT_LENGTH"] = strconv.FormatInt(l, 10)
465
+
466
+ return c.Request(p, body)
467
+}
468
+
469
+// Head issues a HEAD request to the fcgi responder.
470
+func (c *FCGIClient) Head(p map[string]string) (resp *http.Response, err error) {
471
+
472
+ p["REQUEST_METHOD"] = "HEAD"
473
+ p["CONTENT_LENGTH"] = "0"
474
+
475
+ return c.Request(p, nil)
476
+}
477
+
478
+// Options issues an OPTIONS request to the fcgi responder.
479
+func (c *FCGIClient) Options(p map[string]string) (resp *http.Response, err error) {
480
+
481
+ p["REQUEST_METHOD"] = "OPTIONS"
482
+ p["CONTENT_LENGTH"] = "0"
483
+
484
+ return c.Request(p, nil)
485
+}
486
+
487
+// Post issues a POST request to the fcgi responder. with request body
488
+// in the format that bodyType specified
489
+func (c *FCGIClient) Post(p map[string]string, method string, bodyType string, body io.Reader, l int64) (resp *http.Response, err error) {
490
+ if p == nil {
491
+ p = make(map[string]string)
492
+ }
493
+
494
+ p["REQUEST_METHOD"] = strings.ToUpper(method)
495
+
496
+ if len(p["REQUEST_METHOD"]) == 0 || p["REQUEST_METHOD"] == "GET" {
497
+ p["REQUEST_METHOD"] = "POST"
498
+ }
499
+
500
+ p["CONTENT_LENGTH"] = strconv.FormatInt(l, 10)
501
+ if len(bodyType) > 0 {
502
+ p["CONTENT_TYPE"] = bodyType
503
+ } else {
504
+ p["CONTENT_TYPE"] = "application/x-www-form-urlencoded"
505
+ }
506
+
507
+ return c.Request(p, body)
508
+}
509
+
510
+// PostForm issues a POST to the fcgi responder, with form
511
+// as a string key to a list values (url.Values)
512
+func (c *FCGIClient) PostForm(p map[string]string, data url.Values) (resp *http.Response, err error) {
513
+ body := bytes.NewReader([]byte(data.Encode()))
514
+ return c.Post(p, "POST", "application/x-www-form-urlencoded", body, int64(body.Len()))
515
+}
516
+
517
+// PostFile issues a POST to the fcgi responder in multipart(RFC 2046) standard,
518
+// with form as a string key to a list values (url.Values),
519
+// and/or with file as a string key to a list file path.
520
+func (c *FCGIClient) PostFile(p map[string]string, data url.Values, file map[string]string) (resp *http.Response, err error) {
521
+ buf := &bytes.Buffer{}
522
+ writer := multipart.NewWriter(buf)
523
+ bodyType := writer.FormDataContentType()
524
+
525
+ for key, val := range data {
526
+ for _, v0 := range val {
527
+ err = writer.WriteField(key, v0)
528
+ if err != nil {
529
+ return
530
+ }
531
+ }
532
+ }
533
+
534
+ for key, val := range file {
535
+ fd, e := os.Open(val)
536
+ if e != nil {
537
+ return nil, e
538
+ }
539
+ defer fd.Close()
540
+
541
+ part, e := writer.CreateFormFile(key, filepath.Base(val))
542
+ if e != nil {
543
+ return nil, e
544
+ }
545
+ _, err = io.Copy(part, fd)
546
+ if err != nil {
547
+ return
548
+ }
549
+ }
550
+
551
+ err = writer.Close()
552
+ if err != nil {
553
+ return
554
+ }
555
+
556
+ return c.Post(p, "POST", bodyType, buf, int64(buf.Len()))
557
+}
558
+
559
+// SetReadTimeout sets the read timeout for future calls that read from the
560
+// fcgi responder. A zero value for t means no timeout will be set.
561
+func (c *FCGIClient) SetReadTimeout(t time.Duration) error {
562
+ if conn, ok := c.rwc.(net.Conn); ok && t != 0 {
563
+ return conn.SetReadDeadline(time.Now().Add(t))
564
+ }
565
+ return nil
566
+}
567
+
568
+// SetWriteTimeout sets the write timeout for future calls that send data to
569
+// the fcgi responder. A zero value for t means no timeout will be set.
570
+func (c *FCGIClient) SetWriteTimeout(t time.Duration) error {
571
+ if conn, ok := c.rwc.(net.Conn); ok && t != 0 {
572
+ return conn.SetWriteDeadline(time.Now().Add(t))
573
+ }
574
+ return nil
575
+}
576
+
577
+// Checks whether chunked is part of the encodings stack
578
+func chunked(te []string) bool { return len(te) > 0 && te[0] == "chunked" }
modules/caddyhttp/reverseproxy/fastcgi/client_test.go
new
+301
@@ -0,0 +1,301 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
2
+//
3
+// Licensed under the Apache License, Version 2.0 (the "License");
4
+// you may not use this file except in compliance with the License.
5
+// You may obtain a copy of the License at
6
+//
7
+// http://www.apache.org/licenses/LICENSE-2.0
8
+//
9
+// Unless required by applicable law or agreed to in writing, software
10
+// distributed under the License is distributed on an "AS IS" BASIS,
11
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+// See the License for the specific language governing permissions and
13
+// limitations under the License.
14
+
15
+// NOTE: These tests were adapted from the original
16
+// repository from which this package was forked.
17
+// The tests are slow (~10s) and in dire need of rewriting.
18
+// As such, the tests have been disabled to speed up
19
+// automated builds until they can be properly written.
20
+
21
+package fastcgi
22
+
23
+import (
24
+ "bytes"
25
+ "crypto/md5"
26
+ "encoding/binary"
27
+ "fmt"
28
+ "io"
29
+ "io/ioutil"
30
+ "log"
31
+ "math/rand"
32
+ "net"
33
+ "net/http"
34
+ "net/http/fcgi"
35
+ "net/url"
36
+ "os"
37
+ "path/filepath"
38
+ "strconv"
39
+ "strings"
40
+ "testing"
41
+ "time"
42
+)
43
+
44
+// test fcgi protocol includes:
45
+// Get, Post, Post in multipart/form-data, and Post with files
46
+// each key should be the md5 of the value or the file uploaded
47
+// specify remote fcgi responder ip:port to test with php
48
+// test failed if the remote fcgi(script) failed md5 verification
49
+// and output "FAILED" in response
50
+const (
51
+ scriptFile = "/tank/www/fcgic_test.php"
52
+ //ipPort = "remote-php-serv:59000"
53
+ ipPort = "127.0.0.1:59000"
54
+)
55
+
56
+var globalt *testing.T
57
+
58
+type FastCGIServer struct{}
59
+
60
+func (s FastCGIServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
61
+
62
+ if err := req.ParseMultipartForm(100000000); err != nil {
63
+ log.Printf("[ERROR] failed to parse: %v", err)
64
+ }
65
+
66
+ stat := "PASSED"
67
+ fmt.Fprintln(resp, "-")
68
+ fileNum := 0
69
+ {
70
+ length := 0
71
+ for k0, v0 := range req.Form {
72
+ h := md5.New()
73
+ _, _ = io.WriteString(h, v0[0])
74
+ _md5 := fmt.Sprintf("%x", h.Sum(nil))
75
+
76
+ length += len(k0)
77
+ length += len(v0[0])
78
+
79
+ // echo error when key != _md5(val)
80
+ if _md5 != k0 {
81
+ fmt.Fprintln(resp, "server:err ", _md5, k0)
82
+ stat = "FAILED"
83
+ }
84
+ }
85
+ if req.MultipartForm != nil {
86
+ fileNum = len(req.MultipartForm.File)
87
+ for kn, fns := range req.MultipartForm.File {
88
+ //fmt.Fprintln(resp, "server:filekey ", kn )
89
+ length += len(kn)
90
+ for _, f := range fns {
91
+ fd, err := f.Open()
92
+ if err != nil {
93
+ log.Println("server:", err)
94
+ return
95
+ }
96
+ h := md5.New()
97
+ l0, err := io.Copy(h, fd)
98
+ if err != nil {
99
+ log.Println(err)
100
+ return
101
+ }
102
+ length += int(l0)
103
+ defer fd.Close()
104
+ md5 := fmt.Sprintf("%x", h.Sum(nil))
105
+ //fmt.Fprintln(resp, "server:filemd5 ", md5 )
106
+
107
+ if kn != md5 {
108
+ fmt.Fprintln(resp, "server:err ", md5, kn)
109
+ stat = "FAILED"
110
+ }
111
+ //fmt.Fprintln(resp, "server:filename ", f.Filename )
112
+ }
113
+ }
114
+ }
115
+
116
+ fmt.Fprintln(resp, "server:got data length", length)
117
+ }
118
+ fmt.Fprintln(resp, "-"+stat+"-POST(", len(req.Form), ")-FILE(", fileNum, ")--")
119
+}
120
+
121
+func sendFcgi(reqType int, fcgiParams map[string]string, data []byte, posts map[string]string, files map[string]string) (content []byte) {
122
+ fcgi, err := Dial("tcp", ipPort)
123
+ if err != nil {
124
+ log.Println("err:", err)
125
+ return
126
+ }
127
+
128
+ length := 0
129
+
130
+ var resp *http.Response
131
+ switch reqType {
132
+ case 0:
133
+ if len(data) > 0 {
134
+ length = len(data)
135
+ rd := bytes.NewReader(data)
136
+ resp, err = fcgi.Post(fcgiParams, "", "", rd, int64(rd.Len()))
137
+ } else if len(posts) > 0 {
138
+ values := url.Values{}
139
+ for k, v := range posts {
140
+ values.Set(k, v)
141
+ length += len(k) + 2 + len(v)
142
+ }
143
+ resp, err = fcgi.PostForm(fcgiParams, values)
144
+ } else {
145
+ rd := bytes.NewReader(data)
146
+ resp, err = fcgi.Get(fcgiParams, rd, int64(rd.Len()))
147
+ }
148
+
149
+ default:
150
+ values := url.Values{}
151
+ for k, v := range posts {
152
+ values.Set(k, v)
153
+ length += len(k) + 2 + len(v)
154
+ }
155
+
156
+ for k, v := range files {
157
+ fi, _ := os.Lstat(v)
158
+ length += len(k) + int(fi.Size())
159
+ }
160
+ resp, err = fcgi.PostFile(fcgiParams, values, files)
161
+ }
162
+
163
+ if err != nil {
164
+ log.Println("err:", err)
165
+ return
166
+ }
167
+
168
+ defer resp.Body.Close()
169
+ content, _ = ioutil.ReadAll(resp.Body)
170
+
171
+ log.Println("c: send data length ≈", length, string(content))
172
+ fcgi.Close()
173
+ time.Sleep(1 * time.Second)
174
+
175
+ if bytes.Contains(content, []byte("FAILED")) {
176
+ globalt.Error("Server return failed message")
177
+ }
178
+
179
+ return
180
+}
181
+
182
+func generateRandFile(size int) (p string, m string) {
183
+
184
+ p = filepath.Join(os.TempDir(), "fcgict"+strconv.Itoa(rand.Int()))
185
+
186
+ // open output file
187
+ fo, err := os.Create(p)
188
+ if err != nil {
189
+ panic(err)
190
+ }
191
+ // close fo on exit and check for its returned error
192
+ defer func() {
193
+ if err := fo.Close(); err != nil {
194
+ panic(err)
195
+ }
196
+ }()
197
+
198
+ h := md5.New()
199
+ for i := 0; i < size/16; i++ {
200
+ buf := make([]byte, 16)
201
+ binary.PutVarint(buf, rand.Int63())
202
+ if _, err := fo.Write(buf); err != nil {
203
+ log.Printf("[ERROR] failed to write buffer: %v\n", err)
204
+ }
205
+ if _, err := h.Write(buf); err != nil {
206
+ log.Printf("[ERROR] failed to write buffer: %v\n", err)
207
+ }
208
+ }
209
+ m = fmt.Sprintf("%x", h.Sum(nil))
210
+ return
211
+}
212
+
213
+func DisabledTest(t *testing.T) {
214
+ // TODO: test chunked reader
215
+ globalt = t
216
+
217
+ rand.Seed(time.Now().UTC().UnixNano())
218
+
219
+ // server
220
+ go func() {
221
+ listener, err := net.Listen("tcp", ipPort)
222
+ if err != nil {
223
+ log.Println("listener creation failed: ", err)
224
+ }
225
+
226
+ srv := new(FastCGIServer)
227
+ if err := fcgi.Serve(listener, srv); err != nil {
228
+ log.Print("[ERROR] failed to start server: ", err)
229
+ }
230
+ }()
231
+
232
+ time.Sleep(1 * time.Second)
233
+
234
+ // init
235
+ fcgiParams := make(map[string]string)
236
+ fcgiParams["REQUEST_METHOD"] = "GET"
237
+ fcgiParams["SERVER_PROTOCOL"] = "HTTP/1.1"
238
+ //fcgi_params["GATEWAY_INTERFACE"] = "CGI/1.1"
239
+ fcgiParams["SCRIPT_FILENAME"] = scriptFile
240
+
241
+ // simple GET
242
+ log.Println("test:", "get")
243
+ sendFcgi(0, fcgiParams, nil, nil, nil)
244
+
245
+ // simple post data
246
+ log.Println("test:", "post")
247
+ sendFcgi(0, fcgiParams, []byte("c4ca4238a0b923820dcc509a6f75849b=1&7b8b965ad4bca0e41ab51de7b31363a1=n"), nil, nil)
248
+
249
+ log.Println("test:", "post data (more than 60KB)")
250
+ data := ""
251
+ for i := 0x00; i < 0xff; i++ {
252
+ v0 := strings.Repeat(string(i), 256)
253
+ h := md5.New()
254
+ _, _ = io.WriteString(h, v0)
255
+ k0 := fmt.Sprintf("%x", h.Sum(nil))
256
+ data += k0 + "=" + url.QueryEscape(v0) + "&"
257
+ }
258
+ sendFcgi(0, fcgiParams, []byte(data), nil, nil)
259
+
260
+ log.Println("test:", "post form (use url.Values)")
261
+ p0 := make(map[string]string, 1)
262
+ p0["c4ca4238a0b923820dcc509a6f75849b"] = "1"
263
+ p0["7b8b965ad4bca0e41ab51de7b31363a1"] = "n"
264
+ sendFcgi(1, fcgiParams, nil, p0, nil)
265
+
266
+ log.Println("test:", "post forms (256 keys, more than 1MB)")
267
+ p1 := make(map[string]string, 1)
268
+ for i := 0x00; i < 0xff; i++ {
269
+ v0 := strings.Repeat(string(i), 4096)
270
+ h := md5.New()
271
+ _, _ = io.WriteString(h, v0)
272
+ k0 := fmt.Sprintf("%x", h.Sum(nil))
273
+ p1[k0] = v0
274
+ }
275
+ sendFcgi(1, fcgiParams, nil, p1, nil)
276
+
277
+ log.Println("test:", "post file (1 file, 500KB)) ")
278
+ f0 := make(map[string]string, 1)
279
+ path0, m0 := generateRandFile(500000)
280
+ f0[m0] = path0
281
+ sendFcgi(1, fcgiParams, nil, p1, f0)
282
+
283
+ log.Println("test:", "post multiple files (2 files, 5M each) and forms (256 keys, more than 1MB data")
284
+ path1, m1 := generateRandFile(5000000)
285
+ f0[m1] = path1
286
+ sendFcgi(1, fcgiParams, nil, p1, f0)
287
+
288
+ log.Println("test:", "post only files (2 files, 5M each)")
289
+ sendFcgi(1, fcgiParams, nil, nil, f0)
290
+
291
+ log.Println("test:", "post only 1 file")
292
+ delete(f0, "m0")
293
+ sendFcgi(1, fcgiParams, nil, nil, f0)
294
+
295
+ if err := os.Remove(path0); err != nil {
296
+ log.Println("[ERROR] failed to remove path: ", err)
297
+ }
298
+ if err := os.Remove(path1); err != nil {
299
+ log.Println("[ERROR] failed to remove path: ", err)
300
+ }
301
+}
modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
new
+342
@@ -0,0 +1,342 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
2
+//
3
+// Licensed under the Apache License, Version 2.0 (the "License");
4
+// you may not use this file except in compliance with the License.
5
+// You may obtain a copy of the License at
6
+//
7
+// http://www.apache.org/licenses/LICENSE-2.0
8
+//
9
+// Unless required by applicable law or agreed to in writing, software
10
+// distributed under the License is distributed on an "AS IS" BASIS,
11
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+// See the License for the specific language governing permissions and
13
+// limitations under the License.
14
+
15
+package fastcgi
16
+
17
+import (
18
+ "context"
19
+ "crypto/tls"
20
+ "fmt"
21
+ "net/http"
22
+ "path"
23
+ "path/filepath"
24
+ "strconv"
25
+ "strings"
26
+ "time"
27
+
28
+ "github.com/caddyserver/caddy/v2/modules/caddytls"
29
+
30
+ "github.com/caddyserver/caddy/v2"
31
+)
32
+
33
+func init() {
34
+ caddy.RegisterModule(Transport{})
35
+}
36
+
37
+type Transport struct {
38
+ //////////////////////////////
39
+ // TODO: taken from v1 Handler type
40
+
41
+ SoftwareName string
42
+ SoftwareVersion string
43
+ ServerName string
44
+ ServerPort string
45
+
46
+ //////////////////////////
47
+ // TODO: taken from v1 Rule type
48
+
49
+ // The base path to match. Required.
50
+ // Path string
51
+
52
+ // upstream load balancer
53
+ // balancer
54
+
55
+ // Always process files with this extension with fastcgi.
56
+ // Ext string
57
+
58
+ // Use this directory as the fastcgi root directory. Defaults to the root
59
+ // directory of the parent virtual host.
60
+ Root string
61
+
62
+ // The path in the URL will be split into two, with the first piece ending
63
+ // with the value of SplitPath. The first piece will be assumed as the
64
+ // actual resource (CGI script) name, and the second piece will be set to
65
+ // PATH_INFO for the CGI script to use.
66
+ SplitPath string
67
+
68
+ // If the URL ends with '/' (which indicates a directory), these index
69
+ // files will be tried instead.
70
+ IndexFiles []string
71
+
72
+ // Environment Variables
73
+ EnvVars [][2]string
74
+
75
+ // Ignored paths
76
+ IgnoredSubPaths []string
77
+
78
+ // The duration used to set a deadline when connecting to an upstream.
79
+ DialTimeout time.Duration
80
+
81
+ // The duration used to set a deadline when reading from the FastCGI server.
82
+ ReadTimeout time.Duration
83
+
84
+ // The duration used to set a deadline when sending to the FastCGI server.
85
+ WriteTimeout time.Duration
86
+}
87
+
88
+// CaddyModule returns the Caddy module information.
89
+func (Transport) CaddyModule() caddy.ModuleInfo {
90
+ return caddy.ModuleInfo{
91
+ Name: "http.handlers.reverse_proxy.transport.fastcgi",
92
+ New: func() caddy.Module { return new(Transport) },
93
+ }
94
+}
95
+
96
+func (t Transport) RoundTrip(r *http.Request) (*http.Response, error) {
97
+ // Create environment for CGI script
98
+ env, err := t.buildEnv(r)
99
+ if err != nil {
100
+ return nil, fmt.Errorf("building environment: %v", err)
101
+ }
102
+
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
+
112
+ ctx := context.Background()
113
+ if t.DialTimeout > 0 {
114
+ var cancel context.CancelFunc
115
+ ctx, cancel = context.WithTimeout(ctx, t.DialTimeout)
116
+ defer cancel()
117
+ }
118
+
119
+ fcgiBackend, err := DialContext(ctx, network, address)
120
+ if err != nil {
121
+ return nil, fmt.Errorf("dialing backend: %v", err)
122
+ }
123
+ // fcgiBackend is closed when response body is closed (see clientCloser)
124
+
125
+ // read/write timeouts
126
+ if err := fcgiBackend.SetReadTimeout(t.ReadTimeout); err != nil {
127
+ return nil, fmt.Errorf("setting read timeout: %v", err)
128
+ }
129
+ if err := fcgiBackend.SetWriteTimeout(t.WriteTimeout); err != nil {
130
+ return nil, fmt.Errorf("setting write timeout: %v", err)
131
+ }
132
+
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 {
140
+ contentLength, _ = strconv.ParseInt(r.Header.Get("Content-Length"), 10, 64)
141
+ }
142
+ switch r.Method {
143
+ case "HEAD":
144
+ resp, err = fcgiBackend.Head(env)
145
+ case "GET":
146
+ resp, err = fcgiBackend.Get(env, r.Body, contentLength)
147
+ case "OPTIONS":
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:
154
+ 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
192
+}
193
+
194
+// buildEnv returns a set of CGI environment variables for the request.
195
+func (t Transport) buildEnv(r *http.Request) (map[string]string, error) {
196
+ var env map[string]string
197
+
198
+ // Separate remote IP and port; more lenient than net.SplitHostPort
199
+ var ip, port string
200
+ if idx := strings.LastIndex(r.RemoteAddr, ":"); idx > -1 {
201
+ ip = r.RemoteAddr[:idx]
202
+ port = r.RemoteAddr[idx+1:]
203
+ } else {
204
+ ip = r.RemoteAddr
205
+ }
206
+
207
+ // Remove [] from IPv6 addresses
208
+ ip = strings.Replace(ip, "[", "", 1)
209
+ ip = strings.Replace(ip, "]", "", 1)
210
+
211
+ // TODO: respect index files? or leave that to matcher/rewrite (I prefer that)?
212
+ fpath := r.URL.Path
213
+
214
+ // Split path in preparation for env variables.
215
+ // Previous canSplit checks ensure this can never be -1.
216
+ // TODO: I haven't brought over canSplit; make sure this doesn't break
217
+ splitPos := t.splitPos(fpath)
218
+
219
+ // Request has the extension; path was split successfully
220
+ docURI := fpath[:splitPos+len(t.SplitPath)]
221
+ pathInfo := fpath[splitPos+len(t.SplitPath):]
222
+ scriptName := fpath
223
+
224
+ // Strip PATH_INFO from SCRIPT_NAME
225
+ scriptName = strings.TrimSuffix(scriptName, pathInfo)
226
+
227
+ // SCRIPT_FILENAME is the absolute path of SCRIPT_NAME
228
+ scriptFilename := filepath.Join(t.Root, scriptName)
229
+
230
+ // Add vhost path prefix to scriptName. Otherwise, some PHP software will
231
+ // have difficulty discovering its URL.
232
+ pathPrefix, _ := r.Context().Value(caddy.CtxKey("path_prefix")).(string)
233
+ scriptName = path.Join(pathPrefix, scriptName)
234
+
235
+ // TODO: Disabled for now
236
+ // // Get the request URI from context. The context stores the original URI in case
237
+ // // it was changed by a middleware such as rewrite. By default, we pass the
238
+ // // original URI in as the value of REQUEST_URI (the user can overwrite this
239
+ // // if desired). Most PHP apps seem to want the original URI. Besides, this is
240
+ // // how nginx defaults: http://stackoverflow.com/a/12485156/1048862
241
+ // reqURL, _ := r.Context().Value(httpserver.OriginalURLCtxKey).(url.URL)
242
+
243
+ // // Retrieve name of remote user that was set by some downstream middleware such as basicauth.
244
+ // remoteUser, _ := r.Context().Value(httpserver.RemoteUserCtxKey).(string)
245
+
246
+ requestScheme := "http"
247
+ if r.TLS != nil {
248
+ requestScheme = "https"
249
+ }
250
+
251
+ // Some variables are unused but cleared explicitly to prevent
252
+ // the parent environment from interfering.
253
+ env = map[string]string{
254
+ // Variables defined in CGI 1.1 spec
255
+ "AUTH_TYPE": "", // Not used
256
+ "CONTENT_LENGTH": r.Header.Get("Content-Length"),
257
+ "CONTENT_TYPE": r.Header.Get("Content-Type"),
258
+ "GATEWAY_INTERFACE": "CGI/1.1",
259
+ "PATH_INFO": pathInfo,
260
+ "QUERY_STRING": r.URL.RawQuery,
261
+ "REMOTE_ADDR": ip,
262
+ "REMOTE_HOST": ip, // For speed, remote host lookups disabled
263
+ "REMOTE_PORT": port,
264
+ "REMOTE_IDENT": "", // Not used
265
+ // "REMOTE_USER": remoteUser, // TODO:
266
+ "REQUEST_METHOD": r.Method,
267
+ "REQUEST_SCHEME": requestScheme,
268
+ "SERVER_NAME": t.ServerName,
269
+ "SERVER_PORT": t.ServerPort,
270
+ "SERVER_PROTOCOL": r.Proto,
271
+ "SERVER_SOFTWARE": t.SoftwareName + "/" + t.SoftwareVersion,
272
+
273
+ // Other variables
274
+ // "DOCUMENT_ROOT": rule.Root,
275
+ "DOCUMENT_URI": docURI,
276
+ "HTTP_HOST": r.Host, // added here, since not always part of headers
277
+ // "REQUEST_URI": reqURL.RequestURI(), // TODO:
278
+ "SCRIPT_FILENAME": scriptFilename,
279
+ "SCRIPT_NAME": scriptName,
280
+ }
281
+
282
+ // compliance with the CGI specification requires that
283
+ // PATH_TRANSLATED should only exist if PATH_INFO is defined.
284
+ // Info: https://www.ietf.org/rfc/rfc3875 Page 14
285
+ if env["PATH_INFO"] != "" {
286
+ env["PATH_TRANSLATED"] = filepath.Join(t.Root, pathInfo) // Info: http://www.oreilly.com/openbook/cgi/ch02_04.html
287
+ }
288
+
289
+ // Some web apps rely on knowing HTTPS or not
290
+ if r.TLS != nil {
291
+ env["HTTPS"] = "on"
292
+ // and pass the protocol details in a manner compatible with apache's mod_ssl
293
+ // (which is why these have a SSL_ prefix and not TLS_).
294
+ v, ok := tlsProtocolStrings[r.TLS.Version]
295
+ if ok {
296
+ env["SSL_PROTOCOL"] = v
297
+ }
298
+ // and pass the cipher suite in a manner compatible with apache's mod_ssl
299
+ for k, v := range caddytls.SupportedCipherSuites {
300
+ if v == r.TLS.CipherSuite {
301
+ env["SSL_CIPHER"] = k
302
+ break
303
+ }
304
+ }
305
+ }
306
+
307
+ // Add env variables from config (with support for placeholders in values)
308
+ repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
309
+ for _, envVar := range t.EnvVars {
310
+ env[envVar[0]] = repl.ReplaceAll(envVar[1], "")
311
+ }
312
+
313
+ // Add all HTTP headers to env variables
314
+ for field, val := range r.Header {
315
+ header := strings.ToUpper(field)
316
+ header = headerNameReplacer.Replace(header)
317
+ env["HTTP_"+header] = strings.Join(val, ", ")
318
+ }
319
+ return env, nil
320
+}
321
+
322
+// splitPos returns the index where path should
323
+// be split based on t.SplitPath.
324
+func (t Transport) splitPos(path string) int {
325
+ // TODO:
326
+ // if httpserver.CaseSensitivePath {
327
+ // return strings.Index(path, r.SplitPath)
328
+ // }
329
+ return strings.Index(strings.ToLower(path), strings.ToLower(t.SplitPath))
330
+}
331
+
332
+// TODO:
333
+// Map of supported protocols to Apache ssl_mod format
334
+// Note that these are slightly different from SupportedProtocols in caddytls/config.go
335
+var tlsProtocolStrings = map[uint16]string{
336
+ tls.VersionTLS10: "TLSv1",
337
+ tls.VersionTLS11: "TLSv1.1",
338
+ tls.VersionTLS12: "TLSv1.2",
339
+ tls.VersionTLS13: "TLSv1.3",
340
+}
341
+
342
+var headerNameReplacer = strings.NewReplacer(" ", "_", "-", "_")
modules/caddyhttp/reverseproxy/healthchecker.go
deleted
-86
@@ -1,86 +0,0 @@
1
-// Copyright 2015 Matthew Holt and The Caddy Authors
2
-//
3
-// Licensed under the Apache License, Version 2.0 (the "License");
4
-// you may not use this file except in compliance with the License.
5
-// You may obtain a copy of the License at
6
-//
7
-// http://www.apache.org/licenses/LICENSE-2.0
8
-//
9
-// Unless required by applicable law or agreed to in writing, software
10
-// distributed under the License is distributed on an "AS IS" BASIS,
11
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
-// See the License for the specific language governing permissions and
13
-// limitations under the License.
14
-
15
-package reverseproxy
16
-
17
-import (
18
- "net/http"
19
- "time"
20
-)
21
-
22
-// Upstream represents the interface that must be satisfied to use the healthchecker.
23
-type Upstream interface {
24
- SetHealthiness(bool)
25
-}
26
-
27
-// HealthChecker represents a worker that periodically evaluates if proxy upstream host is healthy.
28
-type HealthChecker struct {
29
- upstream Upstream
30
- Ticker *time.Ticker
31
- HTTPClient *http.Client
32
- StopChan chan bool
33
-}
34
-
35
-// ScheduleChecks periodically runs health checks against an upstream host.
36
-func (h *HealthChecker) ScheduleChecks(url string) {
37
- // check if a host is healthy on start vs waiting for timer
38
- h.upstream.SetHealthiness(h.IsHealthy(url))
39
- stop := make(chan bool)
40
- h.StopChan = stop
41
-
42
- go func() {
43
- for {
44
- select {
45
- case <-h.Ticker.C:
46
- h.upstream.SetHealthiness(h.IsHealthy(url))
47
- case <-stop:
48
- return
49
- }
50
- }
51
- }()
52
-}
53
-
54
-// Stop stops the healthchecker from makeing further requests.
55
-func (h *HealthChecker) Stop() {
56
- h.Ticker.Stop()
57
- close(h.StopChan)
58
-}
59
-
60
-// IsHealthy attempts to check if a upstream host is healthy.
61
-func (h *HealthChecker) IsHealthy(url string) bool {
62
- req, err := http.NewRequest(http.MethodGet, url, nil)
63
- if err != nil {
64
- return false
65
- }
66
-
67
- resp, err := h.HTTPClient.Do(req)
68
- if err != nil {
69
- return false
70
- }
71
-
72
- if resp.StatusCode < 200 || resp.StatusCode >= 400 {
73
- return false
74
- }
75
-
76
- return true
77
-}
78
-
79
-// NewHealthCheckWorker returns a new instance of a HealthChecker.
80
-func NewHealthCheckWorker(u Upstream, interval time.Duration, client *http.Client) *HealthChecker {
81
- return &HealthChecker{
82
- upstream: u,
83
- Ticker: time.NewTicker(interval),
84
- HTTPClient: client,
85
- }
86
-}
modules/caddyhttp/reverseproxy/httptransport.go
new
+133
@@ -0,0 +1,133 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
2
+//
3
+// Licensed under the Apache License, Version 2.0 (the "License");
4
+// you may not use this file except in compliance with the License.
5
+// You may obtain a copy of the License at
6
+//
7
+// http://www.apache.org/licenses/LICENSE-2.0
8
+//
9
+// Unless required by applicable law or agreed to in writing, software
10
+// distributed under the License is distributed on an "AS IS" BASIS,
11
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+// See the License for the specific language governing permissions and
13
+// limitations under the License.
14
+
15
+package reverseproxy
16
+
17
+import (
18
+ "net"
19
+ "net/http"
20
+ "time"
21
+
22
+ "github.com/caddyserver/caddy/v2"
23
+)
24
+
25
+func init() {
26
+ caddy.RegisterModule(HTTPTransport{})
27
+}
28
+
29
+// TODO: This is the default transport, basically just http.Transport, but we define JSON struct tags...
30
+type HTTPTransport struct {
31
+ // TODO: Actually this is where the TLS config should go, technically...
32
+ // as well as keepalives and dial timeouts...
33
+ // TODO: It's possible that other transports (like fastcgi) might be
34
+ // able to borrow/use at least some of these config fields; if so,
35
+ // move them into a type called CommonTransport and embed it
36
+
37
+ TLS *TLSConfig `json:"tls,omitempty"`
38
+ KeepAlive *KeepAlive `json:"keep_alive,omitempty"`
39
+ Compression *bool `json:"compression,omitempty"`
40
+ MaxConnsPerHost int `json:"max_conns_per_host,omitempty"` // TODO: NOTE: we use our health check stuff to enforce max REQUESTS per host, but this is connections
41
+ DialTimeout caddy.Duration `json:"dial_timeout,omitempty"`
42
+ FallbackDelay caddy.Duration `json:"dial_fallback_delay,omitempty"`
43
+ ResponseHeaderTimeout caddy.Duration `json:"response_header_timeout,omitempty"`
44
+ ExpectContinueTimeout caddy.Duration `json:"expect_continue_timeout,omitempty"`
45
+ MaxResponseHeaderSize int64 `json:"max_response_header_size,omitempty"`
46
+ WriteBufferSize int `json:"write_buffer_size,omitempty"`
47
+ ReadBufferSize int `json:"read_buffer_size,omitempty"`
48
+ // TODO: ProxyConnectHeader?
49
+
50
+ RoundTripper http.RoundTripper `json:"-"`
51
+}
52
+
53
+// CaddyModule returns the Caddy module information.
54
+func (HTTPTransport) CaddyModule() caddy.ModuleInfo {
55
+ return caddy.ModuleInfo{
56
+ Name: "http.handlers.reverse_proxy.transport.http",
57
+ New: func() caddy.Module { return new(HTTPTransport) },
58
+ }
59
+}
60
+
61
+func (h *HTTPTransport) Provision(ctx caddy.Context) error {
62
+ dialer := &net.Dialer{
63
+ Timeout: time.Duration(h.DialTimeout),
64
+ FallbackDelay: time.Duration(h.FallbackDelay),
65
+ // TODO: Resolver
66
+ }
67
+ rt := &http.Transport{
68
+ DialContext: dialer.DialContext,
69
+ MaxConnsPerHost: h.MaxConnsPerHost,
70
+ ResponseHeaderTimeout: time.Duration(h.ResponseHeaderTimeout),
71
+ ExpectContinueTimeout: time.Duration(h.ExpectContinueTimeout),
72
+ MaxResponseHeaderBytes: h.MaxResponseHeaderSize,
73
+ WriteBufferSize: h.WriteBufferSize,
74
+ ReadBufferSize: h.ReadBufferSize,
75
+ }
76
+
77
+ if h.TLS != nil {
78
+ rt.TLSHandshakeTimeout = time.Duration(h.TLS.HandshakeTimeout)
79
+ // TODO: rest of TLS config
80
+ }
81
+
82
+ if h.KeepAlive != nil {
83
+ dialer.KeepAlive = time.Duration(h.KeepAlive.ProbeInterval)
84
+
85
+ if enabled := h.KeepAlive.Enabled; enabled != nil {
86
+ rt.DisableKeepAlives = !*enabled
87
+ }
88
+ rt.MaxIdleConns = h.KeepAlive.MaxIdleConns
89
+ rt.MaxIdleConnsPerHost = h.KeepAlive.MaxIdleConnsPerHost
90
+ rt.IdleConnTimeout = time.Duration(h.KeepAlive.IdleConnTimeout)
91
+ }
92
+
93
+ if h.Compression != nil {
94
+ rt.DisableCompression = !*h.Compression
95
+ }
96
+
97
+ h.RoundTripper = rt
98
+
99
+ return nil
100
+}
101
+
102
+func (h HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
103
+ return h.RoundTripper.RoundTrip(req)
104
+}
105
+
106
+type TLSConfig struct {
107
+ CAPool []string `json:"ca_pool,omitempty"`
108
+ ClientCertificate string `json:"client_certificate,omitempty"`
109
+ InsecureSkipVerify bool `json:"insecure_skip_verify,omitempty"`
110
+ HandshakeTimeout caddy.Duration `json:"handshake_timeout,omitempty"`
111
+}
112
+
113
+type KeepAlive struct {
114
+ Enabled *bool `json:"enabled,omitempty"`
115
+ ProbeInterval caddy.Duration `json:"probe_interval,omitempty"`
116
+ MaxIdleConns int `json:"max_idle_conns,omitempty"`
117
+ MaxIdleConnsPerHost int `json:"max_idle_conns_per_host,omitempty"`
118
+ IdleConnTimeout caddy.Duration `json:"idle_timeout,omitempty"` // how long should connections be kept alive when idle
119
+}
120
+
121
+var (
122
+ defaultDialer = net.Dialer{
123
+ Timeout: 10 * time.Second,
124
+ KeepAlive: 30 * time.Second,
125
+ }
126
+
127
+ // TODO: does this need to be configured to enable HTTP/2?
128
+ defaultTransport = &http.Transport{
129
+ DialContext: defaultDialer.DialContext,
130
+ TLSHandshakeTimeout: 5 * time.Second,
131
+ IdleConnTimeout: 2 * time.Minute,
132
+ }
133
+)
modules/caddyhttp/reverseproxy/module.go
deleted
-53
@@ -1,53 +0,0 @@
1
-// Copyright 2015 Matthew Holt and The Caddy Authors
2
-//
3
-// Licensed under the Apache License, Version 2.0 (the "License");
4
-// you may not use this file except in compliance with the License.
5
-// You may obtain a copy of the License at
6
-//
7
-// http://www.apache.org/licenses/LICENSE-2.0
8
-//
9
-// Unless required by applicable law or agreed to in writing, software
10
-// distributed under the License is distributed on an "AS IS" BASIS,
11
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
-// See the License for the specific language governing permissions and
13
-// limitations under the License.
14
-
15
-package reverseproxy
16
-
17
-import (
18
- "github.com/caddyserver/caddy/v2"
19
- "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
20
- "github.com/caddyserver/caddy/v2/modules/caddyhttp"
21
-)
22
-
23
-func init() {
24
- caddy.RegisterModule(new(LoadBalanced))
25
- httpcaddyfile.RegisterHandlerDirective("reverse_proxy", parseCaddyfile) // TODO: "proxy"?
26
-}
27
-
28
-// CaddyModule returns the Caddy module information.
29
-func (*LoadBalanced) CaddyModule() caddy.ModuleInfo {
30
- return caddy.ModuleInfo{
31
- Name: "http.handlers.reverse_proxy",
32
- New: func() caddy.Module { return new(LoadBalanced) },
33
- }
34
-}
35
-
36
-// parseCaddyfile sets up the handler from Caddyfile tokens. Syntax:
37
-//
38
-// proxy [<matcher>] <to>
39
-//
40
-// TODO: This needs to be finished. It definitely needs to be able to open a block...
41
-func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
42
- lb := new(LoadBalanced)
43
- for h.Next() {
44
- allTo := h.RemainingArgs()
45
- if len(allTo) == 0 {
46
- return nil, h.ArgErr()
47
- }
48
- for _, to := range allTo {
49
- lb.Upstreams = append(lb.Upstreams, &UpstreamConfig{Host: to})
50
- }
51
- }
52
- return lb, nil
53
-}
modules/caddyhttp/reverseproxy/reverseproxy.go
+566
-280
@@ -15,7 +15,9 @@
15
package reverseproxy
16
17
import (
18
+ "bytes"
19
"context"
20
+ "encoding/json"
21
"fmt"
22
"io"
23
"log"
@@ -24,219 +26,229 @@ import (
26
"net/url"
27
"strings"
28
"sync"
29
+ "sync/atomic"
30
"time"
31
32
+ "github.com/caddyserver/caddy/v2"
33
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp"
34
"golang.org/x/net/http/httpguts"
35
)
36
32
-// ReverseProxy is an HTTP Handler that takes an incoming request and
33
-// sends it to another server, proxying the response back to the
34
-// client.
35
-type ReverseProxy struct {
36
- // Director must be a function which modifies
37
- // the request into a new request to be sent
38
- // using Transport. Its response is then copied
39
- // back to the original client unmodified.
40
- // Director must not access the provided Request
41
- // after returning.
42
- Director func(*http.Request)
43
-
44
- // The transport used to perform proxy requests.
45
- // If nil, http.DefaultTransport is used.
46
- Transport http.RoundTripper
47
-
48
- // FlushInterval specifies the flush interval
49
- // to flush to the client while copying the
50
- // response body.
51
- // If zero, no periodic flushing is done.
52
- // A negative value means to flush immediately
53
- // after each write to the client.
54
- // The FlushInterval is ignored when ReverseProxy
55
- // recognizes a response as a streaming response;
56
- // for such responses, writes are flushed to the client
57
- // immediately.
58
- FlushInterval time.Duration
59
-
60
- // ErrorLog specifies an optional logger for errors
61
- // that occur when attempting to proxy the request.
62
- // If nil, logging goes to os.Stderr via the log package's
63
- // standard logger.
64
- ErrorLog *log.Logger
65
-
66
- // BufferPool optionally specifies a buffer pool to
67
- // get byte slices for use by io.CopyBuffer when
68
- // copying HTTP response bodies.
69
- BufferPool BufferPool
70
-
71
- // ModifyResponse is an optional function that modifies the
72
- // Response from the backend. It is called if the backend
73
- // returns a response at all, with any HTTP status code.
74
- // If the backend is unreachable, the optional ErrorHandler is
75
- // called without any call to ModifyResponse.
76
- //
77
- // If ModifyResponse returns an error, ErrorHandler is called
78
- // with its error value. If ErrorHandler is nil, its default
79
- // implementation is used.
80
- ModifyResponse func(*http.Response) error
81
-
82
- // ErrorHandler is an optional function that handles errors
83
- // reaching the backend or errors from ModifyResponse.
84
- //
85
- // If nil, the default is to log the provided error and return
86
- // a 502 Status Bad Gateway response.
87
- ErrorHandler func(http.ResponseWriter, *http.Request, error)
88
-}
89
-
90
-// A BufferPool is an interface for getting and returning temporary
91
-// byte slices for use by io.CopyBuffer.
92
-type BufferPool interface {
93
- Get() []byte
94
- Put([]byte)
37
+func init() {
38
+ caddy.RegisterModule(Handler{})
39
}
40
97
-func singleJoiningSlash(a, b string) string {
98
- aslash := strings.HasSuffix(a, "/")
99
- bslash := strings.HasPrefix(b, "/")
100
- switch {
101
- case aslash && bslash:
102
- return a + b[1:]
103
- case !aslash && !bslash:
104
- return a + "/" + b
41
+type Handler struct {
42
+ TransportRaw json.RawMessage `json:"transport,omitempty"`
43
+ LoadBalancing *LoadBalancing `json:"load_balancing,omitempty"`
44
+ HealthChecks *HealthChecks `json:"health_checks,omitempty"`
45
+ // UpstreamStorageRaw json.RawMessage `json:"upstream_storage,omitempty"` // TODO:
46
+ Upstreams HostPool `json:"upstreams,omitempty"`
47
+
48
+ // UpstreamProvider UpstreamProvider `json:"-"` // TODO:
49
+ Transport http.RoundTripper `json:"-"`
50
+}
51
+
52
+// CaddyModule returns the Caddy module information.
53
+func (Handler) CaddyModule() caddy.ModuleInfo {
54
+ return caddy.ModuleInfo{
55
+ Name: "http.handlers.reverse_proxy",
56
+ New: func() caddy.Module { return new(Handler) },
57
}
106
- return a + b
58
}
59
109
-// NewSingleHostReverseProxy returns a new ReverseProxy that routes
110
-// URLs to the scheme, host, and base path provided in target. If the
111
-// target's path is "/base" and the incoming request was for "/dir",
112
-// the target request will be for /base/dir.
113
-// NewSingleHostReverseProxy does not rewrite the Host header.
114
-// To rewrite Host headers, use ReverseProxy directly with a custom
115
-// Director policy.
116
-func NewSingleHostReverseProxy(target *url.URL) *ReverseProxy {
117
- targetQuery := target.RawQuery
118
- director := func(req *http.Request) {
119
- req.URL.Scheme = target.Scheme
120
- req.URL.Host = target.Host
121
- req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
122
- if targetQuery == "" || req.URL.RawQuery == "" {
123
- req.URL.RawQuery = targetQuery + req.URL.RawQuery
124
- } else {
125
- req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
60
+func (h *Handler) Provision(ctx caddy.Context) error {
61
+ if h.TransportRaw != nil {
62
+ val, err := ctx.LoadModuleInline("protocol", "http.handlers.reverse_proxy.transport", h.TransportRaw)
63
+ if err != nil {
64
+ return fmt.Errorf("loading transport module: %s", err)
65
}
127
- if _, ok := req.Header["User-Agent"]; !ok {
128
- // explicitly disable User-Agent so it's not set to default value
129
- req.Header.Set("User-Agent", "")
66
+ h.Transport = val.(http.RoundTripper)
67
+ h.TransportRaw = nil // allow GC to deallocate - TODO: Does this help?
68
+ }
69
+ if h.LoadBalancing != nil && h.LoadBalancing.SelectionPolicyRaw != nil {
70
+ val, err := ctx.LoadModuleInline("policy",
71
+ "http.handlers.reverse_proxy.selection_policies",
72
+ h.LoadBalancing.SelectionPolicyRaw)
73
+ if err != nil {
74
+ return fmt.Errorf("loading load balancing selection module: %s", err)
75
}
76
+ h.LoadBalancing.SelectionPolicy = val.(Selector)
77
+ h.LoadBalancing.SelectionPolicyRaw = nil // allow GC to deallocate - TODO: Does this help?
78
}
132
- return &ReverseProxy{Director: director}
133
-}
79
135
-func copyHeader(dst, src http.Header) {
136
- for k, vv := range src {
137
- for _, v := range vv {
138
- dst.Add(k, v)
139
- }
80
+ if h.Transport == nil {
81
+ h.Transport = defaultTransport
82
}
141
-}
83
143
-func cloneHeader(h http.Header) http.Header {
144
- h2 := make(http.Header, len(h))
145
- for k, vv := range h {
146
- vv2 := make([]string, len(vv))
147
- copy(vv2, vv)
148
- h2[k] = vv2
84
+ if h.LoadBalancing == nil {
85
+ h.LoadBalancing = new(LoadBalancing)
86
+ }
87
+ if h.LoadBalancing.SelectionPolicy == nil {
88
+ h.LoadBalancing.SelectionPolicy = RandomSelection{}
89
+ }
90
+ if h.LoadBalancing.TryDuration > 0 && h.LoadBalancing.TryInterval == 0 {
91
+ // a non-zero try_duration with a zero try_interval
92
+ // will always spin the CPU for try_duration if the
93
+ // upstream is local or low-latency; default to some
94
+ // sane waiting period before try attempts
95
+ h.LoadBalancing.TryInterval = caddy.Duration(250 * time.Millisecond)
96
}
150
- return h2
151
-}
97
153
-// Hop-by-hop headers. These are removed when sent to the backend.
154
-// As of RFC 7230, hop-by-hop headers are required to appear in the
155
-// Connection header field. These are the headers defined by the
156
-// obsoleted RFC 2616 (section 13.5.1) and are used for backward
157
-// compatibility.
158
-var hopHeaders = []string{
159
- "Connection",
160
- "Proxy-Connection", // non-standard but still sent by libcurl and rejected by e.g. google
161
- "Keep-Alive",
162
- "Proxy-Authenticate",
163
- "Proxy-Authorization",
164
- "Te", // canonicalized version of "TE"
165
- "Trailer", // not Trailers per URL above; https://www.rfc-editor.org/errata_search.php?eid=4522
166
- "Transfer-Encoding",
167
- "Upgrade",
168
-}
98
+ for _, upstream := range h.Upstreams {
99
+ // url parser requires a scheme
100
+ if !strings.Contains(upstream.Address, "://") {
101
+ upstream.Address = "http://" + upstream.Address
102
+ }
103
+ u, err := url.Parse(upstream.Address)
104
+ if err != nil {
105
+ return fmt.Errorf("invalid upstream address %s: %v", upstream.Address, err)
106
+ }
107
+ upstream.hostURL = u
108
+
109
+ // if host already exists from a current config,
110
+ // use that instead; otherwise, add it
111
+ // TODO: make hosts modular, so that their state can be distributed in enterprise for example
112
+ // TODO: If distributed, the pool should be stored in storage...
113
+ var host Host = new(upstreamHost)
114
+ activeHost, loaded := hosts.LoadOrStore(u.String(), host)
115
+ if loaded {
116
+ host = activeHost.(Host)
117
+ }
118
+ upstream.Host = host
119
+
120
+ // if the passive health checker has a non-zero "unhealthy
121
+ // request count" but the upstream has no MaxRequests set
122
+ // (they are the same thing, but one is a default value for
123
+ // for upstreams with a zero MaxRequests), copy the default
124
+ // value into this upstream, since the value in the upstream
125
+ // is what is used during availability checks
126
+ if h.HealthChecks != nil &&
127
+ h.HealthChecks.Passive != nil &&
128
+ h.HealthChecks.Passive.UnhealthyRequestCount > 0 &&
129
+ upstream.MaxRequests == 0 {
130
+ upstream.MaxRequests = h.HealthChecks.Passive.UnhealthyRequestCount
131
+ }
132
170
-func (p *ReverseProxy) defaultErrorHandler(rw http.ResponseWriter, req *http.Request, err error) {
171
- p.logf("http: proxy error: %v", err)
172
- rw.WriteHeader(http.StatusBadGateway)
173
-}
133
+ // TODO: active health checks
134
175
-func (p *ReverseProxy) getErrorHandler() func(http.ResponseWriter, *http.Request, error) {
176
- if p.ErrorHandler != nil {
177
- return p.ErrorHandler
135
+ if h.HealthChecks != nil {
136
+ // upstreams need independent access to the passive
137
+ // health check policy so they can, you know, passively
138
+ // do health checks
139
+ upstream.healthCheckPolicy = h.HealthChecks.Passive
140
+ }
141
}
179
- return p.defaultErrorHandler
142
+
143
+ return nil
144
}
145
182
-// modifyResponse conditionally runs the optional ModifyResponse hook
183
-// and reports whether the request should proceed.
184
-func (p *ReverseProxy) modifyResponse(rw http.ResponseWriter, res *http.Response, req *http.Request) bool {
185
- if p.ModifyResponse == nil {
186
- return true
187
- }
188
- if err := p.ModifyResponse(res); err != nil {
189
- res.Body.Close()
190
- p.getErrorHandler()(rw, req, err)
191
- return false
146
+func (h *Handler) Cleanup() error {
147
+ // TODO: finish this up, make sure it takes care of any active health checkers or whatever
148
+ for _, upstream := range h.Upstreams {
149
+ hosts.Delete(upstream.hostURL.String())
150
}
193
- return true
151
+ return nil
152
}
153
196
-func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) (*http.Response, error) {
197
- transport := p.Transport
198
- if transport == nil {
199
- transport = http.DefaultTransport
154
+func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
155
+ // prepare the request for proxying; this is needed only once
156
+ err := h.prepareRequest(r)
157
+ if err != nil {
158
+ return caddyhttp.Error(http.StatusInternalServerError,
159
+ fmt.Errorf("preparing request for upstream round-trip: %v", err))
160
}
161
202
- ctx := req.Context()
203
- if cn, ok := rw.(http.CloseNotifier); ok {
204
- var cancel context.CancelFunc
205
- ctx, cancel = context.WithCancel(ctx)
206
- defer cancel()
207
- notifyChan := cn.CloseNotify()
208
- go func() {
209
- select {
210
- case <-notifyChan:
211
- cancel()
212
- case <-ctx.Done():
162
+ start := time.Now()
163
+
164
+ var proxyErr error
165
+ for {
166
+ // choose an available upstream
167
+ upstream := h.LoadBalancing.SelectionPolicy.Select(h.Upstreams, r)
168
+ if upstream == nil {
169
+ if proxyErr == nil {
170
+ proxyErr = fmt.Errorf("no available upstreams")
171
+ }
172
+ if !h.tryAgain(start, proxyErr) {
173
+ break
174
}
214
- }()
175
+ continue
176
+ }
177
+
178
+ // proxy the request to that upstream
179
+ proxyErr = h.reverseProxy(w, r, upstream)
180
+ if proxyErr == nil {
181
+ return nil
182
+ }
183
+
184
+ // remember this failure (if enabled)
185
+ h.countFailure(upstream)
186
+
187
+ // if we've tried long enough, break
188
+ if !h.tryAgain(start, proxyErr) {
189
+ break
190
+ }
191
}
192
217
- outreq := req.WithContext(ctx) // includes shallow copies of maps, but okay
193
+ return caddyhttp.Error(http.StatusBadGateway, proxyErr)
194
+}
195
+
196
+// prepareRequest modifies req so that it is ready to be proxied,
197
+// except for directing to a specific upstream. This method mutates
198
+// headers and other necessary properties of the request and should
199
+// be done just once (before proxying) regardless of proxy retries.
200
+// This assumes that no mutations of the request are performed
201
+// by h during or after proxying.
202
+func (h Handler) prepareRequest(req *http.Request) error {
203
+ // ctx := req.Context()
204
+ // TODO: do we need to support CloseNotifier? It was deprecated years ago.
205
+ // All this does is wrap CloseNotify with context cancel, for those responsewriters
206
+ // which didn't support context, but all the ones we'd use should nowadays, right?
207
+ // if cn, ok := rw.(http.CloseNotifier); ok {
208
+ // var cancel context.CancelFunc
209
+ // ctx, cancel = context.WithCancel(ctx)
210
+ // defer cancel()
211
+ // notifyChan := cn.CloseNotify()
212
+ // go func() {
213
+ // select {
214
+ // case <-notifyChan:
215
+ // cancel()
216
+ // case <-ctx.Done():
217
+ // }
218
+ // }()
219
+ // }
220
+
221
+ // TODO: do we need to call WithContext, since we won't be changing req.Context() above if we remove the CloseNotifier stuff?
222
+ // TODO: (This is where references to req were originally "outreq", a shallow clone, which I think is unnecessary in our case)
223
+ // req = req.WithContext(ctx) // includes shallow copies of maps, but okay
224
if req.ContentLength == 0 {
219
- outreq.Body = nil // Issue 16036: nil Body for http.Transport retries
225
+ req.Body = nil // Issue golang/go#16036: nil Body for http.Transport retries
226
}
227
222
- outreq.Header = cloneHeader(req.Header)
228
+ // TODO: is this needed?
229
+ // req.Header = cloneHeader(req.Header)
230
224
- p.Director(outreq)
225
- outreq.Close = false
231
+ req.Close = false
232
227
- reqUpType := upgradeType(outreq.Header)
228
- removeConnectionHeaders(outreq.Header)
233
+ // if User-Agent is not set by client, then explicitly
234
+ // disable it so it's not set to default value by std lib
235
+ if _, ok := req.Header["User-Agent"]; !ok {
236
+ req.Header.Set("User-Agent", "")
237
+ }
238
+
239
+ reqUpType := upgradeType(req.Header)
240
+ removeConnectionHeaders(req.Header)
241
242
// Remove hop-by-hop headers to the backend. Especially
243
// important is "Connection" because we want a persistent
244
// connection, regardless of what the client sent to us.
245
for _, h := range hopHeaders {
234
- hv := outreq.Header.Get(h)
246
+ hv := req.Header.Get(h)
247
if hv == "" {
248
continue
249
}
250
if h == "Te" && hv == "trailers" {
239
- // Issue 21096: tell backend applications that
251
+ // Issue golang/go#21096: tell backend applications that
252
// care about trailer support that we support
253
// trailers. (We do, but we don't go out of
254
// our way to advertise that unless the
@@ -244,40 +256,65 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) (*ht
256
// worth mentioning)
257
continue
258
}
247
- outreq.Header.Del(h)
259
+ req.Header.Del(h)
260
}
261
262
// After stripping all the hop-by-hop connection headers above, add back any
263
// necessary for protocol upgrades, such as for websockets.
264
if reqUpType != "" {
253
- outreq.Header.Set("Connection", "Upgrade")
254
- outreq.Header.Set("Upgrade", reqUpType)
265
+ req.Header.Set("Connection", "Upgrade")
266
+ req.Header.Set("Upgrade", reqUpType)
267
}
268
269
if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
270
// If we aren't the first proxy retain prior
271
// X-Forwarded-For information as a comma+space
272
// separated list and fold multiple headers into one.
261
- if prior, ok := outreq.Header["X-Forwarded-For"]; ok {
273
+ if prior, ok := req.Header["X-Forwarded-For"]; ok {
274
clientIP = strings.Join(prior, ", ") + ", " + clientIP
275
}
264
- outreq.Header.Set("X-Forwarded-For", clientIP)
276
+ req.Header.Set("X-Forwarded-For", clientIP)
277
}
278
267
- res, err := transport.RoundTrip(outreq)
279
+ return nil
280
+}
281
+
282
+// TODO:
283
+// this code is the entry point to what was borrowed from the net/http/httputil package in the standard library.
284
+func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, upstream *Upstream) error {
285
+ // TODO: count this active request
286
+
287
+ // point the request to this upstream
288
+ h.directRequest(req, upstream)
289
+
290
+ // do the round-trip
291
+ start := time.Now()
292
+ res, err := h.Transport.RoundTrip(req)
293
+ latency := time.Since(start)
294
if err != nil {
269
- p.getErrorHandler()(rw, outreq, err)
270
- return nil, err
295
+ return err
296
}
297
273
- // Deal with 101 Switching Protocols responses: (WebSocket, h2c, etc)
274
- if res.StatusCode == http.StatusSwitchingProtocols {
275
- if !p.modifyResponse(rw, res, outreq) {
276
- return res, nil
298
+ // perform passive health checks (if enabled)
299
+ if h.HealthChecks != nil && h.HealthChecks.Passive != nil {
300
+ // strike if the status code matches one that is "bad"
301
+ for _, badStatus := range h.HealthChecks.Passive.UnhealthyStatus {
302
+ if caddyhttp.StatusCodeMatches(res.StatusCode, badStatus) {
303
+ h.countFailure(upstream)
304
+ }
305
}
306
279
- p.handleUpgradeResponse(rw, outreq, res)
280
- return res, nil
307
+ // strike if the roundtrip took too long
308
+ if h.HealthChecks.Passive.UnhealthyLatency > 0 &&
309
+ latency >= time.Duration(h.HealthChecks.Passive.UnhealthyLatency) {
310
+ h.countFailure(upstream)
311
+ }
312
+ }
313
+
314
+ // Deal with 101 Switching Protocols responses: (WebSocket, h2c, etc)
315
+ if res.StatusCode == http.StatusSwitchingProtocols {
316
+ h.handleUpgradeResponse(rw, req, res)
317
+ return nil
318
}
319
320
removeConnectionHeaders(res.Header)
@@ -286,10 +323,6 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) (*ht
323
res.Header.Del(h)
324
}
325
289
- if !p.modifyResponse(rw, res, outreq) {
290
- return res, nil
291
- }
292
-
326
copyHeader(rw.Header(), res.Header)
327
328
// The "Trailer" header isn't included in the Transport's response,
@@ -305,15 +338,16 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) (*ht
338
339
rw.WriteHeader(res.StatusCode)
340
308
- err = p.copyResponse(rw, res.Body, p.flushInterval(req, res))
341
+ err = h.copyResponse(rw, res.Body, h.flushInterval(req, res))
342
if err != nil {
343
defer res.Body.Close()
344
// Since we're streaming the response, if we run into an error all we can do
312
- // is abort the request. Issue 23643: ReverseProxy should use ErrAbortHandler
345
+ // is abort the request. Issue golang/go#23643: ReverseProxy should use ErrAbortHandler
346
// on read error while copying body.
347
+ // TODO: Look into whether we want to panic at all in our case...
348
if !shouldPanicOnCopyError(req) {
315
- p.logf("suppressing panic for copyResponse error in test; copy error: %v", err)
316
- return nil, err
349
+ // p.logf("suppressing panic for copyResponse error in test; copy error: %v", err)
350
+ return err
351
}
352
353
panic(http.ErrAbortHandler)
@@ -331,7 +365,7 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) (*ht
365
366
if len(res.Trailer) == announcedTrailers {
367
copyHeader(rw.Header(), res.Trailer)
334
- return res, nil
368
+ return nil
369
}
370
371
for k, vv := range res.Trailer {
@@ -341,46 +375,90 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) (*ht
375
}
376
}
377
344
- return res, nil
378
+ return nil
379
}
380
347
-var inOurTests bool // whether we're in our own tests
348
-
349
-// shouldPanicOnCopyError reports whether the reverse proxy should
350
-// panic with http.ErrAbortHandler. This is the right thing to do by
351
-// default, but Go 1.10 and earlier did not, so existing unit tests
352
-// weren't expecting panics. Only panic in our own tests, or when
353
-// running under the HTTP server.
354
-func shouldPanicOnCopyError(req *http.Request) bool {
355
- if inOurTests {
356
- // Our tests know to handle this panic.
357
- return true
381
+// tryAgain takes the time that the handler was initially invoked
382
+// as well as any error currently obtained and returns true if
383
+// another attempt should be made at proxying the request. If
384
+// true is returned, it has already blocked long enough before
385
+// the next retry (i.e. no more sleeping is needed). If false is
386
+// returned, the handler should stop trying to proxy the request.
387
+func (h Handler) tryAgain(start time.Time, proxyErr error) bool {
388
+ // if downstream has canceled the request, break
389
+ if proxyErr == context.Canceled {
390
+ return false
391
}
359
- if req.Context().Value(http.ServerContextKey) != nil {
360
- // We seem to be running under an HTTP server, so
361
- // it'll recover the panic.
362
- return true
392
+ // if we've tried long enough, break
393
+ if time.Since(start) >= time.Duration(h.LoadBalancing.TryDuration) {
394
+ return false
395
}
364
- // Otherwise act like Go 1.10 and earlier to not break
365
- // existing tests.
366
- return false
396
+ // otherwise, wait and try the next available host
397
+ time.Sleep(time.Duration(h.LoadBalancing.TryInterval))
398
+ return true
399
}
400
369
-// removeConnectionHeaders removes hop-by-hop headers listed in the "Connection" header of h.
370
-// See RFC 7230, section 6.1
371
-func removeConnectionHeaders(h http.Header) {
372
- if c := h.Get("Connection"); c != "" {
373
- for _, f := range strings.Split(c, ",") {
374
- if f = strings.TrimSpace(f); f != "" {
375
- h.Del(f)
376
- }
377
- }
401
+// directRequest modifies only req.URL so that it points to the
402
+// given upstream host. It must modify ONLY the request URL.
403
+func (h Handler) directRequest(req *http.Request, upstream *Upstream) {
404
+ target := upstream.hostURL
405
+ req.URL.Scheme = target.Scheme
406
+ req.URL.Host = target.Host
407
+ 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!)
408
+ if target.RawQuery == "" || req.URL.RawQuery == "" {
409
+ req.URL.RawQuery = target.RawQuery + req.URL.RawQuery
410
+ } else {
411
+ req.URL.RawQuery = target.RawQuery + "&" + req.URL.RawQuery
412
}
413
}
414
415
+func (h Handler) handleUpgradeResponse(rw http.ResponseWriter, req *http.Request, res *http.Response) {
416
+ reqUpType := upgradeType(req.Header)
417
+ resUpType := upgradeType(res.Header)
418
+ if reqUpType != resUpType {
419
+ // p.getErrorHandler()(rw, req, fmt.Errorf("backend tried to switch protocol %q when %q was requested", resUpType, reqUpType))
420
+ return
421
+ }
422
+
423
+ copyHeader(res.Header, rw.Header())
424
+
425
+ hj, ok := rw.(http.Hijacker)
426
+ if !ok {
427
+ // p.getErrorHandler()(rw, req, fmt.Errorf("can't switch protocols using non-Hijacker ResponseWriter type %T", rw))
428
+ return
429
+ }
430
+ backConn, ok := res.Body.(io.ReadWriteCloser)
431
+ if !ok {
432
+ // p.getErrorHandler()(rw, req, fmt.Errorf("internal error: 101 switching protocols response with non-writable body"))
433
+ return
434
+ }
435
+ defer backConn.Close()
436
+ conn, brw, err := hj.Hijack()
437
+ if err != nil {
438
+ // p.getErrorHandler()(rw, req, fmt.Errorf("Hijack failed on protocol switch: %v", err))
439
+ return
440
+ }
441
+ defer conn.Close()
442
+ res.Body = nil // so res.Write only writes the headers; we have res.Body in backConn above
443
+ if err := res.Write(brw); err != nil {
444
+ // p.getErrorHandler()(rw, req, fmt.Errorf("response write: %v", err))
445
+ return
446
+ }
447
+ if err := brw.Flush(); err != nil {
448
+ // p.getErrorHandler()(rw, req, fmt.Errorf("response flush: %v", err))
449
+ return
450
+ }
451
+ errc := make(chan error, 1)
452
+ spc := switchProtocolCopier{user: conn, backend: backConn}
453
+ go spc.copyToBackend(errc)
454
+ go spc.copyFromBackend(errc)
455
+ <-errc
456
+ return
457
+}
458
+
459
// flushInterval returns the p.FlushInterval value, conditionally
460
// overriding its value for a specific request/response.
383
-func (p *ReverseProxy) flushInterval(req *http.Request, res *http.Response) time.Duration {
461
+func (h Handler) flushInterval(req *http.Request, res *http.Response) time.Duration {
462
resCT := res.Header.Get("Content-Type")
463
464
// For Server-Sent Events responses, flush immediately.
@@ -390,10 +468,11 @@ func (p *ReverseProxy) flushInterval(req *http.Request, res *http.Response) time
468
}
469
470
// TODO: more specific cases? e.g. res.ContentLength == -1?
393
- return p.FlushInterval
471
+ // return h.FlushInterval
472
+ return 0
473
}
474
396
-func (p *ReverseProxy) copyResponse(dst io.Writer, src io.Reader, flushInterval time.Duration) error {
475
+func (h Handler) copyResponse(dst io.Writer, src io.Reader, flushInterval time.Duration) error {
476
if flushInterval != 0 {
477
if wf, ok := dst.(writeFlusher); ok {
478
mlw := &maxLatencyWriter{
@@ -405,18 +484,25 @@ func (p *ReverseProxy) copyResponse(dst io.Writer, src io.Reader, flushInterval
484
}
485
}
486
487
+ // TODO: Figure out how we want to do this... using custom buffer pool type seems unnecessary
488
+ // or maybe it is, depending on how we want to handle errors,
489
+ // see: https://github.com/golang/go/issues/21814
490
+ // buf := bufPool.Get().(*bytes.Buffer)
491
+ // buf.Reset()
492
+ // defer bufPool.Put(buf)
493
+ // _, err := io.CopyBuffer(dst, src, )
494
var buf []byte
409
- if p.BufferPool != nil {
410
- buf = p.BufferPool.Get()
411
- defer p.BufferPool.Put(buf)
412
- }
413
- _, err := p.copyBuffer(dst, src, buf)
495
+ // if h.BufferPool != nil {
496
+ // buf = h.BufferPool.Get()
497
+ // defer h.BufferPool.Put(buf)
498
+ // }
499
+ _, err := h.copyBuffer(dst, src, buf)
500
return err
501
}
502
503
// copyBuffer returns any write errors or non-EOF read errors, and the amount
504
// of bytes written.
419
-func (p *ReverseProxy) copyBuffer(dst io.Writer, src io.Reader, buf []byte) (int64, error) {
505
+func (h Handler) copyBuffer(dst io.Writer, src io.Reader, buf []byte) (int64, error) {
506
if len(buf) == 0 {
507
buf = make([]byte, 32*1024)
508
}
@@ -424,7 +510,13 @@ func (p *ReverseProxy) copyBuffer(dst io.Writer, src io.Reader, buf []byte) (int
510
for {
511
nr, rerr := src.Read(buf)
512
if rerr != nil && rerr != io.EOF && rerr != context.Canceled {
427
- p.logf("httputil: ReverseProxy read error during body copy: %v", rerr)
513
+ // TODO: this could be useful to know (indeed, it revealed an error in our
514
+ // fastcgi PoC earlier; but it's this single error report here that necessitates
515
+ // a function separate from io.CopyBuffer, since io.CopyBuffer does not distinguish
516
+ // between read or write errors; in a reverse proxy situation, write errors are not
517
+ // something we need to report to the client, but read errors are a problem on our
518
+ // end for sure. so we need to decide what we want.)
519
+ // p.logf("copyBuffer: ReverseProxy read error during body copy: %v", rerr)
520
}
521
if nr > 0 {
522
nw, werr := dst.Write(buf[:nr])
@@ -447,12 +539,36 @@ func (p *ReverseProxy) copyBuffer(dst io.Writer, src io.Reader, buf []byte) (int
539
}
540
}
541
450
-func (p *ReverseProxy) logf(format string, args ...interface{}) {
451
- if p.ErrorLog != nil {
452
- p.ErrorLog.Printf(format, args...)
453
- } else {
454
- log.Printf(format, args...)
542
+// countFailure remembers 1 failure for upstream for the
543
+// configured duration. If passive health checks are
544
+// disabled or failure expiry is 0, this is a no-op.
545
+func (h Handler) countFailure(upstream *Upstream) {
546
+ // only count failures if passive health checking is enabled
547
+ // and if failures are configured have a non-zero expiry
548
+ if h.HealthChecks == nil || h.HealthChecks.Passive == nil {
549
+ return
550
+ }
551
+ failDuration := time.Duration(h.HealthChecks.Passive.FailDuration)
552
+ if failDuration == 0 {
553
+ return
554
+ }
555
+
556
+ // count failure immediately
557
+ err := upstream.Host.CountFail(1)
558
+ if err != nil {
559
+ log.Printf("[ERROR] proxy: upstream %s: counting failure: %v",
560
+ upstream.hostURL, err)
561
}
562
+
563
+ // forget it later
564
+ go func(host Host, failDuration time.Duration) {
565
+ time.Sleep(failDuration)
566
+ err := host.CountFail(-1)
567
+ if err != nil {
568
+ log.Printf("[ERROR] proxy: upstream %s: expiring failure: %v",
569
+ upstream.hostURL, err)
570
+ }
571
+ }(upstream.Host, failDuration)
572
}
573
574
type writeFlusher interface {
@@ -508,6 +624,61 @@ func (m *maxLatencyWriter) stop() {
624
}
625
}
626
627
+// switchProtocolCopier exists so goroutines proxying data back and
628
+// forth have nice names in stacks.
629
+type switchProtocolCopier struct {
630
+ user, backend io.ReadWriter
631
+}
632
+
633
+func (c switchProtocolCopier) copyFromBackend(errc chan<- error) {
634
+ _, err := io.Copy(c.user, c.backend)
635
+ errc <- err
636
+}
637
+
638
+func (c switchProtocolCopier) copyToBackend(errc chan<- error) {
639
+ _, err := io.Copy(c.backend, c.user)
640
+ errc <- err
641
+}
642
+
643
+// shouldPanicOnCopyError reports whether the reverse proxy should
644
+// panic with http.ErrAbortHandler. This is the right thing to do by
645
+// default, but Go 1.10 and earlier did not, so existing unit tests
646
+// weren't expecting panics. Only panic in our own tests, or when
647
+// running under the HTTP server.
648
+// TODO: I don't know if we want this at all...
649
+func shouldPanicOnCopyError(req *http.Request) bool {
650
+ // if inOurTests {
651
+ // // Our tests know to handle this panic.
652
+ // return true
653
+ // }
654
+ if req.Context().Value(http.ServerContextKey) != nil {
655
+ // We seem to be running under an HTTP server, so
656
+ // it'll recover the panic.
657
+ return true
658
+ }
659
+ // Otherwise act like Go 1.10 and earlier to not break
660
+ // existing tests.
661
+ return false
662
+}
663
+
664
+func copyHeader(dst, src http.Header) {
665
+ for k, vv := range src {
666
+ for _, v := range vv {
667
+ dst.Add(k, v)
668
+ }
669
+ }
670
+}
671
+
672
+func cloneHeader(h http.Header) http.Header {
673
+ h2 := make(http.Header, len(h))
674
+ for k, vv := range h {
675
+ vv2 := make([]string, len(vv))
676
+ copy(vv2, vv)
677
+ h2[k] = vv2
678
+ }
679
+ return h2
680
+}
681
+
682
func upgradeType(h http.Header) string {
683
if !httpguts.HeaderValuesContainsToken(h["Connection"], "Upgrade") {
684
return ""
@@ -515,62 +686,177 @@ func upgradeType(h http.Header) string {
686
return strings.ToLower(h.Get("Upgrade"))
687
}
688
518
-func (p *ReverseProxy) handleUpgradeResponse(rw http.ResponseWriter, req *http.Request, res *http.Response) {
519
- reqUpType := upgradeType(req.Header)
520
- resUpType := upgradeType(res.Header)
521
- if reqUpType != resUpType {
522
- p.getErrorHandler()(rw, req, fmt.Errorf("backend tried to switch protocol %q when %q was requested", resUpType, reqUpType))
523
- return
689
+func singleJoiningSlash(a, b string) string {
690
+ aslash := strings.HasSuffix(a, "/")
691
+ bslash := strings.HasPrefix(b, "/")
692
+ switch {
693
+ case aslash && bslash:
694
+ return a + b[1:]
695
+ case !aslash && !bslash:
696
+ return a + "/" + b
697
}
698
+ return a + b
699
+}
700
526
- copyHeader(res.Header, rw.Header())
527
-
528
- hj, ok := rw.(http.Hijacker)
529
- if !ok {
530
- p.getErrorHandler()(rw, req, fmt.Errorf("can't switch protocols using non-Hijacker ResponseWriter type %T", rw))
531
- return
532
- }
533
- backConn, ok := res.Body.(io.ReadWriteCloser)
534
- if !ok {
535
- p.getErrorHandler()(rw, req, fmt.Errorf("internal error: 101 switching protocols response with non-writable body"))
536
- return
701
+// removeConnectionHeaders removes hop-by-hop headers listed in the "Connection" header of h.
702
+// See RFC 7230, section 6.1
703
+func removeConnectionHeaders(h http.Header) {
704
+ if c := h.Get("Connection"); c != "" {
705
+ for _, f := range strings.Split(c, ",") {
706
+ if f = strings.TrimSpace(f); f != "" {
707
+ h.Del(f)
708
+ }
709
+ }
710
}
538
- defer backConn.Close()
539
- conn, brw, err := hj.Hijack()
540
- if err != nil {
541
- p.getErrorHandler()(rw, req, fmt.Errorf("Hijack failed on protocol switch: %v", err))
542
- return
711
+}
712
+
713
+type LoadBalancing struct {
714
+ SelectionPolicyRaw json.RawMessage `json:"selection_policy,omitempty"`
715
+ TryDuration caddy.Duration `json:"try_duration,omitempty"`
716
+ TryInterval caddy.Duration `json:"try_interval,omitempty"`
717
+
718
+ SelectionPolicy Selector `json:"-"`
719
+}
720
+
721
+type Selector interface {
722
+ Select(HostPool, *http.Request) *Upstream
723
+}
724
+
725
+type HealthChecks struct {
726
+ Active *ActiveHealthChecks `json:"active,omitempty"`
727
+ Passive *PassiveHealthChecks `json:"passive,omitempty"`
728
+}
729
+
730
+type ActiveHealthChecks struct {
731
+ Path string `json:"path,omitempty"`
732
+ Port int `json:"port,omitempty"`
733
+ Interval caddy.Duration `json:"interval,omitempty"`
734
+ Timeout caddy.Duration `json:"timeout,omitempty"`
735
+ MaxSize int `json:"max_size,omitempty"`
736
+ ExpectStatus int `json:"expect_status,omitempty"`
737
+ ExpectBody string `json:"expect_body,omitempty"`
738
+}
739
+
740
+type PassiveHealthChecks struct {
741
+ MaxFails int `json:"max_fails,omitempty"`
742
+ FailDuration caddy.Duration `json:"fail_duration,omitempty"`
743
+ UnhealthyRequestCount int `json:"unhealthy_request_count,omitempty"`
744
+ UnhealthyStatus []int `json:"unhealthy_status,omitempty"`
745
+ UnhealthyLatency caddy.Duration `json:"unhealthy_latency,omitempty"`
746
+}
747
+
748
+// Hop-by-hop headers. These are removed when sent to the backend.
749
+// As of RFC 7230, hop-by-hop headers are required to appear in the
750
+// Connection header field. These are the headers defined by the
751
+// obsoleted RFC 2616 (section 13.5.1) and are used for backward
752
+// compatibility.
753
+var hopHeaders = []string{
754
+ "Connection",
755
+ "Proxy-Connection", // non-standard but still sent by libcurl and rejected by e.g. google
756
+ "Keep-Alive",
757
+ "Proxy-Authenticate",
758
+ "Proxy-Authorization",
759
+ "Te", // canonicalized version of "TE"
760
+ "Trailer", // not Trailers per URL above; https://www.rfc-editor.org/errata_search.php?eid=4522
761
+ "Transfer-Encoding",
762
+ "Upgrade",
763
+}
764
+
765
+var bufPool = sync.Pool{
766
+ New: func() interface{} {
767
+ return new(bytes.Buffer)
768
+ },
769
+}
770
+
771
+//////////////////////////////////
772
+// TODO:
773
+
774
+type Host interface {
775
+ NumRequests() int
776
+ Fails() int
777
+ Unhealthy() bool
778
+
779
+ CountRequest(int) error
780
+ CountFail(int) error
781
+}
782
+
783
+type HostPool []*Upstream
784
+
785
+type upstreamHost struct {
786
+ numRequests int64 // must be first field to be 64-bit aligned on 32-bit systems (see https://golang.org/pkg/sync/atomic/#pkg-note-BUG)
787
+ fails int64
788
+ unhealthy int32
789
+}
790
+
791
+func (uh upstreamHost) NumRequests() int {
792
+ return int(atomic.LoadInt64(&uh.numRequests))
793
+}
794
+func (uh upstreamHost) Fails() int {
795
+ return int(atomic.LoadInt64(&uh.fails))
796
+}
797
+func (uh upstreamHost) Unhealthy() bool {
798
+ return atomic.LoadInt32(&uh.unhealthy) == 1
799
+}
800
+func (uh *upstreamHost) CountRequest(delta int) error {
801
+ result := atomic.AddInt64(&uh.numRequests, int64(delta))
802
+ if result < 0 {
803
+ return fmt.Errorf("count below 0: %d", result)
804
}
544
- defer conn.Close()
545
- res.Body = nil // so res.Write only writes the headers; we have res.Body in backConn above
546
- if err := res.Write(brw); err != nil {
547
- p.getErrorHandler()(rw, req, fmt.Errorf("response write: %v", err))
548
- return
805
+ return nil
806
+}
807
+func (uh *upstreamHost) CountFail(delta int) error {
808
+ result := atomic.AddInt64(&uh.fails, int64(delta))
809
+ if result < 0 {
810
+ return fmt.Errorf("count below 0: %d", result)
811
}
550
- if err := brw.Flush(); err != nil {
551
- p.getErrorHandler()(rw, req, fmt.Errorf("response flush: %v", err))
552
- return
812
+ return nil
813
+}
814
+
815
+type Upstream struct {
816
+ Host `json:"-"`
817
+
818
+ Address string `json:"address,omitempty"`
819
+ MaxRequests int `json:"max_requests,omitempty"`
820
+
821
+ // TODO: This could be really cool, to say that requests with
822
+ // certain headers or from certain IPs always go to this upstream
823
+ // HeaderAffinity string
824
+ // IPAffinity string
825
+
826
+ healthCheckPolicy *PassiveHealthChecks
827
+
828
+ hostURL *url.URL
829
+}
830
+
831
+func (u Upstream) Available() bool {
832
+ return u.Healthy() && !u.Full()
833
+}
834
+
835
+func (u Upstream) Healthy() bool {
836
+ healthy := !u.Host.Unhealthy()
837
+ if healthy && u.healthCheckPolicy != nil {
838
+ healthy = u.Host.Fails() < u.healthCheckPolicy.MaxFails
839
}
554
- errc := make(chan error, 1)
555
- spc := switchProtocolCopier{user: conn, backend: backConn}
556
- go spc.copyToBackend(errc)
557
- go spc.copyFromBackend(errc)
558
- <-errc
559
- return
840
+ return healthy
841
}
842
562
-// switchProtocolCopier exists so goroutines proxying data back and
563
-// forth have nice names in stacks.
564
-type switchProtocolCopier struct {
565
- user, backend io.ReadWriter
843
+func (u Upstream) Full() bool {
844
+ return u.MaxRequests > 0 && u.Host.NumRequests() >= u.MaxRequests
845
}
846
568
-func (c switchProtocolCopier) copyFromBackend(errc chan<- error) {
569
- _, err := io.Copy(c.user, c.backend)
570
- errc <- err
847
+func (u Upstream) URL() *url.URL {
848
+ return u.hostURL
849
}
850
573
-func (c switchProtocolCopier) copyToBackend(errc chan<- error) {
574
- _, err := io.Copy(c.backend, c.user)
575
- errc <- err
851
+var hosts = caddy.NewUsagePool()
852
+
853
+// TODO: ...
854
+type UpstreamProvider interface {
855
}
856
+
857
+// Interface guards
858
+var (
859
+ _ caddyhttp.MiddlewareHandler = (*Handler)(nil)
860
+ _ caddy.Provisioner = (*Handler)(nil)
861
+ _ caddy.CleanerUpper = (*Handler)(nil)
862
+)
modules/caddyhttp/reverseproxy/selectionpolicies.go
new
+351
@@ -0,0 +1,351 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
2
+//
3
+// Licensed under the Apache License, Version 2.0 (the "License");
4
+// you may not use this file except in compliance with the License.
5
+// You may obtain a copy of the License at
6
+//
7
+// http://www.apache.org/licenses/LICENSE-2.0
8
+//
9
+// Unless required by applicable law or agreed to in writing, software
10
+// distributed under the License is distributed on an "AS IS" BASIS,
11
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+// See the License for the specific language governing permissions and
13
+// limitations under the License.
14
+
15
+package reverseproxy
16
+
17
+import (
18
+ "fmt"
19
+ "hash/fnv"
20
+ weakrand "math/rand"
21
+ "net"
22
+ "net/http"
23
+ "sync/atomic"
24
+ "time"
25
+
26
+ "github.com/caddyserver/caddy/v2"
27
+)
28
+
29
+func init() {
30
+ caddy.RegisterModule(RandomSelection{})
31
+ caddy.RegisterModule(RandomChoiceSelection{})
32
+ caddy.RegisterModule(LeastConnSelection{})
33
+ caddy.RegisterModule(RoundRobinSelection{})
34
+ caddy.RegisterModule(FirstSelection{})
35
+ caddy.RegisterModule(IPHashSelection{})
36
+ caddy.RegisterModule(URIHashSelection{})
37
+ caddy.RegisterModule(HeaderHashSelection{})
38
+
39
+ weakrand.Seed(time.Now().UTC().UnixNano())
40
+}
41
+
42
+// RandomSelection is a policy that selects
43
+// an available host at random.
44
+type RandomSelection struct{}
45
+
46
+// CaddyModule returns the Caddy module information.
47
+func (RandomSelection) CaddyModule() caddy.ModuleInfo {
48
+ return caddy.ModuleInfo{
49
+ Name: "http.handlers.reverse_proxy.selection_policies.random",
50
+ New: func() caddy.Module { return new(RandomSelection) },
51
+ }
52
+}
53
+
54
+// Select returns an available host, if any.
55
+func (r RandomSelection) Select(pool HostPool, request *http.Request) *Upstream {
56
+ // use reservoir sampling because the number of available
57
+ // hosts isn't known: https://en.wikipedia.org/wiki/Reservoir_sampling
58
+ var randomHost *Upstream
59
+ var count int
60
+ for _, upstream := range pool {
61
+ if !upstream.Available() {
62
+ continue
63
+ }
64
+ // (n % 1 == 0) holds for all n, therefore a
65
+ // upstream will always be chosen if there is at
66
+ // least one available
67
+ count++
68
+ if (weakrand.Int() % count) == 0 {
69
+ randomHost = upstream
70
+ }
71
+ }
72
+ return randomHost
73
+}
74
+
75
+// RandomChoiceSelection is a policy that selects
76
+// two or more available hosts at random, then
77
+// chooses the one with the least load.
78
+type RandomChoiceSelection struct {
79
+ Choose int `json:"choose,omitempty"`
80
+}
81
+
82
+// CaddyModule returns the Caddy module information.
83
+func (RandomChoiceSelection) CaddyModule() caddy.ModuleInfo {
84
+ return caddy.ModuleInfo{
85
+ Name: "http.handlers.reverse_proxy.selection_policies.random_choice",
86
+ New: func() caddy.Module { return new(RandomChoiceSelection) },
87
+ }
88
+}
89
+
90
+func (r *RandomChoiceSelection) Provision(ctx caddy.Context) error {
91
+ if r.Choose == 0 {
92
+ r.Choose = 2
93
+ }
94
+ return nil
95
+}
96
+
97
+func (r RandomChoiceSelection) Validate() error {
98
+ if r.Choose < 2 {
99
+ return fmt.Errorf("choose must be at least 2")
100
+ }
101
+ return nil
102
+}
103
+
104
+// Select returns an available host, if any.
105
+func (r RandomChoiceSelection) Select(pool HostPool, _ *http.Request) *Upstream {
106
+ k := r.Choose
107
+ if k > len(pool) {
108
+ k = len(pool)
109
+ }
110
+ choices := make([]*Upstream, k)
111
+ for i, upstream := range pool {
112
+ if !upstream.Available() {
113
+ continue
114
+ }
115
+ j := weakrand.Intn(i)
116
+ if j < k {
117
+ choices[j] = upstream
118
+ }
119
+ }
120
+ return leastRequests(choices)
121
+}
122
+
123
+// LeastConnSelection is a policy that selects the
124
+// host with the least active requests. If multiple
125
+// hosts have the same fewest number, one is chosen
126
+// randomly. The term "conn" or "connection" is used
127
+// in this policy name due to its similar meaning in
128
+// other software, but our load balancer actually
129
+// counts active requests rather than connections,
130
+// since these days requests are multiplexed onto
131
+// shared connections.
132
+type LeastConnSelection struct{}
133
+
134
+// CaddyModule returns the Caddy module information.
135
+func (LeastConnSelection) CaddyModule() caddy.ModuleInfo {
136
+ return caddy.ModuleInfo{
137
+ Name: "http.handlers.reverse_proxy.selection_policies.least_conn",
138
+ New: func() caddy.Module { return new(LeastConnSelection) },
139
+ }
140
+}
141
+
142
+// Select selects the up host with the least number of connections in the
143
+// pool. If more than one host has the same least number of connections,
144
+// one of the hosts is chosen at random.
145
+func (LeastConnSelection) Select(pool HostPool, _ *http.Request) *Upstream {
146
+ var bestHost *Upstream
147
+ var count int
148
+ var leastReqs int
149
+
150
+ for _, host := range pool {
151
+ if !host.Available() {
152
+ continue
153
+ }
154
+ numReqs := host.NumRequests()
155
+ if numReqs < leastReqs {
156
+ leastReqs = numReqs
157
+ count = 0
158
+ }
159
+
160
+ // among hosts with same least connections, perform a reservoir
161
+ // sample: https://en.wikipedia.org/wiki/Reservoir_sampling
162
+ if numReqs == leastReqs {
163
+ count++
164
+ if (weakrand.Int() % count) == 0 {
165
+ bestHost = host
166
+ }
167
+ }
168
+ }
169
+
170
+ return bestHost
171
+}
172
+
173
+// RoundRobinSelection is a policy that selects
174
+// a host based on round-robin ordering.
175
+type RoundRobinSelection struct {
176
+ robin uint32
177
+}
178
+
179
+// CaddyModule returns the Caddy module information.
180
+func (RoundRobinSelection) CaddyModule() caddy.ModuleInfo {
181
+ return caddy.ModuleInfo{
182
+ Name: "http.handlers.reverse_proxy.selection_policies.round_robin",
183
+ New: func() caddy.Module { return new(RoundRobinSelection) },
184
+ }
185
+}
186
+
187
+// Select returns an available host, if any.
188
+func (r *RoundRobinSelection) Select(pool HostPool, _ *http.Request) *Upstream {
189
+ n := uint32(len(pool))
190
+ if n == 0 {
191
+ return nil
192
+ }
193
+ for i := uint32(0); i < n; i++ {
194
+ atomic.AddUint32(&r.robin, 1)
195
+ host := pool[r.robin%n]
196
+ if host.Available() {
197
+ return host
198
+ }
199
+ }
200
+ return nil
201
+}
202
+
203
+// FirstSelection is a policy that selects
204
+// the first available host.
205
+type FirstSelection struct{}
206
+
207
+// CaddyModule returns the Caddy module information.
208
+func (FirstSelection) CaddyModule() caddy.ModuleInfo {
209
+ return caddy.ModuleInfo{
210
+ Name: "http.handlers.reverse_proxy.selection_policies.first",
211
+ New: func() caddy.Module { return new(FirstSelection) },
212
+ }
213
+}
214
+
215
+// Select returns an available host, if any.
216
+func (FirstSelection) Select(pool HostPool, _ *http.Request) *Upstream {
217
+ for _, host := range pool {
218
+ if host.Available() {
219
+ return host
220
+ }
221
+ }
222
+ return nil
223
+}
224
+
225
+// IPHashSelection is a policy that selects a host
226
+// based on hashing the remote IP of the request.
227
+type IPHashSelection struct{}
228
+
229
+// CaddyModule returns the Caddy module information.
230
+func (IPHashSelection) CaddyModule() caddy.ModuleInfo {
231
+ return caddy.ModuleInfo{
232
+ Name: "http.handlers.reverse_proxy.selection_policies.ip_hash",
233
+ New: func() caddy.Module { return new(IPHashSelection) },
234
+ }
235
+}
236
+
237
+// Select returns an available host, if any.
238
+func (IPHashSelection) Select(pool HostPool, req *http.Request) *Upstream {
239
+ clientIP, _, err := net.SplitHostPort(req.RemoteAddr)
240
+ if err != nil {
241
+ clientIP = req.RemoteAddr
242
+ }
243
+ return hostByHashing(pool, clientIP)
244
+}
245
+
246
+// URIHashSelection is a policy that selects a
247
+// host by hashing the request URI.
248
+type URIHashSelection struct{}
249
+
250
+// CaddyModule returns the Caddy module information.
251
+func (URIHashSelection) CaddyModule() caddy.ModuleInfo {
252
+ return caddy.ModuleInfo{
253
+ Name: "http.handlers.reverse_proxy.selection_policies.uri_hash",
254
+ New: func() caddy.Module { return new(URIHashSelection) },
255
+ }
256
+}
257
+
258
+// Select returns an available host, if any.
259
+func (URIHashSelection) Select(pool HostPool, req *http.Request) *Upstream {
260
+ return hostByHashing(pool, req.RequestURI)
261
+}
262
+
263
+// HeaderHashSelection is a policy that selects
264
+// a host based on a given request header.
265
+type HeaderHashSelection struct {
266
+ Field string `json:"field,omitempty"`
267
+}
268
+
269
+// CaddyModule returns the Caddy module information.
270
+func (HeaderHashSelection) CaddyModule() caddy.ModuleInfo {
271
+ return caddy.ModuleInfo{
272
+ Name: "http.handlers.reverse_proxy.selection_policies.header",
273
+ New: func() caddy.Module { return new(HeaderHashSelection) },
274
+ }
275
+}
276
+
277
+// Select returns an available host, if any.
278
+func (s HeaderHashSelection) Select(pool HostPool, req *http.Request) *Upstream {
279
+ if s.Field == "" {
280
+ return nil
281
+ }
282
+ val := req.Header.Get(s.Field)
283
+ if val == "" {
284
+ return RandomSelection{}.Select(pool, req)
285
+ }
286
+ return hostByHashing(pool, val)
287
+}
288
+
289
+// leastRequests returns the host with the
290
+// least number of active requests to it.
291
+// If more than one host has the same
292
+// least number of active requests, then
293
+// one of those is chosen at random.
294
+func leastRequests(upstreams []*Upstream) *Upstream {
295
+ if len(upstreams) == 0 {
296
+ return nil
297
+ }
298
+ var best []*Upstream
299
+ var bestReqs int
300
+ for _, upstream := range upstreams {
301
+ reqs := upstream.NumRequests()
302
+ if reqs == 0 {
303
+ return upstream
304
+ }
305
+ if reqs <= bestReqs {
306
+ bestReqs = reqs
307
+ best = append(best, upstream)
308
+ }
309
+ }
310
+ return best[weakrand.Intn(len(best))]
311
+}
312
+
313
+// hostByHashing returns an available host
314
+// from pool based on a hashable string s.
315
+func hostByHashing(pool []*Upstream, s string) *Upstream {
316
+ poolLen := uint32(len(pool))
317
+ if poolLen == 0 {
318
+ return nil
319
+ }
320
+ index := hash(s) % poolLen
321
+ for i := uint32(0); i < poolLen; i++ {
322
+ index += i
323
+ upstream := pool[index%poolLen]
324
+ if upstream.Available() {
325
+ return upstream
326
+ }
327
+ }
328
+ return nil
329
+}
330
+
331
+// hash calculates a fast hash based on s.
332
+func hash(s string) uint32 {
333
+ h := fnv.New32a()
334
+ h.Write([]byte(s))
335
+ return h.Sum32()
336
+}
337
+
338
+// Interface guards
339
+var (
340
+ _ Selector = (*RandomSelection)(nil)
341
+ _ Selector = (*RandomChoiceSelection)(nil)
342
+ _ Selector = (*LeastConnSelection)(nil)
343
+ _ Selector = (*RoundRobinSelection)(nil)
344
+ _ Selector = (*FirstSelection)(nil)
345
+ _ Selector = (*IPHashSelection)(nil)
346
+ _ Selector = (*URIHashSelection)(nil)
347
+ _ Selector = (*HeaderHashSelection)(nil)
348
+
349
+ _ caddy.Validator = (*RandomChoiceSelection)(nil)
350
+ _ caddy.Provisioner = (*RandomChoiceSelection)(nil)
351
+)
modules/caddyhttp/reverseproxy/selectionpolicies_test.go
new
+363
@@ -0,0 +1,363 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
2
+//
3
+// Licensed under the Apache License, Version 2.0 (the "License");
4
+// you may not use this file except in compliance with the License.
5
+// You may obtain a copy of the License at
6
+//
7
+// http://www.apache.org/licenses/LICENSE-2.0
8
+//
9
+// Unless required by applicable law or agreed to in writing, software
10
+// distributed under the License is distributed on an "AS IS" BASIS,
11
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+// See the License for the specific language governing permissions and
13
+// limitations under the License.
14
+
15
+package reverseproxy
16
+
17
+// TODO: finish migrating these
18
+
19
+// import (
20
+// "net/http"
21
+// "net/http/httptest"
22
+// "os"
23
+// "testing"
24
+// )
25
+
26
+// var workableServer *httptest.Server
27
+
28
+// func TestMain(m *testing.M) {
29
+// workableServer = httptest.NewServer(http.HandlerFunc(
30
+// func(w http.ResponseWriter, r *http.Request) {
31
+// // do nothing
32
+// }))
33
+// r := m.Run()
34
+// workableServer.Close()
35
+// os.Exit(r)
36
+// }
37
+
38
+// type customPolicy struct{}
39
+
40
+// func (customPolicy) Select(pool HostPool, _ *http.Request) Host {
41
+// return pool[0]
42
+// }
43
+
44
+// func testPool() HostPool {
45
+// pool := []*UpstreamHost{
46
+// {
47
+// Name: workableServer.URL, // this should resolve (healthcheck test)
48
+// },
49
+// {
50
+// Name: "http://localhost:99998", // this shouldn't
51
+// },
52
+// {
53
+// Name: "http://C",
54
+// },
55
+// }
56
+// return HostPool(pool)
57
+// }
58
+
59
+// func TestRoundRobinPolicy(t *testing.T) {
60
+// pool := testPool()
61
+// rrPolicy := &RoundRobin{}
62
+// request, _ := http.NewRequest("GET", "/", nil)
63
+
64
+// h := rrPolicy.Select(pool, request)
65
+// // First selected host is 1, because counter starts at 0
66
+// // and increments before host is selected
67
+// if h != pool[1] {
68
+// t.Error("Expected first round robin host to be second host in the pool.")
69
+// }
70
+// h = rrPolicy.Select(pool, request)
71
+// if h != pool[2] {
72
+// t.Error("Expected second round robin host to be third host in the pool.")
73
+// }
74
+// h = rrPolicy.Select(pool, request)
75
+// if h != pool[0] {
76
+// t.Error("Expected third round robin host to be first host in the pool.")
77
+// }
78
+// // mark host as down
79
+// pool[1].Unhealthy = 1
80
+// h = rrPolicy.Select(pool, request)
81
+// if h != pool[2] {
82
+// t.Error("Expected to skip down host.")
83
+// }
84
+// // mark host as up
85
+// pool[1].Unhealthy = 0
86
+
87
+// h = rrPolicy.Select(pool, request)
88
+// if h == pool[2] {
89
+// t.Error("Expected to balance evenly among healthy hosts")
90
+// }
91
+// // mark host as full
92
+// pool[1].Conns = 1
93
+// pool[1].MaxConns = 1
94
+// h = rrPolicy.Select(pool, request)
95
+// if h != pool[2] {
96
+// t.Error("Expected to skip full host.")
97
+// }
98
+// }
99
+
100
+// func TestLeastConnPolicy(t *testing.T) {
101
+// pool := testPool()
102
+// lcPolicy := &LeastConn{}
103
+// request, _ := http.NewRequest("GET", "/", nil)
104
+
105
+// pool[0].Conns = 10
106
+// pool[1].Conns = 10
107
+// h := lcPolicy.Select(pool, request)
108
+// if h != pool[2] {
109
+// t.Error("Expected least connection host to be third host.")
110
+// }
111
+// pool[2].Conns = 100
112
+// h = lcPolicy.Select(pool, request)
113
+// if h != pool[0] && h != pool[1] {
114
+// t.Error("Expected least connection host to be first or second host.")
115
+// }
116
+// }
117
+
118
+// func TestCustomPolicy(t *testing.T) {
119
+// pool := testPool()
120
+// customPolicy := &customPolicy{}
121
+// request, _ := http.NewRequest("GET", "/", nil)
122
+
123
+// h := customPolicy.Select(pool, request)
124
+// if h != pool[0] {
125
+// t.Error("Expected custom policy host to be the first host.")
126
+// }
127
+// }
128
+
129
+// func TestIPHashPolicy(t *testing.T) {
130
+// pool := testPool()
131
+// ipHash := &IPHash{}
132
+// request, _ := http.NewRequest("GET", "/", nil)
133
+// // We should be able to predict where every request is routed.
134
+// request.RemoteAddr = "172.0.0.1:80"
135
+// h := ipHash.Select(pool, request)
136
+// if h != pool[1] {
137
+// t.Error("Expected ip hash policy host to be the second host.")
138
+// }
139
+// request.RemoteAddr = "172.0.0.2:80"
140
+// h = ipHash.Select(pool, request)
141
+// if h != pool[1] {
142
+// t.Error("Expected ip hash policy host to be the second host.")
143
+// }
144
+// request.RemoteAddr = "172.0.0.3:80"
145
+// h = ipHash.Select(pool, request)
146
+// if h != pool[2] {
147
+// t.Error("Expected ip hash policy host to be the third host.")
148
+// }
149
+// request.RemoteAddr = "172.0.0.4:80"
150
+// h = ipHash.Select(pool, request)
151
+// if h != pool[1] {
152
+// t.Error("Expected ip hash policy host to be the second host.")
153
+// }
154
+
155
+// // we should get the same results without a port
156
+// request.RemoteAddr = "172.0.0.1"
157
+// h = ipHash.Select(pool, request)
158
+// if h != pool[1] {
159
+// t.Error("Expected ip hash policy host to be the second host.")
160
+// }
161
+// request.RemoteAddr = "172.0.0.2"
162
+// h = ipHash.Select(pool, request)
163
+// if h != pool[1] {
164
+// t.Error("Expected ip hash policy host to be the second host.")
165
+// }
166
+// request.RemoteAddr = "172.0.0.3"
167
+// h = ipHash.Select(pool, request)
168
+// if h != pool[2] {
169
+// t.Error("Expected ip hash policy host to be the third host.")
170
+// }
171
+// request.RemoteAddr = "172.0.0.4"
172
+// h = ipHash.Select(pool, request)
173
+// if h != pool[1] {
174
+// t.Error("Expected ip hash policy host to be the second host.")
175
+// }
176
+
177
+// // we should get a healthy host if the original host is unhealthy and a
178
+// // healthy host is available
179
+// request.RemoteAddr = "172.0.0.1"
180
+// pool[1].Unhealthy = 1
181
+// h = ipHash.Select(pool, request)
182
+// if h != pool[2] {
183
+// t.Error("Expected ip hash policy host to be the third host.")
184
+// }
185
+
186
+// request.RemoteAddr = "172.0.0.2"
187
+// h = ipHash.Select(pool, request)
188
+// if h != pool[2] {
189
+// t.Error("Expected ip hash policy host to be the third host.")
190
+// }
191
+// pool[1].Unhealthy = 0
192
+
193
+// request.RemoteAddr = "172.0.0.3"
194
+// pool[2].Unhealthy = 1
195
+// h = ipHash.Select(pool, request)
196
+// if h != pool[0] {
197
+// t.Error("Expected ip hash policy host to be the first host.")
198
+// }
199
+// request.RemoteAddr = "172.0.0.4"
200
+// h = ipHash.Select(pool, request)
201
+// if h != pool[1] {
202
+// t.Error("Expected ip hash policy host to be the second host.")
203
+// }
204
+
205
+// // We should be able to resize the host pool and still be able to predict
206
+// // where a request will be routed with the same IP's used above
207
+// pool = []*UpstreamHost{
208
+// {
209
+// Name: workableServer.URL, // this should resolve (healthcheck test)
210
+// },
211
+// {
212
+// Name: "http://localhost:99998", // this shouldn't
213
+// },
214
+// }
215
+// pool = HostPool(pool)
216
+// request.RemoteAddr = "172.0.0.1:80"
217
+// h = ipHash.Select(pool, request)
218
+// if h != pool[0] {
219
+// t.Error("Expected ip hash policy host to be the first host.")
220
+// }
221
+// request.RemoteAddr = "172.0.0.2:80"
222
+// h = ipHash.Select(pool, request)
223
+// if h != pool[1] {
224
+// t.Error("Expected ip hash policy host to be the second host.")
225
+// }
226
+// request.RemoteAddr = "172.0.0.3:80"
227
+// h = ipHash.Select(pool, request)
228
+// if h != pool[0] {
229
+// t.Error("Expected ip hash policy host to be the first host.")
230
+// }
231
+// request.RemoteAddr = "172.0.0.4:80"
232
+// h = ipHash.Select(pool, request)
233
+// if h != pool[1] {
234
+// t.Error("Expected ip hash policy host to be the second host.")
235
+// }
236
+
237
+// // We should get nil when there are no healthy hosts
238
+// pool[0].Unhealthy = 1
239
+// pool[1].Unhealthy = 1
240
+// h = ipHash.Select(pool, request)
241
+// if h != nil {
242
+// t.Error("Expected ip hash policy host to be nil.")
243
+// }
244
+// }
245
+
246
+// func TestFirstPolicy(t *testing.T) {
247
+// pool := testPool()
248
+// firstPolicy := &First{}
249
+// req := httptest.NewRequest(http.MethodGet, "/", nil)
250
+
251
+// h := firstPolicy.Select(pool, req)
252
+// if h != pool[0] {
253
+// t.Error("Expected first policy host to be the first host.")
254
+// }
255
+
256
+// pool[0].Unhealthy = 1
257
+// h = firstPolicy.Select(pool, req)
258
+// if h != pool[1] {
259
+// t.Error("Expected first policy host to be the second host.")
260
+// }
261
+// }
262
+
263
+// func TestUriPolicy(t *testing.T) {
264
+// pool := testPool()
265
+// uriPolicy := &URIHash{}
266
+
267
+// request := httptest.NewRequest(http.MethodGet, "/test", nil)
268
+// h := uriPolicy.Select(pool, request)
269
+// if h != pool[0] {
270
+// t.Error("Expected uri policy host to be the first host.")
271
+// }
272
+
273
+// pool[0].Unhealthy = 1
274
+// h = uriPolicy.Select(pool, request)
275
+// if h != pool[1] {
276
+// t.Error("Expected uri policy host to be the first host.")
277
+// }
278
+
279
+// request = httptest.NewRequest(http.MethodGet, "/test_2", nil)
280
+// h = uriPolicy.Select(pool, request)
281
+// if h != pool[1] {
282
+// t.Error("Expected uri policy host to be the second host.")
283
+// }
284
+
285
+// // We should be able to resize the host pool and still be able to predict
286
+// // where a request will be routed with the same URI's used above
287
+// pool = []*UpstreamHost{
288
+// {
289
+// Name: workableServer.URL, // this should resolve (healthcheck test)
290
+// },
291
+// {
292
+// Name: "http://localhost:99998", // this shouldn't
293
+// },
294
+// }
295
+
296
+// request = httptest.NewRequest(http.MethodGet, "/test", nil)
297
+// h = uriPolicy.Select(pool, request)
298
+// if h != pool[0] {
299
+// t.Error("Expected uri policy host to be the first host.")
300
+// }
301
+
302
+// pool[0].Unhealthy = 1
303
+// h = uriPolicy.Select(pool, request)
304
+// if h != pool[1] {
305
+// t.Error("Expected uri policy host to be the first host.")
306
+// }
307
+
308
+// request = httptest.NewRequest(http.MethodGet, "/test_2", nil)
309
+// h = uriPolicy.Select(pool, request)
310
+// if h != pool[1] {
311
+// t.Error("Expected uri policy host to be the second host.")
312
+// }
313
+
314
+// pool[0].Unhealthy = 1
315
+// pool[1].Unhealthy = 1
316
+// h = uriPolicy.Select(pool, request)
317
+// if h != nil {
318
+// t.Error("Expected uri policy policy host to be nil.")
319
+// }
320
+// }
321
+
322
+// func TestHeaderPolicy(t *testing.T) {
323
+// pool := testPool()
324
+// tests := []struct {
325
+// Name string
326
+// Policy *Header
327
+// RequestHeaderName string
328
+// RequestHeaderValue string
329
+// NilHost bool
330
+// HostIndex int
331
+// }{
332
+// {"empty config", &Header{""}, "", "", true, 0},
333
+// {"empty config+header+value", &Header{""}, "Affinity", "somevalue", true, 0},
334
+// {"empty config+header", &Header{""}, "Affinity", "", true, 0},
335
+
336
+// {"no header(fallback to roundrobin)", &Header{"Affinity"}, "", "", false, 1},
337
+// {"no header(fallback to roundrobin)", &Header{"Affinity"}, "", "", false, 2},
338
+// {"no header(fallback to roundrobin)", &Header{"Affinity"}, "", "", false, 0},
339
+
340
+// {"hash route to host", &Header{"Affinity"}, "Affinity", "somevalue", false, 1},
341
+// {"hash route to host", &Header{"Affinity"}, "Affinity", "somevalue2", false, 0},
342
+// {"hash route to host", &Header{"Affinity"}, "Affinity", "somevalue3", false, 2},
343
+// {"hash route with empty value", &Header{"Affinity"}, "Affinity", "", false, 1},
344
+// }
345
+
346
+// for idx, test := range tests {
347
+// request, _ := http.NewRequest("GET", "/", nil)
348
+// if test.RequestHeaderName != "" {
349
+// request.Header.Add(test.RequestHeaderName, test.RequestHeaderValue)
350
+// }
351
+
352
+// host := test.Policy.Select(pool, request)
353
+// if test.NilHost && host != nil {
354
+// t.Errorf("%d: Expected host to be nil", idx)
355
+// }
356
+// if !test.NilHost && host == nil {
357
+// t.Errorf("%d: Did not expect host to be nil", idx)
358
+// }
359
+// if !test.NilHost && host != pool[test.HostIndex] {
360
+// t.Errorf("%d: Expected Header policy to be host %d", idx, test.HostIndex)
361
+// }
362
+// }
363
+// }
modules/caddyhttp/reverseproxy/upstream.go
deleted
-450
@@ -1,450 +0,0 @@
1
-// Copyright 2015 Matthew Holt and The Caddy Authors
2
-//
3
-// Licensed under the Apache License, Version 2.0 (the "License");
4
-// you may not use this file except in compliance with the License.
5
-// You may obtain a copy of the License at
6
-//
7
-// http://www.apache.org/licenses/LICENSE-2.0
8
-//
9
-// Unless required by applicable law or agreed to in writing, software
10
-// distributed under the License is distributed on an "AS IS" BASIS,
11
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
-// See the License for the specific language governing permissions and
13
-// limitations under the License.
14
-
15
-// Package reverseproxy implements a load-balanced reverse proxy.
16
-package reverseproxy
17
-
18
-import (
19
- "context"
20
- "encoding/json"
21
- "fmt"
22
- "math/rand"
23
- "net"
24
- "net/http"
25
- "net/url"
26
- "strings"
27
- "sync"
28
- "sync/atomic"
29
- "time"
30
-
31
- "github.com/caddyserver/caddy/v2"
32
- "github.com/caddyserver/caddy/v2/modules/caddyhttp"
33
-)
34
-
35
-// CircuitBreaker defines the functionality of a circuit breaker module.
36
-type CircuitBreaker interface {
37
- Ok() bool
38
- RecordMetric(statusCode int, latency time.Duration)
39
-}
40
-
41
-type noopCircuitBreaker struct{}
42
-
43
-func (ncb noopCircuitBreaker) RecordMetric(statusCode int, latency time.Duration) {}
44
-func (ncb noopCircuitBreaker) Ok() bool {
45
- return true
46
-}
47
-
48
-const (
49
- // TypeBalanceRoundRobin represents the value to use for configuring a load balanced reverse proxy to use round robin load balancing.
50
- TypeBalanceRoundRobin = iota
51
-
52
- // TypeBalanceRandom represents the value to use for configuring a load balanced reverse proxy to use random load balancing.
53
- TypeBalanceRandom
54
-
55
- // TODO: add random with two choices
56
-
57
- // msgNoHealthyUpstreams is returned if there are no upstreams that are healthy to proxy a request to
58
- msgNoHealthyUpstreams = "No healthy upstreams."
59
-
60
- // by default perform health checks every 30 seconds
61
- defaultHealthCheckDur = time.Second * 30
62
-
63
- // used when an upstream is unhealthy, health checks can be configured to perform at a faster rate
64
- defaultFastHealthCheckDur = time.Second * 1
65
-)
66
-
67
-var (
68
- // defaultTransport is the default transport to use for the reverse proxy.
69
- defaultTransport = &http.Transport{
70
- Dial: (&net.Dialer{
71
- Timeout: 5 * time.Second,
72
- }).Dial,
73
- TLSHandshakeTimeout: 5 * time.Second,
74
- }
75
-
76
- // defaultHTTPClient is the default http client to use for the healthchecker.
77
- defaultHTTPClient = &http.Client{
78
- Timeout: time.Second * 10,
79
- Transport: defaultTransport,
80
- }
81
-
82
- // typeMap maps caddy load balance configuration to the internal representation of the loadbalance algorithm type.
83
- typeMap = map[string]int{
84
- "round_robin": TypeBalanceRoundRobin,
85
- "random": TypeBalanceRandom,
86
- }
87
-)
88
-
89
-// NewLoadBalancedReverseProxy returns a collection of Upstreams that are to be loadbalanced.
90
-func NewLoadBalancedReverseProxy(lb *LoadBalanced, ctx caddy.Context) error {
91
- // set defaults
92
- if lb.NoHealthyUpstreamsMessage == "" {
93
- lb.NoHealthyUpstreamsMessage = msgNoHealthyUpstreams
94
- }
95
-
96
- if lb.TryInterval == "" {
97
- lb.TryInterval = "20s"
98
- }
99
-
100
- // set request retry interval
101
- ti, err := time.ParseDuration(lb.TryInterval)
102
- if err != nil {
103
- return fmt.Errorf("NewLoadBalancedReverseProxy: %v", err.Error())
104
- }
105
- lb.tryInterval = ti
106
-
107
- // set load balance algorithm
108
- t, ok := typeMap[lb.LoadBalanceType]
109
- if !ok {
110
- t = TypeBalanceRandom
111
- }
112
- lb.loadBalanceType = t
113
-
114
- // setup each upstream
115
- var us []*upstream
116
- for _, uc := range lb.Upstreams {
117
- // pass the upstream decr and incr methods to keep track of unhealthy nodes
118
- nu, err := newUpstream(uc, lb.decrUnhealthy, lb.incrUnhealthy)
119
- if err != nil {
120
- return err
121
- }
122
-
123
- // setup any configured circuit breakers
124
- var cbModule = "http.handlers.reverse_proxy.circuit_breaker"
125
- var cb CircuitBreaker
126
-
127
- if uc.CircuitBreaker != nil {
128
- if _, err := caddy.GetModule(cbModule); err == nil {
129
- val, err := ctx.LoadModule(cbModule, uc.CircuitBreaker)
130
- if err == nil {
131
- cbv, ok := val.(CircuitBreaker)
132
- if ok {
133
- cb = cbv
134
- } else {
135
- fmt.Printf("\nerr: %v; cannot load circuit_breaker, using noop", err.Error())
136
- cb = noopCircuitBreaker{}
137
- }
138
- } else {
139
- fmt.Printf("\nerr: %v; cannot load circuit_breaker, using noop", err.Error())
140
- cb = noopCircuitBreaker{}
141
- }
142
- } else {
143
- fmt.Println("circuit_breaker module not loaded, using noop")
144
- cb = noopCircuitBreaker{}
145
- }
146
- } else {
147
- cb = noopCircuitBreaker{}
148
- }
149
- nu.CB = cb
150
-
151
- // start a healthcheck worker which will periodically check to see if an upstream is healthy
152
- // to proxy requests to.
153
- nu.healthChecker = NewHealthCheckWorker(nu, defaultHealthCheckDur, defaultHTTPClient)
154
-
155
- // TODO :- if path is empty why does this empty the entire Target?
156
- // nu.Target.Path = uc.HealthCheckPath
157
-
158
- nu.healthChecker.ScheduleChecks(nu.Target.String())
159
- lb.HealthCheckers = append(lb.HealthCheckers, nu.healthChecker)
160
-
161
- us = append(us, nu)
162
- }
163
-
164
- lb.upstreams = us
165
-
166
- return nil
167
-}
168
-
169
-// LoadBalanced represents a collection of upstream hosts that are loadbalanced. It
170
-// contains multiple features like health checking and circuit breaking functionality
171
-// for upstreams.
172
-type LoadBalanced struct {
173
- mu sync.Mutex
174
- numUnhealthy int32
175
- selectedServer int // used during round robin load balancing
176
- loadBalanceType int
177
- tryInterval time.Duration
178
- upstreams []*upstream
179
-
180
- // The following struct fields are set by caddy configuration.
181
- // TryInterval is the max duration for which request retrys will be performed for a request.
182
- TryInterval string `json:"try_interval,omitempty"`
183
-
184
- // Upstreams are the configs for upstream hosts
185
- Upstreams []*UpstreamConfig `json:"upstreams,omitempty"`
186
-
187
- // LoadBalanceType is the string representation of what loadbalancing algorithm to use. i.e. "random" or "round_robin".
188
- LoadBalanceType string `json:"load_balance_type,omitempty"`
189
-
190
- // NoHealthyUpstreamsMessage is returned as a response when there are no healthy upstreams to loadbalance to.
191
- NoHealthyUpstreamsMessage string `json:"no_healthy_upstreams_message,omitempty"`
192
-
193
- // TODO :- store healthcheckers as package level state where each upstream gets a single healthchecker
194
- // currently a healthchecker is created for each upstream defined, even if a healthchecker was previously created
195
- // for that upstream
196
- HealthCheckers []*HealthChecker `json:"health_checkers,omitempty"`
197
-}
198
-
199
-// Cleanup stops all health checkers on a loadbalanced reverse proxy.
200
-func (lb *LoadBalanced) Cleanup() error {
201
- for _, hc := range lb.HealthCheckers {
202
- hc.Stop()
203
- }
204
-
205
- return nil
206
-}
207
-
208
-// Provision sets up a new loadbalanced reverse proxy.
209
-func (lb *LoadBalanced) Provision(ctx caddy.Context) error {
210
- return NewLoadBalancedReverseProxy(lb, ctx)
211
-}
212
-
213
-// ServeHTTP implements the caddyhttp.MiddlewareHandler interface to
214
-// dispatch an HTTP request to the proper server.
215
-func (lb *LoadBalanced) ServeHTTP(w http.ResponseWriter, r *http.Request, _ caddyhttp.Handler) error {
216
- // ensure requests don't hang if an upstream does not respond or is not eventually healthy
217
- var u *upstream
218
- var done bool
219
-
220
- retryTimer := time.NewTicker(lb.tryInterval)
221
- defer retryTimer.Stop()
222
-
223
- go func() {
224
- select {
225
- case <-retryTimer.C:
226
- done = true
227
- }
228
- }()
229
-
230
- // keep trying to get an available upstream to process the request
231
- for {
232
- switch lb.loadBalanceType {
233
- case TypeBalanceRandom:
234
- u = lb.random()
235
- case TypeBalanceRoundRobin:
236
- u = lb.roundRobin()
237
- }
238
-
239
- // if we can't get an upstream and our retry interval has ended return an error response
240
- if u == nil && done {
241
- w.WriteHeader(http.StatusBadGateway)
242
- fmt.Fprint(w, lb.NoHealthyUpstreamsMessage)
243
-
244
- return fmt.Errorf(msgNoHealthyUpstreams)
245
- }
246
-
247
- // attempt to get an available upstream
248
- if u == nil {
249
- continue
250
- }
251
-
252
- start := time.Now()
253
-
254
- // if we get an error retry until we get a healthy upstream
255
- res, err := u.ReverseProxy.ServeHTTP(w, r)
256
- if err != nil {
257
- if err == context.Canceled {
258
- return nil
259
- }
260
-
261
- continue
262
- }
263
-
264
- // record circuit breaker metrics
265
- go u.CB.RecordMetric(res.StatusCode, time.Now().Sub(start))
266
-
267
- return nil
268
- }
269
-}
270
-
271
-// incrUnhealthy increments the amount of unhealthy nodes in a loadbalancer.
272
-func (lb *LoadBalanced) incrUnhealthy() {
273
- atomic.AddInt32(&lb.numUnhealthy, 1)
274
-}
275
-
276
-// decrUnhealthy decrements the amount of unhealthy nodes in a loadbalancer.
277
-func (lb *LoadBalanced) decrUnhealthy() {
278
- atomic.AddInt32(&lb.numUnhealthy, -1)
279
-}
280
-
281
-// roundRobin implements a round robin load balancing algorithm to select
282
-// which server to forward requests to.
283
-func (lb *LoadBalanced) roundRobin() *upstream {
284
- if atomic.LoadInt32(&lb.numUnhealthy) == int32(len(lb.upstreams)) {
285
- return nil
286
- }
287
-
288
- selected := lb.upstreams[lb.selectedServer]
289
-
290
- lb.mu.Lock()
291
- lb.selectedServer++
292
- if lb.selectedServer >= len(lb.upstreams) {
293
- lb.selectedServer = 0
294
- }
295
- lb.mu.Unlock()
296
-
297
- if selected.IsHealthy() && selected.CB.Ok() {
298
- return selected
299
- }
300
-
301
- return nil
302
-}
303
-
304
-// random implements a random server selector for load balancing.
305
-func (lb *LoadBalanced) random() *upstream {
306
- if atomic.LoadInt32(&lb.numUnhealthy) == int32(len(lb.upstreams)) {
307
- return nil
308
- }
309
-
310
- n := rand.Int() % len(lb.upstreams)
311
- selected := lb.upstreams[n]
312
-
313
- if selected.IsHealthy() && selected.CB.Ok() {
314
- return selected
315
- }
316
-
317
- return nil
318
-}
319
-
320
-// UpstreamConfig represents the config of an upstream.
321
-type UpstreamConfig struct {
322
- // Host is the host name of the upstream server.
323
- Host string `json:"host,omitempty"`
324
-
325
- // FastHealthCheckDuration is the duration for which a health check is performed when a node is considered unhealthy.
326
- FastHealthCheckDuration string `json:"fast_health_check_duration,omitempty"`
327
-
328
- CircuitBreaker json.RawMessage `json:"circuit_breaker,omitempty"`
329
-
330
- // // CircuitBreakerConfig is the config passed to setup a circuit breaker.
331
- // CircuitBreakerConfig *circuitbreaker.Config `json:"circuit_breaker,omitempty"`
332
- circuitbreaker CircuitBreaker
333
-
334
- // HealthCheckDuration is the default duration for which a health check is performed.
335
- HealthCheckDuration string `json:"health_check_duration,omitempty"`
336
-
337
- // HealthCheckPath is the path at the upstream host to use for healthchecks.
338
- HealthCheckPath string `json:"health_check_path,omitempty"`
339
-}
340
-
341
-// upstream represents an upstream host.
342
-type upstream struct {
343
- Healthy int32 // 0 = false, 1 = true
344
- Target *url.URL
345
- ReverseProxy *ReverseProxy
346
- Incr func()
347
- Decr func()
348
- CB CircuitBreaker
349
- healthChecker *HealthChecker
350
- healthCheckDur time.Duration
351
- fastHealthCheckDur time.Duration
352
-}
353
-
354
-// newUpstream returns a new upstream.
355
-func newUpstream(uc *UpstreamConfig, d func(), i func()) (*upstream, error) {
356
- host := strings.TrimSpace(uc.Host)
357
- protoIdx := strings.Index(host, "://")
358
- if protoIdx == -1 || len(host[:protoIdx]) == 0 {
359
- return nil, fmt.Errorf("protocol is required for host")
360
- }
361
-
362
- hostURL, err := url.Parse(host)
363
- if err != nil {
364
- return nil, err
365
- }
366
-
367
- // parse healthcheck durations
368
- hcd, err := time.ParseDuration(uc.HealthCheckDuration)
369
- if err != nil {
370
- hcd = defaultHealthCheckDur
371
- }
372
-
373
- fhcd, err := time.ParseDuration(uc.FastHealthCheckDuration)
374
- if err != nil {
375
- fhcd = defaultFastHealthCheckDur
376
- }
377
-
378
- u := upstream{
379
- healthCheckDur: hcd,
380
- fastHealthCheckDur: fhcd,
381
- Target: hostURL,
382
- Decr: d,
383
- Incr: i,
384
- Healthy: int32(0), // assume is unhealthy on start
385
- }
386
-
387
- u.ReverseProxy = newReverseProxy(hostURL, u.SetHealthiness)
388
- return &u, nil
389
-}
390
-
391
-// SetHealthiness sets whether an upstream is healthy or not. The health check worker is updated to
392
-// perform checks faster if a node is unhealthy.
393
-func (u *upstream) SetHealthiness(ok bool) {
394
- h := atomic.LoadInt32(&u.Healthy)
395
- var wasHealthy bool
396
- if h == 1 {
397
- wasHealthy = true
398
- } else {
399
- wasHealthy = false
400
- }
401
-
402
- if ok {
403
- u.healthChecker.Ticker = time.NewTicker(u.healthCheckDur)
404
-
405
- if !wasHealthy {
406
- atomic.AddInt32(&u.Healthy, 1)
407
- u.Decr()
408
- }
409
- } else {
410
- u.healthChecker.Ticker = time.NewTicker(u.fastHealthCheckDur)
411
-
412
- if wasHealthy {
413
- atomic.AddInt32(&u.Healthy, -1)
414
- u.Incr()
415
- }
416
- }
417
-}
418
-
419
-// IsHealthy returns whether an Upstream is healthy or not.
420
-func (u *upstream) IsHealthy() bool {
421
- i := atomic.LoadInt32(&u.Healthy)
422
- if i == 1 {
423
- return true
424
- }
425
-
426
- return false
427
-}
428
-
429
-// newReverseProxy returns a new reverse proxy handler.
430
-func newReverseProxy(target *url.URL, setHealthiness func(bool)) *ReverseProxy {
431
- errorHandler := func(w http.ResponseWriter, r *http.Request, err error) {
432
- // we don't need to worry about cancelled contexts since this doesn't necessarilly mean that
433
- // the upstream is unhealthy.
434
- if err != context.Canceled {
435
- setHealthiness(false)
436
- }
437
- }
438
-
439
- rp := NewSingleHostReverseProxy(target)
440
- rp.ErrorHandler = errorHandler
441
- rp.Transport = defaultTransport // use default transport that times out in 5 seconds
442
- return rp
443
-}
444
-
445
-// Interface guards
446
-var (
447
- _ caddyhttp.MiddlewareHandler = (*LoadBalanced)(nil)
448
- _ caddy.Provisioner = (*LoadBalanced)(nil)
449
- _ caddy.CleanerUpper = (*LoadBalanced)(nil)
450
-)
usagepool.go
new
+86
@@ -0,0 +1,86 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
2
+//
3
+// Licensed under the Apache License, Version 2.0 (the "License");
4
+// you may not use this file except in compliance with the License.
5
+// You may obtain a copy of the License at
6
+//
7
+// http://www.apache.org/licenses/LICENSE-2.0
8
+//
9
+// Unless required by applicable law or agreed to in writing, software
10
+// distributed under the License is distributed on an "AS IS" BASIS,
11
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+// See the License for the specific language governing permissions and
13
+// limitations under the License.
14
+
15
+package caddy
16
+
17
+import (
18
+ "fmt"
19
+ "sync"
20
+ "sync/atomic"
21
+)
22
+
23
+// UsagePool is a thread-safe map that pools values
24
+// based on usage; a LoadOrStore operation increments
25
+// the usage, and a Delete decrements from the usage.
26
+// If the usage count reaches 0, the value will be
27
+// removed from the map. There is no way to overwrite
28
+// existing keys in the pool without first deleting
29
+// it as many times as it was stored. Deleting too
30
+// many times will panic.
31
+//
32
+// An empty UsagePool is NOT safe to use; always call
33
+// NewUsagePool() to make a new value.
34
+type UsagePool struct {
35
+ pool *sync.Map
36
+}
37
+
38
+// NewUsagePool returns a new usage pool.
39
+func NewUsagePool() *UsagePool {
40
+ return &UsagePool{pool: new(sync.Map)}
41
+}
42
+
43
+// Delete decrements the usage count for key and removes the
44
+// value from the underlying map if the usage is 0. It returns
45
+// true if the usage count reached 0 and the value was deleted.
46
+// It panics if the usage count drops below 0; always call
47
+// Delete precisely as many times as LoadOrStore.
48
+func (up *UsagePool) Delete(key interface{}) (deleted bool) {
49
+ usageVal, ok := up.pool.Load(key)
50
+ if !ok {
51
+ return false
52
+ }
53
+ upv := usageVal.(*usagePoolVal)
54
+ newUsage := atomic.AddInt32(&upv.usage, -1)
55
+ if newUsage == 0 {
56
+ up.pool.Delete(key)
57
+ return true
58
+ } else if newUsage < 0 {
59
+ panic(fmt.Sprintf("deleted more than stored: %#v (usage: %d)",
60
+ upv.value, upv.usage))
61
+ }
62
+ return false
63
+}
64
+
65
+// LoadOrStore puts val in the pool and returns false if key does
66
+// not already exist; otherwise if the key exists, it loads the
67
+// existing value, increments the usage for that value, and returns
68
+// the value along with true.
69
+func (up *UsagePool) LoadOrStore(key, val interface{}) (actual interface{}, loaded bool) {
70
+ usageVal := &usagePoolVal{
71
+ usage: 1,
72
+ value: val,
73
+ }
74
+ actual, loaded = up.pool.LoadOrStore(key, usageVal)
75
+ if loaded {
76
+ upv := actual.(*usagePoolVal)
77
+ actual = upv.value
78
+ atomic.AddInt32(&upv.usage, 1)
79
+ }
80
+ return
81
+}
82
+
83
+type usagePoolVal struct {
84
+ usage int32 // accessed atomically; must be 64-bit aligned for 32-bit systems
85
+ value interface{}
86
+}