diff --git a/Foundation.xcodeproj/project.pbxproj b/Foundation.xcodeproj/project.pbxproj index 5461afe6ca..6f03e09012 100644 --- a/Foundation.xcodeproj/project.pbxproj +++ b/Foundation.xcodeproj/project.pbxproj @@ -40,7 +40,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 */; }; @@ -302,11 +301,16 @@ 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 */; }; + 61710D121F67ABCF008D3087 /* TestFTP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61710D111F67ABCF008D3087 /* TestFTP.swift */; }; + 617F57F31F1F5CD30004F3F0 /* FTPURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617F57F21F1F5CD30004F3F0 /* FTPURLProtocol.swift */; }; + 61827A9E1F2208E400016214 /* FTPServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61827A9D1F2208E400016214 /* FTPServer.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 */; }; 6EB768281D18C12C00D4B719 /* UUID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EB768271D18C12C00D4B719 /* UUID.swift */; }; @@ -324,7 +328,6 @@ B910957B1EEF237800A71930 /* NSString-UTF16-BE-data.txt in Resources */ = {isa = PBXBuildFile; fileRef = B91095791EEF237800A71930 /* NSString-UTF16-BE-data.txt */; }; 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 */; }; - 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 */; }; @@ -759,11 +762,15 @@ 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 = ""; }; + 61710D111F67ABCF008D3087 /* TestFTP.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestFTP.swift; sourceTree = ""; }; + 617F57F21F1F5CD30004F3F0 /* FTPURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FTPURLProtocol.swift; sourceTree = ""; }; + 61827A9D1F2208E400016214 /* FTPServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FTPServer.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 = ""; }; @@ -791,13 +798,12 @@ B91095791EEF237800A71930 /* NSString-UTF16-BE-data.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "NSString-UTF16-BE-data.txt"; sourceTree = ""; }; 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 = ""; }; - 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 = ""; }; + 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 = ""; }; @@ -987,6 +993,8 @@ 5B1FD9C71D6D162D0080E83C /* Session */ = { isa = PBXGroup; children = ( + 617F57F11F1F5C5F0004F3F0 /* ftp */, + 617F57F01F1F5C4B0004F3F0 /* libcurl */, E4F889331E9CF04D008A70EB /* http */, 5B1FD9C81D6D16580080E83C /* Configuration.swift */, 5B1FD9CE1D6D16580080E83C /* URLSession.swift */, @@ -994,6 +1002,7 @@ 5B1FD9D01D6D16580080E83C /* URLSessionDelegate.swift */, 5B1FD9D11D6D16580080E83C /* URLSessionTask.swift */, 5B1FD9D21D6D16580080E83C /* TaskRegistry.swift */, + 61EE04541F208805002051A2 /* NativeProtocol.swift */, ); name = Session; path = URLSession; @@ -1345,6 +1354,24 @@ path = Foundation; sourceTree = ""; }; + 617F57F01F1F5C4B0004F3F0 /* libcurl */ = { + isa = PBXGroup; + children = ( + B9974B911EDF4A22007F15B8 /* libcurlHelpers.swift */, + B9974B951EDF4A22007F15B8 /* EasyHandle.swift */, + B9974B901EDF4A22007F15B8 /* MultiHandle.swift */, + ); + path = libcurl; + sourceTree = ""; + }; + 617F57F11F1F5C5F0004F3F0 /* ftp */ = { + isa = PBXGroup; + children = ( + 617F57F21F1F5CD30004F3F0 /* FTPURLProtocol.swift */, + ); + path = ftp; + sourceTree = ""; + }; 9F4ADBCF1ECD4F56001F0B3D /* xdgTestHelper */ = { isa = PBXGroup; children = ( @@ -1359,12 +1386,8 @@ 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 = ""; @@ -1383,6 +1406,7 @@ isa = PBXGroup; children = ( 1520469A1D8AEABE00D02E36 /* HTTPServer.swift */, + 61827A9D1F2208E400016214 /* FTPServer.swift */, EA66F6381BF1619600136161 /* main.swift */, 9F4ADBCF1ECD4F56001F0B3D /* xdgTestHelper */, EA66F65A1BF1976100136161 /* Tests */, @@ -1456,7 +1480,6 @@ D3A597EF1C33A9E500295652 /* TestNSKeyedUnarchiver.swift */, 61A395F91C2484490029B337 /* TestNSLocale.swift */, 61F8AE7C1C180FC600FB62F0 /* TestNotificationCenter.swift */, - 5EF673AB1C28B527006212A3 /* TestNotificationQueue.swift */, 5B6F17921C48631C00935030 /* TestNSNull.swift */, EA66F63F1BF1619600136161 /* TestNSNumber.swift */, D5C40F321CDA1D460005690C /* TestOperationQueue.swift */, @@ -1497,6 +1520,8 @@ 5B6F17961C48631C00935030 /* TestUtils.swift */, 03B6F5831F15F339004F25AF /* TestURLProtocol.swift */, 3E55A2321F52463B00082000 /* TestUnit.swift */, + 61445AE41F67A73100F8C143 /* TestNotificationQueue.swift */, + 61710D111F67ABCF008D3087 /* TestFTP.swift */, ); name = Tests; sourceTree = ""; @@ -2206,7 +2231,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 */, @@ -2216,6 +2240,7 @@ EADE0BB81BD15E0000C49C64 /* Process.swift in Sources */, 5BF7AEB31BCD51F9008F214A /* NSObjCRuntime.swift in Sources */, 5BD31D3F1D5D19D600563814 /* Dictionary.swift in Sources */, + 617F57F31F1F5CD30004F3F0 /* FTPURLProtocol.swift in Sources */, B9974B9B1EDF4A22007F15B8 /* HTTPBodySource.swift in Sources */, 5B94E8821C430DE70055C035 /* NSStringAPI.swift in Sources */, 5B0163BB1D024EB7003CCD96 /* DateComponents.swift in Sources */, @@ -2251,6 +2276,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 */, @@ -2367,6 +2393,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 */, @@ -2374,7 +2401,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 */, @@ -2385,6 +2411,7 @@ 5B13B3281C582D4C00651CE2 /* TestBundle.swift in Sources */, 5B13B32A1C582D4C00651CE2 /* TestCharacterSet.swift in Sources */, BF8E65311DC3B3CB005AB5C3 /* TestNotification.swift in Sources */, + 61710D121F67ABCF008D3087 /* TestFTP.swift in Sources */, 63DCE9D41EAA432400E9CB02 /* TestISO8601DateFormatter.swift in Sources */, EA01AAEC1DA839C4008F4E07 /* TestProgress.swift in Sources */, 03B6F5841F15F339004F25AF /* TestURLProtocol.swift in Sources */, @@ -2416,6 +2443,7 @@ D5C40F331CDA1D460005690C /* TestOperationQueue.swift in Sources */, BDBB65901E256BFA001A7286 /* TestEnergyFormatter.swift in Sources */, 5B13B32F1C582D4C00651CE2 /* TestNSGeometry.swift in Sources */, + 61827A9E1F2208E400016214 /* FTPServer.swift in Sources */, EA08126C1DA810BE00651B70 /* ProgressFraction.swift in Sources */, 5B13B3351C582D4C00651CE2 /* TestNSKeyedUnarchiver.swift in Sources */, 5B13B33D1C582D4C00651CE2 /* TestPipe.swift in Sources */, diff --git a/Foundation/URLSession/NativeProtocol.swift b/Foundation/URLSession/NativeProtocol.swift new file mode 100644 index 0000000000..9cd3e95e56 --- /dev/null +++ b/Foundation/URLSession/NativeProtocol.swift @@ -0,0 +1,754 @@ +// Foundation/NSURLSession/_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 var totalDownloaded = 0 + 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(transferSate:ts) { + ts.response = response + } + notifyDelegate(aboutReceivedData: data) + internalState = .transferInProgress(ts.byAppending(bodyData: data)) + return .proceed + } + + func validateHeaderComplete(transferSate: _TransferState) -> URLResponse? { + guard transferSate.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(withErrorCode errorCode: Int?) { + // At this point the transfer is complete and we can decide what to do. + // If everything went well, we will simply forward the resulting data + // to the delegate. But in case of redirects etc. we might send another + // request. + guard case .transferInProgress(let ts) = internalState else { fatalError("Transfer completed, but it wasn't in progress.") } + guard let request = task?.currentRequest else { fatalError("Transfer completed, but there's no current request.") } + guard errorCode == nil else { + internalState = .transferFailed + failWith(errorCode: errorCode!, request: request) + return + } + + if let response = task?.response { + var transferState = ts + transferState.response = response + } + + guard let response = ts.response else { fatalError("Transfer completed, but there's no response.") } + internalState = .transferCompleted(response: response, bodyDataDrain: ts.bodyDataDrain) + let action = completionAction(forCompletedRequest: request, response: response) + + switch action { + case .completeTask: + completeTask() + case .failWithError(let errorCode): + internalState = .transferFailed + failWith(errorCode: errorCode, request: request) + case .redirectWithRequest(let newRequest): + redirectFor(request: newRequest) + } + } + + func 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 { + NSUnimplemented() + } + + func updateProgressMeter(with propgress: _EasyHandle._Progress) { + } + + 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(errorCode: Int, request: URLRequest) { + //TODO: Error handling + let userInfo: [String : Any]? = request.url.map { + [ + NSURLErrorFailingURLErrorKey: $0, + NSURLErrorFailingURLStringErrorKey: $0.absoluteString, + ] + } + let error = URLError(_nsError: NSError(domain: NSURLErrorDomain, code: errorCode, userInfo: userInfo)) + completeTask(withError: error) + self.client?.urlProtocol(self, didFailWithError: error) + } + + /// 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 is 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 + /// Describes what to do with received body data for this transfer: + } +} + +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 { + /// 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 `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, headerCompleted: (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 headerCompleted(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 _FTPURLProtocol._TransferState { + + enum FTPHeaderCode: String { + case transferCompleted = "226" + case openDataConnection = "150" + case fileStatus = "213" + case syntaxError = "5" // 500 series FTP Syntax errors + case errorOccurred = "4" // 400 Series FTP transfer errors + } + + /// 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 byAppendingFTP(headerLine data: Data, expectedContentLength: Int64) throws -> _NativeProtocol._TransferState { + let line = String(data: data, encoding: String.Encoding.utf8) + if (line?.starts(with: FTPHeaderCode.transferCompleted.rawValue))! { + return self + } + + func isCompleteHeader(_ headerLine: String) ->Bool { + return headerLine.starts(with: FTPHeaderCode.openDataConnection.rawValue) + } + guard let h = parsedResponseHeader.byAppending(headerLine: data,headerCompleted: isCompleteHeader) else { + throw _NativeProtocol._Error.parseSingleLineError + } + + if case .complete(let lines) = h { + let response = lines.createURLResponse(for: url, contentLength: expectedContentLength) + 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: _NativeProtocol._ParsedResponseHeader(), response: nil, requestBodySource: requestBodySource, 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, headerCompleted: 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/URLSession.swift b/Foundation/URLSession/URLSession.swift index 3a07673328..aa6cf3a32a 100644 --- a/Foundation/URLSession/URLSession.swift +++ b/Foundation/URLSession/URLSession.swift @@ -203,6 +203,7 @@ open class URLSession : NSObject { fileprivate static let registerProtocols: () = { // TODO: We register all the native protocols here. let _ = URLProtocol.registerClass(_HTTPURLProtocol.self) + let _ = URLProtocol.registerClass(_FTPURLProtocol.self) }() /* diff --git a/Foundation/URLSession/URLSessionConfiguration.swift b/Foundation/URLSession/URLSessionConfiguration.swift index a83f34ea65..12c5258bb0 100644 --- a/Foundation/URLSession/URLSessionConfiguration.swift +++ b/Foundation/URLSession/URLSessionConfiguration.swift @@ -47,7 +47,7 @@ open class URLSessionConfiguration : NSObject, NSCopying { self.urlCredentialStorage = nil self.urlCache = nil self.shouldUseExtendedBackgroundIdleMode = false - self.protocolClasses = [_HTTPURLProtocol.self] + self.protocolClasses = [_HTTPURLProtocol.self, _FTPURLProtocol.self] super.init() } diff --git a/Foundation/URLSession/ftp/FTPURLProtocol.swift b/Foundation/URLSession/ftp/FTPURLProtocol.swift new file mode 100644 index 0000000000..a72dc46db1 --- /dev/null +++ b/Foundation/URLSession/ftp/FTPURLProtocol.swift @@ -0,0 +1,148 @@ +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// + +import CoreFoundation +import Dispatch + +internal class _FTPURLProtocol: _NativeProtocol { + + public required init(task: URLSessionTask, cachedResponse: CachedURLResponse?, client: URLProtocolClient?) { + super.init(task: task, cachedResponse: cachedResponse, client: client) + } + + public required init(request: URLRequest, cachedResponse: CachedURLResponse?, client: URLProtocolClient?) { + super.init(request: request, cachedResponse: cachedResponse, client: client) + } + + override class func canInit(with request: URLRequest) -> Bool { + guard request.url?.scheme == "ftp" || request.url?.scheme == "ftps" || request.url?.scheme == "sftp" else { return false } + return true + } + + override class func canonicalRequest(for request: URLRequest) -> URLRequest { + return request + } + + override func startLoading() { + resume() + } + + override func stopLoading() { + if task?.state == .suspended { + suspend() + } else { + self.internalState = .transferFailed + guard let error = self.task?.error else { fatalError() } + completeTask(withError: error) + } + } + + override func didReceive(headerData data: Data, contentLength: Int64) -> _EasyHandle._Action { + guard case .transferInProgress(let ts) = internalState else { fatalError("Received body 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.byAppendingFTP(headerLine: data, expectedContentLength: contentLength) + 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 + } + } + + override func seekInputStream(to position: UInt64) throws { + // NSUnimplemented() + } + + override func updateProgressMeter(with propgress: _EasyHandle._Progress) { + //NSUnimplemented() + } + + override func configureEasyHandle(for request: URLRequest) { + easyHandle.set(verboseModeOn: enableLibcurlDebugOutput) + easyHandle.set(debugOutputOn: enableLibcurlDebugOutput, task: task!) + easyHandle.set(skipAllSignalHandling: true) + guard let url = request.url else { fatalError("No URL in request.") } + easyHandle.set(url: url) + easyHandle.set(preferredReceiveBufferSize: Int.max) + do { + switch (task?.body, try task?.body.getBodyLength()) { + case (.some(URLSessionTask._Body.none), _): + set(requestBodyLength: .noBody) + case (_, .some(let length)): + set(requestBodyLength: .length(length)) + task!.countOfBytesExpectedToSend = Int64(length) + case (_, .none): + set(requestBodyLength: .unknown) + } + } catch let e { + // Fail the request here. + // TODO: We have multiple options: + // NSURLErrorNoPermissionsToReadFile + // NSURLErrorFileDoesNotExist + self.internalState = .transferFailed + failWith(errorCode: errorCode(fileSystemError: e), request: request) + return + } + let timeoutHandler = DispatchWorkItem { [weak self] in + guard let _ = self?.task else { fatalError("Timeout on a task that doesn't exist") } //this guard must always pass + self?.internalState = .transferFailed + let urlError = URLError(_nsError: NSError(domain: NSURLErrorDomain, code: NSURLErrorTimedOut, userInfo: nil)) + self?.completeTask(withError: urlError) + self?.client?.urlProtocol(self!, didFailWithError: urlError) + } + guard let task = self.task else { fatalError() } + easyHandle.timeoutTimer = _TimeoutSource(queue: task.workQueue, milliseconds: Int(request.timeoutInterval) * 1000, handler: timeoutHandler) + + easyHandle.set(automaticBodyDecompression: true) + } + +} + + +/// Response processing +internal extension _FTPURLProtocol { + /// Whenever we receive a response (i.e. a complete header) from libcurl, + /// this method gets called. + 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 session = task?.session as? URLSession else { fatalError() } + switch session.behaviour(for: self.task!) { + case .noDelegate: + break + case .taskDelegate(_): + //TODO: There's a problem with libcurl / with how we're using it. + // We're currently unable to pause the transfer / the easy handle: + // https://curl.haxx.se/mail/lib-2016-03/0222.html + // + // For now, we'll notify the delegate, but won't pause the transfer, + // and we'll disregard the completion handler: + + self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + + case .dataCompletionHandler: + break + case .downloadCompletionHandler: + break + } + } + +} + + + + + diff --git a/Foundation/URLSession/http/HTTPBodySource.swift b/Foundation/URLSession/http/HTTPBodySource.swift index a72a28c34e..c597d212c3 100644 --- a/Foundation/URLSession/http/HTTPBodySource.swift +++ b/Foundation/URLSession/http/HTTPBodySource.swift @@ -40,15 +40,15 @@ internal func splitData(dispatchData data: DispatchData, atPosition position: In } /// A (non-blocking) source for HTTP body data. -internal protocol _HTTPBodySource: class { +internal protocol _BodySource: class { /// Get the next chunck of data. /// /// - Returns: `.data` until the source is exhausted, at which point it will /// return `.done`. Since this is non-blocking, it will return `.retryLater` /// if no data is available at this point, but will be available later. - func getNextChunk(withLength length: Int) -> _HTTPBodySourceDataChunk + func getNextChunk(withLength length: Int) -> _BodySourceDataChunk } -internal enum _HTTPBodySourceDataChunk { +internal enum _BodySourceDataChunk { case data(DispatchData) /// The source is depleted. case done @@ -58,25 +58,25 @@ internal enum _HTTPBodySourceDataChunk { } /// A HTTP body data source backed by `dispatch_data_t`. -internal final class _HTTPBodyDataSource { - var data: 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) @@ -98,10 +98,10 @@ extension _HTTPBodyDataSource : _HTTPBodySource { /// - 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 @@ -127,13 +127,14 @@ internal final class _HTTPBodyFileSource { } guard let channel = DispatchIO(type: .stream, path: fileSystemRepresentation, oflag: O_RDONLY, mode: 0, queue: workQueue, - cleanupHandler: {_ in }) else { - fatalError("Cant create DispatchIO channel") + cleanupHandler: {_ in }) + else { + fatalError("Cant create DispatchIO channel") } self.channel = channel self.channel.setLimit(highWater: CFURLSessionMaxWriteSize) } - + fileprivate enum _Chunk { /// Nothing has been read, yet case empty @@ -146,7 +147,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. /// @@ -182,7 +183,7 @@ fileprivate extension _HTTPBodyFileSource { } } } - + fileprivate func append(data: DispatchData, endOfFile: Bool) { switch availableChunk { case .empty: @@ -196,7 +197,7 @@ fileprivate extension _HTTPBodyFileSource { fatalError("Trying to append data, but end-of-file was already detected.") } } - + fileprivate var availableByteCount: Int { switch availableChunk { case .empty: return 0 @@ -208,8 +209,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() @@ -242,3 +243,4 @@ extension _HTTPBodyFileSource : _HTTPBodySource { } } } + diff --git a/Foundation/URLSession/http/HTTPMessage.swift b/Foundation/URLSession/http/HTTPMessage.swift index a45a8cfae4..60747e9fff 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. /// @@ -230,10 +153,10 @@ private extension String { let methodRange = scalars.startIndex..) -> [_HTTPURLProtocol._HTTPMessage._Header]? { - + var headerLines = Array(lines) var headers: [_HTTPURLProtocol._HTTPMessage._Header] = [] while !headerLines.isEmpty { @@ -367,3 +290,5 @@ private extension UnicodeScalar { return !_HTTPCharacters.Separators.characterIsMember(UInt16(self.value)) } } + + diff --git a/Foundation/URLSession/http/HTTPURLProtocol.swift b/Foundation/URLSession/http/HTTPURLProtocol.swift index 78aff84b68..57253b334b 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,65 +42,51 @@ 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) - } - } - didSet { - if !oldValue.isEasyHandleAddedToMultiHandle && internalState.isEasyHandleAddedToMultiHandle { - task?.session.add(handle: easyHandle) - } - if oldValue.isEasyHandlePaused && !internalState.isEasyHandlePaused { - fatalError("Need to solve pausing receive.") + + 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.") } + 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 defaul // values. - + //TODO: We could add a strong reference from the easy handle back to // its URLSessionTask by means of CURLOPT_PRIVATE -- that would ensure // that the task is always around while the handle is running. // We would have to break that retain cycle once the handle completes // its transfer. - + // Behavior Options easyHandle.set(verboseModeOn: enableLibcurlDebugOutput) easyHandle.set(debugOutputOn: enableLibcurlDebugOutput, task: task!) easyHandle.set(passHeadersToDataStream: false) easyHandle.set(progressMeterOff: true) easyHandle.set(skipAllSignalHandling: true) - + // Error Options: easyHandle.set(errorBuffer: nil) easyHandle.set(failOnHTTPErrorCode: false) - + // Network Options: guard let url = request.url else { fatalError("No URL in request.") } easyHandle.set(url: url) @@ -137,22 +111,22 @@ fileprivate extension _HTTPURLProtocol { failWith(errorCode: errorCode(fileSystemError: e), request: request) return } - + // HTTP Options: easyHandle.set(followLocation: false) - + // The httpAdditionalHeaders from session configuration has to be added to the request. // The request.allHTTPHeaders can override the httpAdditionalHeaders elements. Add the // httpAdditionalHeaders from session configuration first and then append/update the // request.allHTTPHeaders so that request.allHTTPHeaders can override httpAdditionalHeaders. - + let httpSession = self.task?.session as! URLSession var httpHeaders: [AnyHashable : Any]? - + if let hh = httpSession.configuration.httpAdditionalHeaders { httpHeaders = hh } - + if let hh = self.task?.originalRequest?.allHTTPHeaderFields { if httpHeaders == nil { httpHeaders = hh @@ -169,18 +143,18 @@ 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 @@ -189,16 +163,78 @@ fileprivate extension _HTTPURLProtocol { 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: + let httpURLResponse = response as! 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(transferSate: _NativeProtocol._TransferState) -> URLResponse? { + if !transferSate.isHeaderComplete { + return HTTPURLResponse(url: transferSate.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 @@ -216,7 +252,7 @@ fileprivate extension _HTTPURLProtocol { let name = $0.0.lowercased() guard !names.contains(name) else { return } names.insert(name) - + if $0.1.isEmpty { result.append($0.0 + ";") } else { @@ -267,31 +303,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)" @@ -305,12 +316,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) { @@ -327,19 +332,7 @@ internal extension _HTTPURLProtocol { case file(URL) case stream(InputStream) } - - func failWith(errorCode: Int, request: URLRequest) { - //TODO: Error handling - let userInfo: [String : Any]? = request.url.map { - [ - NSURLErrorFailingURLErrorKey: $0, - NSURLErrorFailingURLStringErrorKey: $0.absoluteString, - ] - } - let error = URLError(_nsError: NSError(domain: NSURLErrorDomain, code: errorCode, userInfo: userInfo)) - completeTask(withError: error) - self.client?.urlProtocol(self, didFailWithError: error) - } + } fileprivate extension _HTTPURLProtocol._Body { @@ -365,392 +358,9 @@ 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(withErrorCode errorCode: Int?) { - // At this point the transfer is complete and we can decide what to do. - // If everything went well, we will simply forward the resulting data - // to the delegate. But in case of redirects etc. we might send another - // request. - guard case .transferInProgress(let ts) = internalState else { fatalError("Transfer completed, but it wasn't in progress.") } - guard let request = task?.currentRequest else { fatalError("Transfer completed, but there's no current request.") } - guard errorCode == nil else { - internalState = .transferFailed - failWith(errorCode: errorCode!, request: request) - return - } - - if let response = task?.response as? HTTPURLResponse { - var transferState = ts - transferState.response = response - } - - guard let response = ts.response else { fatalError("Transfer completed, but there's no response.") } - internalState = .transferCompleted(response: response, bodyDataDrain: ts.bodyDataDrain) - let action = completionAction(forCompletedRequest: request, response: response) - - switch action { - case .completeTask: - completeTask() - case .failWithError(let errorCode): - internalState = .transferFailed - failWith(errorCode: errorCode, request: request) - case .redirectWithRequest(let newRequest): - redirectFor(request: newRequest) - } - } - - func seekInputStream(to position: UInt64) throws { - // We will reset the body sourse and seek forward. - NSUnimplemented() - } - - func updateProgressMeter(with propgress: _EasyHandle._Progress) { - //TODO: Update progress. Note that a single URLSessionTask might - // perform multiple transfers. The values in `progress` are only for - // the current transfer. - } -} - -extension _HTTPURLProtocol { - /// The is independent of the public `state: URLSessionTask.State`. - enum _InternalState { - /// Task has been created, but nothing has been done, yet - case initial - /// The easy handle has been fully configured. But it is not added to - /// the multi handle. - case transferReady(_HTTPTransferState) - /// The easy handle is currently added to the multi handle - case transferInProgress(_HTTPTransferState) - /// The transfer completed. - /// - /// The easy handle has been removed from the multi handle. This does - /// not (necessarily mean the task completed. A task that gets - /// redirected will do multiple transfers. - case transferCompleted(response: URLResponse, bodyDataDrain: _DataDrain) - /// The transfer failed. - /// - /// Same as `.transferCompleted`, but without response / body data - case transferFailed - /// Waiting for the completion handler of the HTTP redirect callback. - /// - /// When we tell the delegate that we're about to perform an HTTP - /// redirect, we need to wait for the delegate to let us know what - /// action to take. - case waitingForRedirectCompletionHandler(response: URLResponse, bodyDataDrain: _DataDrain) - /// Waiting for the completion handler of the 'did receive response' callback. - /// - /// When we tell the delegate that we received a response (i.e. when - /// we received a complete header), we need to wait for the delegate to - /// let us know what action to take. In this state the easy handle is - /// paused in order to suspend delegate callbacks. - case waitingForResponseCompletionHandler(_HTTPTransferState) - /// The task is completed - /// - /// Contrast this with `.transferCompleted`. - case taskCompleted - } -} - -extension _HTTPURLProtocol._InternalState { - var isEasyHandleAddedToMultiHandle: Bool { - switch self { - case .initial: return false - case .transferReady: return false - case .transferInProgress: return true - case .transferCompleted: return false - case .transferFailed: return false - case .waitingForRedirectCompletionHandler: return false - case .waitingForResponseCompletionHandler: return true - case .taskCompleted: return false - } - } - var isEasyHandlePaused: Bool { - switch self { - case .initial: return false - case .transferReady: return false - case .transferInProgress: return false - case .transferCompleted: return false - case .transferFailed: return false - case .waitingForRedirectCompletionHandler: return false - case .waitingForResponseCompletionHandler: return true - case .taskCompleted: return false - } - } -} - -internal extension _HTTPURLProtocol { - /// Start a new transfer - func startNewTransfer(with request: URLRequest) { - guard let t = task else { fatalError() } - t.currentRequest = request - guard let url = request.url else { fatalError("No URL in request.") } - - self.internalState = .transferReady(createTransferState(url: url, workQueue: t.workQueue)) - configureEasyHandle(for: request) - if (t.suspendCount) < 1 { - resume() - } - } - - func resume() { - if case .initial = self.internalState { - guard let r = task?.originalRequest else { fatalError("Task has no original request.") } - startNewTransfer(with: r) - } - - if case .transferReady(let transferState) = self.internalState { - self.internalState = .transferInProgress(transferState) - } - } - - func suspend() { - if case .transferInProgress(let transferState) = self.internalState { - self.internalState = .transferReady(transferState) - } - } -} - /// State Transfers extension _HTTPURLProtocol { - func completeTask() { - guard case .transferCompleted(response: let response, bodyDataDrain: let bodyDataDrain) = self.internalState else { - fatalError("Trying to complete the task, but its transfer isn't complete.") - } - task?.response = response - - //We don't want a timeout to be triggered after this. The timeout timer needs to be cancelled. - easyHandle.timeoutTimer = nil - - //because we deregister the task with the session on internalState being set to taskCompleted - //we need to do the latter after the delegate/handler was notified/invoked - if case .inMemory(let bodyData) = bodyDataDrain { - var data = Data() - if let body = bodyData { - data = Data(bytes: body.bytes, count: body.length) - } - self.client?.urlProtocol(self, didLoad: data) - self.internalState = .taskCompleted - } - - 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?") @@ -774,7 +384,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: @@ -798,67 +408,9 @@ 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 @@ -874,7 +426,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? @@ -891,17 +443,17 @@ 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 - + var components = URLComponents() components.scheme = scheme components.host = host @@ -910,7 +462,7 @@ internal extension _HTTPURLProtocol { request.url = URL(string: urlString) let timeSpent = easyHandle.getTimeoutIntervalSpent() request.timeoutInterval = fromRequest.timeoutInterval - timeSpent - return request + return request } } @@ -921,12 +473,14 @@ 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 } return nil } } + 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/http/EasyHandle.swift b/Foundation/URLSession/libcurl/EasyHandle.swift similarity index 100% rename from Foundation/URLSession/http/EasyHandle.swift rename to Foundation/URLSession/libcurl/EasyHandle.swift diff --git a/Foundation/URLSession/http/MultiHandle.swift b/Foundation/URLSession/libcurl/MultiHandle.swift similarity index 100% rename from Foundation/URLSession/http/MultiHandle.swift rename to Foundation/URLSession/libcurl/MultiHandle.swift 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/TestFoundation/FTPServer.swift b/TestFoundation/FTPServer.swift new file mode 100644 index 0000000000..baef478b43 --- /dev/null +++ b/TestFoundation/FTPServer.swift @@ -0,0 +1,284 @@ +// 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 +// + + +//This is a very rudimentary FTP server written plainly for testing URLSession FTP Implementation. +import Dispatch + +#if DEPLOYMENT_RUNTIME_OBJC || os(Linux) + import Foundation + import Glibc + import XCTest +#else + import CoreFoundation + import SwiftFoundation + import Darwin + import SwiftXCTest +#endif + + +class _FTPSocket { + + private var listenSocket: Int32! + private var socketAddress = UnsafeMutablePointer.allocate(capacity: 1) + private var socketAddress1 = UnsafeMutablePointer.allocate(capacity: 1) + private var connectionSocket: Int32! + var dataSocket: Int32! // data socket for communication + var dataSocketPort: UInt16! // data socket port ,should be sent as part of header + private func isNotNegative(r: CInt) -> Bool { + return r != -1 + } + + private func isZero(r: CInt) -> Bool { + return r == 0 + } + + private func attempt(_ name: String, file: String = #file, line: UInt = #line, valid: (CInt) -> Bool, _ b: @autoclosure () -> CInt) throws -> CInt { + let r = b() + guard valid(r) else { throw ServerError(operation: name, errno: r, file: file, line: line) } + return r + } + + init(port: UInt16) throws { + #if os(Linux) + let SOCKSTREAM = Int32(SOCK_STREAM.rawValue) + #else + let SOCKSTREAM = SOCK_STREAM + #endif + listenSocket = try attempt("socket", valid: isNotNegative, socket(AF_INET, SOCKSTREAM, Int32(IPPROTO_TCP))) + var on: Int = 1 + _ = try attempt("setsockopt", valid: isZero, setsockopt(listenSocket, SOL_SOCKET, SO_REUSEADDR, &on, socklen_t(MemoryLayout.size))) + let sa = createSockaddr(port) + socketAddress.initialize(to: sa) + try socketAddress.withMemoryRebound(to: sockaddr.self, capacity: MemoryLayout.size, { + let addr = UnsafePointer($0) + _ = try attempt("bind", valid: isZero, bind(listenSocket, addr, socklen_t(MemoryLayout.size))) + }) + + dataSocket = try attempt("socket", valid: isNotNegative, socket(AF_INET, SOCKSTREAM, Int32(IPPROTO_TCP))) + var on1: Int = 1 + _ = try attempt("setsockopt", valid: isZero, setsockopt(dataSocket, SOL_SOCKET, SO_REUSEADDR, &on1, socklen_t(MemoryLayout.size))) + let sa1 = createSockaddr(port+1) + socketAddress1.initialize(to: sa1) + try socketAddress1.withMemoryRebound(to: sockaddr.self, capacity: MemoryLayout.size, { + let addr = UnsafeMutablePointer($0) + _ = try attempt("bind", valid: isZero, bind(dataSocket, addr, socklen_t(MemoryLayout.size))) + var sockLen = socklen_t(MemoryLayout.size) + _ = try attempt("listen", valid: isZero, listen(dataSocket, SOMAXCONN)) + //open the data port asynchronously.Port should be opened before ESPV header communication. + DispatchQueue(label: "delay").async { + do { + self.dataSocket = try self.attempt("accept", valid: self.isNotNegative, accept(self.dataSocket, addr, &sockLen)) + self.dataSocketPort = sa1.sin_port + } catch { + } + } + + }) + } + + + + private func createSockaddr(_ port: UInt16) -> sockaddr_in { + // Listen on the loopback address so that OSX doesnt pop up a dialog + // asking to accept incoming connections if the firewall is enabled. + let addr = UInt32(INADDR_LOOPBACK).bigEndian + #if os(Linux) + return sockaddr_in(sin_family: sa_family_t(AF_INET), sin_port: htons(port), sin_addr: in_addr(s_addr: addr), sin_zero: (0,0,0,0,0,0,0,0)) + #else + return sockaddr_in(sin_len: 0, sin_family: sa_family_t(AF_INET), sin_port: CFSwapInt16HostToBig(port), sin_addr: in_addr(s_addr: addr), sin_zero: (0,0,0,0,0,0,0,0)) + #endif + } + + func acceptConnection(notify: ServerSemaphore) throws { + _ = try attempt("listen", valid: isZero, listen(listenSocket, SOMAXCONN)) + try socketAddress.withMemoryRebound(to: sockaddr.self, capacity: MemoryLayout.size, { + let addr = UnsafeMutablePointer($0) + var sockLen = socklen_t(MemoryLayout.size) + notify.signal() + connectionSocket = try attempt("accept", valid: isNotNegative, accept(listenSocket, addr, &sockLen)) + }) + } + + func readData() throws -> String { + var buffer = [UInt8](repeating: 0, count: 4096) + _ = try attempt("read", valid: isNotNegative, CInt(read(connectionSocket, &buffer, 4096))) + return String(cString: &buffer) + } + + func readDataOnDataSocket() throws -> String { + var buffer = [UInt8](repeating: 0, count: 4096) + _ = try attempt("read", valid: isNotNegative, CInt(read(dataSocket, &buffer, 4096))) + return String(cString: &buffer) + } + + func writeRawData(_ data: Data) throws { + _ = try data.withUnsafeBytes { ptr in + try attempt("write", valid: isNotNegative, CInt(write(connectionSocket, ptr, data.count))) + } + + } + + func writeRawData(socket data: Data) throws -> Int32{ + var bytesWritten: Int32 = 0 + _ = try data.withUnsafeBytes { ptr in + bytesWritten = try attempt("write", valid: isNotNegative, CInt(write(dataSocket, ptr, data.count))) + } + return bytesWritten + } + + func shutdown() { + close(connectionSocket) + close(listenSocket) + close(dataSocket) + } +} + +class _FTPServer { + + let socket: _FTPSocket + let commandPort: UInt16 + + init(port: UInt16) throws { + commandPort = port + socket = try _FTPSocket(port: port) + } + + public class func create(port: UInt16) throws -> _FTPServer { + return try _FTPServer(port: port) + } + + public func listen(notify: ServerSemaphore) throws { + try socket.acceptConnection(notify: notify) + } + + public func stop() { + socket.shutdown() + } + + // parse header information and respond accordingly + + public func parseHeaderData() throws { + let saveData = "FTP Implementation to test FTP download and data tasks.Instead of sending file, we are sending the hardcoded data.We are going to test FTP data,Download aand upload tasks with delegates & completion handlers.Creating the data here as we need to pass the count as part of the header.\r\n ".data(using: String.Encoding.utf8) + let dataCount = saveData?.count + let read = try socket.readData() + if(read.contains("anonymous")) { + try respondWithRawData(with: "331 Please specify the password.\r\n") + } else if(read.contains("PASS")) { + try respondWithRawData(with: "230 Login successful.\r\n") + } else if( read.contains("PWD")) { + try respondWithRawData(with: "257 \"/\"\r\n") + } else if(read.contains("EPSV")) { + try respondWithRawData(with: "229 Entering Extended Passive Mode (|||\(commandPort+1)|).\r\n") + } else if(read.contains("TYPE I")) { + try respondWithRawData(with: "200 Switching to Binary mode.\r\n") + } else if(read.contains("SIZE")) { + try respondWithRawData(with: "213 \(dataCount!)\r\n") + } else if (read.contains("RETR")) { + try respondWithRawData(with: "150 Opening BINARY mode data connection for test.txt (\(dataCount!) bytes).\r\n") + //send Data here through Data Socket + do { + let dataWritten = try respondWithData(with: saveData!) + if dataWritten != -1 { + // send the end header in Command socket + try respondWithRawData(with: "226 Transfer complete.\r\n") + } + } catch { + print(error) + } + } else if(read.contains("STOR")) { + /* + Request is for Upload.As we are only dealing with data,Just read data and ignore + */ + try respondWithRawData(with: "150 Ok to send data.\r\n") + //read data from the data socket and respond with completion header once you are done + do { + let readData = try readDataOnDataSocket() + try respondWithRawData(with: "226 Transfer complete.\r\n") + } catch { + } + } + } + + public func respondWithRawData(with string: String) throws { + try self.socket.writeRawData(string.data(using: String.Encoding.utf8)!) + } + + public func respondWithData(with data: Data) throws -> Int32 { + return try self.socket.writeRawData(socket: data) + } + public func readDataOnDataSocket() throws -> String { + return try self.socket.readDataOnDataSocket() + } +} + + +public class TestFTPURLSessionServer { + let ftpServer: _FTPServer + + public init (port: UInt16) throws { + ftpServer = try _FTPServer.create(port: port) + } + public func start(started: ServerSemaphore) throws { + started.signal() + try ftpServer.listen(notify: started) + } + public func parseHeaderAndRespond() throws{ + try ftpServer.parseHeaderData() + } + + func writeStartHeaderData() throws{ + try ftpServer.respondWithRawData(with: "220 (vsFTPd 2.3.5)\r\n") + } + + func stop() { + ftpServer.stop() + } +} + +class LoopbackFTPServerTest : XCTestCase { + static var serverPort: Int = -1 + + override class func setUp() { + super.setUp() + func runServer(with condition: ServerSemaphore, startDelay: TimeInterval? = nil, sendDelay: TimeInterval? = nil, bodyChunks: Int? = nil) throws { + let start = 21961 //21 21961 + for port in start...(start+100) { //we must find at least one port to bind + do { + serverPort = port + let test = try TestFTPURLSessionServer(port: UInt16(port)) + try test.start(started: condition) + try test.writeStartHeaderData() // Welcome message to start the transfer + for _ in 1...7 { + try test.parseHeaderAndRespond() + } + test.stop() + } catch let e as ServerError { + if e.operation == "bind" { continue } + throw e + } + } + } + + let serverReady = ServerSemaphore() + globalDispatchQueue.async { + do { + try runServer(with: serverReady) + + } catch { + XCTAssertTrue(true) + return + } + } + serverReady.wait() + } +} + + + diff --git a/TestFoundation/TestFTP.swift b/TestFoundation/TestFTP.swift new file mode 100644 index 0000000000..e4c1a30e18 --- /dev/null +++ b/TestFoundation/TestFTP.swift @@ -0,0 +1,110 @@ +// 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 +// + +#if DEPLOYMENT_RUNTIME_OBJC || os(Linux) + import Foundation + import XCTest +#else + import SwiftFoundation + import SwiftXCTest +#endif + +class TestFTP : LoopbackFTPServerTest { + + static var allTests: [(String, (TestFTP) -> () throws -> Void)] { + return [ + + ("test_ftpdatatask", test_ftpdatatask), + ("test_ftpdownloadtask", test_ftpdownloadtask), + ("test_ftpdatataskDelegate", test_ftpdatataskDelegate), + ("test_ftpdownloadtaskDelegate", test_ftpdownloadtaskDelegate), + ("test_ftpuploadtask", test_ftpuploadtask), + //("test_simpleFTPUploadWithDelegate", test_simpleFTPUploadWithDelegate), + ] + } + + let saveData = "FTP Implementation to test FTP Upload,download and data tasks.Instead of sending file, we are sending the hardcoded data.We are going to test FTP data,Download aand upload tasks with delegates & completion handlers.Creating the data here as we need to pass the count as part of the header.\r\n ".data(using: String.Encoding.utf8) + + func test_ftpdatatask() { + let ftpURL = "ftp://127.0.0.1:\(TestFTP.serverPort)/test.txt" + let req = URLRequest(url: URL(string: ftpURL)!) + let configuration = URLSessionConfiguration.default + let expect = expectation(description: "URL test with custom protocol") + let sesh = URLSession(configuration: configuration) + let dataTask1 = sesh.dataTask(with: req, completionHandler: { data, res, error in + XCTAssertNil(error) + defer { expect.fulfill() } + + }) + dataTask1.resume() + waitForExpectations(timeout: 60) + } + func test_ftpdownloadtask() { + let ftpURL = "ftp://127.0.0.1:\(TestFTP.serverPort)/test.txt" + let req = URLRequest(url: URL(string: ftpURL)!) + let configuration = URLSessionConfiguration.default + configuration.timeoutIntervalForRequest = 10 //check this behavior on Darwin + let expect = expectation(description: "URL test with custom protocol") + let sesh = URLSession(configuration: configuration) + let dataTask1 = sesh.downloadTask(with: req, completionHandler: { url, res, error in + XCTAssertNil(error) + defer { expect.fulfill() } + + }) + dataTask1.resume() + waitForExpectations(timeout: 60) + } + + func test_ftpdatataskDelegate() { + let urlString = "ftp://127.0.0.1:\(TestFTP.serverPort)/test.txt" + let url = URL(string: urlString)! + let d = FTPDataTask(with: expectation(description: "data task")) + d.run(with: url) + waitForExpectations(timeout: 60) + if !d.error { + XCTAssertNotNil(d.fileData) + } + } + + func test_ftpdownloadtaskDelegate() { + let urlString = "ftp://127.0.0.1:\(TestFTP.serverPort)/test.txt" + let url = URL(string: urlString)! + let d = DownloadTask(with: expectation(description: "data task")) + d.run(with: url) + waitForExpectations(timeout: 60) + } + + func test_ftpuploadtask() { + let ftpURL = "ftp://127.0.0.1:\(TestFTP.serverPort)/test.txt" + let req = URLRequest(url: URL(string: ftpURL)!) + let configuration = URLSessionConfiguration.default + let expect = expectation(description: "URL test with custom protocol") + let sesh = URLSession(configuration: configuration) + let uploadTask = sesh.uploadTask(with: req, from: saveData, completionHandler: { data, res, error in + XCTAssertNil(error) + defer { expect.fulfill() } + + }) + uploadTask.resume() + waitForExpectations(timeout: 60) + } + + func test_simpleFTPUploadWithDelegate() { + let delegate = FTPUploadDelegate() + let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil) + let ftpURL = "ftp://127.0.0.1:\(TestFTP.serverPort)/test.txt" + let req = URLRequest(url: URL(string: ftpURL)!) + delegate.uploadCompletedExpectation = expectation(description: "PUT \(ftpURL): Upload data") + let task = session.uploadTask(with: req, from: saveData!) + task.resume() + waitForExpectations(timeout: 20) + } +} + + diff --git a/TestFoundation/TestURLSession.swift b/TestFoundation/TestURLSession.swift index c941f1c9a7..8e4082e6b3 100644 --- a/TestFoundation/TestURLSession.swift +++ b/TestFoundation/TestURLSession.swift @@ -492,9 +492,9 @@ class TestURLSession : LoopbackServerTest { } } } - } + class SharedDelegate: NSObject { var dataCompletionExpectation: XCTestExpectation! } @@ -503,12 +503,15 @@ extension SharedDelegate: URLSessionDataDelegate { func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { dataCompletionExpectation.fulfill() } + } extension SharedDelegate: URLSessionDownloadDelegate { func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { } } + + class SessionDelegate: NSObject, URLSessionDelegate { @@ -696,6 +699,67 @@ extension DownloadTask : URLSessionTaskDelegate { } } +class FTPDataTask : NSObject { + let dataTaskExpectation: XCTestExpectation! + var fileData: NSMutableData = NSMutableData() + var session: URLSession! = nil + var task: URLSessionDataTask! = nil + var cancelExpectation: XCTestExpectation? + var responseReceivedExpectation: XCTestExpectation? + var hasTransferCompleted = false + public var error = false + + init(with expectation: XCTestExpectation) { + dataTaskExpectation = expectation + } + + func run(with request: URLRequest) { + let config = URLSessionConfiguration.default + config.timeoutIntervalForRequest = 8 + session = URLSession(configuration: config, delegate: self, delegateQueue: nil) + task = session.dataTask(with: request) + task.resume() + } + + func run(with url: URL) { + let config = URLSessionConfiguration.default + config.timeoutIntervalForRequest = 8 + session = URLSession(configuration: config, delegate: self, delegateQueue: nil) + task = session.dataTask(with: url) + task.resume() + } + + func cancel() { + task.cancel() + } +} + +extension FTPDataTask : URLSessionDataDelegate { + public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + fileData.append(data) + responseReceivedExpectation?.fulfill() + } + + public func urlSession(_ session: URLSession, + dataTask: URLSessionDataTask, + didReceive response: URLResponse, + completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { + guard responseReceivedExpectation != nil else { return } + responseReceivedExpectation!.fulfill() + } +} + +extension FTPDataTask : URLSessionTaskDelegate { + public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + dataTaskExpectation.fulfill() + guard (error as? URLError) != nil else { return } + if let cancellation = cancelExpectation { + cancellation.fulfill() + } + self.error = true + } +} + class HTTPRedirectionDataTask : NSObject { let dataTaskExpectation: XCTestExpectation! var session: URLSession! = nil @@ -771,3 +835,22 @@ extension HTTPUploadDelegate: URLSessionDataDelegate { uploadCompletedExpectation.fulfill() } } + +class FTPUploadDelegate: NSObject { + var uploadCompletedExpectation: XCTestExpectation! + var totalBytesSent: Int64 = 0 +} + +extension FTPUploadDelegate: URLSessionTaskDelegate { + func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { + self.totalBytesSent = totalBytesSent + } +} + +extension FTPUploadDelegate: URLSessionDataDelegate { + func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + XCTAssertEqual(self.totalBytesSent, 285) + uploadCompletedExpectation.fulfill() + } +} + diff --git a/TestFoundation/main.swift b/TestFoundation/main.swift index 9ae6d0f8fc..32c88371dc 100644 --- a/TestFoundation/main.swift +++ b/TestFoundation/main.swift @@ -105,4 +105,5 @@ XCTMain([ testCase(TestJSONEncoder.allTests), testCase(TestCodable.allTests), testCase(TestUnit.allTests), + testCase(TestFTP.allTests), ]) diff --git a/build.py b/build.py index 00d8588852..567a66933f 100755 --- a/build.py +++ b/build.py @@ -423,18 +423,19 @@ 'Foundation/NSURLRequest.swift', 'Foundation/URLResponse.swift', 'Foundation/URLSession/Configuration.swift', - 'Foundation/URLSession/http/EasyHandle.swift', + 'Foundation/URLSession/libcurl/EasyHandle.swift', + 'Foundation/URLSession/libcurl/libcurlHelpers.swift', + 'Foundation/URLSession/libcurl/MultiHandle.swift', + 'Foundation/URLSession/NativeProtocol.swift', 'Foundation/URLSession/http/HTTPBodySource.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/URLSession/ftp/FTPURLProtocol.swift', 'Foundation/UserDefaults.swift', 'Foundation/NSUUID.swift', 'Foundation/NSValue.swift', @@ -510,6 +511,7 @@ foundation_tests = SwiftExecutable('TestFoundation', [ 'TestFoundation/main.swift', 'TestFoundation/HTTPServer.swift', + 'TestFoundation/FTPServer.swift', 'Foundation/ProgressFraction.swift', ] + glob.glob('./TestFoundation/Test*.swift')) # all TestSomething.swift are considered sources to the test project in the TestFoundation directory