This repository has been archived by the owner on Nov 1, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 232
Add support for NSURLCache. #141
Open
kgoodier
wants to merge
5
commits into
develop
Choose a base branch
from
feature/nsurlcache
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 4 commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
11d02ec
Add support for NSURLCache.
kgoodier 2a2c2f8
Workaround for caching with iOS 8 and NSURLSession.
kgoodier 4b9f633
Fix caching on iOS 7
kgoodier a758509
Caching code cleanup per code review feedback.
kgoodier c3d7349
Better parsing of Cache-Control header.
kgoodier File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,3 +12,4 @@ build/ | |
xcuserdata/ | ||
contents.xcworkspacedata | ||
*.xccheckout | ||
*.gcda |
This file contains 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
This file contains 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
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,6 +13,59 @@ | |
|
||
#import "SPDYCacheStoragePolicy.h" | ||
|
||
typedef struct _HTTPTimeFormatInfo { | ||
const char *readFormat; | ||
const char *writeFormat; | ||
BOOL usesHasTimezoneInfo; | ||
} HTTPTimeFormatInfo; | ||
|
||
static HTTPTimeFormatInfo kTimeFormatInfos[] = | ||
{ | ||
{ "%a, %d %b %Y %H:%M:%S %Z", "%a, %d %b %Y %H:%M:%S GMT", YES }, // Sun, 06 Nov 1994 08:49:37 GMT ; RFC 822, updated by RFC 1123 | ||
{ "%A, %d-%b-%y %H:%M:%S %Z", "%A, %d-%b-%y %H:%M:%S GMT", YES }, // Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036 | ||
{ "%a %b %e %H:%M:%S %Y", "%a %b %e %H:%M:%S %Y", NO }, // Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format | ||
}; | ||
|
||
|
||
static NSDate *HTTPDateFromString(NSString *string) | ||
{ | ||
NSDate *date = nil; | ||
if (string) { | ||
struct tm parsedTime; | ||
const char *utf8String = [string UTF8String]; | ||
|
||
for (int format = 0; (size_t)format < (sizeof(kTimeFormatInfos) / sizeof(kTimeFormatInfos[0])); format++) { | ||
HTTPTimeFormatInfo info = kTimeFormatInfos[format]; | ||
if (info.readFormat != NULL && strptime(utf8String, info.readFormat, &parsedTime)) { | ||
NSTimeInterval ti = (info.usesHasTimezoneInfo ? mktime(&parsedTime) : timegm(&parsedTime)); | ||
date = [NSDate dateWithTimeIntervalSince1970:ti]; | ||
if (date) { | ||
break; | ||
} | ||
} | ||
} | ||
} | ||
|
||
return date; | ||
} | ||
|
||
NSDictionary *HTTPCacheControlParameters(NSString *cacheControl) | ||
{ | ||
if (cacheControl.length == 0) { | ||
return nil; | ||
} | ||
|
||
NSArray *components = [cacheControl componentsSeparatedByString:@","]; | ||
NSMutableDictionary *parameters = [NSMutableDictionary dictionaryWithCapacity:components.count]; | ||
for (NSString *component in components) { | ||
NSArray *pair = [component componentsSeparatedByString:@"="]; | ||
NSString *key = [pair[0] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; | ||
NSString *value = pair.count == 2 ? [pair[1] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] : @""; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not right parsing. by RFC definition,
and |
||
parameters[key] = value; | ||
} | ||
return parameters; | ||
} | ||
|
||
extern NSURLCacheStoragePolicy SPDYCacheStoragePolicy(NSURLRequest *request, NSHTTPURLResponse *response) | ||
{ | ||
bool cacheable; | ||
|
@@ -35,37 +88,51 @@ extern NSURLCacheStoragePolicy SPDYCacheStoragePolicy(NSURLRequest *request, NSH | |
break; | ||
} | ||
|
||
// Let's only cache GET requests | ||
if (cacheable) { | ||
if (![request.HTTPMethod isEqualToString:@"GET"]) { | ||
cacheable = NO; | ||
} | ||
} | ||
|
||
// If the response might be cacheable, look at the "Cache-Control" header in | ||
// the response. | ||
|
||
// IMPORTANT: We can't rely on -rangeOfString: returning valid results if the target | ||
// string is nil, so we have to explicitly test for nil in the following two cases. | ||
|
||
if (cacheable) { | ||
NSString *responseHeader; | ||
NSString *cacheResponseHeader; | ||
NSString *dateResponseHeader; | ||
|
||
for (NSString *key in [response.allHeaderFields allKeys]) { | ||
if ([key caseInsensitiveCompare:@"cache-control"] == NSOrderedSame) { | ||
responseHeader = [response.allHeaderFields[key] lowercaseString]; | ||
break; | ||
cacheResponseHeader = [response.allHeaderFields[key] lowercaseString]; | ||
} | ||
else if ([key caseInsensitiveCompare:@"date"] == NSOrderedSame) { | ||
dateResponseHeader = [response.allHeaderFields[key] lowercaseString]; | ||
} | ||
} | ||
|
||
if (responseHeader != nil && [responseHeader rangeOfString:@"no-store"].location != NSNotFound) { | ||
if (cacheResponseHeader != nil && [cacheResponseHeader rangeOfString:@"no-store"].location != NSNotFound) { | ||
cacheable = NO; | ||
} | ||
|
||
// Must have a Date header. Can't validate freshness otherwise. | ||
if (dateResponseHeader == nil) { | ||
cacheable = NO; | ||
} | ||
} | ||
|
||
// If we still think it might be cacheable, look at the "Cache-Control" header in | ||
// the request. | ||
// the request. Also rule out requests with Authorization in them. | ||
|
||
if (cacheable) { | ||
NSString *requestHeader; | ||
|
||
requestHeader = [[request valueForHTTPHeaderField:@"cache-control"] lowercaseString]; | ||
if (requestHeader != nil && | ||
[requestHeader rangeOfString:@"no-store"].location != NSNotFound && | ||
[requestHeader rangeOfString:@"no-cache"].location != NSNotFound) { | ||
if ((requestHeader != nil && [requestHeader rangeOfString:@"no-store"].location != NSNotFound) || | ||
[request valueForHTTPHeaderField:@"authorization"].length > 0) { | ||
cacheable = NO; | ||
} | ||
} | ||
|
@@ -83,3 +150,71 @@ extern NSURLCacheStoragePolicy SPDYCacheStoragePolicy(NSURLRequest *request, NSH | |
|
||
return result; | ||
} | ||
|
||
extern SPDYCachedResponseState SPDYCacheLoadingPolicy(NSURLRequest *request, NSCachedURLResponse *response) | ||
{ | ||
if (request == nil || response == nil) { | ||
return SPDYCachedResponseStateInvalid; | ||
} | ||
|
||
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response.response; | ||
NSString *responseCacheControl; | ||
NSDate *responseDate; | ||
|
||
// Cached response validation | ||
|
||
// Get header values | ||
for (NSString *key in [httpResponse.allHeaderFields allKeys]) { | ||
if ([key caseInsensitiveCompare:@"cache-control"] == NSOrderedSame) { | ||
responseCacheControl = [httpResponse.allHeaderFields[key] lowercaseString]; | ||
} | ||
else if ([key caseInsensitiveCompare:@"date"] == NSOrderedSame) { | ||
NSString *dateString = httpResponse.allHeaderFields[key]; | ||
responseDate = HTTPDateFromString(dateString); | ||
} | ||
} | ||
|
||
if (responseCacheControl == nil || responseDate == nil) { | ||
return SPDYCachedResponseStateMustRevalidate; | ||
} | ||
|
||
if ([responseCacheControl rangeOfString:@"no-cache"].location != NSNotFound || | ||
[responseCacheControl rangeOfString:@"must-revalidate"].location != NSNotFound || | ||
[responseCacheControl rangeOfString:@"max-age=0"].location != NSNotFound) { | ||
return SPDYCachedResponseStateMustRevalidate; | ||
} | ||
|
||
// Verify item has not expired | ||
NSDictionary *cacheControlParams = HTTPCacheControlParameters(responseCacheControl); | ||
if (cacheControlParams[@"max-age"] != nil) { | ||
NSTimeInterval ageOfResponse = [[NSDate date] timeIntervalSinceDate:responseDate]; | ||
NSTimeInterval maxAge = [cacheControlParams[@"max-age"] doubleValue]; | ||
if (ageOfResponse > maxAge) { | ||
return SPDYCachedResponseStateMustRevalidate; | ||
} | ||
} else { | ||
// If no max-age, you have to revalidate | ||
return SPDYCachedResponseStateMustRevalidate; | ||
} | ||
|
||
// Request validation | ||
|
||
NSString *requestCacheControl = [[request valueForHTTPHeaderField:@"cache-control"] lowercaseString]; | ||
|
||
if (requestCacheControl != nil) { | ||
if ([requestCacheControl rangeOfString:@"no-cache"].location != NSNotFound) { | ||
return SPDYCachedResponseStateMustRevalidate; | ||
} | ||
} | ||
|
||
// Note: there's a lot more validation we should do, to be a well-behaving user agent. | ||
// We don't support Pragma header. | ||
// We don't support Expires header. | ||
// We don't support Vary header. | ||
// We don't support ETag response header or If-None-Match request header. | ||
// We don't support Last-Modified response header or If-Modified-Since request header. | ||
// We don't look at more of the Cache-Control parameters, including ones that specify a field name. | ||
// ... | ||
|
||
return SPDYCachedResponseStateValid; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a new line? |
This file contains 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
This file contains 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
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
strptime(3)
doesn't initializestruct tm
, should be clear before using it, or, some of value may be undefined.strptime(3)
using current system locale, which is unknown. must bestrptime_l(3)
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.