cgi.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. package frankenphp
  2. // #include <php_variables.h>
  3. // #include "frankenphp.h"
  4. import "C"
  5. import (
  6. "crypto/tls"
  7. "net"
  8. "net/http"
  9. "path"
  10. "path/filepath"
  11. "strings"
  12. )
  13. var knownServerKeys = map[string]struct{}{
  14. "CONTENT_LENGTH\x00": {},
  15. "DOCUMENT_ROOT\x00": {},
  16. "DOCUMENT_URI\x00": {},
  17. "GATEWAY_INTERFACE\x00": {},
  18. "HTTP_HOST\x00": {},
  19. "HTTPS\x00": {},
  20. "PATH_INFO\x00": {},
  21. "PHP_SELF\x00": {},
  22. "REMOTE_ADDR\x00": {},
  23. "REMOTE_HOST\x00": {},
  24. "REMOTE_PORT\x00": {},
  25. "REQUEST_SCHEME\x00": {},
  26. "SCRIPT_FILENAME\x00": {},
  27. "SCRIPT_NAME\x00": {},
  28. "SERVER_NAME\x00": {},
  29. "SERVER_PORT\x00": {},
  30. "SERVER_PROTOCOL\x00": {},
  31. "SERVER_SOFTWARE\x00": {},
  32. "SSL_PROTOCOL\x00": {},
  33. "AUTH_TYPE\x00": {},
  34. "REMOTE_IDENT\x00": {},
  35. "CONTENT_TYPE\x00": {},
  36. "PATH_TRANSLATED\x00": {},
  37. "QUERY_STRING\x00": {},
  38. "REMOTE_USER\x00": {},
  39. "REQUEST_METHOD\x00": {},
  40. "REQUEST_URI\x00": {},
  41. }
  42. // computeKnownVariables returns a set of CGI environment variables for the request.
  43. //
  44. // TODO: handle this case https://github.com/caddyserver/caddy/issues/3718
  45. // Inspired by https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
  46. func addKnownVariablesToServer(thread *phpThread, request *http.Request, fc *FrankenPHPContext, trackVarsArray *C.zval) {
  47. keys := getKnownVariableKeys(thread)
  48. // Separate remote IP and port; more lenient than net.SplitHostPort
  49. var ip, port string
  50. if idx := strings.LastIndex(request.RemoteAddr, ":"); idx > -1 {
  51. ip = request.RemoteAddr[:idx]
  52. port = request.RemoteAddr[idx+1:]
  53. } else {
  54. ip = request.RemoteAddr
  55. }
  56. // Remove [] from IPv6 addresses
  57. ip = strings.Replace(ip, "[", "", 1)
  58. ip = strings.Replace(ip, "]", "", 1)
  59. ra, raOK := fc.env["REMOTE_ADDR\x00"]
  60. if raOK {
  61. registerTrustedVar(keys["REMOTE_ADDR\x00"], ra, trackVarsArray, thread)
  62. } else {
  63. registerTrustedVar(keys["REMOTE_ADDR\x00"], ip, trackVarsArray, thread)
  64. }
  65. if rh, ok := fc.env["REMOTE_HOST\x00"]; ok {
  66. registerTrustedVar(keys["REMOTE_HOST\x00"], rh, trackVarsArray, thread) // For speed, remote host lookups disabled
  67. } else {
  68. if raOK {
  69. registerTrustedVar(keys["REMOTE_HOST\x00"], ra, trackVarsArray, thread)
  70. } else {
  71. registerTrustedVar(keys["REMOTE_HOST\x00"], ip, trackVarsArray, thread)
  72. }
  73. }
  74. registerTrustedVar(keys["REMOTE_PORT\x00"], port, trackVarsArray, thread)
  75. registerTrustedVar(keys["DOCUMENT_ROOT\x00"], fc.documentRoot, trackVarsArray, thread)
  76. registerTrustedVar(keys["PATH_INFO\x00"], fc.pathInfo, trackVarsArray, thread)
  77. registerTrustedVar(keys["PHP_SELF\x00"], path.Clean(request.URL.Path), trackVarsArray, thread)
  78. registerTrustedVar(keys["DOCUMENT_URI\x00"], fc.docURI, trackVarsArray, thread)
  79. registerTrustedVar(keys["SCRIPT_FILENAME\x00"], fc.scriptFilename, trackVarsArray, thread)
  80. registerTrustedVar(keys["SCRIPT_NAME\x00"], fc.scriptName, trackVarsArray, thread)
  81. var rs string
  82. if request.TLS == nil {
  83. rs = "http"
  84. registerTrustedVar(keys["HTTPS\x00"], "", trackVarsArray, thread)
  85. registerTrustedVar(keys["SSL_PROTOCOL\x00"], "", trackVarsArray, thread)
  86. } else {
  87. rs = "https"
  88. if h, ok := fc.env["HTTPS\x00"]; ok {
  89. registerTrustedVar(keys["HTTPS\x00"], h, trackVarsArray, thread)
  90. } else {
  91. registerTrustedVar(keys["HTTPS\x00"], "on", trackVarsArray, thread)
  92. }
  93. // and pass the protocol details in a manner compatible with apache's mod_ssl
  94. // (which is why these have an SSL_ prefix and not TLS_).
  95. if pr, ok := fc.env["SSL_PROTOCOL\x00"]; ok {
  96. registerTrustedVar(keys["SSL_PROTOCOL\x00"], pr, trackVarsArray, thread)
  97. } else {
  98. if v, ok := tlsProtocolStrings[request.TLS.Version]; ok {
  99. registerTrustedVar(keys["SSL_PROTOCOL\x00"], v, trackVarsArray, thread)
  100. } else {
  101. registerTrustedVar(keys["SSL_PROTOCOL\x00"], "", trackVarsArray, thread)
  102. }
  103. }
  104. }
  105. registerTrustedVar(keys["REQUEST_SCHEME\x00"], rs, trackVarsArray, thread)
  106. reqHost, reqPort, _ := net.SplitHostPort(request.Host)
  107. if reqHost == "" {
  108. // whatever, just assume there was no port
  109. reqHost = request.Host
  110. }
  111. if reqPort == "" {
  112. // compliance with the CGI specification requires that
  113. // the SERVER_PORT variable MUST be set to the TCP/IP port number on which this request is received from the client
  114. // even if the port is the default port for the scheme and could otherwise be omitted from a URI.
  115. // https://tools.ietf.org/html/rfc3875#section-4.1.15
  116. switch rs {
  117. case "https":
  118. reqPort = "443"
  119. case "http":
  120. reqPort = "80"
  121. }
  122. }
  123. registerTrustedVar(keys["SERVER_NAME\x00"], reqHost, trackVarsArray, thread)
  124. if reqPort != "" {
  125. registerTrustedVar(keys["SERVER_PORT\x00"], reqPort, trackVarsArray, thread)
  126. } else {
  127. registerTrustedVar(keys["SERVER_PORT\x00"], "", trackVarsArray, thread)
  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 overridden
  133. registerTrustedVar(keys["CONTENT_LENGTH\x00"], request.Header.Get("Content-Length"), trackVarsArray, thread)
  134. registerTrustedVar(keys["GATEWAY_INTERFACE\x00"], "CGI/1.1", trackVarsArray, thread)
  135. registerTrustedVar(keys["SERVER_PROTOCOL\x00"], request.Proto, trackVarsArray, thread)
  136. registerTrustedVar(keys["SERVER_SOFTWARE\x00"], "FrankenPHP", trackVarsArray, thread)
  137. registerTrustedVar(keys["HTTP_HOST\x00"], request.Host, trackVarsArray, thread) // added here, since not always part of headers
  138. // These values are always empty but must be defined:
  139. registerTrustedVar(keys["AUTH_TYPE\x00"], "", trackVarsArray, thread)
  140. registerTrustedVar(keys["REMOTE_IDENT\x00"], "", trackVarsArray, thread)
  141. // These values are already present in the SG(request_info), so we'll register them from there
  142. C.frankenphp_register_variables_from_request_info(
  143. trackVarsArray,
  144. keys["CONTENT_TYPE\x00"],
  145. keys["PATH_TRANSLATED\x00"],
  146. keys["QUERY_STRING\x00"],
  147. keys["REMOTE_USER\x00"],
  148. keys["REQUEST_METHOD\x00"],
  149. keys["REQUEST_URI\x00"],
  150. )
  151. }
  152. func registerTrustedVar(key *C.zend_string, value string, trackVarsArray *C.zval, thread *phpThread) {
  153. C.frankenphp_register_trusted_var(key, thread.pinString(value), C.int(len(value)), trackVarsArray)
  154. }
  155. func addHeadersToServer(thread *phpThread, request *http.Request, fc *FrankenPHPContext, trackVarsArray *C.zval) {
  156. for field, val := range request.Header {
  157. k, ok := headerKeyCache.Get(field)
  158. if !ok {
  159. k = "HTTP_" + headerNameReplacer.Replace(strings.ToUpper(field)) + "\x00"
  160. headerKeyCache.SetIfAbsent(field, k)
  161. }
  162. if _, ok := fc.env[k]; ok {
  163. continue
  164. }
  165. v := strings.Join(val, ", ")
  166. C.frankenphp_register_variable_safe(thread.pinString(k), thread.pinString(v), C.size_t(len(v)), trackVarsArray)
  167. }
  168. }
  169. func addPreparedEnvToServer(thread *phpThread, fc *FrankenPHPContext, trackVarsArray *C.zval) {
  170. for k, v := range fc.env {
  171. C.frankenphp_register_variable_safe(thread.pinString(k), thread.pinString(v), C.size_t(len(v)), trackVarsArray)
  172. }
  173. fc.env = nil
  174. }
  175. func getKnownVariableKeys(thread *phpThread) map[string]*C.zend_string {
  176. if thread.knownVariableKeys != nil {
  177. return thread.knownVariableKeys
  178. }
  179. threadServerKeys := make(map[string]*C.zend_string)
  180. for k := range knownServerKeys {
  181. keyWithoutNull := strings.Replace(k, "\x00", "", -1)
  182. threadServerKeys[k] = C.frankenphp_init_persistent_string(thread.pinString(keyWithoutNull), C.size_t(len(keyWithoutNull)))
  183. }
  184. thread.knownVariableKeys = threadServerKeys
  185. return threadServerKeys
  186. }
  187. //export go_register_variables
  188. func go_register_variables(threadIndex C.uintptr_t, trackVarsArray *C.zval) {
  189. thread := phpThreads[threadIndex]
  190. r := thread.getActiveRequest()
  191. fc := r.Context().Value(contextKey).(*FrankenPHPContext)
  192. addKnownVariablesToServer(thread, r, fc, trackVarsArray)
  193. addHeadersToServer(thread, r, fc, trackVarsArray)
  194. addPreparedEnvToServer(thread, fc, trackVarsArray)
  195. }
  196. //export go_frankenphp_release_known_variable_keys
  197. func go_frankenphp_release_known_variable_keys(threadIndex C.uintptr_t) {
  198. thread := phpThreads[threadIndex]
  199. if thread.knownVariableKeys == nil {
  200. return
  201. }
  202. for _, v := range thread.knownVariableKeys {
  203. C.frankenphp_release_zend_string(v)
  204. }
  205. // release everything that might still be pinned to the thread
  206. thread.Unpin()
  207. thread.knownVariableKeys = nil
  208. }
  209. // splitPos returns the index where path should
  210. // be split based on SplitPath.
  211. //
  212. // Adapted from https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
  213. // Copyright 2015 Matthew Holt and The Caddy Authors
  214. func splitPos(fc *FrankenPHPContext, path string) int {
  215. if len(fc.splitPath) == 0 {
  216. return 0
  217. }
  218. lowerPath := strings.ToLower(path)
  219. for _, split := range fc.splitPath {
  220. if idx := strings.Index(lowerPath, strings.ToLower(split)); idx > -1 {
  221. return idx + len(split)
  222. }
  223. }
  224. return -1
  225. }
  226. // Map of supported protocols to Apache ssl_mod format
  227. // Note that these are slightly different from SupportedProtocols in caddytls/config.go
  228. var tlsProtocolStrings = map[uint16]string{
  229. tls.VersionTLS10: "TLSv1",
  230. tls.VersionTLS11: "TLSv1.1",
  231. tls.VersionTLS12: "TLSv1.2",
  232. tls.VersionTLS13: "TLSv1.3",
  233. }
  234. var headerNameReplacer = strings.NewReplacer(" ", "_", "-", "_")
  235. // SanitizedPathJoin performs filepath.Join(root, reqPath) that
  236. // is safe against directory traversal attacks. It uses logic
  237. // similar to that in the Go standard library, specifically
  238. // in the implementation of http.Dir. The root is assumed to
  239. // be a trusted path, but reqPath is not; and the output will
  240. // never be outside of root. The resulting path can be used
  241. // with the local file system.
  242. //
  243. // Adapted from https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
  244. // Copyright 2015 Matthew Holt and The Caddy Authors
  245. func sanitizedPathJoin(root, reqPath string) string {
  246. if root == "" {
  247. root = "."
  248. }
  249. path := filepath.Join(root, filepath.Clean("/"+reqPath))
  250. // filepath.Join also cleans the path, and cleaning strips
  251. // the trailing slash, so we need to re-add it afterward.
  252. // if the length is 1, then it's a path to the root,
  253. // and that should return ".", so we don't append the separator.
  254. if strings.HasSuffix(reqPath, "/") && len(reqPath) > 1 {
  255. path += separator
  256. }
  257. return path
  258. }
  259. const separator = string(filepath.Separator)