Merge pull request #1027 from nethraravindran/swift-3.1-branch
diff --git a/Foundation.xcodeproj/project.pbxproj b/Foundation.xcodeproj/project.pbxproj
index 4716c68..3ae9951 100644
--- a/Foundation.xcodeproj/project.pbxproj
+++ b/Foundation.xcodeproj/project.pbxproj
@@ -78,6 +78,7 @@
5B1FD9DE1D6D16580080E83C /* TaskRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B1FD9D21D6D16580080E83C /* TaskRegistry.swift */; };
5B1FD9DF1D6D16580080E83C /* TransferState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B1FD9D31D6D16580080E83C /* TransferState.swift */; };
5B1FD9E11D6D178E0080E83C /* libcurl.3.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 5B1FD9E01D6D178E0080E83C /* libcurl.3.dylib */; };
+ E429ED451E9638DA0031BC20 /* HTTPURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E429ED441E9638DA0031BC20 /* HTTPURLProtocol.swift */; };
5B1FD9E31D6D17B80080E83C /* TestNSURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B1FD9E21D6D17B80080E83C /* TestNSURLSession.swift */; };
5B23AB871CE62D17000DB898 /* Boxing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B23AB861CE62D17000DB898 /* Boxing.swift */; };
5B23AB891CE62D4D000DB898 /* ReferenceConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B23AB881CE62D4D000DB898 /* ReferenceConvertible.swift */; };
@@ -495,6 +496,7 @@
5B1FD9D21D6D16580080E83C /* TaskRegistry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TaskRegistry.swift; sourceTree = "<group>"; };
5B1FD9D31D6D16580080E83C /* TransferState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransferState.swift; sourceTree = "<group>"; };
5B1FD9E01D6D178E0080E83C /* libcurl.3.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libcurl.3.dylib; path = usr/lib/libcurl.3.dylib; sourceTree = SDKROOT; };
+ E429ED441E9638DA0031BC20 /* HTTPURLProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HTTPURLProtocol.swift; path = HTTPURLProtocol.swift; sourceTree = "<group>"; };
5B1FD9E21D6D17B80080E83C /* TestNSURLSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSURLSession.swift; sourceTree = "<group>"; };
5B23AB861CE62D17000DB898 /* Boxing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Boxing.swift; sourceTree = "<group>"; };
5B23AB881CE62D4D000DB898 /* ReferenceConvertible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferenceConvertible.swift; sourceTree = "<group>"; };
@@ -928,23 +930,32 @@
5B1FD9C71D6D162D0080E83C /* Session */ = {
isa = PBXGroup;
children = (
+ E4F889331E9CF04D008A70EB /* http */,
5B1FD9C81D6D16580080E83C /* Configuration.swift */,
- 5B1FD9C91D6D16580080E83C /* EasyHandle.swift */,
- 5B1FD9CA1D6D16580080E83C /* HTTPBodySource.swift */,
- 5B1FD9CB1D6D16580080E83C /* HTTPMessage.swift */,
- 5B1FD9CC1D6D16580080E83C /* libcurlHelpers.swift */,
- 5B1FD9CD1D6D16580080E83C /* MultiHandle.swift */,
5B1FD9CE1D6D16580080E83C /* NSURLSession.swift */,
5B1FD9CF1D6D16580080E83C /* NSURLSessionConfiguration.swift */,
5B1FD9D01D6D16580080E83C /* NSURLSessionDelegate.swift */,
5B1FD9D11D6D16580080E83C /* NSURLSessionTask.swift */,
5B1FD9D21D6D16580080E83C /* TaskRegistry.swift */,
- 5B1FD9D31D6D16580080E83C /* TransferState.swift */,
);
name = Session;
path = NSURLSession;
sourceTree = "<group>";
};
+ E4F889331E9CF04D008A70EB /* http */ = {
+ isa = PBXGroup;
+ children = (
+ E429ED441E9638DA0031BC20 /* HTTPURLProtocol.swift */,
+ 5B1FD9C91D6D16580080E83C /* EasyHandle.swift */,
+ 5B1FD9CA1D6D16580080E83C /* HTTPBodySource.swift */,
+ 5B1FD9CB1D6D16580080E83C /* HTTPMessage.swift */,
+ 5B1FD9CC1D6D16580080E83C /* libcurlHelpers.swift */,
+ 5B1FD9CD1D6D16580080E83C /* MultiHandle.swift */,
+ 5B1FD9D31D6D16580080E83C /* TransferState.swift */,
+ );
+ name = http;
+ sourceTree = "<group>";
+ };
5B5D88531BBC938800234F36 = {
isa = PBXGroup;
children = (
@@ -2040,6 +2051,7 @@
5B23AB871CE62D17000DB898 /* Boxing.swift in Sources */,
5BF7AEA41BCD51F9008F214A /* Bundle.swift in Sources */,
5B23AB891CE62D4D000DB898 /* ReferenceConvertible.swift in Sources */,
+ E429ED451E9638DA0031BC20 /* HTTPURLProtocol.swift in Sources */,
D3E8D6D11C367AB600295652 /* NSSpecialValue.swift in Sources */,
5B1FD9D51D6D16580080E83C /* EasyHandle.swift in Sources */,
EAB57B721BD1C7A5004AC5C5 /* NSPortMessage.swift in Sources */,
diff --git a/Foundation/NSURLProtocol.swift b/Foundation/NSURLProtocol.swift
index 5004d1d..30299fd 100644
--- a/Foundation/NSURLProtocol.swift
+++ b/Foundation/NSURLProtocol.swift
@@ -7,6 +7,9 @@
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
+import CoreFoundation
+import Dispatch
+
/*!
@header NSURLProtocol.h
@@ -142,6 +145,96 @@
func urlProtocol(_ protocol: URLProtocol, didCancel challenge: URLAuthenticationChallenge)
}
+internal class _ProtocolClient : NSObject, URLProtocolClient {
+
+ func urlProtocol(_ protocol: URLProtocol, didReceive response: URLResponse, cacheStoragePolicy policy: URLCache.StoragePolicy) {
+ `protocol`.task?.response = response
+ }
+
+ func urlProtocolDidFinishLoading(_ protocol: URLProtocol) {
+ guard let task = `protocol`.task else { fatalError() }
+ guard let session = task.session as? URLSession else { fatalError() }
+ switch session.behaviour(for: task) {
+ case .taskDelegate(let delegate):
+ guard let s = session as? URLSession else { fatalError() }
+ s.delegateQueue.addOperation {
+ delegate.urlSession(s, task: task, didCompleteWithError: nil)
+ task.state = .completed
+ }
+ case .noDelegate:
+ task.state = .completed
+ case .dataCompletionHandler(let completion):
+ let data = Data()
+ guard let client = `protocol`.client else { fatalError() }
+ client.urlProtocol(`protocol`, didLoad: data)
+ return
+ case .downloadCompletionHandler(let completion):
+ guard let s = session as? URLSession else { fatalError() }
+ s.delegateQueue.addOperation {
+ completion(task.currentRequest?.url, task.response, nil)
+ task.state = .completed
+ }
+ }
+ }
+
+ func urlProtocol(_ protocol: URLProtocol, didCancel challenge: URLAuthenticationChallenge) {
+ NSUnimplemented()
+ }
+
+ func urlProtocol(_ protocol: URLProtocol, didReceive challenge: URLAuthenticationChallenge) {
+ NSUnimplemented()
+ }
+
+ func urlProtocol(_ protocol: URLProtocol, didLoad data: Data) {
+ guard let task = `protocol`.task else { fatalError() }
+ guard let session = task.session as? URLSession else { fatalError() }
+ switch session.behaviour(for: task) {
+ case .dataCompletionHandler(let completion):
+ guard let s = task.session as? URLSession else { fatalError() }
+ s.delegateQueue.addOperation {
+ completion(data, task.response, nil)
+ task.state = .completed
+ }
+ default: return
+ }
+ }
+
+ func urlProtocol(_ protocol: URLProtocol, didFailWithError error: Error) {
+ guard let task = `protocol`.task else { fatalError() }
+ guard let session = task.session as? URLSession else { fatalError() }
+ switch session.behaviour(for: task) {
+ case .taskDelegate(let delegate):
+ guard let s = session as? URLSession else { fatalError() }
+ s.delegateQueue.addOperation {
+ delegate.urlSession(s, task: task, didCompleteWithError: error as Error)
+ task.state = .completed
+ }
+ case .noDelegate:
+ task.state = .completed
+ case .dataCompletionHandler(let completion):
+ guard let s = session as? URLSession else { fatalError() }
+ s.delegateQueue.addOperation {
+ completion(nil, nil, error)
+ task.state = .completed
+ }
+ case .downloadCompletionHandler(let completion):
+ guard let s = session as? URLSession else { fatalError() }
+ s.delegateQueue.addOperation {
+ completion(nil, nil, error)
+ task.state = .completed
+ }
+ }
+ }
+
+ func urlProtocol(_ protocol: URLProtocol, cachedResponseIsValid cachedResponse: CachedURLResponse) {
+ NSUnimplemented()
+ }
+
+ func urlProtocol(_ protocol: URLProtocol, wasRedirectedTo request: URLRequest, redirectResponse: URLResponse) {
+ NSUnimplemented()
+ }
+}
+
/*!
@class NSURLProtocol
@@ -151,7 +244,9 @@
or more protocols or URL schemes.
*/
open class URLProtocol : NSObject {
-
+
+ private static var _registeredProtocolClasses = [AnyClass]()
+ private static var _classesLock = NSLock()
/*!
@method initWithRequest:cachedResponse:client:
@abstract Initializes an NSURLProtocol given request,
@@ -165,28 +260,43 @@
interface the protocol implementation can use to report results back
to the URL loading system.
*/
- public init(request: URLRequest, cachedResponse: CachedURLResponse?, client: URLProtocolClient?) { NSUnimplemented() }
-
+ public required init(request: URLRequest, cachedResponse: CachedURLResponse?, client: URLProtocolClient?) {
+ self._request = request
+ self._cachedResponse = cachedResponse
+ self._client = client ?? _ProtocolClient()
+ }
+
+ private var _request : URLRequest
+ private var _cachedResponse : CachedURLResponse?
+ private var _client : URLProtocolClient?
+
/*!
@method client
@abstract Returns the NSURLProtocolClient of the receiver.
@result The NSURLProtocolClient of the receiver.
*/
- open var client: URLProtocolClient? { NSUnimplemented() }
+ open var client: URLProtocolClient? {
+ set { self._client = newValue }
+ get { return self._client }
+ }
/*!
@method request
@abstract Returns the NSURLRequest of the receiver.
@result The NSURLRequest of the receiver.
*/
- /*@NSCopying*/ open var request: URLRequest { NSUnimplemented() }
+ /*@NSCopying*/ open var request: URLRequest {
+ return _request
+ }
/*!
@method cachedResponse
@abstract Returns the NSCachedURLResponse of the receiver.
@result The NSCachedURLResponse of the receiver.
*/
- /*@NSCopying*/ open var cachedResponse: CachedURLResponse? { NSUnimplemented() }
+ /*@NSCopying*/ open var cachedResponse: CachedURLResponse? {
+ return _cachedResponse
+ }
/*======================================================================
Begin responsibilities for protocol implementors
@@ -207,7 +317,9 @@
@param request A request to inspect.
@result YES if the protocol can handle the given request, NO if not.
*/
- open class func canInit(with request: URLRequest) -> Bool { NSUnimplemented() }
+ open class func canInit(with request: URLRequest) -> Bool {
+ NSRequiresConcreteImplementation()
+ }
/*!
@method canonicalRequestForRequest:
@@ -246,7 +358,9 @@
@discussion When this method is called, the protocol implementation
should start loading a request.
*/
- open func startLoading() { NSUnimplemented() }
+ open func startLoading() {
+ NSRequiresConcreteImplementation()
+ }
/*!
@method stopLoading
@@ -256,7 +370,9 @@
to a cancel operation, so protocol implementations must be able to
handle this call while a load is in progress.
*/
- open func stopLoading() { NSUnimplemented() }
+ open func stopLoading() {
+ NSRequiresConcreteImplementation()
+ }
/*======================================================================
End responsibilities for protocol implementors
@@ -323,8 +439,40 @@
The only way that failure can occur is if the given class is not a
subclass of NSURLProtocol.
*/
- open class func registerClass(_ protocolClass: AnyClass) -> Bool { NSUnimplemented() }
-
+ open class func registerClass(_ protocolClass: AnyClass) -> Bool {
+ if protocolClass is URLProtocol.Type {
+ _classesLock.lock()
+ guard !_registeredProtocolClasses.contains(where: { $0 === protocolClass }) else {
+ _classesLock.unlock()
+ return true
+ }
+ _registeredProtocolClasses.append(protocolClass)
+ _classesLock.unlock()
+ return true
+ }
+ return false
+ }
+
+ internal class func getProtocolClass(protocols: [AnyClass], request: URLRequest) -> AnyClass? {
+ // Registered protocols are consulted in reverse order.
+ // This behaviour makes the latest registered protocol to be consulted first
+ _classesLock.lock()
+ let protocolClasses = protocols
+ for protocolClass in protocolClasses {
+ let urlProtocolClass: AnyClass = protocolClass
+ guard let urlProtocol = urlProtocolClass as? URLProtocol.Type else { fatalError() }
+ if urlProtocol.canInit(with: request) {
+ _classesLock.unlock()
+ return urlProtocol
+ }
+ }
+ _classesLock.unlock()
+ return nil
+ }
+
+ internal class func getProtocols() -> [AnyClass]? {
+ return _registeredProtocolClasses
+ }
/*!
@method unregisterClass:
@abstract This method unregisters a protocol.
@@ -332,10 +480,24 @@
consulted in calls to NSURLProtocol class methods.
@param protocolClass The class to unregister.
*/
- open class func unregisterClass(_ protocolClass: AnyClass) { NSUnimplemented() }
+ open class func unregisterClass(_ protocolClass: AnyClass) {
+ _classesLock.lock()
+ if let idx = _registeredProtocolClasses.index(where: { $0 === protocolClass }) {
+ _registeredProtocolClasses.remove(at: idx)
+ }
+ _classesLock.unlock()
+ }
open class func canInit(with task: URLSessionTask) -> Bool { NSUnimplemented() }
- public convenience init(task: URLSessionTask, cachedResponse: CachedURLResponse?, client: URLProtocolClient?) { NSUnimplemented() }
- /*@NSCopying*/ open var task: URLSessionTask? { NSUnimplemented() }
-}
+ public required convenience init(task: URLSessionTask, cachedResponse: CachedURLResponse?, client: URLProtocolClient?) {
+ let urlRequest = task.originalRequest
+ self.init(request: urlRequest!, cachedResponse: cachedResponse, client: client)
+ self.task = task
+ }
+ /*@NSCopying*/ open var task: URLSessionTask? {
+ set { self._task = newValue }
+ get { return self._task }
+ }
+ private var _task : URLSessionTask? = nil
+}
diff --git a/Foundation/NSURLSession/NSURLSession.swift b/Foundation/NSURLSession/NSURLSession.swift
index ab966c6..805e086 100644
--- a/Foundation/NSURLSession/NSURLSession.swift
+++ b/Foundation/NSURLSession/NSURLSession.swift
@@ -221,6 +221,8 @@
let c = URLSession._Configuration(URLSessionConfiguration: configuration)
self._configuration = c
self.multiHandle = _MultiHandle(configuration: c, workQueue: workQueue)
+ // registering all the protocol classes with URLProtocol
+ let _ = URLProtocol.registerClass(_HTTPURLProtocol.self)
}
/*
@@ -246,6 +248,8 @@
let c = URLSession._Configuration(URLSessionConfiguration: configuration)
self._configuration = c
self.multiHandle = _MultiHandle(configuration: c, workQueue: workQueue)
+ // registering all the protocol classes with URLProtocol
+ let _ = URLProtocol.registerClass(_HTTPURLProtocol.self)
}
open let delegateQueue: OperationQueue
diff --git a/Foundation/NSURLSession/NSURLSessionTask.swift b/Foundation/NSURLSession/NSURLSessionTask.swift
index 0b09fcf..1e010b8 100644
--- a/Foundation/NSURLSession/NSURLSessionTask.swift
+++ b/Foundation/NSURLSession/NSURLSessionTask.swift
@@ -26,55 +26,19 @@
/// of processing a given request.
open class URLSessionTask : NSObject, NSCopying {
/// How many times the task has been suspended, 0 indicating a running task.
- fileprivate var suspendCount = 1
- fileprivate var easyHandle: _EasyHandle!
- fileprivate var totalDownloaded = 0
- fileprivate var session: URLSessionProtocol! //change to nil when task completes
- fileprivate let body: _Body
- fileprivate let tempFileURL: URL
+ internal var suspendCount = 1
+ internal var totalDownloaded = 0
+ internal var session: URLSessionProtocol! //change to nil when task completes
+ internal let body: _Body
+ internal let tempFileURL: URL
+ fileprivate var _protocol: URLProtocol! = nil
- /// 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).
- /// - SeeAlso: URLSessionTask._InternalState
- fileprivate var internalState = _InternalState.initial {
- // 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 {
- session.remove(handle: easyHandle)
- }
- }
- didSet {
- if !oldValue.isEasyHandleAddedToMultiHandle && internalState.isEasyHandleAddedToMultiHandle {
- session.add(handle: easyHandle)
- }
- if oldValue.isEasyHandlePaused && !internalState.isEasyHandlePaused {
- fatalError("Need to solve pausing receive.")
- }
- if case .taskCompleted = internalState {
- updateTaskState()
- guard let s = session as? URLSession else { fatalError() }
- s.workQueue.async {
- s.taskRegistry.remove(self)
- }
- }
- }
- }
/// All operations must run on this queue.
- fileprivate let workQueue: DispatchQueue
+ internal let workQueue: DispatchQueue
/// Using dispatch semaphore to make public attributes thread safe.
/// A semaphore is a simpler option against the usage of concurrent queue
/// as the critical sections are very short.
- fileprivate let semaphore = DispatchSemaphore(value: 1)
+ fileprivate let semaphore = DispatchSemaphore(value: 1)
public override init() {
// Darwin Foundation oddly allows calling this initializer, even though
@@ -111,14 +75,28 @@
_ = FileManager.default.createFile(atPath: fileName, contents: nil)
self.tempFileURL = URL(fileURLWithPath: fileName)
super.init()
- self.easyHandle = _EasyHandle(delegate: self)
+ if session.configuration.protocolClasses != nil {
+ guard let protocolClasses = session.configuration.protocolClasses else { fatalError() }
+ if let urlProtocolClass = URLProtocol.getProtocolClass(protocols: protocolClasses, request: request) {
+ guard let urlProtocol = urlProtocolClass as? URLProtocol.Type else { fatalError() }
+ self._protocol = urlProtocol.init(task: self, cachedResponse: nil, client: nil)
+ } else {
+ guard let protocolClasses = URLProtocol.getProtocols() else { fatalError() }
+ if let urlProtocolClass = URLProtocol.getProtocolClass(protocols: protocolClasses, request: request) {
+ guard let urlProtocol = urlProtocolClass as? URLProtocol.Type else { fatalError() }
+ self._protocol = urlProtocol.init(task: self, cachedResponse: nil, client: nil)
+ }
+ }
+ } else {
+ guard let protocolClasses = URLProtocol.getProtocols() else { fatalError() }
+ if let urlProtocolClass = URLProtocol.getProtocolClass(protocols: protocolClasses, request: request) {
+ guard let urlProtocol = urlProtocolClass as? URLProtocol.Type else { fatalError() }
+ self._protocol = urlProtocol.init(task: self, cachedResponse: nil, client: nil)
+ }
+ }
}
deinit {
- //TODO: Can we ensure this somewhere else? This might run on the wrong
- // thread / queue.
- //if internalState.isEasyHandleAddedToMultiHandle {
- // session.removeHandle(easyHandle)
- //}
+ //TODO: Do we remove the EasyHandle from the session here? This might run on the wrong thread / queue.
}
open override func copy() -> Any {
@@ -136,7 +114,7 @@
/*@NSCopying*/ open let originalRequest: URLRequest?
/// May differ from originalRequest due to http server redirection
- /*@NSCopying*/ open fileprivate(set) var currentRequest: URLRequest? {
+ /*@NSCopying*/ open internal(set) var currentRequest: URLRequest? {
get {
semaphore.wait()
defer {
@@ -151,7 +129,7 @@
}
}
fileprivate var _currentRequest: URLRequest? = nil
- /*@NSCopying*/ open fileprivate(set) var response: URLResponse? {
+ /*@NSCopying*/ open internal(set) var response: URLResponse? {
get {
semaphore.wait()
defer {
@@ -173,7 +151,7 @@
*/
/// Number of body bytes already received
- open fileprivate(set) var countOfBytesReceived: Int64 {
+ open fileprivate(set) var countOfBytesReceived: Int64 {
get {
semaphore.wait()
defer {
@@ -204,6 +182,7 @@
semaphore.signal()
}
}
+
fileprivate var _countOfBytesSent: Int64 = 0
/// Number of body bytes we expect to send, derived from the Content-Length of the HTTP request */
@@ -222,7 +201,18 @@
* cases, the task may signal other work before it acknowledges the
* cancelation. -cancel may be sent to a task that has been suspended.
*/
- open func cancel() { NSUnimplemented() }
+ open func cancel() {
+ workQueue.sync {
+ guard self.state == .running || self.state == .suspended else { return }
+ self.state = .canceling
+ self.workQueue.async {
+ let urlError = URLError(_nsError: NSError(domain: NSURLErrorDomain, code: NSURLErrorCancelled, userInfo: nil))
+ self.error = urlError
+ self._protocol.stopLoading()
+ self._protocol.client?.urlProtocol(self._protocol, didFailWithError: urlError)
+ }
+ }
+ }
/*
* The current state of the task within the session.
@@ -247,7 +237,7 @@
* The error, if any, delivered via -URLSession:task:didCompleteWithError:
* This property will be nil in the event that no error occured.
*/
- /*@NSCopying*/ open fileprivate(set) var error: Error?
+ /*@NSCopying*/ open internal(set) var error: Error?
/// Suspend the task.
///
@@ -281,7 +271,7 @@
if self.suspendCount == 1 {
self.workQueue.async {
- self.performSuspend()
+ self._protocol.stopLoading()
}
}
}
@@ -296,7 +286,7 @@
self.updateTaskState()
if self.suspendCount == 0 {
self.workQueue.async {
- self.performResume()
+ self._protocol.startLoading()
}
}
}
@@ -346,103 +336,12 @@
}
}
-fileprivate extension URLSessionTask {
- /// The calls to `suspend` can be nested. This one is only called when the
- /// task is not suspended and needs to go into suspended state.
- func performSuspend() {
- if case .transferInProgress(let transferState) = internalState {
- internalState = .transferReady(transferState)
- }
- }
- /// The calls to `resume` can be nested. This one is only called when the
- /// task is suspended and needs to go out of suspended state.
- func performResume() {
- if case .initial = internalState {
- guard let r = originalRequest else { fatalError("Task has no original request.") }
- startNewTransfer(with: r)
- }
- if case .transferReady(let transferState) = internalState {
- internalState = .transferInProgress(transferState)
- }
- }
-}
-
-internal extension URLSessionTask {
- /// 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(_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: HTTPURLResponse, bodyDataDrain: _TransferState._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: HTTPURLResponse, bodyDataDrain: _TransferState._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
- }
-}
-
-fileprivate extension URLSessionTask._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 URLSessionTask {
/// Updates the (public) state based on private / internal state.
///
/// - Note: This must be called on the `workQueue`.
- fileprivate func updateTaskState() {
+ internal func updateTaskState() {
func calculateState() -> URLSessionTask.State {
- if case .taskCompleted = internalState {
- return .completed
- }
if suspendCount == 0 {
return .running
} else {
@@ -462,7 +361,7 @@
case stream(InputStream)
}
}
-fileprivate extension URLSessionTask._Body {
+internal extension URLSessionTask._Body {
enum _Error : Error {
case fileForBodyDataNotFound
}
@@ -485,252 +384,6 @@
}
}
-/// Easy handle related
-fileprivate extension URLSessionTask {
- /// Start a new transfer
- func startNewTransfer(with request: URLRequest) {
- currentRequest = request
- guard let url = request.url else { fatalError("No URL in request.") }
- internalState = .transferReady(createTransferState(url: url))
- configureEasyHandle(for: request)
- if suspendCount < 1 {
- performResume()
- }
- }
- /// Creates a new transfer state with the given behaviour:
- func createTransferState(url: URL) -> URLSessionTask._TransferState {
- let drain = createTransferBodyDataDrain()
- switch body {
- case .none:
- return URLSessionTask._TransferState(url: url, bodyDataDrain: drain)
- case .data(let data):
- let source = _HTTPBodyDataSource(data: data)
- return URLSessionTask._TransferState(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 URLSessionTask._TransferState(url: url, bodyDataDrain: drain, bodySource: source)
- case .stream:
- NSUnimplemented()
- }
-
- }
- /// The data drain.
- ///
- /// This depends on what the delegate / completion handler need.
- fileprivate func createTransferBodyDataDrain() -> URLSessionTask._TransferState._DataDrain {
- switch session.behaviour(for: self) {
- 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: tempFileURL)
- return .toFile(tempFileURL, fileHandle)
- }
- }
- /// 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) {
- // 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 defaul
- // 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: self)
- 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.") }
- easyHandle.set(url: url)
- easyHandle.setAllowedProtocolsToHTTPAndHTTPS()
- easyHandle.set(preferredReceiveBufferSize: Int.max)
- do {
- switch (body, try body.getBodyLength()) {
- case (.none, _):
- set(requestBodyLength: .noBody)
- case (_, .some(let length)):
- set(requestBodyLength: .length(length))
- case (_, .none):
- set(requestBodyLength: .unknown)
- }
- } catch let e {
- // Fail the request here.
- // TODO: We have multiple options:
- // NSURLErrorNoPermissionsToReadFile
- // NSURLErrorFileDoesNotExist
- internalState = .transferFailed
- failWith(errorCode: errorCode(fileSystemError: e), 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 = session as! URLSession
- var httpHeaders: [AnyHashable : Any]?
-
- if let hh = httpSession.configuration.httpAdditionalHeaders {
- httpHeaders = hh
- }
-
- if let hh = currentRequest?.allHTTPHeaderFields {
- if httpHeaders == nil {
- httpHeaders = hh
- } else {
- hh.forEach {
- httpHeaders![$0] = $1
- }
- }
- }
-
- let customHeaders: [String]
- let headersForRequest = curlHeaders(for: httpHeaders)
- if ((request.httpMethod == "POST") && (request.value(forHTTPHeaderField: "Content-Type") == nil)) {
- customHeaders = headersForRequest + ["Content-Type:application/x-www-form-urlencoded"]
- } else {
- customHeaders = headersForRequest
- }
-
- easyHandle.set(customHeaders: customHeaders)
-
- //Options unavailable on Ubuntu 14.04 (libcurl 7.36)
- //TODO: Introduce something like an #if
- //easyHandle.set(waitForPipeliningAndMultiplexing: true)
- //easyHandle.set(streamWeight: priority)
-
- //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
- }
- let timeoutHandler = DispatchWorkItem { [weak self] in
- guard let currentTask = self else { fatalError("Timeout on a task that doesn't exist") } //this guard must always pass
- currentTask.internalState = .transferFailed
- let urlError = URLError(_nsError: NSError(domain: NSURLErrorDomain, code: NSURLErrorTimedOut, userInfo: nil))
- currentTask.completeTask(withError: urlError)
- }
- easyHandle.timeoutTimer = _TimeoutSource(queue: workQueue, milliseconds: timeoutInterval, handler: timeoutHandler)
-
- easyHandle.set(automaticBodyDecompression: true)
- easyHandle.set(requestMethod: request.httpMethod ?? "GET")
- if request.httpMethod == "HEAD" {
- easyHandle.set(noBody: true)
- }
- }
-}
-
-fileprivate extension URLSessionTask {
- /// These are a list of headers that should be passed to libcurl.
- ///
- /// Headers will be returned as `Accept: text/html` strings for
- /// setting fields, `Accept:` for disabling the libcurl default header, or
- /// `Accept;` for a header with no content. This is the format that libcurl
- /// expects.
- ///
- /// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_HTTPHEADER.html
- func curlHeaders(for httpHeaders: [AnyHashable : Any]?) -> [String] {
- var result: [String] = []
- var names = Set<String>()
- if httpHeaders != nil {
- let hh = httpHeaders as! [String:String]
- hh.forEach {
- let name = $0.0.lowercased()
- guard !names.contains(name) else { return }
- names.insert(name)
-
- if $0.1.isEmpty {
- result.append($0.0 + ";")
- } else {
- result.append($0.0 + ": " + $0.1)
- }
- }
- }
- curlHeadersToSet.forEach {
- let name = $0.0.lowercased()
- guard !names.contains(name) else { return }
- names.insert(name)
-
- if $0.1.isEmpty {
- result.append($0.0 + ";")
- } else {
- result.append($0.0 + ": " + $0.1)
- }
- }
- curlHeadersToRemove.forEach {
- let name = $0.lowercased()
- guard !names.contains(name) else { return }
- names.insert(name)
- result.append($0 + ":")
- }
- return result
- }
- /// Any header values that should be passed to libcurl
- ///
- /// These will only be set if not already part of the request.
- /// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_HTTPHEADER.html
- var curlHeadersToSet: [(String,String)] {
- var result = [("Connection", "keep-alive"),
- ("User-Agent", userAgentString),
- ]
- if let language = NSLocale.current.languageCode {
- result.append(("Accept-Language", language))
- }
- return result
- }
- /// Any header values that should be removed from the ones set by libcurl
- /// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_HTTPHEADER.html
- var curlHeadersToRemove: [String] {
- if case .none = body {
- return []
- } else {
- return ["Expect"]
- }
- }
-}
-
-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
- let name = info.processName
- let curlVersion = CFURLSessionCurlVersionInfo()
- //TODO: Should probably use sysctl(3) to get these:
- // kern.ostype: Darwin
- // kern.osrelease: 15.4.0
- //TODO: Use NSBundle to get the version number?
- return "\(name) (unknown version) curl/\(curlVersion.major).\(curlVersion.minor).\(curlVersion.patch)"
-}()
-
fileprivate func errorCode(fileSystemError error: Error) -> Int {
func fromCocoaErrorCode(_ code: Int) -> Int {
switch code {
@@ -750,454 +403,15 @@
}
}
-fileprivate extension URLSessionTask {
- /// Set request body length.
- ///
- /// An unknown length
- func set(requestBodyLength length: URLSessionTask._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 URLSessionTask: _EasyHandleDelegate {
- func didReceive(data: Data) -> _EasyHandle._Action {
- guard case .transferInProgress(let ts) = internalState else { fatalError("Received body data, but no transfer in progress.") }
- guard ts.isHeaderComplete else { fatalError("Received body data, but the header is not complete, yet.") }
- notifyDelegate(aboutReceivedData: data)
- internalState = .transferInProgress(ts.byAppending(bodyData: data))
- return .proceed
- }
-
- fileprivate func notifyDelegate(aboutReceivedData data: Data) {
- if case .taskDelegate(let delegate) = session.behaviour(for: self),
- let dataDelegate = delegate as? URLSessionDataDelegate,
- let task = self as? URLSessionDataTask {
- // Forward to the delegate:
- guard let s = session as? URLSession else { fatalError() }
- s.delegateQueue.addOperation {
- dataDelegate.urlSession(s, dataTask: task, didReceive: data)
- }
- } else if case .taskDelegate(let delegate) = session.behaviour(for: self),
- let downloadDelegate = delegate as? URLSessionDownloadDelegate,
- let task = self as? URLSessionDownloadTask {
- guard let s = session as? URLSession else { fatalError() }
- let fileHandle = try! FileHandle(forWritingTo: tempFileURL)
- _ = fileHandle.seekToEndOfFile()
- fileHandle.write(data)
- self.totalDownloaded += data.count
-
- s.delegateQueue.addOperation {
- downloadDelegate.urlSession(s, downloadTask: task, didWriteData: Int64(data.count), totalBytesWritten: Int64(self.totalDownloaded),
- totalBytesExpectedToWrite: Int64(self.easyHandle.fileLength))
- }
- if Int(self.easyHandle.fileLength) == totalDownloaded {
- fileHandle.closeFile()
- s.delegateQueue.addOperation {
- downloadDelegate.urlSession(s, downloadTask: task, didFinishDownloadingTo: self.tempFileURL)
- }
- }
-
- }
- }
-
- func didReceive(headerData data: Data) -> _EasyHandle._Action {
- guard case .transferInProgress(let ts) = internalState else { fatalError("Received body data, but no transfer in progress.") }
- 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
- }
- }
-
- func fill(writeBuffer buffer: UnsafeMutableBufferPointer<Int8>) -> _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)
- 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(withErrorCode errorCode: Int?) {
- // 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 = currentRequest else { fatalError("Transfer completed, but there's no current request.") }
- guard errorCode == nil else {
- internalState = .transferFailed
- failWith(errorCode: errorCode!, request: request)
- return
- }
-
- 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
- failWith(errorCode: errorCode, request: request)
- case .redirectWithRequest(let newRequest):
- redirectFor(request: newRequest)
- }
- }
- 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.
- }
-}
-
-/// State Transfers
-extension URLSessionTask {
- func completeTask() {
- guard case .transferCompleted(response: let response, bodyDataDrain: let bodyDataDrain) = internalState else {
- fatalError("Trying to complete the task, but its transfer isn't complete.")
- }
- self.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
- switch session.behaviour(for: self) {
- case .taskDelegate(let delegate):
- guard let s = session as? URLSession else { fatalError() }
- s.delegateQueue.addOperation {
- delegate.urlSession(s, task: self, didCompleteWithError: nil)
- self.internalState = .taskCompleted
- }
- case .noDelegate:
- internalState = .taskCompleted
- case .dataCompletionHandler(let completion):
- guard case .inMemory(let bodyData) = bodyDataDrain else {
- fatalError("Task has data completion handler, but data drain is not in-memory.")
- }
-
- guard let s = session as? URLSession else { fatalError() }
-
- var data = Data()
- if let body = bodyData {
- data = Data(bytes: body.bytes, count: body.length)
- }
-
- s.delegateQueue.addOperation {
- completion(data, response, nil)
- self.internalState = .taskCompleted
- self.session = nil
- }
- case .downloadCompletionHandler(let completion):
- guard case .toFile(let url, let fileHandle?) = bodyDataDrain else {
- fatalError("Task has data completion handler, but data drain is not a file handle.")
- }
-
- guard let s = session as? URLSession else { fatalError() }
- //The contents are already written, just close the file handle and call the handler
- fileHandle.closeFile()
-
- s.delegateQueue.addOperation {
- completion(url, response, nil)
- self.internalState = .taskCompleted
- self.session = nil
- }
-
- }
- }
- func completeTask(withError error: Error) {
- self.error = error
-
- guard case .transferFailed = 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
-
- switch session.behaviour(for: self) {
- case .taskDelegate(let delegate):
- guard let s = session as? URLSession else { fatalError() }
- s.delegateQueue.addOperation {
- delegate.urlSession(s, task: self, didCompleteWithError: error as Error)
- self.internalState = .taskCompleted
- }
- case .noDelegate:
- internalState = .taskCompleted
- case .dataCompletionHandler(let completion):
- guard let s = session as? URLSession else { fatalError() }
- s.delegateQueue.addOperation {
- completion(nil, nil, error)
- self.internalState = .taskCompleted
- }
- case .downloadCompletionHandler(let completion):
- guard let s = session as? URLSession else { fatalError() }
- s.delegateQueue.addOperation {
- completion(nil, nil, error)
- self.internalState = .taskCompleted
- }
- }
- }
- func failWith(errorCode: Int, request: URLRequest) {
- //TODO: Error handling
- let userInfo: [String : Any]? = request.url.map {
- [
- NSURLErrorFailingURLErrorKey: $0,
- NSURLErrorFailingURLStringErrorKey: $0.absoluteString,
- ]
- }
- let error = URLError(_nsError: NSError(domain: NSURLErrorDomain, code: errorCode, userInfo: userInfo))
- completeTask(withError: error)
- }
- 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) = internalState else {
- fatalError("Trying to redirect, but the transfer is not complete.")
- }
-
- switch session.behaviour(for: self) {
- 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
- // `func urlSession(session: session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: NSHTTPURLResponse, newRequest request: NSURLRequest, completionHandler: (NSURLRequest?) -> Void)`
- // ?
-
- internalState = .waitingForRedirectCompletionHandler(response: response, bodyDataDrain: bodyDataDrain)
- // We need this ugly cast in order to be able to support `URLSessionTask.init()`
- guard let s = session as? URLSession else { fatalError() }
- s.delegateQueue.addOperation {
- delegate.urlSession(s, task: self, willPerformHTTPRedirection: response, newRequest: request) { [weak self] (request: URLRequest?) in
- guard let task = self else { return }
- 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) = internalState else {
- fatalError("Received callback for HTTP redirection, but we're not waiting for it. Was it called multiple times?")
- }
- // If the request is `nil`, we're supposed to treat the current response
- // as the final response, i.e. not do any redirection.
- // Otherwise, we'll start a new transfer with the passed in request.
- if let r = request {
- startNewTransfer(with: r)
- } else {
- internalState = .transferCompleted(response: response, bodyDataDrain: bodyDataDrain)
- completeTask()
- }
- }
-}
-
-
-/// Response processing
-fileprivate extension URLSessionTask {
- /// Whenever we receive a response (i.e. a complete header) from libcurl,
- /// this method gets called.
- func didReceiveResponse() {
- guard let dt = self as? URLSessionDataTask else { return }
- guard case .transferInProgress(let ts) = internalState else { fatalError("Transfer not in progress.") }
- guard let response = ts.response else { fatalError("Header complete, but not URL response.") }
- switch session.behaviour(for: self) {
- case .noDelegate:
- break
- case .taskDelegate(let delegate as URLSessionDataDelegate):
- //TODO: There's a problem with libcurl / with how we're using it.
- // We're currently unable to pause the transfer / the easy handle:
- // https://curl.haxx.se/mail/lib-2016-03/0222.html
- //
- // For now, we'll notify the delegate, but won't pause the transfer,
- // and we'll disregard the completion handler:
- guard let s = session as? URLSession else { fatalError() }
- s.delegateQueue.addOperation {
- delegate.urlSession(s, dataTask: dt, didReceive: response, completionHandler: { _ in
- URLSession.printDebug("warning: Ignoring disposition from completion handler.")
- })
- }
- case .taskDelegate:
- break
- case .dataCompletionHandler:
- break
- case .downloadCompletionHandler:
- 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) = internalState else { fatalError("Transfer not in progress.") }
- internalState = .waitingForResponseCompletionHandler(ts)
-
- let dt = self as! URLSessionDataTask
-
- // We need this ugly cast in order to be able to support `URLSessionTask.init()`
- guard let s = 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 }
- 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) = 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)
- case .allow:
- // Continue the transfer. This will unpause the easy handle.
- 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
- ///
- /// - SeeAlso: <https://tools.ietf.org/html/rfc7231#section-6.4>
- func redirectRequest(for response: HTTPURLResponse, fromRequest: URLRequest) -> URLRequest? {
- //TODO: Do we ever want to redirect for HEAD requests?
- func methodAndURL() -> (String, URL)? {
- guard
- let location = response.value(forHeaderField: .location),
- let targetURL = URL(string: location)
- else {
- // 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?
- case 301, 302, 303:
- // Change into "GET":
- return ("GET", targetURL)
- case 307:
- // Re-use existing method:
- return (fromRequest.httpMethod ?? "GET", targetURL)
- default:
- return nil
- }
- }
- guard let (method, targetURL) = methodAndURL() else { return nil }
- var request = fromRequest
- request.httpMethod = method
- request.url = targetURL
- return request
- }
-}
-
-
-fileprivate extension HTTPURLResponse {
- /// Type safe HTTP header field name(s)
- enum _Field: String {
- /// `Location`
- /// - SeeAlso: RFC 2616 section 14.30 <https://tools.ietf.org/html/rfc2616#section-14.30>
- case location = "Location"
- }
- func value(forHeaderField field: _Field) -> String? {
- return field.rawValue
- }
-}
-
public extension URLSessionTask {
- /// The default URL session task priority, used implicitly for any task you
+ /// The default URL session task priority, used implicitly for any task you
/// have not prioritized. The floating point value of this constant is 0.5.
public static let defaultPriority: Float = 0.5
-
- /// A low URL session task priority, with a floating point value above the
+
+ /// A low URL session task priority, with a floating point value above the
/// minimum of 0 and below the default value.
public static let lowPriority: Float = 0.25
-
+
/// A high URL session task priority, with a floating point value above the
/// default value and below the maximum of 1.0.
public static let highPriority: Float = 0.75
@@ -1314,19 +528,4 @@
}
/* Key in the userInfo dictionary of an NSError received during a failed download. */
-public let URLSessionDownloadTaskResumeData: String = "" // NSUnimplemented
-
-
-extension URLSession {
- static func printDebug(_ text: @autoclosure () -> String) {
- guard enableDebugOutput else { return }
- debugPrint(text())
- }
-}
-
-fileprivate let enableLibcurlDebugOutput: Bool = {
- return (ProcessInfo.processInfo.environment["URLSessionDebugLibcurl"] != nil)
-}()
-fileprivate let enableDebugOutput: Bool = {
- return (ProcessInfo.processInfo.environment["URLSessionDebug"] != nil)
-}()
+public let URLSessionDownloadTaskResumeData: String = "NSURLSessionDownloadTaskResumeData"
diff --git a/Foundation/NSURLSession/EasyHandle.swift b/Foundation/NSURLSession/http/EasyHandle.swift
similarity index 100%
rename from Foundation/NSURLSession/EasyHandle.swift
rename to Foundation/NSURLSession/http/EasyHandle.swift
diff --git a/Foundation/NSURLSession/HTTPBodySource.swift b/Foundation/NSURLSession/http/HTTPBodySource.swift
similarity index 100%
rename from Foundation/NSURLSession/HTTPBodySource.swift
rename to Foundation/NSURLSession/http/HTTPBodySource.swift
diff --git a/Foundation/NSURLSession/HTTPMessage.swift b/Foundation/NSURLSession/http/HTTPMessage.swift
similarity index 83%
rename from Foundation/NSURLSession/HTTPMessage.swift
rename to Foundation/NSURLSession/http/HTTPMessage.swift
index 6598d06..91be843 100644
--- a/Foundation/NSURLSession/HTTPMessage.swift
+++ b/Foundation/NSURLSession/http/HTTPMessage.swift
@@ -20,7 +20,7 @@
import CoreFoundation
-extension URLSessionTask {
+extension _HTTPURLProtocol {
/// An HTTP header being parsed.
///
/// It can either be complete (i.e. the final CR LF CR LF has been
@@ -46,14 +46,14 @@
}
}
-extension URLSessionTask._ParsedResponseHeader {
+extension _HTTPURLProtocol._ParsedResponseHeader {
/// Parse a header line passed by libcurl.
///
/// These contain the <CRLF> 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) -> URLSessionTask._ParsedResponseHeader? {
+ func byAppending(headerLine data: Data) -> _HTTPURLProtocol._ParsedResponseHeader? {
// The buffer must end in CRLF
guard
2 <= data.count &&
@@ -70,33 +70,33 @@
/// is a complete header. Otherwise it's a partial header.
/// - Note: Appending a line to a complete header results in a partial
/// header with just that line.
- private func byAppending(headerLine line: String) -> URLSessionTask._ParsedResponseHeader {
+ private func byAppending(headerLine line: String) -> _HTTPURLProtocol._ParsedResponseHeader {
if line.isEmpty {
switch self {
case .partial(let header): return .complete(header)
- case .complete: return .partial(URLSessionTask._ResponseHeaderLines())
+ case .complete: return .partial(_HTTPURLProtocol._ResponseHeaderLines())
}
} else {
let header = partialResponseHeader
return .partial(header.byAppending(headerLine: line))
}
}
- private var partialResponseHeader: URLSessionTask._ResponseHeaderLines {
+ private var partialResponseHeader: _HTTPURLProtocol._ResponseHeaderLines {
switch self {
case .partial(let header): return header
- case .complete: return URLSessionTask._ResponseHeaderLines()
+ case .complete: return _HTTPURLProtocol._ResponseHeaderLines()
}
}
}
-private extension URLSessionTask._ResponseHeaderLines {
+private extension _HTTPURLProtocol._ResponseHeaderLines {
/// Returns a copy of the lines with the new line appended to it.
- func byAppending(headerLine line: String) -> URLSessionTask._ResponseHeaderLines {
+ func byAppending(headerLine line: String) -> _HTTPURLProtocol._ResponseHeaderLines {
var l = self.lines
l.append(line)
- return URLSessionTask._ResponseHeaderLines(headerLines: l)
+ return _HTTPURLProtocol._ResponseHeaderLines(headerLines: l)
}
}
-internal extension URLSessionTask._ResponseHeaderLines {
+internal extension _HTTPURLProtocol._ResponseHeaderLines {
/// Create an `NSHTTPRULResponse` from the lines.
///
/// This will parse the header lines.
@@ -105,17 +105,17 @@
guard let message = createHTTPMessage() else { return nil }
return HTTPURLResponse(message: message, URL: URL)
}
- /// Parse the lines into a `URLSessionTask.HTTPMessage`.
- func createHTTPMessage() -> URLSessionTask._HTTPMessage? {
+ /// Parse the lines into a `_HTTPURLProtocol.HTTPMessage`.
+ func createHTTPMessage() -> _HTTPURLProtocol._HTTPMessage? {
guard let (head, tail) = lines.decompose else { return nil }
- guard let startline = URLSessionTask._HTTPMessage._StartLine(line: head) else { return nil }
+ guard let startline = _HTTPURLProtocol._HTTPMessage._StartLine(line: head) else { return nil }
guard let headers = createHeaders(from: tail) else { return nil }
- return URLSessionTask._HTTPMessage(startLine: startline, headers: headers)
+ return _HTTPURLProtocol._HTTPMessage(startLine: startline, headers: headers)
}
}
extension HTTPURLResponse {
- fileprivate convenience init?(message: URLSessionTask._HTTPMessage, URL: URL) {
+ fileprivate convenience init?(message: _HTTPURLProtocol._HTTPMessage, URL: URL) {
/// This needs to be a request, i.e. it needs to have a status line.
guard case .statusLine(let statusLine) = message.startLine else { return nil }
let fields = message.headersAsDictionary
@@ -124,7 +124,7 @@
}
-extension URLSessionTask {
+extension _HTTPURLProtocol {
/// HTTP Message
///
/// A message consist of a *start-line* optionally followed by one or multiple
@@ -134,12 +134,12 @@
///
/// - SeeAlso: https://tools.ietf.org/html/rfc2616#section-4
struct _HTTPMessage {
- let startLine: URLSessionTask._HTTPMessage._StartLine
- let headers: [URLSessionTask._HTTPMessage._Header]
+ let startLine: _HTTPURLProtocol._HTTPMessage._StartLine
+ let headers: [_HTTPURLProtocol._HTTPMessage._Header]
}
}
-extension URLSessionTask._HTTPMessage {
+extension _HTTPURLProtocol._HTTPMessage {
var headersAsDictionary: [String: String] {
var result: [String: String] = [:]
headers.forEach {
@@ -153,7 +153,7 @@
return result
}
}
-extension URLSessionTask._HTTPMessage {
+extension _HTTPURLProtocol._HTTPMessage {
/// A single HTTP message header field
///
/// Most HTTP messages have multiple header fields.
@@ -168,17 +168,17 @@
enum _StartLine {
/// RFC 2616 Section 5.1 *Request Line*
/// - SeeAlso: https://tools.ietf.org/html/rfc2616#section-5.1
- case requestLine(method: String, uri: URL, version: URLSessionTask._HTTPMessage._Version)
+ case requestLine(method: String, uri: URL, version: _HTTPURLProtocol._HTTPMessage._Version)
/// RFC 2616 Section 6.1 *Status Line*
/// - SeeAlso: https://tools.ietf.org/html/rfc2616#section-6.1
- case statusLine(version: URLSessionTask._HTTPMessage._Version, status: Int, reason: String)
+ case statusLine(version: _HTTPURLProtocol._HTTPMessage._Version, status: Int, reason: String)
}
/// A HTTP version, e.g. "HTTP/1.1"
struct _Version: RawRepresentable {
let rawValue: String
}
}
-extension URLSessionTask._HTTPMessage._Version {
+extension _HTTPURLProtocol._HTTPMessage._Version {
init?(versionString: String) {
rawValue = versionString
}
@@ -200,14 +200,14 @@
static let Separators = NSCharacterSet(charactersIn: "()<>@,;:\\\"/[]?={} \t")
}
-private extension URLSessionTask._HTTPMessage._StartLine {
+private extension _HTTPURLProtocol._HTTPMessage._StartLine {
init?(line: String) {
guard let r = line.splitRequestLine() else { return nil }
- if let version = URLSessionTask._HTTPMessage._Version(versionString: r.0) {
+ if let version = _HTTPURLProtocol._HTTPMessage._Version(versionString: r.0) {
// Status line:
guard let status = Int(r.1), 100 <= status && status <= 999 else { return nil }
self = .statusLine(version: version, status: status, reason: r.2)
- } else if let version = URLSessionTask._HTTPMessage._Version(versionString: r.2),
+ } else if let version = _HTTPURLProtocol._HTTPMessage._Version(versionString: r.2),
let URI = URL(string: r.1) {
// The request method must be a token (i.e. without seperators):
let seperatorIdx = r.0.unicodeScalars.index(where: { !$0.isValidMessageToken } )
@@ -247,19 +247,19 @@
/// This respects the header folding as described by
/// https://tools.ietf.org/html/rfc2616#section-2.2 :
///
-/// - SeeAlso: `URLSessionTask.HTTPMessage.Header.createOne(from:)`
-private func createHeaders(from lines: ArraySlice<String>) -> [URLSessionTask._HTTPMessage._Header]? {
+/// - SeeAlso: `_HTTPURLProtocol.HTTPMessage.Header.createOne(from:)`
+private func createHeaders(from lines: ArraySlice<String>) -> [_HTTPURLProtocol._HTTPMessage._Header]? {
var headerLines = Array(lines)
- var headers: [URLSessionTask._HTTPMessage._Header] = []
+ var headers: [_HTTPURLProtocol._HTTPMessage._Header] = []
while !headerLines.isEmpty {
- guard let (header, remaining) = URLSessionTask._HTTPMessage._Header.createOne(from: headerLines) else { return nil }
+ guard let (header, remaining) = _HTTPURLProtocol._HTTPMessage._Header.createOne(from: headerLines) else { return nil }
headers.append(header)
headerLines = remaining
}
return headers
}
-private extension URLSessionTask._HTTPMessage._Header {
+private extension _HTTPURLProtocol._HTTPMessage._Header {
/// Parse a single HTTP message header field
///
/// Each header field consists
@@ -278,7 +278,7 @@
/// If an error occurs, it returns `nil`.
///
/// - SeeAlso: https://tools.ietf.org/html/rfc2616#section-4.2
- static func createOne(from lines: [String]) -> (URLSessionTask._HTTPMessage._Header, [String])? {
+ static func createOne(from lines: [String]) -> (_HTTPURLProtocol._HTTPMessage._Header, [String])? {
// HTTP/1.1 header field values can be folded onto multiple lines if the
// continuation line begins with a space or horizontal tab. All linear
// white space, including folding, has the same semantics as SP. A
@@ -309,7 +309,7 @@
let valuePart = String(v)
value = value.map { $0 + " " + valuePart } ?? valuePart
}
- return (URLSessionTask._HTTPMessage._Header(name: name, value: value ?? ""), Array(t))
+ return (_HTTPURLProtocol._HTTPMessage._Header(name: name, value: value ?? ""), Array(t))
}
}
}
diff --git a/Foundation/NSURLSession/http/HTTPURLProtocol.swift b/Foundation/NSURLSession/http/HTTPURLProtocol.swift
new file mode 100644
index 0000000..8f31fa1
--- /dev/null
+++ b/Foundation/NSURLSession/http/HTTPURLProtocol.swift
@@ -0,0 +1,892 @@
+// 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
+//
+
+import CoreFoundation
+import Dispatch
+
+internal class _HTTPURLProtocol: URLProtocol {
+
+ fileprivate var easyHandle: _EasyHandle!
+
+ public override 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 override 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()
+ } else {
+ self.internalState = .transferFailed
+ guard let error = self.task?.error else { fatalError() }
+ completeTask(withError: error)
+ return
+ }
+ }
+
+ /// 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)
+ }
+ }
+ didSet {
+ if !oldValue.isEasyHandleAddedToMultiHandle && internalState.isEasyHandleAddedToMultiHandle {
+ task?.session.add(handle: easyHandle)
+ }
+ if oldValue.isEasyHandlePaused && !internalState.isEasyHandlePaused {
+ fatalError("Need to solve pausing receive.")
+ }
+ }
+ }
+}
+
+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) {
+ // 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 defaul
+ // 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.") }
+ easyHandle.set(url: url)
+ easyHandle.setAllowedProtocolsToHTTPAndHTTPS()
+ easyHandle.set(preferredReceiveBufferSize: Int.max)
+ do {
+ switch (task?.body, try task?.body.getBodyLength()) {
+ case (.none, _):
+ set(requestBodyLength: .noBody)
+ case (_, .some(let length)):
+ set(requestBodyLength: .length(length))
+ case (_, .none):
+ set(requestBodyLength: .unknown)
+ }
+ } catch let e {
+ // Fail the request here.
+ // TODO: We have multiple options:
+ // NSURLErrorNoPermissionsToReadFile
+ // NSURLErrorFileDoesNotExist
+ self.internalState = .transferFailed
+ failWith(errorCode: errorCode(fileSystemError: e), 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
+ } else {
+ hh.forEach {
+ httpHeaders![$0] = $1
+ }
+ }
+ }
+ let customHeaders: [String]
+ let headersForRequest = curlHeaders(for: httpHeaders)
+ if ((request.httpMethod == "POST") && (request.value(forHTTPHeaderField: "Content-Type") == nil)) {
+ customHeaders = headersForRequest + ["Content-Type:application/x-www-form-urlencoded"]
+ } 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
+ }
+ 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
+ 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() }
+ 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)
+ }
+ }
+
+ /// These are a list of headers that should be passed to libcurl.
+ ///
+ /// Headers will be returned as `Accept: text/html` strings for
+ /// setting fields, `Accept:` for disabling the libcurl default header, or
+ /// `Accept;` for a header with no content. This is the format that libcurl
+ /// expects.
+ ///
+ /// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_HTTPHEADER.html
+ func curlHeaders(for httpHeaders: [AnyHashable : Any]?) -> [String] {
+ var result: [String] = []
+ var names = Set<String>()
+ if httpHeaders != nil {
+ let hh = httpHeaders as! [String:String]
+ hh.forEach {
+ let name = $0.0.lowercased()
+ guard !names.contains(name) else { return }
+ names.insert(name)
+
+ if $0.1.isEmpty {
+ result.append($0.0 + ";")
+ } else {
+ result.append($0.0 + ": " + $0.1)
+ }
+ }
+ }
+ curlHeadersToSet.forEach {
+ let name = $0.0.lowercased()
+ guard !names.contains(name) else { return }
+ names.insert(name)
+
+ if $0.1.isEmpty {
+ result.append($0.0 + ";")
+ } else {
+ result.append($0.0 + ": " + $0.1)
+ }
+ }
+ curlHeadersToRemove.forEach {
+ let name = $0.lowercased()
+ guard !names.contains(name) else { return }
+ names.insert(name)
+ result.append($0 + ":")
+ }
+ return result
+ }
+ /// Any header values that should be passed to libcurl
+ ///
+ /// These will only be set if not already part of the request.
+ /// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_HTTPHEADER.html
+ var curlHeadersToSet: [(String,String)] {
+ var result = [("Connection", "keep-alive"),
+ ("User-Agent", userAgentString),
+ ]
+ if let language = NSLocale.current.languageCode {
+ result.append(("Accept-Language", language))
+ }
+ return result
+ }
+ /// Any header values that should be removed from the ones set by libcurl
+ /// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_HTTPHEADER.html
+ var curlHeadersToRemove: [String] {
+ if case .none = task?.body {
+ return []
+ } else {
+ return ["Expect"]
+ }
+ }
+}
+
+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
+ let name = info.processName
+ let curlVersion = CFURLSessionCurlVersionInfo()
+ //TODO: Should probably use sysctl(3) to get these:
+ // kern.ostype: Darwin
+ // kern.osrelease: 15.4.0
+ //TODO: Use NSBundle to get the version number?
+ 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 }
+ debugPrint(text())
+ }
+}
+
+internal extension _HTTPURLProtocol {
+ enum _Body {
+ case none
+ case data(DispatchData)
+ /// Body data is read from the given file URL
+ case file(URL)
+ case stream(InputStream)
+ }
+
+ func failWith(errorCode: Int, request: URLRequest) {
+ //TODO: Error handling
+ let userInfo: [String : Any]? = request.url.map {
+ [
+ NSURLErrorFailingURLErrorKey: $0,
+ NSURLErrorFailingURLStringErrorKey: $0.absoluteString,
+ ]
+ }
+ let error = URLError(_nsError: NSError(domain: NSURLErrorDomain, code: errorCode, userInfo: userInfo))
+ completeTask(withError: error)
+ self.client?.urlProtocol(self, didFailWithError: error)
+ }
+}
+
+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 {
+ case .none:
+ return 0
+ case .data(let d):
+ return UInt64(d.count)
+ /// Body data is read from the given file URL
+ case .file(let fileURL):
+ guard let s = try FileManager.default.attributesOfItem(atPath: fileURL.path)[.size] as? NSNumber else {
+ throw _Error.fileForBodyDataNotFound
+ }
+ return s.uint64Value
+ case .stream:
+ return nil
+ }
+ }
+}
+
+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: task.tempFileURL)
+ return .toFile(task.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(let ts) = internalState else { fatalError("Received body data, but no transfer in progress.") }
+ guard ts.isHeaderComplete else { fatalError("Received body data, but the header is not complete, yet.") }
+ 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 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: task.tempFileURL)
+ _ = fileHandle.seekToEndOfFile()
+ fileHandle.write(data)
+ self.task?.totalDownloaded += data.count
+
+ s.delegateQueue.addOperation {
+ downloadDelegate.urlSession(s, downloadTask: task, didWriteData: Int64(data.count), totalBytesWritten: Int64(t.totalDownloaded),
+ totalBytesExpectedToWrite: Int64(self.easyHandle.fileLength))
+ }
+ if Int(self.easyHandle.fileLength) == self.task?.totalDownloaded {
+ fileHandle.closeFile()
+ s.delegateQueue.addOperation {
+ downloadDelegate.urlSession(s, downloadTask: task, didFinishDownloadingTo: t.tempFileURL)
+ }
+ }
+ }
+ }
+
+ func didReceive(headerData data: Data) -> _EasyHandle._Action {
+ guard case .transferInProgress(let ts) = internalState else { fatalError("Received body data, but no transfer in progress.") }
+ 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
+ }
+ }
+
+ func fill(writeBuffer buffer: UnsafeMutableBufferPointer<Int8>) -> _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)
+ 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(withErrorCode errorCode: Int?) {
+ // 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 errorCode == nil else {
+ internalState = .transferFailed
+ failWith(errorCode: errorCode!, 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
+ failWith(errorCode: errorCode, request: request)
+ case .redirectWithRequest(let newRequest):
+ redirectFor(request: newRequest)
+ }
+ }
+
+ 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.
+ }
+}
+
+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
+ return
+ }
+
+ if case .toFile(let url, let fileHandle?) = bodyDataDrain {
+ 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.")
+ }
+
+ let session = task?.session as! URLSession
+ 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()`
+ guard let s = session as? URLSession else { fatalError() }
+ s.delegateQueue.addOperation {
+ delegate.urlSession(s, 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?")
+ }
+ // If the request is `nil`, we're supposed to treat the current response
+ // as the final response, i.e. not do any redirection.
+ // Otherwise, we'll start a new transfer with the passed in request.
+ if let r = request {
+ startNewTransfer(with: r)
+ } else {
+ self.internalState = .transferCompleted(response: response, bodyDataDrain: bodyDataDrain)
+ completeTask()
+ }
+ }
+}
+
+/// Response processing
+internal extension _HTTPURLProtocol {
+ /// Whenever we receive a response (i.e. a complete header) from libcurl,
+ /// this method gets called.
+ func didReceiveResponse() {
+ guard let dt = 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.") }
+ let session = task?.session as! URLSession
+ switch session.behaviour(for: self.task!) {
+ case .noDelegate:
+ break
+ case .taskDelegate(let delegate as URLSessionDataDelegate):
+ //TODO: There's a problem with libcurl / with how we're using it.
+ // We're currently unable to pause the transfer / the easy handle:
+ // https://curl.haxx.se/mail/lib-2016-03/0222.html
+ //
+ // For now, we'll notify the delegate, but won't pause the transfer,
+ // and we'll disregard the completion handler:
+ guard let s = session as? URLSession else { fatalError() }
+ s.delegateQueue.addOperation {
+ delegate.urlSession(s, dataTask: dt, didReceive: response, completionHandler: { _ in
+ URLSession.printDebug("warning: Ignoring disposition from completion handler.")
+ })
+ }
+ case .taskDelegate:
+ break
+ case .dataCompletionHandler:
+ break
+ case .downloadCompletionHandler:
+ 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
+ ///
+ /// - SeeAlso: <https://tools.ietf.org/html/rfc7231#section-6.4>
+ func redirectRequest(for response: HTTPURLResponse, fromRequest: URLRequest) -> URLRequest? {
+ //TODO: Do we ever want to redirect for HEAD requests?
+ func methodAndURL() -> (String, URL)? {
+ guard
+ let location = response.value(forHeaderField: .location),
+ let targetURL = URL(string: location)
+ else {
+ // 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?
+ case 301, 302, 303:
+ // Change into "GET":
+ return ("GET", targetURL)
+ case 307:
+ // Re-use existing method:
+ return (fromRequest.httpMethod ?? "GET", targetURL)
+ default:
+ return nil
+ }
+ }
+ guard let (method, targetURL) = methodAndURL() else { return nil }
+ var request = fromRequest
+ request.httpMethod = method
+ request.url = targetURL
+ return request
+ }
+}
+
+fileprivate extension HTTPURLResponse {
+ /// Type safe HTTP header field name(s)
+ enum _Field: String {
+ /// `Location`
+ /// - SeeAlso: RFC 2616 section 14.30 <https://tools.ietf.org/html/rfc2616#section-14.30>
+ case location = "Location"
+ }
+ func value(forHeaderField field: _Field) -> String? {
+ return field.rawValue
+ }
+}
diff --git a/Foundation/NSURLSession/MultiHandle.swift b/Foundation/NSURLSession/http/MultiHandle.swift
similarity index 100%
rename from Foundation/NSURLSession/MultiHandle.swift
rename to Foundation/NSURLSession/http/MultiHandle.swift
diff --git a/Foundation/NSURLSession/TransferState.swift b/Foundation/NSURLSession/http/TransferState.swift
similarity index 66%
rename from Foundation/NSURLSession/TransferState.swift
rename to Foundation/NSURLSession/http/TransferState.swift
index 1532067..8e86c35 100644
--- a/Foundation/NSURLSession/TransferState.swift
+++ b/Foundation/NSURLSession/http/TransferState.swift
@@ -21,7 +21,7 @@
-extension URLSessionTask {
+extension _HTTPURLProtocol {
/// State related to an ongoing transfer.
///
/// This contains headers received so far, body data received so far, etc.
@@ -31,51 +31,52 @@
///
/// - TODO: Might move the `EasyHandle` into this `struct` ?
/// - SeeAlso: `URLSessionTask.EasyHandle`
- internal struct _TransferState {
+ 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
- let response: HTTPURLResponse?
+ 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:
- 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 {
+ 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 URLSessionTask._TransferState {
+extension _HTTPURLProtocol._HTTPTransferState {
/// Transfer state that can receive body data, but will not send body data.
- init(url: URL, bodyDataDrain: _DataDrain) {
+ init(url: URL, bodyDataDrain: _HTTPURLProtocol._DataDrain) {
self.url = url
- self.parsedResponseHeader = URLSessionTask._ParsedResponseHeader()
+ 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: _DataDrain, bodySource: _HTTPBodySource) {
+ init(url: URL, bodyDataDrain: _HTTPURLProtocol._DataDrain, bodySource: _HTTPBodySource) {
self.url = url
- self.parsedResponseHeader = URLSessionTask._ParsedResponseHeader()
+ self.parsedResponseHeader = _HTTPURLProtocol._ParsedResponseHeader()
self.response = nil
self.requestBodySource = bodySource
self.bodyDataDrain = bodyDataDrain
}
}
-extension URLSessionTask._TransferState {
+extension _HTTPURLProtocol._HTTPTransferState {
enum _Error: Error {
case parseSingleLineError
case parseCompleteHeaderError
@@ -86,7 +87,7 @@
/// return value's `isHeaderComplete` will then by `true`.
///
/// - Throws: When a parsing error occurs
- func byAppending(headerLine data: Data) throws -> URLSessionTask._TransferState {
+ func byAppending(headerLine data: Data) throws -> _HTTPURLProtocol._HTTPTransferState {
guard let h = parsedResponseHeader.byAppending(headerLine: data) else {
throw _Error.parseSingleLineError
}
@@ -96,9 +97,9 @@
guard response != nil else {
throw _Error.parseCompleteHeaderError
}
- return URLSessionTask._TransferState(url: url, parsedResponseHeader: URLSessionTask._ParsedResponseHeader(), response: response, requestBodySource: requestBodySource, bodyDataDrain: bodyDataDrain)
+ return _HTTPURLProtocol._HTTPTransferState(url: url, parsedResponseHeader: _HTTPURLProtocol._ParsedResponseHeader(), response: response, requestBodySource: requestBodySource, bodyDataDrain: bodyDataDrain)
} else {
- return URLSessionTask._TransferState(url: url, parsedResponseHeader: h, response: nil, requestBodySource: requestBodySource, bodyDataDrain: bodyDataDrain)
+ return _HTTPURLProtocol._HTTPTransferState(url: url, parsedResponseHeader: h, response: nil, requestBodySource: requestBodySource, bodyDataDrain: bodyDataDrain)
}
}
var isHeaderComplete: Bool {
@@ -109,13 +110,13 @@
/// - 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) -> URLSessionTask._TransferState {
+ func byAppending(bodyData buffer: Data) -> _HTTPURLProtocol._HTTPTransferState {
switch bodyDataDrain {
case .inMemory(let bodyData):
let data: NSMutableData = bodyData ?? NSMutableData()
data.append(buffer)
- let drain = _DataDrain.inMemory(data)
- return URLSessionTask._TransferState(url: url, parsedResponseHeader: parsedResponseHeader, response: response, requestBodySource: requestBodySource, bodyDataDrain: drain)
+ 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
@@ -130,8 +131,7 @@
///
/// 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) -> URLSessionTask._TransferState {
- return URLSessionTask._TransferState(url: url, parsedResponseHeader: parsedResponseHeader, response: response, requestBodySource: newSource, bodyDataDrain: bodyDataDrain)
+ func bySetting(bodySource newSource: _HTTPBodySource) -> _HTTPURLProtocol._HTTPTransferState {
+ return _HTTPURLProtocol._HTTPTransferState(url: url, parsedResponseHeader: parsedResponseHeader, response: response, requestBodySource: newSource, bodyDataDrain: bodyDataDrain)
}
}
-
diff --git a/Foundation/NSURLSession/libcurlHelpers.swift b/Foundation/NSURLSession/http/libcurlHelpers.swift
similarity index 100%
rename from Foundation/NSURLSession/libcurlHelpers.swift
rename to Foundation/NSURLSession/http/libcurlHelpers.swift
diff --git a/TestFoundation/TestNSURLSession.swift b/TestFoundation/TestNSURLSession.swift
index 275016c..ea3e3da 100644
--- a/TestFoundation/TestNSURLSession.swift
+++ b/TestFoundation/TestNSURLSession.swift
@@ -24,20 +24,21 @@
//Disabling to avoid https://bugs.swift.org/browse/SR-4677 and a timeout failure
// ("test_dataTaskWithURL", test_dataTaskWithURL),
// ("test_dataTaskWithURLRequest", test_dataTaskWithURLRequest),
-// ("test_dataTaskWithURLCompletionHandler", test_dataTaskWithURLCompletionHandler),
-// ("test_dataTaskWithURLRequestCompletionHandler", test_dataTaskWithURLRequestCompletionHandler),
+ ("test_dataTaskWithURLCompletionHandler", test_dataTaskWithURLCompletionHandler),
+ ("test_dataTaskWithURLRequestCompletionHandler", test_dataTaskWithURLRequestCompletionHandler),
// ("test_downloadTaskWithURL", test_downloadTaskWithURL),
// ("test_downloadTaskWithURLRequest", test_downloadTaskWithURLRequest),
-// ("test_downloadTaskWithRequestAndHandler", test_downloadTaskWithRequestAndHandler),
-// ("test_downloadTaskWithURLAndHandler", test_downloadTaskWithURLAndHandler),
+ ("test_downloadTaskWithRequestAndHandler", test_downloadTaskWithRequestAndHandler),
+ ("test_downloadTaskWithURLAndHandler", test_downloadTaskWithURLAndHandler),
// ("test_finishTaskAndInvalidate", test_finishTasksAndInvalidate),
// ("test_taskError", test_taskError),
-// ("test_taskCopy", test_taskCopy),
+ ("test_taskCopy", test_taskCopy),
// ("test_cancelTask", test_cancelTask),
// ("test_taskTimeout", test_taskTimeout),
-// ("test_verifyRequestHeaders", test_verifyRequestHeaders),
-// ("test_verifyHttpAdditionalHeaders", test_verifyHttpAdditionalHeaders),
+ ("test_verifyRequestHeaders", test_verifyRequestHeaders),
+ ("test_verifyHttpAdditionalHeaders", test_verifyHttpAdditionalHeaders),
("test_timeoutInterval", test_timeoutInterval),
+ ("test_customProtocol", test_customProtocol),
]
}
@@ -418,6 +419,37 @@
waitForExpectations(timeout: 30)
}
+
+ func test_customProtocol () {
+ let serverReady = ServerSemaphore()
+ globalDispatchQueue.async {
+ do {
+ try self.runServer(with: serverReady)
+ } catch {
+ XCTAssertTrue(true)
+ return
+ }
+ }
+ serverReady.wait()
+ let urlString = "http://127.0.0.1:\(serverPort)/USA"
+ let url = URL(string: urlString)!
+ let config = URLSessionConfiguration.default
+ config.protocolClasses = [CustomProtocol.self]
+ config.timeoutIntervalForRequest = 8
+ let session = URLSession(configuration: config, delegate: nil, delegateQueue: nil)
+ let expect = expectation(description: "URL test with custom protocol")
+ let task = session.dataTask(with: url) { data, response, error in
+ defer { expect.fulfill() }
+ if let e = error as? URLError {
+ XCTAssertEqual(e.code, .timedOut, "Unexpected error code")
+ return
+ }
+ let httpResponse = response as! HTTPURLResponse?
+ XCTAssertEqual(429, httpResponse!.statusCode, "HTTP response code is not 429")
+ }
+ task.resume()
+ waitForExpectations(timeout: 12)
+ }
}
class SessionDelegate: NSObject, URLSessionDelegate {
@@ -526,3 +558,28 @@
dwdExpectation.fulfill()
}
}
+
+class CustomProtocol : URLProtocol {
+
+ override class func canInit(with request: URLRequest) -> Bool {
+ return true
+ }
+
+ func sendResponse(statusCode: Int, headers: [String: String] = [:], data: Data) {
+ let response = HTTPURLResponse(url: self.request.url!, statusCode: statusCode, httpVersion: "HTTP/1.1", headerFields: headers)
+ self.client?.urlProtocol(self, didReceive: response!, cacheStoragePolicy: .notAllowed)
+ self.client?.urlProtocolDidFinishLoading(self)
+ }
+
+ override class func canonicalRequest(for request: URLRequest) -> URLRequest {
+ return request
+ }
+
+ override func startLoading() {
+ sendResponse(statusCode: 429, data: Data())
+ }
+
+ override func stopLoading() {
+ return
+ }
+}
diff --git a/build.py b/build.py
index e569e2b..5592735 100644
--- a/build.py
+++ b/build.py
@@ -398,17 +398,18 @@
'Foundation/NSURLRequest.swift',
'Foundation/NSURLResponse.swift',
'Foundation/NSURLSession/Configuration.swift',
- 'Foundation/NSURLSession/EasyHandle.swift',
- 'Foundation/NSURLSession/HTTPBodySource.swift',
- 'Foundation/NSURLSession/HTTPMessage.swift',
- 'Foundation/NSURLSession/MultiHandle.swift',
+ 'Foundation/NSURLSession/http/EasyHandle.swift',
+ 'Foundation/NSURLSession/http/HTTPBodySource.swift',
+ 'Foundation/NSURLSession/http/HTTPMessage.swift',
+ 'Foundation/NSURLSession/http/MultiHandle.swift',
'Foundation/NSURLSession/NSURLSession.swift',
'Foundation/NSURLSession/NSURLSessionConfiguration.swift',
'Foundation/NSURLSession/NSURLSessionDelegate.swift',
'Foundation/NSURLSession/NSURLSessionTask.swift',
'Foundation/NSURLSession/TaskRegistry.swift',
- 'Foundation/NSURLSession/TransferState.swift',
- 'Foundation/NSURLSession/libcurlHelpers.swift',
+ 'Foundation/NSURLSession/http/TransferState.swift',
+ 'Foundation/NSURLSession/http/libcurlHelpers.swift',
+ 'Foundation/NSURLSession/http/HTTPURLProtocol.swift',
'Foundation/NSUserDefaults.swift',
'Foundation/NSUUID.swift',
'Foundation/NSValue.swift',