summaryrefslogtreecommitdiffstats
path: root/src/main/java/com/btr/proxy/selector
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/btr/proxy/selector')
-rw-r--r--src/main/java/com/btr/proxy/selector/direct/NoProxySelector.java70
-rw-r--r--src/main/java/com/btr/proxy/selector/fixed/FixedProxySelector.java69
-rw-r--r--src/main/java/com/btr/proxy/selector/fixed/FixedSocksSelector.java27
-rw-r--r--src/main/java/com/btr/proxy/selector/misc/BufferedProxySelector.java126
-rw-r--r--src/main/java/com/btr/proxy/selector/misc/ProtocolDispatchSelector.java115
-rw-r--r--src/main/java/com/btr/proxy/selector/misc/ProxyListFallbackSelector.java150
-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
-rw-r--r--src/main/java/com/btr/proxy/selector/whitelist/DefaultWhiteListParser.java78
-rw-r--r--src/main/java/com/btr/proxy/selector/whitelist/HostnameFilter.java93
-rw-r--r--src/main/java/com/btr/proxy/selector/whitelist/IPv4WithSubnetChecker.java29
-rw-r--r--src/main/java/com/btr/proxy/selector/whitelist/IpRangeFilter.java83
-rw-r--r--src/main/java/com/btr/proxy/selector/whitelist/ProxyBypassListSelector.java84
-rw-r--r--src/main/java/com/btr/proxy/selector/whitelist/UseProxyWhiteListSelector.java74
-rw-r--r--src/main/java/com/btr/proxy/selector/whitelist/WhiteListParser.java24
23 files changed, 3010 insertions, 0 deletions
diff --git a/src/main/java/com/btr/proxy/selector/direct/NoProxySelector.java b/src/main/java/com/btr/proxy/selector/direct/NoProxySelector.java
new file mode 100644
index 0000000..cf9e150
--- /dev/null
+++ b/src/main/java/com/btr/proxy/selector/direct/NoProxySelector.java
@@ -0,0 +1,70 @@
+package com.btr.proxy.selector.direct;
+
+import java.io.IOException;
+import java.net.Proxy;
+import java.net.ProxySelector;
+import java.net.SocketAddress;
+import java.net.URI;
+import java.util.List;
+
+import com.btr.proxy.util.ProxyUtil;
+
+/*****************************************************************************
+ * This proxy selector will always return a "DIRECT" proxy.
+ * Implemented as singleton.
+ *
+ * @author Bernd Rosstauscher (proxyvole@rosstauscher.de) Copyright 2009
+ ****************************************************************************/
+
+public class NoProxySelector extends ProxySelector {
+
+ private static NoProxySelector instance;
+
+ /*************************************************************************
+ * Constructor
+ ************************************************************************/
+
+ private NoProxySelector() {
+ super();
+ }
+
+ /*************************************************************************
+ * Gets the one and only instance of this selector.
+ * @return a DirectSelector.
+ ************************************************************************/
+
+ public static synchronized NoProxySelector getInstance() {
+ if (NoProxySelector.instance == null) {
+ NoProxySelector.instance = new NoProxySelector();
+ }
+ return instance;
+ }
+
+ /*************************************************************************
+ * 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) {
+ return ProxyUtil.noProxyList();
+ }
+
+}
+
+
+
+
+/*
+ * $Log: $
+ */ \ No newline at end of file
diff --git a/src/main/java/com/btr/proxy/selector/fixed/FixedProxySelector.java b/src/main/java/com/btr/proxy/selector/fixed/FixedProxySelector.java
new file mode 100644
index 0000000..498ac52
--- /dev/null
+++ b/src/main/java/com/btr/proxy/selector/fixed/FixedProxySelector.java
@@ -0,0 +1,69 @@
+package com.btr.proxy.selector.fixed;
+
+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.Collections;
+import java.util.List;
+
+/*****************************************************************************
+ * This proxy selector is configured with a fixed proxy. This proxy will be
+ * returned for all URIs passed to the select method.
+ *
+ * @author Bernd Rosstauscher (proxyvole@rosstauscher.de) Copyright 2009
+ ****************************************************************************/
+
+public class FixedProxySelector extends ProxySelector {
+
+ private final List<Proxy> proxyList;
+
+
+ /*************************************************************************
+ * Constructor
+ * @param proxy the proxy to use.
+ ************************************************************************/
+
+ public FixedProxySelector(Proxy proxy) {
+ super();
+
+ List<Proxy> list = new ArrayList<Proxy>(1);
+ list.add(proxy);
+ this.proxyList = Collections.unmodifiableList(list);
+ }
+
+ /*************************************************************************
+ * Constructor
+ * @param proxyHost the host name or IP address of the proxy to use.
+ * @param proxyPort the port of the proxy.
+ ************************************************************************/
+
+ public FixedProxySelector(String proxyHost, int proxyPort) {
+ this(new Proxy(Proxy.Type.HTTP,
+ InetSocketAddress.createUnresolved(proxyHost, proxyPort)));
+ }
+
+ /*************************************************************************
+ * 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) {
+ return this.proxyList;
+ }
+
+}
diff --git a/src/main/java/com/btr/proxy/selector/fixed/FixedSocksSelector.java b/src/main/java/com/btr/proxy/selector/fixed/FixedSocksSelector.java
new file mode 100644
index 0000000..d1f7ccc
--- /dev/null
+++ b/src/main/java/com/btr/proxy/selector/fixed/FixedSocksSelector.java
@@ -0,0 +1,27 @@
+package com.btr.proxy.selector.fixed;
+
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+
+/*****************************************************************************
+ * This proxy selector is configured with a fixed proxy. This proxy will be
+ * returned for all URIs passed to the select method. This implementation
+ * can be used for SOCKS 4 and 5 proxy support.
+ *
+ * @author Bernd Rosstauscher (proxyvole@rosstauscher.de) Copyright 2009
+ ****************************************************************************/
+
+public class FixedSocksSelector extends FixedProxySelector {
+
+ /*************************************************************************
+ * Constructor
+ * @param proxyHost the host name or IP address of the proxy to use.
+ * @param proxyPort the port of the proxy.
+ ************************************************************************/
+
+ public FixedSocksSelector(String proxyHost, int proxyPort) {
+ super(new Proxy(Proxy.Type.SOCKS,
+ InetSocketAddress.createUnresolved(proxyHost, proxyPort)));
+ }
+
+}
diff --git a/src/main/java/com/btr/proxy/selector/misc/BufferedProxySelector.java b/src/main/java/com/btr/proxy/selector/misc/BufferedProxySelector.java
new file mode 100644
index 0000000..f006314
--- /dev/null
+++ b/src/main/java/com/btr/proxy/selector/misc/BufferedProxySelector.java
@@ -0,0 +1,126 @@
+package com.btr.proxy.selector.misc;
+
+import java.io.IOException;
+import java.net.Proxy;
+import java.net.ProxySelector;
+import java.net.SocketAddress;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.Map.Entry;
+import java.util.concurrent.ConcurrentHashMap;
+
+/*****************************************************************************
+ * Implements a cache that can be used to warp it around an existing ProxySelector.
+ * You can specify a maximum cache size and a "time to live" for positive resolves.
+ * @author Bernd Rosstauscher (proxyvole@rosstauscher.de) Copyright 2009
+ ****************************************************************************/
+
+public class BufferedProxySelector extends ProxySelector {
+
+ private ProxySelector delegate;
+
+ private ConcurrentHashMap<String, CacheEntry> cache;
+ private int maxSize;
+ private long ttl;
+
+ private static class CacheEntry {
+ List<Proxy> result;
+ long expireAt;
+
+ public CacheEntry(List<Proxy> r, long expireAt) {
+ super();
+ this.result = new ArrayList<Proxy>(r.size());
+ this.result.addAll(r);
+ this.result = Collections.unmodifiableList(this.result);
+ this.expireAt = expireAt;
+ }
+
+ public boolean isExpired() {
+ return System.nanoTime() >= this.expireAt;
+ }
+ }
+
+ /*************************************************************************
+ * Constructor
+ * @param maxSize the max size for the cache.
+ * @param ttl the "time to live" for cache entries as amount in milliseconds.
+ * @param delegate the delegate to use.
+ ************************************************************************/
+
+ public BufferedProxySelector(int maxSize, long ttl, ProxySelector delegate) {
+ super();
+ this.cache = new ConcurrentHashMap<String, CacheEntry>();
+ this.maxSize = maxSize;
+ this.delegate = delegate;
+ this.ttl = ttl;
+ }
+
+ /*************************************************************************
+ * 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) {
+ this.delegate.connectFailed(uri, sa, ioe);
+ }
+
+ /*************************************************************************
+ * select
+ * @see java.net.ProxySelector#select(java.net.URI)
+ ************************************************************************/
+
+ @Override
+ public List<Proxy> select(URI uri) {
+ //String cacheKey = uri.getHost(); // Caching per host may produce wrong results
+ String cacheKey = uri.toString();
+
+ CacheEntry entry = this.cache.get(cacheKey);
+ if (entry == null || entry.isExpired()) {
+ List<Proxy> result = this.delegate.select(uri);
+ entry = new CacheEntry(result, System.nanoTime()+this.ttl*1000*1000);
+
+ synchronized (this.cache) {
+ if (this.cache.size() >= this.maxSize) {
+ purgeCache();
+ }
+ this.cache.put(cacheKey, entry);
+ }
+ }
+
+ return entry.result;
+ }
+
+ /*************************************************************************
+ * Purge cache to get some free space for a new entry.
+ ************************************************************************/
+
+ private void purgeCache() {
+
+ // Remove all expired entries and find the oldest.
+ boolean removedOne = false;
+ Entry<String, CacheEntry> oldest = null;
+
+ Set<Entry<String, CacheEntry>> entries = this.cache.entrySet();
+ for (Iterator<Entry<String, CacheEntry>> it = entries.iterator(); it.hasNext();) {
+ Entry<String, CacheEntry> entry = it.next();
+ if (entry.getValue().isExpired()) {
+ it.remove();
+ removedOne = true;
+ } else
+ if (oldest == null || entry.getValue().expireAt < oldest.getValue().expireAt) {
+ oldest = entry;
+ }
+ }
+
+ // Remove oldest if no expired entries were found.
+ if (!removedOne && oldest != null) {
+ this.cache.remove(oldest.getKey());
+ }
+ }
+
+}
diff --git a/src/main/java/com/btr/proxy/selector/misc/ProtocolDispatchSelector.java b/src/main/java/com/btr/proxy/selector/misc/ProtocolDispatchSelector.java
new file mode 100644
index 0000000..5d7f563
--- /dev/null
+++ b/src/main/java/com/btr/proxy/selector/misc/ProtocolDispatchSelector.java
@@ -0,0 +1,115 @@
+package com.btr.proxy.selector.misc;
+
+import java.io.IOException;
+import java.net.Proxy;
+import java.net.ProxySelector;
+import java.net.SocketAddress;
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import com.btr.proxy.selector.direct.NoProxySelector;
+
+/*****************************************************************************
+ * This is a facade for a list of ProxySelecor objects. You can register
+ * different ProxySelectors per Protocol.
+ *
+ * @author Bernd Rosstauscher (proxyvole@rosstauscher.de) Copyright 2009
+ ****************************************************************************/
+
+public class ProtocolDispatchSelector extends ProxySelector {
+
+ private Map<String, ProxySelector> selectors;
+ private ProxySelector fallbackSelector;
+
+ /*************************************************************************
+ * Constructor
+ ************************************************************************/
+
+ public ProtocolDispatchSelector() {
+ super();
+ this.selectors = new ConcurrentHashMap<String, ProxySelector>();
+ this.fallbackSelector = NoProxySelector.getInstance();
+ }
+
+ /*************************************************************************
+ * Sets a selector responsible for the given protocol.
+ * @param protocol the name of the protocol.
+ * @param selector the selector to use.
+ ************************************************************************/
+
+ public void setSelector(String protocol, ProxySelector selector) {
+ if (protocol == null) {
+ throw new NullPointerException("Protocol must not be null.");
+ }
+ if (selector == null) {
+ throw new NullPointerException("Selector must not be null.");
+ }
+ this.selectors.put(protocol, selector);
+ }
+
+ /*************************************************************************
+ * Removes the selector installed for the given protocol.
+ * @param protocol the protocol name.
+ * @return the old selector that is removed.
+ ************************************************************************/
+
+ public ProxySelector removeSelector(String protocol) {
+ return this.selectors.remove(protocol);
+ }
+
+ /*************************************************************************
+ * Gets the selector installed for the given protocol.
+ * @param protocol the protocol name.
+ * @return the selector for that protocol, null if none is currently set.
+ ************************************************************************/
+
+ public ProxySelector getSelector(String protocol) {
+ return this.selectors.get(protocol);
+ }
+
+ /*************************************************************************
+ * Sets the fallback selector that is always called when no matching
+ * protocol selector was found..
+ * @param selector the selector to use.
+ ************************************************************************/
+
+ public void setFallbackSelector(ProxySelector selector) {
+ if (selector == null) {
+ throw new NullPointerException("Selector must not be null.");
+ }
+ this.fallbackSelector = selector;
+ }
+
+ /*************************************************************************
+ * 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) {
+ ProxySelector selector = this.fallbackSelector;
+ String protocol = uri.getScheme();
+ if (protocol != null && this.selectors.get(protocol) != null) {
+ selector = this.selectors.get(protocol);
+ }
+ selector.connectFailed(uri, sa, ioe);
+ }
+
+ /*************************************************************************
+ * select
+ * @see java.net.ProxySelector#select(java.net.URI)
+ ************************************************************************/
+
+ @Override
+ public List<Proxy> select(URI uri) {
+ ProxySelector selector = this.fallbackSelector;
+ String protocol = uri.getScheme();
+ if (protocol != null && this.selectors.get(protocol) != null) {
+ selector = this.selectors.get(protocol);
+ }
+ return selector.select(uri);
+ }
+
+}
diff --git a/src/main/java/com/btr/proxy/selector/misc/ProxyListFallbackSelector.java b/src/main/java/com/btr/proxy/selector/misc/ProxyListFallbackSelector.java
new file mode 100644
index 0000000..41859ec
--- /dev/null
+++ b/src/main/java/com/btr/proxy/selector/misc/ProxyListFallbackSelector.java
@@ -0,0 +1,150 @@
+package com.btr.proxy.selector.misc;
+
+import java.io.IOException;
+import java.net.Proxy;
+import java.net.ProxySelector;
+import java.net.SocketAddress;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.concurrent.ConcurrentHashMap;
+
+/*****************************************************************************
+ * Implements a fallback selector to warp it around an existing ProxySelector.
+ * This will remove proxies from a list of proxies and implement an automatic
+ * retry mechanism.
+ *
+ * @author Bernd Rosstauscher (proxyvole@rosstauscher.de) Copyright 2011
+ ****************************************************************************/
+
+public class ProxyListFallbackSelector extends ProxySelector {
+
+ // Retry a unresponsive proxy after 10 minutes per default.
+ private static final int DEFAULT_RETRY_DELAY = 1000*60*10;
+
+ private ProxySelector delegate;
+ private ConcurrentHashMap<SocketAddress, Long> failedDelayCache;
+ private long retryAfterMs;
+
+ /*************************************************************************
+ * Constructor
+ * @param delegate the delegate to use.
+ ************************************************************************/
+
+ public ProxyListFallbackSelector(ProxySelector delegate) {
+ this(DEFAULT_RETRY_DELAY, delegate);
+ }
+
+ /*************************************************************************
+ * Constructor
+ * @param retryAfterMs the "retry delay" as amount of milliseconds.
+ * @param delegate the delegate to use.
+ ************************************************************************/
+
+ public ProxyListFallbackSelector(long retryAfterMs, ProxySelector delegate) {
+ super();
+ this.failedDelayCache = new ConcurrentHashMap<SocketAddress, Long>();
+ this.delegate = delegate;
+ this.retryAfterMs = retryAfterMs;
+ }
+
+ /*************************************************************************
+ * 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) {
+ this.failedDelayCache.put(sa, System.currentTimeMillis());
+ }
+
+ /*************************************************************************
+ * select
+ * @see java.net.ProxySelector#select(java.net.URI)
+ ************************************************************************/
+
+ @Override
+ public List<Proxy> select(URI uri) {
+ cleanupCache();
+ List<Proxy> proxyList = this.delegate.select(uri);
+ List<Proxy> result = filterUnresponsiveProxiesFromList(proxyList);
+ return result;
+ }
+
+ /*************************************************************************
+ * Cleanup the entries from the cache that are no longer unresponsive.
+ ************************************************************************/
+
+ private void cleanupCache() {
+ Iterator<Entry<SocketAddress, Long>> it
+ = this.failedDelayCache.entrySet().iterator();
+ while (it.hasNext()) {
+ Entry<SocketAddress, Long> e = it.next();
+ Long lastFailTime = e.getValue();
+ if (retryDelayHasPassedBy(lastFailTime)) {
+ it.remove();
+ }
+ }
+ }
+
+ /*************************************************************************
+ * @param proxyList
+ * @return
+ ************************************************************************/
+
+ private List<Proxy> filterUnresponsiveProxiesFromList(List<Proxy> proxyList) {
+ if (this.failedDelayCache.isEmpty()) {
+ return proxyList;
+ }
+ List<Proxy> result = new ArrayList<Proxy>(proxyList.size());
+ for (Proxy proxy : proxyList) {
+ if (isDirect(proxy) || isNotUnresponsive(proxy)) {
+ result.add(proxy);
+ }
+ }
+ return result;
+ }
+
+ /*************************************************************************
+ * @param proxy
+ * @return
+ ************************************************************************/
+
+ private boolean isDirect(Proxy proxy) {
+ return Proxy.NO_PROXY.equals(proxy);
+ }
+
+ /*************************************************************************
+ * @param proxy
+ * @return
+ ************************************************************************/
+
+ private boolean isNotUnresponsive(Proxy proxy) {
+ Long lastFailTime = this.failedDelayCache.get(proxy.address());
+ return retryDelayHasPassedBy(lastFailTime);
+ }
+
+ /*************************************************************************
+ * @param lastFailTime
+ * @return
+ ************************************************************************/
+
+ private boolean retryDelayHasPassedBy(Long lastFailTime) {
+ return lastFailTime == null
+ || lastFailTime + this.retryAfterMs < System.currentTimeMillis();
+ }
+
+ /*************************************************************************
+ * Only used for unit testing not part of the public API.
+ * @param retryAfterMs The retryAfterMs to set.
+ ************************************************************************/
+
+ final void setRetryAfterMs(long retryAfterMs) {
+ this.retryAfterMs = retryAfterMs;
+ }
+
+
+
+}
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;
+ }
+ }
+
+}
diff --git a/src/main/java/com/btr/proxy/selector/whitelist/DefaultWhiteListParser.java b/src/main/java/com/btr/proxy/selector/whitelist/DefaultWhiteListParser.java
new file mode 100644
index 0000000..0f949e1
--- /dev/null
+++ b/src/main/java/com/btr/proxy/selector/whitelist/DefaultWhiteListParser.java
@@ -0,0 +1,78 @@
+package com.btr.proxy.selector.whitelist;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.btr.proxy.selector.whitelist.HostnameFilter.Mode;
+import com.btr.proxy.util.UriFilter;
+
+/*****************************************************************************
+ * Default implementation for an white list parser. This will support the most
+ * common forms of filters found in white lists.
+ * The white list is a comma (or space) separated list of domain names or IP addresses.
+ * The following section shows some examples.
+ *
+ * .mynet.com - Filters all host names ending with .mynet.com
+ * *.mynet.com - Filters all host names ending with .mynet.com
+ * www.mynet.* - Filters all host names starting with www.mynet.
+ * 123.12.32.1 - Filters the IP 123.12.32.1
+ * 123.12.32.1/255 - Filters the IP range
+ * http://www.mynet.com - Filters only HTTP protocol not FTP and no HTTPS
+ *
+ * Example of a list:
+ *
+ * .mynet.com, *.my-other-net.org, 123.55.23.222, 123.55.23.0/24
+ *
+ * Some info about this topic can be found here:
+ * http://kb.mozillazine.org/No_proxy_for
+ * http://technet.microsoft.com/en-us/library/dd361953.aspx
+ *
+ * Note that this implementation does not cover all variations of all browsers
+ * but should cover the most used formats.
+ *
+ * @author Bernd Rosstauscher (proxyvole@rosstauscher.de) Copyright 2009
+ ****************************************************************************/
+
+public class DefaultWhiteListParser implements WhiteListParser {
+
+ /*************************************************************************
+ * parseWhiteList
+ * @see com.btr.proxy.selector.whitelist.WhiteListParser#parseWhiteList(java.lang.String)
+ ************************************************************************/
+
+ public List<UriFilter> parseWhiteList(String whiteList) {
+ List<UriFilter> result = new ArrayList<UriFilter>();
+
+ String[] token = whiteList.split("[, ]+");
+ for (int i = 0; i < token.length; i++) {
+ String tkn = token[i].trim();
+ if (isIP4SubnetFilter(tkn)) {
+ result.add(new IpRangeFilter(tkn));
+ continue;
+ } else
+ if (tkn.endsWith("*")) {
+ tkn = tkn.substring(0, tkn.length()-1);
+ result.add(new HostnameFilter(Mode.BEGINS_WITH, tkn));
+ continue;
+ } else
+ if (tkn.trim().startsWith("*")) {
+ tkn = tkn.substring(1);
+ result.add(new HostnameFilter(Mode.ENDS_WITH, tkn));
+ } else {
+ result.add(new HostnameFilter(Mode.ENDS_WITH, tkn));
+ }
+ }
+
+ return result;
+ }
+
+ /*************************************************************************
+ * @param token
+ * @return
+ ************************************************************************/
+
+ private boolean isIP4SubnetFilter(String token) {
+ return IPv4WithSubnetChecker.isValid(token);
+ }
+
+}
diff --git a/src/main/java/com/btr/proxy/selector/whitelist/HostnameFilter.java b/src/main/java/com/btr/proxy/selector/whitelist/HostnameFilter.java
new file mode 100644
index 0000000..176eaeb
--- /dev/null
+++ b/src/main/java/com/btr/proxy/selector/whitelist/HostnameFilter.java
@@ -0,0 +1,93 @@
+package com.btr.proxy.selector.whitelist;
+
+import java.net.URI;
+import com.btr.proxy.util.UriFilter;
+
+/*****************************************************************************
+ * Tests if a host name of a given URI matches some criteria.
+ *
+ * @author Bernd Rosstauscher (proxyvole@rosstauscher.de) Copyright 2009
+ ****************************************************************************/
+
+public class HostnameFilter implements UriFilter {
+
+ private static final String PROTOCOL_ENDING = "://";
+
+ public enum Mode {BEGINS_WITH, ENDS_WITH, REGEX}
+
+ private String matchTo;
+ private String protocolFilter;
+ private Mode mode;
+
+ /*************************************************************************
+ * Constructor
+ * @param mode the filter mode.
+ * @param matchTo the match criteria.
+ ************************************************************************/
+
+ public HostnameFilter(Mode mode, String matchTo) {
+ super();
+ this.mode = mode;
+ this.matchTo = matchTo.toLowerCase();
+
+ extractProtocolFilter();
+ }
+
+ /*************************************************************************
+ * Extracts the protocol if one is given to initialize the protocol matcher.
+ ************************************************************************/
+
+ private void extractProtocolFilter() {
+ int protocolIndex = this.matchTo.indexOf(PROTOCOL_ENDING);
+ if (protocolIndex != -1) {
+ this.protocolFilter = this.matchTo.substring(0, protocolIndex);
+ this.matchTo = this.matchTo.substring(protocolIndex+PROTOCOL_ENDING.length());
+ }
+ }
+
+ /*************************************************************************
+ * accept
+ * @see com.btr.proxy.util.UriFilter#accept(java.net.URI)
+ ************************************************************************/
+
+ public boolean accept(URI uri) {
+ if (uri == null || uri.getAuthority() == null) {
+ return false;
+ }
+
+ if (!isProtocolMatching(uri)) {
+ return false;
+ }
+
+ String host = uri.getAuthority();
+
+ // Strip away port.
+ int index = host.indexOf(':');
+ if (index != -1) {
+ host = host.substring(0, index);
+ }
+
+ switch (this.mode) {
+ case BEGINS_WITH :
+ return host.toLowerCase().startsWith(this.matchTo);
+ case ENDS_WITH :
+ return host.toLowerCase().endsWith(this.matchTo);
+ case REGEX :
+ return host.toLowerCase().matches(this.matchTo);
+ }
+ return false;
+ }
+
+ /*************************************************************************
+ * Applies the protocol filter if available to see if we have a match.
+ * @param uri to test for a correct protocol.
+ * @return true if passed else false.
+ ************************************************************************/
+
+ private boolean isProtocolMatching(URI uri) {
+ return this.protocolFilter == null
+ || uri.getScheme() == null
+ || uri.getScheme().equalsIgnoreCase(this.protocolFilter);
+ }
+
+}
diff --git a/src/main/java/com/btr/proxy/selector/whitelist/IPv4WithSubnetChecker.java b/src/main/java/com/btr/proxy/selector/whitelist/IPv4WithSubnetChecker.java
new file mode 100644
index 0000000..b8e713e
--- /dev/null
+++ b/src/main/java/com/btr/proxy/selector/whitelist/IPv4WithSubnetChecker.java
@@ -0,0 +1,29 @@
+package com.btr.proxy.selector.whitelist;
+
+import java.util.regex.Pattern;
+
+/*****************************************************************************
+ * Checks if the given string is a IP4 range subnet definition
+ * of the format 192.168.0/24
+ * Based on a contribution by Jan Engler
+ * @author Bernd Rosstauscher (proxyvole@rosstauscher.de) Copyright 2009
+ ****************************************************************************/
+
+public class IPv4WithSubnetChecker {
+
+ private static Pattern IP_SUB_PATTERN = Pattern.compile(
+ "^([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\."
+ + "([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\."
+ + "([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\."
+ + "([01]?\\d\\d?|2[0-4]\\d|25[0-5])/(\\d|([12]\\d|3[0-2]))$");
+
+ /*************************************************************************
+ * Tests if a given string is of in the correct format for an IP4 subnet mask.
+ * @param possibleIPAddress to test for valid format.
+ * @return true if valid else false.
+ ************************************************************************/
+
+ public static boolean isValid(String possibleIPAddress) {
+ return IP_SUB_PATTERN.matcher(possibleIPAddress).matches();
+ }
+}
diff --git a/src/main/java/com/btr/proxy/selector/whitelist/IpRangeFilter.java b/src/main/java/com/btr/proxy/selector/whitelist/IpRangeFilter.java
new file mode 100644
index 0000000..293f520
--- /dev/null
+++ b/src/main/java/com/btr/proxy/selector/whitelist/IpRangeFilter.java
@@ -0,0 +1,83 @@
+package com.btr.proxy.selector.whitelist;
+
+import java.net.InetAddress;
+import java.net.URI;
+import java.net.UnknownHostException;
+
+import com.btr.proxy.util.UriFilter;
+
+/*****************************************************************************
+ * Filters an URI by inspecting it's IP address is in a given range.
+ * The range as must be defined in CIDR notation.
+ * e.g. 192.0.2.1/24,
+ *
+ * @author Bernd Rosstauscher (proxyvole@rosstauscher.de) Copyright 2009
+ ****************************************************************************/
+
+public class IpRangeFilter implements UriFilter {
+
+ private byte[] matchTo;
+ int numOfBits;
+
+ /*************************************************************************
+ * Constructor
+ * @param matchTo the match subnet in CIDR notation.
+ ************************************************************************/
+
+ public IpRangeFilter(String matchTo) {
+ super();
+
+ String[] parts = matchTo.split("/");
+ if (parts.length != 2) {
+ throw new IllegalArgumentException("IP range is not valid:"+matchTo);
+ }
+
+ try {
+ InetAddress address = InetAddress.getByName(parts[0].trim());
+ this.matchTo = address.getAddress();
+ } catch (UnknownHostException e) {
+ throw new IllegalArgumentException("IP range is not valid:"+matchTo);
+ }
+
+ this.numOfBits = Integer.parseInt(parts[1].trim());
+ }
+
+ /*************************************************************************
+ * accept
+ * @see com.btr.proxy.util.UriFilter#accept(java.net.URI)
+ ************************************************************************/
+
+ public boolean accept(URI uri) {
+ if (uri == null || uri.getHost() == null) {
+ return false;
+ }
+ try {
+ InetAddress address = InetAddress.getByName(uri.getHost());
+ byte[] addr = address.getAddress();
+
+ // Comparing IP6 against IP4?
+ if (addr.length != this.matchTo.length) {
+ return false;
+ }
+
+ int bit = 0;
+ for (int nibble = 0; nibble < addr.length; nibble++) {
+ for (int nibblePos = 7; nibblePos >= 0; nibblePos--) {
+ int mask = 1 << nibblePos;
+ if ((this.matchTo[nibble] & mask) != (addr[nibble] & mask)) {
+ return false;
+ }
+ bit++;
+ if (bit >= this.numOfBits) {
+ return true;
+ }
+ }
+ }
+
+ } catch (UnknownHostException e) {
+ // In this case we can not get the IP do not match.
+ }
+ return false;
+ }
+
+}
diff --git a/src/main/java/com/btr/proxy/selector/whitelist/ProxyBypassListSelector.java b/src/main/java/com/btr/proxy/selector/whitelist/ProxyBypassListSelector.java
new file mode 100644
index 0000000..81e2a7d
--- /dev/null
+++ b/src/main/java/com/btr/proxy/selector/whitelist/ProxyBypassListSelector.java
@@ -0,0 +1,84 @@
+package com.btr.proxy.selector.whitelist;
+
+import java.io.IOException;
+import java.net.Proxy;
+import java.net.ProxySelector;
+import java.net.SocketAddress;
+import java.net.URI;
+import java.util.List;
+
+import com.btr.proxy.util.ProxyUtil;
+import com.btr.proxy.util.UriFilter;
+
+/*****************************************************************************
+ * Special purpose ProxySelector used as Facade on top of a normal ProxySelector.
+ * A wrapper that will first check the URI against a white list and if it matches
+ * it will return DIRECT else it will pass the URI to an delegate for inspection.
+ *
+ * @author Bernd Rosstauscher (proxyvole@rosstauscher.de) Copyright 2009
+ ****************************************************************************/
+
+public class ProxyBypassListSelector extends ProxySelector {
+
+ private ProxySelector delegate;
+ private List<UriFilter> whiteListFilter;
+
+
+ /*************************************************************************
+ * Constructor
+ * @param whiteListFilter a list of filters for whitelist URLs.
+ * @param proxySelector the proxy selector to use.
+ ************************************************************************/
+
+ public ProxyBypassListSelector(List<UriFilter> whiteListFilter, ProxySelector proxySelector) {
+ super();
+ if (whiteListFilter == null) {
+ throw new NullPointerException("Whitelist must not be null.");
+ }
+ if (proxySelector == null) {
+ throw new NullPointerException("ProxySelector must not be null.");
+ }
+
+ this.delegate = proxySelector;
+ this.whiteListFilter = whiteListFilter;
+ }
+
+
+ /*************************************************************************
+ * Constructor
+ * @param whiteList a list of filters for whitelist URLs as comma/space separated string.
+ * @param proxySelector the proxy selector to use.
+ ************************************************************************/
+
+ public ProxyBypassListSelector(String whiteList, ProxySelector proxySelector) {
+ this(new DefaultWhiteListParser().parseWhiteList(whiteList), proxySelector);
+ }
+
+ /*************************************************************************
+ * 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) {
+ this.delegate.connectFailed(uri, sa, ioe);
+ }
+
+ /*************************************************************************
+ * select
+ * @see java.net.ProxySelector#select(java.net.URI)
+ ************************************************************************/
+
+ @Override
+ public List<Proxy> select(URI uri) {
+
+ // If in white list, use DIRECT connection.
+ for (UriFilter filter : this.whiteListFilter) {
+ if (filter.accept(uri)) {
+ return ProxyUtil.noProxyList();
+ }
+ }
+
+ return this.delegate.select(uri);
+ }
+
+}
diff --git a/src/main/java/com/btr/proxy/selector/whitelist/UseProxyWhiteListSelector.java b/src/main/java/com/btr/proxy/selector/whitelist/UseProxyWhiteListSelector.java
new file mode 100644
index 0000000..1aface9
--- /dev/null
+++ b/src/main/java/com/btr/proxy/selector/whitelist/UseProxyWhiteListSelector.java
@@ -0,0 +1,74 @@
+package com.btr.proxy.selector.whitelist;
+
+import java.io.IOException;
+import java.net.Proxy;
+import java.net.ProxySelector;
+import java.net.SocketAddress;
+import java.net.URI;
+import java.util.List;
+
+import com.btr.proxy.util.ProxyUtil;
+import com.btr.proxy.util.UriFilter;
+
+/*****************************************************************************
+ * Special purpose ProxySelector used as Facade on top of a normal ProxySelector.
+ * A wrapper that will first check the URI against a white list and if it matches
+ * it will use a proxy as provided by the delegate ProxySelector else it will
+ * return DIRECT.
+ *
+ * @author Bernd Rosstauscher (proxyvole@rosstauscher.de) Copyright 2009
+ ****************************************************************************/
+
+public class UseProxyWhiteListSelector extends ProxySelector {
+
+ private ProxySelector delegate;
+ private List<UriFilter> whiteListFilter;
+
+ /*************************************************************************
+ * Constructor
+ * @param proxySelector the proxy selector to use.
+ ************************************************************************/
+
+ public UseProxyWhiteListSelector(String whiteList, ProxySelector proxySelector) {
+ super();
+ if (whiteList == null) {
+ throw new NullPointerException("Whitelist must not be null.");
+ }
+ if (proxySelector == null) {
+ throw new NullPointerException("ProxySelector must not be null.");
+ }
+
+ this.delegate = proxySelector;
+
+ WhiteListParser parser = new DefaultWhiteListParser();
+ this.whiteListFilter = parser.parseWhiteList(whiteList);
+ }
+
+ /*************************************************************************
+ * 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) {
+ this.delegate.connectFailed(uri, sa, ioe);
+ }
+
+ /*************************************************************************
+ * select
+ * @see java.net.ProxySelector#select(java.net.URI)
+ ************************************************************************/
+
+ @Override
+ public List<Proxy> select(URI uri) {
+
+ // If in white list, use proxy selector.
+ for (UriFilter filter : this.whiteListFilter) {
+ if (filter.accept(uri)) {
+ return this.delegate.select(uri);
+ }
+ }
+
+ return ProxyUtil.noProxyList();
+ }
+
+}
diff --git a/src/main/java/com/btr/proxy/selector/whitelist/WhiteListParser.java b/src/main/java/com/btr/proxy/selector/whitelist/WhiteListParser.java
new file mode 100644
index 0000000..7cd5fd5
--- /dev/null
+++ b/src/main/java/com/btr/proxy/selector/whitelist/WhiteListParser.java
@@ -0,0 +1,24 @@
+package com.btr.proxy.selector.whitelist;
+
+import java.util.List;
+
+import com.btr.proxy.util.UriFilter;
+
+/*****************************************************************************
+ * Interface for an white list parser. This will take an white list string and
+ * parse it into a list of UriFilter rules.
+ *
+ * @author Bernd Rosstauscher (proxyvole@rosstauscher.de) Copyright 2009
+ ****************************************************************************/
+
+public interface WhiteListParser {
+
+ /*************************************************************************
+ * Parses a list of host name and IP filters into UriFilter objects.
+ * @param whiteList the string to parse.
+ * @return a list of UriFilters
+ ************************************************************************/
+
+ public List<UriFilter> parseWhiteList(String whiteList);
+
+}