Skip to content
This repository has been archived by the owner on Nov 1, 2021. It is now read-only.

Commit

Permalink
Add support for NSURLCache.
Browse files Browse the repository at this point in the history
The NSURL loading system has subtle and poorly documented behavior
regarding caching. CocoaSPDY was not doing the right thing, thus
there was no support for NSURLCache in the protocol. This patch
adds basic support for both NSURLConnection and NSURLSession based
requests. This is not yet a fully-featured client caching
implementation.

If the request specifies a NSURLRequest cachePolicy of:

NSURLRequestUseProtocolCachePolicy
- NSURL system does not provide a cached response to the protocol
constructor. It is up to the protocol to load and validate the
cached response.

NSURLRequestReturnCacheDataElseLoad
- NSURL system will provide the cached response, if available. The
protocol is expected to validate the response, and load if not
available or not valid.

NSURLRequestReturnCacheDataDontLoad
- NSURL system will provide the cached response, if available. The
protocol is expected to validate the response. The protocol will not
be loaded if no cached response is available, or if one is but is
invalid, the protocol should not load the request.

In the cases where the protocol has a cached response and it is valid,
it is supposed to call URLProtocol:cachedResponseIsValid. CocoaSPDY
was not doing this. Determining validity of the cached response is the
job of the protocol, and a basic implementation has been provided here.

CocoaSPDY also has to jump through some hoops whenever NSURLSession is
being used, as Apple has not provided a way to get the
NSURLSessionConfiguration and thus we cannot get the right NSURLCache.
Fortunately we have already provided a workaround for this.

SPDYMetadata has been extended to provide the source of the response.
  • Loading branch information
kgoodier committed Dec 4, 2015
1 parent 8519cd1 commit 11d02ec
Show file tree
Hide file tree
Showing 16 changed files with 1,220 additions and 17 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ build/
xcuserdata/
contents.xcworkspacedata
*.xccheckout
*.gcda
16 changes: 16 additions & 0 deletions SPDY.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@
5C48CF8E1B0A684C0082F7EF /* SPDYCacheStoragePolicy.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C48CF8C1B0A68400082F7EF /* SPDYCacheStoragePolicy.m */; };
5C48CF8F1B0A684C0082F7EF /* SPDYCacheStoragePolicy.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C48CF8C1B0A68400082F7EF /* SPDYCacheStoragePolicy.m */; };
5C48CF901B0A684D0082F7EF /* SPDYCacheStoragePolicy.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C48CF8C1B0A68400082F7EF /* SPDYCacheStoragePolicy.m */; };
5C5691E71C0D627400E47EAA /* SPDYMockSessionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C5691E61C0D627400E47EAA /* SPDYMockSessionManager.m */; };
5C5691EA1C0D66F100E47EAA /* SPDYNSURLCachingTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C5691E91C0D66F100E47EAA /* SPDYNSURLCachingTest.m */; };
5C5691ED1C0E7E4400E47EAA /* SPDYIntegrationTestHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C5691EC1C0E7E4400E47EAA /* SPDYIntegrationTestHelper.m */; };
5C5EA46E1A119B630058FB64 /* SPDYOriginEndpoint.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C5EA46A1A119B630058FB64 /* SPDYOriginEndpoint.m */; };
5C5EA46F1A119B630058FB64 /* SPDYOriginEndpoint.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C5EA46A1A119B630058FB64 /* SPDYOriginEndpoint.m */; };
5C5EA4701A119B630058FB64 /* SPDYOriginEndpoint.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C5EA46A1A119B630058FB64 /* SPDYOriginEndpoint.m */; };
Expand Down Expand Up @@ -198,6 +201,11 @@
5C427F101A1D57890072403D /* SPDYStopwatchTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYStopwatchTest.m; sourceTree = "<group>"; };
5C48CF8B1B0A68400082F7EF /* SPDYCacheStoragePolicy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYCacheStoragePolicy.h; sourceTree = "<group>"; };
5C48CF8C1B0A68400082F7EF /* SPDYCacheStoragePolicy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYCacheStoragePolicy.m; sourceTree = "<group>"; };
5C5691E61C0D627400E47EAA /* SPDYMockSessionManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYMockSessionManager.m; sourceTree = "<group>"; };
5C5691E81C0D62D200E47EAA /* SPDYMockSessionManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SPDYMockSessionManager.h; sourceTree = "<group>"; };
5C5691E91C0D66F100E47EAA /* SPDYNSURLCachingTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYNSURLCachingTest.m; sourceTree = "<group>"; };
5C5691EC1C0E7E4400E47EAA /* SPDYIntegrationTestHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYIntegrationTestHelper.m; sourceTree = "<group>"; };
5C5691EE1C0E7E8000E47EAA /* SPDYIntegrationTestHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SPDYIntegrationTestHelper.h; sourceTree = "<group>"; };
5C5EA4691A119B630058FB64 /* SPDYOriginEndpoint.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYOriginEndpoint.h; sourceTree = "<group>"; };
5C5EA46A1A119B630058FB64 /* SPDYOriginEndpoint.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYOriginEndpoint.m; sourceTree = "<group>"; };
5C5EA4711A119C950058FB64 /* SPDYMockOriginEndpointManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYMockOriginEndpointManager.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -300,6 +308,7 @@
069AA03816975B65005A72CA /* SPDYFrameCodecTest.m */,
5C2A211C19F9CA0E00D0EA76 /* SPDYLoggingTest.m */,
5CF0A2C81A089BC500B6D141 /* SPDYMetadataTest.m */,
5C5691E91C0D66F100E47EAA /* SPDYNSURLCachingTest.m */,
5C04570419B043CB009E0AC2 /* SPDYOriginEndpointTest.m */,
0679F3CE186217FC006F122E /* SPDYOriginTest.m */,
5CC7B9041BDECD43006E2952 /* SPDYProtocolContextTest.m */,
Expand Down Expand Up @@ -334,10 +343,14 @@
5C5EA4721A119C950058FB64 /* SPDYMockOriginEndpointManager.m */,
7774C7E1AF717FC36B7F15B6 /* SPDYSocket+SPDYSocketMock.h */,
7774C0ECD0C6E5D73FB38752 /* SPDYSocket+SPDYSocketMock.m */,
5C5691E81C0D62D200E47EAA /* SPDYMockSessionManager.h */,
5C5691E61C0D627400E47EAA /* SPDYMockSessionManager.m */,
7774CFEA3D0DAF374D7C7654 /* SPDYMockSessionTestBase.h */,
7774C193AC525BC3A79F2853 /* SPDYMockSessionTestBase.m */,
5CF0A2CA1A0952BA00B6D141 /* SPDYMockURLProtocolClient.h */,
5CF0A2CB1A0952D900B6D141 /* SPDYMockURLProtocolClient.m */,
5C5691EC1C0E7E4400E47EAA /* SPDYIntegrationTestHelper.m */,
5C5691EE1C0E7E8000E47EAA /* SPDYIntegrationTestHelper.h */,
);
name = "Supporting Files";
sourceTree = "<group>";
Expand Down Expand Up @@ -678,6 +691,7 @@
files = (
5C6D809A1BC44C19003AF2E0 /* SPDYURLCacheTest.m in Sources */,
5C0456FF19B033E9009E0AC2 /* SPDYSocketOps.m in Sources */,
5C5691ED1C0E7E4400E47EAA /* SPDYIntegrationTestHelper.m in Sources */,
06FDA20616717DF100137DBD /* SPDYSocket.m in Sources */,
5CA0B9C81A6486F10068ABD9 /* SPDYSettingsStoreTest.m in Sources */,
5C210A0A1A5F48C500ADB538 /* SPDYSessionPool.m in Sources */,
Expand All @@ -686,6 +700,7 @@
06FDA20B16717DF100137DBD /* SPDYFrameDecoder.m in Sources */,
0679F3CF186217FC006F122E /* SPDYOriginTest.m in Sources */,
06FDA20D16717DF100137DBD /* SPDYProtocol.m in Sources */,
5C5691E71C0D627400E47EAA /* SPDYMockSessionManager.m in Sources */,
5C750B501A390C7200CC0F2F /* SPDYPushStreamManagerTest.m in Sources */,
06FDA20F16717DF100137DBD /* SPDYSession.m in Sources */,
06FDA21116717DF100137DBD /* SPDYSessionManager.m in Sources */,
Expand All @@ -711,6 +726,7 @@
067EBFE717418F350029F16C /* SPDYStreamTest.m in Sources */,
062EA642175D4CD3003BC1CE /* SPDYCommonLogger.m in Sources */,
5C5EA46E1A119B630058FB64 /* SPDYOriginEndpoint.m in Sources */,
5C5691EA1C0D66F100E47EAA /* SPDYNSURLCachingTest.m in Sources */,
5C6D80AB1BC457B3003AF2E0 /* SPDYCanonicalRequest.m in Sources */,
25959A3F1937DE3900FC9731 /* SPDYSessionManagerTest.m in Sources */,
5CE43CE11AD74FC900E73FAC /* SPDYMetadata+Utils.m in Sources */,
Expand Down
10 changes: 10 additions & 0 deletions SPDY/SPDYCacheStoragePolicy.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,13 @@
* \returns A cache storage policy to use.
*/
extern NSURLCacheStoragePolicy SPDYCacheStoragePolicy(NSURLRequest *request, NSHTTPURLResponse *response);

typedef enum {
SPDYCachedResponseStateValid = 0,
SPDYCachedResponseStateInvalid,
SPDYCachedResponseStateMustRevalidate
} SPDYCachedResponseState;

/*! Determines the validity of a cached response
*/
extern SPDYCachedResponseState SPDYCacheLoadingPolicy(NSURLRequest *request, NSCachedURLResponse *response);
151 changes: 143 additions & 8 deletions SPDY/SPDYCacheStoragePolicy.m
Original file line number Diff line number Diff line change
Expand Up @@ -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]] : @"";
parameters[key] = value;
}
return parameters;
}

extern NSURLCacheStoragePolicy SPDYCacheStoragePolicy(NSURLRequest *request, NSHTTPURLResponse *response)
{
bool cacheable;
Expand All @@ -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;
}
}
Expand All @@ -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;
}
1 change: 1 addition & 0 deletions SPDY/SPDYMetadata+Utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
@property (nonatomic) NSUInteger streamId;
@property (nonatomic, copy) NSString *version;
@property (nonatomic) BOOL viaProxy;
@property (nonatomic) SPDYLoadSource loadSource;
@property (nonatomic) NSTimeInterval timeSessionConnected;
@property (nonatomic) NSTimeInterval timeStreamCreated;
@property (nonatomic) NSTimeInterval timeStreamRequestStarted;
Expand Down
9 changes: 9 additions & 0 deletions SPDY/SPDYProtocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ typedef enum {
SPDYProxyStatusConfigWithAuth // info provided in SPDYConfiguration, proxy needs auth
} SPDYProxyStatus;

typedef enum {
SPDYLoadSourceNetwork = 0, // regular stream or push stream from network
SPDYLoadSourceCache, // from NSURLCache
SPDYLoadSourcePushCache // from in-memory cache of in-progress pushed streams
} SPDYLoadSource;

@interface SPDYMetadata : NSObject

// SPDY stream time spent blocked - while queued waiting for connection, flow control, etc.
Expand Down Expand Up @@ -86,6 +92,9 @@ typedef enum {
// Indicates connection used a proxy server
@property (nonatomic, readonly) BOOL viaProxy;

// Indicates where this response came from
@property (nonatomic, readonly) SPDYLoadSource loadSource;

// The following measurements, presented in seconds, use mach_absolute_time() and are point-in-time
// relative to whatever base mach_absolute_time() uses. They use the following function to convert
// to seconds:
Expand Down

1 comment on commit 11d02ec

@kgoodier
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests are failing because NSURLSession on iOS 8 (simulator at least) does not seem to make the URLSession:dataTask:willCacheResponse:completionHandler: callback.

Please sign in to comment.