mirror of
				https://github.com/caddyserver/caddy.git
				synced 2025-11-04 03:27:23 -05:00 
			
		
		
		
	* logging: Initial implementation * logging: More encoder formats, better defaults * logging: Fix repetition bug with FilterEncoder; add more presets * logging: DiscardWriter; delete or no-op logs that discard their output * logging: Add http.handlers.log module; enhance Replacer methods The Replacer interface has new methods to customize how to handle empty or unrecognized placeholders. Closes #2815. * logging: Overhaul HTTP logging, fix bugs, improve filtering, etc. * logging: General cleanup, begin transitioning to using new loggers * Fixes after merge conflict
		
			
				
	
	
		
			306 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			306 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// Copyright 2015 Matthew Holt and The Caddy Authors
 | 
						|
//
 | 
						|
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
						|
// you may not use this file except in compliance with the License.
 | 
						|
// You may obtain a copy of the License at
 | 
						|
//
 | 
						|
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
						|
//
 | 
						|
// Unless required by applicable law or agreed to in writing, software
 | 
						|
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
						|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
// See the License for the specific language governing permissions and
 | 
						|
// limitations under the License.
 | 
						|
 | 
						|
package fastcgi
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"crypto/tls"
 | 
						|
	"fmt"
 | 
						|
	"net/http"
 | 
						|
	"path"
 | 
						|
	"path/filepath"
 | 
						|
	"strconv"
 | 
						|
	"strings"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/caddyserver/caddy/v2/modules/caddyhttp"
 | 
						|
	"github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
 | 
						|
	"github.com/caddyserver/caddy/v2/modules/caddytls"
 | 
						|
 | 
						|
	"github.com/caddyserver/caddy/v2"
 | 
						|
)
 | 
						|
 | 
						|
func init() {
 | 
						|
	caddy.RegisterModule(Transport{})
 | 
						|
}
 | 
						|
 | 
						|
// Transport facilitates FastCGI communication.
 | 
						|
type Transport struct {
 | 
						|
	// TODO: Populate these
 | 
						|
	softwareName    string
 | 
						|
	softwareVersion string
 | 
						|
	serverName      string
 | 
						|
	serverPort      string
 | 
						|
 | 
						|
	// Use this directory as the fastcgi root directory. Defaults to the root
 | 
						|
	// directory of the parent virtual host.
 | 
						|
	Root string `json:"root,omitempty"`
 | 
						|
 | 
						|
	// The path in the URL will be split into two, with the first piece ending
 | 
						|
	// with the value of SplitPath. The first piece will be assumed as the
 | 
						|
	// actual resource (CGI script) name, and the second piece will be set to
 | 
						|
	// PATH_INFO for the CGI script to use.
 | 
						|
	// Future enhancements should be careful to avoid CVE-2019-11043,
 | 
						|
	// which can be mitigated with use of a try_files-like behavior
 | 
						|
	// that 404's 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"`
 | 
						|
}
 | 
						|
 | 
						|
// CaddyModule returns the Caddy module information.
 | 
						|
func (Transport) CaddyModule() caddy.ModuleInfo {
 | 
						|
	return caddy.ModuleInfo{
 | 
						|
		Name: "http.handlers.reverse_proxy.transport.fastcgi",
 | 
						|
		New:  func() caddy.Module { return new(Transport) },
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// Provision sets up t.
 | 
						|
func (t *Transport) Provision(_ caddy.Context) error {
 | 
						|
	if t.Root == "" {
 | 
						|
		t.Root = "{http.vars.root}"
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// RoundTrip implements http.RoundTripper.
 | 
						|
func (t Transport) RoundTrip(r *http.Request) (*http.Response, error) {
 | 
						|
	env, err := t.buildEnv(r)
 | 
						|
	if err != nil {
 | 
						|
		return nil, fmt.Errorf("building environment: %v", err)
 | 
						|
	}
 | 
						|
 | 
						|
	// TODO: doesn't dialer have a Timeout field?
 | 
						|
	ctx := r.Context()
 | 
						|
	if t.DialTimeout > 0 {
 | 
						|
		var cancel context.CancelFunc
 | 
						|
		ctx, cancel = context.WithTimeout(ctx, time.Duration(t.DialTimeout))
 | 
						|
		defer cancel()
 | 
						|
	}
 | 
						|
 | 
						|
	// extract dial information from request (this
 | 
						|
	// should embedded by the reverse proxy)
 | 
						|
	network, address := "tcp", r.URL.Host
 | 
						|
	if dialInfoVal := ctx.Value(reverseproxy.DialInfoCtxKey); dialInfoVal != nil {
 | 
						|
		dialInfo := dialInfoVal.(reverseproxy.DialInfo)
 | 
						|
		network = dialInfo.Network
 | 
						|
		address = dialInfo.Address
 | 
						|
	}
 | 
						|
 | 
						|
	fcgiBackend, err := DialContext(ctx, network, address)
 | 
						|
	if err != nil {
 | 
						|
		// 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)
 | 
						|
 | 
						|
	root := repl.ReplaceAll(t.Root, ".")
 | 
						|
	fpath := r.URL.Path
 | 
						|
 | 
						|
	// Split path in preparation for env variables.
 | 
						|
	// Previous canSplit checks ensure this can never be -1.
 | 
						|
	// TODO: I haven't brought over canSplit; make sure this doesn't break
 | 
						|
	splitPos := t.splitPos(fpath)
 | 
						|
 | 
						|
	// Request has the extension; path was split successfully
 | 
						|
	docURI := fpath[:splitPos+len(t.SplitPath)]
 | 
						|
	pathInfo := fpath[splitPos+len(t.SplitPath):]
 | 
						|
	scriptName := fpath
 | 
						|
 | 
						|
	// Strip PATH_INFO from SCRIPT_NAME
 | 
						|
	scriptName = strings.TrimSuffix(scriptName, pathInfo)
 | 
						|
 | 
						|
	// SCRIPT_FILENAME is the absolute path of SCRIPT_NAME
 | 
						|
	scriptFilename := filepath.Join(root, scriptName)
 | 
						|
 | 
						|
	// Add vhost path prefix to scriptName. Otherwise, some PHP software will
 | 
						|
	// have difficulty discovering its URL.
 | 
						|
	pathPrefix, _ := r.Context().Value(caddy.CtxKey("path_prefix")).(string)
 | 
						|
	scriptName = path.Join(pathPrefix, scriptName)
 | 
						|
 | 
						|
	// Get the request URL from context. The context stores the original URL in case
 | 
						|
	// it was changed by a middleware such as rewrite. By default, we pass the
 | 
						|
	// original URI in as the value of REQUEST_URI (the user can overwrite this
 | 
						|
	// if desired). Most PHP apps seem to want the original URI. Besides, this is
 | 
						|
	// how nginx defaults: http://stackoverflow.com/a/12485156/1048862
 | 
						|
	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"
 | 
						|
	}
 | 
						|
 | 
						|
	// Some variables are unused but cleared explicitly to prevent
 | 
						|
	// the parent environment from interfering.
 | 
						|
	env = map[string]string{
 | 
						|
		// Variables defined in CGI 1.1 spec
 | 
						|
		"AUTH_TYPE":         "", // Not used
 | 
						|
		"CONTENT_LENGTH":    r.Header.Get("Content-Length"),
 | 
						|
		"CONTENT_TYPE":      r.Header.Get("Content-Type"),
 | 
						|
		"GATEWAY_INTERFACE": "CGI/1.1",
 | 
						|
		"PATH_INFO":         pathInfo,
 | 
						|
		"QUERY_STRING":      r.URL.RawQuery,
 | 
						|
		"REMOTE_ADDR":       ip,
 | 
						|
		"REMOTE_HOST":       ip, // For speed, remote host lookups disabled
 | 
						|
		"REMOTE_PORT":       port,
 | 
						|
		"REMOTE_IDENT":      "", // Not used
 | 
						|
		"REMOTE_USER":       "", // TODO: once there are authentication handlers, populate this
 | 
						|
		"REQUEST_METHOD":    r.Method,
 | 
						|
		"REQUEST_SCHEME":    requestScheme,
 | 
						|
		"SERVER_NAME":       t.serverName,
 | 
						|
		"SERVER_PORT":       t.serverPort,
 | 
						|
		"SERVER_PROTOCOL":   r.Proto,
 | 
						|
		"SERVER_SOFTWARE":   t.softwareName + "/" + t.softwareVersion,
 | 
						|
 | 
						|
		// Other variables
 | 
						|
		"DOCUMENT_ROOT":   root,
 | 
						|
		"DOCUMENT_URI":    docURI,
 | 
						|
		"HTTP_HOST":       r.Host, // added here, since not always part of headers
 | 
						|
		"REQUEST_URI":     reqURL.RequestURI(),
 | 
						|
		"SCRIPT_FILENAME": scriptFilename,
 | 
						|
		"SCRIPT_NAME":     scriptName,
 | 
						|
	}
 | 
						|
 | 
						|
	// compliance with the CGI specification requires that
 | 
						|
	// PATH_TRANSLATED should only exist if PATH_INFO is defined.
 | 
						|
	// Info: https://www.ietf.org/rfc/rfc3875 Page 14
 | 
						|
	if env["PATH_INFO"] != "" {
 | 
						|
		env["PATH_TRANSLATED"] = filepath.Join(root, pathInfo) // Info: http://www.oreilly.com/openbook/cgi/ch02_04.html
 | 
						|
	}
 | 
						|
 | 
						|
	// Some web apps rely on knowing HTTPS or not
 | 
						|
	if r.TLS != nil {
 | 
						|
		env["HTTPS"] = "on"
 | 
						|
		// and pass the protocol details in a manner compatible with apache's mod_ssl
 | 
						|
		// (which is why these have a SSL_ prefix and not TLS_).
 | 
						|
		v, ok := tlsProtocolStrings[r.TLS.Version]
 | 
						|
		if ok {
 | 
						|
			env["SSL_PROTOCOL"] = v
 | 
						|
		}
 | 
						|
		// and pass the cipher suite in a manner compatible with apache's mod_ssl
 | 
						|
		for k, v := range caddytls.SupportedCipherSuites {
 | 
						|
			if v == r.TLS.CipherSuite {
 | 
						|
				env["SSL_CIPHER"] = k
 | 
						|
				break
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// Add env variables from config (with support for placeholders in values)
 | 
						|
	for 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:
 | 
						|
	// if httpserver.CaseSensitivePath {
 | 
						|
	// 	return strings.Index(path, r.SplitPath)
 | 
						|
	// }
 | 
						|
	return strings.Index(strings.ToLower(path), strings.ToLower(t.SplitPath))
 | 
						|
}
 | 
						|
 | 
						|
// TODO:
 | 
						|
// Map of supported protocols to Apache ssl_mod format
 | 
						|
// Note that these are slightly different from SupportedProtocols in caddytls/config.go
 | 
						|
var tlsProtocolStrings = map[uint16]string{
 | 
						|
	tls.VersionTLS10: "TLSv1",
 | 
						|
	tls.VersionTLS11: "TLSv1.1",
 | 
						|
	tls.VersionTLS12: "TLSv1.2",
 | 
						|
	tls.VersionTLS13: "TLSv1.3",
 | 
						|
}
 | 
						|
 | 
						|
var headerNameReplacer = strings.NewReplacer(" ", "_", "-", "_")
 | 
						|
 | 
						|
// Interface guards
 | 
						|
var (
 | 
						|
	_ caddy.Provisioner = (*Transport)(nil)
 | 
						|
	_ http.RoundTripper = (*Transport)(nil)
 | 
						|
)
 |