Skip to content

Commit c10e85b

Browse files
refactor: cleanup context (#1816)
* Removes NewRequestWithContext. * Moves cgi logic to cgi.go * Calls 'update_request_info' from the C side. * Calls 'update_request_info' from the C side. * clang-format * Removes unnecessary export. * Adds TODO. * Adds TODO. * Removes 'is_worker_thread' * Shortens return statement. * Removes the context refactor. * adjusts comment. * Skips parsing cgi path variables on explicitly assigned worker. * suggesions by @dunglas. * Re-introduces 'is_worker_thread'. * More formatting.
1 parent 4dd6b5e commit c10e85b

7 files changed

Lines changed: 115 additions & 165 deletions

File tree

cgi.go

Lines changed: 80 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@ import (
2121
"github.com/dunglas/frankenphp/internal/phpheaders"
2222
)
2323

24+
// Protocol versions, in Apache mod_ssl format: https://httpd.apache.org/docs/current/mod/mod_ssl.html
25+
// Note that these are slightly different from SupportedProtocols in caddytls/config.go
26+
var tlsProtocolStrings = map[uint16]string{
27+
tls.VersionTLS10: "TLSv1",
28+
tls.VersionTLS11: "TLSv1.1",
29+
tls.VersionTLS12: "TLSv1.2",
30+
tls.VersionTLS13: "TLSv1.3",
31+
}
32+
33+
// Known $_SERVER keys
2434
var knownServerKeys = []string{
2535
"CONTENT_LENGTH",
2636
"DOCUMENT_ROOT",
@@ -211,32 +221,92 @@ func go_register_variables(threadIndex C.uintptr_t, trackVarsArray *C.zval) {
211221
addPreparedEnvToServer(fc, trackVarsArray)
212222
}
213223

224+
// splitCgiPath splits the request path into SCRIPT_NAME, SCRIPT_FILENAME, PATH_INFO, DOCUMENT_URI
225+
func splitCgiPath(fc *frankenPHPContext) {
226+
path := fc.request.URL.Path
227+
splitPath := fc.splitPath
228+
229+
if splitPath == nil {
230+
splitPath = []string{".php"}
231+
}
232+
233+
if splitPos := splitPos(path, splitPath); splitPos > -1 {
234+
fc.docURI = path[:splitPos]
235+
fc.pathInfo = path[splitPos:]
236+
237+
// Strip PATH_INFO from SCRIPT_NAME
238+
fc.scriptName = strings.TrimSuffix(path, fc.pathInfo)
239+
240+
// Ensure the SCRIPT_NAME has a leading slash for compliance with RFC3875
241+
// Info: https://tools.ietf.org/html/rfc3875#section-4.1.13
242+
if fc.scriptName != "" && !strings.HasPrefix(fc.scriptName, "/") {
243+
fc.scriptName = "/" + fc.scriptName
244+
}
245+
}
246+
247+
// TODO: is it possible to delay this and avoid saving everything in the context?
248+
// SCRIPT_FILENAME is the absolute path of SCRIPT_NAME
249+
fc.scriptFilename = sanitizedPathJoin(fc.documentRoot, fc.scriptName)
250+
fc.worker = getWorkerByPath(fc.scriptFilename)
251+
}
252+
214253
// splitPos returns the index where path should
215254
// be split based on SplitPath.
255+
// example: if splitPath is [".php"]
256+
// "/path/to/script.php/some/path": ("/path/to/script.php", "/some/path")
216257
//
217258
// Adapted from https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
218259
// Copyright 2015 Matthew Holt and The Caddy Authors
219-
func splitPos(fc *frankenPHPContext, path string) int {
220-
if len(fc.splitPath) == 0 {
260+
func splitPos(path string, splitPath []string) int {
261+
if len(splitPath) == 0 {
221262
return 0
222263
}
223264

224265
lowerPath := strings.ToLower(path)
225-
for _, split := range fc.splitPath {
266+
for _, split := range splitPath {
226267
if idx := strings.Index(lowerPath, strings.ToLower(split)); idx > -1 {
227268
return idx + len(split)
228269
}
229270
}
230271
return -1
231272
}
232273

233-
// Map of supported protocols to Apache ssl_mod format
234-
// Note that these are slightly different from SupportedProtocols in caddytls/config.go
235-
var tlsProtocolStrings = map[uint16]string{
236-
tls.VersionTLS10: "TLSv1",
237-
tls.VersionTLS11: "TLSv1.1",
238-
tls.VersionTLS12: "TLSv1.2",
239-
tls.VersionTLS13: "TLSv1.3",
274+
// go_update_request_info updates the sapi_request_info struct
275+
// See: https://github.com/php/php-src/blob/345e04b619c3bc11ea17ee02cdecad6ae8ce5891/main/SAPI.h#L72
276+
//
277+
//export go_update_request_info
278+
func go_update_request_info(threadIndex C.uintptr_t, info *C.sapi_request_info) C.bool {
279+
thread := phpThreads[threadIndex]
280+
fc := thread.getRequestContext()
281+
request := fc.request
282+
283+
authUser, authPassword, ok := request.BasicAuth()
284+
if ok {
285+
if authPassword != "" {
286+
info.auth_password = thread.pinCString(authPassword)
287+
}
288+
if authUser != "" {
289+
info.auth_user = thread.pinCString(authUser)
290+
}
291+
}
292+
293+
info.request_method = thread.pinCString(request.Method)
294+
info.query_string = thread.pinCString(request.URL.RawQuery)
295+
info.content_length = C.zend_long(request.ContentLength)
296+
297+
if contentType := request.Header.Get("Content-Type"); contentType != "" {
298+
info.content_type = thread.pinCString(contentType)
299+
}
300+
301+
if fc.pathInfo != "" {
302+
info.path_translated = thread.pinCString(sanitizedPathJoin(fc.documentRoot, fc.pathInfo)) // See: http://www.oreilly.com/openbook/cgi/ch02_04.html
303+
}
304+
305+
info.request_uri = thread.pinCString(request.URL.RequestURI())
306+
307+
info.proto_num = C.int(request.ProtoMajor*1000 + request.ProtoMinor)
308+
309+
return C.bool(fc.worker != nil)
240310
}
241311

242312
// SanitizedPathJoin performs filepath.Join(root, reqPath) that

context.go

Lines changed: 19 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"log/slog"
66
"net/http"
77
"os"
8+
"strconv"
89
"strings"
910
"time"
1011
)
@@ -67,43 +68,21 @@ func NewRequestWithContext(r *http.Request, opts ...RequestOption) (*http.Reques
6768
}
6869
}
6970

70-
if fc.splitPath == nil {
71-
fc.splitPath = []string{".php"}
72-
}
73-
74-
if fc.env == nil {
75-
fc.env = make(map[string]string)
76-
}
77-
78-
if splitPos := splitPos(fc, r.URL.Path); splitPos > -1 {
79-
fc.docURI = r.URL.Path[:splitPos]
80-
fc.pathInfo = r.URL.Path[splitPos:]
81-
82-
// Strip PATH_INFO from SCRIPT_NAME
83-
fc.scriptName = strings.TrimSuffix(r.URL.Path, fc.pathInfo)
84-
85-
// Ensure the SCRIPT_NAME has a leading slash for compliance with RFC3875
86-
// Info: https://tools.ietf.org/html/rfc3875#section-4.1.13
87-
if fc.scriptName != "" && !strings.HasPrefix(fc.scriptName, "/") {
88-
fc.scriptName = "/" + fc.scriptName
89-
}
90-
}
91-
92-
// if a worker is assigned explicitly, use its filename
93-
// determine if the filename belongs to a worker otherwise
71+
// If a worker is already assigned explicitly, use its filename and skip parsing path variables
9472
if fc.worker != nil {
9573
fc.scriptFilename = fc.worker.fileName
9674
} else {
97-
// SCRIPT_FILENAME is the absolute path of SCRIPT_NAME
98-
fc.scriptFilename = sanitizedPathJoin(fc.documentRoot, fc.scriptName)
99-
fc.worker = getWorkerByPath(fc.scriptFilename)
75+
// If no worker was assigned, split the path into the "traditional" CGI path variables.
76+
// This needs to already happen here in case a worker script still matches the path.
77+
splitCgiPath(fc)
10078
}
10179

10280
c := context.WithValue(r.Context(), contextKey, fc)
10381

10482
return r.WithContext(c), nil
10583
}
10684

85+
// newDummyContext creates a fake context from a request path
10786
func newDummyContext(requestPath string, opts ...RequestOption) (*frankenPHPContext, error) {
10887
r, err := http.NewRequest(http.MethodGet, requestPath, nil)
10988
if err != nil {
@@ -132,13 +111,22 @@ func (fc *frankenPHPContext) closeContext() {
132111

133112
// validate checks if the request should be outright rejected
134113
func (fc *frankenPHPContext) validate() bool {
135-
if !strings.Contains(fc.request.URL.Path, "\x00") {
136-
return true
114+
if strings.Contains(fc.request.URL.Path, "\x00") {
115+
fc.rejectBadRequest("Invalid request path")
116+
117+
return false
137118
}
138119

139-
fc.rejectBadRequest("Invalid request path")
120+
contentLengthStr := fc.request.Header.Get("Content-Length")
121+
if contentLengthStr != "" {
122+
if contentLength, err := strconv.Atoi(contentLengthStr); err != nil || contentLength < 0 {
123+
fc.rejectBadRequest("invalid Content-Length header: " + contentLengthStr)
124+
125+
return false
126+
}
127+
}
140128

141-
return false
129+
return true
142130
}
143131

144132
func (fc *frankenPHPContext) clientHasClosed() bool {

frankenphp.c

Lines changed: 15 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,16 @@ __thread uintptr_t thread_index;
7575
__thread bool is_worker_thread = false;
7676
__thread zval *os_environment = NULL;
7777

78+
static void frankenphp_update_request_context() {
79+
/* the server context is stored on the go side, still SG(server_context) needs
80+
* to not be NULL */
81+
SG(server_context) = (void *)1;
82+
/* status It is not reset by zend engine, set it to 200. */
83+
SG(sapi_headers).http_response_code = 200;
84+
85+
is_worker_thread = go_update_request_info(thread_index, &SG(request_info));
86+
}
87+
7888
static void frankenphp_free_request_context() {
7989
if (SG(request_info).cookie_data != NULL) {
8090
free(SG(request_info).cookie_data);
@@ -174,6 +184,8 @@ void frankenphp_add_assoc_str_ex(zval *track_vars_array, char *key,
174184
static int frankenphp_worker_request_startup() {
175185
int retval = SUCCESS;
176186

187+
frankenphp_update_request_context();
188+
177189
zend_try {
178190
frankenphp_destroy_super_globals();
179191
frankenphp_release_temporary_streams();
@@ -507,36 +519,8 @@ static void frankenphp_request_shutdown() {
507519
zval_ptr_dtor_nogc(&PG(http_globals)[TRACK_VARS_ENV]);
508520
array_init(&PG(http_globals)[TRACK_VARS_ENV]);
509521
}
510-
php_request_shutdown((void *)0);
511522
frankenphp_free_request_context();
512-
}
513-
514-
int frankenphp_update_server_context(bool is_worker_request,
515-
516-
const char *request_method,
517-
char *query_string,
518-
zend_long content_length,
519-
char *path_translated, char *request_uri,
520-
const char *content_type, char *auth_user,
521-
char *auth_password, int proto_num) {
522-
523-
SG(server_context) = (void *)1;
524-
is_worker_thread = is_worker_request;
525-
526-
// It is not reset by zend engine, set it to 200.
527-
SG(sapi_headers).http_response_code = 200;
528-
529-
SG(request_info).auth_password = auth_password;
530-
SG(request_info).auth_user = auth_user;
531-
SG(request_info).request_method = request_method;
532-
SG(request_info).query_string = query_string;
533-
SG(request_info).content_type = content_type;
534-
SG(request_info).content_length = content_length;
535-
SG(request_info).path_translated = path_translated;
536-
SG(request_info).request_uri = request_uri;
537-
SG(request_info).proto_num = proto_num;
538-
539-
return SUCCESS;
523+
php_request_shutdown((void *)0);
540524
}
541525

542526
static int frankenphp_startup(sapi_module_struct *sapi_module) {
@@ -974,7 +958,8 @@ bool frankenphp_new_php_thread(uintptr_t thread_index) {
974958
return true;
975959
}
976960

977-
int frankenphp_request_startup() {
961+
static int frankenphp_request_startup() {
962+
frankenphp_update_request_context();
978963
if (php_request_startup() == SUCCESS) {
979964
return SUCCESS;
980965
}
@@ -1014,7 +999,6 @@ int frankenphp_execute_script(char *file_name) {
1014999

10151000
zend_destroy_file_handle(&file_handle);
10161001

1017-
frankenphp_free_request_context();
10181002
frankenphp_request_shutdown();
10191003

10201004
return status;

frankenphp.go

Lines changed: 0 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ package frankenphp
1212
//
1313
// We also set these flags for hardening: https://github.com/docker-library/php/blob/master/8.2/bookworm/zts/Dockerfile#L57-L59
1414

15-
// #cgo nocallback frankenphp_update_server_context
16-
// #cgo noescape frankenphp_update_server_context
1715
// #include <stdlib.h>
1816
// #include <stdint.h>
1917
// #include <php_variables.h>
@@ -32,7 +30,6 @@ import (
3230
"os"
3331
"os/signal"
3432
"runtime"
35-
"strconv"
3633
"strings"
3734
"sync"
3835
"syscall"
@@ -313,66 +310,6 @@ func Shutdown() {
313310
logger.Debug("FrankenPHP shut down")
314311
}
315312

316-
func updateServerContext(thread *phpThread, fc *frankenPHPContext, isWorkerRequest bool) error {
317-
request := fc.request
318-
authUser, authPassword, ok := request.BasicAuth()
319-
var cAuthUser, cAuthPassword *C.char
320-
if ok && authPassword != "" {
321-
cAuthPassword = thread.pinCString(authPassword)
322-
}
323-
if ok && authUser != "" {
324-
cAuthUser = thread.pinCString(authUser)
325-
}
326-
327-
cMethod := thread.pinCString(request.Method)
328-
cQueryString := thread.pinCString(request.URL.RawQuery)
329-
contentLengthStr := request.Header.Get("Content-Length")
330-
contentLength := 0
331-
if contentLengthStr != "" {
332-
var err error
333-
contentLength, err = strconv.Atoi(contentLengthStr)
334-
if err != nil || contentLength < 0 {
335-
return fmt.Errorf("invalid Content-Length header: %w", err)
336-
}
337-
}
338-
339-
contentType := request.Header.Get("Content-Type")
340-
var cContentType *C.char
341-
if contentType != "" {
342-
cContentType = thread.pinCString(contentType)
343-
}
344-
345-
// compliance with the CGI specification requires that
346-
// PATH_TRANSLATED should only exist if PATH_INFO is defined.
347-
// Info: https://www.ietf.org/rfc/rfc3875 Page 14
348-
var cPathTranslated *C.char
349-
if fc.pathInfo != "" {
350-
cPathTranslated = thread.pinCString(sanitizedPathJoin(fc.documentRoot, fc.pathInfo)) // Info: http://www.oreilly.com/openbook/cgi/ch02_04.html
351-
}
352-
353-
cRequestUri := thread.pinCString(request.URL.RequestURI())
354-
355-
ret := C.frankenphp_update_server_context(
356-
C.bool(isWorkerRequest || fc.responseWriter == nil),
357-
358-
cMethod,
359-
cQueryString,
360-
C.zend_long(contentLength),
361-
cPathTranslated,
362-
cRequestUri,
363-
cContentType,
364-
cAuthUser,
365-
cAuthPassword,
366-
C.int(request.ProtoMajor*1000+request.ProtoMinor),
367-
)
368-
369-
if ret > 0 {
370-
return ErrRequestContextCreation
371-
}
372-
373-
return nil
374-
}
375-
376313
// ServeHTTP executes a PHP script according to the given context.
377314
func ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) error {
378315
if !isRunning {

frankenphp.h

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,6 @@ int frankenphp_new_main_thread(int num_threads);
5151
bool frankenphp_new_php_thread(uintptr_t thread_index);
5252

5353
bool frankenphp_shutdown_dummy_request(void);
54-
int frankenphp_update_server_context(bool is_worker_request,
55-
56-
const char *request_method,
57-
char *query_string,
58-
zend_long content_length,
59-
char *path_translated, char *request_uri,
60-
const char *content_type, char *auth_user,
61-
char *auth_password, int proto_num);
62-
int frankenphp_request_startup();
6354
int frankenphp_execute_script(char *file_name);
6455

6556
int frankenphp_execute_script_cli(char *script, int argc, char **argv,

0 commit comments

Comments
 (0)