diff --git a/Sources/FTPFileProvider.swift b/Sources/FTPFileProvider.swift index 88126b4..a42e670 100644 --- a/Sources/FTPFileProvider.swift +++ b/Sources/FTPFileProvider.swift @@ -35,9 +35,7 @@ open class FTPFileProvider: FileProviderBasicRemote { public var validatingCache: Bool /// Determine either FTP session is in passive or active mode. - /// - Note: Due to `URLSessionStreamTask` restrictions for determining listening port, - /// only passive sessions are available in current implementation. - public let passiveMode = true + public let passiveMode: Bool /// Force to use URLSessionDownloadTask/URLSessionDataTask when possible public var useAppleImplementation = true @@ -72,11 +70,15 @@ open class FTPFileProvider: FileProviderBasicRemote { /** Initializer for FTP provider with given username and password. + - Note: `passive` value should be set according to server settings and firewall presence. + - Parameter baseURL: a url with `ftp://hostaddress/` format. + - Parameter passive: FTP server data connection, `true` means passive connection (data connection created by client) + and `false` means active connection (data connection created by server). Default is `true` (passive mode). - Parameter credential: a `URLCredential` object contains user and password. - Parameter cache: A URLCache to cache downloaded files and contents. (unimplemented for FTP and should be nil) */ - public init? (baseURL: URL, credential: URLCredential? = nil, cache: URLCache? = nil) { + public init? (baseURL: URL, passive: Bool = true, credential: URLCredential? = nil, cache: URLCache? = nil) { guard (baseURL.scheme ?? "ftp").lowercased().hasPrefix("ftp") else { return nil } guard baseURL.host != nil else { return nil } var urlComponents = URLComponents(url: baseURL, resolvingAgainstBaseURL: true)! @@ -84,6 +86,7 @@ open class FTPFileProvider: FileProviderBasicRemote { urlComponents.scheme = urlComponents.scheme ?? "ftp" self.baseURL = (urlComponents.url!.path.hasSuffix("/") ? urlComponents.url! : urlComponents.url!.appendingPathComponent("")).absoluteURL + self.passiveMode = passive self.currentPath = "" self.useCache = false self.validatingCache = true diff --git a/Sources/FTPHelper.swift b/Sources/FTPHelper.swift index de73454..14dc62c 100644 --- a/Sources/FTPHelper.swift +++ b/Sources/FTPHelper.swift @@ -65,7 +65,7 @@ internal extension FTPFileProvider { } if let data = data, let response = String(data: data, encoding: .utf8) { - completionHandler(response.trimmingCharacters(in: CharacterSet(charactersIn: "\r\n")), nil) + completionHandler(response.trimmingCharacters(in: .whitespacesAndNewlines), nil) } else { completionHandler(nil, self.throwError("", code: URLError.cannotParseResponse)) return @@ -120,7 +120,7 @@ internal extension FTPFileProvider { // needs password if response.hasPrefix("33") { self.execute(command: "PASS \(credential?.password ?? "fileprovider@")", on: task) { (response, error) in - if response?.hasPrefix("2") ?? false { + if response?.hasPrefix("23") ?? false { completionHandler(nil) } else { completionHandler(self.throwError("", code: URLError.userAuthenticationRequired)) @@ -137,8 +137,23 @@ internal extension FTPFileProvider { if self.baseURL?.scheme == "ftps" || self.baseURL?.port == 990 { self.execute(command: "AUTH TLS", on: task, minLength: 0, completionHandler: { (response, error) in - task.startSecureConnection() - loginHandle() + if let error = error { + completionHandler(error) + return + } + + if let response = response, response.hasPrefix("23") { + task.startSecureConnection() + self.execute(command: "PBSZ 0\r\nPROT P", on: task, completionHandler: { (response, error) in + if let error = error { + completionHandler(error) + return + } + + loginHandle() + }) + + } }) } else { loginHandle() @@ -205,26 +220,25 @@ internal extension FTPFileProvider { let passiveTask = self.session.fpstreamTask(withHostName: host, port: port) passiveTask.resume() if self.baseURL?.scheme == "ftps" || self.baseURL?.port == 990 { - task.startSecureConnection() + passiveTask.startSecureConnection() } completionHandler(passiveTask, nil) } } func ftpActive(_ task: FileProviderStreamTask, completionHandler: @escaping (_ dataTask: FileProviderStreamTask?, _ error: Error?) -> Void) { - var port: Int32 = 0 - var _activeTask: FileProviderStreamTask? - while (_activeTask?.state ?? .suspended) == .suspended { - port = 32000 + Int32(arc4random_uniform(16384)) - let service = NetService(domain: "", type: "_tcp.", name: "", port: port) - _activeTask = self.session.fpstreamTask(withNetService: service) - _activeTask?.resume() + let service = NetService(domain: "", type: "_tcp.", name: "", port: 0) + service.publish(options: .listenForConnections) + let startTime = Date() + while service.port < 1 && startTime.timeIntervalSinceNow > -self.session.configuration.timeoutIntervalForRequest { + usleep(100_000) } - guard let activeTask = _activeTask else { return } + let activeTask = self.session.fpstreamTask(withNetService: service) + activeTask.resume() if self.baseURL?.scheme == "ftps" || self.baseURL?.port == 990 { - task.startSecureConnection() + activeTask.startSecureConnection() } - self.execute(command: "PORT \(port)", on: task) { (response, error) in + self.execute(command: "PORT \(service.port)", on: task) { (response, error) in if let error = error { activeTask.cancel() completionHandler(nil, error) @@ -232,11 +246,13 @@ internal extension FTPFileProvider { } guard let response = response else { + activeTask.cancel() completionHandler(nil, self.throwError("", code: URLError.badServerResponse)) return } guard !response.hasPrefix("5") else { + activeTask.cancel() completionHandler(nil, self.throwError("", code: URLError.cannotConnectToHost)) return }