mirror of
https://github.com/dunglas/frankenphp
synced 2024-11-22 16:19:32 +00:00
287 lines
8.4 KiB
Go
287 lines
8.4 KiB
Go
package frankenphp
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"net"
|
|
"net/http"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// populateEnv returns a set of CGI environment variables for the request.
|
|
//
|
|
// TODO: handle this case https://github.com/caddyserver/caddy/issues/3718
|
|
// Inspired by https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
|
|
func populateEnv(request *http.Request) error {
|
|
fc, ok := FromContext(request.Context())
|
|
if !ok {
|
|
panic("not a FrankenPHP request")
|
|
}
|
|
|
|
if fc.populated {
|
|
return nil
|
|
}
|
|
|
|
_, addrOk := fc.Env["REMOTE_ADDR"]
|
|
_, portOk := fc.Env["REMOTE_PORT"]
|
|
if !addrOk || !portOk {
|
|
// Separate remote IP and port; more lenient than net.SplitHostPort
|
|
var ip, port string
|
|
if idx := strings.LastIndex(request.RemoteAddr, ":"); idx > -1 {
|
|
ip = request.RemoteAddr[:idx]
|
|
port = request.RemoteAddr[idx+1:]
|
|
} else {
|
|
ip = request.RemoteAddr
|
|
}
|
|
|
|
// Remove [] from IPv6 addresses
|
|
ip = strings.Replace(ip, "[", "", 1)
|
|
ip = strings.Replace(ip, "]", "", 1)
|
|
|
|
if _, ok := fc.Env["REMOTE_ADDR"]; !ok {
|
|
fc.Env["REMOTE_ADDR"] = ip
|
|
}
|
|
if _, ok := fc.Env["REMOTE_HOST"]; !ok {
|
|
fc.Env["REMOTE_HOST"] = ip // For speed, remote host lookups disabled
|
|
}
|
|
if _, ok := fc.Env["REMOTE_PORT"]; !ok {
|
|
fc.Env["REMOTE_PORT"] = port
|
|
}
|
|
}
|
|
|
|
if _, ok := fc.Env["DOCUMENT_ROOT"]; !ok {
|
|
// make sure file root is absolute
|
|
root, err := filepath.Abs(fc.DocumentRoot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if fc.ResolveRootSymlink {
|
|
if root, err = filepath.EvalSymlinks(root); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
fc.Env["DOCUMENT_ROOT"] = root
|
|
}
|
|
|
|
fpath := request.URL.Path
|
|
scriptName := fpath
|
|
|
|
docURI := fpath
|
|
// split "actual path" from "path info" if configured
|
|
if splitPos := splitPos(fc, fpath); splitPos > -1 {
|
|
docURI = fpath[:splitPos]
|
|
fc.Env["PATH_INFO"] = fpath[splitPos:]
|
|
|
|
// Strip PATH_INFO from SCRIPT_NAME
|
|
scriptName = strings.TrimSuffix(scriptName, fc.Env["PATH_INFO"])
|
|
}
|
|
|
|
// SCRIPT_FILENAME is the absolute path of SCRIPT_NAME
|
|
scriptFilename := sanitizedPathJoin(fc.Env["DOCUMENT_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
|
|
}
|
|
|
|
if _, ok := fc.Env["PHP_SELF"]; !ok {
|
|
fc.Env["PHP_SELF"] = fpath
|
|
}
|
|
if _, ok := fc.Env["DOCUMENT_URI"]; !ok {
|
|
fc.Env["DOCUMENT_URI"] = docURI
|
|
}
|
|
if _, ok := fc.Env["SCRIPT_FILENAME"]; !ok {
|
|
fc.Env["SCRIPT_FILENAME"] = scriptFilename
|
|
}
|
|
if _, ok := fc.Env["SCRIPT_NAME"]; !ok {
|
|
fc.Env["SCRIPT_NAME"] = scriptName
|
|
}
|
|
|
|
if _, ok := fc.Env["REQUEST_SCHEME"]; !ok {
|
|
if request.TLS == nil {
|
|
fc.Env["REQUEST_SCHEME"] = "http"
|
|
} else {
|
|
fc.Env["REQUEST_SCHEME"] = "https"
|
|
}
|
|
}
|
|
|
|
if request.TLS != nil {
|
|
if _, ok := fc.Env["HTTPS"]; !ok {
|
|
fc.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_).
|
|
_, sslProtocolOk := fc.Env["SSL_PROTOCOL"]
|
|
v, versionOk := tlsProtocolStrings[request.TLS.Version]
|
|
if !sslProtocolOk && versionOk {
|
|
fc.Env["SSL_PROTOCOL"] = v
|
|
}
|
|
}
|
|
|
|
_, serverNameOk := fc.Env["SERVER_NAME"]
|
|
_, serverPortOk := fc.Env["SERVER_PORT"]
|
|
if !serverNameOk || !serverPortOk {
|
|
reqHost, reqPort, err := net.SplitHostPort(request.Host)
|
|
if err == nil {
|
|
if !serverNameOk {
|
|
fc.Env["SERVER_NAME"] = reqHost
|
|
}
|
|
|
|
// compliance with the CGI specification requires that
|
|
// SERVER_PORT should only exist if it's a valid numeric value.
|
|
// Info: https://www.ietf.org/rfc/rfc3875 Page 18
|
|
if !serverPortOk {
|
|
// 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 != "" {
|
|
fc.Env["SERVER_PORT"] = reqPort
|
|
} else if fc.Env["REQUEST_SCHEME"] == "http" {
|
|
fc.Env["SERVER_PORT"] = "80"
|
|
} else if fc.Env["REQUEST_SCHEME"] == "https" {
|
|
fc.Env["SERVER_PORT"] = "443"
|
|
}
|
|
}
|
|
} else if !serverNameOk {
|
|
// whatever, just assume there was no port
|
|
fc.Env["SERVER_NAME"] = request.Host
|
|
}
|
|
}
|
|
|
|
// Variables defined in CGI 1.1 spec
|
|
// Some variables are unused but cleared explicitly to prevent
|
|
// the parent environment from interfering.
|
|
// We never override an entry previously set
|
|
if _, ok := fc.Env["REMOTE_IDENT"]; !ok {
|
|
fc.Env["REMOTE_IDENT"] = "" // Not used
|
|
}
|
|
if _, ok := fc.Env["AUTH_TYPE"]; !ok {
|
|
fc.Env["AUTH_TYPE"] = "" // Not used
|
|
}
|
|
if _, ok := fc.Env["CONTENT_LENGTH"]; !ok {
|
|
fc.Env["CONTENT_LENGTH"] = request.Header.Get("Content-Length")
|
|
}
|
|
if _, ok := fc.Env["CONTENT_TYPE"]; !ok {
|
|
fc.Env["CONTENT_TYPE"] = request.Header.Get("Content-Type")
|
|
}
|
|
if _, ok := fc.Env["GATEWAY_INTERFACE"]; !ok {
|
|
fc.Env["GATEWAY_INTERFACE"] = "CGI/1.1"
|
|
}
|
|
if _, ok := fc.Env["QUERY_STRING"]; !ok {
|
|
fc.Env["QUERY_STRING"] = request.URL.RawQuery
|
|
}
|
|
if _, ok := fc.Env["QUERY_STRING"]; !ok {
|
|
fc.Env["QUERY_STRING"] = request.URL.RawQuery
|
|
}
|
|
if _, ok := fc.Env["REQUEST_METHOD"]; !ok {
|
|
fc.Env["REQUEST_METHOD"] = request.Method
|
|
}
|
|
if _, ok := fc.Env["SERVER_PROTOCOL"]; !ok {
|
|
fc.Env["SERVER_PROTOCOL"] = request.Proto
|
|
}
|
|
if _, ok := fc.Env["SERVER_SOFTWARE"]; !ok {
|
|
fc.Env["SERVER_SOFTWARE"] = "FrankenPHP"
|
|
}
|
|
if _, ok := fc.Env["HTTP_HOST"]; !ok {
|
|
fc.Env["HTTP_HOST"] = request.Host // added here, since not always part of headers
|
|
}
|
|
if _, ok := fc.Env["REQUEST_URI"]; !ok {
|
|
fc.Env["REQUEST_URI"] = request.URL.RequestURI()
|
|
}
|
|
|
|
// 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 fc.Env["PATH_INFO"] != "" {
|
|
fc.Env["PATH_TRANSLATED"] = sanitizedPathJoin(fc.Env["DOCUMENT_ROOT"], fc.Env["PATH_INFO"]) // Info: http://www.oreilly.com/openbook/cgi/ch02_04.html
|
|
}
|
|
|
|
// Add all HTTP headers to env variables
|
|
for field, val := range request.Header {
|
|
k := "HTTP_" + headerNameReplacer.Replace(strings.ToUpper(field))
|
|
if _, ok := fc.Env[k]; !ok {
|
|
fc.Env[k] = strings.Join(val, ", ")
|
|
}
|
|
}
|
|
|
|
if _, ok := fc.Env["REMOTE_USER"]; !ok {
|
|
var (
|
|
authUser string
|
|
ok bool
|
|
)
|
|
authUser, fc.authPassword, ok = request.BasicAuth()
|
|
if ok {
|
|
fc.Env["REMOTE_USER"] = authUser
|
|
}
|
|
}
|
|
|
|
fc.populated = true
|
|
|
|
return nil
|
|
}
|
|
|
|
// splitPos returns the index where path should
|
|
// be split based on SplitPath.
|
|
//
|
|
// Adapted from https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
|
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
func splitPos(fc *FrankenPHPContext, path string) int {
|
|
if len(fc.SplitPath) == 0 {
|
|
return 0
|
|
}
|
|
|
|
lowerPath := strings.ToLower(path)
|
|
for _, split := range fc.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(" ", "_", "-", "_")
|
|
|
|
// SanitizedPathJoin performs filepath.Join(root, reqPath) that
|
|
// is safe against directory traversal attacks. It uses logic
|
|
// similar to that in the Go standard library, specifically
|
|
// in the implementation of http.Dir. The root is assumed to
|
|
// be a trusted path, but reqPath is not; and the output will
|
|
// never be outside of root. The resulting path can be used
|
|
// with the local file system.
|
|
//
|
|
// Adapted from https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
|
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
func sanitizedPathJoin(root, reqPath string) string {
|
|
if root == "" {
|
|
root = "."
|
|
}
|
|
|
|
path := filepath.Join(root, filepath.Clean("/"+reqPath))
|
|
|
|
// filepath.Join also cleans the path, and cleaning strips
|
|
// the trailing slash, so we need to re-add it afterwards.
|
|
// if the length is 1, then it's a path to the root,
|
|
// and that should return ".", so we don't append the separator.
|
|
if strings.HasSuffix(reqPath, "/") && len(reqPath) > 1 {
|
|
path += separator
|
|
}
|
|
|
|
return path
|
|
}
|
|
|
|
const separator = string(filepath.Separator)
|