Merge remote-tracking branch 'artelysGithub/master'

This commit is contained in:
Paul-Yves Lucas
2016-12-06 14:16:22 +01:00
14 changed files with 473 additions and 300 deletions
+2 -2
View File
@@ -43,8 +43,8 @@ applications.
The latest version of Getdown can be obtained thusly: The latest version of Getdown can be obtained thusly:
* Download the pre-built jar file from Maven Central: * 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) [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.5`. * 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. * [Check out the code](https://github.com/threerings/getdown) and build it yourself.
You can also: You can also:
+1 -1
View File
@@ -10,7 +10,7 @@
<groupId>com.threerings</groupId> <groupId>com.threerings</groupId>
<artifactId>getdown</artifactId> <artifactId>getdown</artifactId>
<packaging>jar</packaging> <packaging>jar</packaging>
<version>1.6-SNAPSHOT</version> <version>1.7-SNAPSHOT</version>
<name>getdown</name> <name>getdown</name>
<description>An application installer and updater.</description> <description>An application installer and updater.</description>
@@ -904,7 +904,7 @@ public class Application
// now re-download our control files; we download the digest first so that if it fails, // 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 // our config file will still reference the old version and re-running the updater will
// start the whole process over again // start the whole process over again
downloadDigestFile(); downloadDigestFiles();
downloadConfigFile(); downloadConfigFile();
} catch (IOException ex) { } catch (IOException ex) {
@@ -1171,7 +1171,7 @@ public class Application
String olddig = (_digest == null) ? "" : _digest.getMetaDigest(); String olddig = (_digest == null) ? "" : _digest.getMetaDigest();
try { try {
status.updateStatus("m.checking"); status.updateStatus("m.checking");
downloadDigestFile(); downloadDigestFiles();
_digest = new Digest(_appdir); _digest = new Digest(_appdir);
if (!olddig.equals(_digest.getMetaDigest())) { if (!olddig.equals(_digest.getMetaDigest())) {
log.info("Unversioned digest changed. Revalidating..."); 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 // exceptions to propagate up to the caller as there is nothing else we can do
if (_digest == null) { if (_digest == null) {
status.updateStatus("m.updating_metadata"); status.updateStatus("m.updating_metadata");
downloadDigestFile(); downloadDigestFiles();
_digest = new Digest(_appdir); _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 // 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 // caller because there's nothing we can do to automatically recover
downloadConfigFile(); downloadConfigFile();
downloadDigestFile(); downloadDigestFiles();
_digest = new Digest(_appdir); _digest = new Digest(_appdir);
// revalidate everything if we end up downloading new metadata // revalidate everything if we end up downloading new metadata
clearValidationMarkers(); 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 * @throws IOException
*/ */
protected void downloadDigestFile () protected void downloadDigestFiles ()
throws IOException 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; FileInputStream dataInput = null;
try { try {
dataInput = new FileInputStream(target); dataInput = new FileInputStream(target);
Signature sig = Signature.getInstance("SHA1withRSA"); Signature sig = Signature.getInstance(Digest.SIG_ALGO);
sig.initVerify(cert); sig.initVerify(cert);
while ((length = dataInput.read(buffer)) != -1) { while ((length = dataInput.read(buffer)) != -1) {
sig.update(buffer, 0, length); sig.update(buffer, 0, length);
@@ -25,26 +25,96 @@ import com.threerings.getdown.util.ProgressObserver;
import static com.threerings.getdown.Log.log; import static com.threerings.getdown.Log.log;
/** /**
* Manages the <code>digest.txt</code> file and the computing and processing of MD5 digests for an * Manages the <code>digest.txt</code> file and the computing and processing of digests for an
* application. * application.
*/ */
public class Digest public class Digest
{ {
/** The name of our MD5 digest file. */ /** The current version of the digest protocol. */
public static final String DIGEST_FILE = "digest.txt"; 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 <code>digest.txt</code> in the * Returns the name of the digest file for the specified protocol version.
* supplied application directory.
*/ */
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<Resource> resources, File output)
throws IOException throws IOException
{ {
// parse and validate our digest file contents MessageDigest md = getMessageDigest(version);
StringBuilder data = new StringBuilder(); 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)) { for (String[] pair : ConfigUtil.parsePairs(dfile, false)) {
if (pair[0].equals(DIGEST_FILE)) { if (pair[0].equals(filename)) {
_metaDigest = pair[1]; _metaDigest = pair[1];
break; break;
} }
@@ -53,11 +123,11 @@ public class Digest
} }
// we've reached the end, validate our contents // we've reached the end, validate our contents
MessageDigest md = getMessageDigest(); MessageDigest md = getMessageDigest(version);
byte[] contents = data.toString().getBytes("UTF-8"); byte[] contents = data.toString().getBytes("UTF-8");
String md5 = StringUtil.hexlate(md.digest(contents)); String hash = StringUtil.hexlate(md.digest(contents));
if (!md5.equals(_metaDigest)) { if (!hash.equals(_metaDigest)) {
String err = MessageUtil.tcompose("m.invalid_digest_file", _metaDigest, md5); String err = MessageUtil.tcompose("m.invalid_digest_file", _metaDigest, hash);
throw new IOException(err); 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. * 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 * @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) public boolean validateResource (Resource resource, ProgressObserver obs)
{ {
try { try {
String cmd5 = resource.computeDigest(getMessageDigest(), obs); String chash = resource.computeDigest(VERSION, getMessageDigest(VERSION), obs);
String emd5 = _digests.get(resource.getPath()); String ehash = _digests.get(resource.getPath());
if (cmd5.equals(emd5)) { if (chash.equals(ehash)) {
return true; return true;
} }
log.info("Resource failed digest check", log.info("Resource failed digest check",
"rsrc", resource, "computed", cmd5, "expected", emd5); "rsrc", resource, "computed", chash, "expected", ehash);
} catch (Throwable t) { } catch (Throwable t) {
log.info("Resource failed digest check", "rsrc", resource, "error", t); log.info("Resource failed digest check", "rsrc", resource, "error", t);
} }
@@ -101,53 +171,6 @@ public class Digest
return _digests.get(resource.getPath()); return _digests.get(resource.getPath());
} }
/**
* Creates a digest file at the specified location using the supplied list of resources.
*/
public static void createDigest (List<Resource> 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}. */ /** Used by {@link #createDigest} and {@link Digest}. */
protected static void note (StringBuilder data, String path, String digest) protected static void note (StringBuilder data, String path, String digest)
{ {
@@ -156,4 +179,7 @@ public class Digest
protected HashMap<String, String> _digests = new HashMap<String, String>(); protected HashMap<String, String> _digests = new HashMap<String, String>();
protected String _metaDigest = ""; protected String _metaDigest = "";
protected static final String FILE_NAME = "digest";
protected static final String FILE_SUFFIX = ".txt";
} }
@@ -27,6 +27,91 @@ import static com.threerings.getdown.Log.log;
*/ */
public class Resource 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<JarEntry> 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. * Creates a resource with the supplied remote URL and local path.
*/ */
@@ -35,6 +120,7 @@ public class Resource
_path = path; _path = path;
_remote = remote; _remote = remote;
_local = local; _local = local;
_localNew = new File(local.toString() + "_new");
String lpath = _local.getPath(); String lpath = _local.getPath();
_marker = new File(lpath + "v"); _marker = new File(lpath + "v");
@@ -66,6 +152,14 @@ public class Resource
return _local; 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. * Returns the location of the unpacked resource.
*/ */
@@ -102,11 +196,18 @@ public class Resource
/** /**
* Computes the MD5 hash of this resource's underlying file. * Computes the MD5 hash of this resource's underlying file.
* <em>Note:</em> This is both CPU and I/O intensive. * <em>Note:</em> 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 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; 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<JarEntry> 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. */ /** Helper function to simplify the process of reporting progress. */
protected static void updateProgress (ProgressObserver obs, long pos, long total) protected static void updateProgress (ProgressObserver obs, long pos, long total)
{ {
@@ -291,7 +310,7 @@ public class Resource
protected static boolean isJar (String path) protected static boolean isJar (String path)
{ {
return path.endsWith(".jar"); return path.endsWith(".jar") || path.endsWith(".jar_new");
} }
protected static boolean isPacked200Jar (String path) protected static boolean isPacked200Jar (String path)
@@ -301,7 +320,7 @@ public class Resource
protected String _path; protected String _path;
protected URL _remote; protected URL _remote;
protected File _local, _marker, _unpacked; protected File _local, _localNew, _marker, _unpacked;
protected boolean _unpack, _isJar, _isPacked200Jar; protected boolean _unpack, _isJar, _isPacked200Jar;
/** Used to sort the entries in a jar file. */ /** Used to sort the entries in a jar file. */
@@ -6,6 +6,7 @@
package com.threerings.getdown.data; package com.threerings.getdown.data;
import com.threerings.getdown.util.VersionUtil; 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 * 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; 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. /** If true, Getdown installs the app without ever bringing up a UI and then launches it.
* Usage: {@code -Dsilent=launch}. */ * Usage: {@code -Dsilent=launch}. */
public static boolean launchInSilent () { public static boolean launchInSilent () {
@@ -68,6 +68,7 @@ import com.threerings.getdown.net.HTTPDownloader;
import com.threerings.getdown.tools.Patcher; import com.threerings.getdown.tools.Patcher;
import com.threerings.getdown.util.ConfigUtil; import com.threerings.getdown.util.ConfigUtil;
import com.threerings.getdown.util.ConnectionUtil; import com.threerings.getdown.util.ConnectionUtil;
import com.threerings.getdown.util.FileUtil;
import com.threerings.getdown.util.LaunchUtil; import com.threerings.getdown.util.LaunchUtil;
import com.threerings.getdown.util.ProgressAggregator; import com.threerings.getdown.util.ProgressAggregator;
import com.threerings.getdown.util.ProgressObserver; import com.threerings.getdown.util.ProgressObserver;
@@ -104,6 +105,7 @@ public abstract class Getdown extends Thread
_launchInSilent = SysProps.launchInSilent(); _launchInSilent = SysProps.launchInSilent();
} }
_delay = SysProps.startDelay(); _delay = SysProps.startDelay();
_noInstall = SysProps.noInstall();
} catch (SecurityException se) { } catch (SecurityException se) {
// don't freak out, just assume non-silent and no delay; we're probably already // don't freak out, just assume non-silent and no delay; we're probably already
// recovering from a security failure // recovering from a security failure
@@ -126,6 +128,39 @@ public abstract class Getdown extends Thread
_startup = System.currentTimeMillis(); _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 * This is used by the applet which always needs a user interface and wants to load it as soon
* as possible. * as possible.
@@ -410,6 +445,9 @@ public abstract class Getdown extends Thread
// we'll keep track of all the resources we unpack // we'll keep track of all the resources we unpack
Set<Resource> unpacked = new HashSet<Resource>(); Set<Resource> unpacked = new HashSet<Resource>();
_toBeInstalledResouces = new ArrayList<Resource>();
_readyToInstall = false;
//setStep(Step.START); //setStep(Step.START);
for (int ii = 0; ii < MAX_LOOPS; ii++) { for (int ii = 0; ii < MAX_LOOPS; ii++) {
// if we aren't running in a JVM that meets our version requirements, either // 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 // Only launch if we aren't in silent mode. Some mystery program starting out
// of the blue would be disconcerting. // of the blue would be disconcerting.
if (!_silent || _launchInSilent) { if (!_silent || _launchInSilent) {
@@ -486,6 +530,13 @@ public abstract class Getdown extends Thread
return; 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 { try {
// if any of our resources have already been marked valid this is not a first // 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 // time install and we don't want to enable tracking
@@ -499,6 +550,7 @@ public abstract class Getdown extends Thread
download(failures); download(failures);
reportTrackingEvent("app_complete", -1); reportTrackingEvent("app_complete", -1);
} finally { } finally {
_enableTracking = false; _enableTracking = false;
} }
@@ -595,21 +647,10 @@ public abstract class Getdown extends Thread
} }
vmjar.markAsValid(); vmjar.markAsValid();
// Sun, why dost thou spite me? Java doesn't know anything about file permissions (and by // these only run on non-Windows platforms, so we use Unix file separators
// extension then, neither does Jar), so on Joonix we have to hackily make java_vm/bin/java String localJavaDir = LaunchUtil.LOCAL_JAVA_DIR + "/";
// executable by execing chmod; a pox on their children! makeExecutable(localJavaDir + "bin/java");
if (!RunAnywhere.isWindows()) { makeExecutable(localJavaDir + "lib/amd64/jspawnhelper");
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
}
}
// lastly regenerate the .jsa dump file that helps Java to start up faster // lastly regenerate the .jsa dump file that helps Java to start up faster
String vmpath = LaunchUtil.getJVMPath(_app.getLocalPath("")); String vmpath = LaunchUtil.getJVMPath(_app.getLocalPath(""));
@@ -617,12 +658,29 @@ public abstract class Getdown extends Thread
log.info("Regenerating classes.jsa for " + vmpath + "..."); log.info("Regenerating classes.jsa for " + vmpath + "...");
Runtime.getRuntime().exec(vmpath + " -Xshare:dump"); Runtime.getRuntime().exec(vmpath + " -Xshare:dump");
} catch (Exception e) { } 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); 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. * 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 _dead;
protected boolean _silent; protected boolean _silent;
protected boolean _noInstall;
protected boolean _launchInSilent; protected boolean _launchInSilent;
protected long _startup; protected long _startup;
protected List<Resource> _toBeInstalledResouces;
protected boolean _readyToInstall;
protected boolean _enableTracking = true; protected boolean _enableTracking = true;
protected int _reportedProgress = 0; protected int _reportedProgress = 0;
@@ -11,11 +11,16 @@ import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent; import java.awt.event.WindowEvent;
import java.io.BufferedOutputStream; import java.io.BufferedOutputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.PrintStream; 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.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.List; import java.util.List;
import javax.swing.JFrame; import javax.swing.JFrame;
@@ -26,6 +31,7 @@ import com.samskivert.util.ArrayUtil;
import com.samskivert.util.RunAnywhere; import com.samskivert.util.RunAnywhere;
import com.samskivert.util.StringUtil; import com.samskivert.util.StringUtil;
import com.threerings.getdown.data.Digest;
import com.threerings.getdown.data.SysProps; import com.threerings.getdown.data.SysProps;
import static com.threerings.getdown.Log.log; import static com.threerings.getdown.Log.log;
@@ -36,6 +42,20 @@ public class GetdownApp
{ {
public static void main (String[] argv) 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; int aidx = 0;
List<String> args = Arrays.asList(argv); List<String> args = Arrays.asList(argv);
@@ -66,6 +86,22 @@ public class GetdownApp
System.exit(-1); System.exit(-1);
} }
// load X.509 certificate if it exists
File crtFile = new File(appDir, Digest.digestFile(Digest.VERSION) + ".crt");
List<Certificate> crts = new ArrayList<Certificate>();
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 // pipe our output into a file in the application directory
if (!SysProps.noLogRedir()) { if (!SysProps.noLogRedir()) {
File logFile = new File(appDir, "launcher.log"); File logFile = new File(appDir, "launcher.log");
@@ -91,118 +127,114 @@ public class GetdownApp
log.info("-- Cur dir: " + System.getProperty("user.dir")); log.info("-- Cur dir: " + System.getProperty("user.dir"));
log.info("---------------------------------------------"); log.info("---------------------------------------------");
try { Getdown app = new Getdown(appDir, appId, crts, null, appArgs) {
Getdown app = new Getdown(appDir, appId, null, null, appArgs) { @Override
@Override protected Container createContainer () {
protected Container createContainer () { // create our user interface, and display it
// create our user interface, and display it String title = StringUtil.isBlank(_ifc.name) ? "" : _ifc.name;
String title = StringUtil.isBlank(_ifc.name) ? "" : _ifc.name; if (_frame == null) {
if (_frame == null) { _frame = new JFrame(title);
_frame = new JFrame(title); _frame.addWindowListener(new WindowAdapter() {
_frame.addWindowListener(new WindowAdapter() { @Override
@Override public void windowClosing (WindowEvent evt) {
public void windowClosing (WindowEvent evt) { handleWindowClose();
handleWindowClose();
}
});
_frame.setUndecorated(_ifc.hideDecorations);
_frame.setResizable(false);
} else {
_frame.setTitle(title);
_frame.getContentPane().removeAll();
}
if (_ifc.iconImages != null) {
ArrayList<Image> icons = new ArrayList<Image>();
for (String path : _ifc.iconImages) {
Image img = loadImage(path);
if (img == null) {
log.warning("Error loading icon image", "path", path);
} else {
icons.add(img);
}
} }
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<Image> icons = new ArrayList<Image>();
for (String path : _ifc.iconImages) {
Image img = loadImage(path);
if (img == null) {
log.warning("Error loading icon image", "path", path);
} else { } else {
SwingUtil.setFrameIcons(_frame, icons); icons.add(img);
} }
} }
if (icons.isEmpty()) {
_frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); log.warning("Failed to load any icons", "iconImages", _ifc.iconImages);
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();
} else { } else {
System.exit(exitCode); SwingUtil.setFrameIcons(_frame, icons);
} }
} }
@Override _frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
protected void fail (String message) { return _frame.getContentPane();
// 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()) { @Override
_frame.dispose(); protected void showContainer () {
_frame.setUndecorated(false); if (_frame != null) {
showContainer(); _frame.pack();
} SwingUtil.centerWindow(_frame);
super.fail(message); _frame.setVisible(true);
} }
}
protected JFrame _frame; @Override
}; protected void disposeContainer () {
app.start(); if (_frame != null) {
_frame.dispose();
_frame = null;
}
}
} catch (Exception e) { @Override
log.warning("main() failed.", e); 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;
} }
} }
@@ -78,7 +78,7 @@ public class HTTPDownloader extends Downloader
long currentSize = 0L; long currentSize = 0L;
try { try {
in = conn.getInputStream(); in = conn.getInputStream();
out = new FileOutputStream(rsrc.getLocal()); out = new FileOutputStream(rsrc.getLocalNew());
int read; int read;
// TODO: look to see if we have a download info file // TODO: look to see if we have a download info file
@@ -13,6 +13,8 @@ import java.security.Signature;
import org.apache.commons.codec.binary.Base64; 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 * 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. * Getdown are not hijacked to run malicious code.
@@ -41,7 +43,7 @@ public class AppletParamSigner
store.load(new BufferedInputStream(new FileInputStream(keystore)), store.load(new BufferedInputStream(new FileInputStream(keystore)),
storepass.toCharArray()); storepass.toCharArray());
PrivateKey key = (PrivateKey)store.getKey(alias, keypass.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.initSign(key);
sig.update(params.getBytes()); sig.update(params.getBytes());
String signed = new String(Base64.encodeBase64(sig.sign())); String signed = new String(Base64.encodeBase64(sig.sign()));
@@ -94,7 +94,8 @@ public class Differ
ArrayList<Resource> nrsrcs, boolean verbose) ArrayList<Resource> nrsrcs, boolean verbose)
throws IOException throws IOException
{ {
MessageDigest md = Digest.getMessageDigest(); int version = Digest.VERSION;
MessageDigest md = Digest.getMessageDigest(version);
JarOutputStream jout = null; JarOutputStream jout = null;
try { try {
jout = new JarOutputStream( jout = new JarOutputStream(
@@ -107,8 +108,8 @@ public class Differ
Resource orsrc = (oidx == -1) ? null : orsrcs.remove(oidx); Resource orsrc = (oidx == -1) ? null : orsrcs.remove(oidx);
if (orsrc != null) { if (orsrc != null) {
// first see if they are the same // first see if they are the same
String odig = orsrc.computeDigest(md, null); String odig = orsrc.computeDigest(version, md, null);
String ndig = rsrc.computeDigest(md, null); String ndig = rsrc.computeDigest(version, md, null);
if (odig.equals(ndig)) { if (odig.equals(ndig)) {
if (verbose) { if (verbose) {
System.out.println("Unchanged: " + rsrc.getPath()); System.out.println("Unchanged: " + rsrc.getPath());
@@ -36,24 +36,40 @@ public class Digester
public static void main (String[] args) public static void main (String[] args)
throws IOException, GeneralSecurityException 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.err.println("Usage: Digester app_dir [keystore_path password alias]");
System.exit(255); System.exit(255);
} }
}
createDigest(new File(args[0])); /**
if (args.length == 4) { * Creates digest file(s) and optionally signs them if {@code keystore} is not null.
signDigest(new File(args[0]), new File(args[1]), args[2], args[3]); */
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. * 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 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 + "'..."); System.out.println("Generating digest file '" + target + "'...");
// create our application and instruct it to parse its business // create our application and instruct it to parse its business
@@ -70,17 +86,19 @@ public class Digester
} }
// now generate the digest file // now generate the digest file
Digest.createDigest(rsrcs, target); Digest.createDigest(version, rsrcs, target);
} }
/** /**
* Creates a digest file in the specified application directory. * 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 throws IOException, GeneralSecurityException
{ {
File inputFile = new File(appdir, Digest.DIGEST_FILE); String filename = Digest.digestFile(version);
File signatureFile = new File(appdir, Digest.DIGEST_FILE + Application.SIGNATURE_SUFFIX); File inputFile = new File(appdir, filename);
File signatureFile = new File(appdir, filename + Application.SIGNATURE_SUFFIX);
FileInputStream storeInput = null, dataInput = null; FileInputStream storeInput = null, dataInput = null;
FileOutputStream signatureOutput = null; FileOutputStream signatureOutput = null;
@@ -92,7 +110,8 @@ public class Digester
PrivateKey key = (PrivateKey)store.getKey(storeAlias, storePass.toCharArray()); PrivateKey key = (PrivateKey)store.getKey(storeAlias, storePass.toCharArray());
// sign the digest file // 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); dataInput = new FileInputStream(inputFile);
byte[] buffer = new byte[8192]; byte[] buffer = new byte[8192];
int length; int length;
@@ -13,6 +13,8 @@ import java.security.GeneralSecurityException;
import org.apache.tools.ant.BuildException; import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task; import org.apache.tools.ant.Task;
import com.threerings.getdown.data.Digest;
/** /**
* An ant task used to create a <code>digest.txt</code> for a Getdown * An ant task used to create a <code>digest.txt</code> for a Getdown
* application deployment. * application deployment.
@@ -70,10 +72,7 @@ public class DigesterTask extends Task
} }
try { try {
Digester.createDigest(_appdir); Digester.createDigests(_appdir, _storepath, _storepass, _storealias);
if (_storepath != null) {
Digester.signDigest(_appdir, _storepath, _storepass, _storealias);
}
} catch (IOException ioe) { } catch (IOException ioe) {
throw new BuildException("Error creating digest: " + ioe.getMessage(), ioe); throw new BuildException("Error creating digest: " + ioe.getMessage(), ioe);
} catch (GeneralSecurityException gse) { } catch (GeneralSecurityException gse) {
@@ -60,6 +60,9 @@ public class FileUtil extends com.samskivert.util.FileUtil
fin = new FileInputStream(source); fin = new FileInputStream(source);
fout = new FileOutputStream(dest); fout = new FileOutputStream(dest);
StreamUtil.copy(fin, fout); StreamUtil.copy(fin, fout);
// close the input stream now so we can delete 'source'
fin.close();
fin = null;
if (!source.delete()) { if (!source.delete()) {
log.warning("Failed to delete " + source + log.warning("Failed to delete " + source +
" after brute force copy to " + dest + "."); " after brute force copy to " + dest + ".");