diff --git a/Signal/src/Loki/Components/PathStatusView.swift b/Signal/src/Loki/Components/PathStatusView.swift index 6038beff0..548c40741 100644 --- a/Signal/src/Loki/Components/PathStatusView.swift +++ b/Signal/src/Loki/Components/PathStatusView.swift @@ -16,6 +16,12 @@ final class PathStatusView : UIView { private func setUpViewHierarchy() { layer.cornerRadius = Values.pathStatusViewSize / 2 layer.masksToBounds = false + if OnionRequestAPI.paths.count < OnionRequestAPI.pathCount { + let storage = OWSPrimaryStorage.shared() + storage.dbReadConnection.read { transaction in + OnionRequestAPI.paths = storage.getOnionRequestPaths(in: transaction) + } + } let color = (OnionRequestAPI.paths.count >= OnionRequestAPI.pathCount) ? Colors.accent : Colors.pathsBuilding setColor(to: color, isAnimated: false) } diff --git a/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift b/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift index a65f41320..ecc02009f 100644 --- a/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift +++ b/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift @@ -123,7 +123,13 @@ public enum OnionRequestAPI { } }.map(on: LokiAPI.workQueue) { paths in OnionRequestAPI.paths = paths + // Dispatch async on the main queue to avoid nested write transactions DispatchQueue.main.async { + let storage = OWSPrimaryStorage.shared() + storage.dbReadWriteConnection.readWrite { transaction in + print("[Loki] Persisting onion request paths to database.") + storage.setOnionRequestPaths(paths, in: transaction) + } NotificationCenter.default.post(name: .pathsBuilt, object: nil) } return paths @@ -136,6 +142,12 @@ public enum OnionRequestAPI { /// - Note: Exposed for testing purposes. internal static func getPath(excluding snode: LokiAPITarget) -> Promise { guard pathSize >= 1 else { preconditionFailure("Can't build path of size zero.") } + if paths.count < pathCount { + let storage = OWSPrimaryStorage.shared() + storage.dbReadConnection.read { transaction in + paths = storage.getOnionRequestPaths(in: transaction) + } + } // randomElement() uses the system's default random generator, which is cryptographically secure if paths.count >= pathCount { return Promise { seal in @@ -148,8 +160,15 @@ public enum OnionRequestAPI { } } - private static func dropPath(containing snode: LokiAPITarget) { - paths = paths.filter { !$0.contains(snode) } + private static func dropPaths() { + paths.removeAll() + // Dispatch async on the main queue to avoid nested write transactions + DispatchQueue.main.async { + let storage = OWSPrimaryStorage.shared() + storage.dbReadWriteConnection.readWrite { transaction in + storage.clearOnionRequestPaths(in: transaction) + } + } } private static func dropGuardSnode(_ snode: LokiAPITarget) { @@ -231,7 +250,7 @@ public enum OnionRequestAPI { } promise.catch(on: LokiAPI.workQueue) { error in // Must be invoked on LokiAPI.workQueue guard case HTTP.Error.httpRequestFailed(_, _) = error else { return } - dropPath(containing: guardSnode) // A snode in the path is bad; retry with a different path + dropPaths() // A snode in the path is bad; retry with a different path dropGuardSnode(guardSnode) } promise.handlingErrorsIfNeeded(forTargetSnode: snode, associatedWith: hexEncodedPublicKey) @@ -262,7 +281,7 @@ private extension Promise where T == JSON { DispatchQueue.main.async { let storage = OWSPrimaryStorage.shared() storage.dbReadWriteConnection.readWrite { transaction in - storage.clearSnodePool(in: transaction) + storage.dropSnode(snode, in: transaction) } } LokiAPI.failureCount[snode] = 0 diff --git a/SignalServiceKit/src/Loki/Database/OWSPrimaryStorage+Loki.swift b/SignalServiceKit/src/Loki/Database/OWSPrimaryStorage+Loki.swift index 0e1c951c6..d67e802a6 100644 --- a/SignalServiceKit/src/Loki/Database/OWSPrimaryStorage+Loki.swift +++ b/SignalServiceKit/src/Loki/Database/OWSPrimaryStorage+Loki.swift @@ -30,6 +30,42 @@ public extension OWSPrimaryStorage { + // MARK: - Onion Request Path + private static let onionRequestPathCollection = "LokiOnionRequestPathCollection" + + public func setOnionRequestPaths(_ paths: [OnionRequestAPI.Path], in transaction: YapDatabaseReadWriteTransaction) { + // FIXME: This is a bit of a dirty approach that assumes 2 paths of length 3 each. We should do better than this. + guard paths.count == 2 else { return } + let path0 = paths[0] + let path1 = paths[1] + guard path0.count == 3, path1.count == 3 else { return } + let collection = OWSPrimaryStorage.onionRequestPathCollection + transaction.setObject(path0[0], forKey: "0-0", inCollection: collection) + transaction.setObject(path0[1], forKey: "0-1", inCollection: collection) + transaction.setObject(path0[2], forKey: "0-2", inCollection: collection) + transaction.setObject(path1[0], forKey: "1-0", inCollection: collection) + transaction.setObject(path1[1], forKey: "1-1", inCollection: collection) + transaction.setObject(path1[2], forKey: "1-2", inCollection: collection) + } + + public func getOnionRequestPaths(in transaction: YapDatabaseReadTransaction) -> [OnionRequestAPI.Path] { + let collection = OWSPrimaryStorage.onionRequestPathCollection + guard + let path0Snode0 = transaction.object(forKey: "0-0", inCollection: collection) as? LokiAPITarget, + let path0Snode1 = transaction.object(forKey: "0-1", inCollection: collection) as? LokiAPITarget, + let path0Snode2 = transaction.object(forKey: "0-2", inCollection: collection) as? LokiAPITarget, + let path1Snode0 = transaction.object(forKey: "1-0", inCollection: collection) as? LokiAPITarget, + let path1Snode1 = transaction.object(forKey: "1-1", inCollection: collection) as? LokiAPITarget, + let path1Snode2 = transaction.object(forKey: "1-2", inCollection: collection) as? LokiAPITarget else { return [] } + return [ [ path0Snode0, path0Snode1, path0Snode2 ], [ path1Snode0, path1Snode1, path1Snode2 ] ] + } + + public func clearOnionRequestPaths(in transaction: YapDatabaseReadWriteTransaction) { + transaction.removeAllObjects(inCollection: OWSPrimaryStorage.onionRequestPathCollection) + } + + + // MARK: - Session Requests private static let sessionRequestTimestampCollection = "LokiSessionRequestTimestampCollection"