From 176cb8ca991eb7d00bf66d4261313dae0c1e69fc Mon Sep 17 00:00:00 2001 From: Saeid Nourian Date: Fri, 17 Jun 2016 11:03:56 -0400 Subject: [PATCH 01/11] Avoid corrupted resources when download is interrupted 1) Download resources first into a temporary file 2) Perform verification and digest comparison with the downloaded temporary file instead of the original if temporary file exist 3) Only when verification completes successfully rename all the temporary files to override the original. This step can be done automatically or manually by calling GetDown.install() which is useful for those who want to download in background and install upon program exit. --- .../com/threerings/getdown/data/Resource.java | 22 +++++++-- .../com/threerings/getdown/data/SysProps.java | 4 ++ .../threerings/getdown/launcher/Getdown.java | 47 ++++++++++++++++++- .../getdown/net/HTTPDownloader.java | 2 +- .../com/threerings/getdown/util/FileUtil.java | 1 + 5 files changed, 69 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/threerings/getdown/data/Resource.java b/src/main/java/com/threerings/getdown/data/Resource.java index 688db55..b5657dc 100644 --- a/src/main/java/com/threerings/getdown/data/Resource.java +++ b/src/main/java/com/threerings/getdown/data/Resource.java @@ -13,7 +13,6 @@ import java.util.Comparator; import java.util.List; import java.util.jar.JarEntry; import java.util.jar.JarFile; -import java.util.jar.JarInputStream; import com.samskivert.io.StreamUtil; import com.samskivert.util.StringUtil; @@ -36,6 +35,7 @@ public class Resource _path = path; _remote = remote; _local = local; + _local_new = new File(local.toString() + "_new"); String lpath = _local.getPath(); _marker = new File(lpath + "v"); @@ -67,6 +67,11 @@ public class Resource return _local; } + public File getLocalNew () + { + return _local_new; + } + /** * Returns the location of the unpacked resource. */ @@ -107,7 +112,16 @@ public class Resource public String computeDigest (MessageDigest md, ProgressObserver obs) throws IOException { - return computeDigest(_local, md, obs); + File file; + if (_local.toString().toLowerCase().endsWith(Application.CONFIG_FILE)) + file = _local; + else { + if (_local_new.exists()) + file = _local_new; + else + file = _local; + } + return computeDigest(file, md, obs); } /** @@ -292,7 +306,7 @@ public class Resource protected static boolean isJar (String path) { - return path.endsWith(".jar"); + return path.endsWith(".jar") || path.endsWith(".jar_new"); } protected static boolean isPacked200Jar (String path) @@ -302,7 +316,7 @@ public class Resource protected String _path; protected URL _remote; - protected File _local, _marker, _unpacked; + protected File _local, _local_new, _marker, _unpacked; protected boolean _unpack, _isJar, _isPacked200Jar; /** Used to sort the entries in a jar file. */ diff --git a/src/main/java/com/threerings/getdown/data/SysProps.java b/src/main/java/com/threerings/getdown/data/SysProps.java index 6dfdf9c..39381b0 100644 --- a/src/main/java/com/threerings/getdown/data/SysProps.java +++ b/src/main/java/com/threerings/getdown/data/SysProps.java @@ -49,6 +49,10 @@ public class SysProps return System.getProperty("silent") != null; } + public static boolean install () { + return System.getProperty("no_install") == null; + } + /** If true, Getdown installs the app without ever bringing up a UI and then launches it. * Usage: {@code -Dsilent=launch}. */ public static boolean launchInSilent () { diff --git a/src/main/java/com/threerings/getdown/launcher/Getdown.java b/src/main/java/com/threerings/getdown/launcher/Getdown.java index 3afc3a9..df5d1ad 100644 --- a/src/main/java/com/threerings/getdown/launcher/Getdown.java +++ b/src/main/java/com/threerings/getdown/launcher/Getdown.java @@ -68,6 +68,7 @@ import com.threerings.getdown.net.HTTPDownloader; import com.threerings.getdown.tools.Patcher; import com.threerings.getdown.util.ConfigUtil; import com.threerings.getdown.util.ConnectionUtil; +import com.threerings.getdown.util.FileUtil; import com.threerings.getdown.util.LaunchUtil; import com.threerings.getdown.util.ProgressAggregator; import com.threerings.getdown.util.ProgressObserver; @@ -100,6 +101,7 @@ public abstract class Getdown extends Thread // 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. _silent = SysProps.silent(); + _install = SysProps.install(); if (_silent) { _launchInSilent = SysProps.launchInSilent(); } @@ -409,6 +411,9 @@ public abstract class Getdown extends Thread // we'll keep track of all the resources we unpack Set unpacked = new HashSet(); + + toBeInstalledResouces = new ArrayList(); + readyToInstall = false; //setStep(Step.START); for (int ii = 0; ii < MAX_LOOPS; ii++) { @@ -442,6 +447,7 @@ public abstract class Getdown extends Thread setStep(Step.VERIFY_RESOURCES); setStatusAsync("m.validating", -1, -1L, false); List failures = _app.verifyResources(_progobs, alreadyValid, unpacked); + addToBeInstalledResources(failures); if (failures == null) { log.info("Resources verified."); @@ -471,6 +477,10 @@ public abstract class Getdown extends Thread } } + readyToInstall = true; + if (_install) + install(); + // Only launch if we aren't in silent mode. Some mystery program starting out // of the blue would be disconcerting. if (!_silent || _launchInSilent) { @@ -531,7 +541,16 @@ public abstract class Getdown extends Thread } } - // documentation inherited from interface + private void addToBeInstalledResources(List resources) { + if (resources == null) + return; + else + for (Resource r : resources) + if (!toBeInstalledResouces.contains(r)) + toBeInstalledResouces.add(r); + } + + // documentation inherited from interface public void updateStatus (String message) { setStatusAsync(message, -1, -1L, true); @@ -754,7 +773,28 @@ public abstract class Getdown extends Thread throw new MultipleGetdownRunning(); } } - + + public static void install () + throws IOException, InterruptedException + { + if (readyToInstall) { + log.info("Installing downloaded resources:"); + for (Resource resource : toBeInstalledResouces) { + log.info(resource); + if (!FileUtil.renameTo(resource.getLocalNew(), resource.getLocal())) + throw new IOException("Failed to rename(" + resource.getLocalNew() + ", " + resource.getLocal() + ")"); + if (Thread.interrupted()) { + throw new InterruptedException("m.applet_stopped"); + } + } + toBeInstalledResouces.clear(); + readyToInstall = false; + log.info("Install completed."); + } else { + log.info("Nothing to install."); + } + } + /** * Called to launch the application if everything is determined to be ready to go. */ @@ -1219,6 +1259,7 @@ public abstract class Getdown extends Thread protected boolean _dead; protected boolean _silent; + protected boolean _install; protected boolean _launchInSilent; protected long _startup; @@ -1239,4 +1280,6 @@ public abstract class Getdown extends Thread protected static final long PLAY_AGAIN_TIME = 3000L; protected static final String PROXY_REGISTRY = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings"; + protected static List toBeInstalledResouces; + protected static boolean readyToInstall; } diff --git a/src/main/java/com/threerings/getdown/net/HTTPDownloader.java b/src/main/java/com/threerings/getdown/net/HTTPDownloader.java index 738d064..286ebc5 100644 --- a/src/main/java/com/threerings/getdown/net/HTTPDownloader.java +++ b/src/main/java/com/threerings/getdown/net/HTTPDownloader.java @@ -78,7 +78,7 @@ public class HTTPDownloader extends Downloader long currentSize = 0L; try { in = conn.getInputStream(); - out = new FileOutputStream(rsrc.getLocal()); + out = new FileOutputStream(rsrc.getLocalNew()); int read; // TODO: look to see if we have a download info file diff --git a/src/main/java/com/threerings/getdown/util/FileUtil.java b/src/main/java/com/threerings/getdown/util/FileUtil.java index c957e59..efde120 100644 --- a/src/main/java/com/threerings/getdown/util/FileUtil.java +++ b/src/main/java/com/threerings/getdown/util/FileUtil.java @@ -57,6 +57,7 @@ public class FileUtil extends com.samskivert.util.FileUtil fin = new FileInputStream(source); fout = new FileOutputStream(dest); StreamUtil.copy(fin, fout); + fin.close(); // needed otherwise cannot delete source if (!source.delete()) { log.warning("Failed to delete " + source + " after brute force copy to " + dest + "."); From d7ec0f05e9d217fce28a6641758ec2a028794b98 Mon Sep 17 00:00:00 2001 From: Saeid Nourian Date: Tue, 11 Oct 2016 15:47:23 -0400 Subject: [PATCH 02/11] Allow checking to see if update is downloaded and ready for install --- src/main/java/com/threerings/getdown/launcher/Getdown.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/com/threerings/getdown/launcher/Getdown.java b/src/main/java/com/threerings/getdown/launcher/Getdown.java index df5d1ad..ef9c280 100644 --- a/src/main/java/com/threerings/getdown/launcher/Getdown.java +++ b/src/main/java/com/threerings/getdown/launcher/Getdown.java @@ -795,6 +795,10 @@ public abstract class Getdown extends Thread } } + public static boolean isUpdateAvailable() { + return readyToInstall && !toBeInstalledResouces.isEmpty(); + } + /** * Called to launch the application if everything is determined to be ready to go. */ From fc9b6d3c8986a0759e9bbe39b40e955b06a6e985 Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Fri, 4 Nov 2016 16:41:37 -0700 Subject: [PATCH 03/11] Include the META-INF files in the digest. We used to ignore them for some reason but I think that whatever benefits that yielded have long been overshadowed by detriments. --- src/main/java/com/threerings/getdown/data/Resource.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/main/java/com/threerings/getdown/data/Resource.java b/src/main/java/com/threerings/getdown/data/Resource.java index 7ad0a1e..67e9496 100644 --- a/src/main/java/com/threerings/getdown/data/Resource.java +++ b/src/main/java/com/threerings/getdown/data/Resource.java @@ -232,13 +232,6 @@ public class Resource int eidx = 0; for (JarEntry entry : entries) { - // skip metadata; we just want the goods - if (entry.getName().startsWith("META-INF")) { - updateProgress(obs, eidx, entries.size()); - continue; - } - - // add this file's data to the MD5 hash InputStream in = null; try { in = jar.getInputStream(entry); From 7c651fbd73c944f9942119cfc4f8ec6551d17204 Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Fri, 4 Nov 2016 17:45:51 -0700 Subject: [PATCH 04/11] Tidy up new resources stuff a bit. The one major change here is that Getdown.isUpdateAvailable/install() are no longer static. GetdownApp has a start() method that you can call if you want to get the Getdown reference back. You can call isUpdateAvailable() on that. It was super weird that you would call GetdownApp.main() and then just call some static methods after that. --- .../com/threerings/getdown/data/Resource.java | 27 ++- .../com/threerings/getdown/data/SysProps.java | 7 +- .../threerings/getdown/launcher/Getdown.java | 103 +++++---- .../getdown/launcher/GetdownApp.java | 214 +++++++++--------- .../com/threerings/getdown/util/FileUtil.java | 4 +- 5 files changed, 189 insertions(+), 166 deletions(-) diff --git a/src/main/java/com/threerings/getdown/data/Resource.java b/src/main/java/com/threerings/getdown/data/Resource.java index c81583a..928d33e 100644 --- a/src/main/java/com/threerings/getdown/data/Resource.java +++ b/src/main/java/com/threerings/getdown/data/Resource.java @@ -35,7 +35,7 @@ public class Resource _path = path; _remote = remote; _local = local; - _local_new = new File(local.toString() + "_new"); + _localNew = new File(local.toString() + "_new"); String lpath = _local.getPath(); _marker = new File(lpath + "v"); @@ -67,9 +67,12 @@ public class Resource return _local; } + /** + * Returns the location of the to-be-installed new version of this resource. + */ public File getLocalNew () { - return _local_new; + return _localNew; } /** @@ -109,18 +112,14 @@ public class Resource * Computes the MD5 hash of this resource's underlying file. * Note: This is both CPU and I/O intensive. */ - public String computeDigest (MessageDigest md, ProgressObserver obs) - throws IOException + public String computeDigest (MessageDigest md, ProgressObserver obs) throws IOException { - File file; - if (_local.toString().toLowerCase().endsWith(Application.CONFIG_FILE)) - file = _local; - else { - if (_local_new.exists()) - file = _local_new; - else - file = _local; - } + File file; + if (_local.toString().toLowerCase().endsWith(Application.CONFIG_FILE)) { + file = _local; + } else { + file = _localNew.exists() ? _localNew : _local; + } return computeDigest(file, md, obs); } @@ -309,7 +308,7 @@ public class Resource protected String _path; protected URL _remote; - protected File _local, _local_new, _marker, _unpacked; + protected File _local, _localNew, _marker, _unpacked; protected boolean _unpack, _isJar, _isPacked200Jar; /** Used to sort the entries in a jar file. */ diff --git a/src/main/java/com/threerings/getdown/data/SysProps.java b/src/main/java/com/threerings/getdown/data/SysProps.java index 39381b0..b883ba5 100644 --- a/src/main/java/com/threerings/getdown/data/SysProps.java +++ b/src/main/java/com/threerings/getdown/data/SysProps.java @@ -49,8 +49,11 @@ public class SysProps return System.getProperty("silent") != null; } - public static boolean install () { - return System.getProperty("no_install") == null; + /** If true, Getdown does not automatically install updates after downloading them. It waits + * for the application to call {@link Getdown#install}. + * Usage: {@code -Dno_install}. */ + public static boolean noInstall () { + return System.getProperty("no_install") != null; } /** If true, Getdown installs the app without ever bringing up a UI and then launches it. diff --git a/src/main/java/com/threerings/getdown/launcher/Getdown.java b/src/main/java/com/threerings/getdown/launcher/Getdown.java index ef9c280..b625ae1 100644 --- a/src/main/java/com/threerings/getdown/launcher/Getdown.java +++ b/src/main/java/com/threerings/getdown/launcher/Getdown.java @@ -101,11 +101,11 @@ public abstract class Getdown extends Thread // 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. _silent = SysProps.silent(); - _install = SysProps.install(); if (_silent) { _launchInSilent = SysProps.launchInSilent(); } _delay = SysProps.startDelay(); + _noInstall = SysProps.noInstall(); } catch (SecurityException se) { // don't freak out, just assume non-silent and no delay; we're probably already // recovering from a security failure @@ -128,6 +128,39 @@ public abstract class Getdown extends Thread _startup = System.currentTimeMillis(); } + /** + * Returns true if there are pending new resources, waiting to be installed. + */ + public boolean isUpdateAvailable () + { + return _readyToInstall && !_toBeInstalledResouces.isEmpty(); + } + + /** + * Installs the currently pending new resources. + */ + public void install () throws IOException, InterruptedException + { + if (_readyToInstall) { + log.info("Installing downloaded resources:"); + for (Resource resource : _toBeInstalledResouces) { + File source = resource.getLocalNew(), dest = resource.getLocal(); + log.info("- " + source); + if (!FileUtil.renameTo(source, dest)) { + throw new IOException("Failed to rename " + source + " to " + dest); + } + if (Thread.interrupted()) { + throw new InterruptedException("m.applet_stopped"); + } + } + _toBeInstalledResouces.clear(); + _readyToInstall = false; + log.info("Install completed."); + } else { + log.info("Nothing to install."); + } + } + /** * This is used by the applet which always needs a user interface and wants to load it as soon * as possible. @@ -411,9 +444,9 @@ public abstract class Getdown extends Thread // we'll keep track of all the resources we unpack Set unpacked = new HashSet(); - - toBeInstalledResouces = new ArrayList(); - readyToInstall = false; + + _toBeInstalledResouces = new ArrayList(); + _readyToInstall = false; //setStep(Step.START); for (int ii = 0; ii < MAX_LOOPS; ii++) { @@ -447,7 +480,6 @@ public abstract class Getdown extends Thread setStep(Step.VERIFY_RESOURCES); setStatusAsync("m.validating", -1, -1L, false); List failures = _app.verifyResources(_progobs, alreadyValid, unpacked); - addToBeInstalledResources(failures); if (failures == null) { log.info("Resources verified."); @@ -477,9 +509,11 @@ public abstract class Getdown extends Thread } } - readyToInstall = true; - if (_install) - install(); + // assuming we're not doing anything funny, install the update + _readyToInstall = true; + if (!_noInstall) { + install(); + } // Only launch if we aren't in silent mode. Some mystery program starting out // of the blue would be disconcerting. @@ -496,6 +530,13 @@ public abstract class Getdown extends Thread return; } + // we have failures, those will be redownloaded so we note them as to-be-installed + for (Resource r : failures) { + if (!_toBeInstalledResouces.contains(r)) { + _toBeInstalledResouces.add(r); + } + } + try { // if any of our resources have already been marked valid this is not a first // time install and we don't want to enable tracking @@ -509,6 +550,7 @@ public abstract class Getdown extends Thread download(failures); reportTrackingEvent("app_complete", -1); + } finally { _enableTracking = false; } @@ -541,16 +583,7 @@ public abstract class Getdown extends Thread } } - private void addToBeInstalledResources(List resources) { - if (resources == null) - return; - else - for (Resource r : resources) - if (!toBeInstalledResouces.contains(r)) - toBeInstalledResouces.add(r); - } - - // documentation inherited from interface + // documentation inherited from interface public void updateStatus (String message) { setStatusAsync(message, -1, -1L, true); @@ -773,32 +806,7 @@ public abstract class Getdown extends Thread throw new MultipleGetdownRunning(); } } - - public static void install () - throws IOException, InterruptedException - { - if (readyToInstall) { - log.info("Installing downloaded resources:"); - for (Resource resource : toBeInstalledResouces) { - log.info(resource); - if (!FileUtil.renameTo(resource.getLocalNew(), resource.getLocal())) - throw new IOException("Failed to rename(" + resource.getLocalNew() + ", " + resource.getLocal() + ")"); - if (Thread.interrupted()) { - throw new InterruptedException("m.applet_stopped"); - } - } - toBeInstalledResouces.clear(); - readyToInstall = false; - log.info("Install completed."); - } else { - log.info("Nothing to install."); - } - } - - public static boolean isUpdateAvailable() { - return readyToInstall && !toBeInstalledResouces.isEmpty(); - } - + /** * Called to launch the application if everything is determined to be ready to go. */ @@ -1263,10 +1271,13 @@ public abstract class Getdown extends Thread protected boolean _dead; protected boolean _silent; - protected boolean _install; + protected boolean _noInstall; protected boolean _launchInSilent; protected long _startup; + protected List _toBeInstalledResouces; + protected boolean _readyToInstall; + protected boolean _enableTracking = true; protected int _reportedProgress = 0; @@ -1284,6 +1295,4 @@ public abstract class Getdown extends Thread protected static final long PLAY_AGAIN_TIME = 3000L; protected static final String PROXY_REGISTRY = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings"; - protected static List toBeInstalledResouces; - protected static boolean readyToInstall; } diff --git a/src/main/java/com/threerings/getdown/launcher/GetdownApp.java b/src/main/java/com/threerings/getdown/launcher/GetdownApp.java index 57818ff..08087c6 100644 --- a/src/main/java/com/threerings/getdown/launcher/GetdownApp.java +++ b/src/main/java/com/threerings/getdown/launcher/GetdownApp.java @@ -36,6 +36,20 @@ public class GetdownApp { public static void main (String[] argv) { + try { + start(argv); + } catch (Exception e) { + log.warning("main() failed.", e); + } + } + + /** + * Runs Getdown as an application, using the arguments supplie as {@code argv}. + * @return the {@code Getdown} instance that is running. {@link Getdown#start} will have been + * called on it. + * @throws Exception if anything goes wrong starting Getdown. + */ + public static Getdown start (String[] argv) throws Exception { int aidx = 0; List args = Arrays.asList(argv); @@ -91,118 +105,114 @@ public class GetdownApp log.info("-- Cur dir: " + System.getProperty("user.dir")); log.info("---------------------------------------------"); - try { - Getdown app = new Getdown(appDir, appId, null, null, appArgs) { - @Override - protected Container createContainer () { - // create our user interface, and display it - String title = StringUtil.isBlank(_ifc.name) ? "" : _ifc.name; - if (_frame == null) { - _frame = new JFrame(title); - _frame.addWindowListener(new WindowAdapter() { - @Override - public void windowClosing (WindowEvent evt) { - handleWindowClose(); - } - }); - _frame.setUndecorated(_ifc.hideDecorations); - _frame.setResizable(false); - } else { - _frame.setTitle(title); - _frame.getContentPane().removeAll(); - } - - if (_ifc.iconImages != null) { - ArrayList icons = new ArrayList(); - for (String path : _ifc.iconImages) { - Image img = loadImage(path); - if (img == null) { - log.warning("Error loading icon image", "path", path); - } else { - icons.add(img); - } + Getdown app = new Getdown(appDir, appId, null, null, appArgs) { + @Override + protected Container createContainer () { + // create our user interface, and display it + String title = StringUtil.isBlank(_ifc.name) ? "" : _ifc.name; + if (_frame == null) { + _frame = new JFrame(title); + _frame.addWindowListener(new WindowAdapter() { + @Override + public void windowClosing (WindowEvent evt) { + handleWindowClose(); } - if (icons.isEmpty()) { - log.warning("Failed to load any icons", "iconImages", _ifc.iconImages); + }); + _frame.setUndecorated(_ifc.hideDecorations); + _frame.setResizable(false); + } else { + _frame.setTitle(title); + _frame.getContentPane().removeAll(); + } + + if (_ifc.iconImages != null) { + ArrayList icons = new ArrayList(); + for (String path : _ifc.iconImages) { + Image img = loadImage(path); + if (img == null) { + log.warning("Error loading icon image", "path", path); } else { - SwingUtil.setFrameIcons(_frame, icons); + icons.add(img); } } - - _frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); - return _frame.getContentPane(); - } - - @Override - protected void showContainer () { - if (_frame != null) { - _frame.pack(); - SwingUtil.centerWindow(_frame); - _frame.setVisible(true); - } - } - - @Override - protected void disposeContainer () { - if (_frame != null) { - _frame.dispose(); - _frame = null; - } - } - - @Override - protected void showDocument (String url) { - String[] cmdarray; - if (RunAnywhere.isWindows()) { - String osName = System.getProperty("os.name"); - if (osName.indexOf("9") != -1 || osName.indexOf("Me") != -1) { - cmdarray = new String[] { - "command.com", "/c", "start", "\"" + url + "\"" }; - } else { - cmdarray = new String[] { - "cmd.exe", "/c", "start", "\"\"", "\"" + url + "\"" }; - } - } else if (RunAnywhere.isMacOS()) { - cmdarray = new String[] { "open", url }; - } else { // Linux, Solaris, etc. - cmdarray = new String[] { "firefox", url }; - } - try { - Runtime.getRuntime().exec(cmdarray); - } catch (Exception e) { - log.warning("Failed to open browser.", "cmdarray", cmdarray, e); - } - } - - @Override - protected void exit (int exitCode) { - // if we're running the app in the same JVM, don't call System.exit, but do - // make double sure that the download window is closed. - if (invokeDirect()) { - disposeContainer(); + if (icons.isEmpty()) { + log.warning("Failed to load any icons", "iconImages", _ifc.iconImages); } else { - System.exit(exitCode); + SwingUtil.setFrameIcons(_frame, icons); } } - @Override - protected void fail (String message) { - // 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()) { - _frame.dispose(); - _frame.setUndecorated(false); - showContainer(); - } - super.fail(message); + _frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); + return _frame.getContentPane(); + } + + @Override + protected void showContainer () { + if (_frame != null) { + _frame.pack(); + SwingUtil.centerWindow(_frame); + _frame.setVisible(true); } + } - protected JFrame _frame; - }; - app.start(); + @Override + protected void disposeContainer () { + if (_frame != null) { + _frame.dispose(); + _frame = null; + } + } - } catch (Exception e) { - log.warning("main() failed.", e); - } + @Override + protected void showDocument (String url) { + String[] cmdarray; + if (RunAnywhere.isWindows()) { + String osName = System.getProperty("os.name"); + if (osName.indexOf("9") != -1 || osName.indexOf("Me") != -1) { + cmdarray = new String[] { + "command.com", "/c", "start", "\"" + url + "\"" }; + } else { + cmdarray = new String[] { + "cmd.exe", "/c", "start", "\"\"", "\"" + url + "\"" }; + } + } else if (RunAnywhere.isMacOS()) { + cmdarray = new String[] { "open", url }; + } else { // Linux, Solaris, etc. + cmdarray = new String[] { "firefox", url }; + } + try { + Runtime.getRuntime().exec(cmdarray); + } catch (Exception e) { + log.warning("Failed to open browser.", "cmdarray", cmdarray, e); + } + } + + @Override + protected void exit (int exitCode) { + // if we're running the app in the same JVM, don't call System.exit, but do + // make double sure that the download window is closed. + if (invokeDirect()) { + disposeContainer(); + } else { + System.exit(exitCode); + } + } + + @Override + protected void fail (String message) { + // 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()) { + _frame.dispose(); + _frame.setUndecorated(false); + showContainer(); + } + super.fail(message); + } + + protected JFrame _frame; + }; + app.start(); + return app; } } diff --git a/src/main/java/com/threerings/getdown/util/FileUtil.java b/src/main/java/com/threerings/getdown/util/FileUtil.java index 05b026d..67c0db9 100644 --- a/src/main/java/com/threerings/getdown/util/FileUtil.java +++ b/src/main/java/com/threerings/getdown/util/FileUtil.java @@ -60,7 +60,9 @@ public class FileUtil extends com.samskivert.util.FileUtil fin = new FileInputStream(source); fout = new FileOutputStream(dest); StreamUtil.copy(fin, fout); - fin.close(); // needed otherwise cannot delete source + // close the input stream now so we can delete 'source' + fin.close(); + fin = null; if (!source.delete()) { log.warning("Failed to delete " + source + " after brute force copy to " + dest + "."); From d75d5a7a69631a22e2de281d369bd4ba18fa23e5 Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Fri, 4 Nov 2016 18:51:17 -0700 Subject: [PATCH 05/11] Create a separate digest.txt file for each "protocol version". Right now there's only two versions of the digester protocol, so I just always generate both of them. If we evolve the protocol further, we could conceivably allow you to specify a "minimum" protocol version, once you were confident that all of your clients had upgraded to a version of Getdown that used that minimum version. --- .../threerings/getdown/data/Application.java | 16 +- .../com/threerings/getdown/data/Digest.java | 119 +++++++------ .../com/threerings/getdown/data/Resource.java | 166 ++++++++++-------- .../com/threerings/getdown/tools/Differ.java | 5 +- .../threerings/getdown/tools/Digester.java | 38 ++-- .../getdown/tools/DigesterTask.java | 7 +- 6 files changed, 199 insertions(+), 152 deletions(-) diff --git a/src/main/java/com/threerings/getdown/data/Application.java b/src/main/java/com/threerings/getdown/data/Application.java index ce72f50..0c56e15 100644 --- a/src/main/java/com/threerings/getdown/data/Application.java +++ b/src/main/java/com/threerings/getdown/data/Application.java @@ -904,7 +904,7 @@ public class Application // now re-download our control files; we download the digest first so that if it fails, // our config file will still reference the old version and re-running the updater will // start the whole process over again - downloadDigestFile(); + downloadDigestFiles(); downloadConfigFile(); } catch (IOException ex) { @@ -1171,7 +1171,7 @@ public class Application String olddig = (_digest == null) ? "" : _digest.getMetaDigest(); try { status.updateStatus("m.checking"); - downloadDigestFile(); + downloadDigestFiles(); _digest = new Digest(_appdir); if (!olddig.equals(_digest.getMetaDigest())) { log.info("Unversioned digest changed. Revalidating..."); @@ -1189,7 +1189,7 @@ public class Application // exceptions to propagate up to the caller as there is nothing else we can do if (_digest == null) { status.updateStatus("m.updating_metadata"); - downloadDigestFile(); + downloadDigestFiles(); _digest = new Digest(_appdir); } @@ -1200,7 +1200,7 @@ public class Application // attempt to redownload both of our metadata files; again we pass errors up to our // caller because there's nothing we can do to automatically recover downloadConfigFile(); - downloadDigestFile(); + downloadDigestFiles(); _digest = new Digest(_appdir); // revalidate everything if we end up downloading new metadata clearValidationMarkers(); @@ -1459,13 +1459,15 @@ public class Application } /** - * Downloads a copy of Digest.DIGEST_FILE and validates its signature. + * Downloads the digest files and validates their signature. * @throws IOException */ - protected void downloadDigestFile () + protected void downloadDigestFiles () throws IOException { - downloadControlFile(Digest.DIGEST_FILE, true); + for (int version = 1; version <= Digest.VERSION; version++) { + downloadControlFile(Digest.digestFile(version), true); + } } /** diff --git a/src/main/java/com/threerings/getdown/data/Digest.java b/src/main/java/com/threerings/getdown/data/Digest.java index 9d30c6f..0de8dc3 100644 --- a/src/main/java/com/threerings/getdown/data/Digest.java +++ b/src/main/java/com/threerings/getdown/data/Digest.java @@ -30,21 +30,80 @@ import static com.threerings.getdown.Log.log; */ public class Digest { - /** The name of our MD5 digest file. */ - public static final String DIGEST_FILE = "digest.txt"; + /** The current version of the digest protocol. */ + public static final int VERSION = 2; + + /** + * Returns the name of the digest file for the specified protocol version. + */ + public static String digestFile (int version) { + String infix = version > 1 ? String.valueOf(version) : ""; + return FILE_NAME + infix + FILE_SUFFIX; + } + + /** + * Creates a digest file at the specified location using the supplied list of resources. + * @param version the version of the digest protocol to use. + */ + public static void createDigest (int version, List resources, File output) + throws IOException + { + MessageDigest md = getMessageDigest(); + StringBuilder data = new StringBuilder(); + PrintWriter pout = null; + try { + pout = new PrintWriter(new OutputStreamWriter(new FileOutputStream(output), "UTF-8")); + + // compute and append the MD5 digest of each resource in the list + for (Resource rsrc : resources) { + String path = rsrc.getPath(); + try { + String digest = rsrc.computeDigest(version, md, null); + note(data, path, digest); + pout.println(path + " = " + digest); + } catch (Throwable t) { + throw (IOException) new IOException( + "Error computing digest for: " + rsrc).initCause(t); + } + } + + // finally compute and append the digest for the file contents + md.reset(); + byte[] contents = data.toString().getBytes("UTF-8"); + String filename = digestFile(version); + pout.println(filename + " = " + StringUtil.hexlate(md.digest(contents))); + + } finally { + StreamUtil.close(pout); + } + } + + /** + * Obtains an appropriate message digest instance for use by the Getdown system. + */ + public static MessageDigest getMessageDigest () + { + try { + return MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException nsae) { + throw new RuntimeException("JVM does not support MD5. Gurp!"); + } + } /** * Creates a digest instance which will parse and validate the digest.txt in the * supplied application directory. + * @param version the version of the digest protocol to use. */ public Digest (File appdir) throws IOException { // parse and validate our digest file contents + String filename = digestFile(VERSION); StringBuilder data = new StringBuilder(); - File dfile = new File(appdir, DIGEST_FILE); + File dfile = new File(appdir, filename); for (String[] pair : ConfigUtil.parsePairs(dfile, false)) { - if (pair[0].equals(DIGEST_FILE)) { + if (pair[0].equals(filename)) { _metaDigest = pair[1]; break; } @@ -80,7 +139,7 @@ public class Digest public boolean validateResource (Resource resource, ProgressObserver obs) { try { - String cmd5 = resource.computeDigest(getMessageDigest(), obs); + String cmd5 = resource.computeDigest(VERSION, getMessageDigest(), obs); String emd5 = _digests.get(resource.getPath()); if (cmd5.equals(emd5)) { return true; @@ -101,53 +160,6 @@ public class Digest return _digests.get(resource.getPath()); } - /** - * Creates a digest file at the specified location using the supplied list of resources. - */ - public static void createDigest (List resources, File output) - throws IOException - { - MessageDigest md = getMessageDigest(); - StringBuilder data = new StringBuilder(); - PrintWriter pout = null; - try { - pout = new PrintWriter(new OutputStreamWriter(new FileOutputStream(output), "UTF-8")); - - // compute and append the MD5 digest of each resource in the list - for (Resource rsrc : resources) { - String path = rsrc.getPath(); - try { - String digest = rsrc.computeDigest(md, null); - note(data, path, digest); - pout.println(path + " = " + digest); - } catch (Throwable t) { - throw (IOException) new IOException( - "Error computing digest for: " + rsrc).initCause(t); - } - } - - // finally compute and append the digest for the file contents - md.reset(); - byte[] contents = data.toString().getBytes("UTF-8"); - pout.println(DIGEST_FILE + " = " + StringUtil.hexlate(md.digest(contents))); - - } finally { - StreamUtil.close(pout); - } - } - - /** - * Obtains an appropriate message digest instance for use by the Getdown system. - */ - public static MessageDigest getMessageDigest () - { - try { - return MessageDigest.getInstance("MD5"); - } catch (NoSuchAlgorithmException nsae) { - throw new RuntimeException("JVM does not support MD5. Gurp!"); - } - } - /** Used by {@link #createDigest} and {@link Digest}. */ protected static void note (StringBuilder data, String path, String digest) { @@ -156,4 +168,7 @@ public class Digest protected HashMap _digests = new HashMap(); protected String _metaDigest = ""; + + protected static final String FILE_NAME = "digest"; + protected static final String FILE_SUFFIX = ".txt"; } diff --git a/src/main/java/com/threerings/getdown/data/Resource.java b/src/main/java/com/threerings/getdown/data/Resource.java index 928d33e..df18c52 100644 --- a/src/main/java/com/threerings/getdown/data/Resource.java +++ b/src/main/java/com/threerings/getdown/data/Resource.java @@ -27,6 +27,91 @@ import static com.threerings.getdown.Log.log; */ public class Resource { + /** + * Computes the MD5 hash of the supplied file. + * @param version the version of the digest protocol to use. + */ + public static String computeDigest (int version, File target, MessageDigest md, + ProgressObserver obs) + throws IOException + { + md.reset(); + byte[] buffer = new byte[DIGEST_BUFFER_SIZE]; + int read; + + boolean isJar = isJar(target.getPath()); + boolean isPacked200Jar = isPacked200Jar(target.getPath()); + + // if this is a jar, we need to compute the digest in a "timestamp and file order" agnostic + // manner to properly correlate jardiff patched jars with their unpatched originals + if (isJar || isPacked200Jar){ + File tmpJarFile = null; + JarFile jar = null; + try { + // if this is a compressed jar file, uncompress it to compute the jar file digest + if (isPacked200Jar){ + tmpJarFile = new File(target.getPath() + ".tmp"); + FileUtil.unpackPacked200Jar(target, tmpJarFile); + jar = new JarFile(tmpJarFile); + } else{ + jar = new JarFile(target); + } + + List entries = Collections.list(jar.entries()); + Collections.sort(entries, ENTRY_COMP); + + int eidx = 0; + for (JarEntry entry : entries) { + // old versions of the digest code skipped metadata + if (version < 2) { + if (entry.getName().startsWith("META-INF")) { + updateProgress(obs, eidx, entries.size()); + continue; + } + } + + InputStream in = null; + try { + in = jar.getInputStream(entry); + while ((read = in.read(buffer)) != -1) { + md.update(buffer, 0, read); + } + } finally { + StreamUtil.close(in); + } + updateProgress(obs, eidx, entries.size()); + } + + } finally { + if (jar != null) { + try { + jar.close(); + } catch (IOException ioe) { + log.warning("Error closing jar", "path", target, "jar", jar, "error", ioe); + } + } + if (tmpJarFile != null) { + tmpJarFile.delete(); + } + } + + } else { + long totalSize = target.length(), position = 0L; + FileInputStream fin = null; + try { + fin = new FileInputStream(target); + while ((read = fin.read(buffer)) != -1) { + md.update(buffer, 0, read); + position += read; + updateProgress(obs, position, totalSize); + } + } finally { + StreamUtil.close(fin); + } + } + return StringUtil.hexlate(md.digest()); + } + /** * Creates a resource with the supplied remote URL and local path. */ @@ -111,8 +196,10 @@ public class Resource /** * Computes the MD5 hash of this resource's underlying file. * Note: This is both CPU and I/O intensive. + * @param version the version of the digest protocol to use. */ - public String computeDigest (MessageDigest md, ProgressObserver obs) throws IOException + public String computeDigest (int version, MessageDigest md, ProgressObserver obs) + throws IOException { File file; if (_local.toString().toLowerCase().endsWith(Application.CONFIG_FILE)) { @@ -120,7 +207,7 @@ public class Resource } else { file = _localNew.exists() ? _localNew : _local; } - return computeDigest(file, md, obs); + return computeDigest(version, file, md, obs); } /** @@ -213,81 +300,6 @@ public class Resource return _path; } - /** - * Computes the MD5 hash of the supplied file. - */ - public static String computeDigest (File target, MessageDigest md, ProgressObserver obs) - throws IOException - { - md.reset(); - byte[] buffer = new byte[DIGEST_BUFFER_SIZE]; - int read; - - boolean isJar = isJar(target.getPath()); - boolean isPacked200Jar = isPacked200Jar(target.getPath()); - - // if this is a jar, we need to compute the digest in a "timestamp and file order" agnostic - // manner to properly correlate jardiff patched jars with their unpatched originals - if (isJar || isPacked200Jar){ - File tmpJarFile = null; - JarFile jar = null; - try { - // if this is a compressed jar file, uncompress it to compute the jar file digest - if (isPacked200Jar){ - tmpJarFile = new File(target.getPath() + ".tmp"); - FileUtil.unpackPacked200Jar(target, tmpJarFile); - jar = new JarFile(tmpJarFile); - } else{ - jar = new JarFile(target); - } - - List entries = Collections.list(jar.entries()); - Collections.sort(entries, ENTRY_COMP); - - int eidx = 0; - for (JarEntry entry : entries) { - InputStream in = null; - try { - in = jar.getInputStream(entry); - while ((read = in.read(buffer)) != -1) { - md.update(buffer, 0, read); - } - } finally { - StreamUtil.close(in); - } - updateProgress(obs, eidx, entries.size()); - } - - } finally { - if (jar != null) { - try { - jar.close(); - } catch (IOException ioe) { - log.warning("Error closing jar", "path", target, "jar", jar, "error", ioe); - } - } - if (tmpJarFile != null) { - tmpJarFile.delete(); - } - } - - } else { - long totalSize = target.length(), position = 0L; - FileInputStream fin = null; - try { - fin = new FileInputStream(target); - while ((read = fin.read(buffer)) != -1) { - md.update(buffer, 0, read); - position += read; - updateProgress(obs, position, totalSize); - } - } finally { - StreamUtil.close(fin); - } - } - return StringUtil.hexlate(md.digest()); - } - /** Helper function to simplify the process of reporting progress. */ protected static void updateProgress (ProgressObserver obs, long pos, long total) { diff --git a/src/main/java/com/threerings/getdown/tools/Differ.java b/src/main/java/com/threerings/getdown/tools/Differ.java index 28fe715..9a34b72 100644 --- a/src/main/java/com/threerings/getdown/tools/Differ.java +++ b/src/main/java/com/threerings/getdown/tools/Differ.java @@ -107,8 +107,9 @@ public class Differ Resource orsrc = (oidx == -1) ? null : orsrcs.remove(oidx); if (orsrc != null) { // first see if they are the same - String odig = orsrc.computeDigest(md, null); - String ndig = rsrc.computeDigest(md, null); + int version = Digest.VERSION; + String odig = orsrc.computeDigest(version, md, null); + String ndig = rsrc.computeDigest(version, md, null); if (odig.equals(ndig)) { if (verbose) { System.out.println("Unchanged: " + rsrc.getPath()); diff --git a/src/main/java/com/threerings/getdown/tools/Digester.java b/src/main/java/com/threerings/getdown/tools/Digester.java index 8ec2411..661abf5 100644 --- a/src/main/java/com/threerings/getdown/tools/Digester.java +++ b/src/main/java/com/threerings/getdown/tools/Digester.java @@ -36,24 +36,40 @@ public class Digester public static void main (String[] args) throws IOException, GeneralSecurityException { - if (args.length != 1 && args.length != 4) { + switch (args.length) { + case 1: + createDigests(new File(args[0]), null, null, null); + break; + case 4: + createDigests(new File(args[0]), new File(args[1]), args[2], args[3]); + break; + default: System.err.println("Usage: Digester app_dir [keystore_path password alias]"); System.exit(255); } + } - createDigest(new File(args[0])); - if (args.length == 4) { - signDigest(new File(args[0]), new File(args[1]), args[2], args[3]); + /** + * Creates digest file(s) and optionally signs them if {@code keystore} is not null. + */ + public static void createDigests (File appdir, File keystore, String password, String alias) + throws IOException, GeneralSecurityException + { + for (int version = 1; version <= Digest.VERSION; version++) { + createDigest(version, appdir); + if (keystore != null) { + signDigest(version, appdir, keystore, password, alias); + } } } /** * Creates a digest file in the specified application directory. */ - public static void createDigest (File appdir) + public static void createDigest (int version, File appdir) throws IOException { - File target = new File(appdir, Digest.DIGEST_FILE); + File target = new File(appdir, Digest.digestFile(version)); System.out.println("Generating digest file '" + target + "'..."); // create our application and instruct it to parse its business @@ -70,17 +86,19 @@ public class Digester } // now generate the digest file - Digest.createDigest(rsrcs, target); + Digest.createDigest(version, rsrcs, target); } /** * Creates a digest file in the specified application directory. */ - public static void signDigest (File appdir, File storePath, String storePass, String storeAlias) + public static void signDigest (int version, File appdir, + File storePath, String storePass, String storeAlias) throws IOException, GeneralSecurityException { - File inputFile = new File(appdir, Digest.DIGEST_FILE); - File signatureFile = new File(appdir, Digest.DIGEST_FILE + Application.SIGNATURE_SUFFIX); + String filename = Digest.digestFile(version); + File inputFile = new File(appdir, filename); + File signatureFile = new File(appdir, filename + Application.SIGNATURE_SUFFIX); FileInputStream storeInput = null, dataInput = null; FileOutputStream signatureOutput = null; diff --git a/src/main/java/com/threerings/getdown/tools/DigesterTask.java b/src/main/java/com/threerings/getdown/tools/DigesterTask.java index a059250..18bcdda 100644 --- a/src/main/java/com/threerings/getdown/tools/DigesterTask.java +++ b/src/main/java/com/threerings/getdown/tools/DigesterTask.java @@ -13,6 +13,8 @@ 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 * application deployment. @@ -70,10 +72,7 @@ public class DigesterTask extends Task } try { - Digester.createDigest(_appdir); - if (_storepath != null) { - Digester.signDigest(_appdir, _storepath, _storepass, _storealias); - } + Digester.createDigests(_appdir, _storepath, _storepass, _storealias); } catch (IOException ioe) { throw new BuildException("Error creating digest: " + ioe.getMessage(), ioe); } catch (GeneralSecurityException gse) { From 41c59d7e1be0c4e326cbce5333541acb9607d3af Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Fri, 4 Nov 2016 19:35:05 -0700 Subject: [PATCH 06/11] Upgrade signing algorithm & digest algo. Since we're upping the protocol version, we can do all this too at the same time. So v2 digest will use SHA256, sign using SHA256withRSA and will include the contents of META-INF in the digests. Upgrades++! This also includes the code from pull request #73 to allow a .crt file to be included with the app to allow for signature verification of standalone apps (we already did this for applets via the applet parameters). --- .../threerings/getdown/data/Application.java | 2 +- .../com/threerings/getdown/data/Digest.java | 51 +++++++++++-------- .../getdown/launcher/GetdownApp.java | 24 ++++++++- .../getdown/tools/AppletParamSigner.java | 4 +- .../com/threerings/getdown/tools/Differ.java | 4 +- .../threerings/getdown/tools/Digester.java | 3 +- 6 files changed, 62 insertions(+), 26 deletions(-) diff --git a/src/main/java/com/threerings/getdown/data/Application.java b/src/main/java/com/threerings/getdown/data/Application.java index 0c56e15..c6dd522 100644 --- a/src/main/java/com/threerings/getdown/data/Application.java +++ b/src/main/java/com/threerings/getdown/data/Application.java @@ -1509,7 +1509,7 @@ public class Application FileInputStream dataInput = null; try { dataInput = new FileInputStream(target); - Signature sig = Signature.getInstance("SHA1withRSA"); + Signature sig = Signature.getInstance(Digest.SIG_ALGO); sig.initVerify(cert); while ((length = dataInput.read(buffer)) != -1) { sig.update(buffer, 0, length); diff --git a/src/main/java/com/threerings/getdown/data/Digest.java b/src/main/java/com/threerings/getdown/data/Digest.java index 0de8dc3..7eb74d2 100644 --- a/src/main/java/com/threerings/getdown/data/Digest.java +++ b/src/main/java/com/threerings/getdown/data/Digest.java @@ -25,7 +25,7 @@ import com.threerings.getdown.util.ProgressObserver; import static com.threerings.getdown.Log.log; /** - * Manages the digest.txt file and the computing and processing of MD5 digests for an + * Manages the digest.txt file and the computing and processing of digests for an * application. */ public class Digest @@ -33,6 +33,9 @@ public class Digest /** The current version of the digest protocol. */ public static final int VERSION = 2; + /** The current algorithm used to sign digest files. */ + public static final String SIG_ALGO = "SHA256withRSA"; + /** * Returns the name of the digest file for the specified protocol version. */ @@ -48,13 +51,13 @@ public class Digest public static void createDigest (int version, List resources, File output) throws IOException { - MessageDigest md = getMessageDigest(); + MessageDigest md = getMessageDigest(version); StringBuilder data = new StringBuilder(); PrintWriter pout = null; try { pout = new PrintWriter(new OutputStreamWriter(new FileOutputStream(output), "UTF-8")); - // compute and append the MD5 digest of each resource in the list + // compute and append the digest of each resource in the list for (Resource rsrc : resources) { String path = rsrc.getPath(); try { @@ -81,25 +84,33 @@ public class Digest /** * Obtains an appropriate message digest instance for use by the Getdown system. */ - public static MessageDigest getMessageDigest () + public static MessageDigest getMessageDigest (int version) { + String algo = version > 1 ? "SHA-256" : "MD5"; try { - return MessageDigest.getInstance("MD5"); + return MessageDigest.getInstance(algo); } catch (NoSuchAlgorithmException nsae) { - throw new RuntimeException("JVM does not support MD5. Gurp!"); + throw new RuntimeException("JVM does not support " + algo + ". Gurp!"); } } /** - * Creates a digest instance which will parse and validate the digest.txt in the - * supplied application directory. + * Creates a digest instance which will parse and validate the digest in the supplied + * application directory, using the current digest version. + */ + public Digest (File appdir) throws IOException { + this(appdir, VERSION); + } + + /** + * Creates a digest instance which will parse and validate the digest in the supplied + * application directory. * @param version the version of the digest protocol to use. */ - public Digest (File appdir) - throws IOException + public Digest (File appdir, int version) throws IOException { // parse and validate our digest file contents - String filename = digestFile(VERSION); + String filename = digestFile(version); StringBuilder data = new StringBuilder(); File dfile = new File(appdir, filename); for (String[] pair : ConfigUtil.parsePairs(dfile, false)) { @@ -112,11 +123,11 @@ public class Digest } // we've reached the end, validate our contents - MessageDigest md = getMessageDigest(); + MessageDigest md = getMessageDigest(version); byte[] contents = data.toString().getBytes("UTF-8"); - String md5 = StringUtil.hexlate(md.digest(contents)); - if (!md5.equals(_metaDigest)) { - String err = MessageUtil.tcompose("m.invalid_digest_file", _metaDigest, md5); + String hash = StringUtil.hexlate(md.digest(contents)); + if (!hash.equals(_metaDigest)) { + String err = MessageUtil.tcompose("m.invalid_digest_file", _metaDigest, hash); throw new IOException(err); } } @@ -130,7 +141,7 @@ public class Digest } /** - * Computes the MD5 hash of the specified resource and compares it with the value parsed from + * Computes the hash of the specified resource and compares it with the value parsed from * the digest file. Logs a message if the resource fails validation. * * @return true if the resource is valid, false if it failed the digest check or if an I/O @@ -139,13 +150,13 @@ public class Digest public boolean validateResource (Resource resource, ProgressObserver obs) { try { - String cmd5 = resource.computeDigest(VERSION, getMessageDigest(), obs); - String emd5 = _digests.get(resource.getPath()); - if (cmd5.equals(emd5)) { + String chash = resource.computeDigest(VERSION, getMessageDigest(VERSION), obs); + String ehash = _digests.get(resource.getPath()); + if (chash.equals(ehash)) { return true; } log.info("Resource failed digest check", - "rsrc", resource, "computed", cmd5, "expected", emd5); + "rsrc", resource, "computed", chash, "expected", ehash); } catch (Throwable t) { log.info("Resource failed digest check", "rsrc", resource, "error", t); } diff --git a/src/main/java/com/threerings/getdown/launcher/GetdownApp.java b/src/main/java/com/threerings/getdown/launcher/GetdownApp.java index 08087c6..fb68080 100644 --- a/src/main/java/com/threerings/getdown/launcher/GetdownApp.java +++ b/src/main/java/com/threerings/getdown/launcher/GetdownApp.java @@ -11,11 +11,16 @@ import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.BufferedOutputStream; import java.io.File; +import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintStream; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import javax.swing.JFrame; @@ -26,6 +31,7 @@ import com.samskivert.util.ArrayUtil; import com.samskivert.util.RunAnywhere; import com.samskivert.util.StringUtil; +import com.threerings.getdown.data.Digest; import com.threerings.getdown.data.SysProps; import static com.threerings.getdown.Log.log; @@ -80,6 +86,22 @@ public class GetdownApp System.exit(-1); } + // load X.509 certificate if it exists + File crtFile = new File(appDir, Digest.digestFile(Digest.VERSION) + ".crt"); + List crts = new ArrayList(); + if (crtFile.exists()) { + try { + FileInputStream fis = new FileInputStream(crtFile); + X509Certificate certificate = (X509Certificate) + CertificateFactory.getInstance("X.509").generateCertificate(fis); + fis.close(); + crts.add(certificate); + } catch (Exception e) { + log.warning("Certificate error: " + e.getMessage()); + System.exit(-1); + } + } + // pipe our output into a file in the application directory if (!SysProps.noLogRedir()) { File logFile = new File(appDir, "launcher.log"); @@ -105,7 +127,7 @@ public class GetdownApp log.info("-- Cur dir: " + System.getProperty("user.dir")); log.info("---------------------------------------------"); - Getdown app = new Getdown(appDir, appId, null, null, appArgs) { + Getdown app = new Getdown(appDir, appId, crts, null, appArgs) { @Override protected Container createContainer () { // create our user interface, and display it diff --git a/src/main/java/com/threerings/getdown/tools/AppletParamSigner.java b/src/main/java/com/threerings/getdown/tools/AppletParamSigner.java index ab83a30..78cac26 100644 --- a/src/main/java/com/threerings/getdown/tools/AppletParamSigner.java +++ b/src/main/java/com/threerings/getdown/tools/AppletParamSigner.java @@ -13,6 +13,8 @@ import java.security.Signature; import org.apache.commons.codec.binary.Base64; +import com.threerings.getdown.data.Digest; + /** * Produces a signed hash of the appbase, appname, and image path to ensure that signed copies of * Getdown are not hijacked to run malicious code. @@ -41,7 +43,7 @@ public class AppletParamSigner store.load(new BufferedInputStream(new FileInputStream(keystore)), storepass.toCharArray()); PrivateKey key = (PrivateKey)store.getKey(alias, keypass.toCharArray()); - Signature sig = Signature.getInstance("SHA1withRSA"); + Signature sig = Signature.getInstance(Digest.SIG_ALGO); sig.initSign(key); sig.update(params.getBytes()); String signed = new String(Base64.encodeBase64(sig.sign())); diff --git a/src/main/java/com/threerings/getdown/tools/Differ.java b/src/main/java/com/threerings/getdown/tools/Differ.java index 9a34b72..fbde2a9 100644 --- a/src/main/java/com/threerings/getdown/tools/Differ.java +++ b/src/main/java/com/threerings/getdown/tools/Differ.java @@ -94,7 +94,8 @@ public class Differ ArrayList nrsrcs, boolean verbose) throws IOException { - MessageDigest md = Digest.getMessageDigest(); + int version = Digest.VERSION; + MessageDigest md = Digest.getMessageDigest(version); JarOutputStream jout = null; try { jout = new JarOutputStream( @@ -107,7 +108,6 @@ public class Differ Resource orsrc = (oidx == -1) ? null : orsrcs.remove(oidx); if (orsrc != null) { // first see if they are the same - int version = Digest.VERSION; String odig = orsrc.computeDigest(version, md, null); String ndig = rsrc.computeDigest(version, md, null); if (odig.equals(ndig)) { diff --git a/src/main/java/com/threerings/getdown/tools/Digester.java b/src/main/java/com/threerings/getdown/tools/Digester.java index 661abf5..a3738c3 100644 --- a/src/main/java/com/threerings/getdown/tools/Digester.java +++ b/src/main/java/com/threerings/getdown/tools/Digester.java @@ -110,7 +110,8 @@ public class Digester PrivateKey key = (PrivateKey)store.getKey(storeAlias, storePass.toCharArray()); // sign the digest file - Signature sig = Signature.getInstance("SHA1withRSA"); + String algo = version > 1 ? Digest.SIG_ALGO : "SHA1withRSA"; + Signature sig = Signature.getInstance(algo); dataInput = new FileInputStream(inputFile); byte[] buffer = new byte[8192]; int length; From d802d2e2967ecf2cc84106d5573fd5e84542e30b Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Sat, 5 Nov 2016 07:07:08 -0700 Subject: [PATCH 07/11] Fix Javadoc. --- src/main/java/com/threerings/getdown/data/SysProps.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/threerings/getdown/data/SysProps.java b/src/main/java/com/threerings/getdown/data/SysProps.java index b883ba5..f983c4d 100644 --- a/src/main/java/com/threerings/getdown/data/SysProps.java +++ b/src/main/java/com/threerings/getdown/data/SysProps.java @@ -6,6 +6,7 @@ package com.threerings.getdown.data; import com.threerings.getdown.util.VersionUtil; +import com.threerings.getdown.launcher.Getdown; /** * This class encapsulates all system properties that are read and processed by Getdown. Don't From 9c528f89c0263690eb8feb5d6185443f3cded777 Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Sat, 5 Nov 2016 07:08:11 -0700 Subject: [PATCH 08/11] [maven-release-plugin] prepare release getdown-1.6 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index acb34bd..5c1b497 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.threerings getdown jar - 1.6-SNAPSHOT + 1.6 getdown An application installer and updater. From f2e600fdf2f430d0ef775965accaecbf30b7c955 Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Sat, 5 Nov 2016 07:08:16 -0700 Subject: [PATCH 09/11] [maven-release-plugin] prepare for next development iteration --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5c1b497..9815d4b 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.threerings getdown jar - 1.6 + 1.7-SNAPSHOT getdown An application installer and updater. From e9d07f3055b1279010723fe0ee16ff3c5a36276d Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Sat, 5 Nov 2016 07:31:47 -0700 Subject: [PATCH 10/11] Reference new 1.6 release. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c975499..cc3f16b 100644 --- a/README.md +++ b/README.md @@ -43,8 +43,8 @@ applications. The latest version of Getdown can be obtained thusly: * Download the pre-built jar file from Maven Central: - [getdown-1.5.jar](http://repo2.maven.org/maven2/com/threerings/getdown/1.5/getdown-1.5.jar) - * Obtain the jar artifact via Maven with the following identifier: `com.threerings:getdown:1.5`. + [getdown-1.6.jar](http://repo2.maven.org/maven2/com/threerings/getdown/1.6/getdown-1.6.jar) + * Obtain the jar artifact via Maven with the following identifier: `com.threerings:getdown:1.6`. * [Check out the code](https://github.com/threerings/getdown) and build it yourself. You can also: From 1053a11c460610c522de781e26da71a2b65dd4b4 Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Fri, 11 Nov 2016 07:57:36 -0800 Subject: [PATCH 11/11] Mark jspawnhelper as executable. Closes #74. --- .../threerings/getdown/launcher/Getdown.java | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/threerings/getdown/launcher/Getdown.java b/src/main/java/com/threerings/getdown/launcher/Getdown.java index b625ae1..1cd00bb 100644 --- a/src/main/java/com/threerings/getdown/launcher/Getdown.java +++ b/src/main/java/com/threerings/getdown/launcher/Getdown.java @@ -647,21 +647,10 @@ public abstract class Getdown extends Thread } vmjar.markAsValid(); - // Sun, why dost thou spite me? Java doesn't know anything about file permissions (and by - // extension then, neither does Jar), so on Joonix we have to hackily make java_vm/bin/java - // executable by execing chmod; a pox on their children! - if (!RunAnywhere.isWindows()) { - String vmbin = LaunchUtil.LOCAL_JAVA_DIR + File.separator + "bin" + - File.separator + "java"; - String cmd = "chmod a+rx " + _app.getLocalPath(vmbin); - try { - log.info("Please smack a Java engineer. Running: " + cmd); - Runtime.getRuntime().exec(cmd); - } catch (Exception e) { - log.warning("Failed to mark VM binary as executable", "cmd", cmd, "error", e); - // we should do something like tell the user or something but fucking fuck - } - } + // these only run on non-Windows platforms, so we use Unix file separators + String localJavaDir = LaunchUtil.LOCAL_JAVA_DIR + "/"; + makeExecutable(localJavaDir + "bin/java"); + makeExecutable(localJavaDir + "lib/amd64/jspawnhelper"); // lastly regenerate the .jsa dump file that helps Java to start up faster String vmpath = LaunchUtil.getJVMPath(_app.getLocalPath("")); @@ -669,12 +658,29 @@ public abstract class Getdown extends Thread log.info("Regenerating classes.jsa for " + vmpath + "..."); Runtime.getRuntime().exec(vmpath + " -Xshare:dump"); } catch (Exception e) { - log.warning("Failed to regenerate .jsa dum file", "error", e); + log.warning("Failed to regenerate .jsa dump file", "error", e); } reportTrackingEvent("jvm_complete", -1); } + protected void makeExecutable (String path) { + // Java doesn't know anything about file permissions (and by extension then, + // neither does Jar), so on Unix we have to hackily do so via chmod + if (!RunAnywhere.isWindows()) { + File target = _app.getLocalPath(path); + String cmd = "chmod a+rx " + target; + try { + if (target.exists()) { + log.info("Running: " + cmd); + Runtime.getRuntime().exec(cmd); + } + } catch (Exception e) { + log.warning("Failed to mark VM binary as executable", "cmd", cmd, "error", e); + } + } + } + /** * Called if the application is determined to be of an old version. */