diff --git a/circle.yml b/circle.yml index d06b17c47..db7c8cebf 100644 --- a/circle.yml +++ b/circle.yml @@ -13,8 +13,9 @@ test: - npm test - mkdir ios.bundle - react-native bundle --minify --dev false --entry-file index.ios.js --platform ios --assets-dest ios.bundle --bundle-output ios.bundle/main.jsbundle - - npm run sign ios.bundle - - zip -r main.zip ios.bundle + - zip -r ios.bundle.zip ios.bundle + - npm run sign ios.bundle.zip + - zip main.zip ios.bundle.zip signature.txt deployment: production: diff --git a/images/test1.png b/images/test1.png new file mode 100644 index 000000000..2fae25190 Binary files /dev/null and b/images/test1.png differ diff --git a/ios/Chat/AppDelegate.m b/ios/Chat/AppDelegate.m index dc94261f2..8a1831eb6 100644 --- a/ios/Chat/AppDelegate.m +++ b/ios/Chat/AppDelegate.m @@ -11,42 +11,38 @@ #import "RCTRootView.h" #import "RemoteBundle.h" +#import "RCTAssert.h" @implementation AppDelegate +-(void)loadBundle:(NSDictionary *)launchOptions { + dispatch_async(dispatch_get_main_queue(), ^{ + self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:[RemoteBundle bundle] + moduleName:@"Chat" + initialProperties:nil + launchOptions:launchOptions]; + + UIViewController *rootViewController = [[UIViewController alloc] init]; + rootViewController.view = rootView; + self.window.rootViewController = rootViewController; + [self.window makeKeyAndVisible]; + }); +} - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - /** - * Loading JavaScript code - uncomment the one you want. - * - * OPTION 1 - * Load from development server. Start the server from the repository root: - * - * $ npm start - * - * To run on device, change `localhost` to the IP address of your computer - * (you can get this by typing `ifconfig` into the terminal and selecting the - * `inet` value under `en0:`) and make sure your computer and iOS device are - * on the same Wi-Fi network. - */ - RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:[RemoteBundle bundle] - moduleName:@"Chat" - initialProperties:nil - launchOptions:launchOptions]; - - self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; - UIViewController *rootViewController = [[UIViewController alloc] init]; - rootViewController.view = rootView; - self.window.rootViewController = rootViewController; - [self.window makeKeyAndVisible]; + [RemoteBundle checkUpdate]; + RCTSetFatalHandler(^(NSError *error) { + // remove loaded version! + if ([RemoteBundle removeCurrentVersion]){ + [self loadBundle:launchOptions]; + } + }); + [self loadBundle:launchOptions]; return YES; } -(void)applicationDidEnterBackground:(UIApplication *)application { - __block UIBackgroundTaskIdentifier taskId = [application beginBackgroundTaskWithExpirationHandler:^{ - taskId = UIBackgroundTaskInvalid; - }]; - [RemoteBundle bundle]; + [RemoteBundle checkUpdate]; } - @end diff --git a/ios/RemoteBundle/RemoteBundle.h b/ios/RemoteBundle/RemoteBundle.h index 8de1579a8..325fc711a 100644 --- a/ios/RemoteBundle/RemoteBundle.h +++ b/ios/RemoteBundle/RemoteBundle.h @@ -8,7 +8,9 @@ #import -@interface RemoteBundle : NSObject +@interface RemoteBundle : NSObject ++(BOOL)removeCurrentVersion; +(NSURL *)bundle; ++(void)checkUpdate; @end diff --git a/ios/RemoteBundle/RemoteBundle.m b/ios/RemoteBundle/RemoteBundle.m index c4be55826..3efde836d 100644 --- a/ios/RemoteBundle/RemoteBundle.m +++ b/ios/RemoteBundle/RemoteBundle.m @@ -11,66 +11,139 @@ #import #import "Shared.h" #import "AH_SSZipArchive.h" +#import "RCTBridge.h" +#import "RCTExceptionsManager.h" +#import "RCTRootView.h" +#import "RCTAssert.h" NSString * const ETag = @"ETag"; - @implementation RemoteBundle - - -+(NSURL *)bundle { -#if TARGET_IPHONE_SIMULATOR && TESTING - return [NSURL URLWithString:@"http://10.0.1.7:8081/index.ios.bundle?platform=ios&dev=true"]; -#else - NSURL *result = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; ++(void)checkUpdate { + // 1. check if there new file on S3 + NSError* error; NSDictionary *info = [[NSBundle mainBundle] infoDictionary]; NSString *version = info[@"CFBundleShortVersionString"]; NSString *filename = [NSString stringWithFormat:@"ios_%@.zip", version]; + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + NSString *documentsDirectory = [paths objectAtIndex:0]; + NSLog(@"Checking for update"); + NSString *filePath = [NSString stringWithFormat:@"%@/%@", documentsDirectory, filename]; + + NSString *newBundlePath = [documentsDirectory stringByAppendingPathComponent:@"new"]; + // create folder for bundle + if (![[NSFileManager defaultManager] fileExistsAtPath:newBundlePath]){ + if( ! [[NSFileManager defaultManager] createDirectoryAtPath:newBundlePath withIntermediateDirectories:NO attributes:nil error:&error]){ + NSLog(@"[%@] ERROR: attempting to write create new directory", [self class]); + return; + } + } NSString *cdn = [NSString stringWithFormat: @"https://rn-chat.s3.amazonaws.com/%@", filename]; NSURL *url = [NSURL URLWithString:cdn]; - NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); - NSString *documentsDirectory = [paths objectAtIndex:0]; - NSLog(@"Documents directory: %@", documentsDirectory); - NSString *filePath = [NSString stringWithFormat:@"%@/%@", documentsDirectory, filename]; - NSString *signaturePath = [NSString stringWithFormat:@"%@/ios.bundle/signature.txt", documentsDirectory]; - NSString *bundlePath = [NSString stringWithFormat:@"%@/ios.bundle/main.jsbundle", documentsDirectory]; - NSString* publicKeyPath = [[NSBundle mainBundle] pathForResource:@"public" ofType:@"pem"]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:1000]; NSString *eTag = [[NSUserDefaults standardUserDefaults] objectForKey:ETag]; - - if (eTag && [Verifier verifyContent:bundlePath publicKeyPath:publicKeyPath signaturePath:signaturePath]){ + if (eTag){ [request addValue:eTag forHTTPHeaderField:@"If-None-Match"]; - result = [[NSURL alloc] initFileURLWithPath:bundlePath]; } NSURLSessionConfiguration *defaultConfigObject = [NSURLSessionConfiguration ephemeralSessionConfiguration]; - defaultConfigObject.requestCachePolicy = NSURLRequestReloadIgnoringLocalAndRemoteCacheData; - NSURLSession *session = [NSURLSession sessionWithConfiguration:defaultConfigObject]; - [[session dataTaskWithRequest:request - completionHandler:^(NSData *data, - NSURLResponse *response, - NSError *error) { - NSInteger code = [((NSHTTPURLResponse *)response) statusCode]; - if (!error && code == 200){ - [data writeToFile:filePath atomically:YES]; - [AH_SSZipArchive unzipFileAtPath:filePath toDestination:documentsDirectory]; - NSDictionary *headers = [((NSHTTPURLResponse *)response) allHeaderFields]; - // save Etag - if (headers[ETag]){ - [[NSUserDefaults standardUserDefaults] setValue:headers[ETag] forKey:ETag]; - [[NSUserDefaults standardUserDefaults] synchronize]; - } - } - - }] resume]; + completionHandler:^(NSData *data, + NSURLResponse *response, + NSError *error) { + NSInteger code = [((NSHTTPURLResponse *)response) statusCode]; + if (!error && code == 200){ + // 2. load new bundle, unzip and verify content + [data writeToFile:filePath atomically:YES]; + [AH_SSZipArchive unzipFileAtPath:filePath toDestination:newBundlePath]; + NSDictionary *headers = [((NSHTTPURLResponse *)response) allHeaderFields]; + // save Etag + if (headers[ETag]){ + [[NSUserDefaults standardUserDefaults] setValue:headers[ETag] forKey:ETag]; + [[NSUserDefaults standardUserDefaults] synchronize]; + } + + // 3. verify signature + NSString* publicKeyPath = [[NSBundle mainBundle] pathForResource:@"public" ofType:@"pem"]; + NSString *signaturePath = [NSString stringWithFormat:@"%@/signature.txt", newBundlePath]; + NSString *bundlePath = [NSString stringWithFormat:@"%@/ios.bundle.zip", newBundlePath]; + + if (![Verifier verifyContent:bundlePath publicKeyPath:publicKeyPath signaturePath:signaturePath]){ + NSLog(@"Verification of signature FAILED!"); + return; + } + + // 4. unzip inner bundle + [AH_SSZipArchive unzipFileAtPath:bundlePath toDestination:newBundlePath]; + NSLog(@"Unzip to %@", newBundlePath); + } else { + NSLog(@"No new update"); + } + + }] resume]; + +} + ++(BOOL)removeCurrentVersion { + NSError *error; + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + NSString *documentsDirectory = [paths objectAtIndex:0]; + NSString *currentBundlePath = [documentsDirectory stringByAppendingPathComponent:@"current"]; + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSString *bundlePath = [NSString stringWithFormat:@"%@/ios.bundle/main.jsbundle", currentBundlePath]; + if ([fileManager fileExistsAtPath:bundlePath]){ + [fileManager removeItemAtPath:currentBundlePath error:&error]; + if (error){ + return NO; + } else { + return YES; + } + + } else { + return NO; + } +} + ++(NSURL *)bundle { +#if TARGET_IPHONE_SIMULATOR && TESTING + return [NSURL URLWithString:@"http://10.0.1.7:8081/index.ios.bundle?platform=ios&dev=true"]; +#else + NSError *error; + // 1. check if new update loaded + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + NSString *documentsDirectory = [paths objectAtIndex:0]; + NSString *newBundlePath = [documentsDirectory stringByAppendingPathComponent:@"new"]; + NSString *currentBundlePath = [documentsDirectory stringByAppendingPathComponent:@"current"]; + NSString *bundlePath = [NSString stringWithFormat:@"%@/ios.bundle/main.jsbundle", newBundlePath]; + NSString *currentBundle = [NSString stringWithFormat:@"%@/ios.bundle/main.jsbundle", currentBundlePath]; + NSFileManager *fileManager = [NSFileManager defaultManager]; + if ([fileManager fileExistsAtPath:bundlePath]){ + [fileManager removeItemAtPath:currentBundlePath error:&error]; + if (error){ + NSLog(@"Error: %@",[ error localizedDescription]); + } + // move it to current + NSLog(@"Found update, moving to current"); + [fileManager moveItemAtPath:newBundlePath toPath:currentBundlePath error:&error]; + if (error){ + NSLog(@"Error: %@",[ error localizedDescription]); + } + } + + if ([fileManager fileExistsAtPath:currentBundle]){ + NSLog(@"Use bundle from: %@", currentBundle); + NSURL *url= [NSURL fileURLWithPath:currentBundle]; + return url; + } else { + NSLog(@"Use built-in bundle"); + return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; + } - return result; #endif } diff --git a/keys/sign.js b/keys/sign.js index 5a3b414dc..218022ed5 100644 --- a/keys/sign.js +++ b/keys/sign.js @@ -2,13 +2,13 @@ var crypto = require('crypto'); var fs = require('fs'); var args = process.argv.slice(2); var pem = fs.readFileSync('keys/private.pem'); -var bundle = fs.readFileSync(args[0]+'/main.jsbundle').toString('utf8'); +var bundle = fs.readFileSync(args[0]); var key = pem.toString('ascii'); var sign = crypto.createSign('RSA-SHA256'); sign.update(bundle); var sig = sign.sign(key, 'base64'); -fs.writeFileSync(args[0]+'/signature.txt',sig); +fs.writeFileSync('signature.txt',sig); //var verifier = crypto.createVerify('sha256'); //verifier.update(bundle);