Source file src/net/http/cgi/host_test.go

     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  
     7  package cgi
     8  
     9  import (
    10  	"bufio"
    11  	"fmt"
    12  	"internal/testenv"
    13  	"io"
    14  	"net"
    15  	"net/http"
    16  	"net/http/httptest"
    17  	"os"
    18  	"path/filepath"
    19  	"regexp"
    20  	"runtime"
    21  	"slices"
    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.
    29  func 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  
    39  func 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  
    49  func 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  
    59  func 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
    65  readlines:
    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  
   103  func 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  
   138  func 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  
   171  func 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  
   189  func 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  
   206  func 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  
   222  func 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.
   245  func 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  
   267  func 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  
   283  func TestCGIBasicPost(t *testing.T) {
   284  	testenv.MustHaveExec(t)
   285  	postReq := `POST /test.cgi?a=b HTTP/1.0
   286  Host: example.com
   287  Content-Type: application/x-www-form-urlencoded
   288  Content-Length: 15
   289  
   290  postfoo=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  
   305  func 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.
   310  func TestCGIPostChunked(t *testing.T) {
   311  	testenv.MustHaveExec(t)
   312  	postReq := `POST /test.cgi?a=b HTTP/1.1
   313  Host: example.com
   314  Content-Type: application/x-www-form-urlencoded
   315  Transfer-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  
   331  func 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  
   346  func 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].
   369  func 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].
   418  func 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  
   432  func 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  
   457  func 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  
   480  func 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  
   497  func 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 !slices.Equal(got, tt.want) {
   514  			t.Errorf("removeLeadingDuplicates(%q) = %q; want %q", tt.env, got, tt.want)
   515  		}
   516  	}
   517  }
   518  

View as plain text