|
|
|
@ -23,6 +23,15 @@ public final class SnodeAPI : NSObject {
|
|
|
|
|
/// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
|
|
|
|
|
public static var swarmCache: [String:Set<Snode>] = [:]
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
@ -39,6 +48,7 @@ public final class SnodeAPI : NSObject {
|
|
|
|
|
case inconsistentSnodePools
|
|
|
|
|
case noKeyPair
|
|
|
|
|
case signingFailed
|
|
|
|
|
case signatureVerificationFailed
|
|
|
|
|
// ONS
|
|
|
|
|
case decryptionFailed
|
|
|
|
|
case hashingFailed
|
|
|
|
@ -52,6 +62,7 @@ public final class SnodeAPI : NSObject {
|
|
|
|
|
case .inconsistentSnodePools: return "Received inconsistent Service Node pool information from the Service Node network."
|
|
|
|
|
case .noKeyPair: return "Missing user key pair."
|
|
|
|
|
case .signingFailed: return "Couldn't sign message."
|
|
|
|
|
case . signatureVerificationFailed: return "Failed to verify the signature."
|
|
|
|
|
// ONS
|
|
|
|
|
case .decryptionFailed: return "Couldn't decrypt ONS name."
|
|
|
|
|
case .hashingFailed: return "Couldn't compute ONS name hash."
|
|
|
|
@ -131,7 +142,22 @@ public final class SnodeAPI : NSObject {
|
|
|
|
|
// MARK: Internal API
|
|
|
|
|
internal static func invoke(_ method: Snode.Method, on snode: Snode, associatedWith publicKey: String? = nil, parameters: JSON) -> RawResponsePromise {
|
|
|
|
|
if Features.useOnionRequests {
|
|
|
|
|
return OnionRequestAPI.sendOnionRequest(to: snode, invoking: method, with: parameters, associatedWith: publicKey).map2 { $0 as Any }
|
|
|
|
|
return OnionRequestAPI.sendOnionRequest(to: snode, invoking: method, with: parameters, associatedWith: publicKey)
|
|
|
|
|
.map2 { json in
|
|
|
|
|
if let hf = json["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 json as Any
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
let url = "\(snode.address):\(snode.port)/storage_rpc/v1"
|
|
|
|
|
return HTTP.execute(.post, url, parameters: parameters).map2 { $0 as Any }.recover2 { error -> Promise<Any> in
|
|
|
|
@ -390,47 +416,131 @@ public final class SnodeAPI : NSObject {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static func getRawMessages(from snode: Snode, associatedWith publicKey: String) -> RawResponsePromise {
|
|
|
|
|
// MARK: Retrieve
|
|
|
|
|
// Not in use until we can batch delete and store config messages
|
|
|
|
|
public static func getConfigMessages(from snode: Snode, associatedWith publicKey: String) -> RawResponsePromise {
|
|
|
|
|
let (promise, seal) = RawResponsePromise.pending()
|
|
|
|
|
Threading.workQueue.async {
|
|
|
|
|
getMessagesWithAuthentication(from: snode, associatedWith: publicKey, namespace: configNamespace).done2 {
|
|
|
|
|
seal.fulfill($0)
|
|
|
|
|
}.catch2 {
|
|
|
|
|
seal.reject($0)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return promise
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static func getRawMessages(from snode: Snode, associatedWith publicKey: String, authenticated: Bool = true) -> RawResponsePromise {
|
|
|
|
|
let (promise, seal) = RawResponsePromise.pending()
|
|
|
|
|
Threading.workQueue.async {
|
|
|
|
|
getMessagesInternal(from: snode, associatedWith: publicKey).done2 { seal.fulfill($0) }.catch2 { seal.reject($0) }
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static func getMessagesInternal(from snode: Snode, associatedWith publicKey: String) -> RawResponsePromise {
|
|
|
|
|
public static func getRawClosedGroupMessagesFromDefaultNamespace(from snode: Snode, associatedWith publicKey: String) -> RawResponsePromise {
|
|
|
|
|
let (promise, seal) = RawResponsePromise.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) -> RawResponsePromise {
|
|
|
|
|
let storage = SNSnodeKitConfiguration.shared.storage
|
|
|
|
|
|
|
|
|
|
// NOTE: All authentication logic is currently commented out, the reason being that we can't currently support
|
|
|
|
|
// 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 = storage.getUserED25519KeyPair() else { return Promise(error: Error.noKeyPair) }
|
|
|
|
|
guard let userED25519KeyPair = storage.getUserED25519KeyPair() else { return Promise(error: Error.noKeyPair) }
|
|
|
|
|
// Get last message hash
|
|
|
|
|
storage.pruneLastMessageHashInfoIfExpired(for: snode, associatedWith: publicKey)
|
|
|
|
|
let lastHash = storage.getLastMessageHash(for: snode, associatedWith: publicKey) ?? ""
|
|
|
|
|
storage.pruneLastMessageHashInfoIfExpired(for: snode, namespace: namespace, associatedWith: publicKey)
|
|
|
|
|
let lastHash = storage.getLastMessageHash(for: snode, namespace: namespace, associatedWith: publicKey) ?? ""
|
|
|
|
|
// Construct signature
|
|
|
|
|
// let timestamp = UInt64(Int64(NSDate.millisecondTimestamp()) + SnodeAPI.clockOffset)
|
|
|
|
|
// let ed25519PublicKey = userED25519KeyPair.publicKey.toHexString()
|
|
|
|
|
// let verificationData = ("retrieve" + String(timestamp)).data(using: String.Encoding.utf8)!
|
|
|
|
|
// let signature = sodium.sign.signature(message: Bytes(verificationData), secretKey: userED25519KeyPair.secretKey)!
|
|
|
|
|
let timestamp = UInt64(Int64(NSDate.millisecondTimestamp()) + SnodeAPI.clockOffset)
|
|
|
|
|
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: Error.signingFailed) }
|
|
|
|
|
// Make the request
|
|
|
|
|
let parameters: JSON = [
|
|
|
|
|
"pubKey" : Features.useTestnet ? publicKey.removing05PrefixIfNeeded() : publicKey,
|
|
|
|
|
"namespace": namespace,
|
|
|
|
|
"lastHash" : lastHash,
|
|
|
|
|
// "timestamp" : timestamp,
|
|
|
|
|
// "pubkey_ed25519" : ed25519PublicKey,
|
|
|
|
|
// "signature" : signature.toBase64()!
|
|
|
|
|
"timestamp" : timestamp,
|
|
|
|
|
"pubkey_ed25519" : ed25519PublicKey,
|
|
|
|
|
"signature" : signature.toBase64()
|
|
|
|
|
]
|
|
|
|
|
return invoke(.getMessages, on: snode, associatedWith: publicKey, parameters: parameters)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static func sendMessage(_ message: SnodeMessage) -> Promise<Set<RawResponsePromise>> {
|
|
|
|
|
private static func getMessagesUnauthenticated(from snode: Snode, associatedWith publicKey: String, namespace: Int = closedGroupNamespace) -> RawResponsePromise {
|
|
|
|
|
let storage = SNSnodeKitConfiguration.shared.storage
|
|
|
|
|
|
|
|
|
|
// Get last message hash
|
|
|
|
|
storage.pruneLastMessageHashInfoIfExpired(for: snode, namespace: namespace, associatedWith: publicKey)
|
|
|
|
|
let lastHash = storage.getLastMessageHash(for: snode, namespace: namespace, associatedWith: publicKey) ?? ""
|
|
|
|
|
|
|
|
|
|
// Make the request
|
|
|
|
|
var parameters: JSON = [
|
|
|
|
|
"pubKey" : Features.useTestnet ? publicKey.removing05PrefixIfNeeded() : 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)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: Store
|
|
|
|
|
|
|
|
|
|
public static func sendMessage(_ message: SnodeMessage, isClosedGroupMessage: Bool, isConfigMessage: Bool) -> Promise<Set<RawResponsePromise>> {
|
|
|
|
|
let namespace = isClosedGroupMessage ? closedGroupNamespace : defaultNamespace
|
|
|
|
|
return sendMessageUnauthenticated(message, namespace: namespace)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Not in use until we can batch delete and store config messages
|
|
|
|
|
private static func sendMessageWithAuthentication(_ message: SnodeMessage, namespace: Int) -> Promise<Set<RawResponsePromise>> {
|
|
|
|
|
let storage = SNSnodeKitConfiguration.shared.storage
|
|
|
|
|
|
|
|
|
|
guard let userED25519KeyPair = storage.getUserED25519KeyPair() else { return Promise(error: Error.noKeyPair) }
|
|
|
|
|
// Construct signature
|
|
|
|
|
let timestamp = UInt64(Int64(NSDate.millisecondTimestamp()) + SnodeAPI.clockOffset)
|
|
|
|
|
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: Error.signingFailed) }
|
|
|
|
|
// Make the request
|
|
|
|
|
let (promise, seal) = Promise<Set<RawResponsePromise>>.pending()
|
|
|
|
|
let publicKey = Features.useTestnet ? message.recipient.removing05PrefixIfNeeded() : message.recipient
|
|
|
|
|
Threading.workQueue.async {
|
|
|
|
|
getTargetSnodes(for: publicKey).map2 { targetSnodes in
|
|
|
|
|
var parameters = message.toJSON()
|
|
|
|
|
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, namespace: Int) -> Promise<Set<RawResponsePromise>> {
|
|
|
|
|
let (promise, seal) = Promise<Set<RawResponsePromise>>.pending()
|
|
|
|
|
let publicKey = Features.useTestnet ? message.recipient.removing05PrefixIfNeeded() : message.recipient
|
|
|
|
|
Threading.workQueue.async {
|
|
|
|
|
getTargetSnodes(for: publicKey).map2 { targetSnodes in
|
|
|
|
|
let parameters = message.toJSON()
|
|
|
|
|
var parameters = message.toJSON()
|
|
|
|
|
parameters["namespace"] = namespace
|
|
|
|
|
return Set(targetSnodes.map { targetSnode in
|
|
|
|
|
attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) {
|
|
|
|
|
invoke(.sendMessage, on: targetSnode, associatedWith: publicKey, parameters: parameters)
|
|
|
|
@ -441,6 +551,8 @@ public final class SnodeAPI : NSObject {
|
|
|
|
|
return promise
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: Delete
|
|
|
|
|
|
|
|
|
|
@objc(deleteMessageForPublickKey:serverHashes:)
|
|
|
|
|
public static func objc_deleteMessage(publicKey: String, serverHashes: [String]) -> AnyPromise {
|
|
|
|
|
AnyPromise.from(deleteMessage(publicKey: publicKey, serverHashes: serverHashes))
|
|
|
|
@ -568,10 +680,10 @@ public final class SnodeAPI : NSObject {
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static func updateLastMessageHashValueIfPossible(for snode: Snode, associatedWith publicKey: String, from lastRawMessage: JSON?) {
|
|
|
|
|
public static func updateLastMessageHashValueIfPossible(for snode: Snode, namespace: Int, associatedWith publicKey: String, from lastRawMessage: JSON?) {
|
|
|
|
|
if let lastMessage = lastRawMessage, let lastHash = lastMessage["hash"] as? String, let expirationDate = lastMessage["expiration"] as? UInt64 {
|
|
|
|
|
SNSnodeKitConfiguration.shared.storage.writeSync { transaction in
|
|
|
|
|
SNSnodeKitConfiguration.shared.storage.setLastMessageHashInfo(for: snode, associatedWith: publicKey,
|
|
|
|
|
SNSnodeKitConfiguration.shared.storage.setLastMessageHashInfo(for: snode, namespace: namespace, associatedWith: publicKey,
|
|
|
|
|
to: [ "hash" : lastHash, "expirationDate" : NSNumber(value: expirationDate) ], using: transaction)
|
|
|
|
|
}
|
|
|
|
|
} else if (lastRawMessage != nil) {
|
|
|
|
|