blob: 860e9b3e8f1b8f67a1b125b7b38e8fe2b5f8c3f5 [file] [log] [blame]
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Tests for package cgi
package cgi
import (
"bufio"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"reflect"
"runtime"
"strconv"
"strings"
"testing"
"time"
)
func newRequest(httpreq string) *http.Request {
buf := bufio.NewReader(strings.NewReader(httpreq))
req, err := http.ReadRequest(buf)
if err != nil {
panic("cgi: bogus http request in test: " + httpreq)
}
req.RemoteAddr = "1.2.3.4:1234"
return req
}
func runCgiTest(t *testing.T, h *Handler,
httpreq string,
expectedMap map[string]string, checks ...func(reqInfo map[string]string)) *httptest.ResponseRecorder {
rw := httptest.NewRecorder()
req := newRequest(httpreq)
h.ServeHTTP(rw, req)
runResponseChecks(t, rw, expectedMap, checks...)
return rw
}
func runResponseChecks(t *testing.T, rw *httptest.ResponseRecorder,
expectedMap map[string]string, checks ...func(reqInfo map[string]string)) {
// Make a map to hold the test map that the CGI returns.
m := make(map[string]string)
m["_body"] = rw.Body.String()
linesRead := 0
readlines:
for {
line, err := rw.Body.ReadString('\n')
switch {
case err == io.EOF:
break readlines
case err != nil:
t.Fatalf("unexpected error reading from CGI: %v", err)
}
linesRead++
trimmedLine := strings.TrimRight(line, "\r\n")
k, v, ok := strings.Cut(trimmedLine, "=")
if !ok {
t.Fatalf("Unexpected response from invalid line number %v: %q; existing map=%v",
linesRead, line, m)
}
m[k] = v
}
for key, expected := range expectedMap {
got := m[key]
if key == "cwd" {
// For Windows. golang.org/issue/4645.
fi1, _ := os.Stat(got)
fi2, _ := os.Stat(expected)
if os.SameFile(fi1, fi2) {
got = expected
}
}
if got != expected {
t.Errorf("for key %q got %q; expected %q", key, got, expected)
}
}
for _, check := range checks {
check(m)
}
}
var cgiTested, cgiWorks bool
func check(t *testing.T) {
if !cgiTested {
cgiTested = true
cgiWorks = exec.Command("./testdata/test.cgi").Run() == nil
}
if !cgiWorks {
// No Perl on Windows, needed by test.cgi
// TODO: make the child process be Go, not Perl.
t.Skip("Skipping test: test.cgi failed.")
}
}
func TestCGIBasicGet(t *testing.T) {
check(t)
h := &Handler{
Path: "testdata/test.cgi",
Root: "/test.cgi",
}
expectedMap := map[string]string{
"test": "Hello CGI",
"param-a": "b",
"param-foo": "bar",
"env-GATEWAY_INTERFACE": "CGI/1.1",
"env-HTTP_HOST": "example.com:80",
"env-PATH_INFO": "",
"env-QUERY_STRING": "foo=bar&a=b",
"env-REMOTE_ADDR": "1.2.3.4",
"env-REMOTE_HOST": "1.2.3.4",
"env-REMOTE_PORT": "1234",
"env-REQUEST_METHOD": "GET",
"env-REQUEST_URI": "/test.cgi?foo=bar&a=b",
"env-SCRIPT_FILENAME": "testdata/test.cgi",
"env-SCRIPT_NAME": "/test.cgi",
"env-SERVER_NAME": "example.com",
"env-SERVER_PORT": "80",
"env-SERVER_SOFTWARE": "go",
}
replay := runCgiTest(t, h, "GET /test.cgi?foo=bar&a=b HTTP/1.0\nHost: example.com:80\n\n", expectedMap)
if expected, got := "text/html", replay.Header().Get("Content-Type"); got != expected {
t.Errorf("got a Content-Type of %q; expected %q", got, expected)
}
if expected, got := "X-Test-Value", replay.Header().Get("X-Test-Header"); got != expected {
t.Errorf("got a X-Test-Header of %q; expected %q", got, expected)
}
}
func TestCGIEnvIPv6(t *testing.T) {
check(t)
h := &Handler{
Path: "testdata/test.cgi",
Root: "/test.cgi",
}
expectedMap := map[string]string{
"test": "Hello CGI",
"param-a": "b",
"param-foo": "bar",
"env-GATEWAY_INTERFACE": "CGI/1.1",
"env-HTTP_HOST": "example.com",
"env-PATH_INFO": "",
"env-QUERY_STRING": "foo=bar&a=b",
"env-REMOTE_ADDR": "2000::3000",
"env-REMOTE_HOST": "2000::3000",
"env-REMOTE_PORT": "12345",
"env-REQUEST_METHOD": "GET",
"env-REQUEST_URI": "/test.cgi?foo=bar&a=b",
"env-SCRIPT_FILENAME": "testdata/test.cgi",
"env-SCRIPT_NAME": "/test.cgi",
"env-SERVER_NAME": "example.com",
"env-SERVER_PORT": "80",
"env-SERVER_SOFTWARE": "go",
}
rw := httptest.NewRecorder()
req := newRequest("GET /test.cgi?foo=bar&a=b HTTP/1.0\nHost: example.com\n\n")
req.RemoteAddr = "[2000::3000]:12345"
h.ServeHTTP(rw, req)
runResponseChecks(t, rw, expectedMap)
}
func TestCGIBasicGetAbsPath(t *testing.T) {
check(t)
pwd, err := os.Getwd()
if err != nil {
t.Fatalf("getwd error: %v", err)
}
h := &Handler{
Path: pwd + "/testdata/test.cgi",
Root: "/test.cgi",
}
expectedMap := map[string]string{
"env-REQUEST_URI": "/test.cgi?foo=bar&a=b",
"env-SCRIPT_FILENAME": pwd + "/testdata/test.cgi",
"env-SCRIPT_NAME": "/test.cgi",
}
runCgiTest(t, h, "GET /test.cgi?foo=bar&a=b HTTP/1.0\nHost: example.com\n\n", expectedMap)
}
func TestPathInfo(t *testing.T) {
check(t)
h := &Handler{
Path: "testdata/test.cgi",
Root: "/test.cgi",
}
expectedMap := map[string]string{
"param-a": "b",
"env-PATH_INFO": "/extrapath",
"env-QUERY_STRING": "a=b",
"env-REQUEST_URI": "/test.cgi/extrapath?a=b",
"env-SCRIPT_FILENAME": "testdata/test.cgi",
"env-SCRIPT_NAME": "/test.cgi",
}
runCgiTest(t, h, "GET /test.cgi/extrapath?a=b HTTP/1.0\nHost: example.com\n\n", expectedMap)
}
func TestPathInfoDirRoot(t *testing.T) {
check(t)
h := &Handler{
Path: "testdata/test.cgi",
Root: "/myscript/",
}
expectedMap := map[string]string{
"env-PATH_INFO": "bar",
"env-QUERY_STRING": "a=b",
"env-REQUEST_URI": "/myscript/bar?a=b",
"env-SCRIPT_FILENAME": "testdata/test.cgi",
"env-SCRIPT_NAME": "/myscript/",
}
runCgiTest(t, h, "GET /myscript/bar?a=b HTTP/1.0\nHost: example.com\n\n", expectedMap)
}
func TestDupHeaders(t *testing.T) {
check(t)
h := &Handler{
Path: "testdata/test.cgi",
}
expectedMap := map[string]string{
"env-REQUEST_URI": "/myscript/bar?a=b",
"env-SCRIPT_FILENAME": "testdata/test.cgi",
"env-HTTP_COOKIE": "nom=NOM; yum=YUM",
"env-HTTP_X_FOO": "val1, val2",
}
runCgiTest(t, h, "GET /myscript/bar?a=b HTTP/1.0\n"+
"Cookie: nom=NOM\n"+
"Cookie: yum=YUM\n"+
"X-Foo: val1\n"+
"X-Foo: val2\n"+
"Host: example.com\n\n",
expectedMap)
}
// Issue 16405: CGI+http.Transport differing uses of HTTP_PROXY.
// Verify we don't set the HTTP_PROXY environment variable.
// Hope nobody was depending on it. It's not a known header, though.
func TestDropProxyHeader(t *testing.T) {
check(t)
h := &Handler{
Path: "testdata/test.cgi",
}
expectedMap := map[string]string{
"env-REQUEST_URI": "/myscript/bar?a=b",
"env-SCRIPT_FILENAME": "testdata/test.cgi",
"env-HTTP_X_FOO": "a",
}
runCgiTest(t, h, "GET /myscript/bar?a=b HTTP/1.0\n"+
"X-Foo: a\n"+
"Proxy: should_be_stripped\n"+
"Host: example.com\n\n",
expectedMap,
func(reqInfo map[string]string) {
if v, ok := reqInfo["env-HTTP_PROXY"]; ok {
t.Errorf("HTTP_PROXY = %q; should be absent", v)
}
})
}
func TestPathInfoNoRoot(t *testing.T) {
check(t)
h := &Handler{
Path: "testdata/test.cgi",
Root: "",
}
expectedMap := map[string]string{
"env-PATH_INFO": "/bar",
"env-QUERY_STRING": "a=b",
"env-REQUEST_URI": "/bar?a=b",
"env-SCRIPT_FILENAME": "testdata/test.cgi",
"env-SCRIPT_NAME": "/",
}
runCgiTest(t, h, "GET /bar?a=b HTTP/1.0\nHost: example.com\n\n", expectedMap)
}
func TestCGIBasicPost(t *testing.T) {
check(t)
postReq := `POST /test.cgi?a=b HTTP/1.0
Host: example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 15
postfoo=postbar`
h := &Handler{
Path: "testdata/test.cgi",
Root: "/test.cgi",
}
expectedMap := map[string]string{
"test": "Hello CGI",
"param-postfoo": "postbar",
"env-REQUEST_METHOD": "POST",
"env-CONTENT_LENGTH": "15",
"env-REQUEST_URI": "/test.cgi?a=b",
}
runCgiTest(t, h, postReq, expectedMap)
}
func chunk(s string) string {
return fmt.Sprintf("%x\r\n%s\r\n", len(s), s)
}
// The CGI spec doesn't allow chunked requests.
func TestCGIPostChunked(t *testing.T) {
check(t)
postReq := `POST /test.cgi?a=b HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
Transfer-Encoding: chunked
` + chunk("postfoo") + chunk("=") + chunk("postbar") + chunk("")
h := &Handler{
Path: "testdata/test.cgi",
Root: "/test.cgi",
}
expectedMap := map[string]string{}
resp := runCgiTest(t, h, postReq, expectedMap)
if got, expected := resp.Code, http.StatusBadRequest; got != expected {
t.Fatalf("Expected %v response code from chunked request body; got %d",
expected, got)
}
}
func TestRedirect(t *testing.T) {
check(t)
h := &Handler{
Path: "testdata/test.cgi",
Root: "/test.cgi",
}
rec := runCgiTest(t, h, "GET /test.cgi?loc=http://foo.com/ HTTP/1.0\nHost: example.com\n\n", nil)
if e, g := 302, rec.Code; e != g {
t.Errorf("expected status code %d; got %d", e, g)
}
if e, g := "http://foo.com/", rec.Header().Get("Location"); e != g {
t.Errorf("expected Location header of %q; got %q", e, g)
}
}
func TestInternalRedirect(t *testing.T) {
check(t)
baseHandler := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
fmt.Fprintf(rw, "basepath=%s\n", req.URL.Path)
fmt.Fprintf(rw, "remoteaddr=%s\n", req.RemoteAddr)
})
h := &Handler{
Path: "testdata/test.cgi",
Root: "/test.cgi",
PathLocationHandler: baseHandler,
}
expectedMap := map[string]string{
"basepath": "/foo",
"remoteaddr": "1.2.3.4:1234",
}
runCgiTest(t, h, "GET /test.cgi?loc=/foo HTTP/1.0\nHost: example.com\n\n", expectedMap)
}
// TestCopyError tests that we kill the process if there's an error copying
// its output. (for example, from the client having gone away)
func TestCopyError(t *testing.T) {
check(t)
if runtime.GOOS == "windows" {
t.Skipf("skipping test on %q", runtime.GOOS)
}
h := &Handler{
Path: "testdata/test.cgi",
Root: "/test.cgi",
}
ts := httptest.NewServer(h)
defer ts.Close()
conn, err := net.Dial("tcp", ts.Listener.Addr().String())
if err != nil {
t.Fatal(err)
}
req, _ := http.NewRequest("GET", "http://example.com/test.cgi?bigresponse=1", nil)
err = req.Write(conn)
if err != nil {
t.Fatalf("Write: %v", err)
}
res, err := http.ReadResponse(bufio.NewReader(conn), req)
if err != nil {
t.Fatalf("ReadResponse: %v", err)
}
pidstr := res.Header.Get("X-CGI-Pid")
if pidstr == "" {
t.Fatalf("expected an X-CGI-Pid header in response")
}
pid, err := strconv.Atoi(pidstr)
if err != nil {
t.Fatalf("invalid X-CGI-Pid value")
}
var buf [5000]byte
n, err := io.ReadFull(res.Body, buf[:])
if err != nil {
t.Fatalf("ReadFull: %d bytes, %v", n, err)
}
childRunning := func() bool {
return isProcessRunning(pid)
}
if !childRunning() {
t.Fatalf("pre-conn.Close, expected child to be running")
}
conn.Close()
tries := 0
for tries < 25 && childRunning() {
time.Sleep(50 * time.Millisecond * time.Duration(tries))
tries++
}
if childRunning() {
t.Fatalf("post-conn.Close, expected child to be gone")
}
}
func TestDirUnix(t *testing.T) {
check(t)
if runtime.GOOS == "windows" {
t.Skipf("skipping test on %q", runtime.GOOS)
}
cwd, _ := os.Getwd()
h := &Handler{
Path: "testdata/test.cgi",
Root: "/test.cgi",
Dir: cwd,
}
expectedMap := map[string]string{
"cwd": cwd,
}
runCgiTest(t, h, "GET /test.cgi HTTP/1.0\nHost: example.com\n\n", expectedMap)
cwd, _ = os.Getwd()
cwd = filepath.Join(cwd, "testdata")
h = &Handler{
Path: "testdata/test.cgi",
Root: "/test.cgi",
}
expectedMap = map[string]string{
"cwd": cwd,
}
runCgiTest(t, h, "GET /test.cgi HTTP/1.0\nHost: example.com\n\n", expectedMap)
}
func findPerl(t *testing.T) string {
t.Helper()
perl, err := exec.LookPath("perl")
if err != nil {
t.Skip("Skipping test: perl not found.")
}
perl, _ = filepath.Abs(perl)
cmd := exec.Command(perl, "-e", "print 123")
cmd.Env = []string{"PATH=/garbage"}
out, err := cmd.Output()
if err != nil || string(out) != "123" {
t.Skipf("Skipping test: %s is not functional", perl)
}
return perl
}
func TestDirWindows(t *testing.T) {
if runtime.GOOS != "windows" {
t.Skip("Skipping windows specific test.")
}
cgifile, _ := filepath.Abs("testdata/test.cgi")
perl := findPerl(t)
cwd, _ := os.Getwd()
h := &Handler{
Path: perl,
Root: "/test.cgi",
Dir: cwd,
Args: []string{cgifile},
Env: []string{"SCRIPT_FILENAME=" + cgifile},
}
expectedMap := map[string]string{
"cwd": cwd,
}
runCgiTest(t, h, "GET /test.cgi HTTP/1.0\nHost: example.com\n\n", expectedMap)
// If not specify Dir on windows, working directory should be
// base directory of perl.
cwd, _ = filepath.Split(perl)
if cwd != "" && cwd[len(cwd)-1] == filepath.Separator {
cwd = cwd[:len(cwd)-1]
}
h = &Handler{
Path: perl,
Root: "/test.cgi",
Args: []string{cgifile},
Env: []string{"SCRIPT_FILENAME=" + cgifile},
}
expectedMap = map[string]string{
"cwd": cwd,
}
runCgiTest(t, h, "GET /test.cgi HTTP/1.0\nHost: example.com\n\n", expectedMap)
}
func TestEnvOverride(t *testing.T) {
check(t)
cgifile, _ := filepath.Abs("testdata/test.cgi")
perl := findPerl(t)
cwd, _ := os.Getwd()
h := &Handler{
Path: perl,
Root: "/test.cgi",
Dir: cwd,
Args: []string{cgifile},
Env: []string{
"SCRIPT_FILENAME=" + cgifile,
"REQUEST_URI=/foo/bar",
"PATH=/wibble"},
}
expectedMap := map[string]string{
"cwd": cwd,
"env-SCRIPT_FILENAME": cgifile,
"env-REQUEST_URI": "/foo/bar",
"env-PATH": "/wibble",
}
runCgiTest(t, h, "GET /test.cgi HTTP/1.0\nHost: example.com\n\n", expectedMap)
}
func TestHandlerStderr(t *testing.T) {
check(t)
var stderr strings.Builder
h := &Handler{
Path: "testdata/test.cgi",
Root: "/test.cgi",
Stderr: &stderr,
}
rw := httptest.NewRecorder()
req := newRequest("GET /test.cgi?writestderr=1 HTTP/1.0\nHost: example.com\n\n")
h.ServeHTTP(rw, req)
if got, want := stderr.String(), "Hello, stderr!\n"; got != want {
t.Errorf("Stderr = %q; want %q", got, want)
}
}
func TestRemoveLeadingDuplicates(t *testing.T) {
tests := []struct {
env []string
want []string
}{
{
env: []string{"a=b", "b=c", "a=b2"},
want: []string{"b=c", "a=b2"},
},
{
env: []string{"a=b", "b=c", "d", "e=f"},
want: []string{"a=b", "b=c", "d", "e=f"},
},
}
for _, tt := range tests {
got := removeLeadingDuplicates(tt.env)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("removeLeadingDuplicates(%q) = %q; want %q", tt.env, got, tt.want)
}
}
}