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';
+ // }
+ // }
+ }
+}