diff --git a/caddy/app.go b/caddy/app.go index e9c31c9fe..cd32859c9 100644 --- a/caddy/app.go +++ b/caddy/app.go @@ -158,6 +158,7 @@ func (f *FrankenPHPApp) Start() error { frankenphp.WithWorkerWatchMode(w.Watch), frankenphp.WithWorkerMaxFailures(w.MaxConsecutiveFailures), frankenphp.WithWorkerMaxThreads(w.MaxThreads), + frankenphp.WithWorkerHTTPDisabled(w.DisableHTTP), ) } else { workerOpts = append( @@ -167,6 +168,7 @@ func (f *FrankenPHPApp) Start() error { frankenphp.WithWorkerMaxFailures(w.MaxConsecutiveFailures), frankenphp.WithWorkerMaxThreads(w.MaxThreads), frankenphp.WithWorkerRequestOptions(w.requestOptions...), + frankenphp.WithWorkerHTTPDisabled(w.DisableHTTP), ) } diff --git a/caddy/workerconfig.go b/caddy/workerconfig.go index baf9ad86b..0a35145d8 100644 --- a/caddy/workerconfig.go +++ b/caddy/workerconfig.go @@ -38,6 +38,8 @@ type workerConfig struct { MatchPath []string `json:"match_path,omitempty"` // MaxConsecutiveFailures sets the maximum number of consecutive failures before panicking (defaults to 6, set to -1 to never panick) MaxConsecutiveFailures int `json:"max_consecutive_failures,omitempty"` + // DisableHTTP specifies if the worker handles HTTP requests + DisableHTTP bool `json:"http_disabled,omitempty"` requestOptions []frankenphp.RequestOption } @@ -116,6 +118,11 @@ func parseWorkerConfig(d *caddyfile.Dispenser) (workerConfig, error) { } else { wc.Watch = append(wc.Watch, d.Val()) } + case "http_disabled": + if d.NextArg() { + return wc, d.ArgErr() + } + wc.DisableHTTP = true case "match": // provision the path so it's identical to Caddy match rules // see: https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/matchers.go @@ -140,7 +147,7 @@ func parseWorkerConfig(d *caddyfile.Dispenser) (workerConfig, error) { wc.MaxConsecutiveFailures = v default: - return wc, wrongSubDirectiveError("worker", "name, file, num, env, watch, match, max_consecutive_failures, max_threads", v) + return wc, wrongSubDirectiveError("worker", "name, file, num, env, watch, match, max_consecutive_failures, max_threads, http_disabled", v) } } diff --git a/frankenphp.c b/frankenphp.c index 04782a9b6..9fd4c12d2 100644 --- a/frankenphp.c +++ b/frankenphp.c @@ -72,10 +72,12 @@ frankenphp_config frankenphp_get_config() { bool should_filter_var = 0; __thread uintptr_t thread_index; __thread bool is_worker_thread = false; +__thread bool is_http_thread = true; __thread zval *os_environment = NULL; -void frankenphp_update_local_thread_context(bool is_worker) { +void frankenphp_update_local_thread_context(bool is_worker, bool httpEnabled) { is_worker_thread = is_worker; + is_http_thread = httpEnabled; } static void frankenphp_update_request_context() { @@ -168,6 +170,9 @@ static void frankenphp_release_temporary_streams() { /* Adapted from php_request_shutdown */ static void frankenphp_worker_request_shutdown() { + if (!is_http_thread) { + return; + } /* Flush all output buffers */ zend_try { php_output_end_all(); } zend_end_try(); @@ -212,6 +217,9 @@ PHPAPI void get_full_env(zval *track_vars_array) { /* Adapted from php_request_startup() */ static int frankenphp_worker_request_startup() { int retval = SUCCESS; + if (!is_http_thread) { + return retval; + } frankenphp_update_request_context(); @@ -486,6 +494,25 @@ PHP_FUNCTION(frankenphp_handle_request) { RETURN_TRUE; } +PHP_FUNCTION(frankenphp_send_request) { + zval *zv; + char *worker_name = NULL; + size_t worker_name_len = 0; + + ZEND_PARSE_PARAMETERS_START(1, 2); + Z_PARAM_ZVAL(zv); + Z_PARAM_OPTIONAL + Z_PARAM_STRING(worker_name, worker_name_len); + ZEND_PARSE_PARAMETERS_END(); + + char *error = go_frankenphp_send_request(thread_index, zv, worker_name, + worker_name_len); + if (error) { + zend_throw_exception(spl_ce_RuntimeException, error, 0); + RETURN_THROWS(); + } +} + PHP_FUNCTION(headers_send) { zend_long response_code = 200; diff --git a/frankenphp.go b/frankenphp.go index d71f25a0b..8717ef667 100644 --- a/frankenphp.go +++ b/frankenphp.go @@ -48,6 +48,7 @@ var ( ErrMainThreadCreation = errors.New("error creating the main thread") ErrScriptExecution = errors.New("error during PHP script execution") ErrNotRunning = errors.New("FrankenPHP is not running. For proper configuration visit: https://frankenphp.dev/docs/config/#caddyfile-config") + ErrNotHTTPWorker = errors.New("worker is not an HTTP worker") ErrInvalidRequestPath = ErrRejected{"invalid request path", http.StatusBadRequest} ErrInvalidContentLengthHeader = ErrRejected{"invalid Content-Length header", http.StatusBadRequest} @@ -399,6 +400,9 @@ func ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) error // Detect if a worker is available to handle this request if fc.worker != nil { + if !fc.worker.httpEnabled { + return ErrNotHTTPWorker + } return fc.worker.handleRequest(ch) } diff --git a/frankenphp.h b/frankenphp.h index efbd5fc48..b0e5f8c85 100644 --- a/frankenphp.h +++ b/frankenphp.h @@ -45,7 +45,7 @@ bool frankenphp_new_php_thread(uintptr_t thread_index); bool frankenphp_shutdown_dummy_request(void); int frankenphp_execute_script(char *file_name); -void frankenphp_update_local_thread_context(bool is_worker); +void frankenphp_update_local_thread_context(bool is_worker, bool httpEnabled); int frankenphp_execute_script_cli(char *script, int argc, char **argv, bool eval); diff --git a/frankenphp.stub.php b/frankenphp.stub.php index 60ac5d588..c790c30a6 100644 --- a/frankenphp.stub.php +++ b/frankenphp.stub.php @@ -4,6 +4,8 @@ function frankenphp_handle_request(callable $callback): bool {} +function frankenphp_send_request(mixed $message, string $workerName = ""): bool {} + function headers_send(int $status = 200): int {} function frankenphp_finish_request(): bool {} diff --git a/frankenphp_arginfo.h b/frankenphp_arginfo.h index 558c6e3cf..22a7d9788 100644 --- a/frankenphp_arginfo.h +++ b/frankenphp_arginfo.h @@ -5,6 +5,11 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_handle_request, 0, 1, ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_send_request, 0, 1, IS_VOID, 0) + ZEND_ARG_TYPE_INFO(0, message, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, worker_name, IS_STRING, 0, "\"\"") +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_headers_send, 0, 0, IS_LONG, 0) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, status, IS_LONG, 0, "200") ZEND_END_ARG_INFO() @@ -37,6 +42,7 @@ ZEND_END_ARG_INFO() ZEND_FUNCTION(frankenphp_handle_request); +ZEND_FUNCTION(frankenphp_send_request); ZEND_FUNCTION(headers_send); ZEND_FUNCTION(frankenphp_finish_request); ZEND_FUNCTION(frankenphp_request_headers); @@ -46,6 +52,7 @@ ZEND_FUNCTION(mercure_publish); static const zend_function_entry ext_functions[] = { ZEND_FE(frankenphp_handle_request, arginfo_frankenphp_handle_request) + ZEND_FE(frankenphp_send_request, arginfo_frankenphp_send_request) ZEND_FE(headers_send, arginfo_headers_send) ZEND_FE(frankenphp_finish_request, arginfo_frankenphp_finish_request) ZEND_FALIAS(fastcgi_finish_request, frankenphp_finish_request, arginfo_fastcgi_finish_request) diff --git a/frankenphp_test.go b/frankenphp_test.go index 427b731f1..e05c80060 100644 --- a/frankenphp_test.go +++ b/frankenphp_test.go @@ -749,6 +749,29 @@ func TestExecuteCLICode(t *testing.T) { assert.Equal(t, stdoutStderrStr, `Hello World`) } +func TestFrankenSendRequest(t *testing.T) { + var buf bytes.Buffer + handler := slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug}) + logger := slog.New(handler) + cwd, _ := os.Getwd() + workerFile := cwd + "/testdata/request-receiver.php" + + runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, _ int) { + body, _ := testGet("http://example.com/request-sender.php?message=hi-from-go", handler, t) + assert.Equal(t, "request sent", body) + }, &testOptions{ + logger: logger, + initOpts: []frankenphp.Option{frankenphp.WithWorkers( + "workerName", + workerFile, + 1, + frankenphp.WithWorkerHTTPDisabled(true), + )}, + }) + + assert.Contains(t, buf.String(), "hi-from-go") +} + func ExampleServeHTTP() { if err := frankenphp.Init(); err != nil { panic(err) diff --git a/options.go b/options.go index b9751ad8b..58ca6dbf3 100644 --- a/options.go +++ b/options.go @@ -44,6 +44,7 @@ type workerOpt struct { onThreadShutdown func(int) onServerStartup func() onServerShutdown func() + httpDisabled bool } // WithContext sets the main context to use. @@ -234,6 +235,15 @@ func WithWorkerOnServerShutdown(f func()) WorkerOption { } } +// AsHTTPWorker determines if the worker will handle HTTP requests (true by default). +func WithWorkerHTTPDisabled(isDisabled bool) WorkerOption { + return func(w *workerOpt) error { + w.httpDisabled = isDisabled + + return nil + } +} + func withExtensionWorkers(w *extensionWorkers) WorkerOption { return func(wo *workerOpt) error { wo.extensionWorkers = w diff --git a/phpthread.go b/phpthread.go index 1726cf9d1..8377bf7a4 100644 --- a/phpthread.go +++ b/phpthread.go @@ -143,8 +143,8 @@ func (thread *phpThread) pinCString(s string) *C.char { return thread.pinString(s + "\x00") } -func (*phpThread) updateContext(isWorker bool) { - C.frankenphp_update_local_thread_context(C.bool(isWorker)) +func (*phpThread) updateContext(isWorker bool, httpEnabled bool) { + C.frankenphp_update_local_thread_context(C.bool(isWorker), C.bool(httpEnabled)) } //export go_frankenphp_before_script_execution diff --git a/testdata/request-receiver.php b/testdata/request-receiver.php new file mode 100644 index 000000000..cca381da9 --- /dev/null +++ b/testdata/request-receiver.php @@ -0,0 +1,7 @@ +