From 6af6081648724ebb7bc62448e44f2ef44159d810 Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Tue, 28 May 2019 15:56:23 -0700 Subject: [PATCH] Support PAC proxies. What a horrible wart lingering from the early days of the Internet. Note to future designers: please do not express configuration using a Turing complete language. Thanks jenriq for getting the ball rolling on this. Closes #196. --- .../threerings/getdown/data/Application.java | 3 +- launcher/pom.xml | 7 + .../getdown/launcher/ProxyUtil.java | 84 ++++++++- .../threerings/getdown/launcher/PacUtils.js | 168 ++++++++++++++++++ .../getdown/launcher/ProxyUtilTest.java | 137 ++++++++++++++ 5 files changed, 391 insertions(+), 8 deletions(-) create mode 100644 launcher/src/main/resources/com/threerings/getdown/launcher/PacUtils.js create mode 100644 launcher/src/test/java/com/threerings/getdown/launcher/ProxyUtilTest.java diff --git a/core/src/main/java/com/threerings/getdown/data/Application.java b/core/src/main/java/com/threerings/getdown/data/Application.java index 432c2b9..aba0ece 100644 --- a/core/src/main/java/com/threerings/getdown/data/Application.java +++ b/core/src/main/java/com/threerings/getdown/data/Application.java @@ -822,8 +822,7 @@ public class Application * Returns a URL from which the specified path can be fetched. Our application base URL is * properly versioned and combined with the supplied path. */ - public URL getRemoteURL (String path) - throws MalformedURLException + public URL getRemoteURL (String path) throws MalformedURLException { return new URL(_vappbase, encodePath(path)); } diff --git a/launcher/pom.xml b/launcher/pom.xml index 8b32172..3b14aba 100644 --- a/launcher/pom.xml +++ b/launcher/pom.xml @@ -38,6 +38,12 @@ 1.0 true + + junit + junit + 4.12 + test + @@ -189,6 +195,7 @@ ${java.home}/jmods/java.base.jmod ${java.home}/jmods/java.desktop.jmod ${java.home}/jmods/java.logging.jmod + ${java.home}/jmods/java.scripting.jmod ${java.home}/jmods/jdk.jsobject.jmod diff --git a/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java b/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java index a36b5fa..43535f0 100644 --- a/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java +++ b/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java @@ -8,17 +8,27 @@ package com.threerings.getdown.launcher; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStreamReader; import java.io.PrintStream; +import java.io.Reader; import java.net.Authenticator; import java.net.HttpURLConnection; +import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.PasswordAuthentication; import java.net.Proxy; import java.net.URL; import java.net.URLConnection; +import java.net.UnknownHostException; import java.util.Iterator; import java.util.ServiceLoader; +import javax.script.Bindings; +import javax.script.Invocable; +import javax.script.ScriptContext; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; + import ca.beq.util.win32.registry.RegistryKey; import ca.beq.util.win32.registry.RegistryValue; import ca.beq.util.win32.registry.RootKey; @@ -61,21 +71,35 @@ public class ProxyUtil { enabled = value.getStringValue().equals("1"); } if (value.getName().equals("ProxyServer")) { - String strval = value.getStringValue(); - int cidx = strval.indexOf(":"); - if (cidx != -1) { - rport = strval.substring(cidx+1); - strval = strval.substring(0, cidx); + String[] hostPort = splitHostPort(value.getStringValue()); + rhost = hostPort[0]; + rport = hostPort[1]; + } + if (value.getName().equals("AutoConfigURL")) { + String acurl = value.getStringValue(); + Reader acjs = new InputStreamReader(new URL(acurl).openStream()); + // technically we should be returning all this info and trying each proxy + // in succession, but that's complexity we'll leave for another day + for (String proxy : findPACProxiesForURL(acjs, app.getRemoteURL(""))) { + if (proxy.startsWith("PROXY ")) { + String[] hostPort = splitHostPort(proxy.substring(6)); + rhost = hostPort[0]; + rport = hostPort[1]; + // TODO: is this valid? Does AutoConfigURL imply proxy enabled? + enabled = true; + break; + } } - rhost = strval; } } + if (enabled) { host = rhost; port = rport; } else { log.info("Detected no proxy settings in the registry."); } + } catch (Throwable t) { log.info("Failed to find proxy settings in Windows registry", "error", t); } @@ -205,6 +229,54 @@ public class ProxyUtil { } } + public static class Resolver { + public String dnsResolve (String host) { + try { + return InetAddress.getByName(host).toString(); + } catch (UnknownHostException uhe) { + return null; + } + } + } + + public static String[] findPACProxiesForURL (Reader pac, URL url) { + ScriptEngineManager manager = new ScriptEngineManager(); + ScriptEngine engine = manager.getEngineByName("javascript"); + Bindings globals = engine.createBindings(); + globals.put("resolver", new Resolver()); + engine.setBindings(globals, ScriptContext.GLOBAL_SCOPE); + try { + URL utils = ProxyUtil.class.getResource("PacUtils.js"); + if (utils == null) { + log.error("Unable to load PacUtils.js"); + return new String[0]; + } + engine.eval(new InputStreamReader(utils.openStream())); + Object res = engine.eval(pac); + if (engine instanceof Invocable) { + Object[] args = new Object[] { url.toString(), url.getHost() }; + res = ((Invocable) engine).invokeFunction("FindProxyForURL", args); + } + String[] proxies = res.toString().split(";"); + for (int ii = 0; ii < proxies.length; ii += 1) { + proxies[ii] = proxies[ii].trim(); + } + return proxies; + } catch (Exception e) { + log.warning("Failed to resolve PAC proxy", e); + } + return new String[0]; + } + + private static String[] splitHostPort (String hostPort) { + int cidx = hostPort.indexOf(":"); + if (cidx == -1) { + return new String[] { hostPort, null}; + } else { + return new String[] { hostPort.substring(0, cidx), hostPort.substring(cidx+1) }; + } + } + protected static final String PROXY_REGISTRY = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings"; } diff --git a/launcher/src/main/resources/com/threerings/getdown/launcher/PacUtils.js b/launcher/src/main/resources/com/threerings/getdown/launcher/PacUtils.js new file mode 100644 index 0000000..96a644e --- /dev/null +++ b/launcher/src/main/resources/com/threerings/getdown/launcher/PacUtils.js @@ -0,0 +1,168 @@ +/** + * Extracted from Mozilla Firefox 3.0.4, \mozilla\netwerk\base\src\nsProxyAutoConfig.js + */ +function dnsDomainIs (host, domain) { + return (host.length >= domain.length && + host.substring(host.length - domain.length) == domain); +} + +function dnsDomainLevels (host) { + return host.split('.').length-1; +} + +function convert_addr (ipchars) { + var bytes = ipchars.split('.'); + var result = (((bytes[0] & 0xff) << 24) | + ((bytes[1] & 0xff) << 16) | + ((bytes[2] & 0xff) << 8) | + ( bytes[3] & 0xff )); + return result; +} + +function dnsResolve (host) { + return resolver.dnsResolve(host) +} + +function isInNet (addrOrHost, pattern, maskstr) { + var testRE = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/; + var test = testRE.exec(addrOrHost); + if (test == null) { + addrOrHost = dnsResolve(addrOrHost); + if (addrOrHost == null) { + return false; + } + } else if (test[1] > 255 || test[2] > 255 || test[3] > 255 || test[4] > 255) { + return false; // not an IP address + } + var host = convert_addr(addrOrHost); + var pat = convert_addr(pattern); + var mask = convert_addr(maskstr); + return ((host & mask) == (pat & mask)); +} + +function isPlainHostName (host) { + return (host.search('\\.') == -1); +} + +function isResolvable (host) { + var ip = dnsResolve(host); + return (ip != null); +} + +function localHostOrDomainIs (host, hostdom) { + return (host == hostdom) || (hostdom.lastIndexOf(host + '.', 0) == 0); +} + +function shExpMatch (url, pattern) { + pattern = pattern.replace(/\./g, '\\.'); + pattern = pattern.replace(/\*/g, '.*'); + pattern = pattern.replace(/\?/g, '.'); + var newRe = new RegExp('^'+pattern+'$'); + return newRe.test(url); +} + +var wdays = { + SUN: 0, MON: 1, TUE: 2, WED: 3, THU: 4, FRI: 5, SAT: 6 +}; + +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 +}; + +function weekdayRange () { + function getDay (weekday) { + if (weekday in wdays) { + return wdays[weekday]; + } + return -1; + } + var date = new Date(); + var argc = arguments.length; + var wday; + if (argc < 1) { + return false; + } + if (arguments[argc - 1] == 'GMT') { + argc--; + wday = date.getUTCDay(); + } else { + wday = date.getDay(); + } + var wd1 = getDay(arguments[0]); + var wd2 = (argc == 2) ? getDay(arguments[1]) : wd1; + return (wd1 == -1 || wd2 == -1) ? false : (wd1 <= wday && wday <= wd2); +} + +function dateRange () { + function getMonth (name) { + if (name in months) { + return months[name]; + } + return -1; + } + var date = new Date(); + var argc = arguments.length; + if (argc < 1) { + return false; + } + var isGMT = (arguments[argc - 1] == 'GMT'); + if (isGMT) { + argc--; + } + + // function will work even without explict handling of this case + if (argc == 1) { + var tmp = parseInt(arguments[0]); + if (isNaN(tmp)) { + return ((isGMT ? date.getUTCMonth() : date.getMonth()) == getMonth(arguments[0])); + } else if (tmp < 32) { + return ((isGMT ? date.getUTCDate() : date.getDate()) == tmp); + } else { + return ((isGMT ? date.getUTCFullYear() : date.getFullYear()) == tmp); + } + } + + var year = date.getFullYear(); + var date1, date2; + date1 = new Date(year, 0, 1, 0, 0, 0); + date2 = new Date(year, 11, 31, 23, 59, 59); + var adjustMonth = false; + for (var i = 0; i < (argc >> 1); i++) { + var tmp = parseInt(arguments[i]); + if (isNaN(tmp)) { + var mon = getMonth(arguments[i]); + date1.setMonth(mon); + } else if (tmp < 32) { + adjustMonth = (argc <= 2); + date1.setDate(tmp); + } else { + date1.setFullYear(tmp); + } + } + for (var i = (argc >> 1); i < argc; i++) { + var tmp = parseInt(arguments[i]); + if (isNaN(tmp)) { + var mon = getMonth(arguments[i]); + date2.setMonth(mon); + } else if(tmp < 32) { + date2.setDate(tmp); + } else { + date2.setFullYear(tmp); + } + } + if (adjustMonth) { + date1.setMonth(date.getMonth()); + date2.setMonth(date.getMonth()); + } + if (isGMT) { + var tmp = date; + tmp.setFullYear(date.getUTCFullYear()); + tmp.setMonth(date.getUTCMonth()); + tmp.setDate(date.getUTCDate()); + tmp.setHours(date.getUTCHours()); + tmp.setMinutes(date.getUTCMinutes()); + tmp.setSeconds(date.getUTCSeconds()); + date = tmp; + } + return ((date1 <= date) && (date <= date2)); +} diff --git a/launcher/src/test/java/com/threerings/getdown/launcher/ProxyUtilTest.java b/launcher/src/test/java/com/threerings/getdown/launcher/ProxyUtilTest.java new file mode 100644 index 0000000..e60ab90 --- /dev/null +++ b/launcher/src/test/java/com/threerings/getdown/launcher/ProxyUtilTest.java @@ -0,0 +1,137 @@ +// +// Getdown - application installer, patcher and launcher +// Copyright (C) 2004-2018 Getdown authors +// https://github.com/threerings/getdown/blob/master/LICENSE + +package com.threerings.getdown.launcher; + +import java.io.StringReader; +import java.net.URL; + +import org.junit.Test; +import static org.junit.Assert.*; + +public class ProxyUtilTest { + + static String[] strs (String... strs) { return strs; } + + static String[] findPAC (String code, String url) throws Exception { + return ProxyUtil.findPACProxiesForURL(new StringReader(code), new URL(url)); + } + + static void testPAC (String code, String url, String... expectedProxies) throws Exception { + assertArrayEquals(expectedProxies, findPAC(code, url)); + } + + @Test public void testPACProxy () throws Exception { + String EXAMPLE0 = + "function FindProxyForURL(url, host) {\n" + + " if (shExpMatch(host, '*.example.com')) { return 'DIRECT'; }\n" + + " if (isInNet(host, '10.0.0.0', '255.255.248.0')) {\n" + + " return 'PROXY fastproxy.example.com:8080';\n" + + " }\n" + + " return 'PROXY proxy.example.com:8080; DIRECT';\n" + + "}\n"; + + testPAC(EXAMPLE0, "http://test.example.com/", "DIRECT"); + testPAC(EXAMPLE0, "http://10.0.1.1/", "PROXY fastproxy.example.com:8080"); + testPAC(EXAMPLE0, "http://chicken.gov/", "PROXY proxy.example.com:8080", "DIRECT"); + + String EXAMPLE1 = + "function FindProxyForURL(url, host) {" + + " if (isPlainHostName(host) || dnsDomainIs(host, '.mozilla.org')) {" + + " return 'DIRECT';" + + " } else {" + + " return 'PROXY w3proxy.mozilla.org:8080; DIRECT';" + + " }" + + "}"; + testPAC(EXAMPLE1, "http://test.example.com/", "PROXY w3proxy.mozilla.org:8080", "DIRECT"); + testPAC(EXAMPLE1, "http://www.mozilla.org/", "DIRECT"); + testPAC(EXAMPLE1, "http://foo.mozilla.org/", "DIRECT"); + testPAC(EXAMPLE1, "http://localhost/", "DIRECT"); + + String EXAMPLE2 = + " function FindProxyForURL(url, host) {\n" + + " if ((isPlainHostName(host) || dnsDomainIs(host, '.mozilla.org')) &&\n" + + " !localHostOrDomainIs(host, 'www.mozilla.org') &&\n" + + " !localHostOrDomainIs(host, 'merchant.mozilla.org')) {\n" + + " return 'DIRECT';\n" + + " } else {\n" + + " return 'PROXY w3proxy.mozilla.org:8080; DIRECT';\n" + + " }\n" + + " }"; + testPAC(EXAMPLE2, "http://test.example.com/", "PROXY w3proxy.mozilla.org:8080", "DIRECT"); + testPAC(EXAMPLE2, "http://www.mozilla.org/", "PROXY w3proxy.mozilla.org:8080", "DIRECT"); + testPAC(EXAMPLE2, "http://www/", "PROXY w3proxy.mozilla.org:8080", "DIRECT"); + testPAC(EXAMPLE2, "http://foo.mozilla.org/", "DIRECT"); + testPAC(EXAMPLE2, "http://localhost/", "DIRECT"); + + String EXAMPLE3A = + "function FindProxyForURL(url, host) {\n" + + " if (isResolvable(host)) return 'DIRECT';\n" + + " else return 'PROXY proxy.mydomain.com:8080';\n" + + "}"; + testPAC(EXAMPLE3A, "http://www.mozilla.org/", "DIRECT"); + testPAC(EXAMPLE3A, "http://doesnotexist.mozilla.org/", "PROXY proxy.mydomain.com:8080"); + + String EXAMPLE3B = + "function FindProxyForURL(url, host) {\n" + + " if (isPlainHostName(host) ||\n" + + " dnsDomainIs(host, '.mydomain.com') ||\n" + + " isResolvable(host)) {\n" + + " return 'DIRECT';\n" + + " } else {\n" + + " return 'PROXY proxy.mydomain.com:8080';\n" + + " }\n" + + "}"; + testPAC(EXAMPLE3B, "http://plain/", "DIRECT"); + testPAC(EXAMPLE3B, "http://foo.mydomain.com/", "DIRECT"); + testPAC(EXAMPLE3B, "http://www.mozilla.org/", "DIRECT"); + testPAC(EXAMPLE3B, "http://doesnotexist.mozilla.org/", "PROXY proxy.mydomain.com:8080"); + + // example 4 + // function FindProxyForURL(url, host) { + // if (isInNet(host, '198.95.0.0', '255.255.0.0')) return 'DIRECT'; + // else return 'PROXY proxy.mydomain.com:8080'; + // } + // function FindProxyForURL(url, host) { + // if (isPlainHostName(host) || + // dnsDomainIs(host, '.mydomain.com') || + // isInNet(host, '198.95.0.0', '255.255.0.0')) { + // return 'DIRECT'; + // } else { + // return 'PROXY proxy.mydomain.com:8080'; + // } + // } + + // example 5 + // function FindProxyForURL(url, host) { + // if (isPlainHostName(host) || dnsDomainIs(host, '.mydomain.com')) return 'DIRECT'; + // else if (shExpMatch(host, '*.com')) return 'PROXY proxy1.mydomain.com:8080; ' + + // 'PROXY proxy4.mydomain.com:8080'; + // else if (shExpMatch(host, '*.edu')) return 'PROXY proxy2.mydomain.com:8080; ' + + // 'PROXY proxy4.mydomain.com:8080'; + // else return 'PROXY proxy3.mydomain.com:8080; ' + + // 'PROXY proxy4.mydomain.com:8080'; + // } + + // example 6 + // function FindProxyForURL(url, host) { + // if (url.substring(0, 5) == 'http:') { + // return 'PROXY http-proxy.mydomain.com:8080'; + // } + // else if (url.substring(0, 4) == 'ftp:') { + // return 'PROXY ftp-proxy.mydomain.com:8080'; + // } + // else if (url.substring(0, 7) == 'gopher:') { + // return 'PROXY gopher-proxy.mydomain.com:8080'; + // } + // else if (url.substring(0, 6) == 'https:' || + // url.substring(0, 6) == 'snews:') { + // return 'PROXY security-proxy.mydomain.com:8080'; + // } else { + // return 'DIRECT'; + // } + // } + } +}