summaryrefslogtreecommitdiff
path: root/src/pkg/http/client.go
diff options
context:
space:
mode:
Diffstat (limited to 'src/pkg/http/client.go')
-rw-r--r--src/pkg/http/client.go216
1 files changed, 150 insertions, 66 deletions
diff --git a/src/pkg/http/client.go b/src/pkg/http/client.go
index 022f4f124..c24eea581 100644
--- a/src/pkg/http/client.go
+++ b/src/pkg/http/client.go
@@ -7,18 +7,41 @@
package http
import (
- "bufio"
"bytes"
- "crypto/tls"
"encoding/base64"
"fmt"
"io"
- "net"
"os"
"strconv"
"strings"
)
+// A Client is an HTTP client. Its zero value (DefaultClient) is a usable client
+// that uses DefaultTransport.
+// Client is not yet very configurable.
+type Client struct {
+ Transport Transport // if nil, DefaultTransport is used
+}
+
+// DefaultClient is the default Client and is used by Get, Head, and Post.
+var DefaultClient = &Client{}
+
+// Transport is an interface representing the ability to execute a
+// single HTTP transaction, obtaining the Response for a given Request.
+type Transport 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,
+ // 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)
+}
+
// Given a string of the form "host", "host:port", or "[ipv6::address]:port",
// return true if the string includes a port.
func hasPort(s string) bool { return strings.LastIndex(s, ":") > strings.LastIndex(s, "]") }
@@ -31,67 +54,83 @@ type readClose struct {
io.Closer
}
-// Send issues an HTTP request. Caller should close resp.Body when done reading it.
+// 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.
+//
+// Callers should close resp.Body when done reading from it.
+//
+// Generally Get, Post, or PostForm will be used instead of Do.
+func (c *Client) Do(req *Request) (resp *Response, err os.Error) {
+ return send(req, c.Transport)
+}
+
+
+// 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) (resp *Response, err os.Error) {
- if req.URL.Scheme != "http" && req.URL.Scheme != "https" {
- return nil, &badStringError{"unsupported protocol scheme", req.URL.Scheme}
+func send(req *Request, t Transport) (resp *Response, err os.Error) {
+ if t == nil {
+ t = DefaultTransport
+ if t == nil {
+ err = os.NewError("no http.Client.Transport or http.DefaultTransport")
+ return
+ }
}
- addr := req.URL.Host
- if !hasPort(addr) {
- addr += ":" + req.URL.Scheme
+ // Most the callers of send (Get, Post, et al) don't need
+ // Headers, leaving it uninitialized. We guarantee to the
+ // Transport that this has been initialized, though.
+ if req.Header == nil {
+ req.Header = make(Header)
}
+
info := req.URL.RawUserinfo
if len(info) > 0 {
enc := base64.URLEncoding
encoded := make([]byte, enc.EncodedLen(len(info)))
enc.Encode(encoded, []byte(info))
if req.Header == nil {
- req.Header = make(map[string]string)
+ req.Header = make(Header)
}
- req.Header["Authorization"] = "Basic " + string(encoded)
- }
-
- var conn io.ReadWriteCloser
- if req.URL.Scheme == "http" {
- conn, err = net.Dial("tcp", "", addr)
- if err != nil {
- return nil, err
- }
- } else { // https
- conn, err = tls.Dial("tcp", "", addr, nil)
- if err != nil {
- return nil, err
- }
- h := req.URL.Host
- if hasPort(h) {
- h = h[0:strings.LastIndex(h, ":")]
- }
- if err := conn.(*tls.Conn).VerifyHostname(h); err != nil {
- return nil, err
- }
- }
-
- err = req.Write(conn)
- if err != nil {
- conn.Close()
- return nil, err
+ req.Header.Set("Authorization", "Basic "+string(encoded))
}
-
- reader := bufio.NewReader(conn)
- resp, err = ReadResponse(reader, req.Method)
- if err != nil {
- conn.Close()
- return nil, err
- }
-
- resp.Body = readClose{resp.Body, conn}
-
- return
+ return t.Do(req)
}
// True if the specified HTTP status code is one for which the Get utility should
@@ -115,12 +154,32 @@ func shouldRedirect(statusCode int) bool {
// finalURL is the URL from which the response was fetched -- identical to the
// input URL unless redirects were followed.
//
-// Caller should close r.Body when done reading it.
+// Caller should close r.Body when done reading from it.
+//
+// Get is a convenience wrapper around DefaultClient.Get.
func Get(url string) (r *Response, finalURL string, err os.Error) {
+ return DefaultClient.Get(url)
+}
+
+// Get issues a GET to the specified URL. If the response is one of the following
+// redirect codes, it follows the redirect, up to a maximum of 10 redirects:
+//
+// 301 (Moved Permanently)
+// 302 (Found)
+// 303 (See Other)
+// 307 (Temporary Redirect)
+//
+// finalURL is the URL from which the response was fetched -- identical to the
+// input URL unless redirects were followed.
+//
+// Caller should close r.Body when done reading from it.
+func (c *Client) Get(url string) (r *Response, finalURL string, err os.Error) {
// TODO: if/when we add cookie support, the redirected request shouldn't
// necessarily supply the same cookies as the original.
// TODO: set referrer header on redirects.
var base *URL
+ // TODO: remove this hard-coded 10 and use the Client's policy
+ // (ClientConfig) instead.
for redirect := 0; ; redirect++ {
if redirect >= 10 {
err = os.ErrorString("stopped after 10 redirects")
@@ -128,6 +187,9 @@ func Get(url string) (r *Response, finalURL string, err os.Error) {
}
var req Request
+ req.Method = "GET"
+ req.ProtoMajor = 1
+ req.ProtoMinor = 1
if base == nil {
req.URL, err = ParseURL(url)
} else {
@@ -137,12 +199,12 @@ func Get(url string) (r *Response, finalURL string, err os.Error) {
break
}
url = req.URL.String()
- if r, err = send(&req); err != nil {
+ if r, err = send(&req, c.Transport); err != nil {
break
}
if shouldRedirect(r.StatusCode) {
r.Body.Close()
- if url = r.GetHeader("Location"); url == "" {
+ if url = r.Header.Get("Location"); url == "" {
err = os.ErrorString(fmt.Sprintf("%d response missing Location header", r.StatusCode))
break
}
@@ -159,16 +221,25 @@ func Get(url string) (r *Response, finalURL string, err os.Error) {
// Post issues a POST to the specified URL.
//
-// Caller should close r.Body when done reading it.
+// Caller should close r.Body when done reading from it.
+//
+// Post is a wrapper around DefaultClient.Post
func Post(url string, bodyType string, body io.Reader) (r *Response, err os.Error) {
+ return DefaultClient.Post(url, bodyType, body)
+}
+
+// Post issues a POST to the specified URL.
+//
+// Caller should close r.Body when done reading from it.
+func (c *Client) Post(url string, bodyType string, body io.Reader) (r *Response, err os.Error) {
var req Request
req.Method = "POST"
req.ProtoMajor = 1
req.ProtoMinor = 1
req.Close = true
req.Body = nopCloser{body}
- req.Header = map[string]string{
- "Content-Type": bodyType,
+ req.Header = Header{
+ "Content-Type": {bodyType},
}
req.TransferEncoding = []string{"chunked"}
@@ -177,14 +248,24 @@ func Post(url string, bodyType string, body io.Reader) (r *Response, err os.Erro
return nil, err
}
- return send(&req)
+ return send(&req, c.Transport)
}
// PostForm issues a POST to the specified URL,
// with data's keys and values urlencoded as the request body.
//
-// Caller should close r.Body when done reading it.
+// Caller should close r.Body when done reading from it.
+//
+// PostForm is a wrapper around DefaultClient.PostForm
func PostForm(url string, data map[string]string) (r *Response, err os.Error) {
+ return DefaultClient.PostForm(url, data)
+}
+
+// PostForm issues a POST to the specified URL,
+// with data's keys and values urlencoded as the request body.
+//
+// Caller should close r.Body when done reading from it.
+func (c *Client) PostForm(url string, data map[string]string) (r *Response, err os.Error) {
var req Request
req.Method = "POST"
req.ProtoMajor = 1
@@ -192,9 +273,9 @@ func PostForm(url string, data map[string]string) (r *Response, err os.Error) {
req.Close = true
body := urlencode(data)
req.Body = nopCloser{body}
- req.Header = map[string]string{
- "Content-Type": "application/x-www-form-urlencoded",
- "Content-Length": strconv.Itoa(body.Len()),
+ req.Header = Header{
+ "Content-Type": {"application/x-www-form-urlencoded"},
+ "Content-Length": {strconv.Itoa(body.Len())},
}
req.ContentLength = int64(body.Len())
@@ -203,7 +284,7 @@ func PostForm(url string, data map[string]string) (r *Response, err os.Error) {
return nil, err
}
- return send(&req)
+ return send(&req, c.Transport)
}
// TODO: remove this function when PostForm takes a multimap.
@@ -216,17 +297,20 @@ func urlencode(data map[string]string) (b *bytes.Buffer) {
}
// Head issues a HEAD to the specified URL.
+//
+// Head is a wrapper around DefaultClient.Head
func Head(url string) (r *Response, err os.Error) {
+ return DefaultClient.Head(url)
+}
+
+// Head issues a HEAD to the specified URL.
+func (c *Client) Head(url string) (r *Response, err os.Error) {
var req Request
req.Method = "HEAD"
if req.URL, err = ParseURL(url); err != nil {
return
}
- url = req.URL.String()
- if r, err = send(&req); err != nil {
- return
- }
- return
+ return send(&req, c.Transport)
}
type nopCloser struct {