summaryrefslogtreecommitdiff
path: root/modules/caddyhttp/reverseproxy/fastcgi
diff options
context:
space:
mode:
authorMatt Holt <mholt@users.noreply.github.com>2019-09-09 21:46:21 -0600
committerGitHub <noreply@github.com>2019-09-09 21:46:21 -0600
commit44b7ce98505ab8a34f6c632e661dd2cfae475a17 (patch)
tree4cd125e485047419fd19098007280b013906a0bc /modules/caddyhttp/reverseproxy/fastcgi
parent9169cd43d49236c69d5c9b7c556cb0ac0c9ce497 (diff)
parentb4f4fcd437c2f9816f9511217bde703679808679 (diff)
Merge pull request #2737 from caddyserver/fastcgi (reverse proxy!)
v2: Refactor reverse proxy and add FastCGI support
Diffstat (limited to 'modules/caddyhttp/reverseproxy/fastcgi')
-rw-r--r--modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go54
-rw-r--r--modules/caddyhttp/reverseproxy/fastcgi/client.go578
-rw-r--r--modules/caddyhttp/reverseproxy/fastcgi/client_test.go301
-rw-r--r--modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go301
4 files changed, 1234 insertions, 0 deletions
diff --git a/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go b/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go
new file mode 100644
index 0000000..c8b9f63
--- /dev/null
+++ b/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go
@@ -0,0 +1,54 @@
+// Copyright 2015 Matthew Holt and The Caddy Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package fastcgi
+
+import "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
+
+// UnmarshalCaddyfile deserializes Caddyfile tokens into h.
+//
+// transport fastcgi {
+// root <path>
+// split <at>
+// env <key> <value>
+// }
+//
+func (t *Transport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
+ for d.NextBlock() {
+ switch d.Val() {
+ case "root":
+ if !d.NextArg() {
+ return d.ArgErr()
+ }
+ t.Root = d.Val()
+
+ case "split":
+ if !d.NextArg() {
+ return d.ArgErr()
+ }
+ t.SplitPath = d.Val()
+
+ case "env":
+ args := d.RemainingArgs()
+ if len(args) != 2 {
+ return d.ArgErr()
+ }
+ t.EnvVars = append(t.EnvVars, [2]string{args[0], args[1]})
+
+ default:
+ return d.Errf("unrecognized subdirective %s", d.Val())
+ }
+ }
+ return nil
+}
diff --git a/modules/caddyhttp/reverseproxy/fastcgi/client.go b/modules/caddyhttp/reverseproxy/fastcgi/client.go
new file mode 100644
index 0000000..ae0de00
--- /dev/null
+++ b/modules/caddyhttp/reverseproxy/fastcgi/client.go
@@ -0,0 +1,578 @@
+// Copyright 2015 Matthew Holt and The Caddy Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Forked Jan. 2015 from http://bitbucket.org/PinIdea/fcgi_client
+// (which is forked from https://code.google.com/p/go-fastcgi-client/).
+// This fork contains several fixes and improvements by Matt Holt and
+// other contributors to the Caddy project.
+
+// Copyright 2012 Junqing Tan <ivan@mysqlab.net> and The Go Authors
+// Use of this source code is governed by a BSD-style
+// Part of source code is from Go fcgi package
+
+package fastcgi
+
+import (
+ "bufio"
+ "bytes"
+ "context"
+ "encoding/binary"
+ "errors"
+ "io"
+ "io/ioutil"
+ "mime/multipart"
+ "net"
+ "net/http"
+ "net/http/httputil"
+ "net/textproto"
+ "net/url"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+)
+
+// FCGIListenSockFileno describes listen socket file number.
+const FCGIListenSockFileno uint8 = 0
+
+// FCGIHeaderLen describes header length.
+const FCGIHeaderLen uint8 = 8
+
+// Version1 describes the version.
+const Version1 uint8 = 1
+
+// FCGINullRequestID describes the null request ID.
+const FCGINullRequestID uint8 = 0
+
+// FCGIKeepConn describes keep connection mode.
+const FCGIKeepConn uint8 = 1
+
+const (
+ // BeginRequest is the begin request flag.
+ BeginRequest uint8 = iota + 1
+ // AbortRequest is the abort request flag.
+ AbortRequest
+ // EndRequest is the end request flag.
+ EndRequest
+ // Params is the parameters flag.
+ Params
+ // Stdin is the standard input flag.
+ Stdin
+ // Stdout is the standard output flag.
+ Stdout
+ // Stderr is the standard error flag.
+ Stderr
+ // Data is the data flag.
+ Data
+ // GetValues is the get values flag.
+ GetValues
+ // GetValuesResult is the get values result flag.
+ GetValuesResult
+ // UnknownType is the unknown type flag.
+ UnknownType
+ // MaxType is the maximum type flag.
+ MaxType = UnknownType
+)
+
+const (
+ // Responder is the responder flag.
+ Responder uint8 = iota + 1
+ // Authorizer is the authorizer flag.
+ Authorizer
+ // Filter is the filter flag.
+ Filter
+)
+
+const (
+ // RequestComplete is the completed request flag.
+ RequestComplete uint8 = iota
+ // CantMultiplexConns is the multiplexed connections flag.
+ CantMultiplexConns
+ // Overloaded is the overloaded flag.
+ Overloaded
+ // UnknownRole is the unknown role flag.
+ UnknownRole
+)
+
+const (
+ // MaxConns is the maximum connections flag.
+ MaxConns string = "MAX_CONNS"
+ // MaxRequests is the maximum requests flag.
+ MaxRequests string = "MAX_REQS"
+ // MultiplexConns is the multiplex connections flag.
+ MultiplexConns string = "MPXS_CONNS"
+)
+
+const (
+ maxWrite = 65500 // 65530 may work, but for compatibility
+ maxPad = 255
+)
+
+type header struct {
+ Version uint8
+ Type uint8
+ ID uint16
+ ContentLength uint16
+ PaddingLength uint8
+ Reserved uint8
+}
+
+// for padding so we don't have to allocate all the time
+// not synchronized because we don't care what the contents are
+var pad [maxPad]byte
+
+func (h *header) init(recType uint8, reqID uint16, contentLength int) {
+ h.Version = 1
+ h.Type = recType
+ h.ID = reqID
+ h.ContentLength = uint16(contentLength)
+ h.PaddingLength = uint8(-contentLength & 7)
+}
+
+type record struct {
+ h header
+ rbuf []byte
+}
+
+func (rec *record) read(r io.Reader) (buf []byte, err error) {
+ if err = binary.Read(r, binary.BigEndian, &rec.h); err != nil {
+ return
+ }
+ if rec.h.Version != 1 {
+ err = errors.New("fcgi: invalid header version")
+ return
+ }
+ if rec.h.Type == EndRequest {
+ err = io.EOF
+ return
+ }
+ n := int(rec.h.ContentLength) + int(rec.h.PaddingLength)
+ if len(rec.rbuf) < n {
+ rec.rbuf = make([]byte, n)
+ }
+ if _, err = io.ReadFull(r, rec.rbuf[:n]); err != nil {
+ return
+ }
+ buf = rec.rbuf[:int(rec.h.ContentLength)]
+
+ return
+}
+
+// FCGIClient implements a FastCGI client, which is a standard for
+// interfacing external applications with Web servers.
+type FCGIClient struct {
+ mutex sync.Mutex
+ rwc io.ReadWriteCloser
+ h header
+ buf bytes.Buffer
+ stderr bytes.Buffer
+ keepAlive bool
+ reqID uint16
+}
+
+// DialWithDialerContext connects to the fcgi responder at the specified network address, using custom net.Dialer
+// and a context.
+// See func net.Dial for a description of the network and address parameters.
+func DialWithDialerContext(ctx context.Context, network, address string, dialer net.Dialer) (fcgi *FCGIClient, err error) {
+ var conn net.Conn
+ conn, err = dialer.DialContext(ctx, network, address)
+ if err != nil {
+ return
+ }
+
+ fcgi = &FCGIClient{
+ rwc: conn,
+ keepAlive: false,
+ reqID: 1,
+ }
+
+ return
+}
+
+// DialContext is like Dial but passes ctx to dialer.Dial.
+func DialContext(ctx context.Context, network, address string) (fcgi *FCGIClient, err error) {
+ // TODO: why not set timeout here?
+ return DialWithDialerContext(ctx, network, address, net.Dialer{})
+}
+
+// Dial connects to the fcgi responder at the specified network address, using default net.Dialer.
+// See func net.Dial for a description of the network and address parameters.
+func Dial(network, address string) (fcgi *FCGIClient, err error) {
+ return DialContext(context.Background(), network, address)
+}
+
+// Close closes fcgi connection
+func (c *FCGIClient) Close() {
+ c.rwc.Close()
+}
+
+func (c *FCGIClient) writeRecord(recType uint8, content []byte) (err error) {
+ c.mutex.Lock()
+ defer c.mutex.Unlock()
+ c.buf.Reset()
+ c.h.init(recType, c.reqID, len(content))
+ if err := binary.Write(&c.buf, binary.BigEndian, c.h); err != nil {
+ return err
+ }
+ if _, err := c.buf.Write(content); err != nil {
+ return err
+ }
+ if _, err := c.buf.Write(pad[:c.h.PaddingLength]); err != nil {
+ return err
+ }
+ _, err = c.rwc.Write(c.buf.Bytes())
+ return err
+}
+
+func (c *FCGIClient) writeBeginRequest(role uint16, flags uint8) error {
+ b := [8]byte{byte(role >> 8), byte(role), flags}
+ return c.writeRecord(BeginRequest, b[:])
+}
+
+func (c *FCGIClient) writeEndRequest(appStatus int, protocolStatus uint8) error {
+ b := make([]byte, 8)
+ binary.BigEndian.PutUint32(b, uint32(appStatus))
+ b[4] = protocolStatus
+ return c.writeRecord(EndRequest, b)
+}
+
+func (c *FCGIClient) writePairs(recType uint8, pairs map[string]string) error {
+ w := newWriter(c, recType)
+ b := make([]byte, 8)
+ nn := 0
+ for k, v := range pairs {
+ m := 8 + len(k) + len(v)
+ if m > maxWrite {
+ // param data size exceed 65535 bytes"
+ vl := maxWrite - 8 - len(k)
+ v = v[:vl]
+ }
+ n := encodeSize(b, uint32(len(k)))
+ n += encodeSize(b[n:], uint32(len(v)))
+ m = n + len(k) + len(v)
+ if (nn + m) > maxWrite {
+ w.Flush()
+ nn = 0
+ }
+ nn += m
+ if _, err := w.Write(b[:n]); err != nil {
+ return err
+ }
+ if _, err := w.WriteString(k); err != nil {
+ return err
+ }
+ if _, err := w.WriteString(v); err != nil {
+ return err
+ }
+ }
+ w.Close()
+ return nil
+}
+
+func encodeSize(b []byte, size uint32) int {
+ if size > 127 {
+ size |= 1 << 31
+ binary.BigEndian.PutUint32(b, size)
+ return 4
+ }
+ b[0] = byte(size)
+ return 1
+}
+
+// bufWriter encapsulates bufio.Writer but also closes the underlying stream when
+// Closed.
+type bufWriter struct {
+ closer io.Closer
+ *bufio.Writer
+}
+
+func (w *bufWriter) Close() error {
+ if err := w.Writer.Flush(); err != nil {
+ w.closer.Close()
+ return err
+ }
+ return w.closer.Close()
+}
+
+func newWriter(c *FCGIClient, recType uint8) *bufWriter {
+ s := &streamWriter{c: c, recType: recType}
+ w := bufio.NewWriterSize(s, maxWrite)
+ return &bufWriter{s, w}
+}
+
+// streamWriter abstracts out the separation of a stream into discrete records.
+// It only writes maxWrite bytes at a time.
+type streamWriter struct {
+ c *FCGIClient
+ recType uint8
+}
+
+func (w *streamWriter) Write(p []byte) (int, error) {
+ nn := 0
+ for len(p) > 0 {
+ n := len(p)
+ if n > maxWrite {
+ n = maxWrite
+ }
+ if err := w.c.writeRecord(w.recType, p[:n]); err != nil {
+ return nn, err
+ }
+ nn += n
+ p = p[n:]
+ }
+ return nn, nil
+}
+
+func (w *streamWriter) Close() error {
+ // send empty record to close the stream
+ return w.c.writeRecord(w.recType, nil)
+}
+
+type streamReader struct {
+ c *FCGIClient
+ buf []byte
+}
+
+func (w *streamReader) Read(p []byte) (n int, err error) {
+
+ if len(p) > 0 {
+ if len(w.buf) == 0 {
+
+ // filter outputs for error log
+ for {
+ rec := &record{}
+ var buf []byte
+ buf, err = rec.read(w.c.rwc)
+ if err != nil {
+ return
+ }
+ // standard error output
+ if rec.h.Type == Stderr {
+ w.c.stderr.Write(buf)
+ continue
+ }
+ w.buf = buf
+ break
+ }
+ }
+
+ n = len(p)
+ if n > len(w.buf) {
+ n = len(w.buf)
+ }
+ copy(p, w.buf[:n])
+ w.buf = w.buf[n:]
+ }
+
+ return
+}
+
+// Do made the request and returns a io.Reader that translates the data read
+// from fcgi responder out of fcgi packet before returning it.
+func (c *FCGIClient) Do(p map[string]string, req io.Reader) (r io.Reader, err error) {
+ err = c.writeBeginRequest(uint16(Responder), 0)
+ if err != nil {
+ return
+ }
+
+ err = c.writePairs(Params, p)
+ if err != nil {
+ return
+ }
+
+ body := newWriter(c, Stdin)
+ if req != nil {
+ _, _ = io.Copy(body, req)
+ }
+ body.Close()
+
+ r = &streamReader{c: c}
+ return
+}
+
+// clientCloser is a io.ReadCloser. It wraps a io.Reader with a Closer
+// that closes FCGIClient connection.
+type clientCloser struct {
+ *FCGIClient
+ io.Reader
+}
+
+func (f clientCloser) Close() error { return f.rwc.Close() }
+
+// Request returns a HTTP Response with Header and Body
+// from fcgi responder
+func (c *FCGIClient) Request(p map[string]string, req io.Reader) (resp *http.Response, err error) {
+ r, err := c.Do(p, req)
+ if err != nil {
+ return
+ }
+
+ rb := bufio.NewReader(r)
+ tp := textproto.NewReader(rb)
+ resp = new(http.Response)
+
+ // Parse the response headers.
+ mimeHeader, err := tp.ReadMIMEHeader()
+ if err != nil && err != io.EOF {
+ return
+ }
+ resp.Header = http.Header(mimeHeader)
+
+ if resp.Header.Get("Status") != "" {
+ statusParts := strings.SplitN(resp.Header.Get("Status"), " ", 2)
+ resp.StatusCode, err = strconv.Atoi(statusParts[0])
+ if err != nil {
+ return
+ }
+ if len(statusParts) > 1 {
+ resp.Status = statusParts[1]
+ }
+
+ } else {
+ resp.StatusCode = http.StatusOK
+ }
+
+ // TODO: fixTransferEncoding ?
+ resp.TransferEncoding = resp.Header["Transfer-Encoding"]
+ resp.ContentLength, _ = strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
+
+ if chunked(resp.TransferEncoding) {
+ resp.Body = clientCloser{c, httputil.NewChunkedReader(rb)}
+ } else {
+ resp.Body = clientCloser{c, ioutil.NopCloser(rb)}
+ }
+ return
+}
+
+// Get issues a GET request to the fcgi responder.
+func (c *FCGIClient) Get(p map[string]string, body io.Reader, l int64) (resp *http.Response, err error) {
+
+ p["REQUEST_METHOD"] = "GET"
+ p["CONTENT_LENGTH"] = strconv.FormatInt(l, 10)
+
+ return c.Request(p, body)
+}
+
+// Head issues a HEAD request to the fcgi responder.
+func (c *FCGIClient) Head(p map[string]string) (resp *http.Response, err error) {
+
+ p["REQUEST_METHOD"] = "HEAD"
+ p["CONTENT_LENGTH"] = "0"
+
+ return c.Request(p, nil)
+}
+
+// Options issues an OPTIONS request to the fcgi responder.
+func (c *FCGIClient) Options(p map[string]string) (resp *http.Response, err error) {
+
+ p["REQUEST_METHOD"] = "OPTIONS"
+ p["CONTENT_LENGTH"] = "0"
+
+ return c.Request(p, nil)
+}
+
+// Post issues a POST request to the fcgi responder. with request body
+// in the format that bodyType specified
+func (c *FCGIClient) Post(p map[string]string, method string, bodyType string, body io.Reader, l int64) (resp *http.Response, err error) {
+ if p == nil {
+ p = make(map[string]string)
+ }
+
+ p["REQUEST_METHOD"] = strings.ToUpper(method)
+
+ if len(p["REQUEST_METHOD"]) == 0 || p["REQUEST_METHOD"] == "GET" {
+ p["REQUEST_METHOD"] = "POST"
+ }
+
+ p["CONTENT_LENGTH"] = strconv.FormatInt(l, 10)
+ if len(bodyType) > 0 {
+ p["CONTENT_TYPE"] = bodyType
+ } else {
+ p["CONTENT_TYPE"] = "application/x-www-form-urlencoded"
+ }
+
+ return c.Request(p, body)
+}
+
+// PostForm issues a POST to the fcgi responder, with form
+// as a string key to a list values (url.Values)
+func (c *FCGIClient) PostForm(p map[string]string, data url.Values) (resp *http.Response, err error) {
+ body := bytes.NewReader([]byte(data.Encode()))
+ return c.Post(p, "POST", "application/x-www-form-urlencoded", body, int64(body.Len()))
+}
+
+// PostFile issues a POST to the fcgi responder in multipart(RFC 2046) standard,
+// with form as a string key to a list values (url.Values),
+// and/or with file as a string key to a list file path.
+func (c *FCGIClient) PostFile(p map[string]string, data url.Values, file map[string]string) (resp *http.Response, err error) {
+ buf := &bytes.Buffer{}
+ writer := multipart.NewWriter(buf)
+ bodyType := writer.FormDataContentType()
+
+ for key, val := range data {
+ for _, v0 := range val {
+ err = writer.WriteField(key, v0)
+ if err != nil {
+ return
+ }
+ }
+ }
+
+ for key, val := range file {
+ fd, e := os.Open(val)
+ if e != nil {
+ return nil, e
+ }
+ defer fd.Close()
+
+ part, e := writer.CreateFormFile(key, filepath.Base(val))
+ if e != nil {
+ return nil, e
+ }
+ _, err = io.Copy(part, fd)
+ if err != nil {
+ return
+ }
+ }
+
+ err = writer.Close()
+ if err != nil {
+ return
+ }
+
+ return c.Post(p, "POST", bodyType, buf, int64(buf.Len()))
+}
+
+// SetReadTimeout sets the read timeout for future calls that read from the
+// fcgi responder. A zero value for t means no timeout will be set.
+func (c *FCGIClient) SetReadTimeout(t time.Duration) error {
+ if conn, ok := c.rwc.(net.Conn); ok && t != 0 {
+ return conn.SetReadDeadline(time.Now().Add(t))
+ }
+ return nil
+}
+
+// SetWriteTimeout sets the write timeout for future calls that send data to
+// the fcgi responder. A zero value for t means no timeout will be set.
+func (c *FCGIClient) SetWriteTimeout(t time.Duration) error {
+ if conn, ok := c.rwc.(net.Conn); ok && t != 0 {
+ return conn.SetWriteDeadline(time.Now().Add(t))
+ }
+ return nil
+}
+
+// Checks whether chunked is part of the encodings stack
+func chunked(te []string) bool { return len(te) > 0 && te[0] == "chunked" }
diff --git a/modules/caddyhttp/reverseproxy/fastcgi/client_test.go b/modules/caddyhttp/reverseproxy/fastcgi/client_test.go
new file mode 100644
index 0000000..c090f3c
--- /dev/null
+++ b/modules/caddyhttp/reverseproxy/fastcgi/client_test.go
@@ -0,0 +1,301 @@
+// Copyright 2015 Matthew Holt and The Caddy Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// NOTE: These tests were adapted from the original
+// repository from which this package was forked.
+// The tests are slow (~10s) and in dire need of rewriting.
+// As such, the tests have been disabled to speed up
+// automated builds until they can be properly written.
+
+package fastcgi
+
+import (
+ "bytes"
+ "crypto/md5"
+ "encoding/binary"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "log"
+ "math/rand"
+ "net"
+ "net/http"
+ "net/http/fcgi"
+ "net/url"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "testing"
+ "time"
+)
+
+// test fcgi protocol includes:
+// Get, Post, Post in multipart/form-data, and Post with files
+// each key should be the md5 of the value or the file uploaded
+// specify remote fcgi responder ip:port to test with php
+// test failed if the remote fcgi(script) failed md5 verification
+// and output "FAILED" in response
+const (
+ scriptFile = "/tank/www/fcgic_test.php"
+ //ipPort = "remote-php-serv:59000"
+ ipPort = "127.0.0.1:59000"
+)
+
+var globalt *testing.T
+
+type FastCGIServer struct{}
+
+func (s FastCGIServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
+
+ if err := req.ParseMultipartForm(100000000); err != nil {
+ log.Printf("[ERROR] failed to parse: %v", err)
+ }
+
+ stat := "PASSED"
+ fmt.Fprintln(resp, "-")
+ fileNum := 0
+ {
+ length := 0
+ for k0, v0 := range req.Form {
+ h := md5.New()
+ _, _ = io.WriteString(h, v0[0])
+ _md5 := fmt.Sprintf("%x", h.Sum(nil))
+
+ length += len(k0)
+ length += len(v0[0])
+
+ // echo error when key != _md5(val)
+ if _md5 != k0 {
+ fmt.Fprintln(resp, "server:err ", _md5, k0)
+ stat = "FAILED"
+ }
+ }
+ if req.MultipartForm != nil {
+ fileNum = len(req.MultipartForm.File)
+ for kn, fns := range req.MultipartForm.File {
+ //fmt.Fprintln(resp, "server:filekey ", kn )
+ length += len(kn)
+ for _, f := range fns {
+ fd, err := f.Open()
+ if err != nil {
+ log.Println("server:", err)
+ return
+ }
+ h := md5.New()
+ l0, err := io.Copy(h, fd)
+ if err != nil {
+ log.Println(err)
+ return
+ }
+ length += int(l0)
+ defer fd.Close()
+ md5 := fmt.Sprintf("%x", h.Sum(nil))
+ //fmt.Fprintln(resp, "server:filemd5 ", md5 )
+
+ if kn != md5 {
+ fmt.Fprintln(resp, "server:err ", md5, kn)
+ stat = "FAILED"
+ }
+ //fmt.Fprintln(resp, "server:filename ", f.Filename )
+ }
+ }
+ }
+
+ fmt.Fprintln(resp, "server:got data length", length)
+ }
+ fmt.Fprintln(resp, "-"+stat+"-POST(", len(req.Form), ")-FILE(", fileNum, ")--")
+}
+
+func sendFcgi(reqType int, fcgiParams map[string]string, data []byte, posts map[string]string, files map[string]string) (content []byte) {
+ fcgi, err := Dial("tcp", ipPort)
+ if err != nil {
+ log.Println("err:", err)
+ return
+ }
+
+ length := 0
+
+ var resp *http.Response
+ switch reqType {
+ case 0:
+ if len(data) > 0 {
+ length = len(data)
+ rd := bytes.NewReader(data)
+ resp, err = fcgi.Post(fcgiParams, "", "", rd, int64(rd.Len()))
+ } else if len(posts) > 0 {
+ values := url.Values{}
+ for k, v := range posts {
+ values.Set(k, v)
+ length += len(k) + 2 + len(v)
+ }
+ resp, err = fcgi.PostForm(fcgiParams, values)
+ } else {
+ rd := bytes.NewReader(data)
+ resp, err = fcgi.Get(fcgiParams, rd, int64(rd.Len()))
+ }
+
+ default:
+ values := url.Values{}
+ for k, v := range posts {
+ values.Set(k, v)
+ length += len(k) + 2 + len(v)
+ }
+
+ for k, v := range files {
+ fi, _ := os.Lstat(v)
+ length += len(k) + int(fi.Size())
+ }
+ resp, err = fcgi.PostFile(fcgiParams, values, files)
+ }
+
+ if err != nil {
+ log.Println("err:", err)
+ return
+ }
+
+ defer resp.Body.Close()
+ content, _ = ioutil.ReadAll(resp.Body)
+
+ log.Println("c: send data length ≈", length, string(content))
+ fcgi.Close()
+ time.Sleep(1 * time.Second)
+
+ if bytes.Contains(content, []byte("FAILED")) {
+ globalt.Error("Server return failed message")
+ }
+
+ return
+}
+
+func generateRandFile(size int) (p string, m string) {
+
+ p = filepath.Join(os.TempDir(), "fcgict"+strconv.Itoa(rand.Int()))
+
+ // open output file
+ fo, err := os.Create(p)
+ if err != nil {
+ panic(err)
+ }
+ // close fo on exit and check for its returned error
+ defer func() {
+ if err := fo.Close(); err != nil {
+ panic(err)
+ }
+ }()
+
+ h := md5.New()
+ for i := 0; i < size/16; i++ {
+ buf := make([]byte, 16)
+ binary.PutVarint(buf, rand.Int63())
+ if _, err := fo.Write(buf); err != nil {
+ log.Printf("[ERROR] failed to write buffer: %v\n", err)
+ }
+ if _, err := h.Write(buf); err != nil {
+ log.Printf("[ERROR] failed to write buffer: %v\n", err)
+ }
+ }
+ m = fmt.Sprintf("%x", h.Sum(nil))
+ return
+}
+
+func DisabledTest(t *testing.T) {
+ // TODO: test chunked reader
+ globalt = t
+
+ rand.Seed(time.Now().UTC().UnixNano())
+
+ // server
+ go func() {
+ listener, err := net.Listen("tcp", ipPort)
+ if err != nil {
+ log.Println("listener creation failed: ", err)
+ }
+
+ srv := new(FastCGIServer)
+ if err := fcgi.Serve(listener, srv); err != nil {
+ log.Print("[ERROR] failed to start server: ", err)
+ }
+ }()
+
+ time.Sleep(1 * time.Second)
+
+ // init
+ fcgiParams := make(map[string]string)
+ fcgiParams["REQUEST_METHOD"] = "GET"
+ fcgiParams["SERVER_PROTOCOL"] = "HTTP/1.1"
+ //fcgi_params["GATEWAY_INTERFACE"] = "CGI/1.1"
+ fcgiParams["SCRIPT_FILENAME"] = scriptFile
+
+ // simple GET
+ log.Println("test:", "get")
+ sendFcgi(0, fcgiParams, nil, nil, nil)
+
+ // simple post data
+ log.Println("test:", "post")
+ sendFcgi(0, fcgiParams, []byte("c4ca4238a0b923820dcc509a6f75849b=1&7b8b965ad4bca0e41ab51de7b31363a1=n"), nil, nil)
+
+ log.Println("test:", "post data (more than 60KB)")
+ data := ""
+ for i := 0x00; i < 0xff; i++ {
+ v0 := strings.Repeat(string(i), 256)
+ h := md5.New()
+ _, _ = io.WriteString(h, v0)
+ k0 := fmt.Sprintf("%x", h.Sum(nil))
+ data += k0 + "=" + url.QueryEscape(v0) + "&"
+ }
+ sendFcgi(0, fcgiParams, []byte(data), nil, nil)
+
+ log.Println("test:", "post form (use url.Values)")
+ p0 := make(map[string]string, 1)
+ p0["c4ca4238a0b923820dcc509a6f75849b"] = "1"
+ p0["7b8b965ad4bca0e41ab51de7b31363a1"] = "n"
+ sendFcgi(1, fcgiParams, nil, p0, nil)
+
+ log.Println("test:", "post forms (256 keys, more than 1MB)")
+ p1 := make(map[string]string, 1)
+ for i := 0x00; i < 0xff; i++ {
+ v0 := strings.Repeat(string(i), 4096)
+ h := md5.New()
+ _, _ = io.WriteString(h, v0)
+ k0 := fmt.Sprintf("%x", h.Sum(nil))
+ p1[k0] = v0
+ }
+ sendFcgi(1, fcgiParams, nil, p1, nil)
+
+ log.Println("test:", "post file (1 file, 500KB)) ")
+ f0 := make(map[string]string, 1)
+ path0, m0 := generateRandFile(500000)
+ f0[m0] = path0
+ sendFcgi(1, fcgiParams, nil, p1, f0)
+
+ log.Println("test:", "post multiple files (2 files, 5M each) and forms (256 keys, more than 1MB data")
+ path1, m1 := generateRandFile(5000000)
+ f0[m1] = path1
+ sendFcgi(1, fcgiParams, nil, p1, f0)
+
+ log.Println("test:", "post only files (2 files, 5M each)")
+ sendFcgi(1, fcgiParams, nil, nil, f0)
+
+ log.Println("test:", "post only 1 file")
+ delete(f0, "m0")
+ sendFcgi(1, fcgiParams, nil, nil, f0)
+
+ if err := os.Remove(path0); err != nil {
+ log.Println("[ERROR] failed to remove path: ", err)
+ }
+ if err := os.Remove(path1); err != nil {
+ log.Println("[ERROR] failed to remove path: ", err)
+ }
+}
diff --git a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
new file mode 100644
index 0000000..91039c9
--- /dev/null
+++ b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
@@ -0,0 +1,301 @@
+// Copyright 2015 Matthew Holt and The Caddy Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package fastcgi
+
+import (
+ "context"
+ "crypto/tls"
+ "fmt"
+ "net/http"
+ "net/url"
+ "path"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp"
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
+ "github.com/caddyserver/caddy/v2/modules/caddytls"
+
+ "github.com/caddyserver/caddy/v2"
+)
+
+func init() {
+ caddy.RegisterModule(Transport{})
+}
+
+// Transport facilitates FastCGI communication.
+type Transport struct {
+ // TODO: Populate these
+ softwareName string
+ softwareVersion string
+ serverName string
+ serverPort string
+
+ // Use this directory as the fastcgi root directory. Defaults to the root
+ // directory of the parent virtual host.
+ Root string `json:"root,omitempty"`
+
+ // The path in the URL will be split into two, with the first piece ending
+ // with the value of SplitPath. The first piece will be assumed as the
+ // actual resource (CGI script) name, and the second piece will be set to
+ // PATH_INFO for the CGI script to use.
+ SplitPath string `json:"split_path,omitempty"`
+
+ // Environment variables (TODO: make a map of string to value...?)
+ EnvVars [][2]string `json:"env,omitempty"`
+
+ // The duration used to set a deadline when connecting to an upstream.
+ DialTimeout caddy.Duration `json:"dial_timeout,omitempty"`
+
+ // The duration used to set a deadline when reading from the FastCGI server.
+ ReadTimeout caddy.Duration `json:"read_timeout,omitempty"`
+
+ // The duration used to set a deadline when sending to the FastCGI server.
+ WriteTimeout caddy.Duration `json:"write_timeout,omitempty"`
+}
+
+// CaddyModule returns the Caddy module information.
+func (Transport) CaddyModule() caddy.ModuleInfo {
+ return caddy.ModuleInfo{
+ Name: "http.handlers.reverse_proxy.transport.fastcgi",
+ New: func() caddy.Module { return new(Transport) },
+ }
+}
+
+// Provision sets up t.
+func (t *Transport) Provision(_ caddy.Context) error {
+ if t.Root == "" {
+ t.Root = "{http.vars.root}"
+ }
+ return nil
+}
+
+// RoundTrip implements http.RoundTripper.
+func (t Transport) RoundTrip(r *http.Request) (*http.Response, error) {
+ env, err := t.buildEnv(r)
+ if err != nil {
+ return nil, fmt.Errorf("building environment: %v", err)
+ }
+
+ // TODO: doesn't dialer have a Timeout field?
+ ctx := r.Context()
+ if t.DialTimeout > 0 {
+ var cancel context.CancelFunc
+ ctx, cancel = context.WithTimeout(ctx, time.Duration(t.DialTimeout))
+ defer cancel()
+ }
+
+ // extract dial information from request (this
+ // should embedded by the reverse proxy)
+ network, address := "tcp", r.URL.Host
+ if dialInfoVal := ctx.Value(reverseproxy.DialInfoCtxKey); dialInfoVal != nil {
+ dialInfo := dialInfoVal.(reverseproxy.DialInfo)
+ network = dialInfo.Network
+ address = dialInfo.Address
+ }
+
+ fcgiBackend, err := DialContext(ctx, network, address)
+ if err != nil {
+ return nil, fmt.Errorf("dialing backend: %v", err)
+ }
+ // fcgiBackend gets closed when response body is closed (see clientCloser)
+
+ // read/write timeouts
+ if err := fcgiBackend.SetReadTimeout(time.Duration(t.ReadTimeout)); err != nil {
+ return nil, fmt.Errorf("setting read timeout: %v", err)
+ }
+ if err := fcgiBackend.SetWriteTimeout(time.Duration(t.WriteTimeout)); err != nil {
+ return nil, fmt.Errorf("setting write timeout: %v", err)
+ }
+
+ contentLength := r.ContentLength
+ if contentLength == 0 {
+ contentLength, _ = strconv.ParseInt(r.Header.Get("Content-Length"), 10, 64)
+ }
+
+ var resp *http.Response
+ switch r.Method {
+ case http.MethodHead:
+ resp, err = fcgiBackend.Head(env)
+ case http.MethodGet:
+ resp, err = fcgiBackend.Get(env, r.Body, contentLength)
+ case http.MethodOptions:
+ resp, err = fcgiBackend.Options(env)
+ default:
+ resp, err = fcgiBackend.Post(env, r.Method, r.Header.Get("Content-Type"), r.Body, contentLength)
+ }
+
+ return resp, err
+}
+
+// buildEnv returns a set of CGI environment variables for the request.
+func (t Transport) buildEnv(r *http.Request) (map[string]string, error) {
+ repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
+
+ var env map[string]string
+
+ // Separate remote IP and port; more lenient than net.SplitHostPort
+ var ip, port string
+ if idx := strings.LastIndex(r.RemoteAddr, ":"); idx > -1 {
+ ip = r.RemoteAddr[:idx]
+ port = r.RemoteAddr[idx+1:]
+ } else {
+ ip = r.RemoteAddr
+ }
+
+ // Remove [] from IPv6 addresses
+ ip = strings.Replace(ip, "[", "", 1)
+ ip = strings.Replace(ip, "]", "", 1)
+
+ root := repl.ReplaceAll(t.Root, ".")
+ fpath := r.URL.Path
+
+ // Split path in preparation for env variables.
+ // Previous canSplit checks ensure this can never be -1.
+ // TODO: I haven't brought over canSplit; make sure this doesn't break
+ splitPos := t.splitPos(fpath)
+
+ // Request has the extension; path was split successfully
+ docURI := fpath[:splitPos+len(t.SplitPath)]
+ pathInfo := fpath[splitPos+len(t.SplitPath):]
+ scriptName := fpath
+
+ // Strip PATH_INFO from SCRIPT_NAME
+ scriptName = strings.TrimSuffix(scriptName, pathInfo)
+
+ // SCRIPT_FILENAME is the absolute path of SCRIPT_NAME
+ scriptFilename := filepath.Join(root, scriptName)
+
+ // Add vhost path prefix to scriptName. Otherwise, some PHP software will
+ // have difficulty discovering its URL.
+ pathPrefix, _ := r.Context().Value(caddy.CtxKey("path_prefix")).(string)
+ scriptName = path.Join(pathPrefix, scriptName)
+
+ // Get the request URL from context. The context stores the original URL in case
+ // it was changed by a middleware such as rewrite. By default, we pass the
+ // original URI in as the value of REQUEST_URI (the user can overwrite this
+ // if desired). Most PHP apps seem to want the original URI. Besides, this is
+ // how nginx defaults: http://stackoverflow.com/a/12485156/1048862
+ reqURL, ok := r.Context().Value(caddyhttp.OriginalURLCtxKey).(url.URL)
+ if !ok {
+ // some requests, like active health checks, don't add this to
+ // the request context, so we can just use the current URL
+ reqURL = *r.URL
+ }
+
+ requestScheme := "http"
+ if r.TLS != nil {
+ requestScheme = "https"
+ }
+
+ // Some variables are unused but cleared explicitly to prevent
+ // the parent environment from interfering.
+ env = map[string]string{
+ // Variables defined in CGI 1.1 spec
+ "AUTH_TYPE": "", // Not used
+ "CONTENT_LENGTH": r.Header.Get("Content-Length"),
+ "CONTENT_TYPE": r.Header.Get("Content-Type"),
+ "GATEWAY_INTERFACE": "CGI/1.1",
+ "PATH_INFO": pathInfo,
+ "QUERY_STRING": r.URL.RawQuery,
+ "REMOTE_ADDR": ip,
+ "REMOTE_HOST": ip, // For speed, remote host lookups disabled
+ "REMOTE_PORT": port,
+ "REMOTE_IDENT": "", // Not used
+ "REMOTE_USER": "", // TODO: once there are authentication handlers, populate this
+ "REQUEST_METHOD": r.Method,
+ "REQUEST_SCHEME": requestScheme,
+ "SERVER_NAME": t.serverName,
+ "SERVER_PORT": t.serverPort,
+ "SERVER_PROTOCOL": r.Proto,
+ "SERVER_SOFTWARE": t.softwareName + "/" + t.softwareVersion,
+
+ // Other variables
+ "DOCUMENT_ROOT": root,
+ "DOCUMENT_URI": docURI,
+ "HTTP_HOST": r.Host, // added here, since not always part of headers
+ "REQUEST_URI": reqURL.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(root, pathInfo) // Info: http://www.oreilly.com/openbook/cgi/ch02_04.html
+ }
+
+ // Some web apps rely on knowing HTTPS or not
+ if r.TLS != nil {
+ env["HTTPS"] = "on"
+ // and pass the protocol details in a manner compatible with apache's mod_ssl
+ // (which is why these have a SSL_ prefix and not TLS_).
+ v, ok := tlsProtocolStrings[r.TLS.Version]
+ if ok {
+ env["SSL_PROTOCOL"] = v
+ }
+ // and pass the cipher suite in a manner compatible with apache's mod_ssl
+ for k, v := range caddytls.SupportedCipherSuites {
+ if v == r.TLS.CipherSuite {
+ env["SSL_CIPHER"] = k
+ break
+ }
+ }
+ }
+
+ // Add env variables from config (with support for placeholders in values)
+ for _, envVar := range t.EnvVars {
+ env[envVar[0]] = repl.ReplaceAll(envVar[1], "")
+ }
+
+ // Add all HTTP headers to env variables
+ for field, val := range r.Header {
+ header := strings.ToUpper(field)
+ header = headerNameReplacer.Replace(header)
+ env["HTTP_"+header] = strings.Join(val, ", ")
+ }
+ return env, nil
+}
+
+// splitPos returns the index where path should
+// be split based on t.SplitPath.
+func (t Transport) splitPos(path string) int {
+ // TODO:
+ // if httpserver.CaseSensitivePath {
+ // return strings.Index(path, r.SplitPath)
+ // }
+ return strings.Index(strings.ToLower(path), strings.ToLower(t.SplitPath))
+}
+
+// TODO:
+// Map of supported protocols to Apache ssl_mod format
+// Note that these are slightly different from SupportedProtocols in caddytls/config.go
+var tlsProtocolStrings = map[uint16]string{
+ tls.VersionTLS10: "TLSv1",
+ tls.VersionTLS11: "TLSv1.1",
+ tls.VersionTLS12: "TLSv1.2",
+ tls.VersionTLS13: "TLSv1.3",
+}
+
+var headerNameReplacer = strings.NewReplacer(" ", "_", "-", "_")
+
+// Interface guards
+var (
+ _ caddy.Provisioner = (*Transport)(nil)
+ _ http.RoundTripper = (*Transport)(nil)
+)