From 4cc91674e234e1ccaace7118109a750551b369df Mon Sep 17 00:00:00 2001 From: Mohammad Gufran Date: Fri, 16 Feb 2018 14:56:14 +0530 Subject: [PATCH 1/2] 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 2/2] 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 {