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

Add support for NSURLCache. #141

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,29 @@ Most existing SPDY implementations use a TLS extension called Next Protocol Impl
In order to aid with protocol inference, this SPDY implementation includes a non-standard settings id at index 0: `SETTINGS_MINOR_VERSION`. This is necessary to differentiate between SPDY/3 and SPDY/3.1 connections that were not negotiated with NPN, since only the major version is included in the frame header. Because not all servers may support this particular setting, sending it can be disabled at runtime through protocol configuration.

## Implementation Notes
### Caching with NSURLCache
CocoaSPDY partially supports HTTP caching using NSURLCaches, though there are a number of caveats. The default policy for `NSURLConnection` is to disable caching, in order to maintain backward-compatibility and mimize risk. CocoaSPDY does not perform HEAD validation requests, so any cached response that will require revalidation with the server will result in a full request and response.

**NSURLConnection**
* Ensure the global `[NSURLCache sharedCached]` is set to the desired `NSURLCache`
* Set the `NSMutableURLRequest.cachePolicy` to the desired policy (`NSURLRequestUseProtocolCachePolicy` will result in no caching)

**NSURLSession**
* Set the `NSMutableURLRequest.SPDYURLSession` to your NSURLSession instance
* Set the `NSURLSessionConfiguration.URLCache` to the desired `NSURLCache`
* Set the `NSURLSessionConfiguration.cachePolicy` to the desired policy (can be left as `NSURLRequestUseProtocolCachePolicy`)

**Request/response requirements**
* Request must not include an `Authorization` header.
* Request must be a GET.
* Response must include a `Date` header.
* Response must include a `Cache-Control` header with a "max-age" parameter set to a non-zero value. And when retrieving the cached response, this must not result in a stale item.
* The `Cache-Control` response header must not have "no-cache", "no-store", or "must-revalidate" in it.
* To prevent caching of a response, set `Cache-Control: no-store` in the request.
* For all the requirements, see implementation details at https://github.com/twitter/CocoaSPDY/blob/develop/SPDY/SPDYCacheStoragePolicy.m

Essentially, you must opt-in with `NSURLConnection` by setting a cache policy or with `NSURLSession` by configuring a cache. The response must be cacheable and have a max-age defined, and the request must be a GET request. The cached response must be usable without revalidation.

### CRIME attack
The [CRIME attack](http://en.wikipedia.org/wiki/CRIME) is a plaintext injection technique that exploits the fact that information can be inferred from compressed content length to potentially reveal the contents of an encrypted stream. This is a serious issue for browsers, which are subject to hijacks that may allow an attacker to issue an arbitrary number of requests with known plaintext header content and observe the resulting effect on compression.

Expand Down
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)) {
Copy link

Choose a reason for hiding this comment

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

strptime(3) doesn't initialize struct tm, should be clear before using it, or, some of value may be undefined.
strptime(3) using current system locale, which is unknown. must be strptime_l(3) ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

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]] : @"";
Copy link

Choose a reason for hiding this comment

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

This is not right parsing. by RFC definition, Cache-Control is defined like

Cache-Control = 1#cache-directive
cache-directive = token [ "=" ( token / quoted-string ) ]

and quoted-string can contains =. We must like concatenating pair[1] to last one with =.

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;
}
Copy link

Choose a reason for hiding this comment

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

Add a new line?

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