From 4cc91674e234e1ccaace7118109a750551b369df Mon Sep 17 00:00:00 2001 From: Mohammad Gufran Date: Fri, 16 Feb 2018 14:56:14 +0530 Subject: [PATCH 01/31] Forking ac865e8 of https://github.com/mholt/caddy/... --- NOTICES.txt | 5 + proxy/fastcgi/fcgiclient.go | 587 ++++++++++++++++++++++++++++++++++++ 2 files changed, 592 insertions(+) create mode 100644 proxy/fastcgi/fcgiclient.go diff --git a/NOTICES.txt b/NOTICES.txt index d1ba4a54b..037f21dd6 100644 --- a/NOTICES.txt +++ b/NOTICES.txt @@ -157,3 +157,8 @@ golang.org/x/sync/singleflight https://golang.org/x/sync/singleflight License: BSD 3-clause (https://golang.org/x/sync/LICENSE) Copyright (c) 2009 The Go Authors. All rights reserved. + +github.com/mholt/caddy +https://github.com/mholt/caddy +License: Apache 2.0 (https://github.com/mholt/caddy/LICENSE.txt) +Copyright (c) 2015, Light Code Labs, LLC diff --git a/proxy/fastcgi/fcgiclient.go b/proxy/fastcgi/fcgiclient.go new file mode 100644 index 000000000..cf6cc23c9 --- /dev/null +++ b/proxy/fastcgi/fcgiclient.go @@ -0,0 +1,587 @@ +// Copyright 2015 Light Code Labs, LLC +// +// 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 ac865e8 on Jan. 2018 from https://github.com/mholt/caddy/blob/master/caddyhttp/fastcgi/fcgiclient.go +// Which is 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 Caddy project. + +// Copyright 2012 Junqing Tan 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 + readTimeout time.Duration + sendTimeout time.Duration +} + +// 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) { + 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 connnection +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) (resp *http.Response, err error) { + + p["REQUEST_METHOD"] = "GET" + p["CONTENT_LENGTH"] = "0" + + return c.Request(p, nil) +} + +// 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 +} + +// SetSendTimeout sets the read 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) SetSendTimeout(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" } + +// Stderr returns any error produced by FCGI backend +// while processing the request. +func (c *FCGIClient) Stderr() string { + return c.stderr.String() +} From d71d11cf50fe6f62810609264e5c5b4112dd219d Mon Sep 17 00:00:00 2001 From: Mohammad Gufran Date: Fri, 16 Feb 2018 14:56:50 +0530 Subject: [PATCH 02/31] Add fastcgi handler --- config/config.go | 9 + config/default.go | 7 + config/load.go | 5 + docs/content/feature/fastcgi-upstream.md | 14 ++ docs/content/quickstart/_index.md | 4 + docs/content/ref/fcgi.index.md | 12 + docs/content/ref/fcgi.path.split.md | 12 + docs/content/ref/fcgi.root.md | 11 + docs/content/ref/fcgi.timeout.read.md | 11 + docs/content/ref/fcgi.timeout.write.md | 11 + main.go | 2 +- proxy/fastcgi/helper_test.go | 48 ++++ proxy/fastcgi/proxy.go | 287 +++++++++++++++++++++++ proxy/fastcgi/proxy_test.go | 180 ++++++++++++++ proxy/http_integration_test.go | 45 ++-- proxy/http_proxy.go | 38 ++- proxy/listen_test.go | 1 + proxy/ws_integration_test.go | 4 +- 18 files changed, 674 insertions(+), 27 deletions(-) create mode 100644 docs/content/feature/fastcgi-upstream.md create mode 100644 docs/content/ref/fcgi.index.md create mode 100644 docs/content/ref/fcgi.path.split.md create mode 100644 docs/content/ref/fcgi.root.md create mode 100644 docs/content/ref/fcgi.timeout.read.md create mode 100644 docs/content/ref/fcgi.timeout.write.md create mode 100644 proxy/fastcgi/helper_test.go create mode 100644 proxy/fastcgi/proxy.go create mode 100644 proxy/fastcgi/proxy_test.go diff --git a/config/config.go b/config/config.go index 4bf32e896..ca3f179ed 100644 --- a/config/config.go +++ b/config/config.go @@ -14,6 +14,7 @@ type Config struct { Metrics Metrics UI UI Runtime Runtime + FastCGI FastCGI ProfileMode string ProfilePath string Insecure bool @@ -142,3 +143,11 @@ type Consul struct { CheckScheme string CheckTLSSkipVerify bool } + +type FastCGI struct { + Index string + Root string + SplitPath string + ReadTimeout time.Duration + WriteTimeout time.Duration +} diff --git a/config/default.go b/config/default.go index c01b22914..14b3f0195 100644 --- a/config/default.go +++ b/config/default.go @@ -75,4 +75,11 @@ var defaultConfig = &Config{ Color: "light-green", Access: "rw", }, + FastCGI: FastCGI{ + Root: "", + Index: "index.php", + SplitPath: ".php", + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + }, } diff --git a/config/load.go b/config/load.go index 4529693f5..3b98b6739 100644 --- a/config/load.go +++ b/config/load.go @@ -184,6 +184,11 @@ func load(cmdline, environ, envprefix []string, props *properties.Properties) (c f.StringVar(&cfg.UI.Title, "ui.title", defaultConfig.UI.Title, "optional title for the UI") f.StringVar(&cfg.ProfileMode, "profile.mode", defaultConfig.ProfileMode, "enable profiling mode, one of [cpu, mem, mutex, block]") f.StringVar(&cfg.ProfilePath, "profile.path", defaultConfig.ProfilePath, "path to profile dump file") + f.StringVar(&cfg.FastCGI.Index, "fcgi.index", defaultConfig.FastCGI.Index, "FastCGI index file name") + f.StringVar(&cfg.FastCGI.Root, "fcgi.root", defaultConfig.FastCGI.Root, "Document root of FastCGI upstream") + f.StringVar(&cfg.FastCGI.SplitPath, "fcgi.path.split", defaultConfig.FastCGI.SplitPath, "String literal to split the document path") + f.DurationVar(&cfg.FastCGI.ReadTimeout, "fcgi.timeout.read", defaultConfig.FastCGI.ReadTimeout, "FastCGI request read timeout") + f.DurationVar(&cfg.FastCGI.WriteTimeout, "fcgi.timeout.write", defaultConfig.FastCGI.WriteTimeout, "FastCGI request write timeout") // deprecated flags var proxyLogRoutes string diff --git a/docs/content/feature/fastcgi-upstream.md b/docs/content/feature/fastcgi-upstream.md new file mode 100644 index 000000000..3532543fa --- /dev/null +++ b/docs/content/feature/fastcgi-upstream.md @@ -0,0 +1,14 @@ +--- +title: "FastCGI Upstream" +--- + +To support FastCGI upstream add `proto=fcgi` option to the `urlprefix-` tag. + +FastCGI upstreams support following configuration options: + + - `index`: Used to specify the index file that should be used if the request URL does not contain a + file. + - `root`: Document root of the FastCGI server. + +Note that `index` and `root` can also be set in Fabio configuration as global default. + diff --git a/docs/content/quickstart/_index.md b/docs/content/quickstart/_index.md index 3bc0f6cd2..4bd376679 100644 --- a/docs/content/quickstart/_index.md +++ b/docs/content/quickstart/_index.md @@ -46,6 +46,10 @@ and you need to add a separate `urlprefix-` tag for every `host/path` prefix the # TCP examples urlprefix-:3306 proto=tcp # route external port 3306 + + # Fast-CGI example + urlprefix-/blog proto=fcgi + urlprefix-/home proto=fcgi strip=/home ``` 5. Start fabio without a config file diff --git a/docs/content/ref/fcgi.index.md b/docs/content/ref/fcgi.index.md new file mode 100644 index 000000000..70f902e0f --- /dev/null +++ b/docs/content/ref/fcgi.index.md @@ -0,0 +1,12 @@ +--- +title: "fcgi.index" +--- + +`fcgi.index` configures the index file to be used in FastCGI requests if the URL does not contain +it. + +Default value is + +``` +fcgi.index = index.php +``` diff --git a/docs/content/ref/fcgi.path.split.md b/docs/content/ref/fcgi.path.split.md new file mode 100644 index 000000000..dca597b32 --- /dev/null +++ b/docs/content/ref/fcgi.path.split.md @@ -0,0 +1,12 @@ +--- +title: "fcgi.path.split" +--- + +`fcgi.path.split` specifies how to split the URL; the split value becomes the end of the first part +and anything in the URL after it becomes part of the `PATH_INFO` CGI variable. + +Default value is + +``` +fcgi.path.split = .php +``` diff --git a/docs/content/ref/fcgi.root.md b/docs/content/ref/fcgi.root.md new file mode 100644 index 000000000..9e4cc1dbf --- /dev/null +++ b/docs/content/ref/fcgi.root.md @@ -0,0 +1,11 @@ +--- +title: "fcgi.root" +--- + +`fcgi.root` sets the document root for FastCGI requests. + +Default value is empty string + +``` +fcgi.root = +``` diff --git a/docs/content/ref/fcgi.timeout.read.md b/docs/content/ref/fcgi.timeout.read.md new file mode 100644 index 000000000..5b6f109fa --- /dev/null +++ b/docs/content/ref/fcgi.timeout.read.md @@ -0,0 +1,11 @@ +--- +title: "fcgi.timeout.read" +--- + +`fcgi.timeout.read` is the time allowed to read a response from upstream. + +Default value is + +``` +fcgi.timeout.read = 10s +``` diff --git a/docs/content/ref/fcgi.timeout.write.md b/docs/content/ref/fcgi.timeout.write.md new file mode 100644 index 000000000..798d145e8 --- /dev/null +++ b/docs/content/ref/fcgi.timeout.write.md @@ -0,0 +1,11 @@ +--- +title: "fcgi.timeout.write" +--- + +`fcgi.timeout.write` is the time allowed to upload complete request to upstream. + +Default value is + +``` +fcgi.timeout.write = 10s +``` diff --git a/main.go b/main.go index a8637354e..859beed6e 100644 --- a/main.go +++ b/main.go @@ -181,7 +181,7 @@ func newHTTPProxy(cfg *config.Config) http.Handler { } return &proxy.HTTPProxy{ - Config: cfg.Proxy, + Config: cfg, Transport: newTransport(nil), InsecureTransport: newTransport(&tls.Config{InsecureSkipVerify: true}), Lookup: func(r *http.Request) *route.Target { diff --git a/proxy/fastcgi/helper_test.go b/proxy/fastcgi/helper_test.go new file mode 100644 index 000000000..ec931b9d5 --- /dev/null +++ b/proxy/fastcgi/helper_test.go @@ -0,0 +1,48 @@ +package fastcgi + +import ( + "io" + "net/http" + "time" +) + +type staticFcgiBackend struct { + OptionsFunc func(map[string]string) (*http.Response, error) + HeadFunc func(map[string]string) (*http.Response, error) + GetFunc func(map[string]string) (*http.Response, error) + PostFunc func(map[string]string, string, string, io.Reader, int64) (*http.Response, error) + SetReadTimeoutFunc func(time.Duration) error + SetSendTimeoutFunc func(time.Duration) error + StderrFunc func() string + CloseFunc func() +} + +func (b *staticFcgiBackend) Options(params map[string]string) (*http.Response, error) { + return b.OptionsFunc(params) +} + +func (b *staticFcgiBackend) Head(params map[string]string) (*http.Response, error) { + return b.HeadFunc(params) +} + +func (b *staticFcgiBackend) Get(params map[string]string) (*http.Response, error) { + return b.GetFunc(params) +} + +func (b *staticFcgiBackend) SetReadTimeout(dur time.Duration) error { + return b.SetReadTimeoutFunc(dur) +} + +func (b *staticFcgiBackend) SetSendTimeout(dur time.Duration) error { + return b.SetSendTimeoutFunc(dur) +} + +func (b *staticFcgiBackend) Stderr() string { + return b.StderrFunc() +} + +func (b *staticFcgiBackend) Post(params map[string]string, method string, bodyType string, body io.Reader, l int64) (*http.Response, error) { + return b.PostFunc(params, method, bodyType, body, l) +} + +func (b *staticFcgiBackend) Close() {} diff --git a/proxy/fastcgi/proxy.go b/proxy/fastcgi/proxy.go new file mode 100644 index 000000000..398e26d85 --- /dev/null +++ b/proxy/fastcgi/proxy.go @@ -0,0 +1,287 @@ +package fastcgi + +import ( + "fmt" + "io" + "log" + "net" + "net/http" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/fabiolb/fabio/config" +) + +var ( + headerNameReplacer = strings.NewReplacer(" ", "_", "-", "_") +) + +type Proxy struct { + root string + index string + stripPrefix string + upstream string + config *config.Config + dialFunc func(string) (FCGIBackend, error) +} + +// FCGIBackend describes the capabilities offered by +// FastCGI bakcned server +type FCGIBackend interface { + // Options proxies HTTP OPTION request to FCGI backend + Options(parameters map[string]string) (resp *http.Response, err error) + + // Head proxies HTTP HEAD request to FCGI backend + Head(parameters map[string]string) (resp *http.Response, err error) + + // Get proxies HTTP GET request to FCGI backend + Get(parameters map[string]string) (resp *http.Response, err error) + + // Post proxies HTTP Post request to FCGI backend + Post(parameters map[string]string, method string, bodyType string, body io.Reader, contentLength int64) (resp *http.Response, err error) + + // SetReadTimeout sets the maximum time duration the + // connection will wait to read full response. If the + // deadline is reached before the response is read in + // full, the client receives a gateway timeout error. + SetReadTimeout(time.Duration) error + + // SetSendTimeout sets the maximum time duration the + // connection will wait to send full request to FCGI backend. + // If the deadline is reached before the request is sent + // completely, the client receives a gateway timeout error. + SetSendTimeout(time.Duration) error + + // Stderr returns any error produced by FCGI backend + // while processing the request. + Stderr() string + + // Close closes the connection. + Close() +} + +func NewProxy(cfg *config.Config, upstream string) *Proxy { + return &Proxy{ + root: cfg.FastCGI.Root, + index: cfg.FastCGI.Index, + upstream: upstream, + config: cfg, + dialFunc: Connect, + } +} + +// Connect to FCGI backend +func Connect(upstream string) (FCGIBackend, error) { + return Dial("tcp", upstream) +} + +func (p *Proxy) SetRoot(root string) { + p.root = root +} + +func (p *Proxy) SetStripPathPrefix(prefix string) { + p.stripPrefix = prefix +} + +func (p *Proxy) SetIndex(index string) { + p.index = index +} + +func (p *Proxy) stripPathPrefix(path string) string { + return strings.TrimPrefix(path, p.stripPrefix) +} + +func (p *Proxy) ensureIndexFile(path string) string { + prefix := "" + if strings.HasPrefix(path, "/") { + prefix = "/" + } + + if strings.HasPrefix(path, prefix+p.index) { + return path + } + + return filepath.Join(p.index, path) +} + +func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { + fpath := p.stripPathPrefix(r.URL.Path) + env, err := p.buildEnv(r, p.ensureIndexFile(fpath)) + if err != nil { + log.Printf("[WARN] failed to create fastcgi environment. %s", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + for x, y := range env { + log.Printf("[INFO] >>>> %s => %s", x, y) + } + + fcgiBackend, err := p.dialFunc(p.upstream) + if err != nil { + log.Printf("[WARN] failed to connect with FastCGI upstream. %s", err) + http.Error(w, "Bad Gateway", http.StatusBadGateway) + return + } + defer fcgiBackend.Close() + + if err := fcgiBackend.SetReadTimeout(p.config.FastCGI.ReadTimeout); err != nil { + log.Printf("[ERROR] failed to set connection read timeout. %s", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + if err := fcgiBackend.SetSendTimeout(p.config.FastCGI.WriteTimeout); err != nil { + log.Printf("[ERROR] failed to set connection write timeout. %s", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + var resp *http.Response + + contentLength := r.ContentLength + if contentLength == 0 { + 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) + case "OPTIONS": + resp, err = fcgiBackend.Options(env) + default: + resp, err = fcgiBackend.Post(env, r.Method, r.Header.Get("Content-Type"), r.Body, contentLength) + } + + if resp != nil && resp.Body != nil { + defer resp.Body.Close() + } + + if err != nil { + if err, ok := err.(net.Error); ok && err.Timeout() { + log.Printf("[ERROR] FastCGI upstream timed out during request. %s", err) + http.Error(w, "Gateway Timeout", http.StatusGatewayTimeout) + return + } else if err != io.EOF { + log.Printf("[ERROR] failed to read response from FastCGI upstream. %s", err) + http.Error(w, "Bad Gateway", http.StatusBadGateway) + return + } + } + + writeHeader(w, resp) + + _, err = io.Copy(w, resp.Body) + if err != nil { + log.Printf("[ERROR] failed to write response body. %s", err) + http.Error(w, "Bad Gateway", http.StatusBadGateway) + return + } + + if errOut := fcgiBackend.Stderr(); errOut != "" { + log.Printf("[WARN] Error from FastCGI upstream: %s", errOut) + return + } +} + +func writeHeader(w http.ResponseWriter, r *http.Response) { + for key, vals := range r.Header { + for _, val := range vals { + w.Header().Add(key, val) + } + } + w.WriteHeader(r.StatusCode) +} + +func (p Proxy) buildEnv(r *http.Request, fpath string) (map[string]string, error) { + absPath := filepath.Join(p.root, fpath) + + // 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 + } + + username := "" + if r.URL.User != nil { + username = r.URL.User.Username() + } + + // Remove [] from IPv6 addresses + ip = strings.TrimPrefix(ip, "[") + ip = strings.TrimSuffix(ip, "]") + + splitPos := p.splitPos(fpath) + if splitPos == -1 { + return nil, fmt.Errorf("cannot split path on %s", p.config.FastCGI.SplitPath) + } + + // Request has the extension; path was split successfully + docURI := fpath[:splitPos+len(p.config.FastCGI.SplitPath)] + pathInfo := fpath[splitPos+len(p.config.FastCGI.SplitPath):] + scriptName := fpath + scriptFilename := absPath + + // Strip PATH_INFO from SCRIPT_NAME + scriptName = strings.TrimSuffix(scriptName, pathInfo) + + // 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": username, + "REQUEST_METHOD": r.Method, + "SERVER_NAME": r.URL.Hostname(), + "SERVER_PORT": r.URL.Port(), + "SERVER_PROTOCOL": r.Proto, + "SERVER_SOFTWARE": "fabio", + + // Other variables + "DOCUMENT_ROOT": p.root, + "DOCUMENT_URI": docURI, + "HTTP_HOST": r.Host, // added here, since not always part of headers + "REQUEST_URI": p.stripPathPrefix(r.URL.RequestURI()), + "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(p.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" + } + + // 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 +} + +func (p *Proxy) splitPos(path string) int { + return strings.Index(strings.ToLower(path), strings.ToLower(p.config.FastCGI.SplitPath)) +} diff --git a/proxy/fastcgi/proxy_test.go b/proxy/fastcgi/proxy_test.go new file mode 100644 index 000000000..2d8865b8d --- /dev/null +++ b/proxy/fastcgi/proxy_test.go @@ -0,0 +1,180 @@ +package fastcgi + +import ( + "crypto/tls" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/fabiolb/fabio/config" +) + +func getBackendDialer(b *staticFcgiBackend) func(string) (FCGIBackend, error) { + return func(u string) (FCGIBackend, error) { + return b, nil + } +} + +type rc struct { + eof error + v []byte +} + +func (r *rc) Close() error { return nil } + +func (r *rc) Read(p []byte) (n int, err error) { + if r.eof != nil { + return 0, r.eof + } + + copy(p, r.v) + r.eof = io.EOF + return len(r.v), nil +} + +func TestServeHTTP(t *testing.T) { + data := struct { + readTimeout time.Duration + sendTimeout time.Duration + env map[string]string + method string + contentType string + body io.Reader + contentLength int64 + }{} + + req, err := http.NewRequest("post", "https://app.host/user/index.php/profile?key=value", strings.NewReader("test request body")) + if err != nil { + t.Error("failed to create new http request", err) + } + req.Header.Add("Content-Length", "17") + req.Header.Add("Content-Type", "text/plain") + + response := http.Response{ + Status: http.StatusText(http.StatusOK), + StatusCode: http.StatusOK, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: http.Header{}, + ContentLength: 10, + TransferEncoding: nil, + Close: true, + Uncompressed: false, + Request: req, + TLS: nil, + Body: &rc{ + v: []byte("successful response"), + }, + } + + backend := &staticFcgiBackend{ + SetReadTimeoutFunc: func(t time.Duration) error { + data.readTimeout = t + return nil + }, + SetSendTimeoutFunc: func(t time.Duration) error { + data.sendTimeout = t + return nil + }, + PostFunc: func(params map[string]string, m string, ct string, b io.Reader, cl int64) (*http.Response, error) { + data.env = params + data.method = m + data.contentType = ct + data.body = b + data.contentLength = cl + return &response, nil + }, + StderrFunc: func() string { return "" }, + } + + proxy := Proxy{ + upstream: "app.fpm.internal", + config: &config.Config{ + FastCGI: config.FastCGI{ + Root: "/site", + SplitPath: ".php", + ReadTimeout: 3 * time.Second, + WriteTimeout: 3 * time.Second, + }, + }, + dialFunc: getBackendDialer(backend), + } + + resp := httptest.NewRecorder() + proxy.ServeHTTP(resp, req) + + if resp.Code != http.StatusOK { + t.Errorf("expected response code '200', got '%d'", resp.Code) + } + if resp.Body.String() != "successful response" { + t.Errorf("expected response body 'successful response', got '%s'", resp.Body.String()) + } +} + +func TestBuildEnv(t *testing.T) { + proxy := Proxy{ + upstream: "app.fpm.internal", + config: &config.Config{ + FastCGI: config.FastCGI{ + Root: "/site", + SplitPath: ".php", + }, + }, + dialFunc: nil, + } + + req, err := http.NewRequest("post", "https://app.host:443/test/url?key=value", strings.NewReader("test request body")) + if err != nil { + t.Error("failed to create new http request", err) + } + + req.Header.Add("Content-Length", "17") + req.Header.Add("Content-Type", "text/plain") + req.Header.Add("X-Custom-Header-One", "One") + req.TLS = &tls.ConnectionState{} + + env, err := proxy.buildEnv(req, "/docs/index.php/user/profile") + if err != nil { + t.Error("failed to build environment", err) + } + + expected := map[string]string{ + "AUTH_TYPE": "", + "CONTENT_LENGTH": "17", + "CONTENT_TYPE": "text/plain", + "GATEWAY_INTERFACE": "CGI/1.1", + "PATH_INFO": "/user/profile", + "QUERY_STRING": "key=value", + "REMOTE_ADDR": "", + "REMOTE_HOST": "", + "REMOTE_PORT": "", + "REMOTE_IDENT": "", + "REMOTE_USER": "", + "REQUEST_METHOD": "post", + "SERVER_NAME": "app.host", + "SERVER_PORT": "443", + "SERVER_PROTOCOL": "HTTP/1.1", + "SERVER_SOFTWARE": "fabio", + "DOCUMENT_ROOT": "/site", + "DOCUMENT_URI": "/docs/index.php", + "HTTP_HOST": "app.host:443", + "REQUEST_URI": "/test/url?key=value", + "SCRIPT_FILENAME": "/site/docs/index.php/user/profile", + "SCRIPT_NAME": "/docs/index.php", + "PATH_TRANSLATED": "/site/user/profile", + "HTTPS": "on", + "HTTP_X_CUSTOM_HEADER_ONE": "One", + } + + for ke, ve := range expected { + if v, ok := env[ke]; !ok { + t.Errorf("key '%s' is not present in environment map", ke) + } else if v != ve { + t.Errorf("Key '%s': expected value '%s', got '%s'", ke, ve, v) + } + } +} diff --git a/proxy/http_integration_test.go b/proxy/http_integration_test.go index c430c4ae0..42ad53e9d 100644 --- a/proxy/http_integration_test.go +++ b/proxy/http_integration_test.go @@ -34,7 +34,9 @@ func TestProxyProducesCorrectXForwardedSomethingHeader(t *testing.T) { defer server.Close() proxy := httptest.NewServer(&HTTPProxy{ - Config: config.Proxy{LocalIP: "1.1.1.1", ClientIPHeader: "X-Forwarded-For"}, + Config: &config.Config{ + Proxy: config.Proxy{LocalIP: "1.1.1.1", ClientIPHeader: "X-Forwarded-For"}, + }, Transport: http.DefaultTransport, Lookup: func(r *http.Request) *route.Target { return &route.Target{URL: mustParse(server.URL)} @@ -63,7 +65,7 @@ func TestProxyRequestIDHeader(t *testing.T) { defer server.Close() proxy := httptest.NewServer(&HTTPProxy{ - Config: config.Proxy{RequestID: "X-Request-Id"}, + Config: &config.Config{Proxy: config.Proxy{RequestID: "X-Request-Id"}}, Transport: http.DefaultTransport, UUID: func() string { return "f47ac10b-58cc-0372-8567-0e02b2c3d479" }, Lookup: func(r *http.Request) *route.Target { @@ -85,11 +87,13 @@ func TestProxySTSHeader(t *testing.T) { defer server.Close() proxy := httptest.NewTLSServer(&HTTPProxy{ - Config: config.Proxy{ - STSHeader: config.STSHeader{ - MaxAge: 31536000, - Subdomains: true, - Preload: true, + Config: &config.Config{ + Proxy: config.Proxy{ + STSHeader: config.STSHeader{ + MaxAge: 31536000, + Subdomains: true, + Preload: true, + }, }, }, Transport: &http.Transport{TLSClientConfig: tlsInsecureConfig()}, @@ -119,6 +123,7 @@ func TestProxyNoRouteHTML(t *testing.T) { want := "503" noroute.SetHTML(want) proxy := httptest.NewServer(&HTTPProxy{ + Config: &config.Config{}, Transport: http.DefaultTransport, Lookup: func(*http.Request) *route.Target { return nil }, }) @@ -132,7 +137,9 @@ func TestProxyNoRouteHTML(t *testing.T) { func TestProxyNoRouteStatus(t *testing.T) { proxy := httptest.NewServer(&HTTPProxy{ - Config: config.Proxy{NoRouteStatus: 999}, + Config: &config.Config{ + Proxy: config.Proxy{NoRouteStatus: 999}, + }, Transport: http.DefaultTransport, Lookup: func(*http.Request) *route.Target { return nil }, }) @@ -155,6 +162,7 @@ func TestProxyStripsPath(t *testing.T) { })) proxy := httptest.NewServer(&HTTPProxy{ + Config: &config.Config{}, Transport: http.DefaultTransport, Lookup: func(r *http.Request) *route.Target { tbl, _ := route.NewTable("route add mock /foo/bar " + server.URL + ` opts "strip=/foo"`) @@ -187,6 +195,7 @@ func TestProxyHost(t *testing.T) { tbl, _ := route.NewTable(routes) proxy := httptest.NewServer(&HTTPProxy{ + Config: &config.Config{}, Transport: &http.Transport{ Dial: func(network, addr string) (net.Conn, error) { addr = server.URL[len("http://"):] @@ -239,6 +248,7 @@ func TestRedirect(t *testing.T) { tbl, _ := route.NewTable(routes) proxy := httptest.NewServer(&HTTPProxy{ + Config: &config.Config{}, Transport: http.DefaultTransport, Lookup: func(r *http.Request) *route.Target { return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"]) @@ -299,6 +309,7 @@ func TestProxyLogOutput(t *testing.T) { // create a proxy handler with mocked time tm := time.Date(2016, 1, 1, 0, 0, 0, 12345678, time.UTC) proxyHandler := &HTTPProxy{ + Config: &config.Config{}, Time: func() time.Time { defer func() { tm = tm.Add(1111111111 * time.Nanosecond) }() return tm @@ -386,7 +397,7 @@ func TestProxyHTTPSUpstream(t *testing.T) { defer server.Close() proxy := httptest.NewServer(&HTTPProxy{ - Config: config.Proxy{}, + Config: &config.Config{}, Transport: &http.Transport{TLSClientConfig: tlsClientConfig()}, Lookup: func(r *http.Request) *route.Target { tbl, _ := route.NewTable("route add srv / " + server.URL + ` opts "proto=https"`) @@ -411,7 +422,7 @@ func TestProxyHTTPSUpstreamSkipVerify(t *testing.T) { defer server.Close() proxy := httptest.NewServer(&HTTPProxy{ - Config: config.Proxy{}, + Config: &config.Config{}, Transport: http.DefaultTransport, InsecureTransport: &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, @@ -491,8 +502,10 @@ func TestProxyGzipHandler(t *testing.T) { defer server.Close() proxy := httptest.NewServer(&HTTPProxy{ - Config: config.Proxy{ - GZIPContentTypes: regexp.MustCompile("^text/plain(;.*)?$"), + Config: &config.Config{ + Proxy: config.Proxy{ + GZIPContentTypes: regexp.MustCompile("^text/plain(;.*)?$"), + }, }, Transport: http.DefaultTransport, Lookup: func(r *http.Request) *route.Target { @@ -612,9 +625,11 @@ func BenchmarkProxyLogger(b *testing.B) { } proxy := &HTTPProxy{ - Config: config.Proxy{ - LocalIP: "1.1.1.1", - ClientIPHeader: "X-Forwarded-For", + Config: &config.Config{ + Proxy: config.Proxy{ + LocalIP: "1.1.1.1", + ClientIPHeader: "X-Forwarded-For", + }, }, Transport: http.DefaultTransport, Lookup: func(r *http.Request) *route.Target { diff --git a/proxy/http_proxy.go b/proxy/http_proxy.go index ba505fc97..605b38fc1 100644 --- a/proxy/http_proxy.go +++ b/proxy/http_proxy.go @@ -15,6 +15,7 @@ import ( "github.com/fabiolb/fabio/logger" "github.com/fabiolb/fabio/metrics" "github.com/fabiolb/fabio/noroute" + "github.com/fabiolb/fabio/proxy/fastcgi" "github.com/fabiolb/fabio/proxy/gzip" "github.com/fabiolb/fabio/route" "github.com/fabiolb/fabio/uuid" @@ -23,7 +24,7 @@ import ( // HTTPProxy is a dynamic reverse proxy for HTTP and HTTPS protocols. type HTTPProxy struct { // Config is the proxy configuration as provided during startup. - Config config.Proxy + Config *config.Config // Time returns the current time as the number of seconds since the epoch. // If Time is nil, time.Now is used. @@ -62,17 +63,17 @@ func (p *HTTPProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { panic("no lookup function") } - if p.Config.RequestID != "" { + if p.Config.Proxy.RequestID != "" { id := p.UUID if id == nil { id = uuid.NewUUID } - r.Header.Set(p.Config.RequestID, id()) + r.Header.Set(p.Config.Proxy.RequestID, id()) } t := p.Lookup(r) if t == nil { - status := p.Config.NoRouteStatus + status := p.Config.Proxy.NoRouteStatus if status < 100 || status > 999 { status = http.StatusNotFound } @@ -129,12 +130,12 @@ func (p *HTTPProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { targetURL.Path = targetURL.Path[len(t.StripPath):] } - if err := addHeaders(r, p.Config, t.StripPath); err != nil { + if err := addHeaders(r, p.Config.Proxy, t.StripPath); err != nil { http.Error(w, "cannot parse "+r.RemoteAddr, http.StatusInternalServerError) return } - if err := addResponseHeaders(w, r, p.Config); err != nil { + if err := addResponseHeaders(w, r, p.Config.Proxy); err != nil { http.Error(w, "cannot add response headers", http.StatusInternalServerError) return } @@ -146,8 +147,27 @@ func (p *HTTPProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { tr = p.InsecureTransport } + isFCGI := false + if v, ok := t.Opts["proto"]; ok && v == "fcgi" { + isFCGI = true + } + var h http.Handler switch { + case isFCGI: + fcgiProxy := fastcgi.NewProxy(p.Config, targetURL.Host) + if fcgiRoot, ok := t.Opts["root"]; ok { + fcgiProxy.SetRoot(fcgiRoot) + } + if stripPrefix, ok := t.Opts["strip"]; ok { + fcgiProxy.SetStripPathPrefix(stripPrefix) + } + if indexFile, ok := t.Opts["index"]; ok { + fcgiProxy.SetIndex(indexFile) + } + + h = fcgiProxy + case upgrade == "websocket" || upgrade == "Websocket": r.URL = targetURL if targetURL.Scheme == "https" || targetURL.Scheme == "wss" { @@ -161,14 +181,14 @@ func (p *HTTPProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { case accept == "text/event-stream": // use the flush interval for SSE (server-sent events) // must be > 0s to be effective - h = newHTTPProxy(targetURL, tr, p.Config.FlushInterval) + h = newHTTPProxy(targetURL, tr, p.Config.Proxy.FlushInterval) default: h = newHTTPProxy(targetURL, tr, time.Duration(0)) } - if p.Config.GZIPContentTypes != nil { - h = gzip.NewGzipHandler(h, p.Config.GZIPContentTypes) + if p.Config.Proxy.GZIPContentTypes != nil { + h = gzip.NewGzipHandler(h, p.Config.Proxy.GZIPContentTypes) } timeNow := p.Time diff --git a/proxy/listen_test.go b/proxy/listen_test.go index 607461641..a57058f65 100644 --- a/proxy/listen_test.go +++ b/proxy/listen_test.go @@ -29,6 +29,7 @@ func TestGracefulShutdown(t *testing.T) { go func() { defer wg.Done() h := &HTTPProxy{ + Config: &config.Config{}, Transport: http.DefaultTransport, Lookup: func(r *http.Request) *route.Target { tbl, _ := route.NewTable("route add svc / " + srv.URL) diff --git a/proxy/ws_integration_test.go b/proxy/ws_integration_test.go index a03977481..24dcbcdf7 100644 --- a/proxy/ws_integration_test.go +++ b/proxy/ws_integration_test.go @@ -39,7 +39,7 @@ func TestProxyWSUpstream(t *testing.T) { routes += "route add ws /foo/strip " + wsServer.URL + ` opts "strip=/foo"` + "\n" httpProxy := httptest.NewServer(&HTTPProxy{ - Config: config.Proxy{NoRouteStatus: 404, GZIPContentTypes: regexp.MustCompile(".*")}, + Config: &config.Config{Proxy: config.Proxy{NoRouteStatus: 404, GZIPContentTypes: regexp.MustCompile(".*")}}, Transport: &http.Transport{TLSClientConfig: tlsClientConfig()}, InsecureTransport: &http.Transport{TLSClientConfig: tlsInsecureConfig()}, Lookup: func(r *http.Request) *route.Target { @@ -51,7 +51,7 @@ func TestProxyWSUpstream(t *testing.T) { t.Log("Started HTTP proxy: ", httpProxy.URL) httpsProxy := httptest.NewUnstartedServer(&HTTPProxy{ - Config: config.Proxy{NoRouteStatus: 404}, + Config: &config.Config{Proxy: config.Proxy{NoRouteStatus: 404}}, Transport: &http.Transport{TLSClientConfig: tlsClientConfig()}, InsecureTransport: &http.Transport{TLSClientConfig: tlsInsecureConfig()}, Lookup: func(r *http.Request) *route.Target { From e056f148431edbcb7842cc6ffcd8a61ca0a9ea4f Mon Sep 17 00:00:00 2001 From: Frank Schroeder Date: Thu, 22 Mar 2018 11:36:36 +0100 Subject: [PATCH 03/31] Require go1.9 --- .travis.yml | 1 - README.md | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index c78562db8..997382f9f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ dist: trusty language: go go: - - 1.8.x - 1.9.x - "1.10" diff --git a/README.md b/README.md index d5d0b76d0..01bd9f28c 100644 --- a/README.md +++ b/README.md @@ -70,8 +70,8 @@ The full documentation is on [fabiolb.net](https://fabiolb.net/) 1. Install from source, [binary](https://github.com/fabiolb/fabio/releases), [Docker](https://hub.docker.com/r/fabiolb/fabio/) or [Homebrew](http://brew.sh). ```shell - # go 1.8 or higher is required - go get github.com/fabiolb/fabio (>= go1.8) + # go 1.9 or higher is required + go get github.com/fabiolb/fabio (>= go1.9) brew install fabio (OSX/macOS stable) brew install --devel fabio (OSX/macOS devel) From 9e268129e35d36e8722574c7ff0fbd0304974e36 Mon Sep 17 00:00:00 2001 From: Frank Schroeder Date: Thu, 22 Mar 2018 09:11:13 +0100 Subject: [PATCH 04/31] Issue #421: Close websocket connection on failure The websocket proxy is implemented as a raw tcp proxy which relies on the client and server to close the connection. When a websocket upgrade fails the upstream server may keep the connection open. If a proxy like nginx is used in front of fabio it will keep its connection to fabio open effectively establishing a direct channel between nginx and the upstream server which will be used for any request forwarded by nginx to fabio. Adding a 'Connection: close' header to the upstream request should indicate to the server to close the connection. If that works then we can keep the raw tcp proxy for websockets. Otherwise, fabio needs to handle the handshake and close the connection itself. Fixes #421 --- proxy/http_proxy.go | 1 + 1 file changed, 1 insertion(+) diff --git a/proxy/http_proxy.go b/proxy/http_proxy.go index b531373af..a19cb2a63 100644 --- a/proxy/http_proxy.go +++ b/proxy/http_proxy.go @@ -162,6 +162,7 @@ func (p *HTTPProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { } else { h = newRawProxy(targetURL.Host, net.Dial) } + r.Header.Set("Connection", "close") case accept == "text/event-stream": // use the flush interval for SSE (server-sent events) From 96f93e09662f69d313e05b2870e84556faaa2315 Mon Sep 17 00:00:00 2001 From: Frank Schroeder Date: Thu, 22 Mar 2018 10:32:56 +0100 Subject: [PATCH 05/31] Verify WS handshake success --- proxy/http_proxy.go | 1 - proxy/http_raw_handler.go | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/proxy/http_proxy.go b/proxy/http_proxy.go index a19cb2a63..b531373af 100644 --- a/proxy/http_proxy.go +++ b/proxy/http_proxy.go @@ -162,7 +162,6 @@ func (p *HTTPProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { } else { h = newRawProxy(targetURL.Host, net.Dial) } - r.Header.Set("Connection", "close") case accept == "text/event-stream": // use the flush interval for SSE (server-sent events) diff --git a/proxy/http_raw_handler.go b/proxy/http_raw_handler.go index a2cf52e30..6f49beff2 100644 --- a/proxy/http_raw_handler.go +++ b/proxy/http_raw_handler.go @@ -1,10 +1,13 @@ package proxy import ( + "bytes" + "fmt" "io" "log" "net" "net/http" + "time" "github.com/fabiolb/fabio/metrics" ) @@ -51,6 +54,36 @@ func newRawProxy(host string, dial dialFunc) http.Handler { return } + // read the initial response to check whether we get an HTTP/1.1 101 ... response + // to determine whether the handshake worked. + b := make([]byte, 1024) + if err := out.SetReadDeadline(time.Now().Add(time.Second)); err != nil { + log.Printf("[ERROR] Error setting read timeout for %s: %s", r.URL, err) + http.Error(w, "error setting read timeout", http.StatusInternalServerError) + return + } + + n, err := out.Read(b) + if err != nil { + log.Printf("[ERROR] Error reading response for %s: %s", r.URL, err) + http.Error(w, "error reading response", http.StatusInternalServerError) + return + } + + b = b[:n] + if m, err := in.Write(b); err != nil || n != m { + log.Printf("[ERROR] Error sending header for %s: %s", r.URL, err) + http.Error(w, "error sending response", http.StatusInternalServerError) + return + } + + if !bytes.HasPrefix(b, []byte("HTTP/1.1 101")) { + fmt.Println("boom") + log.Printf("[INFO] WS Upgrade failed for %s", r.URL) + http.Error(w, "error handling ws upgrade", http.StatusInternalServerError) + return + } + errc := make(chan error, 2) cp := func(dst io.Writer, src io.Reader) { _, err := io.Copy(dst, src) From 51f647108b339a760fe84f96c79c69e73028ba65 Mon Sep 17 00:00:00 2001 From: Frank Schroeder Date: Thu, 22 Mar 2018 11:15:41 +0100 Subject: [PATCH 06/31] drop debug output --- proxy/http_raw_handler.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/proxy/http_raw_handler.go b/proxy/http_raw_handler.go index 6f49beff2..f13f4f7fe 100644 --- a/proxy/http_raw_handler.go +++ b/proxy/http_raw_handler.go @@ -2,7 +2,6 @@ package proxy import ( "bytes" - "fmt" "io" "log" "net" @@ -78,7 +77,6 @@ func newRawProxy(host string, dial dialFunc) http.Handler { } if !bytes.HasPrefix(b, []byte("HTTP/1.1 101")) { - fmt.Println("boom") log.Printf("[INFO] WS Upgrade failed for %s", r.URL) http.Error(w, "error handling ws upgrade", http.StatusInternalServerError) return From ea979c9486774b56e6926512760e50188cdf5c8b Mon Sep 17 00:00:00 2001 From: Frank Schroeder Date: Thu, 22 Mar 2018 11:22:29 +0100 Subject: [PATCH 07/31] rename raw handler to ws handler --- proxy/http_proxy.go | 4 ++-- proxy/{http_raw_handler.go => ws_handler.go} | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) rename proxy/{http_raw_handler.go => ws_handler.go} (89%) diff --git a/proxy/http_proxy.go b/proxy/http_proxy.go index b531373af..49ef857e3 100644 --- a/proxy/http_proxy.go +++ b/proxy/http_proxy.go @@ -156,11 +156,11 @@ func (p *HTTPProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { case upgrade == "websocket" || upgrade == "Websocket": r.URL = targetURL if targetURL.Scheme == "https" || targetURL.Scheme == "wss" { - h = newRawProxy(targetURL.Host, func(network, address string) (net.Conn, error) { + h = newWSHandler(targetURL.Host, func(network, address string) (net.Conn, error) { return tls.Dial(network, address, tr.(*http.Transport).TLSClientConfig) }) } else { - h = newRawProxy(targetURL.Host, net.Dial) + h = newWSHandler(targetURL.Host, net.Dial) } case accept == "text/event-stream": diff --git a/proxy/http_raw_handler.go b/proxy/ws_handler.go similarity index 89% rename from proxy/http_raw_handler.go rename to proxy/ws_handler.go index f13f4f7fe..35c8710a6 100644 --- a/proxy/http_raw_handler.go +++ b/proxy/ws_handler.go @@ -16,10 +16,11 @@ var conn = metrics.DefaultRegistry.GetCounter("ws.conn") type dialFunc func(network, address string) (net.Conn, error) -// newRawProxy returns an HTTP handler which forwards data between -// an incoming and outgoing TCP connection including the original request. -// This handler establishes a new outgoing connection per request. -func newRawProxy(host string, dial dialFunc) http.Handler { +// newWSHandler returns an HTTP handler which forwards data between +// an incoming and outgoing Websocket connection. It checks whether +// the handshake was completed successfully before forwarding data +// between the client and server. +func newWSHandler(host string, dial dialFunc) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { conn.Inc(1) defer func() { conn.Inc(-1) }() From acf07d47fd122cca9c3f844c67455b7e5eaca70c Mon Sep 17 00:00:00 2001 From: Frank Schroeder Date: Thu, 22 Mar 2018 11:27:45 +0100 Subject: [PATCH 08/31] improve error messages --- proxy/ws_handler.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/proxy/ws_handler.go b/proxy/ws_handler.go index 35c8710a6..8f799b381 100644 --- a/proxy/ws_handler.go +++ b/proxy/ws_handler.go @@ -6,6 +6,7 @@ import ( "log" "net" "net/http" + "strings" "time" "github.com/fabiolb/fabio/metrics" @@ -17,7 +18,7 @@ var conn = metrics.DefaultRegistry.GetCounter("ws.conn") type dialFunc func(network, address string) (net.Conn, error) // newWSHandler returns an HTTP handler which forwards data between -// an incoming and outgoing Websocket connection. It checks whether +// an incoming and outgoing websocket connection. It checks whether // the handshake was completed successfully before forwarding data // between the client and server. func newWSHandler(host string, dial dialFunc) http.Handler { @@ -65,21 +66,24 @@ func newWSHandler(host string, dial dialFunc) http.Handler { n, err := out.Read(b) if err != nil { - log.Printf("[ERROR] Error reading response for %s: %s", r.URL, err) - http.Error(w, "error reading response", http.StatusInternalServerError) + log.Printf("[ERROR] Error reading handshake for %s: %s", r.URL, err) + http.Error(w, "error reading handshake", http.StatusInternalServerError) return } b = b[:n] if m, err := in.Write(b); err != nil || n != m { - log.Printf("[ERROR] Error sending header for %s: %s", r.URL, err) - http.Error(w, "error sending response", http.StatusInternalServerError) + log.Printf("[ERROR] Error sending handshake for %s: %s", r.URL, err) + http.Error(w, "error sending handshake", http.StatusInternalServerError) return } + // https://tools.ietf.org/html/rfc6455#section-1.3 + // The websocket server must respond with HTTP/1.1 101 on successful handshake if !bytes.HasPrefix(b, []byte("HTTP/1.1 101")) { - log.Printf("[INFO] WS Upgrade failed for %s", r.URL) - http.Error(w, "error handling ws upgrade", http.StatusInternalServerError) + firstLine := strings.SplitN(string(b), "\n", 1) + log.Printf("[INFO] Websocket upgrade failed for %s: %s", r.URL, firstLine) + http.Error(w, "websocket upgrade failed", http.StatusInternalServerError) return } From 3744866e35324d199cdaf8c3131e2c8324d33e2a Mon Sep 17 00:00:00 2001 From: Aaron Hurt Date: Thu, 22 Mar 2018 22:55:47 -0500 Subject: [PATCH 09/31] fix contributors link The relative link didn't complete when viewing the master branch. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 01bd9f28c..e07f57b9f 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ urlprefix-:3306 proto=tcp # route external port 3306 ### Contributors This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)]. - + ### Backers From 12ddce8080992f1fd5e16831a5a7b8bdf5e61996 Mon Sep 17 00:00:00 2001 From: Frank Schroeder Date: Mon, 26 Mar 2018 10:44:50 +0200 Subject: [PATCH 10/31] Issue #466: make redirect code more robust Check if the redirect url is not nil. See #466 --- proxy/http_proxy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxy/http_proxy.go b/proxy/http_proxy.go index b531373af..a4415d573 100644 --- a/proxy/http_proxy.go +++ b/proxy/http_proxy.go @@ -99,7 +99,7 @@ func (p *HTTPProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { RawQuery: r.URL.RawQuery, } - if t.RedirectCode != 0 { + if t.RedirectCode != 0 && t.RedirectURL != nil { http.Redirect(w, r, t.RedirectURL.String(), t.RedirectCode) if t.Timer != nil { t.Timer.Update(0) From 7310c766fe0316a205af93019eb08613f7c31a77 Mon Sep 17 00:00:00 2001 From: Craig Day Date: Mon, 14 May 2018 09:23:34 +0800 Subject: [PATCH 11/31] Resetting read deadline --- proxy/ws_handler.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/proxy/ws_handler.go b/proxy/ws_handler.go index 8f799b381..ad4915293 100644 --- a/proxy/ws_handler.go +++ b/proxy/ws_handler.go @@ -87,6 +87,8 @@ func newWSHandler(host string, dial dialFunc) http.Handler { return } + out.SetReadDeadline(time.Time{}); + errc := make(chan error, 2) cp := func(dst io.Writer, src io.Reader) { _, err := io.Copy(dst, src) From 7d224612e945d5be390d27e98480480f3a9baa44 Mon Sep 17 00:00:00 2001 From: Frank Schroeder Date: Wed, 16 May 2018 06:18:01 +0200 Subject: [PATCH 12/31] Fix gofmt issue --- proxy/ws_handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxy/ws_handler.go b/proxy/ws_handler.go index ad4915293..830f6c15e 100644 --- a/proxy/ws_handler.go +++ b/proxy/ws_handler.go @@ -87,7 +87,7 @@ func newWSHandler(host string, dial dialFunc) http.Handler { return } - out.SetReadDeadline(time.Time{}); + out.SetReadDeadline(time.Time{}) errc := make(chan error, 2) cp := func(dst io.Writer, src io.Reader) { From 462a12f06481ae6fae5322e2b6ddb6b0db1e1eea Mon Sep 17 00:00:00 2001 From: Frank Schroeder Date: Wed, 16 May 2018 06:21:42 +0200 Subject: [PATCH 13/31] Fix go vet issue --- proxy/http_integration_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/proxy/http_integration_test.go b/proxy/http_integration_test.go index 27f914afc..453d7fdb8 100644 --- a/proxy/http_integration_test.go +++ b/proxy/http_integration_test.go @@ -117,9 +117,8 @@ func TestProxySTSHeader(t *testing.T) { } func TestProxyChecksHeaderForAccessRules(t *testing.T) { - var hdr http.Header = make(http.Header) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - hdr = r.Header + fmt.Fprintln(w, "OK") })) defer server.Close() From 72e6d52fe4f3e2108aa09db798f5113cffea23fd Mon Sep 17 00:00:00 2001 From: Frank Schroeder Date: Wed, 16 May 2018 06:22:50 +0200 Subject: [PATCH 14/31] Update CHANGELOG --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cb8657e8..9e62c6351 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,17 @@ Thanks to [@tino](https://github.com/tino) for the patch. + * [Issue #421](https://github.com/fabiolb/fabio/issues/421): Fabio routing to wrong backend + + Fabio does not close websocket connections if the connection upgrade fails. This can lead to + connections being routed to the wrong backend if there is another HTTP router like nginx in + front of fabio. The failed websocket connection creates a direct TCP tunnel to the original + backend server and that connection is not closed properly. + + The patches detect an unsuccessful handshake and close the connection properly. + + Thanks to [@craigday](https://github.com/craigday) for the original reporting and debugging. + #### Improvements * [Issue #427](https://github.com/fabiolb/fabio/issues/427): Fabio does not remove service when one of the registered health-checks fail From f3eca46fc97489350ff3b0d50d52f4f0419094d9 Mon Sep 17 00:00:00 2001 From: Frank Schroeder Date: Wed, 16 May 2018 06:36:13 +0200 Subject: [PATCH 15/31] Test with consul 1.0.7 and vault 0.9.6. Build only with go1.10.x --- .travis.yml | 11 +++++------ Makefile | 4 ++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 997382f9f..37f01f1a9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,16 +3,15 @@ dist: trusty language: go go: - - 1.9.x - - "1.10" + - "1.10.x" before_script: - echo $HOSTNAME - mkdir -p $GOPATH/bin - wget --version - - wget https://releases.hashicorp.com/consul/1.0.6/consul_1.0.6_linux_amd64.zip - - wget https://releases.hashicorp.com/vault/0.9.3/vault_0.9.3_linux_amd64.zip - - unzip -d $GOPATH/bin consul_1.0.6_linux_amd64.zip - - unzip -d $GOPATH/bin vault_0.9.3_linux_amd64.zip + - wget https://releases.hashicorp.com/consul/1.0.7/consul_1.0.7_linux_amd64.zip + - wget https://releases.hashicorp.com/vault/0.9.6/vault_0.9.6_linux_amd64.zip + - unzip -d $GOPATH/bin consul_1.0.7_linux_amd64.zip + - unzip -d $GOPATH/bin vault_0.9.6_linux_amd64.zip - vault --version - consul --version diff --git a/Makefile b/Makefile index eaecea775..636f5dcc7 100644 --- a/Makefile +++ b/Makefile @@ -119,8 +119,8 @@ docker-aliases: codeship: go version go env - wget -O ~/consul.zip https://releases.hashicorp.com/consul/1.0.6/consul_1.0.6_linux_amd64.zip - wget -O ~/vault.zip https://releases.hashicorp.com/vault/0.9.3/vault_0.9.3_linux_amd64.zip + wget -O ~/consul.zip https://releases.hashicorp.com/consul/1.0.7/consul_1.0.7_linux_amd64.zip + wget -O ~/vault.zip https://releases.hashicorp.com/vault/0.9.6/vault_0.9.6_linux_amd64.zip unzip -o -d ~/bin ~/consul.zip unzip -o -d ~/bin ~/vault.zip vault --version From 287bc6ad8b240feaf9ff56740cc7028a4443c350 Mon Sep 17 00:00:00 2001 From: Frank Schroeder Date: Wed, 16 May 2018 07:38:43 +0200 Subject: [PATCH 16/31] Work on testing * Tests fail with vault > 0.9.6 and consul > 1.0.6 * Added docker file for running repeatable tests --- .dockerignore | 2 ++ .gitignore | 4 +++- .travis.yml | 4 ++-- Dockerfile-test | 12 ++++++++++++ Makefile | 15 +++++++++++++-- 5 files changed, 32 insertions(+), 5 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile-test diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..97e82cc56 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +fabio +dist/ diff --git a/.gitignore b/.gitignore index 3d308ebe8..0860c372a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,15 @@ *-amd64 -*.out *.orig +*.out *.p12 *.pem *.pprof *.sha256 *.swp +*.tar.gz *.test *.un~ +*.zip .DS_Store .idea .vagrant diff --git a/.travis.yml b/.travis.yml index 37f01f1a9..5d6aea8d9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,9 +9,9 @@ before_script: - echo $HOSTNAME - mkdir -p $GOPATH/bin - wget --version - - wget https://releases.hashicorp.com/consul/1.0.7/consul_1.0.7_linux_amd64.zip + - wget https://releases.hashicorp.com/consul/1.0.6/consul_1.0.6_linux_amd64.zip - wget https://releases.hashicorp.com/vault/0.9.6/vault_0.9.6_linux_amd64.zip - - unzip -d $GOPATH/bin consul_1.0.7_linux_amd64.zip + - unzip -d $GOPATH/bin consul_1.0.6_linux_amd64.zip - unzip -d $GOPATH/bin vault_0.9.6_linux_amd64.zip - vault --version - consul --version diff --git a/Dockerfile-test b/Dockerfile-test new file mode 100644 index 000000000..6ab46d54d --- /dev/null +++ b/Dockerfile-test @@ -0,0 +1,12 @@ +FROM ubuntu +COPY consul_1.0.6_linux_amd64.zip /tmp +COPY vault_0.9.6_linux_amd64.zip /tmp +COPY go1.10.2.linux-amd64.tar.gz /tmp +RUN apt-get update && apt-get -y install unzip make git-core +RUN unzip /tmp/consul*.zip -d /usr/local/bin +RUN unzip /tmp/vault*.zip -d /usr/local/bin +RUN tar -C /usr/local -x -f /tmp/go*.tar.gz +ENV PATH=/usr/local/go/bin:/root/go/bin:$PATH +WORKDIR /root/go/src/github.com/fabiolb/fabio +COPY . . +CMD "/bin/bash" diff --git a/Makefile b/Makefile index 636f5dcc7..aed6ca7d4 100644 --- a/Makefile +++ b/Makefile @@ -30,6 +30,10 @@ GOVENDOR = $(shell which govendor) # VENDORFMT is the path to the vendorfmt binary. VENDORFMT = $(shell which vendorfmt) +CONSUL_VERSION=1.0.6 +VAULT_VERSION=0.9.6 +GO_VERSION=1.10.2 + # all is the default target all: test @@ -115,12 +119,19 @@ docker-aliases: docker push magiconair/fabio:$(VERSION)-$(GOVERSION) docker push magiconair/fabio:latest +docker-test: + test -r consul_$(CONSUL_VERSION)_linux_amd64.zip || wget https://releases.hashicorp.com/consul/$(CONSUL_VERSION)/consul_$(CONSUL_VERSION)_linux_amd64.zip + test -r vault_$(VAULT_VERSION)_linux_amd64.zip || wget https://releases.hashicorp.com/vault/$(VAULT_VERSION)/vault_$(VAULT_VERSION)_linux_amd64.zip unzip -o -d ~/bin ~/consul.zip + test -r go$(GO_VERSION).linux-amd64.tar.gz || wget https://dl.google.com/go/go1.10.2.linux-amd64.tar.gz + docker build -t test-fabio -f Dockerfile-test . + docker run -it test-fabio + # codeship runs the CI on codeship codeship: go version go env - wget -O ~/consul.zip https://releases.hashicorp.com/consul/1.0.7/consul_1.0.7_linux_amd64.zip - wget -O ~/vault.zip https://releases.hashicorp.com/vault/0.9.6/vault_0.9.6_linux_amd64.zip + wget -O ~/consul.zip https://releases.hashicorp.com/consul/$(CONSUL_VERSION)/consul_$(CONSUL_VERSION)_linux_amd64.zip + wget -O ~/vault.zip https://releases.hashicorp.com/vault/$(VAULT_VERSION)/vault_$(VAULT_VERSION)_linux_amd64.zip unzip -o -d ~/bin ~/consul.zip unzip -o -d ~/bin ~/vault.zip vault --version From 23eb06e4625cbf697a05b80c63aab94095aa0ea3 Mon Sep 17 00:00:00 2001 From: Frank Schroeder Date: Wed, 16 May 2018 08:04:43 +0200 Subject: [PATCH 17/31] Update CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e62c6351..8c724ca84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ### Unreleased +#### Notes + + * Tests fail with Vault > 0.9.6 and Consul > 1.0.6. + #### Breaking Changes * None From 3a04a7c4197761c380e2cd38824298078828d7c5 Mon Sep 17 00:00:00 2001 From: Frank Schroeder Date: Wed, 16 May 2018 08:10:40 +0200 Subject: [PATCH 18/31] Use only codeship for CI --- .travis.yml | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 5d6aea8d9..000000000 --- a/.travis.yml +++ /dev/null @@ -1,17 +0,0 @@ -dist: trusty - -language: go - -go: - - "1.10.x" - -before_script: - - echo $HOSTNAME - - mkdir -p $GOPATH/bin - - wget --version - - wget https://releases.hashicorp.com/consul/1.0.6/consul_1.0.6_linux_amd64.zip - - wget https://releases.hashicorp.com/vault/0.9.6/vault_0.9.6_linux_amd64.zip - - unzip -d $GOPATH/bin consul_1.0.6_linux_amd64.zip - - unzip -d $GOPATH/bin vault_0.9.6_linux_amd64.zip - - vault --version - - consul --version From d690f6b7e945b290602862db21021ecc6050e0bf Mon Sep 17 00:00:00 2001 From: Frank Schroeder Date: Wed, 16 May 2018 08:29:37 +0200 Subject: [PATCH 19/31] Drop travis badge --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index e07f57b9f..040c36da0 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,6 @@ Release License MIT Codeship CI Status - Travis CI Status Downloads Docker Pulls magiconair Docker Pulls fabiolb From 9db85e306c1abc1505df0088ca5c95a042402c3b Mon Sep 17 00:00:00 2001 From: Frank Schroeder Date: Wed, 16 May 2018 08:30:39 +0200 Subject: [PATCH 20/31] Remove redundant $(GO) variable from Makefile. --- Makefile | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/Makefile b/Makefile index aed6ca7d4..8c6556356 100644 --- a/Makefile +++ b/Makefile @@ -10,10 +10,6 @@ LAST_TAG = $(shell git describe --abbrev=0) # e.g. 1.5.5 VERSION = $(shell git describe --abbrev=0 | cut -c 2-) -# GO runs the go binary with garbage collection disabled for faster builds. -# Do not specify a full path for go since travis will fail. -GO = go - # GOFLAGS is the flags for the go compiler. Currently, only the version number is # passed to the linker via the -ldflags. GOFLAGS = -ldflags "-X main.version=$(CUR_TAG)" @@ -50,20 +46,20 @@ help: # build compiles fabio and the test dependencies build: checkdeps vendorfmt gofmt - $(GO) build + go build # test runs the tests test: build - $(GO) test -v -test.timeout 15s `go list ./... | grep -v '/vendor/'` + go test -v -test.timeout 15s `go list ./... | grep -v '/vendor/'` # checkdeps ensures that all required dependencies are vendored in checkdeps: - [ -x "$(GOVENDOR)" ] || $(GO) get -u github.com/kardianos/govendor + [ -x "$(GOVENDOR)" ] || go get -u github.com/kardianos/govendor govendor list +e | grep '^ e ' && { echo "Found missing packages. Please run 'govendor add +e'"; exit 1; } || : echo # vendorfmt ensures that the vendor/vendor.json file is formatted correctly vendorfmt: - [ -x "$(VENDORFMT)" ] || $(GO) get -u github.com/magiconair/vendorfmt/cmd/vendorfmt + [ -x "$(VENDORFMT)" ] || go get -u github.com/magiconair/vendorfmt/cmd/vendorfmt vendorfmt # gofmt runs gofmt on the code @@ -72,11 +68,11 @@ gofmt: # linux builds a linux binary linux: - GOOS=linux GOARCH=amd64 $(GO) build -tags netgo $(GOFLAGS) + GOOS=linux GOARCH=amd64 go build -tags netgo $(GOFLAGS) # install runs go install install: - $(GO) install $(GOFLAGS) + go install $(GOFLAGS) # pkg builds a fabio.tar.gz package with only fabio in it pkg: build test @@ -140,7 +136,7 @@ codeship: # clean removes intermediate files clean: - $(GO) clean + go clean rm -rf pkg dist fabio find . -name '*.test' -delete From 80ffa49fceda3132892fb260e048f332bb5dafe9 Mon Sep 17 00:00:00 2001 From: Frank Schroeder Date: Wed, 16 May 2018 08:31:14 +0200 Subject: [PATCH 21/31] Parameterize docker-test and make var names unambiguous. --- Dockerfile-test | 15 +++++++++------ Makefile | 36 ++++++++++++++++++++++++++---------- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/Dockerfile-test b/Dockerfile-test index 6ab46d54d..c7d56d2fc 100644 --- a/Dockerfile-test +++ b/Dockerfile-test @@ -1,11 +1,14 @@ FROM ubuntu -COPY consul_1.0.6_linux_amd64.zip /tmp -COPY vault_0.9.6_linux_amd64.zip /tmp -COPY go1.10.2.linux-amd64.tar.gz /tmp RUN apt-get update && apt-get -y install unzip make git-core -RUN unzip /tmp/consul*.zip -d /usr/local/bin -RUN unzip /tmp/vault*.zip -d /usr/local/bin -RUN tar -C /usr/local -x -f /tmp/go*.tar.gz +ARG consul_version +ARG vault_version +ARG go_version +COPY consul_${consul_version}_linux_amd64.zip /tmp +COPY vault_${vault_version}_linux_amd64.zip /tmp +COPY go${go_version}.linux-amd64.tar.gz /tmp +RUN unzip /tmp/consul_${consul_version}_linux_amd64.zip -d /usr/local/bin +RUN unzip /tmp/vault_${vault_version}_linux_amd64.zip -d /usr/local/bin +RUN tar -C /usr/local -x -f /tmp/go${go_version}.linux-amd64.tar.gz ENV PATH=/usr/local/go/bin:/root/go/bin:$PATH WORKDIR /root/go/src/github.com/fabiolb/fabio COPY . . diff --git a/Makefile b/Makefile index 8c6556356..254877fa8 100644 --- a/Makefile +++ b/Makefile @@ -26,9 +26,10 @@ GOVENDOR = $(shell which govendor) # VENDORFMT is the path to the vendorfmt binary. VENDORFMT = $(shell which vendorfmt) -CONSUL_VERSION=1.0.6 -VAULT_VERSION=0.9.6 -GO_VERSION=1.10.2 +# pin versions for CI builds +CI_CONSUL_VERSION=1.0.6 +CI_VAULT_VERSION=0.9.6 +CI_GO_VERSION=1.10.2 # all is the default target all: test @@ -115,19 +116,34 @@ docker-aliases: docker push magiconair/fabio:$(VERSION)-$(GOVERSION) docker push magiconair/fabio:latest +# docker-test runs make test in a Docker container with +# pinned versions of the external dependencies +# +# We download the binaries outside the Docker build to +# cache the binaries and prevent repeated downloads since +# ADD downloads the file every time. docker-test: - test -r consul_$(CONSUL_VERSION)_linux_amd64.zip || wget https://releases.hashicorp.com/consul/$(CONSUL_VERSION)/consul_$(CONSUL_VERSION)_linux_amd64.zip - test -r vault_$(VAULT_VERSION)_linux_amd64.zip || wget https://releases.hashicorp.com/vault/$(VAULT_VERSION)/vault_$(VAULT_VERSION)_linux_amd64.zip unzip -o -d ~/bin ~/consul.zip - test -r go$(GO_VERSION).linux-amd64.tar.gz || wget https://dl.google.com/go/go1.10.2.linux-amd64.tar.gz - docker build -t test-fabio -f Dockerfile-test . - docker run -it test-fabio + test -r consul_$(CI_CONSUL_VERSION)_linux_amd64.zip || \ + wget https://releases.hashicorp.com/consul/$(CI_CONSUL_VERSION)/consul_$(CI_CONSUL_VERSION)_linux_amd64.zip + test -r vault_$(CI_VAULT_VERSION)_linux_amd64.zip || \ + wget https://releases.hashicorp.com/vault/$(CI_VAULT_VERSION)/vault_$(CI_VAULT_VERSION)_linux_amd64.zip + test -r go$(CI_GO_VERSION).linux-amd64.tar.gz || \ + wget https://dl.google.com/go/go$(CI_GO_VERSION).linux-amd64.tar.gz + docker build \ + --build-arg consul_version=$(CI_CONSUL_VERSION) \ + --build-arg vault_version=$(CI_VAULT_VERSION) \ + --build-arg go_version=$(CI_GO_VERSION) \ + -t test-fabio \ + -f Dockerfile-test \ + . + docker run -it test-fabio make test # codeship runs the CI on codeship codeship: go version go env - wget -O ~/consul.zip https://releases.hashicorp.com/consul/$(CONSUL_VERSION)/consul_$(CONSUL_VERSION)_linux_amd64.zip - wget -O ~/vault.zip https://releases.hashicorp.com/vault/$(VAULT_VERSION)/vault_$(VAULT_VERSION)_linux_amd64.zip + wget -O ~/consul.zip https://releases.hashicorp.com/consul/$(CI_CONSUL_VERSION)/consul_$(CI_CONSUL_VERSION)_linux_amd64.zip + wget -O ~/vault.zip https://releases.hashicorp.com/vault/$(CI_VAULT_VERSION)/vault_$(CI_VAULT_VERSION)_linux_amd64.zip unzip -o -d ~/bin ~/consul.zip unzip -o -d ~/bin ~/vault.zip vault --version From 93dfbe033744156353c2b78b0c7d7c376241c234 Mon Sep 17 00:00:00 2001 From: Frank Schroeder Date: Wed, 16 May 2018 08:39:50 +0200 Subject: [PATCH 22/31] Use docker-test for releases --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 254877fa8..6d2ff11ba 100644 --- a/Makefile +++ b/Makefile @@ -87,7 +87,7 @@ pkg: build test # later targets can pick up the new tag value. release: $(MAKE) tag - $(MAKE) preflight test gorelease homebrew docker-aliases + $(MAKE) preflight docker-test gorelease homebrew docker-aliases # preflight runs some checks before a release preflight: From af5f901e93b227738b9a583143ea221f168a280d Mon Sep 17 00:00:00 2001 From: Frank Schroeder Date: Wed, 16 May 2018 08:36:41 +0200 Subject: [PATCH 23/31] Prepare release 1.5.9 --- CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c724ca84..b3b263097 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,12 @@ ## Changelog -### Unreleased +### [v1.5.9](https://github.com/fabiolb/fabio/releases/tag/v1.5.9) - 16 May 2018 #### Notes - * Tests fail with Vault > 0.9.6 and Consul > 1.0.6. + * [Issue #494](https://github.com/fabiolb/fabio/issues/494): Tests fail with Vault > 0.9.6 and Consul > 1.0.6 + + Needs more investigation. #### Breaking Changes From 1fb35b1d864ad2ca85fae64dccbe25691813e782 Mon Sep 17 00:00:00 2001 From: Frank Schroeder Date: Wed, 16 May 2018 08:40:24 +0200 Subject: [PATCH 24/31] Release v1.5.9 --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 601503c6a..86565f4e6 100644 --- a/main.go +++ b/main.go @@ -43,7 +43,7 @@ import ( // It is also set by the linker when fabio // is built via the Makefile or the build/docker.sh // script to ensure the correct version nubmer -var version = "1.5.8" +var version = "1.5.9" var shuttingDown int32 From 4e7600492d810df7f36e1c36628c73f95f4fba64 Mon Sep 17 00:00:00 2001 From: "hao.dong" Date: Wed, 16 May 2018 15:34:56 +0800 Subject: [PATCH 25/31] Delete an unused global variable logOutput logOutput is not used as a global variable, it is re-initialized as short-term variable in func main(). --- main.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/main.go b/main.go index 86565f4e6..264c30978 100644 --- a/main.go +++ b/main.go @@ -47,8 +47,6 @@ var version = "1.5.9" var shuttingDown int32 -var logOutput logger.LevelWriter - func main() { logOutput := logger.NewLevelWriter(os.Stderr, "INFO", "2017/01/01 00:00:00 ") log.SetOutput(logOutput) From 58a07d9f441329c0c0a103683b71856379b5f6d5 Mon Sep 17 00:00:00 2001 From: Petr Mikusek Date: Sat, 19 May 2018 09:58:52 +0200 Subject: [PATCH 26/31] Fix changelog link in docs footer --- docs/layouts/partials/footer.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/layouts/partials/footer.html b/docs/layouts/partials/footer.html index d6b48738b..ef46ae5aa 100644 --- a/docs/layouts/partials/footer.html +++ b/docs/layouts/partials/footer.html @@ -13,7 +13,7 @@