2 * Copyright (C) 2004, 2006, 2007, 2008, 2009, 2010, 2011, 2012 Apple Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
13 * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 #import "ResourceHandleInternal.h"
31 #import "AuthenticationChallenge.h"
32 #import "AuthenticationMac.h"
33 #import "BlockExceptions.h"
34 #import "CookieStorage.h"
35 #import "CredentialStorage.h"
36 #import "CachedResourceLoader.h"
37 #import "EmptyProtocolDefinitions.h"
38 #import "FormDataStreamMac.h"
40 #import "FrameLoader.h"
42 #import "MIMETypeRegistry.h"
43 #import "NetworkingContext.h"
45 #import "ResourceError.h"
46 #import "ResourceResponse.h"
48 #import "SharedBuffer.h"
49 #import "SubresourceLoader.h"
50 #import "WebCoreResourceHandleAsDelegate.h"
51 #import "WebCoreResourceHandleAsOperationQueueDelegate.h"
52 #import "SynchronousLoaderClient.h"
53 #import "WebCoreSystemInterface.h"
54 #import "WebCoreURLResponse.h"
55 #import <wtf/SchedulePair.h>
56 #import <wtf/UnusedParam.h>
57 #import <wtf/text/Base64.h>
58 #import <wtf/text/CString.h>
60 using namespace WebCore;
62 // WebCoreNSURLConnectionDelegateProxy exists so that we can cast m_proxy to it in order
63 // to disambiguate the argument type in the -setDelegate: call. This avoids a spurious
64 // warning that the compiler would otherwise emit.
65 @interface WebCoreNSURLConnectionDelegateProxy : NSObject <NSURLConnectionDelegate>
66 - (void)setDelegate:(id<NSURLConnectionDelegate>)delegate;
69 @interface NSURLConnection (Details)
70 -(id)_initWithRequest:(NSURLRequest *)request delegate:(id)delegate usesCache:(BOOL)usesCacheFlag maxContentLength:(long long)maxContentLength startImmediately:(BOOL)startImmediately connectionProperties:(NSDictionary *)connectionProperties;
75 static void applyBasicAuthorizationHeader(ResourceRequest& request, const Credential& credential)
77 String authenticationHeader = "Basic " + base64Encode(String(credential.user() + ":" + credential.password()).utf8());
78 request.clearHTTPAuthorization(); // FIXME: Should addHTTPHeaderField be smart enough to not build comma-separated lists in headers like Authorization?
79 request.addHTTPHeaderField("Authorization", authenticationHeader);
82 static NSOperationQueue *operationQueueForAsyncClients()
84 static NSOperationQueue *queue;
86 queue = [[NSOperationQueue alloc] init];
87 // Default concurrent operation count depends on current system workload, but delegate methods are mostly idling in IPC, so we can run as many as needed.
88 [queue setMaxConcurrentOperationCount:NSIntegerMax];
93 ResourceHandleInternal::~ResourceHandleInternal()
97 ResourceHandle::~ResourceHandle()
100 d->m_currentWebChallenge.setAuthenticationClient(0);
102 LOG(Network, "Handle %p destroyed", this);
105 void ResourceHandle::createNSURLConnection(id delegate, bool shouldUseCredentialStorage, bool shouldContentSniff)
107 // Credentials for ftp can only be passed in URL, the connection:didReceiveAuthenticationChallenge: delegate call won't be made.
108 if ((!d->m_user.isEmpty() || !d->m_pass.isEmpty()) && !firstRequest().url().protocolIsInHTTPFamily()) {
109 KURL urlWithCredentials(firstRequest().url());
110 urlWithCredentials.setUser(d->m_user);
111 urlWithCredentials.setPass(d->m_pass);
112 firstRequest().setURL(urlWithCredentials);
115 if (shouldUseCredentialStorage && firstRequest().url().protocolIsInHTTPFamily()) {
116 if (d->m_user.isEmpty() && d->m_pass.isEmpty()) {
117 // <rdar://problem/7174050> - For URLs that match the paths of those previously challenged for HTTP Basic authentication,
118 // try and reuse the credential preemptively, as allowed by RFC 2617.
119 d->m_initialCredential = CredentialStorage::get(firstRequest().url());
121 // If there is already a protection space known for the URL, update stored credentials before sending a request.
122 // This makes it possible to implement logout by sending an XMLHttpRequest with known incorrect credentials, and aborting it immediately
123 // (so that an authentication dialog doesn't pop up).
124 CredentialStorage::set(Credential(d->m_user, d->m_pass, CredentialPersistenceNone), firstRequest().url());
128 if (!d->m_initialCredential.isEmpty()) {
129 // FIXME: Support Digest authentication, and Proxy-Authorization.
130 applyBasicAuthorizationHeader(firstRequest(), d->m_initialCredential);
133 NSURLRequest *nsRequest = firstRequest().nsURLRequest(UpdateHTTPBody);
134 if (!shouldContentSniff) {
135 NSMutableURLRequest *mutableRequest = [[nsRequest mutableCopy] autorelease];
136 wkSetNSURLRequestShouldContentSniff(mutableRequest, NO);
137 nsRequest = mutableRequest;
140 if (d->m_storageSession)
141 nsRequest = [wkCopyRequestWithStorageSession(d->m_storageSession.get(), nsRequest) autorelease];
143 ASSERT([NSURLConnection instancesRespondToSelector:@selector(_initWithRequest:delegate:usesCache:maxContentLength:startImmediately:connectionProperties:)]);
145 NSMutableDictionary *streamProperties = [NSMutableDictionary dictionary];
147 if (!shouldUseCredentialStorage)
148 [streamProperties setObject:@"WebKitPrivateSession" forKey:@"_kCFURLConnectionSessionID"];
150 #if __MAC_OS_X_VERSION_MIN_REQUIRED >= 1090
151 RetainPtr<CFDataRef> sourceApplicationAuditData = d->m_context->sourceApplicationAuditData();
152 if (sourceApplicationAuditData)
153 [streamProperties setObject:(NSData *)sourceApplicationAuditData.get() forKey:@"kCFStreamPropertySourceApplication"];
156 NSDictionary *propertyDictionary = [NSDictionary dictionaryWithObject:streamProperties forKey:@"kCFURLConnectionSocketStreamProperties"];
157 d->m_connection.adoptNS([[NSURLConnection alloc] _initWithRequest:nsRequest delegate:delegate usesCache:YES maxContentLength:0 startImmediately:NO connectionProperties:propertyDictionary]);
160 bool ResourceHandle::start()
165 BEGIN_BLOCK_OBJC_EXCEPTIONS;
167 // If NetworkingContext is invalid then we are no longer attached to a Page,
168 // this must be an attempted load from an unload event handler, so let's just block it.
169 if (!d->m_context->isValid())
172 d->m_storageSession = d->m_context->storageSession().platformSession();
175 d->m_proxy.adoptNS(wkCreateNSURLConnectionDelegateProxy());
176 [static_cast<WebCoreNSURLConnectionDelegateProxy*>(d->m_proxy.get()) setDelegate:ResourceHandle::delegate()];
178 bool shouldUseCredentialStorage = !client() || client()->shouldUseCredentialStorage(this);
180 d->m_needsSiteSpecificQuirks = d->m_context->needsSiteSpecificQuirks();
182 createNSURLConnection(
184 shouldUseCredentialStorage,
185 d->m_shouldContentSniff || d->m_context->localFileContentSniffingEnabled());
187 bool scheduled = false;
188 if (SchedulePairHashSet* scheduledPairs = d->m_context->scheduledRunLoopPairs()) {
189 SchedulePairHashSet::iterator end = scheduledPairs->end();
190 for (SchedulePairHashSet::iterator it = scheduledPairs->begin(); it != end; ++it) {
191 if (NSRunLoop *runLoop = (*it)->nsRunLoop()) {
192 [connection() scheduleInRunLoop:runLoop forMode:(NSString *)(*it)->mode()];
198 if (client() && client()->usesAsyncCallbacks()) {
200 [connection() setDelegateQueue:operationQueueForAsyncClients()];
204 // Start the connection if we did schedule with at least one runloop.
205 // We can't start the connection until we have one runloop scheduled.
207 [connection() start];
209 d->m_startWhenScheduled = true;
211 LOG(Network, "Handle %p starting connection %p for %@", this, connection(), firstRequest().nsURLRequest(DoNotUpdateHTTPBody));
213 if (d->m_connection) {
214 if (d->m_defersLoading)
215 wkSetNSURLConnectionDefersCallbacks(connection(), YES);
220 END_BLOCK_OBJC_EXCEPTIONS;
225 void ResourceHandle::cancel()
227 LOG(Network, "Handle %p cancel connection %p", this, d->m_connection.get());
229 // Leaks were seen on HTTP tests without this; can be removed once <rdar://problem/6886937> is fixed.
230 if (d->m_currentMacChallenge)
231 [[d->m_currentMacChallenge sender] cancelAuthenticationChallenge:d->m_currentMacChallenge];
233 [d->m_connection.get() cancel];
236 void ResourceHandle::platformSetDefersLoading(bool defers)
239 wkSetNSURLConnectionDefersCallbacks(d->m_connection.get(), defers);
242 void ResourceHandle::schedule(SchedulePair* pair)
244 NSRunLoop *runLoop = pair->nsRunLoop();
247 [d->m_connection.get() scheduleInRunLoop:runLoop forMode:(NSString *)pair->mode()];
248 if (d->m_startWhenScheduled) {
249 [d->m_connection.get() start];
250 d->m_startWhenScheduled = false;
254 void ResourceHandle::unschedule(SchedulePair* pair)
256 if (NSRunLoop *runLoop = pair->nsRunLoop())
257 [d->m_connection.get() unscheduleFromRunLoop:runLoop forMode:(NSString *)pair->mode()];
260 id ResourceHandle::delegate()
262 if (!d->m_delegate) {
263 id <NSURLConnectionDelegate> delegate = (client() && client()->usesAsyncCallbacks()) ?
264 [[WebCoreResourceHandleAsOperationQueueDelegate alloc] initWithHandle:this]
265 : [[WebCoreResourceHandleAsDelegate alloc] initWithHandle:this];
266 d->m_delegate = delegate;
269 return d->m_delegate.get();
272 void ResourceHandle::releaseDelegate()
277 [d->m_proxy.get() setDelegate:nil];
278 [d->m_delegate.get() detachHandle];
282 id ResourceHandle::releaseProxy()
284 id proxy = [[d->m_proxy.get() retain] autorelease];
286 [proxy setDelegate:nil];
290 NSURLConnection *ResourceHandle::connection() const
292 return d->m_connection.get();
295 bool ResourceHandle::loadsBlocked()
300 CFStringRef ResourceHandle::synchronousLoadRunLoopMode()
302 return CFSTR("WebCoreSynchronousLoaderRunLoopMode");
305 void ResourceHandle::platformLoadResourceSynchronously(NetworkingContext* context, const ResourceRequest& request, StoredCredentials storedCredentials, ResourceError& error, ResourceResponse& response, Vector<char>& data)
307 LOG(Network, "ResourceHandle::platformLoadResourceSynchronously:%@ allowStoredCredentials:%u", request.nsURLRequest(DoNotUpdateHTTPBody), storedCredentials);
309 ASSERT(!request.isEmpty());
311 OwnPtr<SynchronousLoaderClient> client = SynchronousLoaderClient::create();
312 client->setAllowStoredCredentials(storedCredentials == AllowStoredCredentials);
314 RefPtr<ResourceHandle> handle = adoptRef(new ResourceHandle(context, request, client.get(), false /*defersLoading*/, true /*shouldContentSniff*/));
316 handle->d->m_storageSession = context->storageSession().platformSession();
318 if (context && handle->d->m_scheduledFailureType != NoFailure) {
319 error = context->blockedError(request);
323 handle->createNSURLConnection(
324 handle->delegate(), // A synchronous request cannot turn into a download, so there is no need to proxy the delegate.
325 storedCredentials == AllowStoredCredentials,
326 handle->shouldContentSniff() || context->localFileContentSniffingEnabled());
328 [handle->connection() scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:(NSString *)synchronousLoadRunLoopMode()];
329 [handle->connection() start];
331 while (!client->isDone())
332 [[NSRunLoop currentRunLoop] runMode:(NSString *)synchronousLoadRunLoopMode() beforeDate:[NSDate distantFuture]];
334 error = client->error();
336 [handle->connection() cancel];
339 response = client->response();
341 response = ResourceResponse(request.url(), String(), 0, String(), String());
342 if (error.domain() == String(NSURLErrorDomain))
343 switch (error.errorCode()) {
344 case NSURLErrorUserCancelledAuthentication:
345 // FIXME: we should really return the actual HTTP response, but sendSynchronousRequest doesn't provide us with one.
346 response.setHTTPStatusCode(401);
349 response.setHTTPStatusCode(error.errorCode());
352 response.setHTTPStatusCode(404);
355 data.swap(client->mutableData());
358 void ResourceHandle::willSendRequest(ResourceRequest& request, const ResourceResponse& redirectResponse)
360 ASSERT(!redirectResponse.isNull());
362 if (redirectResponse.httpStatusCode() == 307) {
363 String lastHTTPMethod = d->m_lastHTTPMethod;
364 if (!equalIgnoringCase(lastHTTPMethod, request.httpMethod())) {
365 request.setHTTPMethod(lastHTTPMethod);
367 FormData* body = d->m_firstRequest.httpBody();
368 if (!equalIgnoringCase(lastHTTPMethod, "GET") && body && !body->isEmpty())
369 request.setHTTPBody(body);
371 String originalContentType = d->m_firstRequest.httpContentType();
372 if (!originalContentType.isEmpty())
373 request.setHTTPHeaderField("Content-Type", originalContentType);
377 // Should not set Referer after a redirect from a secure resource to non-secure one.
378 if (!request.url().protocolIs("https") && protocolIs(request.httpReferrer(), "https") && d->m_context->shouldClearReferrerOnHTTPSToHTTPRedirect())
379 request.clearHTTPReferrer();
381 const KURL& url = request.url();
382 d->m_user = url.user();
383 d->m_pass = url.pass();
384 d->m_lastHTTPMethod = request.httpMethod();
385 request.removeCredentials();
387 if (!protocolHostAndPortAreEqual(request.url(), redirectResponse.url())) {
388 // If the network layer carries over authentication headers from the original request
389 // in a cross-origin redirect, we want to clear those headers here.
390 // As of Lion, CFNetwork no longer does this.
391 request.clearHTTPAuthorization();
393 // Only consider applying authentication credentials if this is actually a redirect and the redirect
394 // URL didn't include credentials of its own.
395 if (d->m_user.isEmpty() && d->m_pass.isEmpty() && !redirectResponse.isNull()) {
396 Credential credential = CredentialStorage::get(request.url());
397 if (!credential.isEmpty()) {
398 d->m_initialCredential = credential;
400 // FIXME: Support Digest authentication, and Proxy-Authorization.
401 applyBasicAuthorizationHeader(request, d->m_initialCredential);
406 if (client()->usesAsyncCallbacks()) {
407 client()->willSendRequestAsync(this, request, redirectResponse);
409 RefPtr<ResourceHandle> protect(this);
410 client()->willSendRequest(this, request, redirectResponse);
412 // Client call may not preserve the session, especially if the request is sent over IPC.
413 if (!request.isNull())
414 request.setStorageSession(d->m_storageSession.get());
418 void ResourceHandle::continueWillSendRequest(const ResourceRequest& request)
420 ASSERT(client() && client()->usesAsyncCallbacks());
422 // Client call may not preserve the session, especially if the request is sent over IPC.
423 ResourceRequest newRequest = request;
424 if (!newRequest.isNull())
425 newRequest.setStorageSession(d->m_storageSession.get());
426 [(id)delegate() continueWillSendRequest:newRequest.nsURLRequest(UpdateHTTPBody)];
429 bool ResourceHandle::shouldUseCredentialStorage()
431 if (client()->usesAsyncCallbacks()) {
433 client()->shouldUseCredentialStorageAsync(this);
435 continueShouldUseCredentialStorage(false);
436 return false; // Ignored by caller.
438 return client() && client()->shouldUseCredentialStorage(this);
441 void ResourceHandle::continueShouldUseCredentialStorage(bool useCredentialStorage)
443 ASSERT(client() && client()->usesAsyncCallbacks());
445 [(id)delegate() continueShouldUseCredentialStorage:useCredentialStorage];
448 void ResourceHandle::didReceiveAuthenticationChallenge(const AuthenticationChallenge& challenge)
450 ASSERT(!d->m_currentMacChallenge);
451 ASSERT(d->m_currentWebChallenge.isNull());
452 // Since NSURLConnection networking relies on keeping a reference to the original NSURLAuthenticationChallenge,
453 // we make sure that is actually present
454 ASSERT(challenge.nsURLAuthenticationChallenge());
456 // Proxy authentication is handled by CFNetwork internally. We can get here if the user cancels
457 // CFNetwork authentication dialog, and we shouldn't ask the client to display another one in that case.
458 if (challenge.protectionSpace().isProxy()) {
459 // Cannot use receivedRequestToContinueWithoutCredential(), because current challenge is not yet set.
460 [challenge.sender() continueWithoutCredentialForAuthenticationChallenge:challenge.nsURLAuthenticationChallenge()];
464 if (!d->m_user.isNull() && !d->m_pass.isNull()) {
465 NSURLCredential *credential = [[NSURLCredential alloc] initWithUser:d->m_user
467 persistence:NSURLCredentialPersistenceForSession];
468 d->m_currentMacChallenge = challenge.nsURLAuthenticationChallenge();
469 d->m_currentWebChallenge = challenge;
470 receivedCredential(challenge, core(credential));
471 [credential release];
472 // FIXME: Per the specification, the user shouldn't be asked for credentials if there were incorrect ones provided explicitly.
473 d->m_user = String();
474 d->m_pass = String();
478 if (!client() || client()->shouldUseCredentialStorage(this)) {
479 if (!d->m_initialCredential.isEmpty() || challenge.previousFailureCount()) {
480 // The stored credential wasn't accepted, stop using it.
481 // There is a race condition here, since a different credential might have already been stored by another ResourceHandle,
482 // but the observable effect should be very minor, if any.
483 CredentialStorage::remove(challenge.protectionSpace());
486 if (!challenge.previousFailureCount()) {
487 Credential credential = CredentialStorage::get(challenge.protectionSpace());
488 if (!credential.isEmpty() && credential != d->m_initialCredential) {
489 ASSERT(credential.persistence() == CredentialPersistenceNone);
490 if (challenge.failureResponse().httpStatusCode() == 401) {
491 // Store the credential back, possibly adding it as a default for this directory.
492 CredentialStorage::set(credential, challenge.protectionSpace(), challenge.failureResponse().url());
494 [challenge.sender() useCredential:mac(credential) forAuthenticationChallenge:mac(challenge)];
500 d->m_currentMacChallenge = challenge.nsURLAuthenticationChallenge();
501 d->m_currentWebChallenge = core(d->m_currentMacChallenge);
502 d->m_currentWebChallenge.setAuthenticationClient(this);
504 // FIXME: Several concurrent requests can return with the an authentication challenge for the same protection space.
505 // We should avoid making additional client calls for the same protection space when already waiting for the user,
506 // because typing the same credentials several times is annoying.
508 client()->didReceiveAuthenticationChallenge(this, d->m_currentWebChallenge);
511 void ResourceHandle::didCancelAuthenticationChallenge(const AuthenticationChallenge& challenge)
513 ASSERT(d->m_currentMacChallenge);
514 ASSERT(d->m_currentMacChallenge == challenge.nsURLAuthenticationChallenge());
515 ASSERT(!d->m_currentWebChallenge.isNull());
518 client()->didCancelAuthenticationChallenge(this, challenge);
521 #if USE(PROTECTION_SPACE_AUTH_CALLBACK)
522 bool ResourceHandle::canAuthenticateAgainstProtectionSpace(const ProtectionSpace& protectionSpace)
524 if (client()->usesAsyncCallbacks()) {
526 client()->canAuthenticateAgainstProtectionSpaceAsync(this, protectionSpace);
528 continueCanAuthenticateAgainstProtectionSpace(false);
529 return false; // Ignored by caller.
531 return client() && client()->canAuthenticateAgainstProtectionSpace(this, protectionSpace);
534 void ResourceHandle::continueCanAuthenticateAgainstProtectionSpace(bool result)
536 ASSERT(client() && client()->usesAsyncCallbacks());
538 [(id)delegate() continueCanAuthenticateAgainstProtectionSpace:result];
542 void ResourceHandle::receivedCredential(const AuthenticationChallenge& challenge, const Credential& credential)
544 LOG(Network, "Handle %p receivedCredential", this);
546 ASSERT(!challenge.isNull());
547 if (challenge != d->m_currentWebChallenge)
550 // FIXME: Support empty credentials. Currently, an empty credential cannot be stored in WebCore credential storage, as that's empty value for its map.
551 if (credential.isEmpty()) {
552 receivedRequestToContinueWithoutCredential(challenge);
556 if (credential.persistence() == CredentialPersistenceForSession && (!d->m_needsSiteSpecificQuirks || ![[[mac(challenge) protectionSpace] host] isEqualToString:@"gallery.me.com"])) {
557 // Manage per-session credentials internally, because once NSURLCredentialPersistenceForSession is used, there is no way
558 // to ignore it for a particular request (short of removing it altogether).
559 // <rdar://problem/6867598> gallery.me.com is temporarily whitelisted, so that QuickTime plug-in could see the credentials.
560 Credential webCredential(credential, CredentialPersistenceNone);
562 if (challenge.failureResponse().httpStatusCode() == 401)
563 urlToStore = challenge.failureResponse().url();
564 CredentialStorage::set(webCredential, core([d->m_currentMacChallenge protectionSpace]), urlToStore);
565 [[d->m_currentMacChallenge sender] useCredential:mac(webCredential) forAuthenticationChallenge:d->m_currentMacChallenge];
567 [[d->m_currentMacChallenge sender] useCredential:mac(credential) forAuthenticationChallenge:d->m_currentMacChallenge];
569 clearAuthentication();
572 void ResourceHandle::receivedRequestToContinueWithoutCredential(const AuthenticationChallenge& challenge)
574 LOG(Network, "Handle %p receivedRequestToContinueWithoutCredential", this);
576 ASSERT(!challenge.isNull());
577 if (challenge != d->m_currentWebChallenge)
580 [[d->m_currentMacChallenge sender] continueWithoutCredentialForAuthenticationChallenge:d->m_currentMacChallenge];
582 clearAuthentication();
585 void ResourceHandle::receivedCancellation(const AuthenticationChallenge& challenge)
587 LOG(Network, "Handle %p receivedCancellation", this);
589 if (challenge != d->m_currentWebChallenge)
593 client()->receivedCancellation(this, challenge);
596 void ResourceHandle::continueWillCacheResponse(NSCachedURLResponse *response)
598 ASSERT(client() && client()->usesAsyncCallbacks());
600 [(id)delegate() continueWillCacheResponse:response];
604 } // namespace WebCore
606 #endif // !USE(CFNETWORK)