Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 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
136 changes: 125 additions & 11 deletions frankenphp.c
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,75 @@ 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) {
struct go_getfullenv_return full_env = go_getfullenv(thread_index);

array_init(return_value);
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(return_value, ZSTR_VAL(key_str), val_str);

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

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 +329,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 +475,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 @@ -472,7 +563,28 @@ int frankenphp_update_server_context(
return SUCCESS;
}

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can probably cache the environment somewhere here as a thread local (or even global) zend_array, so it will only get imported once. I'll check later this week again, but I think that's how FPM also behaves with since putenv doesn't alter $_ENV or $_SERVER

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably do it on the go side and not mix source of truths, but yeah.

Copy link
Contributor

@AlliBalliBaba AlliBalliBaba Oct 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah even in the manual someone mentions how putenv doesn't affect $_ENV. Since $_ENV is unchanging for the duration of the process I think it would be most efficient to directly store it as a zend_array at the start of the process. I can also do that in a follow-up PR if you want.

putenv and getenv should always access the real environment though like in your implementation 👍

Copy link
Member Author

@withinboredom withinboredom Oct 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is working as it is supposed to. This only gets called in the following scenarios:

  1. on each request in cgi-mode when $_SERVER is filled
  2. at worker startup in worker mode to fill $_SERVER
  3. getenv() without any parameters
  4. jit-ENV when $_ENV is first accessed

There shouldn't be any reason to cache this.


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);
}
}

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 +774,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 +791,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()
}