Created
October 17, 2012 09:55
-
-
Save pita5/3904757 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// RNCachingURL.m | |
// | |
// Created by Robert Napier on 1/10/12. | |
// Copyright (c) 2012 Rob Napier. | |
// | |
// This code is licensed under the MIT License: | |
// | |
// Permission is hereby granted, free of charge, to any person obtaining a | |
// copy of this software and associated documentation files (the "Software"), | |
// to deal in the Software without restriction, including without limitation | |
// the rights to use, copy, modify, merge, publish, distribute, sublicense, | |
// and/or sell copies of the Software, and to permit persons to whom the | |
// Software is furnished to do so, subject to the following conditions: | |
// | |
// The above copyright notice and this permission notice shall be included in | |
// all copies or substantial portions of the Software. | |
// | |
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE | |
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER | |
// DEALINGS IN THE SOFTWARE. | |
// | |
#import "RNCachingURLProtocol.h" | |
#import "CSMConfigSettings.h" | |
#import "CSMNetworkManager.h" | |
#import <CommonCrypto/CommonDigest.h> | |
@interface NSString (Hash) | |
- (NSString *)md5; | |
@end | |
@implementation NSString (Hash) | |
- (NSString *)md5 | |
{ | |
NSData *data = [self dataUsingEncoding:NSUTF8StringEncoding]; | |
uint8_t digest[CC_MD5_DIGEST_LENGTH]; | |
CC_MD5(data.bytes, data.length, digest); | |
NSMutableString *output = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2]; | |
for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++) | |
{ | |
[output appendFormat:@"%02x", digest[i]]; | |
} | |
return output; | |
} | |
@end | |
@interface RNCachedData : NSObject <NSCoding> | |
@property (nonatomic, readwrite, strong) NSData* data; | |
@property (nonatomic, readwrite, strong) NSURLResponse* response; | |
@property (nonatomic, readwrite, strong) NSURLRequest* redirectRequest; | |
@end | |
static NSString *RNCachingURLHeader = @"X-RNCache"; | |
@interface RNCachingURLProtocol () | |
@property (nonatomic, readwrite, strong) RNCachedData* cachedResponseData; | |
@property (nonatomic, readwrite, strong) NSURLConnection* connection; | |
@property (nonatomic, readwrite, strong) NSMutableData* data; | |
@property (nonatomic, readwrite, strong) NSURLResponse* response; | |
- (void)appendData:(NSData *)newData; | |
@end | |
@implementation RNCachingURLProtocol | |
@synthesize connection = connection_; | |
@synthesize data = data_; | |
@synthesize response = response_; | |
static NSString* cachesPath = nil; | |
+ (void)initialize | |
{ | |
cachesPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; | |
} | |
+ (BOOL)canInitWithRequest:(NSURLRequest *)request | |
{ | |
if ([[request HTTPMethod] isEqualToString:@"POST"]) | |
{ | |
NSLog(@"Trying to do a post %@",request); | |
} | |
// This protocol handles HTTP GET request caching. | |
if (([[[request URL] scheme] isEqualToString:@"http"] && [[request HTTPMethod] isEqualToString:@"GET"]) && | |
([request valueForHTTPHeaderField:RNCachingURLHeader] == nil)) | |
{ | |
return YES; | |
} | |
else | |
{ | |
return NO; | |
} | |
} | |
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request | |
{ | |
return request; | |
} | |
- (NSString *)cachePathForRequest:(NSURLRequest *)aRequest | |
{ | |
// This stores in the Caches directory, which can be deleted when space is low, but we only use it for offline access | |
NSString* path = [cachesPath stringByAppendingPathComponent:[[[aRequest URL] absoluteString] md5]]; | |
// Add an extension to the file hash | |
if (aRequest.URL.pathExtension) | |
{ | |
path = [path stringByAppendingPathExtension:aRequest.URL.pathExtension]; | |
} | |
return path; | |
} | |
- (void)_startLoadingConnection | |
{ | |
NSMutableURLRequest *connectionRequest = [[self request] mutableCopy]; | |
// we need to mark this request with our header so we know not to handle it in +[NSURLProtocol canInitWithRequest:]. | |
[connectionRequest setValue:@"" | |
forHTTPHeaderField:RNCachingURLHeader]; | |
// Start the connection | |
NSURLConnection *connection = [NSURLConnection connectionWithRequest:connectionRequest | |
delegate:self]; | |
[self setConnection:connection]; | |
} | |
- (BOOL)_doesLoacalCacheNeedUpdate | |
{ | |
// If we don't have some cached data, we need an update! | |
if (!_cachedResponseData) return YES; | |
BOOL needsUpdate = NO; | |
// Get the cached response headers | |
NSHTTPURLResponse* cacheResponse = (NSHTTPURLResponse *)[_cachedResponseData response]; | |
NSDictionary* cacheHeaders = [cacheResponse allHeaderFields]; | |
// Get the latest response headers | |
NSHTTPURLResponse* freshResponse = (NSHTTPURLResponse *)[self response]; | |
NSDictionary* freshHeaders = [freshResponse allHeaderFields]; | |
// Compare the two | |
if ([freshHeaders objectForKey:@"ETag"] && | |
[cacheHeaders objectForKey:@"ETag"] && | |
![[freshHeaders objectForKey:@"ETag"] isEqualToString:[cacheHeaders objectForKey:@"ETag"]]) | |
{ | |
needsUpdate = YES; | |
} | |
else if ([freshHeaders objectForKey:@"Etag"] && | |
[cacheHeaders objectForKey:@"Etag"] && | |
![[freshHeaders objectForKey:@"Etag"] isEqualToString:[cacheHeaders objectForKey:@"Etag"]]) | |
{ | |
needsUpdate = YES; | |
} | |
else if ([freshHeaders objectForKey:@"Last-Modified"] && | |
[cacheHeaders objectForKey:@"Last-Modified"] && | |
![[freshHeaders objectForKey:@"Last-Modified"] isEqualToString:[cacheHeaders objectForKey:@"Last-Modified"]]) | |
{ | |
needsUpdate = YES; | |
} | |
else if ([freshHeaders objectForKey:@"Content-Length"] && | |
[cacheHeaders objectForKey:@"Content-Length"] && | |
![[freshHeaders objectForKey:@"Content-Length"] isEqualToString:[cacheHeaders objectForKey:@"Content-Length"]]) | |
{ | |
needsUpdate = YES; | |
} | |
return needsUpdate; | |
} | |
- (void)_checkCacheOrFail:(RNCachedData *)cache | |
{ | |
if (cache) | |
{ | |
NSURLResponse* response = [cache response]; | |
NSURLRequest* redirectRequest = [cache redirectRequest]; | |
if (redirectRequest) | |
{ | |
[[self client] URLProtocol:self | |
wasRedirectedToRequest:redirectRequest | |
redirectResponse:response]; | |
} | |
else | |
{ | |
NSData* data = [cache data]; | |
[[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; | |
[[self client] URLProtocol:self didLoadData:data]; | |
[[self client] URLProtocolDidFinishLoading:self]; | |
} | |
} | |
else | |
{ | |
[[self client] URLProtocol:self | |
didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorCannotConnectToHost userInfo:nil]]; | |
} | |
} | |
- (void)unarchive:(RNCachedData **)cached | |
{ | |
// catch any exceptions unarchiving. | |
@try | |
{ | |
*cached = [NSKeyedUnarchiver unarchiveObjectWithFile:[self cachePathForRequest:[self request]]]; | |
} | |
@catch (NSException *exception) | |
{ | |
*cached = nil; | |
} | |
@finally | |
{ | |
} | |
} | |
- (void)startLoading | |
{ | |
// Try to unarchive a cached data | |
RNCachedData* cacheData = nil; | |
[self unarchive:&cacheData]; | |
// Save the cache data for header check | |
self.cachedResponseData = cacheData; | |
// Check if the app is 'online' (passes security check, i.e, app is connected to valid WiFi SSID) | |
// Will default to allowing request if no configuration is available. | |
if (([NETWORK_MANAGER isOnline] || [CSMConfigSettings configAvailable] == NO)) | |
{ | |
// Start loading connection | |
[self _startLoadingConnection]; | |
} | |
else | |
{ | |
// Try to respond with the cached data instead | |
[self _checkCacheOrFail:cacheData]; | |
} | |
} | |
- (void)stopLoading | |
{ | |
[[self connection] cancel]; | |
} | |
- (NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)response | |
{ | |
if (response != nil) | |
{ | |
NSMutableURLRequest* redirectableRequest = [request mutableCopy]; | |
[redirectableRequest setValue:nil | |
forHTTPHeaderField:RNCachingURLHeader]; | |
NSString* cachePath = [self cachePathForRequest:[self request]]; | |
RNCachedData* cache = [RNCachedData new]; | |
[cache setResponse:response]; | |
[cache setData:[self data]]; | |
[cache setRedirectRequest:redirectableRequest]; | |
@try | |
{ | |
[NSKeyedArchiver archiveRootObject:cache toFile:cachePath]; | |
} | |
@catch (NSException *exception) | |
{ | |
} | |
@finally | |
{ | |
} | |
[[self client] URLProtocol:self | |
wasRedirectedToRequest:redirectableRequest | |
redirectResponse:response]; | |
return redirectableRequest; | |
} | |
else | |
{ | |
return request; | |
} | |
} | |
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data | |
{ | |
[[self client] URLProtocol:self didLoadData:data]; | |
[self appendData:data]; | |
} | |
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error | |
{ | |
if (_cachedResponseData) | |
{ | |
NSLog(@"!!! connection failed loading, recovering with cached data"); | |
[self _checkCacheOrFail:_cachedResponseData]; | |
} | |
else | |
{ | |
NSLog(@"connection failed loading, no cache recovery"); | |
[[self client] URLProtocol:self didFailWithError:error]; | |
[self setConnection:nil]; | |
[self setData:nil]; | |
[self setResponse:nil]; | |
} | |
} | |
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response | |
{ | |
[self setResponse:response]; | |
if ([self _doesLoacalCacheNeedUpdate]) | |
{ | |
NSLog(@"Local cache needed update"); | |
// Add a header to the response so we can see where the cache data is on disk | |
NSMutableDictionary* headerFields = [[(NSHTTPURLResponse *)[self response] allHeaderFields] mutableCopy]; | |
[headerFields setValue:[self cachePathForRequest:[self request]] | |
forKey:@"csm-cache-path"]; | |
NSHTTPURLResponse* responseToCache = [[NSHTTPURLResponse alloc] initWithURL:[(NSHTTPURLResponse *)[self response] URL] | |
statusCode:[(NSHTTPURLResponse *)[self response] statusCode] | |
HTTPVersion:@"HTTP/1.1" | |
headerFields:headerFields]; | |
[self setResponse:responseToCache]; | |
// Continue with the connection | |
[[self client] URLProtocol:self | |
didReceiveResponse:[self response] | |
cacheStoragePolicy:NSURLCacheStorageNotAllowed]; | |
} | |
else | |
{ | |
NSLog(@"Cache was hit!"); | |
// Respond with the cache response headers | |
[[self client] URLProtocol:self | |
didReceiveResponse:[[self cachedResponseData] response] | |
cacheStoragePolicy:NSURLCacheStorageNotAllowed]; | |
// Cancel the URL connection | |
[self stopLoading]; | |
// Handle the response with the cache data | |
[self _checkCacheOrFail:self.cachedResponseData]; | |
// Cleanup | |
[self setConnection:nil]; | |
[self setData:nil]; | |
[self setResponse:nil]; | |
} | |
} | |
- (void)connectionDidFinishLoading:(NSURLConnection *)connection | |
{ | |
NSString *cachePath = [self cachePathForRequest:[self request]]; | |
RNCachedData *cache = [RNCachedData new]; | |
[cache setResponse:[self response]]; | |
[cache setData:[self data]]; | |
@try | |
{ | |
// Is NSKeyedArchiver thread safe? NSURLProtocol makes allot of threads, could be unsafe? | |
[NSKeyedArchiver archiveRootObject:cache | |
toFile:cachePath]; | |
} | |
@catch (NSException *exception) | |
{ | |
} | |
@finally | |
{ | |
} | |
[[self client] URLProtocolDidFinishLoading:self]; | |
[self setConnection:nil]; | |
[self setData:nil]; | |
[self setResponse:nil]; | |
} | |
- (void)appendData:(NSData *)newData | |
{ | |
if ([self data] == nil) | |
{ | |
[self setData:[newData mutableCopy]]; | |
} | |
else | |
{ | |
[[self data] appendData:newData]; | |
} | |
} | |
@end | |
static NSString *const kDataKey = @"data"; | |
static NSString *const kResponseKey = @"response"; | |
static NSString *const kRedirectRequestKey = @"redirectRequest"; | |
@implementation RNCachedData | |
@synthesize data = data_; | |
@synthesize response = response_; | |
@synthesize redirectRequest = redirectRequest_; | |
- (void)encodeWithCoder:(NSCoder *)aCoder | |
{ | |
[aCoder encodeObject:[self data] forKey:kDataKey]; | |
[aCoder encodeObject:[self response] forKey:kResponseKey]; | |
[aCoder encodeObject:[self redirectRequest] forKey:kRedirectRequestKey]; | |
} | |
- (id)initWithCoder:(NSCoder *)aDecoder | |
{ | |
self = [super init]; | |
if (self != nil) | |
{ | |
[self setData:[aDecoder decodeObjectForKey:kDataKey]]; | |
[self setResponse:[aDecoder decodeObjectForKey:kResponseKey]]; | |
[self setRedirectRequest:[aDecoder decodeObjectForKey:kRedirectRequestKey]]; | |
} | |
return self; | |
} | |
@end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment