// Copyright 2017 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 httpproxy_test

import (
	"bytes"
	"errors"
	"fmt"
	"net/url"
	"os"
	"strings"
	"testing"

	"golang.org/x/net/http/httpproxy"
)

type proxyForURLTest struct {
	cfg     httpproxy.Config
	req     string // URL to fetch; blank means "http://example.com"
	want    string
	wanterr error
}

func (t proxyForURLTest) String() string {
	var buf bytes.Buffer
	space := func() {
		if buf.Len() > 0 {
			buf.WriteByte(' ')
		}
	}
	if t.cfg.HTTPProxy != "" {
		fmt.Fprintf(&buf, "http_proxy=%q", t.cfg.HTTPProxy)
	}
	if t.cfg.HTTPSProxy != "" {
		space()
		fmt.Fprintf(&buf, "https_proxy=%q", t.cfg.HTTPSProxy)
	}
	if t.cfg.NoProxy != "" {
		space()
		fmt.Fprintf(&buf, "no_proxy=%q", t.cfg.NoProxy)
	}
	req := "http://example.com"
	if t.req != "" {
		req = t.req
	}
	space()
	fmt.Fprintf(&buf, "req=%q", req)
	return strings.TrimSpace(buf.String())
}

var proxyForURLTests = []proxyForURLTest{{
	cfg: httpproxy.Config{
		HTTPProxy: "127.0.0.1:8080",
	},
	want: "http://127.0.0.1:8080",
}, {
	cfg: httpproxy.Config{
		HTTPProxy: "cache.corp.example.com:1234",
	},
	want: "http://cache.corp.example.com:1234",
}, {
	cfg: httpproxy.Config{
		HTTPProxy: "cache.corp.example.com",
	},
	want: "http://cache.corp.example.com",
}, {
	// single label domain is recognized as scheme by url.Parse
	cfg: httpproxy.Config{
		HTTPProxy: "localhost",
	},
	want: "http://localhost",
}, {
	cfg: httpproxy.Config{
		HTTPProxy: "https://cache.corp.example.com",
	},
	want: "https://cache.corp.example.com",
}, {
	cfg: httpproxy.Config{
		HTTPProxy: "http://127.0.0.1:8080",
	},
	want: "http://127.0.0.1:8080",
}, {
	cfg: httpproxy.Config{
		HTTPProxy: "https://127.0.0.1:8080",
	},
	want: "https://127.0.0.1:8080",
}, {
	cfg: httpproxy.Config{
		HTTPProxy: "socks5://127.0.0.1",
	},
	want: "socks5://127.0.0.1",
}, {
	// Preserve unknown schemes.
	cfg: httpproxy.Config{
		HTTPProxy: "foo://host",
	},
	want: "foo://host",
}, {
	// Don't use secure for http
	cfg: httpproxy.Config{
		HTTPProxy:  "http.proxy.tld",
		HTTPSProxy: "secure.proxy.tld",
	},
	req:  "http://insecure.tld/",
	want: "http://http.proxy.tld",
}, {
	// Use secure for https.
	cfg: httpproxy.Config{
		HTTPProxy:  "http.proxy.tld",
		HTTPSProxy: "secure.proxy.tld",
	},
	req:  "https://secure.tld/",
	want: "http://secure.proxy.tld",
}, {
	cfg: httpproxy.Config{
		HTTPProxy:  "http.proxy.tld",
		HTTPSProxy: "https://secure.proxy.tld",
	},
	req:  "https://secure.tld/",
	want: "https://secure.proxy.tld",
}, {
	cfg: httpproxy.Config{
		HTTPProxy: "http.proxy.tld",
	},
	req:  "https://secure.tld/",
	want: "<nil>",
}, {
	cfg: httpproxy.Config{
		HTTPProxy: "http.proxy.tld",
	},
	req:  "ftp://insecure.tld/",
	want: "<nil>",
}, {
	// Issue 16405: don't use HTTP_PROXY in a CGI environment,
	// where HTTP_PROXY can be attacker-controlled.
	cfg: httpproxy.Config{
		HTTPProxy: "http://10.1.2.3:8080",
		CGI:       true,
	},
	want:    "<nil>",
	wanterr: errors.New("refusing to use HTTP_PROXY value in CGI environment; see golang.org/s/cgihttpproxy"),
}, {
	// HTTPS proxy is still used even in CGI environment.
	// (perhaps dubious but it's the historical behaviour).
	cfg: httpproxy.Config{
		HTTPSProxy: "https://secure.proxy.tld",
		CGI:        true,
	},
	req:  "https://secure.tld/",
	want: "https://secure.proxy.tld",
}, {
	want: "<nil>",
}, {
	cfg: httpproxy.Config{
		NoProxy:   "example.com",
		HTTPProxy: "proxy",
	},
	req:  "http://example.com/",
	want: "<nil>",
}, {
	cfg: httpproxy.Config{
		NoProxy:   ".example.com",
		HTTPProxy: "proxy",
	},
	req:  "http://example.com/",
	want: "http://proxy",
}, {
	cfg: httpproxy.Config{
		NoProxy:   "ample.com",
		HTTPProxy: "proxy",
	},
	req:  "http://example.com/",
	want: "http://proxy",
}, {
	cfg: httpproxy.Config{
		NoProxy:   "example.com",
		HTTPProxy: "proxy",
	},
	req:  "http://foo.example.com/",
	want: "<nil>",
}, {
	cfg: httpproxy.Config{
		NoProxy:   ".foo.com",
		HTTPProxy: "proxy",
	},
	req:  "http://example.com/",
	want: "http://proxy",
}, {
	cfg: httpproxy.Config{
		NoProxy:   ".示例.com",
		HTTPProxy: "proxy",
	},
	req:  "http://www.示例.com",
	want: "<nil>",
}, {
	cfg: httpproxy.Config{
		NoProxy:   "xn--fsq092h.com",
		HTTPProxy: "proxy",
	},
	req:  "http://www.示例.com",
	want: "<nil>",
}, {
	cfg: httpproxy.Config{
		NoProxy:   "示例.com",
		HTTPProxy: "proxy",
	},
	req:  "http://www.xn--fsq092h.com",
	want: "<nil>",
}, {
	cfg: httpproxy.Config{
		NoProxy:   "example.com",
		HTTPProxy: "proxy",
	},
	req:  "http://[1000::%25.example.com]:123",
	want: "http://proxy",
},
}

func testProxyForURL(t *testing.T, tt proxyForURLTest) {
	t.Helper()
	reqURLStr := tt.req
	if reqURLStr == "" {
		reqURLStr = "http://example.com"
	}
	reqURL, err := url.Parse(reqURLStr)
	if err != nil {
		t.Errorf("invalid URL %q", reqURLStr)
		return
	}
	cfg := tt.cfg
	proxyForURL := cfg.ProxyFunc()
	url, err := proxyForURL(reqURL)
	if g, e := fmt.Sprintf("%v", err), fmt.Sprintf("%v", tt.wanterr); g != e {
		t.Errorf("%v: got error = %q, want %q", tt, g, e)
		return
	}
	if got := fmt.Sprintf("%s", url); got != tt.want {
		t.Errorf("%v: got URL = %q, want %q", tt, url, tt.want)
	}

	// Check that changing the Config doesn't change the results
	// of the functuon.
	cfg = httpproxy.Config{}
	url, err = proxyForURL(reqURL)
	if g, e := fmt.Sprintf("%v", err), fmt.Sprintf("%v", tt.wanterr); g != e {
		t.Errorf("(after mutating config) %v: got error = %q, want %q", tt, g, e)
		return
	}
	if got := fmt.Sprintf("%s", url); got != tt.want {
		t.Errorf("(after mutating config) %v: got URL = %q, want %q", tt, url, tt.want)
	}
}

func TestProxyForURL(t *testing.T) {
	for _, tt := range proxyForURLTests {
		testProxyForURL(t, tt)
	}
}

func TestFromEnvironment(t *testing.T) {
	os.Setenv("HTTP_PROXY", "httpproxy")
	os.Setenv("HTTPS_PROXY", "httpsproxy")
	os.Setenv("NO_PROXY", "noproxy")
	os.Setenv("REQUEST_METHOD", "")
	got := httpproxy.FromEnvironment()
	want := httpproxy.Config{
		HTTPProxy:  "httpproxy",
		HTTPSProxy: "httpsproxy",
		NoProxy:    "noproxy",
	}
	if *got != want {
		t.Errorf("unexpected proxy config, got %#v want %#v", got, want)
	}
}

func TestFromEnvironmentWithRequestMethod(t *testing.T) {
	os.Setenv("HTTP_PROXY", "httpproxy")
	os.Setenv("HTTPS_PROXY", "httpsproxy")
	os.Setenv("NO_PROXY", "noproxy")
	os.Setenv("REQUEST_METHOD", "PUT")
	got := httpproxy.FromEnvironment()
	want := httpproxy.Config{
		HTTPProxy:  "httpproxy",
		HTTPSProxy: "httpsproxy",
		NoProxy:    "noproxy",
		CGI:        true,
	}
	if *got != want {
		t.Errorf("unexpected proxy config, got %#v want %#v", got, want)
	}
}

func TestFromEnvironmentLowerCase(t *testing.T) {
	os.Setenv("http_proxy", "httpproxy")
	os.Setenv("https_proxy", "httpsproxy")
	os.Setenv("no_proxy", "noproxy")
	os.Setenv("REQUEST_METHOD", "")
	got := httpproxy.FromEnvironment()
	want := httpproxy.Config{
		HTTPProxy:  "httpproxy",
		HTTPSProxy: "httpsproxy",
		NoProxy:    "noproxy",
	}
	if *got != want {
		t.Errorf("unexpected proxy config, got %#v want %#v", got, want)
	}
}

var UseProxyTests = []struct {
	host  string
	match bool
}{
	// Never proxy localhost:
	{"localhost", false},
	{"127.0.0.1", false},
	{"127.0.0.2", false},
	{"[::1]", false},
	{"[::2]", true}, // not a loopback address

	{"192.168.1.1", false},                // matches exact IPv4
	{"192.168.1.2", true},                 // ports do not match
	{"192.168.1.3", false},                // matches exact IPv4:port
	{"192.168.1.4", true},                 // no match
	{"10.0.0.2", false},                   // matches IPv4/CIDR
	{"[2001:db8::52:0:1]", false},         // matches exact IPv6
	{"[2001:db8::52:0:2]", true},          // no match
	{"[2001:db8::52:0:3]", false},         // matches exact [IPv6]:port
	{"[2002:db8:a::123]", false},          // matches IPv6/CIDR
	{"[fe80::424b:c8be:1643:a1b6]", true}, // no match

	{"barbaz.net", true},          // does not match as .barbaz.net
	{"www.barbaz.net", false},     // does match as .barbaz.net
	{"foobar.com", false},         // does match as foobar.com
	{"www.foobar.com", false},     // match because NO_PROXY includes "foobar.com"
	{"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, wrong domain
	{"wildcard.io", true},         // does not match as *.wildcard.io
	{"nested.wildcard.io", false}, // match as *.wildcard.io
	{"awildcard.io", true},        // not a match because of '*'
}

var noProxy = "foobar.com, .barbaz.net, *.wildcard.io, 192.168.1.1, 192.168.1.2:81, 192.168.1.3:80, 10.0.0.0/30, 2001:db8::52:0:1, [2001:db8::52:0:2]:443, [2001:db8::52:0:3]:80, 2002:db8:a::45/64"

func TestUseProxy(t *testing.T) {
	cfg := &httpproxy.Config{
		NoProxy: noProxy,
	}
	for _, test := range UseProxyTests {
		if httpproxy.ExportUseProxy(cfg, test.host+":80") != test.match {
			t.Errorf("useProxy(%v) = %v, want %v", test.host, !test.match, test.match)
		}
	}
}

func TestInvalidNoProxy(t *testing.T) {
	cfg := &httpproxy.Config{
		NoProxy: ":1",
	}
	ok := httpproxy.ExportUseProxy(cfg, "example.com:80") // should not panic
	if !ok {
		t.Errorf("useProxy unexpected return; got false; want true")
	}
}

func TestAllNoProxy(t *testing.T) {
	cfg := &httpproxy.Config{
		NoProxy: "*",
	}
	for _, test := range UseProxyTests {
		if httpproxy.ExportUseProxy(cfg, test.host+":80") != false {
			t.Errorf("useProxy(%v) = true, want false", test.host)
		}
	}
}

func BenchmarkProxyForURL(b *testing.B) {
	cfg := &httpproxy.Config{
		HTTPProxy:  "http://proxy.example.org",
		HTTPSProxy: "https://proxy.example.org",
		NoProxy:    noProxy,
	}
	for _, test := range UseProxyTests {
		u, err := url.Parse("https://" + test.host + ":80")
		if err != nil {
			b.Fatalf("parsed failed: %s", test.host)
		}
		proxyFunc := cfg.ProxyFunc()
		b.Run(test.host, func(b *testing.B) {
			for n := 0; n < b.N; n++ {
				if au, e := proxyFunc(u); e != nil && test.match == (au != nil) {
					b.Errorf("useProxy(%v) = %v, want %v", test.host, !test.match, test.match)
				}
			}
		})
	}
}
