safehttp
API
safehttp
packageAPI reference for the safehttp
package.
Imports
(9)
F
function
NewClient
NewClient returns an HTTP client restricted to public network addresses.
Returns
internal/safehttp/safehttp.go:43-59
func NewClient() *http.Client
{
dialer := &net.Dialer{Timeout: 10 * time.Second, KeepAlive: 30 * time.Second}
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.Proxy = nil
transport.DialContext = publicDialContext(dialer, net.DefaultResolver)
return &http.Client{
Transport: transport,
Timeout: requestTimeout,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if len(via) >= 10 {
return fmt.Errorf("stopped after %d redirects", len(via))
}
return ValidateURL(req.URL)
},
}
}
F
function
NewRequest
NewRequest creates an HTTP request after checking its URL policy.
Parameters
Returns
error
internal/safehttp/safehttp.go:62-71
func NewRequest(ctx context.Context, method, rawURL string) (*http.Request, error)
{
parsed, err := url.ParseRequestURI(rawURL)
if err != nil {
return nil, fmt.Errorf("invalid URL %q: %w", rawURL, err)
}
if err := ValidateURL(parsed); err != nil {
return nil, err
}
return http.NewRequestWithContext(ctx, method, parsed.String(), nil)
}
F
function
ValidateURL
ValidateURL accepts absolute HTTP and HTTPS URLs without embedded
credentials.
Parameters
parsed
Returns
error
internal/safehttp/safehttp.go:75-86
func ValidateURL(parsed *url.URL) error
{
if parsed == nil || !parsed.IsAbs() || parsed.Host == "" || parsed.Hostname() == "" {
return fmt.Errorf("URL must be absolute")
}
if parsed.Scheme != "http" && parsed.Scheme != "https" {
return fmt.Errorf("unsupported URL scheme %q", parsed.Scheme)
}
if parsed.User != nil {
return fmt.Errorf("URL credentials are not allowed")
}
return nil
}
F
function
publicDialContext
Parameters
dialer
resolver
Returns
func(context.Context,
string, string) (net.Conn, error)
internal/safehttp/safehttp.go:88-116
func publicDialContext(dialer *net.Dialer, resolver *net.Resolver) func(context.Context, string, string) (net.Conn, error)
{
return func(ctx context.Context, network, address string) (net.Conn, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, fmt.Errorf("invalid network address %q: %w", address, err)
}
resolved, err := resolver.LookupNetIP(ctx, "ip", host)
if err != nil {
return nil, fmt.Errorf("resolve %q: %w", host, err)
}
if len(resolved) == 0 {
return nil, fmt.Errorf("resolve %q: no addresses", host)
}
var lastErr error
for _, addressIP := range resolved {
if !isPublic(addressIP) {
return nil, fmt.Errorf("network address %q is not public", addressIP)
}
conn, dialErr := dialer.DialContext(ctx, network, net.JoinHostPort(addressIP.String(), port))
if dialErr == nil {
return conn, nil
}
lastErr = dialErr
}
return nil, fmt.Errorf("connect to %q: %w", host, lastErr)
}
}
F
function
isPublic
Parameters
address
Returns
bool
internal/safehttp/safehttp.go:118-129
func isPublic(address netip.Addr) bool
{
if !address.IsValid() {
return false
}
address = address.Unmap()
for _, network := range blockedNetworks {
if network.Contains(address) {
return false
}
}
return address.IsGlobalUnicast()
}
F
function
TestIsPublic
Parameters
t
internal/safehttp/safehttp_test.go:14-32
func TestIsPublic(t *testing.T)
{
tests := map[string]bool{
"8.8.8.8": true,
"2606:4700:4700::1111": true,
"127.0.0.1": false,
"10.0.0.1": false,
"100.64.0.1": false,
"169.254.169.254": false,
"192.168.1.1": false,
"::1": false,
"fd00::1": false,
"fe80::1": false,
}
for rawAddress, want := range tests {
if got := isPublic(netip.MustParseAddr(rawAddress)); got != want {
t.Errorf("isPublic(%q) = %t, want %t", rawAddress, got, want)
}
}
}
F
function
TestNewRequestRejectsUnsafeURLs
Parameters
t
internal/safehttp/safehttp_test.go:34-44
func TestNewRequestRejectsUnsafeURLs(t *testing.T)
{
for _, rawURL := range []string{
"/relative",
"file:///tmp/secret",
"http://user:[email protected]",
} {
if _, err := NewRequest(context.Background(), "GET", rawURL); err == nil {
t.Errorf("expected %q to be rejected", rawURL)
}
}
}
F
function
TestPublicDialRejectsLoopback
Parameters
t
internal/safehttp/safehttp_test.go:46-57
func TestPublicDialRejectsLoopback(t *testing.T)
{
dial := publicDialContext(
&net.Dialer{Timeout: time.Second},
net.DefaultResolver,
)
if conn, err := dial(context.Background(), "tcp", "127.0.0.1:80"); err == nil {
_ = conn.Close()
t.Fatal("expected loopback address to be rejected")
} else if !strings.Contains(err.Error(), "not public") {
t.Fatalf("unexpected rejection error: %v", err)
}
}