mirror of
https://github.com/hanzoai/team.git
synced 2026-09-30 01:57:35 +00:00
* Validate Azure storage account name format For the commercial and government clouds the storage account name becomes part of the service hostname, so enforce Azure's documented format (3 to 24 lowercase letters and digits) both in config validation and when building the service URL. Also treat an empty AzureCloud as commercial, consistent with the URL builder. * Route Azure custom endpoints through httpservice The Azure custom-cloud backend now issues requests through httpservice's transport rather than the SDK default, so they honor ServiceSettings.AllowedUntrustedInternalConnections like other outbound connections. The commercial and government clouds keep the SDK default transport, which Private Link deployments rely on. The allowlist value is plumbed into FileBackendSettings, and the host/IP checks are factored into a shared httpservice helper that MakeTransport now uses too.
417 lines
12 KiB
Go
417 lines
12 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package httpservice
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestHTTPClient(t *testing.T) {
|
|
mockHTTP := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer mockHTTP.Close()
|
|
|
|
mockSelfSignedHTTPS := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer mockSelfSignedHTTPS.Close()
|
|
|
|
t.Run("insecure connections", func(t *testing.T) {
|
|
disableInsecureConnections := false
|
|
enableInsecureConnections := true
|
|
|
|
testCases := []struct {
|
|
description string
|
|
enableInsecureConnections bool
|
|
url string
|
|
expectedAllowed bool
|
|
}{
|
|
{"allow HTTP even when insecure disabled", disableInsecureConnections, mockHTTP.URL, true},
|
|
{"allow HTTP when insecure enabled", enableInsecureConnections, mockHTTP.URL, true},
|
|
{"reject self-signed HTTPS even when insecure disabled", disableInsecureConnections, mockSelfSignedHTTPS.URL, false},
|
|
{"allow self-signed HTTPS when insecure enabled", enableInsecureConnections, mockSelfSignedHTTPS.URL, true},
|
|
}
|
|
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.description, func(t *testing.T) {
|
|
c := NewHTTPClient(NewTransport(testCase.enableInsecureConnections, nil, nil))
|
|
if _, err := c.Get(testCase.url); testCase.expectedAllowed {
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.Error(t, err)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("checks", func(t *testing.T) {
|
|
allowHost := func(_ string) bool { return true }
|
|
rejectHost := func(_ string) bool { return false }
|
|
allowIP := func(_ net.IP) error { return nil }
|
|
rejectIP := func(_ net.IP) error { return fmt.Errorf("IP not allowed") }
|
|
|
|
testCases := []struct {
|
|
description string
|
|
allowHost func(string) bool
|
|
allowIP func(net.IP) error
|
|
expectedAllowed bool
|
|
}{
|
|
{"allow with no checks", nil, nil, true},
|
|
{"reject without host check when ip rejected", nil, rejectIP, false},
|
|
{"allow without host check when ip allowed", nil, allowIP, true},
|
|
|
|
{"reject when host rejected since no ip check", rejectHost, nil, false},
|
|
{"reject when host and ip rejected", rejectHost, rejectIP, false},
|
|
{"allow when host rejected since ip allowed", rejectHost, allowIP, true},
|
|
|
|
{"allow when host allowed even without ip check", allowHost, nil, true},
|
|
{"allow when host allowed even if ip rejected", allowHost, rejectIP, true},
|
|
{"allow when host and ip allowed", allowHost, allowIP, true},
|
|
}
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.description, func(t *testing.T) {
|
|
c := NewHTTPClient(NewTransport(false, testCase.allowHost, testCase.allowIP))
|
|
if _, err := c.Get(mockHTTP.URL); testCase.expectedAllowed {
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.IsType(t, &url.Error{}, err)
|
|
require.Contains(t, err.(*url.Error).Err.Error(), "address forbidden")
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestNewTransportForInternalConnections(t *testing.T) {
|
|
// httptest servers listen on a loopback address, which is a reserved range.
|
|
mockHTTP := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer mockHTTP.Close()
|
|
|
|
u, err := url.Parse(mockHTTP.URL)
|
|
require.NoError(t, err)
|
|
host := u.Hostname()
|
|
|
|
t.Run("blocks reserved IP ranges when the allowlist is empty", func(t *testing.T) {
|
|
c := NewHTTPClient(NewTransportForInternalConnections(false, ""))
|
|
_, err := c.Get(mockHTTP.URL)
|
|
require.IsType(t, &url.Error{}, err)
|
|
require.Contains(t, err.(*url.Error).Err.Error(), "address forbidden")
|
|
})
|
|
|
|
t.Run("allows a host named in the allowlist", func(t *testing.T) {
|
|
c := NewHTTPClient(NewTransportForInternalConnections(false, host))
|
|
resp, err := c.Get(mockHTTP.URL)
|
|
require.NoError(t, err)
|
|
resp.Body.Close()
|
|
})
|
|
|
|
t.Run("allows a CIDR range named in the allowlist", func(t *testing.T) {
|
|
c := NewHTTPClient(NewTransportForInternalConnections(false, "127.0.0.0/8 ::1/128"))
|
|
resp, err := c.Get(mockHTTP.URL)
|
|
require.NoError(t, err)
|
|
resp.Body.Close()
|
|
})
|
|
}
|
|
|
|
func TestHTTPClientWithProxy(t *testing.T) {
|
|
proxy := createProxyServer()
|
|
defer proxy.Close()
|
|
|
|
c := NewHTTPClient(NewTransport(true, nil, nil))
|
|
purl, _ := url.Parse(proxy.URL)
|
|
c.Transport.(*MattermostTransport).Transport.(*http.Transport).Proxy = http.ProxyURL(purl)
|
|
|
|
resp, err := c.Get("http://acme.com")
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "proxy", string(body))
|
|
|
|
t.Run("invalid IPv6 address in proxied URL", func(t *testing.T) {
|
|
c := NewHTTPClient(NewTransport(true, nil, nil))
|
|
|
|
t.Setenv("HTTP_PROXY", "http://proxy.example.org")
|
|
t.Setenv("HTTPS_PROXY", "https://proxy.example.org")
|
|
t.Setenv("NO_PROXY", ".example.com")
|
|
|
|
_, err := c.Get("http://[fe80::8e87:5021:6f6c:605e%25eth0]")
|
|
require.EqualError(t, err, `Get "http://[fe80::8e87:5021:6f6c:605e%25eth0]": invalid IPv6 address in URL: "fe80::8e87:5021:6f6c:605e%eth0"`)
|
|
})
|
|
}
|
|
|
|
func createProxyServer() *httptest.Server {
|
|
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
w.Header().Set("Content-Type", "text/plain; charset=us-ascii")
|
|
fmt.Fprint(w, "proxy")
|
|
}))
|
|
}
|
|
|
|
func TestDialContextFilter(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
Addr string
|
|
IsValid bool
|
|
}{
|
|
{
|
|
Addr: "google.com:80",
|
|
IsValid: true,
|
|
},
|
|
{
|
|
Addr: "8.8.8.8:53",
|
|
IsValid: true,
|
|
},
|
|
{
|
|
Addr: "127.0.0.1:80",
|
|
},
|
|
{
|
|
Addr: "10.0.0.1:80",
|
|
IsValid: true,
|
|
},
|
|
} {
|
|
didDial := false
|
|
filter := dialContextFilter(func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
didDial = true
|
|
return nil, nil
|
|
}, func(host string) bool { return host == "10.0.0.1" }, func(ip net.IP) error {
|
|
if IsReservedIP(ip) {
|
|
return fmt.Errorf("IP %s is reserved", ip)
|
|
}
|
|
return nil
|
|
})
|
|
_, err := filter(context.Background(), "", tc.Addr)
|
|
|
|
if tc.IsValid {
|
|
require.NoError(t, err)
|
|
require.True(t, didDial)
|
|
} else {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "address forbidden")
|
|
require.False(t, didDial)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUserAgentIsSet(t *testing.T) {
|
|
testUserAgent := "test-user-agent"
|
|
defaultUserAgent = testUserAgent
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
ua := req.UserAgent()
|
|
assert.NotEqual(t, "", ua, "expected user-agent to be non-empty")
|
|
assert.Equalf(t, testUserAgent, ua, "expected user-agent to be %q but was %q", testUserAgent, ua)
|
|
}))
|
|
defer ts.Close()
|
|
client := NewHTTPClient(NewTransport(true, nil, nil))
|
|
req, err := http.NewRequest("GET", ts.URL, nil)
|
|
|
|
require.NoError(t, err, "NewRequest failed", err)
|
|
|
|
_, err = client.Do(req)
|
|
require.NoError(t, err, "Do failed", err)
|
|
}
|
|
|
|
func NewHTTPClient(transport http.RoundTripper) *http.Client {
|
|
return &http.Client{
|
|
Transport: transport,
|
|
}
|
|
}
|
|
|
|
func TestIsReservedIP(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
ip net.IP
|
|
want bool
|
|
}{
|
|
{"127.8.3.5", net.IPv4(127, 8, 3, 5), true},
|
|
{"192.168.0.1", net.IPv4(192, 168, 0, 1), true},
|
|
{"169.254.0.6", net.IPv4(169, 254, 0, 6), true},
|
|
{"127.120.6.3", net.IPv4(127, 120, 6, 3), true},
|
|
{"8.8.8.8", net.IPv4(8, 8, 8, 8), false},
|
|
{"9.9.9.9", net.IPv4(9, 9, 9, 8), false},
|
|
// IPv4-mapped IPv6 addresses should be detected as reserved
|
|
{"::ffff:127.0.0.1", net.ParseIP("::ffff:127.0.0.1"), true},
|
|
{"::ffff:192.168.1.1", net.ParseIP("::ffff:192.168.1.1"), true},
|
|
{"::ffff:10.0.0.1", net.ParseIP("::ffff:10.0.0.1"), true},
|
|
{"::ffff:169.254.169.254", net.ParseIP("::ffff:169.254.169.254"), true},
|
|
{"::ffff:8.8.8.8", net.ParseIP("::ffff:8.8.8.8"), false},
|
|
// Pure IPv6 reserved addresses
|
|
{"::1", net.ParseIP("::1"), true},
|
|
{"fe80::1", net.ParseIP("fe80::1"), true},
|
|
// Public IPv6
|
|
{"2607:f8b0:4004:800::200e", net.ParseIP("2607:f8b0:4004:800::200e"), false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := IsReservedIP(tt.ip)
|
|
assert.Equalf(t, tt.want, got, "IsReservedIP() = %v, want %v", got, tt.want)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsOwnIP(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
ip net.IP
|
|
want bool
|
|
}{
|
|
{"127.0.0.1", net.IPv4(127, 0, 0, 1), true},
|
|
{"8.8.8.8", net.IPv4(8, 0, 0, 8), false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := IsOwnIP(tt.ip)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSplitHostnames(t *testing.T) {
|
|
var config string
|
|
var hostnames []string
|
|
|
|
config = ""
|
|
hostnames = strings.FieldsFunc(config, splitFields)
|
|
require.Equal(t, []string{}, hostnames)
|
|
|
|
config = "127.0.0.1 localhost"
|
|
hostnames = strings.FieldsFunc(config, splitFields)
|
|
require.Equal(t, []string{"127.0.0.1", "localhost"}, hostnames)
|
|
|
|
config = "127.0.0.1,localhost"
|
|
hostnames = strings.FieldsFunc(config, splitFields)
|
|
require.Equal(t, []string{"127.0.0.1", "localhost"}, hostnames)
|
|
|
|
config = "127.0.0.1,,localhost"
|
|
hostnames = strings.FieldsFunc(config, splitFields)
|
|
require.Equal(t, []string{"127.0.0.1", "localhost"}, hostnames)
|
|
|
|
config = "127.0.0.1 localhost"
|
|
hostnames = strings.FieldsFunc(config, splitFields)
|
|
require.Equal(t, []string{"127.0.0.1", "localhost"}, hostnames)
|
|
|
|
config = "127.0.0.1 , localhost"
|
|
hostnames = strings.FieldsFunc(config, splitFields)
|
|
require.Equal(t, []string{"127.0.0.1", "localhost"}, hostnames)
|
|
|
|
config = "127.0.0.1 localhost "
|
|
hostnames = strings.FieldsFunc(config, splitFields)
|
|
require.Equal(t, []string{"127.0.0.1", "localhost"}, hostnames)
|
|
|
|
config = " 127.0.0.1 ,,localhost , , ,,"
|
|
hostnames = strings.FieldsFunc(config, splitFields)
|
|
require.Equal(t, []string{"127.0.0.1", "localhost"}, hostnames)
|
|
|
|
config = "127.0.0.1 localhost, 192.168.1.0"
|
|
hostnames = strings.FieldsFunc(config, splitFields)
|
|
require.Equal(t, []string{"127.0.0.1", "localhost", "192.168.1.0"}, hostnames)
|
|
}
|
|
|
|
func TestGetProxyFn(t *testing.T) {
|
|
t.Setenv("HTTP_PROXY", "http://proxy.example.org")
|
|
t.Setenv("HTTPS_PROXY", "https://proxy.example.org")
|
|
t.Setenv("NO_PROXY", ".example.com")
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
input string
|
|
output string
|
|
err string
|
|
}{
|
|
{
|
|
name: "empty",
|
|
},
|
|
{
|
|
name: "no proxy",
|
|
input: "http://test.example.com",
|
|
},
|
|
{
|
|
name: "localhost",
|
|
input: "http://localhost",
|
|
},
|
|
{
|
|
name: "hostname",
|
|
input: "http://example.org",
|
|
output: "http://proxy.example.org",
|
|
},
|
|
{
|
|
name: "hostname with port",
|
|
input: "http://example.org:4545",
|
|
output: "http://proxy.example.org",
|
|
},
|
|
{
|
|
name: "https",
|
|
input: "https://example.org",
|
|
output: "https://proxy.example.org",
|
|
},
|
|
{
|
|
name: "ipv4",
|
|
input: "http://10.0.0.45",
|
|
output: "http://proxy.example.org",
|
|
},
|
|
{
|
|
name: "ipv4 with port",
|
|
input: "http://10.0.0.45:4545",
|
|
output: "http://proxy.example.org",
|
|
},
|
|
{
|
|
name: "ipv6",
|
|
input: "http://[fe80::8e87:5021:6f6c:605e]",
|
|
output: "http://proxy.example.org",
|
|
},
|
|
{
|
|
name: "ipv6 with port",
|
|
input: "http://[fe80::8e87:5021:6f6c:605e]:4545",
|
|
output: "http://proxy.example.org",
|
|
},
|
|
{
|
|
name: "ipv6 with zone",
|
|
input: "http://[fe80::8e87:5021:6f6c:605e%25eth0]",
|
|
output: "http://proxy.example.org",
|
|
err: `invalid IPv6 address in URL: "fe80::8e87:5021:6f6c:605e%eth0"`,
|
|
},
|
|
{
|
|
name: "ipv6 with zone and port",
|
|
input: "http://[fe80::8e87:5021:6f6c:605e%25eth0]:4545",
|
|
output: "http://proxy.example.org",
|
|
err: `invalid IPv6 address in URL: "fe80::8e87:5021:6f6c:605e%eth0"`,
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
inURL, err := url.Parse(tc.input)
|
|
require.NoError(t, err)
|
|
outURL, err := getProxyFn()(&http.Request{
|
|
URL: inURL,
|
|
})
|
|
if tc.err != "" { // error case
|
|
require.EqualError(t, err, tc.err)
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
if tc.output == "" { // not proxied case
|
|
require.Nil(t, outURL)
|
|
} else { // proxied case
|
|
require.NotNil(t, outURL)
|
|
require.Equal(t, tc.output, outURL.String())
|
|
}
|
|
})
|
|
}
|
|
}
|