mirror of
https://github.com/cloudflare/cloudflared.git
synced 2025-07-29 08:29:57 +00:00
TUN-528: Move cloudflared into a separate repo
This commit is contained in:
491
vendor/github.com/mholt/caddy/caddyhttp/fastcgi/fastcgi.go
generated
vendored
Normal file
491
vendor/github.com/mholt/caddy/caddyhttp/fastcgi/fastcgi.go
generated
vendored
Normal 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",
|
||||
}
|
388
vendor/github.com/mholt/caddy/caddyhttp/fastcgi/fastcgi_test.go
generated
vendored
Normal file
388
vendor/github.com/mholt/caddy/caddyhttp/fastcgi/fastcgi_test.go
generated
vendored
Normal 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,
|
||||
}
|
||||
}
|
79
vendor/github.com/mholt/caddy/caddyhttp/fastcgi/fcgi_test.php
generated
vendored
Normal file
79
vendor/github.com/mholt/caddy/caddyhttp/fastcgi/fcgi_test.php
generated
vendored
Normal 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';
|
||||
}
|
||||
}
|
580
vendor/github.com/mholt/caddy/caddyhttp/fastcgi/fcgiclient.go
generated
vendored
Normal file
580
vendor/github.com/mholt/caddy/caddyhttp/fastcgi/fcgiclient.go
generated
vendored
Normal 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" }
|
290
vendor/github.com/mholt/caddy/caddyhttp/fastcgi/fcgiclient_test.go
generated
vendored
Normal file
290
vendor/github.com/mholt/caddy/caddyhttp/fastcgi/fcgiclient_test.go
generated
vendored
Normal 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)
|
||||
}
|
215
vendor/github.com/mholt/caddy/caddyhttp/fastcgi/setup.go
generated
vendored
Normal file
215
vendor/github.com/mholt/caddy/caddyhttp/fastcgi/setup.go
generated
vendored
Normal 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
|
||||
}
|
224
vendor/github.com/mholt/caddy/caddyhttp/fastcgi/setup_test.go
generated
vendored
Normal file
224
vendor/github.com/mholt/caddy/caddyhttp/fastcgi/setup_test.go
generated
vendored
Normal 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
|
||||
}
|
Reference in New Issue
Block a user