http: add proxy support
Fixes #53.
R=agl1, jacek.masiulaniec, adg, rsc, agl
CC=golang-dev
https://golang.org/cl/3794041
diff --git a/src/pkg/http/client.go b/src/pkg/http/client.go
index 022f4f1..ae37879 100644
--- a/src/pkg/http/client.go
+++ b/src/pkg/http/client.go
@@ -31,6 +31,40 @@
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
+}
+
// Send issues an HTTP request. Caller should close resp.Body when done reading it.
//
// TODO: support persistent connections (multiple requests on a single connection).
@@ -56,22 +90,81 @@
req.Header["Authorization"] = "Basic " + string(encoded)
}
- var conn io.ReadWriteCloser
- if req.URL.Scheme == "http" {
- conn, err = net.Dial("tcp", "", addr)
+ var proxyURL *URL
+ proxyAuth := ""
+ proxy := os.Getenv("HTTP_PROXY")
+ if proxy == "" {
+ proxy = os.Getenv("http_proxy")
+ }
+ if matchNoProxy(addr) {
+ proxy = ""
+ }
+
+ if proxy != "" {
+ proxyURL, err = ParseURL(proxy)
if err != nil {
- return nil, err
+ 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)
+ }
+ }
+
+ // Connect to server or proxy.
+ conn, err := net.Dial("tcp", "", addr)
+ if err != nil {
+ return nil, err
+ }
+
+ if req.URL.Scheme == "http" {
+ // Include proxy http header if needed.
+ if proxyAuth != "" {
+ req.Header["Proxy-Authorization"] = proxyAuth
}
} else { // https
- conn, err = tls.Dial("tcp", "", addr, nil)
- if err != nil {
+ 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")
+
+ // 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])
+ }
+ }
+
+ // 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[0:strings.LastIndex(h, ":")]
+ h = h[:strings.LastIndex(h, ":")]
}
- if err := conn.(*tls.Conn).VerifyHostname(h); err != nil {
+ if err = conn.(*tls.Conn).VerifyHostname(h); err != nil {
return nil, err
}
}
diff --git a/src/pkg/http/proxy_test.go b/src/pkg/http/proxy_test.go
new file mode 100644
index 0000000..0f2ca45
--- /dev/null
+++ b/src/pkg/http/proxy_test.go
@@ -0,0 +1,45 @@
+// 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 (
+ "os"
+ "testing"
+)
+
+// TODO(mattn):
+// test ProxyAuth
+
+var MatchNoProxyTests = []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
+}
+
+func TestMatchNoProxy(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 {
+ if test.match {
+ t.Errorf("matchNoProxy(%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)
+ }
+ }
+ }
+}