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: diff --git a/pom.xml b/pom.xml index acb34bd..9815d4b 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.threerings getdown jar - 1.6-SNAPSHOT + 1.7-SNAPSHOT getdown An application installer and updater. diff --git a/src/main/java/com/threerings/getdown/data/Application.java b/src/main/java/com/threerings/getdown/data/Application.java index ce72f50..c6dd522 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); + } } /** @@ -1507,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 9d30c6f..7eb74d2 100644 --- a/src/main/java/com/threerings/getdown/data/Digest.java +++ b/src/main/java/com/threerings/getdown/data/Digest.java @@ -25,26 +25,96 @@ 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 { - /** 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; + + /** The current algorithm used to sign digest files. */ + public static final String SIG_ALGO = "SHA256withRSA"; /** - * Creates a digest instance which will parse and validate the digest.txt in the - * supplied application directory. + * Returns the name of the digest file for the specified protocol version. */ - public Digest (File appdir) + 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 { - // parse and validate our digest file contents + MessageDigest md = getMessageDigest(version); StringBuilder data = new StringBuilder(); - File dfile = new File(appdir, DIGEST_FILE); + PrintWriter pout = null; + try { + pout = new PrintWriter(new OutputStreamWriter(new FileOutputStream(output), "UTF-8")); + + // compute and append the 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 (int version) + { + String algo = version > 1 ? "SHA-256" : "MD5"; + try { + return MessageDigest.getInstance(algo); + } catch (NoSuchAlgorithmException nsae) { + throw new RuntimeException("JVM does not support " + algo + ". Gurp!"); + } + } + + /** + * 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, int version) throws IOException + { + // parse and validate our digest file contents + String filename = digestFile(version); + StringBuilder data = new StringBuilder(); + 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; } @@ -53,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); } } @@ -71,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 @@ -80,13 +150,13 @@ public class Digest public boolean validateResource (Resource resource, ProgressObserver obs) { try { - String cmd5 = resource.computeDigest(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); } @@ -101,53 +171,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 +179,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 7ad0a1e..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. */ @@ -35,6 +120,7 @@ public class Resource _path = path; _remote = remote; _local = local; + _localNew = new File(local.toString() + "_new"); String lpath = _local.getPath(); _marker = new File(lpath + "v"); @@ -66,6 +152,14 @@ public class Resource return _local; } + /** + * Returns the location of the to-be-installed new version of this resource. + */ + public File getLocalNew () + { + return _localNew; + } + /** * Returns the location of the unpacked resource. */ @@ -102,11 +196,18 @@ 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) + public String computeDigest (int version, MessageDigest md, ProgressObserver obs) throws IOException { - return computeDigest(_local, md, obs); + File file; + if (_local.toString().toLowerCase().endsWith(Application.CONFIG_FILE)) { + file = _local; + } else { + file = _localNew.exists() ? _localNew : _local; + } + return computeDigest(version, file, md, obs); } /** @@ -199,88 +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) { - // 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); - 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) { @@ -291,7 +310,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) @@ -301,7 +320,7 @@ public class Resource protected String _path; protected URL _remote; - protected File _local, _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 6dfdf9c..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 @@ -49,6 +50,13 @@ public class SysProps return System.getProperty("silent") != 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. * 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..1cd00bb 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; @@ -104,6 +105,7 @@ public abstract class Getdown extends Thread _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 @@ -126,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. @@ -410,6 +445,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++) { // if we aren't running in a JVM that meets our version requirements, either @@ -471,6 +509,12 @@ public abstract class Getdown extends Thread } } + // 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. if (!_silent || _launchInSilent) { @@ -486,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 @@ -499,6 +550,7 @@ public abstract class Getdown extends Thread download(failures); reportTrackingEvent("app_complete", -1); + } finally { _enableTracking = false; } @@ -595,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("")); @@ -617,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. */ @@ -1219,9 +1277,13 @@ public abstract class Getdown extends Thread protected boolean _dead; protected boolean _silent; + protected boolean _noInstall; protected boolean _launchInSilent; protected long _startup; + protected List _toBeInstalledResouces; + protected boolean _readyToInstall; + protected boolean _enableTracking = true; protected int _reportedProgress = 0; diff --git a/src/main/java/com/threerings/getdown/launcher/GetdownApp.java b/src/main/java/com/threerings/getdown/launcher/GetdownApp.java index 57818ff..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; @@ -36,6 +42,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); @@ -66,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"); @@ -91,118 +127,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, crts, 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/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/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 28fe715..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,8 +108,8 @@ 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); + 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..a3738c3 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; @@ -92,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; 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) { diff --git a/src/main/java/com/threerings/getdown/util/FileUtil.java b/src/main/java/com/threerings/getdown/util/FileUtil.java index f8362eb..67c0db9 100644 --- a/src/main/java/com/threerings/getdown/util/FileUtil.java +++ b/src/main/java/com/threerings/getdown/util/FileUtil.java @@ -60,6 +60,9 @@ public class FileUtil extends com.samskivert.util.FileUtil fin = new FileInputStream(source); fout = new FileOutputStream(dest); StreamUtil.copy(fin, fout); + // 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 + ".");