frankenphp_test.go 35 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030
  1. // In all tests, headers added to requests are copied on the heap using strings.Clone.
  2. // This was originally a workaround for https://github.com/golang/go/issues/65286#issuecomment-1920087884 (fixed in Go 1.22),
  3. // but this allows to catch panics occurring in real life but not when the string is in the internal binary memory.
  4. package frankenphp_test
  5. import (
  6. "bytes"
  7. "context"
  8. "errors"
  9. "fmt"
  10. "github.com/dunglas/frankenphp/internal/fastabs"
  11. "io"
  12. "log"
  13. "mime/multipart"
  14. "net/http"
  15. "net/http/cookiejar"
  16. "net/http/httptest"
  17. "net/http/httptrace"
  18. "net/textproto"
  19. "net/url"
  20. "os"
  21. "os/exec"
  22. "path/filepath"
  23. "strconv"
  24. "strings"
  25. "sync"
  26. "testing"
  27. "github.com/dunglas/frankenphp"
  28. "github.com/stretchr/testify/assert"
  29. "github.com/stretchr/testify/require"
  30. "go.uber.org/zap"
  31. "go.uber.org/zap/zapcore"
  32. "go.uber.org/zap/zaptest"
  33. "go.uber.org/zap/zaptest/observer"
  34. )
  35. type testOptions struct {
  36. workerScript string
  37. watch []string
  38. nbWorkers int
  39. env map[string]string
  40. nbParallelRequests int
  41. realServer bool
  42. logger *zap.Logger
  43. initOpts []frankenphp.Option
  44. }
  45. func runTest(t *testing.T, test func(func(http.ResponseWriter, *http.Request), *httptest.Server, int), opts *testOptions) {
  46. if opts == nil {
  47. opts = &testOptions{}
  48. }
  49. if opts.nbParallelRequests == 0 {
  50. opts.nbParallelRequests = 100
  51. }
  52. cwd, _ := os.Getwd()
  53. testDataDir := cwd + "/testdata/"
  54. if opts.logger == nil {
  55. opts.logger = zaptest.NewLogger(t)
  56. }
  57. initOpts := []frankenphp.Option{frankenphp.WithLogger(opts.logger)}
  58. if opts.workerScript != "" {
  59. initOpts = append(initOpts, frankenphp.WithWorkers(testDataDir+opts.workerScript, opts.nbWorkers, opts.env, opts.watch))
  60. }
  61. initOpts = append(initOpts, opts.initOpts...)
  62. err := frankenphp.Init(initOpts...)
  63. require.Nil(t, err)
  64. defer frankenphp.Shutdown()
  65. handler := func(w http.ResponseWriter, r *http.Request) {
  66. req, err := frankenphp.NewRequestWithContext(r, frankenphp.WithRequestDocumentRoot(testDataDir, false))
  67. assert.NoError(t, err)
  68. err = frankenphp.ServeHTTP(w, req)
  69. assert.NoError(t, err)
  70. }
  71. var ts *httptest.Server
  72. if opts.realServer {
  73. ts = httptest.NewServer(http.HandlerFunc(handler))
  74. defer ts.Close()
  75. }
  76. var wg sync.WaitGroup
  77. wg.Add(opts.nbParallelRequests)
  78. for i := 0; i < opts.nbParallelRequests; i++ {
  79. go func(i int) {
  80. test(handler, ts, i)
  81. wg.Done()
  82. }(i)
  83. }
  84. wg.Wait()
  85. }
  86. func TestHelloWorld_module(t *testing.T) { testHelloWorld(t, nil) }
  87. func TestHelloWorld_worker(t *testing.T) {
  88. testHelloWorld(t, &testOptions{workerScript: "index.php"})
  89. }
  90. func testHelloWorld(t *testing.T, opts *testOptions) {
  91. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  92. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/index.php?i=%d", i), nil)
  93. w := httptest.NewRecorder()
  94. handler(w, req)
  95. resp := w.Result()
  96. body, _ := io.ReadAll(resp.Body)
  97. assert.Equal(t, fmt.Sprintf("I am by birth a Genevese (%d)", i), string(body))
  98. }, opts)
  99. }
  100. func TestFinishRequest_module(t *testing.T) { testFinishRequest(t, nil) }
  101. func TestFinishRequest_worker(t *testing.T) {
  102. testFinishRequest(t, &testOptions{workerScript: "finish-request.php"})
  103. }
  104. func testFinishRequest(t *testing.T, opts *testOptions) {
  105. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  106. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/finish-request.php?i=%d", i), nil)
  107. w := httptest.NewRecorder()
  108. handler(w, req)
  109. resp := w.Result()
  110. body, _ := io.ReadAll(resp.Body)
  111. assert.Equal(t, fmt.Sprintf("This is output %d\n", i), string(body))
  112. }, opts)
  113. }
  114. func TestServerVariable_module(t *testing.T) {
  115. testServerVariable(t, nil)
  116. }
  117. func TestServerVariable_worker(t *testing.T) {
  118. testServerVariable(t, &testOptions{workerScript: "server-variable.php"})
  119. }
  120. func testServerVariable(t *testing.T, opts *testOptions) {
  121. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  122. req := httptest.NewRequest("POST", fmt.Sprintf("http://example.com/server-variable.php/baz/bat?foo=a&bar=b&i=%d#hash", i), strings.NewReader("foo"))
  123. req.SetBasicAuth(strings.Clone("kevin"), strings.Clone("password"))
  124. req.Header.Add(strings.Clone("Content-Type"), strings.Clone("text/plain"))
  125. w := httptest.NewRecorder()
  126. handler(w, req)
  127. resp := w.Result()
  128. body, _ := io.ReadAll(resp.Body)
  129. strBody := string(body)
  130. assert.Contains(t, strBody, "[REMOTE_HOST]")
  131. assert.Contains(t, strBody, "[REMOTE_USER] => kevin")
  132. assert.Contains(t, strBody, "[PHP_AUTH_USER] => kevin")
  133. assert.Contains(t, strBody, "[PHP_AUTH_PW] => password")
  134. assert.Contains(t, strBody, "[HTTP_AUTHORIZATION] => Basic a2V2aW46cGFzc3dvcmQ=")
  135. assert.Contains(t, strBody, "[DOCUMENT_ROOT]")
  136. assert.Contains(t, strBody, "[PHP_SELF] => /server-variable.php/baz/bat")
  137. assert.Contains(t, strBody, "[CONTENT_TYPE] => text/plain")
  138. assert.Contains(t, strBody, fmt.Sprintf("[QUERY_STRING] => foo=a&bar=b&i=%d#hash", i))
  139. assert.Contains(t, strBody, fmt.Sprintf("[REQUEST_URI] => /server-variable.php/baz/bat?foo=a&bar=b&i=%d#hash", i))
  140. assert.Contains(t, strBody, "[CONTENT_LENGTH]")
  141. assert.Contains(t, strBody, "[REMOTE_ADDR]")
  142. assert.Contains(t, strBody, "[REMOTE_PORT]")
  143. assert.Contains(t, strBody, "[REQUEST_SCHEME] => http")
  144. assert.Contains(t, strBody, "[DOCUMENT_URI]")
  145. assert.Contains(t, strBody, "[AUTH_TYPE]")
  146. assert.Contains(t, strBody, "[REMOTE_IDENT]")
  147. assert.Contains(t, strBody, "[REQUEST_METHOD] => POST")
  148. assert.Contains(t, strBody, "[SERVER_NAME] => example.com")
  149. assert.Contains(t, strBody, "[SERVER_PROTOCOL] => HTTP/1.1")
  150. assert.Contains(t, strBody, "[SCRIPT_FILENAME]")
  151. assert.Contains(t, strBody, "[SERVER_SOFTWARE] => FrankenPHP")
  152. assert.Contains(t, strBody, "[REQUEST_TIME_FLOAT]")
  153. assert.Contains(t, strBody, "[REQUEST_TIME]")
  154. assert.Contains(t, strBody, "[SERVER_PORT] => 80")
  155. }, opts)
  156. }
  157. func TestPathInfo_module(t *testing.T) { testPathInfo(t, nil) }
  158. func TestPathInfo_worker(t *testing.T) {
  159. testPathInfo(t, &testOptions{workerScript: "server-variable.php"})
  160. }
  161. func testPathInfo(t *testing.T, opts *testOptions) {
  162. cwd, _ := os.Getwd()
  163. testDataDir := cwd + strings.Clone("/testdata/")
  164. path := strings.Clone("/server-variable.php/pathinfo")
  165. runTest(t, func(_ func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  166. handler := func(w http.ResponseWriter, r *http.Request) {
  167. requestURI := r.URL.RequestURI()
  168. r.URL.Path = path
  169. rewriteRequest, err := frankenphp.NewRequestWithContext(r,
  170. frankenphp.WithRequestDocumentRoot(testDataDir, false),
  171. frankenphp.WithRequestEnv(map[string]string{"REQUEST_URI": requestURI}),
  172. )
  173. assert.NoError(t, err)
  174. err = frankenphp.ServeHTTP(w, rewriteRequest)
  175. assert.NoError(t, err)
  176. }
  177. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/pathinfo/%d", i), nil)
  178. w := httptest.NewRecorder()
  179. handler(w, req)
  180. resp := w.Result()
  181. body, _ := io.ReadAll(resp.Body)
  182. strBody := string(body)
  183. assert.Contains(t, strBody, "[PATH_INFO] => /pathinfo")
  184. assert.Contains(t, strBody, fmt.Sprintf("[REQUEST_URI] => /pathinfo/%d", i))
  185. assert.Contains(t, strBody, "[PATH_TRANSLATED] =>")
  186. assert.Contains(t, strBody, "[SCRIPT_NAME] => /server-variable.php")
  187. }, opts)
  188. }
  189. func TestHeaders_module(t *testing.T) { testHeaders(t, nil) }
  190. func TestHeaders_worker(t *testing.T) { testHeaders(t, &testOptions{workerScript: "headers.php"}) }
  191. func testHeaders(t *testing.T, opts *testOptions) {
  192. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  193. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/headers.php?i=%d", i), nil)
  194. w := httptest.NewRecorder()
  195. handler(w, req)
  196. resp := w.Result()
  197. body, _ := io.ReadAll(resp.Body)
  198. assert.Equal(t, "Hello", string(body))
  199. assert.Equal(t, 201, resp.StatusCode)
  200. assert.Equal(t, "bar", resp.Header.Get("Foo"))
  201. assert.Equal(t, "bar2", resp.Header.Get("Foo2"))
  202. assert.Empty(t, resp.Header.Get("Invalid"))
  203. assert.Equal(t, fmt.Sprintf("%d", i), resp.Header.Get("I"))
  204. }, opts)
  205. }
  206. func TestResponseHeaders_module(t *testing.T) { testResponseHeaders(t, nil) }
  207. func TestResponseHeaders_worker(t *testing.T) {
  208. testResponseHeaders(t, &testOptions{workerScript: "response-headers.php"})
  209. }
  210. func testResponseHeaders(t *testing.T, opts *testOptions) {
  211. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  212. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/response-headers.php?i=%d", i), nil)
  213. w := httptest.NewRecorder()
  214. handler(w, req)
  215. resp := w.Result()
  216. body, _ := io.ReadAll(resp.Body)
  217. if i%3 != 0 {
  218. assert.Equal(t, i+100, resp.StatusCode)
  219. } else {
  220. assert.Equal(t, 200, resp.StatusCode)
  221. }
  222. assert.Contains(t, string(body), "'X-Powered-By' => 'PH")
  223. assert.Contains(t, string(body), "'Foo' => 'bar',")
  224. assert.Contains(t, string(body), "'Foo2' => 'bar2',")
  225. assert.Contains(t, string(body), fmt.Sprintf("'I' => '%d',", i))
  226. assert.NotContains(t, string(body), "Invalid")
  227. }, opts)
  228. }
  229. func TestInput_module(t *testing.T) { testInput(t, nil) }
  230. func TestInput_worker(t *testing.T) { testInput(t, &testOptions{workerScript: "input.php"}) }
  231. func testInput(t *testing.T, opts *testOptions) {
  232. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  233. req := httptest.NewRequest("POST", "http://example.com/input.php", strings.NewReader(fmt.Sprintf("post data %d", i)))
  234. w := httptest.NewRecorder()
  235. handler(w, req)
  236. resp := w.Result()
  237. body, _ := io.ReadAll(resp.Body)
  238. assert.Equal(t, fmt.Sprintf("post data %d", i), string(body))
  239. assert.Equal(t, "bar", resp.Header.Get("Foo"))
  240. }, opts)
  241. }
  242. func TestPostSuperGlobals_module(t *testing.T) { testPostSuperGlobals(t, nil) }
  243. func TestPostSuperGlobals_worker(t *testing.T) {
  244. testPostSuperGlobals(t, &testOptions{workerScript: "super-globals.php"})
  245. }
  246. func testPostSuperGlobals(t *testing.T, opts *testOptions) {
  247. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  248. formData := url.Values{"baz": {"bat"}, "i": {fmt.Sprintf("%d", i)}}
  249. req := httptest.NewRequest("POST", fmt.Sprintf("http://example.com/super-globals.php?foo=bar&iG=%d", i), strings.NewReader(formData.Encode()))
  250. req.Header.Set("Content-Type", strings.Clone("application/x-www-form-urlencoded"))
  251. w := httptest.NewRecorder()
  252. handler(w, req)
  253. resp := w.Result()
  254. body, _ := io.ReadAll(resp.Body)
  255. assert.Contains(t, string(body), "'foo' => 'bar'")
  256. assert.Contains(t, string(body), fmt.Sprintf("'i' => '%d'", i))
  257. assert.Contains(t, string(body), "'baz' => 'bat'")
  258. assert.Contains(t, string(body), fmt.Sprintf("'iG' => '%d'", i))
  259. }, opts)
  260. }
  261. func TestCookies_module(t *testing.T) { testCookies(t, nil) }
  262. func TestCookies_worker(t *testing.T) { testCookies(t, &testOptions{workerScript: "cookies.php"}) }
  263. func testCookies(t *testing.T, opts *testOptions) {
  264. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  265. req := httptest.NewRequest("GET", "http://example.com/cookies.php", nil)
  266. req.AddCookie(&http.Cookie{Name: "foo", Value: "bar"})
  267. req.AddCookie(&http.Cookie{Name: "i", Value: fmt.Sprintf("%d", i)})
  268. w := httptest.NewRecorder()
  269. handler(w, req)
  270. resp := w.Result()
  271. body, _ := io.ReadAll(resp.Body)
  272. assert.Contains(t, string(body), "'foo' => 'bar'")
  273. assert.Contains(t, string(body), fmt.Sprintf("'i' => '%d'", i))
  274. }, opts)
  275. }
  276. func TestMalformedCookie(t *testing.T) {
  277. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  278. req := httptest.NewRequest("GET", "http://example.com/cookies.php", nil)
  279. req.Header.Add("Cookie", "foo =bar; ===;;==; .dot.=val ;\x00 ; PHPSESSID=1234")
  280. // Muliple Cookie header should be joined https://www.rfc-editor.org/rfc/rfc7540#section-8.1.2.5
  281. req.Header.Add("Cookie", "secondCookie=test; secondCookie=overwritten")
  282. w := httptest.NewRecorder()
  283. handler(w, req)
  284. resp := w.Result()
  285. body, _ := io.ReadAll(resp.Body)
  286. assert.Contains(t, string(body), "'foo_' => 'bar'")
  287. assert.Contains(t, string(body), "'_dot_' => 'val '")
  288. // PHPSESSID should still be present since we remove the null byte
  289. assert.Contains(t, string(body), "'PHPSESSID' => '1234'")
  290. // The cookie in the second headers should be present
  291. // but it should not be overwritten by following values
  292. assert.Contains(t, string(body), "'secondCookie' => 'test'")
  293. }, &testOptions{nbParallelRequests: 1})
  294. }
  295. func TestSession_module(t *testing.T) { testSession(t, nil) }
  296. func TestSession_worker(t *testing.T) {
  297. testSession(t, &testOptions{workerScript: "session.php"})
  298. }
  299. func testSession(t *testing.T, opts *testOptions) {
  300. if opts == nil {
  301. opts = &testOptions{}
  302. }
  303. opts.realServer = true
  304. runTest(t, func(_ func(http.ResponseWriter, *http.Request), ts *httptest.Server, i int) {
  305. jar, err := cookiejar.New(&cookiejar.Options{})
  306. assert.NoError(t, err)
  307. client := &http.Client{Jar: jar}
  308. resp1, err := client.Get(ts.URL + "/session.php")
  309. assert.NoError(t, err)
  310. body1, _ := io.ReadAll(resp1.Body)
  311. assert.Equal(t, "Count: 0\n", string(body1))
  312. resp2, err := client.Get(ts.URL + "/session.php")
  313. assert.NoError(t, err)
  314. body2, _ := io.ReadAll(resp2.Body)
  315. assert.Equal(t, "Count: 1\n", string(body2))
  316. }, opts)
  317. }
  318. func TestPhpInfo_module(t *testing.T) { testPhpInfo(t, nil) }
  319. func TestPhpInfo_worker(t *testing.T) { testPhpInfo(t, &testOptions{workerScript: "phpinfo.php"}) }
  320. func testPhpInfo(t *testing.T, opts *testOptions) {
  321. var logOnce sync.Once
  322. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  323. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/phpinfo.php?i=%d", i), nil)
  324. w := httptest.NewRecorder()
  325. handler(w, req)
  326. resp := w.Result()
  327. body, _ := io.ReadAll(resp.Body)
  328. logOnce.Do(func() {
  329. t.Log(string(body))
  330. })
  331. assert.Contains(t, string(body), "frankenphp")
  332. assert.Contains(t, string(body), fmt.Sprintf("i=%d", i))
  333. }, opts)
  334. }
  335. func TestPersistentObject_module(t *testing.T) { testPersistentObject(t, nil) }
  336. func TestPersistentObject_worker(t *testing.T) {
  337. testPersistentObject(t, &testOptions{workerScript: "persistent-object.php"})
  338. }
  339. func testPersistentObject(t *testing.T, opts *testOptions) {
  340. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  341. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/persistent-object.php?i=%d", i), nil)
  342. w := httptest.NewRecorder()
  343. handler(w, req)
  344. resp := w.Result()
  345. body, _ := io.ReadAll(resp.Body)
  346. assert.Equal(t, fmt.Sprintf(`request: %d
  347. class exists: 1
  348. id: obj1
  349. object id: 1`, i), string(body))
  350. }, opts)
  351. }
  352. func TestAutoloader_module(t *testing.T) { testAutoloader(t, nil) }
  353. func TestAutoloader_worker(t *testing.T) {
  354. testAutoloader(t, &testOptions{workerScript: "autoloader.php"})
  355. }
  356. func testAutoloader(t *testing.T, opts *testOptions) {
  357. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  358. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/autoloader.php?i=%d", i), nil)
  359. w := httptest.NewRecorder()
  360. handler(w, req)
  361. resp := w.Result()
  362. body, _ := io.ReadAll(resp.Body)
  363. assert.Equal(t, fmt.Sprintf(`request %d
  364. my_autoloader`, i), string(body))
  365. }, opts)
  366. }
  367. func TestLog_module(t *testing.T) { testLog(t, &testOptions{}) }
  368. func TestLog_worker(t *testing.T) {
  369. testLog(t, &testOptions{workerScript: "log.php"})
  370. }
  371. func testLog(t *testing.T, opts *testOptions) {
  372. logger, logs := observer.New(zapcore.InfoLevel)
  373. opts.logger = zap.New(logger)
  374. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  375. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/log.php?i=%d", i), nil)
  376. w := httptest.NewRecorder()
  377. handler(w, req)
  378. for logs.FilterMessage(fmt.Sprintf("request %d", i)).Len() <= 0 {
  379. }
  380. }, opts)
  381. }
  382. func TestConnectionAbort_module(t *testing.T) { testConnectionAbort(t, &testOptions{}) }
  383. func TestConnectionAbort_worker(t *testing.T) {
  384. testConnectionAbort(t, &testOptions{workerScript: "connectionStatusLog.php"})
  385. }
  386. func testConnectionAbort(t *testing.T, opts *testOptions) {
  387. testFinish := func(finish string) {
  388. t.Run(fmt.Sprintf("finish=%s", finish), func(t *testing.T) {
  389. logger, logs := observer.New(zapcore.InfoLevel)
  390. opts.logger = zap.New(logger)
  391. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  392. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/connectionStatusLog.php?i=%d&finish=%s", i, finish), nil)
  393. w := httptest.NewRecorder()
  394. ctx, cancel := context.WithCancel(req.Context())
  395. req = req.WithContext(ctx)
  396. cancel()
  397. handler(w, req)
  398. for logs.FilterMessage(fmt.Sprintf("request %d: 1", i)).Len() <= 0 {
  399. }
  400. }, opts)
  401. })
  402. }
  403. testFinish("0")
  404. testFinish("1")
  405. }
  406. func TestException_module(t *testing.T) { testException(t, &testOptions{}) }
  407. func TestException_worker(t *testing.T) {
  408. testException(t, &testOptions{workerScript: "exception.php"})
  409. }
  410. func testException(t *testing.T, opts *testOptions) {
  411. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  412. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/exception.php?i=%d", i), nil)
  413. w := httptest.NewRecorder()
  414. handler(w, req)
  415. resp := w.Result()
  416. body, _ := io.ReadAll(resp.Body)
  417. assert.Contains(t, string(body), "hello")
  418. assert.Contains(t, string(body), fmt.Sprintf(`Uncaught Exception: request %d`, i))
  419. }, opts)
  420. }
  421. func TestEarlyHints_module(t *testing.T) { testEarlyHints(t, &testOptions{}) }
  422. func TestEarlyHints_worker(t *testing.T) {
  423. testEarlyHints(t, &testOptions{workerScript: "early-hints.php"})
  424. }
  425. func testEarlyHints(t *testing.T, opts *testOptions) {
  426. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  427. var earlyHintReceived bool
  428. trace := &httptrace.ClientTrace{
  429. Got1xxResponse: func(code int, header textproto.MIMEHeader) error {
  430. switch code {
  431. case http.StatusEarlyHints:
  432. assert.Equal(t, "</style.css>; rel=preload; as=style", header.Get("Link"))
  433. assert.Equal(t, strconv.Itoa(i), header.Get("Request"))
  434. earlyHintReceived = true
  435. }
  436. return nil
  437. },
  438. }
  439. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/early-hints.php?i=%d", i), nil)
  440. w := NewRecorder()
  441. w.ClientTrace = trace
  442. handler(w, req)
  443. assert.Equal(t, strconv.Itoa(i), w.Header().Get("Request"))
  444. assert.Equal(t, "", w.Header().Get("Link"))
  445. assert.True(t, earlyHintReceived)
  446. }, opts)
  447. }
  448. type streamResponseRecorder struct {
  449. *httptest.ResponseRecorder
  450. writeCallback func(buf []byte)
  451. }
  452. func (srr *streamResponseRecorder) Write(buf []byte) (int, error) {
  453. srr.writeCallback(buf)
  454. return srr.ResponseRecorder.Write(buf)
  455. }
  456. func TestFlush_module(t *testing.T) { testFlush(t, &testOptions{}) }
  457. func TestFlush_worker(t *testing.T) {
  458. testFlush(t, &testOptions{workerScript: "flush.php"})
  459. }
  460. func testFlush(t *testing.T, opts *testOptions) {
  461. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  462. var j int
  463. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/flush.php?i=%d", i), nil)
  464. w := &streamResponseRecorder{httptest.NewRecorder(), func(buf []byte) {
  465. if j == 0 {
  466. assert.Equal(t, []byte("He"), buf)
  467. } else {
  468. assert.Equal(t, []byte(fmt.Sprintf("llo %d", i)), buf)
  469. }
  470. j++
  471. }}
  472. handler(w, req)
  473. assert.Equal(t, 2, j)
  474. }, opts)
  475. }
  476. func TestLargeRequest_module(t *testing.T) {
  477. testLargeRequest(t, &testOptions{})
  478. }
  479. func TestLargeRequest_worker(t *testing.T) {
  480. testLargeRequest(t, &testOptions{workerScript: "large-request.php"})
  481. }
  482. func testLargeRequest(t *testing.T, opts *testOptions) {
  483. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  484. req := httptest.NewRequest(
  485. "POST",
  486. fmt.Sprintf("http://example.com/large-request.php?i=%d", i),
  487. strings.NewReader(strings.Repeat("f", 6_048_576)),
  488. )
  489. w := httptest.NewRecorder()
  490. handler(w, req)
  491. resp := w.Result()
  492. body, _ := io.ReadAll(resp.Body)
  493. assert.Contains(t, string(body), fmt.Sprintf("Request body size: 6048576 (%d)", i))
  494. }, opts)
  495. }
  496. func TestVersion(t *testing.T) {
  497. v := frankenphp.Version()
  498. assert.GreaterOrEqual(t, v.MajorVersion, 8)
  499. assert.GreaterOrEqual(t, v.MinorVersion, 0)
  500. assert.GreaterOrEqual(t, v.ReleaseVersion, 0)
  501. assert.GreaterOrEqual(t, v.VersionID, 0)
  502. assert.NotEmpty(t, v.Version, 0)
  503. }
  504. func TestFiberNoCgo_module(t *testing.T) { testFiberNoCgo(t, &testOptions{}) }
  505. func TestFiberNonCgo_worker(t *testing.T) {
  506. testFiberNoCgo(t, &testOptions{workerScript: "fiber-no-cgo.php"})
  507. }
  508. func testFiberNoCgo(t *testing.T, opts *testOptions) {
  509. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  510. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/fiber-no-cgo.php?i=%d", i), nil)
  511. w := httptest.NewRecorder()
  512. handler(w, req)
  513. resp := w.Result()
  514. body, _ := io.ReadAll(resp.Body)
  515. assert.Equal(t, string(body), fmt.Sprintf("Fiber %d", i))
  516. }, opts)
  517. }
  518. func TestFiberBasic_module(t *testing.T) { testFiberBasic(t, &testOptions{}) }
  519. func TestFiberBasic_worker(t *testing.T) {
  520. testFiberBasic(t, &testOptions{workerScript: "fiber-basic.php"})
  521. }
  522. func testFiberBasic(t *testing.T, opts *testOptions) {
  523. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  524. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/fiber-basic.php?i=%d", i), nil)
  525. w := httptest.NewRecorder()
  526. handler(w, req)
  527. resp := w.Result()
  528. body, _ := io.ReadAll(resp.Body)
  529. assert.Equal(t, string(body), fmt.Sprintf("Fiber %d", i))
  530. }, opts)
  531. }
  532. func TestRequestHeaders_module(t *testing.T) { testRequestHeaders(t, &testOptions{}) }
  533. func TestRequestHeaders_worker(t *testing.T) {
  534. testRequestHeaders(t, &testOptions{workerScript: "request-headers.php"})
  535. }
  536. func testRequestHeaders(t *testing.T, opts *testOptions) {
  537. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  538. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/request-headers.php?i=%d", i), nil)
  539. req.Header.Add(strings.Clone("Content-Type"), strings.Clone("text/plain"))
  540. req.Header.Add(strings.Clone("Frankenphp-I"), strings.Clone(strconv.Itoa(i)))
  541. w := httptest.NewRecorder()
  542. handler(w, req)
  543. resp := w.Result()
  544. body, _ := io.ReadAll(resp.Body)
  545. assert.Contains(t, string(body), "[Content-Type] => text/plain")
  546. assert.Contains(t, string(body), fmt.Sprintf("[Frankenphp-I] => %d", i))
  547. }, opts)
  548. }
  549. func TestFailingWorker(t *testing.T) {
  550. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  551. req := httptest.NewRequest("GET", "http://example.com/failing-worker.php", nil)
  552. w := httptest.NewRecorder()
  553. handler(w, req)
  554. resp := w.Result()
  555. body, _ := io.ReadAll(resp.Body)
  556. assert.Contains(t, string(body), "ok")
  557. }, &testOptions{workerScript: "failing-worker.php"})
  558. }
  559. func TestEnv(t *testing.T) {
  560. testEnv(t, &testOptions{})
  561. }
  562. func TestEnvWorker(t *testing.T) {
  563. testEnv(t, &testOptions{workerScript: "test-env.php"})
  564. }
  565. func testEnv(t *testing.T, opts *testOptions) {
  566. assert.NoError(t, os.Setenv("EMPTY", ""))
  567. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  568. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/test-env.php?var=%d", i), nil)
  569. w := httptest.NewRecorder()
  570. handler(w, req)
  571. resp := w.Result()
  572. body, _ := io.ReadAll(resp.Body)
  573. // execute the script as regular php script
  574. cmd := exec.Command("php", "testdata/test-env.php", strconv.Itoa(i))
  575. stdoutStderr, err := cmd.CombinedOutput()
  576. if err != nil {
  577. // php is not installed or other issue, use the hardcoded output below:
  578. 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")
  579. }
  580. assert.Equal(t, string(stdoutStderr), string(body))
  581. }, opts)
  582. }
  583. func TestFileUpload_module(t *testing.T) { testFileUpload(t, &testOptions{}) }
  584. func TestFileUpload_worker(t *testing.T) {
  585. testFileUpload(t, &testOptions{workerScript: "file-upload.php"})
  586. }
  587. func testFileUpload(t *testing.T, opts *testOptions) {
  588. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  589. requestBody := &bytes.Buffer{}
  590. writer := multipart.NewWriter(requestBody)
  591. part, _ := writer.CreateFormFile("file", "foo.txt")
  592. _, err := part.Write([]byte("bar"))
  593. require.NoError(t, err)
  594. writer.Close()
  595. req := httptest.NewRequest("POST", "http://example.com/file-upload.php", requestBody)
  596. req.Header.Add("Content-Type", writer.FormDataContentType())
  597. w := httptest.NewRecorder()
  598. handler(w, req)
  599. resp := w.Result()
  600. body, _ := io.ReadAll(resp.Body)
  601. assert.Contains(t, string(body), "Upload OK")
  602. }, opts)
  603. }
  604. func TestExecuteScriptCLI(t *testing.T) {
  605. if _, err := os.Stat("internal/testcli/testcli"); err != nil {
  606. t.Skip("internal/testcli/testcli has not been compiled, run `cd internal/testcli/ && go build`")
  607. }
  608. cmd := exec.Command("internal/testcli/testcli", "testdata/command.php", "foo", "bar")
  609. stdoutStderr, err := cmd.CombinedOutput()
  610. assert.Error(t, err)
  611. var exitError *exec.ExitError
  612. if errors.As(err, &exitError) {
  613. assert.Equal(t, 3, exitError.ExitCode())
  614. }
  615. stdoutStderrStr := string(stdoutStderr)
  616. assert.Contains(t, stdoutStderrStr, `"foo"`)
  617. assert.Contains(t, stdoutStderrStr, `"bar"`)
  618. assert.Contains(t, stdoutStderrStr, "From the CLI")
  619. }
  620. func ExampleServeHTTP() {
  621. if err := frankenphp.Init(); err != nil {
  622. panic(err)
  623. }
  624. defer frankenphp.Shutdown()
  625. http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  626. req, err := frankenphp.NewRequestWithContext(r, frankenphp.WithRequestDocumentRoot("/path/to/document/root", false))
  627. if err != nil {
  628. panic(err)
  629. }
  630. if err := frankenphp.ServeHTTP(w, req); err != nil {
  631. panic(err)
  632. }
  633. })
  634. log.Fatal(http.ListenAndServe(":8080", nil))
  635. }
  636. func ExampleExecuteScriptCLI() {
  637. if len(os.Args) <= 1 {
  638. log.Println("Usage: my-program script.php")
  639. os.Exit(1)
  640. }
  641. os.Exit(frankenphp.ExecuteScriptCLI(os.Args[1], os.Args))
  642. }
  643. func BenchmarkHelloWorld(b *testing.B) {
  644. if err := frankenphp.Init(frankenphp.WithLogger(zap.NewNop())); err != nil {
  645. panic(err)
  646. }
  647. defer frankenphp.Shutdown()
  648. cwd, _ := os.Getwd()
  649. testDataDir := cwd + "/testdata/"
  650. handler := func(w http.ResponseWriter, r *http.Request) {
  651. req, err := frankenphp.NewRequestWithContext(r, frankenphp.WithRequestDocumentRoot(testDataDir, false))
  652. if err != nil {
  653. panic(err)
  654. }
  655. if err := frankenphp.ServeHTTP(w, req); err != nil {
  656. panic(err)
  657. }
  658. }
  659. req := httptest.NewRequest("GET", "http://example.com/index.php", nil)
  660. w := httptest.NewRecorder()
  661. b.ResetTimer()
  662. for i := 0; i < b.N; i++ {
  663. handler(w, req)
  664. }
  665. }
  666. func BenchmarkEcho(b *testing.B) {
  667. if err := frankenphp.Init(frankenphp.WithLogger(zap.NewNop())); err != nil {
  668. panic(err)
  669. }
  670. defer frankenphp.Shutdown()
  671. cwd, _ := os.Getwd()
  672. testDataDir := cwd + "/testdata/"
  673. handler := func(w http.ResponseWriter, r *http.Request) {
  674. req, err := frankenphp.NewRequestWithContext(r, frankenphp.WithRequestDocumentRoot(testDataDir, false))
  675. if err != nil {
  676. panic(err)
  677. }
  678. if err := frankenphp.ServeHTTP(w, req); err != nil {
  679. panic(err)
  680. }
  681. }
  682. const body = `{
  683. "squadName": "Super hero squad",
  684. "homeTown": "Metro City",
  685. "formed": 2016,
  686. "secretBase": "Super tower",
  687. "active": true,
  688. "members": [
  689. {
  690. "name": "Molecule Man",
  691. "age": 29,
  692. "secretIdentity": "Dan Jukes",
  693. "powers": ["Radiation resistance", "Turning tiny", "Radiation blast"]
  694. },
  695. {
  696. "name": "Madame Uppercut",
  697. "age": 39,
  698. "secretIdentity": "Jane Wilson",
  699. "powers": [
  700. "Million tonne punch",
  701. "Damage resistance",
  702. "Superhuman reflexes"
  703. ]
  704. },
  705. {
  706. "name": "Eternal Flame",
  707. "age": 1000000,
  708. "secretIdentity": "Unknown",
  709. "powers": [
  710. "Immortality",
  711. "Heat Immunity",
  712. "Inferno",
  713. "Teleportation",
  714. "Interdimensional travel"
  715. ]
  716. }
  717. ]
  718. }`
  719. r := strings.NewReader(body)
  720. req := httptest.NewRequest("POST", "http://example.com/echo.php", r)
  721. w := httptest.NewRecorder()
  722. b.ResetTimer()
  723. for i := 0; i < b.N; i++ {
  724. r.Reset(body)
  725. handler(w, req)
  726. }
  727. }
  728. func BenchmarkServerSuperGlobal(b *testing.B) {
  729. if err := frankenphp.Init(frankenphp.WithLogger(zap.NewNop())); err != nil {
  730. panic(err)
  731. }
  732. defer frankenphp.Shutdown()
  733. cwd, _ := os.Getwd()
  734. testDataDir := cwd + "/testdata/"
  735. // Mimics headers of a request sent by Firefox to GitHub
  736. headers := http.Header{}
  737. headers.Add(strings.Clone("Accept"), strings.Clone("text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"))
  738. headers.Add(strings.Clone("Accept-Encoding"), strings.Clone("gzip, deflate, br"))
  739. headers.Add(strings.Clone("Accept-Language"), strings.Clone("fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3"))
  740. headers.Add(strings.Clone("Cache-Control"), strings.Clone("no-cache"))
  741. headers.Add(strings.Clone("Connection"), strings.Clone("keep-alive"))
  742. headers.Add(strings.Clone("Cookie"), strings.Clone("user_session=myrandomuuid; __Host-user_session_same_site=myotherrandomuuid; dotcom_user=dunglas; logged_in=yes; _foo=barbarbarbarbarbar; _device_id=anotherrandomuuid; color_mode=foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobar; preferred_color_mode=light; tz=Europe%2FParis; has_recent_activity=1"))
  743. headers.Add(strings.Clone("DNT"), strings.Clone("1"))
  744. headers.Add(strings.Clone("Host"), strings.Clone("example.com"))
  745. headers.Add(strings.Clone("Pragma"), strings.Clone("no-cache"))
  746. headers.Add(strings.Clone("Sec-Fetch-Dest"), strings.Clone("document"))
  747. headers.Add(strings.Clone("Sec-Fetch-Mode"), strings.Clone("navigate"))
  748. headers.Add(strings.Clone("Sec-Fetch-Site"), strings.Clone("cross-site"))
  749. headers.Add(strings.Clone("Sec-GPC"), strings.Clone("1"))
  750. headers.Add(strings.Clone("Upgrade-Insecure-Requests"), strings.Clone("1"))
  751. headers.Add(strings.Clone("User-Agent"), strings.Clone("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:122.0) Gecko/20100101 Firefox/122.0"))
  752. // Env vars available in a typical Docker container
  753. env := map[string]string{
  754. "HOSTNAME": "a88e81aa22e4",
  755. "PHP_INI_DIR": "/usr/local/etc/php",
  756. "HOME": "/root",
  757. "GODEBUG": "cgocheck=0",
  758. "PHP_LDFLAGS": "-Wl,-O1 -pie",
  759. "PHP_CFLAGS": "-fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64",
  760. "PHP_VERSION": "8.3.2",
  761. "GPG_KEYS": "1198C0117593497A5EC5C199286AF1F9897469DC C28D937575603EB4ABB725861C0779DC5C0A9DE4 AFD8691FDAEDF03BDF6E460563F15A9B715376CA",
  762. "PHP_CPPFLAGS": "-fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64",
  763. "PHP_ASC_URL": "https://www.php.net/distributions/php-8.3.2.tar.xz.asc",
  764. "PHP_URL": "https://www.php.net/distributions/php-8.3.2.tar.xz",
  765. "PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
  766. "XDG_CONFIG_HOME": "/config",
  767. "XDG_DATA_HOME": "/data",
  768. "PHPIZE_DEPS": "autoconf dpkg-dev file g++ gcc libc-dev make pkg-config re2c",
  769. "PWD": "/app",
  770. "PHP_SHA256": "4ffa3e44afc9c590e28dc0d2d31fc61f0139f8b335f11880a121b9f9b9f0634e",
  771. }
  772. preparedEnv := frankenphp.PrepareEnv(env)
  773. handler := func(w http.ResponseWriter, r *http.Request) {
  774. req, err := frankenphp.NewRequestWithContext(r, frankenphp.WithRequestDocumentRoot(testDataDir, false), frankenphp.WithRequestPreparedEnv(preparedEnv))
  775. if err != nil {
  776. panic(err)
  777. }
  778. r.Header = headers
  779. if err := frankenphp.ServeHTTP(w, req); err != nil {
  780. panic(err)
  781. }
  782. }
  783. req := httptest.NewRequest("GET", "http://example.com/server-variable.php", nil)
  784. w := httptest.NewRecorder()
  785. b.ResetTimer()
  786. for i := 0; i < b.N; i++ {
  787. handler(w, req)
  788. }
  789. }
  790. func TestRejectInvalidHeaders_module(t *testing.T) { testRejectInvalidHeaders(t, &testOptions{}) }
  791. func TestRejectInvalidHeaders_worker(t *testing.T) {
  792. testRejectInvalidHeaders(t, &testOptions{workerScript: "headers.php"})
  793. }
  794. func testRejectInvalidHeaders(t *testing.T, opts *testOptions) {
  795. invalidHeaders := [][]string{
  796. {"Content-Length", "-1"},
  797. {"Content-Length", "something"},
  798. }
  799. for _, header := range invalidHeaders {
  800. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, _ int) {
  801. req := httptest.NewRequest("GET", "http://example.com/headers.php", nil)
  802. req.Header.Add(header[0], header[1])
  803. w := httptest.NewRecorder()
  804. handler(w, req)
  805. resp := w.Result()
  806. body, _ := io.ReadAll(resp.Body)
  807. assert.Equal(t, 400, resp.StatusCode)
  808. assert.Contains(t, string(body), "invalid")
  809. }, opts)
  810. }
  811. }
  812. // Worker mode will clean up unreferenced streams between requests
  813. // Make sure referenced streams are not cleaned up
  814. func TestFileStreamInWorkerMode(t *testing.T) {
  815. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, _ int) {
  816. resp1 := fetchBody("GET", "http://example.com/file-stream.php", handler)
  817. assert.Equal(t, resp1, "word1")
  818. resp2 := fetchBody("GET", "http://example.com/file-stream.php", handler)
  819. assert.Equal(t, resp2, "word2")
  820. resp3 := fetchBody("GET", "http://example.com/file-stream.php", handler)
  821. assert.Equal(t, resp3, "word3")
  822. }, &testOptions{workerScript: "file-stream.php", nbParallelRequests: 1, nbWorkers: 1})
  823. }
  824. // To run this fuzzing test use: go test -fuzz FuzzRequest
  825. // TODO: Cover more potential cases
  826. func FuzzRequest(f *testing.F) {
  827. absPath, _ := fastabs.FastAbs("./testdata/")
  828. f.Add("hello world")
  829. f.Add("πŸ˜€πŸ˜…πŸ™ƒπŸ€©πŸ₯²πŸ€ͺπŸ˜˜πŸ˜‡πŸ˜‰πŸ˜πŸ§Ÿ")
  830. f.Add("%00%11%%22%%33%%44%%55%%66%%77%%88%%99%%aa%%bb%%cc%%dd%%ee%%ff")
  831. f.Add("\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f")
  832. f.Fuzz(func(t *testing.T, fuzzedString string) {
  833. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, _ int) {
  834. req := httptest.NewRequest("GET", "http://example.com/server-variable", nil)
  835. req.URL = &url.URL{RawQuery: "test=" + fuzzedString, Path: "/server-variable.php/" + fuzzedString}
  836. req.Header.Add(strings.Clone("Fuzzed"), strings.Clone(fuzzedString))
  837. req.Header.Add(strings.Clone("Content-Type"), fuzzedString)
  838. w := httptest.NewRecorder()
  839. handler(w, req)
  840. resp := w.Result()
  841. body, _ := io.ReadAll(resp.Body)
  842. // The response status must be 400 if the request path contains null bytes
  843. if strings.Contains(req.URL.Path, "\x00") {
  844. assert.Equal(t, 400, resp.StatusCode)
  845. assert.Contains(t, string(body), "Invalid request path")
  846. return
  847. }
  848. // The fuzzed string must be present in the path
  849. assert.Contains(t, string(body), fmt.Sprintf("[PATH_INFO] => /%s", fuzzedString))
  850. assert.Contains(t, string(body), fmt.Sprintf("[PATH_TRANSLATED] => %s", filepath.Join(absPath, fuzzedString)))
  851. // Headers should always be present even if empty
  852. assert.Contains(t, string(body), fmt.Sprintf("[CONTENT_TYPE] => %s", fuzzedString))
  853. assert.Contains(t, string(body), fmt.Sprintf("[HTTP_FUZZED] => %s", fuzzedString))
  854. }, &testOptions{workerScript: "request-headers.php"})
  855. })
  856. }
  857. func fetchBody(method string, url string, handler func(http.ResponseWriter, *http.Request)) string {
  858. req := httptest.NewRequest(method, url, nil)
  859. w := httptest.NewRecorder()
  860. handler(w, req)
  861. resp := w.Result()
  862. body, _ := io.ReadAll(resp.Body)
  863. return string(body)
  864. }