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