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

Commit 6224087

Browse files
committed
Better parsing of Cache-Control header.
Feedback from code review: - parse quoted strings - use strptime_l - zero tm struct for strptime_l - allow requests with Authorization to be cached
1 parent a758509 commit 6224087

File tree

4 files changed

+222
-24
lines changed

4 files changed

+222
-24
lines changed

SPDY/SPDYCacheStoragePolicy.m

Lines changed: 109 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
//
1313

1414
#import "SPDYCacheStoragePolicy.h"
15+
#include <time.h>
16+
#include <xlocale.h>
1517

1618
typedef struct _HTTPTimeFormatInfo {
1719
const char *readFormat;
@@ -31,12 +33,12 @@
3133
{
3234
NSDate *date = nil;
3335
if (string) {
34-
struct tm parsedTime;
3536
const char *utf8String = [string UTF8String];
3637

3738
for (int format = 0; (size_t)format < (sizeof(kTimeFormatInfos) / sizeof(kTimeFormatInfos[0])); format++) {
3839
HTTPTimeFormatInfo info = kTimeFormatInfos[format];
39-
if (info.readFormat != NULL && strptime(utf8String, info.readFormat, &parsedTime)) {
40+
struct tm parsedTime = { 0 };
41+
if (info.readFormat != NULL && strptime_l(utf8String, info.readFormat, &parsedTime, NULL)) {
4042
NSTimeInterval ti = (info.usesHasTimezoneInfo ? mktime(&parsedTime) : timegm(&parsedTime));
4143
date = [NSDate dateWithTimeIntervalSince1970:ti];
4244
if (date) {
@@ -49,18 +51,109 @@
4951
return date;
5052
}
5153

54+
NSString *GetKey(const char **ppStr) {
55+
const char *p = *ppStr;
56+
57+
// Advance to next delimiter
58+
while (*p != '\0' && *p != '=' && *p != ',') {
59+
p++;
60+
}
61+
62+
// No progress? Error.
63+
if (p == *ppStr) {
64+
return nil;
65+
}
66+
67+
NSString *str = [[NSString alloc] initWithBytes:*ppStr length:(p - *ppStr) encoding:NSUTF8StringEncoding];
68+
*ppStr = p;
69+
return [str stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
70+
}
71+
72+
NSString *GetValue(const char **ppStr) {
73+
// **ppStr, at input, should be null for EOS, "," for single token, or "=" for dual token.
74+
const char *p = *ppStr;
75+
76+
// Single token with no value, EOS
77+
if (*p == '\0') {
78+
return @"";
79+
}
80+
81+
// Single token with no value, more after
82+
if (*p == ',') {
83+
(*ppStr)++;
84+
return @"";
85+
}
86+
87+
// Must be token with value, error if not
88+
if (*p != '=') {
89+
return nil;
90+
}
91+
92+
// skip '='
93+
p++;
94+
(*ppStr)++;
95+
96+
// Value is either a quoted string or a token
97+
NSString *str;
98+
if (*p == '"') {
99+
p++; // skip opening quote
100+
101+
// Advance to delimiter
102+
while (*p != '\0' && *p != '"') {
103+
p++;
104+
}
105+
106+
// EOS before closing quote? Error.
107+
if (*p == '\0') {
108+
return nil;
109+
}
110+
111+
p++; // skip closing quote
112+
113+
// Don't trim whitespace from within quoted string
114+
str = [[NSString alloc] initWithBytes:(*ppStr + 1) length:(p - *ppStr - 2) encoding:NSUTF8StringEncoding];
115+
} else {
116+
// Advance to delimiter
117+
while (*p != '\0' && *p != ',') {
118+
p++;
119+
}
120+
121+
// No progress? Error.
122+
if (p == *ppStr) {
123+
return nil;
124+
}
125+
126+
str = [[[NSString alloc] initWithBytes:*ppStr length:(p - *ppStr) encoding:NSUTF8StringEncoding] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
127+
}
128+
129+
// Skip trailing whitespace and trailing token delimiter
130+
while (*p != '\0' && (*p == ' ' || *p == ',')) {
131+
p++;
132+
}
133+
134+
*ppStr = p;
135+
return str;
136+
}
137+
52138
NSDictionary *HTTPCacheControlParameters(NSString *cacheControl)
53139
{
54140
if (cacheControl.length == 0) {
55141
return nil;
56142
}
57143

58-
NSArray *components = [cacheControl componentsSeparatedByString:@","];
59-
NSMutableDictionary *parameters = [NSMutableDictionary dictionaryWithCapacity:components.count];
60-
for (NSString *component in components) {
61-
NSArray *pair = [component componentsSeparatedByString:@"="];
62-
NSString *key = [pair[0] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
63-
NSString *value = pair.count == 2 ? [pair[1] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] : @"";
144+
const char *pStr = [cacheControl cStringUsingEncoding:NSUTF8StringEncoding];
145+
146+
NSMutableDictionary *parameters = [[NSMutableDictionary alloc] init];
147+
148+
while (YES) {
149+
NSString *key = GetKey(&pStr);
150+
if (key.length == 0) {
151+
break;
152+
}
153+
NSString *value = GetValue(&pStr);
154+
if (value == nil) {
155+
break;
156+
}
64157
parameters[key] = value;
65158
}
66159
return parameters;
@@ -125,14 +218,13 @@ extern NSURLCacheStoragePolicy SPDYCacheStoragePolicy(NSURLRequest *request, NSH
125218
}
126219

127220
// If we still think it might be cacheable, look at the "Cache-Control" header in
128-
// the request. Also rule out requests with Authorization in them.
221+
// the request.
129222

130223
if (cacheable) {
131224
NSString *requestHeader;
132225

133226
requestHeader = [[request valueForHTTPHeaderField:@"cache-control"] lowercaseString];
134-
if ((requestHeader != nil && [requestHeader rangeOfString:@"no-store"].location != NSNotFound) ||
135-
[request valueForHTTPHeaderField:@"authorization"].length > 0) {
227+
if (requestHeader != nil && [requestHeader rangeOfString:@"no-store"].location != NSNotFound) {
136228
cacheable = NO;
137229
}
138230
}
@@ -207,14 +299,18 @@ extern SPDYCachedResponseState SPDYCacheLoadingPolicy(NSURLRequest *request, NSC
207299
}
208300
}
209301

210-
// Note: there's a lot more validation we should do, to be a well-behaving user agent.
302+
// Note: there's a lot more validation we should do, to be a well-behaving user agent. RFC7234
303+
// should be consulted.
211304
// We don't support Pragma header.
212305
// We don't support Expires header.
213306
// We don't support Vary header.
214307
// We don't support ETag response header or If-None-Match request header.
215308
// We don't support Last-Modified response header or If-Modified-Since request header.
216309
// We don't look at more of the Cache-Control parameters, including ones that specify a field name.
310+
// We need to generate the Age header in the cached response.
311+
// We need to invalidate the cached item if PUT,POST,DELETE request gets a successful response.
312+
// - including the item in Location header.
217313
// ...
218314

219315
return SPDYCachedResponseStateValid;
220-
}
316+
}

SPDYUnitTests/SPDYMockSessionManager.m

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,6 @@ + (SPDYMockSessionManager *)shared
4848
return instance;
4949
}
5050

51-
- (instancetype)init
52-
{
53-
self = [super init];
54-
if (self != nil) {
55-
}
56-
return self;
57-
}
58-
5951
#pragma mark SPDYSessionManager methods
6052

6153
- (SPDYPushStreamManager *)pushStreamManager

SPDYUnitTests/SPDYNSURLCachingTest.m

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,10 @@ - (void)testCacheableResponse_DoesInsertAndLoadFromCache
9898
XCTAssertTrue(testHelper.didLoadFromNetwork, @"%@", testHelper);
9999
XCTAssertTrue(testHelper.didCacheResponse, @"%@", testHelper);
100100

101-
// Now make request again. Should pull from cache.
101+
// Now make request again. Should pull from cache. Create a new request object just in case.
102+
request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"http://example.com/test/path"]];
103+
request.cachePolicy = cachePolicy;
104+
102105
[testHelper reset];
103106
[testHelper loadRequest:request];
104107

@@ -424,7 +427,7 @@ - (void)testWithExpiredItem_DoesNotUseCache
424427
}
425428
}
426429

427-
- (void)testRequestWithAuthorization_DoesNotUseCache
430+
- (void)testRequestWithAuthorization_DoesUseCache
428431
{
429432
for (NSArray *testParams in [self parameterizedTestInputs]) {
430433
GET_TEST_PARAMS;
@@ -439,7 +442,40 @@ - (void)testRequestWithAuthorization_DoesNotUseCache
439442
[testHelper loadRequest:request];
440443

441444
XCTAssertTrue(testHelper.didLoadFromNetwork, @"%@", testHelper);
442-
XCTAssertFalse(testHelper.didCacheResponse, @"%@", testHelper);
445+
XCTAssertTrue(testHelper.didCacheResponse, @"%@", testHelper);
446+
447+
// Now make request again. Should pull from cache.
448+
[testHelper reset];
449+
[testHelper loadRequest:request];
450+
451+
XCTAssertEqual(testHelper.didLoadFromCache, shouldPullFromCache, @"%@", testHelper);
452+
}
453+
}
454+
455+
- (void)disabled_testRequestWithAuthorization_DoesNotUseCache_WhenHeaderChanges
456+
{
457+
for (NSArray *testParams in [self parameterizedTestInputs]) {
458+
GET_TEST_PARAMS;
459+
460+
NSLog(@"- using %@, policy %tu, shouldPullFromCache %tu", [testHelper class], cachePolicy, NO);
461+
[self resetSharedCache];
462+
463+
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"http://example.com/test/path"]];
464+
request.cachePolicy = cachePolicy;
465+
[request addValue:@"foo" forHTTPHeaderField:@"Authorization"];
466+
467+
[testHelper provideBasicCacheableResponse];
468+
[testHelper loadRequest:request];
469+
470+
XCTAssertTrue(testHelper.didLoadFromNetwork, @"%@", testHelper);
471+
XCTAssertTrue(testHelper.didCacheResponse, @"%@", testHelper);
472+
473+
// Now make request again. Should not pull from cache.
474+
[testHelper reset];
475+
[request setValue:@"bar" forHTTPHeaderField:@"Authorization"];
476+
[testHelper loadRequest:request];
477+
478+
XCTAssertEqual(testHelper.didLoadFromCache, NO, @"%@", testHelper);
443479
}
444480
}
445481

SPDYUnitTests/SPDYURLCacheTest.m

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
#import <XCTest/XCTest.h>
1313
#import "SPDYCacheStoragePolicy.h"
1414

15+
// Access to private function
16+
NSDictionary *HTTPCacheControlParameters(NSString *cacheControl);
17+
1518
@interface SPDYURLCacheTest : XCTestCase
1619
@end
1720

@@ -98,4 +101,75 @@ - (void)testCacheAllowedFor200WithNoStoreResponseHeader
98101
XCTAssertEqual(policy, NSURLCacheStorageNotAllowed);
99102
}
100103

104+
#pragma mark HTTP Cache-Control parsing tests
105+
106+
- (void)testOneTokenWithoutValue
107+
{
108+
NSDictionary *params = HTTPCacheControlParameters(@"no-cache");
109+
XCTAssertEqual(params.count, 1ul);
110+
XCTAssertEqualObjects(params[@"no-cache"], @"");
111+
}
112+
113+
- (void)testTwoTokensWithoutValues
114+
{
115+
NSDictionary *params = HTTPCacheControlParameters(@"no-cache,no-store");
116+
XCTAssertEqual(params.count, 2ul);
117+
XCTAssertEqualObjects(params[@"no-cache"], @"");
118+
XCTAssertEqualObjects(params[@"no-store"], @"");
119+
}
120+
121+
- (void)testTwoTokensWithoutValuesWithSpaces
122+
{
123+
NSDictionary *params = HTTPCacheControlParameters(@" no-cache, no-store ");
124+
XCTAssertEqual(params.count, 2ul);
125+
XCTAssertEqualObjects(params[@"no-cache"], @"");
126+
XCTAssertEqualObjects(params[@"no-store"], @"");
127+
}
128+
129+
- (void)testOneTokenWithValue
130+
{
131+
NSDictionary *params = HTTPCacheControlParameters(@"max-age=5");
132+
XCTAssertEqual(params.count, 1ul);
133+
XCTAssertEqualObjects(params[@"max-age"], @"5");
134+
}
135+
136+
- (void)testTwoTokensWithValues
137+
{
138+
NSDictionary *params = HTTPCacheControlParameters(@"max-age=5,s-maxage=6");
139+
XCTAssertEqual(params.count, 2ul);
140+
XCTAssertEqualObjects(params[@"max-age"], @"5");
141+
XCTAssertEqualObjects(params[@"s-maxage"], @"6");
142+
}
143+
144+
- (void)testTwoTokensWithValuesWithSpaces
145+
{
146+
NSDictionary *params = HTTPCacheControlParameters(@" max-age = 5, s-maxage= 6 ");
147+
XCTAssertEqual(params.count, 2ul);
148+
XCTAssertEqualObjects(params[@"max-age"], @"5");
149+
XCTAssertEqualObjects(params[@"s-maxage"], @"6");
150+
}
151+
152+
- (void)testOneTokenWithQuotedValue
153+
{
154+
NSDictionary *params = HTTPCacheControlParameters(@"vary=\"foo\"");
155+
XCTAssertEqual(params.count, 1ul);
156+
XCTAssertEqualObjects(params[@"vary"], @"foo");
157+
}
158+
159+
- (void)testTwoTokensWithQuotedValues
160+
{
161+
NSDictionary *params = HTTPCacheControlParameters(@"extension=\"foo=bar\",vary=\"foo\"");
162+
XCTAssertEqual(params.count, 2ul);
163+
XCTAssertEqualObjects(params[@"extension"], @"foo=bar");
164+
XCTAssertEqualObjects(params[@"vary"], @"foo");
165+
}
166+
167+
- (void)testTwoTokensWithQuotedValuesWithSpaces
168+
{
169+
NSDictionary *params = HTTPCacheControlParameters(@" extension=\" foo = bar, baz \" , vary=\"foo\" ");
170+
XCTAssertEqual(params.count, 2ul);
171+
XCTAssertEqualObjects(params[@"extension"], @" foo = bar, baz ");
172+
XCTAssertEqualObjects(params[@"vary"], @"foo");
173+
}
174+
101175
@end

0 commit comments

Comments
 (0)