frankenphp_test.go 29 KB

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