From 11d02ece71e113a0d5e13d1f31b524ba1837a48f Mon Sep 17 00:00:00 2001 From: Kevin Goodier Date: Mon, 30 Nov 2015 17:01:14 -0800 Subject: [PATCH] Add support for NSURLCache. 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. --- .gitignore | 1 + SPDY.xcodeproj/project.pbxproj | 16 + SPDY/SPDYCacheStoragePolicy.h | 10 + SPDY/SPDYCacheStoragePolicy.m | 151 ++++++- SPDY/SPDYMetadata+Utils.h | 1 + SPDY/SPDYProtocol.h | 9 + SPDY/SPDYProtocol.m | 74 ++++ SPDY/SPDYPushStreamManager.m | 4 + SPDY/SPDYSession.m | 1 + SPDY/SPDYSessionManager.h | 2 + SPDYUnitTests/SPDYIntegrationTestHelper.h | 53 +++ SPDYUnitTests/SPDYIntegrationTestHelper.m | 300 ++++++++++++++ SPDYUnitTests/SPDYMockSessionManager.h | 35 ++ SPDYUnitTests/SPDYMockSessionManager.m | 83 ++++ SPDYUnitTests/SPDYNSURLCachingTest.m | 479 ++++++++++++++++++++++ SPDYUnitTests/SPDYURLCacheTest.m | 18 +- 16 files changed, 1220 insertions(+), 17 deletions(-) create mode 100644 SPDYUnitTests/SPDYIntegrationTestHelper.h create mode 100644 SPDYUnitTests/SPDYIntegrationTestHelper.m create mode 100644 SPDYUnitTests/SPDYMockSessionManager.h create mode 100644 SPDYUnitTests/SPDYMockSessionManager.m create mode 100644 SPDYUnitTests/SPDYNSURLCachingTest.m diff --git a/.gitignore b/.gitignore index 04e11c3..775d689 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ build/ xcuserdata/ contents.xcworkspacedata *.xccheckout +*.gcda diff --git a/SPDY.xcodeproj/project.pbxproj b/SPDY.xcodeproj/project.pbxproj index 7609579..3662ce4 100644 --- a/SPDY.xcodeproj/project.pbxproj +++ b/SPDY.xcodeproj/project.pbxproj @@ -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 */; }; @@ -198,6 +201,11 @@ 5C427F101A1D57890072403D /* SPDYStopwatchTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYStopwatchTest.m; sourceTree = ""; }; 5C48CF8B1B0A68400082F7EF /* SPDYCacheStoragePolicy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYCacheStoragePolicy.h; sourceTree = ""; }; 5C48CF8C1B0A68400082F7EF /* SPDYCacheStoragePolicy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYCacheStoragePolicy.m; sourceTree = ""; }; + 5C5691E61C0D627400E47EAA /* SPDYMockSessionManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYMockSessionManager.m; sourceTree = ""; }; + 5C5691E81C0D62D200E47EAA /* SPDYMockSessionManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SPDYMockSessionManager.h; sourceTree = ""; }; + 5C5691E91C0D66F100E47EAA /* SPDYNSURLCachingTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYNSURLCachingTest.m; sourceTree = ""; }; + 5C5691EC1C0E7E4400E47EAA /* SPDYIntegrationTestHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYIntegrationTestHelper.m; sourceTree = ""; }; + 5C5691EE1C0E7E8000E47EAA /* SPDYIntegrationTestHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SPDYIntegrationTestHelper.h; sourceTree = ""; }; 5C5EA4691A119B630058FB64 /* SPDYOriginEndpoint.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYOriginEndpoint.h; sourceTree = ""; }; 5C5EA46A1A119B630058FB64 /* SPDYOriginEndpoint.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYOriginEndpoint.m; sourceTree = ""; }; 5C5EA4711A119C950058FB64 /* SPDYMockOriginEndpointManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYMockOriginEndpointManager.h; sourceTree = ""; }; @@ -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 */, @@ -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 = ""; @@ -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 */, @@ -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 */, @@ -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 */, diff --git a/SPDY/SPDYCacheStoragePolicy.h b/SPDY/SPDYCacheStoragePolicy.h index b983ac8..bca4a38 100644 --- a/SPDY/SPDYCacheStoragePolicy.h +++ b/SPDY/SPDYCacheStoragePolicy.h @@ -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); diff --git a/SPDY/SPDYCacheStoragePolicy.m b/SPDY/SPDYCacheStoragePolicy.m index a4c9f4d..c25ea78 100644 --- a/SPDY/SPDYCacheStoragePolicy.m +++ b/SPDY/SPDYCacheStoragePolicy.m @@ -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; @@ -35,6 +88,13 @@ 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. @@ -42,30 +102,37 @@ extern NSURLCacheStoragePolicy SPDYCacheStoragePolicy(NSURLRequest *request, NSH // 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; +} \ No newline at end of file diff --git a/SPDY/SPDYMetadata+Utils.h b/SPDY/SPDYMetadata+Utils.h index 7acc78e..5a6f731 100644 --- a/SPDY/SPDYMetadata+Utils.h +++ b/SPDY/SPDYMetadata+Utils.h @@ -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; diff --git a/SPDY/SPDYProtocol.h b/SPDY/SPDYProtocol.h index 9e392a7..76264e4 100644 --- a/SPDY/SPDYProtocol.h +++ b/SPDY/SPDYProtocol.h @@ -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. @@ -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: diff --git a/SPDY/SPDYProtocol.m b/SPDY/SPDYProtocol.m index d41d28f..c9b7879 100644 --- a/SPDY/SPDYProtocol.m +++ b/SPDY/SPDYProtocol.m @@ -16,6 +16,7 @@ #import #import #import "NSURLRequest+SPDYURLRequest.h" +#import "SPDYCacheStoragePolicy.h" #import "SPDYCanonicalRequest.h" #import "SPDYCommonLogger.h" #import "SPDYMetadata+Utils.h" @@ -148,6 +149,7 @@ @implementation SPDYProtocol SPDYProtocolContext *_context; NSURLSession *_associatedSession; NSURLSessionTask *_associatedSessionTask; + NSCachedURLResponse *_overrideCachedResponse; struct { BOOL didStartLoading:1; BOOL didStopLoading:1; @@ -337,6 +339,45 @@ + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request return canonicalRequest; } +- (NSCachedURLResponse *)cachedResponse +{ + if (_overrideCachedResponse != nil) { + return _overrideCachedResponse; + } else { + return [super cachedResponse]; + } +} + +- (NSCachedURLResponse *)loadCachedResponseIfAllowed +{ + // We're making some choices here to limit the surface area of caching, given we don't yet + // have a fully-featured client caching implementation (missing sufficient validity checks). + // + // For the default request cache policy (NSURLRequestUseProtocolCachePolicy), we have to load + // the cache ourselves. We're applying the following rules in that case: + // - NSURLConnection-based requests will not support caching. + // - NSURLSession-based requests must set the SPDYURLSession property on the request, and + // must provide a NSURLCache in their NSURLSessionConfiguration. There is no fallback to + // other shared caches. + // - NSURLSession-based requests that do not set SPDYURLSession will not support caching. + // + // This behavior may change in the future. + + NSCachedURLResponse *response; + + BOOL isNSURLSession = (_associatedSession != nil || + _associatedSessionTask != nil || + ([self respondsToSelector:@selector(task)] && self.task != nil)); + if (isNSURLSession) { + NSURLSessionConfiguration *config = _associatedSession.configuration; + if (config.requestCachePolicy == NSURLRequestUseProtocolCachePolicy) { + response = [config.URLCache cachedResponseForRequest:self.request]; + } + } + + return response; +} + - (instancetype)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id )client { // iOS 8 will call this using the 'request' returned from canonicalRequestForRequest. However, @@ -451,6 +492,39 @@ - (void)detectSessionAndTaskThenContinueWithOrigin:(SPDYOrigin *)origin - (void)startStreamForOrigin:(SPDYOrigin *)origin { + // Load the cached item, if necessary, now that we (potentially) have the associated NSURLSession. + if (self.cachedResponse == nil) { + _overrideCachedResponse = [self loadCachedResponseIfAllowed]; + } + + // If we have a cached item and it passes validity checks, notify NSURL system and bail out. + // Note we don't support revalidation at this time. + SPDYCachedResponseState cachedState = SPDYCacheLoadingPolicy(self.request, self.cachedResponse); + if (cachedState == SPDYCachedResponseStateValid) { + _context.metadata.loadSource = SPDYLoadSourceCache; + + // Associate a new instance of the SPDYMetadata with this cached response. It has been + // stored in the cache with an old metadata identifier. That metadata no longer exists. + // First break it down and augment + NSCachedURLResponse *oldCachedResponse = self.cachedResponse; + NSHTTPURLResponse *oldHttpResponse = (NSHTTPURLResponse *)oldCachedResponse.response; + NSMutableDictionary *headers = [oldHttpResponse.allHeaderFields mutableCopy]; + [SPDYMetadata setMetadata:_context.metadata forAssociatedDictionary:headers]; + + // Then rebuild it + NSHTTPURLResponse *newHttpResponse = [[NSHTTPURLResponse alloc] initWithURL:oldHttpResponse.URL + statusCode:oldHttpResponse.statusCode + HTTPVersion:@"HTTP/1.1" + headerFields:headers]; + _overrideCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:newHttpResponse + data:oldCachedResponse.data + userInfo:oldCachedResponse.userInfo + storagePolicy:oldCachedResponse.storagePolicy]; + + [self.client URLProtocol:self cachedResponseIsValid:self.cachedResponse]; + return; + } + SPDYSessionManager *manager = [SPDYSessionManager localManagerForOrigin:origin]; // See if this is currently being pushed, and if so, hook it up, else create it diff --git a/SPDY/SPDYPushStreamManager.m b/SPDY/SPDYPushStreamManager.m index 0d4bf41..4969480 100644 --- a/SPDY/SPDYPushStreamManager.m +++ b/SPDY/SPDYPushStreamManager.m @@ -12,6 +12,7 @@ #import #import #import "SPDYCommonLogger.h" +#import "SPDYMetadata+Utils.h" #import "SPDYPushStreamManager.h" #import "SPDYProtocol.h" #import "SPDYStream.h" @@ -271,6 +272,9 @@ - (void)stopLoadingStream:(SPDYStream *)stream [self removeStream:stream]; } else { SPDY_DEBUG(@"PUSH.%u: leaving pushed stream with associated stream %u", stream.streamId, stream.associatedStream.streamId); + + // Transition load source of this stream since it is done pulling from network. + stream.metadata.loadSource = SPDYLoadSourcePushCache; } } } diff --git a/SPDY/SPDYSession.m b/SPDY/SPDYSession.m index 282e166..6558542 100644 --- a/SPDY/SPDYSession.m +++ b/SPDY/SPDYSession.m @@ -198,6 +198,7 @@ - (void)openStream:(SPDYStream *)stream stream.metadata.viaProxy = _socket.connectedToProxy; stream.metadata.proxyStatus = _socket.proxyStatus; stream.metadata.cellular = _cellular; + stream.metadata.loadSource = SPDYLoadSourceNetwork; [stream startWithStreamId:streamId sendWindowSize:_initialSendWindowSize diff --git a/SPDY/SPDYSessionManager.h b/SPDY/SPDYSessionManager.h index 8067d09..e1fc6ce 100644 --- a/SPDY/SPDYSessionManager.h +++ b/SPDY/SPDYSessionManager.h @@ -11,7 +11,9 @@ #import +@class SPDYOrigin; @class SPDYPushStreamManager; +@class SPDYStream; @interface SPDYSessionManager : NSObject diff --git a/SPDYUnitTests/SPDYIntegrationTestHelper.h b/SPDYUnitTests/SPDYIntegrationTestHelper.h new file mode 100644 index 0000000..10291af --- /dev/null +++ b/SPDYUnitTests/SPDYIntegrationTestHelper.h @@ -0,0 +1,53 @@ +// +// SPDYIntegrationTestHelper.h +// SPDY +// +// Copyright (c) 2015 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Kevin Goodier on 11/30/15. +// + +#import + +@class SPDYStream; +@protocol SPDYProtocolContext; + +// Base class. Should use one of the specific implementations below. +@interface SPDYIntegrationTestHelper : NSObject + +@property (nonatomic) SPDYStream *stream; +@property (nonatomic) NSCachedURLResponse *willCacheResponse; +@property (nonatomic) NSHTTPURLResponse *response; +@property (nonatomic) NSMutableData *data; +@property (nonatomic) NSError *connectionError; + ++ (void)setUp; ++ (void)tearDown; + +- (BOOL)didLoadFromNetwork; +- (BOOL)didLoadFromCache; +- (BOOL)didGetResponse; +- (BOOL)didGetError; +- (BOOL)didCacheResponse; + +- (void)reset; +- (void)loadRequest:(NSURLRequest *)request; +- (void)provideResponseWithStatus:(NSUInteger)status cacheControl:(NSString *)cacheControl date:(NSDate *)date; +- (void)provideBasicUncacheableResponse; +- (void)provideBasicCacheableResponse; +- (void)provideErrorResponse; + +@end + +// Request helper that uses NSURLConnection to issue requests. +@interface SPDYURLConnectionIntegrationTestHelper : SPDYIntegrationTestHelper +@end + +// Request helper that uses NSURLSession (data task) to issue requests. +@interface SPDYURLSessionIntegrationTestHelper : SPDYIntegrationTestHelper +@property (nonatomic) id spdyContext; +@end + + diff --git a/SPDYUnitTests/SPDYIntegrationTestHelper.m b/SPDYUnitTests/SPDYIntegrationTestHelper.m new file mode 100644 index 0000000..9426cad --- /dev/null +++ b/SPDYUnitTests/SPDYIntegrationTestHelper.m @@ -0,0 +1,300 @@ +// +// SPDYIntegrationTestHelper.h +// SPDY +// +// Copyright (c) 2015 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Kevin Goodier on 11/30/15. +// + +#import "NSURLRequest+SPDYURLRequest.h" +#import "SPDYIntegrationTestHelper.h" +#import "SPDYMockSessionManager.h" +#import "SPDYProtocol.h" +#import "SPDYStream.h" + +#pragma mark Base implementation + +@implementation SPDYIntegrationTestHelper +{ + CFRunLoopRef _runLoop; +} + ++ (void)setUp +{ + [SPDYMockSessionManager performSwizzling:YES]; +} + ++ (void)tearDown +{ + [SPDYMockSessionManager performSwizzling:NO]; +} + +- (instancetype)init +{ + self = [super init]; + if (self != nil) { + [self provideErrorResponse]; + } + return self; +} + +- (BOOL)didLoadFromNetwork +{ + return self.didGetResponse && _stream != nil; +} + +- (BOOL)didLoadFromCache +{ + return self.didGetResponse && _stream == nil; +} + +- (BOOL)didGetResponse +{ + return _response != nil; +} + +- (BOOL)didGetError +{ + return _connectionError != nil; +} + +- (BOOL)didCacheResponse +{ + return _willCacheResponse != nil; +} + +- (void)reset +{ + _stream = nil; + _response = nil; + _data = nil; + _willCacheResponse = nil; + _connectionError = nil; +} + +- (void)loadRequest:(NSURLRequest *)request +{ + [self reset]; + + // Needs to be implemented by subclass as well +} + +- (NSString *)dateHeaderValue:(NSDate *)date +{ + NSString *string = nil; + if (date) { + time_t timeRaw = (long)date.timeIntervalSince1970; + struct tm timeStruct; + char buffer[80]; + + gmtime_r(&timeRaw, &timeStruct); + size_t charCount = strftime(buffer, sizeof(buffer), "%a, %d %b %Y %H:%M:%S GMT", &timeStruct); + if (0 != charCount) { + string = [[NSString alloc] initWithCString:buffer encoding:NSASCIIStringEncoding]; + } + } + + return string; +} + +- (void)provideResponseWithStatus:(NSUInteger)status cacheControl:(NSString *)cacheControl date:(NSDate *)date +{ + [SPDYMockSessionManager shared].streamQueuedBlock = ^(SPDYStream *stream) { + _stream = stream; + uint8_t dataBytes[] = {1}; + NSData *data = [NSData dataWithBytes:dataBytes length:1]; + NSMutableDictionary *headers = [NSMutableDictionary dictionaryWithDictionary:@{ + @":status": [@(status) stringValue], + @":version": @"1.1", + }]; + if (date != nil) { + headers[@"Date"] = [self dateHeaderValue:date]; + } + if (cacheControl.length > 0) { + headers[@"Cache-Control"] = cacheControl; + } + + [stream mergeHeaders:headers]; + [stream didReceiveResponse]; + [stream didLoadData:data]; + }; +} + +- (void)provideBasicUncacheableResponse +{ + [self provideResponseWithStatus:200 cacheControl:@"no-store, no-cache, must-revalidate" date:[NSDate date]]; +} + +- (void)provideBasicCacheableResponse +{ + [self provideResponseWithStatus:200 cacheControl:@"public, max-age=1200" date:[NSDate date]]; +} + +- (void)provideErrorResponse +{ + [SPDYMockSessionManager shared].streamQueuedBlock = ^(SPDYStream *stream) { + _stream = stream; + [stream closeWithError:[NSError errorWithDomain:@"SPDYUnitTest" code:1ul userInfo:nil]]; + }; +} + +- (void)_waitForRequestToFinish +{ + _runLoop = CFRunLoopGetCurrent(); + CFRunLoopRun(); +} + +- (void)_finishRequest +{ + CFRunLoopStop(_runLoop); +} + +- (NSString *)description +{ + NSDictionary *params = @{ + @"didLoadFromNetwork": @([self didLoadFromNetwork]), + @"didGetResponse": @([self didGetResponse]), + @"didGetError": @([self didGetError]), + @"didCacheResponse": @([self didCacheResponse]), + @"stream": _stream ?: @"", + @"response": _response ?: @"", + @"willCacheResponse": _willCacheResponse ?: @"", + @"connectionError": _connectionError ?: @"", + }; + return [NSString stringWithFormat:@"<%@: %p> %@", [self class], self, params]; +} + +@end + + +#pragma mark NSURLConnection + +@implementation SPDYURLConnectionIntegrationTestHelper + +- (void)loadRequest:(NSURLRequest *)request +{ + [super loadRequest:request]; + + [NSURLConnection connectionWithRequest:request delegate:self]; + + [self _waitForRequestToFinish]; +} + +#pragma mark NSURLConnection delegate methods + +- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response +{ + self.response = (NSHTTPURLResponse *)response; +} + +- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data +{ + if (self.data == nil) { + self.data = [NSMutableData dataWithData:data]; + } else { + [self.data appendData:data]; + } +} + +- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse +{ + self.willCacheResponse = cachedResponse; + return cachedResponse; +} + +- (void)connectionDidFinishLoading:(NSURLConnection *)connection +{ + self.connectionError = nil; + [self _finishRequest]; +} + +- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error +{ + self.connectionError = error; + [self _finishRequest]; +} + +@end + + +#pragma mark NSURLSession + +@implementation SPDYURLSessionIntegrationTestHelper +{ + NSURLCache *_cache; + NSURLSessionConfiguration *_configuration; + NSURLSession *_session; +} + +- (instancetype)init +{ + self = [super init]; + if (self) { + _cache = [[NSURLCache alloc] initWithMemoryCapacity:1000000 diskCapacity:10000000 diskPath:nil]; + } + return self; +} +- (void)loadRequest:(NSMutableURLRequest *)request +{ + [super loadRequest:request]; + + _configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; + _configuration.URLCache = _cache; + _configuration.protocolClasses = @[ [SPDYURLSessionProtocol class] ]; + + // request cache policy should be set in the NSURLSessionConfiguration, not in the request. + // doing that here to simplify the tests. + _configuration.requestCachePolicy = request.cachePolicy; + + _session = [NSURLSession sessionWithConfiguration:_configuration delegate:self delegateQueue:nil]; + + // Special SPDY hack to get access to the NSURLSessionConfiguration + request.SPDYURLSession = _session; + + NSURLSessionDataTask *task = [_session dataTaskWithRequest:request]; + [task resume]; + + [self _waitForRequestToFinish]; +} + +#pragma mark NSURLSession delegate methods + +- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler +{ + self.response = (NSHTTPURLResponse *)response; + completionHandler(NSURLSessionResponseAllow); +} + +- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data +{ + if (self.data == nil) { + self.data = [NSMutableData dataWithData:data]; + } else { + [self.data appendData:data]; + } +} + +- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask willCacheResponse:(NSCachedURLResponse *)proposedResponse completionHandler:(void (^)(NSCachedURLResponse * cachedResponse))completionHandler +{ + self.willCacheResponse = proposedResponse; + completionHandler(proposedResponse); +} + +- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error +{ + // error may be nil + self.connectionError = error; + [self _finishRequest]; +} + +#pragma mark SPDYURLSessionDelegate methods + +- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didStartLoadingRequest:(NSURLRequest *)request withContext:(id)context +{ + self.spdyContext = context; +} + +@end diff --git a/SPDYUnitTests/SPDYMockSessionManager.h b/SPDYUnitTests/SPDYMockSessionManager.h new file mode 100644 index 0000000..0743190 --- /dev/null +++ b/SPDYUnitTests/SPDYMockSessionManager.h @@ -0,0 +1,35 @@ +// +// SPDYMockSessionManager.h +// SPDY +// +// Copyright (c) 2015 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Kevin Goodier on 11/30/15. +// + +#import +#import "SPDYSessionManager.h" + +@class SPDYPushStreamManager; +@class SPDYStream; + +typedef void (^SPDYMockSessionManagerStreamQueuedCallback)(SPDYStream *stream); + +@interface SPDYMockSessionManager : SPDYSessionManager + +#pragma mark Mock methods + +@property (nonatomic, copy) SPDYMockSessionManagerStreamQueuedCallback streamQueuedBlock; + ++ (void)performSwizzling:(BOOL)performSwizzling; ++ (SPDYMockSessionManager *)shared; + +#pragma mark SPDYSessionManager methods + +@property (nonatomic, readonly) SPDYPushStreamManager *pushStreamManager; + +- (void)queueStream:(SPDYStream *)stream; + +@end diff --git a/SPDYUnitTests/SPDYMockSessionManager.m b/SPDYUnitTests/SPDYMockSessionManager.m new file mode 100644 index 0000000..75e9ed7 --- /dev/null +++ b/SPDYUnitTests/SPDYMockSessionManager.m @@ -0,0 +1,83 @@ +// +// SPDYMockSessionManager.m +// SPDY +// +// Copyright (c) 2015 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Kevin Goodier on 11/30/15. +// + +#import +#import "SPDYMockSessionManager.h" +#import "SPDYOrigin.h" +#import "SPDYPushStreamManager.h" +#import "SPDYStream.h" +#import + +@implementation SPDYMockSessionManager + +#pragma mark Mock methods + ++ (void)performSwizzling:(BOOL)performSwizzling +{ + Method original, swizzle; + + original = class_getClassMethod([SPDYSessionManager class], @selector(localManagerForOrigin:)); + swizzle = class_getClassMethod([SPDYMockSessionManager class], @selector(swizzled_localManagerForOrigin:)); + if (performSwizzling) { + method_exchangeImplementations(original, swizzle); + } else { + method_exchangeImplementations(swizzle, original); + } +} + ++ (SPDYSessionManager *)swizzled_localManagerForOrigin:(SPDYOrigin *)origin +{ + return [SPDYMockSessionManager shared]; +} + ++ (SPDYMockSessionManager *)shared +{ + static dispatch_once_t once; + static SPDYMockSessionManager *instance; + dispatch_once(&once, ^{ + instance = [[SPDYMockSessionManager alloc] init]; + }); + return instance; +} + +- (instancetype)init +{ + self = [super init]; + if (self != nil) { + } + return self; +} + +#pragma mark SPDYSessionManager methods + +- (SPDYPushStreamManager *)pushStreamManager +{ + // Not needed + return nil; +} + +- (void)queueStream:(SPDYStream *)stream +{ + // stream.delegate = + [stream startWithStreamId:1 sendWindowSize:1000 receiveWindowSize:1000]; + stream.localSideClosed = YES; + + if (_streamQueuedBlock) { + _streamQueuedBlock(stream); + } + + // NSURL system won't make the didReceiveResponse callback until either data is received + // or the response finishes. At least for iOS 9. Odd. So we'll force the didFinishLoading + // callback to flush out the other. + stream.remoteSideClosed = YES; +} + +@end diff --git a/SPDYUnitTests/SPDYNSURLCachingTest.m b/SPDYUnitTests/SPDYNSURLCachingTest.m new file mode 100644 index 0000000..d9554ba --- /dev/null +++ b/SPDYUnitTests/SPDYNSURLCachingTest.m @@ -0,0 +1,479 @@ +// +// SPDYNSURLCachingTest.m +// SPDY +// +// Copyright (c) 2015 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Kevin Goodier on 11/30/15. +// + +#import +#import +#import "SPDYIntegrationTestHelper.h" +#import "SPDYProtocol.h" + +// Remove this once CocoaSPDY supports fully-featured caching + +@interface SPDYNSURLCachingTest : XCTestCase +@end + +@implementation SPDYNSURLCachingTest + ++ (void)setUp +{ + [super setUp]; + [SPDYIntegrationTestHelper setUp]; + + [SPDYURLConnectionProtocol registerOrigin:@"http://example.com"]; +} + ++ (void)tearDown +{ + [super tearDown]; + [SPDYIntegrationTestHelper tearDown]; +} + +- (void)setUp +{ + [super setUp]; + [self resetSharedCache]; +} + +- (void)tearDown +{ + [super tearDown]; + [self resetSharedCache]; +} + +- (void)resetSharedCache +{ + [[NSURLCache sharedURLCache] removeAllCachedResponses]; +} + +- (NSArray *)parameterizedTestHelpers +{ + // Should behave the same + return @[ + [[SPDYURLConnectionIntegrationTestHelper alloc] init], + [[SPDYURLSessionIntegrationTestHelper alloc] init], + ]; +} + +- (NSArray *)parameterizedTestInputs +{ + return @[ + // Request helper to use Cache policy to use Should pull from cache + @[ [[SPDYURLConnectionIntegrationTestHelper alloc] init], @(NSURLRequestUseProtocolCachePolicy), @(NO) ], + @[ [[SPDYURLSessionIntegrationTestHelper alloc] init], @(NSURLRequestUseProtocolCachePolicy), @(YES) ], + @[ [[SPDYURLConnectionIntegrationTestHelper alloc] init], @(NSURLRequestReturnCacheDataElseLoad), @(YES) ], + @[ [[SPDYURLSessionIntegrationTestHelper alloc] init], @(NSURLRequestReturnCacheDataElseLoad), @(YES) ], + ]; +} + +#pragma mark Tests + +#define GET_TEST_PARAMS \ + SPDYIntegrationTestHelper *testHelper = testParams[0]; \ + NSURLRequestCachePolicy cachePolicy = [testParams[1] integerValue]; \ + BOOL shouldPullFromCache = [testParams[2] boolValue]; (void)shouldPullFromCache + +- (void)testCacheableResponse_DoesInsertAndLoadFromCache +{ + for (NSArray *testParams in [self parameterizedTestInputs]) { + GET_TEST_PARAMS; + NSLog(@"- using %@, policy %tu, shouldPullFromCache %tu", [testHelper class], cachePolicy, shouldPullFromCache); + + [self resetSharedCache]; + + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"http://example.com/test/path"]]; + request.cachePolicy = cachePolicy; + + [testHelper provideBasicCacheableResponse]; + [testHelper loadRequest:request]; + + XCTAssertTrue(testHelper.didLoadFromNetwork, @"%@", testHelper); + XCTAssertTrue(testHelper.didCacheResponse, @"%@", testHelper); + + // Now make request again. Should pull from cache. + [testHelper reset]; + [testHelper loadRequest:request]; + + // Verify response was pulled from cache, not network + XCTAssertEqual(testHelper.didLoadFromCache, shouldPullFromCache, @"%@", testHelper); + } +} + +- (void)testCachedItem_DoesHaveMetadata +{ + for (NSArray *testParams in [self parameterizedTestInputs]) { + GET_TEST_PARAMS; + NSLog(@"- using %@, policy %tu, shouldPullFromCache %tu", [testHelper class], cachePolicy, shouldPullFromCache); + [self resetSharedCache]; + + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"http://example.com/test/path"]]; + request.cachePolicy = cachePolicy; + + [testHelper provideBasicCacheableResponse]; + [testHelper loadRequest:request]; + + XCTAssertTrue(testHelper.didCacheResponse, @"%@", testHelper); + + // Verify metadata + SPDYMetadata *metadata = [SPDYProtocol metadataForResponse:testHelper.response]; + XCTAssertNotNil(metadata, @"%@", testHelper); + XCTAssertEqual(metadata.loadSource, SPDYLoadSourceNetwork, @"%@", testHelper); + + // Now make request again. Should pull from cache. + [testHelper reset]; + [testHelper loadRequest:request]; + + // Verify response was pulled from cache, not network + XCTAssertEqual(testHelper.didLoadFromCache, shouldPullFromCache, @"%@", testHelper); + + // Verify metadata + metadata = [SPDYProtocol metadataForResponse:testHelper.response]; + XCTAssertNotNil(metadata, @"%@", testHelper); + XCTAssertEqual(metadata.loadSource, shouldPullFromCache ? SPDYLoadSourceCache : SPDYLoadSourceNetwork, @"%@", testHelper); + + // Special logic for metadata provided by SPDYProtocolContext + if ([testHelper isMemberOfClass:[SPDYURLSessionIntegrationTestHelper class]]) { + SPDYURLSessionIntegrationTestHelper *testHelperURLSession = (SPDYURLSessionIntegrationTestHelper *)testHelper; + SPDYMetadata *metadata2 = [testHelperURLSession.spdyContext metadata]; + + XCTAssertNotNil(metadata2, @"%@", testHelper); + XCTAssertEqual(metadata2.loadSource, shouldPullFromCache ? SPDYLoadSourceCache : SPDYLoadSourceNetwork, @"%@", testHelper); + } + } +} + +- (void)testWithReturnCacheDontLoadPolicy_DoesFailRequest +{ + for (SPDYIntegrationTestHelper *testHelper in [self parameterizedTestHelpers]) { + NSLog(@"- using %@", [testHelper class]); + [self resetSharedCache]; + + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"http://example.com/test/path"]]; + request.cachePolicy = NSURLRequestReturnCacheDataDontLoad; + + [testHelper loadRequest:request]; + + XCTAssertFalse(testHelper.didLoadFromNetwork, @"%@", testHelper); + XCTAssertTrue(testHelper.didGetError, @"%@", testHelper); + XCTAssertFalse(testHelper.didCacheResponse, @"%@", testHelper); + } +} + +- (void)testWithReturnCacheDontLoadPolicy_DoesUseCacheIfPopulated +{ + for (SPDYIntegrationTestHelper *testHelper in [self parameterizedTestHelpers]) { + NSLog(@"- using %@", [testHelper class]); + [self resetSharedCache]; + + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"http://example.com/test/path"]]; + request.cachePolicy = NSURLRequestUseProtocolCachePolicy; + + [testHelper provideBasicCacheableResponse]; + [testHelper loadRequest:request]; + + XCTAssertTrue(testHelper.didLoadFromNetwork, @"%@", testHelper); + XCTAssertTrue(testHelper.didCacheResponse, @"%@", testHelper); + + // Now make request again. Should pull from cache. + request.cachePolicy = NSURLRequestReturnCacheDataDontLoad; + [testHelper reset]; + [testHelper loadRequest:request]; + + XCTAssertTrue(testHelper.didLoadFromCache, @"%@", testHelper); + } +} + +- (void)testWithReloadIgnoringCachePolicy_DoesNotUseCache +{ + for (SPDYIntegrationTestHelper *testHelper in [self parameterizedTestHelpers]) { + NSLog(@"- using %@", [testHelper class]); + [self resetSharedCache]; + + // First insert into cache + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"http://example.com/test/path"]]; + request.cachePolicy = NSURLRequestUseProtocolCachePolicy; + + [testHelper provideBasicCacheableResponse]; + [testHelper loadRequest:request]; + + XCTAssertTrue(testHelper.didLoadFromNetwork, @"%@", testHelper); + XCTAssertTrue(testHelper.didCacheResponse, @"%@", testHelper); + + // Now make request again with ReloadingIgnoringCache. Should not use cache. + request.cachePolicy = NSURLRequestReloadIgnoringCacheData; + [testHelper reset]; + [testHelper loadRequest:request]; + + XCTAssertTrue(testHelper.didLoadFromNetwork, @"%@", testHelper); + } +} + +- (void)testWith404Response_DoesUseCache +{ + for (SPDYIntegrationTestHelper *testHelper in [self parameterizedTestHelpers]) { + NSLog(@"- using %@", [testHelper class]); + [self resetSharedCache]; + + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"http://example.com/test/path"]]; + request.cachePolicy = NSURLRequestReturnCacheDataElseLoad; + + [testHelper provideResponseWithStatus:404 cacheControl:@"public, max-age=1200" date:[NSDate date]]; + [testHelper loadRequest:request]; + + XCTAssertTrue(testHelper.didLoadFromNetwork, @"%@", testHelper); + XCTAssertTrue(testHelper.didCacheResponse, @"%@", testHelper); + + // Now make request again. Should pull from cache. + [testHelper reset]; + [testHelper loadRequest:request]; + + XCTAssertTrue(testHelper.didLoadFromCache, @"%@", testHelper); + XCTAssertEqual(testHelper.response.statusCode, 404ul, @"%@", testHelper); + } +} + +- (void)testWith500Response_DoesNotUseCache +{ + for (SPDYIntegrationTestHelper *testHelper in [self parameterizedTestHelpers]) { + NSLog(@"- using %@", [testHelper class]); + [self resetSharedCache]; + + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"http://example.com/test/path"]]; + request.cachePolicy = NSURLRequestReturnCacheDataElseLoad; + + [testHelper provideResponseWithStatus:500 cacheControl:@"public, max-age=1200" date:[NSDate date]]; + [testHelper loadRequest:request]; + + XCTAssertTrue(testHelper.didLoadFromNetwork, @"%@", testHelper); + XCTAssertFalse(testHelper.didCacheResponse, @"%@", testHelper); + + // Now make request again. Should not pull from cache. + [testHelper reset]; + [testHelper loadRequest:request]; + + // Verify response was pulled from network + XCTAssertTrue(testHelper.didLoadFromNetwork, @"%@", testHelper); + XCTAssertFalse(testHelper.didCacheResponse, @"%@", testHelper); + XCTAssertEqual(testHelper.response.statusCode, 500ul, @"%@", testHelper); + } +} + +- (void)testWithCacheableRequest_WithNoCacheResponse_DoesNotUseCache +{ + for (NSArray *testParams in [self parameterizedTestInputs]) { + GET_TEST_PARAMS; + NSLog(@"- using %@, policy %tu, shouldPullFromCache %tu", [testHelper class], cachePolicy, shouldPullFromCache); + [self resetSharedCache]; + + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"http://example.com/test/path"]]; + request.cachePolicy = cachePolicy; + + [testHelper provideBasicUncacheableResponse]; + [testHelper loadRequest:request]; + + XCTAssertTrue(testHelper.didLoadFromNetwork, @"%@", testHelper); + XCTAssertFalse(testHelper.didCacheResponse, @"%@", testHelper); + + // Now make request again. Should not pull from cache. + [testHelper reset]; + [testHelper loadRequest:request]; + + XCTAssertTrue(testHelper.didLoadFromNetwork, @"%@", testHelper); + } +} + +- (void)testWithNoCacheRequest_WithCacheableResponse_DoesNotUseCache +{ + for (NSArray *testParams in [self parameterizedTestInputs]) { + GET_TEST_PARAMS; + NSLog(@"- using %@, policy %tu, shouldPullFromCache %tu", [testHelper class], cachePolicy, shouldPullFromCache); + [self resetSharedCache]; + + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"http://example.com/test/path"]]; + request.cachePolicy = cachePolicy; + [request addValue:@"no-store, no-cache" forHTTPHeaderField:@"Cache-Control"]; + + [testHelper provideBasicCacheableResponse]; + [testHelper loadRequest:request]; + + XCTAssertTrue(testHelper.didLoadFromNetwork, @"%@", testHelper); + XCTAssertFalse(testHelper.didCacheResponse, @"%@", testHelper); + + // Now make request again. Should not pull from cache. + [testHelper reset]; + [testHelper loadRequest:request]; + + XCTAssertTrue(testHelper.didLoadFromNetwork, @"%@", testHelper); + } +} + +- (void)testWithNoCacheRequest_WithNoCacheResponse_DoesNotUseCache +{ + for (NSArray *testParams in [self parameterizedTestInputs]) { + GET_TEST_PARAMS; + NSLog(@"- using %@, policy %tu, shouldPullFromCache %tu", [testHelper class], cachePolicy, shouldPullFromCache); + [self resetSharedCache]; + + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"http://example.com/test/path"]]; + request.cachePolicy = cachePolicy; + [request addValue:@"no-store, no-cache" forHTTPHeaderField:@"Cache-Control"]; + + [testHelper provideBasicUncacheableResponse]; + [testHelper loadRequest:request]; + + XCTAssertTrue(testHelper.didLoadFromNetwork, @"%@", testHelper); + XCTAssertFalse(testHelper.didCacheResponse, @"%@", testHelper); + + // Now make request again. Should not pull from cache. + [testHelper reset]; + [testHelper loadRequest:request]; + + XCTAssertTrue(testHelper.didLoadFromNetwork, @"%@", testHelper); + } +} + +- (void)testWithDifferentPathRequest_WithCacheableResponse_DoesNotUseCache +{ + for (NSArray *testParams in [self parameterizedTestInputs]) { + GET_TEST_PARAMS; + NSLog(@"- using %@, policy %tu, shouldPullFromCache %tu", [testHelper class], cachePolicy, shouldPullFromCache); + [self resetSharedCache]; + + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"http://example.com/test/path"]]; + request.cachePolicy = cachePolicy; + + [testHelper provideBasicCacheableResponse]; + [testHelper loadRequest:request]; + + XCTAssertTrue(testHelper.didLoadFromNetwork, @"%@", testHelper); + XCTAssertTrue(testHelper.didCacheResponse, @"%@", testHelper); + + // Now make request again. Should not pull from cache. + request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"http://example.com/test/path/other"]]; + request.cachePolicy = NSURLRequestUseProtocolCachePolicy; + [testHelper reset]; + [testHelper loadRequest:request]; + + XCTAssertTrue(testHelper.didLoadFromNetwork, @"%@", testHelper); + } +} + +- (void)testWithNoCacheSecondRequest_WithCacheableResponse_DoesNotUseCache +{ + for (NSArray *testParams in [self parameterizedTestInputs]) { + GET_TEST_PARAMS; + NSLog(@"- using %@, policy %tu, shouldPullFromCache %tu", [testHelper class], cachePolicy, shouldPullFromCache); + [self resetSharedCache]; + + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"http://example.com/test/path"]]; + request.cachePolicy = cachePolicy; + + [testHelper provideBasicCacheableResponse]; + [testHelper loadRequest:request]; + + // This request and response were cacheable. Verify. + XCTAssertTrue(testHelper.didLoadFromNetwork, @"%@", testHelper); + XCTAssertTrue(testHelper.didCacheResponse, @"%@", testHelper); + + // Now make request again, but request specifies a reload. + [request addValue:@"no-cache" forHTTPHeaderField:@"Cache-Control"]; + [testHelper reset]; + [testHelper loadRequest:request]; + XCTAssertTrue(testHelper.didLoadFromNetwork, @"%@", testHelper); + + [request addValue:@"max-age=0" forHTTPHeaderField:@"Cache-Control"]; + [testHelper reset]; + [testHelper loadRequest:request]; + XCTAssertTrue(testHelper.didLoadFromNetwork, @"%@", testHelper); + } +} + +- (void)testWithExpiredItem_DoesNotUseCache +{ + for (NSArray *testParams in [self parameterizedTestInputs]) { + GET_TEST_PARAMS; + NSLog(@"- using %@, policy %tu, shouldPullFromCache %tu", [testHelper class], cachePolicy, shouldPullFromCache); + [self resetSharedCache]; + + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"http://example.com/test/path"]]; + request.cachePolicy = cachePolicy; + + [testHelper provideResponseWithStatus:200 cacheControl:@"public, max-age=1" date:[NSDate dateWithTimeIntervalSinceNow:-2]]; + [testHelper loadRequest:request]; + + // This request and response were cacheable. Verify. + XCTAssertTrue(testHelper.didLoadFromNetwork, @"%@", testHelper); + XCTAssertTrue(testHelper.didCacheResponse, @"%@", testHelper); + + // Now make request again + [testHelper reset]; + [testHelper loadRequest:request]; + + XCTAssertTrue(testHelper.didLoadFromNetwork, @"%@", testHelper); + } +} + +- (void)testRequestWithAuthorization_DoesNotUseCache +{ + for (NSArray *testParams in [self parameterizedTestInputs]) { + GET_TEST_PARAMS; + NSLog(@"- using %@, policy %tu, shouldPullFromCache %tu", [testHelper class], cachePolicy, shouldPullFromCache); + [self resetSharedCache]; + + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"http://example.com/test/path"]]; + request.cachePolicy = cachePolicy; + [request addValue:@"foo" forHTTPHeaderField:@"Authorization"]; + + [testHelper provideBasicCacheableResponse]; + [testHelper loadRequest:request]; + + XCTAssertTrue(testHelper.didLoadFromNetwork, @"%@", testHelper); + XCTAssertFalse(testHelper.didCacheResponse, @"%@", testHelper); + } +} + +- (void)testResponseWithNoDate_DoesNotUseCache +{ + for (NSArray *testParams in [self parameterizedTestInputs]) { + GET_TEST_PARAMS; + NSLog(@"- using %@, policy %tu", [testHelper class], cachePolicy); + [self resetSharedCache]; + + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"http://example.com/test/path"]]; + request.cachePolicy = cachePolicy; + + [testHelper provideResponseWithStatus:200 cacheControl:@"public,max-age=1200" date:nil]; + [testHelper loadRequest:request]; + + XCTAssertTrue(testHelper.didLoadFromNetwork, @"%@", testHelper); + XCTAssertFalse(testHelper.didCacheResponse, @"%@", testHelper); + } +} + +- (void)testPOSTRequest_DoesNotUseCache +{ + for (NSArray *testParams in [self parameterizedTestInputs]) { + GET_TEST_PARAMS; + NSLog(@"- using %@, policy %tu", [testHelper class], cachePolicy); + + [self resetSharedCache]; + + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"http://example.com/test/path"]]; + request.HTTPMethod = @"POST"; + request.cachePolicy = cachePolicy; + + [testHelper provideBasicCacheableResponse]; + [testHelper loadRequest:request]; + + XCTAssertTrue(testHelper.didLoadFromNetwork, @"%@", testHelper); + XCTAssertFalse(testHelper.didCacheResponse, @"%@", testHelper); + } +} + +@end diff --git a/SPDYUnitTests/SPDYURLCacheTest.m b/SPDYUnitTests/SPDYURLCacheTest.m index a8cefec..4143b26 100644 --- a/SPDYUnitTests/SPDYURLCacheTest.m +++ b/SPDYUnitTests/SPDYURLCacheTest.m @@ -30,7 +30,7 @@ - (void)testCacheAllowedFor200WithNoHeader { NSURL *url = [[NSURL alloc] initWithString:@"http://example.com/test/path"]; NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url]; - NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:url statusCode:200 HTTPVersion:@"1.1" headerFields:@{}]; + NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:url statusCode:200 HTTPVersion:@"1.1" headerFields:@{@"Date":@"Wed, 25 Nov 2015 00:10:13 GMT"}]; NSURLCacheStoragePolicy policy = SPDYCacheStoragePolicy(request, response); XCTAssertEqual(policy, NSURLCacheStorageAllowed); @@ -40,7 +40,7 @@ - (void)testCacheAllowedFor404WithNoHeader { NSURL *url = [[NSURL alloc] initWithString:@"http://example.com/test/path"]; NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url]; - NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:url statusCode:404 HTTPVersion:@"1.1" headerFields:@{}]; + NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:url statusCode:404 HTTPVersion:@"1.1" headerFields:@{@"Date":@"Wed, 25 Nov 2015 00:10:13 GMT"}]; NSURLCacheStoragePolicy policy = SPDYCacheStoragePolicy(request, response); XCTAssertEqual(policy, NSURLCacheStorageAllowed); @@ -56,23 +56,23 @@ - (void)testCacheNotAllowedFor400WithNoHeader XCTAssertEqual(policy, NSURLCacheStorageNotAllowed); } -- (void)testCacheAllowedFor200WithNoStoreRequestHeader +- (void)testCacheNotAllowedFor200WithNoStoreRequestHeader { NSURL *url = [[NSURL alloc] initWithString:@"http://example.com/test/path"]; NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url]; [request addValue:@"no-store" forHTTPHeaderField:@"Cache-Control"]; - NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:url statusCode:200 HTTPVersion:@"1.1" headerFields:@{}]; + NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:url statusCode:200 HTTPVersion:@"1.1" headerFields:@{@"Date":@"Wed, 25 Nov 2015 00:10:13 GMT"}]; NSURLCacheStoragePolicy policy = SPDYCacheStoragePolicy(request, response); - XCTAssertEqual(policy, NSURLCacheStorageAllowed); + XCTAssertEqual(policy, NSURLCacheStorageNotAllowed); } - (void)testCacheNotAllowedFor200WithNoStoreNoCacheRequestHeader { NSURL *url = [[NSURL alloc] initWithString:@"http://example.com/test/path"]; NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url]; - [request addValue:@"no-store; no-cache" forHTTPHeaderField:@"Cache-Control"]; - NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:url statusCode:200 HTTPVersion:@"1.1" headerFields:@{}]; + [request addValue:@"no-store, no-cache" forHTTPHeaderField:@"Cache-Control"]; + NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:url statusCode:200 HTTPVersion:@"1.1" headerFields:@{@"Date":@"Wed, 25 Nov 2015 00:10:13 GMT"}]; NSURLCacheStoragePolicy policy = SPDYCacheStoragePolicy(request, response); XCTAssertEqual(policy, NSURLCacheStorageNotAllowed); @@ -82,7 +82,7 @@ - (void)testCacheAllowedFor200WithNoCacheResponseHeader { NSURL *url = [[NSURL alloc] initWithString:@"http://example.com/test/path"]; NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url]; - NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:url statusCode:200 HTTPVersion:@"1.1" headerFields:@{@"Cache-Control":@"no-cache"}]; + NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:url statusCode:200 HTTPVersion:@"1.1" headerFields:@{@"Cache-Control":@"no-cache",@"Date":@"Wed, 25 Nov 2015 00:10:13 GMT"}]; NSURLCacheStoragePolicy policy = SPDYCacheStoragePolicy(request, response); XCTAssertEqual(policy, NSURLCacheStorageAllowed); @@ -92,7 +92,7 @@ - (void)testCacheAllowedFor200WithNoStoreResponseHeader { NSURL *url = [[NSURL alloc] initWithString:@"http://example.com/test/path"]; NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url]; - NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:url statusCode:200 HTTPVersion:@"1.1" headerFields:@{@"Cache-Control":@"no-store"}]; + NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:url statusCode:200 HTTPVersion:@"1.1" headerFields:@{@"Cache-Control":@"no-store",@"Date":@"Wed, 25 Nov 2015 00:10:13 GMT"}]; NSURLCacheStoragePolicy policy = SPDYCacheStoragePolicy(request, response); XCTAssertEqual(policy, NSURLCacheStorageNotAllowed);