frankenphp_test.go 35 KB

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