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  }