Matthew Holt
5 years ago
17 changed files with 2741 additions and 888 deletions
@ -0,0 +1,578 @@ |
|||||
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
|
//
|
||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
// you may not use this file except in compliance with the License.
|
||||
|
// You may obtain a copy of the License at
|
||||
|
//
|
||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
//
|
||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
// See the License for the specific language governing permissions and
|
||||
|
// limitations under the License.
|
||||
|
|
||||
|
// Forked Jan. 2015 from http://bitbucket.org/PinIdea/fcgi_client
|
||||
|
// (which is forked from https://code.google.com/p/go-fastcgi-client/).
|
||||
|
// This fork contains several fixes and improvements by Matt Holt and
|
||||
|
// other contributors to the Caddy project.
|
||||
|
|
||||
|
// Copyright 2012 Junqing Tan <ivan@mysqlab.net> and The Go Authors
|
||||
|
// Use of this source code is governed by a BSD-style
|
||||
|
// Part of source code is from Go fcgi package
|
||||
|
|
||||
|
package fastcgi |
||||
|
|
||||
|
import ( |
||||
|
"bufio" |
||||
|
"bytes" |
||||
|
"context" |
||||
|
"encoding/binary" |
||||
|
"errors" |
||||
|
"io" |
||||
|
"io/ioutil" |
||||
|
"mime/multipart" |
||||
|
"net" |
||||
|
"net/http" |
||||
|
"net/http/httputil" |
||||
|
"net/textproto" |
||||
|
"net/url" |
||||
|
"os" |
||||
|
"path/filepath" |
||||
|
"strconv" |
||||
|
"strings" |
||||
|
"sync" |
||||
|
"time" |
||||
|
) |
||||
|
|
||||
|
// FCGIListenSockFileno describes listen socket file number.
|
||||
|
const FCGIListenSockFileno uint8 = 0 |
||||
|
|
||||
|
// FCGIHeaderLen describes header length.
|
||||
|
const FCGIHeaderLen uint8 = 8 |
||||
|
|
||||
|
// Version1 describes the version.
|
||||
|
const Version1 uint8 = 1 |
||||
|
|
||||
|
// FCGINullRequestID describes the null request ID.
|
||||
|
const FCGINullRequestID uint8 = 0 |
||||
|
|
||||
|
// FCGIKeepConn describes keep connection mode.
|
||||
|
const FCGIKeepConn uint8 = 1 |
||||
|
|
||||
|
const ( |
||||
|
// BeginRequest is the begin request flag.
|
||||
|
BeginRequest uint8 = iota + 1 |
||||
|
// AbortRequest is the abort request flag.
|
||||
|
AbortRequest |
||||
|
// EndRequest is the end request flag.
|
||||
|
EndRequest |
||||
|
// Params is the parameters flag.
|
||||
|
Params |
||||
|
// Stdin is the standard input flag.
|
||||
|
Stdin |
||||
|
// Stdout is the standard output flag.
|
||||
|
Stdout |
||||
|
// Stderr is the standard error flag.
|
||||
|
Stderr |
||||
|
// Data is the data flag.
|
||||
|
Data |
||||
|
// GetValues is the get values flag.
|
||||
|
GetValues |
||||
|
// GetValuesResult is the get values result flag.
|
||||
|
GetValuesResult |
||||
|
// UnknownType is the unknown type flag.
|
||||
|
UnknownType |
||||
|
// MaxType is the maximum type flag.
|
||||
|
MaxType = UnknownType |
||||
|
) |
||||
|
|
||||
|
const ( |
||||
|
// Responder is the responder flag.
|
||||
|
Responder uint8 = iota + 1 |
||||
|
// Authorizer is the authorizer flag.
|
||||
|
Authorizer |
||||
|
// Filter is the filter flag.
|
||||
|
Filter |
||||
|
) |
||||
|
|
||||
|
const ( |
||||
|
// RequestComplete is the completed request flag.
|
||||
|
RequestComplete uint8 = iota |
||||
|
// CantMultiplexConns is the multiplexed connections flag.
|
||||
|
CantMultiplexConns |
||||
|
// Overloaded is the overloaded flag.
|
||||
|
Overloaded |
||||
|
// UnknownRole is the unknown role flag.
|
||||
|
UnknownRole |
||||
|
) |
||||
|
|
||||
|
const ( |
||||
|
// MaxConns is the maximum connections flag.
|
||||
|
MaxConns string = "MAX_CONNS" |
||||
|
// MaxRequests is the maximum requests flag.
|
||||
|
MaxRequests string = "MAX_REQS" |
||||
|
// MultiplexConns is the multiplex connections flag.
|
||||
|
MultiplexConns string = "MPXS_CONNS" |
||||
|
) |
||||
|
|
||||
|
const ( |
||||
|
maxWrite = 65500 // 65530 may work, but for compatibility
|
||||
|
maxPad = 255 |
||||
|
) |
||||
|
|
||||
|
type header struct { |
||||
|
Version uint8 |
||||
|
Type uint8 |
||||
|
ID uint16 |
||||
|
ContentLength uint16 |
||||
|
PaddingLength uint8 |
||||
|
Reserved uint8 |
||||
|
} |
||||
|
|
||||
|
// for padding so we don't have to allocate all the time
|
||||
|
// not synchronized because we don't care what the contents are
|
||||
|
var pad [maxPad]byte |
||||
|
|
||||
|
func (h *header) init(recType uint8, reqID uint16, contentLength int) { |
||||
|
h.Version = 1 |
||||
|
h.Type = recType |
||||
|
h.ID = reqID |
||||
|
h.ContentLength = uint16(contentLength) |
||||
|
h.PaddingLength = uint8(-contentLength & 7) |
||||
|
} |
||||
|
|
||||
|
type record struct { |
||||
|
h header |
||||
|
rbuf []byte |
||||
|
} |
||||
|
|
||||
|
func (rec *record) read(r io.Reader) (buf []byte, err error) { |
||||
|
if err = binary.Read(r, binary.BigEndian, &rec.h); err != nil { |
||||
|
return |
||||
|
} |
||||
|
if rec.h.Version != 1 { |
||||
|
err = errors.New("fcgi: invalid header version") |
||||
|
return |
||||
|
} |
||||
|
if rec.h.Type == EndRequest { |
||||
|
err = io.EOF |
||||
|
return |
||||
|
} |
||||
|
n := int(rec.h.ContentLength) + int(rec.h.PaddingLength) |
||||
|
if len(rec.rbuf) < n { |
||||
|
rec.rbuf = make([]byte, n) |
||||
|
} |
||||
|
if _, err = io.ReadFull(r, rec.rbuf[:n]); err != nil { |
||||
|
return |
||||
|
} |
||||
|
buf = rec.rbuf[:int(rec.h.ContentLength)] |
||||
|
|
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// FCGIClient implements a FastCGI client, which is a standard for
|
||||
|
// interfacing external applications with Web servers.
|
||||
|
type FCGIClient struct { |
||||
|
mutex sync.Mutex |
||||
|
rwc io.ReadWriteCloser |
||||
|
h header |
||||
|
buf bytes.Buffer |
||||
|
stderr bytes.Buffer |
||||
|
keepAlive bool |
||||
|
reqID uint16 |
||||
|
} |
||||
|
|
||||
|
// DialWithDialerContext connects to the fcgi responder at the specified network address, using custom net.Dialer
|
||||
|
// and a context.
|
||||
|
// See func net.Dial for a description of the network and address parameters.
|
||||
|
func DialWithDialerContext(ctx context.Context, network, address string, dialer net.Dialer) (fcgi *FCGIClient, err error) { |
||||
|
var conn net.Conn |
||||
|
conn, err = dialer.DialContext(ctx, network, address) |
||||
|
if err != nil { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
fcgi = &FCGIClient{ |
||||
|
rwc: conn, |
||||
|
keepAlive: false, |
||||
|
reqID: 1, |
||||
|
} |
||||
|
|
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// DialContext is like Dial but passes ctx to dialer.Dial.
|
||||
|
func DialContext(ctx context.Context, network, address string) (fcgi *FCGIClient, err error) { |
||||
|
// TODO: why not set timeout here?
|
||||
|
return DialWithDialerContext(ctx, network, address, net.Dialer{}) |
||||
|
} |
||||
|
|
||||
|
// Dial connects to the fcgi responder at the specified network address, using default net.Dialer.
|
||||
|
// See func net.Dial for a description of the network and address parameters.
|
||||
|
func Dial(network, address string) (fcgi *FCGIClient, err error) { |
||||
|
return DialContext(context.Background(), network, address) |
||||
|
} |
||||
|
|
||||
|
// Close closes fcgi connection
|
||||
|
func (c *FCGIClient) Close() { |
||||
|
c.rwc.Close() |
||||
|
} |
||||
|
|
||||
|
func (c *FCGIClient) writeRecord(recType uint8, content []byte) (err error) { |
||||
|
c.mutex.Lock() |
||||
|
defer c.mutex.Unlock() |
||||
|
c.buf.Reset() |
||||
|
c.h.init(recType, c.reqID, len(content)) |
||||
|
if err := binary.Write(&c.buf, binary.BigEndian, c.h); err != nil { |
||||
|
return err |
||||
|
} |
||||
|
if _, err := c.buf.Write(content); err != nil { |
||||
|
return err |
||||
|
} |
||||
|
if _, err := c.buf.Write(pad[:c.h.PaddingLength]); err != nil { |
||||
|
return err |
||||
|
} |
||||
|
_, err = c.rwc.Write(c.buf.Bytes()) |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
func (c *FCGIClient) writeBeginRequest(role uint16, flags uint8) error { |
||||
|
b := [8]byte{byte(role >> 8), byte(role), flags} |
||||
|
return c.writeRecord(BeginRequest, b[:]) |
||||
|
} |
||||
|
|
||||
|
func (c *FCGIClient) writeEndRequest(appStatus int, protocolStatus uint8) error { |
||||
|
b := make([]byte, 8) |
||||
|
binary.BigEndian.PutUint32(b, uint32(appStatus)) |
||||
|
b[4] = protocolStatus |
||||
|
return c.writeRecord(EndRequest, b) |
||||
|
} |
||||
|
|
||||
|
func (c *FCGIClient) writePairs(recType uint8, pairs map[string]string) error { |
||||
|
w := newWriter(c, recType) |
||||
|
b := make([]byte, 8) |
||||
|
nn := 0 |
||||
|
for k, v := range pairs { |
||||
|
m := 8 + len(k) + len(v) |
||||
|
if m > maxWrite { |
||||
|
// param data size exceed 65535 bytes"
|
||||
|
vl := maxWrite - 8 - len(k) |
||||
|
v = v[:vl] |
||||
|
} |
||||
|
n := encodeSize(b, uint32(len(k))) |
||||
|
n += encodeSize(b[n:], uint32(len(v))) |
||||
|
m = n + len(k) + len(v) |
||||
|
if (nn + m) > maxWrite { |
||||
|
w.Flush() |
||||
|
nn = 0 |
||||
|
} |
||||
|
nn += m |
||||
|
if _, err := w.Write(b[:n]); err != nil { |
||||
|
return err |
||||
|
} |
||||
|
if _, err := w.WriteString(k); err != nil { |
||||
|
return err |
||||
|
} |
||||
|
if _, err := w.WriteString(v); err != nil { |
||||
|
return err |
||||
|
} |
||||
|
} |
||||
|
w.Close() |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func encodeSize(b []byte, size uint32) int { |
||||
|
if size > 127 { |
||||
|
size |= 1 << 31 |
||||
|
binary.BigEndian.PutUint32(b, size) |
||||
|
return 4 |
||||
|
} |
||||
|
b[0] = byte(size) |
||||
|
return 1 |
||||
|
} |
||||
|
|
||||
|
// bufWriter encapsulates bufio.Writer but also closes the underlying stream when
|
||||
|
// Closed.
|
||||
|
type bufWriter struct { |
||||
|
closer io.Closer |
||||
|
*bufio.Writer |
||||
|
} |
||||
|
|
||||
|
func (w *bufWriter) Close() error { |
||||
|
if err := w.Writer.Flush(); err != nil { |
||||
|
w.closer.Close() |
||||
|
return err |
||||
|
} |
||||
|
return w.closer.Close() |
||||
|
} |
||||
|
|
||||
|
func newWriter(c *FCGIClient, recType uint8) *bufWriter { |
||||
|
s := &streamWriter{c: c, recType: recType} |
||||
|
w := bufio.NewWriterSize(s, maxWrite) |
||||
|
return &bufWriter{s, w} |
||||
|
} |
||||
|
|
||||
|
// streamWriter abstracts out the separation of a stream into discrete records.
|
||||
|
// It only writes maxWrite bytes at a time.
|
||||
|
type streamWriter struct { |
||||
|
c *FCGIClient |
||||
|
recType uint8 |
||||
|
} |
||||
|
|
||||
|
func (w *streamWriter) Write(p []byte) (int, error) { |
||||
|
nn := 0 |
||||
|
for len(p) > 0 { |
||||
|
n := len(p) |
||||
|
if n > maxWrite { |
||||
|
n = maxWrite |
||||
|
} |
||||
|
if err := w.c.writeRecord(w.recType, p[:n]); err != nil { |
||||
|
return nn, err |
||||
|
} |
||||
|
nn += n |
||||
|
p = p[n:] |
||||
|
} |
||||
|
return nn, nil |
||||
|
} |
||||
|
|
||||
|
func (w *streamWriter) Close() error { |
||||
|
// send empty record to close the stream
|
||||
|
return w.c.writeRecord(w.recType, nil) |
||||
|
} |
||||
|
|
||||
|
type streamReader struct { |
||||
|
c *FCGIClient |
||||
|
buf []byte |
||||
|
} |
||||
|
|
||||
|
func (w *streamReader) Read(p []byte) (n int, err error) { |
||||
|
|
||||
|
if len(p) > 0 { |
||||
|
if len(w.buf) == 0 { |
||||
|
|
||||
|
// filter outputs for error log
|
||||
|
for { |
||||
|
rec := &record{} |
||||
|
var buf []byte |
||||
|
buf, err = rec.read(w.c.rwc) |
||||
|
if err != nil { |
||||
|
return |
||||
|
} |
||||
|
// standard error output
|
||||
|
if rec.h.Type == Stderr { |
||||
|
w.c.stderr.Write(buf) |
||||
|
continue |
||||
|
} |
||||
|
w.buf = buf |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
n = len(p) |
||||
|
if n > len(w.buf) { |
||||
|
n = len(w.buf) |
||||
|
} |
||||
|
copy(p, w.buf[:n]) |
||||
|
w.buf = w.buf[n:] |
||||
|
} |
||||
|
|
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Do made the request and returns a io.Reader that translates the data read
|
||||
|
// from fcgi responder out of fcgi packet before returning it.
|
||||
|
func (c *FCGIClient) Do(p map[string]string, req io.Reader) (r io.Reader, err error) { |
||||
|
err = c.writeBeginRequest(uint16(Responder), 0) |
||||
|
if err != nil { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
err = c.writePairs(Params, p) |
||||
|
if err != nil { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
body := newWriter(c, Stdin) |
||||
|
if req != nil { |
||||
|
_, _ = io.Copy(body, req) |
||||
|
} |
||||
|
body.Close() |
||||
|
|
||||
|
r = &streamReader{c: c} |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// clientCloser is a io.ReadCloser. It wraps a io.Reader with a Closer
|
||||
|
// that closes FCGIClient connection.
|
||||
|
type clientCloser struct { |
||||
|
*FCGIClient |
||||
|
io.Reader |
||||
|
} |
||||
|
|
||||
|
func (f clientCloser) Close() error { return f.rwc.Close() } |
||||
|
|
||||
|
// Request returns a HTTP Response with Header and Body
|
||||
|
// from fcgi responder
|
||||
|
func (c *FCGIClient) Request(p map[string]string, req io.Reader) (resp *http.Response, err error) { |
||||
|
r, err := c.Do(p, req) |
||||
|
if err != nil { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
rb := bufio.NewReader(r) |
||||
|
tp := textproto.NewReader(rb) |
||||
|
resp = new(http.Response) |
||||
|
|
||||
|
// Parse the response headers.
|
||||
|
mimeHeader, err := tp.ReadMIMEHeader() |
||||
|
if err != nil && err != io.EOF { |
||||
|
return |
||||
|
} |
||||
|
resp.Header = http.Header(mimeHeader) |
||||
|
|
||||
|
if resp.Header.Get("Status") != "" { |
||||
|
statusParts := strings.SplitN(resp.Header.Get("Status"), " ", 2) |
||||
|
resp.StatusCode, err = strconv.Atoi(statusParts[0]) |
||||
|
if err != nil { |
||||
|
return |
||||
|
} |
||||
|
if len(statusParts) > 1 { |
||||
|
resp.Status = statusParts[1] |
||||
|
} |
||||
|
|
||||
|
} else { |
||||
|
resp.StatusCode = http.StatusOK |
||||
|
} |
||||
|
|
||||
|
// TODO: fixTransferEncoding ?
|
||||
|
resp.TransferEncoding = resp.Header["Transfer-Encoding"] |
||||
|
resp.ContentLength, _ = strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64) |
||||
|
|
||||
|
if chunked(resp.TransferEncoding) { |
||||
|
resp.Body = clientCloser{c, httputil.NewChunkedReader(rb)} |
||||
|
} else { |
||||
|
resp.Body = clientCloser{c, ioutil.NopCloser(rb)} |
||||
|
} |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Get issues a GET request to the fcgi responder.
|
||||
|
func (c *FCGIClient) Get(p map[string]string, body io.Reader, l int64) (resp *http.Response, err error) { |
||||
|
|
||||
|
p["REQUEST_METHOD"] = "GET" |
||||
|
p["CONTENT_LENGTH"] = strconv.FormatInt(l, 10) |
||||
|
|
||||
|
return c.Request(p, body) |
||||
|
} |
||||
|
|
||||
|
// Head issues a HEAD request to the fcgi responder.
|
||||
|
func (c *FCGIClient) Head(p map[string]string) (resp *http.Response, err error) { |
||||
|
|
||||
|
p["REQUEST_METHOD"] = "HEAD" |
||||
|
p["CONTENT_LENGTH"] = "0" |
||||
|
|
||||
|
return c.Request(p, nil) |
||||
|
} |
||||
|
|
||||
|
// Options issues an OPTIONS request to the fcgi responder.
|
||||
|
func (c *FCGIClient) Options(p map[string]string) (resp *http.Response, err error) { |
||||
|
|
||||
|
p["REQUEST_METHOD"] = "OPTIONS" |
||||
|
p["CONTENT_LENGTH"] = "0" |
||||
|
|
||||
|
return c.Request(p, nil) |
||||
|
} |
||||
|
|
||||
|
// Post issues a POST request to the fcgi responder. with request body
|
||||
|
// in the format that bodyType specified
|
||||
|
func (c *FCGIClient) Post(p map[string]string, method string, bodyType string, body io.Reader, l int64) (resp *http.Response, err error) { |
||||
|
if p == nil { |
||||
|
p = make(map[string]string) |
||||
|
} |
||||
|
|
||||
|
p["REQUEST_METHOD"] = strings.ToUpper(method) |
||||
|
|
||||
|
if len(p["REQUEST_METHOD"]) == 0 || p["REQUEST_METHOD"] == "GET" { |
||||
|
p["REQUEST_METHOD"] = "POST" |
||||
|
} |
||||
|
|
||||
|
p["CONTENT_LENGTH"] = strconv.FormatInt(l, 10) |
||||
|
if len(bodyType) > 0 { |
||||
|
p["CONTENT_TYPE"] = bodyType |
||||
|
} else { |
||||
|
p["CONTENT_TYPE"] = "application/x-www-form-urlencoded" |
||||
|
} |
||||
|
|
||||
|
return c.Request(p, body) |
||||
|
} |
||||
|
|
||||
|
// PostForm issues a POST to the fcgi responder, with form
|
||||
|
// as a string key to a list values (url.Values)
|
||||
|
func (c *FCGIClient) PostForm(p map[string]string, data url.Values) (resp *http.Response, err error) { |
||||
|
body := bytes.NewReader([]byte(data.Encode())) |
||||
|
return c.Post(p, "POST", "application/x-www-form-urlencoded", body, int64(body.Len())) |
||||
|
} |
||||
|
|
||||
|
// PostFile issues a POST to the fcgi responder in multipart(RFC 2046) standard,
|
||||
|
// with form as a string key to a list values (url.Values),
|
||||
|
// and/or with file as a string key to a list file path.
|
||||
|
func (c *FCGIClient) PostFile(p map[string]string, data url.Values, file map[string]string) (resp *http.Response, err error) { |
||||
|
buf := &bytes.Buffer{} |
||||
|
writer := multipart.NewWriter(buf) |
||||
|
bodyType := writer.FormDataContentType() |
||||
|
|
||||
|
for key, val := range data { |
||||
|
for _, v0 := range val { |
||||
|
err = writer.WriteField(key, v0) |
||||
|
if err != nil { |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
for key, val := range file { |
||||
|
fd, e := os.Open(val) |
||||
|
if e != nil { |
||||
|
return nil, e |
||||
|
} |
||||
|
defer fd.Close() |
||||
|
|
||||
|
part, e := writer.CreateFormFile(key, filepath.Base(val)) |
||||
|
if e != nil { |
||||
|
return nil, e |
||||
|
} |
||||
|
_, err = io.Copy(part, fd) |
||||
|
if err != nil { |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
err = writer.Close() |
||||
|
if err != nil { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
return c.Post(p, "POST", bodyType, buf, int64(buf.Len())) |
||||
|
} |
||||
|
|
||||
|
// SetReadTimeout sets the read timeout for future calls that read from the
|
||||
|
// fcgi responder. A zero value for t means no timeout will be set.
|
||||
|
func (c *FCGIClient) SetReadTimeout(t time.Duration) error { |
||||
|
if conn, ok := c.rwc.(net.Conn); ok && t != 0 { |
||||
|
return conn.SetReadDeadline(time.Now().Add(t)) |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// SetWriteTimeout sets the write timeout for future calls that send data to
|
||||
|
// the fcgi responder. A zero value for t means no timeout will be set.
|
||||
|
func (c *FCGIClient) SetWriteTimeout(t time.Duration) error { |
||||
|
if conn, ok := c.rwc.(net.Conn); ok && t != 0 { |
||||
|
return conn.SetWriteDeadline(time.Now().Add(t)) |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// Checks whether chunked is part of the encodings stack
|
||||
|
func chunked(te []string) bool { return len(te) > 0 && te[0] == "chunked" } |
@ -0,0 +1,301 @@ |
|||||
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
|
//
|
||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
// you may not use this file except in compliance with the License.
|
||||
|
// You may obtain a copy of the License at
|
||||
|
//
|
||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
//
|
||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
// See the License for the specific language governing permissions and
|
||||
|
// limitations under the License.
|
||||
|
|
||||
|
// NOTE: These tests were adapted from the original
|
||||
|
// repository from which this package was forked.
|
||||
|
// The tests are slow (~10s) and in dire need of rewriting.
|
||||
|
// As such, the tests have been disabled to speed up
|
||||
|
// automated builds until they can be properly written.
|
||||
|
|
||||
|
package fastcgi |
||||
|
|
||||
|
import ( |
||||
|
"bytes" |
||||
|
"crypto/md5" |
||||
|
"encoding/binary" |
||||
|
"fmt" |
||||
|
"io" |
||||
|
"io/ioutil" |
||||
|
"log" |
||||
|
"math/rand" |
||||
|
"net" |
||||
|
"net/http" |
||||
|
"net/http/fcgi" |
||||
|
"net/url" |
||||
|
"os" |
||||
|
"path/filepath" |
||||
|
"strconv" |
||||
|
"strings" |
||||
|
"testing" |
||||
|
"time" |
||||
|
) |
||||
|
|
||||
|
// test fcgi protocol includes:
|
||||
|
// Get, Post, Post in multipart/form-data, and Post with files
|
||||
|
// each key should be the md5 of the value or the file uploaded
|
||||
|
// specify remote fcgi responder ip:port to test with php
|
||||
|
// test failed if the remote fcgi(script) failed md5 verification
|
||||
|
// and output "FAILED" in response
|
||||
|
const ( |
||||
|
scriptFile = "/tank/www/fcgic_test.php" |
||||
|
//ipPort = "remote-php-serv:59000"
|
||||
|
ipPort = "127.0.0.1:59000" |
||||
|
) |
||||
|
|
||||
|
var globalt *testing.T |
||||
|
|
||||
|
type FastCGIServer struct{} |
||||
|
|
||||
|
func (s FastCGIServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) { |
||||
|
|
||||
|
if err := req.ParseMultipartForm(100000000); err != nil { |
||||
|
log.Printf("[ERROR] failed to parse: %v", err) |
||||
|
} |
||||
|
|
||||
|
stat := "PASSED" |
||||
|
fmt.Fprintln(resp, "-") |
||||
|
fileNum := 0 |
||||
|
{ |
||||
|
length := 0 |
||||
|
for k0, v0 := range req.Form { |
||||
|
h := md5.New() |
||||
|
_, _ = io.WriteString(h, v0[0]) |
||||
|
_md5 := fmt.Sprintf("%x", h.Sum(nil)) |
||||
|
|
||||
|
length += len(k0) |
||||
|
length += len(v0[0]) |
||||
|
|
||||
|
// echo error when key != _md5(val)
|
||||
|
if _md5 != k0 { |
||||
|
fmt.Fprintln(resp, "server:err ", _md5, k0) |
||||
|
stat = "FAILED" |
||||
|
} |
||||
|
} |
||||
|
if req.MultipartForm != nil { |
||||
|
fileNum = len(req.MultipartForm.File) |
||||
|
for kn, fns := range req.MultipartForm.File { |
||||
|
//fmt.Fprintln(resp, "server:filekey ", kn )
|
||||
|
length += len(kn) |
||||
|
for _, f := range fns { |
||||
|
fd, err := f.Open() |
||||
|
if err != nil { |
||||
|
log.Println("server:", err) |
||||
|
return |
||||
|
} |
||||
|
h := md5.New() |
||||
|
l0, err := io.Copy(h, fd) |
||||
|
if err != nil { |
||||
|
log.Println(err) |
||||
|
return |
||||
|
} |
||||
|
length += int(l0) |
||||
|
defer fd.Close() |
||||
|
md5 := fmt.Sprintf("%x", h.Sum(nil)) |
||||
|
//fmt.Fprintln(resp, "server:filemd5 ", md5 )
|
||||
|
|
||||
|
if kn != md5 { |
||||
|
fmt.Fprintln(resp, "server:err ", md5, kn) |
||||
|
stat = "FAILED" |
||||
|
} |
||||
|
//fmt.Fprintln(resp, "server:filename ", f.Filename )
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
fmt.Fprintln(resp, "server:got data length", length) |
||||
|
} |
||||
|
fmt.Fprintln(resp, "-"+stat+"-POST(", len(req.Form), ")-FILE(", fileNum, ")--") |
||||
|
} |
||||
|
|
||||
|
func sendFcgi(reqType int, fcgiParams map[string]string, data []byte, posts map[string]string, files map[string]string) (content []byte) { |
||||
|
fcgi, err := Dial("tcp", ipPort) |
||||
|
if err != nil { |
||||
|
log.Println("err:", err) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
length := 0 |
||||
|
|
||||
|
var resp *http.Response |
||||
|
switch reqType { |
||||
|
case 0: |
||||
|
if len(data) > 0 { |
||||
|
length = len(data) |
||||
|
rd := bytes.NewReader(data) |
||||
|
resp, err = fcgi.Post(fcgiParams, "", "", rd, int64(rd.Len())) |
||||
|
} else if len(posts) > 0 { |
||||
|
values := url.Values{} |
||||
|
for k, v := range posts { |
||||
|
values.Set(k, v) |
||||
|
length += len(k) + 2 + len(v) |
||||
|
} |
||||
|
resp, err = fcgi.PostForm(fcgiParams, values) |
||||
|
} else { |
||||
|
rd := bytes.NewReader(data) |
||||
|
resp, err = fcgi.Get(fcgiParams, rd, int64(rd.Len())) |
||||
|
} |
||||
|
|
||||
|
default: |
||||
|
values := url.Values{} |
||||
|
for k, v := range posts { |
||||
|
values.Set(k, v) |
||||
|
length += len(k) + 2 + len(v) |
||||
|
} |
||||
|
|
||||
|
for k, v := range files { |
||||
|
fi, _ := os.Lstat(v) |
||||
|
length += len(k) + int(fi.Size()) |
||||
|
} |
||||
|
resp, err = fcgi.PostFile(fcgiParams, values, files) |
||||
|
} |
||||
|
|
||||
|
if err != nil { |
||||
|
log.Println("err:", err) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
defer resp.Body.Close() |
||||
|
content, _ = ioutil.ReadAll(resp.Body) |
||||
|
|
||||
|
log.Println("c: send data length ≈", length, string(content)) |
||||
|
fcgi.Close() |
||||
|
time.Sleep(1 * time.Second) |
||||
|
|
||||
|
if bytes.Contains(content, []byte("FAILED")) { |
||||
|
globalt.Error("Server return failed message") |
||||
|
} |
||||
|
|
||||
|
return |
||||
|
} |
||||
|
|
||||
|
func generateRandFile(size int) (p string, m string) { |
||||
|
|
||||
|
p = filepath.Join(os.TempDir(), "fcgict"+strconv.Itoa(rand.Int())) |
||||
|
|
||||
|
// open output file
|
||||
|
fo, err := os.Create(p) |
||||
|
if err != nil { |
||||
|
panic(err) |
||||
|
} |
||||
|
// close fo on exit and check for its returned error
|
||||
|
defer func() { |
||||
|
if err := fo.Close(); err != nil { |
||||
|
panic(err) |
||||
|
} |
||||
|
}() |
||||
|
|
||||
|
h := md5.New() |
||||
|
for i := 0; i < size/16; i++ { |
||||
|
buf := make([]byte, 16) |
||||
|
binary.PutVarint(buf, rand.Int63()) |
||||
|
if _, err := fo.Write(buf); err != nil { |
||||
|
log.Printf("[ERROR] failed to write buffer: %v\n", err) |
||||
|
} |
||||
|
if _, err := h.Write(buf); err != nil { |
||||
|
log.Printf("[ERROR] failed to write buffer: %v\n", err) |
||||
|
} |
||||
|
} |
||||
|
m = fmt.Sprintf("%x", h.Sum(nil)) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
func DisabledTest(t *testing.T) { |
||||
|
// TODO: test chunked reader
|
||||
|
globalt = t |
||||
|
|
||||
|
rand.Seed(time.Now().UTC().UnixNano()) |
||||
|
|
||||
|
// server
|
||||
|
go func() { |
||||
|
listener, err := net.Listen("tcp", ipPort) |
||||
|
if err != nil { |
||||
|
log.Println("listener creation failed: ", err) |
||||
|
} |
||||
|
|
||||
|
srv := new(FastCGIServer) |
||||
|
if err := fcgi.Serve(listener, srv); err != nil { |
||||
|
log.Print("[ERROR] failed to start server: ", err) |
||||
|
} |
||||
|
}() |
||||
|
|
||||
|
time.Sleep(1 * time.Second) |
||||
|
|
||||
|
// init
|
||||
|
fcgiParams := make(map[string]string) |
||||
|
fcgiParams["REQUEST_METHOD"] = "GET" |
||||
|
fcgiParams["SERVER_PROTOCOL"] = "HTTP/1.1" |
||||
|
//fcgi_params["GATEWAY_INTERFACE"] = "CGI/1.1"
|
||||
|
fcgiParams["SCRIPT_FILENAME"] = scriptFile |
||||
|
|
||||
|
// simple GET
|
||||
|
log.Println("test:", "get") |
||||
|
sendFcgi(0, fcgiParams, nil, nil, nil) |
||||
|
|
||||
|
// simple post data
|
||||
|
log.Println("test:", "post") |
||||
|
sendFcgi(0, fcgiParams, []byte("c4ca4238a0b923820dcc509a6f75849b=1&7b8b965ad4bca0e41ab51de7b31363a1=n"), nil, nil) |
||||
|
|
||||
|
log.Println("test:", "post data (more than 60KB)") |
||||
|
data := "" |
||||
|
for i := 0x00; i < 0xff; i++ { |
||||
|
v0 := strings.Repeat(string(i), 256) |
||||
|
h := md5.New() |
||||
|
_, _ = io.WriteString(h, v0) |
||||
|
k0 := fmt.Sprintf("%x", h.Sum(nil)) |
||||
|
data += k0 + "=" + url.QueryEscape(v0) + "&" |
||||
|
} |
||||
|
sendFcgi(0, fcgiParams, []byte(data), nil, nil) |
||||
|
|
||||
|
log.Println("test:", "post form (use url.Values)") |
||||
|
p0 := make(map[string]string, 1) |
||||
|
p0["c4ca4238a0b923820dcc509a6f75849b"] = "1" |
||||
|
p0["7b8b965ad4bca0e41ab51de7b31363a1"] = "n" |
||||
|
sendFcgi(1, fcgiParams, nil, p0, nil) |
||||
|
|
||||
|
log.Println("test:", "post forms (256 keys, more than 1MB)") |
||||
|
p1 := make(map[string]string, 1) |
||||
|
for i := 0x00; i < 0xff; i++ { |
||||
|
v0 := strings.Repeat(string(i), 4096) |
||||
|
h := md5.New() |
||||
|
_, _ = io.WriteString(h, v0) |
||||
|
k0 := fmt.Sprintf("%x", h.Sum(nil)) |
||||
|
p1[k0] = v0 |
||||
|
} |
||||
|
sendFcgi(1, fcgiParams, nil, p1, nil) |
||||
|
|
||||
|
log.Println("test:", "post file (1 file, 500KB)) ") |
||||
|
f0 := make(map[string]string, 1) |
||||
|
path0, m0 := generateRandFile(500000) |
||||
|
f0[m0] = path0 |
||||
|
sendFcgi(1, fcgiParams, nil, p1, f0) |
||||
|
|
||||
|
log.Println("test:", "post multiple files (2 files, 5M each) and forms (256 keys, more than 1MB data") |
||||
|
path1, m1 := generateRandFile(5000000) |
||||
|
f0[m1] = path1 |
||||
|
sendFcgi(1, fcgiParams, nil, p1, f0) |
||||
|
|
||||
|
log.Println("test:", "post only files (2 files, 5M each)") |
||||
|
sendFcgi(1, fcgiParams, nil, nil, f0) |
||||
|
|
||||
|
log.Println("test:", "post only 1 file") |
||||
|
delete(f0, "m0") |
||||
|
sendFcgi(1, fcgiParams, nil, nil, f0) |
||||
|
|
||||
|
if err := os.Remove(path0); err != nil { |
||||
|
log.Println("[ERROR] failed to remove path: ", err) |
||||
|
} |
||||
|
if err := os.Remove(path1); err != nil { |
||||
|
log.Println("[ERROR] failed to remove path: ", err) |
||||
|
} |
||||
|
} |
@ -0,0 +1,342 @@ |
|||||
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
|
//
|
||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
// you may not use this file except in compliance with the License.
|
||||
|
// You may obtain a copy of the License at
|
||||
|
//
|
||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
//
|
||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
// See the License for the specific language governing permissions and
|
||||
|
// limitations under the License.
|
||||
|
|
||||
|
package fastcgi |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"crypto/tls" |
||||
|
"fmt" |
||||
|
"net/http" |
||||
|
"path" |
||||
|
"path/filepath" |
||||
|
"strconv" |
||||
|
"strings" |
||||
|
"time" |
||||
|
|
||||
|
"github.com/caddyserver/caddy/v2/modules/caddytls" |
||||
|
|
||||
|
"github.com/caddyserver/caddy/v2" |
||||
|
) |
||||
|
|
||||
|
func init() { |
||||
|
caddy.RegisterModule(Transport{}) |
||||
|
} |
||||
|
|
||||
|
type Transport struct { |
||||
|
//////////////////////////////
|
||||
|
// TODO: taken from v1 Handler type
|
||||
|
|
||||
|
SoftwareName string |
||||
|
SoftwareVersion string |
||||
|
ServerName string |
||||
|
ServerPort string |
||||
|
|
||||
|
//////////////////////////
|
||||
|
// TODO: taken from v1 Rule type
|
||||
|
|
||||
|
// The base path to match. Required.
|
||||
|
// Path string
|
||||
|
|
||||
|
// upstream load balancer
|
||||
|
// balancer
|
||||
|
|
||||
|
// Always process files with this extension with fastcgi.
|
||||
|
// Ext string
|
||||
|
|
||||
|
// Use this directory as the fastcgi root directory. Defaults to the root
|
||||
|
// directory of the parent virtual host.
|
||||
|
Root string |
||||
|
|
||||
|
// The path in the URL will be split into two, with the first piece ending
|
||||
|
// with the value of SplitPath. The first piece will be assumed as the
|
||||
|
// actual resource (CGI script) name, and the second piece will be set to
|
||||
|
// PATH_INFO for the CGI script to use.
|
||||
|
SplitPath string |
||||
|
|
||||
|
// If the URL ends with '/' (which indicates a directory), these index
|
||||
|
// files will be tried instead.
|
||||
|
IndexFiles []string |
||||
|
|
||||
|
// Environment Variables
|
||||
|
EnvVars [][2]string |
||||
|
|
||||
|
// Ignored paths
|
||||
|
IgnoredSubPaths []string |
||||
|
|
||||
|
// The duration used to set a deadline when connecting to an upstream.
|
||||
|
DialTimeout time.Duration |
||||
|
|
||||
|
// The duration used to set a deadline when reading from the FastCGI server.
|
||||
|
ReadTimeout time.Duration |
||||
|
|
||||
|
// The duration used to set a deadline when sending to the FastCGI server.
|
||||
|
WriteTimeout time.Duration |
||||
|
} |
||||
|
|
||||
|
// CaddyModule returns the Caddy module information.
|
||||
|
func (Transport) CaddyModule() caddy.ModuleInfo { |
||||
|
return caddy.ModuleInfo{ |
||||
|
Name: "http.handlers.reverse_proxy.transport.fastcgi", |
||||
|
New: func() caddy.Module { return new(Transport) }, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (t Transport) RoundTrip(r *http.Request) (*http.Response, error) { |
||||
|
// Create environment for CGI script
|
||||
|
env, err := t.buildEnv(r) |
||||
|
if err != nil { |
||||
|
return nil, fmt.Errorf("building environment: %v", err) |
||||
|
} |
||||
|
|
||||
|
// TODO:
|
||||
|
// Connect to FastCGI gateway
|
||||
|
// address, err := f.Address()
|
||||
|
// if err != nil {
|
||||
|
// return http.StatusBadGateway, err
|
||||
|
// }
|
||||
|
// network, address := parseAddress(address)
|
||||
|
network, address := "tcp", r.URL.Host // TODO:
|
||||
|
|
||||
|
ctx := context.Background() |
||||
|
if t.DialTimeout > 0 { |
||||
|
var cancel context.CancelFunc |
||||
|
ctx, cancel = context.WithTimeout(ctx, t.DialTimeout) |
||||
|
defer cancel() |
||||
|
} |
||||
|
|
||||
|
fcgiBackend, err := DialContext(ctx, network, address) |
||||
|
if err != nil { |
||||
|
return nil, fmt.Errorf("dialing backend: %v", err) |
||||
|
} |
||||
|
// fcgiBackend is closed when response body is closed (see clientCloser)
|
||||
|
|
||||
|
// read/write timeouts
|
||||
|
if err := fcgiBackend.SetReadTimeout(t.ReadTimeout); err != nil { |
||||
|
return nil, fmt.Errorf("setting read timeout: %v", err) |
||||
|
} |
||||
|
if err := fcgiBackend.SetWriteTimeout(t.WriteTimeout); err != nil { |
||||
|
return nil, fmt.Errorf("setting write timeout: %v", err) |
||||
|
} |
||||
|
|
||||
|
var resp *http.Response |
||||
|
|
||||
|
var contentLength int64 |
||||
|
// if ContentLength is already set
|
||||
|
if r.ContentLength > 0 { |
||||
|
contentLength = r.ContentLength |
||||
|
} else { |
||||
|
contentLength, _ = strconv.ParseInt(r.Header.Get("Content-Length"), 10, 64) |
||||
|
} |
||||
|
switch r.Method { |
||||
|
case "HEAD": |
||||
|
resp, err = fcgiBackend.Head(env) |
||||
|
case "GET": |
||||
|
resp, err = fcgiBackend.Get(env, r.Body, contentLength) |
||||
|
case "OPTIONS": |
||||
|
resp, err = fcgiBackend.Options(env) |
||||
|
default: |
||||
|
resp, err = fcgiBackend.Post(env, r.Method, r.Header.Get("Content-Type"), r.Body, contentLength) |
||||
|
} |
||||
|
|
||||
|
// TODO:
|
||||
|
return resp, err |
||||
|
|
||||
|
// Stuff brought over from v1 that might not be necessary here:
|
||||
|
|
||||
|
// if resp != nil && resp.Body != nil {
|
||||
|
// defer resp.Body.Close()
|
||||
|
// }
|
||||
|
|
||||
|
// if err != nil {
|
||||
|
// if err, ok := err.(net.Error); ok && err.Timeout() {
|
||||
|
// return http.StatusGatewayTimeout, err
|
||||
|
// } else if err != io.EOF {
|
||||
|
// return http.StatusBadGateway, err
|
||||
|
// }
|
||||
|
// }
|
||||
|
|
||||
|
// // Write response header
|
||||
|
// writeHeader(w, resp)
|
||||
|
|
||||
|
// // Write the response body
|
||||
|
// _, err = io.Copy(w, resp.Body)
|
||||
|
// if err != nil {
|
||||
|
// return http.StatusBadGateway, err
|
||||
|
// }
|
||||
|
|
||||
|
// // Log any stderr output from upstream
|
||||
|
// if fcgiBackend.stderr.Len() != 0 {
|
||||
|
// // Remove trailing newline, error logger already does this.
|
||||
|
// err = LogError(strings.TrimSuffix(fcgiBackend.stderr.String(), "\n"))
|
||||
|
// }
|
||||
|
|
||||
|
// // Normally we would return the status code if it is an error status (>= 400),
|
||||
|
// // however, upstream FastCGI apps don't know about our contract and have
|
||||
|
// // probably already written an error page. So we just return 0, indicating
|
||||
|
// // that the response body is already written. However, we do return any
|
||||
|
// // error value so it can be logged.
|
||||
|
// // Note that the proxy middleware works the same way, returning status=0.
|
||||
|
// return 0, err
|
||||
|
} |
||||
|
|
||||
|
// buildEnv returns a set of CGI environment variables for the request.
|
||||
|
func (t Transport) buildEnv(r *http.Request) (map[string]string, error) { |
||||
|
var env map[string]string |
||||
|
|
||||
|
// Separate remote IP and port; more lenient than net.SplitHostPort
|
||||
|
var ip, port string |
||||
|
if idx := strings.LastIndex(r.RemoteAddr, ":"); idx > -1 { |
||||
|
ip = r.RemoteAddr[:idx] |
||||
|
port = r.RemoteAddr[idx+1:] |
||||
|
} else { |
||||
|
ip = r.RemoteAddr |
||||
|
} |
||||
|
|
||||
|
// Remove [] from IPv6 addresses
|
||||
|
ip = strings.Replace(ip, "[", "", 1) |
||||
|
ip = strings.Replace(ip, "]", "", 1) |
||||
|
|
||||
|
// TODO: respect index files? or leave that to matcher/rewrite (I prefer that)?
|
||||
|
fpath := r.URL.Path |
||||
|
|
||||
|
// Split path in preparation for env variables.
|
||||
|
// Previous canSplit checks ensure this can never be -1.
|
||||
|
// TODO: I haven't brought over canSplit; make sure this doesn't break
|
||||
|
splitPos := t.splitPos(fpath) |
||||
|
|
||||
|
// Request has the extension; path was split successfully
|
||||
|
docURI := fpath[:splitPos+len(t.SplitPath)] |
||||
|
pathInfo := fpath[splitPos+len(t.SplitPath):] |
||||
|
scriptName := fpath |
||||
|
|
||||
|
// Strip PATH_INFO from SCRIPT_NAME
|
||||
|
scriptName = strings.TrimSuffix(scriptName, pathInfo) |
||||
|
|
||||
|
// SCRIPT_FILENAME is the absolute path of SCRIPT_NAME
|
||||
|
scriptFilename := filepath.Join(t.Root, scriptName) |
||||
|
|
||||
|
// Add vhost path prefix to scriptName. Otherwise, some PHP software will
|
||||
|
// have difficulty discovering its URL.
|
||||
|
pathPrefix, _ := r.Context().Value(caddy.CtxKey("path_prefix")).(string) |
||||
|
scriptName = path.Join(pathPrefix, scriptName) |
||||
|
|
||||
|
// TODO: Disabled for now
|
||||
|
// // Get the request URI from context. The context stores the original URI in case
|
||||
|
// // it was changed by a middleware such as rewrite. By default, we pass the
|
||||
|
// // original URI in as the value of REQUEST_URI (the user can overwrite this
|
||||
|
// // if desired). Most PHP apps seem to want the original URI. Besides, this is
|
||||
|
// // how nginx defaults: http://stackoverflow.com/a/12485156/1048862
|
||||
|
// reqURL, _ := r.Context().Value(httpserver.OriginalURLCtxKey).(url.URL)
|
||||
|
|
||||
|
// // Retrieve name of remote user that was set by some downstream middleware such as basicauth.
|
||||
|
// remoteUser, _ := r.Context().Value(httpserver.RemoteUserCtxKey).(string)
|
||||
|
|
||||
|
requestScheme := "http" |
||||
|
if r.TLS != nil { |
||||
|
requestScheme = "https" |
||||
|
} |
||||
|
|
||||
|
// Some variables are unused but cleared explicitly to prevent
|
||||
|
// the parent environment from interfering.
|
||||
|
env = map[string]string{ |
||||
|
// Variables defined in CGI 1.1 spec
|
||||
|
"AUTH_TYPE": "", // Not used
|
||||
|
"CONTENT_LENGTH": r.Header.Get("Content-Length"), |
||||
|
"CONTENT_TYPE": r.Header.Get("Content-Type"), |
||||
|
"GATEWAY_INTERFACE": "CGI/1.1", |
||||
|
"PATH_INFO": pathInfo, |
||||
|
"QUERY_STRING": r.URL.RawQuery, |
||||
|
"REMOTE_ADDR": ip, |
||||
|
"REMOTE_HOST": ip, // For speed, remote host lookups disabled
|
||||
|
"REMOTE_PORT": port, |
||||
|
"REMOTE_IDENT": "", // Not used
|
||||
|
// "REMOTE_USER": remoteUser, // TODO:
|
||||
|
"REQUEST_METHOD": r.Method, |
||||
|
"REQUEST_SCHEME": requestScheme, |
||||
|
"SERVER_NAME": t.ServerName, |
||||
|
"SERVER_PORT": t.ServerPort, |
||||
|
"SERVER_PROTOCOL": r.Proto, |
||||
|
"SERVER_SOFTWARE": t.SoftwareName + "/" + t.SoftwareVersion, |
||||
|
|
||||
|
// Other variables
|
||||
|
// "DOCUMENT_ROOT": rule.Root,
|
||||
|
"DOCUMENT_URI": docURI, |
||||
|
"HTTP_HOST": r.Host, // added here, since not always part of headers
|
||||
|
// "REQUEST_URI": reqURL.RequestURI(), // TODO:
|
||||
|
"SCRIPT_FILENAME": scriptFilename, |
||||
|
"SCRIPT_NAME": scriptName, |
||||
|
} |
||||
|
|
||||
|
// compliance with the CGI specification requires that
|
||||
|
// PATH_TRANSLATED should only exist if PATH_INFO is defined.
|
||||
|
// Info: https://www.ietf.org/rfc/rfc3875 Page 14
|
||||
|
if env["PATH_INFO"] != "" { |
||||
|
env["PATH_TRANSLATED"] = filepath.Join(t.Root, pathInfo) // Info: http://www.oreilly.com/openbook/cgi/ch02_04.html
|
||||
|
} |
||||
|
|
||||
|
// Some web apps rely on knowing HTTPS or not
|
||||
|
if r.TLS != nil { |
||||
|
env["HTTPS"] = "on" |
||||
|
// and pass the protocol details in a manner compatible with apache's mod_ssl
|
||||
|
// (which is why these have a SSL_ prefix and not TLS_).
|
||||
|
v, ok := tlsProtocolStrings[r.TLS.Version] |
||||
|
if ok { |
||||
|
env["SSL_PROTOCOL"] = v |
||||
|
} |
||||
|
// and pass the cipher suite in a manner compatible with apache's mod_ssl
|
||||
|
for k, v := range caddytls.SupportedCipherSuites { |
||||
|
if v == r.TLS.CipherSuite { |
||||
|
env["SSL_CIPHER"] = k |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Add env variables from config (with support for placeholders in values)
|
||||
|
repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer) |
||||
|
for _, envVar := range t.EnvVars { |
||||
|
env[envVar[0]] = repl.ReplaceAll(envVar[1], "") |
||||
|
} |
||||
|
|
||||
|
// Add all HTTP headers to env variables
|
||||
|
for field, val := range r.Header { |
||||
|
header := strings.ToUpper(field) |
||||
|
header = headerNameReplacer.Replace(header) |
||||
|
env["HTTP_"+header] = strings.Join(val, ", ") |
||||
|
} |
||||
|
return env, nil |
||||
|
} |
||||
|
|
||||
|
// splitPos returns the index where path should
|
||||
|
// be split based on t.SplitPath.
|
||||
|
func (t Transport) splitPos(path string) int { |
||||
|
// TODO:
|
||||
|
// if httpserver.CaseSensitivePath {
|
||||
|
// return strings.Index(path, r.SplitPath)
|
||||
|
// }
|
||||
|
return strings.Index(strings.ToLower(path), strings.ToLower(t.SplitPath)) |
||||
|
} |
||||
|
|
||||
|
// TODO:
|
||||
|
// Map of supported protocols to Apache ssl_mod format
|
||||
|
// Note that these are slightly different from SupportedProtocols in caddytls/config.go
|
||||
|
var tlsProtocolStrings = map[uint16]string{ |
||||
|
tls.VersionTLS10: "TLSv1", |
||||
|
tls.VersionTLS11: "TLSv1.1", |
||||
|
tls.VersionTLS12: "TLSv1.2", |
||||
|
tls.VersionTLS13: "TLSv1.3", |
||||
|
} |
||||
|
|
||||
|
var headerNameReplacer = strings.NewReplacer(" ", "_", "-", "_") |
@ -1,86 +0,0 @@ |
|||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
||||
//
|
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||
// you may not use this file except in compliance with the License.
|
|
||||
// You may obtain a copy of the License at
|
|
||||
//
|
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||
//
|
|
||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||
// See the License for the specific language governing permissions and
|
|
||||
// limitations under the License.
|
|
||||
|
|
||||
package reverseproxy |
|
||||
|
|
||||
import ( |
|
||||
"net/http" |
|
||||
"time" |
|
||||
) |
|
||||
|
|
||||
// Upstream represents the interface that must be satisfied to use the healthchecker.
|
|
||||
type Upstream interface { |
|
||||
SetHealthiness(bool) |
|
||||
} |
|
||||
|
|
||||
// HealthChecker represents a worker that periodically evaluates if proxy upstream host is healthy.
|
|
||||
type HealthChecker struct { |
|
||||
upstream Upstream |
|
||||
Ticker *time.Ticker |
|
||||
HTTPClient *http.Client |
|
||||
StopChan chan bool |
|
||||
} |
|
||||
|
|
||||
// ScheduleChecks periodically runs health checks against an upstream host.
|
|
||||
func (h *HealthChecker) ScheduleChecks(url string) { |
|
||||
// check if a host is healthy on start vs waiting for timer
|
|
||||
h.upstream.SetHealthiness(h.IsHealthy(url)) |
|
||||
stop := make(chan bool) |
|
||||
h.StopChan = stop |
|
||||
|
|
||||
go func() { |
|
||||
for { |
|
||||
select { |
|
||||
case <-h.Ticker.C: |
|
||||
h.upstream.SetHealthiness(h.IsHealthy(url)) |
|
||||
case <-stop: |
|
||||
return |
|
||||
} |
|
||||
} |
|
||||
}() |
|
||||
} |
|
||||
|
|
||||
// Stop stops the healthchecker from makeing further requests.
|
|
||||
func (h *HealthChecker) Stop() { |
|
||||
h.Ticker.Stop() |
|
||||
close(h.StopChan) |
|
||||
} |
|
||||
|
|
||||
// IsHealthy attempts to check if a upstream host is healthy.
|
|
||||
func (h *HealthChecker) IsHealthy(url string) bool { |
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil) |
|
||||
if err != nil { |
|
||||
return false |
|
||||
} |
|
||||
|
|
||||
resp, err := h.HTTPClient.Do(req) |
|
||||
if err != nil { |
|
||||
return false |
|
||||
} |
|
||||
|
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 400 { |
|
||||
return false |
|
||||
} |
|
||||
|
|
||||
return true |
|
||||
} |
|
||||
|
|
||||
// NewHealthCheckWorker returns a new instance of a HealthChecker.
|
|
||||
func NewHealthCheckWorker(u Upstream, interval time.Duration, client *http.Client) *HealthChecker { |
|
||||
return &HealthChecker{ |
|
||||
upstream: u, |
|
||||
Ticker: time.NewTicker(interval), |
|
||||
HTTPClient: client, |
|
||||
} |
|
||||
} |
|
@ -0,0 +1,133 @@ |
|||||
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
|
//
|
||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
// you may not use this file except in compliance with the License.
|
||||
|
// You may obtain a copy of the License at
|
||||
|
//
|
||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
//
|
||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
// See the License for the specific language governing permissions and
|
||||
|
// limitations under the License.
|
||||
|
|
||||
|
package reverseproxy |
||||
|
|
||||
|
import ( |
||||
|
"net" |
||||
|
"net/http" |
||||
|
"time" |
||||
|
|
||||
|
"github.com/caddyserver/caddy/v2" |
||||
|
) |
||||
|
|
||||
|
func init() { |
||||
|
caddy.RegisterModule(HTTPTransport{}) |
||||
|
} |
||||
|
|
||||
|
// TODO: This is the default transport, basically just http.Transport, but we define JSON struct tags...
|
||||
|
type HTTPTransport struct { |
||||
|
// TODO: Actually this is where the TLS config should go, technically...
|
||||
|
// as well as keepalives and dial timeouts...
|
||||
|
// TODO: It's possible that other transports (like fastcgi) might be
|
||||
|
// able to borrow/use at least some of these config fields; if so,
|
||||
|
// move them into a type called CommonTransport and embed it
|
||||
|
|
||||
|
TLS *TLSConfig `json:"tls,omitempty"` |
||||
|
KeepAlive *KeepAlive `json:"keep_alive,omitempty"` |
||||
|
Compression *bool `json:"compression,omitempty"` |
||||
|
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
|
||||
|
DialTimeout caddy.Duration `json:"dial_timeout,omitempty"` |
||||
|
FallbackDelay caddy.Duration `json:"dial_fallback_delay,omitempty"` |
||||
|
ResponseHeaderTimeout caddy.Duration `json:"response_header_timeout,omitempty"` |
||||
|
ExpectContinueTimeout caddy.Duration `json:"expect_continue_timeout,omitempty"` |
||||
|
MaxResponseHeaderSize int64 `json:"max_response_header_size,omitempty"` |
||||
|
WriteBufferSize int `json:"write_buffer_size,omitempty"` |
||||
|
ReadBufferSize int `json:"read_buffer_size,omitempty"` |
||||
|
// TODO: ProxyConnectHeader?
|
||||
|
|
||||
|
RoundTripper http.RoundTripper `json:"-"` |
||||
|
} |
||||
|
|
||||
|
// CaddyModule returns the Caddy module information.
|
||||
|
func (HTTPTransport) CaddyModule() caddy.ModuleInfo { |
||||
|
return caddy.ModuleInfo{ |
||||
|
Name: "http.handlers.reverse_proxy.transport.http", |
||||
|
New: func() caddy.Module { return new(HTTPTransport) }, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (h *HTTPTransport) Provision(ctx caddy.Context) error { |
||||
|
dialer := &net.Dialer{ |
||||
|
Timeout: time.Duration(h.DialTimeout), |
||||
|
FallbackDelay: time.Duration(h.FallbackDelay), |
||||
|
// TODO: Resolver
|
||||
|
} |
||||
|
rt := &http.Transport{ |
||||
|
DialContext: dialer.DialContext, |
||||
|
MaxConnsPerHost: h.MaxConnsPerHost, |
||||
|
ResponseHeaderTimeout: time.Duration(h.ResponseHeaderTimeout), |
||||
|
ExpectContinueTimeout: time.Duration(h.ExpectContinueTimeout), |
||||
|
MaxResponseHeaderBytes: h.MaxResponseHeaderSize, |
||||
|
WriteBufferSize: h.WriteBufferSize, |
||||
|
ReadBufferSize: h.ReadBufferSize, |
||||
|
} |
||||
|
|
||||
|
if h.TLS != nil { |
||||
|
rt.TLSHandshakeTimeout = time.Duration(h.TLS.HandshakeTimeout) |
||||
|
// TODO: rest of TLS config
|
||||
|
} |
||||
|
|
||||
|
if h.KeepAlive != nil { |
||||
|
dialer.KeepAlive = time.Duration(h.KeepAlive.ProbeInterval) |
||||
|
|
||||
|
if enabled := h.KeepAlive.Enabled; enabled != nil { |
||||
|
rt.DisableKeepAlives = !*enabled |
||||
|
} |
||||
|
rt.MaxIdleConns = h.KeepAlive.MaxIdleConns |
||||
|
rt.MaxIdleConnsPerHost = h.KeepAlive.MaxIdleConnsPerHost |
||||
|
rt.IdleConnTimeout = time.Duration(h.KeepAlive.IdleConnTimeout) |
||||
|
} |
||||
|
|
||||
|
if h.Compression != nil { |
||||
|
rt.DisableCompression = !*h.Compression |
||||
|
} |
||||
|
|
||||
|
h.RoundTripper = rt |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (h HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) { |
||||
|
return h.RoundTripper.RoundTrip(req) |
||||
|
} |
||||
|
|
||||
|
type TLSConfig struct { |
||||
|
CAPool []string `json:"ca_pool,omitempty"` |
||||
|
ClientCertificate string `json:"client_certificate,omitempty"` |
||||
|
InsecureSkipVerify bool `json:"insecure_skip_verify,omitempty"` |
||||
|
HandshakeTimeout caddy.Duration `json:"handshake_timeout,omitempty"` |
||||
|
} |
||||
|
|
||||
|
type KeepAlive struct { |
||||
|
Enabled *bool `json:"enabled,omitempty"` |
||||
|
ProbeInterval caddy.Duration `json:"probe_interval,omitempty"` |
||||
|
MaxIdleConns int `json:"max_idle_conns,omitempty"` |
||||
|
MaxIdleConnsPerHost int `json:"max_idle_conns_per_host,omitempty"` |
||||
|
IdleConnTimeout caddy.Duration `json:"idle_timeout,omitempty"` // how long should connections be kept alive when idle
|
||||
|
} |
||||
|
|
||||
|
var ( |
||||
|
defaultDialer = net.Dialer{ |
||||
|
Timeout: 10 * time.Second, |
||||
|
KeepAlive: 30 * time.Second, |
||||
|
} |
||||
|
|
||||
|
// TODO: does this need to be configured to enable HTTP/2?
|
||||
|
defaultTransport = &http.Transport{ |
||||
|
DialContext: defaultDialer.DialContext, |
||||
|
TLSHandshakeTimeout: 5 * time.Second, |
||||
|
IdleConnTimeout: 2 * time.Minute, |
||||
|
} |
||||
|
) |
@ -1,53 +0,0 @@ |
|||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
||||
//
|
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||
// you may not use this file except in compliance with the License.
|
|
||||
// You may obtain a copy of the License at
|
|
||||
//
|
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||
//
|
|
||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||
// See the License for the specific language governing permissions and
|
|
||||
// limitations under the License.
|
|
||||
|
|
||||
package reverseproxy |
|
||||
|
|
||||
import ( |
|
||||
"github.com/caddyserver/caddy/v2" |
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" |
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp" |
|
||||
) |
|
||||
|
|
||||
func init() { |
|
||||
caddy.RegisterModule(new(LoadBalanced)) |
|
||||
httpcaddyfile.RegisterHandlerDirective("reverse_proxy", parseCaddyfile) // TODO: "proxy"?
|
|
||||
} |
|
||||
|
|
||||
// CaddyModule returns the Caddy module information.
|
|
||||
func (*LoadBalanced) CaddyModule() caddy.ModuleInfo { |
|
||||
return caddy.ModuleInfo{ |
|
||||
Name: "http.handlers.reverse_proxy", |
|
||||
New: func() caddy.Module { return new(LoadBalanced) }, |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// parseCaddyfile sets up the handler from Caddyfile tokens. Syntax:
|
|
||||
//
|
|
||||
// proxy [<matcher>] <to>
|
|
||||
//
|
|
||||
// TODO: This needs to be finished. It definitely needs to be able to open a block...
|
|
||||
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { |
|
||||
lb := new(LoadBalanced) |
|
||||
for h.Next() { |
|
||||
allTo := h.RemainingArgs() |
|
||||
if len(allTo) == 0 { |
|
||||
return nil, h.ArgErr() |
|
||||
} |
|
||||
for _, to := range allTo { |
|
||||
lb.Upstreams = append(lb.Upstreams, &UpstreamConfig{Host: to}) |
|
||||
} |
|
||||
} |
|
||||
return lb, nil |
|
||||
} |
|
File diff suppressed because it is too large
@ -0,0 +1,351 @@ |
|||||
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
|
//
|
||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
// you may not use this file except in compliance with the License.
|
||||
|
// You may obtain a copy of the License at
|
||||
|
//
|
||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
//
|
||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
// See the License for the specific language governing permissions and
|
||||
|
// limitations under the License.
|
||||
|
|
||||
|
package reverseproxy |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"hash/fnv" |
||||
|
weakrand "math/rand" |
||||
|
"net" |
||||
|
"net/http" |
||||
|
"sync/atomic" |
||||
|
"time" |
||||
|
|
||||
|
"github.com/caddyserver/caddy/v2" |
||||
|
) |
||||
|
|
||||
|
func init() { |
||||
|
caddy.RegisterModule(RandomSelection{}) |
||||
|
caddy.RegisterModule(RandomChoiceSelection{}) |
||||
|
caddy.RegisterModule(LeastConnSelection{}) |
||||
|
caddy.RegisterModule(RoundRobinSelection{}) |
||||
|
caddy.RegisterModule(FirstSelection{}) |
||||
|
caddy.RegisterModule(IPHashSelection{}) |
||||
|
caddy.RegisterModule(URIHashSelection{}) |
||||
|
caddy.RegisterModule(HeaderHashSelection{}) |
||||
|
|
||||
|
weakrand.Seed(time.Now().UTC().UnixNano()) |
||||
|
} |
||||
|
|
||||
|
// RandomSelection is a policy that selects
|
||||
|
// an available host at random.
|
||||
|
type RandomSelection struct{} |
||||
|
|
||||
|
// CaddyModule returns the Caddy module information.
|
||||
|
func (RandomSelection) CaddyModule() caddy.ModuleInfo { |
||||
|
return caddy.ModuleInfo{ |
||||
|
Name: "http.handlers.reverse_proxy.selection_policies.random", |
||||
|
New: func() caddy.Module { return new(RandomSelection) }, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Select returns an available host, if any.
|
||||
|
func (r RandomSelection) Select(pool HostPool, request *http.Request) *Upstream { |
||||
|
// use reservoir sampling because the number of available
|
||||
|
// hosts isn't known: https://en.wikipedia.org/wiki/Reservoir_sampling
|
||||
|
var randomHost *Upstream |
||||
|
var count int |
||||
|
for _, upstream := range pool { |
||||
|
if !upstream.Available() { |
||||
|
continue |
||||
|
} |
||||
|
// (n % 1 == 0) holds for all n, therefore a
|
||||
|
// upstream will always be chosen if there is at
|
||||
|
// least one available
|
||||
|
count++ |
||||
|
if (weakrand.Int() % count) == 0 { |
||||
|
randomHost = upstream |
||||
|
} |
||||
|
} |
||||
|
return randomHost |
||||
|
} |
||||
|
|
||||
|
// RandomChoiceSelection is a policy that selects
|
||||
|
// two or more available hosts at random, then
|
||||
|
// chooses the one with the least load.
|
||||
|
type RandomChoiceSelection struct { |
||||
|
Choose int `json:"choose,omitempty"` |
||||
|
} |
||||
|
|
||||
|
// CaddyModule returns the Caddy module information.
|
||||
|
func (RandomChoiceSelection) CaddyModule() caddy.ModuleInfo { |
||||
|
return caddy.ModuleInfo{ |
||||
|
Name: "http.handlers.reverse_proxy.selection_policies.random_choice", |
||||
|
New: func() caddy.Module { return new(RandomChoiceSelection) }, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (r *RandomChoiceSelection) Provision(ctx caddy.Context) error { |
||||
|
if r.Choose == 0 { |
||||
|
r.Choose = 2 |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (r RandomChoiceSelection) Validate() error { |
||||
|
if r.Choose < 2 { |
||||
|
return fmt.Errorf("choose must be at least 2") |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// Select returns an available host, if any.
|
||||
|
func (r RandomChoiceSelection) Select(pool HostPool, _ *http.Request) *Upstream { |
||||
|
k := r.Choose |
||||
|
if k > len(pool) { |
||||
|
k = len(pool) |
||||
|
} |
||||
|
choices := make([]*Upstream, k) |
||||
|
for i, upstream := range pool { |
||||
|
if !upstream.Available() { |
||||
|
continue |
||||
|
} |
||||
|
j := weakrand.Intn(i) |
||||
|
if j < k { |
||||
|
choices[j] = upstream |
||||
|
} |
||||
|
} |
||||
|
return leastRequests(choices) |
||||
|
} |
||||
|
|
||||
|
// LeastConnSelection is a policy that selects the
|
||||
|
// host with the least active requests. If multiple
|
||||
|
// hosts have the same fewest number, one is chosen
|
||||
|
// randomly. The term "conn" or "connection" is used
|
||||
|
// in this policy name due to its similar meaning in
|
||||
|
// other software, but our load balancer actually
|
||||
|
// counts active requests rather than connections,
|
||||
|
// since these days requests are multiplexed onto
|
||||
|
// shared connections.
|
||||
|
type LeastConnSelection struct{} |
||||
|
|
||||
|
// CaddyModule returns the Caddy module information.
|
||||
|
func (LeastConnSelection) CaddyModule() caddy.ModuleInfo { |
||||
|
return caddy.ModuleInfo{ |
||||
|
Name: "http.handlers.reverse_proxy.selection_policies.least_conn", |
||||
|
New: func() caddy.Module { return new(LeastConnSelection) }, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Select selects the up host with the least number of connections in the
|
||||
|
// pool. If more than one host has the same least number of connections,
|
||||
|
// one of the hosts is chosen at random.
|
||||
|
func (LeastConnSelection) Select(pool HostPool, _ *http.Request) *Upstream { |
||||
|
var bestHost *Upstream |
||||
|
var count int |
||||
|
var leastReqs int |
||||
|
|
||||
|
for _, host := range pool { |
||||
|
if !host.Available() { |
||||
|
continue |
||||
|
} |
||||
|
numReqs := host.NumRequests() |
||||
|
if numReqs < leastReqs { |
||||
|
leastReqs = numReqs |
||||
|
count = 0 |
||||
|
} |
||||
|
|
||||
|
// among hosts with same least connections, perform a reservoir
|
||||
|
// sample: https://en.wikipedia.org/wiki/Reservoir_sampling
|
||||
|
if numReqs == leastReqs { |
||||
|
count++ |
||||
|
if (weakrand.Int() % count) == 0 { |
||||
|
bestHost = host |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return bestHost |
||||
|
} |
||||
|
|
||||
|
// RoundRobinSelection is a policy that selects
|
||||
|
// a host based on round-robin ordering.
|
||||
|
type RoundRobinSelection struct { |
||||
|
robin uint32 |
||||
|
} |
||||
|
|
||||
|
// CaddyModule returns the Caddy module information.
|
||||
|
func (RoundRobinSelection) CaddyModule() caddy.ModuleInfo { |
||||
|
return caddy.ModuleInfo{ |
||||
|
Name: "http.handlers.reverse_proxy.selection_policies.round_robin", |
||||
|
New: func() caddy.Module { return new(RoundRobinSelection) }, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Select returns an available host, if any.
|
||||
|
func (r *RoundRobinSelection) Select(pool HostPool, _ *http.Request) *Upstream { |
||||
|
n := uint32(len(pool)) |
||||
|
if n == 0 { |
||||
|
return nil |
||||
|
} |
||||
|
for i := uint32(0); i < n; i++ { |
||||
|
atomic.AddUint32(&r.robin, 1) |
||||
|
host := pool[r.robin%n] |
||||
|
if host.Available() { |
||||
|
return host |
||||
|
} |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// FirstSelection is a policy that selects
|
||||
|
// the first available host.
|
||||
|
type FirstSelection struct{} |
||||
|
|
||||
|
// CaddyModule returns the Caddy module information.
|
||||
|
func (FirstSelection) CaddyModule() caddy.ModuleInfo { |
||||
|
return caddy.ModuleInfo{ |
||||
|
Name: "http.handlers.reverse_proxy.selection_policies.first", |
||||
|
New: func() caddy.Module { return new(FirstSelection) }, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Select returns an available host, if any.
|
||||
|
func (FirstSelection) Select(pool HostPool, _ *http.Request) *Upstream { |
||||
|
for _, host := range pool { |
||||
|
if host.Available() { |
||||
|
return host |
||||
|
} |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// IPHashSelection is a policy that selects a host
|
||||
|
// based on hashing the remote IP of the request.
|
||||
|
type IPHashSelection struct{} |
||||
|
|
||||
|
// CaddyModule returns the Caddy module information.
|
||||
|
func (IPHashSelection) CaddyModule() caddy.ModuleInfo { |
||||
|
return caddy.ModuleInfo{ |
||||
|
Name: "http.handlers.reverse_proxy.selection_policies.ip_hash", |
||||
|
New: func() caddy.Module { return new(IPHashSelection) }, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Select returns an available host, if any.
|
||||
|
func (IPHashSelection) Select(pool HostPool, req *http.Request) *Upstream { |
||||
|
clientIP, _, err := net.SplitHostPort(req.RemoteAddr) |
||||
|
if err != nil { |
||||
|
clientIP = req.RemoteAddr |
||||
|
} |
||||
|
return hostByHashing(pool, clientIP) |
||||
|
} |
||||
|
|
||||
|
// URIHashSelection is a policy that selects a
|
||||
|
// host by hashing the request URI.
|
||||
|
type URIHashSelection struct{} |
||||
|
|
||||
|
// CaddyModule returns the Caddy module information.
|
||||
|
func (URIHashSelection) CaddyModule() caddy.ModuleInfo { |
||||
|
return caddy.ModuleInfo{ |
||||
|
Name: "http.handlers.reverse_proxy.selection_policies.uri_hash", |
||||
|
New: func() caddy.Module { return new(URIHashSelection) }, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Select returns an available host, if any.
|
||||
|
func (URIHashSelection) Select(pool HostPool, req *http.Request) *Upstream { |
||||
|
return hostByHashing(pool, req.RequestURI) |
||||
|
} |
||||
|
|
||||
|
// HeaderHashSelection is a policy that selects
|
||||
|
// a host based on a given request header.
|
||||
|
type HeaderHashSelection struct { |
||||
|
Field string `json:"field,omitempty"` |
||||
|
} |
||||
|
|
||||
|
// CaddyModule returns the Caddy module information.
|
||||
|
func (HeaderHashSelection) CaddyModule() caddy.ModuleInfo { |
||||
|
return caddy.ModuleInfo{ |
||||
|
Name: "http.handlers.reverse_proxy.selection_policies.header", |
||||
|
New: func() caddy.Module { return new(HeaderHashSelection) }, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Select returns an available host, if any.
|
||||
|
func (s HeaderHashSelection) Select(pool HostPool, req *http.Request) *Upstream { |
||||
|
if s.Field == "" { |
||||
|
return nil |
||||
|
} |
||||
|
val := req.Header.Get(s.Field) |
||||
|
if val == "" { |
||||
|
return RandomSelection{}.Select(pool, req) |
||||
|
} |
||||
|
return hostByHashing(pool, val) |
||||
|
} |
||||
|
|
||||
|
// leastRequests returns the host with the
|
||||
|
// least number of active requests to it.
|
||||
|
// If more than one host has the same
|
||||
|
// least number of active requests, then
|
||||
|
// one of those is chosen at random.
|
||||
|
func leastRequests(upstreams []*Upstream) *Upstream { |
||||
|
if len(upstreams) == 0 { |
||||
|
return nil |
||||
|
} |
||||
|
var best []*Upstream |
||||
|
var bestReqs int |
||||
|
for _, upstream := range upstreams { |
||||
|
reqs := upstream.NumRequests() |
||||
|
if reqs == 0 { |
||||
|
return upstream |
||||
|
} |
||||
|
if reqs <= bestReqs { |
||||
|
bestReqs = reqs |
||||
|
best = append(best, upstream) |
||||
|
} |
||||
|
} |
||||
|
return best[weakrand.Intn(len(best))] |
||||
|
} |
||||
|
|
||||
|
// hostByHashing returns an available host
|
||||
|
// from pool based on a hashable string s.
|
||||
|
func hostByHashing(pool []*Upstream, s string) *Upstream { |
||||
|
poolLen := uint32(len(pool)) |
||||
|
if poolLen == 0 { |
||||
|
return nil |
||||
|
} |
||||
|
index := hash(s) % poolLen |
||||
|
for i := uint32(0); i < poolLen; i++ { |
||||
|
index += i |
||||
|
upstream := pool[index%poolLen] |
||||
|
if upstream.Available() { |
||||
|
return upstream |
||||
|
} |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// hash calculates a fast hash based on s.
|
||||
|
func hash(s string) uint32 { |
||||
|
h := fnv.New32a() |
||||
|
h.Write([]byte(s)) |
||||
|
return h.Sum32() |
||||
|
} |
||||
|
|
||||
|
// Interface guards
|
||||
|
var ( |
||||
|
_ Selector = (*RandomSelection)(nil) |
||||
|
_ Selector = (*RandomChoiceSelection)(nil) |
||||
|
_ Selector = (*LeastConnSelection)(nil) |
||||
|
_ Selector = (*RoundRobinSelection)(nil) |
||||
|
_ Selector = (*FirstSelection)(nil) |
||||
|
_ Selector = (*IPHashSelection)(nil) |
||||
|
_ Selector = (*URIHashSelection)(nil) |
||||
|
_ Selector = (*HeaderHashSelection)(nil) |
||||
|
|
||||
|
_ caddy.Validator = (*RandomChoiceSelection)(nil) |
||||
|
_ caddy.Provisioner = (*RandomChoiceSelection)(nil) |
||||
|
) |
@ -0,0 +1,363 @@ |
|||||
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
|
//
|
||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
// you may not use this file except in compliance with the License.
|
||||
|
// You may obtain a copy of the License at
|
||||
|
//
|
||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
//
|
||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
// See the License for the specific language governing permissions and
|
||||
|
// limitations under the License.
|
||||
|
|
||||
|
package reverseproxy |
||||
|
|
||||
|
// TODO: finish migrating these
|
||||
|
|
||||
|
// import (
|
||||
|
// "net/http"
|
||||
|
// "net/http/httptest"
|
||||
|
// "os"
|
||||
|
// "testing"
|
||||
|
// )
|
||||
|
|
||||
|
// var workableServer *httptest.Server
|
||||
|
|
||||
|
// func TestMain(m *testing.M) {
|
||||
|
// workableServer = httptest.NewServer(http.HandlerFunc(
|
||||
|
// func(w http.ResponseWriter, r *http.Request) {
|
||||
|
// // do nothing
|
||||
|
// }))
|
||||
|
// r := m.Run()
|
||||
|
// workableServer.Close()
|
||||
|
// os.Exit(r)
|
||||
|
// }
|
||||
|
|
||||
|
// type customPolicy struct{}
|
||||
|
|
||||
|
// func (customPolicy) Select(pool HostPool, _ *http.Request) Host {
|
||||
|
// return pool[0]
|
||||
|
// }
|
||||
|
|
||||
|
// func testPool() HostPool {
|
||||
|
// pool := []*UpstreamHost{
|
||||
|
// {
|
||||
|
// Name: workableServer.URL, // this should resolve (healthcheck test)
|
||||
|
// },
|
||||
|
// {
|
||||
|
// Name: "http://localhost:99998", // this shouldn't
|
||||
|
// },
|
||||
|
// {
|
||||
|
// Name: "http://C",
|
||||
|
// },
|
||||
|
// }
|
||||
|
// return HostPool(pool)
|
||||
|
// }
|
||||
|
|
||||
|
// func TestRoundRobinPolicy(t *testing.T) {
|
||||
|
// pool := testPool()
|
||||
|
// rrPolicy := &RoundRobin{}
|
||||
|
// request, _ := http.NewRequest("GET", "/", nil)
|
||||
|
|
||||
|
// h := rrPolicy.Select(pool, request)
|
||||
|
// // First selected host is 1, because counter starts at 0
|
||||
|
// // and increments before host is selected
|
||||
|
// if h != pool[1] {
|
||||
|
// t.Error("Expected first round robin host to be second host in the pool.")
|
||||
|
// }
|
||||
|
// h = rrPolicy.Select(pool, request)
|
||||
|
// if h != pool[2] {
|
||||
|
// t.Error("Expected second round robin host to be third host in the pool.")
|
||||
|
// }
|
||||
|
// h = rrPolicy.Select(pool, request)
|
||||
|
// if h != pool[0] {
|
||||
|
// t.Error("Expected third round robin host to be first host in the pool.")
|
||||
|
// }
|
||||
|
// // mark host as down
|
||||
|
// pool[1].Unhealthy = 1
|
||||
|
// h = rrPolicy.Select(pool, request)
|
||||
|
// if h != pool[2] {
|
||||
|
// t.Error("Expected to skip down host.")
|
||||
|
// }
|
||||
|
// // mark host as up
|
||||
|
// pool[1].Unhealthy = 0
|
||||
|
|
||||
|
// h = rrPolicy.Select(pool, request)
|
||||
|
// if h == pool[2] {
|
||||
|
// t.Error("Expected to balance evenly among healthy hosts")
|
||||
|
// }
|
||||
|
// // mark host as full
|
||||
|
// pool[1].Conns = 1
|
||||
|
// pool[1].MaxConns = 1
|
||||
|
// h = rrPolicy.Select(pool, request)
|
||||
|
// if h != pool[2] {
|
||||
|
// t.Error("Expected to skip full host.")
|
||||
|
// }
|
||||
|
// }
|
||||
|
|
||||
|
// func TestLeastConnPolicy(t *testing.T) {
|
||||
|
// pool := testPool()
|
||||
|
// lcPolicy := &LeastConn{}
|
||||
|
// request, _ := http.NewRequest("GET", "/", nil)
|
||||
|
|
||||
|
// pool[0].Conns = 10
|
||||
|
// pool[1].Conns = 10
|
||||
|
// h := lcPolicy.Select(pool, request)
|
||||
|
// if h != pool[2] {
|
||||
|
// t.Error("Expected least connection host to be third host.")
|
||||
|
// }
|
||||
|
// pool[2].Conns = 100
|
||||
|
// h = lcPolicy.Select(pool, request)
|
||||
|
// if h != pool[0] && h != pool[1] {
|
||||
|
// t.Error("Expected least connection host to be first or second host.")
|
||||
|
// }
|
||||
|
// }
|
||||
|
|
||||
|
// func TestCustomPolicy(t *testing.T) {
|
||||
|
// pool := testPool()
|
||||
|
// customPolicy := &customPolicy{}
|
||||
|
// request, _ := http.NewRequest("GET", "/", nil)
|
||||
|
|
||||
|
// h := customPolicy.Select(pool, request)
|
||||
|
// if h != pool[0] {
|
||||
|
// t.Error("Expected custom policy host to be the first host.")
|
||||
|
// }
|
||||
|
// }
|
||||
|
|
||||
|
// func TestIPHashPolicy(t *testing.T) {
|
||||
|
// pool := testPool()
|
||||
|
// ipHash := &IPHash{}
|
||||
|
// request, _ := http.NewRequest("GET", "/", nil)
|
||||
|
// // We should be able to predict where every request is routed.
|
||||
|
// request.RemoteAddr = "172.0.0.1:80"
|
||||
|
// h := ipHash.Select(pool, request)
|
||||
|
// if h != pool[1] {
|
||||
|
// t.Error("Expected ip hash policy host to be the second host.")
|
||||
|
// }
|
||||
|
// request.RemoteAddr = "172.0.0.2:80"
|
||||
|
// h = ipHash.Select(pool, request)
|
||||
|
// if h != pool[1] {
|
||||
|
// t.Error("Expected ip hash policy host to be the second host.")
|
||||
|
// }
|
||||
|
// request.RemoteAddr = "172.0.0.3:80"
|
||||
|
// h = ipHash.Select(pool, request)
|
||||
|
// if h != pool[2] {
|
||||
|
// t.Error("Expected ip hash policy host to be the third host.")
|
||||
|
// }
|
||||
|
// request.RemoteAddr = "172.0.0.4:80"
|
||||
|
// h = ipHash.Select(pool, request)
|
||||
|
// if h != pool[1] {
|
||||
|
// t.Error("Expected ip hash policy host to be the second host.")
|
||||
|
// }
|
||||
|
|
||||
|
// // we should get the same results without a port
|
||||
|
// request.RemoteAddr = "172.0.0.1"
|
||||
|
// h = ipHash.Select(pool, request)
|
||||
|
// if h != pool[1] {
|
||||
|
// t.Error("Expected ip hash policy host to be the second host.")
|
||||
|
// }
|
||||
|
// request.RemoteAddr = "172.0.0.2"
|
||||
|
// h = ipHash.Select(pool, request)
|
||||
|
// if h != pool[1] {
|
||||
|
// t.Error("Expected ip hash policy host to be the second host.")
|
||||
|
// }
|
||||
|
// request.RemoteAddr = "172.0.0.3"
|
||||
|
// h = ipHash.Select(pool, request)
|
||||
|
// if h != pool[2] {
|
||||
|
// t.Error("Expected ip hash policy host to be the third host.")
|
||||
|
// }
|
||||
|
// request.RemoteAddr = "172.0.0.4"
|
||||
|
// h = ipHash.Select(pool, request)
|
||||
|
// if h != pool[1] {
|
||||
|
// t.Error("Expected ip hash policy host to be the second host.")
|
||||
|
// }
|
||||
|
|
||||
|
// // we should get a healthy host if the original host is unhealthy and a
|
||||
|
// // healthy host is available
|
||||
|
// request.RemoteAddr = "172.0.0.1"
|
||||
|
// pool[1].Unhealthy = 1
|
||||
|
// h = ipHash.Select(pool, request)
|
||||
|
// if h != pool[2] {
|
||||
|
// t.Error("Expected ip hash policy host to be the third host.")
|
||||
|
// }
|
||||
|
|
||||
|
// request.RemoteAddr = "172.0.0.2"
|
||||
|
// h = ipHash.Select(pool, request)
|
||||
|
// if h != pool[2] {
|
||||
|
// t.Error("Expected ip hash policy host to be the third host.")
|
||||
|
// }
|
||||
|
// pool[1].Unhealthy = 0
|
||||
|
|
||||
|
// request.RemoteAddr = "172.0.0.3"
|
||||
|
// pool[2].Unhealthy = 1
|
||||
|
// h = ipHash.Select(pool, request)
|
||||
|
// if h != pool[0] {
|
||||
|
// t.Error("Expected ip hash policy host to be the first host.")
|
||||
|
// }
|
||||
|
// request.RemoteAddr = "172.0.0.4"
|
||||
|
// h = ipHash.Select(pool, request)
|
||||
|
// if h != pool[1] {
|
||||
|
// t.Error("Expected ip hash policy host to be the second host.")
|
||||
|
// }
|
||||
|
|
||||
|
// // We should be able to resize the host pool and still be able to predict
|
||||
|
// // where a request will be routed with the same IP's used above
|
||||
|
// pool = []*UpstreamHost{
|
||||
|
// {
|
||||
|
// Name: workableServer.URL, // this should resolve (healthcheck test)
|
||||
|
// },
|
||||
|
// {
|
||||
|
// Name: "http://localhost:99998", // this shouldn't
|
||||
|
// },
|
||||
|
// }
|
||||
|
// pool = HostPool(pool)
|
||||
|
// request.RemoteAddr = "172.0.0.1:80"
|
||||
|
// h = ipHash.Select(pool, request)
|
||||
|
// if h != pool[0] {
|
||||
|
// t.Error("Expected ip hash policy host to be the first host.")
|
||||
|
// }
|
||||
|
// request.RemoteAddr = "172.0.0.2:80"
|
||||
|
// h = ipHash.Select(pool, request)
|
||||
|
// if h != pool[1] {
|
||||
|
// t.Error("Expected ip hash policy host to be the second host.")
|
||||
|
// }
|
||||
|
// request.RemoteAddr = "172.0.0.3:80"
|
||||
|
// h = ipHash.Select(pool, request)
|
||||
|
// if h != pool[0] {
|
||||
|
// t.Error("Expected ip hash policy host to be the first host.")
|
||||
|
// }
|
||||
|
// request.RemoteAddr = "172.0.0.4:80"
|
||||
|
// h = ipHash.Select(pool, request)
|
||||
|
// if h != pool[1] {
|
||||
|
// t.Error("Expected ip hash policy host to be the second host.")
|
||||
|
// }
|
||||
|
|
||||
|
// // We should get nil when there are no healthy hosts
|
||||
|
// pool[0].Unhealthy = 1
|
||||
|
// pool[1].Unhealthy = 1
|
||||
|
// h = ipHash.Select(pool, request)
|
||||
|
// if h != nil {
|
||||
|
// t.Error("Expected ip hash policy host to be nil.")
|
||||
|
// }
|
||||
|
// }
|
||||
|
|
||||
|
// func TestFirstPolicy(t *testing.T) {
|
||||
|
// pool := testPool()
|
||||
|
// firstPolicy := &First{}
|
||||
|
// req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
|
|
||||
|
// h := firstPolicy.Select(pool, req)
|
||||
|
// if h != pool[0] {
|
||||
|
// t.Error("Expected first policy host to be the first host.")
|
||||
|
// }
|
||||
|
|
||||
|
// pool[0].Unhealthy = 1
|
||||
|
// h = firstPolicy.Select(pool, req)
|
||||
|
// if h != pool[1] {
|
||||
|
// t.Error("Expected first policy host to be the second host.")
|
||||
|
// }
|
||||
|
// }
|
||||
|
|
||||
|
// func TestUriPolicy(t *testing.T) {
|
||||
|
// pool := testPool()
|
||||
|
// uriPolicy := &URIHash{}
|
||||
|
|
||||
|
// request := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
|
// h := uriPolicy.Select(pool, request)
|
||||
|
// if h != pool[0] {
|
||||
|
// t.Error("Expected uri policy host to be the first host.")
|
||||
|
// }
|
||||
|
|
||||
|
// pool[0].Unhealthy = 1
|
||||
|
// h = uriPolicy.Select(pool, request)
|
||||
|
// if h != pool[1] {
|
||||
|
// t.Error("Expected uri policy host to be the first host.")
|
||||
|
// }
|
||||
|
|
||||
|
// request = httptest.NewRequest(http.MethodGet, "/test_2", nil)
|
||||
|
// h = uriPolicy.Select(pool, request)
|
||||
|
// if h != pool[1] {
|
||||
|
// t.Error("Expected uri policy host to be the second host.")
|
||||
|
// }
|
||||
|
|
||||
|
// // We should be able to resize the host pool and still be able to predict
|
||||
|
// // where a request will be routed with the same URI's used above
|
||||
|
// pool = []*UpstreamHost{
|
||||
|
// {
|
||||
|
// Name: workableServer.URL, // this should resolve (healthcheck test)
|
||||
|
// },
|
||||
|
// {
|
||||
|
// Name: "http://localhost:99998", // this shouldn't
|
||||
|
// },
|
||||
|
// }
|
||||
|
|
||||
|
// request = httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
|
// h = uriPolicy.Select(pool, request)
|
||||
|
// if h != pool[0] {
|
||||
|
// t.Error("Expected uri policy host to be the first host.")
|
||||
|
// }
|
||||
|
|
||||
|
// pool[0].Unhealthy = 1
|
||||
|
// h = uriPolicy.Select(pool, request)
|
||||
|
// if h != pool[1] {
|
||||
|
// t.Error("Expected uri policy host to be the first host.")
|
||||
|
// }
|
||||
|
|
||||
|
// request = httptest.NewRequest(http.MethodGet, "/test_2", nil)
|
||||
|
// h = uriPolicy.Select(pool, request)
|
||||
|
// if h != pool[1] {
|
||||
|
// t.Error("Expected uri policy host to be the second host.")
|
||||
|
// }
|
||||
|
|
||||
|
// pool[0].Unhealthy = 1
|
||||
|
// pool[1].Unhealthy = 1
|
||||
|
// h = uriPolicy.Select(pool, request)
|
||||
|
// if h != nil {
|
||||
|
// t.Error("Expected uri policy policy host to be nil.")
|
||||
|
// }
|
||||
|
// }
|
||||
|
|
||||
|
// func TestHeaderPolicy(t *testing.T) {
|
||||
|
// pool := testPool()
|
||||
|
// tests := []struct {
|
||||
|
// Name string
|
||||
|
// Policy *Header
|
||||
|
// RequestHeaderName string
|
||||
|
// RequestHeaderValue string
|
||||
|
// NilHost bool
|
||||
|
// HostIndex int
|
||||
|
// }{
|
||||
|
// {"empty config", &Header{""}, "", "", true, 0},
|
||||
|
// {"empty config+header+value", &Header{""}, "Affinity", "somevalue", true, 0},
|
||||
|
// {"empty config+header", &Header{""}, "Affinity", "", true, 0},
|
||||
|
|
||||
|
// {"no header(fallback to roundrobin)", &Header{"Affinity"}, "", "", false, 1},
|
||||
|
// {"no header(fallback to roundrobin)", &Header{"Affinity"}, "", "", false, 2},
|
||||
|
// {"no header(fallback to roundrobin)", &Header{"Affinity"}, "", "", false, 0},
|
||||
|
|
||||
|
// {"hash route to host", &Header{"Affinity"}, "Affinity", "somevalue", false, 1},
|
||||
|
// {"hash route to host", &Header{"Affinity"}, "Affinity", "somevalue2", false, 0},
|
||||
|
// {"hash route to host", &Header{"Affinity"}, "Affinity", "somevalue3", false, 2},
|
||||
|
// {"hash route with empty value", &Header{"Affinity"}, "Affinity", "", false, 1},
|
||||
|
// }
|
||||
|
|
||||
|
// for idx, test := range tests {
|
||||
|
// request, _ := http.NewRequest("GET", "/", nil)
|
||||
|
// if test.RequestHeaderName != "" {
|
||||
|
// request.Header.Add(test.RequestHeaderName, test.RequestHeaderValue)
|
||||
|
// }
|
||||
|
|
||||
|
// host := test.Policy.Select(pool, request)
|
||||
|
// if test.NilHost && host != nil {
|
||||
|
// t.Errorf("%d: Expected host to be nil", idx)
|
||||
|
// }
|
||||
|
// if !test.NilHost && host == nil {
|
||||
|
// t.Errorf("%d: Did not expect host to be nil", idx)
|
||||
|
// }
|
||||
|
// if !test.NilHost && host != pool[test.HostIndex] {
|
||||
|
// t.Errorf("%d: Expected Header policy to be host %d", idx, test.HostIndex)
|
||||
|
// }
|
||||
|
// }
|
||||
|
// }
|
@ -1,450 +0,0 @@ |
|||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
||||
//
|
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||
// you may not use this file except in compliance with the License.
|
|
||||
// You may obtain a copy of the License at
|
|
||||
//
|
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||
//
|
|
||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||
// See the License for the specific language governing permissions and
|
|
||||
// limitations under the License.
|
|
||||
|
|
||||
// Package reverseproxy implements a load-balanced reverse proxy.
|
|
||||
package reverseproxy |
|
||||
|
|
||||
import ( |
|
||||
"context" |
|
||||
"encoding/json" |
|
||||
"fmt" |
|
||||
"math/rand" |
|
||||
"net" |
|
||||
"net/http" |
|
||||
"net/url" |
|
||||
"strings" |
|
||||
"sync" |
|
||||
"sync/atomic" |
|
||||
"time" |
|
||||
|
|
||||
"github.com/caddyserver/caddy/v2" |
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp" |
|
||||
) |
|
||||
|
|
||||
// CircuitBreaker defines the functionality of a circuit breaker module.
|
|
||||
type CircuitBreaker interface { |
|
||||
Ok() bool |
|
||||
RecordMetric(statusCode int, latency time.Duration) |
|
||||
} |
|
||||
|
|
||||
type noopCircuitBreaker struct{} |
|
||||
|
|
||||
func (ncb noopCircuitBreaker) RecordMetric(statusCode int, latency time.Duration) {} |
|
||||
func (ncb noopCircuitBreaker) Ok() bool { |
|
||||
return true |
|
||||
} |
|
||||
|
|
||||
const ( |
|
||||
// TypeBalanceRoundRobin represents the value to use for configuring a load balanced reverse proxy to use round robin load balancing.
|
|
||||
TypeBalanceRoundRobin = iota |
|
||||
|
|
||||
// TypeBalanceRandom represents the value to use for configuring a load balanced reverse proxy to use random load balancing.
|
|
||||
TypeBalanceRandom |
|
||||
|
|
||||
// TODO: add random with two choices
|
|
||||
|
|
||||
// msgNoHealthyUpstreams is returned if there are no upstreams that are healthy to proxy a request to
|
|
||||
msgNoHealthyUpstreams = "No healthy upstreams." |
|
||||
|
|
||||
// by default perform health checks every 30 seconds
|
|
||||
defaultHealthCheckDur = time.Second * 30 |
|
||||
|
|
||||
// used when an upstream is unhealthy, health checks can be configured to perform at a faster rate
|
|
||||
defaultFastHealthCheckDur = time.Second * 1 |
|
||||
) |
|
||||
|
|
||||
var ( |
|
||||
// defaultTransport is the default transport to use for the reverse proxy.
|
|
||||
defaultTransport = &http.Transport{ |
|
||||
Dial: (&net.Dialer{ |
|
||||
Timeout: 5 * time.Second, |
|
||||
}).Dial, |
|
||||
TLSHandshakeTimeout: 5 * time.Second, |
|
||||
} |
|
||||
|
|
||||
// defaultHTTPClient is the default http client to use for the healthchecker.
|
|
||||
defaultHTTPClient = &http.Client{ |
|
||||
Timeout: time.Second * 10, |
|
||||
Transport: defaultTransport, |
|
||||
} |
|
||||
|
|
||||
// typeMap maps caddy load balance configuration to the internal representation of the loadbalance algorithm type.
|
|
||||
typeMap = map[string]int{ |
|
||||
"round_robin": TypeBalanceRoundRobin, |
|
||||
"random": TypeBalanceRandom, |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
// NewLoadBalancedReverseProxy returns a collection of Upstreams that are to be loadbalanced.
|
|
||||
func NewLoadBalancedReverseProxy(lb *LoadBalanced, ctx caddy.Context) error { |
|
||||
// set defaults
|
|
||||
if lb.NoHealthyUpstreamsMessage == "" { |
|
||||
lb.NoHealthyUpstreamsMessage = msgNoHealthyUpstreams |
|
||||
} |
|
||||
|
|
||||
if lb.TryInterval == "" { |
|
||||
lb.TryInterval = "20s" |
|
||||
} |
|
||||
|
|
||||
// set request retry interval
|
|
||||
ti, err := time.ParseDuration(lb.TryInterval) |
|
||||
if err != nil { |
|
||||
return fmt.Errorf("NewLoadBalancedReverseProxy: %v", err.Error()) |
|
||||
} |
|
||||
lb.tryInterval = ti |
|
||||
|
|
||||
// set load balance algorithm
|
|
||||
t, ok := typeMap[lb.LoadBalanceType] |
|
||||
if !ok { |
|
||||
t = TypeBalanceRandom |
|
||||
} |
|
||||
lb.loadBalanceType = t |
|
||||
|
|
||||
// setup each upstream
|
|
||||
var us []*upstream |
|
||||
for _, uc := range lb.Upstreams { |
|
||||
// pass the upstream decr and incr methods to keep track of unhealthy nodes
|
|
||||
nu, err := newUpstream(uc, lb.decrUnhealthy, lb.incrUnhealthy) |
|
||||
if err != nil { |
|
||||
return err |
|
||||
} |
|
||||
|
|
||||
// setup any configured circuit breakers
|
|
||||
var cbModule = "http.handlers.reverse_proxy.circuit_breaker" |
|
||||
var cb CircuitBreaker |
|
||||
|
|
||||
if uc.CircuitBreaker != nil { |
|
||||
if _, err := caddy.GetModule(cbModule); err == nil { |
|
||||
val, err := ctx.LoadModule(cbModule, uc.CircuitBreaker) |
|
||||
if err == nil { |
|
||||
cbv, ok := val.(CircuitBreaker) |
|
||||
if ok { |
|
||||
cb = cbv |
|
||||
} else { |
|
||||
fmt.Printf("\nerr: %v; cannot load circuit_breaker, using noop", err.Error()) |
|
||||
cb = noopCircuitBreaker{} |
|
||||
} |
|
||||
} else { |
|
||||
fmt.Printf("\nerr: %v; cannot load circuit_breaker, using noop", err.Error()) |
|
||||
cb = noopCircuitBreaker{} |
|
||||
} |
|
||||
} else { |
|
||||
fmt.Println("circuit_breaker module not loaded, using noop") |
|
||||
cb = noopCircuitBreaker{} |
|
||||
} |
|
||||
} else { |
|
||||
cb = noopCircuitBreaker{} |
|
||||
} |
|
||||
nu.CB = cb |
|
||||
|
|
||||
// start a healthcheck worker which will periodically check to see if an upstream is healthy
|
|
||||
// to proxy requests to.
|
|
||||
nu.healthChecker = NewHealthCheckWorker(nu, defaultHealthCheckDur, defaultHTTPClient) |
|
||||
|
|
||||
// TODO :- if path is empty why does this empty the entire Target?
|
|
||||
// nu.Target.Path = uc.HealthCheckPath
|
|
||||
|
|
||||
nu.healthChecker.ScheduleChecks(nu.Target.String()) |
|
||||
lb.HealthCheckers = append(lb.HealthCheckers, nu.healthChecker) |
|
||||
|
|
||||
us = append(us, nu) |
|
||||
} |
|
||||
|
|
||||
lb.upstreams = us |
|
||||
|
|
||||
return nil |
|
||||
} |
|
||||
|
|
||||
// LoadBalanced represents a collection of upstream hosts that are loadbalanced. It
|
|
||||
// contains multiple features like health checking and circuit breaking functionality
|
|
||||
// for upstreams.
|
|
||||
type LoadBalanced struct { |
|
||||
mu sync.Mutex |
|
||||
numUnhealthy int32 |
|
||||
selectedServer int // used during round robin load balancing
|
|
||||
loadBalanceType int |
|
||||
tryInterval time.Duration |
|
||||
upstreams []*upstream |
|
||||
|
|
||||
// The following struct fields are set by caddy configuration.
|
|
||||
// TryInterval is the max duration for which request retrys will be performed for a request.
|
|
||||
TryInterval string `json:"try_interval,omitempty"` |
|
||||
|
|
||||
// Upstreams are the configs for upstream hosts
|
|
||||
Upstreams []*UpstreamConfig `json:"upstreams,omitempty"` |
|
||||
|
|
||||
// LoadBalanceType is the string representation of what loadbalancing algorithm to use. i.e. "random" or "round_robin".
|
|
||||
LoadBalanceType string `json:"load_balance_type,omitempty"` |
|
||||
|
|
||||
// NoHealthyUpstreamsMessage is returned as a response when there are no healthy upstreams to loadbalance to.
|
|
||||
NoHealthyUpstreamsMessage string `json:"no_healthy_upstreams_message,omitempty"` |
|
||||
|
|
||||
// TODO :- store healthcheckers as package level state where each upstream gets a single healthchecker
|
|
||||
// currently a healthchecker is created for each upstream defined, even if a healthchecker was previously created
|
|
||||
// for that upstream
|
|
||||
HealthCheckers []*HealthChecker `json:"health_checkers,omitempty"` |
|
||||
} |
|
||||
|
|
||||
// Cleanup stops all health checkers on a loadbalanced reverse proxy.
|
|
||||
func (lb *LoadBalanced) Cleanup() error { |
|
||||
for _, hc := range lb.HealthCheckers { |
|
||||
hc.Stop() |
|
||||
} |
|
||||
|
|
||||
return nil |
|
||||
} |
|
||||
|
|
||||
// Provision sets up a new loadbalanced reverse proxy.
|
|
||||
func (lb *LoadBalanced) Provision(ctx caddy.Context) error { |
|
||||
return NewLoadBalancedReverseProxy(lb, ctx) |
|
||||
} |
|
||||
|
|
||||
// ServeHTTP implements the caddyhttp.MiddlewareHandler interface to
|
|
||||
// dispatch an HTTP request to the proper server.
|
|
||||
func (lb *LoadBalanced) ServeHTTP(w http.ResponseWriter, r *http.Request, _ caddyhttp.Handler) error { |
|
||||
// ensure requests don't hang if an upstream does not respond or is not eventually healthy
|
|
||||
var u *upstream |
|
||||
var done bool |
|
||||
|
|
||||
retryTimer := time.NewTicker(lb.tryInterval) |
|
||||
defer retryTimer.Stop() |
|
||||
|
|
||||
go func() { |
|
||||
select { |
|
||||
case <-retryTimer.C: |
|
||||
done = true |
|
||||
} |
|
||||
}() |
|
||||
|
|
||||
// keep trying to get an available upstream to process the request
|
|
||||
for { |
|
||||
switch lb.loadBalanceType { |
|
||||
case TypeBalanceRandom: |
|
||||
u = lb.random() |
|
||||
case TypeBalanceRoundRobin: |
|
||||
u = lb.roundRobin() |
|
||||
} |
|
||||
|
|
||||
// if we can't get an upstream and our retry interval has ended return an error response
|
|
||||
if u == nil && done { |
|
||||
w.WriteHeader(http.StatusBadGateway) |
|
||||
fmt.Fprint(w, lb.NoHealthyUpstreamsMessage) |
|
||||
|
|
||||
return fmt.Errorf(msgNoHealthyUpstreams) |
|
||||
} |
|
||||
|
|
||||
// attempt to get an available upstream
|
|
||||
if u == nil { |
|
||||
continue |
|
||||
} |
|
||||
|
|
||||
start := time.Now() |
|
||||
|
|
||||
// if we get an error retry until we get a healthy upstream
|
|
||||
res, err := u.ReverseProxy.ServeHTTP(w, r) |
|
||||
if err != nil { |
|
||||
if err == context.Canceled { |
|
||||
return nil |
|
||||
} |
|
||||
|
|
||||
continue |
|
||||
} |
|
||||
|
|
||||
// record circuit breaker metrics
|
|
||||
go u.CB.RecordMetric(res.StatusCode, time.Now().Sub(start)) |
|
||||
|
|
||||
return nil |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// incrUnhealthy increments the amount of unhealthy nodes in a loadbalancer.
|
|
||||
func (lb *LoadBalanced) incrUnhealthy() { |
|
||||
atomic.AddInt32(&lb.numUnhealthy, 1) |
|
||||
} |
|
||||
|
|
||||
// decrUnhealthy decrements the amount of unhealthy nodes in a loadbalancer.
|
|
||||
func (lb *LoadBalanced) decrUnhealthy() { |
|
||||
atomic.AddInt32(&lb.numUnhealthy, -1) |
|
||||
} |
|
||||
|
|
||||
// roundRobin implements a round robin load balancing algorithm to select
|
|
||||
// which server to forward requests to.
|
|
||||
func (lb *LoadBalanced) roundRobin() *upstream { |
|
||||
if atomic.LoadInt32(&lb.numUnhealthy) == int32(len(lb.upstreams)) { |
|
||||
return nil |
|
||||
} |
|
||||
|
|
||||
selected := lb.upstreams[lb.selectedServer] |
|
||||
|
|
||||
lb.mu.Lock() |
|
||||
lb.selectedServer++ |
|
||||
if lb.selectedServer >= len(lb.upstreams) { |
|
||||
lb.selectedServer = 0 |
|
||||
} |
|
||||
lb.mu.Unlock() |
|
||||
|
|
||||
if selected.IsHealthy() && selected.CB.Ok() { |
|
||||
return selected |
|
||||
} |
|
||||
|
|
||||
return nil |
|
||||
} |
|
||||
|
|
||||
// random implements a random server selector for load balancing.
|
|
||||
func (lb *LoadBalanced) random() *upstream { |
|
||||
if atomic.LoadInt32(&lb.numUnhealthy) == int32(len(lb.upstreams)) { |
|
||||
return nil |
|
||||
} |
|
||||
|
|
||||
n := rand.Int() % len(lb.upstreams) |
|
||||
selected := lb.upstreams[n] |
|
||||
|
|
||||
if selected.IsHealthy() && selected.CB.Ok() { |
|
||||
return selected |
|
||||
} |
|
||||
|
|
||||
return nil |
|
||||
} |
|
||||
|
|
||||
// UpstreamConfig represents the config of an upstream.
|
|
||||
type UpstreamConfig struct { |
|
||||
// Host is the host name of the upstream server.
|
|
||||
Host string `json:"host,omitempty"` |
|
||||
|
|
||||
// FastHealthCheckDuration is the duration for which a health check is performed when a node is considered unhealthy.
|
|
||||
FastHealthCheckDuration string `json:"fast_health_check_duration,omitempty"` |
|
||||
|
|
||||
CircuitBreaker json.RawMessage `json:"circuit_breaker,omitempty"` |
|
||||
|
|
||||
// // CircuitBreakerConfig is the config passed to setup a circuit breaker.
|
|
||||
// CircuitBreakerConfig *circuitbreaker.Config `json:"circuit_breaker,omitempty"`
|
|
||||
circuitbreaker CircuitBreaker |
|
||||
|
|
||||
// HealthCheckDuration is the default duration for which a health check is performed.
|
|
||||
HealthCheckDuration string `json:"health_check_duration,omitempty"` |
|
||||
|
|
||||
// HealthCheckPath is the path at the upstream host to use for healthchecks.
|
|
||||
HealthCheckPath string `json:"health_check_path,omitempty"` |
|
||||
} |
|
||||
|
|
||||
// upstream represents an upstream host.
|
|
||||
type upstream struct { |
|
||||
Healthy int32 // 0 = false, 1 = true
|
|
||||
Target *url.URL |
|
||||
ReverseProxy *ReverseProxy |
|
||||
Incr func() |
|
||||
Decr func() |
|
||||
CB CircuitBreaker |
|
||||
healthChecker *HealthChecker |
|
||||
healthCheckDur time.Duration |
|
||||
fastHealthCheckDur time.Duration |
|
||||
} |
|
||||
|
|
||||
// newUpstream returns a new upstream.
|
|
||||
func newUpstream(uc *UpstreamConfig, d func(), i func()) (*upstream, error) { |
|
||||
host := strings.TrimSpace(uc.Host) |
|
||||
protoIdx := strings.Index(host, "://") |
|
||||
if protoIdx == -1 || len(host[:protoIdx]) == 0 { |
|
||||
return nil, fmt.Errorf("protocol is required for host") |
|
||||
} |
|
||||
|
|
||||
hostURL, err := url.Parse(host) |
|
||||
if err != nil { |
|
||||
return nil, err |
|
||||
} |
|
||||
|
|
||||
// parse healthcheck durations
|
|
||||
hcd, err := time.ParseDuration(uc.HealthCheckDuration) |
|
||||
if err != nil { |
|
||||
hcd = defaultHealthCheckDur |
|
||||
} |
|
||||
|
|
||||
fhcd, err := time.ParseDuration(uc.FastHealthCheckDuration) |
|
||||
if err != nil { |
|
||||
fhcd = defaultFastHealthCheckDur |
|
||||
} |
|
||||
|
|
||||
u := upstream{ |
|
||||
healthCheckDur: hcd, |
|
||||
fastHealthCheckDur: fhcd, |
|
||||
Target: hostURL, |
|
||||
Decr: d, |
|
||||
Incr: i, |
|
||||
Healthy: int32(0), // assume is unhealthy on start
|
|
||||
} |
|
||||
|
|
||||
u.ReverseProxy = newReverseProxy(hostURL, u.SetHealthiness) |
|
||||
return &u, nil |
|
||||
} |
|
||||
|
|
||||
// SetHealthiness sets whether an upstream is healthy or not. The health check worker is updated to
|
|
||||
// perform checks faster if a node is unhealthy.
|
|
||||
func (u *upstream) SetHealthiness(ok bool) { |
|
||||
h := atomic.LoadInt32(&u.Healthy) |
|
||||
var wasHealthy bool |
|
||||
if h == 1 { |
|
||||
wasHealthy = true |
|
||||
} else { |
|
||||
wasHealthy = false |
|
||||
} |
|
||||
|
|
||||
if ok { |
|
||||
u.healthChecker.Ticker = time.NewTicker(u.healthCheckDur) |
|
||||
|
|
||||
if !wasHealthy { |
|
||||
atomic.AddInt32(&u.Healthy, 1) |
|
||||
u.Decr() |
|
||||
} |
|
||||
} else { |
|
||||
u.healthChecker.Ticker = time.NewTicker(u.fastHealthCheckDur) |
|
||||
|
|
||||
if wasHealthy { |
|
||||
atomic.AddInt32(&u.Healthy, -1) |
|
||||
u.Incr() |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// IsHealthy returns whether an Upstream is healthy or not.
|
|
||||
func (u *upstream) IsHealthy() bool { |
|
||||
i := atomic.LoadInt32(&u.Healthy) |
|
||||
if i == 1 { |
|
||||
return true |
|
||||
} |
|
||||
|
|
||||
return false |
|
||||
} |
|
||||
|
|
||||
// newReverseProxy returns a new reverse proxy handler.
|
|
||||
func newReverseProxy(target *url.URL, setHealthiness func(bool)) *ReverseProxy { |
|
||||
errorHandler := func(w http.ResponseWriter, r *http.Request, err error) { |
|
||||
// we don't need to worry about cancelled contexts since this doesn't necessarilly mean that
|
|
||||
// the upstream is unhealthy.
|
|
||||
if err != context.Canceled { |
|
||||
setHealthiness(false) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
rp := NewSingleHostReverseProxy(target) |
|
||||
rp.ErrorHandler = errorHandler |
|
||||
rp.Transport = defaultTransport // use default transport that times out in 5 seconds
|
|
||||
return rp |
|
||||
} |
|
||||
|
|
||||
// Interface guards
|
|
||||
var ( |
|
||||
_ caddyhttp.MiddlewareHandler = (*LoadBalanced)(nil) |
|
||||
_ caddy.Provisioner = (*LoadBalanced)(nil) |
|
||||
_ caddy.CleanerUpper = (*LoadBalanced)(nil) |
|
||||
) |
|
@ -0,0 +1,86 @@ |
|||||
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
|
//
|
||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
// you may not use this file except in compliance with the License.
|
||||
|
// You may obtain a copy of the License at
|
||||
|
//
|
||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
//
|
||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
// See the License for the specific language governing permissions and
|
||||
|
// limitations under the License.
|
||||
|
|
||||
|
package caddy |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"sync" |
||||
|
"sync/atomic" |
||||
|
) |
||||
|
|
||||
|
// UsagePool is a thread-safe map that pools values
|
||||
|
// based on usage; a LoadOrStore operation increments
|
||||
|
// the usage, and a Delete decrements from the usage.
|
||||
|
// If the usage count reaches 0, the value will be
|
||||
|
// removed from the map. There is no way to overwrite
|
||||
|
// existing keys in the pool without first deleting
|
||||
|
// it as many times as it was stored. Deleting too
|
||||
|
// many times will panic.
|
||||
|
//
|
||||
|
// An empty UsagePool is NOT safe to use; always call
|
||||
|
// NewUsagePool() to make a new value.
|
||||
|
type UsagePool struct { |
||||
|
pool *sync.Map |
||||
|
} |
||||
|
|
||||
|
// NewUsagePool returns a new usage pool.
|
||||
|
func NewUsagePool() *UsagePool { |
||||
|
return &UsagePool{pool: new(sync.Map)} |
||||
|
} |
||||
|
|
||||
|
// Delete decrements the usage count for key and removes the
|
||||
|
// value from the underlying map if the usage is 0. It returns
|
||||
|
// true if the usage count reached 0 and the value was deleted.
|
||||
|
// It panics if the usage count drops below 0; always call
|
||||
|
// Delete precisely as many times as LoadOrStore.
|
||||
|
func (up *UsagePool) Delete(key interface{}) (deleted bool) { |
||||
|
usageVal, ok := up.pool.Load(key) |
||||
|
if !ok { |
||||
|
return false |
||||
|
} |
||||
|
upv := usageVal.(*usagePoolVal) |
||||
|
newUsage := atomic.AddInt32(&upv.usage, -1) |
||||
|
if newUsage == 0 { |
||||
|
up.pool.Delete(key) |
||||
|
return true |
||||
|
} else if newUsage < 0 { |
||||
|
panic(fmt.Sprintf("deleted more than stored: %#v (usage: %d)", |
||||
|
upv.value, upv.usage)) |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
// LoadOrStore puts val in the pool and returns false if key does
|
||||
|
// not already exist; otherwise if the key exists, it loads the
|
||||
|
// existing value, increments the usage for that value, and returns
|
||||
|
// the value along with true.
|
||||
|
func (up *UsagePool) LoadOrStore(key, val interface{}) (actual interface{}, loaded bool) { |
||||
|
usageVal := &usagePoolVal{ |
||||
|
usage: 1, |
||||
|
value: val, |
||||
|
} |
||||
|
actual, loaded = up.pool.LoadOrStore(key, usageVal) |
||||
|
if loaded { |
||||
|
upv := actual.(*usagePoolVal) |
||||
|
actual = upv.value |
||||
|
atomic.AddInt32(&upv.usage, 1) |
||||
|
} |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
type usagePoolVal struct { |
||||
|
usage int32 // accessed atomically; must be 64-bit aligned for 32-bit systems
|
||||
|
value interface{} |
||||
|
} |
Loading…
Reference in new issue