summaryrefslogtreecommitdiff
path: root/libgo/go/http
diff options
context:
space:
mode:
authorian <ian@138bc75d-0d04-0410-961f-82ee72b054a4>2011-03-24 23:46:17 +0000
committerian <ian@138bc75d-0d04-0410-961f-82ee72b054a4>2011-03-24 23:46:17 +0000
commitf97228863f84f4d7d87959ea40df40130f2ec912 (patch)
tree9319bca77115a32f6a0b5e8bcd651465b14c76da /libgo/go/http
parentd304b9e1af728d54ec16155c3d2116dc398c33c6 (diff)
downloadgcc-f97228863f84f4d7d87959ea40df40130f2ec912.tar.gz
Update to current version of Go library.
git-svn-id: svn+ssh://gcc.gnu.org/svn/gcc/trunk@171427 138bc75d-0d04-0410-961f-82ee72b054a4
Diffstat (limited to 'libgo/go/http')
-rw-r--r--libgo/go/http/cgi/child.go192
-rw-r--r--libgo/go/http/cgi/child_test.go83
-rw-r--r--libgo/go/http/cgi/host.go221
-rw-r--r--libgo/go/http/cgi/host_test.go273
-rw-r--r--libgo/go/http/cgi/matryoshka_test.go74
-rw-r--r--libgo/go/http/client.go83
-rw-r--r--libgo/go/http/client_test.go23
-rw-r--r--libgo/go/http/cookie.go272
-rw-r--r--libgo/go/http/cookie_test.go110
-rw-r--r--libgo/go/http/dump.go4
-rw-r--r--libgo/go/http/export_test.go34
-rw-r--r--libgo/go/http/fs.go22
-rw-r--r--libgo/go/http/fs_test.go83
-rw-r--r--libgo/go/http/httptest/recorder.go59
-rw-r--r--libgo/go/http/httptest/server.go70
-rw-r--r--libgo/go/http/persist.go112
-rw-r--r--libgo/go/http/pprof/pprof.go6
-rw-r--r--libgo/go/http/proxy_test.go30
-rw-r--r--libgo/go/http/range_test.go57
-rw-r--r--libgo/go/http/readrequest_test.go2
-rw-r--r--libgo/go/http/request.go41
-rw-r--r--libgo/go/http/request_test.go37
-rw-r--r--libgo/go/http/requestwrite_test.go64
-rw-r--r--libgo/go/http/response.go34
-rw-r--r--libgo/go/http/responsewrite_test.go38
-rw-r--r--libgo/go/http/serve_test.go257
-rw-r--r--libgo/go/http/server.go155
-rw-r--r--libgo/go/http/transport.go550
-rw-r--r--libgo/go/http/transport_test.go235
29 files changed, 2714 insertions, 507 deletions
diff --git a/libgo/go/http/cgi/child.go b/libgo/go/http/cgi/child.go
new file mode 100644
index 00000000000..c7d48b9eb3f
--- /dev/null
+++ b/libgo/go/http/cgi/child.go
@@ -0,0 +1,192 @@
+// 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.
+
+// This file implements CGI from the perspective of a child
+// process.
+
+package cgi
+
+import (
+ "bufio"
+ "fmt"
+ "http"
+ "io"
+ "io/ioutil"
+ "os"
+ "strconv"
+ "strings"
+)
+
+// Request returns the HTTP request as represented in the current
+// environment. This assumes the current program is being run
+// by a web server in a CGI environment.
+func Request() (*http.Request, os.Error) {
+ return requestFromEnvironment(envMap(os.Environ()))
+}
+
+func envMap(env []string) map[string]string {
+ m := make(map[string]string)
+ for _, kv := range env {
+ if idx := strings.Index(kv, "="); idx != -1 {
+ m[kv[:idx]] = kv[idx+1:]
+ }
+ }
+ return m
+}
+
+// These environment variables are manually copied into Request
+var skipHeader = map[string]bool{
+ "HTTP_HOST": true,
+ "HTTP_REFERER": true,
+ "HTTP_USER_AGENT": true,
+}
+
+func requestFromEnvironment(env map[string]string) (*http.Request, os.Error) {
+ r := new(http.Request)
+ r.Method = env["REQUEST_METHOD"]
+ if r.Method == "" {
+ return nil, os.NewError("cgi: no REQUEST_METHOD in environment")
+ }
+ r.Close = true
+ r.Trailer = http.Header{}
+ r.Header = http.Header{}
+
+ r.Host = env["HTTP_HOST"]
+ r.Referer = env["HTTP_REFERER"]
+ r.UserAgent = env["HTTP_USER_AGENT"]
+
+ // CGI doesn't allow chunked requests, so these should all be accurate:
+ r.Proto = "HTTP/1.0"
+ r.ProtoMajor = 1
+ r.ProtoMinor = 0
+ r.TransferEncoding = nil
+
+ if lenstr := env["CONTENT_LENGTH"]; lenstr != "" {
+ clen, err := strconv.Atoi64(lenstr)
+ if err != nil {
+ return nil, os.NewError("cgi: bad CONTENT_LENGTH in environment: " + lenstr)
+ }
+ r.ContentLength = clen
+ r.Body = ioutil.NopCloser(io.LimitReader(os.Stdin, clen))
+ }
+
+ // Copy "HTTP_FOO_BAR" variables to "Foo-Bar" Headers
+ for k, v := range env {
+ if !strings.HasPrefix(k, "HTTP_") || skipHeader[k] {
+ continue
+ }
+ r.Header.Add(strings.Replace(k[5:], "_", "-", -1), v)
+ }
+
+ // TODO: cookies. parsing them isn't exported, though.
+
+ if r.Host != "" {
+ // Hostname is provided, so we can reasonably construct a URL,
+ // even if we have to assume 'http' for the scheme.
+ r.RawURL = "http://" + r.Host + env["REQUEST_URI"]
+ url, err := http.ParseURL(r.RawURL)
+ if err != nil {
+ return nil, os.NewError("cgi: failed to parse host and REQUEST_URI into a URL: " + r.RawURL)
+ }
+ r.URL = url
+ }
+ // Fallback logic if we don't have a Host header or the URL
+ // failed to parse
+ if r.URL == nil {
+ r.RawURL = env["REQUEST_URI"]
+ url, err := http.ParseURL(r.RawURL)
+ if err != nil {
+ return nil, os.NewError("cgi: failed to parse REQUEST_URI into a URL: " + r.RawURL)
+ }
+ r.URL = url
+ }
+ return r, nil
+}
+
+// Serve executes the provided Handler on the currently active CGI
+// request, if any. If there's no current CGI environment
+// an error is returned. The provided handler may be nil to use
+// http.DefaultServeMux.
+func Serve(handler http.Handler) os.Error {
+ req, err := Request()
+ if err != nil {
+ return err
+ }
+ if handler == nil {
+ handler = http.DefaultServeMux
+ }
+ rw := &response{
+ req: req,
+ header: make(http.Header),
+ bufw: bufio.NewWriter(os.Stdout),
+ }
+ handler.ServeHTTP(rw, req)
+ if err = rw.bufw.Flush(); err != nil {
+ return err
+ }
+ return nil
+}
+
+type response struct {
+ req *http.Request
+ header http.Header
+ bufw *bufio.Writer
+ headerSent bool
+}
+
+func (r *response) Flush() {
+ r.bufw.Flush()
+}
+
+func (r *response) RemoteAddr() string {
+ return os.Getenv("REMOTE_ADDR")
+}
+
+func (r *response) Header() http.Header {
+ return r.header
+}
+
+func (r *response) Write(p []byte) (n int, err os.Error) {
+ if !r.headerSent {
+ r.WriteHeader(http.StatusOK)
+ }
+ return r.bufw.Write(p)
+}
+
+func (r *response) WriteHeader(code int) {
+ if r.headerSent {
+ // Note: explicitly using Stderr, as Stdout is our HTTP output.
+ fmt.Fprintf(os.Stderr, "CGI attempted to write header twice on request for %s", r.req.URL)
+ return
+ }
+ r.headerSent = true
+ fmt.Fprintf(r.bufw, "Status: %d %s\r\n", code, http.StatusText(code))
+
+ // Set a default Content-Type
+ if _, hasType := r.header["Content-Type"]; !hasType {
+ r.header.Add("Content-Type", "text/html; charset=utf-8")
+ }
+
+ // TODO: add a method on http.Header to write itself to an io.Writer?
+ // This is duplicated code.
+ for k, vv := range r.header {
+ for _, v := range vv {
+ v = strings.Replace(v, "\n", "", -1)
+ v = strings.Replace(v, "\r", "", -1)
+ v = strings.TrimSpace(v)
+ fmt.Fprintf(r.bufw, "%s: %s\r\n", k, v)
+ }
+ }
+ r.bufw.Write([]byte("\r\n"))
+ r.bufw.Flush()
+}
+
+func (r *response) UsingTLS() bool {
+ // There's apparently a de-facto standard for this.
+ // http://docstore.mik.ua/orelly/linux/cgi/ch03_02.htm#ch03-35636
+ if s := os.Getenv("HTTPS"); s == "on" || s == "ON" || s == "1" {
+ return true
+ }
+ return false
+}
diff --git a/libgo/go/http/cgi/child_test.go b/libgo/go/http/cgi/child_test.go
new file mode 100644
index 00000000000..db0e09cf66a
--- /dev/null
+++ b/libgo/go/http/cgi/child_test.go
@@ -0,0 +1,83 @@
+// 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 CGI (the child process perspective)
+
+package cgi
+
+import (
+ "testing"
+)
+
+func TestRequest(t *testing.T) {
+ env := map[string]string{
+ "REQUEST_METHOD": "GET",
+ "HTTP_HOST": "example.com",
+ "HTTP_REFERER": "elsewhere",
+ "HTTP_USER_AGENT": "goclient",
+ "HTTP_FOO_BAR": "baz",
+ "REQUEST_URI": "/path?a=b",
+ "CONTENT_LENGTH": "123",
+ }
+ req, err := requestFromEnvironment(env)
+ if err != nil {
+ t.Fatalf("requestFromEnvironment: %v", err)
+ }
+ if g, e := req.UserAgent, "goclient"; e != g {
+ t.Errorf("expected UserAgent %q; got %q", e, g)
+ }
+ if g, e := req.Method, "GET"; e != g {
+ t.Errorf("expected Method %q; got %q", e, g)
+ }
+ if g, e := req.Header.Get("User-Agent"), ""; e != g {
+ // Tests that we don't put recognized headers in the map
+ t.Errorf("expected User-Agent %q; got %q", e, g)
+ }
+ if g, e := req.ContentLength, int64(123); e != g {
+ t.Errorf("expected ContentLength %d; got %d", e, g)
+ }
+ if g, e := req.Referer, "elsewhere"; e != g {
+ t.Errorf("expected Referer %q; got %q", e, g)
+ }
+ if req.Header == nil {
+ t.Fatalf("unexpected nil Header")
+ }
+ if g, e := req.Header.Get("Foo-Bar"), "baz"; e != g {
+ t.Errorf("expected Foo-Bar %q; got %q", e, g)
+ }
+ if g, e := req.RawURL, "http://example.com/path?a=b"; e != g {
+ t.Errorf("expected RawURL %q; got %q", e, g)
+ }
+ if g, e := req.URL.String(), "http://example.com/path?a=b"; e != g {
+ t.Errorf("expected URL %q; got %q", e, g)
+ }
+ if g, e := req.FormValue("a"), "b"; e != g {
+ t.Errorf("expected FormValue(a) %q; got %q", e, g)
+ }
+ if req.Trailer == nil {
+ t.Errorf("unexpected nil Trailer")
+ }
+}
+
+func TestRequestWithoutHost(t *testing.T) {
+ env := map[string]string{
+ "HTTP_HOST": "",
+ "REQUEST_METHOD": "GET",
+ "REQUEST_URI": "/path?a=b",
+ "CONTENT_LENGTH": "123",
+ }
+ req, err := requestFromEnvironment(env)
+ if err != nil {
+ t.Fatalf("requestFromEnvironment: %v", err)
+ }
+ if g, e := req.RawURL, "/path?a=b"; e != g {
+ t.Errorf("expected RawURL %q; got %q", e, g)
+ }
+ if req.URL == nil {
+ t.Fatalf("unexpected nil URL")
+ }
+ if g, e := req.URL.String(), "/path?a=b"; e != g {
+ t.Errorf("expected URL %q; got %q", e, g)
+ }
+}
diff --git a/libgo/go/http/cgi/host.go b/libgo/go/http/cgi/host.go
new file mode 100644
index 00000000000..22723873749
--- /dev/null
+++ b/libgo/go/http/cgi/host.go
@@ -0,0 +1,221 @@
+// 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.
+
+// This file implements the host side of CGI (being the webserver
+// parent process).
+
+// 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 (
+ "bytes"
+ "encoding/line"
+ "exec"
+ "fmt"
+ "http"
+ "io"
+ "log"
+ "os"
+ "path/filepath"
+ "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
+ Args []string // optional arguments to pass to child process
+}
+
+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=" + req.RemoteAddr,
+ "REMOTE_HOST=" + req.RemoteAddr,
+ "SERVER_PORT=" + port,
+ }
+
+ if req.TLS != nil {
+ env = append(env, "HTTPS=on")
+ }
+
+ if len(req.Cookie) > 0 {
+ b := new(bytes.Buffer)
+ for idx, c := range req.Cookie {
+ if idx > 0 {
+ b.Write([]byte("; "))
+ }
+ fmt.Fprintf(b, "%s=%s", c.Name, c.Value)
+ }
+ env = append(env, "HTTP_COOKIE="+b.String())
+ }
+
+ for k, v := range req.Header {
+ k = strings.Map(upperCaseAndUnderscore, k)
+ env = append(env, "HTTP_"+k+"="+strings.Join(v, ", "))
+ }
+
+ 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...)
+ }
+
+ cwd, pathBase := filepath.Split(h.Path)
+ if cwd == "" {
+ cwd = "."
+ }
+
+ args := []string{h.Path}
+ args = append(args, h.Args...)
+
+ cmd, err := exec.Run(
+ pathBase,
+ args,
+ 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 := rw.Header()
+ 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.Add(header, val)
+ }
+ }
+ 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
+}
diff --git a/libgo/go/http/cgi/host_test.go b/libgo/go/http/cgi/host_test.go
new file mode 100644
index 00000000000..e8084b1134e
--- /dev/null
+++ b/libgo/go/http/cgi/host_test.go
@@ -0,0 +1,273 @@
+// 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"
+ "exec"
+ "fmt"
+ "http"
+ "http/httptest"
+ "os"
+ "strings"
+ "testing"
+)
+
+var cgiScriptWorks = canRun("./testdata/test.cgi")
+
+func canRun(s string) bool {
+ c, err := exec.Run(s, []string{s}, nil, ".", exec.DevNull, exec.DevNull, exec.DevNull)
+ if err != nil {
+ return false
+ }
+ w, err := c.Wait(0)
+ if err != nil {
+ return false
+ }
+ return w.Exited() && w.ExitStatus() == 0
+}
+
+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"
+ 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)
+ linesRead := 0
+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)
+ }
+ linesRead++
+ trimmedLine := strings.TrimRight(line, "\r\n")
+ split := strings.Split(trimmedLine, "=", 2)
+ if len(split) != 2 {
+ t.Fatalf("Unexpected %d parts from invalid line number %v: %q; existing map=%v",
+ len(split), linesRead, line, m)
+ }
+ 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 skipTest(t *testing.T) bool {
+ if !cgiScriptWorks {
+ // No Perl on Windows, needed by test.cgi
+ // TODO: make the child process be Go, not Perl.
+ t.Logf("Skipping test: test.cgi failed.")
+ return true
+ }
+ return false
+}
+
+
+func TestCGIBasicGet(t *testing.T) {
+ if skipTest(t) {
+ return
+ }
+ 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) {
+ if skipTest(t) {
+ return
+ }
+ 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) {
+ if skipTest(t) {
+ return
+ }
+ 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) {
+ if skipTest(t) {
+ return
+ }
+ 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 TestDupHeaders(t *testing.T) {
+ if skipTest(t) {
+ return
+ }
+ h := &Handler{
+ Path: "testdata/test.cgi",
+ }
+ expectedMap := map[string]string{
+ "env-REQUEST_URI": "/myscript/bar?a=b",
+ "env-SCRIPT_FILENAME": "testdata/test.cgi",
+ "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)
+}
+
+func TestPathInfoNoRoot(t *testing.T) {
+ if skipTest(t) {
+ return
+ }
+ 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) {
+ if skipTest(t) {
+ return
+ }
+ 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) {
+ if skipTest(t) {
+ return
+ }
+ 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)
+ }
+}
diff --git a/libgo/go/http/cgi/matryoshka_test.go b/libgo/go/http/cgi/matryoshka_test.go
new file mode 100644
index 00000000000..3e4a6addfa5
--- /dev/null
+++ b/libgo/go/http/cgi/matryoshka_test.go
@@ -0,0 +1,74 @@
+// 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 a Go CGI program running under a Go CGI host process.
+// Further, the two programs are the same binary, just checking
+// their environment to figure out what mode to run in.
+
+package cgi
+
+import (
+ "fmt"
+ "http"
+ "os"
+ "testing"
+)
+
+// This test is a CGI host (testing host.go) that runs its own binary
+// as a child process testing the other half of CGI (child.go).
+func TestHostingOurselves(t *testing.T) {
+ h := &Handler{
+ Path: os.Args[0],
+ Root: "/test.go",
+ Args: []string{"-test.run=TestBeChildCGIProcess"},
+ }
+ expectedMap := map[string]string{
+ "test": "Hello CGI-in-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.go?foo=bar&a=b",
+ "env-SCRIPT_FILENAME": os.Args[0],
+ "env-SCRIPT_NAME": "/test.go",
+ "env-SERVER_NAME": "example.com",
+ "env-SERVER_PORT": "80",
+ "env-SERVER_SOFTWARE": "go",
+ }
+ replay := runCgiTest(t, h, "GET /test.go?foo=bar&a=b HTTP/1.0\nHost: example.com\n\n", expectedMap)
+
+ if expected, got := "text/html; charset=utf-8", 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)
+ }
+}
+
+// Note: not actually a test.
+func TestBeChildCGIProcess(t *testing.T) {
+ if os.Getenv("REQUEST_METHOD") == "" {
+ // Not in a CGI environment; skipping test.
+ return
+ }
+ Serve(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+ rw.Header().Set("X-Test-Header", "X-Test-Value")
+ fmt.Fprintf(rw, "test=Hello CGI-in-CGI\n")
+ req.ParseForm()
+ for k, vv := range req.Form {
+ for _, v := range vv {
+ fmt.Fprintf(rw, "param-%s=%s\n", k, v)
+ }
+ }
+ for _, kv := range os.Environ() {
+ fmt.Fprintf(rw, "env-%s\n", kv)
+ }
+ }))
+ os.Exit(0)
+}
diff --git a/libgo/go/http/client.go b/libgo/go/http/client.go
index b1fe5ec6780..daba3a89b0c 100644
--- a/libgo/go/http/client.go
+++ b/libgo/go/http/client.go
@@ -11,6 +11,7 @@ import (
"encoding/base64"
"fmt"
"io"
+ "io/ioutil"
"os"
"strconv"
"strings"
@@ -20,26 +21,28 @@ import (
// that uses DefaultTransport.
// Client is not yet very configurable.
type Client struct {
- Transport ClientTransport // if nil, DefaultTransport is used
+ Transport RoundTripper // if nil, DefaultTransport is used
}
// DefaultClient is the default Client and is used by Get, Head, and Post.
var DefaultClient = &Client{}
-// ClientTransport is an interface representing the ability to execute a
+// RoundTripper is an interface representing the ability to execute a
// single HTTP transaction, obtaining the Response for a given Request.
-type ClientTransport interface {
- // Do executes a single HTTP transaction, returning the Response for the
- // request req. Do should not attempt to interpret the response.
- // In particular, Do must return err == nil if it obtained a response,
- // regardless of the response's HTTP status code. A non-nil err should
- // be reserved for failure to obtain a response. Similarly, Do should
- // not attempt to handle higher-level protocol details such as redirects,
+type RoundTripper interface {
+ // RoundTrip executes a single HTTP transaction, returning
+ // the Response for the request req. RoundTrip should not
+ // attempt to interpret the response. In particular,
+ // RoundTrip must return err == nil if it obtained a response,
+ // regardless of the response's HTTP status code. A non-nil
+ // err should be reserved for failure to obtain a response.
+ // Similarly, RoundTrip should not attempt to handle
+ // higher-level protocol details such as redirects,
// authentication, or cookies.
//
- // Transports may modify the request. The request Headers field is
- // guaranteed to be initalized.
- Do(req *Request) (resp *Response, err os.Error)
+ // RoundTrip may modify the request. The request Headers field is
+ // guaranteed to be initialized.
+ RoundTrip(req *Request) (resp *Response, err os.Error)
}
// Given a string of the form "host", "host:port", or "[ipv6::address]:port",
@@ -54,40 +57,6 @@ type readClose struct {
io.Closer
}
-// matchNoProxy returns true if requests to addr should not use a proxy,
-// according to the NO_PROXY or no_proxy environment variable.
-func matchNoProxy(addr string) bool {
- if len(addr) == 0 {
- return false
- }
- no_proxy := os.Getenv("NO_PROXY")
- if len(no_proxy) == 0 {
- no_proxy = os.Getenv("no_proxy")
- }
- if no_proxy == "*" {
- return true
- }
-
- addr = strings.ToLower(strings.TrimSpace(addr))
- if hasPort(addr) {
- addr = addr[:strings.LastIndex(addr, ":")]
- }
-
- for _, p := range strings.Split(no_proxy, ",", -1) {
- p = strings.ToLower(strings.TrimSpace(p))
- if len(p) == 0 {
- continue
- }
- if hasPort(p) {
- p = p[:strings.LastIndex(p, ":")]
- }
- if addr == p || (p[0] == '.' && (strings.HasSuffix(addr, p) || addr == p[1:])) {
- return true
- }
- }
- return false
-}
-
// Do sends an HTTP request and returns an HTTP response, following
// policy (e.g. redirects, cookies, auth) as configured on the client.
//
@@ -100,11 +69,7 @@ func (c *Client) Do(req *Request) (resp *Response, err os.Error) {
// send issues an HTTP request. Caller should close resp.Body when done reading from it.
-//
-// TODO: support persistent connections (multiple requests on a single connection).
-// send() method is nonpublic because, when we refactor the code for persistent
-// connections, it may no longer make sense to have a method with this signature.
-func send(req *Request, t ClientTransport) (resp *Response, err os.Error) {
+func send(req *Request, t RoundTripper) (resp *Response, err os.Error) {
if t == nil {
t = DefaultTransport
if t == nil {
@@ -115,9 +80,9 @@ func send(req *Request, t ClientTransport) (resp *Response, err os.Error) {
// Most the callers of send (Get, Post, et al) don't need
// Headers, leaving it uninitialized. We guarantee to the
- // ClientTransport that this has been initialized, though.
+ // Transport that this has been initialized, though.
if req.Header == nil {
- req.Header = Header(make(map[string][]string))
+ req.Header = make(Header)
}
info := req.URL.RawUserinfo
@@ -130,7 +95,7 @@ func send(req *Request, t ClientTransport) (resp *Response, err os.Error) {
}
req.Header.Set("Authorization", "Basic "+string(encoded))
}
- return t.Do(req)
+ return t.RoundTrip(req)
}
// True if the specified HTTP status code is one for which the Get utility should
@@ -237,7 +202,7 @@ func (c *Client) Post(url string, bodyType string, body io.Reader) (r *Response,
req.ProtoMajor = 1
req.ProtoMinor = 1
req.Close = true
- req.Body = nopCloser{body}
+ req.Body = ioutil.NopCloser(body)
req.Header = Header{
"Content-Type": {bodyType},
}
@@ -272,7 +237,7 @@ func (c *Client) PostForm(url string, data map[string]string) (r *Response, err
req.ProtoMinor = 1
req.Close = true
body := urlencode(data)
- req.Body = nopCloser{body}
+ req.Body = ioutil.NopCloser(body)
req.Header = Header{
"Content-Type": {"application/x-www-form-urlencoded"},
"Content-Length": {strconv.Itoa(body.Len())},
@@ -312,9 +277,3 @@ func (c *Client) Head(url string) (r *Response, err os.Error) {
}
return send(&req, c.Transport)
}
-
-type nopCloser struct {
- io.Reader
-}
-
-func (nopCloser) Close() os.Error { return nil }
diff --git a/libgo/go/http/client_test.go b/libgo/go/http/client_test.go
index c89ecbce2d0..3a6f834253b 100644
--- a/libgo/go/http/client_test.go
+++ b/libgo/go/http/client_test.go
@@ -4,20 +4,28 @@
// Tests for client.go
-package http
+package http_test
import (
+ "fmt"
+ . "http"
+ "http/httptest"
"io/ioutil"
"os"
"strings"
"testing"
)
+var robotsTxtHandler = HandlerFunc(func(w ResponseWriter, r *Request) {
+ w.Header().Set("Last-Modified", "sometime")
+ fmt.Fprintf(w, "User-agent: go\nDisallow: /something/")
+})
+
func TestClient(t *testing.T) {
- // TODO: add a proper test suite. Current test merely verifies that
- // we can retrieve the Google robots.txt file.
+ ts := httptest.NewServer(robotsTxtHandler)
+ defer ts.Close()
- r, _, err := Get("http://www.google.com/robots.txt")
+ r, _, err := Get(ts.URL)
var b []byte
if err == nil {
b, err = ioutil.ReadAll(r.Body)
@@ -31,7 +39,10 @@ func TestClient(t *testing.T) {
}
func TestClientHead(t *testing.T) {
- r, err := Head("http://www.google.com/robots.txt")
+ ts := httptest.NewServer(robotsTxtHandler)
+ defer ts.Close()
+
+ r, err := Head(ts.URL)
if err != nil {
t.Fatal(err)
}
@@ -44,7 +55,7 @@ type recordingTransport struct {
req *Request
}
-func (t *recordingTransport) Do(req *Request) (resp *Response, err os.Error) {
+func (t *recordingTransport) RoundTrip(req *Request) (resp *Response, err os.Error) {
t.req = req
return nil, os.NewError("dummy impl")
}
diff --git a/libgo/go/http/cookie.go b/libgo/go/http/cookie.go
new file mode 100644
index 00000000000..2bb66e58e5c
--- /dev/null
+++ b/libgo/go/http/cookie.go
@@ -0,0 +1,272 @@
+// Copyright 2009 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 http
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "os"
+ "sort"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// This implementation is done according to IETF draft-ietf-httpstate-cookie-23, found at
+//
+// http://tools.ietf.org/html/draft-ietf-httpstate-cookie-23
+
+// A Cookie represents an HTTP cookie as sent in the Set-Cookie header of an
+// HTTP response or the Cookie header of an HTTP request.
+type Cookie struct {
+ Name string
+ Value string
+ Path string
+ Domain string
+ Expires time.Time
+ RawExpires string
+
+ // MaxAge=0 means no 'Max-Age' attribute specified.
+ // MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
+ // MaxAge>0 means Max-Age attribute present and given in seconds
+ MaxAge int
+ Secure bool
+ HttpOnly bool
+ Raw string
+ Unparsed []string // Raw text of unparsed attribute-value pairs
+}
+
+// readSetCookies parses all "Set-Cookie" values from
+// the header h, removes the successfully parsed values from the
+// "Set-Cookie" key in h and returns the parsed Cookies.
+func readSetCookies(h Header) []*Cookie {
+ cookies := []*Cookie{}
+ var unparsedLines []string
+ for _, line := range h["Set-Cookie"] {
+ parts := strings.Split(strings.TrimSpace(line), ";", -1)
+ if len(parts) == 1 && parts[0] == "" {
+ continue
+ }
+ parts[0] = strings.TrimSpace(parts[0])
+ j := strings.Index(parts[0], "=")
+ if j < 0 {
+ unparsedLines = append(unparsedLines, line)
+ continue
+ }
+ name, value := parts[0][:j], parts[0][j+1:]
+ if !isCookieNameValid(name) {
+ unparsedLines = append(unparsedLines, line)
+ continue
+ }
+ value, success := parseCookieValue(value)
+ if !success {
+ unparsedLines = append(unparsedLines, line)
+ continue
+ }
+ c := &Cookie{
+ Name: name,
+ Value: value,
+ Raw: line,
+ }
+ for i := 1; i < len(parts); i++ {
+ parts[i] = strings.TrimSpace(parts[i])
+ if len(parts[i]) == 0 {
+ continue
+ }
+
+ attr, val := parts[i], ""
+ if j := strings.Index(attr, "="); j >= 0 {
+ attr, val = attr[:j], attr[j+1:]
+ }
+ val, success = parseCookieValue(val)
+ if !success {
+ c.Unparsed = append(c.Unparsed, parts[i])
+ continue
+ }
+ switch strings.ToLower(attr) {
+ case "secure":
+ c.Secure = true
+ continue
+ case "httponly":
+ c.HttpOnly = true
+ continue
+ case "domain":
+ c.Domain = val
+ // TODO: Add domain parsing
+ continue
+ case "max-age":
+ secs, err := strconv.Atoi(val)
+ if err != nil || secs < 0 || secs != 0 && val[0] == '0' {
+ break
+ }
+ if secs <= 0 {
+ c.MaxAge = -1
+ } else {
+ c.MaxAge = secs
+ }
+ continue
+ case "expires":
+ c.RawExpires = val
+ exptime, err := time.Parse(time.RFC1123, val)
+ if err != nil {
+ c.Expires = time.Time{}
+ break
+ }
+ c.Expires = *exptime
+ continue
+ case "path":
+ c.Path = val
+ // TODO: Add path parsing
+ continue
+ }
+ c.Unparsed = append(c.Unparsed, parts[i])
+ }
+ cookies = append(cookies, c)
+ }
+ h["Set-Cookie"] = unparsedLines, unparsedLines != nil
+ return cookies
+}
+
+// writeSetCookies writes the wire representation of the set-cookies
+// to w. Each cookie is written on a separate "Set-Cookie: " line.
+// This choice is made because HTTP parsers tend to have a limit on
+// line-length, so it seems safer to place cookies on separate lines.
+func writeSetCookies(w io.Writer, kk []*Cookie) os.Error {
+ if kk == nil {
+ return nil
+ }
+ lines := make([]string, 0, len(kk))
+ var b bytes.Buffer
+ for _, c := range kk {
+ b.Reset()
+ fmt.Fprintf(&b, "%s=%s", c.Name, c.Value)
+ if len(c.Path) > 0 {
+ fmt.Fprintf(&b, "; Path=%s", URLEscape(c.Path))
+ }
+ if len(c.Domain) > 0 {
+ fmt.Fprintf(&b, "; Domain=%s", URLEscape(c.Domain))
+ }
+ if len(c.Expires.Zone) > 0 {
+ fmt.Fprintf(&b, "; Expires=%s", c.Expires.Format(time.RFC1123))
+ }
+ if c.MaxAge > 0 {
+ fmt.Fprintf(&b, "; Max-Age=%d", c.MaxAge)
+ } else if c.MaxAge < 0 {
+ fmt.Fprintf(&b, "; Max-Age=0")
+ }
+ if c.HttpOnly {
+ fmt.Fprintf(&b, "; HttpOnly")
+ }
+ if c.Secure {
+ fmt.Fprintf(&b, "; Secure")
+ }
+ lines = append(lines, "Set-Cookie: "+b.String()+"\r\n")
+ }
+ sort.SortStrings(lines)
+ for _, l := range lines {
+ if _, err := io.WriteString(w, l); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// readCookies parses all "Cookie" values from
+// the header h, removes the successfully parsed values from the
+// "Cookie" key in h and returns the parsed Cookies.
+func readCookies(h Header) []*Cookie {
+ cookies := []*Cookie{}
+ lines, ok := h["Cookie"]
+ if !ok {
+ return cookies
+ }
+ unparsedLines := []string{}
+ for _, line := range lines {
+ parts := strings.Split(strings.TrimSpace(line), ";", -1)
+ if len(parts) == 1 && parts[0] == "" {
+ continue
+ }
+ // Per-line attributes
+ parsedPairs := 0
+ for i := 0; i < len(parts); i++ {
+ parts[i] = strings.TrimSpace(parts[i])
+ if len(parts[i]) == 0 {
+ continue
+ }
+ attr, val := parts[i], ""
+ if j := strings.Index(attr, "="); j >= 0 {
+ attr, val = attr[:j], attr[j+1:]
+ }
+ if !isCookieNameValid(attr) {
+ continue
+ }
+ val, success := parseCookieValue(val)
+ if !success {
+ continue
+ }
+ cookies = append(cookies, &Cookie{Name: attr, Value: val})
+ parsedPairs++
+ }
+ if parsedPairs == 0 {
+ unparsedLines = append(unparsedLines, line)
+ }
+ }
+ h["Cookie"] = unparsedLines, len(unparsedLines) > 0
+ return cookies
+}
+
+// writeCookies writes the wire representation of the cookies
+// to w. Each cookie is written on a separate "Cookie: " line.
+// This choice is made because HTTP parsers tend to have a limit on
+// line-length, so it seems safer to place cookies on separate lines.
+func writeCookies(w io.Writer, kk []*Cookie) os.Error {
+ lines := make([]string, 0, len(kk))
+ for _, c := range kk {
+ lines = append(lines, fmt.Sprintf("Cookie: %s=%s\r\n", c.Name, c.Value))
+ }
+ sort.SortStrings(lines)
+ for _, l := range lines {
+ if _, err := io.WriteString(w, l); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func unquoteCookieValue(v string) string {
+ if len(v) > 1 && v[0] == '"' && v[len(v)-1] == '"' {
+ return v[1 : len(v)-1]
+ }
+ return v
+}
+
+func isCookieByte(c byte) bool {
+ switch true {
+ case c == 0x21, 0x23 <= c && c <= 0x2b, 0x2d <= c && c <= 0x3a,
+ 0x3c <= c && c <= 0x5b, 0x5d <= c && c <= 0x7e:
+ return true
+ }
+ return false
+}
+
+func parseCookieValue(raw string) (string, bool) {
+ raw = unquoteCookieValue(raw)
+ for i := 0; i < len(raw); i++ {
+ if !isCookieByte(raw[i]) {
+ return "", false
+ }
+ }
+ return raw, true
+}
+
+func isCookieNameValid(raw string) bool {
+ for _, c := range raw {
+ if !isToken(byte(c)) {
+ return false
+ }
+ }
+ return true
+}
diff --git a/libgo/go/http/cookie_test.go b/libgo/go/http/cookie_test.go
new file mode 100644
index 00000000000..db09970406b
--- /dev/null
+++ b/libgo/go/http/cookie_test.go
@@ -0,0 +1,110 @@
+// Copyright 2010 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 http
+
+import (
+ "bytes"
+ "fmt"
+ "json"
+ "reflect"
+ "testing"
+)
+
+
+var writeSetCookiesTests = []struct {
+ Cookies []*Cookie
+ Raw string
+}{
+ {
+ []*Cookie{
+ &Cookie{Name: "cookie-1", Value: "v$1"},
+ &Cookie{Name: "cookie-2", Value: "two", MaxAge: 3600},
+ },
+ "Set-Cookie: cookie-1=v$1\r\n" +
+ "Set-Cookie: cookie-2=two; Max-Age=3600\r\n",
+ },
+}
+
+func TestWriteSetCookies(t *testing.T) {
+ for i, tt := range writeSetCookiesTests {
+ var w bytes.Buffer
+ writeSetCookies(&w, tt.Cookies)
+ seen := string(w.Bytes())
+ if seen != tt.Raw {
+ t.Errorf("Test %d, expecting:\n%s\nGot:\n%s\n", i, tt.Raw, seen)
+ continue
+ }
+ }
+}
+
+var writeCookiesTests = []struct {
+ Cookies []*Cookie
+ Raw string
+}{
+ {
+ []*Cookie{&Cookie{Name: "cookie-1", Value: "v$1"}},
+ "Cookie: cookie-1=v$1\r\n",
+ },
+}
+
+func TestWriteCookies(t *testing.T) {
+ for i, tt := range writeCookiesTests {
+ var w bytes.Buffer
+ writeCookies(&w, tt.Cookies)
+ seen := string(w.Bytes())
+ if seen != tt.Raw {
+ t.Errorf("Test %d, expecting:\n%s\nGot:\n%s\n", i, tt.Raw, seen)
+ continue
+ }
+ }
+}
+
+var readSetCookiesTests = []struct {
+ Header Header
+ Cookies []*Cookie
+}{
+ {
+ Header{"Set-Cookie": {"Cookie-1=v$1"}},
+ []*Cookie{&Cookie{Name: "Cookie-1", Value: "v$1", Raw: "Cookie-1=v$1"}},
+ },
+}
+
+func toJSON(v interface{}) string {
+ b, err := json.Marshal(v)
+ if err != nil {
+ return fmt.Sprintf("%#v", v)
+ }
+ return string(b)
+}
+
+func TestReadSetCookies(t *testing.T) {
+ for i, tt := range readSetCookiesTests {
+ c := readSetCookies(tt.Header)
+ if !reflect.DeepEqual(c, tt.Cookies) {
+ t.Errorf("#%d readSetCookies: have\n%s\nwant\n%s\n", i, toJSON(c), toJSON(tt.Cookies))
+ continue
+ }
+ }
+}
+
+var readCookiesTests = []struct {
+ Header Header
+ Cookies []*Cookie
+}{
+ {
+ Header{"Cookie": {"Cookie-1=v$1"}},
+ []*Cookie{&Cookie{Name: "Cookie-1", Value: "v$1"}},
+ },
+}
+
+func TestReadCookies(t *testing.T) {
+ for i, tt := range readCookiesTests {
+ c := readCookies(tt.Header)
+ if !reflect.DeepEqual(c, tt.Cookies) {
+ t.Errorf("#%d readCookies: have\n%s\nwant\n%s\n", i, toJSON(c), toJSON(tt.Cookies))
+ continue
+ }
+ }
+}
diff --git a/libgo/go/http/dump.go b/libgo/go/http/dump.go
index 73ac9797399..306c45bc2c9 100644
--- a/libgo/go/http/dump.go
+++ b/libgo/go/http/dump.go
@@ -7,10 +7,10 @@ package http
import (
"bytes"
"io"
+ "io/ioutil"
"os"
)
-
// One of the copies, say from b to r2, could be avoided by using a more
// elaborate trick where the other copy is made during Request/Response.Write.
// This would complicate things too much, given that these functions are for
@@ -23,7 +23,7 @@ func drainBody(b io.ReadCloser) (r1, r2 io.ReadCloser, err os.Error) {
if err = b.Close(); err != nil {
return nil, nil, err
}
- return nopCloser{&buf}, nopCloser{bytes.NewBuffer(buf.Bytes())}, nil
+ return ioutil.NopCloser(&buf), ioutil.NopCloser(bytes.NewBuffer(buf.Bytes())), nil
}
// DumpRequest returns the wire representation of req,
diff --git a/libgo/go/http/export_test.go b/libgo/go/http/export_test.go
new file mode 100644
index 00000000000..a76b70760df
--- /dev/null
+++ b/libgo/go/http/export_test.go
@@ -0,0 +1,34 @@
+// 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.
+
+// Bridge package to expose http internals to tests in the http_test
+// package.
+
+package http
+
+func (t *Transport) IdleConnKeysForTesting() (keys []string) {
+ keys = make([]string, 0)
+ t.lk.Lock()
+ defer t.lk.Unlock()
+ if t.idleConn == nil {
+ return
+ }
+ for key, _ := range t.idleConn {
+ keys = append(keys, key)
+ }
+ return
+}
+
+func (t *Transport) IdleConnCountForTesting(cacheKey string) int {
+ t.lk.Lock()
+ defer t.lk.Unlock()
+ if t.idleConn == nil {
+ return 0
+ }
+ conns, ok := t.idleConn[cacheKey]
+ if !ok {
+ return 0
+ }
+ return len(conns)
+}
diff --git a/libgo/go/http/fs.go b/libgo/go/http/fs.go
index 8e16992e0f0..4ad680ccc31 100644
--- a/libgo/go/http/fs.go
+++ b/libgo/go/http/fs.go
@@ -11,7 +11,7 @@ import (
"io"
"mime"
"os"
- "path"
+ "path/filepath"
"strconv"
"strings"
"time"
@@ -108,11 +108,11 @@ func serveFile(w ResponseWriter, r *Request, name string, redirect bool) {
w.WriteHeader(StatusNotModified)
return
}
- w.SetHeader("Last-Modified", time.SecondsToUTC(d.Mtime_ns/1e9).Format(TimeFormat))
+ w.Header().Set("Last-Modified", time.SecondsToUTC(d.Mtime_ns/1e9).Format(TimeFormat))
// use contents of index.html for directory, if present
if d.IsDirectory() {
- index := name + indexPage
+ index := name + filepath.FromSlash(indexPage)
ff, err := os.Open(index, os.O_RDONLY, 0)
if err == nil {
defer ff.Close()
@@ -135,18 +135,18 @@ func serveFile(w ResponseWriter, r *Request, name string, redirect bool) {
code := StatusOK
// use extension to find content type.
- ext := path.Ext(name)
+ ext := filepath.Ext(name)
if ctype := mime.TypeByExtension(ext); ctype != "" {
- w.SetHeader("Content-Type", ctype)
+ w.Header().Set("Content-Type", ctype)
} else {
// read first chunk to decide between utf-8 text and binary
var buf [1024]byte
n, _ := io.ReadFull(f, buf[:])
b := buf[:n]
if isText(b) {
- w.SetHeader("Content-Type", "text-plain; charset=utf-8")
+ w.Header().Set("Content-Type", "text-plain; charset=utf-8")
} else {
- w.SetHeader("Content-Type", "application/octet-stream") // generic binary
+ w.Header().Set("Content-Type", "application/octet-stream") // generic binary
}
f.Seek(0, 0) // rewind to output whole file
}
@@ -166,11 +166,11 @@ func serveFile(w ResponseWriter, r *Request, name string, redirect bool) {
}
size = ra.length
code = StatusPartialContent
- w.SetHeader("Content-Range", fmt.Sprintf("bytes %d-%d/%d", ra.start, ra.start+ra.length-1, d.Size))
+ w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", ra.start, ra.start+ra.length-1, d.Size))
}
- w.SetHeader("Accept-Ranges", "bytes")
- w.SetHeader("Content-Length", strconv.Itoa64(size))
+ w.Header().Set("Accept-Ranges", "bytes")
+ w.Header().Set("Content-Length", strconv.Itoa64(size))
w.WriteHeader(code)
@@ -202,7 +202,7 @@ func (f *fileHandler) ServeHTTP(w ResponseWriter, r *Request) {
return
}
path = path[len(f.prefix):]
- serveFile(w, r, f.root+"/"+path, true)
+ serveFile(w, r, filepath.Join(f.root, filepath.FromSlash(path)), true)
}
// httpRange specifies the byte range to be sent to the client.
diff --git a/libgo/go/http/fs_test.go b/libgo/go/http/fs_test.go
index a8b67e3f08c..a89c76d0bfb 100644
--- a/libgo/go/http/fs_test.go
+++ b/libgo/go/http/fs_test.go
@@ -2,89 +2,22 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package http
+package http_test
import (
"fmt"
+ . "http"
+ "http/httptest"
"io/ioutil"
- "net"
"os"
- "sync"
"testing"
)
-var ParseRangeTests = []struct {
- s string
- length int64
- r []httpRange
-}{
- {"", 0, nil},
- {"foo", 0, nil},
- {"bytes=", 0, nil},
- {"bytes=5-4", 10, nil},
- {"bytes=0-2,5-4", 10, nil},
- {"bytes=0-9", 10, []httpRange{{0, 10}}},
- {"bytes=0-", 10, []httpRange{{0, 10}}},
- {"bytes=5-", 10, []httpRange{{5, 5}}},
- {"bytes=0-20", 10, []httpRange{{0, 10}}},
- {"bytes=15-,0-5", 10, nil},
- {"bytes=-5", 10, []httpRange{{5, 5}}},
- {"bytes=-15", 10, []httpRange{{0, 10}}},
- {"bytes=0-499", 10000, []httpRange{{0, 500}}},
- {"bytes=500-999", 10000, []httpRange{{500, 500}}},
- {"bytes=-500", 10000, []httpRange{{9500, 500}}},
- {"bytes=9500-", 10000, []httpRange{{9500, 500}}},
- {"bytes=0-0,-1", 10000, []httpRange{{0, 1}, {9999, 1}}},
- {"bytes=500-600,601-999", 10000, []httpRange{{500, 101}, {601, 399}}},
- {"bytes=500-700,601-999", 10000, []httpRange{{500, 201}, {601, 399}}},
-}
-
-func TestParseRange(t *testing.T) {
- for _, test := range ParseRangeTests {
- r := test.r
- ranges, err := parseRange(test.s, test.length)
- if err != nil && r != nil {
- t.Errorf("parseRange(%q) returned error %q", test.s, err)
- }
- if len(ranges) != len(r) {
- t.Errorf("len(parseRange(%q)) = %d, want %d", test.s, len(ranges), len(r))
- continue
- }
- for i := range r {
- if ranges[i].start != r[i].start {
- t.Errorf("parseRange(%q)[%d].start = %d, want %d", test.s, i, ranges[i].start, r[i].start)
- }
- if ranges[i].length != r[i].length {
- t.Errorf("parseRange(%q)[%d].length = %d, want %d", test.s, i, ranges[i].length, r[i].length)
- }
- }
- }
-}
-
const (
testFile = "testdata/file"
testFileLength = 11
)
-var (
- serverOnce sync.Once
- serverAddr string
-)
-
-func startServer(t *testing.T) {
- serverOnce.Do(func() {
- HandleFunc("/ServeFile", func(w ResponseWriter, r *Request) {
- ServeFile(w, r, "testdata/file")
- })
- l, err := net.Listen("tcp", "127.0.0.1:0")
- if err != nil {
- t.Fatal("listen:", err)
- }
- serverAddr = l.Addr().String()
- go Serve(l, nil)
- })
-}
-
var ServeFileRangeTests = []struct {
start, end int
r string
@@ -99,7 +32,11 @@ var ServeFileRangeTests = []struct {
}
func TestServeFile(t *testing.T) {
- startServer(t)
+ ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
+ ServeFile(w, r, "testdata/file")
+ }))
+ defer ts.Close()
+
var err os.Error
file, err := ioutil.ReadFile(testFile)
@@ -110,7 +47,7 @@ func TestServeFile(t *testing.T) {
// set up the Request (re-used for all tests)
var req Request
req.Header = make(Header)
- if req.URL, err = ParseURL("http://" + serverAddr + "/ServeFile"); err != nil {
+ if req.URL, err = ParseURL(ts.URL); err != nil {
t.Fatal("ParseURL:", err)
}
req.Method = "GET"
@@ -149,7 +86,7 @@ func TestServeFile(t *testing.T) {
}
func getBody(t *testing.T, req Request) (*Response, []byte) {
- r, err := send(&req, DefaultTransport)
+ r, err := DefaultClient.Do(&req)
if err != nil {
t.Fatal(req.URL.String(), "send:", err)
}
diff --git a/libgo/go/http/httptest/recorder.go b/libgo/go/http/httptest/recorder.go
new file mode 100644
index 00000000000..0dd19a617cc
--- /dev/null
+++ b/libgo/go/http/httptest/recorder.go
@@ -0,0 +1,59 @@
+// 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 (
+ "bytes"
+ "http"
+ "os"
+)
+
+// ResponseRecorder is an implementation of http.ResponseWriter that
+// records its mutations for later inspection in tests.
+type ResponseRecorder struct {
+ Code int // the HTTP response code from WriteHeader
+ HeaderMap http.Header // the HTTP response headers
+ Body *bytes.Buffer // if non-nil, the bytes.Buffer to append written data to
+ Flushed bool
+}
+
+// NewRecorder returns an initialized ResponseRecorder.
+func NewRecorder() *ResponseRecorder {
+ return &ResponseRecorder{
+ HeaderMap: make(http.Header),
+ 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"
+
+// Header returns the response headers.
+func (rw *ResponseRecorder) Header() http.Header {
+ return rw.HeaderMap
+}
+
+// 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)
+ }
+ if rw.Code == 0 {
+ rw.Code = http.StatusOK
+ }
+ 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
+}
diff --git a/libgo/go/http/httptest/server.go b/libgo/go/http/httptest/server.go
new file mode 100644
index 00000000000..6e825a890d1
--- /dev/null
+++ b/libgo/go/http/httptest/server.go
@@ -0,0 +1,70 @@
+// 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.
+
+// Implementation of Server
+
+package httptest
+
+import (
+ "fmt"
+ "http"
+ "os"
+ "net"
+)
+
+// A Server is an HTTP server listening on a system-chosen port on the
+// local loopback interface, for use in end-to-end HTTP tests.
+type Server struct {
+ URL string // base URL of form http://ipaddr:port with no trailing slash
+ Listener net.Listener
+}
+
+// historyListener keeps track of all connections that it's ever
+// accepted.
+type historyListener struct {
+ net.Listener
+ history []net.Conn
+}
+
+func (hs *historyListener) Accept() (c net.Conn, err os.Error) {
+ c, err = hs.Listener.Accept()
+ if err == nil {
+ hs.history = append(hs.history, c)
+ }
+ return
+}
+
+// NewServer starts and returns a new Server.
+// The caller should call Close when finished, to shut it down.
+func NewServer(handler http.Handler) *Server {
+ ts := new(Server)
+ l, err := net.Listen("tcp", "127.0.0.1:0")
+ if err != nil {
+ if l, err = net.Listen("tcp6", "[::1]:0"); err != nil {
+ panic(fmt.Sprintf("httptest: failed to listen on a port: %v", err))
+ }
+ }
+ ts.Listener = &historyListener{l, make([]net.Conn, 0)}
+ ts.URL = "http://" + l.Addr().String()
+ server := &http.Server{Handler: handler}
+ go server.Serve(ts.Listener)
+ return ts
+}
+
+// Close shuts down the server.
+func (s *Server) Close() {
+ s.Listener.Close()
+}
+
+// CloseClientConnections closes any currently open HTTP connections
+// to the test Server.
+func (s *Server) CloseClientConnections() {
+ hl, ok := s.Listener.(*historyListener)
+ if !ok {
+ return
+ }
+ for _, conn := range hl.history {
+ conn.Close()
+ }
+}
diff --git a/libgo/go/http/persist.go b/libgo/go/http/persist.go
index 000a4200e59..b93c5fe4855 100644
--- a/libgo/go/http/persist.go
+++ b/libgo/go/http/persist.go
@@ -25,15 +25,15 @@ var (
// i.e. requests can be read out of sync (but in the same order) while the
// respective responses are sent.
type ServerConn struct {
+ lk sync.Mutex // read-write protects the following fields
c net.Conn
r *bufio.Reader
- clsd bool // indicates a graceful close
re, we os.Error // read/write errors
lastbody io.ReadCloser
nread, nwritten int
- pipe textproto.Pipeline
pipereq map[*Request]uint
- lk sync.Mutex // protected read/write to re,we
+
+ pipe textproto.Pipeline
}
// NewServerConn returns a new ServerConn reading and writing c. If r is not
@@ -90,15 +90,21 @@ func (sc *ServerConn) Read() (req *Request, err os.Error) {
defer sc.lk.Unlock()
return nil, sc.re
}
+ if sc.r == nil { // connection closed by user in the meantime
+ defer sc.lk.Unlock()
+ return nil, os.EBADF
+ }
+ r := sc.r
+ lastbody := sc.lastbody
+ sc.lastbody = nil
sc.lk.Unlock()
// Make sure body is fully consumed, even if user does not call body.Close
- if sc.lastbody != nil {
+ if lastbody != nil {
// body.Close is assumed to be idempotent and multiple calls to
// it should return the error that its first invokation
// returned.
- err = sc.lastbody.Close()
- sc.lastbody = nil
+ err = lastbody.Close()
if err != nil {
sc.lk.Lock()
defer sc.lk.Unlock()
@@ -107,10 +113,10 @@ func (sc *ServerConn) Read() (req *Request, err os.Error) {
}
}
- req, err = ReadRequest(sc.r)
+ req, err = ReadRequest(r)
+ sc.lk.Lock()
+ defer sc.lk.Unlock()
if err != nil {
- sc.lk.Lock()
- defer sc.lk.Unlock()
if err == io.ErrUnexpectedEOF {
// A close from the opposing client is treated as a
// graceful close, even if there was some unparse-able
@@ -119,18 +125,16 @@ func (sc *ServerConn) Read() (req *Request, err os.Error) {
return nil, sc.re
} else {
sc.re = err
- return
+ return req, err
}
}
sc.lastbody = req.Body
sc.nread++
if req.Close {
- sc.lk.Lock()
- defer sc.lk.Unlock()
sc.re = ErrPersistEOF
return req, sc.re
}
- return
+ return req, err
}
// Pending returns the number of unanswered requests
@@ -165,24 +169,27 @@ func (sc *ServerConn) Write(req *Request, resp *Response) os.Error {
defer sc.lk.Unlock()
return sc.we
}
- sc.lk.Unlock()
+ if sc.c == nil { // connection closed by user in the meantime
+ defer sc.lk.Unlock()
+ return os.EBADF
+ }
+ c := sc.c
if sc.nread <= sc.nwritten {
+ defer sc.lk.Unlock()
return os.NewError("persist server pipe count")
}
-
if resp.Close {
// After signaling a keep-alive close, any pipelined unread
// requests will be lost. It is up to the user to drain them
// before signaling.
- sc.lk.Lock()
sc.re = ErrPersistEOF
- sc.lk.Unlock()
}
+ sc.lk.Unlock()
- err := resp.Write(sc.c)
+ err := resp.Write(c)
+ sc.lk.Lock()
+ defer sc.lk.Unlock()
if err != nil {
- sc.lk.Lock()
- defer sc.lk.Unlock()
sc.we = err
return err
}
@@ -196,14 +203,17 @@ func (sc *ServerConn) Write(req *Request, resp *Response) os.Error {
// responsible for closing the underlying connection. One must call Close to
// regain control of that connection and deal with it as desired.
type ClientConn struct {
+ lk sync.Mutex // read-write protects the following fields
c net.Conn
r *bufio.Reader
re, we os.Error // read/write errors
lastbody io.ReadCloser
nread, nwritten int
- pipe textproto.Pipeline
pipereq map[*Request]uint
- lk sync.Mutex // protects read/write to re,we,pipereq,etc.
+
+ pipe textproto.Pipeline
+ writeReq func(*Request, io.Writer) os.Error
+ readRes func(buf *bufio.Reader, method string) (*Response, os.Error)
}
// NewClientConn returns a new ClientConn reading and writing c. If r is not
@@ -212,7 +222,21 @@ func NewClientConn(c net.Conn, r *bufio.Reader) *ClientConn {
if r == nil {
r = bufio.NewReader(c)
}
- return &ClientConn{c: c, r: r, pipereq: make(map[*Request]uint)}
+ return &ClientConn{
+ c: c,
+ r: r,
+ pipereq: make(map[*Request]uint),
+ writeReq: (*Request).Write,
+ readRes: ReadResponse,
+ }
+}
+
+// NewProxyClientConn works like NewClientConn but writes Requests
+// using Request's WriteProxy method.
+func NewProxyClientConn(c net.Conn, r *bufio.Reader) *ClientConn {
+ cc := NewClientConn(c, r)
+ cc.writeReq = (*Request).WriteProxy
+ return cc
}
// Close detaches the ClientConn and returns the underlying connection as well
@@ -221,11 +245,11 @@ func NewClientConn(c net.Conn, r *bufio.Reader) *ClientConn {
// logic. The user should not call Close while Read or Write is in progress.
func (cc *ClientConn) Close() (c net.Conn, r *bufio.Reader) {
cc.lk.Lock()
+ defer cc.lk.Unlock()
c = cc.c
r = cc.r
cc.c = nil
cc.r = nil
- cc.lk.Unlock()
return
}
@@ -261,20 +285,22 @@ func (cc *ClientConn) Write(req *Request) (err os.Error) {
defer cc.lk.Unlock()
return cc.we
}
- cc.lk.Unlock()
-
+ if cc.c == nil { // connection closed by user in the meantime
+ defer cc.lk.Unlock()
+ return os.EBADF
+ }
+ c := cc.c
if req.Close {
// We write the EOF to the write-side error, because there
// still might be some pipelined reads
- cc.lk.Lock()
cc.we = ErrPersistEOF
- cc.lk.Unlock()
}
+ cc.lk.Unlock()
- err = req.Write(cc.c)
+ err = cc.writeReq(req, c)
+ cc.lk.Lock()
+ defer cc.lk.Unlock()
if err != nil {
- cc.lk.Lock()
- defer cc.lk.Unlock()
cc.we = err
return err
}
@@ -316,15 +342,21 @@ func (cc *ClientConn) Read(req *Request) (resp *Response, err os.Error) {
defer cc.lk.Unlock()
return nil, cc.re
}
+ if cc.r == nil { // connection closed by user in the meantime
+ defer cc.lk.Unlock()
+ return nil, os.EBADF
+ }
+ r := cc.r
+ lastbody := cc.lastbody
+ cc.lastbody = nil
cc.lk.Unlock()
// Make sure body is fully consumed, even if user does not call body.Close
- if cc.lastbody != nil {
+ if lastbody != nil {
// body.Close is assumed to be idempotent and multiple calls to
// it should return the error that its first invokation
// returned.
- err = cc.lastbody.Close()
- cc.lastbody = nil
+ err = lastbody.Close()
if err != nil {
cc.lk.Lock()
defer cc.lk.Unlock()
@@ -333,24 +365,22 @@ func (cc *ClientConn) Read(req *Request) (resp *Response, err os.Error) {
}
}
- resp, err = ReadResponse(cc.r, req.Method)
+ resp, err = cc.readRes(r, req.Method)
+ cc.lk.Lock()
+ defer cc.lk.Unlock()
if err != nil {
- cc.lk.Lock()
- defer cc.lk.Unlock()
cc.re = err
- return
+ return resp, err
}
cc.lastbody = resp.Body
cc.nread++
if resp.Close {
- cc.lk.Lock()
- defer cc.lk.Unlock()
cc.re = ErrPersistEOF // don't send any more requests
return resp, cc.re
}
- return
+ return resp, err
}
// Do is convenience method that writes a request and reads a response.
diff --git a/libgo/go/http/pprof/pprof.go b/libgo/go/http/pprof/pprof.go
index f7db9aab93b..0bac26687d7 100644
--- a/libgo/go/http/pprof/pprof.go
+++ b/libgo/go/http/pprof/pprof.go
@@ -41,14 +41,14 @@ func init() {
// command line, with arguments separated by NUL bytes.
// The package initialization registers it as /debug/pprof/cmdline.
func Cmdline(w http.ResponseWriter, r *http.Request) {
- w.SetHeader("content-type", "text/plain; charset=utf-8")
+ w.Header().Set("content-type", "text/plain; charset=utf-8")
fmt.Fprintf(w, strings.Join(os.Args, "\x00"))
}
// Heap responds with the pprof-formatted heap profile.
// The package initialization registers it as /debug/pprof/heap.
func Heap(w http.ResponseWriter, r *http.Request) {
- w.SetHeader("content-type", "text/plain; charset=utf-8")
+ w.Header().Set("content-type", "text/plain; charset=utf-8")
pprof.WriteHeapProfile(w)
}
@@ -56,7 +56,7 @@ func Heap(w http.ResponseWriter, r *http.Request) {
// responding with a table mapping program counters to function names.
// The package initialization registers it as /debug/pprof/symbol.
func Symbol(w http.ResponseWriter, r *http.Request) {
- w.SetHeader("content-type", "text/plain; charset=utf-8")
+ w.Header().Set("content-type", "text/plain; charset=utf-8")
// We don't know how many symbols we have, but we
// do have symbol information. Pprof only cares whether
diff --git a/libgo/go/http/proxy_test.go b/libgo/go/http/proxy_test.go
index 0f2ca458fed..7050ef5ed06 100644
--- a/libgo/go/http/proxy_test.go
+++ b/libgo/go/http/proxy_test.go
@@ -12,31 +12,33 @@ import (
// TODO(mattn):
// test ProxyAuth
-var MatchNoProxyTests = []struct {
+var UseProxyTests = []struct {
host string
match bool
}{
- {"localhost", true}, // match completely
- {"barbaz.net", true}, // match as .barbaz.net
- {"foobar.com:443", true}, // have a port but match
- {"foofoobar.com", false}, // not match as a part of foobar.com
- {"baz.com", false}, // not match as a part of barbaz.com
- {"localhost.net", false}, // not match as suffix of address
- {"local.localhost", false}, // not match as prefix as address
- {"barbarbaz.net", false}, // not match because NO_PROXY have a '.'
- {"www.foobar.com", false}, // not match because NO_PROXY is not .foobar.com
+ {"localhost", false}, // match completely
+ {"barbaz.net", false}, // match as .barbaz.net
+ {"foobar.com:443", false}, // have a port but match
+ {"foofoobar.com", true}, // not match as a part of foobar.com
+ {"baz.com", true}, // not match as a part of barbaz.com
+ {"localhost.net", true}, // not match as suffix of address
+ {"local.localhost", true}, // not match as prefix as address
+ {"barbarbaz.net", true}, // not match because NO_PROXY have a '.'
+ {"www.foobar.com", true}, // not match because NO_PROXY is not .foobar.com
}
-func TestMatchNoProxy(t *testing.T) {
+func TestUseProxy(t *testing.T) {
oldenv := os.Getenv("NO_PROXY")
no_proxy := "foobar.com, .barbaz.net , localhost"
os.Setenv("NO_PROXY", no_proxy)
defer os.Setenv("NO_PROXY", oldenv)
- for _, test := range MatchNoProxyTests {
- if matchNoProxy(test.host) != test.match {
+ tr := &Transport{}
+
+ for _, test := range UseProxyTests {
+ if tr.useProxy(test.host) != test.match {
if test.match {
- t.Errorf("matchNoProxy(%v) = %v, want %v", test.host, !test.match, test.match)
+ t.Errorf("useProxy(%v) = %v, want %v", test.host, !test.match, test.match)
} else {
t.Errorf("not expected: '%s' shouldn't match as '%s'", test.host, no_proxy)
}
diff --git a/libgo/go/http/range_test.go b/libgo/go/http/range_test.go
new file mode 100644
index 00000000000..5274a81fa34
--- /dev/null
+++ b/libgo/go/http/range_test.go
@@ -0,0 +1,57 @@
+// 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 http
+
+import (
+ "testing"
+)
+
+var ParseRangeTests = []struct {
+ s string
+ length int64
+ r []httpRange
+}{
+ {"", 0, nil},
+ {"foo", 0, nil},
+ {"bytes=", 0, nil},
+ {"bytes=5-4", 10, nil},
+ {"bytes=0-2,5-4", 10, nil},
+ {"bytes=0-9", 10, []httpRange{{0, 10}}},
+ {"bytes=0-", 10, []httpRange{{0, 10}}},
+ {"bytes=5-", 10, []httpRange{{5, 5}}},
+ {"bytes=0-20", 10, []httpRange{{0, 10}}},
+ {"bytes=15-,0-5", 10, nil},
+ {"bytes=-5", 10, []httpRange{{5, 5}}},
+ {"bytes=-15", 10, []httpRange{{0, 10}}},
+ {"bytes=0-499", 10000, []httpRange{{0, 500}}},
+ {"bytes=500-999", 10000, []httpRange{{500, 500}}},
+ {"bytes=-500", 10000, []httpRange{{9500, 500}}},
+ {"bytes=9500-", 10000, []httpRange{{9500, 500}}},
+ {"bytes=0-0,-1", 10000, []httpRange{{0, 1}, {9999, 1}}},
+ {"bytes=500-600,601-999", 10000, []httpRange{{500, 101}, {601, 399}}},
+ {"bytes=500-700,601-999", 10000, []httpRange{{500, 201}, {601, 399}}},
+}
+
+func TestParseRange(t *testing.T) {
+ for _, test := range ParseRangeTests {
+ r := test.r
+ ranges, err := parseRange(test.s, test.length)
+ if err != nil && r != nil {
+ t.Errorf("parseRange(%q) returned error %q", test.s, err)
+ }
+ if len(ranges) != len(r) {
+ t.Errorf("len(parseRange(%q)) = %d, want %d", test.s, len(ranges), len(r))
+ continue
+ }
+ for i := range r {
+ if ranges[i].start != r[i].start {
+ t.Errorf("parseRange(%q)[%d].start = %d, want %d", test.s, i, ranges[i].start, r[i].start)
+ }
+ if ranges[i].length != r[i].length {
+ t.Errorf("parseRange(%q)[%d].length = %d, want %d", test.s, i, ranges[i].length, r[i].length)
+ }
+ }
+ }
+}
diff --git a/libgo/go/http/readrequest_test.go b/libgo/go/http/readrequest_test.go
index 6ee07bc9148..19e2ff77476 100644
--- a/libgo/go/http/readrequest_test.go
+++ b/libgo/go/http/readrequest_test.go
@@ -93,7 +93,7 @@ var reqTests = []reqTest{
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
- Header: map[string][]string{},
+ Header: Header{},
Close: false,
ContentLength: -1,
Host: "test",
diff --git a/libgo/go/http/request.go b/libgo/go/http/request.go
index a7dc328a007..d82894fab08 100644
--- a/libgo/go/http/request.go
+++ b/libgo/go/http/request.go
@@ -11,6 +11,7 @@ package http
import (
"bufio"
+ "crypto/tls"
"container/vector"
"fmt"
"io"
@@ -92,6 +93,9 @@ type Request struct {
// following a hyphen uppercase and the rest lowercase.
Header Header
+ // Cookie records the HTTP cookies sent with the request.
+ Cookie []*Cookie
+
// The message body.
Body io.ReadCloser
@@ -134,6 +138,22 @@ type Request struct {
// response has multiple trailer lines with the same key, they will be
// concatenated, delimited by commas.
Trailer Header
+
+ // RemoteAddr allows HTTP servers and other software to record
+ // the network address that sent the request, usually for
+ // logging. This field is not filled in by ReadRequest and
+ // has no defined format. The HTTP server in this package
+ // sets RemoteAddr to an "IP:port" address before invoking a
+ // handler.
+ RemoteAddr string
+
+ // TLS allows HTTP servers and other software to record
+ // information about the TLS connection on which the request
+ // was received. This field is not filled in by ReadRequest.
+ // The HTTP server in this package sets the field for
+ // TLS-enabled connections before invoking a handler;
+ // otherwise it leaves the field nil.
+ TLS *tls.ConnectionState
}
// ProtoAtLeast returns whether the HTTP protocol used
@@ -190,6 +210,8 @@ func (req *Request) Write(w io.Writer) os.Error {
// WriteProxy is like Write but writes the request in the form
// expected by an HTTP proxy. It includes the scheme and host
// name in the URI instead of using a separate Host: header line.
+// If req.RawURL is non-empty, WriteProxy uses it unchanged
+// instead of URL but still omits the Host: header.
func (req *Request) WriteProxy(w io.Writer) os.Error {
return req.write(w, true)
}
@@ -206,13 +228,12 @@ func (req *Request) write(w io.Writer, usingProxy bool) os.Error {
if req.URL.RawQuery != "" {
uri += "?" + req.URL.RawQuery
}
- }
-
- if usingProxy {
- if uri == "" || uri[0] != '/' {
- uri = "/" + uri
+ if usingProxy {
+ if uri == "" || uri[0] != '/' {
+ uri = "/" + uri
+ }
+ uri = req.URL.Scheme + "://" + host + uri
}
- uri = req.URL.Scheme + "://" + host + uri
}
fmt.Fprintf(w, "%s %s HTTP/1.1\r\n", valueOrDefault(req.Method, "GET"), uri)
@@ -243,11 +264,15 @@ func (req *Request) write(w io.Writer, usingProxy bool) os.Error {
// from Request, and introduce Request methods along the lines of
// Response.{GetHeader,AddHeader} and string constants for "Host",
// "User-Agent" and "Referer".
- err = writeSortedKeyValue(w, req.Header, reqExcludeHeader)
+ err = writeSortedHeader(w, req.Header, reqExcludeHeader)
if err != nil {
return err
}
+ if err = writeCookies(w, req.Cookie); err != nil {
+ return err
+ }
+
io.WriteString(w, "\r\n")
// Write body and trailer
@@ -484,6 +509,8 @@ func ReadRequest(b *bufio.Reader) (req *Request, err os.Error) {
return nil, err
}
+ req.Cookie = readCookies(req.Header)
+
return req, nil
}
diff --git a/libgo/go/http/request_test.go b/libgo/go/http/request_test.go
index ae1c4e98245..19083adf624 100644
--- a/libgo/go/http/request_test.go
+++ b/libgo/go/http/request_test.go
@@ -2,10 +2,15 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package http
+package http_test
import (
"bytes"
+ "fmt"
+ . "http"
+ "http/httptest"
+ "io"
+ "os"
"reflect"
"regexp"
"strings"
@@ -141,17 +146,33 @@ func TestMultipartReader(t *testing.T) {
}
func TestRedirect(t *testing.T) {
- const (
- start = "http://google.com/"
- endRe = "^http://www\\.google\\.[a-z.]+/$"
- )
- var end = regexp.MustCompile(endRe)
- r, url, err := Get(start)
+ ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
+ switch r.URL.Path {
+ case "/":
+ w.Header().Set("Location", "/foo/")
+ w.WriteHeader(StatusSeeOther)
+ case "/foo/":
+ fmt.Fprintf(w, "foo")
+ default:
+ w.WriteHeader(StatusBadRequest)
+ }
+ }))
+ defer ts.Close()
+
+ var end = regexp.MustCompile("/foo/$")
+ r, url, err := Get(ts.URL)
if err != nil {
t.Fatal(err)
}
r.Body.Close()
if r.StatusCode != 200 || !end.MatchString(url) {
- t.Fatalf("Get(%s) got status %d at %q, want 200 matching %q", start, r.StatusCode, url, endRe)
+ t.Fatalf("Get got status %d at %q, want 200 matching /foo/$", r.StatusCode, url)
}
}
+
+// TODO: stop copy/pasting this around. move to io/ioutil?
+type nopCloser struct {
+ io.Reader
+}
+
+func (nopCloser) Close() os.Error { return nil }
diff --git a/libgo/go/http/requestwrite_test.go b/libgo/go/http/requestwrite_test.go
index 55ca745d58c..726baa26686 100644
--- a/libgo/go/http/requestwrite_test.go
+++ b/libgo/go/http/requestwrite_test.go
@@ -6,12 +6,15 @@ package http
import (
"bytes"
+ "io/ioutil"
"testing"
)
type reqWriteTest struct {
- Req Request
- Raw string
+ Req Request
+ Body []byte
+ Raw string
+ RawProxy string
}
var reqWriteTests = []reqWriteTest{
@@ -50,6 +53,8 @@ var reqWriteTests = []reqWriteTest{
Form: map[string][]string{},
},
+ nil,
+
"GET http://www.techcrunch.com/ HTTP/1.1\r\n" +
"Host: www.techcrunch.com\r\n" +
"User-Agent: Fake\r\n" +
@@ -59,6 +64,15 @@ var reqWriteTests = []reqWriteTest{
"Accept-Language: en-us,en;q=0.5\r\n" +
"Keep-Alive: 300\r\n" +
"Proxy-Connection: keep-alive\r\n\r\n",
+
+ "GET http://www.techcrunch.com/ HTTP/1.1\r\n" +
+ "User-Agent: Fake\r\n" +
+ "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" +
+ "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" +
+ "Accept-Encoding: gzip,deflate\r\n" +
+ "Accept-Language: en-us,en;q=0.5\r\n" +
+ "Keep-Alive: 300\r\n" +
+ "Proxy-Connection: keep-alive\r\n\r\n",
},
// HTTP/1.1 => chunked coding; body; empty trailer
{
@@ -71,16 +85,22 @@ var reqWriteTests = []reqWriteTest{
},
ProtoMajor: 1,
ProtoMinor: 1,
- Header: map[string][]string{},
- Body: nopCloser{bytes.NewBufferString("abcdef")},
+ Header: Header{},
TransferEncoding: []string{"chunked"},
},
+ []byte("abcdef"),
+
"GET /search HTTP/1.1\r\n" +
"Host: www.google.com\r\n" +
"User-Agent: Go http package\r\n" +
"Transfer-Encoding: chunked\r\n\r\n" +
"6\r\nabcdef\r\n0\r\n\r\n",
+
+ "GET http://www.google.com/search HTTP/1.1\r\n" +
+ "User-Agent: Go http package\r\n" +
+ "Transfer-Encoding: chunked\r\n\r\n" +
+ "6\r\nabcdef\r\n0\r\n\r\n",
},
// HTTP/1.1 POST => chunked coding; body; empty trailer
{
@@ -93,18 +113,25 @@ var reqWriteTests = []reqWriteTest{
},
ProtoMajor: 1,
ProtoMinor: 1,
- Header: map[string][]string{},
+ Header: Header{},
Close: true,
- Body: nopCloser{bytes.NewBufferString("abcdef")},
TransferEncoding: []string{"chunked"},
},
+ []byte("abcdef"),
+
"POST /search HTTP/1.1\r\n" +
"Host: www.google.com\r\n" +
"User-Agent: Go http package\r\n" +
"Connection: close\r\n" +
"Transfer-Encoding: chunked\r\n\r\n" +
"6\r\nabcdef\r\n0\r\n\r\n",
+
+ "POST http://www.google.com/search HTTP/1.1\r\n" +
+ "User-Agent: Go http package\r\n" +
+ "Connection: close\r\n" +
+ "Transfer-Encoding: chunked\r\n\r\n" +
+ "6\r\nabcdef\r\n0\r\n\r\n",
},
// default to HTTP/1.1
{
@@ -114,16 +141,26 @@ var reqWriteTests = []reqWriteTest{
Host: "www.google.com",
},
+ nil,
+
"GET /search HTTP/1.1\r\n" +
"Host: www.google.com\r\n" +
"User-Agent: Go http package\r\n" +
"\r\n",
+
+ // Looks weird but RawURL overrides what WriteProxy would choose.
+ "GET /search HTTP/1.1\r\n" +
+ "User-Agent: Go http package\r\n" +
+ "\r\n",
},
}
func TestRequestWrite(t *testing.T) {
for i := range reqWriteTests {
tt := &reqWriteTests[i]
+ if tt.Body != nil {
+ tt.Req.Body = ioutil.NopCloser(bytes.NewBuffer(tt.Body))
+ }
var braw bytes.Buffer
err := tt.Req.Write(&braw)
if err != nil {
@@ -135,5 +172,20 @@ func TestRequestWrite(t *testing.T) {
t.Errorf("Test %d, expecting:\n%s\nGot:\n%s\n", i, tt.Raw, sraw)
continue
}
+
+ if tt.Body != nil {
+ tt.Req.Body = ioutil.NopCloser(bytes.NewBuffer(tt.Body))
+ }
+ var praw bytes.Buffer
+ err = tt.Req.WriteProxy(&praw)
+ if err != nil {
+ t.Errorf("error writing #%d: %s", i, err)
+ continue
+ }
+ sraw = praw.String()
+ if sraw != tt.RawProxy {
+ t.Errorf("Test Proxy %d, expecting:\n%s\nGot:\n%s\n", i, tt.RawProxy, sraw)
+ continue
+ }
}
}
diff --git a/libgo/go/http/response.go b/libgo/go/http/response.go
index 3f919c86a3c..1f725ecdddd 100644
--- a/libgo/go/http/response.go
+++ b/libgo/go/http/response.go
@@ -46,6 +46,9 @@ type Response struct {
// Keys in the map are canonicalized (see CanonicalHeaderKey).
Header Header
+ // SetCookie records the Set-Cookie requests sent with the response.
+ SetCookie []*Cookie
+
// Body represents the response body.
Body io.ReadCloser
@@ -64,10 +67,9 @@ type Response struct {
// ReadResponse nor Response.Write ever closes a connection.
Close bool
- // Trailer maps trailer keys to values. Like for Header, if the
- // response has multiple trailer lines with the same key, they will be
- // concatenated, delimited by commas.
- Trailer map[string][]string
+ // Trailer maps trailer keys to values, in the same
+ // format as the header.
+ Trailer Header
}
// ReadResponse reads and returns an HTTP response from r. The RequestMethod
@@ -124,6 +126,8 @@ func ReadResponse(r *bufio.Reader, requestMethod string) (resp *Response, err os
return nil, err
}
+ resp.SetCookie = readSetCookies(resp.Header)
+
return resp, nil
}
@@ -188,11 +192,15 @@ func (resp *Response) Write(w io.Writer) os.Error {
}
// Rest of header
- err = writeSortedKeyValue(w, resp.Header, respExcludeHeader)
+ err = writeSortedHeader(w, resp.Header, respExcludeHeader)
if err != nil {
return err
}
+ if err = writeSetCookies(w, resp.SetCookie); err != nil {
+ return err
+ }
+
// End-of-header
io.WriteString(w, "\r\n")
@@ -206,16 +214,22 @@ func (resp *Response) Write(w io.Writer) os.Error {
return nil
}
-func writeSortedKeyValue(w io.Writer, kvm map[string][]string, exclude map[string]bool) os.Error {
- keys := make([]string, 0, len(kvm))
- for k := range kvm {
- if !exclude[k] {
+func writeSortedHeader(w io.Writer, h Header, exclude map[string]bool) os.Error {
+ keys := make([]string, 0, len(h))
+ for k := range h {
+ if exclude == nil || !exclude[k] {
keys = append(keys, k)
}
}
sort.SortStrings(keys)
for _, k := range keys {
- for _, v := range kvm[k] {
+ for _, v := range h[k] {
+ v = strings.Replace(v, "\n", " ", -1)
+ v = strings.Replace(v, "\r", " ", -1)
+ v = strings.TrimSpace(v)
+ if v == "" {
+ continue
+ }
if _, err := fmt.Fprintf(w, "%s: %s\r\n", k, v); err != nil {
return err
}
diff --git a/libgo/go/http/responsewrite_test.go b/libgo/go/http/responsewrite_test.go
index aabb833f9c8..de0635da516 100644
--- a/libgo/go/http/responsewrite_test.go
+++ b/libgo/go/http/responsewrite_test.go
@@ -6,6 +6,7 @@ package http
import (
"bytes"
+ "io/ioutil"
"testing"
)
@@ -22,8 +23,8 @@ var respWriteTests = []respWriteTest{
ProtoMajor: 1,
ProtoMinor: 0,
RequestMethod: "GET",
- Header: map[string][]string{},
- Body: nopCloser{bytes.NewBufferString("abcdef")},
+ Header: Header{},
+ Body: ioutil.NopCloser(bytes.NewBufferString("abcdef")),
ContentLength: 6,
},
@@ -38,8 +39,8 @@ var respWriteTests = []respWriteTest{
ProtoMajor: 1,
ProtoMinor: 0,
RequestMethod: "GET",
- Header: map[string][]string{},
- Body: nopCloser{bytes.NewBufferString("abcdef")},
+ Header: Header{},
+ Body: ioutil.NopCloser(bytes.NewBufferString("abcdef")),
ContentLength: -1,
},
"HTTP/1.0 200 OK\r\n" +
@@ -53,8 +54,8 @@ var respWriteTests = []respWriteTest{
ProtoMajor: 1,
ProtoMinor: 1,
RequestMethod: "GET",
- Header: map[string][]string{},
- Body: nopCloser{bytes.NewBufferString("abcdef")},
+ Header: Header{},
+ Body: ioutil.NopCloser(bytes.NewBufferString("abcdef")),
ContentLength: 6,
TransferEncoding: []string{"chunked"},
Close: true,
@@ -65,6 +66,29 @@ var respWriteTests = []respWriteTest{
"Transfer-Encoding: chunked\r\n\r\n" +
"6\r\nabcdef\r\n0\r\n\r\n",
},
+
+ // Header value with a newline character (Issue 914).
+ // Also tests removal of leading and trailing whitespace.
+ {
+ Response{
+ StatusCode: 204,
+ ProtoMajor: 1,
+ ProtoMinor: 1,
+ RequestMethod: "GET",
+ Header: Header{
+ "Foo": []string{" Bar\nBaz "},
+ },
+ Body: nil,
+ ContentLength: 0,
+ TransferEncoding: []string{"chunked"},
+ Close: true,
+ },
+
+ "HTTP/1.1 204 No Content\r\n" +
+ "Connection: close\r\n" +
+ "Foo: Bar Baz\r\n" +
+ "\r\n",
+ },
}
func TestResponseWrite(t *testing.T) {
@@ -78,7 +102,7 @@ func TestResponseWrite(t *testing.T) {
}
sraw := braw.String()
if sraw != tt.Raw {
- t.Errorf("Test %d, expecting:\n%s\nGot:\n%s\n", i, tt.Raw, sraw)
+ t.Errorf("Test %d, expecting:\n%q\nGot:\n%q\n", i, tt.Raw, sraw)
continue
}
}
diff --git a/libgo/go/http/serve_test.go b/libgo/go/http/serve_test.go
index 42fe3e5e4d2..683de85b867 100644
--- a/libgo/go/http/serve_test.go
+++ b/libgo/go/http/serve_test.go
@@ -4,16 +4,18 @@
// End-to-end serving tests
-package http
+package http_test
import (
"bufio"
"bytes"
"fmt"
- "io"
+ . "http"
+ "http/httptest"
"io/ioutil"
"os"
"net"
+ "reflect"
"strings"
"testing"
"time"
@@ -143,7 +145,7 @@ func TestConsumingBodyOnNextConn(t *testing.T) {
type stringHandler string
func (s stringHandler) ServeHTTP(w ResponseWriter, r *Request) {
- w.SetHeader("Result", string(s))
+ w.Header().Set("Result", string(s))
}
var handlers = []struct {
@@ -170,13 +172,10 @@ func TestHostHandlers(t *testing.T) {
for _, h := range handlers {
Handle(h.pattern, stringHandler(h.msg))
}
- l, err := net.Listen("tcp", "127.0.0.1:0") // any port
- if err != nil {
- t.Fatal(err)
- }
- defer l.Close()
- go Serve(l, nil)
- conn, err := net.Dial("tcp", "", l.Addr().String())
+ ts := httptest.NewServer(nil)
+ defer ts.Close()
+
+ conn, err := net.Dial("tcp", "", ts.Listener.Addr().String())
if err != nil {
t.Fatal(err)
}
@@ -205,46 +204,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
func TestMuxRedirectLeadingSlashes(t *testing.T) {
paths := []string{"//foo.txt", "///foo.txt", "/../../foo.txt"}
@@ -254,41 +213,24 @@ func TestMuxRedirectLeadingSlashes(t *testing.T) {
t.Errorf("%s", err)
}
mux := NewServeMux()
- resp := new(recordingResponseWriter)
- resp.log = make([]*responseWriterMethodCall, 0)
+ resp := httptest.NewRecorder()
mux.ServeHTTP(resp, req)
- dumpLog := func() {
- t.Logf("For path %q:", path)
- 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
- }
-
- if resp.log[0].method != "SetHeader" ||
- resp.log[0].headerKey != "Location" || resp.log[0].headerValue != "/foo.txt" {
- dumpLog()
- t.Errorf("Expected SetHeader of Location to /foo.txt")
+ if loc, expected := resp.Header().Get("Location"), "/foo.txt"; loc != expected {
+ t.Errorf("Expected Location header set to %q; got %q", expected, loc)
return
}
- if resp.log[1].method != "WriteHeader" || resp.log[1].responseCode != StatusMovedPermanently {
- dumpLog()
- t.Errorf("Expected WriteHeader of StatusMovedPermanently")
+ if code, expected := resp.Code, StatusMovedPermanently; code != expected {
+ t.Errorf("Expected response code of StatusMovedPermanently; got %d", code)
return
}
}
}
func TestServerTimeouts(t *testing.T) {
+ // TODO(bradfitz): convert this to use httptest.Server
l, err := net.ListenTCP("tcp", &net.TCPAddr{Port: 0})
if err != nil {
t.Fatalf("listen error: %v", err)
@@ -308,7 +250,9 @@ func TestServerTimeouts(t *testing.T) {
url := fmt.Sprintf("http://localhost:%d/", addr.Port)
// Hit the HTTP server successfully.
- r, _, err := Get(url)
+ tr := &Transport{DisableKeepAlives: true} // they interfere with this test
+ c := &Client{Transport: tr}
+ r, _, err := c.Get(url)
if err != nil {
t.Fatalf("http Get #1: %v", err)
}
@@ -353,16 +297,9 @@ func TestServerTimeouts(t *testing.T) {
// TestIdentityResponse verifies that a handler can unset
func TestIdentityResponse(t *testing.T) {
- l, err := net.Listen("tcp", "127.0.0.1:0")
- if err != nil {
- t.Fatalf("failed to listen on a port: %v", err)
- }
- defer l.Close()
- urlBase := "http://" + l.Addr().String() + "/"
-
handler := HandlerFunc(func(rw ResponseWriter, req *Request) {
- rw.SetHeader("Content-Length", "3")
- rw.SetHeader("Transfer-Encoding", req.FormValue("te"))
+ rw.Header().Set("Content-Length", "3")
+ rw.Header().Set("Transfer-Encoding", req.FormValue("te"))
switch {
case req.FormValue("overwrite") == "1":
_, err := rw.Write([]byte("foo TOO LONG"))
@@ -370,22 +307,22 @@ func TestIdentityResponse(t *testing.T) {
t.Errorf("expected ErrContentLength; got %v", err)
}
case req.FormValue("underwrite") == "1":
- rw.SetHeader("Content-Length", "500")
+ rw.Header().Set("Content-Length", "500")
rw.Write([]byte("too short"))
default:
rw.Write([]byte("foo"))
}
})
- server := &Server{Handler: handler}
- go server.Serve(l)
+ ts := httptest.NewServer(handler)
+ defer ts.Close()
// Note: this relies on the assumption (which is true) that
// Get sends HTTP/1.1 or greater requests. Otherwise the
// server wouldn't have the choice to send back chunked
// responses.
for _, te := range []string{"", "identity"} {
- url := urlBase + "?te=" + te
+ url := ts.URL + "/?te=" + te
res, _, err := Get(url)
if err != nil {
t.Fatalf("error with Get of %s: %v", url, err)
@@ -400,18 +337,18 @@ func TestIdentityResponse(t *testing.T) {
t.Errorf("for %s expected len(res.TransferEncoding) of %d; got %d (%v)",
url, expected, tl, res.TransferEncoding)
}
+ res.Body.Close()
}
// Verify that ErrContentLength is returned
- url := urlBase + "?overwrite=1"
- _, _, err = Get(url)
+ url := ts.URL + "/?overwrite=1"
+ _, _, err := Get(url)
if err != nil {
t.Fatalf("error with Get of %s: %v", url, err)
}
-
// Verify that the connection is closed when the declared Content-Length
// is larger than what the handler wrote.
- conn, err := net.Dial("tcp", "", l.Addr().String())
+ conn, err := net.Dial("tcp", "", ts.Listener.Addr().String())
if err != nil {
t.Fatalf("error dialing: %v", err)
}
@@ -432,3 +369,141 @@ func TestIdentityResponse(t *testing.T) {
expectedSuffix, string(got))
}
}
+
+// TestServeHTTP10Close verifies that HTTP/1.0 requests won't be kept alive.
+func TestServeHTTP10Close(t *testing.T) {
+ s := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
+ ServeFile(w, r, "testdata/file")
+ }))
+ defer s.Close()
+
+ conn, err := net.Dial("tcp", "", s.Listener.Addr().String())
+ if err != nil {
+ t.Fatal("dial error:", err)
+ }
+ defer conn.Close()
+
+ _, err = fmt.Fprint(conn, "GET / HTTP/1.0\r\n\r\n")
+ if err != nil {
+ t.Fatal("print error:", err)
+ }
+
+ r := bufio.NewReader(conn)
+ _, err = ReadResponse(r, "GET")
+ if err != nil {
+ t.Fatal("ReadResponse error:", err)
+ }
+
+ success := make(chan bool)
+ go func() {
+ select {
+ case <-time.After(5e9):
+ t.Fatal("body not closed after 5s")
+ case <-success:
+ }
+ }()
+
+ _, err = ioutil.ReadAll(r)
+ if err != nil {
+ t.Fatal("read error:", err)
+ }
+
+ success <- true
+}
+
+func TestSetsRemoteAddr(t *testing.T) {
+ ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
+ fmt.Fprintf(w, "%s", r.RemoteAddr)
+ }))
+ defer ts.Close()
+
+ res, _, err := Get(ts.URL)
+ if err != nil {
+ t.Fatalf("Get error: %v", err)
+ }
+ body, err := ioutil.ReadAll(res.Body)
+ if err != nil {
+ t.Fatalf("ReadAll error: %v", err)
+ }
+ ip := string(body)
+ if !strings.HasPrefix(ip, "127.0.0.1:") && !strings.HasPrefix(ip, "[::1]:") {
+ t.Fatalf("Expected local addr; got %q", ip)
+ }
+}
+
+func TestChunkedResponseHeaders(t *testing.T) {
+ ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
+ w.Header().Set("Content-Length", "intentional gibberish") // we check that this is deleted
+ fmt.Fprintf(w, "I am a chunked response.")
+ }))
+ defer ts.Close()
+
+ res, _, err := Get(ts.URL)
+ if err != nil {
+ t.Fatalf("Get error: %v", err)
+ }
+ if g, e := res.ContentLength, int64(-1); g != e {
+ t.Errorf("expected ContentLength of %d; got %d", e, g)
+ }
+ if g, e := res.TransferEncoding, []string{"chunked"}; !reflect.DeepEqual(g, e) {
+ t.Errorf("expected TransferEncoding of %v; got %v", e, g)
+ }
+ if _, haveCL := res.Header["Content-Length"]; haveCL {
+ t.Errorf("Unexpected Content-Length")
+ }
+}
+
+// Test304Responses verifies that 304s don't declare that they're
+// chunking in their response headers and aren't allowed to produce
+// output.
+func Test304Responses(t *testing.T) {
+ ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
+ w.WriteHeader(StatusNotModified)
+ _, err := w.Write([]byte("illegal body"))
+ if err != ErrBodyNotAllowed {
+ t.Errorf("on Write, expected ErrBodyNotAllowed, got %v", err)
+ }
+ }))
+ defer ts.Close()
+ res, _, err := Get(ts.URL)
+ if err != nil {
+ t.Error(err)
+ }
+ if len(res.TransferEncoding) > 0 {
+ t.Errorf("expected no TransferEncoding; got %v", res.TransferEncoding)
+ }
+ body, err := ioutil.ReadAll(res.Body)
+ if err != nil {
+ t.Error(err)
+ }
+ if len(body) > 0 {
+ t.Errorf("got unexpected body %q", string(body))
+ }
+}
+
+// TestHeadResponses verifies that responses to HEAD requests don't
+// declare that they're chunking in their response headers and aren't
+// allowed to produce output.
+func TestHeadResponses(t *testing.T) {
+ ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
+ _, err := w.Write([]byte("Ignored body"))
+ if err != ErrBodyNotAllowed {
+ t.Errorf("on Write, expected ErrBodyNotAllowed, got %v", err)
+ }
+ }))
+ defer ts.Close()
+ res, err := Head(ts.URL)
+ if err != nil {
+ t.Error(err)
+ }
+ if len(res.TransferEncoding) > 0 {
+ t.Errorf("expected no TransferEncoding; got %v", res.TransferEncoding)
+ }
+ body, err := ioutil.ReadAll(res.Body)
+ if err != nil {
+ t.Error(err)
+ }
+ if len(body) > 0 {
+ t.Errorf("got unexpected body %q", string(body))
+ }
+}
diff --git a/libgo/go/http/server.go b/libgo/go/http/server.go
index 977c8c2297a..8e7039371ae 100644
--- a/libgo/go/http/server.go
+++ b/libgo/go/http/server.go
@@ -6,7 +6,6 @@
// TODO(rsc):
// logging
-// cgi support
// post support
package http
@@ -49,23 +48,10 @@ type Handler interface {
// A ResponseWriter interface is used by an HTTP handler to
// construct an HTTP response.
type ResponseWriter interface {
- // RemoteAddr returns the address of the client that sent the current request
- RemoteAddr() string
-
- // UsingTLS returns true if the client is connected using TLS
- UsingTLS() bool
-
- // SetHeader sets a header line in the eventual response.
- // For example, SetHeader("Content-Type", "text/html; charset=utf-8")
- // will result in the header line
- //
- // Content-Type: text/html; charset=utf-8
- //
- // being sent. UTF-8 encoded HTML is the default setting for
- // Content-Type in this library, so users need not make that
- // particular call. Calls to SetHeader after WriteHeader (or Write)
- // are ignored. An empty value removes the header if previously set.
- SetHeader(string, string)
+ // Header returns the header map that will be sent by WriteHeader.
+ // Changing the header after a call to WriteHeader (or Write) has
+ // no effect.
+ Header() Header
// Write writes the data to the connection as part of an HTTP reply.
// If WriteHeader has not yet been called, Write calls WriteHeader(http.StatusOK)
@@ -78,39 +64,52 @@ type ResponseWriter interface {
// Thus explicit calls to WriteHeader are mainly used to
// send error codes.
WriteHeader(int)
+}
+// The Flusher interface is implemented by ResponseWriters that allow
+// an HTTP handler to flush buffered data to the client.
+//
+// Note that even for ResponseWriters that support Flush,
+// if the client is connected through an HTTP proxy,
+// the buffered data may not reach the client until the response
+// completes.
+type Flusher interface {
// Flush sends any buffered data to the client.
Flush()
+}
+// The Hijacker interface is implemented by ResponseWriters that allow
+// an HTTP handler to take over the connection.
+type Hijacker interface {
// Hijack lets the caller take over the connection.
// After a call to Hijack(), the HTTP server library
// will not do anything else with the connection.
// It becomes the caller's responsibility to manage
// and close the connection.
- Hijack() (io.ReadWriteCloser, *bufio.ReadWriter, os.Error)
+ Hijack() (net.Conn, *bufio.ReadWriter, os.Error)
}
// A conn represents the server side of an HTTP connection.
type conn struct {
- remoteAddr string // network address of remote side
- handler Handler // request handler
- rwc io.ReadWriteCloser // i/o connection
- buf *bufio.ReadWriter // buffered rwc
- hijacked bool // connection has been hijacked by handler
- usingTLS bool // a flag indicating connection over TLS
+ remoteAddr string // network address of remote side
+ handler Handler // request handler
+ rwc net.Conn // i/o connection
+ buf *bufio.ReadWriter // buffered rwc
+ hijacked bool // connection has been hijacked by handler
+ tlsState *tls.ConnectionState // or nil when not using TLS
}
// A response represents the server side of an HTTP response.
type response struct {
conn *conn
- req *Request // request for this response
- chunking bool // using chunked transfer encoding for reply body
- wroteHeader bool // reply header has been written
- wroteContinue bool // 100 Continue response was written
- header map[string]string // reply header parameters
- written int64 // number of bytes written in body
- contentLength int64 // explicitly-declared Content-Length; or -1
- status int // status code passed to WriteHeader
+ req *Request // request for this response
+ chunking bool // using chunked transfer encoding for reply body
+ wroteHeader bool // reply header has been written
+ wroteContinue bool // 100 Continue response was written
+ header Header // reply header parameters
+ written int64 // number of bytes written in body
+ contentLength int64 // explicitly-declared Content-Length; or -1
+ status int // status code passed to WriteHeader
// close connection after this reply. set on request and
// updated after response from handler if there's a
@@ -125,10 +124,15 @@ func newConn(rwc net.Conn, handler Handler) (c *conn, err os.Error) {
c.remoteAddr = rwc.RemoteAddr().String()
c.handler = handler
c.rwc = rwc
- _, c.usingTLS = rwc.(*tls.Conn)
br := bufio.NewReader(rwc)
bw := bufio.NewWriter(rwc)
c.buf = bufio.NewReadWriter(br, bw)
+
+ if tlsConn, ok := rwc.(*tls.Conn); ok {
+ c.tlsState = new(tls.ConnectionState)
+ *c.tlsState = tlsConn.ConnectionState()
+ }
+
return c, nil
}
@@ -168,10 +172,13 @@ func (c *conn) readRequest() (w *response, err os.Error) {
return nil, err
}
+ req.RemoteAddr = c.remoteAddr
+ req.TLS = c.tlsState
+
w = new(response)
w.conn = c
w.req = req
- w.header = make(map[string]string)
+ w.header = make(Header)
w.contentLength = -1
// Expect 100 Continue support
@@ -182,21 +189,10 @@ func (c *conn) readRequest() (w *response, err os.Error) {
return w, nil
}
-// UsingTLS implements the ResponseWriter.UsingTLS
-func (w *response) UsingTLS() bool {
- return w.conn.usingTLS
-}
-
-// RemoteAddr implements the ResponseWriter.RemoteAddr method
-func (w *response) RemoteAddr() string { return w.conn.remoteAddr }
-
-// SetHeader implements the ResponseWriter.SetHeader method
-// An empty value removes the header from the map.
-func (w *response) SetHeader(hdr, val string) {
- w.header[CanonicalHeaderKey(hdr)] = val, val != ""
+func (w *response) Header() Header {
+ return w.header
}
-// WriteHeader implements the ResponseWriter.WriteHeader method
func (w *response) WriteHeader(code int) {
if w.conn.hijacked {
log.Print("http: response.WriteHeader on hijacked connection")
@@ -211,55 +207,55 @@ func (w *response) WriteHeader(code int) {
if code == StatusNotModified {
// Must not have body.
for _, header := range []string{"Content-Type", "Content-Length", "Transfer-Encoding"} {
- if w.header[header] != "" {
+ if w.header.Get(header) != "" {
// TODO: return an error if WriteHeader gets a return parameter
// or set a flag on w to make future Writes() write an error page?
// for now just log and drop the header.
log.Printf("http: StatusNotModified response with header %q defined", header)
- w.header[header] = "", false
+ w.header.Del(header)
}
}
} else {
// Default output is HTML encoded in UTF-8.
- if w.header["Content-Type"] == "" {
- w.SetHeader("Content-Type", "text/html; charset=utf-8")
+ if w.header.Get("Content-Type") == "" {
+ w.header.Set("Content-Type", "text/html; charset=utf-8")
}
}
- if w.header["Date"] == "" {
- w.SetHeader("Date", time.UTC().Format(TimeFormat))
+ if w.header.Get("Date") == "" {
+ w.Header().Set("Date", time.UTC().Format(TimeFormat))
}
// Check for a explicit (and valid) Content-Length header.
var hasCL bool
var contentLength int64
- if clenStr, ok := w.header["Content-Length"]; ok {
+ if clenStr := w.header.Get("Content-Length"); clenStr != "" {
var err os.Error
contentLength, err = strconv.Atoi64(clenStr)
if err == nil {
hasCL = true
} else {
log.Printf("http: invalid Content-Length of %q sent", clenStr)
- w.SetHeader("Content-Length", "")
+ w.header.Del("Content-Length")
}
}
- te, hasTE := w.header["Transfer-Encoding"]
+ te := w.header.Get("Transfer-Encoding")
+ hasTE := te != ""
if hasCL && hasTE && te != "identity" {
// TODO: return an error if WriteHeader gets a return parameter
// For now just ignore the Content-Length.
log.Printf("http: WriteHeader called with both Transfer-Encoding of %q and a Content-Length of %d",
te, contentLength)
- w.SetHeader("Content-Length", "")
+ w.header.Del("Content-Length")
hasCL = false
}
- if w.req.Method == "HEAD" {
+ if w.req.Method == "HEAD" || code == StatusNotModified {
// do nothing
} else if hasCL {
- w.chunking = false
w.contentLength = contentLength
- w.SetHeader("Transfer-Encoding", "")
+ w.header.Del("Transfer-Encoding")
} else if w.req.ProtoAtLeast(1, 1) {
// HTTP/1.1 or greater: use chunked transfer encoding
// to avoid closing the connection at EOF.
@@ -267,26 +263,28 @@ func (w *response) WriteHeader(code int) {
// might have set. Deal with that as need arises once we have a valid
// use case.
w.chunking = true
- w.SetHeader("Transfer-Encoding", "chunked")
+ w.header.Set("Transfer-Encoding", "chunked")
} else {
// HTTP version < 1.1: cannot do chunked transfer
// encoding and we don't know the Content-Length so
// signal EOF by closing connection.
w.closeAfterReply = true
- w.chunking = false // redundant
- w.SetHeader("Transfer-Encoding", "") // in case already set
+ w.header.Del("Transfer-Encoding") // in case already set
}
if w.req.wantsHttp10KeepAlive() && (w.req.Method == "HEAD" || hasCL) {
_, connectionHeaderSet := w.header["Connection"]
if !connectionHeaderSet {
- w.SetHeader("Connection", "keep-alive")
+ w.header.Set("Connection", "keep-alive")
}
+ } else if !w.req.ProtoAtLeast(1, 1) {
+ // Client did not ask to keep connection alive.
+ w.closeAfterReply = true
}
// Cannot use Content-Length with non-identity Transfer-Encoding.
if w.chunking {
- w.SetHeader("Content-Length", "")
+ w.header.Del("Content-Length")
}
if !w.req.ProtoAtLeast(1, 0) {
return
@@ -301,13 +299,10 @@ func (w *response) WriteHeader(code int) {
text = "status code " + codestring
}
io.WriteString(w.conn.buf, proto+" "+codestring+" "+text+"\r\n")
- for k, v := range w.header {
- io.WriteString(w.conn.buf, k+": "+v+"\r\n")
- }
+ writeSortedHeader(w.conn.buf, w.header, nil)
io.WriteString(w.conn.buf, "\r\n")
}
-// Write implements the ResponseWriter.Write method
func (w *response) Write(data []byte) (n int, err os.Error) {
if w.conn.hijacked {
log.Print("http: response.Write on hijacked connection")
@@ -382,7 +377,7 @@ func errorKludge(w *response) {
msg += " would ignore this error page if this text weren't here.\n"
// Is it text? ("Content-Type" is always in the map)
- baseType := strings.Split(w.header["Content-Type"], ";", 2)[0]
+ baseType := strings.Split(w.header.Get("Content-Type"), ";", 2)[0]
switch baseType {
case "text/html":
io.WriteString(w, "<!-- ")
@@ -402,8 +397,8 @@ func (w *response) finishRequest() {
// If this was an HTTP/1.0 request with keep-alive and we sent a Content-Length
// back, we can make this a keep-alive response ...
if w.req.wantsHttp10KeepAlive() {
- _, sentLength := w.header["Content-Length"]
- if sentLength && w.header["Connection"] == "keep-alive" {
+ sentLength := w.header.Get("Content-Length") != ""
+ if sentLength && w.header.Get("Connection") == "keep-alive" {
w.closeAfterReply = false
}
}
@@ -425,7 +420,6 @@ func (w *response) finishRequest() {
}
}
-// Flush implements the ResponseWriter.Flush method.
func (w *response) Flush() {
if !w.wroteHeader {
w.WriteHeader(StatusOK)
@@ -469,8 +463,9 @@ func (c *conn) serve() {
c.close()
}
-// Hijack impements the ResponseWriter.Hijack method.
-func (w *response) Hijack() (rwc io.ReadWriteCloser, buf *bufio.ReadWriter, err os.Error) {
+// Hijack implements the Hijacker.Hijack method. Our response is both a ResponseWriter
+// and a Hijacker.
+func (w *response) Hijack() (rwc net.Conn, buf *bufio.ReadWriter, err os.Error) {
if w.conn.hijacked {
return nil, nil, ErrHijacked
}
@@ -497,7 +492,7 @@ func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
// Error replies to the request with the specified error message and HTTP code.
func Error(w ResponseWriter, error string, code int) {
- w.SetHeader("Content-Type", "text/plain; charset=utf-8")
+ w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(code)
fmt.Fprintln(w, error)
}
@@ -550,7 +545,7 @@ func Redirect(w ResponseWriter, r *Request, url string, code int) {
}
}
- w.SetHeader("Location", url)
+ w.Header().Set("Location", url)
w.WriteHeader(code)
// RFC2616 recommends that a short note "SHOULD" be included in the
@@ -673,7 +668,7 @@ func (mux *ServeMux) match(path string) Handler {
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
// Clean path to canonical form and redirect.
if p := cleanPath(r.URL.Path); p != r.URL.Path {
- w.SetHeader("Location", p)
+ w.Header().Set("Location", p)
w.WriteHeader(StatusMovedPermanently)
return
}
@@ -826,7 +821,7 @@ func ListenAndServe(addr string, handler Handler) os.Error {
// )
//
// func handler(w http.ResponseWriter, req *http.Request) {
-// w.SetHeader("Content-Type", "text/plain")
+// w.Header().Set("Content-Type", "text/plain")
// w.Write([]byte("This is an example server.\n"))
// }
//
diff --git a/libgo/go/http/transport.go b/libgo/go/http/transport.go
index 41d639c7e2f..8a73ead31f9 100644
--- a/libgo/go/http/transport.go
+++ b/libgo/go/http/transport.go
@@ -9,53 +9,111 @@ import (
"crypto/tls"
"encoding/base64"
"fmt"
+ "io"
+ "log"
"net"
"os"
"strings"
"sync"
)
-// DefaultTransport is the default implementation of ClientTransport
-// and is used by DefaultClient. It establishes a new network connection for
-// each call to Do and uses HTTP proxies as directed by the $HTTP_PROXY and
-// $NO_PROXY (or $http_proxy and $no_proxy) environment variables.
-var DefaultTransport ClientTransport = &transport{}
-
-// transport implements http.ClientTranport for the default case,
-// using TCP connections to either the host or a proxy, serving
-// http or https schemes. In the future this may become public
-// and support options on keep-alive connection duration, pipelining
-// controls, etc. For now this is simply a port of the old Go code
-// client code to the http.ClientTransport interface.
-type transport struct {
- // TODO: keep-alives, pipelining, etc using a map from
- // scheme/host to a connection. Something like:
- l sync.Mutex
- hostConn map[string]*ClientConn
-}
-
-func (ct *transport) Do(req *Request) (resp *Response, err os.Error) {
+// DefaultTransport is the default implementation of Transport and is
+// used by DefaultClient. It establishes a new network connection for
+// each call to Do and uses HTTP proxies as directed by the
+// $HTTP_PROXY and $NO_PROXY (or $http_proxy and $no_proxy)
+// environment variables.
+var DefaultTransport RoundTripper = &Transport{}
+
+// Transport is an implementation of RoundTripper that supports http,
+// https, and http proxies (for either http or https with CONNECT).
+// Transport can also cache connections for future re-use.
+type Transport struct {
+ lk sync.Mutex
+ idleConn map[string][]*persistConn
+
+ // TODO: tunables on max cached connections (total, per-server), duration
+ // TODO: optional pipelining
+
+ IgnoreEnvironment bool // don't look at environment variables for proxy configuration
+ DisableKeepAlives bool
+}
+
+// RoundTrip implements the RoundTripper interface.
+func (t *Transport) RoundTrip(req *Request) (resp *Response, err os.Error) {
+ if req.URL == nil {
+ if req.URL, err = ParseURL(req.RawURL); err != nil {
+ return
+ }
+ }
if req.URL.Scheme != "http" && req.URL.Scheme != "https" {
return nil, &badStringError{"unsupported protocol scheme", req.URL.Scheme}
}
- addr := req.URL.Host
- if !hasPort(addr) {
- addr += ":" + req.URL.Scheme
+ cm, err := t.connectMethodForRequest(req)
+ if err != nil {
+ return nil, err
+ }
+
+ // Get the cached or newly-created connection to either the
+ // host (for http or https), the http proxy, or the http proxy
+ // pre-CONNECTed to https server. In any case, we'll be ready
+ // to send it requests.
+ pconn, err := t.getConn(cm)
+ if err != nil {
+ return nil, err
}
- var proxyURL *URL
- proxyAuth := ""
- proxy := ""
- if !matchNoProxy(addr) {
- proxy = os.Getenv("HTTP_PROXY")
- if proxy == "" {
- proxy = os.Getenv("http_proxy")
+ return pconn.roundTrip(req)
+}
+
+// CloseIdleConnections closes any connections which were previously
+// connected from previous requests but are now sitting idle in
+// a "keep-alive" state. It does not interrupt any connections currently
+// in use.
+func (t *Transport) CloseIdleConnections() {
+ t.lk.Lock()
+ defer t.lk.Unlock()
+ if t.idleConn == nil {
+ return
+ }
+ for _, conns := range t.idleConn {
+ for _, pconn := range conns {
+ pconn.close()
}
}
+ t.idleConn = nil
+}
- if proxy != "" {
- proxyURL, err = ParseRequestURL(proxy)
+//
+// Private implementation past this point.
+//
+
+func (t *Transport) getenvEitherCase(k string) string {
+ if t.IgnoreEnvironment {
+ return ""
+ }
+ if v := t.getenv(strings.ToUpper(k)); v != "" {
+ return v
+ }
+ return t.getenv(strings.ToLower(k))
+}
+
+func (t *Transport) getenv(k string) string {
+ if t.IgnoreEnvironment {
+ return ""
+ }
+ return os.Getenv(k)
+}
+
+func (t *Transport) connectMethodForRequest(req *Request) (*connectMethod, os.Error) {
+ cm := &connectMethod{
+ targetScheme: req.URL.Scheme,
+ targetAddr: canonicalAddr(req.URL),
+ }
+
+ proxy := t.getenvEitherCase("HTTP_PROXY")
+ if proxy != "" && t.useProxy(cm.targetAddr) {
+ proxyURL, err := ParseRequestURL(proxy)
if err != nil {
return nil, os.ErrorString("invalid proxy address")
}
@@ -65,83 +123,405 @@ func (ct *transport) Do(req *Request) (resp *Response, err os.Error) {
return nil, os.ErrorString("invalid proxy address")
}
}
- addr = proxyURL.Host
- proxyInfo := proxyURL.RawUserinfo
- if proxyInfo != "" {
- enc := base64.URLEncoding
- encoded := make([]byte, enc.EncodedLen(len(proxyInfo)))
- enc.Encode(encoded, []byte(proxyInfo))
- proxyAuth = "Basic " + string(encoded)
+ cm.proxyURL = proxyURL
+ }
+ return cm, nil
+}
+
+// proxyAuth returns the Proxy-Authorization header to set
+// on requests, if applicable.
+func (cm *connectMethod) proxyAuth() string {
+ if cm.proxyURL == nil {
+ return ""
+ }
+ proxyInfo := cm.proxyURL.RawUserinfo
+ if proxyInfo != "" {
+ enc := base64.URLEncoding
+ encoded := make([]byte, enc.EncodedLen(len(proxyInfo)))
+ enc.Encode(encoded, []byte(proxyInfo))
+ return "Basic " + string(encoded)
+ }
+ return ""
+}
+
+func (t *Transport) putIdleConn(pconn *persistConn) {
+ t.lk.Lock()
+ defer t.lk.Unlock()
+ if t.DisableKeepAlives {
+ pconn.close()
+ return
+ }
+ if pconn.isBroken() {
+ return
+ }
+ key := pconn.cacheKey
+ t.idleConn[key] = append(t.idleConn[key], pconn)
+}
+
+func (t *Transport) getIdleConn(cm *connectMethod) (pconn *persistConn) {
+ t.lk.Lock()
+ defer t.lk.Unlock()
+ if t.idleConn == nil {
+ t.idleConn = make(map[string][]*persistConn)
+ }
+ key := cm.String()
+ for {
+ pconns, ok := t.idleConn[key]
+ if !ok {
+ return nil
+ }
+ if len(pconns) == 1 {
+ pconn = pconns[0]
+ t.idleConn[key] = nil, false
+ } else {
+ // 2 or more cached connections; pop last
+ // TODO: queue?
+ pconn = pconns[len(pconns)-1]
+ t.idleConn[key] = pconns[0 : len(pconns)-1]
+ }
+ if !pconn.isBroken() {
+ return
}
}
+ return
+}
+
+// getConn dials and creates a new persistConn to the target as
+// specified in the connectMethod. This includes doing a proxy CONNECT
+// and/or setting up TLS. If this doesn't return an error, the persistConn
+// is ready to write requests to.
+func (t *Transport) getConn(cm *connectMethod) (*persistConn, os.Error) {
+ if pc := t.getIdleConn(cm); pc != nil {
+ return pc, nil
+ }
- // Connect to server or proxy
- conn, err := net.Dial("tcp", "", addr)
+ conn, err := net.Dial("tcp", "", cm.addr())
if err != nil {
return nil, err
}
- if req.URL.Scheme == "http" {
- // Include proxy http header if needed.
- if proxyAuth != "" {
- req.Header.Set("Proxy-Authorization", proxyAuth)
- }
- } else { // https
- if proxyURL != nil {
- // Ask proxy for direct connection to server.
- // addr defaults above to ":https" but we need to use numbers
- addr = req.URL.Host
- if !hasPort(addr) {
- addr += ":443"
- }
- fmt.Fprintf(conn, "CONNECT %s HTTP/1.1\r\n", addr)
- fmt.Fprintf(conn, "Host: %s\r\n", addr)
- if proxyAuth != "" {
- fmt.Fprintf(conn, "Proxy-Authorization: %s\r\n", proxyAuth)
- }
- fmt.Fprintf(conn, "\r\n")
+ pa := cm.proxyAuth()
- // Read response.
- // Okay to use and discard buffered reader here, because
- // TLS server will not speak until spoken to.
- br := bufio.NewReader(conn)
- resp, err := ReadResponse(br, "CONNECT")
- if err != nil {
- return nil, err
- }
- if resp.StatusCode != 200 {
- f := strings.Split(resp.Status, " ", 2)
- return nil, os.ErrorString(f[1])
+ pconn := &persistConn{
+ t: t,
+ cacheKey: cm.String(),
+ conn: conn,
+ reqch: make(chan requestAndChan, 50),
+ }
+ newClientConnFunc := NewClientConn
+
+ switch {
+ case cm.proxyURL == nil:
+ // Do nothing.
+ case cm.targetScheme == "http":
+ newClientConnFunc = NewProxyClientConn
+ if pa != "" {
+ pconn.mutateRequestFunc = func(req *Request) {
+ if req.Header == nil {
+ req.Header = make(Header)
+ }
+ req.Header.Set("Proxy-Authorization", pa)
}
}
+ case cm.targetScheme == "https":
+ fmt.Fprintf(conn, "CONNECT %s HTTP/1.1\r\n", cm.targetAddr)
+ fmt.Fprintf(conn, "Host: %s\r\n", cm.targetAddr)
+ if pa != "" {
+ fmt.Fprintf(conn, "Proxy-Authorization: %s\r\n", pa)
+ }
+ fmt.Fprintf(conn, "\r\n")
+ // Read response.
+ // Okay to use and discard buffered reader here, because
+ // TLS server will not speak until spoken to.
+ br := bufio.NewReader(conn)
+ resp, err := ReadResponse(br, "CONNECT")
+ if err != nil {
+ conn.Close()
+ return nil, err
+ }
+ if resp.StatusCode != 200 {
+ f := strings.Split(resp.Status, " ", 2)
+ conn.Close()
+ return nil, os.ErrorString(f[1])
+ }
+ }
+
+ if cm.targetScheme == "https" {
// Initiate TLS and check remote host name against certificate.
conn = tls.Client(conn, nil)
if err = conn.(*tls.Conn).Handshake(); err != nil {
return nil, err
}
- h := req.URL.Host
- if hasPort(h) {
- h = h[:strings.LastIndex(h, ":")]
- }
- if err = conn.(*tls.Conn).VerifyHostname(h); err != nil {
+ if err = conn.(*tls.Conn).VerifyHostname(cm.tlsHost()); err != nil {
return nil, err
}
+ pconn.conn = conn
}
- err = req.Write(conn)
- if err != nil {
- conn.Close()
- return nil, err
+ pconn.br = bufio.NewReader(pconn.conn)
+ pconn.cc = newClientConnFunc(conn, pconn.br)
+ pconn.cc.readRes = readResponseWithEOFSignal
+ go pconn.readLoop()
+ return pconn, nil
+}
+
+// useProxy returns true if requests to addr should use a proxy,
+// according to the NO_PROXY or no_proxy environment variable.
+func (t *Transport) useProxy(addr string) bool {
+ if len(addr) == 0 {
+ return true
+ }
+ no_proxy := t.getenvEitherCase("NO_PROXY")
+ if no_proxy == "*" {
+ return false
+ }
+
+ addr = strings.ToLower(strings.TrimSpace(addr))
+ if hasPort(addr) {
+ addr = addr[:strings.LastIndex(addr, ":")]
+ }
+
+ for _, p := range strings.Split(no_proxy, ",", -1) {
+ p = strings.ToLower(strings.TrimSpace(p))
+ if len(p) == 0 {
+ continue
+ }
+ if hasPort(p) {
+ p = p[:strings.LastIndex(p, ":")]
+ }
+ if addr == p || (p[0] == '.' && (strings.HasSuffix(addr, p) || addr == p[1:])) {
+ return false
+ }
+ }
+ return true
+}
+
+// connectMethod is the map key (in its String form) for keeping persistent
+// TCP connections alive for subsequent HTTP requests.
+//
+// A connect method may be of the following types:
+//
+// Cache key form Description
+// ----------------- -------------------------
+// ||http|foo.com http directly to server, no proxy
+// ||https|foo.com https directly to server, no proxy
+// http://proxy.com|https|foo.com http to proxy, then CONNECT to foo.com
+// http://proxy.com|http http to proxy, http to anywhere after that
+//
+// Note: no support to https to the proxy yet.
+//
+type connectMethod struct {
+ proxyURL *URL // "" for no proxy, else full proxy URL
+ targetScheme string // "http" or "https"
+ targetAddr string // Not used if proxy + http targetScheme (4th example in table)
+}
+
+func (ck *connectMethod) String() string {
+ proxyStr := ""
+ if ck.proxyURL != nil {
+ proxyStr = ck.proxyURL.String()
+ }
+ return strings.Join([]string{proxyStr, ck.targetScheme, ck.targetAddr}, "|")
+}
+
+// addr returns the first hop "host:port" to which we need to TCP connect.
+func (cm *connectMethod) addr() string {
+ if cm.proxyURL != nil {
+ return canonicalAddr(cm.proxyURL)
+ }
+ return cm.targetAddr
+}
+
+// tlsHost returns the host name to match against the peer's
+// TLS certificate.
+func (cm *connectMethod) tlsHost() string {
+ h := cm.targetAddr
+ if hasPort(h) {
+ h = h[:strings.LastIndex(h, ":")]
+ }
+ return h
+}
+
+type readResult struct {
+ res *Response // either res or err will be set
+ err os.Error
+}
+
+type writeRequest struct {
+ // Set by client (in pc.roundTrip)
+ req *Request
+ resch chan *readResult
+
+ // Set by writeLoop if an error writing headers.
+ writeErr os.Error
+}
+
+// persistConn wraps a connection, usually a persistent one
+// (but may be used for non-keep-alive requests as well)
+type persistConn struct {
+ t *Transport
+ cacheKey string // its connectMethod.String()
+ conn net.Conn
+ cc *ClientConn
+ br *bufio.Reader
+ reqch chan requestAndChan // written by roundTrip(); read by readLoop()
+ mutateRequestFunc func(*Request) // nil or func to modify each outbound request
+
+ lk sync.Mutex // guards numExpectedResponses and broken
+ numExpectedResponses int
+ broken bool // an error has happened on this connection; marked broken so it's not reused.
+}
+
+func (pc *persistConn) isBroken() bool {
+ pc.lk.Lock()
+ defer pc.lk.Unlock()
+ return pc.broken
+}
+
+func (pc *persistConn) expectingResponse() bool {
+ pc.lk.Lock()
+ defer pc.lk.Unlock()
+ return pc.numExpectedResponses > 0
+}
+
+func (pc *persistConn) readLoop() {
+ alive := true
+ for alive {
+ pb, err := pc.br.Peek(1)
+ if err != nil {
+ if (err == os.EOF || err == os.EINVAL) && !pc.expectingResponse() {
+ // Remote side closed on us. (We probably hit their
+ // max idle timeout)
+ pc.close()
+ return
+ }
+ }
+ if !pc.expectingResponse() {
+ log.Printf("Unsolicited response received on idle HTTP channel starting with %q; err=%v",
+ string(pb), err)
+ pc.close()
+ return
+ }
+
+ rc := <-pc.reqch
+ resp, err := pc.cc.Read(rc.req)
+ if err == nil && !rc.req.Close {
+ pc.t.putIdleConn(pc)
+ }
+ if err == ErrPersistEOF {
+ // Succeeded, but we can't send any more
+ // persistent connections on this again. We
+ // hide this error to upstream callers.
+ alive = false
+ err = nil
+ } else if err != nil {
+ alive = false
+ }
+ rc.ch <- responseAndError{resp, err}
+
+ // Wait for the just-returned response body to be fully consumed
+ // before we race and peek on the underlying bufio reader.
+ if alive {
+ <-resp.Body.(*bodyEOFSignal).ch
+ }
+ }
+}
+
+type responseAndError struct {
+ res *Response
+ err os.Error
+}
+
+type requestAndChan struct {
+ req *Request
+ ch chan responseAndError
+}
+
+func (pc *persistConn) roundTrip(req *Request) (resp *Response, err os.Error) {
+ if pc.mutateRequestFunc != nil {
+ pc.mutateRequestFunc(req)
}
- reader := bufio.NewReader(conn)
- resp, err = ReadResponse(reader, req.Method)
+ pc.lk.Lock()
+ pc.numExpectedResponses++
+ pc.lk.Unlock()
+
+ err = pc.cc.Write(req)
if err != nil {
- conn.Close()
- return nil, err
+ pc.close()
+ return
}
- resp.Body = readClose{resp.Body, conn}
+ ch := make(chan responseAndError, 1)
+ pc.reqch <- requestAndChan{req, ch}
+ re := <-ch
+ pc.lk.Lock()
+ pc.numExpectedResponses--
+ pc.lk.Unlock()
+ return re.res, re.err
+}
+
+func (pc *persistConn) close() {
+ pc.lk.Lock()
+ defer pc.lk.Unlock()
+ pc.broken = true
+ pc.cc.Close()
+ pc.conn.Close()
+ pc.mutateRequestFunc = nil
+}
+
+var portMap = map[string]string{
+ "http": "80",
+ "https": "443",
+}
+
+// canonicalAddr returns url.Host but always with a ":port" suffix
+func canonicalAddr(url *URL) string {
+ addr := url.Host
+ if !hasPort(addr) {
+ return addr + ":" + portMap[url.Scheme]
+ }
+ return addr
+}
+
+func responseIsKeepAlive(res *Response) bool {
+ // TODO: implement. for now just always shutting down the connection.
+ return false
+}
+
+// readResponseWithEOFSignal is a wrapper around ReadResponse that replaces
+// the response body with a bodyEOFSignal-wrapped version.
+func readResponseWithEOFSignal(r *bufio.Reader, requestMethod string) (resp *Response, err os.Error) {
+ resp, err = ReadResponse(r, requestMethod)
+ if err == nil {
+ resp.Body = &bodyEOFSignal{resp.Body, make(chan bool, 1), false}
+ }
+ return
+}
+
+// bodyEOFSignal wraps a ReadCloser but sends on ch once once
+// the wrapped ReadCloser is fully consumed (including on Close)
+type bodyEOFSignal struct {
+ body io.ReadCloser
+ ch chan bool
+ done bool
+}
+
+func (es *bodyEOFSignal) Read(p []byte) (n int, err os.Error) {
+ n, err = es.body.Read(p)
+ if err == os.EOF && !es.done {
+ es.ch <- true
+ es.done = true
+ }
+ return
+}
+
+func (es *bodyEOFSignal) Close() (err os.Error) {
+ err = es.body.Close()
+ if err == nil && !es.done {
+ es.ch <- true
+ es.done = true
+ }
return
}
diff --git a/libgo/go/http/transport_test.go b/libgo/go/http/transport_test.go
new file mode 100644
index 00000000000..5c3e1cdb582
--- /dev/null
+++ b/libgo/go/http/transport_test.go
@@ -0,0 +1,235 @@
+// 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 transport.go
+
+package http_test
+
+import (
+ "fmt"
+ . "http"
+ "http/httptest"
+ "io/ioutil"
+ "os"
+ "testing"
+ "time"
+)
+
+// TODO: test 5 pipelined requests with responses: 1) OK, 2) OK, Connection: Close
+// and then verify that the final 2 responses get errors back.
+
+// hostPortHandler writes back the client's "host:port".
+var hostPortHandler = HandlerFunc(func(w ResponseWriter, r *Request) {
+ if r.FormValue("close") == "true" {
+ w.Header().Set("Connection", "close")
+ }
+ fmt.Fprintf(w, "%s", r.RemoteAddr)
+})
+
+// Two subsequent requests and verify their response is the same.
+// The response from the server is our own IP:port
+func TestTransportKeepAlives(t *testing.T) {
+ ts := httptest.NewServer(hostPortHandler)
+ defer ts.Close()
+
+ for _, disableKeepAlive := range []bool{false, true} {
+ tr := &Transport{DisableKeepAlives: disableKeepAlive}
+ c := &Client{Transport: tr}
+
+ fetch := func(n int) string {
+ res, _, err := c.Get(ts.URL)
+ if err != nil {
+ t.Fatalf("error in disableKeepAlive=%v, req #%d, GET: %v", disableKeepAlive, n, err)
+ }
+ body, err := ioutil.ReadAll(res.Body)
+ if err != nil {
+ t.Fatalf("error in disableKeepAlive=%v, req #%d, ReadAll: %v", disableKeepAlive, n, err)
+ }
+ return string(body)
+ }
+
+ body1 := fetch(1)
+ body2 := fetch(2)
+
+ bodiesDiffer := body1 != body2
+ if bodiesDiffer != disableKeepAlive {
+ t.Errorf("error in disableKeepAlive=%v. unexpected bodiesDiffer=%v; body1=%q; body2=%q",
+ disableKeepAlive, bodiesDiffer, body1, body2)
+ }
+ }
+}
+
+func TestTransportConnectionCloseOnResponse(t *testing.T) {
+ ts := httptest.NewServer(hostPortHandler)
+ defer ts.Close()
+
+ for _, connectionClose := range []bool{false, true} {
+ tr := &Transport{}
+ c := &Client{Transport: tr}
+
+ fetch := func(n int) string {
+ req := new(Request)
+ var err os.Error
+ req.URL, err = ParseURL(ts.URL + fmt.Sprintf("?close=%v", connectionClose))
+ if err != nil {
+ t.Fatalf("URL parse error: %v", err)
+ }
+ req.Method = "GET"
+ req.Proto = "HTTP/1.1"
+ req.ProtoMajor = 1
+ req.ProtoMinor = 1
+
+ res, err := c.Do(req)
+ if err != nil {
+ t.Fatalf("error in connectionClose=%v, req #%d, Do: %v", connectionClose, n, err)
+ }
+ body, err := ioutil.ReadAll(res.Body)
+ if err != nil {
+ t.Fatalf("error in connectionClose=%v, req #%d, ReadAll: %v", connectionClose, n, err)
+ }
+ return string(body)
+ }
+
+ body1 := fetch(1)
+ body2 := fetch(2)
+ bodiesDiffer := body1 != body2
+ if bodiesDiffer != connectionClose {
+ t.Errorf("error in connectionClose=%v. unexpected bodiesDiffer=%v; body1=%q; body2=%q",
+ connectionClose, bodiesDiffer, body1, body2)
+ }
+ }
+}
+
+func TestTransportConnectionCloseOnRequest(t *testing.T) {
+ ts := httptest.NewServer(hostPortHandler)
+ defer ts.Close()
+
+ for _, connectionClose := range []bool{false, true} {
+ tr := &Transport{}
+ c := &Client{Transport: tr}
+
+ fetch := func(n int) string {
+ req := new(Request)
+ var err os.Error
+ req.URL, err = ParseURL(ts.URL)
+ if err != nil {
+ t.Fatalf("URL parse error: %v", err)
+ }
+ req.Method = "GET"
+ req.Proto = "HTTP/1.1"
+ req.ProtoMajor = 1
+ req.ProtoMinor = 1
+ req.Close = connectionClose
+
+ res, err := c.Do(req)
+ if err != nil {
+ t.Fatalf("error in connectionClose=%v, req #%d, Do: %v", connectionClose, n, err)
+ }
+ body, err := ioutil.ReadAll(res.Body)
+ if err != nil {
+ t.Fatalf("error in connectionClose=%v, req #%d, ReadAll: %v", connectionClose, n, err)
+ }
+ return string(body)
+ }
+
+ body1 := fetch(1)
+ body2 := fetch(2)
+ bodiesDiffer := body1 != body2
+ if bodiesDiffer != connectionClose {
+ t.Errorf("error in connectionClose=%v. unexpected bodiesDiffer=%v; body1=%q; body2=%q",
+ connectionClose, bodiesDiffer, body1, body2)
+ }
+ }
+}
+
+func TestTransportIdleCacheKeys(t *testing.T) {
+ ts := httptest.NewServer(hostPortHandler)
+ defer ts.Close()
+
+ tr := &Transport{DisableKeepAlives: false}
+ c := &Client{Transport: tr}
+
+ if e, g := 0, len(tr.IdleConnKeysForTesting()); e != g {
+ t.Errorf("After CloseIdleConnections expected %d idle conn cache keys; got %d", e, g)
+ }
+
+ if _, _, err := c.Get(ts.URL); err != nil {
+ t.Error(err)
+ }
+
+ keys := tr.IdleConnKeysForTesting()
+ if e, g := 1, len(keys); e != g {
+ t.Fatalf("After Get expected %d idle conn cache keys; got %d", e, g)
+ }
+
+ if e := "|http|" + ts.Listener.Addr().String(); keys[0] != e {
+ t.Logf("Expected idle cache key %q; got %q", e, keys[0])
+ }
+
+ tr.CloseIdleConnections()
+ if e, g := 0, len(tr.IdleConnKeysForTesting()); e != g {
+ t.Errorf("After CloseIdleConnections expected %d idle conn cache keys; got %d", e, g)
+ }
+}
+
+func TestTransportServerClosingUnexpectedly(t *testing.T) {
+ ts := httptest.NewServer(hostPortHandler)
+ defer ts.Close()
+
+ tr := &Transport{}
+ c := &Client{Transport: tr}
+
+ fetch := func(n int) string {
+ res, _, err := c.Get(ts.URL)
+ if err != nil {
+ t.Fatalf("error in req #%d, GET: %v", n, err)
+ }
+ body, err := ioutil.ReadAll(res.Body)
+ if err != nil {
+ t.Fatalf("error in req #%d, ReadAll: %v", n, err)
+ }
+ res.Body.Close()
+ return string(body)
+ }
+
+ body1 := fetch(1)
+ body2 := fetch(2)
+
+ ts.CloseClientConnections() // surprise!
+ time.Sleep(25e6) // idle for a bit (test is inherently racey, but expectedly)
+
+ body3 := fetch(3)
+
+ if body1 != body2 {
+ t.Errorf("expected body1 and body2 to be equal")
+ }
+ if body2 == body3 {
+ t.Errorf("expected body2 and body3 to be different")
+ }
+}
+
+func TestTransportNilURL(t *testing.T) {
+ ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
+ fmt.Fprintf(w, "Hi")
+ }))
+ defer ts.Close()
+
+ req := new(Request)
+ req.URL = nil // what we're actually testing
+ req.Method = "GET"
+ req.RawURL = ts.URL
+ req.Proto = "HTTP/1.1"
+ req.ProtoMajor = 1
+ req.ProtoMinor = 1
+
+ tr := &Transport{}
+ res, err := tr.RoundTrip(req)
+ if err != nil {
+ t.Fatalf("unexpected RoundTrip error: %v", err)
+ }
+ body, err := ioutil.ReadAll(res.Body)
+ if g, e := string(body), "Hi"; g != e {
+ t.Fatalf("Expected response body of %q; got %q", e, g)
+ }
+}