package main import ( "fmt" "io" "net" "net/http" "net/netip" "net/url" "strings" "sync" "github.com/elazarl/goproxy" "golang.zx2c4.com/wireguard/device" net_proxy "golang.org/x/net/proxy" "gopkg.in/olebedev/go-duktape.v3" ) const ( // Imported from Firefox' ProxyAutoConfig.cpp // https://searchfox.org/mozilla-central/source/netwerk/base/ProxyAutoConfig.cpp // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. ASCII_PAC_UTILS = "function dnsDomainIs(host, domain) {\n" + " return (host.length >= domain.length &&\n" + " host.substring(host.length - domain.length) == domain);\n" + "}\n" + "" + "function dnsDomainLevels(host) {\n" + " return host.split('.').length - 1;\n" + "}\n" + "" + "function isValidIpAddress(ipchars) {\n" + " var matches = " + "/^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$/.exec(ipchars);\n" + " if (matches == null) {\n" + " return false;\n" + " } else if (matches[1] > 255 || matches[2] > 255 || \n" + " matches[3] > 255 || matches[4] > 255) {\n" + " return false;\n" + " }\n" + " return true;\n" + "}\n" + "" + "function convert_addr(ipchars) {\n" + " var bytes = ipchars.split('.');\n" + " var result = ((bytes[0] & 0xff) << 24) |\n" + " ((bytes[1] & 0xff) << 16) |\n" + " ((bytes[2] & 0xff) << 8) |\n" + " (bytes[3] & 0xff);\n" + " return result;\n" + "}\n" + "" + "function isInNet(ipaddr, pattern, maskstr) {\n" + " if (!isValidIpAddress(pattern) || !isValidIpAddress(maskstr)) {\n" + " return false;\n" + " }\n" + " if (!isValidIpAddress(ipaddr)) {\n" + " ipaddr = dnsResolve(ipaddr);\n" + " if (ipaddr == null) {\n" + " return false;\n" + " }\n" + " }\n" + " var host = convert_addr(ipaddr);\n" + " var pat = convert_addr(pattern);\n" + " var mask = convert_addr(maskstr);\n" + " return ((host & mask) == (pat & mask));\n" + " \n" + "}\n" + "" + "function isPlainHostName(host) {\n" + " return (host.search('(\\\\.)|:') == -1);\n" + "}\n" + "" + "function isResolvable(host) {\n" + " var ip = dnsResolve(host);\n" + " return (ip != null);\n" + "}\n" + "" + "function localHostOrDomainIs(host, hostdom) {\n" + " return (host == hostdom) ||\n" + " (hostdom.lastIndexOf(host + '.', 0) == 0);\n" + "}\n" + "" + "function shExpMatch(url, pattern) {\n" + " pattern = pattern.replace(/\\./g, '\\\\.');\n" + " pattern = pattern.replace(/\\*/g, '.*');\n" + " pattern = pattern.replace(/\\?/g, '.');\n" + " var newRe = new RegExp('^'+pattern+'$');\n" + " return newRe.test(url);\n" + "}\n" + "" + "var wdays = {SUN: 0, MON: 1, TUE: 2, WED: 3, THU: 4, FRI: 5, SAT: 6};\n" + "var months = {JAN: 0, FEB: 1, MAR: 2, APR: 3, MAY: 4, JUN: 5, JUL: 6, " + "AUG: 7, SEP: 8, OCT: 9, NOV: 10, DEC: 11};\n" + "" + "function weekdayRange() {\n" + " function getDay(weekday) {\n" + " if (weekday in wdays) {\n" + " return wdays[weekday];\n" + " }\n" + " return -1;\n" + " }\n" + " var date = new Date();\n" + " var argc = arguments.length;\n" + " var wday;\n" + " if (argc < 1)\n" + " return false;\n" + " if (arguments[argc - 1] == 'GMT') {\n" + " argc--;\n" + " wday = date.getUTCDay();\n" + " } else {\n" + " wday = date.getDay();\n" + " }\n" + " var wd1 = getDay(arguments[0]);\n" + " var wd2 = (argc == 2) ? getDay(arguments[1]) : wd1;\n" + " return (wd1 == -1 || wd2 == -1) ? false\n" + " : (wd1 <= wd2) ? (wd1 <= wday && wday " + "<= wd2)\n" + " : (wd2 >= wday || wday " + ">= wd1);\n" + "}\n" + "" + "function dateRange() {\n" + " function getMonth(name) {\n" + " if (name in months) {\n" + " return months[name];\n" + " }\n" + " return -1;\n" + " }\n" + " var date = new Date();\n" + " var argc = arguments.length;\n" + " if (argc < 1) {\n" + " return false;\n" + " }\n" + " var isGMT = (arguments[argc - 1] == 'GMT');\n" + "\n" + " if (isGMT) {\n" + " argc--;\n" + " }\n" + " // function will work even without explict handling of this case\n" + " if (argc == 1) {\n" + " var tmp = parseInt(arguments[0]);\n" + " if (isNaN(tmp)) {\n" + " return ((isGMT ? date.getUTCMonth() : date.getMonth()) ==\n" + " getMonth(arguments[0]));\n" + " } else if (tmp < 32) {\n" + " return ((isGMT ? date.getUTCDate() : date.getDate()) == " + "tmp);\n" + " } else { \n" + " return ((isGMT ? date.getUTCFullYear() : date.getFullYear()) " + "==\n" + " tmp);\n" + " }\n" + " }\n" + " var year = date.getFullYear();\n" + " var date1, date2;\n" + " date1 = new Date(year, 0, 1, 0, 0, 0);\n" + " date2 = new Date(year, 11, 31, 23, 59, 59);\n" + " var adjustMonth = false;\n" + " for (var i = 0; i < (argc >> 1); i++) {\n" + " var tmp = parseInt(arguments[i]);\n" + " if (isNaN(tmp)) {\n" + " var mon = getMonth(arguments[i]);\n" + " date1.setMonth(mon);\n" + " } else if (tmp < 32) {\n" + " adjustMonth = (argc <= 2);\n" + " date1.setDate(tmp);\n" + " } else {\n" + " date1.setFullYear(tmp);\n" + " }\n" + " }\n" + " for (var i = (argc >> 1); i < argc; i++) {\n" + " var tmp = parseInt(arguments[i]);\n" + " if (isNaN(tmp)) {\n" + " var mon = getMonth(arguments[i]);\n" + " date2.setMonth(mon);\n" + " } else if (tmp < 32) {\n" + " date2.setDate(tmp);\n" + " } else {\n" + " date2.setFullYear(tmp);\n" + " }\n" + " }\n" + " if (adjustMonth) {\n" + " date1.setMonth(date.getMonth());\n" + " date2.setMonth(date.getMonth());\n" + " }\n" + " if (isGMT) {\n" + " var tmp = date;\n" + " tmp.setFullYear(date.getUTCFullYear());\n" + " tmp.setMonth(date.getUTCMonth());\n" + " tmp.setDate(date.getUTCDate());\n" + " tmp.setHours(date.getUTCHours());\n" + " tmp.setMinutes(date.getUTCMinutes());\n" + " tmp.setSeconds(date.getUTCSeconds());\n" + " date = tmp;\n" + " }\n" + " return (date1 <= date2) ? (date1 <= date) && (date <= date2)\n" + " : (date2 >= date) || (date >= date1);\n" + "}\n" + "" + "function timeRange() {\n" + " var argc = arguments.length;\n" + " var date = new Date();\n" + " var isGMT= false;\n" + "" + " if (argc < 1) {\n" + " return false;\n" + " }\n" + " if (arguments[argc - 1] == 'GMT') {\n" + " isGMT = true;\n" + " argc--;\n" + " }\n" + "\n" + " var hour = isGMT ? date.getUTCHours() : date.getHours();\n" + " var date1, date2;\n" + " date1 = new Date();\n" + " date2 = new Date();\n" + "\n" + " if (argc == 1) {\n" + " return (hour == arguments[0]);\n" + " } else if (argc == 2) {\n" + " return ((arguments[0] <= hour) && (hour <= arguments[1]));\n" + " } else {\n" + " switch (argc) {\n" + " case 6:\n" + " date1.setSeconds(arguments[2]);\n" + " date2.setSeconds(arguments[5]);\n" + " case 4:\n" + " var middle = argc >> 1;\n" + " date1.setHours(arguments[0]);\n" + " date1.setMinutes(arguments[1]);\n" + " date2.setHours(arguments[middle]);\n" + " date2.setMinutes(arguments[middle + 1]);\n" + " if (middle == 2) {\n" + " date2.setSeconds(59);\n" + " }\n" + " break;\n" + " default:\n" + " throw 'timeRange: bad number of arguments'\n" + " }\n" + " }\n" + "\n" + " if (isGMT) {\n" + " date.setFullYear(date.getUTCFullYear());\n" + " date.setMonth(date.getUTCMonth());\n" + " date.setDate(date.getUTCDate());\n" + " date.setHours(date.getUTCHours());\n" + " date.setMinutes(date.getUTCMinutes());\n" + " date.setSeconds(date.getUTCSeconds());\n" + " }\n" + " return (date1 <= date2) ? (date1 <= date) && (date <= date2)\n" + " : (date2 >= date) || (date >= date1);\n" + "\n" + "}\n" ) type HttpProxy struct { listener net.Listener tlsListener net.Listener logger *device.Logger addrPort netip.AddrPort tlsAddrPort netip.AddrPort ctx *duktape.Context defaultProxy *goproxy.ProxyHttpServer uidRequest chan UidRequest handlers []*HttpHandler } func NewHttpProxy(uidRequest chan UidRequest, logger *device.Logger) *HttpProxy { logger.Verbosef("NewHttpProxy") return &HttpProxy{ listener: nil, logger: logger, uidRequest: uidRequest, handlers: make([]*HttpHandler, 0, 2), } } var ( ASCII_PAC_UTILS_NAMES = []string{"dnsDomainIs", "dnsDomainLevels", "isValidIpAddress", "convert_addr", "isInNet", "isPlainHostName", "isResolvable", "localHostOrDomainIs", "shExpMatch", "weekdayRange", "dateRange", "timeRange"} ) func (p *HttpProxy) GetAddrPort() netip.AddrPort { return p.addrPort } type Logger struct { logger *device.Logger } func (l *Logger) Printf(format string, v ...interface{}) { l.logger.Verbosef(format, v...) } func (p *HttpProxy) newGoProxy(cat, proxyUrl string) (*goproxy.ProxyHttpServer, error) { proxy := goproxy.NewProxyHttpServer() proxy.Logger = &Logger{logger: p.logger} proxy.Verbose = true if cat == "SOCKS5" { socksDialer, err := net_proxy.SOCKS5("tcp", proxyUrl, nil, nil) if err != nil { return nil, err } proxy.Tr.Dial = socksDialer.Dial } proxy.NonproxyHandler = http.HandlerFunc(func (w http.ResponseWriter, req *http.Request) { if req.Host == "" { fmt.Fprintln(w, "Cannot handle requests without Host header, e.g., HTTP 1.0") return } req.URL.Scheme = "http" req.URL.Host = req.Host proxy.ServeHTTP(w, req) }) if cat == "PROXY" { proxy.Tr.Proxy = func(req *http.Request) (*url.URL, error) { return url.Parse(proxyUrl) } proxy.ConnectDial = proxy.NewConnectDialToProxy(proxyUrl) } return proxy, nil } func FindProxyForURL(ctx *duktape.Context, url, host string, logger *device.Logger) (res string, err error) { if !ctx.GetGlobalString("FindProxyForURL") { ctx.Pop() return "", fmt.Errorf("FindProxyForURL not found") } ctx.PushString(url) ctx.PushString(host) res = "DIRECT" logger.Verbosef("Before pcall") r := ctx.Pcall(2) logger.Verbosef("After pcall") if r == 0 { res = ctx.GetString(-1) } else if ctx.IsError(-1) { ctx.GetPropString(-1, "stack") err = fmt.Errorf("Error: %v", ctx.SafeToString(-1)) } else { err = fmt.Errorf("Error: %v", ctx.SafeToString(-1)) } ctx.Pop() return } func FindProxyForPkg(ctx *duktape.Context, pkg string, logger *device.Logger) (res string, err error) { if !ctx.GetGlobalString("FindProxyForPkg") { ctx.Pop() return "", fmt.Errorf("FindProxyForPkg not found") } ctx.PushString(pkg) res = "DIRECT" logger.Verbosef("Before pcall") r := ctx.Pcall(1) logger.Verbosef("After pcall") if r == 0 { res = ctx.GetString(-1) } else if ctx.IsError(-1) { ctx.GetPropString(-1, "stack") err = fmt.Errorf("Error: %v", ctx.SafeToString(-1)) } else { err = fmt.Errorf("Error: %v", ctx.SafeToString(-1)) } ctx.Pop() return } func newPacBodyCtx(body string, logger *device.Logger) *duktape.Context { ctx := duktape.New() ctx.PushGlobalGoFunction("dnsResolve", func(ctx *duktape.Context) int { // Check stack host := ctx.GetString(-1) ctx.Pop() ips, err := net.LookupIP(host) if err != nil { return 0 } for _, ip := range(ips) { ipStr := string(ip) if strings.Contains(ipStr, ".") { // Found IPv4 ctx.PushString(ipStr) return 1 } } // No IPv4 return 0 }) ctx.PevalString(ASCII_PAC_UTILS) logger.Verbosef("ASCII_PAC_UTILS result is: %v stack:%v", ctx.GetType(-1), ctx.GetTop()) ctx.Pop() ctx.PevalString(string(body)) logger.Verbosef("result is: %v stack:%v", ctx.GetType(-1), ctx.GetTop()) ctx.Pop() FindProxyForURL(ctx, "http://www.jabra.se/", "www.jabra.se", logger) return ctx } func (p *HttpProxy) newPacFileCtx(pacFileUrl *url.URL) (*duktape.Context, error) { var pacFileBody = "" if pacFileUrl == nil { return nil, nil } resp, err := http.Get(pacFileUrl.String()) p.logger.Verbosef("pacFile: %v, %v", resp, err) if err == nil { defer resp.Body.Close() ct, ok := resp.Header["Content-Type"] if ok && len(ct) == 1 && ct[0] == "application/x-ns-proxy-autoconfig" { body, err := io.ReadAll(resp.Body) if err == nil { pacFileBody = string(body) return newPacBodyCtx(pacFileBody, p.logger), nil } } } return nil, err } func (p *HttpProxy) SetPacFileUrl(pacFileUrl *url.URL) error { ctx, err := p.newPacFileCtx(pacFileUrl) if err != nil { return err } for _, handler := range(p.handlers) { handler.setPacCtx(ctx) } if p.ctx != nil { p.ctx.Destroy() } p.ctx = ctx p.logger.Verbosef("SetNotMetered(false) from SetPacFileUrl") p.SetNotMetered(false) return nil } func (p *HttpProxy) SetPacFileContent(pacFile string) error { ctx := newPacBodyCtx(pacFile, p.logger) for _, handler := range(p.handlers) { handler.setPacCtx(ctx) } if p.ctx != nil { p.ctx.Destroy() } p.ctx = ctx p.logger.Verbosef("SetNotMetered(false) from SetPacFileContent") p.SetNotMetered(false) return nil } func (p *HttpProxy) SetNotMetered(notMetered bool) error { p.logger.Verbosef("SetNotMetered: %v", notMetered) if p.ctx == nil { return fmt.Errorf("No ductape context") } p.ctx.PushGlobalObject() p.ctx.PushBoolean(notMetered) p.ctx.PutPropString(-2, "notMetered") p.ctx.Pop() return nil } func (p *HttpProxy) Start() (listen_port uint16, err error) { p.logger.Verbosef("HttpProxy.Start()") listen_port = 0 proxyMap := make(map[string]*goproxy.ProxyHttpServer) p.defaultProxy, err = p.newGoProxy("", "") if err != nil { return } proxyMap[""] = p.defaultProxy listen_port, handler, err := p.startRegularProxy(proxyMap) if err != nil { return } p.handlers = append(p.handlers, handler) return } func (p *HttpProxy) startRegularProxy(proxyMap map[string]*goproxy.ProxyHttpServer) (listen_port uint16, handler *HttpHandler, err error) { p.listener, err = net.Listen("tcp", "[::]:") if err != nil { return } p.addrPort, err = netip.ParseAddrPort(p.listener.Addr().String()) if err != nil { return } listen_port = p.addrPort.Port() handler = NewHttpHandler(p, p.defaultProxy, proxyMap, p.logger) go http.Serve(NewListener(p.listener, handler, p.logger), handler) return } func (p *HttpProxy) Stop() { if p.listener != nil { p.logger.Verbosef("Close: %v", p.listener) p.listener.Close() // p.listener = nil } if p.tlsListener != nil { p.logger.Verbosef("Close: %v", p.tlsListener) p.tlsListener.Close() // p.tlsListener = nil } if p.ctx != nil { p.ctx.DestroyHeap() p.ctx = nil } } type HttpHandler struct { p *HttpProxy defaultProxy *goproxy.ProxyHttpServer logger *device.Logger remoteAddrPkgMapMutex sync.RWMutex remoteAddrPkgMap map[string]*goproxy.ProxyHttpServer uidRequest chan UidRequest ctx *duktape.Context proxyMap map[string]*goproxy.ProxyHttpServer // FIXME include type in map key } func NewHttpHandler(p *HttpProxy, defaultProxy *goproxy.ProxyHttpServer, proxyMap map[string]*goproxy.ProxyHttpServer, logger *device.Logger) *HttpHandler{ h := &HttpHandler{ p: p, defaultProxy: defaultProxy, logger: logger, remoteAddrPkgMapMutex: sync.RWMutex{}, remoteAddrPkgMap: make(map[string]*goproxy.ProxyHttpServer), uidRequest: p.uidRequest, proxyMap: proxyMap, } return h } // TODO fix multi-threading func (h *HttpHandler) setPacCtx(ctx *duktape.Context) { h.ctx = ctx } func (h *HttpHandler) addConnToProxyMap(c net.Conn) error { h.logger.Verbosef("Accept: %v -> %v", c.RemoteAddr().String(), c.LocalAddr().String()) local, err := netip.ParseAddrPort(c.RemoteAddr().String()) if err != nil { return fmt.Errorf("Bad local address (%v): %v", c.RemoteAddr(), err) } // Remove IPv6 zone, NOOP on IPv4 local = netip.AddrPortFrom(local.Addr().WithZone(""), local.Port()) remote, err := netip.ParseAddrPort(c.LocalAddr().String()) if err != nil { return fmt.Errorf("Bad remote address (%v): %v", c.LocalAddr(), err) } h.logger.Verbosef("uidRequest: %v -> %v", local, remote) addrPortPair := AddrPortPair{local: local, remote: remote} retCh := make(chan string) h.uidRequest <- UidRequest{Data: addrPortPair, RetCh: retCh} select { case pkg := <-retCh: h.logger.Verbosef("uidResponse: '%v'", pkg) // TODO add NotMetered as proxy, ok := h.proxyMap[pkg] if !ok { proxy, err = h.findProxyForPkg(pkg) if err != nil { return err } } if proxy != nil { h.remoteAddrPkgMapMutex.Lock() h.remoteAddrPkgMap[c.RemoteAddr().String()] = proxy h.remoteAddrPkgMapMutex.Unlock() } } return nil } func (h *HttpHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { h.remoteAddrPkgMapMutex.Lock() proxy, ok := h.remoteAddrPkgMap[req.RemoteAddr] if ok && proxy != nil { delete(h.remoteAddrPkgMap, req.RemoteAddr) h.remoteAddrPkgMapMutex.Unlock() proxyStr := "nil" proxyUrl, err := proxy.Tr.Proxy(req) if err == nil && proxyUrl != nil { proxyStr = proxyUrl.String() } h.logger.Verbosef("ServeHTTP remote:%s proxy:%v", req.RemoteAddr, proxyStr) proxy.ServeHTTP(rw, req) } else { h.remoteAddrPkgMapMutex.Unlock() h.defaultProxy.ServeHTTP(rw, req) } } func (h *HttpHandler) findProxyForPkg(pkg string) (*goproxy.ProxyHttpServer, error) { if h.ctx == nil { return nil, fmt.Errorf("Null context") } find := func() (cat string, res string, err error) { h.logger.Verbosef("Call FindProxyForPkg %v %v", pkg) res, err = FindProxyForPkg(h.ctx, pkg, h.logger) h.logger.Verbosef("FindProxyForPkg res %v %v", res, err) if err != nil { h.logger.Verbosef("FindProxyForPkg result is: %v stack:%v", res, h.ctx.GetTop()) return "", "", err } else if res == "" { return "DIRECT", "", nil } else { values := strings.Split(strings.Trim(res, " "), ";") for _, v := range values { value := strings.Trim(v, " ") parts := strings.SplitN(value, " ", 2) if parts[0] == "PROXY" || parts[0] == "HTTP" { return "PROXY", "http://" + parts[1], nil } else if parts[0] == "HTTPS" { return "PROXY", "https://" + parts[1], nil } else if parts[0] == "DIRECT" { return parts[0], "", nil } else if parts[0] == "SOCKS" || parts[0] == "SOCKS5" { return "SOCKS5", parts[1], nil } } } return "", "", fmt.Errorf("No result") } cat, res, err := find() if err != nil { return nil, err } var proxy *goproxy.ProxyHttpServer if cat == "DIRECT" { proxy = h.defaultProxy } else { proxy, err = h.p.newGoProxy(cat, res) if err != nil { return nil, err } } h.proxyMap[pkg] = proxy return proxy, nil } type AddrPortPair struct { local netip.AddrPort remote netip.AddrPort } // Listener type Listener struct { l net.Listener handler *HttpHandler logger *device.Logger } func NewListener(listener net.Listener, handler *HttpHandler, logger *device.Logger) *Listener{ l := &Listener{ l: listener, handler: handler, logger: logger, } return l } func (l *Listener) Accept() (net.Conn, error) { c, err := l.l.Accept() if err != nil { l.logger.Verbosef("Accept failed: %v", err) return c, err } if err := l.handler.addConnToProxyMap(c); err != nil { err = fmt.Errorf("Reject connection (%v): %v", c, err) c.Close() return nil, err } return c, nil } func (l *Listener) Close() error { return l.l.Close() } func (l *Listener) Addr() net.Addr { return l.l.Addr() }