diff --git a/src/main/java/org/qora/controller/AutoUpdate.java b/src/main/java/org/qora/controller/AutoUpdate.java index 6e67107a..543c6a99 100644 --- a/src/main/java/org/qora/controller/AutoUpdate.java +++ b/src/main/java/org/qora/controller/AutoUpdate.java @@ -2,10 +2,13 @@ package org.qora.controller; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.List; @@ -22,6 +25,7 @@ import org.qora.repository.RepositoryManager; import org.qora.settings.Settings; import org.qora.transaction.ArbitraryTransaction; import org.qora.transaction.Transaction.TransactionType; +import org.qora.transform.Transformer; import org.qora.utils.NTP; import com.google.common.hash.HashCode; @@ -38,6 +42,9 @@ public class AutoUpdate extends Thread { private static final int UPDATE_SERVICE = 1; private static final List ARBITRARY_TX_TYPE = Arrays.asList(TransactionType.ARBITRARY); + private static final int GIT_COMMIT_HASH_LENGTH = 20; // SHA-1 + private static final int EXPECTED_DATA_LENGTH = Transformer.TIMESTAMP_LENGTH + GIT_COMMIT_HASH_LENGTH + Transformer.SHA256_LENGTH; + private static AutoUpdate instance; private boolean isStopping = false; @@ -90,14 +97,29 @@ public class AutoUpdate extends Thread { if (!arbitraryTransaction.isDataLocal()) continue; // We can't access data - // TODO: check arbitrary data length (pre-fetch) matches git commit length (20) + sha256 hash length (32) = 52 bytes + // TODO: check arbitrary data length (pre-fetch) matches build timestamp (8) + git commit length (20) + sha256 hash length (32) = 60 bytes + + byte[] data = arbitraryTransaction.fetchData(); + if (data.length != EXPECTED_DATA_LENGTH) + continue; + + ByteBuffer byteBuffer = ByteBuffer.wrap(data); + + long updateTimestamp = byteBuffer.getLong(); + if (updateTimestamp <= buildTimestamp) + continue; // update is the same, or older, than current code + + byte[] commitHash = new byte[GIT_COMMIT_HASH_LENGTH]; + byteBuffer.get(commitHash); + + byte[] downloadHash = new byte[Transformer.SHA256_LENGTH]; + byteBuffer.get(downloadHash); - byte[] commitHash = arbitraryTransaction.fetchData(); LOGGER.info(String.format("Update's git commit hash: %s", HashCode.fromBytes(commitHash).toString())); String[] autoUpdateRepos = Settings.getInstance().getAutoUpdateRepos(); for (String repo : autoUpdateRepos) - if (attemptUpdate(commitHash, repo)) { + if (attemptUpdate(commitHash, downloadHash, repo)) { // Consider ourselves updated so don't re-re-re-download buildTimestamp = NTP.getTime(); attemptedUpdate = true; @@ -116,7 +138,7 @@ public class AutoUpdate extends Thread { isStopping = true; } - private static boolean attemptUpdate(byte[] commitHash, String repoBaseUri) { + private static boolean attemptUpdate(byte[] commitHash, byte[] downloadHash, String repoBaseUri) { LOGGER.info(String.format("Fetching update from %s", repoBaseUri)); InputStream in = ApiRequest.fetchStream(repoBaseUri + "/raw/" + HashCode.fromBytes(commitHash).toString() + "/" + JAR_FILENAME); if (in == null) { @@ -126,9 +148,35 @@ public class AutoUpdate extends Thread { Path newJar = Paths.get(NEW_JAR_FILENAME); try { + MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); + // Save input stream into new JAR LOGGER.debug(String.format("Saving update from %s into %s", repoBaseUri, newJar.toString())); - Files.copy(in, newJar, StandardCopyOption.REPLACE_EXISTING); + + OutputStream out = Files.newOutputStream(newJar); + byte[] buffer = new byte[1024 * 1024]; + do { + int nread = in.read(buffer); + if (nread == -1) + break; + + sha256.update(buffer, 0, nread); + out.write(buffer, 0, nread); + } while (true); + + // Check hash + byte[] hash = sha256.digest(); + if (!Arrays.equals(downloadHash, hash)) { + LOGGER.warn(String.format("Downloaded JAR's hash %s doesn't match %s", HashCode.fromBytes(hash).toString(), HashCode.fromBytes(downloadHash).toString())); + + try { + Files.deleteIfExists(newJar); + } catch (IOException de) { + LOGGER.warn(String.format("Failed to delete download: %s", de.getMessage())); + } + + return false; + } } catch (IOException e) { LOGGER.warn(String.format("Failed to save update from %s into %s", repoBaseUri, newJar.toString())); @@ -139,6 +187,8 @@ public class AutoUpdate extends Thread { } return false; // failed - try another repo + } catch (NoSuchAlgorithmException e) { + return true; // not repo's fault } // Call ApplyUpdate to end this process (unlocking current JAR so it can be replaced)