mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
d8fd3b35b4
Made a tweak to prevent some odd looking keyboard transitions when going to conversation settings Updated the PagedDatabaseObserver to not call 'onChangeUnsorted' on the main thread (now we can generate the changeset on the background thread so there is less main thread work) Fixed an issue where the most recently received message from the swarm could be removed from the swarm yet the app would still poll for it, resulting in the swarm always returning the oldest possible messages until the user sends a new one-to-one message Fixed an issue where the initial scroll offset could be incorrect due to certain message types Fixed an issue where the title view inside a conversation could jump when pushing to the conversation settings screen Refactored a couple of ObjC functions to Swift as they were crashing (due to memory allocation?) hopefully this will fix it Tweaked some DispatchQueue priorities to ensure PagedDatabaseObserver loading is prioritised Updated buttons to use a standard convention for highlighted states Updated the new conversation button to follow the new highlighted state convention
1104 lines
53 KiB
Swift
1104 lines
53 KiB
Swift
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
import Foundation
|
|
import PromiseKit
|
|
import Sodium
|
|
import GRDB
|
|
import SessionUtilitiesKit
|
|
|
|
public final class SnodeAPI {
|
|
private static let sodium = Sodium()
|
|
|
|
private static var hasLoadedSnodePool: Atomic<Bool> = Atomic(false)
|
|
private static var loadedSwarms: Atomic<Set<String>> = Atomic([])
|
|
private static var getSnodePoolPromise: Atomic<Promise<Set<Snode>>?> = Atomic(nil)
|
|
|
|
/// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
|
|
internal static var snodeFailureCount: Atomic<[Snode: UInt]> = Atomic([:])
|
|
/// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
|
|
internal static var snodePool: Atomic<Set<Snode>> = Atomic([])
|
|
|
|
/// The offset between the user's clock and the Service Node's clock. Used in cases where the
|
|
/// user's clock is incorrect.
|
|
///
|
|
/// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
|
|
public static var clockOffset: Atomic<Int64> = Atomic(0)
|
|
/// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
|
|
public static var swarmCache: Atomic<[String: Set<Snode>]> = Atomic([:])
|
|
|
|
// MARK: - Namespaces
|
|
|
|
public static let defaultNamespace = 0
|
|
public static let closedGroupNamespace = -10
|
|
public static let configNamespace = 5
|
|
|
|
// MARK: - Hardfork version
|
|
|
|
public static var hardfork = UserDefaults.standard[.hardfork]
|
|
public static var softfork = UserDefaults.standard[.softfork]
|
|
|
|
// MARK: - Settings
|
|
|
|
private static let maxRetryCount: UInt = 8
|
|
private static let minSwarmSnodeCount = 3
|
|
private static let seedNodePool: Set<String> = Features.useTestnet ? [ "http://public.loki.foundation:38157" ] : [ "https://storage.seed1.loki.network:4433", "https://storage.seed3.loki.network:4433", "https://public.loki.foundation:4433" ]
|
|
private static let snodeFailureThreshold = 3
|
|
private static let targetSwarmSnodeCount = 2
|
|
private static let minSnodePoolCount = 12
|
|
|
|
// MARK: Snode Pool Interaction
|
|
|
|
private static var hasInsufficientSnodes: Bool { snodePool.wrappedValue.count < minSnodePoolCount }
|
|
|
|
private static func loadSnodePoolIfNeeded() {
|
|
guard !hasLoadedSnodePool.wrappedValue else { return }
|
|
|
|
let fetchedSnodePool: Set<Snode> = Storage.shared
|
|
.read { db in try Snode.fetchSet(db) }
|
|
.defaulting(to: [])
|
|
|
|
snodePool.mutate { $0 = fetchedSnodePool }
|
|
hasLoadedSnodePool.mutate { $0 = true }
|
|
}
|
|
|
|
private static func setSnodePool(to newValue: Set<Snode>, db: Database? = nil) {
|
|
snodePool.mutate { $0 = newValue }
|
|
|
|
if let db: Database = db {
|
|
_ = try? Snode.deleteAll(db)
|
|
newValue.forEach { try? $0.save(db) }
|
|
}
|
|
else {
|
|
Storage.shared.write { db in
|
|
_ = try? Snode.deleteAll(db)
|
|
newValue.forEach { try? $0.save(db) }
|
|
}
|
|
}
|
|
}
|
|
|
|
private static func dropSnodeFromSnodePool(_ snode: Snode) {
|
|
#if DEBUG
|
|
dispatchPrecondition(condition: .onQueue(Threading.workQueue))
|
|
#endif
|
|
var snodePool = SnodeAPI.snodePool.wrappedValue
|
|
snodePool.remove(snode)
|
|
setSnodePool(to: snodePool)
|
|
}
|
|
|
|
@objc public static func clearSnodePool() {
|
|
snodePool.mutate { $0.removeAll() }
|
|
|
|
Threading.workQueue.async {
|
|
setSnodePool(to: [])
|
|
}
|
|
}
|
|
|
|
// MARK: Swarm Interaction
|
|
private static func loadSwarmIfNeeded(for publicKey: String) {
|
|
guard !loadedSwarms.wrappedValue.contains(publicKey) else { return }
|
|
|
|
let updatedCacheForKey: Set<Snode> = Storage.shared
|
|
.read { db in try Snode.fetchSet(db, publicKey: publicKey) }
|
|
.defaulting(to: [])
|
|
|
|
swarmCache.mutate { $0[publicKey] = updatedCacheForKey }
|
|
loadedSwarms.mutate { $0.insert(publicKey) }
|
|
}
|
|
|
|
private static func setSwarm(to newValue: Set<Snode>, for publicKey: String, persist: Bool = true) {
|
|
#if DEBUG
|
|
dispatchPrecondition(condition: .onQueue(Threading.workQueue))
|
|
#endif
|
|
swarmCache.mutate { $0[publicKey] = newValue }
|
|
|
|
guard persist else { return }
|
|
|
|
Storage.shared.write { db in
|
|
try? newValue.save(db, key: publicKey)
|
|
}
|
|
}
|
|
|
|
public static func dropSnodeFromSwarmIfNeeded(_ snode: Snode, publicKey: String) {
|
|
#if DEBUG
|
|
dispatchPrecondition(condition: .onQueue(Threading.workQueue))
|
|
#endif
|
|
let swarmOrNil = swarmCache.wrappedValue[publicKey]
|
|
guard var swarm = swarmOrNil, let index = swarm.firstIndex(of: snode) else { return }
|
|
swarm.remove(at: index)
|
|
setSwarm(to: swarm, for: publicKey)
|
|
}
|
|
|
|
// MARK: Internal API
|
|
|
|
internal static func invoke(_ method: SnodeAPIEndpoint, on snode: Snode, associatedWith publicKey: String? = nil, parameters: JSON) -> Promise<Data> {
|
|
if Features.useOnionRequests {
|
|
return OnionRequestAPI
|
|
.sendOnionRequest(
|
|
to: snode,
|
|
invoking: method,
|
|
with: parameters,
|
|
associatedWith: publicKey
|
|
)
|
|
.map2 { responseData in
|
|
guard let responseJson: JSON = try? JSONSerialization.jsonObject(with: responseData, options: [ .fragmentsAllowed ]) as? JSON else {
|
|
throw HTTP.Error.invalidJSON
|
|
}
|
|
|
|
if let hf = responseJson["hf"] as? [Int] {
|
|
if hf[1] > softfork {
|
|
softfork = hf[1]
|
|
UserDefaults.standard[.softfork] = softfork
|
|
}
|
|
|
|
if hf[0] > hardfork {
|
|
hardfork = hf[0]
|
|
UserDefaults.standard[.hardfork] = hardfork
|
|
softfork = hf[1]
|
|
UserDefaults.standard[.softfork] = softfork
|
|
}
|
|
}
|
|
|
|
return responseData
|
|
}
|
|
}
|
|
else {
|
|
let url = "\(snode.address):\(snode.port)/storage_rpc/v1"
|
|
return HTTP.execute(.post, url, parameters: parameters)
|
|
.recover2 { error -> Promise<Data> in
|
|
guard case HTTP.Error.httpRequestFailed(let statusCode, let data) = error else { throw error }
|
|
throw SnodeAPI.handleError(withStatusCode: statusCode, data: data, forSnode: snode, associatedWith: publicKey) ?? error
|
|
}
|
|
}
|
|
}
|
|
|
|
private static func getNetworkTime(from snode: Snode) -> Promise<UInt64> {
|
|
return invoke(.getInfo, on: snode, parameters: [:]).map2 { responseData in
|
|
guard let responseJson: JSON = try? JSONSerialization.jsonObject(with: responseData, options: [ .fragmentsAllowed ]) as? JSON else {
|
|
throw HTTP.Error.invalidJSON
|
|
}
|
|
guard let timestamp = responseJson["timestamp"] as? UInt64 else { throw HTTP.Error.invalidJSON }
|
|
return timestamp
|
|
}
|
|
}
|
|
|
|
internal static func getRandomSnode() -> Promise<Snode> {
|
|
// randomElement() uses the system's default random generator, which is cryptographically secure
|
|
return getSnodePool().map2 { $0.randomElement()! }
|
|
}
|
|
|
|
private static func getSnodePoolFromSeedNode() -> Promise<Set<Snode>> {
|
|
let target = seedNodePool.randomElement()!
|
|
let url = "\(target)/json_rpc"
|
|
let parameters: JSON = [
|
|
"method": "get_n_service_nodes",
|
|
"params": [
|
|
"active_only": true,
|
|
"limit": 256,
|
|
"fields": [
|
|
"public_ip": true,
|
|
"storage_port": true,
|
|
"pubkey_ed25519": true,
|
|
"pubkey_x25519": true
|
|
]
|
|
]
|
|
]
|
|
SNLog("Populating snode pool using seed node: \(target).")
|
|
let (promise, seal) = Promise<Set<Snode>>.pending()
|
|
|
|
Threading.workQueue.async {
|
|
attempt(maxRetryCount: 4, recoveringOn: Threading.workQueue) {
|
|
HTTP.execute(.post, url, parameters: parameters, useSeedNodeURLSession: true)
|
|
.map2 { responseData -> Set<Snode> in
|
|
guard let snodePool: SnodePoolResponse = try? JSONDecoder().decode(SnodePoolResponse.self, from: responseData) else {
|
|
throw SnodeAPIError.snodePoolUpdatingFailed
|
|
}
|
|
|
|
return snodePool.result
|
|
.serviceNodeStates
|
|
.compactMap { $0.value }
|
|
.asSet()
|
|
}
|
|
}
|
|
.done2 { snodePool in
|
|
SNLog("Got snode pool from seed node: \(target).")
|
|
seal.fulfill(snodePool)
|
|
}
|
|
.catch2 { error in
|
|
SNLog("Failed to contact seed node at: \(target).")
|
|
seal.reject(error)
|
|
}
|
|
}
|
|
|
|
return promise
|
|
}
|
|
|
|
private static func getSnodePoolFromSnode() -> Promise<Set<Snode>> {
|
|
var snodePool = SnodeAPI.snodePool.wrappedValue
|
|
var snodes: Set<Snode> = []
|
|
(0..<3).forEach { _ in
|
|
guard let snode = snodePool.randomElement() else { return }
|
|
|
|
snodePool.remove(snode)
|
|
snodes.insert(snode)
|
|
}
|
|
|
|
let snodePoolPromises: [Promise<Set<Snode>>] = snodes.map { snode in
|
|
return attempt(maxRetryCount: 4, recoveringOn: Threading.workQueue) {
|
|
// Don't specify a limit in the request. Service nodes return a shuffled
|
|
// list of nodes so if we specify a limit the 3 responses we get might have
|
|
// very little overlap.
|
|
let parameters: JSON = [
|
|
"endpoint": "get_service_nodes",
|
|
"params": [
|
|
"active_only": true,
|
|
"fields": [
|
|
"public_ip": true,
|
|
"storage_port": true,
|
|
"pubkey_ed25519": true,
|
|
"pubkey_x25519": true
|
|
]
|
|
]
|
|
]
|
|
|
|
return invoke(.oxenDaemonRPCCall, on: snode, parameters: parameters)
|
|
.map2 { responseData in
|
|
guard let snodePool: SnodePoolResponse = try? JSONDecoder().decode(SnodePoolResponse.self, from: responseData) else {
|
|
throw SnodeAPIError.snodePoolUpdatingFailed
|
|
}
|
|
|
|
return snodePool.result
|
|
.serviceNodeStates
|
|
.compactMap { $0.value }
|
|
.asSet()
|
|
}
|
|
}
|
|
}
|
|
|
|
let promise = when(fulfilled: snodePoolPromises).map2 { results -> Set<Snode> in
|
|
let result: Set<Snode> = results.reduce(Set()) { prev, next in prev.intersection(next) }
|
|
|
|
// We want the snodes to agree on at least this many snodes
|
|
guard result.count > 24 else { throw SnodeAPIError.inconsistentSnodePools }
|
|
|
|
// Limit the snode pool size to 256 so that we don't go too long without
|
|
// refreshing it
|
|
return Set(result.prefix(256))
|
|
}
|
|
|
|
return promise
|
|
}
|
|
|
|
// MARK: Public API
|
|
|
|
public static func hasCachedSnodesInclusingExpired() -> Bool {
|
|
loadSnodePoolIfNeeded()
|
|
|
|
return !hasInsufficientSnodes
|
|
}
|
|
|
|
public static func getSnodePool() -> Promise<Set<Snode>> {
|
|
loadSnodePoolIfNeeded()
|
|
let now = Date()
|
|
let hasSnodePoolExpired = given(Storage.shared[.lastSnodePoolRefreshDate]) {
|
|
now.timeIntervalSince($0) > 2 * 60 * 60
|
|
}.defaulting(to: true)
|
|
let snodePool: Set<Snode> = SnodeAPI.snodePool.wrappedValue
|
|
|
|
guard hasInsufficientSnodes || hasSnodePoolExpired else {
|
|
return Promise.value(snodePool)
|
|
}
|
|
|
|
if let getSnodePoolPromise = getSnodePoolPromise.wrappedValue { return getSnodePoolPromise }
|
|
|
|
let promise: Promise<Set<Snode>>
|
|
if snodePool.count < minSnodePoolCount {
|
|
promise = getSnodePoolFromSeedNode()
|
|
}
|
|
else {
|
|
promise = getSnodePoolFromSnode().recover2 { _ in
|
|
getSnodePoolFromSeedNode()
|
|
}
|
|
}
|
|
|
|
getSnodePoolPromise.mutate { $0 = promise }
|
|
promise.map2 { snodePool -> Set<Snode> in
|
|
guard !snodePool.isEmpty else { throw SnodeAPIError.snodePoolUpdatingFailed }
|
|
|
|
return snodePool
|
|
}
|
|
|
|
promise.then2 { snodePool -> Promise<Set<Snode>> in
|
|
let (promise, seal) = Promise<Set<Snode>>.pending()
|
|
|
|
Storage.shared.writeAsync(
|
|
updates: { db in
|
|
db[.lastSnodePoolRefreshDate] = now
|
|
setSnodePool(to: snodePool, db: db)
|
|
},
|
|
completion: { _, _ in
|
|
seal.fulfill(snodePool)
|
|
}
|
|
)
|
|
|
|
return promise
|
|
}
|
|
promise.done2 { _ in
|
|
getSnodePoolPromise.mutate { $0 = nil }
|
|
}
|
|
promise.catch2 { _ in
|
|
getSnodePoolPromise.mutate { $0 = nil }
|
|
}
|
|
|
|
return promise
|
|
}
|
|
|
|
public static func getSessionID(for onsName: String) -> Promise<String> {
|
|
let validationCount = 3
|
|
let sessionIDByteCount = 33
|
|
// The name must be lowercased
|
|
let onsName = onsName.lowercased()
|
|
// Hash the ONS name using BLAKE2b
|
|
let nameAsData = [UInt8](onsName.data(using: String.Encoding.utf8)!)
|
|
|
|
guard let nameHash = sodium.genericHash.hash(message: nameAsData) else {
|
|
return Promise(error: SnodeAPIError.hashingFailed)
|
|
}
|
|
|
|
// Ask 3 different snodes for the Session ID associated with the given name hash
|
|
let base64EncodedNameHash = nameHash.toBase64()
|
|
let parameters: [String:Any] = [
|
|
"endpoint" : "ons_resolve",
|
|
"params" : [
|
|
"type" : 0, // type 0 means Session
|
|
"name_hash" : base64EncodedNameHash
|
|
]
|
|
]
|
|
let promises = (0..<validationCount).map { _ in
|
|
return getRandomSnode().then2 { snode in
|
|
attempt(maxRetryCount: 4, recoveringOn: Threading.workQueue) {
|
|
invoke(.oxenDaemonRPCCall, on: snode, parameters: parameters)
|
|
}
|
|
}
|
|
}
|
|
let (promise, seal) = Promise<String>.pending()
|
|
|
|
when(resolved: promises).done2 { results in
|
|
var sessionIDs: [String] = []
|
|
for result in results {
|
|
switch result {
|
|
case .rejected(let error): return seal.reject(error)
|
|
|
|
case .fulfilled(let responseData):
|
|
guard let responseJson: JSON = try? JSONSerialization.jsonObject(with: responseData, options: [ .fragmentsAllowed ]) as? JSON else {
|
|
throw HTTP.Error.invalidJSON
|
|
}
|
|
guard
|
|
let intermediate = responseJson["result"] as? JSON,
|
|
let hexEncodedCiphertext = intermediate["encrypted_value"] as? String
|
|
else { return seal.reject(HTTP.Error.invalidJSON) }
|
|
|
|
let ciphertext = [UInt8](Data(hex: hexEncodedCiphertext))
|
|
let isArgon2Based = (intermediate["nonce"] == nil)
|
|
|
|
if isArgon2Based {
|
|
// Handle old Argon2-based encryption used before HF16
|
|
let salt = [UInt8](Data(repeating: 0, count: sodium.pwHash.SaltBytes))
|
|
guard
|
|
let key = sodium.pwHash.hash(
|
|
outputLength: sodium.secretBox.KeyBytes,
|
|
passwd: nameAsData,
|
|
salt: salt,
|
|
opsLimit: sodium.pwHash.OpsLimitModerate,
|
|
memLimit: sodium.pwHash.MemLimitModerate,
|
|
alg: .Argon2ID13
|
|
)
|
|
else { return seal.reject(SnodeAPIError.hashingFailed) }
|
|
|
|
let nonce = [UInt8](Data(repeating: 0, count: sodium.secretBox.NonceBytes))
|
|
|
|
guard let sessionIDAsData = sodium.secretBox.open(authenticatedCipherText: ciphertext, secretKey: key, nonce: nonce) else {
|
|
return seal.reject(SnodeAPIError.decryptionFailed)
|
|
}
|
|
|
|
sessionIDs.append(sessionIDAsData.toHexString())
|
|
}
|
|
else {
|
|
guard let hexEncodedNonce = intermediate["nonce"] as? String else {
|
|
return seal.reject(HTTP.Error.invalidJSON)
|
|
}
|
|
|
|
let nonce = [UInt8](Data(hex: hexEncodedNonce))
|
|
|
|
// xchacha-based encryption
|
|
guard let key = sodium.genericHash.hash(message: nameAsData, key: nameHash) else { // key = H(name, key=H(name))
|
|
return seal.reject(SnodeAPIError.hashingFailed)
|
|
}
|
|
guard ciphertext.count >= (sessionIDByteCount + sodium.aead.xchacha20poly1305ietf.ABytes) else { // Should always be equal in practice
|
|
return seal.reject(SnodeAPIError.decryptionFailed)
|
|
}
|
|
guard let sessionIDAsData = sodium.aead.xchacha20poly1305ietf.decrypt(authenticatedCipherText: ciphertext, secretKey: key, nonce: nonce) else {
|
|
return seal.reject(SnodeAPIError.decryptionFailed)
|
|
}
|
|
|
|
sessionIDs.append(sessionIDAsData.toHexString())
|
|
}
|
|
}
|
|
}
|
|
|
|
guard sessionIDs.count == validationCount && Set(sessionIDs).count == 1 else {
|
|
return seal.reject(SnodeAPIError.validationFailed)
|
|
}
|
|
|
|
seal.fulfill(sessionIDs.first!)
|
|
}
|
|
|
|
return promise
|
|
}
|
|
|
|
public static func getTargetSnodes(for publicKey: String) -> Promise<[Snode]> {
|
|
// shuffled() uses the system's default random generator, which is cryptographically secure
|
|
return getSwarm(for: publicKey).map2 { Array($0.shuffled().prefix(targetSwarmSnodeCount)) }
|
|
}
|
|
|
|
public static func getSwarm(for publicKey: String) -> Promise<Set<Snode>> {
|
|
loadSwarmIfNeeded(for: publicKey)
|
|
|
|
if let cachedSwarm = swarmCache.wrappedValue[publicKey], cachedSwarm.count >= minSwarmSnodeCount {
|
|
return Promise<Set<Snode>> { $0.fulfill(cachedSwarm) }
|
|
}
|
|
|
|
SNLog("Getting swarm for: \((publicKey == getUserHexEncodedPublicKey()) ? "self" : publicKey).")
|
|
let parameters: [String: Any] = [
|
|
"pubKey": (Features.useTestnet ? publicKey.removingIdPrefixIfNeeded() : publicKey)
|
|
]
|
|
|
|
return getRandomSnode()
|
|
.then2 { snode in
|
|
attempt(maxRetryCount: 4, recoveringOn: Threading.workQueue) {
|
|
invoke(.getSwarm, on: snode, associatedWith: publicKey, parameters: parameters)
|
|
}
|
|
}
|
|
.map2 { responseData in
|
|
let swarm = parseSnodes(from: responseData)
|
|
|
|
setSwarm(to: swarm, for: publicKey)
|
|
return swarm
|
|
}
|
|
}
|
|
|
|
// MARK: - Retrieve
|
|
|
|
// Not in use until we can batch delete and store config messages
|
|
public static func getConfigMessages(from snode: Snode, associatedWith publicKey: String) -> Promise<([SnodeReceivedMessage], String?)> {
|
|
let (promise, seal) = Promise<([SnodeReceivedMessage], String?)>.pending()
|
|
|
|
Threading.workQueue.async {
|
|
getMessagesWithAuthentication(from: snode, associatedWith: publicKey, namespace: configNamespace)
|
|
.done2 {
|
|
seal.fulfill($0)
|
|
}
|
|
.catch2 {
|
|
seal.reject($0)
|
|
}
|
|
}
|
|
|
|
return promise
|
|
}
|
|
|
|
public static func getMessages(from snode: Snode, associatedWith publicKey: String, authenticated: Bool = true) -> Promise<([SnodeReceivedMessage], String?)> {
|
|
let (promise, seal) = Promise<([SnodeReceivedMessage], String?)>.pending()
|
|
|
|
Threading.workQueue.async {
|
|
let retrievePromise = (authenticated ?
|
|
getMessagesWithAuthentication(from: snode, associatedWith: publicKey, namespace: defaultNamespace) :
|
|
getMessagesUnauthenticated(from: snode, associatedWith: publicKey)
|
|
)
|
|
|
|
retrievePromise
|
|
.done2 { seal.fulfill($0) }
|
|
.catch2 { seal.reject($0) }
|
|
}
|
|
|
|
return promise
|
|
}
|
|
|
|
public static func getClosedGroupMessagesFromDefaultNamespace(from snode: Snode, associatedWith publicKey: String) -> Promise<([SnodeReceivedMessage], String?)> {
|
|
let (promise, seal) = Promise<([SnodeReceivedMessage], String?)>.pending()
|
|
|
|
Threading.workQueue.async {
|
|
getMessagesUnauthenticated(from: snode, associatedWith: publicKey, namespace: defaultNamespace)
|
|
.done2 { seal.fulfill($0) }
|
|
.catch2 { seal.reject($0) }
|
|
}
|
|
|
|
return promise
|
|
}
|
|
|
|
private static func getMessagesWithAuthentication(from snode: Snode, associatedWith publicKey: String, namespace: Int) -> Promise<([SnodeReceivedMessage], String?)> {
|
|
/// **Note:** All authentication logic is only apply to 1-1 chats, the reason being that we can't currently support it yet for
|
|
/// closed groups. The Storage Server requires an ed25519 key pair, but we don't have that for our closed groups.
|
|
guard let userED25519KeyPair: Box.KeyPair = Storage.shared.read({ db in Identity.fetchUserEd25519KeyPair(db) }) else {
|
|
return Promise(error: SnodeAPIError.noKeyPair)
|
|
}
|
|
|
|
// Get last message hash
|
|
SnodeReceivedMessageInfo.pruneExpiredMessageHashInfo(for: snode, namespace: namespace, associatedWith: publicKey)
|
|
let lastHash = SnodeReceivedMessageInfo.fetchLastNotExpired(for: snode, namespace: namespace, associatedWith: publicKey)?.hash ?? ""
|
|
|
|
// Construct signature
|
|
let timestamp = UInt64(Int64(floor(Date().timeIntervalSince1970 * 1000)) + SnodeAPI.clockOffset.wrappedValue)
|
|
let ed25519PublicKey = userED25519KeyPair.publicKey.toHexString()
|
|
let namespaceVerificationString = (namespace == defaultNamespace ? "" : String(namespace))
|
|
|
|
guard
|
|
let verificationData = ("retrieve" + namespaceVerificationString + String(timestamp)).data(using: String.Encoding.utf8),
|
|
let signature = sodium.sign.signature(message: Bytes(verificationData), secretKey: userED25519KeyPair.secretKey)
|
|
else { return Promise(error: SnodeAPIError.signingFailed) }
|
|
|
|
// Make the request
|
|
let parameters: JSON = [
|
|
"pubKey": Features.useTestnet ? publicKey.removingIdPrefixIfNeeded() : publicKey,
|
|
"namespace": namespace,
|
|
"lastHash": lastHash,
|
|
"timestamp": timestamp,
|
|
"pubkey_ed25519": ed25519PublicKey,
|
|
"signature": signature.toBase64()
|
|
]
|
|
|
|
return invoke(.getMessages, on: snode, associatedWith: publicKey, parameters: parameters)
|
|
.map { responseData -> [SnodeReceivedMessage] in
|
|
guard
|
|
let responseJson: JSON = try? JSONSerialization.jsonObject(with: responseData, options: [ .fragmentsAllowed ]) as? JSON,
|
|
let rawMessages: [JSON] = responseJson["messages"] as? [JSON]
|
|
else {
|
|
return []
|
|
}
|
|
|
|
return rawMessages
|
|
.compactMap { rawMessage -> SnodeReceivedMessage? in
|
|
SnodeReceivedMessage(
|
|
snode: snode,
|
|
publicKey: publicKey,
|
|
namespace: namespace,
|
|
rawMessage: rawMessage
|
|
)
|
|
}
|
|
}
|
|
.map { ($0, lastHash) }
|
|
}
|
|
|
|
private static func getMessagesUnauthenticated(
|
|
from snode: Snode,
|
|
associatedWith publicKey: String,
|
|
namespace: Int = closedGroupNamespace
|
|
) -> Promise<([SnodeReceivedMessage], String?)> {
|
|
// Get last message hash
|
|
SnodeReceivedMessageInfo.pruneExpiredMessageHashInfo(for: snode, namespace: namespace, associatedWith: publicKey)
|
|
let lastHash = SnodeReceivedMessageInfo.fetchLastNotExpired(for: snode, namespace: namespace, associatedWith: publicKey)?.hash ?? ""
|
|
|
|
// Make the request
|
|
var parameters: JSON = [
|
|
"pubKey": (Features.useTestnet ? publicKey.removingIdPrefixIfNeeded() : publicKey),
|
|
"lastHash": lastHash
|
|
]
|
|
|
|
// Don't include namespace if polling for 0 with no authentication
|
|
if namespace != defaultNamespace {
|
|
parameters["namespace"] = namespace
|
|
}
|
|
|
|
return invoke(.getMessages, on: snode, associatedWith: publicKey, parameters: parameters)
|
|
.map { responseData -> [SnodeReceivedMessage] in
|
|
guard
|
|
let responseJson: JSON = try? JSONSerialization.jsonObject(with: responseData, options: [ .fragmentsAllowed ]) as? JSON,
|
|
let rawMessages: [JSON] = responseJson["messages"] as? [JSON]
|
|
else {
|
|
return []
|
|
}
|
|
|
|
return rawMessages
|
|
.compactMap { rawMessage -> SnodeReceivedMessage? in
|
|
SnodeReceivedMessage(
|
|
snode: snode,
|
|
publicKey: publicKey,
|
|
namespace: namespace,
|
|
rawMessage: rawMessage
|
|
)
|
|
}
|
|
}
|
|
.map { ($0, lastHash) }
|
|
}
|
|
|
|
// MARK: Store
|
|
|
|
public static func sendMessage(_ message: SnodeMessage, isClosedGroupMessage: Bool, isConfigMessage: Bool) -> Promise<Set<Promise<Data>>> {
|
|
return sendMessageUnauthenticated(message, isClosedGroupMessage: isClosedGroupMessage)
|
|
}
|
|
|
|
// Not in use until we can batch delete and store config messages
|
|
private static func sendMessageWithAuthentication(_ message: SnodeMessage, namespace: Int) -> Promise<Set<Promise<Data>>> {
|
|
guard
|
|
let messageData: Data = try? JSONEncoder().encode(message),
|
|
let messageJson: JSON = try? JSONSerialization.jsonObject(with: messageData, options: [ .fragmentsAllowed ]) as? JSON
|
|
else { return Promise(error: HTTP.Error.invalidJSON) }
|
|
|
|
guard let userED25519KeyPair: Box.KeyPair = Storage.shared.read({ db in Identity.fetchUserEd25519KeyPair(db) }) else {
|
|
return Promise(error: SnodeAPIError.noKeyPair)
|
|
}
|
|
|
|
// Construct signature
|
|
let timestamp = UInt64(Int64(floor(Date().timeIntervalSince1970 * 1000)) + SnodeAPI.clockOffset.wrappedValue)
|
|
let ed25519PublicKey = userED25519KeyPair.publicKey.toHexString()
|
|
|
|
guard
|
|
let verificationData = ("store" + String(namespace) + String(timestamp)).data(using: String.Encoding.utf8),
|
|
let signature = sodium.sign.signature(message: Bytes(verificationData), secretKey: userED25519KeyPair.secretKey)
|
|
else { return Promise(error: SnodeAPIError.signingFailed) }
|
|
|
|
// Make the request
|
|
let (promise, seal) = Promise<Set<Promise<Data>>>.pending()
|
|
let publicKey = (Features.useTestnet ? message.recipient.removingIdPrefixIfNeeded() : message.recipient)
|
|
|
|
Threading.workQueue.async {
|
|
getTargetSnodes(for: publicKey)
|
|
.map2 { targetSnodes in
|
|
var parameters: JSON = messageJson
|
|
parameters["namespace"] = namespace
|
|
parameters["sig_timestamp"] = timestamp
|
|
parameters["pubkey_ed25519"] = ed25519PublicKey
|
|
parameters["signature"] = signature.toBase64()
|
|
|
|
return Set(targetSnodes.map { targetSnode in
|
|
attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) {
|
|
invoke(.sendMessage, on: targetSnode, associatedWith: publicKey, parameters: parameters)
|
|
}
|
|
})
|
|
}
|
|
.done2 { seal.fulfill($0) }
|
|
.catch2 { seal.reject($0) }
|
|
}
|
|
|
|
return promise
|
|
}
|
|
|
|
private static func sendMessageUnauthenticated(_ message: SnodeMessage, isClosedGroupMessage: Bool) -> Promise<Set<Promise<Data>>> {
|
|
guard
|
|
let messageData: Data = try? JSONEncoder().encode(message),
|
|
let messageJson: JSON = try? JSONSerialization.jsonObject(with: messageData, options: [ .fragmentsAllowed ]) as? JSON
|
|
else { return Promise(error: HTTP.Error.invalidJSON) }
|
|
|
|
let (promise, seal) = Promise<Set<Promise<Data>>>.pending()
|
|
let publicKey = Features.useTestnet ? message.recipient.removingIdPrefixIfNeeded() : message.recipient
|
|
|
|
Threading.workQueue.async {
|
|
getTargetSnodes(for: publicKey)
|
|
.map2 { targetSnodes in
|
|
var rawResponsePromises: Set<Promise<Data>> = Set()
|
|
var parameters: JSON = messageJson
|
|
parameters["namespace"] = (isClosedGroupMessage ? closedGroupNamespace : defaultNamespace)
|
|
|
|
for targetSnode in targetSnodes {
|
|
let rawResponsePromise = attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) {
|
|
invoke(.sendMessage, on: targetSnode, associatedWith: publicKey, parameters: parameters)
|
|
}
|
|
rawResponsePromises.insert(rawResponsePromise)
|
|
}
|
|
|
|
// Send closed group messages to default namespace as well
|
|
if hardfork == 19 && softfork == 0 && isClosedGroupMessage {
|
|
parameters["namespace"] = defaultNamespace
|
|
for targetSnode in targetSnodes {
|
|
let rawResponsePromise = attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) {
|
|
invoke(.sendMessage, on: targetSnode, associatedWith: publicKey, parameters: parameters)
|
|
}
|
|
rawResponsePromises.insert(rawResponsePromise)
|
|
}
|
|
}
|
|
|
|
return rawResponsePromises
|
|
}
|
|
.done2 { seal.fulfill($0) }
|
|
.catch2 { seal.reject($0) }
|
|
}
|
|
|
|
return promise
|
|
}
|
|
|
|
// MARK: Edit
|
|
|
|
public static func updateExpiry(
|
|
publicKey: String,
|
|
edKeyPair: Box.KeyPair,
|
|
updatedExpiryMs: UInt64,
|
|
serverHashes: [String]
|
|
) -> Promise<[String: (hashes: [String], expiry: UInt64)]> {
|
|
let publicKey = (Features.useTestnet ? publicKey.removingIdPrefixIfNeeded() : publicKey)
|
|
|
|
return attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) {
|
|
getSwarm(for: publicKey)
|
|
.then2 { swarm -> Promise<[String: (hashes: [String], expiry: UInt64)]> in
|
|
// "expire" || expiry || messages[0] || ... || messages[N]
|
|
let verificationBytes = SnodeAPIEndpoint.expire.rawValue.bytes
|
|
.appending(contentsOf: "\(updatedExpiryMs)".data(using: .ascii)?.bytes)
|
|
.appending(contentsOf: serverHashes.joined().bytes)
|
|
|
|
guard
|
|
let snode = swarm.randomElement(),
|
|
let signature = sodium.sign.signature(
|
|
message: verificationBytes,
|
|
secretKey: edKeyPair.secretKey
|
|
)
|
|
else {
|
|
throw SnodeAPIError.signingFailed
|
|
}
|
|
|
|
let parameters: JSON = [
|
|
"pubkey" : publicKey,
|
|
"pubkey_ed25519" : edKeyPair.publicKey.toHexString(),
|
|
"expiry": updatedExpiryMs,
|
|
"messages": serverHashes,
|
|
"signature": signature.toBase64()
|
|
]
|
|
|
|
return attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) {
|
|
invoke(.expire, on: snode, associatedWith: publicKey, parameters: parameters)
|
|
.map2 { responseData -> [String: (hashes: [String], expiry: UInt64)] in
|
|
guard let responseJson: JSON = try? JSONSerialization.jsonObject(with: responseData, options: [ .fragmentsAllowed ]) as? JSON else {
|
|
throw HTTP.Error.invalidJSON
|
|
}
|
|
guard let swarm = responseJson["swarm"] as? JSON else { throw HTTP.Error.invalidJSON }
|
|
|
|
var result: [String: (hashes: [String], expiry: UInt64)] = [:]
|
|
|
|
for (snodePublicKey, rawJSON) in swarm {
|
|
guard let json = rawJSON as? JSON else { throw HTTP.Error.invalidJSON }
|
|
guard (json["failed"] as? Bool ?? false) == false else {
|
|
if let reason = json["reason"] as? String, let statusCode = json["code"] as? String {
|
|
SNLog("Couldn't delete data from: \(snodePublicKey) due to error: \(reason) (\(statusCode)).")
|
|
}
|
|
else {
|
|
SNLog("Couldn't delete data from: \(snodePublicKey).")
|
|
}
|
|
result[snodePublicKey] = ([], 0)
|
|
continue
|
|
}
|
|
|
|
guard
|
|
let hashes: [String] = json["updated"] as? [String],
|
|
let expiryApplied: UInt64 = json["expiry"] as? UInt64,
|
|
let signature: String = json["signature"] as? String
|
|
else {
|
|
throw HTTP.Error.invalidJSON
|
|
}
|
|
|
|
// The signature format is ( PUBKEY_HEX || EXPIRY || RMSG[0] || ... || RMSG[N] || UMSG[0] || ... || UMSG[M] )
|
|
let verificationBytes = publicKey.bytes
|
|
.appending(contentsOf: "\(expiryApplied)".data(using: .ascii)?.bytes)
|
|
.appending(contentsOf: serverHashes.joined().bytes)
|
|
.appending(contentsOf: hashes.joined().bytes)
|
|
let isValid = sodium.sign.verify(
|
|
message: verificationBytes,
|
|
publicKey: Bytes(Data(hex: snodePublicKey)),
|
|
signature: Bytes(Data(base64Encoded: signature)!)
|
|
)
|
|
|
|
// Ensure the signature is valid
|
|
guard isValid else {
|
|
throw SnodeAPIError.signatureVerificationFailed
|
|
}
|
|
|
|
result[snodePublicKey] = (hashes, expiryApplied)
|
|
}
|
|
|
|
return result
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: Delete
|
|
|
|
public static func deleteMessage(publicKey: String, serverHashes: [String]) -> Promise<[String: Bool]> {
|
|
guard let userED25519KeyPair = Identity.fetchUserEd25519KeyPair() else {
|
|
return Promise(error: SnodeAPIError.noKeyPair)
|
|
}
|
|
|
|
let publicKey = (Features.useTestnet ? publicKey.removingIdPrefixIfNeeded() : publicKey)
|
|
let userX25519PublicKey: String = getUserHexEncodedPublicKey()
|
|
|
|
return attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) {
|
|
getSwarm(for: publicKey)
|
|
.then2 { swarm -> Promise<[String: Bool]> in
|
|
// "delete" || messages...
|
|
let verificationBytes = SnodeAPIEndpoint.deleteMessage.rawValue.bytes
|
|
.appending(contentsOf: serverHashes.joined().bytes)
|
|
|
|
guard
|
|
let snode = swarm.randomElement(),
|
|
let signature = sodium.sign.signature(
|
|
message: verificationBytes,
|
|
secretKey: userED25519KeyPair.secretKey
|
|
)
|
|
else {
|
|
throw SnodeAPIError.signingFailed
|
|
}
|
|
|
|
let parameters: JSON = [
|
|
"pubkey" : userX25519PublicKey,
|
|
"pubkey_ed25519" : userED25519KeyPair.publicKey.toHexString(),
|
|
"messages": serverHashes,
|
|
"signature": signature.toBase64()
|
|
]
|
|
|
|
return attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) {
|
|
invoke(.deleteMessage, on: snode, associatedWith: publicKey, parameters: parameters)
|
|
.map2 { responseData -> [String: Bool] in
|
|
guard let responseJson: JSON = try? JSONSerialization.jsonObject(with: responseData, options: [ .fragmentsAllowed ]) as? JSON else {
|
|
throw HTTP.Error.invalidJSON
|
|
}
|
|
guard let swarm = responseJson["swarm"] as? JSON else { throw HTTP.Error.invalidJSON }
|
|
|
|
var result: [String: Bool] = [:]
|
|
|
|
for (snodePublicKey, rawJSON) in swarm {
|
|
guard let json = rawJSON as? JSON else { throw HTTP.Error.invalidJSON }
|
|
|
|
let isFailed = (json["failed"] as? Bool ?? false)
|
|
|
|
if !isFailed {
|
|
guard
|
|
let hashes = json["deleted"] as? [String],
|
|
let signature = json["signature"] as? String
|
|
else {
|
|
throw HTTP.Error.invalidJSON
|
|
}
|
|
|
|
// The signature format is ( PUBKEY_HEX || RMSG[0] || ... || RMSG[N] || DMSG[0] || ... || DMSG[M] )
|
|
let verificationBytes = userX25519PublicKey.bytes
|
|
.appending(contentsOf: serverHashes.joined().bytes)
|
|
.appending(contentsOf: hashes.joined().bytes)
|
|
let isValid = sodium.sign.verify(
|
|
message: verificationBytes,
|
|
publicKey: Bytes(Data(hex: snodePublicKey)),
|
|
signature: Bytes(Data(base64Encoded: signature)!)
|
|
)
|
|
|
|
result[snodePublicKey] = isValid
|
|
}
|
|
else {
|
|
if let reason = json["reason"] as? String, let statusCode = json["code"] as? String {
|
|
SNLog("Couldn't delete data from: \(snodePublicKey) due to error: \(reason) (\(statusCode)).")
|
|
}
|
|
else {
|
|
SNLog("Couldn't delete data from: \(snodePublicKey).")
|
|
}
|
|
result[snodePublicKey] = false
|
|
}
|
|
}
|
|
|
|
// If we get to here then we assume it's been deleted from at least one
|
|
// service node and as a result we need to mark the hash as invalid so
|
|
// we don't try to fetch updates since that hash going forward (if we do
|
|
// we would end up re-fetching all old messages)
|
|
Storage.shared.writeAsync { db in
|
|
try? SnodeReceivedMessageInfo.handlePotentialDeletedOrInvalidHash(
|
|
db,
|
|
potentiallyInvalidHashes: serverHashes
|
|
)
|
|
}
|
|
|
|
return result
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Clears all the user's data from their swarm. Returns a dictionary of snode public key to deletion confirmation.
|
|
public static func clearAllData() -> Promise<[String:Bool]> {
|
|
guard let userED25519KeyPair = Identity.fetchUserEd25519KeyPair() else {
|
|
return Promise(error: SnodeAPIError.noKeyPair)
|
|
}
|
|
|
|
let userX25519PublicKey: String = getUserHexEncodedPublicKey()
|
|
|
|
return attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) {
|
|
getSwarm(for: userX25519PublicKey)
|
|
.then2 { swarm -> Promise<[String:Bool]> in
|
|
let snode = swarm.randomElement()!
|
|
|
|
return attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) {
|
|
getNetworkTime(from: snode).then2 { timestamp -> Promise<[String: Bool]> in
|
|
let verificationData = (SnodeAPIEndpoint.clearAllData.rawValue + String(timestamp)).data(using: String.Encoding.utf8)!
|
|
|
|
guard let signature = sodium.sign.signature(message: Bytes(verificationData), secretKey: userED25519KeyPair.secretKey) else {
|
|
throw SnodeAPIError.signingFailed
|
|
}
|
|
|
|
let parameters: JSON = [
|
|
"pubkey": userX25519PublicKey,
|
|
"pubkey_ed25519": userED25519KeyPair.publicKey.toHexString(),
|
|
"timestamp": timestamp,
|
|
"signature": signature.toBase64()
|
|
]
|
|
|
|
return attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) {
|
|
invoke(.clearAllData, on: snode, parameters: parameters)
|
|
.map2 { responseData -> [String: Bool] in
|
|
guard let responseJson: JSON = try? JSONSerialization.jsonObject(with: responseData, options: [ .fragmentsAllowed ]) as? JSON else {
|
|
throw HTTP.Error.invalidJSON
|
|
}
|
|
guard let swarm = responseJson["swarm"] as? JSON else { throw HTTP.Error.invalidJSON }
|
|
|
|
var result: [String: Bool] = [:]
|
|
|
|
for (snodePublicKey, rawJSON) in swarm {
|
|
guard let json = rawJSON as? JSON else { throw HTTP.Error.invalidJSON }
|
|
|
|
let isFailed = json["failed"] as? Bool ?? false
|
|
|
|
if !isFailed {
|
|
guard
|
|
let hashes = json["deleted"] as? [String],
|
|
let signature = json["signature"] as? String
|
|
else { throw HTTP.Error.invalidJSON }
|
|
|
|
// The signature format is ( PUBKEY_HEX || TIMESTAMP || DELETEDHASH[0] || ... || DELETEDHASH[N] )
|
|
let verificationData = [
|
|
userX25519PublicKey,
|
|
String(timestamp),
|
|
hashes.joined()
|
|
]
|
|
.joined()
|
|
.data(using: String.Encoding.utf8)!
|
|
let isValid = sodium.sign.verify(
|
|
message: Bytes(verificationData),
|
|
publicKey: Bytes(Data(hex: snodePublicKey)),
|
|
signature: Bytes(Data(base64Encoded: signature)!)
|
|
)
|
|
|
|
result[snodePublicKey] = isValid
|
|
}
|
|
else {
|
|
if let reason = json["reason"] as? String, let statusCode = json["code"] as? String {
|
|
SNLog("Couldn't delete data from: \(snodePublicKey) due to error: \(reason) (\(statusCode)).")
|
|
} else {
|
|
SNLog("Couldn't delete data from: \(snodePublicKey).")
|
|
}
|
|
|
|
result[snodePublicKey] = false
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: Parsing
|
|
|
|
// The parsing utilities below use a best attempt approach to parsing; they warn for parsing failures but don't throw exceptions.
|
|
|
|
private static func parseSnodes(from responseData: Data) -> Set<Snode> {
|
|
guard let responseJson: JSON = try? JSONSerialization.jsonObject(with: responseData, options: [ .fragmentsAllowed ]) as? JSON else {
|
|
SNLog("Failed to parse snodes from response data.")
|
|
return []
|
|
}
|
|
guard let rawSnodes = responseJson["snodes"] as? [JSON] else {
|
|
SNLog("Failed to parse snodes from: \(responseJson).")
|
|
return []
|
|
}
|
|
|
|
guard let snodeData: Data = try? JSONSerialization.data(withJSONObject: rawSnodes, options: []) else {
|
|
return []
|
|
}
|
|
|
|
// FIXME: Hopefully at some point this different Snode structure will be deprecated and can be removed
|
|
if
|
|
let swarmSnodes: [SwarmSnode] = try? JSONDecoder().decode([Failable<SwarmSnode>].self, from: snodeData).compactMap({ $0.value }),
|
|
!swarmSnodes.isEmpty
|
|
{
|
|
return swarmSnodes.map { $0.toSnode() }.asSet()
|
|
}
|
|
|
|
return ((try? JSONDecoder().decode([Failable<Snode>].self, from: snodeData)) ?? [])
|
|
.compactMap { $0.value }
|
|
.asSet()
|
|
}
|
|
|
|
// MARK: Error Handling
|
|
|
|
/// - Note: Should only be invoked from `Threading.workQueue` to avoid race conditions.
|
|
@discardableResult
|
|
internal static func handleError(withStatusCode statusCode: UInt, data: Data?, forSnode snode: Snode, associatedWith publicKey: String? = nil) -> Error? {
|
|
#if DEBUG
|
|
dispatchPrecondition(condition: .onQueue(Threading.workQueue))
|
|
#endif
|
|
func handleBadSnode() {
|
|
let oldFailureCount = (SnodeAPI.snodeFailureCount.wrappedValue[snode] ?? 0)
|
|
let newFailureCount = oldFailureCount + 1
|
|
SnodeAPI.snodeFailureCount.mutate { $0[snode] = newFailureCount }
|
|
SNLog("Couldn't reach snode at: \(snode); setting failure count to \(newFailureCount).")
|
|
if newFailureCount >= SnodeAPI.snodeFailureThreshold {
|
|
SNLog("Failure threshold reached for: \(snode); dropping it.")
|
|
if let publicKey = publicKey {
|
|
SnodeAPI.dropSnodeFromSwarmIfNeeded(snode, publicKey: publicKey)
|
|
}
|
|
SnodeAPI.dropSnodeFromSnodePool(snode)
|
|
SNLog("Snode pool count: \(snodePool.wrappedValue.count).")
|
|
SnodeAPI.snodeFailureCount.mutate { $0[snode] = 0 }
|
|
}
|
|
}
|
|
|
|
switch statusCode {
|
|
case 500, 502, 503:
|
|
// The snode is unreachable
|
|
handleBadSnode()
|
|
|
|
case 404:
|
|
// May caused by invalid open groups
|
|
SNLog("Can't reach the server.")
|
|
|
|
case 406:
|
|
SNLog("The user's clock is out of sync with the service node network.")
|
|
return SnodeAPIError.clockOutOfSync
|
|
|
|
case 421:
|
|
// The snode isn't associated with the given public key anymore
|
|
if let publicKey = publicKey {
|
|
func invalidateSwarm() {
|
|
SNLog("Invalidating swarm for: \(publicKey).")
|
|
SnodeAPI.dropSnodeFromSwarmIfNeeded(snode, publicKey: publicKey)
|
|
}
|
|
|
|
if let data: Data = data {
|
|
let snodes = parseSnodes(from: data)
|
|
|
|
if !snodes.isEmpty {
|
|
setSwarm(to: snodes, for: publicKey)
|
|
}
|
|
else {
|
|
invalidateSwarm()
|
|
}
|
|
}
|
|
else {
|
|
invalidateSwarm()
|
|
}
|
|
}
|
|
else {
|
|
SNLog("Got a 421 without an associated public key.")
|
|
}
|
|
|
|
default:
|
|
handleBadSnode()
|
|
SNLog("Unhandled response code: \(statusCode).")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|