cgi.go 6.4 KB

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