From b6e298aa58df1945de52f44e4f1f312140a0d782 Mon Sep 17 00:00:00 2001 From: saiHemak Date: Fri, 26 Apr 2019 12:08:36 +0530 Subject: [PATCH] Initial implementation of FTP Protocol --- CMakeLists.txt | 3 + Foundation.xcodeproj/project.pbxproj | 20 ++ Foundation/URLSession/Message.swift | 8 +- Foundation/URLSession/TransferState.swift | 65 +++- Foundation/URLSession/URLSession.swift | 5 +- .../URLSession/URLSessionConfiguration.swift | 2 +- .../URLSession/ftp/FTPURLProtocol.swift | 110 +++++++ .../URLSession/http/HTTPURLProtocol.swift | 2 +- TestFoundation/FTPServer.swift | 285 ++++++++++++++++++ TestFoundation/TestURLSessionFTP.swift | 123 ++++++++ TestFoundation/main.swift | 1 + build.py | 2 + 12 files changed, 614 insertions(+), 12 deletions(-) create mode 100644 Foundation/URLSession/ftp/FTPURLProtocol.swift create mode 100644 TestFoundation/FTPServer.swift create mode 100644 TestFoundation/TestURLSessionFTP.swift diff --git a/CMakeLists.txt b/CMakeLists.txt index 16c834cf66..77ce4e83ab 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -298,6 +298,7 @@ add_swift_library(Foundation Foundation/URLSession/libcurl/MultiHandle.swift Foundation/URLSession/Message.swift Foundation/URLSession/NativeProtocol.swift + Foundation/URLSession/ftp/FTPURLProtocol.swift Foundation/URLSession/TaskRegistry.swift Foundation/URLSession/TransferState.swift Foundation/URLSession/URLSession.swift @@ -421,6 +422,7 @@ if(ENABLE_TESTING) SOURCES TestFoundation/main.swift TestFoundation/HTTPServer.swift + TestFoundation/FTPServer.swift Foundation/ProgressFraction.swift TestFoundation/Utilities.swift TestFoundation/FixtureValues.swift @@ -507,6 +509,7 @@ if(ENABLE_TESTING) TestFoundation/TestURLRequest.swift TestFoundation/TestURLResponse.swift TestFoundation/TestURLSession.swift + TestFoundation/TestURLSessionFTP.swift TestFoundation/TestURL.swift TestFoundation/TestUserDefaults.swift TestFoundation/TestUtils.swift diff --git a/Foundation.xcodeproj/project.pbxproj b/Foundation.xcodeproj/project.pbxproj index 49a3e71312..2fbdddaba0 100644 --- a/Foundation.xcodeproj/project.pbxproj +++ b/Foundation.xcodeproj/project.pbxproj @@ -341,6 +341,9 @@ 5BF9B8021FABD5DA00EE1A7C /* CFBundle_Tables.c in Sources */ = {isa = PBXBuildFile; fileRef = 5BF9B7F71FABD5D400EE1A7C /* CFBundle_Tables.c */; }; 5FE52C951D147D1C00F7D270 /* TestNSTextCheckingResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FE52C941D147D1C00F7D270 /* TestNSTextCheckingResult.swift */; }; 6105D30F1FEBC5FC0022865A /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6105D30E1FEBC5FC0022865A /* Message.swift */; }; + 616068ED225C82C5004FCC54 /* FTPURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616068EC225C82C5004FCC54 /* FTPURLProtocol.swift */; }; + 616068F3225DE5C2004FCC54 /* FTPServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616068F2225DE5C2004FCC54 /* FTPServer.swift */; }; + 616068F5225DE606004FCC54 /* TestURLSessionFTP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616068F4225DE606004FCC54 /* TestURLSessionFTP.swift */; }; 61D2F9AF1FECFB3E0033306A /* NativeProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D2F9AE1FECFB3E0033306A /* NativeProtocol.swift */; }; 61E0117D1C1B5590000037DD /* RunLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = EADE0B761BD15DFF00C49C64 /* RunLoop.swift */; }; 61E0117E1C1B55B9000037DD /* Timer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BDC3F481BCC5DCB00ED97BB /* Timer.swift */; }; @@ -871,6 +874,9 @@ 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 = ""; }; 6105D30E1FEBC5FC0022865A /* Message.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = ""; }; + 616068EC225C82C5004FCC54 /* FTPURLProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTPURLProtocol.swift; sourceTree = ""; }; + 616068F2225DE5C2004FCC54 /* FTPServer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTPServer.swift; sourceTree = ""; }; + 616068F4225DE606004FCC54 /* TestURLSessionFTP.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestURLSessionFTP.swift; sourceTree = ""; }; 61A395F91C2484490029B337 /* TestNSLocale.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSLocale.swift; sourceTree = ""; }; 61D2F9AE1FECFB3E0033306A /* NativeProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeProtocol.swift; sourceTree = ""; }; 61D6C9EE1C1DFE9500DEF583 /* TestTimer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestTimer.swift; sourceTree = ""; }; @@ -1116,6 +1122,7 @@ 5B1FD9C71D6D162D0080E83C /* Session */ = { isa = PBXGroup; children = ( + 616068EB225C82C5004FCC54 /* ftp */, 614732781FC2DEB7005B5E61 /* libcurl */, E4F889331E9CF04D008A70EB /* http */, 5B1FD9C81D6D16580080E83C /* Configuration.swift */, @@ -1516,6 +1523,14 @@ path = libcurl; sourceTree = ""; }; + 616068EB225C82C5004FCC54 /* ftp */ = { + isa = PBXGroup; + children = ( + 616068EC225C82C5004FCC54 /* FTPURLProtocol.swift */, + ); + path = ftp; + sourceTree = ""; + }; 9F4ADBCF1ECD4F56001F0B3D /* xdgTestHelper */ = { isa = PBXGroup; children = ( @@ -1547,6 +1562,7 @@ EA66F6371BF1619600136161 /* TestFoundation */ = { isa = PBXGroup; children = ( + 616068F2225DE5C2004FCC54 /* FTPServer.swift */, 1520469A1D8AEABE00D02E36 /* HTTPServer.swift */, EA66F6381BF1619600136161 /* main.swift */, BB3D7557208A1E500085CFDC /* TestImports.swift */, @@ -1603,6 +1619,7 @@ BDBB658F1E256BFA001A7286 /* TestEnergyFormatter.swift */, D512D17B1CD883F00032E6A5 /* TestFileHandle.swift */, 525AECEB1BF2C96400D15BB0 /* TestFileManager.swift */, + 616068F4225DE606004FCC54 /* TestURLSessionFTP.swift */, 63DCE9D31EAA432400E9CB02 /* TestISO8601DateFormatter.swift */, BD8042151E09857800487EB8 /* TestLengthFormatter.swift */, A058C2011E529CF100B07AA1 /* TestMassFormatter.swift */, @@ -2455,6 +2472,7 @@ 5BF7AEB31BCD51F9008F214A /* NSObjCRuntime.swift in Sources */, 5BD31D3F1D5D19D600563814 /* Dictionary.swift in Sources */, B9974B9B1EDF4A22007F15B8 /* BodySource.swift in Sources */, + 616068ED225C82C5004FCC54 /* FTPURLProtocol.swift in Sources */, 5B94E8821C430DE70055C035 /* NSStringAPI.swift in Sources */, 5B0163BB1D024EB7003CCD96 /* DateComponents.swift in Sources */, 5BF7AEAB1BCD51F9008F214A /* NSDictionary.swift in Sources */, @@ -2678,6 +2696,7 @@ 5B13B34F1C582D4C00651CE2 /* TestXMLParser.swift in Sources */, BF85E9D81FBDCC2000A79793 /* TestHost.swift in Sources */, D5C40F331CDA1D460005690C /* TestOperationQueue.swift in Sources */, + 616068F3225DE5C2004FCC54 /* FTPServer.swift in Sources */, BDBB65901E256BFA001A7286 /* TestEnergyFormatter.swift in Sources */, 5B13B32F1C582D4C00651CE2 /* TestNSGeometry.swift in Sources */, 7D0DE86E211883F500540061 /* TestDateComponents.swift in Sources */, @@ -2705,6 +2724,7 @@ D4FE895B1D703D1100DA7986 /* TestURLRequest.swift in Sources */, 684C79011F62B611005BD73E /* TestNSNumberBridging.swift in Sources */, DAA79BD920D42C07004AF044 /* TestURLProtectionSpace.swift in Sources */, + 616068F5225DE606004FCC54 /* TestURLSessionFTP.swift in Sources */, B951B5EC1F4E2A2000D8B332 /* TestNSLock.swift in Sources */, 5B13B33A1C582D4C00651CE2 /* TestNSNumber.swift in Sources */, 5B13B3521C582D4C00651CE2 /* TestNSValue.swift in Sources */, diff --git a/Foundation/URLSession/Message.swift b/Foundation/URLSession/Message.swift index c569f412fa..e07bc9ee21 100644 --- a/Foundation/URLSession/Message.swift +++ b/Foundation/URLSession/Message.swift @@ -49,7 +49,7 @@ extension _NativeProtocol._ParsedResponseHeader { /// that ending. /// - Returns: Returning nil indicates failure. Otherwise returns a new /// `ParsedResponseHeader` with the given line added. - func byAppending(headerLine data: Data) -> _NativeProtocol._ParsedResponseHeader? { + func byAppending(headerLine data: Data, onHeaderCompleted: (String) -> Bool) -> _NativeProtocol._ParsedResponseHeader? { // The buffer must end in CRLF guard 2 <= data.count && data[data.endIndex - 2] == _Delimiters.CR && @@ -57,7 +57,7 @@ extension _NativeProtocol._ParsedResponseHeader { else { return nil } let lineBuffer = data.subdata(in: data.startIndex.. _NativeProtocol._ParsedResponseHeader { - if line.isEmpty { + private func _byAppending(headerLine line: String, onHeaderCompleted: (String) -> Bool) -> _NativeProtocol._ParsedResponseHeader { + if onHeaderCompleted(line) { switch self { case .partial(let header): return .complete(header) case .complete: return .partial(_NativeProtocol._ResponseHeaderLines()) diff --git a/Foundation/URLSession/TransferState.swift b/Foundation/URLSession/TransferState.swift index f4dc2a4566..ab450a09bf 100644 --- a/Foundation/URLSession/TransferState.swift +++ b/Foundation/URLSession/TransferState.swift @@ -83,8 +83,16 @@ extension _HTTPURLProtocol._TransferState { /// return value's `isHeaderComplete` will then by `true`. /// /// - Throws: When a parsing error occurs - func byAppending(headerLine data: Data) throws -> _NativeProtocol._TransferState { - guard let h = parsedResponseHeader.byAppending(headerLine: data) else { + func byAppendingHTTP(headerLine data: Data) throws -> _NativeProtocol._TransferState { + // If the line is empty, it marks the end of the header, and the result + // is a complete header. Otherwise it's a partial header. + // - Note: Appending a line to a complete header results in a partial + // header with just that line. + + func isCompleteHeader(_ headerLine: String) -> Bool { + return headerLine.isEmpty + } + guard let h = parsedResponseHeader.byAppending(headerLine: data, onHeaderCompleted: isCompleteHeader) else { throw _Error.parseSingleLineError } if case .complete(let lines) = h { @@ -93,9 +101,57 @@ extension _HTTPURLProtocol._TransferState { guard response != nil else { throw _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) + } + } +} + +// specific to FTP +extension _FTPURLProtocol._TransferState { + enum FTPHeaderCode: Int { + case transferCompleted = 226 + case openDataConnection = 150 + case fileStatus = 213 + case syntaxError = 500// 500 series FTP Syntax errors + case errorOccurred = 400 // 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 { + guard let line = String(data: data, encoding: String.Encoding.utf8) else { + fatalError("Data on command port is nil") + } + + //FTP Status code 226 marks the end of the transfer + if (line.starts(with: String(FTPHeaderCode.transferCompleted.rawValue))) { + return self + } + //FTP Status code 213 marks the end of the header and start of the + //transfer on data port + func isCompleteHeader(_ headerLine: String) -> Bool { + return headerLine.starts(with: String(FTPHeaderCode.openDataConnection.rawValue)) + } + guard let h = parsedResponseHeader.byAppending(headerLine: data, onHeaderCompleted: 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: h, response: nil, requestBodySource: requestBodySource, bodyDataDrain: bodyDataDrain) + return _NativeProtocol._TransferState(url: url, parsedResponseHeader: _NativeProtocol._ParsedResponseHeader(), response: nil, requestBodySource: requestBodySource, bodyDataDrain: bodyDataDrain) } } } @@ -137,6 +193,7 @@ extension _NativeProtocol._TransferState { /// This can be used to either set the initial body source, or to reset it /// e.g. when restarting a transfer. func bySetting(bodySource newSource: _BodySource) -> _NativeProtocol._TransferState { - return _NativeProtocol._TransferState(url: url, parsedResponseHeader: parsedResponseHeader, response: response, requestBodySource: newSource, bodyDataDrain: bodyDataDrain) + return _NativeProtocol._TransferState(url: url, + parsedResponseHeader: parsedResponseHeader, response: response, requestBodySource: newSource, bodyDataDrain: bodyDataDrain) } } diff --git a/Foundation/URLSession/URLSession.swift b/Foundation/URLSession/URLSession.swift index 6c35812d85..d813116392 100644 --- a/Foundation/URLSession/URLSession.swift +++ b/Foundation/URLSession/URLSession.swift @@ -193,8 +193,9 @@ open class URLSession : NSObject { fileprivate let identifier: Int32 fileprivate var invalidated = false fileprivate static let registerProtocols: () = { - // TODO: We register all the native protocols here. - let _ = URLProtocol.registerClass(_HTTPURLProtocol.self) + // TODO: We register all the native protocols here. + _ = URLProtocol.registerClass(_HTTPURLProtocol.self) + _ = URLProtocol.registerClass(_FTPURLProtocol.self) }() /* diff --git a/Foundation/URLSession/URLSessionConfiguration.swift b/Foundation/URLSession/URLSessionConfiguration.swift index aec4520102..cf969b4946 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..7452021d61 --- /dev/null +++ b/Foundation/URLSession/ftp/FTPURLProtocol.swift @@ -0,0 +1,110 @@ +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2019 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 { + // TODO: Implement sftp and ftps + guard request.url?.scheme == "ftp" + else { return false } + return true + } + + 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 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 + let error = NSError(domain: NSURLErrorDomain, code: errorCode(fileSystemError: e), + userInfo: [NSLocalizedDescriptionKey: "File system error"]) + failWith(error: error, 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: + self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + case .dataCompletionHandler: + break + case .downloadCompletionHandler: + break + } + } +} diff --git a/Foundation/URLSession/http/HTTPURLProtocol.swift b/Foundation/URLSession/http/HTTPURLProtocol.swift index 3fbfb22fc4..3ec152442d 100644 --- a/Foundation/URLSession/http/HTTPURLProtocol.swift +++ b/Foundation/URLSession/http/HTTPURLProtocol.swift @@ -33,7 +33,7 @@ internal class _HTTPURLProtocol: _NativeProtocol { fatalError("Received header data but no task available.") } do { - let newTS = try ts.byAppending(headerLine: data) + let newTS = try ts.byAppendingHTTP(headerLine: data) internalState = .transferInProgress(newTS) let didCompleteHeader = !ts.isHeaderComplete && newTS.isHeaderComplete if didCompleteHeader { diff --git a/TestFoundation/FTPServer.swift b/TestFoundation/FTPServer.swift new file mode 100644 index 0000000000..e7ab693047 --- /dev/null +++ b/TestFoundation/FTPServer.swift @@ -0,0 +1,285 @@ +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2019 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 isNotMinusOne(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: isNotMinusOne, 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: isNotMinusOne, + 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.isNotMinusOne, accept(self.dataSocket, addr, &sockLen)) + self.dataSocketPort = sa1.sin_port + } catch { + NSLog("Could not open data port.") + } + } + }) + } + + 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: isNotMinusOne, accept(listenSocket, addr, &sockLen)) + }) + } + + func readData() throws -> String { + var buffer = [UInt8](repeating: 0, count: 4096) + _ = try attempt("read", valid: isNotMinusOne, CInt(read(connectionSocket, &buffer, 4096))) + return String(cString: &buffer) + } + + func readDataOnDataSocket() throws -> String { + var buffer = [UInt8](repeating: 0, count: 4096) + _ = try attempt("read", valid: isNotMinusOne, CInt(read(dataSocket, &buffer, 4096))) + return String(cString: &buffer) + } + + func writeRawData(_ data: Data) throws { + _ = try data.withUnsafeBytes { ptr in + try attempt("write", valid: isNotMinusOne, 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: isNotMinusOne, 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 + upload, download and data tasks. Instead of sending a file, + we are sending the hardcoded data.We are going to test FTP + data, download and 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 port + do { + let dataWritten = try respondWithData(with: saveData!) + if dataWritten != -1 { + // Send the end header on command port + try respondWithRawData(with: "226 Transfer complete.\r\n") + } + } catch { + NSLog("Transfer failed.") + } + } else if read.contains("STOR") { + // Request is for upload. As we are only dealing with data, just read the data and ignore + try respondWithRawData(with: "150 Ok to send data.\r\n") + // Read data from the data socket and respond with completion header after the transfer + do { + let readData = try readDataOnDataSocket() + try respondWithRawData(with: "226 Transfer complete.\r\n") + } catch { + NSLog("Transfer failed.") + } + } + } + + 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 // 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 err as ServerError { + if err.operation == "bind" { continue } + throw err + } + } + } + + let serverReady = ServerSemaphore() + globalDispatchQueue.async { + do { + try runServer(with: serverReady) + } catch { + XCTAssertTrue(true) + return + } + } + let timeout = DispatchTime(uptimeNanoseconds: DispatchTime.now().uptimeNanoseconds + 2_000_000_000) + + serverReady.wait(timeout: timeout) + } +} diff --git a/TestFoundation/TestURLSessionFTP.swift b/TestFoundation/TestURLSessionFTP.swift new file mode 100644 index 0000000000..202fb19540 --- /dev/null +++ b/TestFoundation/TestURLSessionFTP.swift @@ -0,0 +1,123 @@ +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2019 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 TestURLSessionFTP : LoopbackFTPServerTest { + + static var allTests: [(String, (TestURLSessionFTP) -> () throws -> Void)] { + return [ + ("test_ftpDataTask", test_ftpDataTask), + ("test_ftpDataTaskDelegate", test_ftpDataTaskDelegate), + ] + } + + let saveString = """ + FTP implementation to test FTP + upload, download and data tasks. Instead of sending a file, + we are sending the hardcoded data.We are going to test FTP + data, download and upload tasks with delegates & completion handlers. + Creating the data here as we need to pass the count + as part of the header.\r\n + """ + + func test_ftpDataTask() { + let ftpURL = "ftp://127.0.0.1:\(TestURLSessionFTP.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 + defer { expect.fulfill() } + XCTAssertNil(error) + XCTAssertEqual(self.saveString, String(data: data!, encoding: String.Encoding.utf8)) + + }) + dataTask1.resume() + waitForExpectations(timeout: 60) + } + + func test_ftpDataTaskDelegate() { + let urlString = "ftp://127.0.0.1:\(TestURLSessionFTP.serverPort)/test.txt" + let url = URL(string: urlString)! + let dataTask = FTPDataTask(with: expectation(description: "data task")) + dataTask.run(with: url) + waitForExpectations(timeout: 60) + if !dataTask.error { + XCTAssertNotNil(dataTask.fileData) + } + } +} + +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 + } +} diff --git a/TestFoundation/main.swift b/TestFoundation/main.swift index 002a48472d..880edd7348 100644 --- a/TestFoundation/main.swift +++ b/TestFoundation/main.swift @@ -113,6 +113,7 @@ var allTestCases = [ testCase(TestMeasurement.allTests), testCase(TestNSLock.allTests), testCase(TestNSSortDescriptor.allTests), + testCase(TestURLSessionFTP.allTests), ] XCTMain(allTestCases) diff --git a/build.py b/build.py index 1eef0ca327..db42bc763c 100755 --- a/build.py +++ b/build.py @@ -458,6 +458,7 @@ 'Foundation/URLSession/TransferState.swift', 'Foundation/URLSession/libcurl/libcurlHelpers.swift', 'Foundation/URLSession/http/HTTPURLProtocol.swift', + 'Foundation/URLSession/ftp/FTPURLProtocol.swift', 'Foundation/UserDefaults.swift', 'Foundation/NSUUID.swift', 'Foundation/NSValue.swift', @@ -538,6 +539,7 @@ foundation_tests = SwiftExecutable('TestFoundation', [ 'TestFoundation/main.swift', 'TestFoundation/HTTPServer.swift', + 'TestFoundation/FTPServer.swift', 'Foundation/ProgressFraction.swift', 'TestFoundation/Utilities.swift', ] + glob.glob('./TestFoundation/Test*.swift')) # all TestSomething.swift are considered sources to the test project in the TestFoundation directory