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