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