Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 11, 02:15 UTC
| 1 | // Copyright 2015 Matthew Holt and The Caddy Authors |
| 2 | // |
| 3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | // you may not use this file except in compliance with the License. |
| 5 | // You may obtain a copy of the License at |
| 6 | // |
| 7 | // http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | // |
| 9 | // Unless required by applicable law or agreed to in writing, software |
| 10 | // distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | // See the License for the specific language governing permissions and |
| 13 | // limitations under the License. |
| 14 | |
| 15 | package fastcgi |
| 16 | |
| 17 | import ( |
| 18 | "crypto/tls" |
| 19 | "errors" |
| 20 | "fmt" |
| 21 | "net" |
| 22 | "net/http" |
| 23 | "net/url" |
| 24 | "path/filepath" |
| 25 | "runtime" |
| 26 | "strconv" |
| 27 | "strings" |
| 28 | "time" |
| 29 | "unicode/utf8" |
| 30 | |
| 31 | "go.uber.org/zap" |
| 32 | "go.uber.org/zap/zapcore" |
| 33 | |
| 34 | "github.com/caddyserver/caddy/v2" |
| 35 | "github.com/caddyserver/caddy/v2/modules/caddyhttp" |
| 36 | "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy" |
| 37 | "github.com/caddyserver/caddy/v2/modules/caddytls" |
| 38 | ) |
| 39 | |
| 40 | var ( |
| 41 | ErrInvalidSplitPath = errors.New("split path contains non-ASCII characters") |
| 42 | |
| 43 | noopLogger = zap.NewNop() |
| 44 | ) |
| 45 | |
| 46 | func init() { |
| 47 | caddy.RegisterModule(Transport{}) |
| 48 | } |
| 49 | |
| 50 | // Transport facilitates FastCGI communication. |
| 51 | type Transport struct { |
| 52 | // Use this directory as the fastcgi root directory. Defaults to the root |
| 53 | // directory of the parent virtual host. |
| 54 | Root string `json:"root,omitempty"` |
| 55 | |
| 56 | // The path in the URL will be split into two, with the first piece ending |
| 57 | // with the value of SplitPath. The first piece will be assumed as the |
| 58 | // actual resource (CGI script) name, and the second piece will be set to |
| 59 | // PATH_INFO for the CGI script to use. |
| 60 | // |
| 61 | // Split paths can only contain ASCII characters. |
| 62 | // Comparison is case-insensitive. |
| 63 | // |
| 64 | // Future enhancements should be careful to avoid CVE-2019-11043, |
| 65 | // which can be mitigated with use of a try_files-like behavior |
| 66 | // that 404s if the fastcgi path info is not found. |
| 67 | SplitPath []string `json:"split_path,omitempty"` |
| 68 | |
| 69 | // Path declared as root directory will be resolved to its absolute value |
| 70 | // after the evaluation of any symbolic links. |
| 71 | // Due to the nature of PHP opcache, root directory path is cached: when |
| 72 | // using a symlinked directory as root this could generate errors when |
| 73 | // symlink is changed without php-fpm being restarted; enabling this |
| 74 | // directive will set $_SERVER['DOCUMENT_ROOT'] to the real directory path. |
| 75 | ResolveRootSymlink bool `json:"resolve_root_symlink,omitempty"` |
| 76 | |
| 77 | // Extra environment variables. |
| 78 | EnvVars map[string]string `json:"env,omitempty"` |
| 79 | |
| 80 | // The duration used to set a deadline when connecting to an upstream. Default: `3s`. |
| 81 | DialTimeout caddy.Duration `json:"dial_timeout,omitempty"` |
| 82 | |
| 83 | // The duration used to set a deadline when reading from the FastCGI server. |
| 84 | ReadTimeout caddy.Duration `json:"read_timeout,omitempty"` |
| 85 | |
| 86 | // The duration used to set a deadline when sending to the FastCGI server. |
| 87 | WriteTimeout caddy.Duration `json:"write_timeout,omitempty"` |
| 88 | |
| 89 | // Capture and log any messages sent by the upstream on stderr. Logs at WARN |
| 90 | // level by default. If the response has a 4xx or 5xx status ERROR level will |
| 91 | // be used instead. |
| 92 | CaptureStderr bool `json:"capture_stderr,omitempty"` |
| 93 | |
| 94 | serverSoftware string |
| 95 | logger *zap.Logger |
| 96 | } |
| 97 | |
| 98 | // CaddyModule returns the Caddy module information. |
| 99 | func (Transport) CaddyModule() caddy.ModuleInfo { |
| 100 | return caddy.ModuleInfo{ |
| 101 | ID: "http.reverse_proxy.transport.fastcgi", |
| 102 | New: func() caddy.Module { return new(Transport) }, |
| 103 | } |
| 104 | } |
| 105 | |
| 106 | // Provision sets up t. |
| 107 | func (t *Transport) Provision(ctx caddy.Context) error { |
| 108 | t.logger = ctx.Logger() |
| 109 | |
| 110 | if t.Root == "" { |
| 111 | t.Root = "{http.vars.root}" |
| 112 | } |
| 113 | |
| 114 | version, _ := caddy.Version() |
| 115 | t.serverSoftware = "Caddy/" + version |
| 116 | |
| 117 | // Set a relatively short default dial timeout. |
| 118 | // This is helpful to make load-balancer retries more speedy. |
| 119 | if t.DialTimeout == 0 { |
| 120 | t.DialTimeout = caddy.Duration(3 * time.Second) |
| 121 | } |
| 122 | |
| 123 | var b strings.Builder |
| 124 | |
| 125 | for i, split := range t.SplitPath { |
| 126 | b.Grow(len(split)) |
| 127 | |
| 128 | for j := 0; j < len(split); j++ { |
| 129 | c := split[j] |
| 130 | if c >= utf8.RuneSelf { |
| 131 | return ErrInvalidSplitPath |
| 132 | } |
| 133 | |
| 134 | if 'A' <= c && c <= 'Z' { |
| 135 | b.WriteByte(c + 'a' - 'A') |
| 136 | } else { |
| 137 | b.WriteByte(c) |
| 138 | } |
| 139 | } |
| 140 | |
| 141 | t.SplitPath[i] = b.String() |
| 142 | b.Reset() |
| 143 | } |
| 144 | |
| 145 | return nil |
| 146 | } |
| 147 | |
| 148 | // DefaultBufferSizes enables request buffering for fastcgi if not configured. |
| 149 | // This is because most fastcgi servers are php-fpm that require the content length to be set to read the body, golang |
| 150 | // std has fastcgi implementation that doesn't need this value to process the body, but we can safely assume that's |
| 151 | // not used. |
| 152 | // http3 requests have a negative content length for GET and HEAD requests, if that header is not sent. |
| 153 | // see: https://github.com/caddyserver/caddy/issues/6678#issuecomment-2472224182 |
| 154 | // Though it appears even if CONTENT_LENGTH is invalid, php-fpm can handle just fine if the body is empty (no Stdin records sent). |
| 155 | // php-fpm will hang if there is any data in the body though, https://github.com/caddyserver/caddy/issues/5420#issuecomment-2415943516 |
| 156 | |
| 157 | // TODO: better default buffering for fastcgi requests without content length, in theory a value of 1 should be enough, make it bigger anyway |
| 158 | func (t Transport) DefaultBufferSizes() (int64, int64) { |
| 159 | return 4096, 0 |
| 160 | } |
| 161 | |
| 162 | // RequiresContentLength reports that fastcgi cannot forward a body of unknown |
| 163 | // length: without a valid CONTENT_LENGTH the client rejects the request rather |
| 164 | // than let the upstream hang, so the request buffer above is the only way to |
| 165 | // learn the length of a body that does not announce one. |
| 166 | func (t Transport) RequiresContentLength() bool { |
| 167 | return true |
| 168 | } |
| 169 | |
| 170 | // RoundTrip implements http.RoundTripper. |
| 171 | func (t Transport) RoundTrip(r *http.Request) (*http.Response, error) { |
| 172 | server := r.Context().Value(caddyhttp.ServerCtxKey).(*caddyhttp.Server) |
| 173 | |
| 174 | // Disallow null bytes in the request path, because |
| 175 | // PHP upstreams may do bad things, like execute a |
| 176 | // non-PHP file as PHP code. See #4574 |
| 177 | if strings.Contains(r.URL.Path, "\x00") { |
| 178 | return nil, caddyhttp.Error(http.StatusBadRequest, fmt.Errorf("invalid request path")) |
| 179 | } |
| 180 | |
| 181 | env, err := t.buildEnv(r) |
| 182 | if err != nil { |
| 183 | return nil, fmt.Errorf("building environment: %w", err) |
| 184 | } |
| 185 | |
| 186 | ctx := r.Context() |
| 187 | |
| 188 | // extract dial information from request (should have been embedded by the reverse proxy) |
| 189 | network, address := "tcp", r.URL.Host |
| 190 | if dialInfo, ok := reverseproxy.GetDialInfo(ctx); ok { |
| 191 | network = dialInfo.Network |
| 192 | address = dialInfo.Address |
| 193 | } |
| 194 | |
| 195 | logCreds := server.Logs != nil && server.Logs.ShouldLogCredentials |
| 196 | loggableReq := caddyhttp.LoggableHTTPRequest{ |
| 197 | Request: r, |
| 198 | ShouldLogCredentials: logCreds, |
| 199 | } |
| 200 | loggableEnv := loggableEnv{vars: env, logCredentials: logCreds} |
| 201 | |
| 202 | logger := t.logger.With( |
| 203 | zap.Object("request", loggableReq), |
| 204 | zap.Object("env", loggableEnv), |
| 205 | ) |
| 206 | if c := t.logger.Check(zapcore.DebugLevel, "roundtrip"); c != nil { |
| 207 | c.Write( |
| 208 | zap.String("dial", address), |
| 209 | zap.Object("env", loggableEnv), |
| 210 | zap.Object("request", loggableReq), |
| 211 | ) |
| 212 | } |
| 213 | |
| 214 | // connect to the backend |
| 215 | dialer := net.Dialer{Timeout: time.Duration(t.DialTimeout)} |
| 216 | conn, err := dialer.DialContext(ctx, network, address) |
| 217 | if err != nil { |
| 218 | return nil, reverseproxy.NewDialError(fmt.Errorf("dialing backend: %w", err)) |
| 219 | } |
| 220 | defer func() { |
| 221 | // conn will be closed with the response body unless there's an error |
| 222 | if err != nil { |
| 223 | conn.Close() |
| 224 | } |
| 225 | }() |
| 226 | |
| 227 | // create the client that will facilitate the protocol |
| 228 | client := client{ |
| 229 | rwc: conn, |
| 230 | reqID: 1, |
| 231 | logger: logger, |
| 232 | stderr: t.CaptureStderr, |
| 233 | } |
| 234 | |
| 235 | // read/write timeouts |
| 236 | if err = client.SetReadTimeout(time.Duration(t.ReadTimeout)); err != nil { |
| 237 | return nil, fmt.Errorf("setting read timeout: %v", err) |
| 238 | } |
| 239 | if err = client.SetWriteTimeout(time.Duration(t.WriteTimeout)); err != nil { |
| 240 | return nil, fmt.Errorf("setting write timeout: %v", err) |
| 241 | } |
| 242 | |
| 243 | contentLength := r.ContentLength |
| 244 | if contentLength == 0 { |
| 245 | contentLength, _ = strconv.ParseInt(r.Header.Get("Content-Length"), 10, 64) |
| 246 | } |
| 247 | |
| 248 | var resp *http.Response |
| 249 | switch r.Method { |
| 250 | case http.MethodHead: |
| 251 | resp, err = client.Head(env) |
| 252 | case http.MethodGet: |
| 253 | resp, err = client.Get(env, r.Body, contentLength) |
| 254 | case http.MethodOptions: |
| 255 | resp, err = client.Options(env) |
| 256 | default: |
| 257 | resp, err = client.Post(env, r.Method, r.Header.Get("Content-Type"), r.Body, contentLength) |
| 258 | } |
| 259 | if err != nil { |
| 260 | return nil, err |
| 261 | } |
| 262 | |
| 263 | return resp, nil |
| 264 | } |
| 265 | |
| 266 | // buildEnv returns a set of CGI environment variables for the request. |
| 267 | func (t Transport) buildEnv(r *http.Request) (envVars, error) { |
| 268 | repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) |
| 269 | |
| 270 | var env envVars |
| 271 | |
| 272 | // Separate remote IP and port; more lenient than net.SplitHostPort |
| 273 | var ip, port string |
| 274 | if idx := strings.LastIndex(r.RemoteAddr, ":"); idx > -1 { |
| 275 | ip = r.RemoteAddr[:idx] |
| 276 | port = r.RemoteAddr[idx+1:] |
| 277 | } else { |
| 278 | ip = r.RemoteAddr |
| 279 | } |
| 280 | |
| 281 | // Remove [] from IPv6 addresses |
| 282 | ip = strings.Replace(ip, "[", "", 1) |
| 283 | ip = strings.Replace(ip, "]", "", 1) |
| 284 | |
| 285 | // make sure file root is absolute |
| 286 | root, err := caddy.FastAbs(repl.ReplaceAll(t.Root, ".")) |
| 287 | if err != nil { |
| 288 | return nil, err |
| 289 | } |
| 290 | |
| 291 | if t.ResolveRootSymlink { |
| 292 | root, err = filepath.EvalSymlinks(root) |
| 293 | if err != nil { |
| 294 | return nil, err |
| 295 | } |
| 296 | } |
| 297 | |
| 298 | // Derive the script from the same path model the matchers judged: Windows |
| 299 | // separators and per-component trailing dots/spaces normalized, then cleaned |
| 300 | // (mirroring MatchPath and MatchURLPattern). Caddy does not rewrite |
| 301 | // r.URL.Path itself, and the rewrite route php_fastcgi relies on to normalize |
| 302 | // it can fail to match, so the transport must establish this invariant rather |
| 303 | // than inherit it. Without this, /admin.php/../../other.php (or, on Windows, |
| 304 | // /admin.php\..\other.php) is judged by every path matcher as /other.php |
| 305 | // while SCRIPT_FILENAME resolves to /admin.php. |
| 306 | fpath := caddyhttp.NormalizedPath(r.URL.Path, true) |
| 307 | scriptName := fpath |
| 308 | |
| 309 | docURI := fpath |
| 310 | // split "actual path" from "path info" if configured |
| 311 | var pathInfo string |
| 312 | if splitPos := t.splitPos(fpath); splitPos > -1 { |
| 313 | docURI = fpath[:splitPos] |
| 314 | pathInfo = fpath[splitPos:] |
| 315 | |
| 316 | // Strip PATH_INFO from SCRIPT_NAME |
| 317 | scriptName = strings.TrimSuffix(scriptName, pathInfo) |
| 318 | } |
| 319 | |
| 320 | // On Windows, an 8.3 alias or an alternate data stream can name a non-PHP file |
| 321 | // with a script-looking path, and path matchers cannot see through either. |
| 322 | if runtime.GOOS == "windows" && (strings.Contains(scriptName, ":") || caddyhttp.HasWindowsShortName(scriptName)) { |
| 323 | return nil, caddyhttp.Error(http.StatusBadRequest, errors.New("illegal script path")) |
| 324 | } |
| 325 | |
| 326 | // Path matchers whose pattern contains "%" clean the escaped path, where "%2f" |
| 327 | // and "%2e%2e" are neither separators nor dot segments, so both forms must |
| 328 | // select the same script. On Windows, they read a backslash, escaped even |
| 329 | // without RawPath, as "%2f". |
| 330 | backslash := runtime.GOOS == "windows" && strings.Contains(r.URL.Path, `\`) |
| 331 | if r.URL.RawPath != "" || backslash { |
| 332 | escaped, err := url.PathUnescape(caddyhttp.NormalizedPath(r.URL.EscapedPath(), true)) |
| 333 | if backslash { |
| 334 | escaped = strings.ReplaceAll(escaped, `\`, "/") |
| 335 | } |
| 336 | if err != nil || t.scriptPart(escaped) != docURI { |
| 337 | return nil, caddyhttp.Error(http.StatusBadRequest, errors.New("escaped and decoded paths select different scripts")) |
| 338 | } |
| 339 | } |
| 340 | |
| 341 | // Try to grab the path remainder from a file matcher |
| 342 | // if we didn't get a split result here. |
| 343 | // See https://github.com/caddyserver/caddy/issues/3718 |
| 344 | if pathInfo == "" { |
| 345 | pathInfo, _ = repl.GetString("http.matchers.file.remainder") |
| 346 | } |
| 347 | |
| 348 | // SCRIPT_FILENAME is the absolute path of SCRIPT_NAME |
| 349 | scriptFilename := caddyhttp.SanitizedPathJoin(root, scriptName) |
| 350 | |
| 351 | // Ensure the SCRIPT_NAME has a leading slash for compliance with RFC3875 |
| 352 | // Info: https://tools.ietf.org/html/rfc3875#section-4.1.13 |
| 353 | if scriptName != "" && !strings.HasPrefix(scriptName, "/") { |
| 354 | scriptName = "/" + scriptName |
| 355 | } |
| 356 | |
| 357 | // Get the request URL from context. The context stores the original URL in case |
| 358 | // it was changed by a middleware such as rewrite. By default, we pass the |
| 359 | // original URI in as the value of REQUEST_URI (the user can overwrite this |
| 360 | // if desired). Most PHP apps seem to want the original URI. Besides, this is |
| 361 | // how nginx defaults: http://stackoverflow.com/a/12485156/1048862 |
| 362 | origReq := r.Context().Value(caddyhttp.OriginalRequestCtxKey).(http.Request) |
| 363 | |
| 364 | requestScheme := "http" |
| 365 | if r.TLS != nil { |
| 366 | requestScheme = "https" |
| 367 | } |
| 368 | |
| 369 | reqHost, reqPort, err := net.SplitHostPort(r.Host) |
| 370 | if err != nil { |
| 371 | // whatever, just assume there was no port |
| 372 | reqHost = r.Host |
| 373 | } |
| 374 | |
| 375 | authUser, _ := repl.GetString("http.auth.user.id") |
| 376 | |
| 377 | // Some variables are unused but cleared explicitly to prevent |
| 378 | // the parent environment from interfering. |
| 379 | env = envVars{ |
| 380 | // Variables defined in CGI 1.1 spec |
| 381 | "AUTH_TYPE": "", // Not used |
| 382 | "CONTENT_LENGTH": r.Header.Get("Content-Length"), |
| 383 | "CONTENT_TYPE": r.Header.Get("Content-Type"), |
| 384 | "GATEWAY_INTERFACE": "CGI/1.1", |
| 385 | "PATH_INFO": pathInfo, |
| 386 | "QUERY_STRING": r.URL.RawQuery, |
| 387 | "REMOTE_ADDR": ip, |
| 388 | "REMOTE_HOST": ip, // For speed, remote host lookups disabled |
| 389 | "REMOTE_PORT": port, |
| 390 | "REMOTE_IDENT": "", // Not used |
| 391 | "REMOTE_USER": authUser, |
| 392 | "REQUEST_METHOD": r.Method, |
| 393 | "REQUEST_SCHEME": requestScheme, |
| 394 | "SERVER_NAME": reqHost, |
| 395 | "SERVER_PROTOCOL": r.Proto, |
| 396 | "SERVER_SOFTWARE": t.serverSoftware, |
| 397 | |
| 398 | // Other variables |
| 399 | "DOCUMENT_ROOT": root, |
| 400 | "DOCUMENT_URI": docURI, |
| 401 | "HTTP_HOST": r.Host, // added here, since not always part of headers |
| 402 | "REQUEST_URI": origReq.URL.RequestURI(), |
| 403 | "SCRIPT_FILENAME": scriptFilename, |
| 404 | "SCRIPT_NAME": scriptName, |
| 405 | } |
| 406 | |
| 407 | if localAddr, ok := r.Context().Value(http.LocalAddrContextKey).(net.Addr); ok { |
| 408 | var ipStr string |
| 409 | if host, _, err := net.SplitHostPort(localAddr.String()); err == nil { |
| 410 | ipStr = host |
| 411 | } else { |
| 412 | ipStr = localAddr.String() |
| 413 | } |
| 414 | if ip := net.ParseIP(ipStr); ip != nil { |
| 415 | env["SERVER_ADDR"] = ipStr |
| 416 | } |
| 417 | } |
| 418 | |
| 419 | // compliance with the CGI specification requires that |
| 420 | // PATH_TRANSLATED should only exist if PATH_INFO is defined. |
| 421 | // Info: https://www.ietf.org/rfc/rfc3875 Page 14 |
| 422 | if env["PATH_INFO"] != "" { |
| 423 | env["PATH_TRANSLATED"] = caddyhttp.SanitizedPathJoin(root, pathInfo) // Info: http://www.oreilly.com/openbook/cgi/ch02_04.html |
| 424 | } |
| 425 | |
| 426 | // compliance with the CGI specification requires that |
| 427 | // the SERVER_PORT variable MUST be set to the TCP/IP port number on which this request is received from the client |
| 428 | // even if the port is the default port for the scheme and could otherwise be omitted from a URI. |
| 429 | // https://tools.ietf.org/html/rfc3875#section-4.1.15 |
| 430 | if reqPort != "" { |
| 431 | env["SERVER_PORT"] = reqPort |
| 432 | } else if requestScheme == "http" { |
| 433 | env["SERVER_PORT"] = "80" |
| 434 | } else if requestScheme == "https" { |
| 435 | env["SERVER_PORT"] = "443" |
| 436 | } |
| 437 | |
| 438 | // Some web apps rely on knowing HTTPS or not |
| 439 | if r.TLS != nil { |
| 440 | env["HTTPS"] = "on" |
| 441 | // and pass the protocol details in a manner compatible with apache's mod_ssl |
| 442 | // (which is why these have a SSL_ prefix and not TLS_). |
| 443 | v, ok := tlsProtocolStrings[r.TLS.Version] |
| 444 | if ok { |
| 445 | env["SSL_PROTOCOL"] = v |
| 446 | } |
| 447 | // and pass the cipher suite in a manner compatible with apache's mod_ssl |
| 448 | for _, cs := range caddytls.SupportedCipherSuites() { |
| 449 | if cs.ID == r.TLS.CipherSuite { |
| 450 | env["SSL_CIPHER"] = cs.Name |
| 451 | break |
| 452 | } |
| 453 | } |
| 454 | } |
| 455 | |
| 456 | // Add all HTTP headers to env variables |
| 457 | for field, val := range r.Header { |
| 458 | switch http.CanonicalHeaderKey(field) { |
| 459 | // HTTP_HOST comes from r.Host, the host the request was routed on: |
| 460 | // over HTTP/2, net/http keeps a "host" field distinct from :authority. |
| 461 | case "Host": |
| 462 | continue |
| 463 | // HTTPoxy mitigation: client-supplied Proxy header must not |
| 464 | // be trusted for setting the HTTP_PROXY environment variable. |
| 465 | // See https://httpoxy.org for details. |
| 466 | case "Proxy": |
| 467 | continue |
| 468 | } |
| 469 | |
| 470 | header := strings.ToUpper(field) |
| 471 | header = headerNameReplacer.Replace(header) |
| 472 | env["HTTP_"+header] = strings.Join(val, ", ") |
| 473 | } |
| 474 | |
| 475 | // Add env variables from config (with support for placeholders in values) |
| 476 | // last, so a client header cannot override an HTTP_* variable set by the operator |
| 477 | for key, value := range t.EnvVars { |
| 478 | env[key] = repl.ReplaceAll(value, "") |
| 479 | } |
| 480 | |
| 481 | return env, nil |
| 482 | } |
| 483 | |
| 484 | // splitPos returns the index where path should |
| 485 | // be split based on t.SplitPath. |
| 486 | // |
| 487 | // example: if splitPath is [".php"] |
| 488 | // "/path/to/script.php/some/path": ("/path/to/script.php", "/some/path") |
| 489 | // |
| 490 | // A match must end the path or be followed by "/", as in the file matcher, so |
| 491 | // "/admin.php.php" or "/admin.phpx/y.php" can't run "/admin.php" past route |
| 492 | // matchers that judged another file. |
| 493 | // |
| 494 | // Matching is strictly ASCII case-insensitive. Bytes >= utf8.RuneSelf in path |
| 495 | // never match any split entry: split strings are validated ASCII-only and |
| 496 | // lower-cased in Provision(), so any Unicode equivalence (e.g. fullwidth or |
| 497 | // mathematical letters folding to ASCII) would let an attacker upload a file |
| 498 | // whose name contains such code points and have it served as PHP. See |
| 499 | // FrankenPHP advisories GHSA-3g8v-8r37-cgjm and GHSA-v4h7-cj44-8fc8. |
| 500 | // |
| 501 | // Adapted from FrankenPHP's code (copyright 2026 Kévin Dunglas, MIT license) |
| 502 | func (t Transport) splitPos(path string) int { |
| 503 | // TODO: from v1... |
| 504 | // if httpserver.CaseSensitivePath { |
| 505 | // return strings.Index(path, r.SplitPath) |
| 506 | // } |
| 507 | if len(t.SplitPath) == 0 { |
| 508 | return 0 |
| 509 | } |
| 510 | |
| 511 | pathLen := len(path) |
| 512 | |
| 513 | for _, split := range t.SplitPath { |
| 514 | splitLen := len(split) |
| 515 | if splitLen == 0 || splitLen > pathLen { |
| 516 | continue |
| 517 | } |
| 518 | |
| 519 | for i := 0; i <= pathLen-splitLen; i++ { |
| 520 | match := true |
| 521 | for j := range splitLen { |
| 522 | c := path[i+j] |
| 523 | if c >= utf8.RuneSelf { |
| 524 | match = false |
| 525 | |
| 526 | break |
| 527 | } |
| 528 | |
| 529 | if 'A' <= c && c <= 'Z' { |
| 530 | c += 'a' - 'A' |
| 531 | } |
| 532 | |
| 533 | if c != split[j] { |
| 534 | match = false |
| 535 | |
| 536 | break |
| 537 | } |
| 538 | } |
| 539 | |
| 540 | if end := i + splitLen; match && (end == pathLen || path[end] == '/') { |
| 541 | return end |
| 542 | } |
| 543 | } |
| 544 | } |
| 545 | |
| 546 | return -1 |
| 547 | } |
| 548 | |
| 549 | // scriptPart returns the part of path naming the script. |
| 550 | func (t Transport) scriptPart(path string) string { |
| 551 | if pos := t.splitPos(path); pos > -1 { |
| 552 | return path[:pos] |
| 553 | } |
| 554 | return path |
| 555 | } |
| 556 | |
| 557 | type envVars map[string]string |
| 558 | |
| 559 | // loggableEnv is a simple type to allow for speeding up zap log encoding. |
| 560 | type loggableEnv struct { |
| 561 | vars envVars |
| 562 | logCredentials bool |
| 563 | } |
| 564 | |
| 565 | func (env loggableEnv) MarshalLogObject(enc zapcore.ObjectEncoder) error { |
| 566 | for k, v := range env.vars { |
| 567 | if !env.logCredentials { |
| 568 | switch strings.ToLower(k) { |
| 569 | case "http_cookie", "http_set_cookie", "http_authorization", "http_proxy_authorization": |
| 570 | v = "" |
| 571 | } |
| 572 | } |
| 573 | enc.AddString(k, v) |
| 574 | } |
| 575 | return nil |
| 576 | } |
| 577 | |
| 578 | // Map of supported protocols to Apache ssl_mod format |
| 579 | // Note that these are slightly different from SupportedProtocols in caddytls/config.go |
| 580 | var tlsProtocolStrings = map[uint16]string{ |
| 581 | tls.VersionTLS10: "TLSv1", |
| 582 | tls.VersionTLS11: "TLSv1.1", |
| 583 | tls.VersionTLS12: "TLSv1.2", |
| 584 | tls.VersionTLS13: "TLSv1.3", |
| 585 | } |
| 586 | |
| 587 | var headerNameReplacer = strings.NewReplacer("-", "_") |
| 588 | |
| 589 | // Interface guards |
| 590 | var ( |
| 591 | _ zapcore.ObjectMarshaler = (*loggableEnv)(nil) |
| 592 | |
| 593 | _ caddy.Provisioner = (*Transport)(nil) |
| 594 | _ http.RoundTripper = (*Transport)(nil) |
| 595 | _ reverseproxy.BufferedTransport = (*Transport)(nil) |
| 596 | _ reverseproxy.ContentLengthRequiredTransport = (*Transport)(nil) |
| 597 | ) |