TUN-528: Move cloudflared into a separate repo

This commit is contained in:
Areg Harutyunyan
2018-05-01 18:45:06 -05:00
parent e8c621a648
commit d06fc520c7
4726 changed files with 1763680 additions and 0 deletions

View File

@@ -0,0 +1,491 @@
// 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.
// Package fastcgi has middleware that acts as a FastCGI client. Requests
// that get forwarded to FastCGI stop the middleware execution chain.
// The most common use for this package is to serve PHP websites via php-fpm.
package fastcgi
import (
"context"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"sync/atomic"
"time"
"crypto/tls"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
"github.com/mholt/caddy/caddytls"
)
// Handler is a middleware type that can handle requests as a FastCGI client.
type Handler struct {
Next httpserver.Handler
Rules []Rule
Root string
FileSys http.FileSystem
// These are sent to CGI scripts in env variables
SoftwareName string
SoftwareVersion string
ServerName string
ServerPort string
}
// ServeHTTP satisfies the httpserver.Handler interface.
func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
for _, rule := range h.Rules {
// First requirement: Base path must match request path. If it doesn't,
// we check to make sure the leading slash is not missing, and if so,
// we check again with it prepended. This is in case people forget
// a leading slash when performing rewrites, and we don't want to expose
// the contents of the (likely PHP) script. See issue #1645.
hpath := httpserver.Path(r.URL.Path)
if !hpath.Matches(rule.Path) {
if strings.HasPrefix(string(hpath), "/") {
// this is a normal-looking path, and it doesn't match; try next rule
continue
}
hpath = httpserver.Path("/" + string(hpath)) // prepend leading slash
if !hpath.Matches(rule.Path) {
// even after fixing the request path, it still doesn't match; try next rule
continue
}
}
// The path must also be allowed (not ignored).
if !rule.AllowedPath(r.URL.Path) {
continue
}
// In addition to matching the path, a request must meet some
// other criteria before being proxied as FastCGI. For example,
// we probably want to exclude static assets (CSS, JS, images...)
// but we also want to be flexible for the script we proxy to.
fpath := r.URL.Path
if idx, ok := httpserver.IndexFile(h.FileSys, fpath, rule.IndexFiles); ok {
fpath = idx
// Index file present.
// If request path cannot be split, return error.
if !rule.canSplit(fpath) {
return http.StatusInternalServerError, ErrIndexMissingSplit
}
} else {
// No index file present.
// If request path cannot be split, ignore request.
if !rule.canSplit(fpath) {
continue
}
}
// These criteria work well in this order for PHP sites
if !h.exists(fpath) || fpath[len(fpath)-1] == '/' || strings.HasSuffix(fpath, rule.Ext) {
// Create environment for CGI script
env, err := h.buildEnv(r, rule, fpath)
if err != nil {
return http.StatusInternalServerError, err
}
// Connect to FastCGI gateway
address, err := rule.Address()
if err != nil {
return http.StatusBadGateway, err
}
network, address := parseAddress(address)
ctx := context.Background()
if rule.ConnectTimeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, rule.ConnectTimeout)
defer cancel()
}
fcgiBackend, err := DialContext(ctx, network, address)
if err != nil {
return http.StatusBadGateway, err
}
defer fcgiBackend.Close()
// read/write timeouts
if err := fcgiBackend.SetReadTimeout(rule.ReadTimeout); err != nil {
return http.StatusInternalServerError, err
}
if err := fcgiBackend.SetSendTimeout(rule.SendTimeout); err != nil {
return http.StatusInternalServerError, err
}
var resp *http.Response
var contentLength int64
// if ContentLength is already set
if r.ContentLength > 0 {
contentLength = r.ContentLength
} else {
contentLength, _ = strconv.ParseInt(r.Header.Get("Content-Length"), 10, 64)
}
switch r.Method {
case "HEAD":
resp, err = fcgiBackend.Head(env)
case "GET":
resp, err = fcgiBackend.Get(env, r.Body, contentLength)
case "OPTIONS":
resp, err = fcgiBackend.Options(env)
default:
resp, err = fcgiBackend.Post(env, r.Method, r.Header.Get("Content-Type"), r.Body, contentLength)
}
if resp != nil && resp.Body != nil {
defer resp.Body.Close()
}
if err != nil {
if err, ok := err.(net.Error); ok && err.Timeout() {
return http.StatusGatewayTimeout, err
} else if err != io.EOF {
return http.StatusBadGateway, err
}
}
// Write response header
writeHeader(w, resp)
// Write the response body
_, err = io.Copy(w, resp.Body)
if err != nil {
return http.StatusBadGateway, err
}
// Log any stderr output from upstream
if fcgiBackend.stderr.Len() != 0 {
// Remove trailing newline, error logger already does this.
err = LogError(strings.TrimSuffix(fcgiBackend.stderr.String(), "\n"))
}
// Normally we would return the status code if it is an error status (>= 400),
// however, upstream FastCGI apps don't know about our contract and have
// probably already written an error page. So we just return 0, indicating
// that the response body is already written. However, we do return any
// error value so it can be logged.
// Note that the proxy middleware works the same way, returning status=0.
return 0, err
}
}
return h.Next.ServeHTTP(w, r)
}
// parseAddress returns the network and address of fcgiAddress.
// The first string is the network, "tcp" or "unix", implied from the scheme and address.
// The second string is fcgiAddress, with scheme prefixes removed.
// The two returned strings can be used as parameters to the Dial() function.
func parseAddress(fcgiAddress string) (string, string) {
// check if address has tcp scheme explicitly set
if strings.HasPrefix(fcgiAddress, "tcp://") {
return "tcp", fcgiAddress[len("tcp://"):]
}
// check if address has fastcgi scheme explicitly set
if strings.HasPrefix(fcgiAddress, "fastcgi://") {
return "tcp", fcgiAddress[len("fastcgi://"):]
}
// check if unix socket
if trim := strings.HasPrefix(fcgiAddress, "unix"); strings.HasPrefix(fcgiAddress, "/") || trim {
if trim {
return "unix", fcgiAddress[len("unix:"):]
}
return "unix", fcgiAddress
}
// default case, a plain tcp address with no scheme
return "tcp", fcgiAddress
}
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 (h Handler) exists(path string) bool {
if _, err := os.Stat(h.Root + path); err == nil {
return true
}
return false
}
// buildEnv returns a set of CGI environment variables for the request.
func (h Handler) buildEnv(r *http.Request, rule Rule, fpath string) (map[string]string, error) {
var env map[string]string
// Get absolute path of requested resource
absPath := filepath.Join(rule.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
}
// Remove [] from IPv6 addresses
ip = strings.Replace(ip, "[", "", 1)
ip = strings.Replace(ip, "]", "", 1)
// Split path in preparation for env variables.
// Previous rule.canSplit checks ensure this can never be -1.
splitPos := rule.splitPos(fpath)
// Request has the extension; path was split successfully
docURI := fpath[:splitPos+len(rule.SplitPath)]
pathInfo := fpath[splitPos+len(rule.SplitPath):]
scriptName := fpath
scriptFilename := absPath
// Strip PATH_INFO from SCRIPT_NAME
scriptName = strings.TrimSuffix(scriptName, pathInfo)
// 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 URI from context. The context stores the original URI in case
// it was changed by a middleware such as rewrite. By default, we pass the
// original URI in as the value of REQUEST_URI (the user can overwrite this
// if desired). Most PHP apps seem to want the original URI. Besides, this is
// how nginx defaults: http://stackoverflow.com/a/12485156/1048862
reqURL, _ := r.Context().Value(httpserver.OriginalURLCtxKey).(url.URL)
// Retrieve name of remote user that was set by some downstream middleware such as basicauth.
remoteUser, _ := r.Context().Value(httpserver.RemoteUserCtxKey).(string)
// Some variables are unused but cleared explicitly to prevent
// the parent environment from interfering.
env = map[string]string{
// Variables defined in CGI 1.1 spec
"AUTH_TYPE": "", // Not used
"CONTENT_LENGTH": r.Header.Get("Content-Length"),
"CONTENT_TYPE": r.Header.Get("Content-Type"),
"GATEWAY_INTERFACE": "CGI/1.1",
"PATH_INFO": pathInfo,
"QUERY_STRING": r.URL.RawQuery,
"REMOTE_ADDR": ip,
"REMOTE_HOST": ip, // For speed, remote host lookups disabled
"REMOTE_PORT": port,
"REMOTE_IDENT": "", // Not used
"REMOTE_USER": remoteUser,
"REQUEST_METHOD": r.Method,
"SERVER_NAME": h.ServerName,
"SERVER_PORT": h.ServerPort,
"SERVER_PROTOCOL": r.Proto,
"SERVER_SOFTWARE": h.SoftwareName + "/" + h.SoftwareVersion,
// Other variables
"DOCUMENT_ROOT": rule.Root,
"DOCUMENT_URI": docURI,
"HTTP_HOST": r.Host, // added here, since not always part of headers
"REQUEST_URI": reqURL.RequestURI(),
"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(rule.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 they have a SSL_ prefix and not TLS_).
v, ok := tlsProtocolStringToMap[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.SupportedCiphersMap {
if v == r.TLS.CipherSuite {
env["SSL_CIPHER"] = k
break
}
}
}
// Add env variables from config (with support for placeholders in values)
replacer := httpserver.NewReplacer(r, nil, "")
for _, envVar := range rule.EnvVars {
env[envVar[0]] = replacer.Replace(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
}
// Rule represents a FastCGI handling rule.
// It is parsed from the fastcgi directive in the Caddyfile, see setup.go.
type Rule struct {
// The base path to match. Required.
Path string
// upstream load balancer
balancer
// Always process files with this extension with fastcgi.
Ext string
// Use this directory as the fastcgi root directory. Defaults to the root
// directory of the parent virtual host.
Root string
// The path in the URL will be split into two, with the first piece ending
// with the value of SplitPath. The first piece will be assumed as the
// actual resource (CGI script) name, and the second piece will be set to
// PATH_INFO for the CGI script to use.
SplitPath string
// If the URL ends with '/' (which indicates a directory), these index
// files will be tried instead.
IndexFiles []string
// Environment Variables
EnvVars [][2]string
// Ignored paths
IgnoredSubPaths []string
// The duration used to set a deadline when connecting to an upstream.
ConnectTimeout time.Duration
// The duration used to set a deadline when reading from the FastCGI server.
ReadTimeout time.Duration
// The duration used to set a deadline when sending to the FastCGI server.
SendTimeout time.Duration
}
// balancer is a fastcgi upstream load balancer.
type balancer interface {
// Address picks an upstream address from the
// underlying load balancer.
Address() (string, error)
}
// roundRobin is a round robin balancer for fastcgi upstreams.
type roundRobin struct {
// Known Go bug: https://golang.org/pkg/sync/atomic/#pkg-note-BUG
// must be first field for 64 bit alignment
// on x86 and arm.
index int64
addresses []string
}
func (r *roundRobin) Address() (string, error) {
index := atomic.AddInt64(&r.index, 1) % int64(len(r.addresses))
return r.addresses[index], nil
}
// srvResolver is a private interface used to abstract
// the DNS resolver. It is mainly used to facilitate testing.
type srvResolver interface {
LookupSRV(ctx context.Context, service, proto, name string) (string, []*net.SRV, error)
}
// srv is a service locator for fastcgi upstreams
type srv struct {
resolver srvResolver
service string
}
// Address looks up the service and returns the address:port
// from first result in resolved list.
// No explicit balancing is required because net.LookupSRV
// sorts the results by priority and randomizes within priority.
func (s *srv) Address() (string, error) {
_, addrs, err := s.resolver.LookupSRV(context.Background(), "", "", s.service)
if err != nil {
return "", err
}
return fmt.Sprintf("%s:%d", strings.TrimRight(addrs[0].Target, "."), addrs[0].Port), nil
}
// canSplit checks if path can split into two based on rule.SplitPath.
func (r Rule) canSplit(path string) bool {
return r.splitPos(path) >= 0
}
// splitPos returns the index where path should be split
// based on rule.SplitPath.
func (r Rule) splitPos(path string) int {
if httpserver.CaseSensitivePath {
return strings.Index(path, r.SplitPath)
}
return strings.Index(strings.ToLower(path), strings.ToLower(r.SplitPath))
}
// AllowedPath checks if requestPath is not an ignored path.
func (r Rule) AllowedPath(requestPath string) bool {
for _, ignoredSubPath := range r.IgnoredSubPaths {
if httpserver.Path(path.Clean(requestPath)).Matches(path.Join(r.Path, ignoredSubPath)) {
return false
}
}
return true
}
var (
headerNameReplacer = strings.NewReplacer(" ", "_", "-", "_")
// ErrIndexMissingSplit describes an index configuration error.
ErrIndexMissingSplit = errors.New("configured index file(s) must include split value")
)
// LogError is a non fatal error that allows requests to go through.
type LogError string
// Error satisfies error interface.
func (l LogError) Error() string {
return string(l)
}
// Map of supported protocols to Apache ssl_mod format
// Note that these are slightly different from SupportedProtocols in caddytls/config.go's
var tlsProtocolStringToMap = map[uint16]string{
tls.VersionTLS10: "TLSv1",
tls.VersionTLS11: "TLSv1.1",
tls.VersionTLS12: "TLSv1.2",
}

View File

@@ -0,0 +1,388 @@
// 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.
package fastcgi
import (
"context"
"net"
"net/http"
"net/http/fcgi"
"net/http/httptest"
"net/url"
"strconv"
"sync"
"testing"
"time"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
func TestServeHTTP(t *testing.T) {
body := "This is some test body content"
bodyLenStr := strconv.Itoa(len(body))
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("Unable to create listener for test: %v", err)
}
defer listener.Close()
go fcgi.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Length", bodyLenStr)
w.Write([]byte(body))
}))
handler := Handler{
Next: nil,
Rules: []Rule{{Path: "/", balancer: address(listener.Addr().String())}},
}
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatalf("Unable to create request: %v", err)
}
w := httptest.NewRecorder()
status, err := handler.ServeHTTP(w, r)
if got, want := status, 0; got != want {
t.Errorf("Expected returned status code to be %d, got %d", want, got)
}
if err != nil {
t.Errorf("Expected nil error, got: %v", err)
}
if got, want := w.Header().Get("Content-Length"), bodyLenStr; got != want {
t.Errorf("Expected Content-Length to be '%s', got: '%s'", want, got)
}
if got, want := w.Body.String(), body; got != want {
t.Errorf("Expected response body to be '%s', got: '%s'", want, got)
}
}
func TestRuleParseAddress(t *testing.T) {
getClientTestTable := []struct {
rule *Rule
expectednetwork string
expectedaddress string
}{
{&Rule{balancer: address("tcp://172.17.0.1:9000")}, "tcp", "172.17.0.1:9000"},
{&Rule{balancer: address("fastcgi://localhost:9000")}, "tcp", "localhost:9000"},
{&Rule{balancer: address("172.17.0.15")}, "tcp", "172.17.0.15"},
{&Rule{balancer: address("/my/unix/socket")}, "unix", "/my/unix/socket"},
{&Rule{balancer: address("unix:/second/unix/socket")}, "unix", "/second/unix/socket"},
}
for _, entry := range getClientTestTable {
addr, err := entry.rule.Address()
if err != nil {
t.Errorf("Unexpected error in retrieving address: %s", err.Error())
}
if actualnetwork, _ := parseAddress(addr); actualnetwork != entry.expectednetwork {
t.Errorf("Unexpected network for address string %v. Got %v, expected %v", addr, actualnetwork, entry.expectednetwork)
}
if _, actualaddress := parseAddress(addr); actualaddress != entry.expectedaddress {
t.Errorf("Unexpected parsed address for address string %v. Got %v, expected %v", addr, actualaddress, entry.expectedaddress)
}
}
}
func TestRuleIgnoredPath(t *testing.T) {
rule := &Rule{
Path: "/fastcgi",
IgnoredSubPaths: []string{"/download", "/static"},
}
tests := []struct {
url string
expected bool
}{
{"/fastcgi", true},
{"/fastcgi/dl", true},
{"/fastcgi/download", false},
{"/fastcgi/download/static", false},
{"/fastcgi/static", false},
{"/fastcgi/static/download", false},
{"/fastcgi/something/download", true},
{"/fastcgi/something/static", true},
{"/fastcgi//static", false},
{"/fastcgi//static//download", false},
{"/fastcgi//download", false},
}
for i, test := range tests {
allowed := rule.AllowedPath(test.url)
if test.expected != allowed {
t.Errorf("Test %d: expected %v found %v", i, test.expected, allowed)
}
}
}
func TestBuildEnv(t *testing.T) {
testBuildEnv := func(r *http.Request, rule Rule, fpath string, envExpected map[string]string) {
var h Handler
env, err := h.buildEnv(r, rule, fpath)
if err != nil {
t.Error("Unexpected error:", err.Error())
}
for k, v := range envExpected {
if env[k] != v {
t.Errorf("Unexpected %v. Got %v, expected %v", k, env[k], v)
}
}
}
rule := Rule{
Ext: ".php",
SplitPath: ".php",
IndexFiles: []string{"index.php"},
}
url, err := url.Parse("http://localhost:2015/fgci_test.php?test=foobar")
if err != nil {
t.Error("Unexpected error:", err.Error())
}
var newReq = func() *http.Request {
r := http.Request{
Method: "GET",
URL: url,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Host: "localhost:2015",
RemoteAddr: "[2b02:1810:4f2d:9400:70ab:f822:be8a:9093]:51688",
RequestURI: "/fgci_test.php",
Header: map[string][]string{
"Foo": {"Bar", "two"},
},
}
ctx := context.WithValue(r.Context(), httpserver.OriginalURLCtxKey, *r.URL)
return r.WithContext(ctx)
}
fpath := "/fgci_test.php"
var newEnv = func() map[string]string {
return map[string]string{
"REMOTE_ADDR": "2b02:1810:4f2d:9400:70ab:f822:be8a:9093",
"REMOTE_PORT": "51688",
"SERVER_PROTOCOL": "HTTP/1.1",
"QUERY_STRING": "test=foobar",
"REQUEST_METHOD": "GET",
"HTTP_HOST": "localhost:2015",
"SCRIPT_NAME": "/fgci_test.php",
}
}
// request
var r *http.Request
// expected environment variables
var envExpected map[string]string
// 1. Test for full canonical IPv6 address
r = newReq()
testBuildEnv(r, rule, fpath, envExpected)
// 2. Test for shorthand notation of IPv6 address
r = newReq()
r.RemoteAddr = "[::1]:51688"
envExpected = newEnv()
envExpected["REMOTE_ADDR"] = "::1"
testBuildEnv(r, rule, fpath, envExpected)
// 3. Test for IPv4 address
r = newReq()
r.RemoteAddr = "192.168.0.10:51688"
envExpected = newEnv()
envExpected["REMOTE_ADDR"] = "192.168.0.10"
testBuildEnv(r, rule, fpath, envExpected)
// 4. Test for environment variable
r = newReq()
rule.EnvVars = [][2]string{
{"HTTP_HOST", "localhost:2016"},
{"REQUEST_METHOD", "POST"},
}
envExpected = newEnv()
envExpected["HTTP_HOST"] = "localhost:2016"
envExpected["REQUEST_METHOD"] = "POST"
testBuildEnv(r, rule, fpath, envExpected)
// 5. Test for environment variable placeholders
r = newReq()
rule.EnvVars = [][2]string{
{"HTTP_HOST", "{host}"},
{"CUSTOM_URI", "custom_uri{uri}"},
{"CUSTOM_QUERY", "custom=true&{query}"},
}
envExpected = newEnv()
envExpected["HTTP_HOST"] = "localhost:2015"
envExpected["CUSTOM_URI"] = "custom_uri/fgci_test.php?test=foobar"
envExpected["CUSTOM_QUERY"] = "custom=true&test=foobar"
testBuildEnv(r, rule, fpath, envExpected)
// 6. Test SCRIPT_NAME includes path prefix
r = newReq()
ctx := context.WithValue(r.Context(), caddy.CtxKey("path_prefix"), "/test")
r = r.WithContext(ctx)
envExpected = newEnv()
envExpected["SCRIPT_NAME"] = "/test/fgci_test.php"
testBuildEnv(r, rule, fpath, envExpected)
}
func TestReadTimeout(t *testing.T) {
tests := []struct {
sleep time.Duration
readTimeout time.Duration
shouldErr bool
}{
{75 * time.Millisecond, 50 * time.Millisecond, true},
{0, -1 * time.Second, true},
{0, time.Minute, false},
}
var wg sync.WaitGroup
for i, test := range tests {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("Test %d: Unable to create listener for test: %v", i, err)
}
defer listener.Close()
handler := Handler{
Next: nil,
Rules: []Rule{
{
Path: "/",
balancer: address(listener.Addr().String()),
ReadTimeout: test.readTimeout,
},
},
}
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatalf("Test %d: Unable to create request: %v", i, err)
}
w := httptest.NewRecorder()
wg.Add(1)
go fcgi.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(test.sleep)
w.WriteHeader(http.StatusOK)
wg.Done()
}))
got, err := handler.ServeHTTP(w, r)
if test.shouldErr {
if err == nil {
t.Errorf("Test %d: Expected i/o timeout error but had none", i)
} else if err, ok := err.(net.Error); !ok || !err.Timeout() {
t.Errorf("Test %d: Expected i/o timeout error, got: '%s'", i, err.Error())
}
want := http.StatusGatewayTimeout
if got != want {
t.Errorf("Test %d: Expected returned status code to be %d, got: %d",
i, want, got)
}
} else if err != nil {
t.Errorf("Test %d: Expected nil error, got: %v", i, err)
}
wg.Wait()
}
}
func TestSendTimeout(t *testing.T) {
tests := []struct {
sendTimeout time.Duration
shouldErr bool
}{
{-1 * time.Second, true},
{time.Minute, false},
}
for i, test := range tests {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("Test %d: Unable to create listener for test: %v", i, err)
}
defer listener.Close()
handler := Handler{
Next: nil,
Rules: []Rule{
{
Path: "/",
balancer: address(listener.Addr().String()),
SendTimeout: test.sendTimeout,
},
},
}
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatalf("Test %d: Unable to create request: %v", i, err)
}
w := httptest.NewRecorder()
go fcgi.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
got, err := handler.ServeHTTP(w, r)
if test.shouldErr {
if err == nil {
t.Errorf("Test %d: Expected i/o timeout error but had none", i)
} else if err, ok := err.(net.Error); !ok || !err.Timeout() {
t.Errorf("Test %d: Expected i/o timeout error, got: '%s'", i, err.Error())
}
want := http.StatusGatewayTimeout
if got != want {
t.Errorf("Test %d: Expected returned status code to be %d, got: %d",
i, want, got)
}
} else if err != nil {
t.Errorf("Test %d: Expected nil error, got: %v", i, err)
}
}
}
func TestBalancer(t *testing.T) {
tests := [][]string{
{"localhost", "host.local"},
{"localhost"},
{"localhost", "host.local", "example.com"},
{"localhost", "host.local", "example.com", "127.0.0.1"},
}
for i, test := range tests {
b := address(test...)
for _, host := range test {
a, err := b.Address()
if err != nil {
t.Errorf("Unexpected error in trying to retrieve address: %s", err.Error())
}
if a != host {
t.Errorf("Test %d: expected %s, found %s", i, host, a)
}
}
}
}
func address(addresses ...string) balancer {
return &roundRobin{
addresses: addresses,
index: -1,
}
}

View File

@@ -0,0 +1,79 @@
<?php
ini_set("display_errors",1);
echo "resp: start\n";//.print_r($GLOBALS,1)."\n".print_r($_SERVER,1)."\n";
//echo print_r($_SERVER,1)."\n";
$length = 0;
$stat = "PASSED";
$ret = "[";
if (count($_POST) || count($_FILES)) {
foreach($_POST as $key => $val) {
$md5 = md5($val);
if ($key != $md5) {
$stat = "FAILED";
echo "server:err ".$md5." != ".$key."\n";
}
$length += strlen($key) + strlen($val);
$ret .= $key."(".strlen($key).") ";
}
$ret .= "] [";
foreach ($_FILES as $k0 => $val) {
$error = $val["error"];
if ($error == UPLOAD_ERR_OK) {
$tmp_name = $val["tmp_name"];
$name = $val["name"];
$datafile = "/tmp/test.go";
move_uploaded_file($tmp_name, $datafile);
$md5 = md5_file($datafile);
if ($k0 != $md5) {
$stat = "FAILED";
echo "server:err ".$md5." != ".$key."\n";
}
$length += strlen($k0) + filesize($datafile);
unlink($datafile);
$ret .= $k0."(".strlen($k0).") ";
}
else{
$stat = "FAILED";
echo "server:file err ".file_upload_error_message($error)."\n";
}
}
$ret .= "]";
echo "server:got data length " .$length."\n";
}
echo "-{$stat}-POST(".count($_POST).") FILE(".count($_FILES).")\n";
function file_upload_error_message($error_code) {
switch ($error_code) {
case UPLOAD_ERR_INI_SIZE:
return 'The uploaded file exceeds the upload_max_filesize directive in php.ini';
case UPLOAD_ERR_FORM_SIZE:
return 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form';
case UPLOAD_ERR_PARTIAL:
return 'The uploaded file was only partially uploaded';
case UPLOAD_ERR_NO_FILE:
return 'No file was uploaded';
case UPLOAD_ERR_NO_TMP_DIR:
return 'Missing a temporary folder';
case UPLOAD_ERR_CANT_WRITE:
return 'Failed to write file to disk';
case UPLOAD_ERR_EXTENSION:
return 'File upload stopped by extension';
default:
return 'Unknown upload error';
}
}

View File

@@ -0,0 +1,580 @@
// 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 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 this 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
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, 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
}
// 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" }

View File

@@ -0,0 +1,290 @@
// 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.
// 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
// sepicify remote fcgi responer 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) {
req.ParseMultipartForm(100000000)
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())
fo.Write(buf)
h.Write(buf)
}
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 {
// handle error
log.Println("listener creation failed: ", err)
}
srv := new(FastCGIServer)
fcgi.Serve(listener, srv)
}()
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)
os.Remove(path0)
os.Remove(path1)
}

View File

@@ -0,0 +1,215 @@
// 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.
package fastcgi
import (
"errors"
"fmt"
"net"
"net/http"
"path/filepath"
"strings"
"time"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
func init() {
caddy.RegisterPlugin("fastcgi", caddy.Plugin{
ServerType: "http",
Action: setup,
})
}
// setup configures a new FastCGI middleware instance.
func setup(c *caddy.Controller) error {
cfg := httpserver.GetConfig(c)
rules, err := fastcgiParse(c)
if err != nil {
return err
}
cfg.AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
return Handler{
Next: next,
Rules: rules,
Root: cfg.Root,
FileSys: http.Dir(cfg.Root),
SoftwareName: caddy.AppName,
SoftwareVersion: caddy.AppVersion,
ServerName: cfg.Addr.Host,
ServerPort: cfg.Addr.Port,
}
})
return nil
}
func fastcgiParse(c *caddy.Controller) ([]Rule, error) {
var rules []Rule
cfg := httpserver.GetConfig(c)
absRoot, err := filepath.Abs(cfg.Root)
if err != nil {
return nil, err
}
for c.Next() {
args := c.RemainingArgs()
if len(args) < 2 || len(args) > 3 {
return rules, c.ArgErr()
}
rule := Rule{
Root: absRoot,
Path: args[0],
}
upstreams := []string{args[1]}
srvUpstream := false
if strings.HasPrefix(upstreams[0], "srv://") {
srvUpstream = true
}
if len(args) == 3 {
if err := fastcgiPreset(args[2], &rule); err != nil {
return rules, err
}
}
var err error
for c.NextBlock() {
switch c.Val() {
case "root":
if !c.NextArg() {
return rules, c.ArgErr()
}
rule.Root = c.Val()
case "ext":
if !c.NextArg() {
return rules, c.ArgErr()
}
rule.Ext = c.Val()
case "split":
if !c.NextArg() {
return rules, c.ArgErr()
}
rule.SplitPath = c.Val()
case "index":
args := c.RemainingArgs()
if len(args) == 0 {
return rules, c.ArgErr()
}
rule.IndexFiles = args
case "upstream":
if srvUpstream {
return rules, c.Err("additional upstreams are not supported with SRV upstream")
}
args := c.RemainingArgs()
if len(args) != 1 {
return rules, c.ArgErr()
}
upstreams = append(upstreams, args[0])
case "env":
envArgs := c.RemainingArgs()
if len(envArgs) < 2 {
return rules, c.ArgErr()
}
rule.EnvVars = append(rule.EnvVars, [2]string{envArgs[0], envArgs[1]})
case "except":
ignoredPaths := c.RemainingArgs()
if len(ignoredPaths) == 0 {
return rules, c.ArgErr()
}
rule.IgnoredSubPaths = ignoredPaths
case "connect_timeout":
if !c.NextArg() {
return rules, c.ArgErr()
}
rule.ConnectTimeout, err = time.ParseDuration(c.Val())
if err != nil {
return rules, err
}
case "read_timeout":
if !c.NextArg() {
return rules, c.ArgErr()
}
readTimeout, err := time.ParseDuration(c.Val())
if err != nil {
return rules, err
}
rule.ReadTimeout = readTimeout
case "send_timeout":
if !c.NextArg() {
return rules, c.ArgErr()
}
sendTimeout, err := time.ParseDuration(c.Val())
if err != nil {
return rules, err
}
rule.SendTimeout = sendTimeout
}
}
if srvUpstream {
balancer, err := parseSRV(upstreams[0])
if err != nil {
return rules, c.Err("malformed service locator string: " + err.Error())
}
rule.balancer = balancer
} else {
rule.balancer = &roundRobin{addresses: upstreams, index: -1}
}
rules = append(rules, rule)
}
return rules, nil
}
func parseSRV(locator string) (*srv, error) {
if locator[6:] == "" {
return nil, fmt.Errorf("%s does not include the host", locator)
}
return &srv{
service: locator[6:],
resolver: &net.Resolver{},
}, nil
}
// fastcgiPreset configures rule according to name. It returns an error if
// name is not a recognized preset name.
func fastcgiPreset(name string, rule *Rule) error {
switch name {
case "php":
rule.Ext = ".php"
rule.SplitPath = ".php"
rule.IndexFiles = []string{"index.php"}
default:
return errors.New(name + " is not a valid preset name")
}
return nil
}

View File

@@ -0,0 +1,224 @@
// 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.
package fastcgi
import (
"context"
"fmt"
"net"
"testing"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
func TestSetup(t *testing.T) {
c := caddy.NewTestController("http", `fastcgi / 127.0.0.1:9000`)
err := setup(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
mids := httpserver.GetConfig(c).Middleware()
if len(mids) == 0 {
t.Fatal("Expected middleware, got 0 instead")
}
handler := mids[0](httpserver.EmptyNext)
myHandler, ok := handler.(Handler)
if !ok {
t.Fatalf("Expected handler to be type , got: %#v", handler)
}
if myHandler.Rules[0].Path != "/" {
t.Errorf("Expected / as the Path")
}
addr, err := myHandler.Rules[0].Address()
if err != nil {
t.Errorf("Unexpected error in trying to retrieve address: %s", err.Error())
}
if addr != "127.0.0.1:9000" {
t.Errorf("Expected 127.0.0.1:9000 as the Address")
}
}
func TestFastcgiParse(t *testing.T) {
tests := []struct {
inputFastcgiConfig string
shouldErr bool
expectedFastcgiConfig []Rule
}{
{`fastcgi /blog 127.0.0.1:9000 php`,
false, []Rule{{
Path: "/blog",
balancer: &roundRobin{addresses: []string{"127.0.0.1:9000"}},
Ext: ".php",
SplitPath: ".php",
IndexFiles: []string{"index.php"},
}}},
{`fastcgi / 127.0.0.1:9001 {
split .html
}`,
false, []Rule{{
Path: "/",
balancer: &roundRobin{addresses: []string{"127.0.0.1:9001"}},
Ext: "",
SplitPath: ".html",
IndexFiles: []string{},
}}},
{`fastcgi / 127.0.0.1:9001 {
split .html
except /admin /user
}`,
false, []Rule{{
Path: "/",
balancer: &roundRobin{addresses: []string{"127.0.0.1:9001"}},
Ext: "",
SplitPath: ".html",
IndexFiles: []string{},
IgnoredSubPaths: []string{"/admin", "/user"},
}}},
}
for i, test := range tests {
actualFastcgiConfigs, err := fastcgiParse(caddy.NewTestController("http", test.inputFastcgiConfig))
if err == nil && test.shouldErr {
t.Errorf("Test %d didn't error, but it should have", i)
} else if err != nil && !test.shouldErr {
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
}
if len(actualFastcgiConfigs) != len(test.expectedFastcgiConfig) {
t.Fatalf("Test %d expected %d no of FastCGI configs, but got %d ",
i, len(test.expectedFastcgiConfig), len(actualFastcgiConfigs))
}
for j, actualFastcgiConfig := range actualFastcgiConfigs {
if actualFastcgiConfig.Path != test.expectedFastcgiConfig[j].Path {
t.Errorf("Test %d expected %dth FastCGI Path to be %s , but got %s",
i, j, test.expectedFastcgiConfig[j].Path, actualFastcgiConfig.Path)
}
actualAddr, err := actualFastcgiConfig.Address()
if err != nil {
t.Errorf("Test %d unexpected error in trying to retrieve %dth actual address: %s", i, j, err.Error())
}
expectedAddr, err := test.expectedFastcgiConfig[j].Address()
if err != nil {
t.Errorf("Test %d unexpected error in trying to retrieve %dth expected address: %s", i, j, err.Error())
}
if actualAddr != expectedAddr {
t.Errorf("Test %d expected %dth FastCGI Address to be %s , but got %s",
i, j, expectedAddr, actualAddr)
}
if actualFastcgiConfig.Ext != test.expectedFastcgiConfig[j].Ext {
t.Errorf("Test %d expected %dth FastCGI Ext to be %s , but got %s",
i, j, test.expectedFastcgiConfig[j].Ext, actualFastcgiConfig.Ext)
}
if actualFastcgiConfig.SplitPath != test.expectedFastcgiConfig[j].SplitPath {
t.Errorf("Test %d expected %dth FastCGI SplitPath to be %s , but got %s",
i, j, test.expectedFastcgiConfig[j].SplitPath, actualFastcgiConfig.SplitPath)
}
if fmt.Sprint(actualFastcgiConfig.IndexFiles) != fmt.Sprint(test.expectedFastcgiConfig[j].IndexFiles) {
t.Errorf("Test %d expected %dth FastCGI IndexFiles to be %s , but got %s",
i, j, test.expectedFastcgiConfig[j].IndexFiles, actualFastcgiConfig.IndexFiles)
}
if fmt.Sprint(actualFastcgiConfig.IgnoredSubPaths) != fmt.Sprint(test.expectedFastcgiConfig[j].IgnoredSubPaths) {
t.Errorf("Test %d expected %dth FastCGI IgnoredSubPaths to be %s , but got %s",
i, j, test.expectedFastcgiConfig[j].IgnoredSubPaths, actualFastcgiConfig.IgnoredSubPaths)
}
}
}
}
func TestFastCGIResolveSRV(t *testing.T) {
tests := []struct {
inputFastcgiConfig string
locator string
target string
port uint16
shouldErr bool
}{
{
`fastcgi / srv://fpm.tcp.service.consul {
upstream yolo
}`,
"fpm.tcp.service.consul",
"127.0.0.1",
9000,
true,
},
{
`fastcgi / srv://fpm.tcp.service.consul`,
"fpm.tcp.service.consul",
"127.0.0.1",
9000,
false,
},
}
for i, test := range tests {
actualFastcgiConfigs, err := fastcgiParse(caddy.NewTestController("http", test.inputFastcgiConfig))
if err == nil && test.shouldErr {
t.Errorf("Test %d didn't error, but it should have", i)
} else if err != nil && !test.shouldErr {
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
}
for _, actualFastcgiConfig := range actualFastcgiConfigs {
resolver, ok := (actualFastcgiConfig.balancer).(*srv)
if !ok {
t.Errorf("Test %d upstream balancer is not srv", i)
}
resolver.resolver = buildTestResolver(test.target, test.port)
addr, err := actualFastcgiConfig.Address()
if err != nil {
t.Errorf("Test %d failed to retrieve upstream address. %s", i, err.Error())
}
expectedAddr := fmt.Sprintf("%s:%d", test.target, test.port)
if addr != expectedAddr {
t.Errorf("Test %d expected upstream address to be %s, got %s", i, expectedAddr, addr)
}
}
}
}
func buildTestResolver(target string, port uint16) srvResolver {
return &testSRVResolver{target, port}
}
type testSRVResolver struct {
target string
port uint16
}
func (r *testSRVResolver) LookupSRV(ctx context.Context, service, proto, name string) (string, []*net.SRV, error) {
return "", []*net.SRV{
{Target: r.target,
Port: r.port,
Priority: 1,
Weight: 1}}, nil
}