This repository has no description
1package hostutil
2
3import (
4 "fmt"
5 "net"
6 "net/http"
7 "net/url"
8 "syscall"
9 "time"
10)
11
12// isBlockedIP reports whether ip is loopback, private, link-local (incl. the
13// 169.254.169.254 metadata endpoint), multicast, or unspecified.
14func isBlockedIP(ip net.IP) bool {
15 return ip.IsLoopback() ||
16 ip.IsPrivate() ||
17 ip.IsLinkLocalUnicast() ||
18 ip.IsLinkLocalMulticast() ||
19 ip.IsMulticast() ||
20 ip.IsUnspecified()
21}
22
23// safeDialer rejects dials to non-public addresses. the Control hook runs after
24// dns resolution, so it also covers rebinding and redirects. disabled in dev.
25func safeDialer(dev bool) *net.Dialer {
26 d := &net.Dialer{
27 Timeout: 10 * time.Second,
28 KeepAlive: 30 * time.Second,
29 }
30 if dev {
31 return d
32 }
33 d.Control = func(_, address string, _ syscall.RawConn) error {
34 host, _, err := net.SplitHostPort(address)
35 if err != nil {
36 return fmt.Errorf("invalid dial address %q: %w", address, err)
37 }
38 ip := net.ParseIP(host)
39 if ip == nil {
40 return fmt.Errorf("dial address %q did not resolve to an IP", address)
41 }
42 if isBlockedIP(ip) {
43 return fmt.Errorf("refusing to dial %s: reserved or private address", ip)
44 }
45 return nil
46 }
47 return d
48}
49
50// ValidateExternalURL checks raw is a well-formed http(s) url and rejects
51// ip-literal hosts in blocked ranges; dns hosts are re-checked at dial time.
52func ValidateExternalURL(raw string, dev bool) error {
53 u, err := url.Parse(raw)
54 if err != nil {
55 return fmt.Errorf("invalid URL: %w", err)
56 }
57 if u.Scheme != "http" && u.Scheme != "https" {
58 return fmt.Errorf("URL must use http or https")
59 }
60 if u.Hostname() == "" {
61 return fmt.Errorf("URL must include a host")
62 }
63 if dev {
64 return nil
65 }
66 if ip := net.ParseIP(u.Hostname()); ip != nil && isBlockedIP(ip) {
67 return fmt.Errorf("URL host is a reserved or private address")
68 }
69 return nil
70}
71
72// SafeClient returns an http.Client for fetching untrusted urls (e.g.
73// webhooks): it blocks internal address ranges and won't follow redirects.
74func SafeClient(dev bool, timeout time.Duration) *http.Client {
75 return &http.Client{
76 Timeout: timeout,
77 Transport: &http.Transport{
78 DialContext: safeDialer(dev).DialContext,
79 },
80 CheckRedirect: func(*http.Request, []*http.Request) error {
81 return http.ErrUseLastResponse
82 },
83 }
84}