Merge pull request #205 from Microsoft/verify-hash-after-download

Verify hash after download
This commit is contained in:
Geoffrey Goh
2016-02-19 10:59:12 -08:00
48 changed files with 377 additions and 162 deletions

View File

@@ -21,13 +21,12 @@
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
146834051AC3E58100842450 /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 146834041AC3E56700842450 /* libReact.a */; };
542D39D41C22CED700D4B648 /* libCodePush.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 81551E0F1B3B427200F5B9F1 /* libCodePush.a */; };
540D20251C76EA0400D6EF41 /* libCodePush.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 540D20241C76E4D300D6EF41 /* libCodePush.a */; };
544161591B8BCA81000D9E25 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; };
5451ACBA1B86A5B600E2A7DF /* CheckForUpdateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 5451ACB81B86A5B600E2A7DF /* CheckForUpdateTests.m */; };
5451ACEC1B86E40A00E2A7DF /* libRCTTest.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5451ACEB1B86E34300E2A7DF /* libRCTTest.a */; };
54D774BA1B87DAF800F2ABF8 /* InstallUpdateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 54D774B91B87DAF800F2ABF8 /* InstallUpdateTests.m */; };
54F5F2B41BF6B45D007C3CEA /* DownloadProgressTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 54F5F2B31BF6B45D007C3CEA /* DownloadProgressTests.m */; };
81551E1B1B3B428000F5B9F1 /* libCodePush.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 81551E0F1B3B427200F5B9F1 /* libCodePush.a */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -101,6 +100,13 @@
remoteGlobalIDString = 83CBBA2E1A601D0E00E9B192;
remoteInfo = React;
};
540D20231C76E4D300D6EF41 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 81551E0A1B3B427200F5B9F1 /* CodePush.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 134814201AA4EA6300B7C361;
remoteInfo = CodePush;
};
5451ACEA1B86E34300E2A7DF /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 5451ACE61B86E34300E2A7DF /* RCTTest.xcodeproj */;
@@ -115,13 +121,6 @@
remoteGlobalIDString = 134814201AA4EA6300B7C361;
remoteInfo = RCTLinking;
};
81551E0E1B3B427200F5B9F1 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 81551E0A1B3B427200F5B9F1 /* CodePush.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 134814201AA4EA6300B7C361;
remoteInfo = CodePush;
};
832341B41AAA6A8300B99B32 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */;
@@ -155,7 +154,7 @@
54D774B91B87DAF800F2ABF8 /* InstallUpdateTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = InstallUpdateTests.m; sourceTree = "<group>"; };
54F5F2B31BF6B45D007C3CEA /* DownloadProgressTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DownloadProgressTests.m; sourceTree = "<group>"; };
78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = "../node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj"; sourceTree = "<group>"; };
81551E0A1B3B427200F5B9F1 /* CodePush.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = CodePush.xcodeproj; path = ../../../CodePush.xcodeproj; sourceTree = "<group>"; };
81551E0A1B3B427200F5B9F1 /* CodePush.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = CodePush.xcodeproj; path = ../../../ios/CodePush.xcodeproj; sourceTree = "<group>"; };
832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = "../node_modules/react-native/Libraries/Text/RCTText.xcodeproj"; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -164,7 +163,6 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
542D39D41C22CED700D4B648 /* libCodePush.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -172,6 +170,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
540D20251C76EA0400D6EF41 /* libCodePush.a in Frameworks */,
544161591B8BCA81000D9E25 /* libRCTText.a in Frameworks */,
5451ACEC1B86E40A00E2A7DF /* libRCTTest.a in Frameworks */,
146834051AC3E58100842450 /* libReact.a in Frameworks */,
@@ -184,7 +183,6 @@
139105C61AF99C1200B5F7CC /* libRCTSettings.a in Frameworks */,
00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */,
139FDEF61B0652A700C62182 /* libRCTWebSocket.a in Frameworks */,
81551E1B1B3B428000F5B9F1 /* libCodePush.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -295,6 +293,14 @@
name = Products;
sourceTree = "<group>";
};
540D20201C76E4D300D6EF41 /* Products */ = {
isa = PBXGroup;
children = (
540D20241C76E4D300D6EF41 /* libCodePush.a */,
);
name = Products;
sourceTree = "<group>";
};
5451ACE71B86E34300E2A7DF /* Products */ = {
isa = PBXGroup;
children = (
@@ -311,14 +317,6 @@
name = Products;
sourceTree = "<group>";
};
81551E0B1B3B427200F5B9F1 /* Products */ = {
isa = PBXGroup;
children = (
81551E0F1B3B427200F5B9F1 /* libCodePush.a */,
);
name = Products;
sourceTree = "<group>";
};
832341AE1AAA6A7D00B99B32 /* Libraries */ = {
isa = PBXGroup;
children = (
@@ -435,7 +433,7 @@
projectDirPath = "";
projectReferences = (
{
ProductGroup = 81551E0B1B3B427200F5B9F1 /* Products */;
ProductGroup = 540D20201C76E4D300D6EF41 /* Products */;
ProjectRef = 81551E0A1B3B427200F5B9F1 /* CodePush.xcodeproj */;
},
{
@@ -559,6 +557,13 @@
remoteRef = 146834031AC3E56700842450 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
540D20241C76E4D300D6EF41 /* libCodePush.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
path = libCodePush.a;
remoteRef = 540D20231C76E4D300D6EF41 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
5451ACEB1B86E34300E2A7DF /* libRCTTest.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
@@ -573,13 +578,6 @@
remoteRef = 78C398B81ACF4ADC00677621 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
81551E0F1B3B427200F5B9F1 /* libCodePush.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
path = libCodePush.a;
remoteRef = 81551E0E1B3B427200F5B9F1 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
832341B51AAA6A8300B99B32 /* libRCTText.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
@@ -684,7 +682,7 @@
"$(inherited)",
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
"$(SRCROOT)/../node_modules/react-native/React/**",
"$(SRCROOT)/../../..",
"$(SRCROOT)/../../../ios/**",
"$(SRCROOT)/../node_modules/react-native/Libraries/Text/",
);
INFOPLIST_FILE = ../CodePushDemoAppTests/Info.plist;
@@ -708,7 +706,7 @@
"$(inherited)",
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
"$(SRCROOT)/../node_modules/react-native/React/**",
"$(SRCROOT)/../../..",
"$(SRCROOT)/../../../ios/**",
"$(SRCROOT)/../node_modules/react-native/Libraries/Text/",
);
INFOPLIST_FILE = ../CodePushDemoAppTests/Info.plist;
@@ -727,7 +725,7 @@
"$(inherited)",
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
"$(SRCROOT)/../node_modules/react-native/React/**",
"$(SRCROOT)/../../..",
"$(SRCROOT)/../../../ios/**",
);
INFOPLIST_FILE = "$(SRCROOT)/CodePushDemoApp/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
@@ -747,7 +745,7 @@
"$(inherited)",
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
"$(SRCROOT)/../node_modules/react-native/React/**",
"$(SRCROOT)/../../..",
"$(SRCROOT)/../../../ios/**",
);
INFOPLIST_FILE = "$(SRCROOT)/CodePushDemoApp/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";

View File

@@ -1,7 +1,7 @@
package com.microsoft.codepush.react;
public class CodePushInvalidUpdateException extends RuntimeException {
public CodePushInvalidUpdateException() {
super("Update is invalid - no files with extension .bundle, .js or .jsbundle were found in the update package.");
public CodePushInvalidUpdateException(String message) {
super(message);
}
}

View File

@@ -153,12 +153,13 @@ public class CodePushPackage {
public void downloadPackage(Context applicationContext, ReadableMap updatePackage,
DownloadProgressCallback progressCallback) throws IOException {
String newPackageFolderPath = getPackageFolderPath(CodePushUtils.tryGetString(updatePackage, PACKAGE_HASH_KEY));
if (FileUtils.fileAtPathExists(newPackageFolderPath)) {
String newUpdateHash = CodePushUtils.tryGetString(updatePackage, PACKAGE_HASH_KEY);
String newUpdateFolderPath = getPackageFolderPath(newUpdateHash);
String newUpdateMetadataPath = CodePushUtils.appendPathComponent(newUpdateFolderPath, PACKAGE_FILE_NAME);
if (FileUtils.fileAtPathExists(newUpdateFolderPath)) {
// This removes any stale data in newPackageFolderPath that could have been left
// uncleared due to a crash or error during the download or install process.
FileUtils.deleteDirectoryAtPath(newPackageFolderPath);
FileUtils.deleteDirectoryAtPath(newUpdateFolderPath);
}
String downloadUrlString = CodePushUtils.tryGetString(updatePackage, DOWNLOAD_URL_KEY);
@@ -229,21 +230,33 @@ public class CodePushPackage {
// Merge contents with current update based on the manifest
String diffManifestFilePath = CodePushUtils.appendPathComponent(unzippedFolderPath,
DIFF_MANIFEST_FILE_NAME);
if (FileUtils.fileAtPathExists(diffManifestFilePath)) {
boolean isDiffUpdate = FileUtils.fileAtPathExists(diffManifestFilePath);
if (isDiffUpdate) {
String currentPackageFolderPath = getCurrentPackageFolderPath();
CodePushUpdateUtils.copyNecessaryFilesFromCurrentPackage(diffManifestFilePath, currentPackageFolderPath, newPackageFolderPath);
CodePushUpdateUtils.copyNecessaryFilesFromCurrentPackage(diffManifestFilePath, currentPackageFolderPath, newUpdateFolderPath);
File diffManifestFile = new File(diffManifestFilePath);
diffManifestFile.delete();
}
FileUtils.copyDirectoryContents(unzippedFolderPath, newPackageFolderPath);
FileUtils.copyDirectoryContents(unzippedFolderPath, newUpdateFolderPath);
FileUtils.deleteFileAtPathSilently(unzippedFolderPath);
// For zip updates, we need to find the relative path to the jsBundle and save it in the
// metadata so that we can find and run it easily the next time.
String relativeBundlePath = CodePushUpdateUtils.findJSBundleInUpdateContents(newPackageFolderPath);
String relativeBundlePath = CodePushUpdateUtils.findJSBundleInUpdateContents(newUpdateFolderPath);
if (relativeBundlePath == null) {
throw new CodePushInvalidUpdateException();
throw new CodePushInvalidUpdateException("Update is invalid - no files with extension .bundle, .js or .jsbundle were found in the update package.");
} else {
if (FileUtils.fileAtPathExists(newUpdateMetadataPath)) {
File metadataFileFromOldUpdate = new File(newUpdateMetadataPath);
metadataFileFromOldUpdate.delete();
}
if (isDiffUpdate) {
CodePushUpdateUtils.verifyHashForDiffUpdate(newUpdateFolderPath, newUpdateHash);
}
JSONObject updatePackageJSON = CodePushUtils.convertReadableToJsonObject(updatePackage);
try {
updatePackageJSON.put(RELATIVE_BUNDLE_PATH_KEY, relativeBundlePath);
@@ -257,12 +270,11 @@ public class CodePushPackage {
}
} else {
// File is a jsbundle, move it to a folder with the packageHash as its name
FileUtils.moveFile(downloadFile, newPackageFolderPath, UPDATE_BUNDLE_FILE_NAME);
FileUtils.moveFile(downloadFile, newUpdateFolderPath, UPDATE_BUNDLE_FILE_NAME);
}
// Save metadata to the folder.
String bundlePath = CodePushUtils.appendPathComponent(newPackageFolderPath, PACKAGE_FILE_NAME);
CodePushUtils.writeReadableMapToFile(updatePackage, bundlePath);
CodePushUtils.writeReadableMapToFile(updatePackage, newUpdateMetadataPath);
}
public void installPackage(ReadableMap updatePackage, boolean removePendingUpdate) throws IOException {

View File

@@ -1,13 +1,69 @@
package com.microsoft.codepush.react;
import android.util.Base64;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.WritableMap;
import org.json.JSONArray;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
public class CodePushUpdateUtils {
private static void addContentsOfFolderToManifest(String folderPath, String pathPrefix, ArrayList<String> manifest) {
File folder = new File(folderPath);
File[] folderFiles = folder.listFiles();
for (File file : folderFiles) {
String fullFilePath = file.getAbsolutePath();
String relativePath = (pathPrefix.isEmpty() ? "" : (pathPrefix + "/")) + file.getName();
if (file.isDirectory()) {
addContentsOfFolderToManifest(fullFilePath, relativePath, manifest);
} else {
try {
manifest.add(relativePath + ":" + computeHash(new FileInputStream(file)));
} catch (FileNotFoundException e) {
// Should not happen.
throw new CodePushUnknownException("Unable to compute hash of update contents.", e);
}
}
}
}
private static String computeHash(InputStream dataStream) {
MessageDigest messageDigest = null;
DigestInputStream digestInputStream = null;
try {
messageDigest = MessageDigest.getInstance("SHA-256");
digestInputStream = new DigestInputStream(dataStream, messageDigest);
byte[] byteBuffer = new byte[1024 * 8];
while (digestInputStream.read(byteBuffer) != -1);
} catch (NoSuchAlgorithmException | IOException e) {
// Should not happen.
throw new CodePushUnknownException("Unable to compute hash of update contents.", e);
} finally {
try {
if (digestInputStream != null) digestInputStream.close();
if (dataStream != null) dataStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
byte[] hash = messageDigest.digest();
return Base64.encodeToString(hash, 0, hash.length, 0);
}
public static void copyNecessaryFilesFromCurrentPackage(String diffManifestFilePath, String currentPackageFolderPath, String newPackageFolderPath) throws IOException{
FileUtils.copyDirectoryContents(currentPackageFolderPath, newPackageFolderPath);
WritableMap diffManifest = CodePushUtils.getWritableMapFromFile(diffManifestFilePath);
@@ -15,7 +71,9 @@ public class CodePushUpdateUtils {
for (int i = 0; i < deletedFiles.size(); i++) {
String fileNameToDelete = deletedFiles.getString(i);
File fileToDelete = new File(newPackageFolderPath, fileNameToDelete);
FileUtils.deleteFileOrFolderSilently(fileToDelete);
if (fileToDelete.exists()) {
fileToDelete.delete();
}
}
}
@@ -43,4 +101,21 @@ public class CodePushUpdateUtils {
return null;
}
public static void verifyHashForDiffUpdate(String folderPath, String expectedHash) {
ArrayList<String> updateContentsManifest = new ArrayList<String>();
addContentsOfFolderToManifest(folderPath, "", updateContentsManifest);
Collections.sort(updateContentsManifest);
JSONArray updateContentsJSONArray = new JSONArray();
for (String manifestEntry : updateContentsManifest) {
updateContentsJSONArray.put(manifestEntry);
}
// The JSON serialization turns path separators into "\/", e.g. "CodePush\/assets\/image.png"
String updateContentsManifestString = updateContentsJSONArray.toString().replace("\\/", "/");
String updateContentsManifestHash = computeHash(new ByteArrayInputStream(updateContentsManifestString.getBytes()));
if (!expectedHash.equals(updateContentsManifestHash)) {
throw new CodePushInvalidUpdateException("The update contents failed the data integrity check.");
}
}
}

View File

@@ -9,6 +9,7 @@
/* Begin PBXBuildFile section */
13BE3DEE1AC21097009241FE /* CodePush.m in Sources */ = {isa = PBXBuildFile; fileRef = 13BE3DED1AC21097009241FE /* CodePush.m */; };
1B23B9141BF9267B000BB2F0 /* RCTConvert+CodePushInstallMode.m in Sources */ = {isa = PBXBuildFile; fileRef = 1B23B9131BF9267B000BB2F0 /* RCTConvert+CodePushInstallMode.m */; };
540D20121C7684FE00D6EF41 /* CodePushUpdateUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 540D20111C7684FE00D6EF41 /* CodePushUpdateUtils.m */; };
5421FE311C58AD5A00986A55 /* CodePushTelemetryManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 5421FE301C58AD5A00986A55 /* CodePushTelemetryManager.m */; };
54A0026C1C0E2880004C3CEC /* aescrypt.c in Sources */ = {isa = PBXBuildFile; fileRef = 54A0024C1C0E2880004C3CEC /* aescrypt.c */; };
54A0026D1C0E2880004C3CEC /* aeskey.c in Sources */ = {isa = PBXBuildFile; fileRef = 54A0024D1C0E2880004C3CEC /* aeskey.c */; };
@@ -43,10 +44,11 @@
/* Begin PBXFileReference section */
134814201AA4EA6300B7C361 /* libCodePush.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libCodePush.a; sourceTree = BUILT_PRODUCTS_DIR; };
13BE3DEC1AC21097009241FE /* CodePush.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CodePush.h; sourceTree = "<group>"; };
13BE3DED1AC21097009241FE /* CodePush.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CodePush.m; sourceTree = "<group>"; };
1B23B9131BF9267B000BB2F0 /* RCTConvert+CodePushInstallMode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "RCTConvert+CodePushInstallMode.m"; sourceTree = "<group>"; };
5421FE301C58AD5A00986A55 /* CodePushTelemetryManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CodePushTelemetryManager.m; sourceTree = "<group>"; };
13BE3DEC1AC21097009241FE /* CodePush.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CodePush.h; path = CodePush/CodePush.h; sourceTree = "<group>"; };
13BE3DED1AC21097009241FE /* CodePush.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = CodePush.m; path = CodePush/CodePush.m; sourceTree = "<group>"; };
1B23B9131BF9267B000BB2F0 /* RCTConvert+CodePushInstallMode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "RCTConvert+CodePushInstallMode.m"; path = "CodePush/RCTConvert+CodePushInstallMode.m"; sourceTree = "<group>"; };
540D20111C7684FE00D6EF41 /* CodePushUpdateUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = CodePushUpdateUtils.m; path = CodePush/CodePushUpdateUtils.m; sourceTree = "<group>"; };
5421FE301C58AD5A00986A55 /* CodePushTelemetryManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = CodePushTelemetryManager.m; path = CodePush/CodePushTelemetryManager.m; sourceTree = "<group>"; };
54A0024A1C0E2880004C3CEC /* aes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = aes.h; sourceTree = "<group>"; };
54A0024B1C0E2880004C3CEC /* aes_via_ace.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = aes_via_ace.h; sourceTree = "<group>"; };
54A0024C1C0E2880004C3CEC /* aescrypt.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = aescrypt.c; sourceTree = "<group>"; };
@@ -80,9 +82,9 @@
54A002691C0E2880004C3CEC /* zip.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = zip.h; sourceTree = "<group>"; };
54A0026A1C0E2880004C3CEC /* SSZipArchive.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SSZipArchive.h; sourceTree = "<group>"; };
54A0026B1C0E2880004C3CEC /* SSZipArchive.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SSZipArchive.m; sourceTree = "<group>"; };
54FFEDDF1BF550630061DD23 /* CodePushDownloadHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CodePushDownloadHandler.m; sourceTree = "<group>"; };
810D4E6C1B96935000B397E9 /* CodePushPackage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CodePushPackage.m; sourceTree = "<group>"; };
81D51F391B6181C2000DA084 /* CodePushConfig.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CodePushConfig.m; sourceTree = "<group>"; };
54FFEDDF1BF550630061DD23 /* CodePushDownloadHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = CodePushDownloadHandler.m; path = CodePush/CodePushDownloadHandler.m; sourceTree = "<group>"; };
810D4E6C1B96935000B397E9 /* CodePushPackage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = CodePushPackage.m; path = CodePush/CodePushPackage.m; sourceTree = "<group>"; };
81D51F391B6181C2000DA084 /* CodePushConfig.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = CodePushConfig.m; path = CodePush/CodePushConfig.m; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -113,7 +115,8 @@
54A0026A1C0E2880004C3CEC /* SSZipArchive.h */,
54A0026B1C0E2880004C3CEC /* SSZipArchive.m */,
);
path = SSZipArchive;
name = SSZipArchive;
path = CodePush/SSZipArchive;
sourceTree = "<group>";
};
54A002491C0E2880004C3CEC /* aes */ = {
@@ -169,6 +172,7 @@
810D4E6C1B96935000B397E9 /* CodePushPackage.m */,
5421FE301C58AD5A00986A55 /* CodePushTelemetryManager.m */,
81D51F391B6181C2000DA084 /* CodePushConfig.m */,
540D20111C7684FE00D6EF41 /* CodePushUpdateUtils.m */,
13BE3DEC1AC21097009241FE /* CodePush.h */,
13BE3DED1AC21097009241FE /* CodePush.m */,
134814211AA4EA7D00B7C361 /* Products */,
@@ -232,6 +236,7 @@
buildActionMask = 2147483647;
files = (
54A0026D1C0E2880004C3CEC /* aeskey.c in Sources */,
540D20121C7684FE00D6EF41 /* CodePushUpdateUtils.m in Sources */,
54A0026E1C0E2880004C3CEC /* aestab.c in Sources */,
54A002761C0E2880004C3CEC /* mztools.c in Sources */,
54A002781C0E2880004C3CEC /* zip.c in Sources */,
@@ -337,9 +342,8 @@
HEADER_SEARCH_PATHS = (
"$(inherited)",
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
"$(SRCROOT)/node_modules/react-native/React/**",
"$(SRCROOT)/Examples/CodePushDemoApp/node_modules/react-native/React/**",
"$(SRCROOT)/../react-native/React/**",
"$(SRCROOT)/../node_modules/react-native/React/**",
"$(SRCROOT)/../Examples/CodePushDemoApp/node_modules/react-native/React/**",
);
LIBRARY_SEARCH_PATHS = "$(inherited)";
OTHER_LDFLAGS = (
@@ -357,9 +361,8 @@
HEADER_SEARCH_PATHS = (
"$(inherited)",
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
"$(SRCROOT)/node_modules/react-native/React/**",
"$(SRCROOT)/Examples/CodePushDemoApp/node_modules/react-native/React/**",
"$(SRCROOT)/../react-native/React/**",
"$(SRCROOT)/../node_modules/react-native/React/**",
"$(SRCROOT)/../Examples/CodePushDemoApp/node_modules/react-native/React/**",
);
LIBRARY_SEARCH_PATHS = "$(inherited)";
OTHER_LDFLAGS = (

View File

@@ -104,6 +104,19 @@ failCallback:(void (^)(NSError *err))failCallback;
@end
@interface CodePushUpdateUtils : NSObject
+ (void)copyEntriesInFolder:(NSString *)sourceFolder
destFolder:(NSString *)destFolder
error:(NSError **)error;
+ (NSString *)findMainBundleInFolder:(NSString *)folderPath
error:(NSError **)error;
+ (BOOL)verifyHashForDiffUpdate:(NSString *)finalUpdateFolder
expectedHash:(NSString *)expectedHash
error:(NSError **)error;
@end
typedef NS_ENUM(NSInteger, CodePushInstallMode) {
CodePushInstallModeImmediate,
CodePushInstallModeOnNextRestart,

View File

@@ -203,13 +203,15 @@ NSString * const UnzippedFolderName = @"unzipped";
doneCallback:(void (^)())doneCallback
failCallback:(void (^)(NSError *err))failCallback
{
NSString *newPackageFolderPath = [self getPackageFolderPath:updatePackage[@"packageHash"]];
NSString *newUpdateHash = updatePackage[@"packageHash"];
NSString *newUpdateFolderPath = [self getPackageFolderPath:newUpdateHash];
NSString *newUpdateMetadataPath = [newUpdateFolderPath stringByAppendingPathComponent:@"app.json"];
NSError *error;
if ([[NSFileManager defaultManager] fileExistsAtPath:newPackageFolderPath]) {
if ([[NSFileManager defaultManager] fileExistsAtPath:newUpdateFolderPath]) {
// This removes any stale data in newPackageFolderPath that could have been left
// uncleared due to a crash or error during the download or install process.
[[NSFileManager defaultManager] removeItemAtPath:newPackageFolderPath
[[NSFileManager defaultManager] removeItemAtPath:newUpdateFolderPath
error:&error];
} else if (![[NSFileManager defaultManager] fileExistsAtPath:[self getCodePushPath]]) {
[[NSFileManager defaultManager] createDirectoryAtPath:[self getCodePushPath]
@@ -223,7 +225,7 @@ NSString * const UnzippedFolderName = @"unzipped";
}
NSString *downloadFilePath = [self getDownloadFilePath];
NSString *bundleFilePath = [newPackageFolderPath stringByAppendingPathComponent:UpdateBundleFileName];
NSString *bundleFilePath = [newUpdateFolderPath stringByAppendingPathComponent:UpdateBundleFileName];
CodePushDownloadHandler *downloadHandler = [[CodePushDownloadHandler alloc]
init:downloadFilePath
@@ -255,8 +257,9 @@ NSString * const UnzippedFolderName = @"unzipped";
}
NSString *diffManifestFilePath = [unzippedFolderPath stringByAppendingPathComponent:DiffManifestFileName];
BOOL isDiffUpdate = [[NSFileManager defaultManager] fileExistsAtPath:diffManifestFilePath];
if ([[NSFileManager defaultManager] fileExistsAtPath:diffManifestFilePath]) {
if (isDiffUpdate) {
// Copy the current package to the new package.
NSString *currentPackageFolderPath = [self getCurrentPackageFolderPath:&error];
if (error) {
@@ -265,7 +268,7 @@ NSString * const UnzippedFolderName = @"unzipped";
}
[[NSFileManager defaultManager] copyItemAtPath:currentPackageFolderPath
toPath:newPackageFolderPath
toPath:newUpdateFolderPath
error:&error];
if (error) {
failCallback(error);
@@ -287,19 +290,28 @@ NSString * const UnzippedFolderName = @"unzipped";
error:&error];
NSArray *deletedFiles = manifestJSON[@"deletedFiles"];
for (NSString *deletedFileName in deletedFiles) {
[[NSFileManager defaultManager] removeItemAtPath:[newPackageFolderPath stringByAppendingPathComponent:deletedFileName]
error:&nonFailingError];
if (nonFailingError) {
NSLog(@"Error deleting file from current package: %@", nonFailingError);
nonFailingError = nil;
NSString *absoluteDeletedFilePath = [newUpdateFolderPath stringByAppendingPathComponent:deletedFileName];
if ([[NSFileManager defaultManager] fileExistsAtPath:absoluteDeletedFilePath]) {
[[NSFileManager defaultManager] removeItemAtPath:absoluteDeletedFilePath
error:&error];
if (error) {
failCallback(error);
return;
}
}
}
[[NSFileManager defaultManager] removeItemAtPath:diffManifestFilePath
error:&error];
if (error) {
failCallback(error);
return;
}
}
[CodePushPackage copyEntriesInFolder:unzippedFolderPath
destFolder:newPackageFolderPath
error:&error];
[CodePushUpdateUtils copyEntriesInFolder:unzippedFolderPath
destFolder:newUpdateFolderPath
error:&error];
if (error) {
failCallback(error);
return;
@@ -312,15 +324,15 @@ NSString * const UnzippedFolderName = @"unzipped";
nonFailingError = nil;
}
NSString *relativeBundlePath = [self findMainBundleInFolder:newPackageFolderPath
error:&error];
NSString *relativeBundlePath = [CodePushUpdateUtils findMainBundleInFolder:newUpdateFolderPath
error:&error];
if (error) {
failCallback(error);
return;
}
if (relativeBundlePath) {
NSString *absoluteBundlePath = [newPackageFolderPath stringByAppendingPathComponent:relativeBundlePath];
NSString *absoluteBundlePath = [newUpdateFolderPath stringByAppendingPathComponent:relativeBundlePath];
NSDictionary *bundleFileAttributes = [[[NSFileManager defaultManager] attributesOfItemAtPath:absoluteBundlePath error:&error] mutableCopy];
if (error) {
failCallback(error);
@@ -347,8 +359,35 @@ NSString * const UnzippedFolderName = @"unzipped";
failCallback(error);
return;
}
if ([[NSFileManager defaultManager] fileExistsAtPath:newUpdateMetadataPath]) {
[[NSFileManager defaultManager] removeItemAtPath:newUpdateMetadataPath
error:&error];
if (error) {
failCallback(error);
return;
}
}
if (isDiffUpdate && ![CodePushUpdateUtils verifyHashForDiffUpdate:newUpdateFolderPath
expectedHash:newUpdateHash
error:&error]) {
if (error) {
failCallback(error);
return;
}
error = [[NSError alloc] initWithDomain:CodePushErrorDomain
code:CodePushErrorCode
userInfo:@{
NSLocalizedDescriptionKey:
NSLocalizedString(@"The update contents failed the data integrity check.", nil)
}];
failCallback(error);
return;
}
} else {
[[NSFileManager defaultManager] createDirectoryAtPath:newPackageFolderPath
[[NSFileManager defaultManager] createDirectoryAtPath:newUpdateFolderPath
withIntermediateDirectories:YES
attributes:nil
error:&error];
@@ -367,7 +406,7 @@ NSString * const UnzippedFolderName = @"unzipped";
NSString *packageJsonString = [[NSString alloc] initWithData:updateSerializedData
encoding:NSUTF8StringEncoding];
[packageJsonString writeToFile:[newPackageFolderPath stringByAppendingPathComponent:@"app.json"]
[packageJsonString writeToFile:newUpdateMetadataPath
atomically:YES
encoding:NSUTF8StringEncoding
error:&error];
@@ -383,87 +422,6 @@ NSString * const UnzippedFolderName = @"unzipped";
[downloadHandler download:updatePackage[@"downloadUrl"]];
}
+ (NSString *)findMainBundleInFolder:(NSString *)folderPath
error:(NSError **)error
{
NSArray* folderFiles = [[NSFileManager defaultManager]
contentsOfDirectoryAtPath:folderPath
error:error];
if (*error) {
return nil;
}
for (NSString *fileName in folderFiles) {
NSString *fullFilePath = [folderPath stringByAppendingPathComponent:fileName];
BOOL isDir = NO;
if ([[NSFileManager defaultManager] fileExistsAtPath:fullFilePath
isDirectory:&isDir] && isDir) {
NSString *mainBundlePathInFolder = [self findMainBundleInFolder:fullFilePath error:error];
if (*error) {
return nil;
}
if (mainBundlePathInFolder) {
return [fileName stringByAppendingPathComponent:mainBundlePathInFolder];
}
} else if ([[fileName pathExtension] isEqualToString:@"bundle"] ||
[[fileName pathExtension] isEqualToString:@"jsbundle"] ||
[[fileName pathExtension] isEqualToString:@"js"]) {
return fileName;
}
}
return nil;
}
+ (void)copyEntriesInFolder:(NSString *)sourceFolder
destFolder:(NSString *)destFolder
error:(NSError **)error
{
NSArray* files = [[NSFileManager defaultManager]
contentsOfDirectoryAtPath:sourceFolder
error:error];
if (*error) {
return;
}
for (NSString *fileName in files) {
NSString * fullFilePath = [sourceFolder stringByAppendingPathComponent:fileName];
BOOL isDir = NO;
if ([[NSFileManager defaultManager] fileExistsAtPath:fullFilePath
isDirectory:&isDir] && isDir) {
NSString *nestedDestFolder = [destFolder stringByAppendingPathComponent:fileName];
[self copyEntriesInFolder:fullFilePath
destFolder:nestedDestFolder
error:error];
} else {
NSString *destFileName = [destFolder stringByAppendingPathComponent:fileName];
if ([[NSFileManager defaultManager] fileExistsAtPath:destFileName]) {
[[NSFileManager defaultManager] removeItemAtPath:destFileName error:error];
if (*error) {
return;
}
}
if (![[NSFileManager defaultManager] fileExistsAtPath:destFolder]) {
[[NSFileManager defaultManager] createDirectoryAtPath:destFolder
withIntermediateDirectories:YES
attributes:nil
error:error];
if (*error) {
return;
}
}
[[NSFileManager defaultManager] copyItemAtPath:fullFilePath toPath:destFileName error:error];
if (*error) {
return;
}
}
}
}
+ (void)installPackage:(NSDictionary *)updatePackage
removePendingUpdate:(BOOL)removePendingUpdate
error:(NSError **)error

View File

@@ -0,0 +1,156 @@
#import "CodePush.h"
#include <CommonCrypto/CommonDigest.h>
@implementation CodePushUpdateUtils
+ (void)addContentsOfFolderToManifest:(NSString *)folderPath
pathPrefix:(NSString *)pathPrefix
manifest:(NSMutableArray *)manifest
error:(NSError **)error
{
NSArray* folderFiles = [[NSFileManager defaultManager]
contentsOfDirectoryAtPath:folderPath
error:error];
if (*error) {
return;
}
for (NSString *fileName in folderFiles) {
NSString *fullFilePath = [folderPath stringByAppendingPathComponent:fileName];
NSString *relativePath = [pathPrefix stringByAppendingPathComponent:fileName];
BOOL isDir = NO;
if ([[NSFileManager defaultManager] fileExistsAtPath:fullFilePath
isDirectory:&isDir] && isDir) {
[self addContentsOfFolderToManifest:fullFilePath
pathPrefix:relativePath
manifest:manifest
error:error];
if (*error) {
return;
}
} else {
NSData *fileContents = [NSData dataWithContentsOfFile:fullFilePath];
NSString *fileContentsHash = [self computeHash:fileContents];
[manifest addObject:[[relativePath stringByAppendingString:@":"] stringByAppendingString:fileContentsHash]];
}
}
}
+ (NSString *)computeHash:(NSData *)inputData
{
uint8_t digest[CC_SHA256_DIGEST_LENGTH];
CC_SHA256(inputData.bytes, inputData.length, digest);
NSMutableString* inputHash = [NSMutableString stringWithCapacity:CC_SHA256_DIGEST_LENGTH * 2];
for (int i = 0; i < CC_SHA256_DIGEST_LENGTH; i++) {
[inputHash appendFormat:@"%02x", digest[i]];
}
return inputHash;
}
+ (void)copyEntriesInFolder:(NSString *)sourceFolder
destFolder:(NSString *)destFolder
error:(NSError **)error
{
NSArray* files = [[NSFileManager defaultManager]
contentsOfDirectoryAtPath:sourceFolder
error:error];
if (*error) {
return;
}
for (NSString *fileName in files) {
NSString * fullFilePath = [sourceFolder stringByAppendingPathComponent:fileName];
BOOL isDir = NO;
if ([[NSFileManager defaultManager] fileExistsAtPath:fullFilePath
isDirectory:&isDir] && isDir) {
NSString *nestedDestFolder = [destFolder stringByAppendingPathComponent:fileName];
[self copyEntriesInFolder:fullFilePath
destFolder:nestedDestFolder
error:error];
} else {
NSString *destFileName = [destFolder stringByAppendingPathComponent:fileName];
if ([[NSFileManager defaultManager] fileExistsAtPath:destFileName]) {
[[NSFileManager defaultManager] removeItemAtPath:destFileName error:error];
if (*error) {
return;
}
}
if (![[NSFileManager defaultManager] fileExistsAtPath:destFolder]) {
[[NSFileManager defaultManager] createDirectoryAtPath:destFolder
withIntermediateDirectories:YES
attributes:nil
error:error];
if (*error) {
return;
}
}
[[NSFileManager defaultManager] copyItemAtPath:fullFilePath toPath:destFileName error:error];
if (*error) {
return;
}
}
}
}
+ (NSString *)findMainBundleInFolder:(NSString *)folderPath
error:(NSError **)error
{
NSArray* folderFiles = [[NSFileManager defaultManager]
contentsOfDirectoryAtPath:folderPath
error:error];
if (*error) {
return nil;
}
for (NSString *fileName in folderFiles) {
NSString *fullFilePath = [folderPath stringByAppendingPathComponent:fileName];
BOOL isDir = NO;
if ([[NSFileManager defaultManager] fileExistsAtPath:fullFilePath
isDirectory:&isDir] && isDir) {
NSString *mainBundlePathInFolder = [self findMainBundleInFolder:fullFilePath error:error];
if (*error) {
return nil;
}
if (mainBundlePathInFolder) {
return [fileName stringByAppendingPathComponent:mainBundlePathInFolder];
}
} else if ([[fileName pathExtension] isEqualToString:@"bundle"] ||
[[fileName pathExtension] isEqualToString:@"jsbundle"] ||
[[fileName pathExtension] isEqualToString:@"js"]) {
return fileName;
}
}
return nil;
}
+ (BOOL)verifyHashForDiffUpdate:(NSString *)finalUpdateFolder
expectedHash:(NSString *)expectedHash
error:(NSError **)error
{
NSMutableArray *updateContentsManifest = [NSMutableArray array];
[self addContentsOfFolderToManifest:finalUpdateFolder
pathPrefix:@""
manifest:updateContentsManifest
error:error];
if (*error) {
return NO;
}
NSArray *sortedUpdateContentsManifest = [updateContentsManifest sortedArrayUsingSelector:@selector(compare:)];
NSData *updateContentsManifestData = [NSJSONSerialization dataWithJSONObject:sortedUpdateContentsManifest
options:kNilOptions
error:error];
NSString *updateContentsManifestString = [[NSString alloc] initWithData:updateContentsManifestData
encoding:NSUTF8StringEncoding];
// The JSON serialization turns path separators into "\/", e.g. "CodePush\/assets\/image.png"
updateContentsManifestString = [updateContentsManifestString stringByReplacingOccurrencesOfString:@"\\/"
withString:@"/"];
NSString *updateContentsManifestHash = [self computeHash:[NSData dataWithBytes:updateContentsManifestString.UTF8String length:updateContentsManifestString.length]];
return [updateContentsManifestHash isEqualToString:expectedHash];
}
@end