diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e2a029..c0b2310 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ * Fixed issues with proxy information not getting properly passed through to app. Via [#216](//github.com/threerings/getdown/pull/216). +* `appbase` and `latest` properties in `getdown.txt` now process env var subtitutions. + +* Added support for [Proxy Auto-config](https://en.wikipedia.org/wiki/Proxy_auto-config) via PAC + files. + +* Proxy handling can now recover from credentials going out of date. It will detect the error and + ask for updated credentials. + +* Added `try_no_proxy` system property. This instructs Getdown to always first try to run without a + proxy, regardless of whether it has been configured to use a proxy in the past. And if it can run + without a proxy, it does so for that session, but retains the proxy config for future sessions in + which the proxy may again be needed. + ## 1.8.4 - May 14, 2019 * Added `verify_timeout` config to allow customization of the default (60 second) timeout during diff --git a/ant/src/main/java/com/threerings/getdown/tools/DigesterTask.java b/ant/src/main/java/com/threerings/getdown/tools/DigesterTask.java index 48cc8d4..76212ae 100644 --- a/ant/src/main/java/com/threerings/getdown/tools/DigesterTask.java +++ b/ant/src/main/java/com/threerings/getdown/tools/DigesterTask.java @@ -7,16 +7,13 @@ package com.threerings.getdown.tools; import java.io.File; import java.io.IOException; - import java.security.GeneralSecurityException; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.Task; -import com.threerings.getdown.data.Digest; - /** - * An ant task used to create a digest.txt for a Getdown + * An ant task used to create a {@code digest.txt} for a Getdown * application deployment. */ public class DigesterTask extends Task diff --git a/core/src/it/java/com/threerings/getdown/tests/DigesterIT.java b/core/src/it/java/com/threerings/getdown/tests/DigesterIT.java index 52b4b5e..d2ddaf2 100644 --- a/core/src/it/java/com/threerings/getdown/tests/DigesterIT.java +++ b/core/src/it/java/com/threerings/getdown/tests/DigesterIT.java @@ -5,16 +5,16 @@ package com.threerings.getdown.tests; -import java.io.File; import java.nio.charset.StandardCharsets; -import java.nio.file.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Arrays; import java.util.List; -import org.junit.*; -import static org.junit.Assert.*; - import com.threerings.getdown.tools.Digester; +import org.junit.Test; +import static org.junit.Assert.assertEquals; public class DigesterIT { @@ -32,23 +32,23 @@ public class DigesterIT { Files.delete(digest2); assertEquals(Arrays.asList( - "getdown.txt = 779c74fb4b251e18faf6e240a0667964", + "getdown.txt = 9c9b2494929c99d44ae51034d59e1a1b", "testapp.jar = 404dafa55e78b25ec0e3a936357b1883", "funny%test dir/some=file.txt = d8e8fca2dc0f896fd7cb4cb0031ba249", "crazyhashfile#txt = f29d23fd5ab1781bd8d0760b3a516f16", "foo.jar = 46ca4cc9079d9d019bb30cd21ebbc1ec", "script.sh = f66e8ea25598e67e99c47d9b0b2a2cdf", - "digest.txt = f5561d85e4d80cc85883963897e58ff6" + "digest.txt = 11f9ba349cf9edacac4d72a3158447e5" ), digestLines); assertEquals(Arrays.asList( - "getdown.txt = 4f0c657895c3c3a35fa55bf5951c64fa9b0694f8fc685af3f1d8635c639e066b", + "getdown.txt = 1efecfae2a189002a6658f17d162b1922c7bde978944949276dc038a0df2461f", "testapp.jar = c9cb1906afbf48f8654b416c3f831046bd3752a76137e5bf0a9af2f790bf48e0", "funny%test dir/some=file.txt = f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd2", "crazyhashfile#txt = 6816889f922de38f145db215a28ad7c5e1badf7354b5cdab225a27486789fa3b", "foo.jar = ea188b872e0496debcbe00aaadccccb12a8aa9b025bb62c130cd3d9b8540b062", "script.sh = cca1c5c7628d9bf7533f655a9cfa6573d64afb8375f81960d1d832dc5135c988", - "digest2.txt = 70b442c9f56660561921da3368e1a206f05c379182fab3062750b7ddcf303407" + "digest2.txt = 41eacdabda8909bdbbf61e4f980867f4003c16a12f6770e6fc619b6af100e05b" ), digest2Lines); } } diff --git a/core/src/it/resources/testapp/getdown.txt b/core/src/it/resources/testapp/getdown.txt index 3e0e538..ab0e473 100644 --- a/core/src/it/resources/testapp/getdown.txt +++ b/core/src/it/resources/testapp/getdown.txt @@ -13,6 +13,18 @@ apparg = %APPDIR% # test the %env% mechanism jvmarg = -Dusername=\%ENV.USER% +# test various java_*** configs, they are not interesting for digester +java_local_dir = jre +java_max_version = 1089999 +java_min_version = [windows] 1080111 +java_min_version = [!windows] 1080192 +java_exact_version_required = [linux] true +java_location = [linux-amd64] /files/java/java_linux_64.zip +java_location = [linux-i386] /files/java/java_linux_32.zip +java_location = [mac] /files/java/java_mac_64.zip +java_location = [windows-amd64] /files/java/java_windows_64.zip +java_location = [windows-x86] /files/java/java_windows_32.zip + strict_comments = true resource = funny%test dir/some=file.txt resource = crazyhashfile#txt diff --git a/core/src/main/java/com/threerings/getdown/Log.java b/core/src/main/java/com/threerings/getdown/Log.java index d00a131..da98c90 100644 --- a/core/src/main/java/com/threerings/getdown/Log.java +++ b/core/src/main/java/com/threerings/getdown/Log.java @@ -15,7 +15,7 @@ import java.util.logging.*; /** * A placeholder class that contains a reference to the log object used by the Getdown code. */ -public class Log +public final class Log { public static class Shim { /** @@ -143,6 +143,5 @@ public class Log protected FieldPosition _fpos = new FieldPosition(SimpleDateFormat.DATE_FIELD); } - protected static final String DATE_FORMAT = "{0,date} {0,time}"; protected static final Level[] LEVELS = {Level.FINE, Level.INFO, Level.WARNING, Level.SEVERE}; } diff --git a/core/src/main/java/com/threerings/getdown/cache/GarbageCollector.java b/core/src/main/java/com/threerings/getdown/cache/GarbageCollector.java index ae18990..7e01e87 100644 --- a/core/src/main/java/com/threerings/getdown/cache/GarbageCollector.java +++ b/core/src/main/java/com/threerings/getdown/cache/GarbageCollector.java @@ -96,6 +96,6 @@ public class GarbageCollector private static File getCachedFile (File file) { return !isLastAccessedFile(file) ? file : new File( - file.getParentFile(), file.getName().substring(0, file.getName().lastIndexOf("."))); + file.getParentFile(), file.getName().substring(0, file.getName().lastIndexOf('.'))); } } diff --git a/core/src/main/java/com/threerings/getdown/cache/ResourceCache.java b/core/src/main/java/com/threerings/getdown/cache/ResourceCache.java index 0210e9a..41f0c5f 100644 --- a/core/src/main/java/com/threerings/getdown/cache/ResourceCache.java +++ b/core/src/main/java/com/threerings/getdown/cache/ResourceCache.java @@ -69,7 +69,7 @@ public class ResourceCache private String getFileSuffix (File fileToCache) { String fileName = fileToCache.getName(); - int index = fileName.lastIndexOf("."); + int index = fileName.lastIndexOf('.'); return index > -1 ? fileName.substring(index) : ""; } 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 06eab4d..a1deca4 100644 --- a/core/src/main/java/com/threerings/getdown/data/Application.java +++ b/core/src/main/java/com/threerings/getdown/data/Application.java @@ -7,7 +7,6 @@ package com.threerings.getdown.data; import java.io.*; import java.lang.reflect.Method; -import java.net.InetSocketAddress; import java.net.MalformedURLException; import java.net.Proxy; import java.net.URL; @@ -23,8 +22,8 @@ import java.util.*; import java.util.concurrent.*; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.zip.GZIPInputStream; +import com.threerings.getdown.net.Connector; import com.threerings.getdown.util.*; // avoid ambiguity with java.util.Base64 which we can't use as it's 1.8+ import com.threerings.getdown.util.Base64; @@ -33,7 +32,7 @@ import static com.threerings.getdown.Log.log; import static java.nio.charset.StandardCharsets.UTF_8; /** - * Parses and provide access to the information contained in the getdown.txt + * Parses and provide access to the information contained in the {@code getdown.txt} * configuration file. */ public class Application @@ -212,10 +211,10 @@ public class Application * Used by {@link #verifyMetadata} to communicate status in circumstances where it needs to * take network actions. */ - public static interface StatusDisplay + public interface StatusDisplay { /** Requests that the specified status message be displayed. */ - public void updateStatus (String message); + void updateStatus (String message); } /** @@ -233,19 +232,56 @@ public class Application } } - /** The proxy that should be used to do HTTP downloads. This must be configured prior to using - * the application instance. Yes this is a public mutable field, no I'm not going to create a + /** + * Reads the {@code getdown.txt} config file into a {@code Config} object and returns it. + */ + public static Config readConfig (EnvConfig envc, boolean checkPlatform) throws IOException { + Config config = null; + File cfgfile = new File(envc.appDir, CONFIG_FILE); + Config.ParseOpts opts = Config.createOpts(checkPlatform); + try { + // if we have a configuration file, read the data from it + if (cfgfile.exists()) { + config = Config.parseConfig(cfgfile, opts); + } + // otherwise, try reading data from our backup config file; thanks to funny windows + // bullshit, we have to do this backup file fiddling in case we got screwed while + // updating getdown.txt during normal operation + else if ((cfgfile = new File(envc.appDir, Application.CONFIG_FILE + "_old")).exists()) { + config = Config.parseConfig(cfgfile, opts); + } + // otherwise, issue a warning that we found no getdown file + else { + log.info("Found no getdown.txt file", "appdir", envc.appDir); + } + } catch (Exception e) { + log.warning("Failure reading config file", "file", config, e); + } + + // if we failed to read our config file, check for an appbase specified via a system + // property; we can use that to bootstrap ourselves back into operation + if (config == null) { + log.info("Using 'appbase' from bootstrap config", "appbase", envc.appBase); + Map cdata = new HashMap<>(); + cdata.put("appbase", envc.appBase); + config = new Config(cdata); + } + + return config; + } + + /** A helper that is used to do HTTP downloads. This must be configured prior to using the + * application instance. Yes this is a public mutable field, no I'm not going to create a * getter and setter just to pretend like that's not the case. */ - public Proxy proxy = Proxy.NO_PROXY; + public Connector conn = Connector.DEFAULT; /** - * Creates an application instance which records the location of the getdown.txt + * Creates an application instance which records the location of the {@code getdown.txt} * configuration file from the supplied application directory. * */ public Application (EnvConfig envc) { _envc = envc; - _config = new File(envc.appDir, CONFIG_FILE); } /** @@ -369,8 +405,7 @@ public class Application */ public List getActiveCodeResources () { - ArrayList codes = new ArrayList<>(); - codes.addAll(getCodeResources()); + List codes = new ArrayList<>(getCodeResources()); for (AuxGroup aux : getAuxGroups()) { if (isAuxGroupActive(aux.name)) { codes.addAll(aux.codes); @@ -398,8 +433,7 @@ public class Application */ public List getActiveResources () { - ArrayList rsrcs = new ArrayList<>(); - rsrcs.addAll(getResources()); + List rsrcs = new ArrayList<>(getResources()); for (AuxGroup aux : getAuxGroups()) { if (isAuxGroupActive(aux.name)) { rsrcs.addAll(aux.rsrcs); @@ -544,44 +578,22 @@ public class Application * @exception IOException thrown if there is an error reading the file or an error encountered * during its parsing. */ - public Config init (boolean checkPlatform) - throws IOException + public Config init (boolean checkPlatform) throws IOException { - Config config = null; - File cfgfile = _config; - Config.ParseOpts opts = Config.createOpts(checkPlatform); - try { - // if we have a configuration file, read the data from it - if (cfgfile.exists()) { - config = Config.parseConfig(_config, opts); - } - // otherwise, try reading data from our backup config file; thanks to funny windows - // bullshit, we have to do this backup file fiddling in case we got screwed while - // updating getdown.txt during normal operation - else if ((cfgfile = getLocalPath(CONFIG_FILE + "_old")).exists()) { - config = Config.parseConfig(cfgfile, opts); - } - // otherwise, issue a warning that we found no getdown file - else { - log.info("Found no getdown.txt file", "appdir", getAppDir()); - } - } catch (Exception e) { - log.warning("Failure reading config file", "file", config, e); - } - - // if we failed to read our config file, check for an appbase specified via a system - // property; we can use that to bootstrap ourselves back into operation - if (config == null) { - String appbase = _envc.appBase; - log.info("Using 'appbase' from bootstrap config", "appbase", appbase); - Map cdata = new HashMap<>(); - cdata.put("appbase", appbase); - config = new Config(cdata); - } - - // extract our version information - _version = config.getLong("version", -1L); + Config config = readConfig(_envc, checkPlatform); + initBase(config); + initJava(config); + initTracking(config); + initResources(config); + initArgs(config); + return config; + } + /** + * Reads the basic config info from {@code config} into this instance. This includes things + * like the appbase and version. + */ + public void initBase (Config config) throws IOException { // first determine our application base, this way if anything goes wrong later in the // process, our caller can use the appbase to download a new configuration file _appbase = config.getString("appbase"); @@ -589,25 +601,29 @@ public class Application throw new RuntimeException("m.missing_appbase"); } - // check if we're overriding the domain in the appbase + // check if we're overriding the domain in the appbase, and sub envvars _appbase = processArg(SysProps.overrideAppbase(_appbase)); // make sure there's a trailing slash if (!_appbase.endsWith("/")) { - _appbase = _appbase + "/"; + _appbase += "/"; } + // extract our version information + _version = config.getLong("version", -1L); + // if we are a versioned deployment, create a versioned appbase try { _vappbase = createVAppBase(_version); } catch (MalformedURLException mue) { String err = MessageUtil.tcompose("m.invalid_appbase", _appbase); - throw (IOException) new IOException(err).initCause(mue); + throw new IOException(err, mue); } // check for a latest config URL - String latest = processArg(config.getString("latest")); + String latest = config.getString("latest"); if (latest != null) { + latest = processArg(latest); if (latest.startsWith(_appbase)) { latest = _appbase + latest.substring(_appbase.length()); } else { @@ -620,20 +636,28 @@ public class Application } } - String appPrefix = _envc.appId == null ? "" : (_envc.appId + "."); - - // determine our application class name (use app-specific class _if_ one is provided) - _class = config.getString("class"); - if (appPrefix.length() > 0) { - _class = config.getString(appPrefix + "class", _class); - } - if (_class == null) { - throw new IOException("m.missing_class"); - } - // determine whether we want strict comments _strictComments = config.getBoolean("strict_comments"); + // determine whether we want to allow offline operation (defaults to false) + _allowOffline = config.getBoolean("allow_offline"); + + // whether to cache code resources and launch from cache + _useCodeCache = config.getBoolean("use_code_cache"); + _codeCacheRetentionDays = config.getInt("code_cache_retention_days", 7); + + // maximum simultaneous downloads + _maxConcDownloads = Math.max(1, config.getInt("max_concurrent_downloads", + SysProps.threadPoolSize())); + + _verifyTimeout = config.getInt("verify_timeout", 60); + } + + /** + * Reads the JVM requirements from {@code config} into this instance. This includes things like + * the min and max java version, location of a locally installed JRE, etc. + */ + public void initJava (Config config) { // check to see if we're using a custom java.version property and regex _javaVersionProp = config.getString("java_version_prop", _javaVersionProp); _javaVersionRegex = config.getString("java_version_regex", _javaVersionRegex); @@ -648,17 +672,16 @@ public class Application // check to see if we require a particular JVM version and have a supplied JVM _javaExactVersionRequired = config.getBoolean("java_exact_version_required"); - // this is a little weird, but when we're run from the digester, we see a String[] which - // contains java locations for all platforms which we can't grok, but the digester doesn't - // need to know about that; when we're run in a real application there will be only one! - Object javaloc = config.getRaw("java_location"); - if (javaloc instanceof String) { - _javaLocation = (String)javaloc; - } + _javaLocation = config.getString("java_location"); // used only in conjunction with java_location _javaLocalDir = getLocalPath(config.getString("java_local_dir", LaunchUtil.LOCAL_JAVA_DIR)); + } + /** + * Reads the install tracking info from {@code config} into this instance. + */ + public void initTracking (Config config) { // determine whether we have any tracking configuration _trackingURL = config.getString("tracking_url"); @@ -683,14 +706,16 @@ public class Application // Some app may need to generate google analytics code _trackingGAHash = config.getString("tracking_ga_hash"); + } + /** + * Reads the app resource info from {@code config} into this instance. + */ + public void initResources (Config config) throws IOException { // clear our arrays as we may be reinitializing _codes.clear(); _resources.clear(); _auxgroups.clear(); - _jvmargs.clear(); - _appargs.clear(); - _txtJvmArgs.clear(); // parse our code resources if (config.getMultiValue("code") == null && @@ -709,10 +734,10 @@ public class Application // parse our auxiliary resource groups for (String auxgroup : config.getList("auxgroups")) { - ArrayList codes = new ArrayList<>(); + List codes = new ArrayList<>(); parseResources(config, auxgroup + ".code", Resource.NORMAL, codes); parseResources(config, auxgroup + ".ucode", Resource.UNPACK, codes); - ArrayList rsrcs = new ArrayList<>(); + List rsrcs = new ArrayList<>(); parseResources(config, auxgroup + ".resource", Resource.NORMAL, rsrcs); parseResources(config, auxgroup + ".xresource", Resource.EXEC, rsrcs); parseResources(config, auxgroup + ".uresource", Resource.UNPACK, rsrcs); @@ -720,21 +745,38 @@ public class Application parseResources(config, auxgroup + ".nresource", Resource.NATIVE, rsrcs); _auxgroups.put(auxgroup, new AuxGroup(auxgroup, codes, rsrcs)); } + } + + /** + * Reads the command line arg info from {@code config} into this instance. + */ + public void initArgs (Config config) throws IOException { + _jvmargs.clear(); + _appargs.clear(); + _txtJvmArgs.clear(); + + String appPrefix = _envc.appId == null ? "" : (_envc.appId + "."); + + // determine our application class name (use app-specific class _if_ one is provided) + _class = config.getString("class"); + if (appPrefix.length() > 0) { + _class = config.getString(appPrefix + "class", _class); + } + if (_class == null) { + throw new IOException("m.missing_class"); + } // transfer our JVM arguments (we include both "global" args and app_id-prefixed args) - String[] jvmargs = config.getMultiValue("jvmarg"); - addAll(jvmargs, _jvmargs); + addAll(config.getMultiValue("jvmarg"), _jvmargs); if (appPrefix.length() > 0) { - jvmargs = config.getMultiValue(appPrefix + "jvmarg"); - addAll(jvmargs, _jvmargs); + addAll(config.getMultiValue(appPrefix + "jvmarg"), _jvmargs); } // get the set of optimum JVM arguments _optimumJvmArgs = config.getMultiValue("optimum_jvmarg"); // transfer our application arguments - String[] appargs = config.getMultiValue(appPrefix + "apparg"); - addAll(appargs, _appargs); + addAll(config.getMultiValue(appPrefix + "apparg"), _appargs); // add the launch specific application arguments _appargs.addAll(_envc.appArgs); @@ -742,27 +784,13 @@ public class Application // look for custom arguments fillAssignmentListFromPairs("extra.txt", _txtJvmArgs); - // determine whether we want to allow offline operation (defaults to false) - _allowOffline = config.getBoolean("allow_offline"); - - // look for a debug.txt file which causes us to run in java.exe on Windows so that we can - // obtain a thread dump of the running JVM - _windebug = getLocalPath("debug.txt").exists(); - - // whether to cache code resources and launch from cache - _useCodeCache = config.getBoolean("use_code_cache"); - _codeCacheRetentionDays = config.getInt("code_cache_retention_days", 7); - - // maximum simultaneous downloads - _maxConcDownloads = Math.max(1, config.getInt("max_concurrent_downloads", - SysProps.threadPoolSize())); - // extract some info used to configure our child process on macOS _dockName = config.getString("ui.name"); _dockIconPath = config.getString("ui.mac_dock_icon", "../desktop.icns"); - _verifyTimeout = config.getInt("verify_timeout", 60); - return config; + // look for a debug.txt file which causes us to run in java.exe on Windows so that we can + // obtain a thread dump of the running JVM + _windebug = getLocalPath("debug.txt").exists(); } /** @@ -791,8 +819,7 @@ public class Application * Returns a URL from which the specified path can be fetched. Our application base URL is * properly versioned and combined with the supplied path. */ - public URL getRemoteURL (String path) - throws MalformedURLException + public URL getRemoteURL (String path) throws MalformedURLException { return new URL(_vappbase, encodePath(path)); } @@ -882,7 +909,7 @@ public class Application } /** - * Attempts to redownload the getdown.txt file based on information parsed from a + * Attempts to redownload the {@code getdown.txt} file based on information parsed from a * previous call to {@link #init}. */ public void attemptRecovery (StatusDisplay status) @@ -893,7 +920,7 @@ public class Application } /** - * Downloads and replaces the getdown.txt and digest.txt files with + * Downloads and replaces the {@code getdown.txt} and {@code digest.txt} files with * those for the target version of our application. */ public void updateMetadata () @@ -904,7 +931,7 @@ public class Application _vappbase = createVAppBase(_targetVersion); } catch (MalformedURLException mue) { String err = MessageUtil.tcompose("m.invalid_appbase", _appbase); - throw (IOException) new IOException(err).initCause(mue); + throw new IOException(err, mue); } try { @@ -961,16 +988,8 @@ public class Application args.add("-Xdock:name=" + _dockName); } - // pass along our proxy settings - if (proxy.type() == Proxy.Type.HTTP && proxy.address() instanceof InetSocketAddress) { - InetSocketAddress proxyAddr = (InetSocketAddress) proxy.address(); - String proxyHost = proxyAddr.getHostString(); - int proxyPort = proxyAddr.getPort(); - args.add("-Dhttp.proxyHost=" + proxyHost); - args.add("-Dhttp.proxyPort=" + proxyPort); - args.add("-Dhttps.proxyHost=" + proxyHost); - args.add("-Dhttps.proxyPort=" + proxyPort); - } + // forward our proxy settings + conn.addProxyArgs(args); // add the marker indicating the app is running in getdown args.add("-D" + Properties.GETDOWN + "=true"); @@ -1080,7 +1099,7 @@ public class Application for (String jvmarg : _jvmargs) { if (jvmarg.startsWith("-D")) { jvmarg = processArg(jvmarg.substring(2)); - int eqidx = jvmarg.indexOf("="); + int eqidx = jvmarg.indexOf('='); if (eqidx == -1) { log.warning("Bogus system property: '" + jvmarg + "'?"); } else { @@ -1121,10 +1140,6 @@ public class Application /** Replaces the application directory and version in any argument. */ protected String processArg (String arg) { - if (arg == null) { - return null; - } - arg = arg.replace("%APPDIR%", getAppDir().getAbsolutePath()); arg = arg.replace("%VERSION%", String.valueOf(_version)); @@ -1146,8 +1161,8 @@ public class Application } /** - * Loads the digest.txt file and verifies the contents of both that file and the - * getdown.text file. Then it loads the version.txt and decides + * Loads the {@code digest.txt} file and verifies the contents of both that file and the + * {@code getdown.text} file. Then it loads the {@code version.txt} and decides * whether or not the application needs to be updated or whether we can proceed to verification * and execution. * @@ -1234,11 +1249,11 @@ public class Application } if (_latest != null) { - try (InputStream in = ConnectionUtil.open(proxy, _latest, 0, 0).getInputStream(); - InputStreamReader reader = new InputStreamReader(in, UTF_8); - BufferedReader bin = new BufferedReader(reader)) { - for (String[] pair : Config.parsePairs(bin, Config.createOpts(false))) { - if (pair[0].equals("version")) { + try { + List vdata = Config.parsePairs( + new StringReader(conn.fetch(_latest)), Config.createOpts(false)); + for (String[] pair : vdata) { + if ("version".equals(pair[0])) { _targetVersion = Math.max(Long.parseLong(pair[1]), _targetVersion); if (fileVersion != -1 && _targetVersion > fileVersion) { // replace the file with the newest version @@ -1444,7 +1459,7 @@ public class Application protected URL createVAppBase (long version) throws MalformedURLException { - String url = version < 0 ? _appbase : _appbase.replace("%VERSION%", "" + version); + String url = version < 0 ? _appbase : _appbase.replace("%VERSION%", String.valueOf(version)); return HostWhitelist.verify(new URL(url)); } @@ -1461,8 +1476,7 @@ public class Application /** * Downloads a new copy of CONFIG_FILE. */ - protected void downloadConfigFile () - throws IOException + protected void downloadConfigFile () throws IOException { downloadControlFile(CONFIG_FILE, 0); } @@ -1604,8 +1618,7 @@ public class Application * Download a path to a temporary file, returning a {@link File} instance with the path * contents. */ - protected File downloadFile (String path) - throws IOException + protected File downloadFile (String path) throws IOException { File target = getLocalPath(path + "_new"); @@ -1615,30 +1628,11 @@ public class Application } catch (Exception e) { log.warning("Requested to download invalid control file", "appbase", _vappbase, "path", path, "error", e); - throw (IOException) new IOException("Invalid path '" + path + "'.").initCause(e); + throw new IOException("Invalid path '" + path + "'.", e); } log.info("Attempting to refetch '" + path + "' from '" + targetURL + "'."); - - // stream the URL into our temporary file - URLConnection uconn = ConnectionUtil.open(proxy, targetURL, 0, 0); - // we have to tell Java not to use caches here, otherwise it will cache any request for - // same URL for the lifetime of this JVM (based on the URL string, not the URL object); - // if the getdown.txt file, for example, changes in the meanwhile, we would never hear - // about it; turning off caches is not a performance concern, because when Getdown asks - // to download a file, it expects it to come over the wire, not from a cache - uconn.setUseCaches(false); - uconn.setRequestProperty("Accept-Encoding", "gzip"); - try (InputStream fin = uconn.getInputStream()) { - String encoding = uconn.getContentEncoding(); - boolean gzip = "gzip".equalsIgnoreCase(encoding); - try (InputStream fin2 = (gzip ? new GZIPInputStream(fin) : fin)) { - try (FileOutputStream fout = new FileOutputStream(target)) { - StreamUtil.copy(fin2, fout); - } - } - } - + conn.download(targetURL, target); // stream the URL into our temporary file return target; } @@ -1652,9 +1646,7 @@ public class Application /** Helper function to add all values in {@code values} (if non-null) to {@code target}. */ protected static void addAll (String[] values, List target) { if (values != null) { - for (String value : values) { - target.add(value); - } + Collections.addAll(target, values); } } @@ -1737,7 +1729,6 @@ public class Application } protected final EnvConfig _envc; - protected File _config; protected Digest _digest; protected long _version = -1; diff --git a/core/src/main/java/com/threerings/getdown/data/Digest.java b/core/src/main/java/com/threerings/getdown/data/Digest.java index bc8d140..e310a52 100644 --- a/core/src/main/java/com/threerings/getdown/data/Digest.java +++ b/core/src/main/java/com/threerings/getdown/data/Digest.java @@ -21,7 +21,7 @@ import static com.threerings.getdown.Log.log; import static java.nio.charset.StandardCharsets.UTF_8; /** - * Manages the digest.txt file and the computing and processing of digests for an + * Manages the {@code digest.txt} file and the computing and processing of digests for an * application. */ public class Digest @@ -72,8 +72,7 @@ public class Digest digests.put(rsrc, rsrc.computeDigest(fversion, md, null)); completed.add(rsrc); } catch (Throwable t) { - completed.add(new IOException("Error computing digest for: " + rsrc). - initCause(t)); + completed.add(new IOException("Error computing digest for: " + rsrc, t)); } } }); @@ -88,7 +87,7 @@ public class Digest if (done instanceof IOException) { throw (IOException)done; } else if (done instanceof Resource) { - pending.remove((Resource)done); + pending.remove(done); } else { throw new AssertionError("What is this? " + done); } diff --git a/core/src/main/java/com/threerings/getdown/data/EnvConfig.java b/core/src/main/java/com/threerings/getdown/data/EnvConfig.java index 1d66ebb..a14b02c 100644 --- a/core/src/main/java/com/threerings/getdown/data/EnvConfig.java +++ b/core/src/main/java/com/threerings/getdown/data/EnvConfig.java @@ -19,7 +19,7 @@ public final class EnvConfig { /** Used to report problems or feedback by {@link #create}. */ public static final class Note { - public static enum Level { INFO, WARN, ERROR }; + public enum Level { INFO, WARN, ERROR } public static Note info (String msg) { return new Note(Level.INFO, msg); } public static Note warn (String msg) { return new Note(Level.WARN, msg); } public static Note error (String msg) { return new Note(Level.ERROR, msg); } diff --git a/core/src/main/java/com/threerings/getdown/data/Resource.java b/core/src/main/java/com/threerings/getdown/data/Resource.java index c7c380c..d1ccba3 100644 --- a/core/src/main/java/com/threerings/getdown/data/Resource.java +++ b/core/src/main/java/com/threerings/getdown/data/Resource.java @@ -23,7 +23,7 @@ import static com.threerings.getdown.Log.log; public class Resource implements Comparable { /** Defines special attributes for resources. */ - public static enum Attr { + public enum Attr { /** Indicates that the resource should be unpacked. */ UNPACK, /** If present, when unpacking a resource, any directories created by the newly @@ -35,7 +35,7 @@ public class Resource implements Comparable PRELOAD, /** Indicates that the resource is a jar containing native libs. */ NATIVE - }; + } public static final EnumSet NORMAL = EnumSet.noneOf(Attr.class); public static final EnumSet UNPACK = EnumSet.of(Attr.UNPACK); diff --git a/core/src/main/java/com/threerings/getdown/data/SysProps.java b/core/src/main/java/com/threerings/getdown/data/SysProps.java index 0d96ecb..1de9cd4 100644 --- a/core/src/main/java/com/threerings/getdown/data/SysProps.java +++ b/core/src/main/java/com/threerings/getdown/data/SysProps.java @@ -16,7 +16,7 @@ import com.threerings.getdown.util.VersionUtil; * accessor so that it's easy to see all of the secret system property arguments that Getdown makes * use of. */ -public class SysProps +public final class SysProps { /** Configures the appdir (in lieu of passing it in argv). Usage: {@code -Dappdir=foo}. */ public static String appDir () { @@ -101,6 +101,16 @@ public class SysProps return Boolean.getBoolean("direct"); } + /** If true, Getdown will always try to connect without proxy settings even it a proxy is set + * in {@code proxy.txt}. If direct access is possible it will not clear {@code proxy.txt}, it + * will preserve the settings. This is to support cases where a user uses a workstation in two + * different networks, one with proxy the other one without. They should not be asked for + * proxy settings again each time they switch back to the proxy network. + * Usage: {@code -Dtry_no_proxy}. */ + public static boolean tryNoProxyFirst () { + return Boolean.getBoolean("try_no_proxy"); + } + /** Specifies the connection timeout (in seconds) to use when downloading control files from * the server. This is chiefly useful when you are running in versionless mode and want Getdown * to more quickly timeout its startup update check if the server with which it is diff --git a/core/src/main/java/com/threerings/getdown/net/Connector.java b/core/src/main/java/com/threerings/getdown/net/Connector.java new file mode 100644 index 0000000..9e74787 --- /dev/null +++ b/core/src/main/java/com/threerings/getdown/net/Connector.java @@ -0,0 +1,182 @@ +// +// Getdown - application installer, patcher and launcher +// Copyright (C) 2004-2018 Getdown authors +// https://github.com/threerings/getdown/blob/master/LICENSE + +package com.threerings.getdown.net; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLDecoder; +import java.util.List; +import java.util.zip.GZIPInputStream; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.threerings.getdown.data.SysProps; +import com.threerings.getdown.util.Base64; +import com.threerings.getdown.util.StreamUtil; + +/** + * Manages the process of making HTTP connections, using a proxy if necessary. Tracks and reports + * when proxy credentials are rejected. + */ +public class Connector { + + /** The default connector uses no proxy. */ + public static final Connector DEFAULT = new Connector(Proxy.NO_PROXY); + + /** Tracks the state of a connector. If it fails for proxy-related reasons, it may transition + * to a need_proxy or need_proxy_auth state. */ + public enum State { ACTIVE, NEED_PROXY, NEED_PROXY_AUTH } + + /** The proxy used by this connector. */ + public final Proxy proxy; + + /** The current state of this connector. */ + public State state = State.ACTIVE; + + public Connector (Proxy proxy) { + this.proxy = proxy; + } + + /** + * Opens a connection to a URL, setting the authentication header if user info is present. + * @param proxy the proxy via which to perform HTTP connections. + * @param url the URL to which to open a connection. + * @param connectTimeout if {@code > 0} then a timeout, in seconds, to use when opening the + * connection. If {@code 0} is supplied, the connection timeout specified via system properties + * will be used instead. + * @param readTimeout if {@code > 0} then a timeout, in seconds, to use while reading data from + * the connection. If {@code 0} is supplied, the read timeout specified via system properties + * will be used instead. + */ + public URLConnection open (URL url, int connectTimeout, int readTimeout) + throws IOException + { + URLConnection conn = url.openConnection(proxy); + + // configure a connect timeout, if requested + int ctimeout = connectTimeout > 0 ? connectTimeout : SysProps.connectTimeout(); + if (ctimeout > 0) { + conn.setConnectTimeout(ctimeout * 1000); + } + + // configure a read timeout, if requested + int rtimeout = readTimeout > 0 ? readTimeout : SysProps.readTimeout(); + if (rtimeout > 0) { + conn.setReadTimeout(rtimeout * 1000); + } + + // If URL has a username:password@ before hostname, use HTTP basic auth + String userInfo = url.getUserInfo(); + if (userInfo != null) { + // Remove any percent-encoding in the username/password + userInfo = URLDecoder.decode(userInfo, "UTF-8"); + // Now base64 encode the auth info and make it a single line + String encoded = Base64.encodeToString(userInfo.getBytes(UTF_8), Base64.DEFAULT). + replaceAll("\\n","").replaceAll("\\r", ""); + conn.setRequestProperty("Authorization", "Basic " + encoded); + } + + return conn; + } + + /** + * Opens a connection to a http or https URL, setting the authentication header if user info is + * present. Throws a class cast exception if the connection returned is not the right type. See + * {@link #open} for parameter documentation. + */ + public HttpURLConnection openHttp (URL url, int connectTimeout, int readTimeout) + throws IOException + { + return (HttpURLConnection)open(url, connectTimeout, readTimeout); + } + + /** + * Downloads {@code url} into {@code target}. + */ + public void download (URL url, File target) throws IOException { + URLConnection conn = open(url, 0, 0); + // we have to tell Java not to use caches here, otherwise it will cache any request for + // same URL for the lifetime of this JVM (based on the URL string, not the URL object); + // if the getdown.txt file, for example, changes in the meanwhile, we would never hear + // about it; turning off caches is not a performance concern, because when Getdown asks + // to download a file, it expects it to come over the wire, not from a cache + conn.setUseCaches(false); + conn.setRequestProperty("Accept-Encoding", "gzip"); + checkConnectOK(conn, "Unable to download " + url); + try (InputStream fin = conn.getInputStream()) { + String encoding = conn.getContentEncoding(); + boolean gzip = "gzip".equalsIgnoreCase(encoding); + try (InputStream fin2 = (gzip ? new GZIPInputStream(fin) : fin)) { + try (FileOutputStream fout = new FileOutputStream(target)) { + StreamUtil.copy(fin2, fout); + } + } + } + } + + /** + * Fetches the data at {@code url} into a string. + */ + public String fetch (URL url) throws IOException { + URLConnection conn = open(url, 0, 0); + checkConnectOK(conn, "Unable to fetch " + url); + int size = conn.getContentLength(); + ByteArrayOutputStream out = new ByteArrayOutputStream(size > 0 ? size : 1024); + try (InputStream in = conn.getInputStream()) { + StreamUtil.copy(in, out); + } + return out.toString(UTF_8.toString()); + } + + /** + * Checks that {@code conn} returned an {@code OK} response code iff it is an HTTP connection. + * If the connection failed for proxy related reasons, this changes the state of this connector + * to reflect the needed proxy information. + */ + public void checkConnectOK (URLConnection conn, String errpre) throws IOException + { + // if it's not an HTTP connection, there's nothing to check + if (!(conn instanceof HttpURLConnection)) return; + + int code = ((HttpURLConnection)conn).getResponseCode(); + switch (code) { + case HttpURLConnection.HTTP_OK: + return; + case HttpURLConnection.HTTP_FORBIDDEN: + case HttpURLConnection.HTTP_USE_PROXY: + state = State.NEED_PROXY; + break; + case HttpURLConnection.HTTP_PROXY_AUTH: + state = State.NEED_PROXY_AUTH; + break; + } + throw new IOException(errpre + " [code=" + code + "]"); + } + + /** + * Adds appropriate proxy args from this connector's configuration to the supplied list of + * command line args that will be used to launch the app. + */ + public void addProxyArgs (List args) { + if (proxy.type() == Proxy.Type.HTTP && proxy.address() instanceof InetSocketAddress) { + InetSocketAddress proxyAddr = (InetSocketAddress) proxy.address(); + String proxyHost = proxyAddr.getHostString(); + int proxyPort = proxyAddr.getPort(); + args.add("-Dhttp.proxyHost=" + proxyHost); + args.add("-Dhttp.proxyPort=" + proxyPort); + args.add("-Dhttps.proxyHost=" + proxyHost); + args.add("-Dhttps.proxyPort=" + proxyPort); + } + } +} diff --git a/core/src/main/java/com/threerings/getdown/net/Downloader.java b/core/src/main/java/com/threerings/getdown/net/Downloader.java index 6033e2f..505958a 100644 --- a/core/src/main/java/com/threerings/getdown/net/Downloader.java +++ b/core/src/main/java/com/threerings/getdown/net/Downloader.java @@ -6,7 +6,13 @@ package com.threerings.getdown.net; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URLConnection; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; import java.util.Collection; import java.util.HashMap; @@ -26,8 +32,13 @@ import static com.threerings.getdown.Log.log; * implementors must take care to only execute thread-safe code or simply pass a message to the AWT * thread, for example. */ -public abstract class Downloader +public class Downloader { + public Downloader (Connector conn) + { + _conn = conn; + } + /** * Start the downloading process. * @param resources the resources to download. @@ -132,7 +143,22 @@ public abstract class Downloader /** * Performs the protocol-specific portion of checking download size. */ - protected abstract long checkSize (Resource rsrc) throws IOException; + protected long checkSize (Resource rsrc) throws IOException { + URLConnection conn = _conn.open(rsrc.getRemote(), 0, 0); + try { + // if we're accessing our data via HTTP, we only need a HEAD request + if (conn instanceof HttpURLConnection) { + ((HttpURLConnection)conn).setRequestMethod("HEAD"); + } + // make sure we got a satisfactory response code + _conn.checkConnectOK(conn, "Unable to check up-to-date for " + rsrc.getRemote()); + return conn.getContentLength(); + + } finally { + // let it be known that we're done with this connection + conn.getInputStream().close(); + } + } /** * Periodically called by the protocol-specific downloaders to update their progress. This @@ -203,13 +229,62 @@ public abstract class Downloader * protocol-specific code. This method should periodically check whether {@code _state} is set * to aborted and abort any in-progress download if so. */ - protected abstract void download (Resource rsrc) throws IOException; + protected void download (Resource rsrc) throws IOException { + URLConnection conn = _conn.open(rsrc.getRemote(), 0, 0); + // make sure we got a satisfactory response code + _conn.checkConnectOK(conn, "Unable to download resource " + rsrc.getRemote()); + + // TODO: make FileChannel download impl (below) robust and allow apps to opt-into it via a + // system property + if (true) { + // download the resource from the specified URL + long actualSize = conn.getContentLength(); + log.info("Downloading resource", "url", rsrc.getRemote(), "size", actualSize); + long currentSize = 0L; + byte[] buffer = new byte[4*4096]; + try (InputStream in = conn.getInputStream(); + FileOutputStream out = new FileOutputStream(rsrc.getLocalNew())) { + + // TODO: look to see if we have a download info file + // containing info on potentially partially downloaded data; + // if so, use a "Range: bytes=HAVE-" header. + + // read in the file data + int read; + while ((read = in.read(buffer)) != -1) { + // abort the download if the downloader is aborted + if (_state == State.ABORTED) { + break; + } + // write it out to our local copy + out.write(buffer, 0, read); + // note that we've downloaded some data + currentSize += read; + reportProgress(rsrc, currentSize, actualSize); + } + } + + } else { + log.info("Downloading resource", "url", rsrc.getRemote(), "size", "unknown"); + File localNew = rsrc.getLocalNew(); + try (ReadableByteChannel rbc = Channels.newChannel(conn.getInputStream()); + FileOutputStream fos = new FileOutputStream(localNew)) { + // TODO: more work is needed here, transferFrom can fail to transfer the entire + // file, in which case it's not clear what we're supposed to do.. call it again? + // will it repeatedly fail? + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + reportProgress(rsrc, localNew.length(), localNew.length()); + } + } + } + + protected final Connector _conn; /** The reported sizes of our resources. */ - protected Map _sizes = new HashMap<>(); + protected final Map _sizes = new HashMap<>(); /** The bytes downloaded for each resource. */ - protected Map _downloaded = new HashMap<>(); + protected final Map _downloaded = new HashMap<>(); /** The time at which the file transfer began. */ protected long _start; diff --git a/core/src/main/java/com/threerings/getdown/net/HTTPDownloader.java b/core/src/main/java/com/threerings/getdown/net/HTTPDownloader.java deleted file mode 100644 index a7a3287..0000000 --- a/core/src/main/java/com/threerings/getdown/net/HTTPDownloader.java +++ /dev/null @@ -1,115 +0,0 @@ -// -// Getdown - application installer, patcher and launcher -// Copyright (C) 2004-2018 Getdown authors -// https://github.com/threerings/getdown/blob/master/LICENSE - -package com.threerings.getdown.net; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.Proxy; -import java.net.URL; -import java.net.URLConnection; -import java.nio.channels.Channels; -import java.nio.channels.ReadableByteChannel; - -import com.threerings.getdown.data.Resource; -import com.threerings.getdown.util.ConnectionUtil; - -import static com.threerings.getdown.Log.log; - -/** - * Implements downloading files over HTTP - */ -public class HTTPDownloader extends Downloader -{ - public HTTPDownloader (Proxy proxy) - { - _proxy = proxy; - } - - @Override protected long checkSize (Resource rsrc) throws IOException - { - URLConnection conn = ConnectionUtil.open(_proxy, rsrc.getRemote(), 0, 0); - try { - // if we're accessing our data via HTTP, we only need a HEAD request - if (conn instanceof HttpURLConnection) { - HttpURLConnection hcon = (HttpURLConnection)conn; - hcon.setRequestMethod("HEAD"); - hcon.connect(); - // make sure we got a satisfactory response code - if (hcon.getResponseCode() != HttpURLConnection.HTTP_OK) { - throw new IOException("Unable to check up-to-date for " + - rsrc.getRemote() + ": " + hcon.getResponseCode()); - } - } - return conn.getContentLength(); - - } finally { - // let it be known that we're done with this connection - conn.getInputStream().close(); - } - } - - @Override protected void download (Resource rsrc) throws IOException - { - // TODO: make FileChannel download impl (below) robust and allow apps to opt-into it via a - // system property - if (true) { - // download the resource from the specified URL - URLConnection conn = ConnectionUtil.open(_proxy, rsrc.getRemote(), 0, 0); - conn.connect(); - - // make sure we got a satisfactory response code - if (conn instanceof HttpURLConnection) { - HttpURLConnection hcon = (HttpURLConnection)conn; - if (hcon.getResponseCode() != HttpURLConnection.HTTP_OK) { - throw new IOException("Unable to download resource " + rsrc.getRemote() + ": " + - hcon.getResponseCode()); - } - } - long actualSize = conn.getContentLength(); - log.info("Downloading resource", "url", rsrc.getRemote(), "size", actualSize); - long currentSize = 0L; - byte[] buffer = new byte[4*4096]; - try (InputStream in = conn.getInputStream(); - FileOutputStream out = new FileOutputStream(rsrc.getLocalNew())) { - - // TODO: look to see if we have a download info file - // containing info on potentially partially downloaded data; - // if so, use a "Range: bytes=HAVE-" header. - - // read in the file data - int read; - while ((read = in.read(buffer)) != -1) { - // abort the download if the downloader is aborted - if (_state == State.ABORTED) { - break; - } - // write it out to our local copy - out.write(buffer, 0, read); - // note that we've downloaded some data - currentSize += read; - reportProgress(rsrc, currentSize, actualSize); - } - } - - } else { - log.info("Downloading resource", "url", rsrc.getRemote(), "size", "unknown"); - File localNew = rsrc.getLocalNew(); - try (ReadableByteChannel rbc = Channels.newChannel(rsrc.getRemote().openStream()); - FileOutputStream fos = new FileOutputStream(localNew)) { - // TODO: more work is needed here, transferFrom can fail to transfer the entire - // file, in which case it's not clear what we're supposed to do.. call it again? - // will it repeatedly fail? - fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); - reportProgress(rsrc, localNew.length(), localNew.length()); - } - } - } - - protected final Proxy _proxy; -} diff --git a/core/src/main/java/com/threerings/getdown/spi/ProxyAuth.java b/core/src/main/java/com/threerings/getdown/spi/ProxyAuth.java index 22446ec..8c6d841 100644 --- a/core/src/main/java/com/threerings/getdown/spi/ProxyAuth.java +++ b/core/src/main/java/com/threerings/getdown/spi/ProxyAuth.java @@ -11,7 +11,7 @@ package com.threerings.getdown.spi; public interface ProxyAuth { /** Credentials for a proxy server. */ - public static class Credentials { + class Credentials { public final String username; public final String password; public Credentials (String username, String password) { @@ -23,10 +23,10 @@ public interface ProxyAuth /** * Loads the credentials for the app installed in {@code appDir}. */ - public Credentials loadCredentials (String appDir); + Credentials loadCredentials (String appDir); /** * Encrypts and saves the credentials for the app installed in {@code appDir}. */ - public void saveCredentials (String appDir, String username, String password); + void saveCredentials (String appDir, String username, String password); } diff --git a/core/src/main/java/com/threerings/getdown/tools/Differ.java b/core/src/main/java/com/threerings/getdown/tools/Differ.java index 496f81c..4f8e50e 100644 --- a/core/src/main/java/com/threerings/getdown/tools/Differ.java +++ b/core/src/main/java/com/threerings/getdown/tools/Differ.java @@ -5,10 +5,17 @@ package com.threerings.getdown.tools; -import java.io.*; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.security.MessageDigest; import java.util.ArrayList; import java.util.Enumeration; +import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; @@ -31,8 +38,8 @@ public class Differ /** * Creates a single patch file that contains the differences between * the two specified application directories. The patch file will be - * created in the nvdir directory with name - * patchV.dat where V is the old application version. + * created in the {@code nvdir} directory with name + * {@code patchV.dat} where V is the old application version. */ public void createDiff (File nvdir, File ovdir, boolean verbose) throws IOException @@ -53,13 +60,13 @@ public class Differ Application oapp = new Application(new EnvConfig(ovdir)); oapp.init(false); - ArrayList orsrcs = new ArrayList<>(); + List orsrcs = new ArrayList<>(); orsrcs.addAll(oapp.getCodeResources()); orsrcs.addAll(oapp.getResources()); Application napp = new Application(new EnvConfig(nvdir)); napp.init(false); - ArrayList nrsrcs = new ArrayList<>(); + List nrsrcs = new ArrayList<>(); nrsrcs.addAll(napp.getCodeResources()); nrsrcs.addAll(napp.getResources()); @@ -83,8 +90,8 @@ public class Differ } } - protected void createPatch (File patch, ArrayList orsrcs, - ArrayList nrsrcs, boolean verbose) + protected void createPatch (File patch, List orsrcs, + List nrsrcs, boolean verbose) throws IOException { int version = Digest.VERSION; @@ -200,7 +207,7 @@ public class Differ Differ differ = new Differ(); boolean verbose = false; int aidx = 0; - if (args[0].equals("-verbose")) { + if ("-verbose".equals(args[0])) { verbose = true; aidx++; } diff --git a/core/src/main/java/com/threerings/getdown/tools/Digester.java b/core/src/main/java/com/threerings/getdown/tools/Digester.java index b04a653..ae61b43 100644 --- a/core/src/main/java/com/threerings/getdown/tools/Digester.java +++ b/core/src/main/java/com/threerings/getdown/tools/Digester.java @@ -23,6 +23,7 @@ import com.threerings.getdown.data.Digest; import com.threerings.getdown.data.EnvConfig; import com.threerings.getdown.data.Resource; import com.threerings.getdown.util.Base64; +import com.threerings.getdown.util.Config; import static java.nio.charset.StandardCharsets.UTF_8; @@ -74,8 +75,11 @@ public class Digester System.out.println("Generating digest file '" + target + "'..."); // create our application and instruct it to parse its business - Application app = new Application(new EnvConfig(appdir)); - app.init(false); + EnvConfig envc = new EnvConfig(appdir); + Application app = new Application(envc); + Config config = Application.readConfig(envc, false); + app.initBase(config); + app.initResources(config); List rsrcs = new ArrayList<>(); rsrcs.add(app.getConfigResource()); @@ -86,6 +90,9 @@ public class Digester rsrcs.addAll(ag.rsrcs); } + // reinit app just to verify that getdown.txt has valid format + app.init(true); + // now generate the digest file Digest.createDigest(version, rsrcs, target); } diff --git a/core/src/main/java/com/threerings/getdown/tools/JarDiff.java b/core/src/main/java/com/threerings/getdown/tools/JarDiff.java index 107b74e..f0db8ac 100644 --- a/core/src/main/java/com/threerings/getdown/tools/JarDiff.java +++ b/core/src/main/java/com/threerings/getdown/tools/JarDiff.java @@ -67,15 +67,15 @@ import static java.nio.charset.StandardCharsets.UTF_8; public class JarDiff implements JarDiffCodes { private static final int DEFAULT_READ_SIZE = 2048; - private static byte[] newBytes = new byte[DEFAULT_READ_SIZE]; - private static byte[] oldBytes = new byte[DEFAULT_READ_SIZE]; + private static final byte[] newBytes = new byte[DEFAULT_READ_SIZE]; + private static final byte[] oldBytes = new byte[DEFAULT_READ_SIZE]; // The JARDiff.java is the stand-alone jardiff.jar tool. Thus, we do not depend on Globals.java // and other stuff here. Instead, we use an explicit _debug flag. private static boolean _debug; /** - * Creates a patch from the two passed in files, writing the result to os. + * Creates a patch from the two passed in files, writing the result to {@code os}. */ public static void createPatch (String oldPath, String newPath, OutputStream os, boolean minimal) throws IOException @@ -83,10 +83,10 @@ public class JarDiff implements JarDiffCodes try (ZipFile2 oldArchive = new ZipFile2(oldPath); ZipFile2 newArchive = new ZipFile2(newPath)) { - HashMap moved = new HashMap<>(); - HashSet implicit = new HashSet<>(); - HashSet moveSrc = new HashSet<>(); - HashSet newEntries = new HashSet<>(); + Map moved = new HashMap<>(); + Set implicit = new HashSet<>(); + Set moveSrc = new HashSet<>(); + Set newEntries = new HashSet<>(); // FIRST PASS // Go through the entries in new archive and determine which files are candidates for @@ -157,7 +157,7 @@ public class JarDiff implements JarDiffCodes // SECOND PASS: = - - // - - ArrayList deleted = new ArrayList<>(); + List deleted = new ArrayList<>(); for (ZipEntry oldEntry : oldArchive) { String oldName = oldEntry.getName(); if (!implicit.contains(oldName) && !moveSrc.contains(oldName) @@ -203,9 +203,9 @@ public class JarDiff implements JarDiffCodes } /** - * Writes the index file out to jos. - * oldEntries gives the names of the files that were removed, - * movedMap maps from the new name to the old name. + * Writes the index file out to {@code jos}. + * {@code oldEntries} gives the names of the files that were removed, + * {@code movedMap} maps from the new name to the old name. */ private static void createIndex (ZipOutputStream jos, List oldEntries, Map movedMap) @@ -286,7 +286,7 @@ public class JarDiff implements JarDiffCodes */ private static class ZipFile2 implements Iterable, Closeable { - private ZipFile _archive; + private final ZipFile _archive; private List _entries; private HashMap _nameToEntryMap; private HashMap> _crcToEntryMap; @@ -384,7 +384,7 @@ public class JarDiff implements JarDiffCodes public String hasSameContent (ZipFile2 file, ZipEntry entry) throws IOException { String thisName = null; - Long crcL = Long.valueOf(entry.getCrc()); + Long crcL = entry.getCrc(); // check if this archive contains files with the passed in entry's crc if (_crcToEntryMap.containsKey(crcL)) { // get the Linked List with files with the crc @@ -419,7 +419,7 @@ public class JarDiff implements JarDiffCodes while (entries.hasMoreElements()) { ZipEntry entry = entries.nextElement(); long crc = entry.getCrc(); - Long crcL = Long.valueOf(crc); + Long crcL = crc; if (_debug) { System.out.println("\t" + entry.getName() + " CRC " + crc); } diff --git a/core/src/main/java/com/threerings/getdown/tools/JarDiffPatcher.java b/core/src/main/java/com/threerings/getdown/tools/JarDiffPatcher.java index ac837a9..e55034b 100644 --- a/core/src/main/java/com/threerings/getdown/tools/JarDiffPatcher.java +++ b/core/src/main/java/com/threerings/getdown/tools/JarDiffPatcher.java @@ -215,7 +215,7 @@ public class JarDiffPatcher implements JarDiffCodes { int index = 0; int length = path.length(); - ArrayList sub = new ArrayList<>(); + List sub = new ArrayList<>(); while (index < length) { while (index < length && Character.isWhitespace @@ -223,9 +223,8 @@ public class JarDiffPatcher implements JarDiffCodes index++; } if (index < length) { - int start = index; - int last = start; - String subString = null; + int last = index; + StringBuilder subString = null; while (index < length) { char aChar = path.charAt(index); @@ -233,9 +232,9 @@ public class JarDiffPatcher implements JarDiffCodes path.charAt(index + 1) == ' ') { if (subString == null) { - subString = path.substring(last, index); + subString = new StringBuilder(path.substring(last, index)); } else { - subString += path.substring(last, index); + subString.append(path, last, index); } last = ++index; } else if (Character.isWhitespace(aChar)) { @@ -245,12 +244,14 @@ public class JarDiffPatcher implements JarDiffCodes } if (last != index) { if (subString == null) { - subString = path.substring(last, index); + subString = new StringBuilder(path.substring(last, index)); } else { - subString += path.substring(last, index); + subString.append(path, last, index); } } - sub.add(subString); + if (subString != null) { + sub.add(subString.toString()); + } } } return sub; diff --git a/core/src/main/java/com/threerings/getdown/util/Base64.java b/core/src/main/java/com/threerings/getdown/util/Base64.java index 2a5db79..233f1e7 100644 --- a/core/src/main/java/com/threerings/getdown/util/Base64.java +++ b/core/src/main/java/com/threerings/getdown/util/Base64.java @@ -24,7 +24,7 @@ import static java.nio.charset.StandardCharsets.US_ASCII; * href="http://www.ietf.org/rfc/rfc2045.txt">2045 and 3548. */ -public class Base64 { +public final class Base64 { /** * Default values for encoder/decoder flags. */ @@ -68,7 +68,7 @@ public class Base64 { // shared code // -------------------------------------------------------- - /* package */ static abstract class Coder { + /* package */ abstract static class Coder { public byte[] output; public int op; @@ -178,7 +178,7 @@ public class Base64 { * Lookup table for turning bytes into their position in the * Base64 alphabet. */ - private static final int DECODE[] = { + private static final int[] DECODE = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, @@ -201,7 +201,7 @@ public class Base64 { * Decode lookup table for the "web safe" variant (RFC 3548 * sec. 4) where - and _ replace + and /. */ - private static final int DECODE_WEBSAFE[] = { + private static final int[] DECODE_WEBSAFE = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, @@ -236,7 +236,7 @@ public class Base64 { private int state; // state number (0 to 6) private int value; - final private int[] alphabet; + private final int[] alphabet; public Decoder(int flags, byte[] output) { this.output = output; @@ -541,7 +541,7 @@ public class Base64 { * Lookup table for turning Base64 alphabet positions (6 bits) * into output bytes. */ - private static final byte ENCODE[] = { + private static final byte[] ENCODE = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', @@ -552,21 +552,21 @@ public class Base64 { * Lookup table for turning Base64 alphabet positions (6 bits) * into output bytes. */ - private static final byte ENCODE_WEBSAFE[] = { + private static final byte[] ENCODE_WEBSAFE = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_', }; - final private byte[] tail; + private final byte[] tail; /* package */ int tailLen; private int count; - final public boolean do_padding; - final public boolean do_newline; - final public boolean do_cr; - final private byte[] alphabet; + public final boolean do_padding; + public final boolean do_newline; + public final boolean do_cr; + private final byte[] alphabet; public Encoder(int flags, byte[] output) { this.output = output; @@ -618,7 +618,7 @@ public class Base64 { ((input[p++] & 0xff) << 8) | (input[p++] & 0xff); tailLen = 0; - }; + } break; case 2: diff --git a/core/src/main/java/com/threerings/getdown/util/Color.java b/core/src/main/java/com/threerings/getdown/util/Color.java index 047cead..416f77e 100644 --- a/core/src/main/java/com/threerings/getdown/util/Color.java +++ b/core/src/main/java/com/threerings/getdown/util/Color.java @@ -8,11 +8,11 @@ package com.threerings.getdown.util; /** * Utilities for handling ARGB colors. */ -public class Color +public final class Color { - public final static int CLEAR = 0x00000000; - public final static int WHITE = 0xFFFFFFFF; - public final static int BLACK = 0xFF000000; + public static final int CLEAR = 0x00000000; + public static final int WHITE = 0xFFFFFFFF; + public static final int BLACK = 0xFF000000; public static float brightness (int argb) { // TODO: we're ignoring alpha here... 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 4fc5e16..5c0b65e 100644 --- a/core/src/main/java/com/threerings/getdown/util/Config.java +++ b/core/src/main/java/com/threerings/getdown/util/Config.java @@ -62,7 +62,7 @@ public class Config * * @param opts options that influence the parsing. See {@link #createOpts}. * - * @return a list of String[] instances containing the key/value pairs in the + * @return a list of {@code String[]} instances containing the key/value pairs in the * order they were parsed from the file. */ public static List parsePairs (File source, ParseOpts opts) @@ -83,7 +83,7 @@ public class Config List pairs = new ArrayList<>(); for (String line : FileUtil.readLines(source)) { // nix comments - int cidx = line.indexOf("#"); + int cidx = line.indexOf('#'); if (opts.strictComments ? cidx == 0 : cidx != -1) { line = line.substring(0, cidx); } @@ -97,7 +97,7 @@ public class Config // parse our key/value pair String[] pair = new String[2]; // if we're biasing toward key, put all the extra = in the key rather than the value - int eidx = opts.biasToKey ? line.lastIndexOf("=") : line.indexOf("="); + int eidx = opts.biasToKey ? line.lastIndexOf('=') : line.indexOf('='); if (eidx != -1) { pair[0] = line.substring(0, eidx).trim(); pair[1] = line.substring(eidx+1).trim(); @@ -108,7 +108,7 @@ public class Config // if the pair has an os qualifier, we need to process it if (pair[1].startsWith("[")) { - int qidx = pair[1].indexOf("]"); + int qidx = pair[1].indexOf(']'); if (qidx == -1) { log.warning("Bogus platform specifier", "key", pair[0], "value", pair[1]); continue; // omit the pair entirely @@ -351,7 +351,7 @@ public class Config protected static boolean checkQualifiers (String quals, String osname, String osarch) { if (quals.startsWith("!")) { - if (quals.indexOf(",") != -1) { // sanity check + if (quals.contains(",")) { // sanity check log.warning("Multiple qualifiers cannot be used when one of the qualifiers " + "is negative", "quals", quals); return false; @@ -371,7 +371,7 @@ public class Config { String[] bits = qual.trim().toLowerCase(Locale.ROOT).split("-"); String os = bits[0], arch = (bits.length > 1) ? bits[1] : ""; - return (osname.indexOf(os) != -1) && (osarch.indexOf(arch) != -1); + return (osname.contains(os)) && (osarch.contains(arch)); } private final Map _data; diff --git a/core/src/main/java/com/threerings/getdown/util/ConnectionUtil.java b/core/src/main/java/com/threerings/getdown/util/ConnectionUtil.java deleted file mode 100644 index 21b0569..0000000 --- a/core/src/main/java/com/threerings/getdown/util/ConnectionUtil.java +++ /dev/null @@ -1,73 +0,0 @@ -// -// Getdown - application installer, patcher and launcher -// Copyright (C) 2004-2018 Getdown authors -// https://github.com/threerings/getdown/blob/master/LICENSE - -package com.threerings.getdown.util; - -import java.io.IOException; -import java.net.HttpURLConnection; -import java.net.Proxy; -import java.net.URL; -import java.net.URLConnection; -import java.net.URLDecoder; - -import com.threerings.getdown.data.SysProps; - -import static java.nio.charset.StandardCharsets.UTF_8; - -public class ConnectionUtil -{ - /** - * Opens a connection to a URL, setting the authentication header if user info is present. - * @param proxy the proxy via which to perform HTTP connections. - * @param url the URL to which to open a connection. - * @param connectTimeout if {@code > 0} then a timeout, in seconds, to use when opening the - * connection. If {@code 0} is supplied, the connection timeout specified via system properties - * will be used instead. - * @param readTimeout if {@code > 0} then a timeout, in seconds, to use while reading data from - * the connection. If {@code 0} is supplied, the read timeout specified via system properties - * will be used instead. - */ - public static URLConnection open (Proxy proxy, URL url, int connectTimeout, int readTimeout) - throws IOException - { - URLConnection conn = url.openConnection(proxy); - - // configure a connect timeout, if requested - int ctimeout = connectTimeout > 0 ? connectTimeout : SysProps.connectTimeout(); - if (ctimeout > 0) { - conn.setConnectTimeout(ctimeout * 1000); - } - - // configure a read timeout, if requested - int rtimeout = readTimeout > 0 ? readTimeout : SysProps.readTimeout(); - if (rtimeout > 0) { - conn.setReadTimeout(rtimeout * 1000); - } - - // If URL has a username:password@ before hostname, use HTTP basic auth - String userInfo = url.getUserInfo(); - if (userInfo != null) { - // Remove any percent-encoding in the username/password - userInfo = URLDecoder.decode(userInfo, "UTF-8"); - // Now base64 encode the auth info and make it a single line - String encoded = Base64.encodeToString(userInfo.getBytes(UTF_8), Base64.DEFAULT). - replaceAll("\\n","").replaceAll("\\r", ""); - conn.setRequestProperty("Authorization", "Basic " + encoded); - } - - return conn; - } - - /** - * Opens a connection to a http or https URL, setting the authentication header if user info is - * present. Throws a class cast exception if the connection returned is not the right type. See - * {@link #open} for parameter documentation. - */ - public static HttpURLConnection openHttp ( - Proxy proxy, URL url, int connectTimeout, int readTimeout) throws IOException - { - return (HttpURLConnection)open(proxy, url, connectTimeout, readTimeout); - } -} diff --git a/core/src/main/java/com/threerings/getdown/util/LaunchUtil.java b/core/src/main/java/com/threerings/getdown/util/LaunchUtil.java index 08a8b26..cc51794 100644 --- a/core/src/main/java/com/threerings/getdown/util/LaunchUtil.java +++ b/core/src/main/java/com/threerings/getdown/util/LaunchUtil.java @@ -17,19 +17,19 @@ import static com.threerings.getdown.Log.log; * Useful routines for launching Java applications from within other Java * applications. */ -public class LaunchUtil +public final class LaunchUtil { /** The default directory into which a local VM installation should be unpacked. */ public static final String LOCAL_JAVA_DIR = "java_vm"; /** - * Writes a version.txt file into the specified application directory and + * Writes a {@code version.txt} file into the specified application directory and * attempts to relaunch Getdown in that directory which will cause it to upgrade to the newly * specified version and relaunch the application. * * @param appdir the directory in which the application is installed. * @param getdownJarName the name of the getdown jar file in the application directory. This is - * probably getdown-pro.jar or getdown-retro-pro.jar if you are using + * probably {@code getdown-pro.jar} or {@code getdown-retro-pro.jar} if you are using * the results of the standard build. * @param newVersion the new version to which Getdown will update when it is executed. * @param javaLocalDir the name of the directory (inside {@code appdir}) that contains a @@ -41,7 +41,7 @@ public class LaunchUtil * after making this call as it will be upgraded and restarted. If false is returned, the * application should tell the user that they must restart the application manually. * - * @exception IOException thrown if we were unable to create the version.txt file + * @exception IOException thrown if we were unable to create the {@code version.txt} file * in the supplied application directory. If the version.txt file cannot be created, restarting * Getdown will not cause the application to be upgraded, so the application will have to * resort to telling the user that it is in a bad way. @@ -177,23 +177,23 @@ public class LaunchUtil public static boolean mustMonitorChildren () { String osname = System.getProperty("os.name", "").toLowerCase(Locale.ROOT); - return (osname.indexOf("windows 98") != -1 || osname.indexOf("windows me") != -1); + return (osname.contains("windows 98") || osname.contains("windows me")); } /** * Returns true if we're running in a JVM that identifies its operating system as Windows. */ - public static final boolean isWindows () { return _isWindows; } + public static boolean isWindows () { return _isWindows; } /** * Returns true if we're running in a JVM that identifies its operating system as MacOS. */ - public static final boolean isMacOS () { return _isMacOS; } + public static boolean isMacOS () { return _isMacOS; } /** * Returns true if we're running in a JVM that identifies its operating system as Linux. */ - public static final boolean isLinux () { return _isLinux; } + public static boolean isLinux () { return _isLinux; } /** * Checks whether a Java Virtual Machine can be located in the supplied path. @@ -232,10 +232,9 @@ public class LaunchUtil try { String osname = System.getProperty("os.name"); osname = (osname == null) ? "" : osname; - _isWindows = (osname.indexOf("Windows") != -1); - _isMacOS = (osname.indexOf("Mac OS") != -1 || - osname.indexOf("MacOS") != -1); - _isLinux = (osname.indexOf("Linux") != -1); + _isWindows = (osname.contains("Windows")); + _isMacOS = (osname.contains("Mac OS") || osname.contains("MacOS")); + _isLinux = (osname.contains("Linux")); } catch (Exception e) { // can't grab system properties; we'll just pretend we're not on any of these OSes } diff --git a/core/src/main/java/com/threerings/getdown/util/MessageUtil.java b/core/src/main/java/com/threerings/getdown/util/MessageUtil.java index 28dbdcf..f2ee9a2 100644 --- a/core/src/main/java/com/threerings/getdown/util/MessageUtil.java +++ b/core/src/main/java/com/threerings/getdown/util/MessageUtil.java @@ -5,7 +5,7 @@ package com.threerings.getdown.util; -public class MessageUtil { +public final class MessageUtil { /** * Returns whether or not the provided string is tainted. See {@link #taint}. Null strings @@ -101,11 +101,11 @@ public class MessageUtil { } /** - * Used to escape single quotes so that they are not interpreted by MessageFormat. + * Used to escape single quotes so that they are not interpreted by {@code MessageFormat}. * As we assume all single quotes are to be escaped, we cannot use the characters * { and } in our translation strings, but this is a small price to * pay to have to differentiate between messages that will and won't eventually be parsed by a - * MessageFormat instance. + * {@code MessageFormat} instance. */ public static String escape (String message) { diff --git a/core/src/main/java/com/threerings/getdown/util/ProgressObserver.java b/core/src/main/java/com/threerings/getdown/util/ProgressObserver.java index ad4c560..ac3e0a3 100644 --- a/core/src/main/java/com/threerings/getdown/util/ProgressObserver.java +++ b/core/src/main/java/com/threerings/getdown/util/ProgressObserver.java @@ -14,5 +14,5 @@ public interface ProgressObserver * Informs the observer that we have completed the specified * percentage of the process. */ - public void progress (int percent); + void progress (int percent); } diff --git a/core/src/main/java/com/threerings/getdown/util/StreamUtil.java b/core/src/main/java/com/threerings/getdown/util/StreamUtil.java index 373cfff..5cb1a40 100644 --- a/core/src/main/java/com/threerings/getdown/util/StreamUtil.java +++ b/core/src/main/java/com/threerings/getdown/util/StreamUtil.java @@ -11,11 +11,10 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.Reader; import java.io.Writer; -import java.nio.charset.Charset; import static com.threerings.getdown.Log.log; -public class StreamUtil { +public final class StreamUtil { /** * Convenient close for a stream. Use in a finally clause and love life. */ diff --git a/core/src/main/java/com/threerings/getdown/util/StringUtil.java b/core/src/main/java/com/threerings/getdown/util/StringUtil.java index 03d3c9c..86277c8 100644 --- a/core/src/main/java/com/threerings/getdown/util/StringUtil.java +++ b/core/src/main/java/com/threerings/getdown/util/StringUtil.java @@ -7,14 +7,14 @@ package com.threerings.getdown.util; import java.util.StringTokenizer; -public class StringUtil { +public final class StringUtil { /** * @return true if the specified string could be a valid URL (contains no illegal characters) */ public static boolean couldBeValidUrl (String url) { - return url.matches("[A-Za-z0-9\\-\\._~:/\\?#\\[\\]@!$&'\\(\\)\\*\\+,;=%]+"); + return url.matches("[A-Za-z0-9\\-._~:/?#\\[\\]@!$&'()*+,;=%]+"); } /** @@ -84,7 +84,7 @@ public class StringUtil { source = source.replace(",,", "%COMMA%"); // count up the number of tokens - while ((tpos = source.indexOf(",", tpos+1)) != -1) { + while ((tpos = source.indexOf(',', tpos+1)) != -1) { tcount++; } @@ -92,7 +92,7 @@ public class StringUtil { tpos = -1; tcount = 0; // do the split - while ((tpos = source.indexOf(",", tpos+1)) != -1) { + while ((tpos = source.indexOf(',', tpos+1)) != -1) { tokens[tcount] = source.substring(tstart, tpos); tokens[tcount] = tokens[tcount].trim().replace("%COMMA%", ","); if (intern) { @@ -119,7 +119,7 @@ public class StringUtil { /** * Generates a string from the supplied bytes that is the HEX encoded representation of those - * bytes. Returns the empty string for a null or empty byte array. + * bytes. Returns the empty string for a {@code null} or empty byte array. * * @param bytes the bytes for which we want a string representation. * @param count the number of bytes to stop at (which will be coerced into being {@code <=} the @@ -185,7 +185,7 @@ public class StringUtil { } /** - * Helper function for the various join methods. + * Helper function for the various {@code join} methods. */ protected static String join (Object[] values, String separator, boolean escape) { @@ -201,6 +201,6 @@ public class StringUtil { return buf.toString(); } - /** Used by {@link #hexlate} and {@link #unhexlate}. */ + /** Used by {@link #hexlate}. */ protected static final String XLATE = "0123456789abcdef"; } diff --git a/core/src/main/java/com/threerings/getdown/util/VersionUtil.java b/core/src/main/java/com/threerings/getdown/util/VersionUtil.java index 49e4e6e..b2f2894 100644 --- a/core/src/main/java/com/threerings/getdown/util/VersionUtil.java +++ b/core/src/main/java/com/threerings/getdown/util/VersionUtil.java @@ -22,7 +22,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; /** * Version related utilities. */ -public class VersionUtil +public final class VersionUtil { /** * Reads a version number from a file. diff --git a/launcher/pom.xml b/launcher/pom.xml index 8b32172..3b14aba 100644 --- a/launcher/pom.xml +++ b/launcher/pom.xml @@ -38,6 +38,12 @@ 1.0 true + + junit + junit + 4.12 + test + @@ -189,6 +195,7 @@ ${java.home}/jmods/java.base.jmod ${java.home}/jmods/java.desktop.jmod ${java.home}/jmods/java.logging.jmod + ${java.home}/jmods/java.scripting.jmod ${java.home}/jmods/jdk.jsobject.jmod diff --git a/launcher/src/main/java/com/threerings/getdown/launcher/AbortPanel.java b/launcher/src/main/java/com/threerings/getdown/launcher/AbortPanel.java index dc1e54e..4bd9f90 100644 --- a/launcher/src/main/java/com/threerings/getdown/launcher/AbortPanel.java +++ b/launcher/src/main/java/com/threerings/getdown/launcher/AbortPanel.java @@ -72,7 +72,7 @@ public final class AbortPanel extends JFrame public void actionPerformed (ActionEvent e) { String cmd = e.getActionCommand(); - if (cmd.equals("ok")) { + if ("ok".equals(cmd)) { System.exit(0); } else { setVisible(false); diff --git a/launcher/src/main/java/com/threerings/getdown/launcher/Getdown.java b/launcher/src/main/java/com/threerings/getdown/launcher/Getdown.java index 57021f4..09d2d6e 100644 --- a/launcher/src/main/java/com/threerings/getdown/launcher/Getdown.java +++ b/launcher/src/main/java/com/threerings/getdown/launcher/Getdown.java @@ -22,7 +22,15 @@ import java.io.InputStreamReader; import java.io.PrintStream; import java.net.HttpURLConnection; import java.net.URL; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.concurrent.TimeUnit; import javax.imageio.ImageIO; import javax.swing.AbstractAction; @@ -31,24 +39,44 @@ import javax.swing.JFrame; import javax.swing.JLayeredPane; import com.samskivert.swing.util.SwingUtil; -import com.threerings.getdown.data.*; import com.threerings.getdown.data.Application.UpdateInterface.Step; +import com.threerings.getdown.data.Application; +import com.threerings.getdown.data.Build; +import com.threerings.getdown.data.EnvConfig; +import com.threerings.getdown.data.Resource; +import com.threerings.getdown.data.SysProps; +import com.threerings.getdown.net.Connector; import com.threerings.getdown.net.Downloader; -import com.threerings.getdown.net.HTTPDownloader; import com.threerings.getdown.tools.Patcher; -import com.threerings.getdown.util.*; - +import com.threerings.getdown.util.Config; +import com.threerings.getdown.util.FileUtil; +import com.threerings.getdown.util.LaunchUtil; +import com.threerings.getdown.util.MessageUtil; +import com.threerings.getdown.util.ProgressAggregator; +import com.threerings.getdown.util.ProgressObserver; +import com.threerings.getdown.util.StringUtil; +import com.threerings.getdown.util.VersionUtil; import static com.threerings.getdown.Log.log; /** * Manages the main control for the Getdown application updater and deployment system. */ -public abstract class Getdown extends Thread +public abstract class Getdown implements Application.StatusDisplay, RotatingBackgrounds.ImageLoader { + /** + * Starts a thread to run Getdown and ultimately (hopefully) launch the target app. + */ + public static void run (final Getdown getdown) { + new Thread("Getdown") { + @Override public void run () { + getdown.run(); + } + }.start(); + } + public Getdown (EnvConfig envc) { - super("Getdown"); try { // If the silent property exists, install without bringing up any gui. If it equals // launch, start the application after installing. Otherwise, just install and exit. @@ -75,7 +103,7 @@ public abstract class Getdown extends Thread // welcome to hell, where java can't cope with a classpath that contains jars that live // in a directory that contains a !, at least the same bug happens on all platforms String dir = envc.appDir.toString(); - if (dir.equals(".")) { + if (".".equals(dir)) { dir = System.getProperty("user.dir"); } String errmsg = "The directory in which this application is installed:\n" + dir + @@ -115,8 +143,28 @@ public abstract class Getdown extends Thread } } - @Override - public void run () + /** + * Configures our proxy settings (called by {@link ProxyPanel}) and fires up the launcher. + */ + public void configProxy (String host, String port, String username, String password) + { + log.info("User configured proxy", "host", host, "port", port); + ProxyUtil.configProxy(_app, host, port, username, password); + + // clear out our UI + disposeContainer(); + _container = null; + + // fire up a new thread + run(this); + } + + /** + * The main entry point of Getdown: does some sanity checks and preparation, then delegates the + * actual getting down to {@link #getdown}. This is not called directly, but rather via the + * static {@code run} method as Getdown does its main work on a separate thread. + */ + protected void run () { // if we have no messages, just bail because we're hosed; the error message will be // displayed to the user already @@ -130,79 +178,27 @@ public abstract class Getdown extends Thread File instdir = _app.getLocalPath(""); if (!instdir.canWrite()) { String path = instdir.getPath(); - if (path.equals(".")) { + if (".".equals(path)) { path = System.getProperty("user.dir"); } fail(MessageUtil.tcompose("m.readonly_error", path)); return; } - try { - _dead = false; - // if we fail to detect a proxy, but we're allowed to run offline, then go ahead and - // run the app anyway because we're prepared to cope with not being able to update - if (detectProxy() || _app.allowOffline()) { - getdown(); - } else if (_silent) { - log.warning("Need a proxy, but we don't want to bother anyone. Exiting."); - } else { - // create a panel they can use to configure the proxy settings - _container = createContainer(); - // allow them to close the window to abort the proxy configuration - _dead = true; - configureContainer(); - ProxyPanel panel = new ProxyPanel(this, _msgs); - // set up any existing configured proxy - String[] hostPort = ProxyUtil.loadProxy(_app); - panel.setProxy(hostPort[0], hostPort[1]); - _container.add(panel, BorderLayout.CENTER); - showContainer(); - } - - } catch (Exception e) { - log.warning("run() failed.", e); - String msg = e.getMessage(); - if (msg == null) { - msg = MessageUtil.compose("m.unknown_error", _ifc.installError); - } else if (!msg.startsWith("m.")) { - // try to do something sensible based on the type of error - if (e instanceof FileNotFoundException) { - msg = MessageUtil.compose( - "m.missing_resource", MessageUtil.taint(msg), _ifc.installError); - } else { - msg = MessageUtil.compose( - "m.init_error", MessageUtil.taint(msg), _ifc.installError); - } - } - fail(msg); - } - } - - /** - * Configures our proxy settings (called by {@link ProxyPanel}) and fires up the launcher. - */ - public void configProxy (String host, String port, String username, String password) - { - log.info("User configured proxy", "host", host, "port", port); - - if (!StringUtil.isBlank(host)) { - ProxyUtil.configProxy(_app, host, port, username, password); - } - - // clear out our UI - disposeContainer(); - _container = null; - - // fire up a new thread - new Thread(this).start(); + _dead = false; + // if we fail to detect a proxy, but we're allowed to run offline, then go ahead and + // run the app anyway because we're prepared to cope with not being able to update + if (detectProxy() || _app.allowOffline()) getdown(); + else requestProxyInfo(false); } protected boolean detectProxy () { - if (ProxyUtil.autoDetectProxy(_app)) { + boolean tryNoProxy = SysProps.tryNoProxyFirst(); + if (!tryNoProxy && ProxyUtil.autoDetectProxy(_app)) { return true; } - // otherwise see if we actually need a proxy; first we have to initialize our application + // see if we actually need a proxy; first we have to initialize our application // to get some sort of interface configuration and the appbase URL log.info("Checking whether we need to use a proxy..."); try { @@ -211,14 +207,18 @@ public abstract class Getdown extends Thread // no worries } updateStatus("m.detecting_proxy"); - if (!ProxyUtil.canLoadWithoutProxy(_app.getConfigResource().getRemote())) { - return false; + URL configURL = _app.getConfigResource().getRemote(); + if (!ProxyUtil.canLoadWithoutProxy(configURL, tryNoProxy ? 2 : 5)) { + // if we didn't auto-detect proxy first thing, do auto-detect now + return tryNoProxy ? ProxyUtil.autoDetectProxy(_app) : false; } - // we got through, so we appear not to require a proxy; make a blank proxy config so that - // we don't go through this whole detection process again next time log.info("No proxy appears to be needed."); - ProxyUtil.saveProxy(_app, null, null); + if (!tryNoProxy) { + // we got through, so we appear not to require a proxy; make a blank proxy config so + // that we don't go through this whole detection process again next time + ProxyUtil.saveProxy(_app, null, null); + } return true; } @@ -228,6 +228,25 @@ public abstract class Getdown extends Thread _ifc = new Application.UpdateInterface(config); } + protected void requestProxyInfo (boolean reinitAuth) { + if (_silent) { + log.warning("Need a proxy, but we don't want to bother anyone. Exiting."); + return; + } + + // create a panel they can use to configure the proxy settings + _container = createContainer(); + // allow them to close the window to abort the proxy configuration + _dead = true; + configureContainer(); + ProxyPanel panel = new ProxyPanel(this, _msgs, reinitAuth); + // set up any existing configured proxy + String[] hostPort = ProxyUtil.loadProxy(_app); + panel.setProxy(hostPort[0], hostPort[1]); + _container.add(panel, BorderLayout.CENTER); + showContainer(); + } + /** * Downloads and installs (without verifying) any resources that are marked with a * {@code PRELOAD} attribute. @@ -258,7 +277,7 @@ public abstract class Getdown extends Thread protected void getdown () { try { - // first parses our application deployment file + // first parse our application deployment file try { readConfig(true); } catch (IOException ioe) { @@ -273,7 +292,7 @@ public abstract class Getdown extends Thread throw new MultipleGetdownRunning(); } - // Update the config modtime so a sleeping getdown will notice the change. + // update the config modtime so a sleeping getdown will notice the change File config = _app.getLocalPath(Application.CONFIG_FILE); if (!config.setLastModified(System.currentTimeMillis())) { log.warning("Unable to set modtime on config file, will be unable to check for " + @@ -285,7 +304,7 @@ public abstract class Getdown extends Thread // Store the config modtime before waiting the delay amount of time long lastConfigModtime = config.lastModified(); log.info("Waiting " + _delay + " minutes before beginning actual work."); - Thread.sleep(_delay * 60 * 1000); + TimeUnit.MINUTES.sleep(_delay); if (lastConfigModtime < config.lastModified()) { log.warning("getdown.txt was modified while getdown was waiting."); throw new MultipleGetdownRunning(); @@ -334,11 +353,7 @@ public abstract class Getdown extends Thread if (toDownload.size() > 0) { // we have resources to download, also note them as to-be-installed - for (Resource r : toDownload) { - if (!_toInstallResources.contains(r)) { - _toInstallResources.add(r); - } - } + _toInstallResources.addAll(toDownload); try { // if any of our resources have already been marked valid this is not a @@ -422,24 +437,20 @@ public abstract class Getdown extends Thread throw new IOException("m.unable_to_repair"); } catch (Exception e) { - log.warning("getdown() failed.", e); - String msg = e.getMessage(); - if (msg == null) { - msg = MessageUtil.compose("m.unknown_error", _ifc.installError); - } else if (!msg.startsWith("m.")) { - // try to do something sensible based on the type of error - if (e instanceof FileNotFoundException) { - msg = MessageUtil.compose( - "m.missing_resource", MessageUtil.taint(msg), _ifc.installError); - } else { - msg = MessageUtil.compose( - "m.init_error", MessageUtil.taint(msg), _ifc.installError); - } + // if we failed due to proxy errors, ask for proxy info + switch (_app.conn.state) { + case NEED_PROXY: + requestProxyInfo(false); + break; + case NEED_PROXY_AUTH: + requestProxyInfo(true); + break; + default: + log.warning("getdown() failed.", e); + fail(e); + _app.releaseLock(); + break; } - // Since we're dead, clear off the 'time remaining' label along with displaying the - // error message - fail(msg); - _app.releaseLock(); } } @@ -525,7 +536,7 @@ public abstract class Getdown extends Thread // lastly regenerate the .jsa dump file that helps Java to start up faster String vmpath = LaunchUtil.getJVMBinaryPath(javaLocalDir, false); - String[] command = new String[] { vmpath, "-Xshare:dump" }; + String[] command = { vmpath, "-Xshare:dump" }; try { log.info("Regenerating classes.jsa for " + vmpath + "..."); Runtime.getRuntime().exec(command); @@ -620,7 +631,7 @@ public abstract class Getdown extends Thread // create our user interface createInterfaceAsync(false); - Downloader dl = new HTTPDownloader(_app.proxy) { + Downloader dl = new Downloader(_app.conn) { @Override protected void resolvingDownloads () { updateStatus("m.resolving"); } @@ -732,7 +743,7 @@ public abstract class Getdown extends Thread long minshow = _ifc.minShowSeconds * 1000L; if (_container != null && uptime < minshow) { try { - Thread.sleep(minshow - uptime); + TimeUnit.MILLISECONDS.sleep(minshow - uptime); } catch (Exception e) { } } @@ -848,8 +859,23 @@ public abstract class Getdown extends Thread } } + private void fail (Exception e) { + String msg = e.getMessage(); + if (msg == null) { + msg = MessageUtil.compose("m.unknown_error", _ifc.installError); + } else if (!msg.startsWith("m.")) { + // try to do something sensible based on the type of error + msg = MessageUtil.taint(msg); + msg = e instanceof FileNotFoundException ? + MessageUtil.compose("m.missing_resource", msg, _ifc.installError) : + MessageUtil.compose("m.init_error", msg, _ifc.installError); + } + // since we're dead, clear off the 'time remaining' label along with displaying the error + fail(msg); + } + /** - * Update the status to indicate getdown has failed for the reason in message. + * Update the status to indicate getdown has failed for the reason in {@code message}. */ protected void fail (String message) { @@ -930,14 +956,14 @@ public abstract class Getdown extends Thread do { URL url = _app.getTrackingProgressURL(++_reportedProgress); if (url != null) { - new ProgressReporter(url).start(); + reportProgress(url); } } while (_reportedProgress <= progress); } else { URL url = _app.getTrackingURL(event); if (url != null) { - new ProgressReporter(url).start(); + reportProgress(url); } } } @@ -1000,44 +1026,40 @@ public abstract class Getdown extends Thread } /** Used to fetch a progress report URL. */ - protected class ProgressReporter extends Thread - { - public ProgressReporter (URL url) { - setDaemon(true); - _url = url; - } - - @Override - public void run () { - try { - HttpURLConnection ucon = ConnectionUtil.openHttp(_app.proxy, _url, 0, 0); - - // if we have a tracking cookie configured, configure the request with it - if (_app.getTrackingCookieName() != null && - _app.getTrackingCookieProperty() != null) { - String val = System.getProperty(_app.getTrackingCookieProperty()); - if (val != null) { - ucon.setRequestProperty("Cookie", _app.getTrackingCookieName() + "=" + val); - } - } - - // now request our tracking URL and ensure that we get a non-error response - ucon.connect(); + protected void reportProgress (final URL url) { + Thread reporter = new Thread("Progress reporter") { + public void run () { try { - if (ucon.getResponseCode() != HttpURLConnection.HTTP_OK) { - log.warning("Failed to report tracking event", - "url", _url, "rcode", ucon.getResponseCode()); + HttpURLConnection ucon = _app.conn.openHttp(url, 0, 0); + + // if we have a tracking cookie configured, configure the request with it + if (_app.getTrackingCookieName() != null && + _app.getTrackingCookieProperty() != null) { + String val = System.getProperty(_app.getTrackingCookieProperty()); + if (val != null) { + ucon.setRequestProperty( + "Cookie", _app.getTrackingCookieName() + "=" + val); + } } - } finally { - ucon.disconnect(); + + // now request our tracking URL and ensure that we get a non-error response + ucon.connect(); + try { + if (ucon.getResponseCode() != HttpURLConnection.HTTP_OK) { + log.warning("Failed to report tracking event", + "url", url, "rcode", ucon.getResponseCode()); + } + } finally { + ucon.disconnect(); + } + + } catch (IOException ioe) { + log.warning("Failed to report tracking event", "url", url, "error", ioe); } - - } catch (IOException ioe) { - log.warning("Failed to report tracking event", "url", _url, "error", ioe); } - } - - protected URL _url; + }; + reporter.setDaemon(true); + reporter.start(); } /** Used to pass progress on to our user interface. */ diff --git a/launcher/src/main/java/com/threerings/getdown/launcher/GetdownApp.java b/launcher/src/main/java/com/threerings/getdown/launcher/GetdownApp.java index fde79f3..e4e3320 100644 --- a/launcher/src/main/java/com/threerings/getdown/launcher/GetdownApp.java +++ b/launcher/src/main/java/com/threerings/getdown/launcher/GetdownApp.java @@ -101,7 +101,7 @@ public class GetdownApp log.info("-- Cur dir: " + System.getProperty("user.dir")); log.info("---------------------------------------------"); - Getdown app = new Getdown(envc) { + Getdown getdown = new Getdown(envc) { @Override protected Container createContainer () { // create our user interface, and display it @@ -146,7 +146,7 @@ public class GetdownApp } if (_ifc.iconImages != null) { - ArrayList icons = new ArrayList<>(); + List icons = new ArrayList<>(); for (String path : _ifc.iconImages) { Image img = loadImage(path); if (img == null) { @@ -190,7 +190,7 @@ public class GetdownApp String[] cmdarray; if (LaunchUtil.isWindows()) { String osName = System.getProperty("os.name", ""); - if (osName.indexOf("9") != -1 || osName.indexOf("Me") != -1) { + if (osName.contains("9") || osName.contains("Me")) { cmdarray = new String[] { "command.com", "/c", "start", "\"" + url + "\"" }; } else { @@ -226,8 +226,7 @@ public class GetdownApp // super.fail causes the UI to be created (if needed) on the next UI tick, so we // want to wait until that happens before we attempt to redecorate the window EventQueue.invokeLater(new Runnable() { - @Override - public void run() { + @Override public void run () { // if the frame was set to be undecorated, make window decoration available // to allow the user to close the window if (_frame != null && _frame.isUndecorated()) { @@ -247,7 +246,7 @@ public class GetdownApp protected JFrame _frame; }; - app.start(); - return app; + Getdown.run(getdown); + return getdown; } } diff --git a/launcher/src/main/java/com/threerings/getdown/launcher/ProxyPanel.java b/launcher/src/main/java/com/threerings/getdown/launcher/ProxyPanel.java index 2178273..5f18896 100644 --- a/launcher/src/main/java/com/threerings/getdown/launcher/ProxyPanel.java +++ b/launcher/src/main/java/com/threerings/getdown/launcher/ProxyPanel.java @@ -35,14 +35,16 @@ import static com.threerings.getdown.Log.log; */ public final class ProxyPanel extends JPanel implements ActionListener { - public ProxyPanel (Getdown getdown, ResourceBundle msgs) + public ProxyPanel (Getdown getdown, ResourceBundle msgs, boolean updateAuth) { _getdown = getdown; _msgs = msgs; + _updateAuth = updateAuth; setLayout(new VGroupLayout()); setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); - add(new SaneLabelField(get("m.configure_proxy"))); + String title = get(updateAuth ? "m.update_proxy_auth" : "m.configure_proxy"); + add(new SaneLabelField(title)); add(new Spacer(5, 5)); JPanel row = new JPanel(new GridLayout()); @@ -61,19 +63,20 @@ public final class ProxyPanel extends JPanel implements ActionListener row.add(new SaneLabelField(get("m.proxy_auth_required")), BorderLayout.WEST); _useAuth = new JCheckBox(); row.add(_useAuth); + _useAuth.setSelected(updateAuth); add(row); row = new JPanel(new GridLayout()); row.add(new SaneLabelField(get("m.proxy_username")), BorderLayout.WEST); _username = new SaneTextField(); - _username.setEnabled(false); + _username.setEnabled(updateAuth); row.add(_username); add(row); row = new JPanel(new GridLayout()); row.add(new SaneLabelField(get("m.proxy_password")), BorderLayout.WEST); _password = new SanePasswordField(); - _password.setEnabled(false); + _password.setEnabled(updateAuth); row.add(_password); add(row); @@ -112,7 +115,13 @@ public final class ProxyPanel extends JPanel implements ActionListener public void addNotify () { super.addNotify(); - _host.requestFocusInWindow(); + if (_updateAuth) { + // we are asking the user to update the credentials for an existing proxy + // configuration, so focus that instead of the proxy host config + _username.requestFocusInWindow(); + } else { + _host.requestFocusInWindow(); + } } // documentation inherited @@ -131,7 +140,7 @@ public final class ProxyPanel extends JPanel implements ActionListener public void actionPerformed (ActionEvent e) { String cmd = e.getActionCommand(); - if (cmd.equals("ok")) { + if ("ok".equals(cmd)) { String user = null, pass = null; if (_useAuth.isSelected()) { user = _username.getText(); @@ -184,8 +193,9 @@ public final class ProxyPanel extends JPanel implements ActionListener return dim; } - protected Getdown _getdown; - protected ResourceBundle _msgs; + protected final Getdown _getdown; + protected final ResourceBundle _msgs; + protected final boolean _updateAuth; protected JTextField _host; protected JTextField _port; diff --git a/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java b/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java index a36b5fa..209c7bb 100644 --- a/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java +++ b/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java @@ -8,31 +8,41 @@ package com.threerings.getdown.launcher; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStreamReader; import java.io.PrintStream; +import java.io.Reader; import java.net.Authenticator; import java.net.HttpURLConnection; +import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.PasswordAuthentication; import java.net.Proxy; import java.net.URL; import java.net.URLConnection; +import java.net.UnknownHostException; import java.util.Iterator; import java.util.ServiceLoader; +import javax.script.Bindings; +import javax.script.Invocable; +import javax.script.ScriptContext; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; + import ca.beq.util.win32.registry.RegistryKey; import ca.beq.util.win32.registry.RegistryValue; import ca.beq.util.win32.registry.RootKey; import com.threerings.getdown.data.Application; +import com.threerings.getdown.net.Connector; import com.threerings.getdown.spi.ProxyAuth; import com.threerings.getdown.util.Config; -import com.threerings.getdown.util.ConnectionUtil; import com.threerings.getdown.util.LaunchUtil; import com.threerings.getdown.util.StringUtil; import static com.threerings.getdown.Log.log; -public class ProxyUtil { +public final class ProxyUtil { public static boolean autoDetectProxy (Application app) { @@ -57,25 +67,39 @@ public class ProxyUtil { RegistryKey r = new RegistryKey(RootKey.HKEY_CURRENT_USER, PROXY_REGISTRY); for (Iterator iter = r.values(); iter.hasNext(); ) { RegistryValue value = (RegistryValue)iter.next(); - if (value.getName().equals("ProxyEnable")) { - enabled = value.getStringValue().equals("1"); + if ("ProxyEnable".equals(value.getName())) { + enabled = "1".equals(value.getStringValue()); } if (value.getName().equals("ProxyServer")) { - String strval = value.getStringValue(); - int cidx = strval.indexOf(":"); - if (cidx != -1) { - rport = strval.substring(cidx+1); - strval = strval.substring(0, cidx); + String[] hostPort = splitHostPort(value.getStringValue()); + rhost = hostPort[0]; + rport = hostPort[1]; + } + if (value.getName().equals("AutoConfigURL")) { + String acurl = value.getStringValue(); + Reader acjs = new InputStreamReader(new URL(acurl).openStream()); + // technically we should be returning all this info and trying each proxy + // in succession, but that's complexity we'll leave for another day + for (String proxy : findPACProxiesForURL(acjs, app.getRemoteURL(""))) { + if (proxy.startsWith("PROXY ")) { + String[] hostPort = splitHostPort(proxy.substring(6)); + rhost = hostPort[0]; + rport = hostPort[1]; + // TODO: is this valid? Does AutoConfigURL imply proxy enabled? + enabled = true; + break; + } } - rhost = strval; } } + if (enabled) { host = rhost; port = rport; } else { log.info("Detected no proxy settings in the registry."); } + } catch (Throwable t) { log.info("Failed to find proxy settings in Windows registry", "error", t); } @@ -97,32 +121,31 @@ public class ProxyUtil { return true; } - public static boolean canLoadWithoutProxy (URL rurl) + public static boolean canLoadWithoutProxy (URL rurl, int timeoutSeconds) { - log.info("Testing whether proxy is needed, via: " + rurl); + log.info("Attempting to fetch without proxy: " + rurl); try { - // try to make a HEAD request for this URL (use short connect and read timeouts) - URLConnection conn = ConnectionUtil.open(Proxy.NO_PROXY, rurl, 5, 5); - if (conn instanceof HttpURLConnection) { - HttpURLConnection hcon = (HttpURLConnection)conn; - try { - hcon.setRequestMethod("HEAD"); - hcon.connect(); - // make sure we got a satisfactory response code - int rcode = hcon.getResponseCode(); - if (rcode == HttpURLConnection.HTTP_PROXY_AUTH || - rcode == HttpURLConnection.HTTP_FORBIDDEN) { - log.warning("Got an 'HTTP credentials needed' response", "code", rcode); - } else { - return true; - } - } finally { - hcon.disconnect(); - } - } else { - // if the appbase is not an HTTP/S URL (like file:), then we don't need a proxy + URLConnection conn = Connector.DEFAULT.open(rurl, timeoutSeconds, timeoutSeconds); + // if the appbase is not an HTTP/S URL (like file:), then we don't need a proxy + if (!(conn instanceof HttpURLConnection)) { return true; } + // otherwise, try to make a HEAD request for this URL + HttpURLConnection hcon = (HttpURLConnection)conn; + try { + hcon.setRequestMethod("HEAD"); + hcon.connect(); + // make sure we got a satisfactory response code + int rcode = hcon.getResponseCode(); + if (rcode == HttpURLConnection.HTTP_PROXY_AUTH || + rcode == HttpURLConnection.HTTP_FORBIDDEN) { + log.warning("Got an 'HTTP credentials needed' response", "code", rcode); + } else { + return true; + } + } finally { + hcon.disconnect(); + } } catch (IOException ioe) { log.info("Failed to HEAD " + rurl + ": " + ioe); log.info("We probably need a proxy, but auto-detection failed."); @@ -190,9 +213,14 @@ public class ProxyUtil { } boolean haveCreds = !StringUtil.isBlank(username) && !StringUtil.isBlank(password); - int pport = StringUtil.isBlank(port) ? 80 : Integer.valueOf(port); - log.info("Using proxy", "host", host, "port", pport, "haveCreds", haveCreds); - app.proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(host, pport)); + if (StringUtil.isBlank(host)) { + log.info("Using no proxy"); + app.conn = new Connector(Proxy.NO_PROXY); + } else { + int pp = StringUtil.isBlank(port) ? 80 : Integer.valueOf(port); + log.info("Using proxy", "host", host, "port", pp, "haveCreds", haveCreds); + app.conn = new Connector(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(host, pp))); + } if (haveCreds) { final String fuser = username; @@ -205,6 +233,54 @@ public class ProxyUtil { } } + public static class Resolver { + public String dnsResolve (String host) { + try { + return InetAddress.getByName(host).toString(); + } catch (UnknownHostException uhe) { + return null; + } + } + } + + public static String[] findPACProxiesForURL (Reader pac, URL url) { + ScriptEngineManager manager = new ScriptEngineManager(); + ScriptEngine engine = manager.getEngineByName("javascript"); + Bindings globals = engine.createBindings(); + globals.put("resolver", new Resolver()); + engine.setBindings(globals, ScriptContext.GLOBAL_SCOPE); + try { + URL utils = ProxyUtil.class.getResource("PacUtils.js"); + if (utils == null) { + log.error("Unable to load PacUtils.js"); + return new String[0]; + } + engine.eval(new InputStreamReader(utils.openStream())); + Object res = engine.eval(pac); + if (engine instanceof Invocable) { + Object[] args = new Object[] { url.toString(), url.getHost() }; + res = ((Invocable) engine).invokeFunction("FindProxyForURL", args); + } + String[] proxies = res.toString().split(";"); + for (int ii = 0; ii < proxies.length; ii += 1) { + proxies[ii] = proxies[ii].trim(); + } + return proxies; + } catch (Exception e) { + log.warning("Failed to resolve PAC proxy", e); + } + return new String[0]; + } + + private static String[] splitHostPort (String hostPort) { + int cidx = hostPort.indexOf(":"); + if (cidx == -1) { + return new String[] { hostPort, null}; + } else { + return new String[] { hostPort.substring(0, cidx), hostPort.substring(cidx+1) }; + } + } + protected static final String PROXY_REGISTRY = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings"; } diff --git a/launcher/src/main/java/com/threerings/getdown/launcher/RotatingBackgrounds.java b/launcher/src/main/java/com/threerings/getdown/launcher/RotatingBackgrounds.java index d3aa2bd..d64e5f0 100644 --- a/launcher/src/main/java/com/threerings/getdown/launcher/RotatingBackgrounds.java +++ b/launcher/src/main/java/com/threerings/getdown/launcher/RotatingBackgrounds.java @@ -14,7 +14,7 @@ public final class RotatingBackgrounds { public interface ImageLoader { /** Loads and returns the image with the supplied path. */ - public Image loadImage (String path); + Image loadImage (String path); } /** @@ -35,7 +35,7 @@ public final class RotatingBackgrounds } /** - * Create a sequence of images to be rotated through from backgrounds. + * Create a sequence of images to be rotated through from {@code backgrounds}. * * Each String in backgrounds should be the path to the image, a semicolon, and the minimum * amount of time to display the image in seconds. Each image will be active for an equal diff --git a/launcher/src/main/java/com/threerings/getdown/launcher/StatusPanel.java b/launcher/src/main/java/com/threerings/getdown/launcher/StatusPanel.java index 99f44ca..197dc91 100644 --- a/launcher/src/main/java/com/threerings/getdown/launcher/StatusPanel.java +++ b/launcher/src/main/java/com/threerings/getdown/launcher/StatusPanel.java @@ -26,12 +26,9 @@ import com.samskivert.swing.Label; import com.samskivert.swing.LabelStyleConstants; import com.samskivert.swing.util.SwingUtil; import com.samskivert.util.Throttle; - import com.threerings.getdown.data.Application.UpdateInterface; import com.threerings.getdown.util.MessageUtil; import com.threerings.getdown.util.Rectangle; -import com.threerings.getdown.util.StringUtil; - import static com.threerings.getdown.Log.log; /** @@ -344,7 +341,7 @@ public final class StatusPanel extends JComponent { String msg = get(key); if (msg != null) return MessageFormat.format(MessageUtil.escape(msg), (Object[])args); - return key + String.valueOf(Arrays.asList(args)); + return key + Arrays.asList(args); } /** Used by {@link #setStatus}, and {@link #setProgress}. */ diff --git a/launcher/src/main/resources/com/threerings/getdown/launcher/PacUtils.js b/launcher/src/main/resources/com/threerings/getdown/launcher/PacUtils.js new file mode 100644 index 0000000..96a644e --- /dev/null +++ b/launcher/src/main/resources/com/threerings/getdown/launcher/PacUtils.js @@ -0,0 +1,168 @@ +/** + * Extracted from Mozilla Firefox 3.0.4, \mozilla\netwerk\base\src\nsProxyAutoConfig.js + */ +function dnsDomainIs (host, domain) { + return (host.length >= domain.length && + host.substring(host.length - domain.length) == domain); +} + +function dnsDomainLevels (host) { + return host.split('.').length-1; +} + +function convert_addr (ipchars) { + var bytes = ipchars.split('.'); + var result = (((bytes[0] & 0xff) << 24) | + ((bytes[1] & 0xff) << 16) | + ((bytes[2] & 0xff) << 8) | + ( bytes[3] & 0xff )); + return result; +} + +function dnsResolve (host) { + return resolver.dnsResolve(host) +} + +function isInNet (addrOrHost, pattern, maskstr) { + var testRE = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/; + var test = testRE.exec(addrOrHost); + if (test == null) { + addrOrHost = dnsResolve(addrOrHost); + if (addrOrHost == null) { + return false; + } + } else if (test[1] > 255 || test[2] > 255 || test[3] > 255 || test[4] > 255) { + return false; // not an IP address + } + var host = convert_addr(addrOrHost); + var pat = convert_addr(pattern); + var mask = convert_addr(maskstr); + return ((host & mask) == (pat & mask)); +} + +function isPlainHostName (host) { + return (host.search('\\.') == -1); +} + +function isResolvable (host) { + var ip = dnsResolve(host); + return (ip != null); +} + +function localHostOrDomainIs (host, hostdom) { + return (host == hostdom) || (hostdom.lastIndexOf(host + '.', 0) == 0); +} + +function shExpMatch (url, pattern) { + pattern = pattern.replace(/\./g, '\\.'); + pattern = pattern.replace(/\*/g, '.*'); + pattern = pattern.replace(/\?/g, '.'); + var newRe = new RegExp('^'+pattern+'$'); + return newRe.test(url); +} + +var wdays = { + SUN: 0, MON: 1, TUE: 2, WED: 3, THU: 4, FRI: 5, SAT: 6 +}; + +var months = { + JAN: 0, FEB: 1, MAR: 2, APR: 3, MAY: 4, JUN: 5, JUL: 6, AUG: 7, SEP: 8, OCT: 9, NOV: 10, DEC: 11 +}; + +function weekdayRange () { + function getDay (weekday) { + if (weekday in wdays) { + return wdays[weekday]; + } + return -1; + } + var date = new Date(); + var argc = arguments.length; + var wday; + if (argc < 1) { + return false; + } + if (arguments[argc - 1] == 'GMT') { + argc--; + wday = date.getUTCDay(); + } else { + wday = date.getDay(); + } + var wd1 = getDay(arguments[0]); + var wd2 = (argc == 2) ? getDay(arguments[1]) : wd1; + return (wd1 == -1 || wd2 == -1) ? false : (wd1 <= wday && wday <= wd2); +} + +function dateRange () { + function getMonth (name) { + if (name in months) { + return months[name]; + } + return -1; + } + var date = new Date(); + var argc = arguments.length; + if (argc < 1) { + return false; + } + var isGMT = (arguments[argc - 1] == 'GMT'); + if (isGMT) { + argc--; + } + + // function will work even without explict handling of this case + if (argc == 1) { + var tmp = parseInt(arguments[0]); + if (isNaN(tmp)) { + return ((isGMT ? date.getUTCMonth() : date.getMonth()) == getMonth(arguments[0])); + } else if (tmp < 32) { + return ((isGMT ? date.getUTCDate() : date.getDate()) == tmp); + } else { + return ((isGMT ? date.getUTCFullYear() : date.getFullYear()) == tmp); + } + } + + var year = date.getFullYear(); + var date1, date2; + date1 = new Date(year, 0, 1, 0, 0, 0); + date2 = new Date(year, 11, 31, 23, 59, 59); + var adjustMonth = false; + for (var i = 0; i < (argc >> 1); i++) { + var tmp = parseInt(arguments[i]); + if (isNaN(tmp)) { + var mon = getMonth(arguments[i]); + date1.setMonth(mon); + } else if (tmp < 32) { + adjustMonth = (argc <= 2); + date1.setDate(tmp); + } else { + date1.setFullYear(tmp); + } + } + for (var i = (argc >> 1); i < argc; i++) { + var tmp = parseInt(arguments[i]); + if (isNaN(tmp)) { + var mon = getMonth(arguments[i]); + date2.setMonth(mon); + } else if(tmp < 32) { + date2.setDate(tmp); + } else { + date2.setFullYear(tmp); + } + } + if (adjustMonth) { + date1.setMonth(date.getMonth()); + date2.setMonth(date.getMonth()); + } + if (isGMT) { + var tmp = date; + tmp.setFullYear(date.getUTCFullYear()); + tmp.setMonth(date.getUTCMonth()); + tmp.setDate(date.getUTCDate()); + tmp.setHours(date.getUTCHours()); + tmp.setMinutes(date.getUTCMinutes()); + tmp.setSeconds(date.getUTCSeconds()); + date = tmp; + } + return ((date1 <= date) && (date <= date2)); +} diff --git a/launcher/src/main/resources/com/threerings/getdown/messages.properties b/launcher/src/main/resources/com/threerings/getdown/messages.properties index 92e13ee..b113c8e 100644 --- a/launcher/src/main/resources/com/threerings/getdown/messages.properties +++ b/launcher/src/main/resources/com/threerings/getdown/messages.properties @@ -17,6 +17,9 @@ m.configure_proxy = We were unable to connect to the application server to

Your computer may access the Internet through a proxy and we were unable to automatically \ detect your proxy settings. If you know your proxy settings, you can enter them below. +m.update_proxy_auth = The stored proxy user/password is wrong or obsolete. \ +

Please provide an updated user/password combination. + m.proxy_extra = If you are sure that you don't use a proxy then \ perhaps there is a temporary Internet outage that is preventing us from \ communicating with the servers. In this case, you can cancel and try \ diff --git a/launcher/src/main/resources/com/threerings/getdown/messages_de.properties b/launcher/src/main/resources/com/threerings/getdown/messages_de.properties index b013f15..1c3af16 100644 --- a/launcher/src/main/resources/com/threerings/getdown/messages_de.properties +++ b/launcher/src/main/resources/com/threerings/getdown/messages_de.properties @@ -16,6 +16,9 @@ m.configure_proxy = Es konnte keine Verbindung zum Applikations-Server auf Wenn kein Proxy verwendet werden soll, löschen Sie bitte alle Einträge in den unten \ stehenden Feldern und klicken sie auf OK. +m.update_proxy_auth = Gespeicherte Proxy User/Passwort Kombination ungültig oder obsolet. \ +

Bitte geben Sie die korrekte User/Passwort Kombination ein. + m.proxy_extra = Sollten Sie keine Proxyeinstellungen gesetzt haben wenden Sie sich bitte \ an Ihren Administrator. diff --git a/launcher/src/main/resources/com/threerings/getdown/messages_it.properties b/launcher/src/main/resources/com/threerings/getdown/messages_it.properties index 1328650..c2481ea 100644 --- a/launcher/src/main/resources/com/threerings/getdown/messages_it.properties +++ b/launcher/src/main/resources/com/threerings/getdown/messages_it.properties @@ -20,7 +20,11 @@ m.configure_proxy = Impossibile collegarsi al server per \ questo potrebbe non essere stato riconosciuto automaticamente. Se conosci le \ tue impostazioni del proxy, puoi inserirle di seguito. -m.proxy_extra = Se sei sicuro di non usare proxy \ +m.update_proxy_auth = Combinazione User/Password salvata per il proxy non è valida \ + oppure obsoleta. \ +

Perfavore inserire una combinazione User/Password valida. + +m.proxy_extra = Se sei sicuro di non usare proxy \ potrebbe essere un problema di internet o di collegamento con il server. \ In questo caso puoi annullare e ripetere l'installazione più tardi. diff --git a/launcher/src/test/java/com/threerings/getdown/launcher/ProxyUtilTest.java b/launcher/src/test/java/com/threerings/getdown/launcher/ProxyUtilTest.java new file mode 100644 index 0000000..e60ab90 --- /dev/null +++ b/launcher/src/test/java/com/threerings/getdown/launcher/ProxyUtilTest.java @@ -0,0 +1,137 @@ +// +// Getdown - application installer, patcher and launcher +// Copyright (C) 2004-2018 Getdown authors +// https://github.com/threerings/getdown/blob/master/LICENSE + +package com.threerings.getdown.launcher; + +import java.io.StringReader; +import java.net.URL; + +import org.junit.Test; +import static org.junit.Assert.*; + +public class ProxyUtilTest { + + static String[] strs (String... strs) { return strs; } + + static String[] findPAC (String code, String url) throws Exception { + return ProxyUtil.findPACProxiesForURL(new StringReader(code), new URL(url)); + } + + static void testPAC (String code, String url, String... expectedProxies) throws Exception { + assertArrayEquals(expectedProxies, findPAC(code, url)); + } + + @Test public void testPACProxy () throws Exception { + String EXAMPLE0 = + "function FindProxyForURL(url, host) {\n" + + " if (shExpMatch(host, '*.example.com')) { return 'DIRECT'; }\n" + + " if (isInNet(host, '10.0.0.0', '255.255.248.0')) {\n" + + " return 'PROXY fastproxy.example.com:8080';\n" + + " }\n" + + " return 'PROXY proxy.example.com:8080; DIRECT';\n" + + "}\n"; + + testPAC(EXAMPLE0, "http://test.example.com/", "DIRECT"); + testPAC(EXAMPLE0, "http://10.0.1.1/", "PROXY fastproxy.example.com:8080"); + testPAC(EXAMPLE0, "http://chicken.gov/", "PROXY proxy.example.com:8080", "DIRECT"); + + String EXAMPLE1 = + "function FindProxyForURL(url, host) {" + + " if (isPlainHostName(host) || dnsDomainIs(host, '.mozilla.org')) {" + + " return 'DIRECT';" + + " } else {" + + " return 'PROXY w3proxy.mozilla.org:8080; DIRECT';" + + " }" + + "}"; + testPAC(EXAMPLE1, "http://test.example.com/", "PROXY w3proxy.mozilla.org:8080", "DIRECT"); + testPAC(EXAMPLE1, "http://www.mozilla.org/", "DIRECT"); + testPAC(EXAMPLE1, "http://foo.mozilla.org/", "DIRECT"); + testPAC(EXAMPLE1, "http://localhost/", "DIRECT"); + + String EXAMPLE2 = + " function FindProxyForURL(url, host) {\n" + + " if ((isPlainHostName(host) || dnsDomainIs(host, '.mozilla.org')) &&\n" + + " !localHostOrDomainIs(host, 'www.mozilla.org') &&\n" + + " !localHostOrDomainIs(host, 'merchant.mozilla.org')) {\n" + + " return 'DIRECT';\n" + + " } else {\n" + + " return 'PROXY w3proxy.mozilla.org:8080; DIRECT';\n" + + " }\n" + + " }"; + testPAC(EXAMPLE2, "http://test.example.com/", "PROXY w3proxy.mozilla.org:8080", "DIRECT"); + testPAC(EXAMPLE2, "http://www.mozilla.org/", "PROXY w3proxy.mozilla.org:8080", "DIRECT"); + testPAC(EXAMPLE2, "http://www/", "PROXY w3proxy.mozilla.org:8080", "DIRECT"); + testPAC(EXAMPLE2, "http://foo.mozilla.org/", "DIRECT"); + testPAC(EXAMPLE2, "http://localhost/", "DIRECT"); + + String EXAMPLE3A = + "function FindProxyForURL(url, host) {\n" + + " if (isResolvable(host)) return 'DIRECT';\n" + + " else return 'PROXY proxy.mydomain.com:8080';\n" + + "}"; + testPAC(EXAMPLE3A, "http://www.mozilla.org/", "DIRECT"); + testPAC(EXAMPLE3A, "http://doesnotexist.mozilla.org/", "PROXY proxy.mydomain.com:8080"); + + String EXAMPLE3B = + "function FindProxyForURL(url, host) {\n" + + " if (isPlainHostName(host) ||\n" + + " dnsDomainIs(host, '.mydomain.com') ||\n" + + " isResolvable(host)) {\n" + + " return 'DIRECT';\n" + + " } else {\n" + + " return 'PROXY proxy.mydomain.com:8080';\n" + + " }\n" + + "}"; + testPAC(EXAMPLE3B, "http://plain/", "DIRECT"); + testPAC(EXAMPLE3B, "http://foo.mydomain.com/", "DIRECT"); + testPAC(EXAMPLE3B, "http://www.mozilla.org/", "DIRECT"); + testPAC(EXAMPLE3B, "http://doesnotexist.mozilla.org/", "PROXY proxy.mydomain.com:8080"); + + // example 4 + // function FindProxyForURL(url, host) { + // if (isInNet(host, '198.95.0.0', '255.255.0.0')) return 'DIRECT'; + // else return 'PROXY proxy.mydomain.com:8080'; + // } + // function FindProxyForURL(url, host) { + // if (isPlainHostName(host) || + // dnsDomainIs(host, '.mydomain.com') || + // isInNet(host, '198.95.0.0', '255.255.0.0')) { + // return 'DIRECT'; + // } else { + // return 'PROXY proxy.mydomain.com:8080'; + // } + // } + + // example 5 + // function FindProxyForURL(url, host) { + // if (isPlainHostName(host) || dnsDomainIs(host, '.mydomain.com')) return 'DIRECT'; + // else if (shExpMatch(host, '*.com')) return 'PROXY proxy1.mydomain.com:8080; ' + + // 'PROXY proxy4.mydomain.com:8080'; + // else if (shExpMatch(host, '*.edu')) return 'PROXY proxy2.mydomain.com:8080; ' + + // 'PROXY proxy4.mydomain.com:8080'; + // else return 'PROXY proxy3.mydomain.com:8080; ' + + // 'PROXY proxy4.mydomain.com:8080'; + // } + + // example 6 + // function FindProxyForURL(url, host) { + // if (url.substring(0, 5) == 'http:') { + // return 'PROXY http-proxy.mydomain.com:8080'; + // } + // else if (url.substring(0, 4) == 'ftp:') { + // return 'PROXY ftp-proxy.mydomain.com:8080'; + // } + // else if (url.substring(0, 7) == 'gopher:') { + // return 'PROXY gopher-proxy.mydomain.com:8080'; + // } + // else if (url.substring(0, 6) == 'https:' || + // url.substring(0, 6) == 'snews:') { + // return 'PROXY security-proxy.mydomain.com:8080'; + // } else { + // return 'DIRECT'; + // } + // } + } +}