From 64429de422b95d49d1c2df570285c8bdfac7b57e Mon Sep 17 00:00:00 2001 From: saiHemak Date: Fri, 10 Nov 2017 12:09:41 +0530 Subject: [PATCH 01/11] Deleting EasyHandle,MultiHandle and libcurlHelpers from URLSession/http Moved lib curl specific code [libcurlHelpers.swift,EasyHandle.swift and MultiHandle.swift] from URLSession/http to URLSession/libcurl libcurl abstractions like EasyHandle and MultiHandle are not specific to HTTP.Hence moved out of the HTTP folder to a common licurl folder --- Foundation/URLSession/http/MultiHandle.swift | 459 ------------------ .../URLSession/http/libcurlHelpers.swift | 50 -- 2 files changed, 509 deletions(-) delete mode 100644 Foundation/URLSession/http/MultiHandle.swift delete mode 100644 Foundation/URLSession/http/libcurlHelpers.swift diff --git a/Foundation/URLSession/http/MultiHandle.swift b/Foundation/URLSession/http/MultiHandle.swift deleted file mode 100644 index 4998899bc3..0000000000 --- a/Foundation/URLSession/http/MultiHandle.swift +++ /dev/null @@ -1,459 +0,0 @@ -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See http://swift.org/LICENSE.txt for license information -// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -// ----------------------------------------------------------------------------- -/// -/// libcurl *multi handle* wrapper. -/// These are libcurl helpers for the URLSession API code. -/// - SeeAlso: https://curl.haxx.se/libcurl/c/ -/// - SeeAlso: URLSession.swift -/// -// ----------------------------------------------------------------------------- - -import CoreFoundation -import Dispatch - - - -extension URLSession { - /// Minimal wrapper around [curl multi interface](https://curl.haxx.se/libcurl/c/libcurl-multi.html). - /// - /// The the *multi handle* manages the sockets for easy handles - /// (`_EasyHandle`), and this implementation uses - /// libdispatch to listen for sockets being read / write ready. - /// - /// Using `DispatchSource` allows this implementation to be - /// non-blocking and all code to run on the same thread / - /// `DispatchQueue` -- thus keeping is simple. - /// - /// - SeeAlso: _EasyHandle - internal final class _MultiHandle { - let rawHandle = CFURLSessionMultiHandleInit() - let queue: DispatchQueue - let group = DispatchGroup() - fileprivate var easyHandles: [_EasyHandle] = [] - fileprivate var timeoutSource: _TimeoutSource? = nil - - init(configuration: URLSession._Configuration, workQueue: DispatchQueue) { - queue = DispatchQueue(label: "MultiHandle.isolation", target: workQueue) - setupCallbacks() - configure(with: configuration) - } - deinit { - // C.f.: - easyHandles.forEach { - try! CFURLSessionMultiHandleRemoveHandle(rawHandle, $0.rawHandle).asError() - } - try! CFURLSessionMultiHandleDeinit(rawHandle).asError() - } - } -} - -extension URLSession._MultiHandle { - func configure(with configuration: URLSession._Configuration) { - try! CFURLSession_multi_setopt_l(rawHandle, CFURLSessionMultiOptionMAX_HOST_CONNECTIONS, configuration.httpMaximumConnectionsPerHost).asError() - try! CFURLSession_multi_setopt_l(rawHandle, CFURLSessionMultiOptionPIPELINING, configuration.httpShouldUsePipelining ? 3 : 2).asError() - //TODO: We may want to set - // CFURLSessionMultiOptionMAXCONNECTS - // CFURLSessionMultiOptionMAX_TOTAL_CONNECTIONS - } -} - -fileprivate extension URLSession._MultiHandle { - static func from(callbackUserData userdata: UnsafeMutableRawPointer?) -> URLSession._MultiHandle? { - guard let userdata = userdata else { return nil } - return Unmanaged.fromOpaque(userdata).takeUnretainedValue() - } -} - -fileprivate extension URLSession._MultiHandle { - /// Forward the libcurl callbacks into Swift methods - func setupCallbacks() { - // Socket - try! CFURLSession_multi_setopt_ptr(rawHandle, CFURLSessionMultiOptionSOCKETDATA, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())).asError() - try! CFURLSession_multi_setopt_sf(rawHandle, CFURLSessionMultiOptionSOCKETFUNCTION) { (easyHandle: CFURLSessionEasyHandle, socket: CFURLSession_socket_t, what: Int32, userdata: UnsafeMutableRawPointer?, socketptr: UnsafeMutableRawPointer?) -> Int32 in - guard let handle = URLSession._MultiHandle.from(callbackUserData: userdata) else { fatalError() } - return handle.register(socket: socket, for: easyHandle, what: what, socketSourcePtr: socketptr) - }.asError() - // Timeout: - try! CFURLSession_multi_setopt_ptr(rawHandle, CFURLSessionMultiOptionTIMERDATA, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())).asError() - try! CFURLSession_multi_setopt_tf(rawHandle, CFURLSessionMultiOptionTIMERFUNCTION) { (_, timeout: Int, userdata: UnsafeMutableRawPointer?) -> Int32 in - guard let handle = URLSession._MultiHandle.from(callbackUserData: userdata) else { fatalError() } - handle.updateTimeoutTimer(to: timeout) - return 0 - }.asError() - } - /// and - /// - func register(socket: CFURLSession_socket_t, for easyHandle: CFURLSessionEasyHandle, what: Int32, socketSourcePtr: UnsafeMutableRawPointer?) -> Int32 { - // We get this callback whenever we need to register or unregister a - // given socket with libdispatch. - // The `action` / `what` defines if we should register or unregister - // that we're interested in read and/or write readiness. We will do so - // through libdispatch (DispatchSource) and store the source(s) inside - // a `SocketSources` which we in turn store inside libcurl's multi handle - // by means of curl_multi_assign() -- we retain the object fist. - let action = _SocketRegisterAction(rawValue: CFURLSessionPoll(value: what)) - var socketSources = _SocketSources.from(socketSourcePtr: socketSourcePtr) - if socketSources == nil && action.needsSource { - let s = _SocketSources() - let p = Unmanaged.passRetained(s).toOpaque() - CFURLSessionMultiHandleAssign(rawHandle, socket, UnsafeMutableRawPointer(p)) - socketSources = s - } else if socketSources != nil && action == .unregister { - // We need to release the stored pointer: - if let opaque = socketSourcePtr { - Unmanaged<_SocketSources>.fromOpaque(opaque).release() - } - socketSources = nil - } - if let ss = socketSources { - let handler = DispatchWorkItem { [weak self] in - self?.performAction(for: socket) - } - ss.createSources(with: action, fileDescriptor: Int(socket), queue: queue, handler: handler) - } - return 0 - } - - /// What read / write ready event to register / unregister. - /// - /// This re-maps `CFURLSessionPoll` / `CURL_POLL`. - enum _SocketRegisterAction { - case none - case registerRead - case registerWrite - case registerReadAndWrite - case unregister - } -} - -internal extension URLSession._MultiHandle { - /// Add an easy handle -- start its transfer. - func add(_ handle: _EasyHandle) { - // If this is the first handle being added, we need to `kick` the - // underlying multi handle by calling `timeoutTimerFired` as - // described in - // . - // That will initiate the registration for timeout timer and socket - // readiness. - let needsTimeout = self.easyHandles.isEmpty - self.easyHandles.append(handle) - try! CFURLSessionMultiHandleAddHandle(self.rawHandle, handle.rawHandle).asError() - if needsTimeout { - self.timeoutTimerFired() - } - } - /// Remove an easy handle -- stop its transfer. - func remove(_ handle: _EasyHandle) { - guard let idx = self.easyHandles.index(of: handle) else { - fatalError("Handle not in list.") - } - self.easyHandles.remove(at: idx) - try! CFURLSessionMultiHandleRemoveHandle(self.rawHandle, handle.rawHandle).asError() - } -} - -fileprivate extension URLSession._MultiHandle { - /// This gets called when we should ask curl to perform action on a socket. - func performAction(for socket: CFURLSession_socket_t) { - try! readAndWriteAvailableData(on: socket) - } - /// This gets called when our timeout timer fires. - /// - /// libcurl relies on us calling curl_multi_socket_action() every now and then. - func timeoutTimerFired() { - try! readAndWriteAvailableData(on: CFURLSessionSocketTimeout) - } - /// reads/writes available data given an action - func readAndWriteAvailableData(on socket: CFURLSession_socket_t) throws { - var runningHandlesCount = Int32(0) - try CFURLSessionMultiHandleAction(rawHandle, socket, 0, &runningHandlesCount).asError() - //TODO: Do we remove the timeout timer here if / when runningHandles == 0 ? - readMessages() - } - - /// Check the status of all individual transfers. - /// - /// libcurl refers to this as “read multi stack informationals”. - /// Check for transfers that completed. - func readMessages() { - // We pop the messages one by one in a loop: - repeat { - // count will contain the messages left in the queue - var count = Int32(0) - let info = CFURLSessionMultiHandleInfoRead(rawHandle, &count) - guard let handle = info.easyHandle else { break } - let code = info.resultCode - completedTransfer(forEasyHandle: handle, easyCode: code) - } while true - } - /// Transfer completed. - func completedTransfer(forEasyHandle handle: CFURLSessionEasyHandle, easyCode: CFURLSessionEasyCode) { - // Look up the matching wrapper: - guard let idx = easyHandles.index(where: { $0.rawHandle == handle }) else { - fatalError("Tansfer completed for easy handle, but it is not in the list of added handles.") - } - let easyHandle = easyHandles[idx] - // Find the NSURLError code - var error: NSError? - if let errorCode = easyHandle.urlErrorCode(for: easyCode) { - let errorDescription = easyHandle.errorBuffer[0] != 0 ? - String(cString: easyHandle.errorBuffer) : - CFURLSessionCreateErrorDescription(easyCode.value)._swiftObject - error = NSError(domain: NSURLErrorDomain, code: errorCode, userInfo: [ - NSLocalizedDescriptionKey: errorDescription - ]) - } - completedTransfer(forEasyHandle: easyHandle, error: error) - } - /// Transfer completed. - func completedTransfer(forEasyHandle handle: _EasyHandle, error: NSError?) { - handle.completedTransfer(withError: error) - } -} - -fileprivate extension _EasyHandle { - /// An error code within the `NSURLErrorDomain` based on the error of the - /// easy handle. - /// - Note: The error value is set only on failure. You can't use it to - /// determine *if* something failed or not, only *why* it failed. - func urlErrorCode(for easyCode: CFURLSessionEasyCode) -> Int? { - switch (easyCode, CInt(connectFailureErrno)) { - case (CFURLSessionEasyCodeOK, _): - return nil - case (_, ECONNREFUSED): - return NSURLErrorCannotConnectToHost - case (CFURLSessionEasyCodeUNSUPPORTED_PROTOCOL, _): - return NSURLErrorUnsupportedURL - case (CFURLSessionEasyCodeURL_MALFORMAT, _): - return NSURLErrorBadURL - case (CFURLSessionEasyCodeCOULDNT_RESOLVE_HOST, _): - // Oddly, this appears to happen for malformed URLs, too. - return NSURLErrorCannotFindHost - case (CFURLSessionEasyCodeRECV_ERROR, ECONNRESET): - return NSURLErrorNetworkConnectionLost - case (CFURLSessionEasyCodeSEND_ERROR, ECONNRESET): - return NSURLErrorNetworkConnectionLost - case (CFURLSessionEasyCodeGOT_NOTHING, _): - return NSURLErrorBadServerResponse - case (CFURLSessionEasyCodeABORTED_BY_CALLBACK, _): - return NSURLErrorUnknown // Or NSURLErrorCancelled if we're in such a state - case (CFURLSessionEasyCodeCOULDNT_CONNECT, ETIMEDOUT): - return NSURLErrorTimedOut - case (CFURLSessionEasyCodeOPERATION_TIMEDOUT, _): - return NSURLErrorTimedOut - default: - //TODO: Need to map to one of the NSURLError... constants - return NSURLErrorUnknown - } - } -} - -fileprivate extension URLSession._MultiHandle._SocketRegisterAction { - init(rawValue: CFURLSessionPoll) { - switch rawValue { - case CFURLSessionPollNone: - self = .none - case CFURLSessionPollIn: - self = .registerRead - case CFURLSessionPollOut: - self = .registerWrite - case CFURLSessionPollInOut: - self = .registerReadAndWrite - case CFURLSessionPollRemove: - self = .unregister - default: - fatalError("Invalid CFURLSessionPoll value.") - } - } -} -extension CFURLSessionPoll : Equatable { - public static func ==(lhs: CFURLSessionPoll, rhs: CFURLSessionPoll) -> Bool { - return lhs.value == rhs.value - } -} -fileprivate extension URLSession._MultiHandle._SocketRegisterAction { - /// Should a libdispatch source be registered for **read** readiness? - var needsReadSource: Bool { - switch self { - case .none: return false - case .registerRead: return true - case .registerWrite: return false - case .registerReadAndWrite: return true - case .unregister: return false - } - } - /// Should a libdispatch source be registered for **write** readiness? - var needsWriteSource: Bool { - switch self { - case .none: return false - case .registerRead: return false - case .registerWrite: return true - case .registerReadAndWrite: return true - case .unregister: return false - } - } - /// Should either a **read** or a **write** readiness libdispatch source be - /// registered? - var needsSource: Bool { - return needsReadSource || needsWriteSource - } -} - -/// A helper class that wraps a libdispatch timer. -/// -/// Used to implement the timeout of `URLSession.MultiHandle` and `URLSession.EasyHandle` -class _TimeoutSource { - let rawSource: DispatchSource - let milliseconds: Int - let queue: DispatchQueue //needed to restart the timer for EasyHandles - let handler: DispatchWorkItem //needed to restart the timer for EasyHandles - init(queue: DispatchQueue, milliseconds: Int, handler: DispatchWorkItem) { - self.queue = queue - self.handler = handler - self.milliseconds = milliseconds - self.rawSource = DispatchSource.makeTimerSource(queue: queue) as! DispatchSource - - let delay = UInt64(max(1, milliseconds - 1)) - let start = DispatchTime.now() + DispatchTimeInterval.milliseconds(Int(delay)) - - rawSource.schedule(deadline: start, repeating: .milliseconds(Int(delay)), leeway: (milliseconds == 1) ? .microseconds(Int(1)) : .milliseconds(Int(1))) - rawSource.setEventHandler(handler: handler) - rawSource.resume() - } - deinit { - rawSource.cancel() - } -} - -fileprivate extension URLSession._MultiHandle { - - /// - func updateTimeoutTimer(to value: Int) { - updateTimeoutTimer(to: _Timeout(timeout: value)) - } - - func updateTimeoutTimer(to timeout: _Timeout) { - // Set up a timeout timer based on the given value: - switch timeout { - case .none: - timeoutSource = nil - case .immediate: - timeoutSource = nil - timeoutTimerFired() - case .milliseconds(let milliseconds): - if (timeoutSource == nil) || timeoutSource!.milliseconds != milliseconds { - //TODO: Could simply change the existing timer by calling - // dispatch_source_set_timer() again. - let block = DispatchWorkItem { [weak self] in - self?.timeoutTimerFired() - } - timeoutSource = _TimeoutSource(queue: queue, milliseconds: milliseconds, handler: block) - } - } - } - enum _Timeout { - case milliseconds(Int) - case none - case immediate - } -} - -fileprivate extension URLSession._MultiHandle._Timeout { - init(timeout: Int) { - switch timeout { - case -1: - self = .none - case 0: - self = .immediate - default: - self = .milliseconds(timeout) - } - } -} - - -/// Read and write libdispatch sources for a specific socket. -/// -/// A simple helper that combines two sources -- both being optional. -/// -/// This info is stored into the socket using `curl_multi_assign()`. -/// -/// - SeeAlso: URLSession.MultiHandle.SocketRegisterAction -fileprivate class _SocketSources { - var readSource: DispatchSource? - var writeSource: DispatchSource? - - func createReadSource(fileDescriptor fd: Int, queue: DispatchQueue, handler: DispatchWorkItem) { - guard readSource == nil else { return } - let s = DispatchSource.makeReadSource(fileDescriptor: Int32(fd), queue: queue) - s.setEventHandler(handler: handler) - readSource = s as? DispatchSource - s.resume() - } - - func createWriteSource(fileDescriptor fd: Int, queue: DispatchQueue, handler: DispatchWorkItem) { - guard writeSource == nil else { return } - let s = DispatchSource.makeWriteSource(fileDescriptor: Int32(fd), queue: queue) - s.setEventHandler(handler: handler) - writeSource = s as? DispatchSource - s.resume() - } - - func tearDown() { - if let s = readSource { - s.cancel() - } - readSource = nil - if let s = writeSource { - s.cancel() - } - writeSource = nil - } -} -extension _SocketSources { - /// Create a read and/or write source as specified by the action. - func createSources(with action: URLSession._MultiHandle._SocketRegisterAction, fileDescriptor fd: Int, queue: DispatchQueue, handler: DispatchWorkItem) { - if action.needsReadSource { - createReadSource(fileDescriptor: fd, queue: queue, handler: handler) - } - if action.needsWriteSource { - createWriteSource(fileDescriptor: fd, queue: queue, handler: handler) - } - } -} -extension _SocketSources { - /// Unwraps the `SocketSources` - /// - /// A `SocketSources` is stored into the multi handle's socket using - /// `curl_multi_assign()`. This helper unwraps it from the returned - /// `UnsafeMutablePointer`. - static func from(socketSourcePtr ptr: UnsafeMutableRawPointer?) -> _SocketSources? { - guard let ptr = ptr else { return nil } - return Unmanaged<_SocketSources>.fromOpaque(ptr).takeUnretainedValue() - } -} - - -extension CFURLSessionMultiCode : Equatable { - public static func ==(lhs: CFURLSessionMultiCode, rhs: CFURLSessionMultiCode) -> Bool { - return lhs.value == rhs.value - } -} -extension CFURLSessionMultiCode : Error { - public var _domain: String { return "libcurl.Multi" } - public var _code: Int { return Int(self.value) } -} -internal extension CFURLSessionMultiCode { - func asError() throws { - if self == CFURLSessionMultiCodeOK { return } - throw self - } -} diff --git a/Foundation/URLSession/http/libcurlHelpers.swift b/Foundation/URLSession/http/libcurlHelpers.swift deleted file mode 100644 index 7820f9c92a..0000000000 --- a/Foundation/URLSession/http/libcurlHelpers.swift +++ /dev/null @@ -1,50 +0,0 @@ -// Foundation/URLSession/libcurlHelpers - URLSession & libcurl -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See http://swift.org/LICENSE.txt for license information -// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -// ----------------------------------------------------------------------------- -/// -/// These are libcurl helpers for the URLSession API code. -/// - SeeAlso: https://curl.haxx.se/libcurl/c/ -/// - SeeAlso: URLSession.swift -/// -// ----------------------------------------------------------------------------- - - -import CoreFoundation - - -//TODO: Move things in this file? - - -internal func initializeLibcurl() { - try! CFURLSessionInit().asError() -} - - -internal extension String { - /// Create a string by a buffer of UTF 8 code points that is not zero - /// terminated. - init?(utf8Buffer: UnsafeBufferPointer) { - var bufferIterator = utf8Buffer.makeIterator() - var codec = UTF8() - var result: String = "" - iter: repeat { - switch codec.decode(&bufferIterator) { - case .scalarValue(let scalar): - result.append(String(describing: scalar)) - case .error: - return nil - case .emptyInput: - break iter - } - } while true - self.init(stringLiteral: result) - } -} From a44a9f95b82b133eb0f07a1f64359ef851058713 Mon Sep 17 00:00:00 2001 From: saiHemak Date: Fri, 10 Nov 2017 12:10:58 +0530 Subject: [PATCH 02/11] Add EasyHandle,MultiHandle and libcurlHelpers to a common libcurl directory Added libcurl abstractions to libcurl directory --- .../URLSession/libcurl/EasyHandle.swift | 646 ++++++++++++++++++ .../URLSession/libcurl/MultiHandle.swift | 459 +++++++++++++ .../URLSession/libcurl/libcurlHelpers.swift | 50 ++ 3 files changed, 1155 insertions(+) create mode 100644 Foundation/URLSession/libcurl/EasyHandle.swift create mode 100644 Foundation/URLSession/libcurl/MultiHandle.swift create mode 100644 Foundation/URLSession/libcurl/libcurlHelpers.swift diff --git a/Foundation/URLSession/libcurl/EasyHandle.swift b/Foundation/URLSession/libcurl/EasyHandle.swift new file mode 100644 index 0000000000..02ae3d2147 --- /dev/null +++ b/Foundation/URLSession/libcurl/EasyHandle.swift @@ -0,0 +1,646 @@ +// Foundation/URLSession/EasyHandle.swift - URLSession & libcurl +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +// ----------------------------------------------------------------------------- +/// +/// libcurl *easy handle* wrapper. +/// These are libcurl helpers for the URLSession API code. +/// - SeeAlso: https://curl.haxx.se/libcurl/c/ +/// - SeeAlso: URLSession.swift +/// +// ----------------------------------------------------------------------------- + +import CoreFoundation +import Dispatch + + + +/// Minimal wrapper around the [curl easy interface](https://curl.haxx.se/libcurl/c/) +/// +/// An *easy handle* manages the state of a transfer inside libcurl. +/// +/// As such the easy handle's responsibility is implementing the HTTP +/// protocol while the *multi handle* is in charge of managing sockets and +/// reading from / writing to these sockets. +/// +/// An easy handle is added to a multi handle in order to associate it with +/// an actual socket. The multi handle will then feed bytes into the easy +/// handle and read bytes from the easy handle. But this process is opaque +/// to use. It is further worth noting, that with HTTP/1.1 persistent +/// connections and with HTTP/2 there's a 1-to-many relationship between +/// TCP streams and HTTP transfers / easy handles. A single TCP stream and +/// its socket may be shared by multiple easy handles. +/// +/// A single HTTP request-response exchange (refered to here as a +/// *transfer*) corresponds directly to an easy handle. Hence anything that +/// needs to be configured for a specific transfer (e.g. the URL) will be +/// configured on an easy handle. +/// +/// A single `URLSessionTask` may do multiple, sonecutive transfers, and +/// as a result it will have to reconfigure it's easy handle between +/// transfers. An easy handle can be re-used once its transfer has +/// completed. +/// +/// - Note: All code assumes that it is being called on a single thread / +/// `Dispatch` only -- it is intentionally **not** thread safe. +internal final class _EasyHandle { + let rawHandle = CFURLSessionEasyHandleInit() + weak var delegate: _EasyHandleDelegate? + fileprivate var headerList: _CurlStringList? + fileprivate var pauseState: _PauseState = [] + internal var timeoutTimer: _TimeoutSource! + internal lazy var errorBuffer = [UInt8](repeating: 0, count: Int(CFURLSessionEasyErrorSize)) + + init(delegate: _EasyHandleDelegate) { + self.delegate = delegate + setupCallbacks() + } + deinit { + CFURLSessionEasyHandleDeinit(rawHandle) + } +} + +extension _EasyHandle: Equatable {} + internal func ==(lhs: _EasyHandle, rhs: _EasyHandle) -> Bool { + return lhs.rawHandle == rhs.rawHandle +} + +extension _EasyHandle { + enum _Action { + case abort + case proceed + case pause + } + enum _WriteBufferResult { + case abort + case pause + /// Write the given number of bytes into the buffer + case bytes(Int) + } +} + +internal extension _EasyHandle { + func completedTransfer(withError error: NSError?) { + delegate?.transferCompleted(withError: error) + } +} +internal protocol _EasyHandleDelegate: class { + /// Handle data read from the network. + /// - returns: the action to be taken: abort, proceed, or pause. + func didReceive(data: Data) -> _EasyHandle._Action + /// Handle header data read from the network. + /// - returns: the action to be taken: abort, proceed, or pause. + func didReceive(headerData data: Data, contentLength: Int64) -> _EasyHandle._Action + /// Fill a buffer with data to be sent. + /// + /// - parameter data: The buffer to fill + /// - returns: the number of bytes written to the `data` buffer, or `nil` to stop the current transfer immediately. + func fill(writeBuffer buffer: UnsafeMutableBufferPointer) -> _EasyHandle._WriteBufferResult + /// The transfer for this handle completed. + /// - parameter errorCode: An NSURLError code, or `nil` if no error occured. + func transferCompleted(withError error: NSError?) + /// Seek the input stream to the given position + func seekInputStream(to position: UInt64) throws + /// Gets called during the transfer to update progress. + func updateProgressMeter(with propgress: _EasyHandle._Progress) +} +extension _EasyHandle { + func set(verboseModeOn flag: Bool) { + try! CFURLSession_easy_setopt_long(rawHandle, CFURLSessionOptionVERBOSE, flag ? 1 : 0).asError() + } + /// - SeeAlso: https://curl.haxx.se/libcurl/c/CFURLSessionOptionDEBUGFUNCTION.html + func set(debugOutputOn flag: Bool, task: URLSessionTask) { + if flag { + try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionDEBUGDATA, UnsafeMutableRawPointer(Unmanaged.passUnretained(task).toOpaque())).asError() + try! CFURLSession_easy_setopt_dc(rawHandle, CFURLSessionOptionDEBUGFUNCTION, printLibcurlDebug(handle:type:data:size:userInfo:)).asError() + } else { + try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionDEBUGDATA, nil).asError() + try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionDEBUGFUNCTION, nil).asError() + } + } + func set(passHeadersToDataStream flag: Bool) { + try! CFURLSession_easy_setopt_long(rawHandle, CFURLSessionOptionHEADER, flag ? 1 : 0).asError() + } + /// Follow any Location: header that the server sends as part of a HTTP header in a 3xx response + func set(followLocation flag: Bool) { + try! CFURLSession_easy_setopt_long(rawHandle, CFURLSessionOptionFOLLOWLOCATION, flag ? 1 : 0).asError() + } + /// Switch off the progress meter. It will also prevent the CFURLSessionOptionPROGRESSFUNCTION from getting called. + func set(progressMeterOff flag: Bool) { + try! CFURLSession_easy_setopt_long(rawHandle, CFURLSessionOptionNOPROGRESS, flag ? 1 : 0).asError() + } + /// Skip all signal handling + /// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_NOSIGNAL.html + func set(skipAllSignalHandling flag: Bool) { + try! CFURLSession_easy_setopt_long(rawHandle, CFURLSessionOptionNOSIGNAL, flag ? 1 : 0).asError() + } + /// Set error buffer for error messages + /// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_ERRORBUFFER.html + func set(errorBuffer buffer: UnsafeMutableBufferPointer?) { + let buffer = buffer ?? errorBuffer.withUnsafeMutableBufferPointer { $0 } + try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionERRORBUFFER, buffer.baseAddress).asError() + } + /// Request failure on HTTP response >= 400 + func set(failOnHTTPErrorCode flag: Bool) { + try! CFURLSession_easy_setopt_long(rawHandle, CFURLSessionOptionFAILONERROR, flag ? 1 : 0).asError() + } + /// URL to use in the request + /// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_URL.html + func set(url: URL) { + url.absoluteString.withCString { + try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionURL, UnsafeMutablePointer(mutating: $0)).asError() + } + } + /// Set allowed protocols + /// + /// - Note: This has security implications. Not limiting this, someone could + /// redirect a HTTP request into one of the many other protocols that libcurl + /// supports. + /// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_PROTOCOLS.html + /// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_REDIR_PROTOCOLS.html + func setAllowedProtocolsToHTTPAndHTTPS() { + let protocols = (CFURLSessionProtocolHTTP | CFURLSessionProtocolHTTPS) + try! CFURLSession_easy_setopt_long(rawHandle, CFURLSessionOptionPROTOCOLS, protocols).asError() + try! CFURLSession_easy_setopt_long(rawHandle, CFURLSessionOptionREDIR_PROTOCOLS, protocols).asError() +#if os(Android) + // See https://curl.haxx.se/docs/sslcerts.html + // For SSL on Android you need a "cacert.pem" to be + // accessible at the path pointed to by this env var. + // Downloadable here: https://curl.haxx.se/ca/cacert.pem + if let caInfo = getenv("URLSessionCertificateAuthorityInfoFile") { + if String(cString: caInfo) == "INSECURE_SSL_NO_VERIFY" { + try! CFURLSession_easy_setopt_long(rawHandle, CFURLSessionOptionSSL_VERIFYPEER, 0).asError() + } + else { + try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionCAINFO, caInfo).asError() + } + } +#endif + //TODO: Added in libcurl 7.45.0 + //TODO: Set default protocol for schemeless URLs + //CURLOPT_DEFAULT_PROTOCOL available only in libcurl 7.45.0 + } + + //TODO: Proxy setting, namely CFURLSessionOptionPROXY, CFURLSessionOptionPROXYPORT, + // CFURLSessionOptionPROXYTYPE, CFURLSessionOptionNOPROXY, CFURLSessionOptionHTTPPROXYTUNNEL, CFURLSessionOptionPROXYHEADER, + // CFURLSessionOptionHEADEROPT, etc. + + /// set preferred receive buffer size + /// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_BUFFERSIZE.html + func set(preferredReceiveBufferSize size: Int) { + try! CFURLSession_easy_setopt_long(rawHandle, CFURLSessionOptionBUFFERSIZE, min(size, Int(CFURLSessionMaxWriteSize))).asError() + } + /// Set custom HTTP headers + /// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_HTTPHEADER.html + func set(customHeaders headers: [String]) { + let list = _CurlStringList(headers) + try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionHTTPHEADER, list.asUnsafeMutablePointer).asError() + // We need to retain the list for as long as the rawHandle is in use. + headerList = list + } + ///TODO: Wait for pipelining/multiplexing. Unavailable on Ubuntu 14.0 + /// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_PIPEWAIT.html + + //TODO: The public API does not allow us to use CFURLSessionOptionSTREAM_DEPENDS / CFURLSessionOptionSTREAM_DEPENDS_E + // Might be good to add support for it, though. + + ///TODO: Set numerical stream weight when CURLOPT_PIPEWAIT is enabled + /// - Parameter weight: values are clamped to lie between 0 and 1 + /// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_STREAM_WEIGHT.html + /// - SeeAlso: http://httpwg.org/specs/rfc7540.html#StreamPriority + + /// Enable automatic decompression of HTTP downloads + /// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_ACCEPT_ENCODING.html + /// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_HTTP_CONTENT_DECODING.html + + func set(automaticBodyDecompression flag: Bool) { + if flag { + "".withCString { + try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionACCEPT_ENCODING, UnsafeMutableRawPointer(mutating: $0)).asError() + } + try! CFURLSession_easy_setopt_long(rawHandle, CFURLSessionOptionHTTP_CONTENT_DECODING, 1).asError() + } else { + try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionACCEPT_ENCODING, nil).asError() + try! CFURLSession_easy_setopt_long(rawHandle, CFURLSessionOptionHTTP_CONTENT_DECODING, 0).asError() + } + } + /// Set request method + /// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_CUSTOMREQUEST.html + func set(requestMethod method: String) { + method.withCString { + try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionCUSTOMREQUEST, UnsafeMutableRawPointer(mutating: $0)).asError() + } + } + + /// Download request without body + /// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_NOBODY.html + func set(noBody flag: Bool) { + try! CFURLSession_easy_setopt_long(rawHandle, CFURLSessionOptionNOBODY, flag ? 1 : 0).asError() + } + /// Enable data upload + /// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_UPLOAD.html + func set(upload flag: Bool) { + try! CFURLSession_easy_setopt_long(rawHandle, CFURLSessionOptionUPLOAD, flag ? 1 : 0).asError() + } + /// Set size of the request body to send + /// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_INFILESIZE_LARGE.html + func set(requestBodyLength length: Int64) { + try! CFURLSession_easy_setopt_int64(rawHandle, CFURLSessionOptionINFILESIZE_LARGE, length).asError() + } + + func set(timeout value: Int) { + try! CFURLSession_easy_setopt_long(rawHandle, CFURLSessionOptionTIMEOUT, value).asError() + } + + func getTimeoutIntervalSpent() -> Double { + var timeSpent = Double() + CFURLSession_easy_getinfo_double(rawHandle, CFURLSessionInfoTOTAL_TIME, &timeSpent) + return timeSpent / 1000 + } + +} + +fileprivate func printLibcurlDebug(handle: CFURLSessionEasyHandle, type: CInt, data: UnsafeMutablePointer, size: Int, userInfo: UnsafeMutableRawPointer?) -> CInt { + // C.f. + let info = CFURLSessionInfo(value: type) + let text = data.withMemoryRebound(to: UInt8.self, capacity: size, { + let buffer = UnsafeBufferPointer(start: $0, count: size) + return String(utf8Buffer: buffer) + }) ?? ""; + + guard let userInfo = userInfo else { return 0 } + let task = Unmanaged.fromOpaque(userInfo).takeUnretainedValue() + printLibcurlDebug(type: info, data: text, task: task) + return 0 +} + +fileprivate func printLibcurlDebug(type: CFURLSessionInfo, data: String, task: URLSessionTask) { + // libcurl sends is data with trailing CRLF which inserts lots of newlines into our output. + NSLog("[\(task.taskIdentifier)] \(type.debugHeader) \(data.mapControlToPictures)") +} + +fileprivate extension String { + /// Replace control characters U+0000 - U+0019 to Control Pictures U+2400 - U+2419 + var mapControlToPictures: String { + let d = self.unicodeScalars.map { (u: UnicodeScalar) -> UnicodeScalar in + switch u.value { + case 0..<0x20: return UnicodeScalar(u.value + 0x2400)! + default: return u + } + } + return String(String.UnicodeScalarView(d)) + } +} + +extension _EasyHandle { + /// Send and/or receive pause state for an `EasyHandle` + struct _PauseState : OptionSet { + let rawValue: Int8 + init(rawValue: Int8) { self.rawValue = rawValue } + static let receivePaused = _PauseState(rawValue: 1 << 0) + static let sendPaused = _PauseState(rawValue: 1 << 1) + } +} +extension _EasyHandle._PauseState { + func setState(on handle: _EasyHandle) { + try! CFURLSessionEasyHandleSetPauseState(handle.rawHandle, contains(.sendPaused) ? 1 : 0, contains(.receivePaused) ? 1 : 0).asError() + } +} +extension _EasyHandle._PauseState : TextOutputStreamable { + func write(to target: inout Target) { + switch (self.contains(.receivePaused), self.contains(.sendPaused)) { + case (false, false): target.write("unpaused") + case (true, false): target.write("receive paused") + case (false, true): target.write("send paused") + case (true, true): target.write("send & receive paused") + } + } +} +extension _EasyHandle { + /// Pause receiving data. + /// + /// - SeeAlso: https://curl.haxx.se/libcurl/c/curl_easy_pause.html + func pauseReceive() { + guard !pauseState.contains(.receivePaused) else { return } + pauseState.insert(.receivePaused) + pauseState.setState(on: self) + } + /// Pause receiving data. + /// + /// - Note: Chances are high that delegate callbacks (with pending data) + /// will be called before this method returns. + /// - SeeAlso: https://curl.haxx.se/libcurl/c/curl_easy_pause.html + func unpauseReceive() { + guard pauseState.contains(.receivePaused) else { return } + pauseState.remove(.receivePaused) + pauseState.setState(on: self) + } + /// Pause sending data. + /// + /// - SeeAlso: https://curl.haxx.se/libcurl/c/curl_easy_pause.html + func pauseSend() { + guard !pauseState.contains(.sendPaused) else { return } + pauseState.insert(.sendPaused) + pauseState.setState(on: self) + } + /// Pause sending data. + /// + /// - Note: Chances are high that delegate callbacks (with pending data) + /// will be called before this method returns. + /// - SeeAlso: https://curl.haxx.se/libcurl/c/curl_easy_pause.html + func unpauseSend() { + guard pauseState.contains(.sendPaused) else { return } + pauseState.remove(.sendPaused) + pauseState.setState(on: self) + } +} + +internal extension _EasyHandle { + /// errno number from last connect failure + /// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLINFO_OS_ERRNO.html + var connectFailureErrno: Int { + var errno = Int() + try! CFURLSession_easy_getinfo_long(rawHandle, CFURLSessionInfoOS_ERRNO, &errno).asError() + return errno + } +} + + +extension CFURLSessionInfo : Equatable { + public static func ==(lhs: CFURLSessionInfo, rhs: CFURLSessionInfo) -> Bool { + return lhs.value == rhs.value + } +} + +extension CFURLSessionInfo { + public var debugHeader: String { + switch self { + case CFURLSessionInfoTEXT: return " " + case CFURLSessionInfoHEADER_OUT: return "=> Send header "; + case CFURLSessionInfoDATA_OUT: return "=> Send data "; + case CFURLSessionInfoSSL_DATA_OUT: return "=> Send SSL data "; + case CFURLSessionInfoHEADER_IN: return "<= Recv header "; + case CFURLSessionInfoDATA_IN: return "<= Recv data "; + case CFURLSessionInfoSSL_DATA_IN: return "<= Recv SSL data "; + default: return " " + } + } +} +extension _EasyHandle { + /// the URL a redirect would go to + /// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLINFO_REDIRECT_URL.html + var redirectURL: URL? { + var p: UnsafeMutablePointer? = nil + try! CFURLSession_easy_getinfo_charp(rawHandle, CFURLSessionInfoREDIRECT_URL, &p).asError() + guard let cstring = p else { return nil } + guard let s = String(cString: cstring, encoding: String.Encoding.utf8) else { return nil } + return URL(string: s) + } +} + +fileprivate extension _EasyHandle { + static func from(callbackUserData userdata: UnsafeMutableRawPointer?) -> _EasyHandle? { + guard let userdata = userdata else { return nil } + return Unmanaged<_EasyHandle>.fromOpaque(userdata).takeUnretainedValue() + } +} + +fileprivate extension _EasyHandle { + + func resetTimer() { + //simply create a new timer with the same queue, timeout and handler + //this must cancel the old handler and reset the timer + timeoutTimer = _TimeoutSource(queue: timeoutTimer.queue, milliseconds: timeoutTimer.milliseconds, handler: timeoutTimer.handler) + } + + /// Forward the libcurl callbacks into Swift methods + func setupCallbacks() { + // write + try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionWRITEDATA, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())).asError() + + try! CFURLSession_easy_setopt_wc(rawHandle, CFURLSessionOptionWRITEFUNCTION) { (data: UnsafeMutablePointer, size: Int, nmemb: Int, userdata: UnsafeMutableRawPointer?) -> Int in + guard let handle = _EasyHandle.from(callbackUserData: userdata) else { return 0 } + defer { + handle.resetTimer() + } + return handle.didReceive(data: data, size: size, nmemb: nmemb) + }.asError() + + // read + try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionREADDATA, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())).asError() + try! CFURLSession_easy_setopt_wc(rawHandle, CFURLSessionOptionREADFUNCTION) { (data: UnsafeMutablePointer, size: Int, nmemb: Int, userdata: UnsafeMutableRawPointer?) -> Int in + guard let handle = _EasyHandle.from(callbackUserData: userdata) else { return 0 } + defer { + handle.resetTimer() + } + return handle.fill(writeBuffer: data, size: size, nmemb: nmemb) + }.asError() + + // header + try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionHEADERDATA, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())).asError() + try! CFURLSession_easy_setopt_wc(rawHandle, CFURLSessionOptionHEADERFUNCTION) { (data: UnsafeMutablePointer, size: Int, nmemb: Int, userdata: UnsafeMutableRawPointer?) -> Int in + guard let handle = _EasyHandle.from(callbackUserData: userdata) else { return 0 } + defer { + handle.resetTimer() + } + var length = Double() + try! CFURLSession_easy_getinfo_double(handle.rawHandle, CFURLSessionInfoCONTENT_LENGTH_DOWNLOAD, &length).asError() + return handle.didReceive(headerData: data, size: size, nmemb: nmemb, contentLength: length) + }.asError() + + // socket options + try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionSOCKOPTDATA, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())).asError() + try! CFURLSession_easy_setopt_sc(rawHandle, CFURLSessionOptionSOCKOPTFUNCTION) { (userdata: UnsafeMutableRawPointer?, fd: CInt, type: CFURLSessionSocketType) -> CInt in + guard let handle = _EasyHandle.from(callbackUserData: userdata) else { return 0 } + guard type == CFURLSessionSocketTypeIPCXN else { return 0 } + do { + try handle.setSocketOptions(for: fd) + return 0 + } catch { + return 1 + } + }.asError() + // seeking in input stream + try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionSEEKDATA, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())).asError() + try! CFURLSession_easy_setopt_seek(rawHandle, CFURLSessionOptionSEEKFUNCTION, { (userdata, offset, origin) -> Int32 in + guard let handle = _EasyHandle.from(callbackUserData: userdata) else { return CFURLSessionSeekFail } + return handle.seekInputStream(offset: offset, origin: origin) + }).asError() + + // progress + + try! CFURLSession_easy_setopt_long(rawHandle, CFURLSessionOptionNOPROGRESS, 0).asError() + + try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionPROGRESSDATA, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())).asError() + + try! CFURLSession_easy_setopt_tc(rawHandle, CFURLSessionOptionXFERINFOFUNCTION, { (userdata: UnsafeMutableRawPointer?, dltotal :Int64, dlnow: Int64, ultotal: Int64, ulnow: Int64) -> Int32 in + guard let handle = _EasyHandle.from(callbackUserData: userdata) else { return -1 } + handle.updateProgressMeter(with: _Progress(totalBytesSent: ulnow, totalBytesExpectedToSend: ultotal, totalBytesReceived: dlnow, totalBytesExpectedToReceive: dltotal)) + return 0 + }).asError() + + } + /// This callback function gets called by libcurl when it receives body + /// data. + /// + /// - SeeAlso: + func didReceive(data: UnsafeMutablePointer, size: Int, nmemb: Int) -> Int { + let d: Int = { + let buffer = Data(bytes: data, count: size*nmemb) + switch delegate?.didReceive(data: buffer) { + case .some(.proceed): return size * nmemb + case .some(.abort): return 0 + case .some(.pause): + pauseState.insert(.receivePaused) + return Int(CFURLSessionWriteFuncPause) + case .none: + /* the delegate disappeared */ + return 0 + } + }() + return d + } + /// This callback function gets called by libcurl when it receives header + /// data. + /// + /// - SeeAlso: + func didReceive(headerData data: UnsafeMutablePointer, size: Int, nmemb: Int, contentLength: Double) -> Int { + let d: Int = { + let buffer = Data(bytes: data, count: size*nmemb) + switch delegate?.didReceive(headerData: buffer, contentLength: Int64(contentLength)) { + case .some(.proceed): return size * nmemb + case .some(.abort): return 0 + case .some(.pause): + pauseState.insert(.receivePaused) + return Int(CFURLSessionWriteFuncPause) + case .none: + /* the delegate disappeared */ + return 0 + } + }() + return d + } + /// This callback function gets called by libcurl when it wants to send data + /// it to the network. + /// + /// - SeeAlso: + func fill(writeBuffer data: UnsafeMutablePointer, size: Int, nmemb: Int) -> Int { + let d: Int = { + let buffer = UnsafeMutableBufferPointer(start: data, count: size * nmemb) + switch delegate?.fill(writeBuffer: buffer) { + case .some(.pause): + pauseState.insert(.sendPaused) + return Int(CFURLSessionReadFuncPause) + case .some(.abort): + return Int(CFURLSessionReadFuncAbort) + case .some(.bytes(let length)): + return length + case .none: + /* the delegate disappeared */ + return Int(CFURLSessionReadFuncAbort) + } + }() + return d + } + + func setSocketOptions(for fd: CInt) throws { + //TODO: At this point we should call setsockopt(2) to set the QoS on + // the socket based on the QoS of the request. + // + // On Linux this can be done with IP_TOS. But there's both IntServ and + // DiffServ. + // + // Not sure what Darwin uses. + // + // C.f.: + // + // + } + func updateProgressMeter(with propgress: _Progress) { + delegate?.updateProgressMeter(with: propgress) + } + + func seekInputStream(offset: Int64, origin: CInt) -> CInt { + let d: Int32 = { + /// libcurl should only use SEEK_SET + guard origin == SEEK_SET else { fatalError("Unexpected 'origin' in seek.") } + do { + if let delegate = delegate { + try delegate.seekInputStream(to: UInt64(offset)) + return CFURLSessionSeekOk + } else { + return CFURLSessionSeekCantSeek + } + } catch { + return CFURLSessionSeekCantSeek + } + }() + return d + } +} + +extension _EasyHandle { + /// The progress of a transfer. + /// + /// The number of bytes that we expect to download and upload, and the + /// number of bytes downloaded and uploaded so far. + /// + /// Unknown values will be set to zero. E.g. if the number of bytes + /// expected to be downloaded is unknown, `totalBytesExpectedToReceive` + /// will be zero. + struct _Progress { + let totalBytesSent: Int64 + let totalBytesExpectedToSend: Int64 + let totalBytesReceived: Int64 + let totalBytesExpectedToReceive: Int64 + } +} + +extension _EasyHandle { + /// A simple wrapper / helper for libcurl’s `slist`. + /// + /// It's libcurl's way to represent an array of strings. + internal class _CurlStringList { + fileprivate var rawList: OpaquePointer? = nil + init() {} + init(_ strings: [String]) { + strings.forEach { append($0) } + } + deinit { + CFURLSessionSListFreeAll(rawList) + } + } +} +extension _EasyHandle._CurlStringList { + func append(_ string: String) { + string.withCString { + rawList = CFURLSessionSListAppend(rawList, $0) + } + } + var asUnsafeMutablePointer: UnsafeMutableRawPointer? { + return rawList.map{ UnsafeMutableRawPointer($0) } + } +} + +extension CFURLSessionEasyCode : Equatable { + public static func ==(lhs: CFURLSessionEasyCode, rhs: CFURLSessionEasyCode) -> Bool { + return lhs.value == rhs.value + } +} +extension CFURLSessionEasyCode : Error { + public var _domain: String { return "libcurl.Easy" } + public var _code: Int { return Int(self.value) } +} +internal extension CFURLSessionEasyCode { + func asError() throws { + if self == CFURLSessionEasyCodeOK { return } + throw self + } +} diff --git a/Foundation/URLSession/libcurl/MultiHandle.swift b/Foundation/URLSession/libcurl/MultiHandle.swift new file mode 100644 index 0000000000..4998899bc3 --- /dev/null +++ b/Foundation/URLSession/libcurl/MultiHandle.swift @@ -0,0 +1,459 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +// ----------------------------------------------------------------------------- +/// +/// libcurl *multi handle* wrapper. +/// These are libcurl helpers for the URLSession API code. +/// - SeeAlso: https://curl.haxx.se/libcurl/c/ +/// - SeeAlso: URLSession.swift +/// +// ----------------------------------------------------------------------------- + +import CoreFoundation +import Dispatch + + + +extension URLSession { + /// Minimal wrapper around [curl multi interface](https://curl.haxx.se/libcurl/c/libcurl-multi.html). + /// + /// The the *multi handle* manages the sockets for easy handles + /// (`_EasyHandle`), and this implementation uses + /// libdispatch to listen for sockets being read / write ready. + /// + /// Using `DispatchSource` allows this implementation to be + /// non-blocking and all code to run on the same thread / + /// `DispatchQueue` -- thus keeping is simple. + /// + /// - SeeAlso: _EasyHandle + internal final class _MultiHandle { + let rawHandle = CFURLSessionMultiHandleInit() + let queue: DispatchQueue + let group = DispatchGroup() + fileprivate var easyHandles: [_EasyHandle] = [] + fileprivate var timeoutSource: _TimeoutSource? = nil + + init(configuration: URLSession._Configuration, workQueue: DispatchQueue) { + queue = DispatchQueue(label: "MultiHandle.isolation", target: workQueue) + setupCallbacks() + configure(with: configuration) + } + deinit { + // C.f.: + easyHandles.forEach { + try! CFURLSessionMultiHandleRemoveHandle(rawHandle, $0.rawHandle).asError() + } + try! CFURLSessionMultiHandleDeinit(rawHandle).asError() + } + } +} + +extension URLSession._MultiHandle { + func configure(with configuration: URLSession._Configuration) { + try! CFURLSession_multi_setopt_l(rawHandle, CFURLSessionMultiOptionMAX_HOST_CONNECTIONS, configuration.httpMaximumConnectionsPerHost).asError() + try! CFURLSession_multi_setopt_l(rawHandle, CFURLSessionMultiOptionPIPELINING, configuration.httpShouldUsePipelining ? 3 : 2).asError() + //TODO: We may want to set + // CFURLSessionMultiOptionMAXCONNECTS + // CFURLSessionMultiOptionMAX_TOTAL_CONNECTIONS + } +} + +fileprivate extension URLSession._MultiHandle { + static func from(callbackUserData userdata: UnsafeMutableRawPointer?) -> URLSession._MultiHandle? { + guard let userdata = userdata else { return nil } + return Unmanaged.fromOpaque(userdata).takeUnretainedValue() + } +} + +fileprivate extension URLSession._MultiHandle { + /// Forward the libcurl callbacks into Swift methods + func setupCallbacks() { + // Socket + try! CFURLSession_multi_setopt_ptr(rawHandle, CFURLSessionMultiOptionSOCKETDATA, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())).asError() + try! CFURLSession_multi_setopt_sf(rawHandle, CFURLSessionMultiOptionSOCKETFUNCTION) { (easyHandle: CFURLSessionEasyHandle, socket: CFURLSession_socket_t, what: Int32, userdata: UnsafeMutableRawPointer?, socketptr: UnsafeMutableRawPointer?) -> Int32 in + guard let handle = URLSession._MultiHandle.from(callbackUserData: userdata) else { fatalError() } + return handle.register(socket: socket, for: easyHandle, what: what, socketSourcePtr: socketptr) + }.asError() + // Timeout: + try! CFURLSession_multi_setopt_ptr(rawHandle, CFURLSessionMultiOptionTIMERDATA, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())).asError() + try! CFURLSession_multi_setopt_tf(rawHandle, CFURLSessionMultiOptionTIMERFUNCTION) { (_, timeout: Int, userdata: UnsafeMutableRawPointer?) -> Int32 in + guard let handle = URLSession._MultiHandle.from(callbackUserData: userdata) else { fatalError() } + handle.updateTimeoutTimer(to: timeout) + return 0 + }.asError() + } + /// and + /// + func register(socket: CFURLSession_socket_t, for easyHandle: CFURLSessionEasyHandle, what: Int32, socketSourcePtr: UnsafeMutableRawPointer?) -> Int32 { + // We get this callback whenever we need to register or unregister a + // given socket with libdispatch. + // The `action` / `what` defines if we should register or unregister + // that we're interested in read and/or write readiness. We will do so + // through libdispatch (DispatchSource) and store the source(s) inside + // a `SocketSources` which we in turn store inside libcurl's multi handle + // by means of curl_multi_assign() -- we retain the object fist. + let action = _SocketRegisterAction(rawValue: CFURLSessionPoll(value: what)) + var socketSources = _SocketSources.from(socketSourcePtr: socketSourcePtr) + if socketSources == nil && action.needsSource { + let s = _SocketSources() + let p = Unmanaged.passRetained(s).toOpaque() + CFURLSessionMultiHandleAssign(rawHandle, socket, UnsafeMutableRawPointer(p)) + socketSources = s + } else if socketSources != nil && action == .unregister { + // We need to release the stored pointer: + if let opaque = socketSourcePtr { + Unmanaged<_SocketSources>.fromOpaque(opaque).release() + } + socketSources = nil + } + if let ss = socketSources { + let handler = DispatchWorkItem { [weak self] in + self?.performAction(for: socket) + } + ss.createSources(with: action, fileDescriptor: Int(socket), queue: queue, handler: handler) + } + return 0 + } + + /// What read / write ready event to register / unregister. + /// + /// This re-maps `CFURLSessionPoll` / `CURL_POLL`. + enum _SocketRegisterAction { + case none + case registerRead + case registerWrite + case registerReadAndWrite + case unregister + } +} + +internal extension URLSession._MultiHandle { + /// Add an easy handle -- start its transfer. + func add(_ handle: _EasyHandle) { + // If this is the first handle being added, we need to `kick` the + // underlying multi handle by calling `timeoutTimerFired` as + // described in + // . + // That will initiate the registration for timeout timer and socket + // readiness. + let needsTimeout = self.easyHandles.isEmpty + self.easyHandles.append(handle) + try! CFURLSessionMultiHandleAddHandle(self.rawHandle, handle.rawHandle).asError() + if needsTimeout { + self.timeoutTimerFired() + } + } + /// Remove an easy handle -- stop its transfer. + func remove(_ handle: _EasyHandle) { + guard let idx = self.easyHandles.index(of: handle) else { + fatalError("Handle not in list.") + } + self.easyHandles.remove(at: idx) + try! CFURLSessionMultiHandleRemoveHandle(self.rawHandle, handle.rawHandle).asError() + } +} + +fileprivate extension URLSession._MultiHandle { + /// This gets called when we should ask curl to perform action on a socket. + func performAction(for socket: CFURLSession_socket_t) { + try! readAndWriteAvailableData(on: socket) + } + /// This gets called when our timeout timer fires. + /// + /// libcurl relies on us calling curl_multi_socket_action() every now and then. + func timeoutTimerFired() { + try! readAndWriteAvailableData(on: CFURLSessionSocketTimeout) + } + /// reads/writes available data given an action + func readAndWriteAvailableData(on socket: CFURLSession_socket_t) throws { + var runningHandlesCount = Int32(0) + try CFURLSessionMultiHandleAction(rawHandle, socket, 0, &runningHandlesCount).asError() + //TODO: Do we remove the timeout timer here if / when runningHandles == 0 ? + readMessages() + } + + /// Check the status of all individual transfers. + /// + /// libcurl refers to this as “read multi stack informationals”. + /// Check for transfers that completed. + func readMessages() { + // We pop the messages one by one in a loop: + repeat { + // count will contain the messages left in the queue + var count = Int32(0) + let info = CFURLSessionMultiHandleInfoRead(rawHandle, &count) + guard let handle = info.easyHandle else { break } + let code = info.resultCode + completedTransfer(forEasyHandle: handle, easyCode: code) + } while true + } + /// Transfer completed. + func completedTransfer(forEasyHandle handle: CFURLSessionEasyHandle, easyCode: CFURLSessionEasyCode) { + // Look up the matching wrapper: + guard let idx = easyHandles.index(where: { $0.rawHandle == handle }) else { + fatalError("Tansfer completed for easy handle, but it is not in the list of added handles.") + } + let easyHandle = easyHandles[idx] + // Find the NSURLError code + var error: NSError? + if let errorCode = easyHandle.urlErrorCode(for: easyCode) { + let errorDescription = easyHandle.errorBuffer[0] != 0 ? + String(cString: easyHandle.errorBuffer) : + CFURLSessionCreateErrorDescription(easyCode.value)._swiftObject + error = NSError(domain: NSURLErrorDomain, code: errorCode, userInfo: [ + NSLocalizedDescriptionKey: errorDescription + ]) + } + completedTransfer(forEasyHandle: easyHandle, error: error) + } + /// Transfer completed. + func completedTransfer(forEasyHandle handle: _EasyHandle, error: NSError?) { + handle.completedTransfer(withError: error) + } +} + +fileprivate extension _EasyHandle { + /// An error code within the `NSURLErrorDomain` based on the error of the + /// easy handle. + /// - Note: The error value is set only on failure. You can't use it to + /// determine *if* something failed or not, only *why* it failed. + func urlErrorCode(for easyCode: CFURLSessionEasyCode) -> Int? { + switch (easyCode, CInt(connectFailureErrno)) { + case (CFURLSessionEasyCodeOK, _): + return nil + case (_, ECONNREFUSED): + return NSURLErrorCannotConnectToHost + case (CFURLSessionEasyCodeUNSUPPORTED_PROTOCOL, _): + return NSURLErrorUnsupportedURL + case (CFURLSessionEasyCodeURL_MALFORMAT, _): + return NSURLErrorBadURL + case (CFURLSessionEasyCodeCOULDNT_RESOLVE_HOST, _): + // Oddly, this appears to happen for malformed URLs, too. + return NSURLErrorCannotFindHost + case (CFURLSessionEasyCodeRECV_ERROR, ECONNRESET): + return NSURLErrorNetworkConnectionLost + case (CFURLSessionEasyCodeSEND_ERROR, ECONNRESET): + return NSURLErrorNetworkConnectionLost + case (CFURLSessionEasyCodeGOT_NOTHING, _): + return NSURLErrorBadServerResponse + case (CFURLSessionEasyCodeABORTED_BY_CALLBACK, _): + return NSURLErrorUnknown // Or NSURLErrorCancelled if we're in such a state + case (CFURLSessionEasyCodeCOULDNT_CONNECT, ETIMEDOUT): + return NSURLErrorTimedOut + case (CFURLSessionEasyCodeOPERATION_TIMEDOUT, _): + return NSURLErrorTimedOut + default: + //TODO: Need to map to one of the NSURLError... constants + return NSURLErrorUnknown + } + } +} + +fileprivate extension URLSession._MultiHandle._SocketRegisterAction { + init(rawValue: CFURLSessionPoll) { + switch rawValue { + case CFURLSessionPollNone: + self = .none + case CFURLSessionPollIn: + self = .registerRead + case CFURLSessionPollOut: + self = .registerWrite + case CFURLSessionPollInOut: + self = .registerReadAndWrite + case CFURLSessionPollRemove: + self = .unregister + default: + fatalError("Invalid CFURLSessionPoll value.") + } + } +} +extension CFURLSessionPoll : Equatable { + public static func ==(lhs: CFURLSessionPoll, rhs: CFURLSessionPoll) -> Bool { + return lhs.value == rhs.value + } +} +fileprivate extension URLSession._MultiHandle._SocketRegisterAction { + /// Should a libdispatch source be registered for **read** readiness? + var needsReadSource: Bool { + switch self { + case .none: return false + case .registerRead: return true + case .registerWrite: return false + case .registerReadAndWrite: return true + case .unregister: return false + } + } + /// Should a libdispatch source be registered for **write** readiness? + var needsWriteSource: Bool { + switch self { + case .none: return false + case .registerRead: return false + case .registerWrite: return true + case .registerReadAndWrite: return true + case .unregister: return false + } + } + /// Should either a **read** or a **write** readiness libdispatch source be + /// registered? + var needsSource: Bool { + return needsReadSource || needsWriteSource + } +} + +/// A helper class that wraps a libdispatch timer. +/// +/// Used to implement the timeout of `URLSession.MultiHandle` and `URLSession.EasyHandle` +class _TimeoutSource { + let rawSource: DispatchSource + let milliseconds: Int + let queue: DispatchQueue //needed to restart the timer for EasyHandles + let handler: DispatchWorkItem //needed to restart the timer for EasyHandles + init(queue: DispatchQueue, milliseconds: Int, handler: DispatchWorkItem) { + self.queue = queue + self.handler = handler + self.milliseconds = milliseconds + self.rawSource = DispatchSource.makeTimerSource(queue: queue) as! DispatchSource + + let delay = UInt64(max(1, milliseconds - 1)) + let start = DispatchTime.now() + DispatchTimeInterval.milliseconds(Int(delay)) + + rawSource.schedule(deadline: start, repeating: .milliseconds(Int(delay)), leeway: (milliseconds == 1) ? .microseconds(Int(1)) : .milliseconds(Int(1))) + rawSource.setEventHandler(handler: handler) + rawSource.resume() + } + deinit { + rawSource.cancel() + } +} + +fileprivate extension URLSession._MultiHandle { + + /// + func updateTimeoutTimer(to value: Int) { + updateTimeoutTimer(to: _Timeout(timeout: value)) + } + + func updateTimeoutTimer(to timeout: _Timeout) { + // Set up a timeout timer based on the given value: + switch timeout { + case .none: + timeoutSource = nil + case .immediate: + timeoutSource = nil + timeoutTimerFired() + case .milliseconds(let milliseconds): + if (timeoutSource == nil) || timeoutSource!.milliseconds != milliseconds { + //TODO: Could simply change the existing timer by calling + // dispatch_source_set_timer() again. + let block = DispatchWorkItem { [weak self] in + self?.timeoutTimerFired() + } + timeoutSource = _TimeoutSource(queue: queue, milliseconds: milliseconds, handler: block) + } + } + } + enum _Timeout { + case milliseconds(Int) + case none + case immediate + } +} + +fileprivate extension URLSession._MultiHandle._Timeout { + init(timeout: Int) { + switch timeout { + case -1: + self = .none + case 0: + self = .immediate + default: + self = .milliseconds(timeout) + } + } +} + + +/// Read and write libdispatch sources for a specific socket. +/// +/// A simple helper that combines two sources -- both being optional. +/// +/// This info is stored into the socket using `curl_multi_assign()`. +/// +/// - SeeAlso: URLSession.MultiHandle.SocketRegisterAction +fileprivate class _SocketSources { + var readSource: DispatchSource? + var writeSource: DispatchSource? + + func createReadSource(fileDescriptor fd: Int, queue: DispatchQueue, handler: DispatchWorkItem) { + guard readSource == nil else { return } + let s = DispatchSource.makeReadSource(fileDescriptor: Int32(fd), queue: queue) + s.setEventHandler(handler: handler) + readSource = s as? DispatchSource + s.resume() + } + + func createWriteSource(fileDescriptor fd: Int, queue: DispatchQueue, handler: DispatchWorkItem) { + guard writeSource == nil else { return } + let s = DispatchSource.makeWriteSource(fileDescriptor: Int32(fd), queue: queue) + s.setEventHandler(handler: handler) + writeSource = s as? DispatchSource + s.resume() + } + + func tearDown() { + if let s = readSource { + s.cancel() + } + readSource = nil + if let s = writeSource { + s.cancel() + } + writeSource = nil + } +} +extension _SocketSources { + /// Create a read and/or write source as specified by the action. + func createSources(with action: URLSession._MultiHandle._SocketRegisterAction, fileDescriptor fd: Int, queue: DispatchQueue, handler: DispatchWorkItem) { + if action.needsReadSource { + createReadSource(fileDescriptor: fd, queue: queue, handler: handler) + } + if action.needsWriteSource { + createWriteSource(fileDescriptor: fd, queue: queue, handler: handler) + } + } +} +extension _SocketSources { + /// Unwraps the `SocketSources` + /// + /// A `SocketSources` is stored into the multi handle's socket using + /// `curl_multi_assign()`. This helper unwraps it from the returned + /// `UnsafeMutablePointer`. + static func from(socketSourcePtr ptr: UnsafeMutableRawPointer?) -> _SocketSources? { + guard let ptr = ptr else { return nil } + return Unmanaged<_SocketSources>.fromOpaque(ptr).takeUnretainedValue() + } +} + + +extension CFURLSessionMultiCode : Equatable { + public static func ==(lhs: CFURLSessionMultiCode, rhs: CFURLSessionMultiCode) -> Bool { + return lhs.value == rhs.value + } +} +extension CFURLSessionMultiCode : Error { + public var _domain: String { return "libcurl.Multi" } + public var _code: Int { return Int(self.value) } +} +internal extension CFURLSessionMultiCode { + func asError() throws { + if self == CFURLSessionMultiCodeOK { return } + throw self + } +} diff --git a/Foundation/URLSession/libcurl/libcurlHelpers.swift b/Foundation/URLSession/libcurl/libcurlHelpers.swift new file mode 100644 index 0000000000..7820f9c92a --- /dev/null +++ b/Foundation/URLSession/libcurl/libcurlHelpers.swift @@ -0,0 +1,50 @@ +// Foundation/URLSession/libcurlHelpers - URLSession & libcurl +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +// ----------------------------------------------------------------------------- +/// +/// These are libcurl helpers for the URLSession API code. +/// - SeeAlso: https://curl.haxx.se/libcurl/c/ +/// - SeeAlso: URLSession.swift +/// +// ----------------------------------------------------------------------------- + + +import CoreFoundation + + +//TODO: Move things in this file? + + +internal func initializeLibcurl() { + try! CFURLSessionInit().asError() +} + + +internal extension String { + /// Create a string by a buffer of UTF 8 code points that is not zero + /// terminated. + init?(utf8Buffer: UnsafeBufferPointer) { + var bufferIterator = utf8Buffer.makeIterator() + var codec = UTF8() + var result: String = "" + iter: repeat { + switch codec.decode(&bufferIterator) { + case .scalarValue(let scalar): + result.append(String(describing: scalar)) + case .error: + return nil + case .emptyInput: + break iter + } + } while true + self.init(stringLiteral: result) + } +} From b2faece86ff5d64afd0ace04f416f563ee2573b5 Mon Sep 17 00:00:00 2001 From: saiHemak Date: Fri, 10 Nov 2017 12:14:29 +0530 Subject: [PATCH 03/11] Common BodySource for native protocols The code respective to data handling is same irrespective to protocol.So moving it under URLSession folder. --- .../URLSession/http/HTTPBodySource.swift | 244 ------------------ 1 file changed, 244 deletions(-) delete mode 100644 Foundation/URLSession/http/HTTPBodySource.swift diff --git a/Foundation/URLSession/http/HTTPBodySource.swift b/Foundation/URLSession/http/HTTPBodySource.swift deleted file mode 100644 index a72a28c34e..0000000000 --- a/Foundation/URLSession/http/HTTPBodySource.swift +++ /dev/null @@ -1,244 +0,0 @@ -// Foundation/URLSession/HTTPBodySource.swift - URLSession & libcurl -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See http://swift.org/LICENSE.txt for license information -// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -// ----------------------------------------------------------------------------- -/// -/// These are libcurl helpers for the URLSession API code. -/// - SeeAlso: https://curl.haxx.se/libcurl/c/ -/// - SeeAlso: URLSession.swift -/// -// ----------------------------------------------------------------------------- - -import CoreFoundation -import Dispatch - - -/// Turn `NSData` into `dispatch_data_t` -internal func createDispatchData(_ data: Data) -> DispatchData { - //TODO: Avoid copying data - let buffer = UnsafeRawBufferPointer(start: data._backing.bytes, - count: data.count) - return DispatchData(bytes: buffer) -} - -/// Copy data from `dispatch_data_t` into memory pointed to by an `UnsafeMutableBufferPointer`. -internal func copyDispatchData(_ data: DispatchData, infoBuffer buffer: UnsafeMutableBufferPointer) { - precondition(data.count <= (buffer.count * MemoryLayout.size)) - _ = data.copyBytes(to: buffer) -} - -/// Split `dispatch_data_t` into `(head, tail)` pair. -internal func splitData(dispatchData data: DispatchData, atPosition position: Int) -> (DispatchData,DispatchData) { - return (data.subdata(in: 0.. _HTTPBodySourceDataChunk -} -internal enum _HTTPBodySourceDataChunk { - case data(DispatchData) - /// The source is depleted. - case done - /// Retry later to get more data. - case retryLater - case error -} - -/// A HTTP body data source backed by `dispatch_data_t`. -internal final class _HTTPBodyDataSource { - var data: DispatchData! - init(data: DispatchData) { - self.data = data - } -} - -extension _HTTPBodyDataSource : _HTTPBodySource { - enum _Error : Error { - case unableToRewindData - } - - func getNextChunk(withLength length: Int) -> _HTTPBodySourceDataChunk { - let remaining = data.count - if remaining == 0 { - return .done - } else if remaining <= length { - let r: DispatchData! = data - data = DispatchData.empty - return .data(r) - } else { - let (chunk, remainder) = splitData(dispatchData: data, atPosition: length) - data = remainder - return .data(chunk) - } - } -} - - -/// A HTTP body data source backed by a file. -/// -/// This allows non-blocking streaming of file data to the remote server. -/// -/// The source reads data using a `dispatch_io_t` channel, and hence reading -/// file data is non-blocking. It has a local buffer that it fills as calls -/// to `getNextChunk(withLength:)` drain it. -/// -/// - Note: Calls to `getNextChunk(withLength:)` and callbacks from libdispatch -/// should all happen on the same (serial) queue, and hence this code doesn't -/// have to be thread safe. -internal final class _HTTPBodyFileSource { - fileprivate let fileURL: URL - fileprivate let channel: DispatchIO - fileprivate let workQueue: DispatchQueue - fileprivate let dataAvailableHandler: () -> Void - fileprivate var hasActiveReadHandler = false - fileprivate var availableChunk: _Chunk = .empty - /// Create a new data source backed by a file. - /// - /// - Parameter fileURL: the file to read from - /// - Parameter workQueue: the queue that it's safe to call - /// `getNextChunk(withLength:)` on, and that the `dataAvailableHandler` - /// will be called on. - /// - Parameter dataAvailableHandler: Will be called when data becomes - /// available. Reading data is done in a non-blocking way, such that - /// no data may be available even if there's more data in the file. - /// if `getNextChunk(withLength:)` returns `.retryLater`, this handler - /// will be called once data becomes available. - init(fileURL: URL, workQueue: DispatchQueue, dataAvailableHandler: @escaping () -> Void) { - guard fileURL.isFileURL else { fatalError("The body data URL must be a file URL.") } - self.fileURL = fileURL - self.workQueue = workQueue - self.dataAvailableHandler = dataAvailableHandler - var fileSystemRepresentation: UnsafePointer! = nil - fileURL.withUnsafeFileSystemRepresentation { - fileSystemRepresentation = $0 - } - guard let channel = DispatchIO(type: .stream, path: fileSystemRepresentation, - oflag: O_RDONLY, mode: 0, queue: workQueue, - cleanupHandler: {_ in }) else { - fatalError("Cant create DispatchIO channel") - } - self.channel = channel - self.channel.setLimit(highWater: CFURLSessionMaxWriteSize) - } - - fileprivate enum _Chunk { - /// Nothing has been read, yet - case empty - /// An error has occured while reading - case errorDetected(Int) - /// Data has been read - case data(DispatchData) - /// All data has been read from the file (EOF). - case done(DispatchData?) - } -} - -fileprivate extension _HTTPBodyFileSource { - fileprivate var desiredBufferLength: Int { return 3 * CFURLSessionMaxWriteSize } - /// Enqueue a dispatch I/O read to fill the buffer. - /// - /// - Note: This is a no-op if the buffer is full, or if a read operation - /// is already enqueued. - fileprivate func readNextChunk() { - // libcurl likes to use a buffer of size CFURLSessionMaxWriteSize, we'll - // try to keep 3 x of that around in the `chunk` buffer. - guard availableByteCount < desiredBufferLength else { return } - guard !hasActiveReadHandler else { return } // We're already reading - hasActiveReadHandler = true - - let lengthToRead = desiredBufferLength - availableByteCount - channel.read(offset: 0, length: lengthToRead, queue: workQueue) { (done: Bool, data: DispatchData?, errno: Int32) in - let wasEmpty = self.availableByteCount == 0 - self.hasActiveReadHandler = !done - - switch (done, data, errno) { - case (true, _, errno) where errno != 0: - self.availableChunk = .errorDetected(Int(errno)) - case (true, .some(let d), 0) where d.isEmpty: - self.append(data: d, endOfFile: true) - case (true, .some(let d), 0): - self.append(data: d, endOfFile: false) - case (false, .some(let d), 0): - self.append(data: d, endOfFile: false) - default: - fatalError("Invalid arguments to read(3) callback.") - } - - if wasEmpty && (0 < self.availableByteCount) { - self.dataAvailableHandler() - } - } - } - - fileprivate func append(data: DispatchData, endOfFile: Bool) { - switch availableChunk { - case .empty: - availableChunk = endOfFile ? .done(data) : .data(data) - case .errorDetected: - break - case .data(var oldData): - oldData.append(data) - availableChunk = endOfFile ? .done(oldData) : .data(oldData) - case .done: - fatalError("Trying to append data, but end-of-file was already detected.") - } - } - - fileprivate var availableByteCount: Int { - switch availableChunk { - case .empty: return 0 - case .errorDetected: return 0 - case .data(let d): return d.count - case .done(.some(let d)): return d.count - case .done(.none): return 0 - } - } -} - -extension _HTTPBodyFileSource : _HTTPBodySource { - func getNextChunk(withLength length: Int) -> _HTTPBodySourceDataChunk { - switch availableChunk { - case .empty: - readNextChunk() - return .retryLater - case .errorDetected: - return .error - case .data(let data): - let l = min(length, data.count) - let (head, tail) = splitData(dispatchData: data, atPosition: l) - - availableChunk = tail.isEmpty ? .empty : .data(tail) - readNextChunk() - - if head.isEmpty { - return .retryLater - } else { - return .data(head) - } - case .done(.some(let data)): - let l = min(length, data.count) - let (head, tail) = splitData(dispatchData: data, atPosition: l) - availableChunk = tail.isEmpty ? .done(nil) : .done(tail) - if head.isEmpty { - return .done - } else { - return .data(head) - } - case .done(.none): - return .done - } - } -} From 2e6330d9185ddbe41b8bfb0562ae0f629ad0af3a Mon Sep 17 00:00:00 2001 From: saiHemak Date: Fri, 10 Nov 2017 12:16:25 +0530 Subject: [PATCH 04/11] Adding BodySource for native protocols Moved under URLSession as the code related to data handling like read backed up by a file or buffer is common to all protocols. --- Foundation/URLSession/BodySource.swift | 244 +++++++++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100644 Foundation/URLSession/BodySource.swift diff --git a/Foundation/URLSession/BodySource.swift b/Foundation/URLSession/BodySource.swift new file mode 100644 index 0000000000..d427c504a9 --- /dev/null +++ b/Foundation/URLSession/BodySource.swift @@ -0,0 +1,244 @@ +// Foundation/URLSession/BodySource.swift - URLSession & libcurl +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +// ----------------------------------------------------------------------------- +/// +/// These are libcurl helpers for the URLSession API code. +/// - SeeAlso: https://curl.haxx.se/libcurl/c/ +/// - SeeAlso: URLSession.swift +/// +// ----------------------------------------------------------------------------- + +import CoreFoundation +import Dispatch + + +/// Turn `NSData` into `dispatch_data_t` +internal func createDispatchData(_ data: Data) -> DispatchData { + //TODO: Avoid copying data + let buffer = UnsafeRawBufferPointer(start: data._backing.bytes, + count: data.count) + return DispatchData(bytes: buffer) +} + +/// Copy data from `dispatch_data_t` into memory pointed to by an `UnsafeMutableBufferPointer`. +internal func copyDispatchData(_ data: DispatchData, infoBuffer buffer: UnsafeMutableBufferPointer) { + precondition(data.count <= (buffer.count * MemoryLayout.size)) + _ = data.copyBytes(to: buffer) +} + +/// Split `dispatch_data_t` into `(head, tail)` pair. +internal func splitData(dispatchData data: DispatchData, atPosition position: Int) -> (DispatchData,DispatchData) { + return (data.subdata(in: 0.. _BodySourceDataChunk +} +internal enum _BodySourceDataChunk { + case data(DispatchData) + /// The source is depleted. + case done + /// Retry later to get more data. + case retryLater + case error +} + +/// A body data source backed by `dispatch_data_t`. +internal final class _BodyDataSource { + var data: DispatchData! + init(data: DispatchData) { + self.data = data + } +} + +extension _BodyDataSource : _BodySource { + enum _Error : Error { + case unableToRewindData + } + + func getNextChunk(withLength length: Int) -> _BodySourceDataChunk { + let remaining = data.count + if remaining == 0 { + return .done + } else if remaining <= length { + let r: DispatchData! = data + data = DispatchData.empty + return .data(r) + } else { + let (chunk, remainder) = splitData(dispatchData: data, atPosition: length) + data = remainder + return .data(chunk) + } + } +} + + +/// A body data source backed by a file. +/// +/// This allows non-blocking streaming of file data to the remote server. +/// +/// The source reads data using a `dispatch_io_t` channel, and hence reading +/// file data is non-blocking. It has a local buffer that it fills as calls +/// to `getNextChunk(withLength:)` drain it. +/// +/// - Note: Calls to `getNextChunk(withLength:)` and callbacks from libdispatch +/// should all happen on the same (serial) queue, and hence this code doesn't +/// have to be thread safe. +internal final class _BodyFileSource { + fileprivate let fileURL: URL + fileprivate let channel: DispatchIO + fileprivate let workQueue: DispatchQueue + fileprivate let dataAvailableHandler: () -> Void + fileprivate var hasActiveReadHandler = false + fileprivate var availableChunk: _Chunk = .empty + /// Create a new data source backed by a file. + /// + /// - Parameter fileURL: the file to read from + /// - Parameter workQueue: the queue that it's safe to call + /// `getNextChunk(withLength:)` on, and that the `dataAvailableHandler` + /// will be called on. + /// - Parameter dataAvailableHandler: Will be called when data becomes + /// available. Reading data is done in a non-blocking way, such that + /// no data may be available even if there's more data in the file. + /// if `getNextChunk(withLength:)` returns `.retryLater`, this handler + /// will be called once data becomes available. + init(fileURL: URL, workQueue: DispatchQueue, dataAvailableHandler: @escaping () -> Void) { + guard fileURL.isFileURL else { fatalError("The body data URL must be a file URL.") } + self.fileURL = fileURL + self.workQueue = workQueue + self.dataAvailableHandler = dataAvailableHandler + var fileSystemRepresentation: UnsafePointer! = nil + fileURL.withUnsafeFileSystemRepresentation { + fileSystemRepresentation = $0 + } + guard let channel = DispatchIO(type: .stream, path: fileSystemRepresentation, + oflag: O_RDONLY, mode: 0, queue: workQueue, + cleanupHandler: {_ in }) else { + fatalError("Cant create DispatchIO channel") + } + self.channel = channel + self.channel.setLimit(highWater: CFURLSessionMaxWriteSize) + } + + fileprivate enum _Chunk { + /// Nothing has been read, yet + case empty + /// An error has occured while reading + case errorDetected(Int) + /// Data has been read + case data(DispatchData) + /// All data has been read from the file (EOF). + case done(DispatchData?) + } +} + +fileprivate extension _BodyFileSource { + fileprivate var desiredBufferLength: Int { return 3 * CFURLSessionMaxWriteSize } + /// Enqueue a dispatch I/O read to fill the buffer. + /// + /// - Note: This is a no-op if the buffer is full, or if a read operation + /// is already enqueued. + fileprivate func readNextChunk() { + // libcurl likes to use a buffer of size CFURLSessionMaxWriteSize, we'll + // try to keep 3 x of that around in the `chunk` buffer. + guard availableByteCount < desiredBufferLength else { return } + guard !hasActiveReadHandler else { return } // We're already reading + hasActiveReadHandler = true + + let lengthToRead = desiredBufferLength - availableByteCount + channel.read(offset: 0, length: lengthToRead, queue: workQueue) { (done: Bool, data: DispatchData?, errno: Int32) in + let wasEmpty = self.availableByteCount == 0 + self.hasActiveReadHandler = !done + + switch (done, data, errno) { + case (true, _, errno) where errno != 0: + self.availableChunk = .errorDetected(Int(errno)) + case (true, .some(let d), 0) where d.isEmpty: + self.append(data: d, endOfFile: true) + case (true, .some(let d), 0): + self.append(data: d, endOfFile: false) + case (false, .some(let d), 0): + self.append(data: d, endOfFile: false) + default: + fatalError("Invalid arguments to read(3) callback.") + } + + if wasEmpty && (0 < self.availableByteCount) { + self.dataAvailableHandler() + } + } + } + + fileprivate func append(data: DispatchData, endOfFile: Bool) { + switch availableChunk { + case .empty: + availableChunk = endOfFile ? .done(data) : .data(data) + case .errorDetected: + break + case .data(var oldData): + oldData.append(data) + availableChunk = endOfFile ? .done(oldData) : .data(oldData) + case .done: + fatalError("Trying to append data, but end-of-file was already detected.") + } + } + + fileprivate var availableByteCount: Int { + switch availableChunk { + case .empty: return 0 + case .errorDetected: return 0 + case .data(let d): return d.count + case .done(.some(let d)): return d.count + case .done(.none): return 0 + } + } +} + +extension _BodyFileSource : _BodySource { + func getNextChunk(withLength length: Int) -> _BodySourceDataChunk { + switch availableChunk { + case .empty: + readNextChunk() + return .retryLater + case .errorDetected: + return .error + case .data(let data): + let l = min(length, data.count) + let (head, tail) = splitData(dispatchData: data, atPosition: l) + + availableChunk = tail.isEmpty ? .empty : .data(tail) + readNextChunk() + + if head.isEmpty { + return .retryLater + } else { + return .data(head) + } + case .done(.some(let data)): + let l = min(length, data.count) + let (head, tail) = splitData(dispatchData: data, atPosition: l) + availableChunk = tail.isEmpty ? .done(nil) : .done(tail) + if head.isEmpty { + return .done + } else { + return .data(head) + } + case .done(.none): + return .done + } + } +} From c2018e500fabaa63ed61359b8a2035d3bcb96123 Mon Sep 17 00:00:00 2001 From: saiHemak Date: Fri, 10 Nov 2017 12:21:33 +0530 Subject: [PATCH 05/11] Code moved to NativeProtocol Transfer State has 1-0-1 relationship with Easy handle and is not bound to any protocol.So moved the code to NativeProtocol and renamed to _NativeProtocol. TransferState instead of HTTPTranserState --- .../URLSession/http/TransferState.swift | 137 ------------------ 1 file changed, 137 deletions(-) delete mode 100644 Foundation/URLSession/http/TransferState.swift diff --git a/Foundation/URLSession/http/TransferState.swift b/Foundation/URLSession/http/TransferState.swift deleted file mode 100644 index 9443a19284..0000000000 --- a/Foundation/URLSession/http/TransferState.swift +++ /dev/null @@ -1,137 +0,0 @@ -// Foundation/URLSession/TransferState.swift - URLSession & libcurl -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See http://swift.org/LICENSE.txt for license information -// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -// ----------------------------------------------------------------------------- -/// -/// The state of a single transfer. -/// These are libcurl helpers for the URLSession API code. -/// - SeeAlso: https://curl.haxx.se/libcurl/c/ -/// - SeeAlso: URLSession.swift -/// -// ----------------------------------------------------------------------------- - -import CoreFoundation - - - -extension _HTTPURLProtocol { - /// State related to an ongoing transfer. - /// - /// This contains headers received so far, body data received so far, etc. - /// - /// There's a strict 1-to-1 relationship between an `EasyHandle` and a - /// `TransferState`. - /// - /// - TODO: Might move the `EasyHandle` into this `struct` ? - /// - SeeAlso: `URLSessionTask.EasyHandle` - internal struct _HTTPTransferState { - /// The URL that's being requested - let url: URL - /// Raw headers received. - let parsedResponseHeader: _ParsedResponseHeader - /// Once the headers is complete, this will contain the response - var response: HTTPURLResponse? - /// The body data to be sent in the request - let requestBodySource: _HTTPBodySource? - /// Body data received - let bodyDataDrain: _DataDrain - /// Describes what to do with received body data for this transfer: - } -} - -extension _HTTPURLProtocol { - enum _DataDrain { - /// Concatenate in-memory - case inMemory(NSMutableData?) - /// Write to file - case toFile(URL, FileHandle?) - /// Do nothing. Might be forwarded to delegate - case ignore - } -} - -extension _HTTPURLProtocol._HTTPTransferState { - /// Transfer state that can receive body data, but will not send body data. - init(url: URL, bodyDataDrain: _HTTPURLProtocol._DataDrain) { - self.url = url - self.parsedResponseHeader = _HTTPURLProtocol._ParsedResponseHeader() - self.response = nil - self.requestBodySource = nil - self.bodyDataDrain = bodyDataDrain - } - /// Transfer state that sends body data and can receive body data. - init(url: URL, bodyDataDrain: _HTTPURLProtocol._DataDrain, bodySource: _HTTPBodySource) { - self.url = url - self.parsedResponseHeader = _HTTPURLProtocol._ParsedResponseHeader() - self.response = nil - self.requestBodySource = bodySource - self.bodyDataDrain = bodyDataDrain - } -} - -extension _HTTPURLProtocol._HTTPTransferState { - enum _Error: Error { - case parseSingleLineError - case parseCompleteHeaderError - } - /// Appends a header line - /// - /// Will set the complete response once the header is complete, i.e. the - /// return value's `isHeaderComplete` will then by `true`. - /// - /// - Throws: When a parsing error occurs - func byAppending(headerLine data: Data) throws -> _HTTPURLProtocol._HTTPTransferState { - guard let h = parsedResponseHeader.byAppending(headerLine: data) else { - throw _Error.parseSingleLineError - } - if case .complete(let lines) = h { - // Header is complete - let response = lines.createHTTPURLResponse(for: url) - guard response != nil else { - throw _Error.parseCompleteHeaderError - } - return _HTTPURLProtocol._HTTPTransferState(url: url, parsedResponseHeader: _HTTPURLProtocol._ParsedResponseHeader(), response: response, requestBodySource: requestBodySource, bodyDataDrain: bodyDataDrain) - } else { - return _HTTPURLProtocol._HTTPTransferState(url: url, parsedResponseHeader: h, response: nil, requestBodySource: requestBodySource, bodyDataDrain: bodyDataDrain) - } - } - var isHeaderComplete: Bool { - return response != nil - } - /// Append body data - /// - /// - Important: This will mutate the existing `NSMutableData` that the - /// struct may already have in place -- copying the data is too - /// expensive. This behaviour - func byAppending(bodyData buffer: Data) -> _HTTPURLProtocol._HTTPTransferState { - switch bodyDataDrain { - case .inMemory(let bodyData): - let data: NSMutableData = bodyData ?? NSMutableData() - data.append(buffer) - let drain = _HTTPURLProtocol._DataDrain.inMemory(data) - return _HTTPURLProtocol._HTTPTransferState(url: url, parsedResponseHeader: parsedResponseHeader, response: response, requestBodySource: requestBodySource, bodyDataDrain: drain) - case .toFile(_, let fileHandle): - //TODO: Create / open the file for writing - // Append to the file - _ = fileHandle!.seekToEndOfFile() - fileHandle!.write(buffer) - return self - case .ignore: - return self - } - } - /// Sets the given body source on the transfer state. - /// - /// This can be used to either set the initial body source, or to reset it - /// e.g. when restarting a transfer. - func bySetting(bodySource newSource: _HTTPBodySource) -> _HTTPURLProtocol._HTTPTransferState { - return _HTTPURLProtocol._HTTPTransferState(url: url, parsedResponseHeader: parsedResponseHeader, response: response, requestBodySource: newSource, bodyDataDrain: bodyDataDrain) - } -} From 785ca596c15c18db5006961505315e15e106635b Mon Sep 17 00:00:00 2001 From: saiHemak Date: Fri, 10 Nov 2017 12:22:26 +0530 Subject: [PATCH 06/11] Adding common protocol implementation Created a class NativeProtocol which basically holds the common implementation of native protocols like HTTP , FTP and so on . It implements the URLProtocol and EashHandleDelegate.Moved enableLibcurlDebugOutput and enableDebugOutput debug flags from HTTPURLProtocol to NativeProtocol as these can be enabled on EasyHandle irrespective of the protocol.Moved _HTTPURLProtocol.InternalState from HTTPURLProtocol to _NativeProtocol.InternalState. Moved inits from HTTPURLProtocol to NativeProtocol.Moved easyHandle and tempFileURL instance variables to NativeProtocol.EasyHandleDelegate methods provides functionality for transfer irrespective of protocol to NativeProtocol- fill(writeBuffer buffer: UnsafeMutableBufferPointer) , notifyDelegate(aboutUploadedData), notifyDelegate(aboutReceivedData), didReceive(data). transferCompleted(withError error: NSError?) createTransferState() createTransferBodyDataDrain However few methods like below are protocol specific hence have been overridden in respective protocol classes . didReceive(headerData data: Data, contentLength: Int64) Though the headers basically ends with \r\n irrespective of protocol the header format in FTP is different from HTTP. For e.g.: Headers in FTP looks like a plain text header with status code and description unlike the key-value pair headers in HTTP. NativeProtocol has definition for configureEasyHandle(easyHandle) which needs concrete implementation which is protocol specific by sub-classes like FTP and HTTP. For e.g.:HTTP requires some more additional configurations like adding the headers, setting allowed protocols. Moved HTTPURLProtocol. _ParsedResponseHeader from HTTPMessage.swift to NativeProtocol.swift under _NativeProtocol._ParsedResponseHeader . This implementation basically does header parsing based on \r\n delimiters. Except the fact that empty header is received in case of HTTP to mark as Header completion and 150 status marks the open of data communication in FTP New Implementation : NativeProtocol.validateHeaderComplete Should not throw error incase of simple-responseshttps://github.com/apple/swift-corelibs-foundation/pull/1097 isCompleteHeader -> Inner method In order to support both protocols this method has been introduced -> which returns to true when the header is empty in case of HTTP. -> Returns true if the header starts with 150 --- Foundation/URLSession/NativeProtocol.swift | 755 +++++++++++++++++++++ 1 file changed, 755 insertions(+) create mode 100644 Foundation/URLSession/NativeProtocol.swift diff --git a/Foundation/URLSession/NativeProtocol.swift b/Foundation/URLSession/NativeProtocol.swift new file mode 100644 index 0000000000..9b8629a108 --- /dev/null +++ b/Foundation/URLSession/NativeProtocol.swift @@ -0,0 +1,755 @@ +// Foundation/URLSession/NativeProtocol.swift - NSURLSession & libcurl +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +// ----------------------------------------------------------------------------- +/// +/// This file has the common implementation of Native protocols like HTTP,FTP,Data +/// These are libcurl helpers for the URLSession API code. +/// - SeeAlso: https://curl.haxx.se/libcurl/c/ +/// - SeeAlso: NSURLSession.swift +/// +// ----------------------------------------------------------------------------- + +import CoreFoundation +import Dispatch + +internal let enableLibcurlDebugOutput: Bool = { + return ProcessInfo.processInfo.environment["URLSessionDebugLibcurl"] != nil +}() +internal let enableDebugOutput: Bool = { + return ProcessInfo.processInfo.environment["URLSessionDebug"] != nil +}() + +class _NativeProtocol: URLProtocol, _EasyHandleDelegate { + internal var easyHandle: _EasyHandle! + internal lazy var tempFileURL: URL = { + let fileName = NSTemporaryDirectory() + NSUUID().uuidString + ".tmp" + _ = FileManager.default.createFile(atPath: fileName, contents: nil) + return URL(fileURLWithPath: fileName) + }() + + public required init(task: URLSessionTask, cachedResponse: CachedURLResponse?, client: URLProtocolClient?) { + self.internalState = _InternalState.initial + super.init(request: task.originalRequest!, cachedResponse: cachedResponse, client: client) + self.task = task + self.easyHandle = _EasyHandle(delegate: self) + } + + public required init(request: URLRequest, cachedResponse: CachedURLResponse?, client: URLProtocolClient?) { + self.internalState = _InternalState.initial + super.init(request: request, cachedResponse: cachedResponse, client: client) + self.easyHandle = _EasyHandle(delegate: self) + } + + var internalState: _InternalState { + // We manage adding / removing the easy handle and pausing / unpausing + // here at a centralized place to make sure the internal state always + // matches up with the state of the easy handle being added and paused. + willSet { + if !internalState.isEasyHandlePaused && newValue.isEasyHandlePaused { + fatalError("Need to solve pausing receive.") + } + if internalState.isEasyHandleAddedToMultiHandle && !newValue.isEasyHandleAddedToMultiHandle { + task?.session.remove(handle: easyHandle) + } + } + didSet { + if !oldValue.isEasyHandleAddedToMultiHandle && internalState.isEasyHandleAddedToMultiHandle { + task?.session.add(handle: easyHandle) + } + if oldValue.isEasyHandlePaused && !internalState.isEasyHandlePaused { + fatalError("Need to solve pausing receive.") + } + } + } + + func didReceive(data: Data) -> _EasyHandle._Action { + guard case .transferInProgress(var ts) = internalState else { + fatalError("Received body data, but no transfer in progress.") + } + + if let response = validateHeaderComplete(transferState:ts) { + ts.response = response + } + notifyDelegate(aboutReceivedData: data) + internalState = .transferInProgress(ts.byAppending(bodyData: data)) + return .proceed + } + + func validateHeaderComplete(transferState: _TransferState) -> URLResponse? { + guard transferState.isHeaderComplete else { + fatalError("Received body data, but the header is not complete, yet.") + } + return nil + } + + fileprivate func notifyDelegate(aboutReceivedData data: Data) { + guard let t = self.task else { + fatalError("Cannot notify") + } + if case .taskDelegate(let delegate) = t.session.behaviour(for: self.task!), + let dataDelegate = delegate as? URLSessionDataDelegate, + let task = self.task as? URLSessionDataTask { + // Forward to the delegate: + guard let s = self.task?.session as? URLSession else { + fatalError() + } + s.delegateQueue.addOperation { + dataDelegate.urlSession(s, dataTask: task, didReceive: data) + } + } else if case .taskDelegate(let delegate) = t.session.behaviour(for: self.task!), + let downloadDelegate = delegate as? URLSessionDownloadDelegate, + let task = self.task as? URLSessionDownloadTask { + guard let s = self.task?.session as? URLSession else { + fatalError() + } + let fileHandle = try! FileHandle(forWritingTo: self.tempFileURL) + _ = fileHandle.seekToEndOfFile() + fileHandle.write(data) + task.countOfBytesReceived += Int64(data.count) + + s.delegateQueue.addOperation { + downloadDelegate.urlSession(s, downloadTask: task, didWriteData: Int64(data.count), totalBytesWritten: task.countOfBytesReceived, + totalBytesExpectedToWrite: task.countOfBytesExpectedToReceive) + } + if task.countOfBytesExpectedToReceive == task.countOfBytesReceived { + fileHandle.closeFile() + self.properties[.temporaryFileURL] = self.tempFileURL + } + } + } + + fileprivate func notifyDelegate(aboutUploadedData count: Int64) { + guard let task = self.task as? URLSessionUploadTask, + let session = self.task?.session as? URLSession, + case .taskDelegate(let delegate) = session.behaviour(for: task) else { return } + task.countOfBytesSent += count + session.delegateQueue.addOperation { + delegate.urlSession(session, task: task, didSendBodyData: count, + totalBytesSent: task.countOfBytesSent, totalBytesExpectedToSend: task.countOfBytesExpectedToSend) + } + } + + func didReceive(headerData data: Data, contentLength: Int64) -> _EasyHandle._Action { + NSRequiresConcreteImplementation() + } + + func fill(writeBuffer buffer: UnsafeMutableBufferPointer) -> _EasyHandle._WriteBufferResult { + guard case .transferInProgress(let ts) = internalState else { + fatalError("Requested to fill write buffer, but transfer isn't in progress.") + } + guard let source = ts.requestBodySource else { + fatalError("Requested to fill write buffer, but transfer state has no body source.") + } + switch source.getNextChunk(withLength: buffer.count) { + case .data(let data): + copyDispatchData(data, infoBuffer: buffer) + let count = data.count + assert(count > 0) + notifyDelegate(aboutUploadedData: Int64(count)) + return .bytes(count) + case .done: + return .bytes(0) + case .retryLater: + // At this point we'll try to pause the easy handle. The body source + // is responsible for un-pausing the handle once data becomes + // available. + return .pause + case .error: + return .abort + } + } + + func transferCompleted(withError error: NSError?) { + // At this point the transfer is complete and we can decide what to do. + // If everything went well, we will simply forward the resulting data + // to the delegate. But in case of redirects etc. we might send another + // request. + guard case .transferInProgress(let ts) = internalState else { + fatalError("Transfer completed, but it wasn't in progress.") + } + guard let request = task?.currentRequest else { + fatalError("Transfer completed, but there's no current request.") + } + guard error == nil else { + internalState = .transferFailed + failWith(error: error!, request: request) + return + } + + if let response = task?.response { + var transferState = ts + transferState.response = response + } + + guard let response = ts.response else { + fatalError("Transfer completed, but there's no response.") + } + internalState = .transferCompleted(response: response, bodyDataDrain: ts.bodyDataDrain) + let action = completionAction(forCompletedRequest: request, response: response) + + switch action { + case .completeTask: + completeTask() + case .failWithError(let errorCode): + internalState = .transferFailed + let error = NSError(domain: NSURLErrorDomain, code: errorCode, + userInfo: [NSLocalizedDescriptionKey: "Completion failure"]) + failWith(error: error, request: request) + case .redirectWithRequest(let newRequest): + redirectFor(request: newRequest) + } + } + + func redirectFor(request: URLRequest) { + NSRequiresConcreteImplementation() + } + + func completeTask() { + guard case .transferCompleted(response: let response, bodyDataDrain: let bodyDataDrain) = self.internalState else { + fatalError("Trying to complete the task, but its transfer isn't complete.") + } + task?.response = response + // We don't want a timeout to be triggered after this. The timeout timer needs to be cancelled. + easyHandle.timeoutTimer = nil + // because we deregister the task with the session on internalState being set to taskCompleted + // we need to do the latter after the delegate/handler was notified/invoked + if case .inMemory(let bodyData) = bodyDataDrain { + var data = Data() + if let body = bodyData { + data = Data(bytes: body.bytes, count: body.length) + } + self.client?.urlProtocol(self, didLoad: data) + self.internalState = .taskCompleted + } + + if case .toFile(let url, let fileHandle?) = bodyDataDrain { + self.properties[.temporaryFileURL] = url + fileHandle.closeFile() + } + self.client?.urlProtocolDidFinishLoading(self) + self.internalState = .taskCompleted + } + + func completionAction(forCompletedRequest request: URLRequest, response: URLResponse) -> _CompletionAction { + return .completeTask + } + + func seekInputStream(to position: UInt64) throws { + // We will reset the body sourse and seek forward. + NSUnimplemented() + } + + func updateProgressMeter(with propgress: _EasyHandle._Progress) { + //TODO: Update progress. Note that a single URLSessionTask might + // perform multiple transfers. The values in `progress` are only for + // the current transfer. + } + + /// The data drain. + /// + /// This depends on what the delegate / completion handler need. + fileprivate func createTransferBodyDataDrain() -> _DataDrain { + guard let task = task else { + fatalError() + } + let s = task.session as! URLSession + switch s.behaviour(for: task) { + case .noDelegate: + return .ignore + case .taskDelegate: + // Data will be forwarded to the delegate as we receive it, we don't + // need to do anything about it. + return .ignore + case .dataCompletionHandler: + // Data needs to be concatenated in-memory such that we can pass it + // to the completion handler upon completion. + return .inMemory(nil) + case .downloadCompletionHandler: + // Data needs to be written to a file (i.e. a download task). + let fileHandle = try! FileHandle(forWritingTo: self.tempFileURL) + return .toFile(self.tempFileURL, fileHandle) + } + } + + func createTransferState(url: URL, workQueue: DispatchQueue) -> _TransferState { + let drain = createTransferBodyDataDrain() + guard let t = task else { + fatalError("Cannot create transfer state") + } + switch t.body { + case .none: + return _TransferState(url: url, bodyDataDrain: drain) + case .data(let data): + let source = _BodyDataSource(data: data) + return _TransferState(url: url, bodyDataDrain: drain,bodySource: source) + case .file(let fileURL): + let source = _BodyFileSource(fileURL: fileURL, workQueue: workQueue, dataAvailableHandler: { [weak self] in + // Unpause the easy handle + self?.easyHandle.unpauseSend() + }) + return _TransferState(url: url, bodyDataDrain: drain,bodySource: source) + case .stream: + NSUnimplemented() + } + } + + /// Start a new transfer + func startNewTransfer(with request: URLRequest) { + guard let t = task else { + fatalError() + } + t.currentRequest = request + guard let url = request.url else { + fatalError("No URL in request.") + } + + self.internalState = .transferReady(createTransferState(url: url, workQueue: t.workQueue)) + configureEasyHandle(for: request) + if (t.suspendCount) < 1 { + resume() + } + } + + func resume() { + if case .initial = self.internalState { + guard let r = task?.originalRequest else { + fatalError("Task has no original request.") + } + startNewTransfer(with: r) + } + + if case .transferReady(let transferState) = self.internalState { + self.internalState = .transferInProgress(transferState) + } + } + + func suspend() { + if case .transferInProgress(let transferState) = self.internalState { + self.internalState = .transferReady(transferState) + } + } + + func configureEasyHandle(for: URLRequest) { + NSRequiresConcreteImplementation() + } +} + +extension _NativeProtocol { + /// Action to be taken after a transfer completes + enum _CompletionAction { + case completeTask + case failWithError(Int) + case redirectWithRequest(URLRequest) + } + + func completeTask(withError error: Error) { + task?.error = error + guard case .transferFailed = self.internalState else { + fatalError("Trying to complete the task, but its transfer isn't complete / failed.") + } + //We don't want a timeout to be triggered after this. The timeout timer needs to be cancelled. + easyHandle.timeoutTimer = nil + self.internalState = .taskCompleted + } + + func failWith(error: NSError, request: URLRequest) { + //TODO: Error handling + let userInfo: [String : Any]? = request.url.map { + [ + NSUnderlyingErrorKey: error, + NSURLErrorFailingURLErrorKey: $0, + NSURLErrorFailingURLStringErrorKey: $0.absoluteString, + NSLocalizedDescriptionKey: NSLocalizedString(error.localizedDescription, comment: "N/A") + ] + } + let urlError = URLError(_nsError: NSError(domain: NSURLErrorDomain, code: error.code, userInfo: userInfo)) + completeTask(withError: urlError) + self.client?.urlProtocol(self, didFailWithError: urlError) + } + + /// Give the delegate a chance to tell us how to proceed once we have a + /// response / complete header. + /// + /// This will pause the transfer. + func askDelegateHowToProceedAfterCompleteResponse(_ response: URLResponse, delegate: URLSessionDataDelegate) { + // Ask the delegate how to proceed. + + // This will pause the easy handle. We need to wait for the + // delegate before processing any more data. + guard case .transferInProgress(let ts) = self.internalState else { + fatalError("Transfer not in progress.") + } + self.internalState = .waitingForResponseCompletionHandler(ts) + + let dt = task as! URLSessionDataTask + + // We need this ugly cast in order to be able to support `URLSessionTask.init()` + guard let s = task?.session as? URLSession else { + fatalError() + } + s.delegateQueue.addOperation { + delegate.urlSession(s, dataTask: dt, didReceive: response, completionHandler: { [weak self] disposition in + guard let task = self else { return } + self?.task?.workQueue.async { + task.didCompleteResponseCallback(disposition: disposition) + } + }) + } + } + + /// This gets called (indirectly) when the data task delegates lets us know + /// how we should proceed after receiving a response (i.e. complete header). + func didCompleteResponseCallback(disposition: URLSession.ResponseDisposition) { + guard case .waitingForResponseCompletionHandler(let ts) = self.internalState else { + fatalError("Received response disposition, but we're not waiting for it.") + } + switch disposition { + case .cancel: + let error = URLError(_nsError: NSError(domain: NSURLErrorDomain, code: NSURLErrorCancelled)) + self.completeTask(withError: error) + self.client?.urlProtocol(self, didFailWithError: error) + case .allow: + // Continue the transfer. This will unpause the easy handle. + self.internalState = .transferInProgress(ts) + case .becomeDownload: + /* Turn this request into a download */ + NSUnimplemented() + case .becomeStream: + /* Turn this task into a stream task */ + NSUnimplemented() + } + } +} + +extension _NativeProtocol { + /// State related to an ongoing transfer. + /// + /// This contains headers received so far, body data received so far, etc. + /// + /// There's a strict 1-to-1 relationship between an `EasyHandle` and a + /// `TransferState`. + /// + /// - TODO: Might move the `EasyHandle` into this `struct` ? + /// - SeeAlso: `URLSessionTask.EasyHandle` + internal struct _TransferState { + /// The URL that's being requested + let url: URL + /// Raw headers received. + let parsedResponseHeader: _ParsedResponseHeader + /// Once the headers are complete, this will contain the response + var response: URLResponse? + /// The body data to be sent in the request + let requestBodySource: _BodySource? + /// Body data received + let bodyDataDrain: _NativeProtocol._DataDrain + } +} + +extension _NativeProtocol { + + enum _InternalState { + /// Task has been created, but nothing has been done, yet + case initial + /// The easy handle has been fully configured. But it is not added to + /// the multi handle. + case transferReady(_TransferState) + /// The easy handle is currently added to the multi handle + case transferInProgress(_TransferState) + /// The transfer completed. + /// + /// The easy handle has been removed from the multi handle. This does + /// not necessarily mean the task completed. A task that gets + /// redirected will do multiple transfers. + case transferCompleted(response: URLResponse, bodyDataDrain: _NativeProtocol._DataDrain) + /// The transfer failed. + /// + /// Same as `.transferCompleted`, but without response / body data + case transferFailed + /// Waiting for the completion handler of the HTTP redirect callback. + /// + /// When we tell the delegate that we're about to perform an HTTP + /// redirect, we need to wait for the delegate to let us know what + /// action to take. + case waitingForRedirectCompletionHandler(response: URLResponse, bodyDataDrain: _NativeProtocol._DataDrain) + /// Waiting for the completion handler of the 'did receive response' callback. + /// + /// When we tell the delegate that we received a response (i.e. when + /// we received a complete header), we need to wait for the delegate to + /// let us know what action to take. In this state the easy handle is + /// paused in order to suspend delegate callbacks. + case waitingForResponseCompletionHandler(_TransferState) + /// The task is completed + /// + /// Contrast this with `.transferCompleted`. + case taskCompleted + } +} + +extension _NativeProtocol._InternalState { + var isEasyHandleAddedToMultiHandle: Bool { + switch self { + case .initial: return false + case .transferReady: return false + case .transferInProgress: return true + case .transferCompleted: return false + case .transferFailed: return false + case .waitingForRedirectCompletionHandler: return false + case .waitingForResponseCompletionHandler: return true + case .taskCompleted: return false + } + } + + var isEasyHandlePaused: Bool { + switch self { + case .initial: return false + case .transferReady: return false + case .transferInProgress: return false + case .transferCompleted: return false + case .transferFailed: return false + case .waitingForRedirectCompletionHandler: return false + case .waitingForResponseCompletionHandler: return true + case .taskCompleted: return false + } + } +} + +extension _NativeProtocol { + + enum _DataDrain { + /// Concatenate in-memory + case inMemory(NSMutableData?) + /// Write to file + case toFile(URL, FileHandle?) + /// Do nothing. Might be forwarded to delegate + case ignore + } + + enum _Error: Error { + case parseSingleLineError + case parseCompleteHeaderError + } + + func errorCode(fileSystemError error: Error) -> Int { + func fromCocoaErrorCode(_ code: Int) -> Int { + switch code { + case CocoaError.fileReadNoSuchFile.rawValue: + return NSURLErrorFileDoesNotExist + case CocoaError.fileReadNoPermission.rawValue: + return NSURLErrorNoPermissionsToReadFile + default: + return NSURLErrorUnknown + } + } + switch error { + case let e as NSError where e.domain == NSCocoaErrorDomain: + return fromCocoaErrorCode(e.code) + default: + return NSURLErrorUnknown + } + } +} + +extension _NativeProtocol._TransferState { + /// Transfer state that can receive body data, but will not send body data. + init(url: URL, bodyDataDrain: _NativeProtocol._DataDrain) { + self.url = url + self.bodyDataDrain = bodyDataDrain + self.response = nil + self.parsedResponseHeader = _NativeProtocol._ParsedResponseHeader() + self.requestBodySource = nil + } + + /// Transfer state that sends body data and can receive body data. + init(url: URL, bodyDataDrain: _NativeProtocol._DataDrain, bodySource: _BodySource) { + self.url = url + self.parsedResponseHeader = _NativeProtocol._ParsedResponseHeader() + self.response = nil + self.requestBodySource = bodySource + self.bodyDataDrain = bodyDataDrain + } + +} + +struct _Delimiters { + /// *Carriage Return* symbol + static let CR: UInt8 = 0x0d + /// *Line Feed* symbol + static let LF: UInt8 = 0x0a + /// *Space* symbol + static let Space = UnicodeScalar(0x20) + static let HorizontalTab = UnicodeScalar(0x09) + static let Colon = UnicodeScalar(0x3a) + /// *Separators* according to RFC 2616 + static let Separators = NSCharacterSet(charactersIn: "()<>@,;:\\\"/[]?={} \t") +} + +extension _NativeProtocol { + /// Response header being parsed. + /// + /// It can either be complete (i.e. the final CR LF CR LF has been + /// received), or partial. + internal enum _ParsedResponseHeader { + case partial(_ResponseHeaderLines) + case complete(_ResponseHeaderLines) + init() { + self = .partial(_ResponseHeaderLines()) + } + } + /// A type safe wrapper around multiple lines of headers. + /// + /// This can be converted into an `NSHTTPURLResponse`. + internal struct _ResponseHeaderLines { + let lines: [String] + init() { + self.lines = [] + } + init(headerLines: [String]) { + self.lines = headerLines + } + } + +} + +extension _NativeProtocol._ResponseHeaderLines { + func createURLResponse(for URL: URL, contentLength: Int64) -> URLResponse? { + return URLResponse(url: URL, mimeType: nil, expectedContentLength: Int(contentLength), textEncodingName: nil) + } +} + +extension _NativeProtocol { + /// Set request body length. + /// + /// An unknown length + func set(requestBodyLength length: _HTTPURLProtocol._RequestBodyLength) { + switch length { + case .noBody: + easyHandle.set(upload: false) + easyHandle.set(requestBodyLength: 0) + case .length(let length): + easyHandle.set(upload: true) + easyHandle.set(requestBodyLength: Int64(length)) + case .unknown: + easyHandle.set(upload: true) + easyHandle.set(requestBodyLength: -1) + } + } + enum _RequestBodyLength { + case noBody + /// + case length(UInt64) + /// Will result in a chunked upload + case unknown + } + } + +extension _NativeProtocol._ParsedResponseHeader { + /// Parse a header line passed by libcurl. + /// + /// These contain the ending and the final line contains nothing but + /// that ending. + /// - Returns: Returning nil indicates failure. Otherwise returns a new + /// `ParsedResponseHeader` with the given line added. + func byAppending(headerLine data: Data, isHeaderComplete: (String) -> Bool) -> _NativeProtocol._ParsedResponseHeader? { + // The buffer must end in CRLF + guard 2 <= data.count && + data[data.endIndex - 2] == _Delimiters.CR && + data[data.endIndex - 1] == _Delimiters.LF + else { return nil } + let lineBuffer = data.subdata(in: Range(data.startIndex.. Bool) -> _NativeProtocol._ParsedResponseHeader { + if isHeaderComplete(line) { + switch self { + case .partial(let header): return .complete(header) + case .complete: return .partial(_NativeProtocol._ResponseHeaderLines()) + } + } else { + let header = partialResponseHeader + return .partial(header.byAppending(headerLine: line)) + } + } + + private var partialResponseHeader: _NativeProtocol._ResponseHeaderLines { + switch self { + case .partial(let header): return header + case .complete: return _NativeProtocol._ResponseHeaderLines() + } + } +} + +extension _NativeProtocol._ResponseHeaderLines { + /// Returns a copy of the lines with the new line appended to it. + func byAppending(headerLine line: String) -> _NativeProtocol._ResponseHeaderLines { + var l = self.lines + l.append(line) + return _NativeProtocol._ResponseHeaderLines(headerLines: l) + } +} + +extension _NativeProtocol._TransferState { + var isHeaderComplete: Bool { + return response != nil + } + func byAppending(bodyData buffer: Data) -> _NativeProtocol._TransferState { + switch bodyDataDrain { + case .inMemory(let bodyData): + let data: NSMutableData = bodyData ?? NSMutableData() + data.append(buffer) + let drain = _NativeProtocol._DataDrain.inMemory(data) + return _NativeProtocol._TransferState(url: url, parsedResponseHeader: parsedResponseHeader, response: response, requestBodySource: requestBodySource, bodyDataDrain: drain) + case .toFile(_, let fileHandle): + //TODO: Create / open the file for writing + // Append to the file + _ = fileHandle!.seekToEndOfFile() + fileHandle!.write(buffer) + return self + case .ignore: + return self + } + } + /// Sets the given body source on the transfer state. + /// + /// This can be used to either set the initial body source, or to reset it + /// e.g. when restarting a transfer. + func bySetting(bodySource newSource: _BodySource) -> _NativeProtocol._TransferState { + return _NativeProtocol._TransferState(url: url, parsedResponseHeader: parsedResponseHeader, response: response, requestBodySource: newSource, bodyDataDrain: bodyDataDrain) + } +} + +extension _HTTPURLProtocol._TransferState { + /// Appends a header line + /// + /// Will set the complete response once the header is complete, i.e. the + /// return value's `isHeaderComplete` will then by `true`. + /// + /// - Throws: When a parsing error occurs + func byAppendingHTTP(headerLine data: Data) throws -> _NativeProtocol._TransferState { + func isCompleteHeader(_ headerLine: String) -> Bool { + return headerLine.isEmpty + } + guard let h = parsedResponseHeader.byAppending(headerLine: data, isHeaderComplete: isCompleteHeader) else { + throw _NativeProtocol._Error.parseSingleLineError + } + if case .complete(let lines) = h { + // Header is complete + let response = lines.createHTTPURLResponse(for: url) + guard response != nil else { + throw _NativeProtocol._Error.parseCompleteHeaderError + } + return _NativeProtocol._TransferState(url: url, parsedResponseHeader: _NativeProtocol._ParsedResponseHeader(), response: response, requestBodySource: requestBodySource, bodyDataDrain: bodyDataDrain) + } else { + return _NativeProtocol._TransferState(url: url, parsedResponseHeader: h, response: nil, requestBodySource: requestBodySource, bodyDataDrain: bodyDataDrain) + } + } +} From 9202c7211f3733fa0cc8245dbf454584dcd746bf Mon Sep 17 00:00:00 2001 From: saiHemak Date: Fri, 10 Nov 2017 12:27:59 +0530 Subject: [PATCH 07/11] Moved protocol independent code to NativeProtocol.swift Moved HTTPURLProtocol. _ParsedResponseHeader from HTTPMessage.swift to NativeProtocol.swift under _NativeProtocol._ParsedResponseHeader. This implementation basically does header parsing based on \r\n delimiters. Except the fact that empty header is received in case of HTTP to mark as Header completion and 150 status marks the open of data communication in FTP. --- Foundation/URLSession/http/HTTPMessage.swift | 77 -------------------- 1 file changed, 77 deletions(-) diff --git a/Foundation/URLSession/http/HTTPMessage.swift b/Foundation/URLSession/http/HTTPMessage.swift index a45a8cfae4..3150bf2c4b 100644 --- a/Foundation/URLSession/http/HTTPMessage.swift +++ b/Foundation/URLSession/http/HTTPMessage.swift @@ -19,83 +19,6 @@ import CoreFoundation - -extension _HTTPURLProtocol { - /// An HTTP header being parsed. - /// - /// It can either be complete (i.e. the final CR LF CR LF has been - /// received), or partial. - internal enum _ParsedResponseHeader { - case partial(_ResponseHeaderLines) - case complete(_ResponseHeaderLines) - init() { - self = .partial(_ResponseHeaderLines()) - } - } - /// A type safe wrapper around multiple lines of headers. - /// - /// This can be converted into an `HTTPURLResponse`. - internal struct _ResponseHeaderLines { - let lines: [String] - init() { - self.lines = [] - } - init(headerLines: [String]) { - self.lines = headerLines - } - } -} - -extension _HTTPURLProtocol._ParsedResponseHeader { - /// Parse a header line passed by libcurl. - /// - /// These contain the ending and the final line contains nothing but - /// that ending. - /// - Returns: Returning nil indicates failure. Otherwise returns a new - /// `ParsedResponseHeader` with the given line added. - func byAppending(headerLine data: Data) -> _HTTPURLProtocol._ParsedResponseHeader? { - // The buffer must end in CRLF - guard - 2 <= data.count && - data[data.endIndex - 2] == _HTTPCharacters.CR && - data[data.endIndex - 1] == _HTTPCharacters.LF - else { return nil } - let lineBuffer = data.subdata(in: Range(data.startIndex.. _HTTPURLProtocol._ParsedResponseHeader { - if line.isEmpty { - switch self { - case .partial(let header): return .complete(header) - case .complete: return .partial(_HTTPURLProtocol._ResponseHeaderLines()) - } - } else { - let header = partialResponseHeader - return .partial(header.byAppending(headerLine: line)) - } - } - private var partialResponseHeader: _HTTPURLProtocol._ResponseHeaderLines { - switch self { - case .partial(let header): return header - case .complete: return _HTTPURLProtocol._ResponseHeaderLines() - } - } -} -private extension _HTTPURLProtocol._ResponseHeaderLines { - /// Returns a copy of the lines with the new line appended to it. - func byAppending(headerLine line: String) -> _HTTPURLProtocol._ResponseHeaderLines { - var l = self.lines - l.append(line) - return _HTTPURLProtocol._ResponseHeaderLines(headerLines: l) - } -} internal extension _HTTPURLProtocol._ResponseHeaderLines { /// Create an `NSHTTPRULResponse` from the lines. /// From ac1d667ffc58fb0dff54542618e7014f485fe609 Mon Sep 17 00:00:00 2001 From: saiHemak Date: Fri, 10 Nov 2017 12:30:17 +0530 Subject: [PATCH 08/11] Refactored common code out of HTTPURLProtocol.swift Moved methods specific to EasyHandle,common variables and common initializers to NativeProtocol.swift. Also overridden methods like configureEasyHandle which are specific to HTTP --- .../URLSession/http/HTTPURLProtocol.swift | 687 +++--------------- 1 file changed, 120 insertions(+), 567 deletions(-) diff --git a/Foundation/URLSession/http/HTTPURLProtocol.swift b/Foundation/URLSession/http/HTTPURLProtocol.swift index 1c1c8700cd..fba4400bb4 100644 --- a/Foundation/URLSession/http/HTTPURLProtocol.swift +++ b/Foundation/URLSession/http/HTTPURLProtocol.swift @@ -10,41 +10,29 @@ import CoreFoundation import Dispatch -internal class _HTTPURLProtocol: URLProtocol { - - fileprivate var easyHandle: _EasyHandle! - fileprivate lazy var tempFileURL: URL = { - let fileName = NSTemporaryDirectory() + NSUUID().uuidString + ".tmp" - _ = FileManager.default.createFile(atPath: fileName, contents: nil) - return URL(fileURLWithPath: fileName) - }() - +internal class _HTTPURLProtocol: _NativeProtocol { + public required init(task: URLSessionTask, cachedResponse: CachedURLResponse?, client: URLProtocolClient?) { - self.internalState = _InternalState.initial - super.init(request: task.originalRequest!, cachedResponse: cachedResponse, client: client) - self.task = task - self.easyHandle = _EasyHandle(delegate: self) + super.init(task: task, cachedResponse: cachedResponse, client: client) } public required init(request: URLRequest, cachedResponse: CachedURLResponse?, client: URLProtocolClient?) { - self.internalState = _InternalState.initial super.init(request: request, cachedResponse: cachedResponse, client: client) - self.easyHandle = _EasyHandle(delegate: self) } - + override class func canInit(with request: URLRequest) -> Bool { guard request.url?.scheme == "http" || request.url?.scheme == "https" else { return false } return true } - + override class func canonicalRequest(for request: URLRequest) -> URLRequest { return request } - + override func startLoading() { resume() } - + override func stopLoading() { if task?.state == .suspended { suspend() @@ -54,67 +42,59 @@ internal class _HTTPURLProtocol: URLProtocol { completeTask(withError: error) } } - - /// The internal state that the task is in. - /// - /// Setting this value will also add / remove the easy handle. - /// It is independt of the `state: URLSessionTask.State`. The - /// `internalState` tracks the state of transfers / waiting for callbacks. - /// The `state` tracks the overall state of the task (running vs. - /// completed). - fileprivate var internalState: _InternalState { - // We manage adding / removing the easy handle and pausing / unpausing - // here at a centralized place to make sure the internal state always - // matches up with the state of the easy handle being added and paused. - willSet { - if !internalState.isEasyHandlePaused && newValue.isEasyHandlePaused { - fatalError("Need to solve pausing receive.") - } - if internalState.isEasyHandleAddedToMultiHandle && !newValue.isEasyHandleAddedToMultiHandle { - task?.session.remove(handle: easyHandle) - } + + override func didReceive(headerData data: Data, contentLength: Int64) -> _EasyHandle._Action { + guard case .transferInProgress(let ts) = internalState else { + fatalError("Received header data, but no transfer in progress.") } - didSet { - if !oldValue.isEasyHandleAddedToMultiHandle && internalState.isEasyHandleAddedToMultiHandle { - task?.session.add(handle: easyHandle) - } - if oldValue.isEasyHandlePaused && !internalState.isEasyHandlePaused { - fatalError("Need to solve pausing receive.") + guard let task = task else { + fatalError("Received header data but no task available.") + } + task.countOfBytesExpectedToReceive = contentLength > 0 ? contentLength : NSURLSessionTransferSizeUnknown + do { + let newTS = try ts.byAppendingHTTP(headerLine: data) + internalState = .transferInProgress(newTS) + let didCompleteHeader = !ts.isHeaderComplete && newTS.isHeaderComplete + if didCompleteHeader { + // The header is now complete, but wasn't before. + didReceiveResponse() } + return .proceed + } catch { + return .abort } } -} - -fileprivate extension _HTTPURLProtocol { - + /// Set options on the easy handle to match the given request. /// /// This performs a series of `curl_easy_setopt()` calls. - fileprivate func configureEasyHandle(for request: URLRequest) { + override func configureEasyHandle(for request: URLRequest) { // At this point we will call the equivalent of curl_easy_setopt() // to configure everything on the handle. Since we might be re-using // a handle, we must be sure to set everything and not rely on default // values. - + //TODO: We could add a strong reference from the easy handle back to // its URLSessionTask by means of CURLOPT_PRIVATE -- that would ensure // that the task is always around while the handle is running. // We would have to break that retain cycle once the handle completes // its transfer. - + // Behavior Options easyHandle.set(verboseModeOn: enableLibcurlDebugOutput) easyHandle.set(debugOutputOn: enableLibcurlDebugOutput, task: task!) easyHandle.set(passHeadersToDataStream: false) easyHandle.set(progressMeterOff: true) easyHandle.set(skipAllSignalHandling: true) - + // Error Options: easyHandle.set(errorBuffer: nil) easyHandle.set(failOnHTTPErrorCode: false) - + // Network Options: - guard let url = request.url else { fatalError("No URL in request.") } + guard let url = request.url else { + fatalError("No URL in request.") + } easyHandle.set(url: url) easyHandle.setAllowedProtocolsToHTTPAndHTTPS() easyHandle.set(preferredReceiveBufferSize: Int.max) @@ -139,22 +119,22 @@ fileprivate extension _HTTPURLProtocol { failWith(error: error, request: request) return } - + // HTTP Options: easyHandle.set(followLocation: false) - + // The httpAdditionalHeaders from session configuration has to be added to the request. // The request.allHTTPHeaders can override the httpAdditionalHeaders elements. Add the // httpAdditionalHeaders from session configuration first and then append/update the // request.allHTTPHeaders so that request.allHTTPHeaders can override httpAdditionalHeaders. - + let httpSession = self.task?.session as! URLSession var httpHeaders: [AnyHashable : Any]? - + if let hh = httpSession.configuration.httpAdditionalHeaders { httpHeaders = hh } - + if let hh = self.task?.originalRequest?.allHTTPHeaderFields { if httpHeaders == nil { httpHeaders = hh @@ -171,36 +151,101 @@ fileprivate extension _HTTPURLProtocol { } else { customHeaders = headersForRequest } - + easyHandle.set(customHeaders: customHeaders) - + //TODO: The CURLOPT_PIPEDWAIT option is unavailable on Ubuntu 14.04 (libcurl 7.36) //TODO: Introduce something like an #if, if we want to set them here - + //set the request timeout //TODO: the timeout value needs to be reset on every data transfer - + var timeoutInterval = Int(httpSession.configuration.timeoutIntervalForRequest) * 1000 if request.isTimeoutIntervalSet { - timeoutInterval = Int(request.timeoutInterval) * 1000 + timeoutInterval = Int(request.timeoutInterval) * 1000 } let timeoutHandler = DispatchWorkItem { [weak self] in - guard let _ = self?.task else { fatalError("Timeout on a task that doesn't exist") } //this guard must always pass + guard let _ = self?.task else { + fatalError("Timeout on a task that doesn't exist") + } //this guard must always pass self?.internalState = .transferFailed let urlError = URLError(_nsError: NSError(domain: NSURLErrorDomain, code: NSURLErrorTimedOut, userInfo: nil)) self?.completeTask(withError: urlError) self?.client?.urlProtocol(self!, didFailWithError: urlError) } - guard let task = self.task else { fatalError() } + guard let task = self.task else { fatalError() } easyHandle.timeoutTimer = _TimeoutSource(queue: task.workQueue, milliseconds: timeoutInterval, handler: timeoutHandler) - + easyHandle.set(automaticBodyDecompression: true) easyHandle.set(requestMethod: request.httpMethod ?? "GET") if request.httpMethod == "HEAD" { easyHandle.set(noBody: true) } } + + /// What action to take + override func completionAction(forCompletedRequest request: URLRequest, response: URLResponse) -> _CompletionAction { + // Redirect: + guard let httpURLResponse = response as? HTTPURLResponse else { + fatalError("Reponse was not HTTPURLResponse") + } + if let request = redirectRequest(for: httpURLResponse, fromRequest: request) { + return .redirectWithRequest(request) + } + return .completeTask + } + + override func redirectFor(request: URLRequest) { + //TODO: Should keep track of the number of redirects that this + // request has gone through and err out once it's too large, i.e. + // call into `failWith(errorCode: )` with NSURLErrorHTTPTooManyRedirects + guard case .transferCompleted(response: let response, bodyDataDrain: let bodyDataDrain) = self.internalState else { + fatalError("Trying to redirect, but the transfer is not complete.") + } + + guard let session = task?.session as? URLSession else { fatalError() } + switch session.behaviour(for: task!) { + case .taskDelegate(let delegate): + // At this point we need to change the internal state to note + // that we're waiting for the delegate to call the completion + // handler. Then we'll call the delegate callback + // (willPerformHTTPRedirection). The task will then switch out of + // its internal state once the delegate calls the completion + // handler. + + //TODO: Should the `public response: URLResponse` property be updated + // before we call delegate API + + self.internalState = .waitingForRedirectCompletionHandler(response: response, bodyDataDrain: bodyDataDrain) + // We need this ugly cast in order to be able to support `URLSessionTask.init()` + session.delegateQueue.addOperation { + delegate.urlSession(session, task: self.task!, willPerformHTTPRedirection: response as! HTTPURLResponse, newRequest: request) { [weak self] (request: URLRequest?) in + guard let task = self else { return } + self?.task?.workQueue.async { + task.didCompleteRedirectCallback(request) + } + } + } + case .noDelegate, .dataCompletionHandler, .downloadCompletionHandler: + // Follow the redirect. + startNewTransfer(with: request) + } + } + + override func validateHeaderComplete(transferState: _NativeProtocol._TransferState) -> URLResponse? { + if !transferState.isHeaderComplete { + return HTTPURLResponse(url: transferState.url, statusCode: 200, httpVersion: "HTTP/0.9", headerFields: [:]) + /* we received body data before CURL tells us that the headers are complete, that happens for HTTP/0.9 simple responses, see + - https://www.w3.org/Protocols/HTTP/1.0/spec.html#Message-Types + - https://github.com/curl/curl/issues/467 + */ + } + return nil + } +} +fileprivate extension _HTTPURLProtocol { + /// These are a list of headers that should be passed to libcurl. /// /// Headers will be returned as `Accept: text/html` strings for @@ -269,32 +314,6 @@ fileprivate extension _HTTPURLProtocol { } } -fileprivate extension _HTTPURLProtocol { - /// Set request body length. - /// - /// An unknown length - func set(requestBodyLength length: _HTTPURLProtocol._RequestBodyLength) { - switch length { - case .noBody: - easyHandle.set(upload: false) - easyHandle.set(requestBodyLength: 0) - case .length(let length): - easyHandle.set(upload: true) - easyHandle.set(requestBodyLength: Int64(length)) - case .unknown: - easyHandle.set(upload: true) - easyHandle.set(requestBodyLength: -1) - } - } - enum _RequestBodyLength { - case noBody - /// - case length(UInt64) - /// Will result in a chunked upload - case unknown - } -} - fileprivate var userAgentString: String = { // Darwin uses something like this: "xctest (unknown version) CFNetwork/760.4.2 Darwin/15.4.0 (x86_64)" let info = ProcessInfo.processInfo @@ -307,13 +326,6 @@ fileprivate var userAgentString: String = { return "\(name) (unknown version) curl/\(curlVersion.major).\(curlVersion.minor).\(curlVersion.patch)" }() -fileprivate let enableLibcurlDebugOutput: Bool = { - return (ProcessInfo.processInfo.environment["URLSessionDebugLibcurl"] != nil) -}() -fileprivate let enableDebugOutput: Bool = { - return (ProcessInfo.processInfo.environment["URLSessionDebug"] != nil) -}() - extension URLSession { static func printDebug(_ text: @autoclosure () -> String) { guard enableDebugOutput else { return } @@ -329,27 +341,13 @@ internal extension _HTTPURLProtocol { case file(URL) case stream(InputStream) } - - func failWith(error: NSError, request: URLRequest) { - //TODO: Error handling - let userInfo: [String : Any]? = request.url.map { - [ - NSUnderlyingErrorKey: error, - NSURLErrorFailingURLErrorKey: $0, - NSURLErrorFailingURLStringErrorKey: $0.absoluteString, - NSLocalizedDescriptionKey: NSLocalizedString(error.localizedDescription, comment: "N/A") - ] - } - let urlError = URLError(_nsError: NSError(domain: NSURLErrorDomain, code: error.code, userInfo: userInfo)) - completeTask(withError: urlError) - self.client?.urlProtocol(self, didFailWithError: urlError) - } } fileprivate extension _HTTPURLProtocol._Body { enum _Error : Error { case fileForBodyDataNotFound } + /// - Returns: The body length, or `nil` for no body (e.g. `GET` request). func getBodyLength() throws -> UInt64? { switch self { @@ -369,394 +367,8 @@ fileprivate extension _HTTPURLProtocol._Body { } } -fileprivate func errorCode(fileSystemError error: Error) -> Int { - func fromCocoaErrorCode(_ code: Int) -> Int { - switch code { - case CocoaError.fileReadNoSuchFile.rawValue: - return NSURLErrorFileDoesNotExist - case CocoaError.fileReadNoPermission.rawValue: - return NSURLErrorNoPermissionsToReadFile - default: - return NSURLErrorUnknown - } - } - switch error { - case let e as NSError where e.domain == NSCocoaErrorDomain: - return fromCocoaErrorCode(e.code) - default: - return NSURLErrorUnknown - } -} - -internal extension _HTTPURLProtocol { - /// The data drain. - /// - /// This depends on what the delegate / completion handler need. - fileprivate func createTransferBodyDataDrain() -> _DataDrain { - guard let task = task else { fatalError() } - let s = task.session as! URLSession - switch s.behaviour(for: task) { - case .noDelegate: - return .ignore - case .taskDelegate: - // Data will be forwarded to the delegate as we receive it, we don't - // need to do anything about it. - return .ignore - case .dataCompletionHandler: - // Data needs to be concatenated in-memory such that we can pass it - // to the completion handler upon completion. - return .inMemory(nil) - case .downloadCompletionHandler: - // Data needs to be written to a file (i.e. a download task). - let fileHandle = try! FileHandle(forWritingTo: self.tempFileURL) - return .toFile(self.tempFileURL, fileHandle) - } - } -} - -extension _HTTPURLProtocol { - - /// Creates a new transfer state with the given behaviour: - func createTransferState(url: URL, workQueue: DispatchQueue) -> _HTTPTransferState { - let drain = createTransferBodyDataDrain() - guard let t = task else { fatalError("Cannot create transfer state") } - switch t.body { - case .none: - return _HTTPTransferState(url: url, bodyDataDrain: drain) - case .data(let data): - let source = _HTTPBodyDataSource(data: data) - return _HTTPTransferState(url: url, bodyDataDrain: drain, bodySource: source) - case .file(let fileURL): - let source = _HTTPBodyFileSource(fileURL: fileURL, workQueue: workQueue, dataAvailableHandler: { [weak self] in - // Unpause the easy handle - self?.easyHandle.unpauseSend() - }) - return _HTTPTransferState(url: url, bodyDataDrain: drain, bodySource: source) - case .stream: - NSUnimplemented() - } - } -} - -extension _HTTPURLProtocol: _EasyHandleDelegate { - - func didReceive(data: Data) -> _EasyHandle._Action { - guard case .transferInProgress(var ts) = internalState else { fatalError("Received body data, but no transfer in progress.") } - if !ts.isHeaderComplete { - ts.response = HTTPURLResponse(url: ts.url, statusCode: 200, httpVersion: "HTTP/0.9", headerFields: [:]) - /* we received body data before CURL tells us that the headers are complete, that happens for HTTP/0.9 simple responses, see - - https://www.w3.org/Protocols/HTTP/1.0/spec.html#Message-Types - - https://github.com/curl/curl/issues/467 - */ - } - notifyDelegate(aboutReceivedData: data) - internalState = .transferInProgress(ts.byAppending(bodyData: data)) - return .proceed - } - - fileprivate func notifyDelegate(aboutReceivedData data: Data) { - guard let t = self.task else { fatalError("Cannot notify") } - if case .taskDelegate(let delegate) = t.session.behaviour(for: self.task!), - let _ = delegate as? URLSessionDataDelegate, - let _ = self.task as? URLSessionDataTask { - // Forward to protocol client: - self.client?.urlProtocol(self, didLoad: data) - } else if case .taskDelegate(let delegate) = t.session.behaviour(for: self.task!), - let downloadDelegate = delegate as? URLSessionDownloadDelegate, - let task = self.task as? URLSessionDownloadTask { - guard let s = self.task?.session as? URLSession else { fatalError() } - let fileHandle = try! FileHandle(forWritingTo: self.tempFileURL) - _ = fileHandle.seekToEndOfFile() - fileHandle.write(data) - task.countOfBytesReceived += Int64(data.count) - - s.delegateQueue.addOperation { - downloadDelegate.urlSession(s, downloadTask: task, didWriteData: Int64(data.count), totalBytesWritten: task.countOfBytesReceived, - totalBytesExpectedToWrite: task.countOfBytesExpectedToReceive) - } - if task.countOfBytesExpectedToReceive == task.countOfBytesReceived { - fileHandle.closeFile() - self.properties[.temporaryFileURL] = self.tempFileURL - } - } - } - - func didReceive(headerData data: Data, contentLength: Int64) -> _EasyHandle._Action { - guard case .transferInProgress(let ts) = internalState else { fatalError("Received header data, but no transfer in progress.") } - guard let task = task else { fatalError("Received header data but no task available.") } - task.countOfBytesExpectedToReceive = contentLength > 0 ? contentLength : NSURLSessionTransferSizeUnknown - do { - let newTS = try ts.byAppending(headerLine: data) - internalState = .transferInProgress(newTS) - let didCompleteHeader = !ts.isHeaderComplete && newTS.isHeaderComplete - if didCompleteHeader { - // The header is now complete, but wasn't before. - didReceiveResponse() - } - return .proceed - } catch { - return .abort - } - } - - fileprivate func notifyDelegate(aboutUploadedData count: Int64) { - guard let task = self.task as? URLSessionUploadTask, - let session = self.task?.session as? URLSession, - case .taskDelegate(let delegate) = session.behaviour(for: task) else { return } - task.countOfBytesSent += count - session.delegateQueue.addOperation { - delegate.urlSession(session, task: task, didSendBodyData: count, - totalBytesSent: task.countOfBytesSent, totalBytesExpectedToSend: task.countOfBytesExpectedToSend) - } - } - - func fill(writeBuffer buffer: UnsafeMutableBufferPointer) -> _EasyHandle._WriteBufferResult { - guard case .transferInProgress(let ts) = internalState else { fatalError("Requested to fill write buffer, but transfer isn't in progress.") } - guard let source = ts.requestBodySource else { fatalError("Requested to fill write buffer, but transfer state has no body source.") } - switch source.getNextChunk(withLength: buffer.count) { - case .data(let data): - copyDispatchData(data, infoBuffer: buffer) - let count = data.count - assert(count > 0) - notifyDelegate(aboutUploadedData: Int64(count)) - return .bytes(count) - case .done: - return .bytes(0) - case .retryLater: - // At this point we'll try to pause the easy handle. The body source - // is responsible for un-pausing the handle once data becomes - // available. - return .pause - case .error: - return .abort - } - } - - func transferCompleted(withError error: NSError?) { - // At this point the transfer is complete and we can decide what to do. - // If everything went well, we will simply forward the resulting data - // to the delegate. But in case of redirects etc. we might send another - // request. - guard case .transferInProgress(let ts) = internalState else { fatalError("Transfer completed, but it wasn't in progress.") } - guard let request = task?.currentRequest else { fatalError("Transfer completed, but there's no current request.") } - guard error == nil else { - internalState = .transferFailed - failWith(error: error!, request: request) - return - } - - if let response = task?.response as? HTTPURLResponse { - var transferState = ts - transferState.response = response - } - - guard let response = ts.response else { fatalError("Transfer completed, but there's no response.") } - internalState = .transferCompleted(response: response, bodyDataDrain: ts.bodyDataDrain) - let action = completionAction(forCompletedRequest: request, response: response) - - switch action { - case .completeTask: - completeTask() - case .failWithError(let errorCode): - internalState = .transferFailed - let error = NSError(domain: NSURLErrorDomain, code: errorCode, - userInfo: [NSLocalizedDescriptionKey: "Completion failure"]) - failWith(error: error, request: request) - case .redirectWithRequest(let newRequest): - redirectFor(request: newRequest) - } - } - - func seekInputStream(to position: UInt64) throws { - // We will reset the body source and seek forward. - NSUnimplemented() - } - - func updateProgressMeter(with propgress: _EasyHandle._Progress) { - //TODO: Update progress. Note that a single URLSessionTask might - // perform multiple transfers. The values in `progress` are only for - // the current transfer. - } -} - -extension _HTTPURLProtocol { - /// The is independent of the public `state: URLSessionTask.State`. - enum _InternalState { - /// Task has been created, but nothing has been done, yet - case initial - /// The easy handle has been fully configured. But it is not added to - /// the multi handle. - case transferReady(_HTTPTransferState) - /// The easy handle is currently added to the multi handle - case transferInProgress(_HTTPTransferState) - /// The transfer completed. - /// - /// The easy handle has been removed from the multi handle. This does - /// not (necessarily mean the task completed. A task that gets - /// redirected will do multiple transfers. - case transferCompleted(response: URLResponse, bodyDataDrain: _DataDrain) - /// The transfer failed. - /// - /// Same as `.transferCompleted`, but without response / body data - case transferFailed - /// Waiting for the completion handler of the HTTP redirect callback. - /// - /// When we tell the delegate that we're about to perform an HTTP - /// redirect, we need to wait for the delegate to let us know what - /// action to take. - case waitingForRedirectCompletionHandler(response: URLResponse, bodyDataDrain: _DataDrain) - /// Waiting for the completion handler of the 'did receive response' callback. - /// - /// When we tell the delegate that we received a response (i.e. when - /// we received a complete header), we need to wait for the delegate to - /// let us know what action to take. In this state the easy handle is - /// paused in order to suspend delegate callbacks. - case waitingForResponseCompletionHandler(_HTTPTransferState) - /// The task is completed - /// - /// Contrast this with `.transferCompleted`. - case taskCompleted - } -} - -extension _HTTPURLProtocol._InternalState { - var isEasyHandleAddedToMultiHandle: Bool { - switch self { - case .initial: return false - case .transferReady: return false - case .transferInProgress: return true - case .transferCompleted: return false - case .transferFailed: return false - case .waitingForRedirectCompletionHandler: return false - case .waitingForResponseCompletionHandler: return true - case .taskCompleted: return false - } - } - var isEasyHandlePaused: Bool { - switch self { - case .initial: return false - case .transferReady: return false - case .transferInProgress: return false - case .transferCompleted: return false - case .transferFailed: return false - case .waitingForRedirectCompletionHandler: return false - case .waitingForResponseCompletionHandler: return true - case .taskCompleted: return false - } - } -} - -internal extension _HTTPURLProtocol { - /// Start a new transfer - func startNewTransfer(with request: URLRequest) { - guard let t = task else { fatalError() } - t.currentRequest = request - guard let url = request.url else { fatalError("No URL in request.") } - - self.internalState = .transferReady(createTransferState(url: url, workQueue: t.workQueue)) - configureEasyHandle(for: request) - if (t.suspendCount) < 1 { - resume() - } - } - - func resume() { - if case .initial = self.internalState { - guard let r = task?.originalRequest else { fatalError("Task has no original request.") } - startNewTransfer(with: r) - } - - if case .transferReady(let transferState) = self.internalState { - self.internalState = .transferInProgress(transferState) - } - } - - func suspend() { - if case .transferInProgress(let transferState) = self.internalState { - self.internalState = .transferReady(transferState) - } - } -} - /// State Transfers extension _HTTPURLProtocol { - func completeTask() { - guard case .transferCompleted(response: let response, bodyDataDrain: let bodyDataDrain) = self.internalState else { - fatalError("Trying to complete the task, but its transfer isn't complete.") - } - task?.response = response - - //We don't want a timeout to be triggered after this. The timeout timer needs to be cancelled. - easyHandle.timeoutTimer = nil - - //because we deregister the task with the session on internalState being set to taskCompleted - //we need to do the latter after the delegate/handler was notified/invoked - if case .inMemory(let bodyData) = bodyDataDrain { - var data = Data() - if let body = bodyData { - data = Data(bytes: body.bytes, count: body.length) - } - self.client?.urlProtocol(self, didLoad: data) - self.internalState = .taskCompleted - } - - if case .toFile(let url, let fileHandle?) = bodyDataDrain { - self.properties[.temporaryFileURL] = url - fileHandle.closeFile() - } - self.client?.urlProtocolDidFinishLoading(self) - self.internalState = .taskCompleted - } - - func completeTask(withError error: Error) { - task?.error = error - - guard case .transferFailed = self.internalState else { - fatalError("Trying to complete the task, but its transfer isn't complete / failed.") - } - - //We don't want a timeout to be triggered after this. The timeout timer needs to be cancelled. - easyHandle.timeoutTimer = nil - self.internalState = .taskCompleted - } - - func redirectFor(request: URLRequest) { - //TODO: Should keep track of the number of redirects that this - // request has gone through and err out once it's too large, i.e. - // call into `failWith(errorCode: )` with NSURLErrorHTTPTooManyRedirects - guard case .transferCompleted(response: let response, bodyDataDrain: let bodyDataDrain) = self.internalState else { - fatalError("Trying to redirect, but the transfer is not complete.") - } - - guard let session = task?.session as? URLSession else { fatalError() } - switch session.behaviour(for: task!) { - case .taskDelegate(let delegate): - // At this point we need to change the internal state to note - // that we're waiting for the delegate to call the completion - // handler. Then we'll call the delegate callback - // (willPerformHTTPRedirection). The task will then switch out of - // its internal state once the delegate calls the completion - // handler. - - //TODO: Should the `public response: URLResponse` property be updated - // before we call delegate API - - self.internalState = .waitingForRedirectCompletionHandler(response: response, bodyDataDrain: bodyDataDrain) - // We need this ugly cast in order to be able to support `URLSessionTask.init()` - session.delegateQueue.addOperation { - delegate.urlSession(session, task: self.task!, willPerformHTTPRedirection: response as! HTTPURLResponse, newRequest: request) { [weak self] (request: URLRequest?) in - guard let task = self else { return } - self?.task?.workQueue.async { - task.didCompleteRedirectCallback(request) - } - } - } - case .noDelegate, .dataCompletionHandler, .downloadCompletionHandler: - // Follow the redirect. - startNewTransfer(with: request) - } - } - fileprivate func didCompleteRedirectCallback(_ request: URLRequest?) { guard case .waitingForRedirectCompletionHandler(response: let response, bodyDataDrain: let bodyDataDrain) = self.internalState else { fatalError("Received callback for HTTP redirection, but we're not waiting for it. Was it called multiple times?") @@ -780,7 +392,7 @@ internal extension _HTTPURLProtocol { func didReceiveResponse() { guard let _ = task as? URLSessionDataTask else { return } guard case .transferInProgress(let ts) = self.internalState else { fatalError("Transfer not in progress.") } - guard let response = ts.response else { fatalError("Header complete, but not URL response.") } + guard let response = ts.response as? HTTPURLResponse else { fatalError("Header complete, but not URL response.") } guard let session = task?.session as? URLSession else { fatalError() } switch session.behaviour(for: self.task!) { case .noDelegate: @@ -804,67 +416,7 @@ internal extension _HTTPURLProtocol { break } } - /// Give the delegate a chance to tell us how to proceed once we have a - /// response / complete header. - /// - /// This will pause the transfer. - func askDelegateHowToProceedAfterCompleteResponse(_ response: HTTPURLResponse, delegate: URLSessionDataDelegate) { - // Ask the delegate how to proceed. - - // This will pause the easy handle. We need to wait for the - // delegate before processing any more data. - guard case .transferInProgress(let ts) = self.internalState else { fatalError("Transfer not in progress.") } - self.internalState = .waitingForResponseCompletionHandler(ts) - - let dt = task as! URLSessionDataTask - - // We need this ugly cast in order to be able to support `URLSessionTask.init()` - guard let s = task?.session as? URLSession else { fatalError() } - s.delegateQueue.addOperation { - delegate.urlSession(s, dataTask: dt, didReceive: response, completionHandler: { [weak self] disposition in - guard let task = self else { return } - self?.task?.workQueue.async { - task.didCompleteResponseCallback(disposition: disposition) - } - }) - } - } - /// This gets called (indirectly) when the data task delegates lets us know - /// how we should proceed after receiving a response (i.e. complete header). - func didCompleteResponseCallback(disposition: URLSession.ResponseDisposition) { - guard case .waitingForResponseCompletionHandler(let ts) = self.internalState else { fatalError("Received response disposition, but we're not waiting for it.") } - switch disposition { - case .cancel: - let error = URLError(_nsError: NSError(domain: NSURLErrorDomain, code: NSURLErrorCancelled)) - self.completeTask(withError: error) - self.client?.urlProtocol(self, didFailWithError: error) - case .allow: - // Continue the transfer. This will unpause the easy handle. - self.internalState = .transferInProgress(ts) - case .becomeDownload: - /* Turn this request into a download */ - NSUnimplemented() - case .becomeStream: - /* Turn this task into a stream task */ - NSUnimplemented() - } - } - - /// Action to be taken after a transfer completes - enum _CompletionAction { - case completeTask - case failWithError(Int) - case redirectWithRequest(URLRequest) - } - /// What action to take - func completionAction(forCompletedRequest request: URLRequest, response: HTTPURLResponse) -> _CompletionAction { - // Redirect: - if let request = redirectRequest(for: response, fromRequest: request) { - return .redirectWithRequest(request) - } - return .completeTask - } /// If the response is a redirect, return the new request /// /// RFC 7231 section 6.4 defines redirection behavior for HTTP/1.1 @@ -880,7 +432,7 @@ internal extension _HTTPURLProtocol { // Can't redirect when there's no location to redirect to. return nil } - + // Check for a redirect: switch response.statusCode { //TODO: Should we do this for 300 "Multiple Choices", too? @@ -897,14 +449,14 @@ internal extension _HTTPURLProtocol { guard let (method, targetURL) = methodAndURL() else { return nil } var request = fromRequest request.httpMethod = method - + // If targetURL has only relative path of url, create a new valid url with relative path // Otherwise, return request with targetURL ie.url from location field guard targetURL.scheme == nil || targetURL.host == nil else { request.url = targetURL return request } - + let scheme = request.url?.scheme let host = request.url?.host @@ -916,7 +468,7 @@ internal extension _HTTPURLProtocol { request.url = URL(string: urlString) let timeSpent = easyHandle.getTimeoutIntervalSpent() request.timeoutInterval = fromRequest.timeoutInterval - timeSpent - return request + return request } } @@ -927,9 +479,10 @@ fileprivate extension HTTPURLResponse { /// - SeeAlso: RFC 2616 section 14.30 case location = "Location" } + func value(forHeaderField field: _Field, response: HTTPURLResponse?) -> String? { let value = field.rawValue - guard let response = response else { fatalError("Response is nil") } + guard let response = response else { fatalError("Response is nil") } if let location = response.allHeaderFields[value] as? String { return location } From 701cd1b3656389829854304472534a0a52c8b815 Mon Sep 17 00:00:00 2001 From: saiHemak Date: Fri, 10 Nov 2017 12:31:12 +0530 Subject: [PATCH 09/11] build file changes --- Foundation.xcodeproj/project.pbxproj | 48 +++++++++++++++++----------- build.py | 12 +++---- 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/Foundation.xcodeproj/project.pbxproj b/Foundation.xcodeproj/project.pbxproj index 4fff0629c4..fdea5403a4 100644 --- a/Foundation.xcodeproj/project.pbxproj +++ b/Foundation.xcodeproj/project.pbxproj @@ -1,4 +1,4 @@ -// !$*UTF8*$! + // !$*UTF8*$! { archiveVersion = 1; classes = { @@ -41,7 +41,6 @@ 5B13B3351C582D4C00651CE2 /* TestNSKeyedUnarchiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3A597EF1C33A9E500295652 /* TestNSKeyedUnarchiver.swift */; }; 5B13B3361C582D4C00651CE2 /* TestNSLocale.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61A395F91C2484490029B337 /* TestNSLocale.swift */; }; 5B13B3371C582D4C00651CE2 /* TestNotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F8AE7C1C180FC600FB62F0 /* TestNotificationCenter.swift */; }; - 5B13B3381C582D4C00651CE2 /* TestNotificationQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EF673AB1C28B527006212A3 /* TestNotificationQueue.swift */; }; 5B13B3391C582D4C00651CE2 /* TestNSNull.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B6F17921C48631C00935030 /* TestNSNull.swift */; }; 5B13B33A1C582D4C00651CE2 /* TestNSNumber.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA66F63F1BF1619600136161 /* TestNSNumber.swift */; }; 5B13B33B1C582D4C00651CE2 /* TestNumberFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B6F17931C48631C00935030 /* TestNumberFormatter.swift */; }; @@ -303,11 +302,13 @@ 5BF7AEC01BCD51F9008F214A /* NSUUID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BDC3F4B1BCC5DCB00ED97BB /* NSUUID.swift */; }; 5BF7AEC11BCD51F9008F214A /* NSValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BDC3F4C1BCC5DCB00ED97BB /* NSValue.swift */; }; 5FE52C951D147D1C00F7D270 /* TestNSTextCheckingResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FE52C941D147D1C00F7D270 /* TestNSTextCheckingResult.swift */; }; + 61445AE51F67A73100F8C143 /* TestNotificationQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61445AE41F67A73100F8C143 /* TestNotificationQueue.swift */; }; 61E0117D1C1B5590000037DD /* RunLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = EADE0B761BD15DFF00C49C64 /* RunLoop.swift */; }; 61E0117E1C1B55B9000037DD /* Timer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BDC3F481BCC5DCB00ED97BB /* Timer.swift */; }; 61E0117F1C1B5990000037DD /* CFRunLoop.c in Sources */ = {isa = PBXBuildFile; fileRef = 5B5D88D81BBC9AD800234F36 /* CFRunLoop.c */; }; 61E011811C1B5998000037DD /* CFMessagePort.c in Sources */ = {isa = PBXBuildFile; fileRef = 5B5D88DC1BBC9AEC00234F36 /* CFMessagePort.c */; }; 61E011821C1B599A000037DD /* CFMachPort.c in Sources */ = {isa = PBXBuildFile; fileRef = 5B5D88D01BBC9AAC00234F36 /* CFMachPort.c */; }; + 61EE04551F208805002051A2 /* NativeProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61EE04541F208805002051A2 /* NativeProtocol.swift */; }; 63DCE9D21EAA430100E9CB02 /* ISO8601DateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63DCE9D11EAA430100E9CB02 /* ISO8601DateFormatter.swift */; }; 63DCE9D41EAA432400E9CB02 /* TestISO8601DateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63DCE9D31EAA432400E9CB02 /* TestISO8601DateFormatter.swift */; }; 684C79011F62B611005BD73E /* TestNSNumberBridging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 684C79001F62B611005BD73E /* TestNSNumberBridging.swift */; }; @@ -327,12 +328,11 @@ B933A79E1F3055F700FE6846 /* NSString-UTF32-BE-data.txt in Resources */ = {isa = PBXBuildFile; fileRef = B933A79C1F3055F600FE6846 /* NSString-UTF32-BE-data.txt */; }; B933A79F1F3055F700FE6846 /* NSString-UTF32-LE-data.txt in Resources */ = {isa = PBXBuildFile; fileRef = B933A79D1F3055F600FE6846 /* NSString-UTF32-LE-data.txt */; }; B951B5EC1F4E2A2000D8B332 /* TestNSLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B951B5EB1F4E2A2000D8B332 /* TestNSLock.swift */; }; - B9974B961EDF4A22007F15B8 /* TransferState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9974B8F1EDF4A22007F15B8 /* TransferState.swift */; }; B9974B971EDF4A22007F15B8 /* MultiHandle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9974B901EDF4A22007F15B8 /* MultiHandle.swift */; }; B9974B981EDF4A22007F15B8 /* libcurlHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9974B911EDF4A22007F15B8 /* libcurlHelpers.swift */; }; B9974B991EDF4A22007F15B8 /* HTTPURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9974B921EDF4A22007F15B8 /* HTTPURLProtocol.swift */; }; B9974B9A1EDF4A22007F15B8 /* HTTPMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9974B931EDF4A22007F15B8 /* HTTPMessage.swift */; }; - B9974B9B1EDF4A22007F15B8 /* HTTPBodySource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9974B941EDF4A22007F15B8 /* HTTPBodySource.swift */; }; + B9974B9B1EDF4A22007F15B8 /* BodySource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9974B941EDF4A22007F15B8 /* BodySource.swift */; }; B9974B9C1EDF4A22007F15B8 /* EasyHandle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9974B951EDF4A22007F15B8 /* EasyHandle.swift */; }; BD8042161E09857800487EB8 /* TestLengthFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8042151E09857800487EB8 /* TestLengthFormatter.swift */; }; BDBB65901E256BFA001A7286 /* TestEnergyFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDBB658F1E256BFA001A7286 /* TestEnergyFormatter.swift */; }; @@ -763,11 +763,12 @@ 5BF7AEC21BCD568D008F214A /* ForSwiftFoundationOnly.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ForSwiftFoundationOnly.h; sourceTree = ""; }; 5E5835F31C20C9B500C81317 /* TestThread.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestThread.swift; sourceTree = ""; }; 5EB6A15C1C188FC40037DCB8 /* TestJSONSerialization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestJSONSerialization.swift; sourceTree = ""; }; - 5EF673AB1C28B527006212A3 /* TestNotificationQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNotificationQueue.swift; sourceTree = ""; }; 5FE52C941D147D1C00F7D270 /* TestNSTextCheckingResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSTextCheckingResult.swift; sourceTree = ""; }; + 61445AE41F67A73100F8C143 /* TestNotificationQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestNotificationQueue.swift; sourceTree = ""; }; 61A395F91C2484490029B337 /* TestNSLocale.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSLocale.swift; sourceTree = ""; }; 61D6C9EE1C1DFE9500DEF583 /* TestTimer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestTimer.swift; sourceTree = ""; }; 61E0117B1C1B554D000037DD /* TestRunLoop.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestRunLoop.swift; sourceTree = ""; }; + 61EE04541F208805002051A2 /* NativeProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeProtocol.swift; sourceTree = ""; }; 61F8AE7C1C180FC600FB62F0 /* TestNotificationCenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNotificationCenter.swift; sourceTree = ""; }; 63DCE9D11EAA430100E9CB02 /* ISO8601DateFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ISO8601DateFormatter.swift; sourceTree = ""; }; 63DCE9D31EAA432400E9CB02 /* TestISO8601DateFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestISO8601DateFormatter.swift; sourceTree = ""; }; @@ -797,13 +798,12 @@ B933A79C1F3055F600FE6846 /* NSString-UTF32-BE-data.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = "NSString-UTF32-BE-data.txt"; sourceTree = ""; }; B933A79D1F3055F600FE6846 /* NSString-UTF32-LE-data.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = "NSString-UTF32-LE-data.txt"; sourceTree = ""; }; B951B5EB1F4E2A2000D8B332 /* TestNSLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestNSLock.swift; sourceTree = ""; }; - B9974B8F1EDF4A22007F15B8 /* TransferState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TransferState.swift; path = http/TransferState.swift; sourceTree = ""; }; - B9974B901EDF4A22007F15B8 /* MultiHandle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MultiHandle.swift; path = http/MultiHandle.swift; sourceTree = ""; }; - B9974B911EDF4A22007F15B8 /* libcurlHelpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = libcurlHelpers.swift; path = http/libcurlHelpers.swift; sourceTree = ""; }; + B9974B901EDF4A22007F15B8 /* MultiHandle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultiHandle.swift; sourceTree = ""; }; + B9974B911EDF4A22007F15B8 /* libcurlHelpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = libcurlHelpers.swift; sourceTree = ""; }; B9974B921EDF4A22007F15B8 /* HTTPURLProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HTTPURLProtocol.swift; path = http/HTTPURLProtocol.swift; sourceTree = ""; }; B9974B931EDF4A22007F15B8 /* HTTPMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HTTPMessage.swift; path = http/HTTPMessage.swift; sourceTree = ""; }; - B9974B941EDF4A22007F15B8 /* HTTPBodySource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HTTPBodySource.swift; path = http/HTTPBodySource.swift; sourceTree = ""; }; - B9974B951EDF4A22007F15B8 /* EasyHandle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = EasyHandle.swift; path = http/EasyHandle.swift; sourceTree = ""; }; + B9974B941EDF4A22007F15B8 /* BodySource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BodySource.swift; sourceTree = ""; }; + B9974B951EDF4A22007F15B8 /* EasyHandle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EasyHandle.swift; sourceTree = ""; }; BD8042151E09857800487EB8 /* TestLengthFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestLengthFormatter.swift; sourceTree = ""; }; BDBB658F1E256BFA001A7286 /* TestEnergyFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestEnergyFormatter.swift; sourceTree = ""; }; BDFDF0A61DFF5B3E00C04CC5 /* TestPersonNameComponents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestPersonNameComponents.swift; sourceTree = ""; }; @@ -993,6 +993,8 @@ 5B1FD9C71D6D162D0080E83C /* Session */ = { isa = PBXGroup; children = ( + 617F57F11F1F5C5F0004F3F0 /* ftp */, + 617F57F01F1F5C4B0004F3F0 /* libcurl */, E4F889331E9CF04D008A70EB /* http */, 5B1FD9C81D6D16580080E83C /* Configuration.swift */, 5B1FD9CE1D6D16580080E83C /* URLSession.swift */, @@ -1000,6 +1002,8 @@ 5B1FD9D01D6D16580080E83C /* URLSessionDelegate.swift */, 5B1FD9D11D6D16580080E83C /* URLSessionTask.swift */, 5B1FD9D21D6D16580080E83C /* TaskRegistry.swift */, + 61EE04541F208805002051A2 /* NativeProtocol.swift */, + B9974B941EDF4A22007F15B8 /* BodySource.swift */, ); name = Session; path = URLSession; @@ -1352,6 +1356,16 @@ path = Foundation; sourceTree = ""; }; + 617F57F01F1F5C4B0004F3F0 /* libcurl */ = { + isa = PBXGroup; + children = ( + B9974B911EDF4A22007F15B8 /* libcurlHelpers.swift */, + B9974B951EDF4A22007F15B8 /* EasyHandle.swift */, + B9974B901EDF4A22007F15B8 /* MultiHandle.swift */, + ); + path = libcurl; + sourceTree = ""; + }; 9F4ADBCF1ECD4F56001F0B3D /* xdgTestHelper */ = { isa = PBXGroup; children = ( @@ -1366,12 +1380,7 @@ isa = PBXGroup; children = ( B9974B921EDF4A22007F15B8 /* HTTPURLProtocol.swift */, - B9974B8F1EDF4A22007F15B8 /* TransferState.swift */, - B9974B901EDF4A22007F15B8 /* MultiHandle.swift */, - B9974B911EDF4A22007F15B8 /* libcurlHelpers.swift */, B9974B931EDF4A22007F15B8 /* HTTPMessage.swift */, - B9974B941EDF4A22007F15B8 /* HTTPBodySource.swift */, - B9974B951EDF4A22007F15B8 /* EasyHandle.swift */, ); name = http; sourceTree = ""; @@ -1463,7 +1472,6 @@ D3A597EF1C33A9E500295652 /* TestNSKeyedUnarchiver.swift */, 61A395F91C2484490029B337 /* TestNSLocale.swift */, 61F8AE7C1C180FC600FB62F0 /* TestNotificationCenter.swift */, - 5EF673AB1C28B527006212A3 /* TestNotificationQueue.swift */, 5B6F17921C48631C00935030 /* TestNSNull.swift */, EA66F63F1BF1619600136161 /* TestNSNumber.swift */, 684C79001F62B611005BD73E /* TestNSNumberBridging.swift */, @@ -1506,6 +1514,7 @@ 5B6F17961C48631C00935030 /* TestUtils.swift */, 03B6F5831F15F339004F25AF /* TestURLProtocol.swift */, 3E55A2321F52463B00082000 /* TestUnit.swift */, + 61445AE41F67A73100F8C143 /* TestNotificationQueue.swift */, ); name = Tests; sourceTree = ""; @@ -2215,7 +2224,6 @@ EADE0BB31BD15E0000C49C64 /* NSRegularExpression.swift in Sources */, EADE0BA41BD15E0000C49C64 /* LengthFormatter.swift in Sources */, 5BDC3FCA1BCF176100ED97BB /* NSCFArray.swift in Sources */, - B9974B961EDF4A22007F15B8 /* TransferState.swift in Sources */, EADE0BB21BD15E0000C49C64 /* Progress.swift in Sources */, EADE0B961BD15DFF00C49C64 /* DateIntervalFormatter.swift in Sources */, 5B5BFEAC1E6CC0C200AC8D9E /* NSCFBoolean.swift in Sources */, @@ -2225,7 +2233,7 @@ EADE0BB81BD15E0000C49C64 /* Process.swift in Sources */, 5BF7AEB31BCD51F9008F214A /* NSObjCRuntime.swift in Sources */, 5BD31D3F1D5D19D600563814 /* Dictionary.swift in Sources */, - B9974B9B1EDF4A22007F15B8 /* HTTPBodySource.swift in Sources */, + B9974B9B1EDF4A22007F15B8 /* BodySource.swift in Sources */, 5B94E8821C430DE70055C035 /* NSStringAPI.swift in Sources */, 5B0163BB1D024EB7003CCD96 /* DateComponents.swift in Sources */, 5BF7AEAB1BCD51F9008F214A /* NSDictionary.swift in Sources */, @@ -2260,6 +2268,7 @@ EADE0BA61BD15E0000C49C64 /* MassFormatter.swift in Sources */, 5BECBA3A1D1CAE9A00B39B1F /* NSMeasurement.swift in Sources */, 5BF7AEB21BCD51F9008F214A /* NSNumber.swift in Sources */, + 61EE04551F208805002051A2 /* NativeProtocol.swift in Sources */, B9974B991EDF4A22007F15B8 /* HTTPURLProtocol.swift in Sources */, 5BCD03821D3EE35C00E3FF9B /* TimeZone.swift in Sources */, EADE0BBC1BD15E0000C49C64 /* URLCache.swift in Sources */, @@ -2377,6 +2386,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 61445AE51F67A73100F8C143 /* TestNotificationQueue.swift in Sources */, 159884921DCC877700E3314C /* TestHTTPCookieStorage.swift in Sources */, 5FE52C951D147D1C00F7D270 /* TestNSTextCheckingResult.swift in Sources */, 5B13B3451C582D4C00651CE2 /* TestNSString.swift in Sources */, @@ -2384,7 +2394,6 @@ B90C57BC1EEEEA5A005208AE /* TestThread.swift in Sources */, B90C57BB1EEEEA5A005208AE /* TestFileManager.swift in Sources */, A058C2021E529CF100B07AA1 /* TestMassFormatter.swift in Sources */, - 5B13B3381C582D4C00651CE2 /* TestNotificationQueue.swift in Sources */, CC5249C01D341D23007CB54D /* TestUnitConverter.swift in Sources */, 5B13B3331C582D4C00651CE2 /* TestJSONSerialization.swift in Sources */, 5B13B33C1C582D4C00651CE2 /* TestNSOrderedSet.swift in Sources */, @@ -2992,3 +3001,4 @@ }; rootObject = 5B5D88541BBC938800234F36 /* Project object */; } + diff --git a/build.py b/build.py index efe0ce1046..fe15748af5 100755 --- a/build.py +++ b/build.py @@ -424,18 +424,18 @@ 'Foundation/NSURLRequest.swift', 'Foundation/URLResponse.swift', 'Foundation/URLSession/Configuration.swift', - 'Foundation/URLSession/http/EasyHandle.swift', - 'Foundation/URLSession/http/HTTPBodySource.swift', + 'Foundation/URLSession/libcurl/EasyHandle.swift', + 'Foundation/URLSession/libcurl/MultiHandle.swift', + 'Foundation/URLSession/libcurl/libcurlHelpers.swift', + 'Foundation/URLSession/BodySource.swift', + 'Foundation/URLSession/NativeProtocol.swift', 'Foundation/URLSession/http/HTTPMessage.swift', - 'Foundation/URLSession/http/MultiHandle.swift', 'Foundation/URLSession/URLSession.swift', 'Foundation/URLSession/URLSessionConfiguration.swift', 'Foundation/URLSession/URLSessionDelegate.swift', 'Foundation/URLSession/URLSessionTask.swift', 'Foundation/URLSession/TaskRegistry.swift', - 'Foundation/URLSession/http/TransferState.swift', - 'Foundation/URLSession/http/libcurlHelpers.swift', - 'Foundation/URLSession/http/HTTPURLProtocol.swift', + 'Foundation/URLSession/http/HTTPURLProtocol.swift', 'Foundation/UserDefaults.swift', 'Foundation/NSUUID.swift', 'Foundation/NSValue.swift', From 976f82b9781d35dbab57485ff3bfd0538dbb1f94 Mon Sep 17 00:00:00 2001 From: saiHemak Date: Fri, 10 Nov 2017 14:34:22 +0530 Subject: [PATCH 10/11] PR1310 changes --- Foundation/URLSession/NativeProtocol.swift | 2 +- Foundation/URLSession/libcurl/EasyHandle.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Foundation/URLSession/NativeProtocol.swift b/Foundation/URLSession/NativeProtocol.swift index 9b8629a108..abfe4ed04d 100644 --- a/Foundation/URLSession/NativeProtocol.swift +++ b/Foundation/URLSession/NativeProtocol.swift @@ -243,7 +243,7 @@ class _NativeProtocol: URLProtocol, _EasyHandleDelegate { } func seekInputStream(to position: UInt64) throws { - // We will reset the body sourse and seek forward. + // We will reset the body source and seek forward. NSUnimplemented() } diff --git a/Foundation/URLSession/libcurl/EasyHandle.swift b/Foundation/URLSession/libcurl/EasyHandle.swift index 02ae3d2147..4bfbbec218 100644 --- a/Foundation/URLSession/libcurl/EasyHandle.swift +++ b/Foundation/URLSession/libcurl/EasyHandle.swift @@ -43,8 +43,8 @@ import Dispatch /// needs to be configured for a specific transfer (e.g. the URL) will be /// configured on an easy handle. /// -/// A single `URLSessionTask` may do multiple, sonecutive transfers, and -/// as a result it will have to reconfigure it's easy handle between +/// A single `URLSessionTask` may do multiple, consecutive transfers, and +/// as a result it will have to reconfigure its easy handle between /// transfers. An easy handle can be re-used once its transfer has /// completed. /// From e01c49805efbee0340198d1e2768d8972fc49619 Mon Sep 17 00:00:00 2001 From: saiHemak Date: Fri, 10 Nov 2017 17:59:31 +0530 Subject: [PATCH 11/11] PR1311 changes --- Foundation/URLSession/BodySource.swift | 10 +++++----- Foundation/URLSession/libcurl/MultiHandle.swift | 3 +-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Foundation/URLSession/BodySource.swift b/Foundation/URLSession/BodySource.swift index d427c504a9..7e8f289e54 100644 --- a/Foundation/URLSession/BodySource.swift +++ b/Foundation/URLSession/BodySource.swift @@ -20,7 +20,7 @@ import CoreFoundation import Dispatch -/// Turn `NSData` into `dispatch_data_t` +/// Turn `Data` into `DispatchData` internal func createDispatchData(_ data: Data) -> DispatchData { //TODO: Avoid copying data let buffer = UnsafeRawBufferPointer(start: data._backing.bytes, @@ -28,13 +28,13 @@ internal func createDispatchData(_ data: Data) -> DispatchData { return DispatchData(bytes: buffer) } -/// Copy data from `dispatch_data_t` into memory pointed to by an `UnsafeMutableBufferPointer`. +/// Copy data from `DispatchData` into memory pointed to by an `UnsafeMutableBufferPointer`. internal func copyDispatchData(_ data: DispatchData, infoBuffer buffer: UnsafeMutableBufferPointer) { precondition(data.count <= (buffer.count * MemoryLayout.size)) _ = data.copyBytes(to: buffer) } -/// Split `dispatch_data_t` into `(head, tail)` pair. +/// Split `DispatchData` into `(head, tail)` pair. internal func splitData(dispatchData data: DispatchData, atPosition position: Int) -> (DispatchData,DispatchData) { return (data.subdata(in: 0..