Просмотр исходного кода

Merge branch 'main' into feat/auto-scale-clock-time

# Conflicts:
#	phpmainthread.go
#	phpthread.go
Alliballibaba 1 месяц назад
Родитель
Сommit
b3fd756838
10 измененных файлов с 293 добавлено и 72 удалено
  1. 1 1
      .github/workflows/docker.yaml
  2. 1 1
      .github/workflows/static.yaml
  3. 13 36
      cgi.go
  4. 41 5
      frankenphp.c
  5. 7 21
      frankenphp.go
  6. 2 0
      frankenphp.h
  7. 58 8
      frankenphp_test.go
  8. 136 0
      internal/phpheaders/phpheaders.go
  9. 17 0
      phpmainthread.go
  10. 17 0
      phpmainthread_test.go

+ 1 - 1
.github/workflows/docker.yaml

@@ -140,7 +140,7 @@ jobs:
           password: ${{ secrets.REGISTRY_PASSWORD }}
       - name: Build
         id: build
-        uses: docker/bake-action@v5
+        uses: docker/bake-action@v6
         with:
           pull: true
           load: ${{ !fromJson(needs.prepare.outputs.push) }}

+ 1 - 1
.github/workflows/static.yaml

@@ -103,7 +103,7 @@ jobs:
           password: ${{ secrets.REGISTRY_PASSWORD }}
       - name: Build
         id: build
-        uses: docker/bake-action@v5
+        uses: docker/bake-action@v6
         with:
           pull: true
           load: ${{ !fromJson(needs.prepare.outputs.push) || matrix.debug || matrix.mimalloc }}

+ 13 - 36
cgi.go

@@ -10,6 +10,8 @@ import (
 	"path/filepath"
 	"strings"
 	"unsafe"
+
+	"github.com/dunglas/frankenphp/internal/phpheaders"
 )
 
 var knownServerKeys = []string{
@@ -47,7 +49,7 @@ var knownServerKeys = []string{
 // TODO: handle this case https://github.com/caddyserver/caddy/issues/3718
 // Inspired by https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
 func addKnownVariablesToServer(thread *phpThread, request *http.Request, fc *FrankenPHPContext, trackVarsArray *C.zval) {
-	keys := getKnownVariableKeys(thread)
+	keys := mainThread.knownServerKeys
 	// Separate remote IP and port; more lenient than net.SplitHostPort
 	var ip, port string
 	if idx := strings.LastIndex(request.RemoteAddr, ":"); idx > -1 {
@@ -158,18 +160,17 @@ func packCgiVariable(key *C.zend_string, value string) C.ht_key_value_pair {
 	return C.ht_key_value_pair{key, toUnsafeChar(value), C.size_t(len(value))}
 }
 
-func addHeadersToServer(request *http.Request, fc *FrankenPHPContext, trackVarsArray *C.zval) {
+func addHeadersToServer(request *http.Request, thread *phpThread, fc *FrankenPHPContext, trackVarsArray *C.zval) {
 	for field, val := range request.Header {
-		k, ok := headerKeyCache.Get(field)
-		if !ok {
-			k = "HTTP_" + headerNameReplacer.Replace(strings.ToUpper(field)) + "\x00"
-			headerKeyCache.SetIfAbsent(field, k)
-		}
-
-		if _, ok := fc.env[k]; ok {
+		if k := mainThread.commonHeaders[field]; k != nil {
+			v := strings.Join(val, ", ")
+			C.frankenphp_register_single(k, toUnsafeChar(v), C.size_t(len(v)), trackVarsArray)
 			continue
 		}
 
+		// if the header name could not be cached, it needs to be registered safely
+		// this is more inefficient but allows additional sanitizing by PHP
+		k := phpheaders.GetUnCommonHeader(field)
 		v := strings.Join(val, ", ")
 		C.frankenphp_register_variable_safe(toUnsafeChar(k), toUnsafeChar(v), C.size_t(len(v)), trackVarsArray)
 	}
@@ -182,18 +183,6 @@ func addPreparedEnvToServer(fc *FrankenPHPContext, trackVarsArray *C.zval) {
 	fc.env = nil
 }
 
-func getKnownVariableKeys(thread *phpThread) map[string]*C.zend_string {
-	if thread.knownVariableKeys != nil {
-		return thread.knownVariableKeys
-	}
-	threadServerKeys := make(map[string]*C.zend_string)
-	for _, k := range knownServerKeys {
-		threadServerKeys[k] = C.frankenphp_init_persistent_string(toUnsafeChar(k), C.size_t(len(k)))
-	}
-	thread.knownVariableKeys = threadServerKeys
-	return threadServerKeys
-}
-
 //export go_register_variables
 func go_register_variables(threadIndex C.uintptr_t, trackVarsArray *C.zval) {
 	thread := phpThreads[threadIndex]
@@ -201,20 +190,10 @@ func go_register_variables(threadIndex C.uintptr_t, trackVarsArray *C.zval) {
 	fc := r.Context().Value(contextKey).(*FrankenPHPContext)
 
 	addKnownVariablesToServer(thread, r, fc, trackVarsArray)
-	addHeadersToServer(r, fc, trackVarsArray)
-	addPreparedEnvToServer(fc, trackVarsArray)
-}
+	addHeadersToServer(r, thread, fc, trackVarsArray)
 
-//export go_frankenphp_release_known_variable_keys
-func go_frankenphp_release_known_variable_keys(threadIndex C.uintptr_t) {
-	thread := phpThreads[threadIndex]
-	if thread.knownVariableKeys == nil {
-		return
-	}
-	for _, v := range thread.knownVariableKeys {
-		C.frankenphp_release_zend_string(v)
-	}
-	thread.knownVariableKeys = nil
+	// The Prepared Environment is registered last and can overwrite any previous values
+	addPreparedEnvToServer(fc, trackVarsArray)
 }
 
 // splitPos returns the index where path should
@@ -245,8 +224,6 @@ var tlsProtocolStrings = map[uint16]string{
 	tls.VersionTLS13: "TLSv1.3",
 }
 
-var headerNameReplacer = strings.NewReplacer(" ", "_", "-", "_")
-
 // SanitizedPathJoin performs filepath.Join(root, reqPath) that
 // is safe against directory traversal attacks. It uses logic
 // similar to that in the Go standard library, specifically

+ 41 - 5
frankenphp.c

@@ -108,6 +108,29 @@ static void frankenphp_destroy_super_globals() {
   zend_end_try();
 }
 
+/*
+ * free php_stream resources that are temporary (php_stream_temp_ops)
+ * streams are globally registered in EG(regular_list), see zend_list.c
+ * this fixes a leak when reading the body of a request
+ */
+static void frankenphp_release_temporary_streams() {
+  zend_resource *val;
+  int stream_type = php_file_le_stream();
+  ZEND_HASH_FOREACH_PTR(&EG(regular_list), val) {
+    /* verify the resource is a stream */
+    if (val->type == stream_type) {
+      php_stream *stream = (php_stream *)val->ptr;
+      if (stream != NULL && stream->ops == &php_stream_temp_ops &&
+          !stream->is_persistent && stream->__exposed == 0 &&
+          GC_REFCOUNT(val) == 1) {
+        zend_list_close(val);
+        zend_list_delete(val);
+      }
+    }
+  }
+  ZEND_HASH_FOREACH_END();
+}
+
 /* Adapted from php_request_shutdown */
 static void frankenphp_worker_request_shutdown() {
   /* Flush all output buffers */
@@ -162,6 +185,7 @@ static int frankenphp_worker_request_startup() {
 
   zend_try {
     frankenphp_destroy_super_globals();
+    frankenphp_release_temporary_streams();
     php_output_activate();
 
     /* initialize global variables */
@@ -659,6 +683,12 @@ void frankenphp_register_trusted_var(zend_string *z_key, char *value,
   }
 }
 
+void frankenphp_register_single(zend_string *z_key, char *value, size_t val_len,
+                                zval *track_vars_array) {
+  HashTable *ht = Z_ARRVAL_P(track_vars_array);
+  frankenphp_register_trusted_var(z_key, value, val_len, ht);
+}
+
 /* Register known $_SERVER variables in bulk to avoid cgo overhead */
 void frankenphp_register_bulk(
     zval *track_vars_array, ht_key_value_pair remote_addr,
@@ -719,10 +749,15 @@ void frankenphp_register_bulk(
                                   request_uri.val_len, ht);
 }
 
-/** Persistent strings are ignored by the PHP GC, we have to release these
- * ourselves **/
+/** Create an immutable zend_string that lasts for the whole process **/
 zend_string *frankenphp_init_persistent_string(const char *string, size_t len) {
-  return zend_string_init(string, len, 1);
+  /* persistent strings will be ignored by the GC at the end of a request */
+  zend_string *z_string = zend_string_init(string, len, 1);
+
+  /* interned strings will not be ref counted by the GC */
+  GC_ADD_FLAGS(z_string, IS_STR_INTERNED);
+
+  return z_string;
 }
 
 void frankenphp_release_zend_string(zend_string *z_string) {
@@ -891,14 +926,15 @@ static void *php_thread(void *arg) {
                                          frankenphp_execute_script(scriptName));
   }
 
-  go_frankenphp_release_known_variable_keys(thread_index);
-
 #ifdef ZTS
   ts_free_thread();
 #endif
 
   go_frankenphp_on_thread_shutdown(thread_index);
 
+  free(local_ctx);
+  local_ctx = NULL;
+
   return NULL;
 }
 

+ 7 - 21
frankenphp.go

@@ -43,7 +43,6 @@ import (
 	"time"
 	"unsafe"
 
-	"github.com/maypok86/otter"
 	"go.uber.org/zap"
 	"go.uber.org/zap/zapcore"
 	// debug on Linux
@@ -525,17 +524,6 @@ func go_ub_write(threadIndex C.uintptr_t, cBuf *C.char, length C.int) (C.size_t,
 	return C.size_t(i), C.bool(clientHasClosed(r))
 }
 
-// There are around 60 common request headers according to https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Request_fields
-// Give some space for custom headers
-var headerKeyCache = func() otter.Cache[string, string] {
-	c, err := otter.MustBuilder[string, string](256).Build()
-	if err != nil {
-		panic(err)
-	}
-
-	return c
-}()
-
 //export go_apache_request_headers
 func go_apache_request_headers(threadIndex C.uintptr_t, hasActiveRequest bool) (*C.go_string, C.size_t) {
 	thread := phpThreads[threadIndex]
@@ -651,19 +639,17 @@ func go_read_post(threadIndex C.uintptr_t, cBuf *C.char, countBytes C.size_t) (r
 
 //export go_read_cookies
 func go_read_cookies(threadIndex C.uintptr_t) *C.char {
-	r := phpThreads[threadIndex].getActiveRequest()
-
-	cookies := r.Cookies()
-	if len(cookies) == 0 {
+	cookies := phpThreads[threadIndex].getActiveRequest().Header.Values("Cookie")
+	cookie := strings.Join(cookies, "; ")
+	if cookie == "" {
 		return nil
 	}
-	cookieStrings := make([]string, len(cookies))
-	for i, cookie := range cookies {
-		cookieStrings[i] = cookie.String()
-	}
+
+	// remove potential null bytes
+	cookie = strings.ReplaceAll(cookie, "\x00", "")
 
 	// freed in frankenphp_free_request_context()
-	return C.CString(strings.Join(cookieStrings, "; "))
+	return C.CString(cookie)
 }
 
 //export go_log

+ 2 - 0
frankenphp.h

@@ -73,6 +73,8 @@ void frankenphp_release_zend_string(zend_string *z_string);
 int frankenphp_reset_opcache(void);
 int frankenphp_get_current_memory_limit();
 
+void frankenphp_register_single(zend_string *z_key, char *value, size_t val_len,
+                                zval *track_vars_array);
 void frankenphp_register_bulk(
     zval *track_vars_array, ht_key_value_pair remote_addr,
     ht_key_value_pair remote_host, ht_key_value_pair remote_port,

+ 58 - 8
frankenphp_test.go

@@ -37,14 +37,14 @@ import (
 )
 
 type testOptions struct {
-	workerScript        string
-	watch               []string
-	nbWorkers           int
-	env                 map[string]string
-	nbParallelRequests  int
-	realServer          bool
-	logger              *zap.Logger
-	initOpts            []frankenphp.Option
+	workerScript       string
+	watch              []string
+	nbWorkers          int
+	env                map[string]string
+	nbParallelRequests int
+	realServer         bool
+	logger             *zap.Logger
+	initOpts           []frankenphp.Option
 }
 
 func runTest(t *testing.T, test func(func(http.ResponseWriter, *http.Request), *httptest.Server, int), opts *testOptions) {
@@ -321,6 +321,31 @@ func testCookies(t *testing.T, opts *testOptions) {
 	}, opts)
 }
 
+func TestMalformedCookie(t *testing.T) {
+	runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
+		req := httptest.NewRequest("GET", "http://example.com/cookies.php", nil)
+		req.Header.Add("Cookie", "foo =bar; ===;;==;  .dot.=val  ;\x00 ; PHPSESSID=1234")
+		// Muliple Cookie header should be joined https://www.rfc-editor.org/rfc/rfc7540#section-8.1.2.5
+		req.Header.Add("Cookie", "secondCookie=test; secondCookie=overwritten")
+		w := httptest.NewRecorder()
+		handler(w, req)
+
+		resp := w.Result()
+		body, _ := io.ReadAll(resp.Body)
+
+		assert.Contains(t, string(body), "'foo_' => 'bar'")
+		assert.Contains(t, string(body), "'_dot_' => 'val  '")
+
+		// PHPSESSID should still be present since we remove the null byte
+		assert.Contains(t, string(body), "'PHPSESSID' => '1234'")
+
+		// The cookie in the second headers should be present
+		// but it should not be overwritten by following values
+		assert.Contains(t, string(body), "'secondCookie' => 'test'")
+
+	}, &testOptions{nbParallelRequests: 1})
+}
+
 func TestSession_module(t *testing.T) { testSession(t, nil) }
 func TestSession_worker(t *testing.T) {
 	testSession(t, &testOptions{workerScript: "session.php"})
@@ -938,6 +963,21 @@ func testRejectInvalidHeaders(t *testing.T, opts *testOptions) {
 	}
 }
 
+// Worker mode will clean up unreferenced streams between requests
+// Make sure referenced streams are not cleaned up
+func TestFileStreamInWorkerMode(t *testing.T) {
+	runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, _ int) {
+		resp1 := fetchBody("GET", "http://example.com/file-stream.php", handler)
+		assert.Equal(t, resp1, "word1")
+
+		resp2 := fetchBody("GET", "http://example.com/file-stream.php", handler)
+		assert.Equal(t, resp2, "word2")
+
+		resp3 := fetchBody("GET", "http://example.com/file-stream.php", handler)
+		assert.Equal(t, resp3, "word3")
+	}, &testOptions{workerScript: "file-stream.php", nbParallelRequests: 1, nbWorkers: 1})
+}
+
 // To run this fuzzing test use: go test -fuzz FuzzRequest
 // TODO: Cover more potential cases
 func FuzzRequest(f *testing.F) {
@@ -978,3 +1018,13 @@ func FuzzRequest(f *testing.F) {
 		}, &testOptions{workerScript: "request-headers.php"})
 	})
 }
+
+func fetchBody(method string, url string, handler func(http.ResponseWriter, *http.Request)) string {
+	req := httptest.NewRequest(method, url, nil)
+	w := httptest.NewRecorder()
+	handler(w, req)
+	resp := w.Result()
+	body, _ := io.ReadAll(resp.Body)
+
+	return string(body)
+}

+ 136 - 0
internal/phpheaders/phpheaders.go

@@ -0,0 +1,136 @@
+package phpheaders
+
+import (
+	"strings"
+
+	"github.com/maypok86/otter"
+)
+
+// Translate header names to PHP header names
+// All headers in 'commonHeaders' can be cached and registered safely
+// All other headers must be sanitized
+// Note: net/http will capitalize lowercase headers, so we don't need to worry about case sensitivity
+var CommonRequestHeaders = map[string]string{
+	"Accept":                            "HTTP_ACCEPT",
+	"Accept-Charset":                    "HTTP_ACCEPT_CHARSET",
+	"Accept-Encoding":                   "HTTP_ACCEPT_ENCODING",
+	"Accept-Language":                   "HTTP_ACCEPT_LANGUAGE",
+	"Access-Control-Request-Headers":    "HTTP_ACCESS_CONTROL_REQUEST_HEADERS",
+	"Access-Control-Request-Method":     "HTTP_ACCESS_CONTROL_REQUEST_METHOD",
+	"Authorization":                     "HTTP_AUTHORIZATION",
+	"Cache-Control":                     "HTTP_CACHE_CONTROL",
+	"Connection":                        "HTTP_CONNECTION",
+	"Content-Disposition":               "HTTP_CONTENT_DISPOSITION",
+	"Content-Encoding":                  "HTTP_CONTENT_ENCODING",
+	"Content-Length":                    "HTTP_CONTENT_LENGTH",
+	"Content-Type":                      "HTTP_CONTENT_TYPE",
+	"Cookie":                            "HTTP_COOKIE",
+	"Date":                              "HTTP_DATE",
+	"Device-Memory":                     "HTTP_DEVICE_MEMORY",
+	"Dnt":                               "HTTP_DNT",
+	"Downlink":                          "HTTP_DOWNLINK",
+	"Dpr":                               "HTTP_DPR",
+	"Early-Data":                        "HTTP_EARLY_DATA",
+	"Ect":                               "HTTP_ECT",
+	"Am-I":                              "HTTP_AM_I",
+	"Expect":                            "HTTP_EXPECT",
+	"Forwarded":                         "HTTP_FORWARDED",
+	"From":                              "HTTP_FROM",
+	"Host":                              "HTTP_HOST",
+	"If-Match":                          "HTTP_IF_MATCH",
+	"If-Modified-Since":                 "HTTP_IF_MODIFIED_SINCE",
+	"If-None-Match":                     "HTTP_IF_NONE_MATCH",
+	"If-Range":                          "HTTP_IF_RANGE",
+	"If-Unmodified-Since":               "HTTP_IF_UNMODIFIED_SINCE",
+	"Keep-Alive":                        "HTTP_KEEP_ALIVE",
+	"Max-Forwards":                      "HTTP_MAX_FORWARDS",
+	"Origin":                            "HTTP_ORIGIN",
+	"Pragma":                            "HTTP_PRAGMA",
+	"Proxy-Authorization":               "HTTP_PROXY_AUTHORIZATION",
+	"Range":                             "HTTP_RANGE",
+	"Referer":                           "HTTP_REFERER",
+	"Rtt":                               "HTTP_RTT",
+	"Save-Data":                         "HTTP_SAVE_DATA",
+	"Sec-Ch-Ua":                         "HTTP_SEC_CH_UA",
+	"Sec-Ch-Ua-Arch":                    "HTTP_SEC_CH_UA_ARCH",
+	"Sec-Ch-Ua-Bitness":                 "HTTP_SEC_CH_UA_BITNESS",
+	"Sec-Ch-Ua-Full-Version":            "HTTP_SEC_CH_UA_FULL_VERSION",
+	"Sec-Ch-Ua-Full-Version-List":       "HTTP_SEC_CH_UA_FULL_VERSION_LIST",
+	"Sec-Ch-Ua-Mobile":                  "HTTP_SEC_CH_UA_MOBILE",
+	"Sec-Ch-Ua-Model":                   "HTTP_SEC_CH_UA_MODEL",
+	"Sec-Ch-Ua-Platform":                "HTTP_SEC_CH_UA_PLATFORM",
+	"Sec-Ch-Ua-Platform-Version":        "HTTP_SEC_CH_UA_PLATFORM_VERSION",
+	"Sec-Fetch-Dest":                    "HTTP_SEC_FETCH_DEST",
+	"Sec-Fetch-Mode":                    "HTTP_SEC_FETCH_MODE",
+	"Sec-Fetch-Site":                    "HTTP_SEC_FETCH_SITE",
+	"Sec-Fetch-User":                    "HTTP_SEC_FETCH_USER",
+	"Sec-Gpc":                           "HTTP_SEC_GPC",
+	"Service-Worker-Navigation-Preload": "HTTP_SERVICE_WORKER_NAVIGATION_PRELOAD",
+	"Te":                                "HTTP_TE",
+	"Priority":                          "HTTP_PRIORITY",
+	"Trailer":                           "HTTP_TRAILER",
+	"Transfer-Encoding":                 "HTTP_TRANSFER_ENCODING",
+	"Upgrade":                           "HTTP_UPGRADE",
+	"Upgrade-Insecure-Requests":         "HTTP_UPGRADE_INSECURE_REQUESTS",
+	"User-Agent":                        "HTTP_USER_AGENT",
+	"Via":                               "HTTP_VIA",
+	"Viewport-Width":                    "HTTP_VIEWPORT_WIDTH",
+	"Want-Digest":                       "HTTP_WANT_DIGEST",
+	"Warning":                           "HTTP_WARNING",
+	"Width":                             "HTTP_WIDTH",
+	"X-Forwarded-For":                   "HTTP_X_FORWARDED_FOR",
+	"X-Forwarded-Host":                  "HTTP_X_FORWARDED_HOST",
+	"X-Forwarded-Proto":                 "HTTP_X_FORWARDED_PROTO",
+	"A-Im":                              "HTTP_A_IM",
+	"Accept-Datetime":                   "HTTP_ACCEPT_DATETIME",
+	"Content-Md5":                       "HTTP_CONTENT_MD5",
+	"Http2-Settings":                    "HTTP_HTTP2_SETTINGS",
+	"Prefer":                            "HTTP_PREFER",
+	"X-Requested-With":                  "HTTP_X_REQUESTED_WITH",
+	"Front-End-Https":                   "HTTP_FRONT_END_HTTPS",
+	"X-Http-Method-Override":            "HTTP_X_HTTP_METHOD_OVERRIDE",
+	"X-Att-Deviceid":                    "HTTP_X_ATT_DEVICEID",
+	"X-Wap-Profile":                     "HTTP_X_WAP_PROFILE",
+	"Proxy-Connection":                  "HTTP_PROXY_CONNECTION",
+	"X-Uidh":                            "HTTP_X_UIDH",
+	"X-Csrf-Token":                      "HTTP_X_CSRF_TOKEN",
+	"X-Request-Id":                      "HTTP_X_REQUEST_ID",
+	"X-Correlation-Id":                  "HTTP_X_CORRELATION_ID",
+	// Additional CDN/Framework headers
+	"Cloudflare-Visitor":        "HTTP_CLOUDFLARE_VISITOR",
+	"Cloudfront-Viewer-Address": "HTTP_CLOUDFRONT_VIEWER_ADDRESS",
+	"Cloudfront-Viewer-Country": "HTTP_CLOUDFRONT_VIEWER_COUNTRY",
+	"X-Amzn-Trace-Id":           "HTTP_X_AMZN_TRACE_ID",
+	"X-Cloud-Trace-Context":     "HTTP_X_CLOUD_TRACE_CONTEXT",
+	"Cf-Ray":                    "HTTP_CF_RAY",
+	"Cf-Visitor":                "HTTP_CF_VISITOR",
+	"Cf-Request-Id":             "HTTP_CF_REQUEST_ID",
+	"Cf-Ipcountry":              "HTTP_CF_IPCOUNTRY",
+	"X-Device-Type":             "HTTP_X_DEVICE_TYPE",
+	"X-Network-Info":            "HTTP_X_NETWORK_INFO",
+	"X-Client-Id":               "HTTP_X_CLIENT_ID",
+	"X-Livewire":                "HTTP_X_LIVEWIRE",
+}
+
+// Cache up to 256 uncommon headers
+// This is ~2.5x faster than converting the header each time
+var headerKeyCache = func() otter.Cache[string, string] {
+	c, err := otter.MustBuilder[string, string](256).Build()
+	if err != nil {
+		panic(err)
+	}
+
+	return c
+}()
+
+var headerNameReplacer = strings.NewReplacer(" ", "_", "-", "_")
+
+func GetUnCommonHeader(key string) string {
+	phpHeaderKey, ok := headerKeyCache.Get(key)
+	if !ok {
+		phpHeaderKey = "HTTP_" + headerNameReplacer.Replace(strings.ToUpper(key)) + "\x00"
+		headerKeyCache.SetIfAbsent(key, phpHeaderKey)
+	}
+
+	return phpHeaderKey
+}

+ 17 - 0
phpmainthread.go

@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"sync"
 
+	"github.com/dunglas/frankenphp/internal/phpheaders"
 	"github.com/dunglas/frankenphp/internal/memory"
 	"go.uber.org/zap"
 )
@@ -18,6 +19,9 @@ type phpMainThread struct {
 	numThreads int
 	maxThreads int
 	phpIni     map[string]string
+	commonHeaders   map[string]*C.zend_string
+	knownServerKeys map[string]*C.zend_string
+
 }
 
 var (
@@ -115,6 +119,19 @@ func (mainThread *phpMainThread) start() error {
 	}
 
 	mainThread.state.waitFor(stateReady)
+
+	// cache common request headers as zend_strings (HTTP_ACCEPT, HTTP_USER_AGENT, etc.)
+	mainThread.commonHeaders = make(map[string]*C.zend_string, len(phpheaders.CommonRequestHeaders))
+	for key, phpKey := range phpheaders.CommonRequestHeaders {
+		mainThread.commonHeaders[key] = C.frankenphp_init_persistent_string(C.CString(phpKey), C.size_t(len(phpKey)))
+	}
+
+	// cache $_SERVER keys as zend_strings (SERVER_PROTOCOL, SERVER_SOFTWARE, etc.)
+	mainThread.knownServerKeys = make(map[string]*C.zend_string, len(knownServerKeys))
+	for _, phpKey := range knownServerKeys {
+		mainThread.knownServerKeys[phpKey] = C.frankenphp_init_persistent_string(toUnsafeChar(phpKey), C.size_t(len(phpKey)))
+	}
+
 	return nil
 }
 

+ 17 - 0
phpmainthread_test.go

@@ -10,6 +10,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/dunglas/frankenphp/internal/phpheaders"
 	"github.com/stretchr/testify/assert"
 	"go.uber.org/zap"
 )
@@ -136,6 +137,22 @@ func TestTransitionThreadsWhileDoingRequests(t *testing.T) {
 	Shutdown()
 }
 
+// Note: this test is here since it would break compilation when put into the phpheaders package
+func TestAllCommonHeadersAreCorrect(t *testing.T) {
+	fakeRequest := httptest.NewRequest("GET", "http://localhost", nil)
+
+	for header, phpHeader := range phpheaders.CommonRequestHeaders {
+		// verify that common and uncommon headers return the same result
+		expectedPHPHeader := phpheaders.GetUnCommonHeader(header)
+		assert.Equal(t, phpHeader+"\x00", expectedPHPHeader, "header is not well formed: "+phpHeader)
+
+		// net/http will capitalize lowercase headers, verify that headers are capitalized
+		fakeRequest.Header.Add(header, "foo")
+		_, ok := fakeRequest.Header[header]
+		assert.True(t, ok, "header is not correctly capitalized: "+header)
+	}
+}
+
 func getDummyWorker(fileName string) *worker {
 	if workers == nil {
 		workers = make(map[string]*worker)

Некоторые файлы не были показаны из-за большого количества измененных файлов