diff options
Diffstat (limited to 'modules/caddyhttp/reverseproxy/fastcgi')
| -rw-r--r-- | modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go | 54 | ||||
| -rw-r--r-- | modules/caddyhttp/reverseproxy/fastcgi/client.go | 578 | ||||
| -rw-r--r-- | modules/caddyhttp/reverseproxy/fastcgi/client_test.go | 301 | ||||
| -rw-r--r-- | modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go | 301 | 
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) +) | 
