caddy_test.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750
  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/testutil"
  13. "github.com/stretchr/testify/require"
  14. "github.com/caddyserver/caddy/v2"
  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. mercure {
  295. transport local
  296. anonymous
  297. publisher_jwt !ChangeMe!
  298. }
  299. php {
  300. root ../testdata
  301. }
  302. }
  303. }
  304. example.com:`+testPort+` {
  305. route {
  306. mercure {
  307. transport local
  308. anonymous
  309. publisher_jwt !ChangeMe!
  310. }
  311. php {
  312. root ../testdata
  313. }
  314. }
  315. }
  316. `, "caddyfile")
  317. // Make some requests
  318. for i := 0; i < 10; i++ {
  319. wg.Add(1)
  320. go func(i int) {
  321. tester.AssertGetResponse(fmt.Sprintf("http://localhost:"+testPort+"/index.php?i=%d", i), http.StatusOK, fmt.Sprintf("I am by birth a Genevese (%d)", i))
  322. wg.Done()
  323. }(i)
  324. }
  325. wg.Wait()
  326. // Fetch metrics
  327. resp, err := http.Get("http://localhost:2999/metrics")
  328. if err != nil {
  329. t.Fatalf("failed to fetch metrics: %v", err)
  330. }
  331. defer resp.Body.Close()
  332. // Read and parse metrics
  333. metrics := new(bytes.Buffer)
  334. _, err = metrics.ReadFrom(resp.Body)
  335. if err != nil {
  336. t.Fatalf("failed to read metrics: %v", err)
  337. }
  338. cpus := fmt.Sprintf("%d", frankenphp.MaxThreads)
  339. // Check metrics
  340. expectedMetrics := `
  341. # HELP frankenphp_total_threads Total number of PHP threads
  342. # TYPE frankenphp_total_threads counter
  343. frankenphp_total_threads ` + cpus + `
  344. # HELP frankenphp_busy_threads Number of busy PHP threads
  345. # TYPE frankenphp_busy_threads gauge
  346. frankenphp_busy_threads 0
  347. `
  348. ctx := caddy.ActiveContext()
  349. require.NoError(t, testutil.GatherAndCompare(ctx.GetMetricsRegistry(), strings.NewReader(expectedMetrics), "frankenphp_total_threads", "frankenphp_busy_threads"))
  350. }
  351. func TestWorkerMetrics(t *testing.T) {
  352. var wg sync.WaitGroup
  353. tester := caddytest.NewTester(t)
  354. tester.InitServer(`
  355. {
  356. skip_install_trust
  357. admin localhost:2999
  358. http_port `+testPort+`
  359. https_port 9443
  360. frankenphp {
  361. worker ../testdata/index.php 2
  362. }
  363. }
  364. localhost:`+testPort+` {
  365. route {
  366. php {
  367. root ../testdata
  368. }
  369. }
  370. }
  371. example.com:`+testPort+` {
  372. route {
  373. php {
  374. root ../testdata
  375. }
  376. }
  377. }
  378. `, "caddyfile")
  379. // Make some requests
  380. for i := 0; i < 10; i++ {
  381. wg.Add(1)
  382. go func(i int) {
  383. tester.AssertGetResponse(fmt.Sprintf("http://localhost:"+testPort+"/index.php?i=%d", i), http.StatusOK, fmt.Sprintf("I am by birth a Genevese (%d)", i))
  384. wg.Done()
  385. }(i)
  386. }
  387. wg.Wait()
  388. // Fetch metrics
  389. resp, err := http.Get("http://localhost:2999/metrics")
  390. if err != nil {
  391. t.Fatalf("failed to fetch metrics: %v", err)
  392. }
  393. defer resp.Body.Close()
  394. // Read and parse metrics
  395. metrics := new(bytes.Buffer)
  396. _, err = metrics.ReadFrom(resp.Body)
  397. if err != nil {
  398. t.Fatalf("failed to read metrics: %v", err)
  399. }
  400. cpus := fmt.Sprintf("%d", frankenphp.MaxThreads)
  401. // Check metrics
  402. expectedMetrics := `
  403. # HELP frankenphp_total_threads Total number of PHP threads
  404. # TYPE frankenphp_total_threads counter
  405. frankenphp_total_threads ` + cpus + `
  406. # HELP frankenphp_busy_threads Number of busy PHP threads
  407. # TYPE frankenphp_busy_threads gauge
  408. frankenphp_busy_threads 2
  409. # HELP frankenphp_testdata_index_php_busy_workers Number of busy PHP workers for this worker
  410. # TYPE frankenphp_testdata_index_php_busy_workers gauge
  411. frankenphp_testdata_index_php_busy_workers 0
  412. # HELP frankenphp_testdata_index_php_total_workers Total number of PHP workers for this worker
  413. # TYPE frankenphp_testdata_index_php_total_workers gauge
  414. frankenphp_testdata_index_php_total_workers 2
  415. # HELP frankenphp_testdata_index_php_worker_request_count
  416. # TYPE frankenphp_testdata_index_php_worker_request_count counter
  417. frankenphp_testdata_index_php_worker_request_count 10
  418. # HELP frankenphp_testdata_index_php_ready_workers Running workers that have successfully called frankenphp_handle_request at least once
  419. # TYPE frankenphp_testdata_index_php_ready_workers gauge
  420. frankenphp_testdata_index_php_ready_workers 2
  421. # HELP frankenphp_testdata_index_php_worker_crashes Number of PHP worker crashes for this worker
  422. # TYPE frankenphp_testdata_index_php_worker_crashes counter
  423. frankenphp_testdata_index_php_worker_crashes 0
  424. # HELP frankenphp_testdata_index_php_worker_restarts Number of PHP worker restarts for this worker
  425. # TYPE frankenphp_testdata_index_php_worker_restarts counter
  426. frankenphp_testdata_index_php_worker_restarts 0
  427. `
  428. ctx := caddy.ActiveContext()
  429. require.NoError(t,
  430. testutil.GatherAndCompare(
  431. ctx.GetMetricsRegistry(),
  432. strings.NewReader(expectedMetrics),
  433. "frankenphp_total_threads",
  434. "frankenphp_busy_threads",
  435. "frankenphp_testdata_index_php_busy_workers",
  436. "frankenphp_testdata_index_php_total_workers",
  437. "frankenphp_testdata_index_php_worker_request_count",
  438. "frankenphp_testdata_index_php_worker_crashes",
  439. "frankenphp_testdata_index_php_worker_restarts",
  440. "frankenphp_testdata_index_php_ready_workers",
  441. ))
  442. }
  443. func TestAutoWorkerConfig(t *testing.T) {
  444. var wg sync.WaitGroup
  445. tester := caddytest.NewTester(t)
  446. tester.InitServer(`
  447. {
  448. skip_install_trust
  449. admin localhost:2999
  450. http_port `+testPort+`
  451. https_port 9443
  452. frankenphp {
  453. worker ../testdata/index.php
  454. }
  455. }
  456. localhost:`+testPort+` {
  457. route {
  458. php {
  459. root ../testdata
  460. }
  461. }
  462. }
  463. `, "caddyfile")
  464. // Make some requests
  465. for i := 0; i < 10; i++ {
  466. wg.Add(1)
  467. go func(i int) {
  468. tester.AssertGetResponse(fmt.Sprintf("http://localhost:"+testPort+"/index.php?i=%d", i), http.StatusOK, fmt.Sprintf("I am by birth a Genevese (%d)", i))
  469. wg.Done()
  470. }(i)
  471. }
  472. wg.Wait()
  473. // Fetch metrics
  474. resp, err := http.Get("http://localhost:2999/metrics")
  475. if err != nil {
  476. t.Fatalf("failed to fetch metrics: %v", err)
  477. }
  478. defer resp.Body.Close()
  479. // Read and parse metrics
  480. metrics := new(bytes.Buffer)
  481. _, err = metrics.ReadFrom(resp.Body)
  482. if err != nil {
  483. t.Fatalf("failed to read metrics: %v", err)
  484. }
  485. cpus := fmt.Sprintf("%d", frankenphp.MaxThreads)
  486. workers := fmt.Sprintf("%d", frankenphp.MaxThreads-1)
  487. // Check metrics
  488. expectedMetrics := `
  489. # HELP frankenphp_total_threads Total number of PHP threads
  490. # TYPE frankenphp_total_threads counter
  491. frankenphp_total_threads ` + cpus + `
  492. # HELP frankenphp_busy_threads Number of busy PHP threads
  493. # TYPE frankenphp_busy_threads gauge
  494. frankenphp_busy_threads ` + workers + `
  495. # HELP frankenphp_testdata_index_php_busy_workers Number of busy PHP workers for this worker
  496. # TYPE frankenphp_testdata_index_php_busy_workers gauge
  497. frankenphp_testdata_index_php_busy_workers 0
  498. # HELP frankenphp_testdata_index_php_total_workers Total number of PHP workers for this worker
  499. # TYPE frankenphp_testdata_index_php_total_workers gauge
  500. frankenphp_testdata_index_php_total_workers ` + workers + `
  501. # HELP frankenphp_testdata_index_php_worker_request_count
  502. # TYPE frankenphp_testdata_index_php_worker_request_count counter
  503. frankenphp_testdata_index_php_worker_request_count 10
  504. # HELP frankenphp_testdata_index_php_ready_workers Running workers that have successfully called frankenphp_handle_request at least once
  505. # TYPE frankenphp_testdata_index_php_ready_workers gauge
  506. frankenphp_testdata_index_php_ready_workers ` + workers + `
  507. # HELP frankenphp_testdata_index_php_worker_crashes Number of PHP worker crashes for this worker
  508. # TYPE frankenphp_testdata_index_php_worker_crashes counter
  509. frankenphp_testdata_index_php_worker_crashes 0
  510. # HELP frankenphp_testdata_index_php_worker_restarts Number of PHP worker restarts for this worker
  511. # TYPE frankenphp_testdata_index_php_worker_restarts counter
  512. frankenphp_testdata_index_php_worker_restarts 0
  513. `
  514. ctx := caddy.ActiveContext()
  515. require.NoError(t,
  516. testutil.GatherAndCompare(
  517. ctx.GetMetricsRegistry(),
  518. strings.NewReader(expectedMetrics),
  519. "frankenphp_total_threads",
  520. "frankenphp_busy_threads",
  521. "frankenphp_testdata_index_php_busy_workers",
  522. "frankenphp_testdata_index_php_total_workers",
  523. "frankenphp_testdata_index_php_worker_request_count",
  524. "frankenphp_testdata_index_php_worker_crashes",
  525. "frankenphp_testdata_index_php_worker_restarts",
  526. "frankenphp_testdata_index_php_ready_workers",
  527. ))
  528. }
  529. func TestAllDefinedServerVars(t *testing.T) {
  530. documentRoot, _ := filepath.Abs("../testdata/")
  531. expectedBodyFile, _ := os.ReadFile("../testdata/server-all-vars-ordered.txt")
  532. expectedBody := string(expectedBodyFile)
  533. expectedBody = strings.ReplaceAll(expectedBody, "{documentRoot}", documentRoot)
  534. expectedBody = strings.ReplaceAll(expectedBody, "\r\n", "\n")
  535. expectedBody = strings.ReplaceAll(expectedBody, "{testPort}", testPort)
  536. tester := caddytest.NewTester(t)
  537. tester.InitServer(`
  538. {
  539. skip_install_trust
  540. admin localhost:2999
  541. http_port `+testPort+`
  542. frankenphp
  543. }
  544. localhost:`+testPort+` {
  545. route {
  546. root ../testdata
  547. # rewrite to test that the original path is passed as $REQUEST_URI
  548. rewrite /server-all-vars-ordered.php/path
  549. php
  550. }
  551. }
  552. `, "caddyfile")
  553. tester.AssertPostResponseBody(
  554. "http://user@localhost:"+testPort+"/original-path?specialChars=%3E\\x00%00</>",
  555. []string{
  556. "Content-Type: application/x-www-form-urlencoded",
  557. "Content-Length: 14", // maliciously set to 14
  558. "Special-Chars: <%00>",
  559. "Host: Malicious Host",
  560. "X-Empty-Header:",
  561. },
  562. bytes.NewBufferString("foo=bar"),
  563. http.StatusOK,
  564. expectedBody,
  565. )
  566. }
  567. func TestPHPIniConfiguration(t *testing.T) {
  568. tester := caddytest.NewTester(t)
  569. tester.InitServer(`
  570. {
  571. skip_install_trust
  572. admin localhost:2999
  573. http_port `+testPort+`
  574. frankenphp {
  575. num_threads 2
  576. worker ../testdata/ini.php 1
  577. php_ini max_execution_time 100
  578. php_ini memory_limit 10000000
  579. }
  580. }
  581. localhost:`+testPort+` {
  582. route {
  583. root ../testdata
  584. php
  585. }
  586. }
  587. `, "caddyfile")
  588. testSingleIniConfiguration(tester, "max_execution_time", "100")
  589. testSingleIniConfiguration(tester, "memory_limit", "10000000")
  590. }
  591. func TestPHPIniBlockConfiguration(t *testing.T) {
  592. tester := caddytest.NewTester(t)
  593. tester.InitServer(`
  594. {
  595. skip_install_trust
  596. admin localhost:2999
  597. http_port `+testPort+`
  598. frankenphp {
  599. num_threads 1
  600. php_ini {
  601. max_execution_time 15
  602. memory_limit 20000000
  603. }
  604. }
  605. }
  606. localhost:`+testPort+` {
  607. route {
  608. root ../testdata
  609. php
  610. }
  611. }
  612. `, "caddyfile")
  613. testSingleIniConfiguration(tester, "max_execution_time", "15")
  614. testSingleIniConfiguration(tester, "memory_limit", "20000000")
  615. }
  616. func testSingleIniConfiguration(tester *caddytest.Tester, key string, value string) {
  617. // test twice to ensure the ini setting is not lost
  618. for i := 0; i < 2; i++ {
  619. tester.AssertGetResponse(
  620. "http://localhost:"+testPort+"/ini.php?key="+key,
  621. http.StatusOK,
  622. key+":"+value,
  623. )
  624. }
  625. }
  626. func TestOsEnv(t *testing.T) {
  627. os.Setenv("ENV1", "value1")
  628. os.Setenv("ENV2", "value2")
  629. tester := caddytest.NewTester(t)
  630. tester.InitServer(`
  631. {
  632. skip_install_trust
  633. admin localhost:2999
  634. http_port `+testPort+`
  635. frankenphp {
  636. num_threads 2
  637. php_ini variables_order "EGPCS"
  638. worker ../testdata/env/env.php 1
  639. }
  640. }
  641. localhost:`+testPort+` {
  642. route {
  643. root ../testdata
  644. php
  645. }
  646. }
  647. `, "caddyfile")
  648. tester.AssertGetResponse(
  649. "http://localhost:"+testPort+"/env/env.php?keys[]=ENV1&keys[]=ENV2",
  650. http.StatusOK,
  651. "ENV1=value1,ENV2=value2",
  652. )
  653. }