Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
121 changes: 110 additions & 11 deletions frankenphp.c
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,25 @@ static void frankenphp_worker_request_shutdown() {
SG(rfc1867_uploaded_files) = NULL;
}

PHPAPI void get_full_env(zval *track_vars_array) {
struct go_getfullenv_return full_env = go_getfullenv(thread_index);

for (int i = 0; i < full_env.r1; i++) {
go_string key = full_env.r0[i * 2];
go_string val = full_env.r0[i * 2 + 1];

// create PHP strings for key and value
zend_string *key_str = zend_string_init(key.data, key.len, 0);
zend_string *val_str = zend_string_init(val.data, val.len, 0);

// add to the associative array
add_assoc_str(track_vars_array, ZSTR_VAL(key_str), val_str);

// release the key string
zend_string_release(key_str);
}
}

/* Adapted from php_request_startup() */
static int frankenphp_worker_request_startup() {
int retval = SUCCESS;
Expand Down Expand Up @@ -242,6 +261,60 @@ PHP_FUNCTION(frankenphp_finish_request) { /* {{{ */
RETURN_TRUE;
} /* }}} */

/* {{{ Call go's putenv to prevent race conditions */
PHP_FUNCTION(frankenphp_putenv) {
char *setting;
size_t setting_len;

ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_STRING(setting, setting_len)
ZEND_PARSE_PARAMETERS_END();

// Cast str_len to int (ensure it fits in an int)
if (setting_len > INT_MAX) {
php_error(E_WARNING, "String length exceeds maximum integer value");
RETURN_FALSE;
}

if (go_putenv(setting, (int)setting_len)) {
RETURN_TRUE;
} else {
RETURN_FALSE;
}
} /* }}} */

/* {{{ Call go's getenv to prevent race conditions */
PHP_FUNCTION(frankenphp_getenv) {
char *name = NULL;
size_t name_len = 0;
bool local_only = 0;

ZEND_PARSE_PARAMETERS_START(0, 2)
Z_PARAM_OPTIONAL
Z_PARAM_STRING_OR_NULL(name, name_len)
Z_PARAM_BOOL(local_only)
ZEND_PARSE_PARAMETERS_END();

if (!name) {
array_init(return_value);
get_full_env(return_value);

return;
}

go_string gname = {name_len, name};

struct go_getenv_return result = go_getenv(thread_index, &gname);

if (result.r0) {
// Return the single environment variable as a string
RETVAL_STRINGL(result.r1->data, result.r1->len);
} else {
// Environment variable does not exist
RETVAL_FALSE;
}
} /* }}} */

/* {{{ Fetch all HTTP request headers */
PHP_FUNCTION(frankenphp_request_headers) {
if (zend_parse_parameters_none() == FAILURE) {
Expand All @@ -260,8 +333,6 @@ PHP_FUNCTION(frankenphp_request_headers) {

add_assoc_stringl_ex(return_value, key.data, key.len, val.data, val.len);
}

go_apache_request_cleanup(thread_index);
}
/* }}} */

Expand Down Expand Up @@ -408,15 +479,39 @@ PHP_FUNCTION(headers_send) {
RETURN_LONG(sapi_send_headers());
}

PHP_MINIT_FUNCTION(frankenphp) {
zend_function *func;

// Override putenv
func = zend_hash_str_find_ptr(CG(function_table), "putenv",
sizeof("putenv") - 1);
if (func != NULL && func->type == ZEND_INTERNAL_FUNCTION) {
((zend_internal_function *)func)->handler = ZEND_FN(frankenphp_putenv);
} else {
php_error(E_WARNING, "Failed to find built-in putenv function");
}

// Override getenv
func = zend_hash_str_find_ptr(CG(function_table), "getenv",
sizeof("getenv") - 1);
if (func != NULL && func->type == ZEND_INTERNAL_FUNCTION) {
((zend_internal_function *)func)->handler = ZEND_FN(frankenphp_getenv);
} else {
php_error(E_WARNING, "Failed to find built-in getenv function");
}

return SUCCESS;
}

static zend_module_entry frankenphp_module = {
STANDARD_MODULE_HEADER,
"frankenphp",
ext_functions, /* function table */
NULL, /* initialization */
NULL, /* shutdown */
NULL, /* request initialization */
NULL, /* request shutdown */
NULL, /* information */
ext_functions, /* function table */
PHP_MINIT(frankenphp), /* initialization */
NULL, /* shutdown */
NULL, /* request initialization */
NULL, /* request shutdown */
NULL, /* information */
TOSTRING(FRANKENPHP_VERSION),
STANDARD_MODULE_PROPERTIES};

Expand Down Expand Up @@ -473,6 +568,8 @@ int frankenphp_update_server_context(
}

static int frankenphp_startup(sapi_module_struct *sapi_module) {
php_import_environment_variables = get_full_env;

return php_module_startup(sapi_module, &frankenphp_module);
}

Expand Down Expand Up @@ -662,14 +759,15 @@ static void frankenphp_register_variables(zval *track_vars_array) {
/* https://www.php.net/manual/en/reserved.variables.server.php */

/* In CGI mode, we consider the environment to be a part of the server
* variables
* variables.
*/

frankenphp_server_context *ctx = SG(server_context);

/* in non-worker mode we import the os environment regularly */
if (!ctx->has_main_request) {
php_import_environment_variables(track_vars_array);
get_full_env(track_vars_array);
// php_import_environment_variables(track_vars_array);
go_register_variables(thread_index, track_vars_array);
return;
}
Expand All @@ -678,7 +776,8 @@ static void frankenphp_register_variables(zval *track_vars_array) {
if (os_environment == NULL) {
os_environment = malloc(sizeof(zval));
array_init(os_environment);
php_import_environment_variables(os_environment);
get_full_env(os_environment);
// php_import_environment_variables(os_environment);
}
zend_hash_copy(Z_ARR_P(track_vars_array), Z_ARR_P(os_environment),
(copy_ctor_func_t)zval_add_ref);
Expand Down
78 changes: 71 additions & 7 deletions frankenphp.go
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,76 @@ func ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) error
return nil
}

//export go_putenv
func go_putenv(str *C.char, length C.int) C.bool {
// Create a byte slice from C string with a specified length
s := C.GoBytes(unsafe.Pointer(str), length)

// Convert byte slice to string
envString := string(s)

// Check if '=' is present in the string
if key, val, found := strings.Cut(envString, "="); found {
if os.Setenv(key, val) != nil {
return false // Failure
}
} else {
// No '=', unset the environment variable
if os.Unsetenv(envString) != nil {
return false // Failure
}
}

return true // Success
}

//export go_getfullenv
func go_getfullenv(threadIndex C.uintptr_t) (*C.go_string, C.size_t) {
thread := phpThreads[threadIndex]

env := os.Environ()
goStrings := make([]C.go_string, len(env)*2)

for i, envVar := range env {
key, val, _ := strings.Cut(envVar, "=")
k := unsafe.StringData(key)
v := unsafe.StringData(val)
thread.Pin(k)
thread.Pin(v)

goStrings[i*2] = C.go_string{C.size_t(len(key)), (*C.char)(unsafe.Pointer(k))}
goStrings[i*2+1] = C.go_string{C.size_t(len(val)), (*C.char)(unsafe.Pointer(v))}
}

value := unsafe.SliceData(goStrings)
thread.Pin(value)

return value, C.size_t(len(env))
}

//export go_getenv
func go_getenv(threadIndex C.uintptr_t, name *C.go_string) (C.bool, *C.go_string) {
thread := phpThreads[threadIndex]

// Create a byte slice from C string with a specified length
envName := C.GoStringN(name.data, C.int(name.len))

// Get the environment variable value
envValue, exists := os.LookupEnv(envName)
if !exists {
// Environment variable does not exist
return false, nil // Return 0 to indicate failure
}

// Convert Go string to C string
val := unsafe.StringData(envValue)
thread.Pin(val)
value := &C.go_string{C.size_t(len(envValue)), (*C.char)(unsafe.Pointer(val))}
thread.Pin(value)

return true, value // Return 1 to indicate success
}

//export go_handle_request
func go_handle_request(threadIndex C.uintptr_t) bool {
select {
Expand All @@ -518,6 +588,7 @@ func go_handle_request(threadIndex C.uintptr_t) bool {
defer func() {
maybeCloseContext(fc)
thread.mainRequest = nil
thread.Unpin()
}()

if err := updateServerContext(r, true, false); err != nil {
Expand Down Expand Up @@ -641,8 +712,6 @@ func go_register_variables(threadIndex C.uintptr_t, trackVarsArray *C.zval) {

C.frankenphp_register_bulk_variables(&knownVariables[0], dvsd, C.size_t(l), trackVarsArray)

thread.Unpin()

fc.env = nil
}

Expand Down Expand Up @@ -685,11 +754,6 @@ func go_apache_request_headers(threadIndex C.uintptr_t, hasActiveRequest bool) (
return sd, C.size_t(len(r.Header))
}

//export go_apache_request_cleanup
func go_apache_request_cleanup(threadIndex C.uintptr_t) {
phpThreads[threadIndex].Unpin()
}

func addHeader(fc *FrankenPHPContext, cString *C.char, length C.int) {
parts := strings.SplitN(C.GoStringN(cString, length), ": ", 2)
if len(parts) != 2 {
Expand Down
27 changes: 27 additions & 0 deletions frankenphp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,33 @@ func TestFailingWorker(t *testing.T) {
}, &testOptions{workerScript: "failing-worker.php"})
}

func TestEnv(t *testing.T) {
testEnv(t, &testOptions{})
}
func TestEnvWorker(t *testing.T) {
testEnv(t, &testOptions{workerScript: "test-env.php"})
}
func testEnv(t *testing.T, opts *testOptions) {
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/test-env.php?var=%d", i), nil)
w := httptest.NewRecorder()
handler(w, req)

resp := w.Result()
body, _ := io.ReadAll(resp.Body)

// execute the script as regular php script
cmd := exec.Command("php", "testdata/test-env.php", strconv.Itoa(i))
stdoutStderr, err := cmd.CombinedOutput()
if err != nil {
// php is not installed or other issue, use the hardcoded output below:
stdoutStderr = []byte("Set MY_VAR successfully.\nMY_VAR = HelloWorld\nUnset MY_VAR successfully.\nMY_VAR is unset.\nMY_VAR set to empty successfully.\nMY_VAR = \nUnset NON_EXISTING_VAR successfully.\n")
}

assert.Equal(t, string(stdoutStderr), string(body))
}, opts)
}

func TestFileUpload_module(t *testing.T) { testFileUpload(t, &testOptions{}) }
func TestFileUpload_worker(t *testing.T) {
testFileUpload(t, &testOptions{workerScript: "file-upload.php"})
Expand Down
50 changes: 50 additions & 0 deletions testdata/test-env.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

require_once __DIR__.'/_executor.php';

return function() {
$var = 'MY_VAR_' . ($_GET['var'] ?? '');
// Setting an environment variable
$result = putenv("$var=HelloWorld");
if ($result) {
echo "Set MY_VAR successfully.\n";
echo "MY_VAR = " . getenv($var) . "\n";
} else {
echo "Failed to set MY_VAR.\n";
}

// Unsetting the environment variable
$result = putenv($var);
if ($result) {
echo "Unset MY_VAR successfully.\n";
$value = getenv($var);
if ($value === false) {
echo "MY_VAR is unset.\n";
} else {
echo "MY_VAR = " . $value . "\n";
}
} else {
echo "Failed to unset MY_VAR.\n";
}

$result = putenv("$var=");
if ($result) {
echo "MY_VAR set to empty successfully.\n";
$value = getenv($var);
if ($value === false) {
echo "MY_VAR is unset.\n";
} else {
echo "MY_VAR = " . $value . "\n";
}
} else {
echo "Failed to set MY_VAR.\n";
}

// Attempt to unset a non-existing variable
$result = putenv('NON_EXISTING_VAR' . ($_GET['var'] ?? ''));
if ($result) {
echo "Unset NON_EXISTING_VAR successfully.\n";
} else {
echo "Failed to unset NON_EXISTING_VAR.\n";
}
};
2 changes: 2 additions & 0 deletions worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -310,4 +310,6 @@ func go_frankenphp_finish_request(threadIndex C.uintptr_t, isWorkerRequest bool)

c.Write(fields...)
}

thread.Unpin()
}