1/* 2 * 3 * Copyright 2019 gRPC authors. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 * 17 */ 18 19#import "CacheInterceptor.h" 20 21@implementation RequestCacheEntry { 22 @protected 23 NSString *_path; 24 id<NSObject> _message; 25} 26 27@synthesize path = _path; 28@synthesize message = _message; 29 30- (instancetype)initWithPath:(NSString *)path message:(id)message { 31 if ((self = [super init])) { 32 _path = [path copy]; 33 _message = [message copy]; 34 } 35 return self; 36} 37 38- (id)copyWithZone:(NSZone *)zone { 39 return [[RequestCacheEntry allocWithZone:zone] initWithPath:_path message:_message]; 40} 41 42- (BOOL)isEqual:(id)object { 43 if ([self class] != [object class]) return NO; 44 RequestCacheEntry *rhs = (RequestCacheEntry *)object; 45 return ([_path isEqualToString:rhs.path] && [_message isEqual:rhs.message]); 46} 47 48- (NSUInteger)hash { 49 return _path.hash ^ _message.hash; 50} 51 52@end 53 54@implementation MutableRequestCacheEntry 55 56@dynamic path; 57@dynamic message; 58 59- (void)setPath:(NSString *)path { 60 _path = [path copy]; 61} 62 63- (void)setMessage:(id)message { 64 _message = [message copy]; 65} 66 67@end 68 69@implementation ResponseCacheEntry { 70 @protected 71 NSDate *_deadline; 72 NSDictionary *_headers; 73 id _message; 74 NSDictionary *_trailers; 75} 76 77@synthesize deadline = _deadline; 78@synthesize headers = _headers; 79@synthesize message = _message; 80@synthesize trailers = _trailers; 81 82- (instancetype)initWithDeadline:(NSDate *)deadline 83 headers:(NSDictionary *)headers 84 message:(id)message 85 trailers:(NSDictionary *)trailers { 86 if (([super init])) { 87 _deadline = [deadline copy]; 88 _headers = [[NSDictionary alloc] initWithDictionary:headers copyItems:YES]; 89 _message = [message copy]; 90 _trailers = [[NSDictionary alloc] initWithDictionary:trailers copyItems:YES]; 91 } 92 return self; 93} 94 95- (id)copyWithZone:(NSZone *)zone { 96 return [[ResponseCacheEntry allocWithZone:zone] initWithDeadline:_deadline 97 headers:_headers 98 message:_message 99 trailers:_trailers]; 100} 101 102@end 103 104@implementation MutableResponseCacheEntry 105 106@dynamic deadline; 107@dynamic headers; 108@dynamic message; 109@dynamic trailers; 110 111- (void)setDeadline:(NSDate *)deadline { 112 _deadline = [deadline copy]; 113} 114 115- (void)setHeaders:(NSDictionary *)headers { 116 _headers = [[NSDictionary alloc] initWithDictionary:headers copyItems:YES]; 117} 118 119- (void)setMessage:(id)message { 120 _message = [message copy]; 121} 122 123- (void)setTrailers:(NSDictionary *)trailers { 124 _trailers = [[NSDictionary alloc] initWithDictionary:trailers copyItems:YES]; 125} 126 127@end 128 129@implementation CacheContext { 130 NSCache<RequestCacheEntry *, ResponseCacheEntry *> *_cache; 131} 132 133- (instancetype)init { 134 if ((self = [super init])) { 135 _cache = [[NSCache alloc] init]; 136 } 137 return self; 138} 139 140- (GRPCInterceptor *)createInterceptorWithManager:(GRPCInterceptorManager *)interceptorManager { 141 return [[CacheInterceptor alloc] initWithInterceptorManager:interceptorManager cacheContext:self]; 142} 143 144- (ResponseCacheEntry *)getCachedResponseForRequest:(RequestCacheEntry *)request { 145 ResponseCacheEntry *response = nil; 146 @synchronized(self) { 147 response = [_cache objectForKey:request]; 148 if ([response.deadline timeIntervalSinceNow] < 0) { 149 [_cache removeObjectForKey:request]; 150 response = nil; 151 } 152 } 153 return response; 154} 155 156- (void)setCachedResponse:(ResponseCacheEntry *)response forRequest:(RequestCacheEntry *)request { 157 @synchronized(self) { 158 [_cache setObject:response forKey:request]; 159 } 160} 161 162@end 163 164@implementation CacheInterceptor { 165 GRPCInterceptorManager *_manager; 166 CacheContext *_context; 167 dispatch_queue_t _dispatchQueue; 168 169 BOOL _cacheable; 170 BOOL _writeMessageSeen; 171 BOOL _readMessageSeen; 172 GRPCCallOptions *_callOptions; 173 GRPCRequestOptions *_requestOptions; 174 id _requestMessage; 175 MutableRequestCacheEntry *_request; 176 MutableResponseCacheEntry *_response; 177} 178 179- (dispatch_queue_t)requestDispatchQueue { 180 return _dispatchQueue; 181} 182 183- (dispatch_queue_t)dispatchQueue { 184 return _dispatchQueue; 185} 186 187- (instancetype)initWithInterceptorManager:(GRPCInterceptorManager *_Nonnull)intercepterManager 188 cacheContext:(CacheContext *_Nonnull)cacheContext { 189 if ((self = [super initWithInterceptorManager:intercepterManager 190 requestDispatchQueue:dispatch_get_main_queue() 191 responseDispatchQueue:dispatch_get_main_queue()])) { 192 _manager = intercepterManager; 193 _context = cacheContext; 194 _dispatchQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL); 195 196 _cacheable = YES; 197 _writeMessageSeen = NO; 198 _readMessageSeen = NO; 199 _request = nil; 200 _response = nil; 201 } 202 return self; 203} 204 205- (void)startWithRequestOptions:(GRPCRequestOptions *)requestOptions 206 callOptions:(GRPCCallOptions *)callOptions { 207 if (requestOptions.safety != GRPCCallSafetyCacheableRequest) { 208 _cacheable = NO; 209 [_manager startNextInterceptorWithRequest:requestOptions callOptions:callOptions]; 210 } else { 211 _requestOptions = [requestOptions copy]; 212 _callOptions = [callOptions copy]; 213 } 214} 215 216- (void)writeData:(id)data { 217 if (!_cacheable) { 218 [_manager writeNextInterceptorWithData:data]; 219 } else { 220 NSAssert(!_writeMessageSeen, @"CacheInterceptor does not support streaming call"); 221 if (_writeMessageSeen) { 222 NSLog(@"CacheInterceptor does not support streaming call"); 223 } 224 _writeMessageSeen = YES; 225 _requestMessage = [data copy]; 226 } 227} 228 229- (void)finish { 230 if (!_cacheable) { 231 [_manager finishNextInterceptor]; 232 } else { 233 _request = [[MutableRequestCacheEntry alloc] init]; 234 _request.path = _requestOptions.path; 235 _request.message = [_requestMessage copy]; 236 _response = [[_context getCachedResponseForRequest:_request] copy]; 237 if (!_response) { 238 [_manager startNextInterceptorWithRequest:_requestOptions callOptions:_callOptions]; 239 [_manager writeNextInterceptorWithData:_requestMessage]; 240 [_manager finishNextInterceptor]; 241 } else { 242 [_manager forwardPreviousInterceptorWithInitialMetadata:_response.headers]; 243 [_manager forwardPreviousInterceptorWithData:_response.message]; 244 [_manager forwardPreviousInterceptorCloseWithTrailingMetadata:_response.trailers error:nil]; 245 [_manager shutDown]; 246 } 247 } 248} 249 250- (void)didReceiveInitialMetadata:(NSDictionary *)initialMetadata { 251 if (_cacheable) { 252 NSDate *deadline = nil; 253 for (NSString *key in initialMetadata) { 254 if ([key.lowercaseString isEqualToString:@"cache-control"]) { 255 NSArray *cacheControls = [initialMetadata[key] componentsSeparatedByString:@","]; 256 for (NSString *option in cacheControls) { 257 NSString *trimmedOption = 258 [option stringByTrimmingCharactersInSet:[NSCharacterSet 259 characterSetWithCharactersInString:@" "]]; 260 if ([trimmedOption.lowercaseString isEqualToString:@"no-cache"] || 261 [trimmedOption.lowercaseString isEqualToString:@"no-store"] || 262 [trimmedOption.lowercaseString isEqualToString:@"no-transform"]) { 263 _cacheable = NO; 264 break; 265 } else if ([trimmedOption.lowercaseString hasPrefix:@"max-age="]) { 266 NSArray<NSString *> *components = [trimmedOption componentsSeparatedByString:@"="]; 267 if (components.count == 2) { 268 NSUInteger maxAge = components[1].intValue; 269 deadline = [NSDate dateWithTimeIntervalSinceNow:maxAge]; 270 } 271 } 272 } 273 } 274 } 275 if (_cacheable) { 276 _response = [[MutableResponseCacheEntry alloc] init]; 277 _response.headers = [initialMetadata copy]; 278 _response.deadline = deadline; 279 } 280 } 281 [_manager forwardPreviousInterceptorWithInitialMetadata:initialMetadata]; 282} 283 284- (void)didReceiveData:(id)data { 285 if (_cacheable) { 286 NSAssert(!_readMessageSeen, @"CacheInterceptor does not support streaming call"); 287 if (_readMessageSeen) { 288 NSLog(@"CacheInterceptor does not support streaming call"); 289 } 290 _readMessageSeen = YES; 291 _response.message = [data copy]; 292 } 293 [_manager forwardPreviousInterceptorWithData:data]; 294} 295 296- (void)didCloseWithTrailingMetadata:(NSDictionary *)trailingMetadata error:(NSError *)error { 297 if (error == nil && _cacheable) { 298 _response.trailers = [trailingMetadata copy]; 299 [_context setCachedResponse:_response forRequest:_request]; 300 NSLog(@"Write cache for %@", _request); 301 } 302 [_manager forwardPreviousInterceptorCloseWithTrailingMetadata:trailingMetadata error:error]; 303 [_manager shutDown]; 304} 305 306@end 307