Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100755 591 lines (470 sloc) 17.13 kB
d2d4ed0 Full Support for Google Reader Sync!
Salvatore Ansani authored
1 /* Copyright (c) 2010 Google Inc.
2 *
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16 //
17 // GTMHTTPFetchHistory.m
18 //
19
20 #define GTMHTTPFETCHHISTORY_DEFINE_GLOBALS 1
21
22 #import "GTMHTTPFetchHistory.h"
23
24 const NSTimeInterval kCachedURLReservationInterval = 60.0; // 1 minute
25 static NSString* const kGTMIfNoneMatchHeader = @"If-None-Match";
26 static NSString* const kGTMETagHeader = @"Etag";
27
28 @implementation GTMCookieStorage
29
30 - (id)init {
31 self = [super init];
32 if (self != nil) {
33 cookies_ = [[NSMutableArray alloc] init];
34 }
35 return self;
36 }
37
38 - (void)dealloc {
39 [cookies_ release];
40 [super dealloc];
41 }
42
43 // add all cookies in the new cookie array to the storage,
44 // replacing stored cookies as appropriate
45 //
46 // Side effect: removes expired cookies from the storage array
47 - (void)setCookies:(NSArray *)newCookies {
48
49 @synchronized(cookies_) {
50 [self removeExpiredCookies];
51
52 for (NSHTTPCookie *newCookie in newCookies) {
53 if ([[newCookie name] length] > 0
54 && [[newCookie domain] length] > 0
55 && [[newCookie path] length] > 0) {
56
57 // remove the cookie if it's currently in the array
58 NSHTTPCookie *oldCookie = [self cookieMatchingCookie:newCookie];
59 if (oldCookie) {
60 [cookies_ removeObjectIdenticalTo:oldCookie];
61 }
62
63 // make sure the cookie hasn't already expired
64 NSDate *expiresDate = [newCookie expiresDate];
65 if ((!expiresDate) || [expiresDate timeIntervalSinceNow] > 0) {
66 [cookies_ addObject:newCookie];
67 }
68
69 } else {
70 NSAssert1(NO, @"Cookie incomplete: %@", newCookie);
71 }
72 }
73 }
74 }
75
76 - (void)deleteCookie:(NSHTTPCookie *)cookie {
77 @synchronized(cookies_) {
78 NSHTTPCookie *foundCookie = [self cookieMatchingCookie:cookie];
79 if (foundCookie) {
80 [cookies_ removeObjectIdenticalTo:foundCookie];
81 }
82 }
83 }
84
85 // retrieve all cookies appropriate for the given URL, considering
86 // domain, path, cookie name, expiration, security setting.
87 // Side effect: removed expired cookies from the storage array
88 - (NSArray *)cookiesForURL:(NSURL *)theURL {
89
90 NSMutableArray *foundCookies = nil;
91
92 @synchronized(cookies_) {
93 [self removeExpiredCookies];
94
95 // we'll prepend "." to the desired domain, since we want the
96 // actual domain "nytimes.com" to still match the cookie domain
97 // ".nytimes.com" when we check it below with hasSuffix
98 NSString *host = [[theURL host] lowercaseString];
99 NSString *path = [theURL path];
100 NSString *scheme = [theURL scheme];
101
102 NSString *domain = nil;
103 BOOL isLocalhostRetrieval = NO;
104
105 if ([host isEqual:@"localhost"]) {
106 isLocalhostRetrieval = YES;
107 } else {
108 if (host) {
109 domain = [@"." stringByAppendingString:host];
110 }
111 }
112
113 NSUInteger numberOfCookies = [cookies_ count];
114 for (NSUInteger idx = 0; idx < numberOfCookies; idx++) {
115
116 NSHTTPCookie *storedCookie = [cookies_ objectAtIndex:idx];
117
118 NSString *cookieDomain = [[storedCookie domain] lowercaseString];
119 NSString *cookiePath = [storedCookie path];
120 BOOL cookieIsSecure = [storedCookie isSecure];
121
122 BOOL isDomainOK;
123
124 if (isLocalhostRetrieval) {
125 // prior to 10.5.6, the domain stored into NSHTTPCookies for localhost
126 // is "localhost.local"
127 isDomainOK = [cookieDomain isEqual:@"localhost"]
128 || [cookieDomain isEqual:@"localhost.local"];
129 } else {
130 isDomainOK = [domain hasSuffix:cookieDomain];
131 }
132
133 BOOL isPathOK = [cookiePath isEqual:@"/"] || [path hasPrefix:cookiePath];
134 BOOL isSecureOK = (!cookieIsSecure) || [scheme isEqual:@"https"];
135
136 if (isDomainOK && isPathOK && isSecureOK) {
137 if (foundCookies == nil) {
138 foundCookies = [NSMutableArray arrayWithCapacity:1];
139 }
140 [foundCookies addObject:storedCookie];
141 }
142 }
143 }
144 return foundCookies;
145 }
146
147 // return a cookie from the array with the same name, domain, and path as the
148 // given cookie, or else return nil if none found
149 //
150 // Both the cookie being tested and all cookies in the storage array should
151 // be valid (non-nil name, domains, paths)
152 //
153 // note: this should only be called from inside a @synchronized(cookies_) block
154 - (NSHTTPCookie *)cookieMatchingCookie:(NSHTTPCookie *)cookie {
155
156 NSUInteger numberOfCookies = [cookies_ count];
157 NSString *name = [cookie name];
158 NSString *domain = [cookie domain];
159 NSString *path = [cookie path];
160
161 NSAssert3(name && domain && path, @"Invalid cookie (name:%@ domain:%@ path:%@)",
162 name, domain, path);
163
164 for (NSUInteger idx = 0; idx < numberOfCookies; idx++) {
165
166 NSHTTPCookie *storedCookie = [cookies_ objectAtIndex:idx];
167
168 if ([[storedCookie name] isEqual:name]
169 && [[storedCookie domain] isEqual:domain]
170 && [[storedCookie path] isEqual:path]) {
171
172 return storedCookie;
173 }
174 }
175 return nil;
176 }
177
178
179 // internal routine to remove any expired cookies from the array, excluding
180 // cookies with nil expirations
181 //
182 // note: this should only be called from inside a @synchronized(cookies_) block
183 - (void)removeExpiredCookies {
184
185 // count backwards since we're deleting items from the array
186 for (NSInteger idx = [cookies_ count] - 1; idx >= 0; idx--) {
187
188 NSHTTPCookie *storedCookie = [cookies_ objectAtIndex:idx];
189
190 NSDate *expiresDate = [storedCookie expiresDate];
191 if (expiresDate && [expiresDate timeIntervalSinceNow] < 0) {
192 [cookies_ removeObjectAtIndex:idx];
193 }
194 }
195 }
196
197 - (void)removeAllCookies {
198 @synchronized(cookies_) {
199 [cookies_ removeAllObjects];
200 }
201 }
202 @end
203
204 //
205 // GTMCachedURLResponse
206 //
207
208 @implementation GTMCachedURLResponse
209
210 @synthesize response = response_;
211 @synthesize data = data_;
212 @synthesize reservationDate = reservationDate_;
213 @synthesize useDate = useDate_;
214
215 - (id)initWithResponse:(NSURLResponse *)response data:(NSData *)data {
216 self = [super init];
217 if (self != nil) {
218 response_ = [response retain];
219 data_ = [data retain];
220 useDate_ = [[NSDate alloc] init];
221 }
222 return self;
223 }
224
225 - (void)dealloc {
226 [response_ release];
227 [data_ release];
228 [useDate_ release];
229 [reservationDate_ release];
230 [super dealloc];
231 }
232
233 - (NSString *)description {
234 NSString *reservationStr = reservationDate_ ?
235 [NSString stringWithFormat:@" resDate:%@", reservationDate_] : @"";
236
237 return [NSString stringWithFormat:@"%@ %p: {bytes:%@ useDate:%@%@}",
238 [self class], self,
239 data_ ? [NSNumber numberWithInt:(int)[data_ length]] : nil,
240 useDate_,
75c7351 @barijaona Apply recent changes to Google's gtm-oauth2 and gtm-http-request
barijaona authored
241 reservationStr];
d2d4ed0 Full Support for Google Reader Sync!
Salvatore Ansani authored
242 }
243
244 - (NSComparisonResult)compareUseDate:(GTMCachedURLResponse *)other {
245 return [useDate_ compare:[other useDate]];
246 }
247
248 @end
249
250 //
251 // GTMURLCache
252 //
253
254 @implementation GTMURLCache
255
256 @dynamic memoryCapacity;
257
258 - (id)init {
259 return [self initWithMemoryCapacity:kGTMDefaultETaggedDataCacheMemoryCapacity];
260 }
261
262 - (id)initWithMemoryCapacity:(NSUInteger)totalBytes {
263 self = [super init];
264 if (self != nil) {
265 memoryCapacity_ = totalBytes;
266
267 responses_ = [[NSMutableDictionary alloc] initWithCapacity:5];
268
269 reservationInterval_ = kCachedURLReservationInterval;
270 }
271 return self;
272 }
273
274 - (void)dealloc {
275 [responses_ release];
276 [super dealloc];
277 }
278
279 - (NSString *)description {
280 return [NSString stringWithFormat:@"%@ %p: {responses:%@}",
281 [self class], self, [responses_ allValues]];
282 }
283
284 // setters/getters
285
286 - (void)pruneCacheResponses {
287 // internal routine to remove the least-recently-used responses when the
288 // cache has grown too large
289 if (memoryCapacity_ >= totalDataSize_) return;
290
291 // sort keys by date
292 SEL sel = @selector(compareUseDate:);
293 NSArray *sortedKeys = [responses_ keysSortedByValueUsingSelector:sel];
294
295 // the least-recently-used keys are at the beginning of the sorted array;
296 // remove those (except ones still reserved) until the total data size is
297 // reduced sufficiently
298 for (NSURL *key in sortedKeys) {
299 GTMCachedURLResponse *response = [responses_ objectForKey:key];
300
301 NSDate *resDate = [response reservationDate];
302 BOOL isResponseReserved = (resDate != nil)
303 && ([resDate timeIntervalSinceNow] > -reservationInterval_);
304
305 if (!isResponseReserved) {
306 // we can remove this response from the cache
307 NSUInteger storedSize = [[response data] length];
308 totalDataSize_ -= storedSize;
309 [responses_ removeObjectForKey:key];
310 }
311
312 // if we've removed enough response data, then we're done
313 if (memoryCapacity_ >= totalDataSize_) break;
314 }
315 }
316
317 - (void)storeCachedResponse:(GTMCachedURLResponse *)cachedResponse
318 forRequest:(NSURLRequest *)request {
319 @synchronized(self) {
320 // remove any previous entry for this request
321 [self removeCachedResponseForRequest:request];
322
323 // cache this one only if it's not bigger than our cache
324 NSUInteger storedSize = [[cachedResponse data] length];
325 if (storedSize < memoryCapacity_) {
326
327 NSURL *key = [request URL];
328 [responses_ setObject:cachedResponse forKey:key];
329 totalDataSize_ += storedSize;
330
331 [self pruneCacheResponses];
332 }
333 }
334 }
335
336 - (GTMCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request {
337 GTMCachedURLResponse *response;
338
339 @synchronized(self) {
340 NSURL *key = [request URL];
341 response = [[[responses_ objectForKey:key] retain] autorelease];
342
343 // touch the date to indicate this was recently retrieved
344 [response setUseDate:[NSDate date]];
345 }
346 return response;
347 }
348
349 - (void)removeCachedResponseForRequest:(NSURLRequest *)request {
350 @synchronized(self) {
351 NSURL *key = [request URL];
352 totalDataSize_ -= [[[responses_ objectForKey:key] data] length];
353 [responses_ removeObjectForKey:key];
354 }
355 }
356
357 - (void)removeAllCachedResponses {
358 @synchronized(self) {
359 [responses_ removeAllObjects];
360 totalDataSize_ = 0;
361 }
362 }
363
364 - (NSUInteger)memoryCapacity {
365 return memoryCapacity_;
366 }
367
368 - (void)setMemoryCapacity:(NSUInteger)totalBytes {
369 @synchronized(self) {
370 BOOL didShrink = (totalBytes < memoryCapacity_);
371 memoryCapacity_ = totalBytes;
372
373 if (didShrink) {
374 [self pruneCacheResponses];
375 }
376 }
377 }
378
379 // methods for unit testing
380 - (void)setReservationInterval:(NSTimeInterval)secs {
381 reservationInterval_ = secs;
382 }
383
384 - (NSDictionary *)responses {
385 return responses_;
386 }
387
388 - (NSUInteger)totalDataSize {
389 return totalDataSize_;
390 }
391
392 @end
393
394 //
395 // GTMHTTPFetchHistory
396 //
397
398 @interface GTMHTTPFetchHistory ()
399 - (NSString *)cachedETagForRequest:(NSURLRequest *)request;
400 - (void)removeCachedDataForRequest:(NSURLRequest *)request;
401 @end
402
403 @implementation GTMHTTPFetchHistory
404
405 @synthesize cookieStorage = cookieStorage_;
406
407 @dynamic shouldRememberETags;
408 @dynamic shouldCacheETaggedData;
409 @dynamic memoryCapacity;
410
411 - (id)init {
412 return [self initWithMemoryCapacity:kGTMDefaultETaggedDataCacheMemoryCapacity
413 shouldCacheETaggedData:NO];
414 }
415
416 - (id)initWithMemoryCapacity:(NSUInteger)totalBytes
417 shouldCacheETaggedData:(BOOL)shouldCacheETaggedData {
418 self = [super init];
419 if (self != nil) {
420 etaggedDataCache_ = [[GTMURLCache alloc] initWithMemoryCapacity:totalBytes];
421 shouldRememberETags_ = shouldCacheETaggedData;
422 shouldCacheETaggedData_ = shouldCacheETaggedData;
423 cookieStorage_ = [[GTMCookieStorage alloc] init];
424 }
425 return self;
426 }
427
428 - (void)dealloc {
429 [etaggedDataCache_ release];
430 [cookieStorage_ release];
431 [super dealloc];
432 }
433
434 - (void)updateRequest:(NSMutableURLRequest *)request isHTTPGet:(BOOL)isHTTPGet {
435 if ([self shouldRememberETags]) {
436 // If this URL is in the history, and no ETag has been set, then
437 // set the ETag header field
438
439 // if we have a history, we're tracking across fetches, so we don't
440 // want to pull results from any other cache
441 [request setCachePolicy:NSURLRequestReloadIgnoringCacheData];
442
443 if (isHTTPGet) {
444 // we'll only add an ETag if there's no ETag specified in the user's
445 // request
446 NSString *specifiedETag = [request valueForHTTPHeaderField:kGTMIfNoneMatchHeader];
447 if (specifiedETag == nil) {
448 // no ETag: extract the previous ETag for this request from the
449 // fetch history, and add it to the request
450 NSString *cachedETag = [self cachedETagForRequest:request];
451
452 if (cachedETag != nil) {
453 [request addValue:cachedETag forHTTPHeaderField:kGTMIfNoneMatchHeader];
454 }
455 } else {
456 // has an ETag: remove any stored response in the fetch history
457 // for this request, as the If-None-Match header could lead to
458 // a 304 Not Modified, and we want that error delivered to the
459 // user since they explicitly specified the ETag
460 [self removeCachedDataForRequest:request];
461 }
462 }
463 }
464 }
465
466 - (void)updateFetchHistoryWithRequest:(NSURLRequest *)request
467 response:(NSURLResponse *)response
468 downloadedData:(NSData *)downloadedData {
469 if (![self shouldRememberETags]) return;
470
471 if (![response respondsToSelector:@selector(allHeaderFields)]) return;
472
473 NSInteger statusCode = [(NSHTTPURLResponse *)response statusCode];
474
475 if (statusCode != kGTMHTTPFetcherStatusNotModified) {
476 // save this ETag string for successful results (<300)
477 // If there's no last modified string, clear the dictionary
478 // entry for this URL. Also cache or delete the data, if appropriate
479 // (when etaggedDataCache is non-nil.)
480 NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields];
481 NSString* etag = [headers objectForKey:kGTMETagHeader];
482
483 if (etag != nil && statusCode < 300) {
484
485 // we want to cache responses for the headers, even if the client
486 // doesn't want the response body data caches
487 NSData *dataToStore = shouldCacheETaggedData_ ? downloadedData : nil;
488
489 GTMCachedURLResponse *cachedResponse;
490 cachedResponse = [[[GTMCachedURLResponse alloc] initWithResponse:response
491 data:dataToStore] autorelease];
492 [etaggedDataCache_ storeCachedResponse:cachedResponse
493 forRequest:request];
494 } else {
495 [etaggedDataCache_ removeCachedResponseForRequest:request];
496 }
497 }
498 }
499
500 - (NSString *)cachedETagForRequest:(NSURLRequest *)request {
501 GTMCachedURLResponse *cachedResponse;
502 cachedResponse = [etaggedDataCache_ cachedResponseForRequest:request];
503
504 NSURLResponse *response = [cachedResponse response];
505 NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields];
506 NSString *cachedETag = [headers objectForKey:kGTMETagHeader];
507 if (cachedETag) {
508 // since the request having an ETag implies this request is about
509 // to be fetched again, reserve the cached response to ensure that
510 // that it will be around at least until the fetch completes
511 //
512 // when the fetch completes, either the cached response will be replaced
513 // with a new response, or the cachedDataForRequest: method below will
514 // clear the reservation
515 [cachedResponse setReservationDate:[NSDate date]];
516 }
517 return cachedETag;
518 }
519
520 - (NSData *)cachedDataForRequest:(NSURLRequest *)request {
521 GTMCachedURLResponse *cachedResponse;
522 cachedResponse = [etaggedDataCache_ cachedResponseForRequest:request];
523
524 NSData *cachedData = [cachedResponse data];
525
526 // since the data for this cached request is being obtained from the cache,
527 // we can clear the reservation as the fetch has completed
528 [cachedResponse setReservationDate:nil];
529
530 return cachedData;
531 }
532
533 - (void)removeCachedDataForRequest:(NSURLRequest *)request {
534 [etaggedDataCache_ removeCachedResponseForRequest:request];
535 }
536
537 - (void)clearETaggedDataCache {
538 [etaggedDataCache_ removeAllCachedResponses];
539 }
540
541 - (void)clearHistory {
542 [self clearETaggedDataCache];
543 [cookieStorage_ removeAllCookies];
544 }
545
546 - (void)removeAllCookies {
547 [cookieStorage_ removeAllCookies];
548 }
549
550 - (BOOL)shouldRememberETags {
551 return shouldRememberETags_;
552 }
553
554 - (void)setShouldRememberETags:(BOOL)flag {
555 BOOL wasRemembering = shouldRememberETags_;
556 shouldRememberETags_ = flag;
557
558 if (wasRemembering && !flag) {
559 // free up the cache memory
560 [self clearETaggedDataCache];
561 }
562 }
563
564 - (BOOL)shouldCacheETaggedData {
565 return shouldCacheETaggedData_;
566 }
567
568 - (void)setShouldCacheETaggedData:(BOOL)flag {
569 BOOL wasCaching = shouldCacheETaggedData_;
570 shouldCacheETaggedData_ = flag;
571
572 if (flag) {
573 self.shouldRememberETags = YES;
574 }
575
576 if (wasCaching && !flag) {
577 // users expect turning off caching to free up the cache memory
578 [self clearETaggedDataCache];
579 }
580 }
581
582 - (NSUInteger)memoryCapacity {
583 return [etaggedDataCache_ memoryCapacity];
584 }
585
586 - (void)setMemoryCapacity:(NSUInteger)totalBytes {
587 [etaggedDataCache_ setMemoryCapacity:totalBytes];
588 }
589
590 @end
Something went wrong with that request. Please try again.