@marcel91200 / caddy Mirror · read-only / commits / 026df7c5
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 14, 05:15 UTC

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 +}