diff options
author | Mikael Magnusson <mikma@users.sourceforge.net> | 2022-03-29 23:51:21 +0200 |
---|---|---|
committer | Mikael Magnusson <mikma@users.sourceforge.net> | 2023-03-23 22:52:53 +0100 |
commit | a323baea9b1337ae69c6fb90bbd90f87873192af (patch) | |
tree | 88d230266294125111c3ea41f0c05443c01d1e3d /tunnel/tools/libwg-go/http-proxy.go | |
parent | e23eadf7e3c05b30c7aee51b422d82e38bdefcc3 (diff) |
tunnel: implement http proxy for Android 10+
Allow getting package names of tv apps.
Diffstat (limited to 'tunnel/tools/libwg-go/http-proxy.go')
-rw-r--r-- | tunnel/tools/libwg-go/http-proxy.go | 678 |
1 files changed, 678 insertions, 0 deletions
diff --git a/tunnel/tools/libwg-go/http-proxy.go b/tunnel/tools/libwg-go/http-proxy.go new file mode 100644 index 00000000..c73e1edf --- /dev/null +++ b/tunnel/tools/libwg-go/http-proxy.go @@ -0,0 +1,678 @@ +package main + +import ( + "fmt" + "io" + "net" + "net/http" + "net/netip" + "net/url" + "strings" + "sync" + + "github.com/elazarl/goproxy" + + "golang.zx2c4.com/wireguard/device" + + "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(proxyUrl string) *goproxy.ProxyHttpServer { + proxy := goproxy.NewProxyHttpServer() + proxy.Logger = &Logger{logger: p.logger} + proxy.Verbose = true + 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 proxyUrl != "" { + proxy.Tr.Proxy = func(req *http.Request) (*url.URL, error) { + return url.Parse(proxyUrl) + } + proxy.ConnectDial = proxy.NewConnectDialToProxy(proxyUrl) + } + + return proxy +} + +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 + 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 = p.newGoProxy("") + 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 +} + +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) + + proxy, ok := h.proxyMap[pkg] + if !ok { + proxy = h.findProxyForPkg(pkg) + } + + 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 { + if h.ctx == nil { + return nil + } + + 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 { + values := strings.Split(strings.Trim(res, " "), ";") + for _, v := range values { + value := strings.Trim(v, " ") + parts := strings.SplitN(value, " ", 2) + if parts[0] == "PROXY" { + return parts[0], parts[1], nil + } else if parts[0] == "DIRECT" { + return parts[0], "", nil + } + } + } + return "", "", fmt.Errorf("No result") + } + cat, res, err := find() + if err != nil { + return nil + } + + var proxy *goproxy.ProxyHttpServer + if cat == "PROXY" { + proxy = h.p.newGoProxy("http://" + res) + } else { + proxy = h.defaultProxy + } + h.proxyMap[pkg] = proxy + + return proxy +} + +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() +} |