cgi.go 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. package frankenphp
  2. import "C"
  3. import (
  4. "crypto/tls"
  5. "net"
  6. "net/http"
  7. "path/filepath"
  8. "strings"
  9. )
  10. type serverKey int
  11. const (
  12. contentLength serverKey = iota
  13. documentRoot
  14. documentUri
  15. gatewayInterface
  16. httpHost
  17. https
  18. pathInfo
  19. phpSelf
  20. remoteAddr
  21. remoteHost
  22. remotePort
  23. requestScheme
  24. scriptFilename
  25. scriptName
  26. serverName
  27. serverPort
  28. serverProtocol
  29. serverSoftware
  30. sslProtocol
  31. )
  32. func allocServerVariable(cArr *[27]*C.char, env map[string]string, serverKey serverKey, envKey string, val string, pointers *pointerList) {
  33. if val, ok := env[envKey]; ok {
  34. cArr[serverKey] = pointers.ToCString(val)
  35. delete(env, envKey)
  36. return
  37. }
  38. cArr[serverKey] = pointers.ToCString(val)
  39. }
  40. // computeKnownVariables returns a set of CGI environment variables for the request.
  41. //
  42. // TODO: handle this case https://github.com/caddyserver/caddy/issues/3718
  43. // Inspired by https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
  44. func computeKnownVariables(request *http.Request) (cArr [27]*C.char) {
  45. fc, fcOK := FromContext(request.Context())
  46. if !fcOK {
  47. panic("not a FrankenPHP request")
  48. }
  49. pointers := getPointersForRequest(request)
  50. // Separate remote IP and port; more lenient than net.SplitHostPort
  51. var ip, port string
  52. if idx := strings.LastIndex(request.RemoteAddr, ":"); idx > -1 {
  53. ip = request.RemoteAddr[:idx]
  54. port = request.RemoteAddr[idx+1:]
  55. } else {
  56. ip = request.RemoteAddr
  57. }
  58. // Remove [] from IPv6 addresses
  59. ip = strings.Replace(ip, "[", "", 1)
  60. ip = strings.Replace(ip, "]", "", 1)
  61. ra, raOK := fc.env["REMOTE_ADDR"]
  62. if raOK {
  63. cArr[remoteAddr] = pointers.ToCString(ra)
  64. delete(fc.env, "REMOTE_ADDR")
  65. } else {
  66. cArr[remoteAddr] = pointers.ToCString(ip)
  67. }
  68. if rh, ok := fc.env["REMOTE_HOST"]; ok {
  69. cArr[remoteHost] = pointers.ToCString(rh) // For speed, remote host lookups disabled
  70. delete(fc.env, "REMOTE_HOST")
  71. } else {
  72. if raOK {
  73. cArr[remoteHost] = pointers.ToCString(ip)
  74. } else {
  75. cArr[remoteHost] = cArr[remoteAddr]
  76. }
  77. }
  78. allocServerVariable(&cArr, fc.env, remotePort, "REMOTE_PORT", port, pointers)
  79. allocServerVariable(&cArr, fc.env, documentRoot, "DOCUMENT_ROOT", fc.documentRoot, pointers)
  80. allocServerVariable(&cArr, fc.env, pathInfo, "PATH_INFO", fc.pathInfo, pointers)
  81. allocServerVariable(&cArr, fc.env, phpSelf, "PHP_SELF", request.URL.Path, pointers)
  82. allocServerVariable(&cArr, fc.env, documentUri, "DOCUMENT_URI", fc.docURI, pointers)
  83. allocServerVariable(&cArr, fc.env, scriptFilename, "SCRIPT_FILENAME", fc.scriptFilename, pointers)
  84. allocServerVariable(&cArr, fc.env, scriptName, "SCRIPT_NAME", fc.scriptName, pointers)
  85. var rs string
  86. if request.TLS == nil {
  87. rs = "http"
  88. } else {
  89. rs = "https"
  90. if h, ok := fc.env["HTTPS"]; ok {
  91. cArr[https] = pointers.ToCString(h)
  92. delete(fc.env, "HTTPS")
  93. } else {
  94. cArr[https] = pointers.ToCString("on")
  95. }
  96. // and pass the protocol details in a manner compatible with apache's mod_ssl
  97. // (which is why these have a SSL_ prefix and not TLS_).
  98. if p, ok := fc.env["SSL_PROTOCOL"]; ok {
  99. cArr[sslProtocol] = pointers.ToCString(p)
  100. delete(fc.env, "SSL_PROTOCOL")
  101. } else {
  102. if v, ok := tlsProtocolStrings[request.TLS.Version]; ok {
  103. cArr[sslProtocol] = pointers.ToCString(v)
  104. }
  105. }
  106. }
  107. allocServerVariable(&cArr, fc.env, requestScheme, "REQUEST_SCHEME", rs, pointers)
  108. reqHost, reqPort, _ := net.SplitHostPort(request.Host)
  109. if reqHost == "" {
  110. // whatever, just assume there was no port
  111. reqHost = request.Host
  112. }
  113. if reqPort == "" {
  114. // compliance with the CGI specification requires that
  115. // the SERVER_PORT variable MUST be set to the TCP/IP port number on which this request is received from the client
  116. // even if the port is the default port for the scheme and could otherwise be omitted from a URI.
  117. // https://tools.ietf.org/html/rfc3875#section-4.1.15
  118. switch rs {
  119. case "https":
  120. reqPort = "443"
  121. case "http":
  122. reqPort = "80"
  123. }
  124. }
  125. allocServerVariable(&cArr, fc.env, serverName, "SERVER_NAME", reqHost, pointers)
  126. if reqPort != "" {
  127. allocServerVariable(&cArr, fc.env, serverPort, "SERVER_PORT", reqPort, pointers)
  128. }
  129. // Variables defined in CGI 1.1 spec
  130. // Some variables are unused but cleared explicitly to prevent
  131. // the parent environment from interfering.
  132. // These values can not be override
  133. cArr[contentLength] = pointers.ToCString(request.Header.Get("Content-Length"))
  134. allocServerVariable(&cArr, fc.env, gatewayInterface, "GATEWAY_INTERFACE", "CGI/1.1", pointers)
  135. allocServerVariable(&cArr, fc.env, serverProtocol, "SERVER_PROTOCOL", request.Proto, pointers)
  136. allocServerVariable(&cArr, fc.env, serverSoftware, "SERVER_SOFTWARE", "FrankenPHP", pointers)
  137. allocServerVariable(&cArr, fc.env, httpHost, "HTTP_HOST", request.Host, pointers) // added here, since not always part of headers
  138. return
  139. }
  140. // splitPos returns the index where path should
  141. // be split based on SplitPath.
  142. //
  143. // Adapted from https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
  144. // Copyright 2015 Matthew Holt and The Caddy Authors
  145. func splitPos(fc *FrankenPHPContext, path string) int {
  146. if len(fc.splitPath) == 0 {
  147. return 0
  148. }
  149. lowerPath := strings.ToLower(path)
  150. for _, split := range fc.splitPath {
  151. if idx := strings.Index(lowerPath, strings.ToLower(split)); idx > -1 {
  152. return idx + len(split)
  153. }
  154. }
  155. return -1
  156. }
  157. // Map of supported protocols to Apache ssl_mod format
  158. // Note that these are slightly different from SupportedProtocols in caddytls/config.go
  159. var tlsProtocolStrings = map[uint16]string{
  160. tls.VersionTLS10: "TLSv1",
  161. tls.VersionTLS11: "TLSv1.1",
  162. tls.VersionTLS12: "TLSv1.2",
  163. tls.VersionTLS13: "TLSv1.3",
  164. }
  165. var headerNameReplacer = strings.NewReplacer(" ", "_", "-", "_")
  166. // SanitizedPathJoin performs filepath.Join(root, reqPath) that
  167. // is safe against directory traversal attacks. It uses logic
  168. // similar to that in the Go standard library, specifically
  169. // in the implementation of http.Dir. The root is assumed to
  170. // be a trusted path, but reqPath is not; and the output will
  171. // never be outside of root. The resulting path can be used
  172. // with the local file system.
  173. //
  174. // Adapted from https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
  175. // Copyright 2015 Matthew Holt and The Caddy Authors
  176. func sanitizedPathJoin(root, reqPath string) string {
  177. if root == "" {
  178. root = "."
  179. }
  180. path := filepath.Join(root, filepath.Clean("/"+reqPath))
  181. // filepath.Join also cleans the path, and cleaning strips
  182. // the trailing slash, so we need to re-add it afterwards.
  183. // if the length is 1, then it's a path to the root,
  184. // and that should return ".", so we don't append the separator.
  185. if strings.HasSuffix(reqPath, "/") && len(reqPath) > 1 {
  186. path += separator
  187. }
  188. return path
  189. }
  190. const separator = string(filepath.Separator)