summaryrefslogtreecommitdiffstats
path: root/src/main/java/com/btr/proxy/selector/pac
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/btr/proxy/selector/pac')
-rw-r--r--src/main/java/com/btr/proxy/selector/pac/JavaxPacScriptParser.java147
-rw-r--r--src/main/java/com/btr/proxy/selector/pac/PacProxySelector.java184
-rw-r--r--src/main/java/com/btr/proxy/selector/pac/PacScriptMethods.java656
-rw-r--r--src/main/java/com/btr/proxy/selector/pac/PacScriptParser.java29
-rw-r--r--src/main/java/com/btr/proxy/selector/pac/PacScriptSource.java31
-rw-r--r--src/main/java/com/btr/proxy/selector/pac/ProxyEvaluationException.java51
-rw-r--r--src/main/java/com/btr/proxy/selector/pac/RhinoPacScriptParser.java318
-rw-r--r--src/main/java/com/btr/proxy/selector/pac/ScriptAvailability.java46
-rw-r--r--src/main/java/com/btr/proxy/selector/pac/ScriptMethods.java256
-rw-r--r--src/main/java/com/btr/proxy/selector/pac/UrlPacScriptSource.java270
10 files changed, 1988 insertions, 0 deletions
diff --git a/src/main/java/com/btr/proxy/selector/pac/JavaxPacScriptParser.java b/src/main/java/com/btr/proxy/selector/pac/JavaxPacScriptParser.java
new file mode 100644
index 0000000..5d293fa
--- /dev/null
+++ b/src/main/java/com/btr/proxy/selector/pac/JavaxPacScriptParser.java
@@ -0,0 +1,147 @@
+package com.btr.proxy.selector.pac;
+
+import java.lang.reflect.Method;
+
+import javax.script.ScriptEngine;
+import javax.script.ScriptEngineManager;
+import javax.script.ScriptException;
+
+import com.btr.proxy.util.Logger;
+import com.btr.proxy.util.Logger.LogLevel;
+
+/*****************************************************************************
+ * PAC parser using the Rhino JavaScript engine bundled with Java 1.6<br/>
+ * If you need PAC support with Java 1.5 then you should have a look at
+ * RhinoPacScriptParser.
+ *
+ * More information about PAC can be found there:<br/>
+ * <a href="http://en.wikipedia.org/wiki/Proxy_auto-config">Proxy_auto-config</a><br/>
+ * <a href="http://homepages.tesco.net/~J.deBoynePollard/FGA/web-browser-auto-proxy-configuration.html">web-browser-auto-proxy-configuration</a>
+ * </p>
+ * @author Bernd Rosstauscher (proxyvole@rosstauscher.de) Copyright 2009
+ ****************************************************************************/
+public class JavaxPacScriptParser implements PacScriptParser {
+ static final String SCRIPT_METHODS_OBJECT = "__pacutil";
+
+ private final PacScriptSource source;
+ private final ScriptEngine engine;
+
+ /*************************************************************************
+ * Constructor
+ *
+ * @param source
+ * the source for the PAC script.
+ * @throws ProxyEvaluationException
+ * on error.
+ ************************************************************************/
+ public JavaxPacScriptParser(PacScriptSource source)
+ throws ProxyEvaluationException {
+ this.source = source;
+ this.engine = setupEngine();
+ }
+
+ /*************************************************************************
+ * Initializes the JavaScript engine and adds aliases for the functions
+ * defined in ScriptMethods.
+ *
+ * @throws ProxyEvaluationException
+ * on error.
+ ************************************************************************/
+ private ScriptEngine setupEngine() throws ProxyEvaluationException {
+ ScriptEngineManager mng = new ScriptEngineManager();
+ ScriptEngine engine = mng.getEngineByMimeType("text/javascript");
+ engine.put(SCRIPT_METHODS_OBJECT, new PacScriptMethods());
+
+ Class<?> scriptMethodsClazz = ScriptMethods.class;
+ Method[] scriptMethods = scriptMethodsClazz.getMethods();
+
+ for (Method method : scriptMethods) {
+ String name = method.getName();
+ int args = method.getParameterTypes().length;
+ StringBuilder toEval = new StringBuilder(name).append(" = function(");
+ for (int i = 0; i < args; i++) {
+ if (i > 0) {
+ toEval.append(",");
+ }
+ toEval.append("arg").append(i);
+ }
+ toEval.append(") {return ");
+
+ String functionCall = buildFunctionCallCode(name, args);
+
+ // If return type is java.lang.String convert it to a JS string
+ if (String.class.isAssignableFrom(method.getReturnType())) {
+ functionCall = "String("+functionCall+")";
+ }
+ toEval.append(functionCall).append("; }");
+ try {
+ engine.eval(toEval.toString());
+ } catch (ScriptException e) {
+ Logger.log(getClass(), LogLevel.ERROR,
+ "JS evaluation error when creating alias for " + name + ".", e);
+ throw new ProxyEvaluationException(
+ "Error setting up script engine", e);
+ }
+ }
+
+ return engine;
+ }
+
+ /*************************************************************************
+ * Builds a JavaScript code snippet to call a function that we bind.
+ * @param functionName of the bound function
+ * @param args of the bound function
+ * @return the JS code to invoke the method.
+ ************************************************************************/
+
+ private String buildFunctionCallCode(String functionName, int args) {
+ StringBuilder functionCall = new StringBuilder();
+ functionCall.append(SCRIPT_METHODS_OBJECT)
+ .append(".").append(functionName).append("(");
+ for (int i = 0; i < args; i++) {
+ if (i > 0) {
+ functionCall.append(",");
+ }
+ functionCall.append("arg").append(i);
+ }
+ functionCall.append(")");
+ return functionCall.toString();
+ }
+
+ /***************************************************************************
+ * Gets the source of the PAC script used by this parser.
+ *
+ * @return a PacScriptSource.
+ **************************************************************************/
+ public PacScriptSource getScriptSource() {
+ return this.source;
+ }
+
+ /*************************************************************************
+ * Evaluates the given URL and host against the PAC script.
+ *
+ * @param url
+ * the URL to evaluate.
+ * @param host
+ * the host name part of the URL.
+ * @return the script result.
+ * @throws ProxyEvaluationException
+ * on execution error.
+ ************************************************************************/
+ public String evaluate(String url, String host)
+ throws ProxyEvaluationException {
+ try {
+ StringBuilder script = new StringBuilder(
+ this.source.getScriptContent());
+ String evalMethod = " ;FindProxyForURL (\"" + url + "\",\"" + host + "\")";
+ script.append(evalMethod);
+ Object result = this.engine.eval(script.toString());
+ return (String) result;
+ } catch (Exception e) {
+ Logger.log(getClass(), LogLevel.ERROR, "JS evaluation error.", e);
+ throw new ProxyEvaluationException(
+ "Error while executing PAC script: " + e.getMessage(), e);
+ }
+
+ }
+}
diff --git a/src/main/java/com/btr/proxy/selector/pac/PacProxySelector.java b/src/main/java/com/btr/proxy/selector/pac/PacProxySelector.java
new file mode 100644
index 0000000..8e81b03
--- /dev/null
+++ b/src/main/java/com/btr/proxy/selector/pac/PacProxySelector.java
@@ -0,0 +1,184 @@
+package com.btr.proxy.selector.pac;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.ProxySelector;
+import java.net.SocketAddress;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+import com.btr.proxy.util.Logger;
+import com.btr.proxy.util.Logger.LogLevel;
+import com.btr.proxy.util.ProxyUtil;
+
+
+/*****************************************************************************
+ * ProxySelector that will use a PAC script to find an proxy for a given URI.
+ *
+ * @author Bernd Rosstauscher (proxyvole@rosstauscher.de) Copyright 2009
+ ****************************************************************************/
+public class PacProxySelector extends ProxySelector {
+
+ private final boolean JAVAX_PARSER = ScriptAvailability.isJavaxScriptingAvailable();
+
+ // private static final String PAC_PROXY = "PROXY";
+ private static final String PAC_SOCKS = "SOCKS";
+ private static final String PAC_DIRECT = "DIRECT";
+
+ private PacScriptParser pacScriptParser;
+
+ private static volatile boolean enabled = true;
+
+ /*************************************************************************
+ * Constructor
+ * @param pacSource the source for the PAC file.
+ ************************************************************************/
+
+ public PacProxySelector(PacScriptSource pacSource) {
+ super();
+ selectEngine(pacSource);
+ }
+
+ /*************************************************************************
+ * Can be used to enable / disable the proxy selector.
+ * If disabled it will return DIRECT for all urls.
+ * @param enable the new status to set.
+ ************************************************************************/
+
+ public static void setEnabled(boolean enable) {
+ enabled = enable;
+ }
+
+ /*************************************************************************
+ * Checks if the selector is currently enabled.
+ * @return true if enabled else false.
+ ************************************************************************/
+
+ public static boolean isEnabled() {
+ return enabled;
+ }
+
+ /*************************************************************************
+ * Selects one of the available PAC parser engines.
+ * @param pacSource to use as input.
+ ************************************************************************/
+
+ private void selectEngine(PacScriptSource pacSource) {
+ try {
+ if (this.JAVAX_PARSER) {
+ Logger.log(getClass(), LogLevel.INFO,
+ "Using javax.script JavaScript engine.");
+ this.pacScriptParser = new JavaxPacScriptParser(pacSource);
+ } else {
+ Logger.log(getClass(), LogLevel.INFO,
+ "Using Rhino JavaScript engine.");
+ this.pacScriptParser = new RhinoPacScriptParser(pacSource);
+ }
+ } catch (Exception e) {
+ Logger.log(getClass(), LogLevel.ERROR, "PAC parser error.", e);
+ }
+ }
+
+ /*************************************************************************
+ * connectFailed
+ * @see java.net.ProxySelector#connectFailed(java.net.URI, java.net.SocketAddress, java.io.IOException)
+ ************************************************************************/
+ @Override
+ public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
+ // Not used.
+ }
+
+ /*************************************************************************
+ * select
+ * @see java.net.ProxySelector#select(java.net.URI)
+ ************************************************************************/
+ @Override
+ public List<Proxy> select(URI uri) {
+ if (uri == null) {
+ throw new IllegalArgumentException("URI must not be null.");
+ }
+
+ // Fix for Java 1.6.16+ where we get a infinite loop because
+ // URL.connect(Proxy.NO_PROXY) does not work as expected.
+ if (!enabled) {
+ return ProxyUtil.noProxyList();
+ }
+
+ return findProxy(uri);
+ }
+
+ /*************************************************************************
+ * Evaluation of the given URL with the PAC-file.
+ *
+ * Two cases can be handled here:
+ * DIRECT Fetch the object directly from the content HTTP server denoted by its URL
+ * PROXY name:port Fetch the object via the proxy HTTP server at the given location (name and port)
+ *
+ * @param uri <code>URI</code> to be evaluated.
+ * @return <code>Proxy</code>-object list as result of the evaluation.
+ ************************************************************************/
+
+ private List<Proxy> findProxy(URI uri) {
+ try {
+ List<Proxy> proxies = new ArrayList<Proxy>();
+ String parseResult = this.pacScriptParser.evaluate(uri.toString(),
+ uri.getHost());
+ String[] proxyDefinitions = parseResult.split("[;]");
+ for (String proxyDef : proxyDefinitions) {
+ if (proxyDef.trim().length() > 0) {
+ Proxy proxy = buildProxyFromPacResult(proxyDef);
+ if (proxy.type() == Proxy.Type.SOCKS) {
+ if (!proxies.isEmpty() && proxies.get(0).type() == Proxy.Type.DIRECT) {
+ proxies.add(1, proxy);
+ } else
+ proxies.add(0, proxy);
+ } else {
+ proxies.add(proxy);
+ }
+ }
+ }
+ return proxies;
+ } catch (ProxyEvaluationException e) {
+ Logger.log(getClass(), LogLevel.ERROR, "PAC resolving error.", e);
+ return ProxyUtil.noProxyList();
+ }
+ }
+
+ /*************************************************************************
+ * The proxy evaluator will return a proxy string. This method will
+ * take this string and build a matching <code>Proxy</code> for it.
+ * @param pacResult the result from the PAC parser.
+ * @return a Proxy
+ ************************************************************************/
+
+ private Proxy buildProxyFromPacResult(String pacResult) {
+ if (pacResult == null || pacResult.trim().length() < 6) {
+ return Proxy.NO_PROXY;
+ }
+ String proxyDef = pacResult.trim();
+ if (proxyDef.toUpperCase().startsWith(PAC_DIRECT)) {
+ return Proxy.NO_PROXY;
+ }
+
+ // Check proxy type.
+ Proxy.Type type = Proxy.Type.HTTP;
+ if (proxyDef.toUpperCase().startsWith(PAC_SOCKS)) {
+ type = Proxy.Type.SOCKS;
+ }
+
+ String host = proxyDef.substring(6);
+ Integer port = ProxyUtil.DEFAULT_PROXY_PORT;
+
+ // Split port from host
+ int indexOfPort = host.indexOf(':');
+ if (indexOfPort != -1) {
+ port = Integer.parseInt(host.substring(indexOfPort+1).trim());
+ host = host.substring(0, indexOfPort).trim();
+ }
+
+ SocketAddress adr = InetSocketAddress.createUnresolved(host, port);
+ return new Proxy(type, adr);
+ }
+
+}
diff --git a/src/main/java/com/btr/proxy/selector/pac/PacScriptMethods.java b/src/main/java/com/btr/proxy/selector/pac/PacScriptMethods.java
new file mode 100644
index 0000000..101c267
--- /dev/null
+++ b/src/main/java/com/btr/proxy/selector/pac/PacScriptMethods.java
@@ -0,0 +1,656 @@
+package com.btr.proxy.selector.pac;
+
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.StringTokenizer;
+import java.util.TimeZone;
+
+import com.btr.proxy.util.Logger;
+import com.btr.proxy.util.Logger.LogLevel;
+
+/***************************************************************************
+ * Implementation of PAC JavaScript functions.
+ *
+ * @author Bernd Rosstauscher (proxyvole@rosstauscher.de) Copyright 2009
+ ***************************************************************************
+ */
+public class PacScriptMethods implements ScriptMethods {
+
+ public static final String OVERRIDE_LOCAL_IP = "com.btr.proxy.pac.overrideLocalIP";
+
+ private final static String GMT = "GMT";
+
+ private final static List<String> DAYS = Collections.unmodifiableList(
+ Arrays.asList("SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"));
+
+ private final static List<String> MONTH = Collections.unmodifiableList(
+ Arrays.asList("JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"));
+
+ private Calendar currentTime;
+
+ /*************************************************************************
+ * Constructor
+ ************************************************************************/
+
+ public PacScriptMethods() {
+ super();
+ }
+
+ /*************************************************************************
+ * isPlainHostName
+ * @see com.btr.proxy.selector.pac.ScriptMethods#isPlainHostName(java.lang.String)
+ ************************************************************************/
+
+ public boolean isPlainHostName(String host) {
+ return host.indexOf(".") < 0;
+ }
+
+ /*************************************************************************
+ * Tests if an URL is in a given domain.
+ *
+ * @param host
+ * is the host name from the URL.
+ * @param domain
+ * is the domain name to test the host name against.
+ * @return true if the domain of host name matches.
+ ************************************************************************/
+
+ public boolean dnsDomainIs(String host, String domain) {
+ return host.endsWith(domain);
+ }
+
+ /*************************************************************************
+ * Is true if the host name matches exactly the specified host name, or if
+ * there is no domain name part in the host name, but the unqualified host
+ * name matches.
+ *
+ * @param host
+ * the host name from the URL.
+ * @param domain
+ * fully qualified host name with domain to match against.
+ * @return true if matches else false.
+ ************************************************************************/
+
+ public boolean localHostOrDomainIs(String host, String domain) {
+ return domain.startsWith(host);
+ }
+
+ /*************************************************************************
+ * Tries to resolve the host name. Returns true if succeeds.
+ *
+ * @param host
+ * is the host name from the URL.
+ * @return true if resolvable else false.
+ ************************************************************************/
+
+ public boolean isResolvable(String host) {
+ try {
+ InetAddress.getByName(host).getHostAddress();
+ return true;
+ } catch (UnknownHostException ex) {
+ Logger.log(JavaxPacScriptParser.class, LogLevel.DEBUG,
+ "Hostname not resolveable {0}.", host);
+ }
+ return false;
+ }
+
+ /*************************************************************************
+ * Returns true if the IP address of the host matches the specified IP
+ * address pattern. Pattern and mask specification is done the same way as
+ * for SOCKS configuration.
+ *
+ * Example: isInNet(host, "198.95.0.0", "255.255.0.0") is true if the IP
+ * address of the host matches 198.95.*.*.
+ *
+ * @param host
+ * a DNS host name, or IP address. If a host name is passed, it
+ * will be resolved into an IP address by this function.
+ * @param pattern
+ * an IP address pattern in the dot-separated format.
+ * @param mask
+ * mask for the IP address pattern informing which parts of the
+ * IP address should be matched against. 0 means ignore, 255
+ * means match.
+ * @return true if it matches else false.
+ ************************************************************************/
+
+ public boolean isInNet(String host, String pattern, String mask) {
+ host = dnsResolve(host);
+ if (host == null || host.length() == 0) {
+ return false;
+ }
+ long lhost = parseIpAddressToLong(host);
+ long lpattern = parseIpAddressToLong(pattern);
+ long lmask = parseIpAddressToLong(mask);
+ return (lhost & lmask) == lpattern;
+ }
+
+ /*************************************************************************
+ * Convert a string representation of a IP to a long.
+ * @param address to convert.
+ * @return the address as long.
+ ************************************************************************/
+
+ private long parseIpAddressToLong(String address) {
+ long result = 0;
+ String[] parts = address.split("\\.");
+ long shift = 24;
+ for (String part : parts) {
+ long lpart = Long.parseLong(part);
+
+ result |= (lpart << shift);
+ shift -= 8;
+ }
+ return result;
+ }
+
+ /*************************************************************************
+ * Resolves the given DNS host name into an IP address, and returns it in
+ * the dot separated format as a string.
+ *
+ * @param host
+ * the host to resolve.
+ * @return the resolved IP, empty string if not resolvable.
+ ************************************************************************/
+
+ public String dnsResolve(String host) {
+ try {
+ return InetAddress.getByName(host).getHostAddress();
+ } catch (UnknownHostException e) {
+ Logger.log(JavaxPacScriptParser.class, LogLevel.DEBUG,
+ "DNS name not resolvable {0}.", host);
+ }
+ return "";
+ }
+
+ /*************************************************************************
+ * Returns the IP address of the host that the process is running on, as a
+ * string in the dot-separated integer format.
+ *
+ * @return an IP as string.
+ ************************************************************************/
+
+ public String myIpAddress() {
+ return getLocalAddressOfType(Inet4Address.class);
+ }
+
+ /*************************************************************************
+ * Get the current IP address of the computer.
+ * This will return the first address of the first network interface that is
+ * a "real" IP address of the given type.
+ * @param cl the type of address we are searching for.
+ * @return the address as string or "" if not found.
+ ************************************************************************/
+
+ private String getLocalAddressOfType(Class<? extends InetAddress> cl) {
+ try {
+ String overrideIP = System.getProperty(OVERRIDE_LOCAL_IP);
+ if (overrideIP != null && overrideIP.trim().length() > 0) {
+ return overrideIP.trim();
+ }
+ Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
+ while (interfaces.hasMoreElements()){
+ NetworkInterface current = interfaces.nextElement();
+ if (!current.isUp() || current.isLoopback() || current.isVirtual()) {
+ continue;
+ }
+ Enumeration<InetAddress> addresses = current.getInetAddresses();
+ while (addresses.hasMoreElements()){
+ InetAddress adr = addresses.nextElement();
+ if (cl.isInstance(adr)) {
+ Logger.log(JavaxPacScriptParser.class, LogLevel.TRACE,
+ "Local address resolved to {0}", adr);
+ return adr.getHostAddress();
+ }
+ }
+ }
+ return "";
+ } catch (IOException e) {
+ Logger.log(JavaxPacScriptParser.class, LogLevel.DEBUG,
+ "Local address not resolvable.");
+ return "";
+ }
+ }
+
+ /*************************************************************************
+ * Returns the number of DNS domain levels (number of dots) in the host
+ * name.
+ *
+ * @param host
+ * is the host name from the URL.
+ * @return number of DNS domain levels.
+ ************************************************************************/
+
+ public int dnsDomainLevels(String host) {
+ int count = 0;
+ int startPos = 0;
+ while ((startPos = host.indexOf(".", startPos + 1)) > -1) {
+ count++;
+ }
+ return count;
+ }
+
+ /*************************************************************************
+ * Returns true if the string matches the specified shell expression.
+ * Actually, currently the patterns are shell expressions, not regular
+ * expressions.
+ *
+ * @param str
+ * is any string to compare (e.g. the URL, or the host name).
+ * @param shexp
+ * is a shell expression to compare against.
+ * @return true if the string matches, else false.
+ ************************************************************************/
+
+ public boolean shExpMatch(String str, String shexp) {
+ StringTokenizer tokenizer = new StringTokenizer(shexp, "*");
+ int startPos = 0;
+ while (tokenizer.hasMoreTokens()) {
+ String token = tokenizer.nextToken();
+ // 07.05.2009 Incorrect? first token can be startsWith and last one
+ // can be endsWith
+ int temp = str.indexOf(token, startPos);
+ if (temp == -1) {
+ return false;
+ } else {
+ startPos = temp + token.length();
+ }
+ }
+ return true;
+ }
+
+ /*************************************************************************
+ * Only the first parameter is mandatory. Either the second, the third, or
+ * both may be left out. If only one parameter is present, the function
+ * yields a true value on the weekday that the parameter represents. If the
+ * string "GMT" is specified as a second parameter, times are taken to be in
+ * GMT, otherwise in local time zone. If both wd1 and wd2 are defined, the
+ * condition is true if the current weekday is in between those two
+ * weekdays. Bounds are inclusive. If the "GMT" parameter is specified,
+ * times are taken to be in GMT, otherwise the local time zone is used.
+ *
+ * @param wd1
+ * weekday 1 is one of SUN MON TUE WED THU FRI SAT
+ * @param wd2
+ * weekday 2 is one of SUN MON TUE WED THU FRI SAT
+ * @param gmt
+ * "GMT" for gmt time format else "undefined"
+ * @return true if current day matches the criteria.
+ ************************************************************************/
+
+ public boolean weekdayRange(String wd1, String wd2, String gmt) {
+ boolean useGmt = GMT.equalsIgnoreCase(wd2) || GMT.equalsIgnoreCase(gmt);
+ Calendar cal = getCurrentTime(useGmt);
+
+ int currentDay = cal.get(Calendar.DAY_OF_WEEK) - 1;
+ int from = DAYS.indexOf(wd1 == null ? null : wd1.toUpperCase());
+ int to = DAYS.indexOf(wd2 == null ? null : wd2.toUpperCase());
+ if (to == -1) {
+ to = from;
+ }
+
+ if (to < from) {
+ return currentDay >= from || currentDay <= to;
+ } else {
+ return currentDay >= from && currentDay <= to;
+ }
+ }
+
+ /*************************************************************************
+ * Sets a calendar with the current time. If this is set all date and time
+ * based methods will use this calendar to determine the current time
+ * instead of the real time. This is only be used by unit tests and is not
+ * part of the public API.
+ *
+ * @param cal
+ * a Calendar to set.
+ ************************************************************************/
+
+ public void setCurrentTime(Calendar cal) {
+ this.currentTime = cal;
+ }
+
+ /*************************************************************************
+ * Gets a calendar set to the current time. This is used by the date and
+ * time based methods.
+ *
+ * @param useGmt
+ * flag to indicate if the calendar is to be created in GMT time
+ * or local time.
+ * @return a Calendar set to the current time.
+ ************************************************************************/
+
+ private Calendar getCurrentTime(boolean useGmt) {
+ if (this.currentTime != null) { // Only used for unit tests
+ return (Calendar) this.currentTime.clone();
+ }
+ return Calendar.getInstance(useGmt ? TimeZone.getTimeZone(GMT)
+ : TimeZone.getDefault());
+ }
+
+ /*************************************************************************
+ * Only the first parameter is mandatory. All other parameters can be left
+ * out therefore the meaning of the parameters changes. The method
+ * definition shows the version with the most possible parameters filled.
+ * The real meaning of the parameters is guessed from it's value. If "from"
+ * and "to" are specified then the bounds are inclusive. If the "GMT"
+ * parameter is specified, times are taken to be in GMT, otherwise the local
+ * time zone is used.
+ *
+ * @param day1
+ * is the day of month between 1 and 31 (as an integer).
+ * @param month1
+ * one of JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC
+ * @param year1
+ * is the full year number, for example 1995 (but not 95).
+ * Integer.
+ * @param day2
+ * is the day of month between 1 and 31 (as an integer).
+ * @param month2
+ * one of JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC
+ * @param year2
+ * is the full year number, for example 1995 (but not 95).
+ * Integer.
+ * @param gmt
+ * "GMT" for gmt time format else "undefined"
+ * @return true if the current date matches the given range.
+ ************************************************************************/
+
+ public boolean dateRange(Object day1, Object month1, Object year1,
+ Object day2, Object month2, Object year2, Object gmt) {
+
+ // Guess the parameter meanings.
+ Map<String, Integer> params = new HashMap<String, Integer>();
+ parseDateParam(params, day1);
+ parseDateParam(params, month1);
+ parseDateParam(params, year1);
+ parseDateParam(params, day2);
+ parseDateParam(params, month2);
+ parseDateParam(params, year2);
+ parseDateParam(params, gmt);
+
+ // Get current date
+ boolean useGmt = params.get("gmt") != null;
+ Calendar cal = getCurrentTime(useGmt);
+ Date current = cal.getTime();
+
+ // Build the "from" date
+ if (params.get("day1") != null) {
+ cal.set(Calendar.DAY_OF_MONTH, params.get("day1"));
+ }
+ if (params.get("month1") != null) {
+ cal.set(Calendar.MONTH, params.get("month1"));
+ }
+ if (params.get("year1") != null) {
+ cal.set(Calendar.YEAR, params.get("year1"));
+ }
+ Date from = cal.getTime();
+
+ // Build the "to" date
+ Date to;
+ if (params.get("day2") != null) {
+ cal.set(Calendar.DAY_OF_MONTH, params.get("day2"));
+ }
+ if (params.get("month2") != null) {
+ cal.set(Calendar.MONTH, params.get("month2"));
+ }
+ if (params.get("year2") != null) {
+ cal.set(Calendar.YEAR, params.get("year2"));
+ }
+ to = cal.getTime();
+
+ // Need to increment to the next month?
+ if (to.before(from)) {
+ cal.add(Calendar.MONTH, +1);
+ to = cal.getTime();
+ }
+ // Need to increment to the next year?
+ if (to.before(from)) {
+ cal.add(Calendar.YEAR, +1);
+ cal.add(Calendar.MONTH, -1);
+ to = cal.getTime();
+ }
+
+ return current.compareTo(from) >= 0 && current.compareTo(to) <= 0;
+ }
+
+ /*************************************************************************
+ * Try to guess the type of the given parameter and put it into the params
+ * map.
+ *
+ * @param params
+ * a map to put the parsed parameters into.
+ * @param value
+ * to parse and specify the type for.
+ ************************************************************************/
+
+ private void parseDateParam(Map<String, Integer> params, Object value) {
+ if (value instanceof Number) {
+ int n = ((Number) value).intValue();
+ if (n <= 31) {
+ // Its a day
+ if (params.get("day1") == null) {
+ params.put("day1", n);
+ } else {
+ params.put("day2", n);
+ }
+ } else {
+ // Its a year
+ if (params.get("year1") == null) {
+ params.put("year1", n);
+ } else {
+ params.put("year2", n);
+ }
+ }
+ }
+
+ if (value instanceof String) {
+ int n = MONTH.indexOf(((String) value).toUpperCase());
+ if (n > -1) {
+ // Its a month
+ if (params.get("month1") == null) {
+ params.put("month1", n);
+ } else {
+ params.put("month2", n);
+ }
+ }
+ }
+
+ if (GMT.equalsIgnoreCase(String.valueOf(value))) {
+ params.put("gmt", 1);
+ }
+ }
+
+ /*************************************************************************
+ * Some parameters can be left out therefore the meaning of the parameters
+ * changes. The method definition shows the version with the most possible
+ * parameters filled. The real meaning of the parameters is guessed from
+ * it's value. If "from" and "to" are specified then the bounds are
+ * inclusive. If the "GMT" parameter is specified, times are taken to be in
+ * GMT, otherwise the local time zone is used.<br/>
+ *
+ * <pre>
+ * timeRange(hour)
+ * timeRange(hour1, hour2)
+ * timeRange(hour1, min1, hour2, min2)
+ * timeRange(hour1, min1, sec1, hour2, min2, sec2)
+ * timeRange(hour1, min1, sec1, hour2, min2, sec2, gmt)
+ * </pre>
+ *
+ * @param hour1
+ * is the hour from 0 to 23. (0 is midnight, 23 is 11 pm.)
+ * @param min1
+ * minutes from 0 to 59.
+ * @param sec1
+ * seconds from 0 to 59.
+ * @param hour2
+ * is the hour from 0 to 23. (0 is midnight, 23 is 11 pm.)
+ * @param min2
+ * minutes from 0 to 59.
+ * @param sec2
+ * seconds from 0 to 59.
+ * @param gmt
+ * "GMT" for gmt time format else "undefined"
+ * @return true if the current time matches the given range.
+ ************************************************************************/
+
+ public boolean timeRange(Object hour1, Object min1, Object sec1,
+ Object hour2, Object min2, Object sec2, Object gmt) {
+ boolean useGmt = GMT.equalsIgnoreCase(String.valueOf(min1))
+ || GMT.equalsIgnoreCase(String.valueOf(sec1))
+ || GMT.equalsIgnoreCase(String.valueOf(min2))
+ || GMT.equalsIgnoreCase(String.valueOf(gmt));
+
+ Calendar cal = getCurrentTime(useGmt);
+ cal.set(Calendar.MILLISECOND, 0);
+ Date current = cal.getTime();
+ Date from;
+ Date to;
+ if (sec2 instanceof Number) {
+ cal.set(Calendar.HOUR_OF_DAY, ((Number) hour1).intValue());
+ cal.set(Calendar.MINUTE, ((Number) min1).intValue());
+ cal.set(Calendar.SECOND, ((Number) sec1).intValue());
+ from = cal.getTime();
+
+ cal.set(Calendar.HOUR_OF_DAY, ((Number) hour2).intValue());
+ cal.set(Calendar.MINUTE, ((Number) min2).intValue());
+ cal.set(Calendar.SECOND, ((Number) sec2).intValue());
+ to = cal.getTime();
+ } else if (hour2 instanceof Number) {
+ cal.set(Calendar.HOUR_OF_DAY, ((Number) hour1).intValue());
+ cal.set(Calendar.MINUTE, ((Number) min1).intValue());
+ cal.set(Calendar.SECOND, 0);
+ from = cal.getTime();
+
+ cal.set(Calendar.HOUR_OF_DAY, ((Number) sec1).intValue());
+ cal.set(Calendar.MINUTE, ((Number) hour2).intValue());
+ cal.set(Calendar.SECOND, 59);
+ to = cal.getTime();
+ } else if (min1 instanceof Number) {
+ cal.set(Calendar.HOUR_OF_DAY, ((Number) hour1).intValue());
+ cal.set(Calendar.MINUTE, 0);
+ cal.set(Calendar.SECOND, 0);
+ from = cal.getTime();
+
+ cal.set(Calendar.HOUR_OF_DAY, ((Number) min1).intValue());
+ cal.set(Calendar.MINUTE, 59);
+ cal.set(Calendar.SECOND, 59);
+ to = cal.getTime();
+ } else {
+ cal.set(Calendar.HOUR_OF_DAY, ((Number) hour1).intValue());
+ cal.set(Calendar.MINUTE, 0);
+ cal.set(Calendar.SECOND, 0);
+ from = cal.getTime();
+
+ cal.set(Calendar.HOUR_OF_DAY, ((Number) hour1).intValue());
+ cal.set(Calendar.MINUTE, 59);
+ cal.set(Calendar.SECOND, 59);
+ to = cal.getTime();
+ }
+
+ if (to.before(from)) {
+ cal.setTime(to);
+ cal.add(Calendar.DATE, +1);
+ to = cal.getTime();
+ }
+
+ return current.compareTo(from) >= 0 && current.compareTo(to) <= 0;
+ }
+
+ // Microsoft PAC extensions for IPv6 support.
+
+ /*************************************************************************
+ * isResolvableEx
+ * @see com.btr.proxy.selector.pac.ScriptMethods#isResolvableEx(java.lang.String)
+ ************************************************************************/
+
+ public boolean isResolvableEx(String host) {
+ return isResolvable(host);
+ }
+
+ /*************************************************************************
+ * isInNetEx
+ * @see com.btr.proxy.selector.pac.ScriptMethods#isInNetEx(java.lang.String, java.lang.String)
+ ************************************************************************/
+
+ public boolean isInNetEx(String ipAddress, String ipPrefix) {
+ // TODO rossi 27.06.2011 Auto-generated method stub
+ return false;
+ }
+
+ /*************************************************************************
+ * dnsResolveEx
+ * @see com.btr.proxy.selector.pac.ScriptMethods#dnsResolveEx(java.lang.String)
+ ************************************************************************/
+
+ public String dnsResolveEx(String host) {
+ StringBuilder result = new StringBuilder();
+ try {
+ InetAddress[] list = InetAddress.getAllByName(host);
+ for (InetAddress inetAddress : list) {
+ result.append(inetAddress.getHostAddress());
+ result.append("; ");
+ }
+ } catch (UnknownHostException e) {
+ Logger.log(JavaxPacScriptParser.class, LogLevel.DEBUG,
+ "DNS name not resolvable {0}.", host);
+ }
+ return result.toString();
+ }
+
+ /*************************************************************************
+ * myIpAddressEx
+ * @see com.btr.proxy.selector.pac.ScriptMethods#myIpAddressEx()
+ ************************************************************************/
+
+ public String myIpAddressEx() {
+ return getLocalAddressOfType(Inet6Address.class);
+ }
+
+ /*************************************************************************
+ * sortIpAddressList
+ * @see com.btr.proxy.selector.pac.ScriptMethods#sortIpAddressList(java.lang.String)
+ ************************************************************************/
+
+ public String sortIpAddressList(String ipAddressList) {
+ if (ipAddressList == null || ipAddressList.trim().length() == 0) {
+ return "";
+ }
+ String[] ipAddressToken = ipAddressList.split(";");
+ List<InetAddress> parsedAddresses = new ArrayList<InetAddress>();
+ for (String ip : ipAddressToken) {
+ try {
+ parsedAddresses.add(InetAddress.getByName(ip));
+ } catch (UnknownHostException e) {
+ // TODO rossi 01.11.2011 Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+ Collections.sort(parsedAddresses, null);
+ // TODO rossi 27.06.2011 Implement me.
+ return ipAddressList;
+ }
+
+ /*************************************************************************
+ * getClientVersion
+ * @see com.btr.proxy.selector.pac.ScriptMethods#getClientVersion()
+ ************************************************************************/
+
+ public String getClientVersion() {
+ return "1.0";
+ }
+
+}
diff --git a/src/main/java/com/btr/proxy/selector/pac/PacScriptParser.java b/src/main/java/com/btr/proxy/selector/pac/PacScriptParser.java
new file mode 100644
index 0000000..5e7fd73
--- /dev/null
+++ b/src/main/java/com/btr/proxy/selector/pac/PacScriptParser.java
@@ -0,0 +1,29 @@
+package com.btr.proxy.selector.pac;
+
+/***************************************************************************
+ * Common interface for PAC script parsers.
+ *
+ * @author Bernd Rosstauscher (proxyvole@rosstauscher.de) Copyright 2009
+ ***************************************************************************/
+public interface PacScriptParser {
+
+ /***************************************************************************
+ * Gets the source of the PAC script used by this parser.
+ *
+ * @return a PacScriptSource.
+ **************************************************************************/
+ public PacScriptSource getScriptSource();
+
+ /*************************************************************************
+ * Evaluates the given URL and host against the PAC script.
+ *
+ * @param url
+ * the URL to evaluate.
+ * @param host
+ * the host name part of the URL.
+ * @return the script result.
+ * @throws ProxyEvaluationException
+ * on execution error.
+ ************************************************************************/
+ public String evaluate(String url, String host) throws ProxyEvaluationException;
+}
diff --git a/src/main/java/com/btr/proxy/selector/pac/PacScriptSource.java b/src/main/java/com/btr/proxy/selector/pac/PacScriptSource.java
new file mode 100644
index 0000000..05e00b6
--- /dev/null
+++ b/src/main/java/com/btr/proxy/selector/pac/PacScriptSource.java
@@ -0,0 +1,31 @@
+package com.btr.proxy.selector.pac;
+
+import java.io.IOException;
+
+/*****************************************************************************
+ * An source to fetch the PAC script from.
+ *
+ * @author Bernd Rosstauscher (proxyvole@rosstauscher.de) Copyright 2009
+ ****************************************************************************/
+
+public interface PacScriptSource {
+
+ /*************************************************************************
+ * Gets the PAC script content as String.
+ * @return a script.
+ * @throws IOException on read error.
+ ************************************************************************/
+
+ public String getScriptContent() throws IOException;
+
+ /*************************************************************************
+ * Checks if the content of the script is valid and if it is possible
+ * to use this script source for a PAC selector.
+ * Note that this might trigger a download of the script content from
+ * a remote location.
+ * @return true if everything is fine, else false.
+ ************************************************************************/
+
+ public boolean isScriptValid();
+
+} \ No newline at end of file
diff --git a/src/main/java/com/btr/proxy/selector/pac/ProxyEvaluationException.java b/src/main/java/com/btr/proxy/selector/pac/ProxyEvaluationException.java
new file mode 100644
index 0000000..25fd977
--- /dev/null
+++ b/src/main/java/com/btr/proxy/selector/pac/ProxyEvaluationException.java
@@ -0,0 +1,51 @@
+package com.btr.proxy.selector.pac;
+
+import com.btr.proxy.util.ProxyException;
+
+/*****************************************************************************
+ * Exception for PAC script errors.
+ *
+ * @author Bernd Rosstauscher (proxyvole@rosstauscher.de) Copyright 2009
+ ****************************************************************************/
+
+public class ProxyEvaluationException extends ProxyException {
+
+ private static final long serialVersionUID = 1L;
+
+ /*************************************************************************
+ * Constructor
+ ************************************************************************/
+
+ public ProxyEvaluationException() {
+ super();
+ }
+
+ /*************************************************************************
+ * Constructor
+ * @param message the error message.
+ * @param cause the causing exception for exception chaining.
+ ************************************************************************/
+
+ public ProxyEvaluationException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ /*************************************************************************
+ * Constructor
+ * @param message the error message.
+ ************************************************************************/
+
+ public ProxyEvaluationException(String message) {
+ super(message);
+ }
+
+ /*************************************************************************
+ * Constructor
+ * @param cause the causing exception for exception chaining.
+ ************************************************************************/
+
+ public ProxyEvaluationException(Throwable cause) {
+ super(cause);
+ }
+
+}
diff --git a/src/main/java/com/btr/proxy/selector/pac/RhinoPacScriptParser.java b/src/main/java/com/btr/proxy/selector/pac/RhinoPacScriptParser.java
new file mode 100644
index 0000000..f6ff6e2
--- /dev/null
+++ b/src/main/java/com/btr/proxy/selector/pac/RhinoPacScriptParser.java
@@ -0,0 +1,318 @@
+package com.btr.proxy.selector.pac;
+
+import java.util.Calendar;
+import org.mozilla.javascript.Context;
+import org.mozilla.javascript.ContextFactory;
+import org.mozilla.javascript.Scriptable;
+import org.mozilla.javascript.ScriptableObject;
+
+import com.btr.proxy.util.Logger;
+import com.btr.proxy.util.Logger.LogLevel;
+
+/*****************************************************************************
+ * PAC parser using the Rhino JavaScript engine.<br/>
+ * Depends on js.jar of the <a href="http://www.mozilla.org/rhino/">Apache Rhino </a> project.
+ * <p>
+ * More information about PAC can be found there:<br/>
+ * <a href="http://en.wikipedia.org/wiki/Proxy_auto-config">Proxy_auto-config</a><br/>
+ * <a href="http://homepages.tesco.net/~J.deBoynePollard/FGA/web-browser-auto-proxy-configuration.html">web-browser-auto-proxy-configuration</a>
+ * </p>
+ * @author Bernd Rosstauscher (proxyvole@rosstauscher.de) Copyright 2009
+ ****************************************************************************/
+
+public class RhinoPacScriptParser extends ScriptableObject implements PacScriptParser {
+
+ private static final long serialVersionUID = 1L;
+
+ // Define some PAC script functions. These functions are not part of ECMA.
+ private static final String[] JS_FUNCTION_NAMES = {
+ "shExpMatch", "dnsResolve", "isResolvable",
+ "isInNet", "dnsDomainIs", "isPlainHostName", "myIpAddress",
+ "dnsDomainLevels", "localHostOrDomainIs", "weekdayRange",
+ "dateRange", "timeRange"
+ };
+
+ private Scriptable scope;
+ private PacScriptSource source;
+ private static final PacScriptMethods SCRIPT_METHODS = new PacScriptMethods();
+
+ /*************************************************************************
+ * Constructor
+ * @param source the source for the PAC script.
+ * @throws ProxyEvaluationException on error.
+ ************************************************************************/
+
+ public RhinoPacScriptParser(PacScriptSource source) throws ProxyEvaluationException {
+ super();
+ this.source = source;
+
+ setupEngine();
+ }
+
+ /*************************************************************************
+ * Initializes the JavaScript engine.
+ * @throws ProxyEvaluationException on error.
+ ************************************************************************/
+
+ public void setupEngine() throws ProxyEvaluationException {
+
+ Context context = new ContextFactory().enterContext();
+ try {
+ defineFunctionProperties(JS_FUNCTION_NAMES, RhinoPacScriptParser.class, ScriptableObject.DONTENUM);
+ } catch (Exception e) {
+ Logger.log(getClass(), LogLevel.ERROR, "JS Engine setup error.", e);
+ throw new ProxyEvaluationException(e.getMessage(), e);
+ }
+
+ this.scope = context.initStandardObjects(this);
+ }
+
+ /***************************************************************************
+ * Gets the source of the PAC script used by this parser.
+ * @return a PacScriptSource.
+ **************************************************************************/
+
+ public PacScriptSource getScriptSource() {
+ return this.source;
+ }
+
+ /*************************************************************************
+ * Evaluates the given URL and host against the PAC script.
+ * @param url the URL to evaluate.
+ * @param host the host name part of the URL.
+ * @return the script result.
+ * @throws ProxyEvaluationException on execution error.
+ ************************************************************************/
+
+ public String evaluate(String url, String host) throws ProxyEvaluationException {
+ try {
+ // FindProxyForURL function signature
+ StringBuilder script = new StringBuilder(this.source.getScriptContent());
+ String evalMethod = " ;FindProxyForURL (\"" + url + "\",\"" + host + "\")";
+ script.append(evalMethod);
+
+ Context context = Context.enter();
+ try {
+ Object result = context.evaluateString(this.scope,
+ script.toString(), "userPacFile", 1, null);
+
+ return Context.toString(result);
+ } finally {
+ Context.exit();
+ }
+ } catch (Exception e) {
+ Logger.log(getClass(), LogLevel.ERROR, "JS evaluation error.", e);
+ throw new ProxyEvaluationException(
+ "Error while executing PAC script: " + e.getMessage(), e);
+ }
+ }
+
+ /*************************************************************************
+ * getClassName
+ * See also org.mozilla.javascript.ScriptableObject#getClassName()
+ ************************************************************************/
+ @Override
+ public String getClassName() {
+ return getClass().getSimpleName();
+ }
+
+
+
+// ***************************************************************************
+// Defining PAC script methods needed in JS
+// ***************************************************************************
+
+
+ /*************************************************************************
+ * Tests if the given name is a plain host name without a domain name.
+ * @param host the host name from the URL (excluding port number)
+ * @return true if there is no domain name in the host name (no dots).
+ ************************************************************************/
+
+ public static boolean isPlainHostName(String host) {
+ return SCRIPT_METHODS.isPlainHostName(host);
+ }
+
+ /*************************************************************************
+ * Tests if an URL is in a given domain.
+ * @param host is the host name from the URL.
+ * @param domain is the domain name to test the host name against.
+ * @return true if the domain of host name matches.
+ ************************************************************************/
+
+ public static boolean dnsDomainIs(String host, String domain) {
+ return SCRIPT_METHODS.dnsDomainIs(host, domain);
+ }
+
+ /*************************************************************************
+ * Is true if the host name matches exactly the specified host name,
+ * or if there is no domain name part in the host name, but the unqualified
+ * host name matches.
+ * @param host the host name from the URL.
+ * @param domain fully qualified host name with domain to match against.
+ * @return true if matches else false.
+ ************************************************************************/
+
+ public static boolean localHostOrDomainIs(String host, String domain) {
+ return SCRIPT_METHODS.localHostOrDomainIs(host, domain);
+ }
+
+ /*************************************************************************
+ * Tries to resolve the host name. Returns true if succeeds.
+ * @param host is the host name from the URL.
+ * @return true if resolvable else false.
+ ************************************************************************/
+
+ public static boolean isResolvable(String host) {
+ return SCRIPT_METHODS.isResolvable(host);
+ }
+
+ /*************************************************************************
+ * Returns true if the IP address of the host matches the specified IP
+ * address pattern. Pattern and mask specification is done the same way
+ * as for SOCKS configuration.
+ *
+ * Example:
+ * isInNet(host, "198.95.0.0", "255.255.0.0")
+ * is true if the IP address of the host matches 198.95.*.*.
+ *
+ * @param host a DNS host name, or IP address.
+ * If a host name is passed, it will be resolved into an IP address by this function.
+ * @param pattern an IP address pattern in the dot-separated format.
+ * @param mask mask for the IP address pattern informing which parts of
+ * the IP address should be matched against. 0 means ignore, 255 means match.
+ * @return true if it matches else false.
+ ************************************************************************/
+
+ public static boolean isInNet(String host, String pattern, String mask) {
+ return SCRIPT_METHODS.isInNet(host, pattern, mask);
+ }
+
+ /*************************************************************************
+ * Resolves the given DNS host name into an IP address, and returns it in
+ * the dot separated format as a string.
+ * @param host the host to resolve.
+ * @return the resolved IP, empty string if not resolvable.
+ ************************************************************************/
+
+ public static String dnsResolve(String host) {
+ return SCRIPT_METHODS.dnsResolve(host);
+ }
+
+ /*************************************************************************
+ * Returns the IP address of the host that the process is running on,
+ * as a string in the dot-separated integer format.
+ * @return an IP as string.
+ ************************************************************************/
+
+ public static String myIpAddress() {
+ return SCRIPT_METHODS.myIpAddress();
+ }
+
+ /*************************************************************************
+ * Returns the number of DNS domain levels (number of dots) in the host name.
+ * @param host is the host name from the URL.
+ * @return number of DNS domain levels.
+ ************************************************************************/
+
+ public static int dnsDomainLevels(String host) {
+ return SCRIPT_METHODS.dnsDomainLevels(host);
+ }
+
+ /*************************************************************************
+ * Returns true if the string matches the specified shell expression.
+ * Actually, currently the patterns are shell expressions, not regular expressions.
+ * @param str is any string to compare (e.g. the URL, or the host name).
+ * @param shexp is a shell expression to compare against.
+ * @return true if the string matches, else false.
+ ************************************************************************/
+
+ public static boolean shExpMatch(String str, String shexp) {
+ return SCRIPT_METHODS.shExpMatch(str, shexp);
+ }
+
+ /*************************************************************************
+ * Only the first parameter is mandatory.
+ * Either the second, the third, or both may be left out.
+ * If only one parameter is present, the function yields a true value on
+ * the weekday that the parameter represents. If the string "GMT" is
+ * specified as a second parameter, times are taken to be in GMT,
+ * otherwise in local time zone. If both wd1 and wd2 are defined, the
+ * condition is true if the current weekday is in between those two weekdays.
+ * Bounds are inclusive. If the "GMT" parameter is specified, times are
+ * taken to be in GMT, otherwise the local time zone is used.
+ * @param wd1 weekday 1 is one of SUN MON TUE WED THU FRI SAT
+ * @param wd2 weekday 2 is one of SUN MON TUE WED THU FRI SAT
+ * @param gmt "GMT" for gmt time format else "undefined"
+ * @return true if current day matches the criteria.
+ ************************************************************************/
+
+ public static boolean weekdayRange(String wd1, String wd2, String gmt) {
+ return SCRIPT_METHODS.weekdayRange(wd1, wd2, gmt);
+ }
+
+ /*************************************************************************
+ * Sets a calendar with the current time. If this is set all date and time
+ * based methods will use this calendar to determine the current time
+ * instead of the real time. This is only be used by unit tests and is not
+ * part of the public API.
+ * @param cal a Calendar to set.
+ ************************************************************************/
+
+ static void setCurrentTime(Calendar cal) {
+ SCRIPT_METHODS.setCurrentTime(cal);
+ }
+
+ /*************************************************************************
+ * Only the first parameter is mandatory.
+ * All other parameters can be left out therefore the meaning of the parameters
+ * changes. The method definition shows the version with the most possible
+ * parameters filled. The real meaning of the parameters is guessed from it's
+ * value. If "from" and "to" are specified then the bounds are inclusive.
+ * If the "GMT" parameter is specified, times are taken to be in GMT,
+ * otherwise the local time zone is used.
+ * @param day1 is the day of month between 1 and 31 (as an integer).
+ * @param month1 one of JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC
+ * @param year1 is the full year number, for example 1995 (but not 95). Integer.
+ * @param day2 is the day of month between 1 and 31 (as an integer).
+ * @param month2 one of JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC
+ * @param year2 is the full year number, for example 1995 (but not 95). Integer.
+ * @param gmt "GMT" for gmt time format else "undefined"
+ * @return true if the current date matches the given range.
+ ************************************************************************/
+
+ public static boolean dateRange(Object day1, Object month1, Object year1, Object day2, Object month2, Object year2, Object gmt) {
+ return SCRIPT_METHODS.dateRange(day1, month1, year1, day2, month2, year2, gmt);
+ }
+
+ /*************************************************************************
+ * Some parameters can be left out therefore the meaning of the parameters
+ * changes. The method definition shows the version with the most possible
+ * parameters filled. The real meaning of the parameters is guessed from it's
+ * value. If "from" and "to" are specified then the bounds are inclusive.
+ * If the "GMT" parameter is specified, times are taken to be in GMT,
+ * otherwise the local time zone is used.<br/>
+ *
+ * <pre>
+ * timeRange(hour)
+ * timeRange(hour1, hour2)
+ * timeRange(hour1, min1, hour2, min2)
+ * timeRange(hour1, min1, sec1, hour2, min2, sec2)
+ * timeRange(hour1, min1, sec1, hour2, min2, sec2, gmt)
+ * </pre>
+ *
+ * @param hour1 is the hour from 0 to 23. (0 is midnight, 23 is 11 pm.)
+ * @param min1 minutes from 0 to 59.
+ * @param sec1 seconds from 0 to 59.
+ * @param hour2 is the hour from 0 to 23. (0 is midnight, 23 is 11 pm.)
+ * @param min2 minutes from 0 to 59.
+ * @param sec2 seconds from 0 to 59.
+ * @param gmt "GMT" for gmt time format else "undefined"
+ * @return true if the current time matches the given range.
+ ************************************************************************/
+
+ public static boolean timeRange(Object hour1, Object min1, Object sec1, Object hour2, Object min2, Object sec2, Object gmt) {
+ return SCRIPT_METHODS.timeRange(hour1, min1, sec1, hour2, min2, sec2, gmt);
+ }
+
+}
diff --git a/src/main/java/com/btr/proxy/selector/pac/ScriptAvailability.java b/src/main/java/com/btr/proxy/selector/pac/ScriptAvailability.java
new file mode 100644
index 0000000..d7f6e04
--- /dev/null
+++ b/src/main/java/com/btr/proxy/selector/pac/ScriptAvailability.java
@@ -0,0 +1,46 @@
+package com.btr.proxy.selector.pac;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/****************************************************************************
+ * Utility to check availablility of javax.script
+ *
+ * @author Bernd Rosstauscher (proxyvole@rosstauscher.de) Copyright 2009
+ ***************************************************************************/
+abstract class ScriptAvailability {
+
+ /*************************************************************************
+ * Checks whether javax.script is available or not.
+ * Completely done per Reflection to allow compilation under Java 1.5
+ * @return true if javax.script is available; false otherwise
+ ************************************************************************/
+ public static boolean isJavaxScriptingAvailable() {
+ Object engine = null;
+ try {
+ Class<?> managerClass = Class.forName("javax.script.ScriptEngineManager");
+ Method m = managerClass.getMethod("getEngineByMimeType", String.class);
+ engine = m.invoke(managerClass.newInstance(), "text/javascript");
+ } catch (ClassNotFoundException e) {
+ // javax.script not available
+ } catch (NoSuchMethodException e) {
+ // javax.script not available
+ } catch (IllegalAccessException e) {
+ // javax.script not available
+ } catch (InvocationTargetException e) {
+ // javax.script not available
+ } catch (InstantiationException e) {
+ // javax.script not available
+ }
+
+ return engine != null;
+ }
+
+ /*************************************************************************
+ * Constructor
+ ************************************************************************/
+
+ ScriptAvailability() {
+ super();
+ }
+}
diff --git a/src/main/java/com/btr/proxy/selector/pac/ScriptMethods.java b/src/main/java/com/btr/proxy/selector/pac/ScriptMethods.java
new file mode 100644
index 0000000..9f559c5
--- /dev/null
+++ b/src/main/java/com/btr/proxy/selector/pac/ScriptMethods.java
@@ -0,0 +1,256 @@
+package com.btr.proxy.selector.pac;
+
+/***************************************************************************
+ * Defines the public interface for PAC scripts.
+ *
+ * @author Bernd Rosstauscher (proxyvole@rosstauscher.de) Copyright 2009
+ ***************************************************************************/
+public interface ScriptMethods {
+
+ public boolean isPlainHostName(String host);
+
+ /*************************************************************************
+ * Tests if an URL is in a given domain.
+ *
+ * @param host
+ * is the host name from the URL.
+ * @param domain
+ * is the domain name to test the host name against.
+ * @return true if the domain of host name matches.
+ ************************************************************************/
+
+ public boolean dnsDomainIs(String host, String domain);
+
+ /*************************************************************************
+ * Is true if the host name matches exactly the specified host name, or if
+ * there is no domain name part in the host name, but the unqualified host
+ * name matches.
+ *
+ * @param host
+ * the host name from the URL.
+ * @param domain
+ * fully qualified host name with domain to match against.
+ * @return true if matches else false.
+ ************************************************************************/
+
+ public boolean localHostOrDomainIs(String host, String domain);
+
+ /*************************************************************************
+ * Tries to resolve the host name. Returns true if succeeds.
+ *
+ * @param host
+ * is the host name from the URL.
+ * @return true if resolvable else false.
+ ************************************************************************/
+
+ public boolean isResolvable(String host);
+
+ /*************************************************************************
+ * Tries to resolve the host name. Returns true if succeeds to resolve
+ * the host to an IPv4 or IPv6 address.
+ *
+ * @param host
+ * is the host name from the URL.
+ * @return true if resolvable else false.
+ ************************************************************************/
+
+ public boolean isResolvableEx(String host);
+
+ /*************************************************************************
+ * Returns true if the IP address of the host matches the specified IP
+ * address pattern. Pattern and mask specification is done the same way as
+ * for SOCKS configuration.
+ *
+ * Example: isInNet(host, "198.95.0.0", "255.255.0.0") is true if the IP
+ * address of the host matches 198.95.*.*.
+ *
+ * @param host
+ * a DNS host name, or IP address. If a host name is passed, it
+ * will be resolved into an IP address by this function.
+ * @param pattern
+ * an IP address pattern in the dot-separated format.
+ * @param mask
+ * mask for the IP address pattern informing which parts of the
+ * IP address should be matched against. 0 means ignore, 255
+ * means match.
+ * @return true if it matches else false.
+ ************************************************************************/
+
+ public boolean isInNet(String host, String pattern, String mask);
+
+ /*************************************************************************
+ * Extension of the isInNet method to support IPv6.
+ * @param ipAddress an IP4 or IP6 address
+ * @param ipPrefix A string containing colon delimited IP prefix
+ * with top n bits specified in the bit field
+ * (i.e. 3ffe:8311:ffff::/48 or 123.112.0.0/16).
+ * @return true if the host is in the given subnet, else false.
+ ************************************************************************/
+
+ public boolean isInNetEx(String ipAddress, String ipPrefix);
+
+ /*************************************************************************
+ * Resolves the given DNS host name into an IP address, and returns it in
+ * the dot separated format as a string.
+ *
+ * @param host
+ * the host to resolve.
+ * @return the resolved IP, empty string if not resolvable.
+ ************************************************************************/
+
+ public String dnsResolve(String host);
+
+ /*************************************************************************
+ * @param host the host to resolve
+ * @return a semicolon separated list of IP6 and IP4 addresses the host
+ * name resolves to, empty string if not resolvable.
+ ************************************************************************/
+
+ public String dnsResolveEx(String host);
+
+ /*************************************************************************
+ * Returns the IP address of the host that the process is running on, as a
+ * string in the dot-separated integer format.
+ *
+ * @return an IP as string.
+ ************************************************************************/
+
+ public String myIpAddress();
+
+ /*************************************************************************
+ * Returns a list of IP4 and IP6 addresses of the host that the process
+ * is running on. The list is separated with semicolons.
+ * @return the list, empty string if not available.
+ ************************************************************************/
+
+ public String myIpAddressEx();
+
+ /*************************************************************************
+ * Returns the number of DNS domain levels (number of dots) in the host
+ * name.
+ *
+ * @param host
+ * is the host name from the URL.
+ * @return number of DNS domain levels.
+ ************************************************************************/
+
+ public int dnsDomainLevels(String host);
+
+ /*************************************************************************
+ * Returns true if the string matches the specified shell expression.
+ * Actually, currently the patterns are shell expressions, not regular
+ * expressions.
+ *
+ * @param str
+ * is any string to compare (e.g. the URL, or the host name).
+ * @param shexp
+ * is a shell expression to compare against.
+ * @return true if the string matches, else false.
+ ************************************************************************/
+
+ public boolean shExpMatch(String str, String shexp);
+
+ /*************************************************************************
+ * Only the first parameter is mandatory. Either the second, the third, or
+ * both may be left out. If only one parameter is present, the function
+ * yields a true value on the weekday that the parameter represents. If the
+ * string "GMT" is specified as a second parameter, times are taken to be in
+ * GMT, otherwise in local time zone. If both wd1 and wd2 are defined, the
+ * condition is true if the current weekday is in between those two
+ * weekdays. Bounds are inclusive. If the "GMT" parameter is specified,
+ * times are taken to be in GMT, otherwise the local time zone is used.
+ *
+ * @param wd1
+ * weekday 1 is one of SUN MON TUE WED THU FRI SAT
+ * @param wd2
+ * weekday 2 is one of SUN MON TUE WED THU FRI SAT
+ * @param gmt
+ * "GMT" for gmt time format else "undefined"
+ * @return true if current day matches the criteria.
+ ************************************************************************/
+
+ public boolean weekdayRange(String wd1, String wd2, String gmt);
+
+ /*************************************************************************
+ * Only the first parameter is mandatory. All other parameters can be left
+ * out therefore the meaning of the parameters changes. The method
+ * definition shows the version with the most possible parameters filled.
+ * The real meaning of the parameters is guessed from it's value. If "from"
+ * and "to" are specified then the bounds are inclusive. If the "GMT"
+ * parameter is specified, times are taken to be in GMT, otherwise the local
+ * time zone is used.
+ *
+ * @param day1
+ * is the day of month between 1 and 31 (as an integer).
+ * @param month1
+ * one of JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC
+ * @param year1
+ * is the full year number, for example 1995 (but not 95).
+ * Integer.
+ * @param day2
+ * is the day of month between 1 and 31 (as an integer).
+ * @param month2
+ * one of JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC
+ * @param year2
+ * is the full year number, for example 1995 (but not 95).
+ * Integer.
+ * @param gmt
+ * "GMT" for gmt time format else "undefined"
+ * @return true if the current date matches the given range.
+ ************************************************************************/
+
+ public boolean dateRange(Object day1, Object month1, Object year1, Object day2, Object month2, Object year2,
+ Object gmt);
+
+ /*************************************************************************
+ * Some parameters can be left out therefore the meaning of the parameters
+ * changes. The method definition shows the version with the most possible
+ * parameters filled. The real meaning of the parameters is guessed from
+ * it's value. If "from" and "to" are specified then the bounds are
+ * inclusive. If the "GMT" parameter is specified, times are taken to be in
+ * GMT, otherwise the local time zone is used.<br/>
+ *
+ * <pre>
+ * timeRange(hour)
+ * timeRange(hour1, hour2)
+ * timeRange(hour1, min1, hour2, min2)
+ * timeRange(hour1, min1, sec1, hour2, min2, sec2)
+ * timeRange(hour1, min1, sec1, hour2, min2, sec2, gmt)
+ * </pre>
+ *
+ * @param hour1
+ * is the hour from 0 to 23. (0 is midnight, 23 is 11 pm.)
+ * @param min1
+ * minutes from 0 to 59.
+ * @param sec1
+ * seconds from 0 to 59.
+ * @param hour2
+ * is the hour from 0 to 23. (0 is midnight, 23 is 11 pm.)
+ * @param min2
+ * minutes from 0 to 59.
+ * @param sec2
+ * seconds from 0 to 59.
+ * @param gmt
+ * "GMT" for gmt time format else "undefined"
+ * @return true if the current time matches the given range.
+ ************************************************************************/
+
+ public boolean timeRange(Object hour1, Object min1, Object sec1, Object hour2, Object min2, Object sec2, Object gmt);
+
+ /*************************************************************************
+ * Sorts a list of IP4 and IP6 addresses. Separated by semicolon.
+ * Dual addresses first, then IPv6 and last IPv4.
+ * @param ipAddressList the address list.
+ * @return the sorted list, empty string if sort is not possible
+ ************************************************************************/
+
+ public String sortIpAddressList(String ipAddressList);
+
+ /*************************************************************************
+ * Gets the version of the PAC extension that is available.
+ * @return the extension version, currently 1.0
+ ************************************************************************/
+
+ public String getClientVersion();
+
+}
diff --git a/src/main/java/com/btr/proxy/selector/pac/UrlPacScriptSource.java b/src/main/java/com/btr/proxy/selector/pac/UrlPacScriptSource.java
new file mode 100644
index 0000000..6ac8faa
--- /dev/null
+++ b/src/main/java/com/btr/proxy/selector/pac/UrlPacScriptSource.java
@@ -0,0 +1,270 @@
+package com.btr.proxy.selector.pac;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.Proxy;
+import java.net.URISyntaxException;
+import java.net.URL;
+
+import com.btr.proxy.util.Logger;
+import com.btr.proxy.util.Logger.LogLevel;
+
+/*****************************************************************************
+ * Script source that will load the content of a PAC file from an webserver.
+ * The script content is cached once it was downloaded.
+ *
+ * @author Bernd Rosstauscher (proxyvole@rosstauscher.de) Copyright 2009
+ ****************************************************************************/
+
+public class UrlPacScriptSource implements PacScriptSource {
+
+ private static final int DEFAULT_CONNECT_TIMEOUT = 15 * 1000; // seconds
+ private static final int DEFAULT_READ_TIMEOUT = 20 * 1000; // seconds
+ public static final String OVERRIDE_CONNECT_TIMEOUT = "com.btr.proxy.url.connectTimeout";
+ public static final String OVERRIDE_READ_TIMEOUT = "com.btr.proxy.url.readTimeout";
+
+ private final String scriptUrl;
+ private String scriptContent;
+ private long expireAtMillis;
+
+ /*************************************************************************
+ * Constructor
+ * @param url the URL to download the script from.
+ ************************************************************************/
+
+ public UrlPacScriptSource(String url) {
+ super();
+ this.expireAtMillis = 0;
+ this.scriptUrl = url;
+ }
+
+ /*************************************************************************
+ * getScriptContent
+ * @see com.btr.proxy.selector.pac.PacScriptSource#getScriptContent()
+ ************************************************************************/
+
+ public synchronized String getScriptContent() throws IOException {
+ if (this.scriptContent == null ||
+ (this.expireAtMillis > 0
+ && this.expireAtMillis > System.currentTimeMillis())) {
+ try {
+ if (this.scriptUrl.startsWith("file:/") || this.scriptUrl.indexOf(":/") == -1) {
+ this.scriptContent = readPacFileContent(this.scriptUrl);
+ } else {
+ this.scriptContent = downloadPacContent(this.scriptUrl);
+ }
+ } catch (IOException e) {
+ Logger.log(getClass(), LogLevel.ERROR, "Loading script failed from: {0} with error {1}", this.scriptUrl, e);
+ this.scriptContent = "";
+ throw e;
+ }
+ }
+ return this.scriptContent;
+ }
+
+ /*************************************************************************
+ * Reads a PAC script from a local file.
+ * @param scriptUrl
+ * @return the content of the script file.
+ * @throws IOException
+ * @throws URISyntaxException
+ ************************************************************************/
+
+ private String readPacFileContent(String scriptUrl) throws IOException {
+ try {
+ File file = null;
+ if (scriptUrl.indexOf(":/") == -1) {
+ file = new File(scriptUrl);
+ } else {
+ file = new File(new URL(scriptUrl).toURI());
+ }
+ BufferedReader r = new BufferedReader(new FileReader(file));
+ StringBuilder result = new StringBuilder();
+ try {
+ String line;
+ while ((line = r.readLine()) != null) {
+ result.append(line).append("\n");
+ }
+ } finally {
+ r.close();
+ }
+ return result.toString();
+ } catch (Exception e) {
+ Logger.log(getClass(), LogLevel.ERROR, "File reading error.", e);
+ throw new IOException(e.getMessage());
+ }
+ }
+
+ /*************************************************************************
+ * Downloads the script from a webserver.
+ * @param url the URL to the script file.
+ * @return the script content.
+ * @throws IOException on read error.
+ ************************************************************************/
+
+ private String downloadPacContent(String url) throws IOException {
+ if (url == null) {
+ throw new IOException("Invalid PAC script URL: null");
+ }
+
+ setPacProxySelectorEnabled(false);
+
+ HttpURLConnection con = null;
+ try {
+ con = setupHTTPConnection(url);
+ if (con.getResponseCode() != 200) {
+ throw new IOException("Server returned: "+con.getResponseCode()+" "+con.getResponseMessage());
+ }
+ // Read expire date.
+ this.expireAtMillis = con.getExpiration();
+
+ BufferedReader r = getReader(con);
+ String result = readAllContent(r);
+ r.close();
+ return result;
+ } finally {
+ setPacProxySelectorEnabled(true);
+ if (con != null) {
+ con.disconnect();
+ }
+ }
+ }
+
+ /*************************************************************************
+ * Enables/disables the PAC proxy selector while we download to prevent recursion.
+ * See issue: 26 in the change tracker.
+ ************************************************************************/
+
+ private void setPacProxySelectorEnabled(boolean enable) {
+ PacProxySelector.setEnabled(enable);
+ }
+
+ /*************************************************************************
+ * Reads the whole content available into a String.
+ * @param r to read from.
+ * @return the complete PAC file content.
+ * @throws IOException
+ ************************************************************************/
+
+ private String readAllContent(BufferedReader r) throws IOException {
+ StringBuilder result = new StringBuilder();
+ String line;
+ while ((line = r.readLine()) != null) {
+ result.append(line).append("\n");
+ }
+ return result.toString();
+ }
+
+ /*************************************************************************
+ * Build a BufferedReader around the open HTTP connection.
+ * @param con to read from
+ * @return the BufferedReader.
+ * @throws UnsupportedEncodingException
+ * @throws IOException
+ ************************************************************************/
+
+ private BufferedReader getReader(HttpURLConnection con)
+ throws UnsupportedEncodingException, IOException {
+ String charsetName = parseCharsetFromHeader(con.getContentType());
+ BufferedReader r = new BufferedReader(new InputStreamReader(con.getInputStream(), charsetName));
+ return r;
+ }
+
+ /*************************************************************************
+ * Configure the connection to download from.
+ * @param url to get the pac file content from
+ * @return a HTTPUrlConnecion to this url.
+ * @throws IOException
+ * @throws MalformedURLException
+ ************************************************************************/
+
+ private HttpURLConnection setupHTTPConnection(String url)
+ throws IOException, MalformedURLException {
+ HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection(Proxy.NO_PROXY);
+ con.setConnectTimeout(getTimeOut(OVERRIDE_CONNECT_TIMEOUT, DEFAULT_CONNECT_TIMEOUT));
+ con.setReadTimeout(getTimeOut(OVERRIDE_READ_TIMEOUT, DEFAULT_READ_TIMEOUT));
+ con.setInstanceFollowRedirects(true);
+ con.setRequestProperty("accept", "application/x-ns-proxy-autoconfig, */*;q=0.8");
+ return con;
+ }
+
+ /*************************************************************************
+ * Gets the timeout value from a property or uses the given default value if
+ * the property cannot be parsed.
+ * @param overrideProperty the property to define the timeout value in milliseconds
+ * @param defaultValue the default timeout value in milliseconds.
+ * @return the value to use.
+ ************************************************************************/
+
+ protected int getTimeOut(String overrideProperty, int defaultValue) {
+ int timeout = defaultValue;
+ String prop = System.getProperty(overrideProperty);
+ if (prop != null && prop.trim().length() > 0) {
+ try {
+ timeout = Integer.parseInt(prop.trim());
+ } catch (NumberFormatException e) {
+ Logger.log(getClass(), LogLevel.DEBUG, "Invalid override property : {0}={1}", overrideProperty, prop);
+ // In this case use the default value.
+ }
+ }
+ return timeout;
+ }
+
+ /*************************************************************************
+ * Response Content-Type could be something like this:
+ * application/x-ns-proxy-autoconfig; charset=UTF-8
+ * @param contentType header field.
+ * @return the extracted charset if set else a default charset.
+ ************************************************************************/
+
+ String parseCharsetFromHeader(String contentType) {
+ String result = "ISO-8859-1";
+ if (contentType != null) {
+ String[] paramList = contentType.split(";");
+ for (String param : paramList) {
+ if (param.toLowerCase().trim().startsWith("charset") && param.indexOf("=") != -1) {
+ result = param.substring(param.indexOf("=")+1).trim();
+ }
+ }
+ }
+ return result;
+ }
+
+ /***************************************************************************
+ * @see java.lang.Object#toString()
+ **************************************************************************/
+ @Override
+ public String toString() {
+ return this.scriptUrl;
+ }
+
+ /*************************************************************************
+ * isScriptValid
+ * @see com.btr.proxy.selector.pac.PacScriptSource#isScriptValid()
+ ************************************************************************/
+
+ public boolean isScriptValid() {
+ try {
+ String script = getScriptContent();
+ if (script == null || script.trim().length() == 0) {
+ Logger.log(getClass(), LogLevel.DEBUG, "PAC script is empty. Skipping script!");
+ return false;
+ }
+ if (script.indexOf("FindProxyForURL") == -1) {
+ Logger.log(getClass(), LogLevel.DEBUG, "PAC script entry point FindProxyForURL not found. Skipping script!");
+ return false;
+ }
+ return true;
+ } catch (IOException e) {
+ Logger.log(getClass(), LogLevel.DEBUG, "File reading error: {0}", e);
+ return false;
+ }
+ }
+
+}