| 1 | // Package sysproxy resolves the OS-level proxy (Windows system/PAC settings) |
| 2 | // for a target URL. ForURL returns nil on platforms without system-proxy |
| 3 | // support or when no proxy applies, so callers fall back to direct/env. |
| 4 | package sysproxy |
| 5 | |
| 6 | import ( |
| 7 | "net/netip" |
| 8 | "net/url" |
| 9 | "slices" |
| 10 | "strings" |
| 11 | ) |
| 12 | |
| 13 | func splitList(s string) []string { |
| 14 | return strings.FieldsFunc(s, func(r rune) bool { |
| 15 | return r == ';' || r == ' ' || r == '\t' || r == '\n' || r == '\r' |
| 16 | }) |
| 17 | } |
| 18 | |
| 19 | // parseProxyList picks a proxy from a WinHTTP/IE proxy string for scheme. The |
| 20 | // string is either "host:port" (all protocols) or "http=h:p;https=h:p" form. |
| 21 | func parseProxyList(list, scheme string) *url.URL { |
| 22 | var fallback string |
| 23 | for _, f := range splitList(list) { |
| 24 | if before, after, ok := strings.Cut(f, "="); ok { |
| 25 | if strings.EqualFold(before, scheme) { |
| 26 | return hostProxyURL(after) |
| 27 | } |
| 28 | continue |
| 29 | } |
| 30 | if fallback == "" { |
| 31 | fallback = f |
| 32 | } |
| 33 | } |
| 34 | if fallback != "" { |
| 35 | return hostProxyURL(fallback) |
| 36 | } |
| 37 | return nil |
| 38 | } |
| 39 | |
| 40 | func hostProxyURL(hostport string) *url.URL { |
| 41 | hostport = strings.TrimSpace(hostport) |
| 42 | if i := strings.Index(hostport, "://"); i >= 0 { |
| 43 | hostport = hostport[i+3:] |
| 44 | } |
| 45 | if hostport == "" { |
| 46 | return nil |
| 47 | } |
| 48 | return &url.URL{Scheme: "http", Host: hostport} |
| 49 | } |
| 50 | |
| 51 | // bypassed reports whether host matches a WinINET proxy-bypass entry. "<local>" |
| 52 | // matches dotless (intranet) hosts, and "*" is a wildcard anywhere in an entry |
| 53 | // ("127.*", "*.corp.local"). Loopback is bypassed unless "<-loopback>" is listed, |
| 54 | // as WinINET does. |
| 55 | func bypassed(host, bypass string) bool { |
| 56 | host = strings.ToLower(strings.TrimSpace(host)) |
| 57 | if host == "" { |
| 58 | return false |
| 59 | } |
| 60 | entries := splitList(strings.ToLower(bypass)) |
| 61 | if isLoopback(host) && !slices.Contains(entries, "<-loopback>") { |
| 62 | return true |
| 63 | } |
| 64 | for _, e := range entries { |
| 65 | if e == "<local>" { |
| 66 | if !strings.Contains(host, ".") { |
| 67 | return true |
| 68 | } |
| 69 | continue |
| 70 | } |
| 71 | if wildcardMatch(strings.Trim(e, "[]"), host) { |
| 72 | return true |
| 73 | } |
| 74 | } |
| 75 | return false |
| 76 | } |
| 77 | |
| 78 | func isLoopback(host string) bool { |
| 79 | if host == "localhost" { |
| 80 | return true |
| 81 | } |
| 82 | ip, err := netip.ParseAddr(host) |
| 83 | return err == nil && ip.IsLoopback() |
| 84 | } |
| 85 | |
| 86 | // wildcardMatch reports whether s matches pattern, where "*" matches any run of |
| 87 | // characters and every other character matches itself. |
| 88 | func wildcardMatch(pattern, s string) bool { |
| 89 | parts := strings.Split(pattern, "*") |
| 90 | if len(parts) == 1 { |
| 91 | return pattern == s |
| 92 | } |
| 93 | if !strings.HasPrefix(s, parts[0]) { |
| 94 | return false |
| 95 | } |
| 96 | s = s[len(parts[0]):] |
| 97 | last := parts[len(parts)-1] |
| 98 | for _, part := range parts[1 : len(parts)-1] { |
| 99 | i := strings.Index(s, part) |
| 100 | if i < 0 { |
| 101 | return false |
| 102 | } |
| 103 | s = s[i+len(part):] |
| 104 | } |
| 105 | return len(s) >= len(last) && strings.HasSuffix(s, last) |
| 106 | } |
| 107 |