// 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"
	"internal/testenv"
	"io"
	"net"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"regexp"
	"runtime"
	"slices"
	"strings"
	"testing"
	"time"
)

// TestMain executes the test binary as the cgi server if
// SERVER_SOFTWARE is set, and runs the tests otherwise.
func TestMain(m *testing.M) {
	// SERVER_SOFTWARE swap variable is set when starting the cgi server.
	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)) {
	// 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)
	}
}

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)
}

// 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) {
	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)
}

// The CGI spec doesn't allow chunked requests.
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)
}

// TestCopyError tests that we kill the process if there's an error copying
// its output. (for example, from the client having gone away)
//
// If we fail to do so, the test will time out (and dump its goroutines) with a
// call to [Handler.ServeHTTP] blocked on a deferred call to [exec.Cmd.Wait].
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))
	}
}

// handlerRunning reports whether any goroutine is currently running
// [Handler.ServeHTTP].
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])
		}
		// Buffer wasn't large enough for a full goroutine dump.
		// Resize it and try again.
		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)
		}
	}
}