diff --git a/Package.swift b/Package.swift index 3bc8363f52ec3a8a43c913ae95e0a02a3bad3d55..69613b7a89cfceac7bbcbe3eaa5b1e32bb39b380 100644 --- a/Package.swift +++ b/Package.swift @@ -69,6 +69,7 @@ let package = Package( .package(url: "https://github.com/google/google-api-objectivec-client-for-rest", from: "1.6.0"), .package(url: "https://git.xx.network/elixxir/client-ios-db.git", .upToNextMajor(from: "1.0.5")), .package(url: "https://github.com/firebase/firebase-ios-sdk.git", .upToNextMajor(from: "8.10.0")), + .package(url: "https://github.com/darrarski/Shout.git", revision: "df5a662293f0ac15eeb4f2fd3ffd0c07b73d0de0"), .package(url: "https://github.com/pointfreeco/swift-composable-architecture.git",.upToNextMajor(from: "0.32.0")) ], targets: [ @@ -216,7 +217,13 @@ let package = Package( name: "SFTPFeature", dependencies: [ "Shared", - "InputField" + "Keychain", + "InputField", + "DependencyInjection", + .product( + name: "Shout", + package: "Shout" + ) ] ), diff --git a/Sources/BackupFeature/ViewModels/BackupSFTPViewModel.swift b/Sources/BackupFeature/ViewModels/BackupSFTPViewModel.swift index 8368713e723419159be8c112d8ffa729dae5d79a..f9518a57613cf453cccc293ef3862f3dd8f4d2b6 100644 --- a/Sources/BackupFeature/ViewModels/BackupSFTPViewModel.swift +++ b/Sources/BackupFeature/ViewModels/BackupSFTPViewModel.swift @@ -54,9 +54,14 @@ final class BackupSFTPViewModel { let password = stateSubject.value.password let authParams = SFTPAuthParams(host, username, password) - service.justAuthenticate(authParams) - hudSubject.send(.none) - popSubject.send(()) + + do { + try service.justAuthenticate(authParams) + hudSubject.send(.none) + popSubject.send(()) + } catch { + hudSubject.send(.error(.init(with: error))) + } } private func validate() { diff --git a/Sources/Keychain/KeychainHandler.swift b/Sources/Keychain/KeychainHandler.swift index 23f248879a7f2275b0faddd9f4c08962d9ed6246..6ac0d645d6def33360ee5ed0d0ab1e973a0487fc 100644 --- a/Sources/Keychain/KeychainHandler.swift +++ b/Sources/Keychain/KeychainHandler.swift @@ -1,15 +1,24 @@ import Foundation import KeychainAccess +public enum KeychainSFTP: String { + case pwd + case host + case username +} + public protocol KeychainHandling { func clear() throws func getPassword() throws -> Data? func store(password pwd: Data) throws + + func get(key: KeychainSFTP) throws -> String? + func store(key: KeychainSFTP, value: String) throws } public struct KeychainHandler: KeychainHandling { - private let password = "password" private let keychain: Keychain + private let password = "password" public init() { self.keychain = Keychain(service: "XXM") @@ -26,4 +35,12 @@ public struct KeychainHandler: KeychainHandling { public func getPassword() throws -> Data? { try keychain.getData(password) } + + public func get(key: KeychainSFTP) throws -> String? { + try keychain.get(key.rawValue) + } + + public func store(key: KeychainSFTP, value: String) throws { + try keychain.set(value, key: key.rawValue) + } } diff --git a/Sources/Keychain/MockKeychainHandler.swift b/Sources/Keychain/MockKeychainHandler.swift index c1ff10dd802d5cd230cffbe25f20b744aa06126b..39d4a33ddb1e0e6d8a75d8b5a1ea980d8fb189bf 100644 --- a/Sources/Keychain/MockKeychainHandler.swift +++ b/Sources/Keychain/MockKeychainHandler.swift @@ -6,4 +6,6 @@ public struct MockKeychainHandler: KeychainHandling { public func clear() throws {} public func store(password pwd: Data) throws {} public func getPassword() throws -> Data? { Data() } + public func get(key: KeychainSFTP) throws -> String? { nil } + public func store(key: KeychainSFTP, value: String) throws {} } diff --git a/Sources/RestoreFeature/ViewModels/RestoreViewModel.swift b/Sources/RestoreFeature/ViewModels/RestoreViewModel.swift index 0a7d988ef270cbcfabbe6d1e929809ba3c24e9c9..ccb417df5624d7bd0036e34356c996c923a8a6d3 100644 --- a/Sources/RestoreFeature/ViewModels/RestoreViewModel.swift +++ b/Sources/RestoreFeature/ViewModels/RestoreViewModel.swift @@ -8,6 +8,7 @@ import Integration import BackupFeature import DependencyInjection +import SFTPFeature import iCloudFeature import DropboxFeature import GoogleDriveFeature @@ -38,13 +39,16 @@ extension RestorationStep: Equatable { } final class RestoreViewModel { + @Dependency private var sftpService: SFTPService @Dependency private var iCloudService: iCloudInterface @Dependency private var dropboxService: DropboxInterface @Dependency private var googleService: GoogleDriveInterface @KeyObject(.username, defaultValue: nil) var username: String? - var step: AnyPublisher<RestorationStep, Never> { stepRelay.eraseToAnyPublisher() } + var step: AnyPublisher<RestorationStep, Never> { + stepRelay.eraseToAnyPublisher() + } // TO REFACTOR: // @@ -86,7 +90,11 @@ final class RestoreViewModel { } private func downloadBackupForSFTP(_ backup: Backup) { - // TODO + do { + try sftpService.download(backup.id) + } catch { + print(error.localizedDescription) + } } private func downloadBackupForDropbox(_ backup: Backup) { diff --git a/Sources/SFTPFeature/SFTPService.swift b/Sources/SFTPFeature/SFTPService.swift index 30b92573a75e1cc4fc956e3f68f8433aa59f9e13..3bb8c0620c70c0d3a84c6e627e64be9e7f484556 100644 --- a/Sources/SFTPFeature/SFTPService.swift +++ b/Sources/SFTPFeature/SFTPService.swift @@ -1,5 +1,8 @@ +import Shout import Models +import Keychain import Foundation +import DependencyInjection public typealias SFTPAuthParams = (String, String, String) public typealias SFTPFetchResult = (Result<RestoreSettings?, Error>) -> Void @@ -8,42 +11,88 @@ public typealias SFTPFetchParams = (SFTPAuthParams, SFTPFetchResult) public struct SFTPService { public var isAuthorized: () -> Bool public var fetch: (SFTPFetchParams) -> Void - public var justAuthenticate: (SFTPAuthParams) -> Void + public var download: (String) throws -> Void + public var justAuthenticate: (SFTPAuthParams) throws -> Void } public extension SFTPService { static var mock = SFTPService( isAuthorized: { - false + true }, fetch: { (authParams, completion) in - print("^^^ RestoreSFTP Host: \(authParams.0)") - print("^^^ RestoreSFTP Username: \(authParams.1)") - print("^^^ RestoreSFTP Password: \(authParams.2)") - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - completion(.success(.init( - backup: .init(id: "ASDF", date: Date.distantPast, size: 100_000_000), - cloudService: .sftp - ))) - } + completion(.success(nil)) + }, + download: { path in + }, justAuthenticate: { host, username, password in - // TODO: Store these params on the keychain }) static var live = SFTPService( isAuthorized: { - /// If it has host/username/password on keychain - /// means its authorized, not that is working - /// - true + if let keychain = try? DependencyInjection.Container.shared.resolve() as KeychainHandling, + let pwd = try? keychain.get(key: .pwd), + let host = try? keychain.get(key: .host), + let username = try? keychain.get(key: .username) { + return true + } else { + return false + } }, fetch: { (authParams, completion) in - // TODO: Store host/username/password on keychain + let host = authParams.0 + let username = authParams.1 + let password = authParams.2 + + do { + let ssh = try SSH(host: host, port: 22) + try ssh.authenticate(username: username, password: password) + let sftp = try ssh.openSftp() + + let keychain = try DependencyInjection.Container.shared.resolve() as KeychainHandling + try keychain.store(key: .host, value: host) + try keychain.store(key: .pwd, value: password) + try keychain.store(key: .username, value: username) + + if let files = try? sftp.listFiles(in: "backup"), + let backup = files.filter({ file in file.0 == "backup.xxm" }).first { + completion(.success(.init( + backup: .init( + id: "backup/backup.xxm", + date: backup.value.lastModified, + size: Float(backup.value.size) + ), + cloudService: .sftp + ))) + + return + } + + completion(.success(nil)) + } catch { + completion(.failure(error)) + } + }, + download: { path in + let keychain = try DependencyInjection.Container.shared.resolve() as KeychainHandling + let host = try keychain.get(key: .host) + let password = try keychain.get(key: .pwd) + let username = try keychain.get(key: .username) + + let ssh = try SSH(host: host!, port: 22) + try ssh.authenticate(username: username!, password: password!) + let sftp = try ssh.openSftp() + + let temp = NSTemporaryDirectory() + try sftp.download(remotePath: path, localURL: URL(string: temp)!) + print(FileManager.default.fileExists(atPath: temp)) }, justAuthenticate: { host, username, password in - // TODO: Store host/username/password on keychain + let keychain = try DependencyInjection.Container.shared.resolve() as KeychainHandling + try keychain.store(key: .host, value: host) + try keychain.store(key: .pwd, value: password) + try keychain.store(key: .username, value: username) } ) } diff --git a/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved b/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved index eeb145af40bba398a5526019333b5de39606557e..102be628eff74bf62db32030a74c7eed314ba3f3 100644 --- a/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -27,6 +27,15 @@ "version" : "1.4.0" } }, + { + "identity" : "bluesocket", + "kind" : "remoteSourceControl", + "location" : "https://github.com/IBM-Swift/BlueSocket.git", + "state" : { + "revision" : "c9894fd117457f1d006575fbfb2fdfd6f79eac03", + "version" : "1.0.200" + } + }, { "identity" : "boringssl-swiftpm", "kind" : "remoteSourceControl", @@ -207,6 +216,15 @@ "version" : "1.22.2" } }, + { + "identity" : "libssh2prebuild", + "kind" : "remoteSourceControl", + "location" : "https://github.com/DimaRU/Libssh2Prebuild.git", + "state" : { + "branch" : "1.10.0+OpenSSL_1_1_1o", + "revision" : "a91bcf205a6cbc84144f840c44145656abbd266a" + } + }, { "identity" : "nanopb", "kind" : "remoteSourceControl", @@ -261,6 +279,14 @@ "version" : "1.2.0" } }, + { + "identity" : "shout", + "kind" : "remoteSourceControl", + "location" : "https://github.com/darrarski/Shout.git", + "state" : { + "revision" : "df5a662293f0ac15eeb4f2fd3ffd0c07b73d0de0" + } + }, { "identity" : "snapkit", "kind" : "remoteSourceControl",