1// Copyright 2011 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5// Tests for package cgi
6
7package cgi
8
9import (
10	"bufio"
11	"fmt"
12	"internal/testenv"
13	"io"
14	"net"
15	"net/http"
16	"net/http/httptest"
17	"os"
18	"path/filepath"
19	"reflect"
20	"regexp"
21	"runtime"
22	"strings"
23	"testing"
24	"time"
25)
26
27// TestMain executes the test binary as the cgi server if
28// SERVER_SOFTWARE is set, and runs the tests otherwise.
29func TestMain(m *testing.M) {
30	// SERVER_SOFTWARE swap variable is set when starting the cgi server.
31	if os.Getenv("SERVER_SOFTWARE") != "" {
32		cgiMain()
33		os.Exit(0)
34	}
35
36	os.Exit(m.Run())
37}
38
39func newRequest(httpreq string) *http.Request {
40	buf := bufio.NewReader(strings.NewReader(httpreq))
41	req, err := http.ReadRequest(buf)
42	if err != nil {
43		panic("cgi: bogus http request in test: " + httpreq)
44	}
45	req.RemoteAddr = "1.2.3.4:1234"
46	return req
47}
48
49func runCgiTest(t *testing.T, h *Handler,
50	httpreq string,
51	expectedMap map[string]string, checks ...func(reqInfo map[string]string)) *httptest.ResponseRecorder {
52	rw := httptest.NewRecorder()
53	req := newRequest(httpreq)
54	h.ServeHTTP(rw, req)
55	runResponseChecks(t, rw, expectedMap, checks...)
56	return rw
57}
58
59func runResponseChecks(t *testing.T, rw *httptest.ResponseRecorder,
60	expectedMap map[string]string, checks ...func(reqInfo map[string]string)) {
61	// Make a map to hold the test map that the CGI returns.
62	m := make(map[string]string)
63	m["_body"] = rw.Body.String()
64	linesRead := 0
65readlines:
66	for {
67		line, err := rw.Body.ReadString('\n')
68		switch {
69		case err == io.EOF:
70			break readlines
71		case err != nil:
72			t.Fatalf("unexpected error reading from CGI: %v", err)
73		}
74		linesRead++
75		trimmedLine := strings.TrimRight(line, "\r\n")
76		k, v, ok := strings.Cut(trimmedLine, "=")
77		if !ok {
78			t.Fatalf("Unexpected response from invalid line number %v: %q; existing map=%v",
79				linesRead, line, m)
80		}
81		m[k] = v
82	}
83
84	for key, expected := range expectedMap {
85		got := m[key]
86		if key == "cwd" {
87			// For Windows. golang.org/issue/4645.
88			fi1, _ := os.Stat(got)
89			fi2, _ := os.Stat(expected)
90			if os.SameFile(fi1, fi2) {
91				got = expected
92			}
93		}
94		if got != expected {
95			t.Errorf("for key %q got %q; expected %q", key, got, expected)
96		}
97	}
98	for _, check := range checks {
99		check(m)
100	}
101}
102
103func TestCGIBasicGet(t *testing.T) {
104	testenv.MustHaveExec(t)
105	h := &Handler{
106		Path: os.Args[0],
107		Root: "/test.cgi",
108	}
109	expectedMap := map[string]string{
110		"test":                  "Hello CGI",
111		"param-a":               "b",
112		"param-foo":             "bar",
113		"env-GATEWAY_INTERFACE": "CGI/1.1",
114		"env-HTTP_HOST":         "example.com:80",
115		"env-PATH_INFO":         "",
116		"env-QUERY_STRING":      "foo=bar&a=b",
117		"env-REMOTE_ADDR":       "1.2.3.4",
118		"env-REMOTE_HOST":       "1.2.3.4",
119		"env-REMOTE_PORT":       "1234",
120		"env-REQUEST_METHOD":    "GET",
121		"env-REQUEST_URI":       "/test.cgi?foo=bar&a=b",
122		"env-SCRIPT_FILENAME":   os.Args[0],
123		"env-SCRIPT_NAME":       "/test.cgi",
124		"env-SERVER_NAME":       "example.com",
125		"env-SERVER_PORT":       "80",
126		"env-SERVER_SOFTWARE":   "go",
127	}
128	replay := runCgiTest(t, h, "GET /test.cgi?foo=bar&a=b HTTP/1.0\nHost: example.com:80\n\n", expectedMap)
129
130	if expected, got := "text/html", replay.Header().Get("Content-Type"); got != expected {
131		t.Errorf("got a Content-Type of %q; expected %q", got, expected)
132	}
133	if expected, got := "X-Test-Value", replay.Header().Get("X-Test-Header"); got != expected {
134		t.Errorf("got a X-Test-Header of %q; expected %q", got, expected)
135	}
136}
137
138func TestCGIEnvIPv6(t *testing.T) {
139	testenv.MustHaveExec(t)
140	h := &Handler{
141		Path: os.Args[0],
142		Root: "/test.cgi",
143	}
144	expectedMap := map[string]string{
145		"test":                  "Hello CGI",
146		"param-a":               "b",
147		"param-foo":             "bar",
148		"env-GATEWAY_INTERFACE": "CGI/1.1",
149		"env-HTTP_HOST":         "example.com",
150		"env-PATH_INFO":         "",
151		"env-QUERY_STRING":      "foo=bar&a=b",
152		"env-REMOTE_ADDR":       "2000::3000",
153		"env-REMOTE_HOST":       "2000::3000",
154		"env-REMOTE_PORT":       "12345",
155		"env-REQUEST_METHOD":    "GET",
156		"env-REQUEST_URI":       "/test.cgi?foo=bar&a=b",
157		"env-SCRIPT_FILENAME":   os.Args[0],
158		"env-SCRIPT_NAME":       "/test.cgi",
159		"env-SERVER_NAME":       "example.com",
160		"env-SERVER_PORT":       "80",
161		"env-SERVER_SOFTWARE":   "go",
162	}
163
164	rw := httptest.NewRecorder()
165	req := newRequest("GET /test.cgi?foo=bar&a=b HTTP/1.0\nHost: example.com\n\n")
166	req.RemoteAddr = "[2000::3000]:12345"
167	h.ServeHTTP(rw, req)
168	runResponseChecks(t, rw, expectedMap)
169}
170
171func TestCGIBasicGetAbsPath(t *testing.T) {
172	absPath, err := filepath.Abs(os.Args[0])
173	if err != nil {
174		t.Fatal(err)
175	}
176	testenv.MustHaveExec(t)
177	h := &Handler{
178		Path: absPath,
179		Root: "/test.cgi",
180	}
181	expectedMap := map[string]string{
182		"env-REQUEST_URI":     "/test.cgi?foo=bar&a=b",
183		"env-SCRIPT_FILENAME": absPath,
184		"env-SCRIPT_NAME":     "/test.cgi",
185	}
186	runCgiTest(t, h, "GET /test.cgi?foo=bar&a=b HTTP/1.0\nHost: example.com\n\n", expectedMap)
187}
188
189func TestPathInfo(t *testing.T) {
190	testenv.MustHaveExec(t)
191	h := &Handler{
192		Path: os.Args[0],
193		Root: "/test.cgi",
194	}
195	expectedMap := map[string]string{
196		"param-a":             "b",
197		"env-PATH_INFO":       "/extrapath",
198		"env-QUERY_STRING":    "a=b",
199		"env-REQUEST_URI":     "/test.cgi/extrapath?a=b",
200		"env-SCRIPT_FILENAME": os.Args[0],
201		"env-SCRIPT_NAME":     "/test.cgi",
202	}
203	runCgiTest(t, h, "GET /test.cgi/extrapath?a=b HTTP/1.0\nHost: example.com\n\n", expectedMap)
204}
205
206func TestPathInfoDirRoot(t *testing.T) {
207	testenv.MustHaveExec(t)
208	h := &Handler{
209		Path: os.Args[0],
210		Root: "/myscript//",
211	}
212	expectedMap := map[string]string{
213		"env-PATH_INFO":       "/bar",
214		"env-QUERY_STRING":    "a=b",
215		"env-REQUEST_URI":     "/myscript/bar?a=b",
216		"env-SCRIPT_FILENAME": os.Args[0],
217		"env-SCRIPT_NAME":     "/myscript",
218	}
219	runCgiTest(t, h, "GET /myscript/bar?a=b HTTP/1.0\nHost: example.com\n\n", expectedMap)
220}
221
222func TestDupHeaders(t *testing.T) {
223	testenv.MustHaveExec(t)
224	h := &Handler{
225		Path: os.Args[0],
226	}
227	expectedMap := map[string]string{
228		"env-REQUEST_URI":     "/myscript/bar?a=b",
229		"env-SCRIPT_FILENAME": os.Args[0],
230		"env-HTTP_COOKIE":     "nom=NOM; yum=YUM",
231		"env-HTTP_X_FOO":      "val1, val2",
232	}
233	runCgiTest(t, h, "GET /myscript/bar?a=b HTTP/1.0\n"+
234		"Cookie: nom=NOM\n"+
235		"Cookie: yum=YUM\n"+
236		"X-Foo: val1\n"+
237		"X-Foo: val2\n"+
238		"Host: example.com\n\n",
239		expectedMap)
240}
241
242// Issue 16405: CGI+http.Transport differing uses of HTTP_PROXY.
243// Verify we don't set the HTTP_PROXY environment variable.
244// Hope nobody was depending on it. It's not a known header, though.
245func TestDropProxyHeader(t *testing.T) {
246	testenv.MustHaveExec(t)
247	h := &Handler{
248		Path: os.Args[0],
249	}
250	expectedMap := map[string]string{
251		"env-REQUEST_URI":     "/myscript/bar?a=b",
252		"env-SCRIPT_FILENAME": os.Args[0],
253		"env-HTTP_X_FOO":      "a",
254	}
255	runCgiTest(t, h, "GET /myscript/bar?a=b HTTP/1.0\n"+
256		"X-Foo: a\n"+
257		"Proxy: should_be_stripped\n"+
258		"Host: example.com\n\n",
259		expectedMap,
260		func(reqInfo map[string]string) {
261			if v, ok := reqInfo["env-HTTP_PROXY"]; ok {
262				t.Errorf("HTTP_PROXY = %q; should be absent", v)
263			}
264		})
265}
266
267func TestPathInfoNoRoot(t *testing.T) {
268	testenv.MustHaveExec(t)
269	h := &Handler{
270		Path: os.Args[0],
271		Root: "",
272	}
273	expectedMap := map[string]string{
274		"env-PATH_INFO":       "/bar",
275		"env-QUERY_STRING":    "a=b",
276		"env-REQUEST_URI":     "/bar?a=b",
277		"env-SCRIPT_FILENAME": os.Args[0],
278		"env-SCRIPT_NAME":     "",
279	}
280	runCgiTest(t, h, "GET /bar?a=b HTTP/1.0\nHost: example.com\n\n", expectedMap)
281}
282
283func TestCGIBasicPost(t *testing.T) {
284	testenv.MustHaveExec(t)
285	postReq := `POST /test.cgi?a=b HTTP/1.0
286Host: example.com
287Content-Type: application/x-www-form-urlencoded
288Content-Length: 15
289
290postfoo=postbar`
291	h := &Handler{
292		Path: os.Args[0],
293		Root: "/test.cgi",
294	}
295	expectedMap := map[string]string{
296		"test":               "Hello CGI",
297		"param-postfoo":      "postbar",
298		"env-REQUEST_METHOD": "POST",
299		"env-CONTENT_LENGTH": "15",
300		"env-REQUEST_URI":    "/test.cgi?a=b",
301	}
302	runCgiTest(t, h, postReq, expectedMap)
303}
304
305func chunk(s string) string {
306	return fmt.Sprintf("%x\r\n%s\r\n", len(s), s)
307}
308
309// The CGI spec doesn't allow chunked requests.
310func TestCGIPostChunked(t *testing.T) {
311	testenv.MustHaveExec(t)
312	postReq := `POST /test.cgi?a=b HTTP/1.1
313Host: example.com
314Content-Type: application/x-www-form-urlencoded
315Transfer-Encoding: chunked
316
317` + chunk("postfoo") + chunk("=") + chunk("postbar") + chunk("")
318
319	h := &Handler{
320		Path: os.Args[0],
321		Root: "/test.cgi",
322	}
323	expectedMap := map[string]string{}
324	resp := runCgiTest(t, h, postReq, expectedMap)
325	if got, expected := resp.Code, http.StatusBadRequest; got != expected {
326		t.Fatalf("Expected %v response code from chunked request body; got %d",
327			expected, got)
328	}
329}
330
331func TestRedirect(t *testing.T) {
332	testenv.MustHaveExec(t)
333	h := &Handler{
334		Path: os.Args[0],
335		Root: "/test.cgi",
336	}
337	rec := runCgiTest(t, h, "GET /test.cgi?loc=http://foo.com/ HTTP/1.0\nHost: example.com\n\n", nil)
338	if e, g := 302, rec.Code; e != g {
339		t.Errorf("expected status code %d; got %d", e, g)
340	}
341	if e, g := "http://foo.com/", rec.Header().Get("Location"); e != g {
342		t.Errorf("expected Location header of %q; got %q", e, g)
343	}
344}
345
346func TestInternalRedirect(t *testing.T) {
347	testenv.MustHaveExec(t)
348	baseHandler := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
349		fmt.Fprintf(rw, "basepath=%s\n", req.URL.Path)
350		fmt.Fprintf(rw, "remoteaddr=%s\n", req.RemoteAddr)
351	})
352	h := &Handler{
353		Path:                os.Args[0],
354		Root:                "/test.cgi",
355		PathLocationHandler: baseHandler,
356	}
357	expectedMap := map[string]string{
358		"basepath":   "/foo",
359		"remoteaddr": "1.2.3.4:1234",
360	}
361	runCgiTest(t, h, "GET /test.cgi?loc=/foo HTTP/1.0\nHost: example.com\n\n", expectedMap)
362}
363
364// TestCopyError tests that we kill the process if there's an error copying
365// its output. (for example, from the client having gone away)
366//
367// If we fail to do so, the test will time out (and dump its goroutines) with a
368// call to [Handler.ServeHTTP] blocked on a deferred call to [exec.Cmd.Wait].
369func TestCopyError(t *testing.T) {
370	testenv.MustHaveExec(t)
371
372	h := &Handler{
373		Path: os.Args[0],
374		Root: "/test.cgi",
375	}
376	ts := httptest.NewServer(h)
377	defer ts.Close()
378
379	conn, err := net.Dial("tcp", ts.Listener.Addr().String())
380	if err != nil {
381		t.Fatal(err)
382	}
383	req, _ := http.NewRequest("GET", "http://example.com/test.cgi?bigresponse=1", nil)
384	err = req.Write(conn)
385	if err != nil {
386		t.Fatalf("Write: %v", err)
387	}
388	res, err := http.ReadResponse(bufio.NewReader(conn), req)
389	if err != nil {
390		t.Fatalf("ReadResponse: %v", err)
391	}
392	defer res.Body.Close()
393	var buf [5000]byte
394	n, err := io.ReadFull(res.Body, buf[:])
395	if err != nil {
396		t.Fatalf("ReadFull: %d bytes, %v", n, err)
397	}
398
399	if !handlerRunning() {
400		t.Fatalf("pre-conn.Close, expected handler to still be running")
401	}
402	conn.Close()
403	closed := time.Now()
404
405	nextSleep := 1 * time.Millisecond
406	for {
407		time.Sleep(nextSleep)
408		nextSleep *= 2
409		if !handlerRunning() {
410			break
411		}
412		t.Logf("handler still running %v after conn.Close", time.Since(closed))
413	}
414}
415
416// handlerRunning reports whether any goroutine is currently running
417// [Handler.ServeHTTP].
418func handlerRunning() bool {
419	r := regexp.MustCompile(`net/http/cgi\.\(\*Handler\)\.ServeHTTP`)
420	buf := make([]byte, 64<<10)
421	for {
422		n := runtime.Stack(buf, true)
423		if n < len(buf) {
424			return r.Match(buf[:n])
425		}
426		// Buffer wasn't large enough for a full goroutine dump.
427		// Resize it and try again.
428		buf = make([]byte, 2*len(buf))
429	}
430}
431
432func TestDir(t *testing.T) {
433	testenv.MustHaveExec(t)
434	cwd, _ := os.Getwd()
435	h := &Handler{
436		Path: os.Args[0],
437		Root: "/test.cgi",
438		Dir:  cwd,
439	}
440	expectedMap := map[string]string{
441		"cwd": cwd,
442	}
443	runCgiTest(t, h, "GET /test.cgi HTTP/1.0\nHost: example.com\n\n", expectedMap)
444
445	cwd, _ = os.Getwd()
446	cwd, _ = filepath.Split(os.Args[0])
447	h = &Handler{
448		Path: os.Args[0],
449		Root: "/test.cgi",
450	}
451	expectedMap = map[string]string{
452		"cwd": cwd,
453	}
454	runCgiTest(t, h, "GET /test.cgi HTTP/1.0\nHost: example.com\n\n", expectedMap)
455}
456
457func TestEnvOverride(t *testing.T) {
458	testenv.MustHaveExec(t)
459	cgifile, _ := filepath.Abs("testdata/test.cgi")
460
461	cwd, _ := os.Getwd()
462	h := &Handler{
463		Path: os.Args[0],
464		Root: "/test.cgi",
465		Dir:  cwd,
466		Env: []string{
467			"SCRIPT_FILENAME=" + cgifile,
468			"REQUEST_URI=/foo/bar",
469			"PATH=/wibble"},
470	}
471	expectedMap := map[string]string{
472		"cwd":                 cwd,
473		"env-SCRIPT_FILENAME": cgifile,
474		"env-REQUEST_URI":     "/foo/bar",
475		"env-PATH":            "/wibble",
476	}
477	runCgiTest(t, h, "GET /test.cgi HTTP/1.0\nHost: example.com\n\n", expectedMap)
478}
479
480func TestHandlerStderr(t *testing.T) {
481	testenv.MustHaveExec(t)
482	var stderr strings.Builder
483	h := &Handler{
484		Path:   os.Args[0],
485		Root:   "/test.cgi",
486		Stderr: &stderr,
487	}
488
489	rw := httptest.NewRecorder()
490	req := newRequest("GET /test.cgi?writestderr=1 HTTP/1.0\nHost: example.com\n\n")
491	h.ServeHTTP(rw, req)
492	if got, want := stderr.String(), "Hello, stderr!\n"; got != want {
493		t.Errorf("Stderr = %q; want %q", got, want)
494	}
495}
496
497func TestRemoveLeadingDuplicates(t *testing.T) {
498	tests := []struct {
499		env  []string
500		want []string
501	}{
502		{
503			env:  []string{"a=b", "b=c", "a=b2"},
504			want: []string{"b=c", "a=b2"},
505		},
506		{
507			env:  []string{"a=b", "b=c", "d", "e=f"},
508			want: []string{"a=b", "b=c", "d", "e=f"},
509		},
510	}
511	for _, tt := range tests {
512		got := removeLeadingDuplicates(tt.env)
513		if !reflect.DeepEqual(got, tt.want) {
514			t.Errorf("removeLeadingDuplicates(%q) = %q; want %q", tt.env, got, tt.want)
515		}
516	}
517}
518