-
-
Notifications
You must be signed in to change notification settings - Fork 244
/
cgi.go
293 lines (258 loc) · 10.1 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
283
284
285
286
287
288
289
290
291
292
293
package frankenphp
// #include <php_variables.h>
// #include "frankenphp.h"
import "C"
import (
"crypto/tls"
"net"
"net/http"
"path/filepath"
"strings"
)
var knownServerKeys = map[string]struct{}{
"CONTENT_LENGTH\x00": {},
"DOCUMENT_ROOT\x00": {},
"DOCUMENT_URI\x00": {},
"GATEWAY_INTERFACE\x00": {},
"HTTP_HOST\x00": {},
"HTTPS\x00": {},
"PATH_INFO\x00": {},
"PHP_SELF\x00": {},
"REMOTE_ADDR\x00": {},
"REMOTE_HOST\x00": {},
"REMOTE_PORT\x00": {},
"REQUEST_SCHEME\x00": {},
"SCRIPT_FILENAME\x00": {},
"SCRIPT_NAME\x00": {},
"SERVER_NAME\x00": {},
"SERVER_PORT\x00": {},
"SERVER_PROTOCOL\x00": {},
"SERVER_SOFTWARE\x00": {},
"SSL_PROTOCOL\x00": {},
"AUTH_TYPE\x00": {},
"REMOTE_IDENT\x00": {},
"CONTENT_TYPE\x00": {},
"PATH_TRANSLATED\x00": {},
"QUERY_STRING\x00": {},
"REMOTE_USER\x00": {},
"REQUEST_METHOD\x00": {},
"REQUEST_URI\x00": {},
}
// computeKnownVariables 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 addKnownVariablesToServer(thread *phpThread, request *http.Request, fc *FrankenPHPContext, trackVarsArray *C.zval) {
keys := getKnownVariableKeys(thread)
// 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)
ra, raOK := fc.env["REMOTE_ADDR\x00"]
if raOK {
registerTrustedVar(keys["REMOTE_ADDR\x00"], ra, trackVarsArray, thread)
} else {
registerTrustedVar(keys["REMOTE_ADDR\x00"], ip, trackVarsArray, thread)
}
if rh, ok := fc.env["REMOTE_HOST\x00"]; ok {
registerTrustedVar(keys["REMOTE_HOST\x00"], rh, trackVarsArray, thread) // For speed, remote host lookups disabled
} else {
if raOK {
registerTrustedVar(keys["REMOTE_HOST\x00"], ra, trackVarsArray, thread)
} else {
registerTrustedVar(keys["REMOTE_HOST\x00"], ip, trackVarsArray, thread)
}
}
registerTrustedVar(keys["REMOTE_PORT\x00"], port, trackVarsArray, thread)
registerTrustedVar(keys["DOCUMENT_ROOT\x00"], fc.documentRoot, trackVarsArray, thread)
registerTrustedVar(keys["PATH_INFO\x00"], fc.pathInfo, trackVarsArray, thread)
registerTrustedVar(keys["PHP_SELF\x00"], request.URL.Path, trackVarsArray, thread)
registerTrustedVar(keys["DOCUMENT_URI\x00"], fc.docURI, trackVarsArray, thread)
registerTrustedVar(keys["SCRIPT_FILENAME\x00"], fc.scriptFilename, trackVarsArray, thread)
registerTrustedVar(keys["SCRIPT_NAME\x00"], fc.scriptName, trackVarsArray, thread)
var rs string
if request.TLS == nil {
rs = "http"
registerTrustedVar(keys["HTTPS\x00"], "", trackVarsArray, thread)
registerTrustedVar(keys["SSL_PROTOCOL\x00"], "", trackVarsArray, thread)
} else {
rs = "https"
if h, ok := fc.env["HTTPS\x00"]; ok {
registerTrustedVar(keys["HTTPS\x00"], h, trackVarsArray, thread)
} else {
registerTrustedVar(keys["HTTPS\x00"], "on", trackVarsArray, thread)
}
// and pass the protocol details in a manner compatible with apache's mod_ssl
// (which is why these have an SSL_ prefix and not TLS_).
if pr, ok := fc.env["SSL_PROTOCOL\x00"]; ok {
registerTrustedVar(keys["SSL_PROTOCOL\x00"], pr, trackVarsArray, thread)
} else {
if v, ok := tlsProtocolStrings[request.TLS.Version]; ok {
registerTrustedVar(keys["SSL_PROTOCOL\x00"], v, trackVarsArray, thread)
} else {
registerTrustedVar(keys["SSL_PROTOCOL\x00"], "", trackVarsArray, thread)
}
}
}
registerTrustedVar(keys["REQUEST_SCHEME\x00"], rs, trackVarsArray, thread)
reqHost, reqPort, _ := net.SplitHostPort(request.Host)
if reqHost == "" {
// whatever, just assume there was no port
reqHost = request.Host
}
if reqPort == "" {
// 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
switch rs {
case "https":
reqPort = "443"
case "http":
reqPort = "80"
}
}
registerTrustedVar(keys["SERVER_NAME\x00"], reqHost, trackVarsArray, thread)
if reqPort != "" {
registerTrustedVar(keys["SERVER_PORT\x00"], reqPort, trackVarsArray, thread)
} else {
registerTrustedVar(keys["SERVER_PORT\x00"], "", trackVarsArray, thread)
}
// Variables defined in CGI 1.1 spec
// Some variables are unused but cleared explicitly to prevent
// the parent environment from interfering.
// These values can not be overridden
registerTrustedVar(keys["CONTENT_LENGTH\x00"], request.Header.Get("Content-Length"), trackVarsArray, thread)
registerTrustedVar(keys["GATEWAY_INTERFACE\x00"], "CGI/1.1", trackVarsArray, thread)
registerTrustedVar(keys["SERVER_PROTOCOL\x00"], request.Proto, trackVarsArray, thread)
registerTrustedVar(keys["SERVER_SOFTWARE\x00"], "FrankenPHP", trackVarsArray, thread)
registerTrustedVar(keys["HTTP_HOST\x00"], request.Host, trackVarsArray, thread) // added here, since not always part of headers
// These values are always empty but must be defined:
registerTrustedVar(keys["AUTH_TYPE\x00"], "", trackVarsArray, thread)
registerTrustedVar(keys["REMOTE_IDENT\x00"], "", trackVarsArray, thread)
// These values are already present in the SG(request_info), so we'll register them from there
C.frankenphp_register_variables_from_request_info(
trackVarsArray,
keys["CONTENT_TYPE\x00"],
keys["PATH_TRANSLATED\x00"],
keys["QUERY_STRING\x00"],
keys["REMOTE_USER\x00"],
keys["REQUEST_METHOD\x00"],
keys["REQUEST_URI\x00"],
)
}
func registerTrustedVar(key *C.zend_string, value string, trackVarsArray *C.zval, thread *phpThread) {
C.frankenphp_register_trusted_var(key, thread.pinString(value), C.int(len(value)), trackVarsArray)
}
func addHeadersToServer(thread *phpThread, request *http.Request, fc *FrankenPHPContext, trackVarsArray *C.zval) {
for field, val := range request.Header {
k, ok := headerKeyCache.Get(field)
if !ok {
k = "HTTP_" + headerNameReplacer.Replace(strings.ToUpper(field)) + "\x00"
headerKeyCache.SetIfAbsent(field, k)
}
if _, ok := fc.env[k]; ok {
continue
}
v := strings.Join(val, ", ")
C.frankenphp_register_variable_safe(thread.pinString(k), thread.pinString(v), C.size_t(len(v)), trackVarsArray)
}
}
func addPreparedEnvToServer(thread *phpThread, fc *FrankenPHPContext, trackVarsArray *C.zval) {
for k, v := range fc.env {
C.frankenphp_register_variable_safe(thread.pinString(k), thread.pinString(v), C.size_t(len(v)), trackVarsArray)
}
fc.env = nil
}
func getKnownVariableKeys(thread *phpThread) map[string]*C.zend_string {
if thread.knownVariableKeys != nil {
return thread.knownVariableKeys
}
threadServerKeys := make(map[string]*C.zend_string)
for k := range knownServerKeys {
keyWithoutNull := strings.Replace(k, "\x00", "", -1)
threadServerKeys[k] = C.frankenphp_init_persistent_string(thread.pinString(keyWithoutNull), C.size_t(len(keyWithoutNull)))
}
thread.knownVariableKeys = threadServerKeys
return threadServerKeys
}
//export go_register_variables
func go_register_variables(threadIndex C.uintptr_t, trackVarsArray *C.zval) {
thread := phpThreads[threadIndex]
r := thread.getActiveRequest()
fc := r.Context().Value(contextKey).(*FrankenPHPContext)
addKnownVariablesToServer(thread, r, fc, trackVarsArray)
addHeadersToServer(thread, r, fc, trackVarsArray)
addPreparedEnvToServer(thread, fc, trackVarsArray)
}
//export go_frankenphp_release_known_variable_keys
func go_frankenphp_release_known_variable_keys(threadIndex C.uintptr_t) {
thread := phpThreads[threadIndex]
if thread.knownVariableKeys == nil {
return
}
for _, v := range thread.knownVariableKeys {
C.frankenphp_release_zend_string(v)
}
// release everything that might still be pinned to the thread
thread.Unpin()
thread.knownVariableKeys = 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 afterward.
// 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)