proxy: add support for ALL_PROXY and NO_PROXY

Fixes golang/go#13456

Change-Id: I0b938f824c47b29ac2026eff83e61c2f227a6cc1
Reviewed-on: https://go-review.googlesource.com/47530
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/proxy/proxy.go b/proxy/proxy.go
index 78a8b7b..553ead7 100644
--- a/proxy/proxy.go
+++ b/proxy/proxy.go
@@ -11,6 +11,7 @@
 	"net"
 	"net/url"
 	"os"
+	"sync"
 )
 
 // A Dialer is a means to establish a connection.
@@ -27,7 +28,7 @@
 // FromEnvironment returns the dialer specified by the proxy related variables in
 // the environment.
 func FromEnvironment() Dialer {
-	allProxy := os.Getenv("all_proxy")
+	allProxy := allProxyEnv.Get()
 	if len(allProxy) == 0 {
 		return Direct
 	}
@@ -41,7 +42,7 @@
 		return Direct
 	}
 
-	noProxy := os.Getenv("no_proxy")
+	noProxy := noProxyEnv.Get()
 	if len(noProxy) == 0 {
 		return proxy
 	}
@@ -92,3 +93,42 @@
 
 	return nil, errors.New("proxy: unknown scheme: " + u.Scheme)
 }
+
+var (
+	allProxyEnv = &envOnce{
+		names: []string{"ALL_PROXY", "all_proxy"},
+	}
+	noProxyEnv = &envOnce{
+		names: []string{"NO_PROXY", "no_proxy"},
+	}
+)
+
+// envOnce looks up an environment variable (optionally by multiple
+// names) once. It mitigates expensive lookups on some platforms
+// (e.g. Windows).
+// (Borrowed from net/http/transport.go)
+type envOnce struct {
+	names []string
+	once  sync.Once
+	val   string
+}
+
+func (e *envOnce) Get() string {
+	e.once.Do(e.init)
+	return e.val
+}
+
+func (e *envOnce) init() {
+	for _, n := range e.names {
+		e.val = os.Getenv(n)
+		if e.val != "" {
+			return
+		}
+	}
+}
+
+// reset is used by tests
+func (e *envOnce) reset() {
+	e.once = sync.Once{}
+	e.val = ""
+}
diff --git a/proxy/proxy_test.go b/proxy/proxy_test.go
index c19a5c0..0f31e21 100644
--- a/proxy/proxy_test.go
+++ b/proxy/proxy_test.go
@@ -5,14 +5,73 @@
 package proxy
 
 import (
+	"bytes"
+	"fmt"
 	"io"
 	"net"
 	"net/url"
+	"os"
 	"strconv"
+	"strings"
 	"sync"
 	"testing"
 )
 
+type proxyFromEnvTest struct {
+	allProxyEnv string
+	noProxyEnv  string
+	wantTypeOf  Dialer
+}
+
+func (t proxyFromEnvTest) String() string {
+	var buf bytes.Buffer
+	space := func() {
+		if buf.Len() > 0 {
+			buf.WriteByte(' ')
+		}
+	}
+	if t.allProxyEnv != "" {
+		fmt.Fprintf(&buf, "all_proxy=%q", t.allProxyEnv)
+	}
+	if t.noProxyEnv != "" {
+		space()
+		fmt.Fprintf(&buf, "no_proxy=%q", t.noProxyEnv)
+	}
+	return strings.TrimSpace(buf.String())
+}
+
+func TestFromEnvironment(t *testing.T) {
+	ResetProxyEnv()
+
+	type dummyDialer struct {
+		direct
+	}
+
+	RegisterDialerType("irc", func(_ *url.URL, _ Dialer) (Dialer, error) {
+		return dummyDialer{}, nil
+	})
+
+	proxyFromEnvTests := []proxyFromEnvTest{
+		{allProxyEnv: "127.0.0.1:8080", noProxyEnv: "localhost, 127.0.0.1", wantTypeOf: direct{}},
+		{allProxyEnv: "ftp://example.com:8000", noProxyEnv: "localhost, 127.0.0.1", wantTypeOf: direct{}},
+		{allProxyEnv: "socks5://example.com:8080", noProxyEnv: "localhost, 127.0.0.1", wantTypeOf: &PerHost{}},
+		{allProxyEnv: "irc://example.com:8000", wantTypeOf: dummyDialer{}},
+		{noProxyEnv: "localhost, 127.0.0.1", wantTypeOf: direct{}},
+		{wantTypeOf: direct{}},
+	}
+
+	for _, tt := range proxyFromEnvTests {
+		os.Setenv("ALL_PROXY", tt.allProxyEnv)
+		os.Setenv("NO_PROXY", tt.noProxyEnv)
+		ResetCachedEnvironment()
+
+		d := FromEnvironment()
+		if got, want := fmt.Sprintf("%T", d), fmt.Sprintf("%T", tt.wantTypeOf); got != want {
+			t.Errorf("%v: got type = %T, want %T", tt, d, tt.wantTypeOf)
+		}
+	}
+}
+
 func TestFromURL(t *testing.T) {
 	endSystem, err := net.Listen("tcp", "127.0.0.1:0")
 	if err != nil {
@@ -140,3 +199,17 @@
 		return
 	}
 }
+
+func ResetProxyEnv() {
+	for _, env := range []*envOnce{allProxyEnv, noProxyEnv} {
+		for _, v := range env.names {
+			os.Setenv(v, "")
+		}
+	}
+	ResetCachedEnvironment()
+}
+
+func ResetCachedEnvironment() {
+	allProxyEnv.reset()
+	noProxyEnv.reset()
+}