element-x-ios/ElementX/Sources/Services/UserSession/UserSessionStore.swift

153 lines
6.4 KiB
Swift

//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
import Kingfisher
import MatrixRustSDK
class UserSessionStore: UserSessionStoreProtocol {
private let keychainController: KeychainControllerProtocol
private let backgroundTaskService: BackgroundTaskServiceProtocol
/// Whether or not there are sessions in the store.
var hasSessions: Bool { !keychainController.restorationTokens().isEmpty }
/// The base directory where all session data is stored.
let baseDirectory: URL
init(backgroundTaskService: BackgroundTaskServiceProtocol) {
keychainController = KeychainController(service: .sessions,
accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier)
self.backgroundTaskService = backgroundTaskService
baseDirectory = .sessionsBaseDirectory
MXLog.info("Setup base directory at: \(baseDirectory)")
}
/// Deletes all data stored in the shared container and keychain
func reset() {
try? FileManager.default.removeItem(at: baseDirectory)
keychainController.removeAllRestorationTokens()
}
func restoreUserSession() async -> Result<UserSessionProtocol, UserSessionStoreError> {
let availableCredentials = keychainController.restorationTokens()
guard let credentials = availableCredentials.first else {
return .failure(.missingCredentials)
}
switch await restorePreviousLogin(credentials) {
case .success(let clientProxy):
return .success(buildUserSessionWithClient(clientProxy))
case .failure(let error):
MXLog.error("Failed restoring login with error: \(error)")
// On any restoration failure reset the token and restart
keychainController.removeAllRestorationTokens()
deleteSessionDirectory(for: credentials.userID)
return .failure(error)
}
}
func userSession(for client: Client) async -> Result<UserSessionProtocol, UserSessionStoreError> {
switch await setupProxyForClient(client) {
case .success(let clientProxy):
return .success(buildUserSessionWithClient(clientProxy))
case .failure(let error):
MXLog.error("Failed creating user session with error: \(error)")
return .failure(error)
}
}
func refreshRestorationToken(for userSession: UserSessionProtocol) -> Result<Void, UserSessionStoreError> {
guard let restorationToken = userSession.clientProxy.restorationToken else {
return .failure(.failedRefreshingRestoreToken)
}
keychainController.setRestorationToken(restorationToken, forUsername: userSession.clientProxy.userID)
return .success(())
}
func logout(userSession: UserSessionProtocol) {
let userID = userSession.clientProxy.userID
keychainController.removeRestorationTokenForUsername(userID)
deleteSessionDirectory(for: userID)
}
// MARK: - Private
private func buildUserSessionWithClient(_ clientProxy: ClientProxyProtocol) -> UserSessionProtocol {
let imageCache = ImageCache.onlyInMemory
imageCache.memoryStorage.config.keepWhenEnteringBackground = true
return UserSession(clientProxy: clientProxy,
mediaProvider: MediaProvider(mediaLoader: clientProxy,
imageCache: imageCache,
backgroundTaskService: backgroundTaskService))
}
private func restorePreviousLogin(_ credentials: KeychainCredentials) async -> Result<ClientProxyProtocol, UserSessionStoreError> {
let builder = ClientBuilder()
.basePath(path: baseDirectory.path)
.username(username: credentials.userID)
.homeserverUrl(url: credentials.restorationToken.session.homeserverUrl)
.userAgent(userAgent: UserAgentBuilder.makeASCIIUserAgent() ?? "unknown")
.serverVersions(versions: ["v1.0", "v1.1", "v1.2", "v1.3", "v1.4", "v1.5"]) // FIXME: Quick and dirty fix for stopping version requests on startup https://github.com/matrix-org/matrix-rust-sdk/pull/1376
do {
let client: Client = try await Task.dispatch(on: .global()) {
let client = try builder.build()
try client.restoreSession(session: credentials.restorationToken.session)
return client
}
return await setupProxyForClient(client)
} catch {
MXLog.error("Failed restoring login with error: \(error)")
return .failure(.failedRestoringLogin)
}
}
private func setupProxyForClient(_ client: Client) async -> Result<ClientProxyProtocol, UserSessionStoreError> {
do {
let session = try client.session()
let userId = try client.userId()
keychainController.setRestorationToken(RestorationToken(session: session), forUsername: userId)
} catch {
MXLog.error("Failed setting up user session with error: \(error)")
return .failure(.failedSettingUpSession)
}
let clientProxy = await ClientProxy(client: client, backgroundTaskService: backgroundTaskService)
return .success(clientProxy)
}
private func deleteSessionDirectory(for userID: String) {
// Rust sanitises the user ID replacing invalid characters with an _
let sanitisedUserID = userID.replacingOccurrences(of: ":", with: "_")
let url = baseDirectory.appendingPathComponent(sanitisedUserID)
do {
try FileManager.default.removeItem(at: url)
} catch {
MXLog.failure("Failed deleting the session data: \(error)")
}
}
}