You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							330 lines
						
					
					
						
							10 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							330 lines
						
					
					
						
							10 KiB
						
					
					
				| // Copyright 2015 Matthew Holt and The Caddy Authors | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| //     http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
|  | |
| package fastcgi | |
| 
 | |
| import ( | |
| 	"context" | |
| 	"crypto/tls" | |
| 	"fmt" | |
| 	"net" | |
| 	"net/http" | |
| 	"path" | |
| 	"path/filepath" | |
| 	"strconv" | |
| 	"strings" | |
| 	"time" | |
| 
 | |
| 	"github.com/caddyserver/caddy/v2/modules/caddyhttp" | |
| 	"github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy" | |
| 	"github.com/caddyserver/caddy/v2/modules/caddytls" | |
| 	"go.uber.org/zap" | |
| 
 | |
| 	"github.com/caddyserver/caddy/v2" | |
| ) | |
| 
 | |
| func init() { | |
| 	caddy.RegisterModule(Transport{}) | |
| } | |
| 
 | |
| // Transport facilitates FastCGI communication. | |
| type Transport struct { | |
| 	// Use this directory as the fastcgi root directory. Defaults to the root | |
| 	// directory of the parent virtual host. | |
| 	Root string `json:"root,omitempty"` | |
| 
 | |
| 	// The path in the URL will be split into two, with the first piece ending | |
| 	// with the value of SplitPath. The first piece will be assumed as the | |
| 	// actual resource (CGI script) name, and the second piece will be set to | |
| 	// PATH_INFO for the CGI script to use. | |
| 	// | |
| 	// Future enhancements should be careful to avoid CVE-2019-11043, | |
| 	// which can be mitigated with use of a try_files-like behavior | |
| 	// that 404s if the fastcgi path info is not found. | |
| 	SplitPath []string `json:"split_path,omitempty"` | |
| 
 | |
| 	// Extra environment variables. | |
| 	EnvVars map[string]string `json:"env,omitempty"` | |
| 
 | |
| 	// The duration used to set a deadline when connecting to an upstream. | |
| 	DialTimeout caddy.Duration `json:"dial_timeout,omitempty"` | |
| 
 | |
| 	// The duration used to set a deadline when reading from the FastCGI server. | |
| 	ReadTimeout caddy.Duration `json:"read_timeout,omitempty"` | |
| 
 | |
| 	// The duration used to set a deadline when sending to the FastCGI server. | |
| 	WriteTimeout caddy.Duration `json:"write_timeout,omitempty"` | |
| 
 | |
| 	serverSoftware string | |
| 	logger         *zap.Logger | |
| } | |
| 
 | |
| // CaddyModule returns the Caddy module information. | |
| func (Transport) CaddyModule() caddy.ModuleInfo { | |
| 	return caddy.ModuleInfo{ | |
| 		ID:  "http.reverse_proxy.transport.fastcgi", | |
| 		New: func() caddy.Module { return new(Transport) }, | |
| 	} | |
| } | |
| 
 | |
| // Provision sets up t. | |
| func (t *Transport) Provision(ctx caddy.Context) error { | |
| 	t.logger = ctx.Logger(t) | |
| 	if t.Root == "" { | |
| 		t.Root = "{http.vars.root}" | |
| 	} | |
| 	t.serverSoftware = "Caddy" | |
| 	if mod := caddy.GoModule(); mod.Version != "" { | |
| 		t.serverSoftware += "/" + mod.Version | |
| 	} | |
| 	return nil | |
| } | |
| 
 | |
| // RoundTrip implements http.RoundTripper. | |
| func (t Transport) RoundTrip(r *http.Request) (*http.Response, error) { | |
| 	env, err := t.buildEnv(r) | |
| 	if err != nil { | |
| 		return nil, fmt.Errorf("building environment: %v", err) | |
| 	} | |
| 
 | |
| 	// TODO: doesn't dialer have a Timeout field? | |
| 	ctx := r.Context() | |
| 	if t.DialTimeout > 0 { | |
| 		var cancel context.CancelFunc | |
| 		ctx, cancel = context.WithTimeout(ctx, time.Duration(t.DialTimeout)) | |
| 		defer cancel() | |
| 	} | |
| 
 | |
| 	// extract dial information from request (should have been embedded by the reverse proxy) | |
| 	network, address := "tcp", r.URL.Host | |
| 	if dialInfo, ok := reverseproxy.GetDialInfo(ctx); ok { | |
| 		network = dialInfo.Network | |
| 		address = dialInfo.Address | |
| 	} | |
| 
 | |
| 	t.logger.Debug("roundtrip", | |
| 		zap.Object("request", caddyhttp.LoggableHTTPRequest{Request: r}), | |
| 		zap.String("dial", address), | |
| 		zap.Any("env", env), // TODO: this uses reflection I think | |
| 	) | |
| 
 | |
| 	fcgiBackend, err := DialContext(ctx, network, address) | |
| 	if err != nil { | |
| 		// TODO: wrap in a special error type if the dial failed, so retries can happen if enabled | |
| 		return nil, fmt.Errorf("dialing backend: %v", err) | |
| 	} | |
| 	// fcgiBackend gets closed when response body is closed (see clientCloser) | |
|  | |
| 	// read/write timeouts | |
| 	if err := fcgiBackend.SetReadTimeout(time.Duration(t.ReadTimeout)); err != nil { | |
| 		return nil, fmt.Errorf("setting read timeout: %v", err) | |
| 	} | |
| 	if err := fcgiBackend.SetWriteTimeout(time.Duration(t.WriteTimeout)); err != nil { | |
| 		return nil, fmt.Errorf("setting write timeout: %v", err) | |
| 	} | |
| 
 | |
| 	contentLength := r.ContentLength | |
| 	if contentLength == 0 { | |
| 		contentLength, _ = strconv.ParseInt(r.Header.Get("Content-Length"), 10, 64) | |
| 	} | |
| 
 | |
| 	var resp *http.Response | |
| 	switch r.Method { | |
| 	case http.MethodHead: | |
| 		resp, err = fcgiBackend.Head(env) | |
| 	case http.MethodGet: | |
| 		resp, err = fcgiBackend.Get(env, r.Body, contentLength) | |
| 	case http.MethodOptions: | |
| 		resp, err = fcgiBackend.Options(env) | |
| 	default: | |
| 		resp, err = fcgiBackend.Post(env, r.Method, r.Header.Get("Content-Type"), r.Body, contentLength) | |
| 	} | |
| 
 | |
| 	return resp, err | |
| } | |
| 
 | |
| // buildEnv returns a set of CGI environment variables for the request. | |
| func (t Transport) buildEnv(r *http.Request) (map[string]string, error) { | |
| 	repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) | |
| 
 | |
| 	var env map[string]string | |
| 
 | |
| 	// Separate remote IP and port; more lenient than net.SplitHostPort | |
| 	var ip, port string | |
| 	if idx := strings.LastIndex(r.RemoteAddr, ":"); idx > -1 { | |
| 		ip = r.RemoteAddr[:idx] | |
| 		port = r.RemoteAddr[idx+1:] | |
| 	} else { | |
| 		ip = r.RemoteAddr | |
| 	} | |
| 
 | |
| 	// Remove [] from IPv6 addresses | |
| 	ip = strings.Replace(ip, "[", "", 1) | |
| 	ip = strings.Replace(ip, "]", "", 1) | |
| 
 | |
| 	// make sure file root is absolute | |
| 	root, err := filepath.Abs(repl.ReplaceAll(t.Root, ".")) | |
| 	if err != nil { | |
| 		return nil, err | |
| 	} | |
| 
 | |
| 	fpath := r.URL.Path | |
| 
 | |
| 	// split "actual path" from "path info" if configured | |
| 	var docURI, pathInfo string | |
| 	if splitPos := t.splitPos(fpath); splitPos > -1 { | |
| 		docURI = fpath[:splitPos] | |
| 		pathInfo = fpath[splitPos:] | |
| 	} else { | |
| 		docURI = fpath | |
| 	} | |
| 	scriptName := fpath | |
| 
 | |
| 	// Strip PATH_INFO from SCRIPT_NAME | |
| 	scriptName = strings.TrimSuffix(scriptName, pathInfo) | |
| 
 | |
| 	// SCRIPT_FILENAME is the absolute path of SCRIPT_NAME | |
| 	scriptFilename := filepath.Join(root, scriptName) | |
| 
 | |
| 	// Add vhost path prefix to scriptName. Otherwise, some PHP software will | |
| 	// have difficulty discovering its URL. | |
| 	pathPrefix, _ := r.Context().Value(caddy.CtxKey("path_prefix")).(string) | |
| 	scriptName = path.Join(pathPrefix, scriptName) | |
| 
 | |
| 	// Get the request URL from context. The context stores the original URL in case | |
| 	// it was changed by a middleware such as rewrite. By default, we pass the | |
| 	// original URI in as the value of REQUEST_URI (the user can overwrite this | |
| 	// if desired). Most PHP apps seem to want the original URI. Besides, this is | |
| 	// how nginx defaults: http://stackoverflow.com/a/12485156/1048862 | |
| 	origReq, ok := r.Context().Value(caddyhttp.OriginalRequestCtxKey).(http.Request) | |
| 	if !ok { | |
| 		// some requests, like active health checks, don't add this to | |
| 		// the request context, so we can just use the current URL | |
| 		origReq = *r | |
| 	} | |
| 	reqURL := origReq.URL | |
| 
 | |
| 	requestScheme := "http" | |
| 	if r.TLS != nil { | |
| 		requestScheme = "https" | |
| 	} | |
| 
 | |
| 	reqHost, reqPort, err := net.SplitHostPort(r.Host) | |
| 	if err != nil { | |
| 		// whatever, just assume there was no port | |
| 		reqHost = r.Host | |
| 	} | |
| 
 | |
| 	// Some variables are unused but cleared explicitly to prevent | |
| 	// the parent environment from interfering. | |
| 	env = map[string]string{ | |
| 		// Variables defined in CGI 1.1 spec | |
| 		"AUTH_TYPE":         "", // Not used | |
| 		"CONTENT_LENGTH":    r.Header.Get("Content-Length"), | |
| 		"CONTENT_TYPE":      r.Header.Get("Content-Type"), | |
| 		"GATEWAY_INTERFACE": "CGI/1.1", | |
| 		"PATH_INFO":         pathInfo, | |
| 		"QUERY_STRING":      r.URL.RawQuery, | |
| 		"REMOTE_ADDR":       ip, | |
| 		"REMOTE_HOST":       ip, // For speed, remote host lookups disabled | |
| 		"REMOTE_PORT":       port, | |
| 		"REMOTE_IDENT":      "", // Not used | |
| 		"REMOTE_USER":       "", // TODO: once there are authentication handlers, populate this | |
| 		"REQUEST_METHOD":    r.Method, | |
| 		"REQUEST_SCHEME":    requestScheme, | |
| 		"SERVER_NAME":       reqHost, | |
| 		"SERVER_PORT":       reqPort, | |
| 		"SERVER_PROTOCOL":   r.Proto, | |
| 		"SERVER_SOFTWARE":   t.serverSoftware, | |
| 
 | |
| 		// Other variables | |
| 		"DOCUMENT_ROOT":   root, | |
| 		"DOCUMENT_URI":    docURI, | |
| 		"HTTP_HOST":       r.Host, // added here, since not always part of headers | |
| 		"REQUEST_URI":     reqURL.RequestURI(), | |
| 		"SCRIPT_FILENAME": scriptFilename, | |
| 		"SCRIPT_NAME":     scriptName, | |
| 	} | |
| 
 | |
| 	// compliance with the CGI specification requires that | |
| 	// PATH_TRANSLATED should only exist if PATH_INFO is defined. | |
| 	// Info: https://www.ietf.org/rfc/rfc3875 Page 14 | |
| 	if env["PATH_INFO"] != "" { | |
| 		env["PATH_TRANSLATED"] = filepath.Join(root, pathInfo) // Info: http://www.oreilly.com/openbook/cgi/ch02_04.html | |
| 	} | |
| 
 | |
| 	// Some web apps rely on knowing HTTPS or not | |
| 	if r.TLS != nil { | |
| 		env["HTTPS"] = "on" | |
| 		// and pass the protocol details in a manner compatible with apache's mod_ssl | |
| 		// (which is why these have a SSL_ prefix and not TLS_). | |
| 		v, ok := tlsProtocolStrings[r.TLS.Version] | |
| 		if ok { | |
| 			env["SSL_PROTOCOL"] = v | |
| 		} | |
| 		// and pass the cipher suite in a manner compatible with apache's mod_ssl | |
| 		for _, cs := range caddytls.SupportedCipherSuites() { | |
| 			if cs.ID == r.TLS.CipherSuite { | |
| 				env["SSL_CIPHER"] = cs.Name | |
| 				break | |
| 			} | |
| 		} | |
| 	} | |
| 
 | |
| 	// Add env variables from config (with support for placeholders in values) | |
| 	for key, value := range t.EnvVars { | |
| 		env[key] = repl.ReplaceAll(value, "") | |
| 	} | |
| 
 | |
| 	// Add all HTTP headers to env variables | |
| 	for field, val := range r.Header { | |
| 		header := strings.ToUpper(field) | |
| 		header = headerNameReplacer.Replace(header) | |
| 		env["HTTP_"+header] = strings.Join(val, ", ") | |
| 	} | |
| 	return env, nil | |
| } | |
| 
 | |
| // splitPos returns the index where path should | |
| // be split based on t.SplitPath. | |
| func (t Transport) splitPos(path string) int { | |
| 	// TODO: from v1... | |
| 	// if httpserver.CaseSensitivePath { | |
| 	// 	return strings.Index(path, r.SplitPath) | |
| 	// } | |
| 	lowerPath := strings.ToLower(path) | |
| 	for _, split := range t.SplitPath { | |
| 		if idx := strings.Index(lowerPath, strings.ToLower(split)); idx > -1 { | |
| 			return idx + len(split) | |
| 		} | |
| 	} | |
| 	return -1 | |
| } | |
| 
 | |
| // Map of supported protocols to Apache ssl_mod format | |
| // Note that these are slightly different from SupportedProtocols in caddytls/config.go | |
| var tlsProtocolStrings = map[uint16]string{ | |
| 	tls.VersionTLS10: "TLSv1", | |
| 	tls.VersionTLS11: "TLSv1.1", | |
| 	tls.VersionTLS12: "TLSv1.2", | |
| 	tls.VersionTLS13: "TLSv1.3", | |
| } | |
| 
 | |
| var headerNameReplacer = strings.NewReplacer(" ", "_", "-", "_") | |
| 
 | |
| // Interface guards | |
| var ( | |
| 	_ caddy.Provisioner = (*Transport)(nil) | |
| 	_ http.RoundTripper = (*Transport)(nil) | |
| )
 | |
| 
 |