github.com/iDigitalFlame/xmt@v0.5.4/device/proxy.go (about) 1 // Copyright (C) 2020 - 2023 iDigitalFlame 2 // 3 // This program is free software: you can redistribute it and/or modify 4 // it under the terms of the GNU General Public License as published by 5 // the Free Software Foundation, either version 3 of the License, or 6 // any later version. 7 // 8 // This program is distributed in the hope that it will be useful, 9 // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 // GNU General Public License for more details. 12 // 13 // You should have received a copy of the GNU General Public License 14 // along with this program. If not, see <https://www.gnu.org/licenses/>. 15 // 16 17 package device 18 19 import ( 20 "net" 21 "net/http" 22 "net/url" 23 "strings" 24 "sync" 25 "syscall" 26 ) 27 28 var proxySync struct { 29 f func(reqURL *url.URL) (*url.URL, error) 30 sync.Once 31 } 32 33 type config struct { 34 http, https *url.URL 35 HTTPProxy string 36 HTTPSProxy string 37 NoProxy string 38 ip, domain []matcher 39 CGI bool 40 } 41 type matchIP struct { 42 p string 43 i net.IP 44 } 45 type matchAll struct{} 46 type matchCIDR struct { 47 _ [0]func() 48 *net.IPNet 49 } 50 type matcher interface { 51 match(string, string, net.IP) bool 52 } 53 type matchDomain struct { 54 _ [0]func() 55 h string 56 p string 57 x bool 58 } 59 60 func (c *config) parse() { 61 if u, err := parse(c.HTTPProxy); err == nil { 62 c.http = u 63 } 64 if u, err := parse(c.HTTPSProxy); err == nil { 65 c.https = u 66 } 67 if len(c.NoProxy) == 0 { 68 return 69 } 70 for _, v := range strings.Split(c.NoProxy, ",") { 71 if v = strings.ToLower(strings.TrimSpace(v)); len(v) == 0 { 72 continue 73 } 74 if v == "*" { 75 c.ip, c.domain = []matcher{matchAll{}}, []matcher{matchAll{}} 76 return 77 } 78 if _, r, err := net.ParseCIDR(v); err == nil { 79 c.ip = append(c.ip, matchCIDR{IPNet: r}) 80 continue 81 } 82 h, p, err := net.SplitHostPort(v) 83 if err == nil { 84 if len(h) == 0 { 85 continue 86 } 87 if h[0] == '[' && h[len(h)-1] == ']' { 88 h = h[1 : len(h)-1] 89 } 90 } else { 91 h = v 92 } 93 if i := net.ParseIP(h); i != nil { 94 c.ip = append(c.ip, matchIP{i: i, p: p}) 95 continue 96 } 97 if len(h) == 0 { 98 continue 99 } 100 if strings.HasPrefix(h, "*.") { 101 h = h[1:] 102 } 103 if h[0] != '.' { 104 c.domain = append(c.domain, matchDomain{h: "." + h, p: p, x: true}) 105 } else { 106 c.domain = append(c.domain, matchDomain{h: h, p: p, x: false}) 107 } 108 } 109 } 110 func realAddr(u *url.URL) string { 111 var ( 112 a = u.Hostname() 113 p = u.Port() 114 ) 115 if len(p) == 0 && len(u.Scheme) >= 4 { 116 if u.Scheme[0] == 'h' || u.Scheme[0] == 'H' { 117 if len(u.Scheme) == 5 && (u.Scheme[4] == 's' || u.Scheme[4] == 'S') { 118 p = "443" 119 } else { 120 p = "80" 121 } 122 } else if u.Scheme[0] == 's' || u.Scheme[0] == 'S' { 123 p = "1080" 124 } 125 } 126 return net.JoinHostPort(a, p) 127 } 128 func (c config) usable(u string) bool { 129 if len(u) == 0 { 130 return true 131 } 132 h, p, err := net.SplitHostPort(u) 133 if err != nil { 134 return false 135 } 136 if h == "localhost" { 137 return false 138 } 139 i := net.ParseIP(h) 140 if i != nil && i.IsLoopback() { 141 return false 142 } 143 a := strings.ToLower(strings.TrimSpace(h)) 144 if i != nil { 145 for x := range c.ip { 146 if c.ip[x].match(a, p, i) { 147 return false 148 } 149 } 150 } 151 for x := range c.domain { 152 if c.domain[x].match(a, p, i) { 153 return false 154 } 155 } 156 return true 157 } 158 func parse(u string) (*url.URL, error) { 159 if len(u) == 0 { 160 return nil, nil 161 } 162 v, err := url.Parse(u) 163 if err != nil || (v.Scheme != "http" && v.Scheme != "https" && v.Scheme != "socks5") { 164 if v, err := url.Parse("http://" + u); err == nil { 165 return v, nil 166 } 167 } 168 if err != nil { 169 return nil, syscall.EINVAL 170 } 171 return v, nil 172 } 173 174 // Proxy returns the URL of the proxy to use for a given request, as 175 // indicated by the on-device settings. 176 // 177 // Unix/Linux/BSD devices use the environment variables 178 // HTTP_PROXY, HTTPS_PROXY and NO_PROXY (or the lowercase versions 179 // thereof). HTTPS_PROXY takes precedence over HTTP_PROXY for https 180 // requests. 181 // 182 // Windows devices will query the Windows API and resolve the system setting 183 // values. 184 // 185 // The environment values may be either a complete URL or a 186 // "host[:port]", in which case the "http" scheme is assumed. 187 // The schemes "http", "https", and "socks5" are supported. 188 // An error is returned if the value is a different form. 189 // 190 // A nil URL and nil error are returned if no proxy is defined in the 191 // environment, or a proxy should not be used for the given request, 192 // as defined by NO_PROXY or ProxyBypass. 193 // 194 // As a special case, if req.URL.Host is "localhost" (with or without 195 // a port number), then a nil URL and nil error will be returned. 196 // 197 // NOTE(dij): I don't have handling of "<local>" (Windows specific) bypass 198 // 199 // rules in place. I would have to re-implement "httpproxy" code 200 // and might not be worth it. 201 func Proxy(r *http.Request) (*url.URL, error) { 202 proxySync.Do(func() { 203 v := proxyInit() 204 v.parse() 205 proxySync.f = v.proxyForURL 206 }) 207 if proxySync.f == nil { 208 return nil, nil 209 } 210 return proxySync.f(r.URL) 211 } 212 func (matchAll) match(_, _ string, _ net.IP) bool { 213 return true 214 } 215 func (m matchIP) match(_, p string, i net.IP) bool { 216 if m.i.Equal(i) { 217 return len(m.p) == 0 || m.p == p 218 } 219 return false 220 } 221 func (m matchCIDR) match(_, _ string, i net.IP) bool { 222 return m.Contains(i) 223 } 224 func (m matchDomain) match(h, p string, _ net.IP) bool { 225 if strings.HasSuffix(h, m.h) || (m.x && h == m.h[1:]) { 226 return len(m.p) == 0 || m.p == p 227 } 228 return false 229 } 230 func (c config) proxyForURL(r *url.URL) (*url.URL, error) { 231 if !c.usable(realAddr(r)) { 232 return nil, nil 233 } 234 if len(r.Scheme) == 5 && (r.Scheme[4] == 's' || r.Scheme[4] == 'S') { 235 return c.https, nil 236 } 237 if c.http != nil && c.CGI { 238 return nil, syscall.EINVAL 239 } 240 return c.http, nil 241 }