frankenphp_test.go 37 KB

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