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.
This commit is contained in:
Michael Bayne
2019-05-28 15:56:23 -07:00
parent 490420852e
commit 6af6081648
5 changed files with 391 additions and 8 deletions
@@ -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));
}
+7
View File
@@ -38,6 +38,12 @@
<version>1.0</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
@@ -189,6 +195,7 @@
<lib>${java.home}/jmods/java.base.jmod</lib>
<lib>${java.home}/jmods/java.desktop.jmod</lib>
<lib>${java.home}/jmods/java.logging.jmod</lib>
<lib>${java.home}/jmods/java.scripting.jmod</lib>
<lib>${java.home}/jmods/jdk.jsobject.jmod</lib>
</libs>
</configuration>
@@ -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";
}
@@ -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));
}
@@ -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';
// }
// }
}
}