From f04b5a3bac0206d507a4984897c60a362a3bdab7 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 16 Feb 2011 14:06:50 -0500 Subject: [PATCH] http: add proxy support Fixes #53. R=agl1, jacek.masiulaniec, adg, rsc, agl CC=golang-dev https://golang.org/cl/3794041 --- src/pkg/http/client.go | 109 ++++++++++++++++++++++++++++++++++--- src/pkg/http/proxy_test.go | 45 +++++++++++++++ 2 files changed, 146 insertions(+), 8 deletions(-) create mode 100644 src/pkg/http/proxy_test.go diff --git a/src/pkg/http/client.go b/src/pkg/http/client.go index 022f4f124a..ae37879ae9 100644 --- a/src/pkg/http/client.go +++ b/src/pkg/http/client.go @@ -31,6 +31,40 @@ 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 +} + // 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 @@ func send(req *Request) (resp *Response, err os.Error) { 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 0000000000..0f2ca458fe --- /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) + } + } + } +}