| | |
| | |
| | |
| |
|
| | |
| |
|
| | package cgi |
| |
|
| | import ( |
| | "bufio" |
| | "fmt" |
| | "internal/testenv" |
| | "io" |
| | "net" |
| | "net/http" |
| | "net/http/httptest" |
| | "os" |
| | "path/filepath" |
| | "regexp" |
| | "runtime" |
| | "slices" |
| | "strings" |
| | "testing" |
| | "time" |
| | ) |
| |
|
| | |
| | |
| | func TestMain(m *testing.M) { |
| | |
| | if os.Getenv("SERVER_SOFTWARE") != "" { |
| | cgiMain() |
| | os.Exit(0) |
| | } |
| |
|
| | os.Exit(m.Run()) |
| | } |
| |
|
| | 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)) { |
| | |
| | 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" { |
| | |
| | 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) |
| | } |
| | } |
| |
|
| | func TestCGIBasicGet(t *testing.T) { |
| | testenv.MustHaveExec(t) |
| | h := &Handler{ |
| | Path: os.Args[0], |
| | 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": os.Args[0], |
| | "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) { |
| | testenv.MustHaveExec(t) |
| | h := &Handler{ |
| | Path: os.Args[0], |
| | 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": os.Args[0], |
| | "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) { |
| | absPath, err := filepath.Abs(os.Args[0]) |
| | if err != nil { |
| | t.Fatal(err) |
| | } |
| | testenv.MustHaveExec(t) |
| | h := &Handler{ |
| | Path: absPath, |
| | Root: "/test.cgi", |
| | } |
| | expectedMap := map[string]string{ |
| | "env-REQUEST_URI": "/test.cgi?foo=bar&a=b", |
| | "env-SCRIPT_FILENAME": absPath, |
| | "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) { |
| | testenv.MustHaveExec(t) |
| | h := &Handler{ |
| | Path: os.Args[0], |
| | 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": os.Args[0], |
| | "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) { |
| | testenv.MustHaveExec(t) |
| | h := &Handler{ |
| | Path: os.Args[0], |
| | 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": os.Args[0], |
| | "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) { |
| | testenv.MustHaveExec(t) |
| | h := &Handler{ |
| | Path: os.Args[0], |
| | } |
| | expectedMap := map[string]string{ |
| | "env-REQUEST_URI": "/myscript/bar?a=b", |
| | "env-SCRIPT_FILENAME": os.Args[0], |
| | "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) |
| | } |
| |
|
| | |
| | |
| | |
| | func TestDropProxyHeader(t *testing.T) { |
| | testenv.MustHaveExec(t) |
| | h := &Handler{ |
| | Path: os.Args[0], |
| | } |
| | expectedMap := map[string]string{ |
| | "env-REQUEST_URI": "/myscript/bar?a=b", |
| | "env-SCRIPT_FILENAME": os.Args[0], |
| | "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) { |
| | testenv.MustHaveExec(t) |
| | h := &Handler{ |
| | Path: os.Args[0], |
| | Root: "", |
| | } |
| | expectedMap := map[string]string{ |
| | "env-PATH_INFO": "/bar", |
| | "env-QUERY_STRING": "a=b", |
| | "env-REQUEST_URI": "/bar?a=b", |
| | "env-SCRIPT_FILENAME": os.Args[0], |
| | "env-SCRIPT_NAME": "", |
| | } |
| | runCgiTest(t, h, "GET /bar?a=b HTTP/1.0\nHost: example.com\n\n", expectedMap) |
| | } |
| |
|
| | func TestCGIBasicPost(t *testing.T) { |
| | testenv.MustHaveExec(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: os.Args[0], |
| | 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) |
| | } |
| |
|
| | |
| | func TestCGIPostChunked(t *testing.T) { |
| | testenv.MustHaveExec(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: os.Args[0], |
| | 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) { |
| | testenv.MustHaveExec(t) |
| | h := &Handler{ |
| | Path: os.Args[0], |
| | 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) { |
| | testenv.MustHaveExec(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: os.Args[0], |
| | 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) |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | func TestCopyError(t *testing.T) { |
| | testenv.MustHaveExec(t) |
| |
|
| | h := &Handler{ |
| | Path: os.Args[0], |
| | 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) |
| | } |
| | defer res.Body.Close() |
| | var buf [5000]byte |
| | n, err := io.ReadFull(res.Body, buf[:]) |
| | if err != nil { |
| | t.Fatalf("ReadFull: %d bytes, %v", n, err) |
| | } |
| |
|
| | if !handlerRunning() { |
| | t.Fatalf("pre-conn.Close, expected handler to still be running") |
| | } |
| | conn.Close() |
| | closed := time.Now() |
| |
|
| | nextSleep := 1 * time.Millisecond |
| | for { |
| | time.Sleep(nextSleep) |
| | nextSleep *= 2 |
| | if !handlerRunning() { |
| | break |
| | } |
| | t.Logf("handler still running %v after conn.Close", time.Since(closed)) |
| | } |
| | } |
| |
|
| | |
| | |
| | func handlerRunning() bool { |
| | r := regexp.MustCompile(`net/http/cgi\.\(\*Handler\)\.ServeHTTP`) |
| | buf := make([]byte, 64<<10) |
| | for { |
| | n := runtime.Stack(buf, true) |
| | if n < len(buf) { |
| | return r.Match(buf[:n]) |
| | } |
| | |
| | |
| | buf = make([]byte, 2*len(buf)) |
| | } |
| | } |
| |
|
| | func TestDir(t *testing.T) { |
| | testenv.MustHaveExec(t) |
| | cwd, _ := os.Getwd() |
| | h := &Handler{ |
| | Path: os.Args[0], |
| | 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.Split(os.Args[0]) |
| | h = &Handler{ |
| | Path: os.Args[0], |
| | 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 TestEnvOverride(t *testing.T) { |
| | testenv.MustHaveExec(t) |
| | cgifile, _ := filepath.Abs("testdata/test.cgi") |
| |
|
| | cwd, _ := os.Getwd() |
| | h := &Handler{ |
| | Path: os.Args[0], |
| | Root: "/test.cgi", |
| | Dir: cwd, |
| | 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) { |
| | testenv.MustHaveExec(t) |
| | var stderr strings.Builder |
| | h := &Handler{ |
| | Path: os.Args[0], |
| | 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 !slices.Equal(got, tt.want) { |
| | t.Errorf("removeLeadingDuplicates(%q) = %q; want %q", tt.env, got, tt.want) |
| | } |
| | } |
| | } |
| |
|