cgi.go 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. package frankenphp
  2. import (
  3. "crypto/tls"
  4. "net"
  5. "net/http"
  6. "path/filepath"
  7. "strings"
  8. )
  9. // populateEnv returns a set of CGI environment variables for the request.
  10. //
  11. // TODO: handle this case https://github.com/caddyserver/caddy/issues/3718
  12. // Inspired by https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
  13. func populateEnv(request *http.Request) error {
  14. fc, ok := FromContext(request.Context())
  15. if !ok {
  16. panic("not a FrankenPHP request")
  17. }
  18. if fc.populated {
  19. return nil
  20. }
  21. _, addrOk := fc.Env["REMOTE_ADDR"]
  22. _, portOk := fc.Env["REMOTE_PORT"]
  23. if !addrOk || !portOk {
  24. // Separate remote IP and port; more lenient than net.SplitHostPort
  25. var ip, port string
  26. if idx := strings.LastIndex(request.RemoteAddr, ":"); idx > -1 {
  27. ip = request.RemoteAddr[:idx]
  28. port = request.RemoteAddr[idx+1:]
  29. } else {
  30. ip = request.RemoteAddr
  31. }
  32. // Remove [] from IPv6 addresses
  33. ip = strings.Replace(ip, "[", "", 1)
  34. ip = strings.Replace(ip, "]", "", 1)
  35. if _, ok := fc.Env["REMOTE_ADDR"]; !ok {
  36. fc.Env["REMOTE_ADDR"] = ip
  37. }
  38. if _, ok := fc.Env["REMOTE_HOST"]; !ok {
  39. fc.Env["REMOTE_HOST"] = ip // For speed, remote host lookups disabled
  40. }
  41. if _, ok := fc.Env["REMOTE_PORT"]; !ok {
  42. fc.Env["REMOTE_PORT"] = port
  43. }
  44. }
  45. if _, ok := fc.Env["DOCUMENT_ROOT"]; !ok {
  46. // make sure file root is absolute
  47. root, err := filepath.Abs(fc.DocumentRoot)
  48. if err != nil {
  49. return err
  50. }
  51. if fc.ResolveRootSymlink {
  52. if root, err = filepath.EvalSymlinks(root); err != nil {
  53. return err
  54. }
  55. }
  56. fc.Env["DOCUMENT_ROOT"] = root
  57. }
  58. fpath := request.URL.Path
  59. scriptName := fpath
  60. docURI := fpath
  61. // split "actual path" from "path info" if configured
  62. if splitPos := splitPos(fc, fpath); splitPos > -1 {
  63. docURI = fpath[:splitPos]
  64. fc.Env["PATH_INFO"] = fpath[splitPos:]
  65. // Strip PATH_INFO from SCRIPT_NAME
  66. scriptName = strings.TrimSuffix(scriptName, fc.Env["PATH_INFO"])
  67. }
  68. // SCRIPT_FILENAME is the absolute path of SCRIPT_NAME
  69. scriptFilename := sanitizedPathJoin(fc.Env["DOCUMENT_ROOT"], scriptName)
  70. // Ensure the SCRIPT_NAME has a leading slash for compliance with RFC3875
  71. // Info: https://tools.ietf.org/html/rfc3875#section-4.1.13
  72. if scriptName != "" && !strings.HasPrefix(scriptName, "/") {
  73. scriptName = "/" + scriptName
  74. }
  75. if _, ok := fc.Env["PHP_SELF"]; !ok {
  76. fc.Env["PHP_SELF"] = fpath
  77. }
  78. if _, ok := fc.Env["DOCUMENT_URI"]; !ok {
  79. fc.Env["DOCUMENT_URI"] = docURI
  80. }
  81. if _, ok := fc.Env["SCRIPT_FILENAME"]; !ok {
  82. fc.Env["SCRIPT_FILENAME"] = scriptFilename
  83. }
  84. if _, ok := fc.Env["SCRIPT_NAME"]; !ok {
  85. fc.Env["SCRIPT_NAME"] = scriptName
  86. }
  87. if _, ok := fc.Env["REQUEST_SCHEME"]; !ok {
  88. if request.TLS == nil {
  89. fc.Env["REQUEST_SCHEME"] = "http"
  90. } else {
  91. fc.Env["REQUEST_SCHEME"] = "https"
  92. }
  93. }
  94. if request.TLS != nil {
  95. if _, ok := fc.Env["HTTPS"]; !ok {
  96. fc.Env["HTTPS"] = "on"
  97. }
  98. // and pass the protocol details in a manner compatible with apache's mod_ssl
  99. // (which is why these have a SSL_ prefix and not TLS_).
  100. _, sslProtocolOk := fc.Env["SSL_PROTOCOL"]
  101. v, versionOk := tlsProtocolStrings[request.TLS.Version]
  102. if !sslProtocolOk && versionOk {
  103. fc.Env["SSL_PROTOCOL"] = v
  104. }
  105. }
  106. _, serverNameOk := fc.Env["SERVER_NAME"]
  107. _, serverPortOk := fc.Env["SERVER_PORT"]
  108. if !serverNameOk || !serverPortOk {
  109. reqHost, reqPort, err := net.SplitHostPort(request.Host)
  110. if err == nil {
  111. if !serverNameOk {
  112. fc.Env["SERVER_NAME"] = reqHost
  113. }
  114. // compliance with the CGI specification requires that
  115. // SERVER_PORT should only exist if it's a valid numeric value.
  116. // Info: https://www.ietf.org/rfc/rfc3875 Page 18
  117. if !serverPortOk {
  118. // compliance with the CGI specification requires that
  119. // the SERVER_PORT variable MUST be set to the TCP/IP port number on which this request is received from the client
  120. // even if the port is the default port for the scheme and could otherwise be omitted from a URI.
  121. // https://tools.ietf.org/html/rfc3875#section-4.1.15
  122. if reqPort != "" {
  123. fc.Env["SERVER_PORT"] = reqPort
  124. } else if fc.Env["REQUEST_SCHEME"] == "http" {
  125. fc.Env["SERVER_PORT"] = "80"
  126. } else if fc.Env["REQUEST_SCHEME"] == "https" {
  127. fc.Env["SERVER_PORT"] = "443"
  128. }
  129. }
  130. } else if !serverNameOk {
  131. // whatever, just assume there was no port
  132. fc.Env["SERVER_NAME"] = request.Host
  133. }
  134. }
  135. // Variables defined in CGI 1.1 spec
  136. // Some variables are unused but cleared explicitly to prevent
  137. // the parent environment from interfering.
  138. // We never override an entry previously set
  139. if _, ok := fc.Env["REMOTE_IDENT"]; !ok {
  140. fc.Env["REMOTE_IDENT"] = "" // Not used
  141. }
  142. if _, ok := fc.Env["AUTH_TYPE"]; !ok {
  143. fc.Env["AUTH_TYPE"] = "" // Not used
  144. }
  145. if _, ok := fc.Env["CONTENT_LENGTH"]; !ok {
  146. fc.Env["CONTENT_LENGTH"] = request.Header.Get("Content-Length")
  147. }
  148. if _, ok := fc.Env["CONTENT_TYPE"]; !ok {
  149. fc.Env["CONTENT_TYPE"] = request.Header.Get("Content-Type")
  150. }
  151. if _, ok := fc.Env["GATEWAY_INTERFACE"]; !ok {
  152. fc.Env["GATEWAY_INTERFACE"] = "CGI/1.1"
  153. }
  154. if _, ok := fc.Env["QUERY_STRING"]; !ok {
  155. fc.Env["QUERY_STRING"] = request.URL.RawQuery
  156. }
  157. if _, ok := fc.Env["QUERY_STRING"]; !ok {
  158. fc.Env["QUERY_STRING"] = request.URL.RawQuery
  159. }
  160. if _, ok := fc.Env["REQUEST_METHOD"]; !ok {
  161. fc.Env["REQUEST_METHOD"] = request.Method
  162. }
  163. if _, ok := fc.Env["SERVER_PROTOCOL"]; !ok {
  164. fc.Env["SERVER_PROTOCOL"] = request.Proto
  165. }
  166. if _, ok := fc.Env["SERVER_SOFTWARE"]; !ok {
  167. fc.Env["SERVER_SOFTWARE"] = "FrankenPHP"
  168. }
  169. if _, ok := fc.Env["HTTP_HOST"]; !ok {
  170. fc.Env["HTTP_HOST"] = request.Host // added here, since not always part of headers
  171. }
  172. if _, ok := fc.Env["REQUEST_URI"]; !ok {
  173. fc.Env["REQUEST_URI"] = request.URL.RequestURI()
  174. }
  175. // compliance with the CGI specification requires that
  176. // PATH_TRANSLATED should only exist if PATH_INFO is defined.
  177. // Info: https://www.ietf.org/rfc/rfc3875 Page 14
  178. if fc.Env["PATH_INFO"] != "" {
  179. fc.Env["PATH_TRANSLATED"] = sanitizedPathJoin(fc.Env["DOCUMENT_ROOT"], fc.Env["PATH_INFO"]) // Info: http://www.oreilly.com/openbook/cgi/ch02_04.html
  180. }
  181. // Add all HTTP headers to env variables
  182. for field, val := range request.Header {
  183. k := "HTTP_" + headerNameReplacer.Replace(strings.ToUpper(field))
  184. if _, ok := fc.Env[k]; !ok {
  185. fc.Env[k] = strings.Join(val, ", ")
  186. }
  187. }
  188. if _, ok := fc.Env["REMOTE_USER"]; !ok {
  189. var (
  190. authUser string
  191. ok bool
  192. )
  193. authUser, fc.authPassword, ok = request.BasicAuth()
  194. if ok {
  195. fc.Env["REMOTE_USER"] = authUser
  196. }
  197. }
  198. fc.populated = true
  199. return nil
  200. }
  201. // splitPos returns the index where path should
  202. // be split based on SplitPath.
  203. //
  204. // Adapted from https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
  205. // Copyright 2015 Matthew Holt and The Caddy Authors
  206. func splitPos(fc *FrankenPHPContext, path string) int {
  207. if len(fc.SplitPath) == 0 {
  208. return 0
  209. }
  210. lowerPath := strings.ToLower(path)
  211. for _, split := range fc.SplitPath {
  212. if idx := strings.Index(lowerPath, strings.ToLower(split)); idx > -1 {
  213. return idx + len(split)
  214. }
  215. }
  216. return -1
  217. }
  218. // Map of supported protocols to Apache ssl_mod format
  219. // Note that these are slightly different from SupportedProtocols in caddytls/config.go
  220. var tlsProtocolStrings = map[uint16]string{
  221. tls.VersionTLS10: "TLSv1",
  222. tls.VersionTLS11: "TLSv1.1",
  223. tls.VersionTLS12: "TLSv1.2",
  224. tls.VersionTLS13: "TLSv1.3",
  225. }
  226. var headerNameReplacer = strings.NewReplacer(" ", "_", "-", "_")
  227. // SanitizedPathJoin performs filepath.Join(root, reqPath) that
  228. // is safe against directory traversal attacks. It uses logic
  229. // similar to that in the Go standard library, specifically
  230. // in the implementation of http.Dir. The root is assumed to
  231. // be a trusted path, but reqPath is not; and the output will
  232. // never be outside of root. The resulting path can be used
  233. // with the local file system.
  234. //
  235. // Adapted from https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
  236. // Copyright 2015 Matthew Holt and The Caddy Authors
  237. func sanitizedPathJoin(root, reqPath string) string {
  238. if root == "" {
  239. root = "."
  240. }
  241. path := filepath.Join(root, filepath.Clean("/"+reqPath))
  242. // filepath.Join also cleans the path, and cleaning strips
  243. // the trailing slash, so we need to re-add it afterwards.
  244. // if the length is 1, then it's a path to the root,
  245. // and that should return ".", so we don't append the separator.
  246. if strings.HasSuffix(reqPath, "/") && len(reqPath) > 1 {
  247. path += separator
  248. }
  249. return path
  250. }
  251. const separator = string(filepath.Separator)