mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-16 21:56:40 -05:00
f4bf4e0097
* perf: use zap's Check() to prevent useless allocs * fix * fix * fix * fix * restore previous replacer behavior * fix linter
432 lines
13 KiB
Go
432 lines
13 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 (
|
|
"crypto/tls"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
"go.uber.org/zap/zapcore"
|
|
|
|
"github.com/caddyserver/caddy/v2"
|
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
|
|
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
|
)
|
|
|
|
var noopLogger = zap.NewNop()
|
|
|
|
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"`
|
|
|
|
// Path declared as root directory will be resolved to its absolute value
|
|
// after the evaluation of any symbolic links.
|
|
// Due to the nature of PHP opcache, root directory path is cached: when
|
|
// using a symlinked directory as root this could generate errors when
|
|
// symlink is changed without php-fpm being restarted; enabling this
|
|
// directive will set $_SERVER['DOCUMENT_ROOT'] to the real directory path.
|
|
ResolveRootSymlink bool `json:"resolve_root_symlink,omitempty"`
|
|
|
|
// Extra environment variables.
|
|
EnvVars map[string]string `json:"env,omitempty"`
|
|
|
|
// The duration used to set a deadline when connecting to an upstream. Default: `3s`.
|
|
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"`
|
|
|
|
// Capture and log any messages sent by the upstream on stderr. Logs at WARN
|
|
// level by default. If the response has a 4xx or 5xx status ERROR level will
|
|
// be used instead.
|
|
CaptureStderr bool `json:"capture_stderr,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()
|
|
|
|
if t.Root == "" {
|
|
t.Root = "{http.vars.root}"
|
|
}
|
|
|
|
version, _ := caddy.Version()
|
|
t.serverSoftware = "Caddy/" + version
|
|
|
|
// Set a relatively short default dial timeout.
|
|
// This is helpful to make load-balancer retries more speedy.
|
|
if t.DialTimeout == 0 {
|
|
t.DialTimeout = caddy.Duration(3 * time.Second)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RoundTrip implements http.RoundTripper.
|
|
func (t Transport) RoundTrip(r *http.Request) (*http.Response, error) {
|
|
server := r.Context().Value(caddyhttp.ServerCtxKey).(*caddyhttp.Server)
|
|
|
|
// Disallow null bytes in the request path, because
|
|
// PHP upstreams may do bad things, like execute a
|
|
// non-PHP file as PHP code. See #4574
|
|
if strings.Contains(r.URL.Path, "\x00") {
|
|
return nil, caddyhttp.Error(http.StatusBadRequest, fmt.Errorf("invalid request path"))
|
|
}
|
|
|
|
env, err := t.buildEnv(r)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("building environment: %v", err)
|
|
}
|
|
|
|
ctx := r.Context()
|
|
|
|
// 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
|
|
}
|
|
|
|
logCreds := server.Logs != nil && server.Logs.ShouldLogCredentials
|
|
loggableReq := caddyhttp.LoggableHTTPRequest{
|
|
Request: r,
|
|
ShouldLogCredentials: logCreds,
|
|
}
|
|
loggableEnv := loggableEnv{vars: env, logCredentials: logCreds}
|
|
|
|
logger := t.logger.With(
|
|
zap.Object("request", loggableReq),
|
|
zap.Object("env", loggableEnv),
|
|
)
|
|
if c := t.logger.Check(zapcore.DebugLevel, "roundtrip"); c != nil {
|
|
c.Write(
|
|
zap.String("dial", address),
|
|
zap.Object("env", loggableEnv),
|
|
zap.Object("request", loggableReq),
|
|
)
|
|
}
|
|
|
|
// connect to the backend
|
|
dialer := net.Dialer{Timeout: time.Duration(t.DialTimeout)}
|
|
conn, err := dialer.DialContext(ctx, network, address)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("dialing backend: %v", err)
|
|
}
|
|
defer func() {
|
|
// conn will be closed with the response body unless there's an error
|
|
if err != nil {
|
|
conn.Close()
|
|
}
|
|
}()
|
|
|
|
// create the client that will facilitate the protocol
|
|
client := client{
|
|
rwc: conn,
|
|
reqID: 1,
|
|
logger: logger,
|
|
stderr: t.CaptureStderr,
|
|
}
|
|
|
|
// read/write timeouts
|
|
if err = client.SetReadTimeout(time.Duration(t.ReadTimeout)); err != nil {
|
|
return nil, fmt.Errorf("setting read timeout: %v", err)
|
|
}
|
|
if err = client.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 = client.Head(env)
|
|
case http.MethodGet:
|
|
resp, err = client.Get(env, r.Body, contentLength)
|
|
case http.MethodOptions:
|
|
resp, err = client.Options(env)
|
|
default:
|
|
resp, err = client.Post(env, r.Method, r.Header.Get("Content-Type"), r.Body, contentLength)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
// buildEnv returns a set of CGI environment variables for the request.
|
|
func (t Transport) buildEnv(r *http.Request) (envVars, error) {
|
|
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
|
|
|
|
var env envVars
|
|
|
|
// 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
|
|
}
|
|
|
|
if t.ResolveRootSymlink {
|
|
root, err = filepath.EvalSymlinks(root)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
fpath := r.URL.Path
|
|
scriptName := fpath
|
|
|
|
docURI := fpath
|
|
// split "actual path" from "path info" if configured
|
|
var pathInfo string
|
|
if splitPos := t.splitPos(fpath); splitPos > -1 {
|
|
docURI = fpath[:splitPos]
|
|
pathInfo = fpath[splitPos:]
|
|
|
|
// Strip PATH_INFO from SCRIPT_NAME
|
|
scriptName = strings.TrimSuffix(scriptName, pathInfo)
|
|
}
|
|
|
|
// Try to grab the path remainder from a file matcher
|
|
// if we didn't get a split result here.
|
|
// See https://github.com/caddyserver/caddy/issues/3718
|
|
if pathInfo == "" {
|
|
pathInfo, _ = repl.GetString("http.matchers.file.remainder")
|
|
}
|
|
|
|
// SCRIPT_FILENAME is the absolute path of SCRIPT_NAME
|
|
scriptFilename := caddyhttp.SanitizedPathJoin(root, scriptName)
|
|
|
|
// Ensure the SCRIPT_NAME has a leading slash for compliance with RFC3875
|
|
// Info: https://tools.ietf.org/html/rfc3875#section-4.1.13
|
|
if scriptName != "" && !strings.HasPrefix(scriptName, "/") {
|
|
scriptName = "/" + 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 := r.Context().Value(caddyhttp.OriginalRequestCtxKey).(http.Request)
|
|
|
|
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
|
|
}
|
|
|
|
authUser, _ := repl.GetString("http.auth.user.id")
|
|
|
|
// Some variables are unused but cleared explicitly to prevent
|
|
// the parent environment from interfering.
|
|
env = envVars{
|
|
// 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": authUser,
|
|
"REQUEST_METHOD": r.Method,
|
|
"REQUEST_SCHEME": requestScheme,
|
|
"SERVER_NAME": reqHost,
|
|
"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": origReq.URL.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"] = caddyhttp.SanitizedPathJoin(root, pathInfo) // Info: http://www.oreilly.com/openbook/cgi/ch02_04.html
|
|
}
|
|
|
|
// compliance with the CGI specification requires that
|
|
// the SERVER_PORT variable MUST be set to the TCP/IP port number on which this request is received from the client
|
|
// even if the port is the default port for the scheme and could otherwise be omitted from a URI.
|
|
// https://tools.ietf.org/html/rfc3875#section-4.1.15
|
|
if reqPort != "" {
|
|
env["SERVER_PORT"] = reqPort
|
|
} else if requestScheme == "http" {
|
|
env["SERVER_PORT"] = "80"
|
|
} else if requestScheme == "https" {
|
|
env["SERVER_PORT"] = "443"
|
|
}
|
|
|
|
// 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)
|
|
// }
|
|
if len(t.SplitPath) == 0 {
|
|
return 0
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
type envVars map[string]string
|
|
|
|
// loggableEnv is a simple type to allow for speeding up zap log encoding.
|
|
type loggableEnv struct {
|
|
vars envVars
|
|
logCredentials bool
|
|
}
|
|
|
|
func (env loggableEnv) MarshalLogObject(enc zapcore.ObjectEncoder) error {
|
|
for k, v := range env.vars {
|
|
if !env.logCredentials {
|
|
switch strings.ToLower(k) {
|
|
case "http_cookie", "http_set_cookie", "http_authorization", "http_proxy_authorization":
|
|
v = ""
|
|
}
|
|
}
|
|
enc.AddString(k, v)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 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 (
|
|
_ zapcore.ObjectMarshaler = (*loggableEnv)(nil)
|
|
|
|
_ caddy.Provisioner = (*Transport)(nil)
|
|
_ http.RoundTripper = (*Transport)(nil)
|
|
)
|