// Copyright 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #import "ios/net/crn_http_protocol_handler_proxy_with_client_thread.h" #include #include "base/logging.h" #import "base/mac/scoped_nsobject.h" #include "base/time/time.h" #import "ios/net/protocol_handler_util.h" #include "net/base/auth.h" #include "net/url_request/url_request.h" // When the protocol is invalidated, no synchronization (lock) is needed: // - The actual calls to the protocol and its invalidation are all done on // clientThread_ and thus are serialized. // - When a proxy method is called, the protocol is compared to nil. There may // be a conflict at this point, in the case the protocol is being invalidated // during this comparison. However, in such a case, the actual value of the // pointer does not matter: an invalid pointer will behave as a valid one and // post a task on the clientThread_, and that task will be handled correctly, // as described by the item above. @interface CRNHTTPProtocolHandlerProxyWithClientThread () { NSURLProtocol* _protocol; // weak // Thread used to call the client back. // This thread does not have a base::MessageLoop, and thus does not work with // the usual task posting functions. NSThread* _clientThread; // weak // The run loop modes to use when posting tasks to |clientThread_|. base::scoped_nsobject _runLoopModes; // The request URL. base::scoped_nsobject _url; // The creation time of the request. base::Time _creationTime; // |requestComplete_| is used in debug to check that the client is not called // after completion. BOOL _requestComplete; BOOL _paused; base::scoped_nsobject _queuedInvocations; } // Performs the selector on |clientThread_| using |runLoopModes_|. - (void)runInvocationQueueOnClientThread; - (void)postToClientThread:(SEL)aSelector, ... NS_REQUIRES_NIL_TERMINATION; - (void)invokeOnClientThread:(NSInvocation*)invocation; // These functions are just wrappers around the corresponding // NSURLProtocolClient methods, used for task posting. - (void)didFailWithErrorOnClientThread:(NSError*)error; - (void)didLoadDataOnClientThread:(NSData*)data; - (void)didReceiveResponseOnClientThread:(NSURLResponse*)response; - (void)wasRedirectedToRequestOnClientThread:(NSURLRequest*)request redirectResponse:(NSURLResponse*)response; - (void)didFinishLoadingOnClientThread; @end @implementation CRNHTTPProtocolHandlerProxyWithClientThread - (instancetype)initWithProtocol:(NSURLProtocol*)protocol clientThread:(NSThread*)clientThread runLoopMode:(NSString*)mode { DCHECK(protocol); DCHECK(clientThread); if ((self = [super init])) { _protocol = protocol; _url.reset([[[[protocol request] URL] absoluteString] copy]); _creationTime = base::Time::Now(); _clientThread = clientThread; // Use the common run loop mode in addition to the client thread mode, in // hope that our tasks are executed even if the client thread changes mode // later on. if ([mode isEqualToString:NSRunLoopCommonModes]) _runLoopModes.reset([@[ NSRunLoopCommonModes ] retain]); else _runLoopModes.reset([@[ mode, NSRunLoopCommonModes ] retain]); _queuedInvocations.reset([[NSMutableArray alloc] init]); } return self; } - (void)invalidate { DCHECK([NSThread currentThread] == _clientThread); _protocol = nil; _requestComplete = YES; // Note that there may still be queued invocations here, if the chrome network // stack continues to emit events after the system network stack has paused // the request, and then the system network stack destroys the request. _queuedInvocations.reset(); } - (void)runInvocationQueueOnClientThread { DCHECK([NSThread currentThread] == _clientThread); DCHECK(!_requestComplete || !_protocol); // Each of the queued invocations may cause the system network stack to pause // this request, in which case |runInvocationQueueOnClientThread| should // immediately stop running further queued invocations. The queue will be // drained again the next time the system network stack calls |resume|. // // Specifically, the system stack can call back into |pause| with this // function still on the call stack. However, since new invocations are // enqueued on this thread via posted invocations, no new invocations can be // added while this function is running. while (!_paused && _queuedInvocations.get().count > 0) { NSInvocation* invocation = [_queuedInvocations objectAtIndex:0]; // Since |_queuedInvocations| owns the only reference to each queued // invocation, this function has to retain another reference before removing // the queued invocation from the array. [invocation invoke]; [_queuedInvocations removeObjectAtIndex:0]; } } - (void)postToClientThread:(SEL)aSelector, ... { // Build an NSInvocation representing an invocation of |aSelector| on |self| // with the supplied varargs passed as arguments to the invocation. NSMethodSignature* sig = [self methodSignatureForSelector:aSelector]; DCHECK(sig != nil); NSInvocation* inv = [NSInvocation invocationWithMethodSignature:sig]; [inv setTarget:self]; [inv setSelector:aSelector]; [inv retainArguments]; size_t arg_index = 2; va_list args; va_start(args, aSelector); NSObject* arg = va_arg(args, NSObject*); while (arg != nil) { [inv setArgument:&arg atIndex:arg_index]; arg = va_arg(args, NSObject*); arg_index++; } va_end(args); DCHECK(arg_index == sig.numberOfArguments); [self performSelector:@selector(invokeOnClientThread:) onThread:_clientThread withObject:inv waitUntilDone:NO modes:_runLoopModes]; } - (void)invokeOnClientThread:(NSInvocation*)invocation { DCHECK([NSThread currentThread] == _clientThread); DCHECK(!_requestComplete || !_protocol); if (!_paused) { [invocation invoke]; } else { [_queuedInvocations addObject:invocation]; } } #pragma mark Proxy methods called from any thread. - (void)didFailWithNSErrorCode:(NSInteger)nsErrorCode netErrorCode:(int)netErrorCode { DCHECK(_clientThread); if (!_protocol) return; NSError* error = net::GetIOSError(nsErrorCode, netErrorCode, _url, _creationTime); [self postToClientThread:@selector(didFailWithErrorOnClientThread:), error, nil]; } - (void)didLoadData:(NSData*)data { DCHECK(_clientThread); if (!_protocol) return; [self postToClientThread:@selector(didLoadDataOnClientThread:), data, nil]; } - (void)didReceiveResponse:(NSURLResponse*)response { DCHECK(_clientThread); if (!_protocol) return; [self postToClientThread:@selector(didReceiveResponseOnClientThread:), response, nil]; } - (void)wasRedirectedToRequest:(NSURLRequest*)request nativeRequest:(net::URLRequest*)nativeRequest redirectResponse:(NSURLResponse*)redirectResponse { DCHECK(_clientThread); if (!_protocol) return; [self postToClientThread:@selector(wasRedirectedToRequestOnClientThread: redirectResponse:), request, redirectResponse, nil]; } - (void)didFinishLoading { DCHECK(_clientThread); if (!_protocol) return; [self postToClientThread:@selector(didFinishLoadingOnClientThread), nil]; } // Feature support methods that don't forward to the NSURLProtocolClient. - (void)didCreateNativeRequest:(net::URLRequest*)nativeRequest { // no-op. } - (void)didRecieveAuthChallenge:(net::AuthChallengeInfo*)authInfo nativeRequest:(const net::URLRequest&)nativeRequest callback:(const network_client::AuthCallback&)callback { // If we get this far, authentication has failed. base::string16 empty; callback.Run(false, empty, empty); } - (void)cancelAuthRequest { // no-op. } - (void)setUnderlyingClient:(id)underlyingClient { // This is the lowest level. DCHECK(!underlyingClient); } #pragma mark Proxy methods called from the client thread. - (void)didFailWithErrorOnClientThread:(NSError*)error { _requestComplete = YES; [[_protocol client] URLProtocol:_protocol didFailWithError:error]; } - (void)didLoadDataOnClientThread:(NSData*)data { [[_protocol client] URLProtocol:_protocol didLoadData:data]; } - (void)didReceiveResponseOnClientThread:(NSURLResponse*)response { [[_protocol client] URLProtocol:_protocol didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; } - (void)wasRedirectedToRequestOnClientThread:(NSURLRequest*)request redirectResponse:(NSURLResponse*)redirectResponse { [[_protocol client] URLProtocol:_protocol wasRedirectedToRequest:request redirectResponse:redirectResponse]; } - (void)didFinishLoadingOnClientThread { _requestComplete = YES; [[_protocol client] URLProtocolDidFinishLoading:_protocol]; } - (void)pause { DCHECK([NSThread currentThread] == _clientThread); // It's legal (in fact, required) for |pause| to be called after the request // has already finished, so the usual invalidation DCHECK is missing here. _paused = YES; } - (void)resume { DCHECK([NSThread currentThread] == _clientThread); DCHECK(!_requestComplete || !_protocol); _paused = NO; [self runInvocationQueueOnClientThread]; } @end