-
+
-
+
+
diff --git a/core/src/main/java/com/threerings/getdown/data/Application.java b/core/src/main/java/com/threerings/getdown/data/Application.java
index ef282d6..2d0d352 100644
--- a/core/src/main/java/com/threerings/getdown/data/Application.java
+++ b/core/src/main/java/com/threerings/getdown/data/Application.java
@@ -484,7 +484,7 @@ public class Application
String suffix = _trackingURLSuffix == null ? "" : _trackingURLSuffix;
String ga = getGATrackingCode();
return _trackingURL == null ? null :
- new URL(_trackingURL + encodePath(event + suffix + ga));
+ HostWhitelist.INSTANCE.verify(new URL(_trackingURL + encodePath(event + suffix + ga)));
} catch (MalformedURLException mue) {
log.warning("Invalid tracking URL", "path", _trackingURL, "event", event, "error", mue);
return null;
@@ -586,7 +586,7 @@ public class Application
// if we are a versioned deployment, create a versioned appbase
try {
- _vappbase = (_version < 0) ? new URL(_appbase) : createVAppBase(_version);
+ _vappbase = createVAppBase(_version);
} catch (MalformedURLException mue) {
String err = MessageUtil.tcompose("m.invalid_appbase", _appbase);
throw (IOException) new IOException(err).initCause(mue);
@@ -601,7 +601,7 @@ public class Application
latest = SysProps.replaceDomain(latest);
}
try {
- _latest = new URL(latest);
+ _latest = HostWhitelist.INSTANCE.verify(new URL(latest));
} catch (MalformedURLException mue) {
log.warning("Invalid URL for latest attribute.", mue);
}
@@ -1408,7 +1408,8 @@ public class Application
protected URL createVAppBase (long version)
throws MalformedURLException
{
- return new URL(_appbase.replace("%VERSION%", "" + version));
+ String url = version < 0 ? _appbase : _appbase.replace("%VERSION%", "" + version);
+ return HostWhitelist.INSTANCE.verify(new URL(url));
}
/**
diff --git a/core/src/main/java/com/threerings/getdown/data/Build.java.tmpl b/core/src/main/java/com/threerings/getdown/data/Build.java.tmpl
index d3fdccf..60a8ff3 100644
--- a/core/src/main/java/com/threerings/getdown/data/Build.java.tmpl
+++ b/core/src/main/java/com/threerings/getdown/data/Build.java.tmpl
@@ -5,6 +5,11 @@
package com.threerings.getdown.data;
+import java.util.Arrays;
+import java.util.List;
+
+import com.threerings.getdown.util.StringUtil;
+
/**
* Contains static data provided during the build process.
*/
@@ -19,4 +24,20 @@ public class Build {
public static String version () {
return "@build_version@";
}
+
+ /**
+ * The hosts which Getdown is allowed to communicate with. An empty list indicates that
+ * no whitelist is configured and there are no limitations. By default, no host whitelist
+ * is added to the binary, so it can be used to download and run applications from any
+ * server.
+ *
+ *
To create a custom Getdown build that can only talk to whitelisted servers, set
+ * the {@code getdown.host.whitelist} property on the command line while building the JAR
+ * (e.g. {@code mvn package -Dgetdown.host.whitelist=my.server.com}). Wildcards can be used
+ * (e.g. {@code *.mycompany.com}) and multiple values can be separated by commas
+ * (e.g. {@code app1.foo.com,app2.bar.com,app3.baz.com}).
+ */
+ public static List hostWhitelist () {
+ return Arrays.asList(StringUtil.parseStringArray("@host_whitelist@"));
+ }
}
diff --git a/core/src/main/java/com/threerings/getdown/util/Config.java b/core/src/main/java/com/threerings/getdown/util/Config.java
index ed7303b..8a34b1b 100644
--- a/core/src/main/java/com/threerings/getdown/util/Config.java
+++ b/core/src/main/java/com/threerings/getdown/util/Config.java
@@ -10,6 +10,8 @@ import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
+import java.net.MalformedURLException;
+import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
@@ -17,8 +19,6 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
-import com.threerings.getdown.util.StringUtil;
-
import static com.threerings.getdown.Log.log;
/**
@@ -320,11 +320,21 @@ public class Config
public String getUrl (String name, String def)
{
String value = getString(name + "." + Locale.getDefault().getLanguage());
- if (!StringUtil.isBlank(value)) {
- return value;
+ if (StringUtil.isBlank(value)) {
+ value = getString(name);
}
- value = getString(name);
- return StringUtil.isBlank(value) ? def : value;
+ if (StringUtil.isBlank(value)) {
+ value = def;
+ }
+ if (!StringUtil.isBlank(value)) {
+ try {
+ HostWhitelist.INSTANCE.verify(new URL(value));
+ } catch (MalformedURLException e) {
+ log.warning("Invalid URL.", "url", value, e);
+ value = null;
+ }
+ }
+ return value;
}
/**
diff --git a/core/src/main/java/com/threerings/getdown/util/HostWhitelist.java b/core/src/main/java/com/threerings/getdown/util/HostWhitelist.java
new file mode 100644
index 0000000..c44c3af
--- /dev/null
+++ b/core/src/main/java/com/threerings/getdown/util/HostWhitelist.java
@@ -0,0 +1,68 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2016 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.util;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Collections;
+import java.util.List;
+
+import com.threerings.getdown.data.Build;
+
+/**
+ * Optional support for compiling a URL host whitelist into the Getdown JAR.
+ * Useful if you're on the paranoid end of the security spectrum.
+ *
+ * @see Build#hostWhitelist()
+ */
+public final class HostWhitelist
+{
+ public static final HostWhitelist INSTANCE = new HostWhitelist();
+
+ private final List _hosts;
+
+ HostWhitelist ()
+ {
+ this(Build.hostWhitelist());
+ }
+
+ HostWhitelist (List hosts)
+ {
+ _hosts = Collections.unmodifiableList(hosts);
+ }
+
+ /**
+ * Verifies that the specified URL should be accessible, per this host whitelist.
+ * If the URL should not be accessible, this method throws a {@link MalformedURLException}.
+ * If the URL should be accessible, this method simply returns the {@link URL} passed in.
+ */
+ public final URL verify (URL url) throws MalformedURLException
+ {
+ if (url == null || _hosts.isEmpty()) {
+ // either there is no URL to validate or no whitelist was configured
+ return url;
+ }
+
+ String urlHost = url.getHost();
+
+ for (String host : _hosts) {
+ String regex = host.replace(".", "\\.").replace("*", ".*");
+ if (urlHost.matches(regex)) {
+ return url;
+ }
+ }
+
+ throw new MalformedURLException("The host for the specified URL (" + url
+ + ") is not in the host whitelist: " + _hosts);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString()
+ {
+ return _hosts.toString();
+ }
+}
diff --git a/core/src/test/java/com/threerings/getdown/util/HostWhitelistTest.java b/core/src/test/java/com/threerings/getdown/util/HostWhitelistTest.java
new file mode 100644
index 0000000..c8fc24f
--- /dev/null
+++ b/core/src/test/java/com/threerings/getdown/util/HostWhitelistTest.java
@@ -0,0 +1,158 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2016 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.util;
+
+import static org.junit.Assert.assertEquals;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Arrays;
+
+import org.junit.Test;
+
+/**
+ * Tests {@link HostWhitelist}.
+ */
+public class HostWhitelistTest
+{
+ @Test
+ public void testVerify () throws MalformedURLException
+ {
+ checkCanVerify("foo.com", "http://foo.com", true);
+ checkCanVerify("foo.com", "http://foo.com/", true);
+ checkCanVerify("foo.com", "http://foo.com/x/y/z", true);
+ checkCanVerify("foo.com", "http://www.foo.com", false);
+ checkCanVerify("foo.com", "http://www.foo.com/", false);
+ checkCanVerify("foo.com", "http://www.foo.com/x/y/z", false);
+ checkCanVerify("foo.com", "http://a.b.foo.com", false);
+ checkCanVerify("foo.com", "http://a.b.foo.com/", false);
+ checkCanVerify("foo.com", "http://a.b.foo.com/x/y/z", false);
+ checkCanVerify("foo.com", "http://oo.com", false);
+ checkCanVerify("foo.com", "http://f.oo.com", false);
+ checkCanVerify("foo.com", "http://a.f.oo.com", false);
+
+ checkCanVerify("*.foo.com", "http://foo.com", false);
+ checkCanVerify("*.foo.com", "http://foo.com/", false);
+ checkCanVerify("*.foo.com", "http://foo.com/x/y/z", false);
+ checkCanVerify("*.foo.com", "http://www.foo.com", true);
+ checkCanVerify("*.foo.com", "http://www.foo.com/", true);
+ checkCanVerify("*.foo.com", "http://www.foo.com/x/y/z", true);
+ checkCanVerify("*.foo.com", "http://a.b.foo.com", true);
+ checkCanVerify("*.foo.com", "http://a.b.foo.com/", true);
+ checkCanVerify("*.foo.com", "http://a.b.foo.com/x/y/z", true);
+ checkCanVerify("*.foo.com", "http://oo.com", false);
+ checkCanVerify("*.foo.com", "http://f.oo.com", false);
+ checkCanVerify("*.foo.com", "http://a.f.oo.com", false);
+
+ checkCanVerify("*.com", "http://foo.com", true);
+ checkCanVerify("*.com", "http://foo.com/", true);
+ checkCanVerify("*.com", "http://foo.com/x/y/z", true);
+ checkCanVerify("*.com", "http://www.foo.com", true);
+ checkCanVerify("*.com", "http://www.foo.com/", true);
+ checkCanVerify("*.com", "http://www.foo.com/x/y/z", true);
+ checkCanVerify("*.com", "http://a.b.foo.com", true);
+ checkCanVerify("*.com", "http://a.b.foo.com/", true);
+ checkCanVerify("*.com", "http://a.b.foo.com/x/y/z", true);
+ checkCanVerify("*.com", "http://oo.com", true);
+ checkCanVerify("*.com", "http://f.oo.com", true);
+ checkCanVerify("*.com", "http://a.f.oo.com", true);
+
+ checkCanVerify("*.net", "http://foo.com", false);
+ checkCanVerify("*.net", "http://foo.com/", false);
+ checkCanVerify("*.net", "http://foo.com/x/y/z", false);
+ checkCanVerify("*.net", "http://www.foo.com", false);
+ checkCanVerify("*.net", "http://www.foo.com/", false);
+ checkCanVerify("*.net", "http://www.foo.com/x/y/z", false);
+ checkCanVerify("*.net", "http://a.b.foo.com", false);
+ checkCanVerify("*.net", "http://a.b.foo.com/", false);
+ checkCanVerify("*.net", "http://a.b.foo.com/x/y/z", false);
+ checkCanVerify("*.net", "http://oo.com", false);
+ checkCanVerify("*.net", "http://f.oo.com", false);
+ checkCanVerify("*.net", "http://a.f.oo.com", false);
+
+ checkCanVerify("www.*.com", "http://foo.com", false);
+ checkCanVerify("www.*.com", "http://foo.com/", false);
+ checkCanVerify("www.*.com", "http://foo.com/x/y/z", false);
+ checkCanVerify("www.*.com", "http://www.foo.com", true);
+ checkCanVerify("www.*.com", "http://www.foo.com/", true);
+ checkCanVerify("www.*.com", "http://www.foo.com/x/y/z", true);
+ checkCanVerify("www.*.com", "http://a.b.foo.com", false);
+ checkCanVerify("www.*.com", "http://a.b.foo.com/", false);
+ checkCanVerify("www.*.com", "http://a.b.foo.com/x/y/z", false);
+ checkCanVerify("www.*.com", "http://oo.com", false);
+ checkCanVerify("www.*.com", "http://f.oo.com", false);
+ checkCanVerify("www.*.com", "http://a.f.oo.com", false);
+ checkCanVerify("www.*.com", "http://www.a.f.oo.com", true);
+
+ checkCanVerify("foo.*", "http://foo.com", true);
+ checkCanVerify("foo.*", "http://foo.com/", true);
+ checkCanVerify("foo.*", "http://foo.com/x/y/z", true);
+ checkCanVerify("foo.*", "http://www.foo.com", false);
+ checkCanVerify("foo.*", "http://www.foo.com/", false);
+ checkCanVerify("foo.*", "http://www.foo.com/x/y/z", false);
+ checkCanVerify("foo.*", "http://a.b.foo.com", false);
+ checkCanVerify("foo.*", "http://a.b.foo.com/", false);
+ checkCanVerify("foo.*", "http://a.b.foo.com/x/y/z", false);
+ checkCanVerify("foo.*", "http://oo.com", false);
+ checkCanVerify("foo.*", "http://f.oo.com", false);
+ checkCanVerify("foo.*", "http://a.f.oo.com", false);
+
+ checkCanVerify("*.foo.*", "http://foo.com", false);
+ checkCanVerify("*.foo.*", "http://foo.com/", false);
+ checkCanVerify("*.foo.*", "http://foo.com/x/y/z", false);
+ checkCanVerify("*.foo.*", "http://www.foo.com", true);
+ checkCanVerify("*.foo.*", "http://www.foo.com/", true);
+ checkCanVerify("*.foo.*", "http://www.foo.com/x/y/z", true);
+ checkCanVerify("*.foo.*", "http://a.b.foo.com", true);
+ checkCanVerify("*.foo.*", "http://a.b.foo.com/", true);
+ checkCanVerify("*.foo.*", "http://a.b.foo.com/x/y/z", true);
+ checkCanVerify("*.foo.*", "http://oo.com", false);
+ checkCanVerify("*.foo.*", "http://f.oo.com", false);
+ checkCanVerify("*.foo.*", "http://a.f.oo.com", false);
+
+ checkCanVerify("127.0.0.1", "http://127.0.0.1", true);
+ checkCanVerify("127.0.0.1", "http://127.0.0.1/", true);
+ checkCanVerify("127.0.0.1", "http://127.0.0.1/x/y/z", true);
+ checkCanVerify("*.0.0.1", "http://127.0.0.1/abc", true);
+ checkCanVerify("127.*.0.1", "http://127.0.0.1/abc", true);
+ checkCanVerify("127.0.*.1", "http://127.0.0.1/abc", true);
+ checkCanVerify("127.0.0.*", "http://127.0.0.1/abc", true);
+ checkCanVerify("127.*.1", "http://127.0.0.1/abc", true);
+ checkCanVerify("*.0.1", "http://127.0.0.1/abc", true);
+ checkCanVerify("127.0.*", "http://127.0.0.1/abc", true);
+ checkCanVerify("*", "http://127.0.0.1/abc", true);
+ checkCanVerify("127.0.0.2", "http://127.0.0.1", false);
+ checkCanVerify("127.0.2.1", "http://127.0.0.1", false);
+ checkCanVerify("127.2.0.1", "http://127.0.0.1", false);
+ checkCanVerify("222.0.0.1", "http://127.0.0.1", false);
+
+ checkCanVerify("", "http://foo.com", true);
+ checkCanVerify("", "http://aaa.bbb.net/xyz", true);
+ checkCanVerify("", "https://127.0.0.1/abc", true);
+
+ checkCanVerify("aaa.bbb.com,xxx.yyy.com, *.jjj.net", "http://aaa.bbb.com/m", true);
+ checkCanVerify("aaa.bbb.com, xxx.yyy.com,*.jjj.net", "http://xxx.yyy.com/n", true);
+ checkCanVerify("aaa.bbb.com,xxx.yyy.com, *.jjj.net", "http://www.jjj.net/o", true);
+ }
+
+ private static void checkCanVerify (String whitelist, String url, boolean expectedToPass)
+ throws MalformedURLException
+ {
+ HostWhitelist w = new HostWhitelist(Arrays.asList(StringUtil.parseStringArray(whitelist)));
+ URL u = new URL(url);
+ boolean passed;
+
+ try {
+ w.verify(u);
+ passed = true;
+ } catch (MalformedURLException e) {
+ passed = false;
+ }
+
+ assertEquals("with whitelist '" + whitelist + "' and URL '" + url + "'",
+ expectedToPass, passed);
+ }
+}