Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
f3bf7db
test.
AlliBalliBaba Jan 10, 2026
84be9df
Extracts zend_strings.
AlliBalliBaba Jan 12, 2026
d44c476
Merge branch 'main' into perf/cgi-strings
AlliBalliBaba Jan 12, 2026
9eb215d
Rehashes to total num of vars.
AlliBalliBaba Jan 25, 2026
90aff62
Cleanup.
AlliBalliBaba Jan 25, 2026
bad4a06
Merge branch 'main' into perf/cgi-strings
AlliBalliBaba Jan 25, 2026
43b77e1
Merge branch 'main' into perf/cgi-strings
AlliBalliBaba Feb 8, 2026
d217257
Merge branch 'refs/heads/main' into perf/cgi-strings
AlliBalliBaba Feb 14, 2026
416905d
Cleanup
AlliBalliBaba Feb 14, 2026
3b2847a
Moves struct to the C side.
AlliBalliBaba Feb 14, 2026
bbdf3f8
Cleanup.
AlliBalliBaba Feb 14, 2026
2d5546f
Formatting.
AlliBalliBaba Feb 14, 2026
63becf1
Makes registration nicer.
AlliBalliBaba Feb 14, 2026
5e6b36d
Makes macro names less generic.
AlliBalliBaba Feb 14, 2026
419257c
Makes macro names less generic.
AlliBalliBaba Feb 14, 2026
26c3b7f
Removes unwanted commits.
AlliBalliBaba Feb 14, 2026
806f558
Resets to indirct update.
AlliBalliBaba Feb 15, 2026
4897775
Makes info registration nicer.
AlliBalliBaba Feb 15, 2026
04509c8
Naming for clarity.
AlliBalliBaba Feb 15, 2026
9fa5c31
Cleanup.
AlliBalliBaba Feb 15, 2026
7fee4b3
Merge branch 'main' into perf/cgi-strings
AlliBalliBaba Feb 16, 2026
76c13f3
Merge branch 'main' into perf/cgi-strings
AlliBalliBaba Feb 27, 2026
8653cf8
Merge branch 'main' into perf/cgi-strings
AlliBalliBaba Feb 27, 2026
d7cdcb4
Gathers all string caches in a separate module
AlliBalliBaba Feb 27, 2026
782fe9b
More refactoring.
AlliBalliBaba Feb 27, 2026
66bcee9
Cleanup.
AlliBalliBaba Feb 27, 2026
0a29f82
Lowercase fixes.
AlliBalliBaba Feb 27, 2026
39bf5dc
More macro shenanigans.
AlliBalliBaba Feb 28, 2026
848a864
Cleanup.
AlliBalliBaba Feb 28, 2026
e322077
More hard-coded zend_strings.
AlliBalliBaba Feb 28, 2026
fd55e6a
More hard-coded zend_strings.
AlliBalliBaba Feb 28, 2026
28d0443
Cleanup.
AlliBalliBaba Feb 28, 2026
a97a126
Uses own empty string for window compatibiltiy.
AlliBalliBaba Mar 1, 2026
ce84ef1
Cleanup.
AlliBalliBaba Mar 1, 2026
378a1a4
Fixes import
AlliBalliBaba Mar 1, 2026
44ac7a5
Adjusts var name.
AlliBalliBaba Mar 1, 2026
5dc9416
Adjusts comments
AlliBalliBaba Mar 3, 2026
2ab3d54
Adjusts comments.
AlliBalliBaba Mar 3, 2026
d9bbced
fixes comments.
AlliBalliBaba Mar 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 92 additions & 113 deletions cgi.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package frankenphp

// #cgo nocallback frankenphp_register_bulk
// #cgo nocallback frankenphp_register_variables_from_request_info
// #cgo nocallback frankenphp_register_server_vars
// #cgo nocallback frankenphp_register_variable_safe
// #cgo nocallback frankenphp_register_single
// #cgo noescape frankenphp_register_bulk
// #cgo noescape frankenphp_register_variables_from_request_info
// #cgo nocallback frankenphp_register_known_variable
// #cgo nocallback frankenphp_init_persistent_string
// #cgo noescape frankenphp_register_server_vars
// #cgo noescape frankenphp_register_variable_safe
// #cgo noescape frankenphp_register_single
// #cgo noescape frankenphp_register_known_variable
// #cgo noescape frankenphp_init_persistent_string
// #include "frankenphp.h"
// #include <php_variables.h>
import "C"
Expand All @@ -26,47 +26,6 @@ import (
"golang.org/x/text/search"
)

// Protocol versions, in Apache mod_ssl format: https://httpd.apache.org/docs/current/mod/mod_ssl.html
// 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",
}

// Known $_SERVER keys
var knownServerKeys = []string{
"CONTENT_LENGTH",
"DOCUMENT_ROOT",
"DOCUMENT_URI",
"GATEWAY_INTERFACE",
"HTTP_HOST",
"HTTPS",
"PATH_INFO",
"PHP_SELF",
"REMOTE_ADDR",
"REMOTE_HOST",
"REMOTE_PORT",
"REQUEST_SCHEME",
"SCRIPT_FILENAME",
"SCRIPT_NAME",
"SERVER_NAME",
"SERVER_PORT",
"SERVER_PROTOCOL",
"SERVER_SOFTWARE",
"SSL_PROTOCOL",
"SSL_CIPHER",
"AUTH_TYPE",
"REMOTE_IDENT",
"CONTENT_TYPE",
"PATH_TRANSLATED",
"QUERY_STRING",
"REMOTE_USER",
"REQUEST_METHOD",
"REQUEST_URI",
}

// cStringHTTPMethods caches C string versions of common HTTP methods
// to avoid allocations in pinCString on every request.
var cStringHTTPMethods = map[string]*C.char{
Expand All @@ -87,7 +46,6 @@ var cStringHTTPMethods = map[string]*C.char{
// Inspired by https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
func addKnownVariablesToServer(fc *frankenPHPContext, trackVarsArray *C.zval) {
request := fc.request
keys := mainThread.knownServerKeys
// Separate remote IP and port; more lenient than net.SplitHostPort
var ip, port string
if idx := strings.LastIndex(request.RemoteAddr, ":"); idx > -1 {
Expand All @@ -102,24 +60,21 @@ func addKnownVariablesToServer(fc *frankenPHPContext, trackVarsArray *C.zval) {
ip = ip[1 : len(ip)-1]
}

var https, sslProtocol, sslCipher, rs string
var rs, https, sslProtocol *C.zend_string
var sslCipher string

if request.TLS == nil {
rs = "http"
https = ""
sslProtocol = ""
rs = C.frankenphp_strings.httpLowercase
https = C.frankenphp_strings.empty
sslProtocol = C.frankenphp_strings.empty
sslCipher = ""
} else {
rs = "https"
https = "on"
rs = C.frankenphp_strings.httpsLowercase
https = C.frankenphp_strings.on

// 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 v, ok := tlsProtocolStrings[request.TLS.Version]; ok {
sslProtocol = v
} else {
sslProtocol = ""
}
sslProtocol = tlsProtocol(request.TLS.Version)

if request.TLS.CipherSuite != 0 {
sslCipher = tls.CipherSuiteName(request.TLS.CipherSuite)
Expand All @@ -139,9 +94,9 @@ func addKnownVariablesToServer(fc *frankenPHPContext, trackVarsArray *C.zval) {
// 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":
case C.frankenphp_strings.httpsLowercase:
reqPort = "443"
case "http":
case C.frankenphp_strings.httpLowercase:
reqPort = "80"
}
}
Expand All @@ -156,59 +111,59 @@ func addKnownVariablesToServer(fc *frankenPHPContext, trackVarsArray *C.zval) {
requestURI = fc.requestURI
}

C.frankenphp_register_bulk(
trackVarsArray,
packCgiVariable(keys["REMOTE_ADDR"], ip),
packCgiVariable(keys["REMOTE_HOST"], ip),
packCgiVariable(keys["REMOTE_PORT"], port),
packCgiVariable(keys["DOCUMENT_ROOT"], fc.documentRoot),
packCgiVariable(keys["PATH_INFO"], fc.pathInfo),
packCgiVariable(keys["PHP_SELF"], ensureLeadingSlash(request.URL.Path)),
packCgiVariable(keys["DOCUMENT_URI"], fc.docURI),
packCgiVariable(keys["SCRIPT_FILENAME"], fc.scriptFilename),
packCgiVariable(keys["SCRIPT_NAME"], fc.scriptName),
packCgiVariable(keys["HTTPS"], https),
packCgiVariable(keys["SSL_PROTOCOL"], sslProtocol),
packCgiVariable(keys["REQUEST_SCHEME"], rs),
packCgiVariable(keys["SERVER_NAME"], reqHost),
packCgiVariable(keys["SERVER_PORT"], serverPort),
// 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
packCgiVariable(keys["CONTENT_LENGTH"], contentLength),
packCgiVariable(keys["GATEWAY_INTERFACE"], "CGI/1.1"),
packCgiVariable(keys["SERVER_PROTOCOL"], request.Proto),
packCgiVariable(keys["SERVER_SOFTWARE"], "FrankenPHP"),
packCgiVariable(keys["HTTP_HOST"], request.Host),
// These values are always empty but must be defined:
packCgiVariable(keys["AUTH_TYPE"], ""),
packCgiVariable(keys["REMOTE_IDENT"], ""),
// Request uri of the original request
packCgiVariable(keys["REQUEST_URI"], requestURI),
packCgiVariable(keys["SSL_CIPHER"], sslCipher),
)

// 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"],
keys["PATH_TRANSLATED"],
keys["QUERY_STRING"],
keys["REMOTE_USER"],
keys["REQUEST_METHOD"],
)
}

func packCgiVariable(key *C.zend_string, value string) C.ht_key_value_pair {
return C.ht_key_value_pair{key, toUnsafeChar(value), C.size_t(len(value))}
requestPath := ensureLeadingSlash(request.URL.Path)

C.frankenphp_register_server_vars(trackVarsArray, C.frankenphp_server_vars{
// approximate total length to avoid array re-hashing:
// 28 CGI vars + headers + environment
total_num_vars: C.size_t(28 + len(request.Header) + len(fc.env) + lengthOfEnv),

// CGI vars with variable values
remote_addr: toUnsafeChar(ip),
remote_addr_len: C.size_t(len(ip)),
remote_host: toUnsafeChar(ip),
remote_host_len: C.size_t(len(ip)),
remote_port: toUnsafeChar(port),
remote_port_len: C.size_t(len(port)),
document_root: toUnsafeChar(fc.documentRoot),
document_root_len: C.size_t(len(fc.documentRoot)),
path_info: toUnsafeChar(fc.pathInfo),
path_info_len: C.size_t(len(fc.pathInfo)),
php_self: toUnsafeChar(requestPath),
php_self_len: C.size_t(len(requestPath)),
document_uri: toUnsafeChar(fc.docURI),
document_uri_len: C.size_t(len(fc.docURI)),
script_filename: toUnsafeChar(fc.scriptFilename),
script_filename_len: C.size_t(len(fc.scriptFilename)),
script_name: toUnsafeChar(fc.scriptName),
script_name_len: C.size_t(len(fc.scriptName)),
server_name: toUnsafeChar(reqHost),
server_name_len: C.size_t(len(reqHost)),
server_port: toUnsafeChar(serverPort),
server_port_len: C.size_t(len(serverPort)),
content_length: toUnsafeChar(contentLength),
content_length_len: C.size_t(len(contentLength)),
server_protocol: toUnsafeChar(request.Proto),
server_protocol_len: C.size_t(len(request.Proto)),
http_host: toUnsafeChar(request.Host),
http_host_len: C.size_t(len(request.Host)),
request_uri: toUnsafeChar(requestURI),
request_uri_len: C.size_t(len(requestURI)),
ssl_cipher: toUnsafeChar(sslCipher),
ssl_cipher_len: C.size_t(len(sslCipher)),

// CGI vars with known values
request_scheme: rs, // "http" or "https"
ssl_protocol: sslProtocol, // values from tlsProtocol
https: https, // "on" or empty
})
}

func addHeadersToServer(ctx context.Context, request *http.Request, trackVarsArray *C.zval) {
for field, val := range request.Header {
if k := mainThread.commonHeaders[field]; k != nil {
if k := commonHeaders[field]; k != nil {
v := strings.Join(val, ", ")
C.frankenphp_register_single(k, toUnsafeChar(v), C.size_t(len(v)), trackVarsArray)
C.frankenphp_register_known_variable(k, toUnsafeChar(v), C.size_t(len(v)), trackVarsArray)
continue
}

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

//export go_register_variables
func go_register_variables(threadIndex C.uintptr_t, trackVarsArray *C.zval) {
//export go_register_server_variables
func go_register_server_variables(threadIndex C.uintptr_t, trackVarsArray *C.zval) {
thread := phpThreads[threadIndex]
fc := thread.frankenPHPContext()

Expand Down Expand Up @@ -410,8 +365,32 @@ func ensureLeadingSlash(path string) string {
return "/" + path
}

// toUnsafeChar returns a *C.char pointing at the backing bytes the Go string.
// If C does not store the string, it may be passed directly in a Cgo call (most efficient).
// If C stores the string, it must be pinned explicitly instead (inefficient).
// C may never modify the string.
func toUnsafeChar(s string) *C.char {
sData := unsafe.StringData(s)
return (*C.char)(unsafe.Pointer(unsafe.StringData(s)))
}

// initialize a global zend_string that must never be freed and is ignored by GC
func newPersistentZendString(str string) *C.zend_string {
return C.frankenphp_init_persistent_string(toUnsafeChar(str), C.size_t(len(str)))
}

return (*C.char)(unsafe.Pointer(sData))
// Protocol versions, in Apache mod_ssl format: https://httpd.apache.org/docs/current/mod/mod_ssl.html
// Note that these are slightly different from SupportedProtocols in caddytls/config.go
func tlsProtocol(proto uint16) *C.zend_string {
switch proto {
case tls.VersionTLS10:
return C.frankenphp_strings.tls1
case tls.VersionTLS11:
return C.frankenphp_strings.tls11
case tls.VersionTLS12:
return C.frankenphp_strings.tls12
case tls.VersionTLS13:
return C.frankenphp_strings.tls13
default:
return C.frankenphp_strings.empty
}
}
13 changes: 8 additions & 5 deletions env.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package frankenphp

// #cgo nocallback frankenphp_init_persistent_string
// #cgo noescape frankenphp_init_persistent_string
// #include "frankenphp.h"
// #include "types.h"
import "C"
Expand All @@ -10,12 +8,17 @@ import (
"strings"
)

var lengthOfEnv = 0

//export go_init_os_env
func go_init_os_env(mainThreadEnv *C.zend_array) {
for _, envVar := range os.Environ() {
fullEnv := os.Environ()
lengthOfEnv = len(fullEnv)

for _, envVar := range fullEnv {
key, val, _ := strings.Cut(envVar, "=")
zkey := C.frankenphp_init_persistent_string(toUnsafeChar(key), C.size_t(len(key)))
zStr := C.frankenphp_init_persistent_string(toUnsafeChar(val), C.size_t(len(val)))
zkey := newPersistentZendString(key)
zStr := newPersistentZendString(val)
C.__hash_update_string__(mainThreadEnv, zkey, zStr)
}
}
Expand Down
Loading
Loading