forked from dunglas/frankenphp
-
Notifications
You must be signed in to change notification settings - Fork 0
/
cgi.go
282 lines (246 loc) · 8.15 KB
/
cgi.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
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
}
}
if fc.Env["SERVER_NAME"] == "" || fc.Env["SERVER_PORT"] == "" {
reqHost, reqPort, _ := net.SplitHostPort(request.Host)
if fc.Env["SERVER_NAME"] == "" {
fc.Env["SERVER_NAME"] = reqHost
}
if fc.Env["SERVER_PORT"] == "" {
fc.Env["SERVER_PORT"] = reqPort
}
if fc.Env["SERVER_NAME"] == "" {
// whatever, just assume there was no port
fc.Env["SERVER_NAME"] = request.Host
}
// 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 fc.Env["SERVER_PORT"] == "" {
if fc.Env["REQUEST_SCHEME"] == "https" {
fc.Env["SERVER_PORT"] = "443"
} else {
fc.Env["SERVER_PORT"] = "80"
}
}
}
// 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:https://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)