| /* |
| * Copyright (C) 2009-2016 Apple Inc. All rights reserved. |
| * Copyright (C) 2009 Google Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are |
| * met: |
| * |
| * * Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * * Redistributions in binary form must reproduce the above |
| * copyright notice, this list of conditions and the following disclaimer |
| * in the documentation and/or other materials provided with the |
| * distribution. |
| * * Neither the name of Google Inc. nor the names of its |
| * contributors may be used to endorse or promote products derived from |
| * this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "config.h" |
| #include "SocketStreamHandleImpl.h" |
| |
| #include "Credential.h" |
| #include "CredentialStorage.h" |
| #include "DeprecatedGlobalSettings.h" |
| #include "Logging.h" |
| #include "NetworkStorageSession.h" |
| #include "ProtectionSpace.h" |
| #include "SocketStreamError.h" |
| #include "SocketStreamHandleClient.h" |
| #include "StorageSessionProvider.h" |
| #include <CFNetwork/CFNetwork.h> |
| #include <wtf/Condition.h> |
| #include <wtf/Lock.h> |
| #include <wtf/MainThread.h> |
| #include <wtf/SoftLinking.h> |
| #include <wtf/cf/TypeCastsCF.h> |
| #include <wtf/text/WTFString.h> |
| |
| #if PLATFORM(WIN) |
| #include "LoaderRunLoopCF.h" |
| #include <pal/spi/cf/CFNetworkSPI.h> |
| #endif |
| |
| #if PLATFORM(IOS_FAMILY) |
| #include "WebCoreThreadInternal.h" |
| #endif |
| |
| #if PLATFORM(COCOA) |
| extern "C" const CFStringRef kCFStreamPropertySourceApplication; |
| extern "C" const CFStringRef _kCFStreamSocketSetNoDelay; |
| #endif |
| |
| #if PLATFORM(COCOA) |
| #import <pal/spi/cf/CFNetworkSPI.h> |
| #endif |
| |
| #if PLATFORM(WIN) |
| SOFT_LINK_LIBRARY(CFNetwork); |
| SOFT_LINK_OPTIONAL(CFNetwork, _CFHTTPMessageSetResponseProxyURL, void, __cdecl, (CFHTTPMessageRef, CFURLRef)); |
| #endif |
| |
| WTF_DECLARE_CF_TYPE_TRAIT(CFHTTPMessage); |
| |
| namespace WebCore { |
| |
| static inline CFRunLoopRef callbacksRunLoop() |
| { |
| #if PLATFORM(WIN) |
| return loaderRunLoop(); |
| #elif PLATFORM(IOS_FAMILY) |
| return WebThreadRunLoop(); |
| #else |
| return CFRunLoopGetMain(); |
| #endif |
| } |
| |
| static inline auto callbacksRunLoopMode() |
| { |
| #if PLATFORM(WIN) |
| return kCFRunLoopDefaultMode; |
| #else |
| return kCFRunLoopCommonModes; |
| #endif |
| } |
| |
| SocketStreamHandleImpl::SocketStreamHandleImpl(const URL& url, SocketStreamHandleClient& client, PAL::SessionID sessionID, const String& credentialPartition, SourceApplicationAuditToken&& auditData, const StorageSessionProvider* provider) |
| : SocketStreamHandle(url, client) |
| , m_connectingSubstate(New) |
| , m_connectionType(Unknown) |
| , m_sentStoredCredentials(false) |
| , m_credentialPartition(credentialPartition) |
| , m_auditData(WTFMove(auditData)) |
| , m_storageSessionProvider(provider) |
| { |
| LOG(Network, "SocketStreamHandle %p new client %p", this, &m_client); |
| |
| ASSERT(url.protocolIs("ws") || url.protocolIs("wss")); |
| |
| URL httpsURL(URL(), "https://" + m_url.host()); |
| m_httpsURL = httpsURL.createCFURL(); |
| |
| #if PLATFORM(COCOA) |
| // Don't check for HSTS violation for ephemeral sessions since |
| // HSTS state should not transfer between regular and private browsing. |
| if (url.protocolIs("ws") |
| && !sessionID.isEphemeral() |
| && _CFNetworkIsKnownHSTSHostWithSession(m_httpsURL.get(), nullptr)) { |
| // Call this asynchronously because the socket stream is not fully constructed at this point. |
| callOnMainThread([this, protectedThis = makeRef(*this)] { |
| m_client.didFailSocketStream(*this, SocketStreamError(0, m_url.string(), "WebSocket connection failed because it violates HTTP Strict Transport Security.")); |
| }); |
| return; |
| } |
| #endif |
| |
| createStreams(); |
| ASSERT(!m_readStream == !m_writeStream); |
| if (!m_readStream) // Doing asynchronous PAC file processing, streams will be created later. |
| return; |
| |
| scheduleStreams(); |
| } |
| |
| void SocketStreamHandleImpl::scheduleStreams() |
| { |
| ASSERT(m_readStream); |
| ASSERT(m_writeStream); |
| |
| CFStreamClientContext clientContext = { 0, this, retainSocketStreamHandle, releaseSocketStreamHandle, copyCFStreamDescription }; |
| // FIXME: Pass specific events we're interested in instead of -1. |
| CFReadStreamSetClient(m_readStream.get(), static_cast<CFOptionFlags>(-1), readStreamCallback, &clientContext); |
| CFWriteStreamSetClient(m_writeStream.get(), static_cast<CFOptionFlags>(-1), writeStreamCallback, &clientContext); |
| |
| CFReadStreamScheduleWithRunLoop(m_readStream.get(), callbacksRunLoop(), callbacksRunLoopMode()); |
| CFWriteStreamScheduleWithRunLoop(m_writeStream.get(), callbacksRunLoop(), callbacksRunLoopMode()); |
| |
| CFReadStreamOpen(m_readStream.get()); |
| CFWriteStreamOpen(m_writeStream.get()); |
| |
| if (m_pacRunLoopSource) |
| removePACRunLoopSource(); |
| |
| m_connectingSubstate = WaitingForConnect; |
| RELEASE_LOG(Network, "SocketStreamHandleImpl::scheduleStreams - m_connectionSubState is WaitingForConnect"); |
| } |
| |
| void* SocketStreamHandleImpl::retainSocketStreamHandle(void* info) |
| { |
| SocketStreamHandle* handle = static_cast<SocketStreamHandle*>(info); |
| handle->ref(); |
| return handle; |
| } |
| |
| void SocketStreamHandleImpl::releaseSocketStreamHandle(void* info) |
| { |
| SocketStreamHandle* handle = static_cast<SocketStreamHandle*>(info); |
| handle->deref(); |
| } |
| |
| CFStringRef SocketStreamHandleImpl::copyPACExecutionDescription(void*) |
| { |
| return CFSTR("WebSocket proxy PAC file execution"); |
| } |
| |
| struct MainThreadPACCallbackInfo { |
| MainThreadPACCallbackInfo(SocketStreamHandle* handle, CFArrayRef proxyList) |
| : handle(handle), proxyList(proxyList) |
| { } |
| RefPtr<SocketStreamHandle> handle; |
| CFArrayRef proxyList; |
| }; |
| |
| void SocketStreamHandleImpl::pacExecutionCallback(void* client, CFArrayRef proxyList, CFErrorRef) |
| { |
| SocketStreamHandleImpl* handle = static_cast<SocketStreamHandleImpl*>(client); |
| |
| RefPtr<SocketStreamHandle> protector(handle); |
| callOnMainThreadAndWait([&] { |
| ASSERT(handle->m_connectingSubstate == ExecutingPACFile); |
| // This time, the array won't have PAC as a first entry. |
| if (handle->m_state != Connecting) |
| return; |
| handle->chooseProxyFromArray(proxyList); |
| handle->createStreams(); |
| handle->scheduleStreams(); |
| }); |
| } |
| |
| void SocketStreamHandleImpl::executePACFileURL(CFURLRef pacFileURL) |
| { |
| // CFNetwork returns an empty proxy array for WebSocket schemes, so use m_httpsURL. |
| CFStreamClientContext clientContext = { 0, this, retainSocketStreamHandle, releaseSocketStreamHandle, copyPACExecutionDescription }; |
| m_pacRunLoopSource = adoptCF(CFNetworkExecuteProxyAutoConfigurationURL(pacFileURL, m_httpsURL.get(), pacExecutionCallback, &clientContext)); |
| CFRunLoopAddSource(callbacksRunLoop(), m_pacRunLoopSource.get(), callbacksRunLoopMode()); |
| m_connectingSubstate = ExecutingPACFile; |
| } |
| |
| void SocketStreamHandleImpl::removePACRunLoopSource() |
| { |
| ASSERT(m_pacRunLoopSource); |
| |
| CFRunLoopSourceInvalidate(m_pacRunLoopSource.get()); |
| CFRunLoopRemoveSource(callbacksRunLoop(), m_pacRunLoopSource.get(), callbacksRunLoopMode()); |
| m_pacRunLoopSource = 0; |
| } |
| |
| void SocketStreamHandleImpl::chooseProxy() |
| { |
| RetainPtr<CFDictionaryRef> proxyDictionary = adoptCF(CFNetworkCopySystemProxySettings()); |
| |
| // SOCKS or HTTPS (AKA CONNECT) proxies are supported. |
| // WebSocket protocol relies on handshake being transferred unchanged, so we need a proxy that will not modify headers. |
| // Since HTTP proxies must add Via headers, they are highly unlikely to work. |
| // Many CONNECT proxies limit connectivity to port 443, so we prefer SOCKS, if configured. |
| |
| if (!proxyDictionary) { |
| m_connectionType = Direct; |
| return; |
| } |
| |
| // CFNetworkCopyProxiesForURL doesn't know about WebSocket schemes, so pretend to use http. |
| // Always use "https" to get HTTPS proxies in result - we'll try to use those for ws:// even though many are configured to reject connections to ports other than 443. |
| RetainPtr<CFArrayRef> proxyArray = adoptCF(CFNetworkCopyProxiesForURL(m_httpsURL.get(), proxyDictionary.get())); |
| |
| chooseProxyFromArray(proxyArray.get()); |
| } |
| |
| void SocketStreamHandleImpl::chooseProxyFromArray(CFArrayRef proxyArray) |
| { |
| if (!proxyArray) { |
| m_connectionType = Direct; |
| return; |
| } |
| |
| CFIndex proxyArrayCount = CFArrayGetCount(proxyArray); |
| |
| // PAC is always the first entry, if present. |
| if (proxyArrayCount) { |
| CFDictionaryRef proxyInfo = static_cast<CFDictionaryRef>(CFArrayGetValueAtIndex(proxyArray, 0)); |
| CFTypeRef proxyType = CFDictionaryGetValue(proxyInfo, kCFProxyTypeKey); |
| if (proxyType && CFGetTypeID(proxyType) == CFStringGetTypeID()) { |
| if (CFEqual(proxyType, kCFProxyTypeAutoConfigurationURL)) { |
| CFTypeRef pacFileURL = CFDictionaryGetValue(proxyInfo, kCFProxyAutoConfigurationURLKey); |
| if (pacFileURL && CFGetTypeID(pacFileURL) == CFURLGetTypeID()) { |
| executePACFileURL(static_cast<CFURLRef>(pacFileURL)); |
| return; |
| } |
| } |
| } |
| } |
| |
| CFDictionaryRef chosenProxy = 0; |
| for (CFIndex i = 0; i < proxyArrayCount; ++i) { |
| CFDictionaryRef proxyInfo = static_cast<CFDictionaryRef>(CFArrayGetValueAtIndex(proxyArray, i)); |
| CFTypeRef proxyType = CFDictionaryGetValue(proxyInfo, kCFProxyTypeKey); |
| if (proxyType && CFGetTypeID(proxyType) == CFStringGetTypeID()) { |
| if (CFEqual(proxyType, kCFProxyTypeSOCKS)) { |
| m_connectionType = SOCKSProxy; |
| chosenProxy = proxyInfo; |
| break; |
| } |
| if (CFEqual(proxyType, kCFProxyTypeHTTPS)) { |
| m_connectionType = CONNECTProxy; |
| chosenProxy = proxyInfo; |
| // Keep looking for proxies, as a SOCKS one is preferable. |
| } |
| } |
| } |
| |
| if (chosenProxy) { |
| ASSERT(m_connectionType != Unknown); |
| ASSERT(m_connectionType != Direct); |
| |
| CFTypeRef proxyHost = CFDictionaryGetValue(chosenProxy, kCFProxyHostNameKey); |
| CFTypeRef proxyPort = CFDictionaryGetValue(chosenProxy, kCFProxyPortNumberKey); |
| |
| if (proxyHost && CFGetTypeID(proxyHost) == CFStringGetTypeID() && proxyPort && CFGetTypeID(proxyPort) == CFNumberGetTypeID()) { |
| m_proxyHost = static_cast<CFStringRef>(proxyHost); |
| m_proxyPort = static_cast<CFNumberRef>(proxyPort); |
| return; |
| } |
| } |
| |
| m_connectionType = Direct; |
| } |
| |
| static void setCONNECTProxyForStream(CFReadStreamRef stream, CFStringRef proxyHost, CFNumberRef proxyPort) |
| { |
| const void* proxyKeys[] = { kCFStreamPropertyCONNECTProxyHost, kCFStreamPropertyCONNECTProxyPort }; |
| const void* proxyValues[] = { proxyHost, proxyPort }; |
| auto connectDictionary = adoptCF(CFDictionaryCreate(kCFAllocatorDefault, proxyKeys, proxyValues, sizeof(proxyKeys) / sizeof(*proxyKeys), &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks)); |
| CFReadStreamSetProperty(stream, kCFStreamPropertyCONNECTProxy, connectDictionary.get()); |
| } |
| |
| static bool gLegacyTLSEnabled = false; |
| |
| void SocketStreamHandleImpl::setLegacyTLSEnabled(bool enabled) |
| { |
| gLegacyTLSEnabled = enabled; |
| } |
| |
| void SocketStreamHandleImpl::createStreams() |
| { |
| if (m_connectionType == Unknown) |
| chooseProxy(); |
| |
| // If it's still unknown, then we're resolving a PAC file asynchronously. |
| if (m_connectionType == Unknown) |
| return; |
| |
| RetainPtr<CFStringRef> host = m_url.host().createCFString(); |
| |
| // Creating streams to final destination, not to proxy. |
| CFReadStreamRef readStream = 0; |
| CFWriteStreamRef writeStream = 0; |
| CFStreamCreatePairWithSocketToHost(0, host.get(), port(), &readStream, &writeStream); |
| #if PLATFORM(COCOA) |
| // <rdar://problem/12855587> _kCFStreamSocketSetNoDelay is not exported on Windows |
| CFWriteStreamSetProperty(writeStream, _kCFStreamSocketSetNoDelay, kCFBooleanTrue); |
| if (m_auditData.sourceApplicationAuditData && m_auditData.sourceApplicationAuditData.get()) { |
| CFReadStreamSetProperty(readStream, kCFStreamPropertySourceApplication, m_auditData.sourceApplicationAuditData.get()); |
| CFWriteStreamSetProperty(writeStream, kCFStreamPropertySourceApplication, m_auditData.sourceApplicationAuditData.get()); |
| } |
| #endif |
| |
| m_readStream = adoptCF(readStream); |
| m_writeStream = adoptCF(writeStream); |
| |
| switch (m_connectionType) { |
| case Unknown: |
| ASSERT_NOT_REACHED(); |
| break; |
| case Direct: |
| break; |
| case SOCKSProxy: { |
| // FIXME: SOCKS5 doesn't do challenge-response, should we try to apply credentials from Keychain right away? |
| // But SOCKS5 credentials don't work at the time of this writing anyway, see <rdar://6776698>. |
| const void* proxyKeys[] = { kCFStreamPropertySOCKSProxyHost, kCFStreamPropertySOCKSProxyPort }; |
| const void* proxyValues[] = { m_proxyHost.get(), m_proxyPort.get() }; |
| RetainPtr<CFDictionaryRef> connectDictionary = adoptCF(CFDictionaryCreate(0, proxyKeys, proxyValues, WTF_ARRAY_LENGTH(proxyKeys), &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks)); |
| CFReadStreamSetProperty(m_readStream.get(), kCFStreamPropertySOCKSProxy, connectDictionary.get()); |
| break; |
| } |
| case CONNECTProxy: |
| setCONNECTProxyForStream(m_readStream.get(), m_proxyHost.get(), m_proxyPort.get()); |
| break; |
| } |
| |
| if (shouldUseSSL()) { |
| CFBooleanRef validateCertificateChain = DeprecatedGlobalSettings::allowsAnySSLCertificate() ? kCFBooleanFalse : kCFBooleanTrue; |
| const void* keys[] = { |
| kCFStreamSSLPeerName, |
| kCFStreamSSLLevel, |
| kCFStreamSSLValidatesCertificateChain |
| }; |
| const void* values[] = { |
| host.get(), |
| #if PLATFORM(COCOA) |
| gLegacyTLSEnabled ? kCFStreamSocketSecurityLevelNegotiatedSSL : kCFStreamSocketSecurityLevelTLSv1_2, |
| #else |
| kCFStreamSocketSecurityLevelNegotiatedSSL, |
| #endif |
| validateCertificateChain |
| }; |
| RetainPtr<CFDictionaryRef> settings = adoptCF(CFDictionaryCreate(0, keys, values, WTF_ARRAY_LENGTH(keys), &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks)); |
| CFReadStreamSetProperty(m_readStream.get(), kCFStreamPropertySSLSettings, settings.get()); |
| CFWriteStreamSetProperty(m_writeStream.get(), kCFStreamPropertySSLSettings, settings.get()); |
| } |
| } |
| |
| bool SocketStreamHandleImpl::getStoredCONNECTProxyCredentials(const ProtectionSpace& protectionSpace, String& login, String& password) |
| { |
| // FIXME (<rdar://problem/10416495>): Proxy credentials should be retrieved from AuthBrokerAgent. |
| |
| // Try system credential storage first, matching HTTP behavior (CFNetwork only asks the client for password if it couldn't find it in Keychain). |
| Credential storedCredential; |
| if (auto* storageSession = m_storageSessionProvider ? m_storageSessionProvider->storageSession() : nullptr) { |
| storedCredential = CredentialStorage::getFromPersistentStorage(protectionSpace); |
| if (storedCredential.isEmpty()) |
| storedCredential = storageSession->credentialStorage().get(m_credentialPartition, protectionSpace); |
| } |
| |
| if (storedCredential.isEmpty()) |
| return false; |
| |
| login = storedCredential.user(); |
| password = storedCredential.password(); |
| |
| return true; |
| } |
| |
| static ProtectionSpaceAuthenticationScheme authenticationSchemeFromAuthenticationMethod(CFStringRef method) |
| { |
| if (CFEqual(method, kCFHTTPAuthenticationSchemeBasic)) |
| return ProtectionSpaceAuthenticationSchemeHTTPBasic; |
| if (CFEqual(method, kCFHTTPAuthenticationSchemeDigest)) |
| return ProtectionSpaceAuthenticationSchemeHTTPDigest; |
| if (CFEqual(method, kCFHTTPAuthenticationSchemeNTLM)) |
| return ProtectionSpaceAuthenticationSchemeNTLM; |
| if (CFEqual(method, kCFHTTPAuthenticationSchemeNegotiate)) |
| return ProtectionSpaceAuthenticationSchemeNegotiate; |
| ASSERT_NOT_REACHED(); |
| return ProtectionSpaceAuthenticationSchemeUnknown; |
| } |
| |
| static void setCONNECTProxyAuthorizationForStream(CFReadStreamRef stream, CFStringRef proxyAuthorizationString) |
| { |
| auto originalCONNECTDictionary = adoptCF((CFDictionaryRef)CFReadStreamCopyProperty(stream, kCFStreamPropertyCONNECTProxy)); |
| auto connectDictionary = adoptCF(CFDictionaryCreateMutableCopy(kCFAllocatorDefault, 0, originalCONNECTDictionary.get())); |
| |
| const void* headerFieldNames[] = { CFSTR("Proxy-Authorization") }; |
| const void* headerFieldValues[] = { proxyAuthorizationString }; |
| auto additionalHeaderFields = adoptCF(CFDictionaryCreate(kCFAllocatorDefault, headerFieldNames, headerFieldValues, sizeof(headerFieldNames) / sizeof(*headerFieldValues), &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks)); |
| |
| CFDictionarySetValue(connectDictionary.get(), kCFStreamPropertyCONNECTAdditionalHeaders, additionalHeaderFields.get()); |
| CFReadStreamSetProperty(stream, kCFStreamPropertyCONNECTProxy, connectDictionary.get()); |
| } |
| |
| void SocketStreamHandleImpl::addCONNECTCredentials(CFHTTPMessageRef proxyResponse) |
| { |
| RetainPtr<CFHTTPAuthenticationRef> authentication = adoptCF(CFHTTPAuthenticationCreateFromResponse(0, proxyResponse)); |
| |
| if (!CFHTTPAuthenticationRequiresUserNameAndPassword(authentication.get())) { |
| // That's all we can offer... |
| m_client.didFailSocketStream(*this, SocketStreamError(0, m_url.string(), "Proxy authentication scheme is not supported for WebSockets")); |
| return; |
| } |
| |
| int port = 0; |
| CFNumberGetValue(m_proxyPort.get(), kCFNumberIntType, &port); |
| RetainPtr<CFStringRef> methodCF = adoptCF(CFHTTPAuthenticationCopyMethod(authentication.get())); |
| RetainPtr<CFStringRef> realmCF = adoptCF(CFHTTPAuthenticationCopyRealm(authentication.get())); |
| |
| if (!methodCF || !realmCF) { |
| // This shouldn't happen, but on some OS versions we get incomplete authentication data, see <rdar://problem/10416316>. |
| m_client.didFailSocketStream(*this, SocketStreamError(0, m_url.string(), "WebSocket proxy authentication couldn't be handled")); |
| return; |
| } |
| |
| ProtectionSpace protectionSpace(String(m_proxyHost.get()), port, ProtectionSpaceProxyHTTPS, String(realmCF.get()), authenticationSchemeFromAuthenticationMethod(methodCF.get())); |
| String login; |
| String password; |
| if (!m_sentStoredCredentials && getStoredCONNECTProxyCredentials(protectionSpace, login, password)) { |
| // Try to apply stored credentials, if we haven't tried those already. |
| // Create a temporary request to make CFNetwork apply credentials to it. Unfortunately, this cannot work with NTLM authentication. |
| RetainPtr<CFHTTPMessageRef> dummyRequest = adoptCF(CFHTTPMessageCreateRequest(0, CFSTR("GET"), m_httpsURL.get(), kCFHTTPVersion1_1)); |
| |
| Boolean appliedCredentials = CFHTTPMessageApplyCredentials(dummyRequest.get(), authentication.get(), login.createCFString().get(), password.createCFString().get(), 0); |
| ASSERT_UNUSED(appliedCredentials, appliedCredentials); |
| |
| RetainPtr<CFStringRef> proxyAuthorizationString = adoptCF(CFHTTPMessageCopyHeaderFieldValue(dummyRequest.get(), CFSTR("Proxy-Authorization"))); |
| |
| if (!proxyAuthorizationString) { |
| // Fails e.g. for NTLM auth. |
| m_client.didFailSocketStream(*this, SocketStreamError(0, m_url.string(), "Proxy authentication scheme is not supported for WebSockets")); |
| return; |
| } |
| |
| // Setting the authorization results in a new connection attempt. |
| setCONNECTProxyAuthorizationForStream(m_readStream.get(), proxyAuthorizationString.get()); |
| m_sentStoredCredentials = true; |
| return; |
| } |
| |
| // FIXME: On platforms where AuthBrokerAgent is not available, ask the client if credentials could not be found. |
| |
| m_client.didFailSocketStream(*this, SocketStreamError(0, m_url.string(), "Proxy credentials are not available")); |
| } |
| |
| CFStringRef SocketStreamHandleImpl::copyCFStreamDescription(void* info) |
| { |
| SocketStreamHandleImpl* handle = static_cast<SocketStreamHandleImpl*>(info); |
| return String("WebKit socket stream, " + handle->m_url.string()).createCFString().leakRef(); |
| } |
| |
| void SocketStreamHandleImpl::readStreamCallback(CFReadStreamRef stream, CFStreamEventType type, void* clientCallBackInfo) |
| { |
| SocketStreamHandleImpl* handle = static_cast<SocketStreamHandleImpl*>(clientCallBackInfo); |
| ASSERT_UNUSED(stream, stream == handle->m_readStream.get()); |
| // Workaround for <rdar://problem/17727073>. Keeping this below the assertion as we'd like better steps to reproduce this. |
| if (!handle->m_readStream) |
| return; |
| |
| RefPtr<SocketStreamHandle> protector(handle); |
| callOnMainThreadAndWait([&] { |
| if (handle->m_readStream) |
| handle->readStreamCallback(type); |
| }); |
| } |
| |
| void SocketStreamHandleImpl::writeStreamCallback(CFWriteStreamRef stream, CFStreamEventType type, void* clientCallBackInfo) |
| { |
| SocketStreamHandleImpl* handle = static_cast<SocketStreamHandleImpl*>(clientCallBackInfo); |
| ASSERT_UNUSED(stream, stream == handle->m_writeStream.get()); |
| // This wasn't seen happening in practice, yet it seems like it could, due to symmetry with read stream callback. |
| if (!handle->m_writeStream) |
| return; |
| |
| RefPtr<SocketStreamHandle> protector(handle); |
| callOnMainThreadAndWait([&] { |
| if (handle->m_writeStream) |
| handle->writeStreamCallback(type); |
| }); |
| } |
| |
| #if !PLATFORM(IOS_FAMILY) |
| static void setResponseProxyURL(CFHTTPMessageRef message, CFURLRef proxyURL) |
| { |
| #if PLATFORM(WIN) |
| if (_CFHTTPMessageSetResponseProxyURLPtr()) |
| _CFHTTPMessageSetResponseProxyURLPtr()(message, proxyURL); |
| #else |
| _CFHTTPMessageSetResponseProxyURL(message, proxyURL); |
| #endif |
| } |
| #endif |
| |
| static RetainPtr<CFHTTPMessageRef> copyCONNECTProxyResponse(CFReadStreamRef stream, CFURLRef responseURL, CFStringRef proxyHost, CFNumberRef proxyPort) |
| { |
| auto message = adoptCF(checked_cf_cast<CFHTTPMessageRef>(CFReadStreamCopyProperty(stream, kCFStreamPropertyCONNECTResponse))); |
| // CFNetwork needs URL to be set on response in order to handle authentication - even though it doesn't seem to make sense to provide ultimate target URL when authenticating to a proxy. |
| // This is set by CFNetwork internally for normal HTTP responses, but not for proxies. |
| _CFHTTPMessageSetResponseURL(message.get(), responseURL); |
| |
| #if !PLATFORM(IOS_FAMILY) |
| // Ditto for proxy URL. |
| auto proxyURLString = adoptCF(CFStringCreateWithFormat(kCFAllocatorDefault, nullptr, CFSTR("https://%@:%@"), proxyHost, proxyPort)); |
| auto proxyURL = adoptCF(CFURLCreateWithString(kCFAllocatorDefault, proxyURLString.get(), nullptr)); |
| setResponseProxyURL(message.get(), proxyURL.get()); |
| #else |
| UNUSED_PARAM(proxyHost); |
| UNUSED_PARAM(proxyPort); |
| #endif |
| |
| return message; |
| } |
| |
| void SocketStreamHandleImpl::readStreamCallback(CFStreamEventType type) |
| { |
| switch (type) { |
| case kCFStreamEventNone: |
| return; |
| case kCFStreamEventOpenCompleted: |
| return; |
| case kCFStreamEventHasBytesAvailable: { |
| if (m_connectingSubstate == WaitingForCredentials) |
| return; |
| |
| if (m_connectingSubstate == WaitingForConnect) { |
| if (m_connectionType == CONNECTProxy) { |
| RetainPtr<CFHTTPMessageRef> proxyResponse = copyCONNECTProxyResponse(m_readStream.get(), m_httpsURL.get(), m_proxyHost.get(), m_proxyPort.get()); |
| if (!proxyResponse) |
| return; |
| |
| CFIndex proxyResponseCode = CFHTTPMessageGetResponseStatusCode(proxyResponse.get()); |
| switch (proxyResponseCode) { |
| case 200: |
| // Successful connection. |
| break; |
| case 407: |
| addCONNECTCredentials(proxyResponse.get()); |
| return; |
| default: |
| m_client.didFailSocketStream(*this, SocketStreamError(static_cast<int>(proxyResponseCode), m_url.string(), "Proxy connection could not be established, unexpected response code")); |
| platformClose(); |
| return; |
| } |
| } |
| RELEASE_LOG(Network, "SocketStreamHandleImpl::readStreamCallback - m_connectionSubState is Connected"); |
| m_connectingSubstate = Connected; |
| m_state = Open; |
| m_client.didOpenSocketStream(*this); |
| } |
| |
| // Not an "else if", we could have made a client call above, and it could close the connection. |
| if (m_state == Closed) |
| return; |
| |
| ASSERT(m_state == Open); |
| ASSERT(m_connectingSubstate == Connected); |
| |
| CFIndex length; |
| UInt8 localBuffer[1024]; // Used if CFReadStreamGetBuffer couldn't return anything. |
| const UInt8* ptr = CFReadStreamGetBuffer(m_readStream.get(), 0, &length); |
| if (!ptr) { |
| length = CFReadStreamRead(m_readStream.get(), localBuffer, sizeof(localBuffer)); |
| ptr = localBuffer; |
| } |
| |
| if (!length) |
| return; |
| |
| if (length == -1) |
| m_client.didFailToReceiveSocketStreamData(*this); |
| else |
| m_client.didReceiveSocketStreamData(*this, reinterpret_cast<const char*>(ptr), length); |
| |
| return; |
| } |
| case kCFStreamEventCanAcceptBytes: |
| ASSERT_NOT_REACHED(); |
| return; |
| case kCFStreamEventErrorOccurred: { |
| RetainPtr<CFErrorRef> error = adoptCF(CFReadStreamCopyError(m_readStream.get())); |
| reportErrorToClient(error.get()); |
| return; |
| } |
| case kCFStreamEventEndEncountered: |
| platformClose(); |
| return; |
| } |
| } |
| |
| void SocketStreamHandleImpl::writeStreamCallback(CFStreamEventType type) |
| { |
| switch (type) { |
| case kCFStreamEventNone: |
| return; |
| case kCFStreamEventOpenCompleted: |
| return; |
| case kCFStreamEventHasBytesAvailable: |
| ASSERT_NOT_REACHED(); |
| return; |
| case kCFStreamEventCanAcceptBytes: { |
| // Can be false if read stream callback just decided to retry a CONNECT with credentials. |
| if (!CFWriteStreamCanAcceptBytes(m_writeStream.get())) |
| return; |
| |
| if (m_connectingSubstate == WaitingForCredentials) |
| return; |
| |
| if (m_connectingSubstate == WaitingForConnect) { |
| if (m_connectionType == CONNECTProxy) { |
| RetainPtr<CFHTTPMessageRef> proxyResponse = copyCONNECTProxyResponse(m_readStream.get(), m_httpsURL.get(), m_proxyHost.get(), m_proxyPort.get()); |
| if (!proxyResponse) |
| return; |
| |
| // Don't write anything until read stream callback has dealt with CONNECT credentials. |
| // The order of callbacks is not defined, so this can be called before readStreamCallback's kCFStreamEventHasBytesAvailable. |
| CFIndex proxyResponseCode = CFHTTPMessageGetResponseStatusCode(proxyResponse.get()); |
| if (proxyResponseCode != 200) |
| return; |
| } |
| m_connectingSubstate = Connected; |
| m_state = Open; |
| m_client.didOpenSocketStream(*this); |
| } |
| |
| // Not an "else if", we could have made a client call above, and it could close the connection. |
| if (m_state == Closed) |
| return; |
| |
| ASSERT(m_state == Open); |
| ASSERT(m_connectingSubstate == Connected); |
| |
| sendPendingData(); |
| return; |
| } |
| case kCFStreamEventErrorOccurred: { |
| RetainPtr<CFErrorRef> error = adoptCF(CFWriteStreamCopyError(m_writeStream.get())); |
| reportErrorToClient(error.get()); |
| return; |
| } |
| case kCFStreamEventEndEncountered: |
| // FIXME: Currently, we handle closing in read callback, but these can come independently (e.g. a server can stop listening, but keep sending data). |
| return; |
| } |
| } |
| |
| void SocketStreamHandleImpl::reportErrorToClient(CFErrorRef error) |
| { |
| CFIndex errorCode = CFErrorGetCode(error); |
| String description; |
| |
| #if PLATFORM(MAC) |
| |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| |
| if (CFEqual(CFErrorGetDomain(error), kCFErrorDomainOSStatus)) { |
| const char* descriptionOSStatus = GetMacOSStatusCommentString(static_cast<OSStatus>(errorCode)); |
| if (descriptionOSStatus && descriptionOSStatus[0] != '\0') |
| description = makeString("OSStatus Error ", errorCode, ": ", descriptionOSStatus); |
| } |
| |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| |
| #endif |
| |
| if (description.isNull()) { |
| RetainPtr<CFStringRef> descriptionCF = adoptCF(CFErrorCopyDescription(error)); |
| description = String(descriptionCF.get()); |
| } |
| |
| m_client.didFailSocketStream(*this, SocketStreamError(static_cast<int>(errorCode), m_url.string(), description)); |
| } |
| |
| SocketStreamHandleImpl::~SocketStreamHandleImpl() |
| { |
| LOG(Network, "SocketStreamHandle %p dtor", this); |
| |
| ASSERT(!m_pacRunLoopSource); |
| } |
| |
| Optional<size_t> SocketStreamHandleImpl::platformSendInternal(const uint8_t* data, size_t length) |
| { |
| if (!m_writeStream) |
| return 0; |
| |
| if (!CFWriteStreamCanAcceptBytes(m_writeStream.get())) |
| return 0; |
| |
| CFIndex result = CFWriteStreamWrite(m_writeStream.get(), reinterpret_cast<const UInt8*>(data), length); |
| if (result == -1) |
| return WTF::nullopt; |
| |
| ASSERT(result >= 0); |
| return static_cast<size_t>(result); |
| } |
| |
| void SocketStreamHandleImpl::platformClose() |
| { |
| LOG(Network, "SocketStreamHandle %p platformClose", this); |
| |
| if (m_pacRunLoopSource) |
| removePACRunLoopSource(); |
| |
| ASSERT(!m_readStream == !m_writeStream); |
| if (!m_readStream) { |
| if (m_connectingSubstate == New || m_connectingSubstate == ExecutingPACFile) |
| m_client.didCloseSocketStream(*this); |
| return; |
| } |
| |
| CFReadStreamUnscheduleFromRunLoop(m_readStream.get(), callbacksRunLoop(), callbacksRunLoopMode()); |
| CFWriteStreamUnscheduleFromRunLoop(m_writeStream.get(), callbacksRunLoop(), callbacksRunLoopMode()); |
| |
| CFReadStreamClose(m_readStream.get()); |
| CFWriteStreamClose(m_writeStream.get()); |
| |
| m_readStream = nullptr; |
| m_writeStream = nullptr; |
| |
| m_client.didCloseSocketStream(*this); |
| } |
| |
| unsigned short SocketStreamHandleImpl::port() const |
| { |
| if (auto urlPort = m_url.port()) |
| return urlPort.value(); |
| if (shouldUseSSL()) |
| return 443; |
| return 80; |
| } |
| |
| } // namespace WebCore |