Skip to content

Commit

Permalink
implement getenv and putenv in go (#1086)
Browse files Browse the repository at this point in the history
* implement getenv and putenv in go

* fix typo

* apply formatting

* return a bool

* prevent ENV= from crashing

* optimization

* optimization

* split env workflows and use go_strings

* clean up unused code

* update tests

* remove useless sprintf

* see if this fixes the asan issues

* clean up comments

* check that VAR= works correctly and use actual php to validate the behavior

* move all unpinning to the end of the request

* handle the case where php is not installed

* fix copy-paste

* optimization

* use strings.cut

* fix lint

* override how env is filled

* reuse fullenv

* use corect function
  • Loading branch information
withinboredom authored Oct 18, 2024
1 parent 5ec0308 commit e812473
Show file tree
Hide file tree
Showing 5 changed files with 260 additions and 18 deletions.
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 @@ -507,6 +507,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 @@ -524,6 +594,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 @@ -647,8 +718,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 @@ -691,11 +760,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()
}

0 comments on commit e812473

Please sign in to comment.