1
0
mirror of https://github.com/golang/go synced 2024-11-22 02:54:39 -07:00

http: add packages http/cgi and http/httptest

R=rsc, adg, jnw, bradfitzwork
CC=golang-dev
https://golang.org/cl/4247048
This commit is contained in:
Brad Fitzpatrick 2011-03-04 10:55:47 -08:00
parent 89f890f266
commit da679db0f1
8 changed files with 556 additions and 65 deletions

View File

@ -96,7 +96,9 @@ DIRS=\
hash/crc64\ hash/crc64\
html\ html\
http\ http\
http/cgi\
http/pprof\ http/pprof\
http/httptest\
image\ image\
image/jpeg\ image/jpeg\
image/png\ image/png\
@ -172,6 +174,7 @@ NOTEST=\
go/token\ go/token\
hash\ hash\
http/pprof\ http/pprof\
http/httptest\
image/jpeg\ image/jpeg\
net/dict\ net/dict\
rand\ rand\

11
src/pkg/http/cgi/Makefile Normal file
View File

@ -0,0 +1,11 @@
# 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.
include ../../../Make.inc
TARG=http/cgi
GOFILES=\
cgi.go\
include ../../../Make.pkg

201
src/pkg/http/cgi/cgi.go Normal file
View File

@ -0,0 +1,201 @@
// 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.
// Package cgi implements CGI (Common Gateway Interface) as specified
// in RFC 3875.
//
// Note that using CGI means starting a new process to handle each
// request, which is typically less efficient than using a
// long-running server. This package is intended primarily for
// compatibility with existing systems.
package cgi
import (
"encoding/line"
"exec"
"fmt"
"http"
"io"
"log"
"os"
"path"
"regexp"
"strconv"
"strings"
)
var trailingPort = regexp.MustCompile(`:([0-9]+)$`)
// Handler runs an executable in a subprocess with a CGI environment.
type Handler struct {
Path string // path to the CGI executable
Root string // root URI prefix of handler or empty for "/"
Env []string // extra environment variables to set, if any
Logger *log.Logger // optional log for errors or nil to use log.Print
}
func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
root := h.Root
if root == "" {
root = "/"
}
if len(req.TransferEncoding) > 0 && req.TransferEncoding[0] == "chunked" {
rw.WriteHeader(http.StatusBadRequest)
rw.Write([]byte("Chunked request bodies are not supported by CGI."))
return
}
pathInfo := req.URL.Path
if root != "/" && strings.HasPrefix(pathInfo, root) {
pathInfo = pathInfo[len(root):]
}
port := "80"
if matches := trailingPort.FindStringSubmatch(req.Host); len(matches) != 0 {
port = matches[1]
}
env := []string{
"SERVER_SOFTWARE=go",
"SERVER_NAME=" + req.Host,
"HTTP_HOST=" + req.Host,
"GATEWAY_INTERFACE=CGI/1.1",
"REQUEST_METHOD=" + req.Method,
"QUERY_STRING=" + req.URL.RawQuery,
"REQUEST_URI=" + req.URL.RawPath,
"PATH_INFO=" + pathInfo,
"SCRIPT_NAME=" + root,
"SCRIPT_FILENAME=" + h.Path,
"REMOTE_ADDR=" + rw.RemoteAddr(),
"REMOTE_HOST=" + rw.RemoteAddr(),
"SERVER_PORT=" + port,
}
for k, _ := range req.Header {
k = strings.Map(upperCaseAndUnderscore, k)
env = append(env, "HTTP_"+k+"="+req.Header.Get(k))
}
if req.ContentLength > 0 {
env = append(env, fmt.Sprintf("CONTENT_LENGTH=%d", req.ContentLength))
}
if ctype := req.Header.Get("Content-Type"); ctype != "" {
env = append(env, "CONTENT_TYPE="+ctype)
}
if h.Env != nil {
env = append(env, h.Env...)
}
// TODO: use filepath instead of path when available
cwd, pathBase := path.Split(h.Path)
if cwd == "" {
cwd = "."
}
cmd, err := exec.Run(
pathBase,
[]string{h.Path},
env,
cwd,
exec.Pipe, // stdin
exec.Pipe, // stdout
exec.PassThrough, // stderr (for now)
)
if err != nil {
rw.WriteHeader(http.StatusInternalServerError)
h.printf("CGI error: %v", err)
return
}
defer func() {
cmd.Stdin.Close()
cmd.Stdout.Close()
cmd.Wait(0) // no zombies
}()
if req.ContentLength != 0 {
go io.Copy(cmd.Stdin, req.Body)
}
linebody := line.NewReader(cmd.Stdout, 1024)
headers := make(map[string]string)
statusCode := http.StatusOK
for {
line, isPrefix, err := linebody.ReadLine()
if isPrefix {
rw.WriteHeader(http.StatusInternalServerError)
h.printf("CGI: long header line from subprocess.")
return
}
if err == os.EOF {
break
}
if err != nil {
rw.WriteHeader(http.StatusInternalServerError)
h.printf("CGI: error reading headers: %v", err)
return
}
if len(line) == 0 {
break
}
parts := strings.Split(string(line), ":", 2)
if len(parts) < 2 {
h.printf("CGI: bogus header line: %s", string(line))
continue
}
header, val := parts[0], parts[1]
header = strings.TrimSpace(header)
val = strings.TrimSpace(val)
switch {
case header == "Status":
if len(val) < 3 {
h.printf("CGI: bogus status (short): %q", val)
return
}
code, err := strconv.Atoi(val[0:3])
if err != nil {
h.printf("CGI: bogus status: %q", val)
h.printf("CGI: line was %q", line)
return
}
statusCode = code
default:
headers[header] = val
}
}
for h, v := range headers {
rw.SetHeader(h, v)
}
rw.WriteHeader(statusCode)
_, err = io.Copy(rw, linebody)
if err != nil {
h.printf("CGI: copy error: %v", err)
}
}
func (h *Handler) printf(format string, v ...interface{}) {
if h.Logger != nil {
h.Logger.Printf(format, v...)
} else {
log.Printf(format, v...)
}
}
func upperCaseAndUnderscore(rune int) int {
switch {
case rune >= 'a' && rune <= 'z':
return rune - ('a' - 'A')
case rune == '-':
return '_'
case rune == '=':
// Maybe not part of the CGI 'spec' but would mess up
// the environment in any case, as Go represents the
// environment as a slice of "key=value" strings.
return '_'
}
// TODO: other transformations in spec or practice?
return rune
}

View File

@ -0,0 +1,200 @@
// 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"
"http"
"http/httptest"
"os"
"strings"
"testing"
)
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)
}
return req
}
func runCgiTest(t *testing.T, h *Handler, httpreq string, expectedMap map[string]string) *httptest.ResponseRecorder {
rw := httptest.NewRecorder()
req := newRequest(httpreq)
h.ServeHTTP(rw, req)
// Make a map to hold the test map that the CGI returns.
m := make(map[string]string)
readlines:
for {
line, err := rw.Body.ReadString('\n')
switch {
case err == os.EOF:
break readlines
case err != nil:
t.Fatalf("unexpected error reading from CGI: %v", err)
}
line = strings.TrimRight(line, "\r\n")
split := strings.Split(line, "=", 2)
if len(split) != 2 {
t.Fatalf("Unexpected %d parts from invalid line: %q", len(split), line)
}
m[split[0]] = split[1]
}
for key, expected := range expectedMap {
if got := m[key]; got != expected {
t.Errorf("for key %q got %q; expected %q", key, got, expected)
}
}
return rw
}
func TestCGIBasicGet(t *testing.T) {
h := &Handler{
Path: "testdata/test.cgi",
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": "1.2.3.4",
"env-REMOTE_HOST": "1.2.3.4",
"env-REQUEST_METHOD": "GET",
"env-REQUEST_URI": "/test.cgi?foo=bar&a=b",
"env-SCRIPT_FILENAME": "testdata/test.cgi",
"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\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 TestCGIBasicGetAbsPath(t *testing.T) {
pwd, err := os.Getwd()
if err != nil {
t.Fatalf("getwd error: %v", err)
}
h := &Handler{
Path: pwd + "/testdata/test.cgi",
Root: "/test.cgi",
}
expectedMap := map[string]string{
"env-REQUEST_URI": "/test.cgi?foo=bar&a=b",
"env-SCRIPT_FILENAME": pwd + "/testdata/test.cgi",
"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) {
h := &Handler{
Path: "testdata/test.cgi",
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": "testdata/test.cgi",
"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) {
h := &Handler{
Path: "testdata/test.cgi",
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": "testdata/test.cgi",
"env-SCRIPT_NAME": "/myscript/",
}
runCgiTest(t, h, "GET /myscript/bar?a=b HTTP/1.0\nHost: example.com\n\n", expectedMap)
}
func TestPathInfoNoRoot(t *testing.T) {
h := &Handler{
Path: "testdata/test.cgi",
Root: "",
}
expectedMap := map[string]string{
"env-PATH_INFO": "/bar",
"env-QUERY_STRING": "a=b",
"env-REQUEST_URI": "/bar?a=b",
"env-SCRIPT_FILENAME": "testdata/test.cgi",
"env-SCRIPT_NAME": "/",
}
runCgiTest(t, h, "GET /bar?a=b HTTP/1.0\nHost: example.com\n\n", expectedMap)
}
func TestCGIBasicPost(t *testing.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: "testdata/test.cgi",
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) {
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: "testdata/test.cgi",
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)
}
}

34
src/pkg/http/cgi/testdata/test.cgi vendored Executable file
View File

@ -0,0 +1,34 @@
#!/usr/bin/perl
# 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.
#
# Test script run as a child process under cgi_test.go
use strict;
use CGI;
my $q = CGI->new;
my $params = $q->Vars;
my $NL = "\r\n";
$NL = "\n" if 1 || $params->{mode} eq "NL";
my $p = sub {
print "$_[0]$NL";
};
# With carriage returns
$p->("Content-Type: text/html");
$p->("X-Test-Header: X-Test-Value");
$p->("");
print "test=Hello CGI\n";
foreach my $k (sort keys %$params) {
print "param-$k=$params->{$k}\n";
}
foreach my $k (sort keys %ENV) {
print "env-$k=$ENV{$k}\n";
}

View File

@ -0,0 +1,11 @@
# 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.
include ../../../Make.inc
TARG=http/httptest
GOFILES=\
recorder.go\
include ../../../Make.pkg

View File

@ -0,0 +1,88 @@
// 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.
// The httptest package provides utilities for HTTP testing.
package httptest
import (
"bufio"
"bytes"
"http"
"io"
"os"
)
// ResponseRecorder is an implementation of http.ResponseWriter that
// records its mutations for later inspection in tests.
//
// Note that Hijack is not implemented and simply panics.
type ResponseRecorder struct {
Code int // the HTTP response code from WriteHeader
Header http.Header // if non-nil, the headers to populate
Body *bytes.Buffer // if non-nil, the bytes.Buffer to append written data to
Flushed bool
FakeRemoteAddr string // the fake RemoteAddr to return, or "" for DefaultRemoteAddr
FakeUsingTLS bool // whether to return true from the UsingTLS method
}
// NewRecorder returns an initialized ResponseRecorder.
func NewRecorder() *ResponseRecorder {
return &ResponseRecorder{
Header: http.Header(make(map[string][]string)),
Body: new(bytes.Buffer),
}
}
// DefaultRemoteAddr is the default remote address to return in RemoteAddr if
// an explicit DefaultRemoteAddr isn't set on ResponseRecorder.
const DefaultRemoteAddr = "1.2.3.4"
// RemoteAddr returns the value of rw.FakeRemoteAddr, if set, else
// returns DefaultRemoteAddr.
func (rw *ResponseRecorder) RemoteAddr() string {
if rw.FakeRemoteAddr != "" {
return rw.FakeRemoteAddr
}
return DefaultRemoteAddr
}
// UsingTLS returns the fake value in rw.FakeUsingTLS
func (rw *ResponseRecorder) UsingTLS() bool {
return rw.FakeUsingTLS
}
// SetHeader populates rw.Header, if non-nil.
func (rw *ResponseRecorder) SetHeader(k, v string) {
if rw.Header != nil {
if v == "" {
rw.Header.Del(k)
} else {
rw.Header.Set(k, v)
}
}
}
// Write always succeeds and writes to rw.Body, if not nil.
func (rw *ResponseRecorder) Write(buf []byte) (int, os.Error) {
if rw.Body != nil {
rw.Body.Write(buf)
}
return len(buf), nil
}
// WriteHeader sets rw.Code.
func (rw *ResponseRecorder) WriteHeader(code int) {
rw.Code = code
}
// Flush sets rw.Flushed to true.
func (rw *ResponseRecorder) Flush() {
rw.Flushed = true
}
// Hijack is not implemented in ResponseRecorder and instead panics.
func (rw *ResponseRecorder) Hijack() (io.ReadWriteCloser, *bufio.ReadWriter, os.Error) {
panic("Hijack not implemented in ResponseRecorder")
}

View File

@ -4,13 +4,14 @@
// End-to-end serving tests // End-to-end serving tests
package http package http_test
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"fmt" "fmt"
"io" . "http"
"http/httptest"
"io/ioutil" "io/ioutil"
"os" "os"
"net" "net"
@ -205,46 +206,6 @@ func TestHostHandlers(t *testing.T) {
} }
} }
type responseWriterMethodCall struct {
method string
headerKey, headerValue string // if method == "SetHeader"
bytesWritten []byte // if method == "Write"
responseCode int // if method == "WriteHeader"
}
type recordingResponseWriter struct {
log []*responseWriterMethodCall
}
func (rw *recordingResponseWriter) RemoteAddr() string {
return "1.2.3.4"
}
func (rw *recordingResponseWriter) UsingTLS() bool {
return false
}
func (rw *recordingResponseWriter) SetHeader(k, v string) {
rw.log = append(rw.log, &responseWriterMethodCall{method: "SetHeader", headerKey: k, headerValue: v})
}
func (rw *recordingResponseWriter) Write(buf []byte) (int, os.Error) {
rw.log = append(rw.log, &responseWriterMethodCall{method: "Write", bytesWritten: buf})
return len(buf), nil
}
func (rw *recordingResponseWriter) WriteHeader(code int) {
rw.log = append(rw.log, &responseWriterMethodCall{method: "WriteHeader", responseCode: code})
}
func (rw *recordingResponseWriter) Flush() {
rw.log = append(rw.log, &responseWriterMethodCall{method: "Flush"})
}
func (rw *recordingResponseWriter) Hijack() (io.ReadWriteCloser, *bufio.ReadWriter, os.Error) {
panic("Not supported")
}
// Tests for http://code.google.com/p/go/issues/detail?id=900 // Tests for http://code.google.com/p/go/issues/detail?id=900
func TestMuxRedirectLeadingSlashes(t *testing.T) { func TestMuxRedirectLeadingSlashes(t *testing.T) {
paths := []string{"//foo.txt", "///foo.txt", "/../../foo.txt"} paths := []string{"//foo.txt", "///foo.txt", "/../../foo.txt"}
@ -254,35 +215,17 @@ func TestMuxRedirectLeadingSlashes(t *testing.T) {
t.Errorf("%s", err) t.Errorf("%s", err)
} }
mux := NewServeMux() mux := NewServeMux()
resp := new(recordingResponseWriter) resp := httptest.NewRecorder()
resp.log = make([]*responseWriterMethodCall, 0)
mux.ServeHTTP(resp, req) mux.ServeHTTP(resp, req)
dumpLog := func() { if loc, expected := resp.Header.Get("Location"), "/foo.txt"; loc != expected {
t.Logf("For path %q:", path) t.Errorf("Expected Location header set to %q; got %q", expected, loc)
for _, call := range resp.log {
t.Logf("Got call: %s, header=%s, value=%s, buf=%q, code=%d", call.method,
call.headerKey, call.headerValue, call.bytesWritten, call.responseCode)
}
}
if len(resp.log) != 2 {
dumpLog()
t.Errorf("expected 2 calls to response writer; got %d", len(resp.log))
return return
} }
if resp.log[0].method != "SetHeader" || if code, expected := resp.Code, StatusMovedPermanently; code != expected {
resp.log[0].headerKey != "Location" || resp.log[0].headerValue != "/foo.txt" { t.Errorf("Expected response code of StatusMovedPermanently; got %d", code)
dumpLog()
t.Errorf("Expected SetHeader of Location to /foo.txt")
return
}
if resp.log[1].method != "WriteHeader" || resp.log[1].responseCode != StatusMovedPermanently {
dumpLog()
t.Errorf("Expected WriteHeader of StatusMovedPermanently")
return return
} }
} }