caddy_test.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616
  1. package caddy_test
  2. import (
  3. "bytes"
  4. "fmt"
  5. "net/http"
  6. "os"
  7. "path/filepath"
  8. "strings"
  9. "sync"
  10. "testing"
  11. "github.com/dunglas/frankenphp"
  12. "github.com/prometheus/client_golang/prometheus"
  13. "github.com/prometheus/client_golang/prometheus/testutil"
  14. "github.com/stretchr/testify/require"
  15. "github.com/caddyserver/caddy/v2/caddytest"
  16. )
  17. var testPort = "9080"
  18. func TestPHP(t *testing.T) {
  19. var wg sync.WaitGroup
  20. tester := caddytest.NewTester(t)
  21. tester.InitServer(`
  22. {
  23. skip_install_trust
  24. admin localhost:2999
  25. http_port `+testPort+`
  26. https_port 9443
  27. frankenphp
  28. }
  29. localhost:`+testPort+` {
  30. route {
  31. php {
  32. root ../testdata
  33. }
  34. }
  35. }
  36. `, "caddyfile")
  37. for i := 0; i < 100; i++ {
  38. wg.Add(1)
  39. go func(i int) {
  40. tester.AssertGetResponse(fmt.Sprintf("http://localhost:"+testPort+"/index.php?i=%d", i), http.StatusOK, fmt.Sprintf("I am by birth a Genevese (%d)", i))
  41. wg.Done()
  42. }(i)
  43. }
  44. wg.Wait()
  45. }
  46. func TestLargeRequest(t *testing.T) {
  47. tester := caddytest.NewTester(t)
  48. tester.InitServer(`
  49. {
  50. skip_install_trust
  51. admin localhost:2999
  52. http_port `+testPort+`
  53. https_port 9443
  54. frankenphp
  55. }
  56. localhost:`+testPort+` {
  57. route {
  58. php {
  59. root ../testdata
  60. }
  61. }
  62. }
  63. `, "caddyfile")
  64. tester.AssertPostResponseBody(
  65. "http://localhost:"+testPort+"/large-request.php",
  66. []string{},
  67. bytes.NewBufferString(strings.Repeat("f", 1_048_576)),
  68. http.StatusOK,
  69. "Request body size: 1048576 (unknown)",
  70. )
  71. }
  72. func TestWorker(t *testing.T) {
  73. var wg sync.WaitGroup
  74. tester := caddytest.NewTester(t)
  75. tester.InitServer(`
  76. {
  77. skip_install_trust
  78. admin localhost:2999
  79. http_port `+testPort+`
  80. https_port 9443
  81. frankenphp {
  82. worker ../testdata/index.php 2
  83. }
  84. }
  85. localhost:`+testPort+` {
  86. route {
  87. php {
  88. root ../testdata
  89. }
  90. }
  91. }
  92. `, "caddyfile")
  93. for i := 0; i < 100; i++ {
  94. wg.Add(1)
  95. go func(i int) {
  96. tester.AssertGetResponse(fmt.Sprintf("http://localhost:"+testPort+"/index.php?i=%d", i), http.StatusOK, fmt.Sprintf("I am by birth a Genevese (%d)", i))
  97. wg.Done()
  98. }(i)
  99. }
  100. wg.Wait()
  101. }
  102. func TestEnv(t *testing.T) {
  103. tester := caddytest.NewTester(t)
  104. tester.InitServer(`
  105. {
  106. skip_install_trust
  107. admin localhost:2999
  108. http_port `+testPort+`
  109. https_port 9443
  110. frankenphp {
  111. worker {
  112. file ../testdata/worker-env.php
  113. num 1
  114. env FOO bar
  115. }
  116. }
  117. }
  118. localhost:`+testPort+` {
  119. route {
  120. php {
  121. root ../testdata
  122. env FOO baz
  123. }
  124. }
  125. }
  126. `, "caddyfile")
  127. tester.AssertGetResponse("http://localhost:"+testPort+"/worker-env.php", http.StatusOK, "bazbar")
  128. }
  129. func TestJsonEnv(t *testing.T) {
  130. tester := caddytest.NewTester(t)
  131. tester.InitServer(`
  132. {
  133. "admin": {
  134. "listen": "localhost:2999"
  135. },
  136. "apps": {
  137. "frankenphp": {
  138. "workers": [
  139. {
  140. "env": {
  141. "FOO": "bar"
  142. },
  143. "file_name": "../testdata/worker-env.php",
  144. "num": 1
  145. }
  146. ]
  147. },
  148. "http": {
  149. "http_port": `+testPort+`,
  150. "https_port": 9443,
  151. "servers": {
  152. "srv0": {
  153. "listen": [
  154. ":`+testPort+`"
  155. ],
  156. "routes": [
  157. {
  158. "handle": [
  159. {
  160. "handler": "subroute",
  161. "routes": [
  162. {
  163. "handle": [
  164. {
  165. "handler": "subroute",
  166. "routes": [
  167. {
  168. "handle": [
  169. {
  170. "env": {
  171. "FOO": "baz"
  172. },
  173. "handler": "php",
  174. "root": "../testdata"
  175. }
  176. ]
  177. }
  178. ]
  179. }
  180. ]
  181. }
  182. ]
  183. }
  184. ],
  185. "match": [
  186. {
  187. "host": [
  188. "localhost"
  189. ]
  190. }
  191. ],
  192. "terminal": true
  193. }
  194. ]
  195. }
  196. }
  197. },
  198. "pki": {
  199. "certificate_authorities": {
  200. "local": {
  201. "install_trust": false
  202. }
  203. }
  204. }
  205. }
  206. }
  207. `, "json")
  208. tester.AssertGetResponse("http://localhost:"+testPort+"/worker-env.php", http.StatusOK, "bazbar")
  209. }
  210. func TestCustomCaddyVariablesInEnv(t *testing.T) {
  211. tester := caddytest.NewTester(t)
  212. tester.InitServer(`
  213. {
  214. skip_install_trust
  215. admin localhost:2999
  216. http_port `+testPort+`
  217. https_port 9443
  218. frankenphp {
  219. worker {
  220. file ../testdata/worker-env.php
  221. num 1
  222. env FOO world
  223. }
  224. }
  225. }
  226. localhost:`+testPort+` {
  227. route {
  228. map 1 {my_customvar} {
  229. default "hello "
  230. }
  231. php {
  232. root ../testdata
  233. env FOO {my_customvar}
  234. }
  235. }
  236. }
  237. `, "caddyfile")
  238. tester.AssertGetResponse("http://localhost:"+testPort+"/worker-env.php", http.StatusOK, "hello world")
  239. }
  240. func TestPHPServerDirective(t *testing.T) {
  241. tester := caddytest.NewTester(t)
  242. tester.InitServer(`
  243. {
  244. skip_install_trust
  245. admin localhost:2999
  246. http_port `+testPort+`
  247. https_port 9443
  248. frankenphp
  249. }
  250. localhost:`+testPort+` {
  251. root * ../testdata
  252. php_server
  253. }
  254. `, "caddyfile")
  255. tester.AssertGetResponse("http://localhost:"+testPort, http.StatusOK, "I am by birth a Genevese (i not set)")
  256. tester.AssertGetResponse("http://localhost:"+testPort+"/hello.txt", http.StatusOK, "Hello")
  257. tester.AssertGetResponse("http://localhost:"+testPort+"/not-found.txt", http.StatusOK, "I am by birth a Genevese (i not set)")
  258. }
  259. func TestPHPServerDirectiveDisableFileServer(t *testing.T) {
  260. tester := caddytest.NewTester(t)
  261. tester.InitServer(`
  262. {
  263. skip_install_trust
  264. admin localhost:2999
  265. http_port `+testPort+`
  266. https_port 9443
  267. frankenphp
  268. order php_server before respond
  269. }
  270. localhost:`+testPort+` {
  271. root * ../testdata
  272. php_server {
  273. file_server off
  274. }
  275. respond "Not found" 404
  276. }
  277. `, "caddyfile")
  278. tester.AssertGetResponse("http://localhost:"+testPort, http.StatusOK, "I am by birth a Genevese (i not set)")
  279. tester.AssertGetResponse("http://localhost:"+testPort+"/not-found.txt", http.StatusOK, "I am by birth a Genevese (i not set)")
  280. }
  281. func TestMetrics(t *testing.T) {
  282. var wg sync.WaitGroup
  283. tester := caddytest.NewTester(t)
  284. tester.InitServer(`
  285. {
  286. skip_install_trust
  287. admin localhost:2999
  288. http_port `+testPort+`
  289. https_port 9443
  290. frankenphp
  291. }
  292. localhost:`+testPort+` {
  293. route {
  294. php {
  295. root ../testdata
  296. }
  297. }
  298. }
  299. `, "caddyfile")
  300. // Make some requests
  301. for i := 0; i < 10; i++ {
  302. wg.Add(1)
  303. go func(i int) {
  304. tester.AssertGetResponse(fmt.Sprintf("http://localhost:"+testPort+"/index.php?i=%d", i), http.StatusOK, fmt.Sprintf("I am by birth a Genevese (%d)", i))
  305. wg.Done()
  306. }(i)
  307. }
  308. wg.Wait()
  309. // Fetch metrics
  310. resp, err := http.Get("http://localhost:2999/metrics")
  311. if err != nil {
  312. t.Fatalf("failed to fetch metrics: %v", err)
  313. }
  314. defer resp.Body.Close()
  315. // Read and parse metrics
  316. metrics := new(bytes.Buffer)
  317. _, err = metrics.ReadFrom(resp.Body)
  318. if err != nil {
  319. t.Fatalf("failed to read metrics: %v", err)
  320. }
  321. cpus := fmt.Sprintf("%d", frankenphp.MaxThreads)
  322. // Check metrics
  323. expectedMetrics := `
  324. # HELP frankenphp_total_threads Total number of PHP threads
  325. # TYPE frankenphp_total_threads counter
  326. frankenphp_total_threads ` + cpus + `
  327. # HELP frankenphp_busy_threads Number of busy PHP threads
  328. # TYPE frankenphp_busy_threads gauge
  329. frankenphp_busy_threads 0
  330. `
  331. require.NoError(t, testutil.GatherAndCompare(prometheus.DefaultGatherer, strings.NewReader(expectedMetrics), "frankenphp_total_threads", "frankenphp_busy_threads"))
  332. }
  333. func TestWorkerMetrics(t *testing.T) {
  334. var wg sync.WaitGroup
  335. tester := caddytest.NewTester(t)
  336. tester.InitServer(`
  337. {
  338. skip_install_trust
  339. admin localhost:2999
  340. http_port `+testPort+`
  341. https_port 9443
  342. frankenphp {
  343. worker ../testdata/index.php 2
  344. }
  345. }
  346. localhost:`+testPort+` {
  347. route {
  348. php {
  349. root ../testdata
  350. }
  351. }
  352. }
  353. `, "caddyfile")
  354. // Make some requests
  355. for i := 0; i < 10; i++ {
  356. wg.Add(1)
  357. go func(i int) {
  358. tester.AssertGetResponse(fmt.Sprintf("http://localhost:"+testPort+"/index.php?i=%d", i), http.StatusOK, fmt.Sprintf("I am by birth a Genevese (%d)", i))
  359. wg.Done()
  360. }(i)
  361. }
  362. wg.Wait()
  363. // Fetch metrics
  364. resp, err := http.Get("http://localhost:2999/metrics")
  365. if err != nil {
  366. t.Fatalf("failed to fetch metrics: %v", err)
  367. }
  368. defer resp.Body.Close()
  369. // Read and parse metrics
  370. metrics := new(bytes.Buffer)
  371. _, err = metrics.ReadFrom(resp.Body)
  372. if err != nil {
  373. t.Fatalf("failed to read metrics: %v", err)
  374. }
  375. cpus := fmt.Sprintf("%d", frankenphp.MaxThreads)
  376. // Check metrics
  377. expectedMetrics := `
  378. # HELP frankenphp_total_threads Total number of PHP threads
  379. # TYPE frankenphp_total_threads counter
  380. frankenphp_total_threads ` + cpus + `
  381. # HELP frankenphp_busy_threads Number of busy PHP threads
  382. # TYPE frankenphp_busy_threads gauge
  383. frankenphp_busy_threads 2
  384. # HELP frankenphp_testdata_index_php_busy_workers Number of busy PHP workers for this worker
  385. # TYPE frankenphp_testdata_index_php_busy_workers gauge
  386. frankenphp_testdata_index_php_busy_workers 0
  387. # HELP frankenphp_testdata_index_php_total_workers Total number of PHP workers for this worker
  388. # TYPE frankenphp_testdata_index_php_total_workers gauge
  389. frankenphp_testdata_index_php_total_workers 2
  390. # HELP frankenphp_testdata_index_php_worker_request_count
  391. # TYPE frankenphp_testdata_index_php_worker_request_count counter
  392. frankenphp_testdata_index_php_worker_request_count 10
  393. # HELP frankenphp_testdata_index_php_ready_workers Running workers that have successfully called frankenphp_handle_request at least once
  394. # TYPE frankenphp_testdata_index_php_ready_workers gauge
  395. frankenphp_testdata_index_php_ready_workers 2
  396. # HELP frankenphp_testdata_index_php_worker_crashes Number of PHP worker crashes for this worker
  397. # TYPE frankenphp_testdata_index_php_worker_crashes counter
  398. frankenphp_testdata_index_php_worker_crashes 0
  399. # HELP frankenphp_testdata_index_php_worker_restarts Number of PHP worker restarts for this worker
  400. # TYPE frankenphp_testdata_index_php_worker_restarts counter
  401. frankenphp_testdata_index_php_worker_restarts 0
  402. `
  403. require.NoError(t,
  404. testutil.GatherAndCompare(
  405. prometheus.DefaultGatherer,
  406. strings.NewReader(expectedMetrics),
  407. "frankenphp_total_threads",
  408. "frankenphp_busy_threads",
  409. "frankenphp_testdata_index_php_busy_workers",
  410. "frankenphp_testdata_index_php_total_workers",
  411. "frankenphp_testdata_index_php_worker_request_count",
  412. "frankenphp_testdata_index_php_worker_crashes",
  413. "frankenphp_testdata_index_php_worker_restarts",
  414. "frankenphp_testdata_index_php_ready_workers",
  415. ))
  416. }
  417. func TestAutoWorkerConfig(t *testing.T) {
  418. var wg sync.WaitGroup
  419. tester := caddytest.NewTester(t)
  420. tester.InitServer(`
  421. {
  422. skip_install_trust
  423. admin localhost:2999
  424. http_port `+testPort+`
  425. https_port 9443
  426. frankenphp {
  427. worker ../testdata/index.php
  428. }
  429. }
  430. localhost:`+testPort+` {
  431. route {
  432. php {
  433. root ../testdata
  434. }
  435. }
  436. }
  437. `, "caddyfile")
  438. // Make some requests
  439. for i := 0; i < 10; i++ {
  440. wg.Add(1)
  441. go func(i int) {
  442. tester.AssertGetResponse(fmt.Sprintf("http://localhost:"+testPort+"/index.php?i=%d", i), http.StatusOK, fmt.Sprintf("I am by birth a Genevese (%d)", i))
  443. wg.Done()
  444. }(i)
  445. }
  446. wg.Wait()
  447. // Fetch metrics
  448. resp, err := http.Get("http://localhost:2999/metrics")
  449. if err != nil {
  450. t.Fatalf("failed to fetch metrics: %v", err)
  451. }
  452. defer resp.Body.Close()
  453. // Read and parse metrics
  454. metrics := new(bytes.Buffer)
  455. _, err = metrics.ReadFrom(resp.Body)
  456. if err != nil {
  457. t.Fatalf("failed to read metrics: %v", err)
  458. }
  459. cpus := fmt.Sprintf("%d", frankenphp.MaxThreads)
  460. workers := fmt.Sprintf("%d", frankenphp.MaxThreads-1)
  461. // Check metrics
  462. expectedMetrics := `
  463. # HELP frankenphp_total_threads Total number of PHP threads
  464. # TYPE frankenphp_total_threads counter
  465. frankenphp_total_threads ` + cpus + `
  466. # HELP frankenphp_busy_threads Number of busy PHP threads
  467. # TYPE frankenphp_busy_threads gauge
  468. frankenphp_busy_threads ` + workers + `
  469. # HELP frankenphp_testdata_index_php_busy_workers Number of busy PHP workers for this worker
  470. # TYPE frankenphp_testdata_index_php_busy_workers gauge
  471. frankenphp_testdata_index_php_busy_workers 0
  472. # HELP frankenphp_testdata_index_php_total_workers Total number of PHP workers for this worker
  473. # TYPE frankenphp_testdata_index_php_total_workers gauge
  474. frankenphp_testdata_index_php_total_workers ` + workers + `
  475. # HELP frankenphp_testdata_index_php_worker_request_count
  476. # TYPE frankenphp_testdata_index_php_worker_request_count counter
  477. frankenphp_testdata_index_php_worker_request_count 10
  478. # HELP frankenphp_testdata_index_php_ready_workers Running workers that have successfully called frankenphp_handle_request at least once
  479. # TYPE frankenphp_testdata_index_php_ready_workers gauge
  480. frankenphp_testdata_index_php_ready_workers ` + workers + `
  481. # HELP frankenphp_testdata_index_php_worker_crashes Number of PHP worker crashes for this worker
  482. # TYPE frankenphp_testdata_index_php_worker_crashes counter
  483. frankenphp_testdata_index_php_worker_crashes 0
  484. # HELP frankenphp_testdata_index_php_worker_restarts Number of PHP worker restarts for this worker
  485. # TYPE frankenphp_testdata_index_php_worker_restarts counter
  486. frankenphp_testdata_index_php_worker_restarts 0
  487. `
  488. require.NoError(t,
  489. testutil.GatherAndCompare(
  490. prometheus.DefaultGatherer,
  491. strings.NewReader(expectedMetrics),
  492. "frankenphp_total_threads",
  493. "frankenphp_busy_threads",
  494. "frankenphp_testdata_index_php_busy_workers",
  495. "frankenphp_testdata_index_php_total_workers",
  496. "frankenphp_testdata_index_php_worker_request_count",
  497. "frankenphp_testdata_index_php_worker_crashes",
  498. "frankenphp_testdata_index_php_worker_restarts",
  499. "frankenphp_testdata_index_php_ready_workers",
  500. ))
  501. }
  502. func TestAllDefinedServerVars(t *testing.T) {
  503. documentRoot, _ := filepath.Abs("../testdata/")
  504. expectedBodyFile, _ := os.ReadFile("../testdata/server-all-vars-ordered.txt")
  505. expectedBody := string(expectedBodyFile)
  506. expectedBody = strings.ReplaceAll(expectedBody, "{documentRoot}", documentRoot)
  507. expectedBody = strings.ReplaceAll(expectedBody, "\r\n", "\n")
  508. expectedBody = strings.ReplaceAll(expectedBody, "{testPort}", testPort)
  509. tester := caddytest.NewTester(t)
  510. tester.InitServer(`
  511. {
  512. skip_install_trust
  513. admin localhost:2999
  514. http_port `+testPort+`
  515. frankenphp
  516. }
  517. localhost:`+testPort+` {
  518. route {
  519. root ../testdata
  520. php
  521. }
  522. }
  523. `, "caddyfile")
  524. tester.AssertPostResponseBody(
  525. "http://user@localhost:"+testPort+"/server-all-vars-ordered.php/path?specialChars=%3E\\x00%00</>",
  526. []string{
  527. "Content-Type: application/x-www-form-urlencoded",
  528. "Content-Length: 14", // maliciously set to 14
  529. "Special-Chars: <%00>",
  530. "Host: Malicous Host",
  531. "X-Empty-Header:",
  532. },
  533. bytes.NewBufferString("foo=bar"),
  534. http.StatusOK,
  535. expectedBody,
  536. )
  537. }