diff --git a/Foundation.xcodeproj/project.pbxproj b/Foundation.xcodeproj/project.pbxproj index 77ead891a9..0eebe4db35 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 */; }; @@ -500,6 +501,7 @@ 5B1FD9D21D6D16580080E83C /* TaskRegistry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TaskRegistry.swift; sourceTree = ""; }; 5B1FD9D31D6D16580080E83C /* TransferState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransferState.swift; sourceTree = ""; }; 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 = ""; }; 5B1FD9E21D6D17B80080E83C /* TestNSURLSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSURLSession.swift; sourceTree = ""; }; 5B23AB861CE62D17000DB898 /* Boxing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Boxing.swift; sourceTree = ""; }; 5B23AB881CE62D4D000DB898 /* ReferenceConvertible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferenceConvertible.swift; sourceTree = ""; }; @@ -938,23 +940,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 = ""; }; + 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 = ""; + }; 5B5D88531BBC938800234F36 = { isa = PBXGroup; children = ( @@ -2057,6 +2068,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 4362d71cb9..11a23408fb 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 @@ public protocol URLProtocolClient : NSObjectProtocol { 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 @@ public protocol URLProtocolClient : NSObjectProtocol { 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 @@ open class URLProtocol : NSObject { 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 @@ open class URLProtocol : NSObject { @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 @@ open class URLProtocol : NSObject { @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 @@ open class URLProtocol : NSObject { 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 @@ open class URLProtocol : NSObject { 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 @@ open class URLProtocol : NSObject { 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 bd93bcc92d..f58ee17890 100644 --- a/Foundation/NSURLSession/NSURLSession.swift +++ b/Foundation/NSURLSession/NSURLSession.swift @@ -220,6 +220,8 @@ open class URLSession : NSObject { 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) } /* @@ -245,6 +247,8 @@ open class URLSession : NSObject { 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 347aba257f..876ef58c4a 100644 --- a/Foundation/NSURLSession/NSURLSessionTask.swift +++ b/Foundation/NSURLSession/NSURLSessionTask.swift @@ -26,55 +26,19 @@ import Dispatch /// 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,7 +75,25 @@ open class URLSessionTask : NSObject, NSCopying { _ = 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: Do we remove the EasyHandle from the session here? This might run on the wrong thread / queue. @@ -132,7 +114,7 @@ open class URLSessionTask : NSObject, NSCopying { /*@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 { @@ -147,7 +129,7 @@ open class URLSessionTask : NSObject, NSCopying { } } fileprivate var _currentRequest: URLRequest? = nil - /*@NSCopying*/ open fileprivate(set) var response: URLResponse? { + /*@NSCopying*/ open internal(set) var response: URLResponse? { get { semaphore.wait() defer { @@ -169,7 +151,7 @@ open class URLSessionTask : NSObject, NSCopying { */ /// Number of body bytes already received - open fileprivate(set) var countOfBytesReceived: Int64 { + open fileprivate(set) var countOfBytesReceived: Int64 { get { semaphore.wait() defer { @@ -200,6 +182,7 @@ open class URLSessionTask : NSObject, NSCopying { semaphore.signal() } } + fileprivate var _countOfBytesSent: Int64 = 0 /// Number of body bytes we expect to send, derived from the Content-Length of the HTTP request */ @@ -223,13 +206,14 @@ open class URLSessionTask : NSObject, NSCopying { guard self.state == .running || self.state == .suspended else { return } self.state = .canceling self.workQueue.async { - self.internalState = .transferFailed let urlError = URLError(_nsError: NSError(domain: NSURLErrorDomain, code: NSURLErrorCancelled, userInfo: nil)) - self.completeTask(withError: urlError) + self.error = urlError + self._protocol.stopLoading() + self._protocol.client?.urlProtocol(self._protocol, didFailWithError: urlError) } } } - + /* * The current state of the task within the session. */ @@ -253,7 +237,7 @@ open class URLSessionTask : NSObject, NSCopying { * 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. /// @@ -287,7 +271,7 @@ open class URLSessionTask : NSObject, NSCopying { if self.suspendCount == 1 { self.workQueue.async { - self.performSuspend() + self._protocol.stopLoading() } } } @@ -302,7 +286,7 @@ open class URLSessionTask : NSObject, NSCopying { self.updateTaskState() if self.suspendCount == 0 { self.workQueue.async { - self.performResume() + self._protocol.startLoading() } } } @@ -352,103 +336,12 @@ extension URLSessionTask { } } -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 { @@ -468,7 +361,7 @@ internal extension URLSessionTask { case stream(InputStream) } } -fileprivate extension URLSessionTask._Body { +internal extension URLSessionTask._Body { enum _Error : Error { case fileForBodyDataNotFound } @@ -491,249 +384,6 @@ fileprivate extension URLSessionTask._Body { } } -/// 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) - - //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 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() - 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 { @@ -754,452 +404,15 @@ fileprivate func errorCode(fileSystemError error: Error) -> Int { } } -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) -> _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 - - 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: - 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 - 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 @@ -1317,18 +530,3 @@ open class URLSessionStreamTask : URLSessionTask { /* Key in the userInfo dictionary of an NSError received during a failed download. */ public let URLSessionDownloadTaskResumeData: String = "NSURLSessionDownloadTaskResumeData" - - -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) -}() 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 6598d06cbd..91be8434c5 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 { } } -extension URLSessionTask._ParsedResponseHeader { +extension _HTTPURLProtocol._ParsedResponseHeader { /// Parse a header line passed by libcurl. /// /// These contain the ending and the final line contains nothing but /// that ending. /// - Returns: Returning nil indicates failure. Otherwise returns a new /// `ParsedResponseHeader` with the given line added. - func byAppending(headerLine data: Data) -> URLSessionTask._ParsedResponseHeader? { + func byAppending(headerLine data: Data) -> _HTTPURLProtocol._ParsedResponseHeader? { // The buffer must end in CRLF guard 2 <= data.count && @@ -70,33 +70,33 @@ extension URLSessionTask._ParsedResponseHeader { /// 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 @@ internal extension URLSessionTask._ResponseHeaderLines { 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 HTTPURLResponse { } -extension URLSessionTask { +extension _HTTPURLProtocol { /// HTTP Message /// /// A message consist of a *start-line* optionally followed by one or multiple @@ -134,12 +134,12 @@ extension URLSessionTask { /// /// - 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 @@ extension URLSessionTask._HTTPMessage { 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 @@ extension URLSessionTask._HTTPMessage { 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 @@ struct _HTTPCharacters { 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 @@ private extension String { /// 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) -> [URLSessionTask._HTTPMessage._Header]? { +/// - SeeAlso: `_HTTPURLProtocol.HTTPMessage.Header.createOne(from:)` +private func createHeaders(from lines: ArraySlice) -> [_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 @@ private extension URLSessionTask._HTTPMessage._Header { /// 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 @@ private extension URLSessionTask._HTTPMessage._Header { 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 0000000000..8f31fa1989 --- /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() + 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) -> _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: + 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 + 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 1532067b44..8e86c3560e 100644 --- a/Foundation/NSURLSession/TransferState.swift +++ b/Foundation/NSURLSession/http/TransferState.swift @@ -21,7 +21,7 @@ import CoreFoundation -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 @@ extension URLSessionTask { /// /// - 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 @@ extension URLSessionTask._TransferState { /// 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 @@ extension URLSessionTask._TransferState { 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 @@ extension URLSessionTask._TransferState { /// - 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 @@ extension URLSessionTask._TransferState { /// /// 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 d06e88db77..c89f5aef44 100644 --- a/TestFoundation/TestNSURLSession.swift +++ b/TestFoundation/TestNSURLSession.swift @@ -38,6 +38,7 @@ class TestURLSession : XCTestCase { ("test_verifyRequestHeaders", test_verifyRequestHeaders), ("test_verifyHttpAdditionalHeaders", test_verifyHttpAdditionalHeaders), ("test_timeoutInterval", test_timeoutInterval), + ("test_customProtocol", test_customProtocol), ] } @@ -435,6 +436,37 @@ class TestURLSession : XCTestCase { 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 { @@ -551,3 +583,28 @@ extension DownloadTask : URLSessionTaskDelegate { 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 669199ad13..9d4e4e151e 100644 --- a/build.py +++ b/build.py @@ -402,17 +402,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',