Skip to content

Commit 356d2e1

Browse files
refactor: cleaner cgi string handling
Introduces C-side interned string registry (frankenphp_strings) and a frankenphp_server_vars struct to bulk-register known $_SERVER variables with pre-sized hashtable capacity.
1 parent 27ff6b4 commit 356d2e1

6 files changed

Lines changed: 306 additions & 245 deletions

File tree

cgi.go

Lines changed: 92 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
package frankenphp
22

3-
// #cgo nocallback frankenphp_register_bulk
4-
// #cgo nocallback frankenphp_register_variables_from_request_info
3+
// #cgo nocallback frankenphp_register_server_vars
54
// #cgo nocallback frankenphp_register_variable_safe
6-
// #cgo nocallback frankenphp_register_single
7-
// #cgo noescape frankenphp_register_bulk
8-
// #cgo noescape frankenphp_register_variables_from_request_info
5+
// #cgo nocallback frankenphp_register_known_variable
6+
// #cgo nocallback frankenphp_init_persistent_string
7+
// #cgo noescape frankenphp_register_server_vars
98
// #cgo noescape frankenphp_register_variable_safe
10-
// #cgo noescape frankenphp_register_single
9+
// #cgo noescape frankenphp_register_known_variable
10+
// #cgo noescape frankenphp_init_persistent_string
1111
// #include "frankenphp.h"
1212
// #include <php_variables.h>
1313
import "C"
@@ -26,47 +26,6 @@ import (
2626
"golang.org/x/text/search"
2727
)
2828

29-
// Protocol versions, in Apache mod_ssl format: https://httpd.apache.org/docs/current/mod/mod_ssl.html
30-
// Note that these are slightly different from SupportedProtocols in caddytls/config.go
31-
var tlsProtocolStrings = map[uint16]string{
32-
tls.VersionTLS10: "TLSv1",
33-
tls.VersionTLS11: "TLSv1.1",
34-
tls.VersionTLS12: "TLSv1.2",
35-
tls.VersionTLS13: "TLSv1.3",
36-
}
37-
38-
// Known $_SERVER keys
39-
var knownServerKeys = []string{
40-
"CONTENT_LENGTH",
41-
"DOCUMENT_ROOT",
42-
"DOCUMENT_URI",
43-
"GATEWAY_INTERFACE",
44-
"HTTP_HOST",
45-
"HTTPS",
46-
"PATH_INFO",
47-
"PHP_SELF",
48-
"REMOTE_ADDR",
49-
"REMOTE_HOST",
50-
"REMOTE_PORT",
51-
"REQUEST_SCHEME",
52-
"SCRIPT_FILENAME",
53-
"SCRIPT_NAME",
54-
"SERVER_NAME",
55-
"SERVER_PORT",
56-
"SERVER_PROTOCOL",
57-
"SERVER_SOFTWARE",
58-
"SSL_PROTOCOL",
59-
"SSL_CIPHER",
60-
"AUTH_TYPE",
61-
"REMOTE_IDENT",
62-
"CONTENT_TYPE",
63-
"PATH_TRANSLATED",
64-
"QUERY_STRING",
65-
"REMOTE_USER",
66-
"REQUEST_METHOD",
67-
"REQUEST_URI",
68-
}
69-
7029
// cStringHTTPMethods caches C string versions of common HTTP methods
7130
// to avoid allocations in pinCString on every request.
7231
var cStringHTTPMethods = map[string]*C.char{
@@ -87,7 +46,6 @@ var cStringHTTPMethods = map[string]*C.char{
8746
// Inspired by https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
8847
func addKnownVariablesToServer(fc *frankenPHPContext, trackVarsArray *C.zval) {
8948
request := fc.request
90-
keys := mainThread.knownServerKeys
9149
// Separate remote IP and port; more lenient than net.SplitHostPort
9250
var ip, port string
9351
if idx := strings.LastIndex(request.RemoteAddr, ":"); idx > -1 {
@@ -102,24 +60,21 @@ func addKnownVariablesToServer(fc *frankenPHPContext, trackVarsArray *C.zval) {
10260
ip = ip[1 : len(ip)-1]
10361
}
10462

105-
var https, sslProtocol, sslCipher, rs string
63+
var rs, https, sslProtocol *C.zend_string
64+
var sslCipher string
10665

10766
if request.TLS == nil {
108-
rs = "http"
109-
https = ""
110-
sslProtocol = ""
67+
rs = C.frankenphp_strings.httpLowercase
68+
https = C.frankenphp_strings.empty
69+
sslProtocol = C.frankenphp_strings.empty
11170
sslCipher = ""
11271
} else {
113-
rs = "https"
114-
https = "on"
72+
rs = C.frankenphp_strings.httpsLowercase
73+
https = C.frankenphp_strings.on
11574

11675
// and pass the protocol details in a manner compatible with Apache's mod_ssl
11776
// (which is why these have an SSL_ prefix and not TLS_).
118-
if v, ok := tlsProtocolStrings[request.TLS.Version]; ok {
119-
sslProtocol = v
120-
} else {
121-
sslProtocol = ""
122-
}
77+
sslProtocol = tlsProtocol(request.TLS.Version)
12378

12479
if request.TLS.CipherSuite != 0 {
12580
sslCipher = tls.CipherSuiteName(request.TLS.CipherSuite)
@@ -139,9 +94,9 @@ func addKnownVariablesToServer(fc *frankenPHPContext, trackVarsArray *C.zval) {
13994
// even if the port is the default port for the scheme and could otherwise be omitted from a URI.
14095
// https://tools.ietf.org/html/rfc3875#section-4.1.15
14196
switch rs {
142-
case "https":
97+
case C.frankenphp_strings.httpsLowercase:
14398
reqPort = "443"
144-
case "http":
99+
case C.frankenphp_strings.httpLowercase:
145100
reqPort = "80"
146101
}
147102
}
@@ -156,59 +111,59 @@ func addKnownVariablesToServer(fc *frankenPHPContext, trackVarsArray *C.zval) {
156111
requestURI = fc.requestURI
157112
}
158113

159-
C.frankenphp_register_bulk(
160-
trackVarsArray,
161-
packCgiVariable(keys["REMOTE_ADDR"], ip),
162-
packCgiVariable(keys["REMOTE_HOST"], ip),
163-
packCgiVariable(keys["REMOTE_PORT"], port),
164-
packCgiVariable(keys["DOCUMENT_ROOT"], fc.documentRoot),
165-
packCgiVariable(keys["PATH_INFO"], fc.pathInfo),
166-
packCgiVariable(keys["PHP_SELF"], ensureLeadingSlash(request.URL.Path)),
167-
packCgiVariable(keys["DOCUMENT_URI"], fc.docURI),
168-
packCgiVariable(keys["SCRIPT_FILENAME"], fc.scriptFilename),
169-
packCgiVariable(keys["SCRIPT_NAME"], fc.scriptName),
170-
packCgiVariable(keys["HTTPS"], https),
171-
packCgiVariable(keys["SSL_PROTOCOL"], sslProtocol),
172-
packCgiVariable(keys["REQUEST_SCHEME"], rs),
173-
packCgiVariable(keys["SERVER_NAME"], reqHost),
174-
packCgiVariable(keys["SERVER_PORT"], serverPort),
175-
// Variables defined in CGI 1.1 spec
176-
// Some variables are unused but cleared explicitly to prevent
177-
// the parent environment from interfering.
178-
// These values can not be overridden
179-
packCgiVariable(keys["CONTENT_LENGTH"], contentLength),
180-
packCgiVariable(keys["GATEWAY_INTERFACE"], "CGI/1.1"),
181-
packCgiVariable(keys["SERVER_PROTOCOL"], request.Proto),
182-
packCgiVariable(keys["SERVER_SOFTWARE"], "FrankenPHP"),
183-
packCgiVariable(keys["HTTP_HOST"], request.Host),
184-
// These values are always empty but must be defined:
185-
packCgiVariable(keys["AUTH_TYPE"], ""),
186-
packCgiVariable(keys["REMOTE_IDENT"], ""),
187-
// Request uri of the original request
188-
packCgiVariable(keys["REQUEST_URI"], requestURI),
189-
packCgiVariable(keys["SSL_CIPHER"], sslCipher),
190-
)
191-
192-
// These values are already present in the SG(request_info), so we'll register them from there
193-
C.frankenphp_register_variables_from_request_info(
194-
trackVarsArray,
195-
keys["CONTENT_TYPE"],
196-
keys["PATH_TRANSLATED"],
197-
keys["QUERY_STRING"],
198-
keys["REMOTE_USER"],
199-
keys["REQUEST_METHOD"],
200-
)
201-
}
202-
203-
func packCgiVariable(key *C.zend_string, value string) C.ht_key_value_pair {
204-
return C.ht_key_value_pair{key, toUnsafeChar(value), C.size_t(len(value))}
114+
requestPath := ensureLeadingSlash(request.URL.Path)
115+
116+
C.frankenphp_register_server_vars(trackVarsArray, C.frankenphp_server_vars{
117+
// approximate total length to avoid array re-hashing:
118+
// 28 CGI vars + headers + environment
119+
total_num_vars: C.size_t(28 + len(request.Header) + len(fc.env) + lengthOfEnv),
120+
121+
// CGI vars with variable values
122+
remote_addr: toUnsafeChar(ip),
123+
remote_addr_len: C.size_t(len(ip)),
124+
remote_host: toUnsafeChar(ip),
125+
remote_host_len: C.size_t(len(ip)),
126+
remote_port: toUnsafeChar(port),
127+
remote_port_len: C.size_t(len(port)),
128+
document_root: toUnsafeChar(fc.documentRoot),
129+
document_root_len: C.size_t(len(fc.documentRoot)),
130+
path_info: toUnsafeChar(fc.pathInfo),
131+
path_info_len: C.size_t(len(fc.pathInfo)),
132+
php_self: toUnsafeChar(requestPath),
133+
php_self_len: C.size_t(len(requestPath)),
134+
document_uri: toUnsafeChar(fc.docURI),
135+
document_uri_len: C.size_t(len(fc.docURI)),
136+
script_filename: toUnsafeChar(fc.scriptFilename),
137+
script_filename_len: C.size_t(len(fc.scriptFilename)),
138+
script_name: toUnsafeChar(fc.scriptName),
139+
script_name_len: C.size_t(len(fc.scriptName)),
140+
server_name: toUnsafeChar(reqHost),
141+
server_name_len: C.size_t(len(reqHost)),
142+
server_port: toUnsafeChar(serverPort),
143+
server_port_len: C.size_t(len(serverPort)),
144+
content_length: toUnsafeChar(contentLength),
145+
content_length_len: C.size_t(len(contentLength)),
146+
server_protocol: toUnsafeChar(request.Proto),
147+
server_protocol_len: C.size_t(len(request.Proto)),
148+
http_host: toUnsafeChar(request.Host),
149+
http_host_len: C.size_t(len(request.Host)),
150+
request_uri: toUnsafeChar(requestURI),
151+
request_uri_len: C.size_t(len(requestURI)),
152+
ssl_cipher: toUnsafeChar(sslCipher),
153+
ssl_cipher_len: C.size_t(len(sslCipher)),
154+
155+
// CGI vars with known values
156+
request_scheme: rs, // "http" or "https"
157+
ssl_protocol: sslProtocol, // values from tlsProtocol
158+
https: https, // "on" or empty
159+
})
205160
}
206161

207162
func addHeadersToServer(ctx context.Context, request *http.Request, trackVarsArray *C.zval) {
208163
for field, val := range request.Header {
209-
if k := mainThread.commonHeaders[field]; k != nil {
164+
if k := commonHeaders[field]; k != nil {
210165
v := strings.Join(val, ", ")
211-
C.frankenphp_register_single(k, toUnsafeChar(v), C.size_t(len(v)), trackVarsArray)
166+
C.frankenphp_register_known_variable(k, toUnsafeChar(v), C.size_t(len(v)), trackVarsArray)
212167
continue
213168
}
214169

@@ -227,8 +182,8 @@ func addPreparedEnvToServer(fc *frankenPHPContext, trackVarsArray *C.zval) {
227182
fc.env = nil
228183
}
229184

230-
//export go_register_variables
231-
func go_register_variables(threadIndex C.uintptr_t, trackVarsArray *C.zval) {
185+
//export go_register_server_variables
186+
func go_register_server_variables(threadIndex C.uintptr_t, trackVarsArray *C.zval) {
232187
thread := phpThreads[threadIndex]
233188
fc := thread.frankenPHPContext()
234189

@@ -410,8 +365,32 @@ func ensureLeadingSlash(path string) string {
410365
return "/" + path
411366
}
412367

368+
// toUnsafeChar returns a *C.char pointing at the backing bytes the Go string.
369+
// If C does not store the string, it may be passed directly in a Cgo call (most efficient).
370+
// If C stores the string, it must be pinned explicitly instead (inefficient).
371+
// C may never modify the string.
413372
func toUnsafeChar(s string) *C.char {
414-
sData := unsafe.StringData(s)
373+
return (*C.char)(unsafe.Pointer(unsafe.StringData(s)))
374+
}
375+
376+
// initialize a global zend_string that must never be freed and is ignored by GC
377+
func newPersistentZendString(str string) *C.zend_string {
378+
return C.frankenphp_init_persistent_string(toUnsafeChar(str), C.size_t(len(str)))
379+
}
415380

416-
return (*C.char)(unsafe.Pointer(sData))
381+
// Protocol versions, in Apache mod_ssl format: https://httpd.apache.org/docs/current/mod/mod_ssl.html
382+
// Note that these are slightly different from SupportedProtocols in caddytls/config.go
383+
func tlsProtocol(proto uint16) *C.zend_string {
384+
switch proto {
385+
case tls.VersionTLS10:
386+
return C.frankenphp_strings.tls1
387+
case tls.VersionTLS11:
388+
return C.frankenphp_strings.tls11
389+
case tls.VersionTLS12:
390+
return C.frankenphp_strings.tls12
391+
case tls.VersionTLS13:
392+
return C.frankenphp_strings.tls13
393+
default:
394+
return C.frankenphp_strings.empty
395+
}
417396
}

env.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package frankenphp
22

3-
// #cgo nocallback frankenphp_init_persistent_string
4-
// #cgo noescape frankenphp_init_persistent_string
53
// #include "frankenphp.h"
64
// #include "types.h"
75
import "C"
@@ -10,12 +8,17 @@ import (
108
"strings"
119
)
1210

11+
var lengthOfEnv = 0
12+
1313
//export go_init_os_env
1414
func go_init_os_env(mainThreadEnv *C.zend_array) {
15-
for _, envVar := range os.Environ() {
15+
fullEnv := os.Environ()
16+
lengthOfEnv = len(fullEnv)
17+
18+
for _, envVar := range fullEnv {
1619
key, val, _ := strings.Cut(envVar, "=")
17-
zkey := C.frankenphp_init_persistent_string(toUnsafeChar(key), C.size_t(len(key)))
18-
zStr := C.frankenphp_init_persistent_string(toUnsafeChar(val), C.size_t(len(val)))
20+
zkey := newPersistentZendString(key)
21+
zStr := newPersistentZendString(val)
1922
C.__hash_update_string__(mainThreadEnv, zkey, zStr)
2023
}
2124
}

0 commit comments

Comments
 (0)