diff --git a/caddy/caddy.go b/caddy/caddy.go index aa01af76..a6f7aa6c 100644 --- a/caddy/caddy.go +++ b/caddy/caddy.go @@ -205,7 +205,7 @@ type FrankenPHPModule struct { // ResolveRootSymlink enables resolving the `root` directory to its actual value by evaluating a symbolic link, if one exists. ResolveRootSymlink *bool `json:"resolve_root_symlink,omitempty"` // Env sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables. - Env map[string]string `json:"env,omitempty"` + Env frankenphp.PreparedEnv `json:"env,omitempty"` logger *zap.Logger } @@ -256,7 +256,7 @@ func (f FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ ca documentRoot := repl.ReplaceKnown(f.Root, "") env := make(map[string]string, len(f.Env)+1) - env["REQUEST_URI"] = origReq.URL.RequestURI() + env["REQUEST_URI\x00"] = origReq.URL.RequestURI() for k, v := range f.Env { env[k] = repl.ReplaceKnown(v, "") } @@ -265,7 +265,7 @@ func (f FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ ca r, frankenphp.WithRequestDocumentRoot(documentRoot, *f.ResolveRootSymlink), frankenphp.WithRequestSplitPath(f.SplitPath), - frankenphp.WithRequestEnv(env), + frankenphp.WithRequestPreparedEnv(env), ) if err != nil { @@ -298,9 +298,9 @@ func (f *FrankenPHPModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return d.ArgErr() } if f.Env == nil { - f.Env = make(map[string]string) + f.Env = make(frankenphp.PreparedEnv) } - f.Env[args[0]] = args[1] + f.Env[args[0]+"\x00"] = args[1] case "resolve_root_symlink": if d.NextArg() { diff --git a/caddy/go.mod b/caddy/go.mod index ca53c3e5..cfd00649 100644 --- a/caddy/go.mod +++ b/caddy/go.mod @@ -47,6 +47,8 @@ require ( github.com/dgraph-io/ristretto v0.1.1 // indirect github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect github.com/dlclark/regexp2 v1.11.0 // indirect + github.com/dolthub/maphash v0.1.0 // indirect + github.com/dolthub/swiss v0.2.1 // indirect github.com/dunglas/httpsfv v1.0.2 // indirect github.com/dunglas/mercure v0.15.9 // indirect github.com/dunglas/vulcain v1.0.1 // indirect @@ -54,6 +56,7 @@ require ( github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fxamacker/cbor/v2 v2.5.0 // indirect + github.com/gammazero/deque v0.2.1 // indirect github.com/getkin/kin-openapi v0.122.0 // indirect github.com/go-chi/chi/v5 v5.0.10 // indirect github.com/go-kit/kit v0.13.0 // indirect @@ -105,6 +108,7 @@ require ( github.com/mastercactapus/proxyprotocol v0.0.4 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/maypok86/otter v1.1.1 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mholt/acmez v1.2.0 // indirect github.com/micromdm/scep/v2 v2.1.0 // indirect @@ -175,13 +179,13 @@ require ( go.step.sm/linkedca v0.20.1 // indirect go.uber.org/mock v0.4.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.20.0 // indirect + golang.org/x/crypto v0.21.0 // indirect golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect golang.org/x/mod v0.15.0 // indirect - golang.org/x/net v0.21.0 // indirect + golang.org/x/net v0.22.0 // indirect golang.org/x/sync v0.6.0 // indirect - golang.org/x/sys v0.17.0 // indirect - golang.org/x/term v0.17.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/term v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.18.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240228224816-df926f6c8641 // indirect diff --git a/caddy/go.sum b/caddy/go.sum index 73a3d055..e22cd5dd 100644 --- a/caddy/go.sum +++ b/caddy/go.sum @@ -106,6 +106,10 @@ github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55k github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ= +github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4= +github.com/dolthub/swiss v0.2.1 h1:gs2osYs5SJkAaH5/ggVJqXQxRXtWshF6uE0lgR/Y3Gw= +github.com/dolthub/swiss v0.2.1/go.mod h1:8AhKZZ1HK7g18j7v7k6c5cYIGEZJcPn0ARsai8cUrh0= github.com/dunglas/caddy-cbrotli v1.0.0 h1:+WNqXBkWyMcIpXB2rVZ3nwcElUbuAzf0kPxNXU4D+u0= github.com/dunglas/caddy-cbrotli v1.0.0/go.mod h1:KZsUu3fnQBgO0o3YDoQuO3Z61dFgUncr1F8rg8acwQw= github.com/dunglas/httpsfv v1.0.2 h1:iERDp/YAfnojSDJ7PW3dj1AReJz4MrwbECSSE59JWL0= @@ -130,6 +134,8 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE= github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= +github.com/gammazero/deque v0.2.1 h1:qSdsbG6pgp6nL7A0+K/B7s12mcCY/5l5SIUpMOl+dC0= +github.com/gammazero/deque v0.2.1/go.mod h1:LFroj8x4cMYCukHJDbxFCkT+r9AndaJnFMuZDV34tuU= github.com/getkin/kin-openapi v0.122.0 h1:WB9Jbl0Hp/T79/JF9xlSW5Kl9uYdk/AWD0yAd9HOM10= github.com/getkin/kin-openapi v0.122.0/go.mod h1:PCWw/lfBrJY4HcdqE3jj+QFkaFK8ABoqo7PvqVhXXqw= github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= @@ -339,6 +345,8 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/maypok86/otter v1.1.1 h1:1WyOfBz2m8bjc4dAmJeUSuyUshoTP8ViiYmvRWO+53w= +github.com/maypok86/otter v1.1.1/go.mod h1:IuSnpxeUyjKPPjqGzhGKOO26tedMNl45vwGcdXsEi8U= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mholt/acmez v1.2.0 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30= @@ -589,8 +597,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg= -golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ= golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -608,8 +616,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ= golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -641,15 +649,15 @@ golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/cgi.go b/cgi.go index 05901820..4fe3c009 100644 --- a/cgi.go +++ b/cgi.go @@ -1,12 +1,15 @@ package frankenphp +// #include "frankenphp.h" import "C" import ( "crypto/tls" "net" "net/http" "path/filepath" + "runtime" "strings" + "unsafe" ) type serverKey int @@ -33,22 +36,44 @@ const ( sslProtocol ) -func allocServerVariable(cArr *[27]*C.char, env map[string]string, serverKey serverKey, envKey string, val string) { - if val, ok := env[envKey]; ok { - cArr[serverKey] = C.CString(val) - delete(env, envKey) +var knownServerKeys = map[string]struct{}{ + "CONTENT_LENGTH\x00": {}, + "DOCUMENT_ROOT\x00": {}, + "DOCUMENT_URI\x00": {}, + "GATEWAY_INTERFACE\x00": {}, + "HTTP_HOST\x00": {}, + "HTTPS\x00": {}, + "PATH_INFO\x00": {}, + "PHP_SELF\x00": {}, + "REMOTE_ADDR\x00": {}, + "REMOTE_HOST\x00": {}, + "REMOTE_PORT\x00": {}, + "REQUEST_SCHEME\x00": {}, + "SCRIPT_FILENAME\x00": {}, + "SCRIPT_NAME\x00": {}, + "SERVER_NAME\x00": {}, + "SERVER_PORT\x00": {}, + "SERVER_PROTOCOL\x00": {}, + "SERVER_SOFTWARE\x00": {}, + "SSL_PROTOCOL\x00": {}, +} +func setKnownServerVariable(p *runtime.Pinner, cArr *[27]C.go_string, serverKey serverKey, val string) { + if val == "" { return } - cArr[serverKey] = C.CString(val) + valData := unsafe.StringData(val) + p.Pin(valData) + cArr[serverKey].len = C.size_t(len(val)) + cArr[serverKey].data = (*C.char)(unsafe.Pointer(valData)) } // computeKnownVariables returns a set of CGI environment variables for the request. // // TODO: handle this case https://github.com/caddyserver/caddy/issues/3718 // Inspired by https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go -func computeKnownVariables(request *http.Request) (cArr [27]*C.char) { +func computeKnownVariables(request *http.Request, p *runtime.Pinner) (cArr [27]C.go_string) { fc, fcOK := FromContext(request.Context()) if !fcOK { panic("not a FrankenPHP request") @@ -67,32 +92,30 @@ func computeKnownVariables(request *http.Request) (cArr [27]*C.char) { ip = strings.Replace(ip, "[", "", 1) ip = strings.Replace(ip, "]", "", 1) - ra, raOK := fc.env["REMOTE_ADDR"] + ra, raOK := fc.env["REMOTE_ADDR\x00"] if raOK { - cArr[remoteAddr] = C.CString(ra) - delete(fc.env, "REMOTE_ADDR") + setKnownServerVariable(p, &cArr, remoteAddr, ra) } else { - cArr[remoteAddr] = C.CString(ip) + setKnownServerVariable(p, &cArr, remoteAddr, ip) } - if rh, ok := fc.env["REMOTE_HOST"]; ok { - cArr[remoteHost] = C.CString(rh) // For speed, remote host lookups disabled - delete(fc.env, "REMOTE_HOST") + if rh, ok := fc.env["REMOTE_HOST\x00"]; ok { + setKnownServerVariable(p, &cArr, remoteHost, rh) // For speed, remote host lookups disabled } else { if raOK { - cArr[remoteHost] = C.CString(ip) + setKnownServerVariable(p, &cArr, remoteHost, ip) } else { cArr[remoteHost] = cArr[remoteAddr] } } - allocServerVariable(&cArr, fc.env, remotePort, "REMOTE_PORT", port) - allocServerVariable(&cArr, fc.env, documentRoot, "DOCUMENT_ROOT", fc.documentRoot) - allocServerVariable(&cArr, fc.env, pathInfo, "PATH_INFO", fc.pathInfo) - allocServerVariable(&cArr, fc.env, phpSelf, "PHP_SELF", request.URL.Path) - allocServerVariable(&cArr, fc.env, documentUri, "DOCUMENT_URI", fc.docURI) - allocServerVariable(&cArr, fc.env, scriptFilename, "SCRIPT_FILENAME", fc.scriptFilename) - allocServerVariable(&cArr, fc.env, scriptName, "SCRIPT_NAME", fc.scriptName) + setKnownServerVariable(p, &cArr, remotePort, port) + setKnownServerVariable(p, &cArr, documentRoot, fc.documentRoot) + setKnownServerVariable(p, &cArr, pathInfo, fc.pathInfo) + setKnownServerVariable(p, &cArr, phpSelf, request.URL.Path) + setKnownServerVariable(p, &cArr, documentUri, fc.docURI) + setKnownServerVariable(p, &cArr, scriptFilename, fc.scriptFilename) + setKnownServerVariable(p, &cArr, scriptName, fc.scriptName) var rs string if request.TLS == nil { @@ -100,26 +123,24 @@ func computeKnownVariables(request *http.Request) (cArr [27]*C.char) { } else { rs = "https" - if h, ok := fc.env["HTTPS"]; ok { - cArr[https] = C.CString(h) - delete(fc.env, "HTTPS") + if h, ok := fc.env["HTTPS\x00"]; ok { + setKnownServerVariable(p, &cArr, https, h) } else { - cArr[https] = C.CString("on") + setKnownServerVariable(p, &cArr, https, "on") } // and pass the protocol details in a manner compatible with apache's mod_ssl // (which is why these have a SSL_ prefix and not TLS_). - if p, ok := fc.env["SSL_PROTOCOL"]; ok { - cArr[sslProtocol] = C.CString(p) - delete(fc.env, "SSL_PROTOCOL") + if pr, ok := fc.env["SSL_PROTOCOL\x00"]; ok { + setKnownServerVariable(p, &cArr, sslProtocol, pr) } else { if v, ok := tlsProtocolStrings[request.TLS.Version]; ok { - cArr[sslProtocol] = C.CString(v) + setKnownServerVariable(p, &cArr, sslProtocol, v) } } } - allocServerVariable(&cArr, fc.env, requestScheme, "REQUEST_SCHEME", rs) + setKnownServerVariable(p, &cArr, requestScheme, rs) reqHost, reqPort, _ := net.SplitHostPort(request.Host) if reqHost == "" { @@ -140,9 +161,9 @@ func computeKnownVariables(request *http.Request) (cArr [27]*C.char) { } } - allocServerVariable(&cArr, fc.env, serverName, "SERVER_NAME", reqHost) + setKnownServerVariable(p, &cArr, serverName, reqHost) if reqPort != "" { - allocServerVariable(&cArr, fc.env, serverPort, "SERVER_PORT", reqPort) + setKnownServerVariable(p, &cArr, serverPort, reqPort) } // Variables defined in CGI 1.1 spec @@ -150,12 +171,11 @@ func computeKnownVariables(request *http.Request) (cArr [27]*C.char) { // the parent environment from interfering. // These values can not be override - cArr[contentLength] = C.CString(request.Header.Get("Content-Length")) - - allocServerVariable(&cArr, fc.env, gatewayInterface, "GATEWAY_INTERFACE", "CGI/1.1") - allocServerVariable(&cArr, fc.env, serverProtocol, "SERVER_PROTOCOL", request.Proto) - allocServerVariable(&cArr, fc.env, serverSoftware, "SERVER_SOFTWARE", "FrankenPHP") - allocServerVariable(&cArr, fc.env, httpHost, "HTTP_HOST", request.Host) // added here, since not always part of headers + setKnownServerVariable(p, &cArr, contentLength, request.Header.Get("Content-Length")) + setKnownServerVariable(p, &cArr, gatewayInterface, "CGI/1.1") + setKnownServerVariable(p, &cArr, serverProtocol, request.Proto) + setKnownServerVariable(p, &cArr, serverSoftware, "FrankenPHP") + setKnownServerVariable(p, &cArr, httpHost, request.Host) // added here, since not always part of headers return } diff --git a/frankenphp.c b/frankenphp.c index bed5ed50..d783e382 100644 --- a/frankenphp.c +++ b/frankenphp.c @@ -433,6 +433,7 @@ static uintptr_t frankenphp_request_shutdown() { free(ctx); SG(server_context) = NULL; + ctx = NULL; #if defined(ZTS) ts_free_thread(); @@ -572,8 +573,23 @@ static char *frankenphp_read_cookies(void) { return ctx->cookie_data; } -static void frankenphp_register_known_variable(const char *key, char *value, - zval *track_vars_array, bool f) { +static void frankenphp_register_known_variable(const char *key, go_string value, + zval *track_vars_array) { + if (value.data == NULL) { + php_register_variable_safe(key, "", 0, track_vars_array); + return; + } + + size_t new_val_len; + if (sapi_module.input_filter(PARSE_SERVER, key, &value.data, value.len, + &new_val_len)) { + php_register_variable_safe(key, value.data, new_val_len, track_vars_array); + } +} + +static void +frankenphp_register_variable_from_request_info(const char *key, char *value, + zval *track_vars_array) { if (value == NULL) { return; } @@ -583,91 +599,80 @@ static void frankenphp_register_known_variable(const char *key, char *value, &new_val_len)) { php_register_variable_safe(key, value, new_val_len, track_vars_array); } - - if (f) { - free(value); - value = NULL; - } } -void frankenphp_register_bulk_variables(char *known_variables[27], - char **dynamic_variables, size_t size, - zval *track_vars_array) { +void frankenphp_register_bulk_variables(go_string known_variables[27], + php_variable *dynamic_variables, + size_t size, zval *track_vars_array) { /* Not used, but must be present */ - frankenphp_register_known_variable("AUTH_TYPE", "", track_vars_array, false); - frankenphp_register_known_variable("REMOTE_IDENT", "", track_vars_array, - false); + php_register_variable_safe("AUTH_TYPE", "", 0, track_vars_array); + php_register_variable_safe("REMOTE_IDENT", "", 0, track_vars_array); /* Allocated in frankenphp_update_server_context() */ - frankenphp_register_known_variable("CONTENT_TYPE", - (char *)SG(request_info).content_type, - track_vars_array, false); - frankenphp_register_known_variable("PATH_TRANSLATED", - (char *)SG(request_info).path_translated, - track_vars_array, false); - frankenphp_register_known_variable( - "QUERY_STRING", SG(request_info).query_string, track_vars_array, false); - frankenphp_register_known_variable("REMOTE_USER", - (char *)SG(request_info).auth_user, - track_vars_array, false); - frankenphp_register_known_variable("REQUEST_METHOD", - (char *)SG(request_info).request_method, - track_vars_array, false); - frankenphp_register_known_variable( - "REQUEST_URI", SG(request_info).request_uri, track_vars_array, false); + frankenphp_register_variable_from_request_info( + "CONTENT_TYPE", (char *)SG(request_info).content_type, track_vars_array); + frankenphp_register_variable_from_request_info( + "PATH_TRANSLATED", (char *)SG(request_info).path_translated, + track_vars_array); + frankenphp_register_variable_from_request_info( + "QUERY_STRING", SG(request_info).query_string, track_vars_array); + frankenphp_register_variable_from_request_info( + "REMOTE_USER", (char *)SG(request_info).auth_user, track_vars_array); + frankenphp_register_variable_from_request_info( + "REQUEST_METHOD", (char *)SG(request_info).request_method, + track_vars_array); + frankenphp_register_variable_from_request_info( + "REQUEST_URI", SG(request_info).request_uri, track_vars_array); /* Known variables */ frankenphp_register_known_variable("CONTENT_LENGTH", known_variables[0], - track_vars_array, true); + track_vars_array); frankenphp_register_known_variable("DOCUMENT_ROOT", known_variables[1], - track_vars_array, true); + track_vars_array); frankenphp_register_known_variable("DOCUMENT_URI", known_variables[2], - track_vars_array, true); + track_vars_array); frankenphp_register_known_variable("GATEWAY_INTERFACE", known_variables[3], - track_vars_array, true); + track_vars_array); frankenphp_register_known_variable("HTTP_HOST", known_variables[4], - track_vars_array, true); + track_vars_array); frankenphp_register_known_variable("HTTPS", known_variables[5], - track_vars_array, true); + track_vars_array); frankenphp_register_known_variable("PATH_INFO", known_variables[6], - track_vars_array, true); + track_vars_array); frankenphp_register_known_variable("PHP_SELF", known_variables[7], - track_vars_array, true); + track_vars_array); frankenphp_register_known_variable("REMOTE_ADDR", known_variables[8], - track_vars_array, - known_variables[8] != known_variables[9]); + track_vars_array); frankenphp_register_known_variable("REMOTE_HOST", known_variables[9], - track_vars_array, true); + track_vars_array); frankenphp_register_known_variable("REMOTE_PORT", known_variables[10], - track_vars_array, true); + track_vars_array); frankenphp_register_known_variable("REQUEST_SCHEME", known_variables[11], - track_vars_array, true); + track_vars_array); frankenphp_register_known_variable("SCRIPT_FILENAME", known_variables[12], - track_vars_array, true); + track_vars_array); frankenphp_register_known_variable("SCRIPT_NAME", known_variables[13], - track_vars_array, true); + track_vars_array); frankenphp_register_known_variable("SERVER_NAME", known_variables[14], - track_vars_array, true); + track_vars_array); frankenphp_register_known_variable("SERVER_PORT", known_variables[15], - track_vars_array, true); + track_vars_array); frankenphp_register_known_variable("SERVER_PROTOCOL", known_variables[16], - track_vars_array, true); + track_vars_array); frankenphp_register_known_variable("SERVER_SOFTWARE", known_variables[17], - track_vars_array, true); + track_vars_array); frankenphp_register_known_variable("SSL_PROTOCOL", known_variables[18], - track_vars_array, true); + track_vars_array); size_t new_val_len; - for (size_t i = 0; i < size; i = i + 2) { - if (sapi_module.input_filter( - PARSE_SERVER, dynamic_variables[i], &dynamic_variables[i + 1], - strlen(dynamic_variables[i + 1]), &new_val_len)) { - php_register_variable_safe(dynamic_variables[i], dynamic_variables[i + 1], - new_val_len, track_vars_array); + for (size_t i = 0; i < size; i++) { + if (sapi_module.input_filter(PARSE_SERVER, dynamic_variables[i].var, + &dynamic_variables[i].data, + dynamic_variables[i].data_len, &new_val_len)) { + php_register_variable_safe(dynamic_variables[i].var, + dynamic_variables[i].data, new_val_len, + track_vars_array); } - - free(dynamic_variables[i]); - free(dynamic_variables[i + 1]); } } @@ -746,6 +751,7 @@ static void *manager_thread(void *arg) { threadpool thpool = thpool_init(*((int *)arg)); free(arg); + arg = NULL; uintptr_t rh; while ((rh = go_fetch_request())) { @@ -799,6 +805,7 @@ int frankenphp_request_startup() { frankenphp_server_context *ctx = SG(server_context); SG(server_context) = NULL; free(ctx); + ctx = NULL; php_request_shutdown((void *)0); @@ -808,6 +815,7 @@ int frankenphp_request_startup() { int frankenphp_execute_script(char *file_name) { if (frankenphp_request_startup() == FAILURE) { free(file_name); + file_name = NULL; return FAILURE; } @@ -817,6 +825,7 @@ int frankenphp_execute_script(char *file_name) { zend_file_handle file_handle; zend_stream_init_filename(&file_handle, file_name); free(file_name); + file_name = NULL; file_handle.primary_script = 1; diff --git a/frankenphp.go b/frankenphp.go index 9fe685f7..7589c265 100644 --- a/frankenphp.go +++ b/frankenphp.go @@ -47,6 +47,7 @@ import ( "sync" "unsafe" + "github.com/maypok86/otter" "go.uber.org/zap" // debug on Linux //_ "github.com/ianlancetaylor/cgosymbolizer" @@ -115,7 +116,7 @@ func (l syslogLevel) String() string { type FrankenPHPContext struct { documentRoot string splitPath []string - env map[string]string + env PreparedEnv logger *zap.Logger docURI string @@ -539,44 +540,81 @@ func go_ub_write(rh C.uintptr_t, cBuf *C.char, length C.int) (C.size_t, C.bool) return C.size_t(i), C.bool(clientHasClosed(r)) } +func createHeaderKeyCache() otter.Cache[string, string] { + c, err := otter.MustBuilder[string, string](256).Build() + if err != nil { + panic(err) + } + + return c +} + +var headerKeyCache = createHeaderKeyCache() + +// There are around 60 common request headers according to https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Request_fields +// Give some space for custom headers + //export go_register_variables func go_register_variables(rh C.uintptr_t, trackVarsArray *C.zval) { r := cgo.Handle(rh).Value().(*http.Request) fc := r.Context().Value(contextKey).(*FrankenPHPContext) - le := (len(fc.env) + len(r.Header)) * 2 - dynamicVariables := make([]*C.char, le) + p := &runtime.Pinner{} + + dynamicVariables := make([]C.php_variable, len(fc.env)+len(r.Header)) + p.Pin(unsafe.SliceData(dynamicVariables)) + + var l int - var i int // Add all HTTP headers to env variables for field, val := range r.Header { - k := "HTTP_" + headerNameReplacer.Replace(strings.ToUpper(field)) + k, ok := headerKeyCache.Get(field) + if !ok { + k = "HTTP_" + headerNameReplacer.Replace(strings.ToUpper(field)) + "\x00" + headerKeyCache.SetIfAbsent(field, k) + } + if _, ok := fc.env[k]; ok { continue } - dynamicVariables[i] = C.CString(k) - i++ + v := strings.Join(val, ", ") - dynamicVariables[i] = C.CString(strings.Join(val, ", ")) - i++ + kData := unsafe.StringData(k) + vData := unsafe.StringData(v) + + p.Pin(kData) + p.Pin(vData) + + dynamicVariables[l]._var = (*C.char)(unsafe.Pointer(kData)) + dynamicVariables[l].data_len = C.size_t(len(v)) + dynamicVariables[l].data = (*C.char)(unsafe.Pointer(vData)) + + l++ } for k, v := range fc.env { - dynamicVariables[i] = C.CString(k) - i++ + if _, ok := knownServerKeys[k]; ok { + continue + } - dynamicVariables[i] = C.CString(v) - i++ + kData := unsafe.StringData(k) + vData := unsafe.Pointer(unsafe.StringData(v)) + + p.Pin(kData) + p.Pin(vData) + + dynamicVariables[l]._var = (*C.char)(unsafe.Pointer(kData)) + dynamicVariables[l].data_len = C.size_t(len(v)) + dynamicVariables[l].data = (*C.char)(unsafe.Pointer(vData)) + + l++ } - var dynamicVariablesPtr **C.char = nil - if le > 0 { - dynamicVariablesPtr = &dynamicVariables[0] - } + knownVariables := computeKnownVariables(r, p) + C.frankenphp_register_bulk_variables(&knownVariables[0], unsafe.SliceData(dynamicVariables), C.size_t(l), trackVarsArray) - knownVariables := computeKnownVariables(r) - C.frankenphp_register_bulk_variables(&knownVariables[0], dynamicVariablesPtr, C.size_t(le), trackVarsArray) + p.Unpin() fc.env = nil } diff --git a/frankenphp.h b/frankenphp.h index 5364e9de..8cb3761b 100644 --- a/frankenphp.h +++ b/frankenphp.h @@ -13,9 +13,15 @@ typedef struct go_string { size_t len; - const char *data; + char *data; } go_string; +typedef struct php_variable { + const char *var; + size_t data_len; + char *data; +} php_variable; + typedef struct frankenphp_version { unsigned char major_version; unsigned char minor_version; @@ -44,9 +50,9 @@ int frankenphp_update_server_context( char *auth_user, char *auth_password, int proto_num); int frankenphp_request_startup(); int frankenphp_execute_script(char *file_name); -void frankenphp_register_bulk_variables(char *known_variables[27], - char **dynamic_variables, size_t size, - zval *track_vars_array); +void frankenphp_register_bulk_variables(go_string known_variables[27], + php_variable *dynamic_variables, + size_t size, zval *track_vars_array); int frankenphp_execute_script_cli(char *script, int argc, char **argv); diff --git a/frankenphp_test.go b/frankenphp_test.go index 690e1938..43120607 100644 --- a/frankenphp_test.go +++ b/frankenphp_test.go @@ -1,3 +1,7 @@ +// In all tests, headers added to requests are copied on the heap using strings.Clone. +// This was originally a workaround for https://github.com/golang/go/issues/65286#issuecomment-1920087884 (fixed in Go 1.22), +// but this allows to catch panics occuring in real life but not when the string is in the internal binary memory. + package frankenphp_test import ( @@ -128,8 +132,8 @@ func TestServerVariable_worker(t *testing.T) { func testServerVariable(t *testing.T, opts *testOptions) { runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) { 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")) - req.SetBasicAuth("kevin", "password") - req.Header.Add("Content-Type", "text/plain") + req.SetBasicAuth(strings.Clone("kevin"), strings.Clone("password")) + req.Header.Add(strings.Clone("Content-Type"), strings.Clone("text/plain")) w := httptest.NewRecorder() handler(w, req) @@ -171,13 +175,14 @@ func TestPathInfo_worker(t *testing.T) { testPathInfo(t, &testOptions{workerScript: "server-variable.php"}) } func testPathInfo(t *testing.T, opts *testOptions) { + cwd, _ := os.Getwd() + testDataDir := cwd + strings.Clone("/testdata/") + path := strings.Clone("/server-variable.php/pathinfo") + runTest(t, func(_ func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) { handler := func(w http.ResponseWriter, r *http.Request) { - cwd, _ := os.Getwd() - testDataDir := cwd + "/testdata/" - requestURI := r.URL.RequestURI() - r.URL.Path = "/server-variable.php/pathinfo" + r.URL.Path = path rewriteRequest, err := frankenphp.NewRequestWithContext(r, frankenphp.WithRequestDocumentRoot(testDataDir, false), @@ -271,7 +276,7 @@ func testPostSuperGlobals(t *testing.T, opts *testOptions) { runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) { formData := url.Values{"baz": {"bat"}, "i": {fmt.Sprintf("%d", i)}} req := httptest.NewRequest("POST", fmt.Sprintf("http://example.com/super-globals.php?foo=bar&iG=%d", i), strings.NewReader(formData.Encode())) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Content-Type", strings.Clone("application/x-www-form-urlencoded")) w := httptest.NewRecorder() handler(w, req) @@ -785,8 +790,10 @@ func BenchmarkServerSuperGlobal(b *testing.B) { "PHP_SHA256": "4ffa3e44afc9c590e28dc0d2d31fc61f0139f8b335f11880a121b9f9b9f0634e", } + preparedEnv := frankenphp.PrepareEnv(env) + handler := func(w http.ResponseWriter, r *http.Request) { - req, err := frankenphp.NewRequestWithContext(r, frankenphp.WithRequestDocumentRoot(testDataDir, false), frankenphp.WithRequestEnv(env)) + req, err := frankenphp.NewRequestWithContext(r, frankenphp.WithRequestDocumentRoot(testDataDir, false), frankenphp.WithRequestPreparedEnv(preparedEnv)) if err != nil { panic(err) } diff --git a/go.mod b/go.mod index 2e4edb2f..a374fb9a 100644 --- a/go.mod +++ b/go.mod @@ -7,13 +7,17 @@ toolchain go1.22.0 retract v1.0.0-rc.1 // Human error require ( + github.com/maypok86/otter v1.1.1 github.com/stretchr/testify v1.9.0 go.uber.org/zap v1.27.0 - golang.org/x/net v0.21.0 + golang.org/x/net v0.22.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dolthub/maphash v0.1.0 // indirect + github.com/dolthub/swiss v0.2.1 // indirect + github.com/gammazero/deque v0.2.1 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect diff --git a/go.sum b/go.sum index 32cd4c8a..71614710 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,12 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ= +github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4= +github.com/dolthub/swiss v0.2.1 h1:gs2osYs5SJkAaH5/ggVJqXQxRXtWshF6uE0lgR/Y3Gw= +github.com/dolthub/swiss v0.2.1/go.mod h1:8AhKZZ1HK7g18j7v7k6c5cYIGEZJcPn0ARsai8cUrh0= +github.com/gammazero/deque v0.2.1 h1:qSdsbG6pgp6nL7A0+K/B7s12mcCY/5l5SIUpMOl+dC0= +github.com/gammazero/deque v0.2.1/go.mod h1:LFroj8x4cMYCukHJDbxFCkT+r9AndaJnFMuZDV34tuU= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -8,6 +14,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/maypok86/otter v1.1.1 h1:1WyOfBz2m8bjc4dAmJeUSuyUshoTP8ViiYmvRWO+53w= +github.com/maypok86/otter v1.1.1/go.mod h1:IuSnpxeUyjKPPjqGzhGKOO26tedMNl45vwGcdXsEi8U= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -22,8 +30,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/options.go b/options.go index 13c22c98..0c5622b8 100644 --- a/options.go +++ b/options.go @@ -19,7 +19,7 @@ type opt struct { type workerOpt struct { fileName string num int - env map[string]string + env PreparedEnv } // WithNumThreads configures the number of PHP threads to start. @@ -34,7 +34,7 @@ func WithNumThreads(numThreads int) Option { // WithWorkers configures the PHP workers to start. func WithWorkers(fileName string, num int, env map[string]string) Option { return func(o *opt) error { - o.workers = append(o.workers, workerOpt{fileName, num, env}) + o.workers = append(o.workers, workerOpt{fileName, num, PrepareEnv(env)}) return nil } diff --git a/request_options.go b/request_options.go index 54521462..727f8652 100644 --- a/request_options.go +++ b/request_options.go @@ -52,9 +52,24 @@ func WithRequestSplitPath(splitPath []string) RequestOption { } } +type PreparedEnv = map[string]string + +func PrepareEnv(env map[string]string) PreparedEnv { + preparedEnv := make(PreparedEnv, len(env)) + for k, v := range env { + preparedEnv[k+"\x00"] = v + } + + return preparedEnv +} + // WithEnv set CGI-like environment variables that will be available in $_SERVER. // Values set with WithEnv always have priority over automatically populated values. func WithRequestEnv(env map[string]string) RequestOption { + return WithRequestPreparedEnv(PrepareEnv(env)) +} + +func WithRequestPreparedEnv(env PreparedEnv) RequestOption { return func(o *FrankenPHPContext) error { o.env = env diff --git a/worker.go b/worker.go index d1d2470b..b858cf3e 100644 --- a/worker.go +++ b/worker.go @@ -30,7 +30,7 @@ func initWorkers(opt []workerOpt) error { return nil } -func startWorkers(fileName string, nbWorkers int, env map[string]string) error { +func startWorkers(fileName string, nbWorkers int, env PreparedEnv) error { absFileName, err := filepath.Abs(fileName) if err != nil { return fmt.Errorf("workers %q: %w", fileName, err) @@ -50,10 +50,10 @@ func startWorkers(fileName string, nbWorkers int, env map[string]string) error { ) if env == nil { - env = make(map[string]string, 1) + env = make(PreparedEnv, 1) } - env["FRANKENPHP_WORKER"] = "1" + env["FRANKENPHP_WORKER\x00"] = "1\x00" l := getLogger() for i := 0; i < nbWorkers; i++ { @@ -68,7 +68,7 @@ func startWorkers(fileName string, nbWorkers int, env map[string]string) error { r, err = NewRequestWithContext( r, WithRequestDocumentRoot(filepath.Dir(absFileName), false), - WithRequestEnv(env), + WithRequestPreparedEnv(env), ) if err != nil { panic(err) diff --git a/worker_test.go b/worker_test.go index 9bd844d1..8341ce09 100644 --- a/worker_test.go +++ b/worker_test.go @@ -18,7 +18,7 @@ func TestWorker(t *testing.T) { runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) { formData := url.Values{"baz": {"bat"}} req := httptest.NewRequest("POST", "http://example.com/worker.php?foo=bar", strings.NewReader(formData.Encode())) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Content-Type", strings.Clone("application/x-www-form-urlencoded")) w := httptest.NewRecorder() handler(w, req) @@ -29,7 +29,7 @@ func TestWorker(t *testing.T) { formData2 := url.Values{"baz2": {"bat2"}} req2 := httptest.NewRequest("POST", "http://example.com/worker.php?foo2=bar2", strings.NewReader(formData2.Encode())) - req2.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req2.Header.Set("Content-Type", strings.Clone("application/x-www-form-urlencoded")) w2 := httptest.NewRecorder() handler(w2, req2)