frankenphp_test.go 33 KB

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