mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
Further SOGS V4 integration work
Added in the v4 onion requests logic Added in the new pin/unpin APIs Split up additional legacy methods to try and simplify the refactoring Added a number of TODOs around usage of legacy request methods
This commit is contained in:
parent
4f3900771e
commit
c90f346d6a
18 changed files with 944 additions and 253 deletions
|
@ -826,6 +826,7 @@
|
||||||
FDC4387427B5BB9B00C60D73 /* Promise+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4387327B5BB9B00C60D73 /* Promise+Utilities.swift */; };
|
FDC4387427B5BB9B00C60D73 /* Promise+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4387327B5BB9B00C60D73 /* Promise+Utilities.swift */; };
|
||||||
FDC4387627B5BEF300C60D73 /* FileDownloadResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4387527B5BEF300C60D73 /* FileDownloadResponse.swift */; };
|
FDC4387627B5BEF300C60D73 /* FileDownloadResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4387527B5BEF300C60D73 /* FileDownloadResponse.swift */; };
|
||||||
FDC4387827B5C35400C60D73 /* SendMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4387727B5C35400C60D73 /* SendMessageRequest.swift */; };
|
FDC4387827B5C35400C60D73 /* SendMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4387727B5C35400C60D73 /* SendMessageRequest.swift */; };
|
||||||
|
FDC4387C27B9C6C900C60D73 /* LegacyOnionRequestAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4387B27B9C6C900C60D73 /* LegacyOnionRequestAPI.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
@ -1917,6 +1918,7 @@
|
||||||
FDC4387327B5BB9B00C60D73 /* Promise+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+Utilities.swift"; sourceTree = "<group>"; };
|
FDC4387327B5BB9B00C60D73 /* Promise+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+Utilities.swift"; sourceTree = "<group>"; };
|
||||||
FDC4387527B5BEF300C60D73 /* FileDownloadResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileDownloadResponse.swift; sourceTree = "<group>"; };
|
FDC4387527B5BEF300C60D73 /* FileDownloadResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileDownloadResponse.swift; sourceTree = "<group>"; };
|
||||||
FDC4387727B5C35400C60D73 /* SendMessageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageRequest.swift; sourceTree = "<group>"; };
|
FDC4387727B5C35400C60D73 /* SendMessageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageRequest.swift; sourceTree = "<group>"; };
|
||||||
|
FDC4387B27B9C6C900C60D73 /* LegacyOnionRequestAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyOnionRequestAPI.swift; sourceTree = "<group>"; };
|
||||||
FEDBAE1B98C49BBE8C87F575 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; sourceTree = "<group>"; };
|
FEDBAE1B98C49BBE8C87F575 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
FF9BA33D021B115B1F5B4E46 /* Pods-SessionMessagingKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionMessagingKit.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SessionMessagingKit/Pods-SessionMessagingKit.app store release.xcconfig"; sourceTree = "<group>"; };
|
FF9BA33D021B115B1F5B4E46 /* Pods-SessionMessagingKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionMessagingKit.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SessionMessagingKit/Pods-SessionMessagingKit.app store release.xcconfig"; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
@ -3363,6 +3365,7 @@
|
||||||
C3C2A5B9255385ED00C340D1 /* Configuration.swift */,
|
C3C2A5B9255385ED00C340D1 /* Configuration.swift */,
|
||||||
C3C2A5BD255385EE00C340D1 /* Notification+OnionRequestAPI.swift */,
|
C3C2A5BD255385EE00C340D1 /* Notification+OnionRequestAPI.swift */,
|
||||||
C3C2A5BA255385ED00C340D1 /* OnionRequestAPI.swift */,
|
C3C2A5BA255385ED00C340D1 /* OnionRequestAPI.swift */,
|
||||||
|
FDC4387B27B9C6C900C60D73 /* LegacyOnionRequestAPI.swift */,
|
||||||
C3C2A5BB255385ED00C340D1 /* OnionRequestAPI+Encryption.swift */,
|
C3C2A5BB255385ED00C340D1 /* OnionRequestAPI+Encryption.swift */,
|
||||||
C3C2A5B7255385EC00C340D1 /* Snode.swift */,
|
C3C2A5B7255385EC00C340D1 /* Snode.swift */,
|
||||||
C3C2A5BE255385EE00C340D1 /* SnodeAPI.swift */,
|
C3C2A5BE255385EE00C340D1 /* SnodeAPI.swift */,
|
||||||
|
@ -4795,6 +4798,7 @@
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
C3C2A5E02553860B00C340D1 /* Threading.swift in Sources */,
|
C3C2A5E02553860B00C340D1 /* Threading.swift in Sources */,
|
||||||
|
FDC4387C27B9C6C900C60D73 /* LegacyOnionRequestAPI.swift in Sources */,
|
||||||
C3C2A5BF255385EE00C340D1 /* SnodeMessage.swift in Sources */,
|
C3C2A5BF255385EE00C340D1 /* SnodeMessage.swift in Sources */,
|
||||||
C3C2A5C0255385EE00C340D1 /* Snode.swift in Sources */,
|
C3C2A5C0255385EE00C340D1 /* Snode.swift in Sources */,
|
||||||
C3C2A5C7255385EE00C340D1 /* SnodeAPI.swift in Sources */,
|
C3C2A5C7255385EE00C340D1 /* SnodeAPI.swift in Sources */,
|
||||||
|
|
|
@ -94,7 +94,9 @@ public final class FileServerAPIV2 : NSObject {
|
||||||
preconditionFailure("It's currently not allowed to send non onion routed requests.")
|
preconditionFailure("It's currently not allowed to send non onion routed requests.")
|
||||||
}
|
}
|
||||||
|
|
||||||
return OnionRequestAPI.sendOnionRequest(urlRequest, to: server, using: serverPublicKey)
|
// TODO: Upgrade this to use the V4 onion requests once supported
|
||||||
|
return LegacyOnionRequestAPI.sendOnionRequest(urlRequest, to: server, using: serverPublicKey)
|
||||||
|
.map2 { json in try JSONSerialization.data(withJSONObject: json, options: []) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: File Storage
|
// MARK: File Storage
|
||||||
|
|
|
@ -101,7 +101,8 @@ public final class AttachmentDownloadJob : NSObject, Job, NSCoding { // NSObject
|
||||||
guard let fileAsString = pointer.downloadURL.split(separator: "/").last, let file = UInt64(fileAsString) else {
|
guard let fileAsString = pointer.downloadURL.split(separator: "/").last, let file = UInt64(fileAsString) else {
|
||||||
return handleFailure(Error.invalidURL)
|
return handleFailure(Error.invalidURL)
|
||||||
}
|
}
|
||||||
OpenGroupAPIV2.download(file, from: v2OpenGroup.room, on: v2OpenGroup.server).done(on: DispatchQueue.global(qos: .userInitiated)) { data in
|
// TODO: Upgrade this to use the non-legacy version
|
||||||
|
OpenGroupAPIV2.legacyDownload(file, from: v2OpenGroup.room, on: v2OpenGroup.server).done(on: DispatchQueue.global(qos: .userInitiated)) { data in
|
||||||
self.handleDownloadedAttachment(data: data, temporaryFilePath: temporaryFilePath, pointer: pointer, failureHandler: handleFailure)
|
self.handleDownloadedAttachment(data: data, temporaryFilePath: temporaryFilePath, pointer: pointer, failureHandler: handleFailure)
|
||||||
}.catch(on: DispatchQueue.global()) { error in
|
}.catch(on: DispatchQueue.global()) { error in
|
||||||
handleFailure(error)
|
handleFailure(error)
|
||||||
|
|
|
@ -69,7 +69,16 @@ public final class AttachmentUploadJob : NSObject, Job, NSCoding { // NSObject/N
|
||||||
guard !stream.isUploaded else { return handleSuccess() } // Should never occur
|
guard !stream.isUploaded else { return handleSuccess() } // Should never occur
|
||||||
let storage = SNMessagingKitConfiguration.shared.storage
|
let storage = SNMessagingKitConfiguration.shared.storage
|
||||||
if let v2OpenGroup = storage.getV2OpenGroup(for: threadID) {
|
if let v2OpenGroup = storage.getV2OpenGroup(for: threadID) {
|
||||||
AttachmentUploadJob.upload(stream, using: { data in return OpenGroupAPIV2.upload(data, to: v2OpenGroup.room, on: v2OpenGroup.server) }, encrypt: false, onSuccess: handleSuccess, onFailure: handleFailure)
|
AttachmentUploadJob.upload(
|
||||||
|
stream,
|
||||||
|
using: { data in
|
||||||
|
// TODO: Upgrade this to use the non-legacy version
|
||||||
|
return OpenGroupAPIV2.legacyUpload(data, to: v2OpenGroup.room, on: v2OpenGroup.server)
|
||||||
|
},
|
||||||
|
encrypt: false,
|
||||||
|
onSuccess: handleSuccess,
|
||||||
|
onFailure: handleFailure
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
AttachmentUploadJob.upload(stream, using: FileServerAPIV2.upload, encrypt: true, onSuccess: handleSuccess, onFailure: handleFailure)
|
AttachmentUploadJob.upload(stream, using: FileServerAPIV2.upload, encrypt: true, onSuccess: handleSuccess, onFailure: handleFailure)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import PromiseKit
|
import PromiseKit
|
||||||
import SessionUtilitiesKit
|
import SessionUtilitiesKit
|
||||||
|
import SessionSnodeKit
|
||||||
|
|
||||||
extension OpenGroupAPIV2 {
|
extension OpenGroupAPIV2 {
|
||||||
// MARK: - BatchSubRequest
|
// MARK: - BatchSubRequest
|
||||||
|
@ -66,10 +67,11 @@ public extension Decodable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Promise where T == Data {
|
extension Promise where T == (OnionRequestAPI.ResponseInfo, Data?) {
|
||||||
func decoded(as types: OpenGroupAPIV2.BatchResponseTypes, on queue: DispatchQueue? = nil, error: Error? = nil) -> Promise<OpenGroupAPIV2.BatchResponse> {
|
func decoded(as types: OpenGroupAPIV2.BatchResponseTypes, on queue: DispatchQueue? = nil, error: Error? = nil) -> Promise<OpenGroupAPIV2.BatchResponse> {
|
||||||
self.map(on: queue) { data -> OpenGroupAPIV2.BatchResponse in
|
self.map(on: queue) { responseInfo, maybeData -> OpenGroupAPIV2.BatchResponse in
|
||||||
// Need to split the data into an array of data so each item can be Decoded correctly
|
// Need to split the data into an array of data so each item can be Decoded correctly
|
||||||
|
guard let data: Data = maybeData else { throw OpenGroupAPIV2.Error.parsingFailed }
|
||||||
guard let jsonObject: Any = try? JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed]) else {
|
guard let jsonObject: Any = try? JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed]) else {
|
||||||
throw OpenGroupAPIV2.Error.parsingFailed
|
throw OpenGroupAPIV2.Error.parsingFailed
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,8 @@ extension OpenGroupAPIV2 {
|
||||||
|
|
||||||
@objc(deleteMessageWithServerID:fromRoom:onServer:)
|
@objc(deleteMessageWithServerID:fromRoom:onServer:)
|
||||||
public static func objc_deleteMessage(with serverID: Int64, from room: String, on server: String) -> AnyPromise {
|
public static func objc_deleteMessage(with serverID: Int64, from room: String, on server: String) -> AnyPromise {
|
||||||
return AnyPromise.from(deleteMessage(with: serverID, from: room, on: server))
|
// TODO: Upgrade this to use the non-legacy version
|
||||||
|
return AnyPromise.from(legacyDeleteMessage(with: serverID, from: room, on: server))
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc(isUserModerator:forRoom:onServer:)
|
@objc(isUserModerator:forRoom:onServer:)
|
||||||
|
|
|
@ -156,7 +156,9 @@ public final class OpenGroupAPIV2: NSObject {
|
||||||
)
|
)
|
||||||
|
|
||||||
return send(request)
|
return send(request)
|
||||||
.then(on: OpenGroupAPIV2.workQueue) { data -> Promise<LegacyCompactPollResponse> in
|
.then(on: OpenGroupAPIV2.workQueue) { _, maybeData -> Promise<LegacyCompactPollResponse> in
|
||||||
|
guard let data: Data = maybeData else { throw Error.parsingFailed }
|
||||||
|
|
||||||
let response: LegacyCompactPollResponse = try data.decoded(as: LegacyCompactPollResponse.self, customError: Error.parsingFailed)
|
let response: LegacyCompactPollResponse = try data.decoded(as: LegacyCompactPollResponse.self, customError: Error.parsingFailed)
|
||||||
|
|
||||||
return when(
|
return when(
|
||||||
|
@ -241,7 +243,7 @@ public final class OpenGroupAPIV2: NSObject {
|
||||||
|
|
||||||
// MARK: - Capabilities
|
// MARK: - Capabilities
|
||||||
|
|
||||||
public static func capabilities(on server: String) -> Promise<Capabilities> {
|
public static func capabilities(on server: String) -> Promise<(OnionRequestAPI.ResponseInfo, Capabilities)> {
|
||||||
let request: Request = Request(
|
let request: Request = Request(
|
||||||
server: server,
|
server: server,
|
||||||
endpoint: .capabilities,
|
endpoint: .capabilities,
|
||||||
|
@ -255,7 +257,7 @@ public final class OpenGroupAPIV2: NSObject {
|
||||||
|
|
||||||
// MARK: - Room
|
// MARK: - Room
|
||||||
|
|
||||||
public static func rooms(for server: String) -> Promise<[Room]> {
|
public static func rooms(for server: String) -> Promise<(OnionRequestAPI.ResponseInfo, [Room])> {
|
||||||
let request: Request = Request(
|
let request: Request = Request(
|
||||||
server: server,
|
server: server,
|
||||||
endpoint: .rooms
|
endpoint: .rooms
|
||||||
|
@ -265,7 +267,7 @@ public final class OpenGroupAPIV2: NSObject {
|
||||||
.decoded(as: [Room].self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed)
|
.decoded(as: [Room].self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func room(for roomToken: String, on server: String) -> Promise<Room> {
|
public static func room(for roomToken: String, on server: String) -> Promise<(OnionRequestAPI.ResponseInfo, Room)> {
|
||||||
let request: Request = Request(
|
let request: Request = Request(
|
||||||
server: server,
|
server: server,
|
||||||
endpoint: .room(roomToken)
|
endpoint: .room(roomToken)
|
||||||
|
@ -275,7 +277,7 @@ public final class OpenGroupAPIV2: NSObject {
|
||||||
.decoded(as: Room.self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed)
|
.decoded(as: Room.self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func roomPollInfo(lastUpdated: Int64, for roomToken: String, on server: String) -> Promise<RoomPollInfo> {
|
public static func roomPollInfo(lastUpdated: Int64, for roomToken: String, on server: String) -> Promise<(OnionRequestAPI.ResponseInfo, RoomPollInfo)> {
|
||||||
let request: Request = Request(
|
let request: Request = Request(
|
||||||
server: server,
|
server: server,
|
||||||
endpoint: .roomPollInfo(roomToken, lastUpdated)
|
endpoint: .roomPollInfo(roomToken, lastUpdated)
|
||||||
|
@ -294,7 +296,7 @@ public final class OpenGroupAPIV2: NSObject {
|
||||||
whisperTo: String?,
|
whisperTo: String?,
|
||||||
whisperMods: Bool,
|
whisperMods: Bool,
|
||||||
with serverPublicKey: String
|
with serverPublicKey: String
|
||||||
) -> Promise<Message> {
|
) -> Promise<(OnionRequestAPI.ResponseInfo, Message)> {
|
||||||
// TODO: Change this to use '.blinded' once it's working
|
// TODO: Change this to use '.blinded' once it's working
|
||||||
guard let signedRequest: (data: Data, signature: Data) = SendMessageRequest.sign(message: plaintext, for: .standard, with: serverPublicKey) else {
|
guard let signedRequest: (data: Data, signature: Data) = SendMessageRequest.sign(message: plaintext, for: .standard, with: serverPublicKey) else {
|
||||||
return Promise(error: Error.signingFailed)
|
return Promise(error: Error.signingFailed)
|
||||||
|
@ -322,9 +324,8 @@ public final class OpenGroupAPIV2: NSObject {
|
||||||
return send(request)
|
return send(request)
|
||||||
.decoded(as: Message.self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed)
|
.decoded(as: Message.self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func recentMessages(in roomToken: String, on server: String) -> Promise<(OnionRequestAPI.ResponseInfo, [Message])> {
|
||||||
public static func recentMessages(in roomToken: String, on server: String) -> Promise<[Message]> {
|
|
||||||
// TODO: Recent vs. Since?
|
// TODO: Recent vs. Since?
|
||||||
let request: Request = Request(
|
let request: Request = Request(
|
||||||
server: server,
|
server: server,
|
||||||
|
@ -337,12 +338,13 @@ public final class OpenGroupAPIV2: NSObject {
|
||||||
|
|
||||||
return send(request)
|
return send(request)
|
||||||
.decoded(as: [Message].self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed)
|
.decoded(as: [Message].self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed)
|
||||||
.then(on: OpenGroupAPIV2.workQueue) { messages -> Promise<[Message]> in
|
.then(on: OpenGroupAPIV2.workQueue) { responseInfo, messages -> Promise<(OnionRequestAPI.ResponseInfo, [Message])> in
|
||||||
process(messages: messages, for: roomToken, on: server)
|
process(messages: messages, for: roomToken, on: server)
|
||||||
|
.map { processedMessages in (responseInfo, processedMessages) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func messagesBefore(messageId: Int64, in roomToken: String, on server: String) -> Promise<[Message]> {
|
public static func messagesBefore(messageId: Int64, in roomToken: String, on server: String) -> Promise<(OnionRequestAPI.ResponseInfo, [Message])> {
|
||||||
// TODO: Recent vs. Since?
|
// TODO: Recent vs. Since?
|
||||||
let request: Request = Request(
|
let request: Request = Request(
|
||||||
server: server,
|
server: server,
|
||||||
|
@ -355,12 +357,13 @@ public final class OpenGroupAPIV2: NSObject {
|
||||||
|
|
||||||
return send(request)
|
return send(request)
|
||||||
.decoded(as: [Message].self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed)
|
.decoded(as: [Message].self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed)
|
||||||
.then(on: OpenGroupAPIV2.workQueue) { messages -> Promise<[Message]> in
|
.then(on: OpenGroupAPIV2.workQueue) { responseInfo, messages -> Promise<(OnionRequestAPI.ResponseInfo, [Message])> in
|
||||||
process(messages: messages, for: roomToken, on: server)
|
process(messages: messages, for: roomToken, on: server)
|
||||||
|
.map { processedMessages in (responseInfo, processedMessages) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func messagesSince(seqNo: Int64, in roomToken: String, on server: String) -> Promise<[Message]> {
|
public static func messagesSince(seqNo: Int64, in roomToken: String, on server: String) -> Promise<(OnionRequestAPI.ResponseInfo, [Message])> {
|
||||||
// TODO: Recent vs. Since?
|
// TODO: Recent vs. Since?
|
||||||
let request: Request = Request(
|
let request: Request = Request(
|
||||||
server: server,
|
server: server,
|
||||||
|
@ -373,11 +376,47 @@ public final class OpenGroupAPIV2: NSObject {
|
||||||
|
|
||||||
return send(request)
|
return send(request)
|
||||||
.decoded(as: [Message].self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed)
|
.decoded(as: [Message].self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed)
|
||||||
.then(on: OpenGroupAPIV2.workQueue) { messages -> Promise<[Message]> in
|
.then(on: OpenGroupAPIV2.workQueue) { responseInfo, messages -> Promise<(OnionRequestAPI.ResponseInfo, [Message])> in
|
||||||
process(messages: messages, for: roomToken, on: server)
|
process(messages: messages, for: roomToken, on: server)
|
||||||
|
.map { processedMessages in (responseInfo, processedMessages) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Pinning
|
||||||
|
|
||||||
|
public static func pinMessage(id: Int64, in roomToken: String, on server: String) -> Promise<OnionRequestAPI.ResponseInfo> {
|
||||||
|
let request: Request = Request(
|
||||||
|
method: .post,
|
||||||
|
server: server,
|
||||||
|
endpoint: .roomPinMessage(roomToken, id: id)
|
||||||
|
)
|
||||||
|
|
||||||
|
return send(request)
|
||||||
|
.map { responseInfo, _ in responseInfo }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func unpinMessage(id: Int64, in roomToken: String, on server: String) -> Promise<OnionRequestAPI.ResponseInfo> {
|
||||||
|
let request: Request = Request(
|
||||||
|
method: .post,
|
||||||
|
server: server,
|
||||||
|
endpoint: .roomUnpinMessage(roomToken, id: id)
|
||||||
|
)
|
||||||
|
|
||||||
|
return send(request)
|
||||||
|
.map { responseInfo, _ in responseInfo }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func unpinAll(in roomToken: String, on server: String) -> Promise<OnionRequestAPI.ResponseInfo> {
|
||||||
|
let request: Request = Request(
|
||||||
|
method: .post,
|
||||||
|
server: server,
|
||||||
|
endpoint: .roomUnpinAll(roomToken)
|
||||||
|
)
|
||||||
|
|
||||||
|
return send(request)
|
||||||
|
.map { responseInfo, _ in responseInfo }
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Files
|
// MARK: - Files
|
||||||
|
|
||||||
// TODO: Shift this logic to the `OpenGroupManager` (makes more sense since it's not API logic)
|
// TODO: Shift this logic to the `OpenGroupManager` (makes more sense since it's not API logic)
|
||||||
|
@ -405,6 +444,7 @@ public final class OpenGroupAPIV2: NSObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
let promise: Promise<Data> = downloadFile(fileId, from: roomToken, on: server)
|
let promise: Promise<Data> = downloadFile(fileId, from: roomToken, on: server)
|
||||||
|
.map { _, data in data }
|
||||||
_ = promise.done(on: OpenGroupAPIV2.workQueue) { imageData in
|
_ = promise.done(on: OpenGroupAPIV2.workQueue) { imageData in
|
||||||
if server == defaultServer {
|
if server == defaultServer {
|
||||||
Storage.shared.write { transaction in
|
Storage.shared.write { transaction in
|
||||||
|
@ -418,7 +458,7 @@ public final class OpenGroupAPIV2: NSObject {
|
||||||
return promise
|
return promise
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func uploadFile(_ bytes: [UInt8], fileName: String? = nil, to roomToken: String, on server: String) -> Promise<FileUploadResponse> {
|
public static func uploadFile(_ bytes: [UInt8], fileName: String? = nil, to roomToken: String, on server: String) -> Promise<(OnionRequestAPI.ResponseInfo, FileUploadResponse)> {
|
||||||
let request: Request = Request(
|
let request: Request = Request(
|
||||||
method: .post,
|
method: .post,
|
||||||
server: server,
|
server: server,
|
||||||
|
@ -433,7 +473,7 @@ public final class OpenGroupAPIV2: NSObject {
|
||||||
|
|
||||||
/// Warning: This approach is less efficient as it expects the data to be base64Encoded (with is 33% larger than binary), please use the binary approach
|
/// Warning: This approach is less efficient as it expects the data to be base64Encoded (with is 33% larger than binary), please use the binary approach
|
||||||
/// whenever possible
|
/// whenever possible
|
||||||
public static func uploadFile(_ base64EncodedString: String, fileName: String? = nil, to roomToken: String, on server: String) -> Promise<FileUploadResponse> {
|
public static func uploadFile(_ base64EncodedString: String, fileName: String? = nil, to roomToken: String, on server: String) -> Promise<(OnionRequestAPI.ResponseInfo, FileUploadResponse)> {
|
||||||
let request: Request = Request(
|
let request: Request = Request(
|
||||||
method: .post,
|
method: .post,
|
||||||
server: server,
|
server: server,
|
||||||
|
@ -446,21 +486,26 @@ public final class OpenGroupAPIV2: NSObject {
|
||||||
.decoded(as: FileUploadResponse.self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed)
|
.decoded(as: FileUploadResponse.self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func downloadFile(_ fileId: Int64, from roomToken: String, on server: String) -> Promise<Data> {
|
public static func downloadFile(_ fileId: Int64, from roomToken: String, on server: String) -> Promise<(OnionRequestAPI.ResponseInfo, Data)> {
|
||||||
let request: Request = Request(
|
let request: Request = Request(
|
||||||
server: server,
|
server: server,
|
||||||
endpoint: .roomFileIndividual(roomToken, fileId)
|
endpoint: .roomFileIndividual(roomToken, fileId)
|
||||||
)
|
)
|
||||||
|
|
||||||
return send(request)
|
return send(request)
|
||||||
|
.map { responseInfo, maybeData in
|
||||||
|
guard let data: Data = maybeData else { throw Error.parsingFailed }
|
||||||
|
|
||||||
|
return (responseInfo, data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func downloadFileJson(_ fileId: Int64, from roomToken: String, on server: String) -> Promise<FileDownloadResponse> {
|
public static func downloadFileJson(_ fileId: Int64, from roomToken: String, on server: String) -> Promise<(OnionRequestAPI.ResponseInfo, FileDownloadResponse)> {
|
||||||
let request: Request = Request(
|
let request: Request = Request(
|
||||||
server: server,
|
server: server,
|
||||||
endpoint: .roomFileIndividualJson(roomToken, fileId)
|
endpoint: .roomFileIndividualJson(roomToken, fileId)
|
||||||
)
|
)
|
||||||
|
// TODO: This endpoint is getting rewritten to return just data (properties would come through as headers)
|
||||||
return send(request)
|
return send(request)
|
||||||
.decoded(as: FileDownloadResponse.self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed)
|
.decoded(as: FileDownloadResponse.self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed)
|
||||||
}
|
}
|
||||||
|
@ -532,6 +577,7 @@ public final class OpenGroupAPIV2: NSObject {
|
||||||
completion: {
|
completion: {
|
||||||
let promise = attempt(maxRetryCount: 8, recoveringOn: DispatchQueue.main) {
|
let promise = attempt(maxRetryCount: 8, recoveringOn: DispatchQueue.main) {
|
||||||
OpenGroupAPIV2.rooms(for: defaultServer)
|
OpenGroupAPIV2.rooms(for: defaultServer)
|
||||||
|
.map { _, data in data }
|
||||||
}
|
}
|
||||||
_ = promise.done(on: OpenGroupAPIV2.workQueue) { items in
|
_ = promise.done(on: OpenGroupAPIV2.workQueue) { items in
|
||||||
items
|
items
|
||||||
|
@ -555,7 +601,7 @@ public final class OpenGroupAPIV2: NSObject {
|
||||||
|
|
||||||
// MARK: - Convenience
|
// MARK: - Convenience
|
||||||
|
|
||||||
private static func send(_ request: Request) -> Promise<Data> {
|
private static func send(_ request: Request, through api: OnionRequestAPIType.Type = OnionRequestAPI.self) -> Promise<(OnionRequestAPI.ResponseInfo, Data?)> {
|
||||||
guard let url: URL = request.url else { return Promise(error: Error.invalidURL) }
|
guard let url: URL = request.url else { return Promise(error: Error.invalidURL) }
|
||||||
|
|
||||||
var urlRequest: URLRequest = URLRequest(url: url)
|
var urlRequest: URLRequest = URLRequest(url: url)
|
||||||
|
@ -571,39 +617,6 @@ public final class OpenGroupAPIV2: NSObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
if request.isAuthRequired {
|
if request.isAuthRequired {
|
||||||
// Determine if we should be using legacy auth for this endpoint
|
|
||||||
// TODO: Might need to store this at an OpenGroup level (so all requests can use the appropriate method).
|
|
||||||
if request.endpoint.useLegacyAuth {
|
|
||||||
// Because legacy auth happens on a per-room basis, we need to have a room to
|
|
||||||
// make an authenticated request
|
|
||||||
guard let room = request.room else {
|
|
||||||
return OnionRequestAPI.sendOnionRequest(urlRequest, to: request.server, using: publicKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
return legacyGetAuthToken(for: room, on: request.server)
|
|
||||||
.then(on: OpenGroupAPIV2.workQueue) { authToken -> Promise<Data> in
|
|
||||||
urlRequest.setValue(authToken, forHTTPHeaderField: Header.authorization.rawValue)
|
|
||||||
|
|
||||||
let promise = OnionRequestAPI.sendOnionRequest(urlRequest, to: request.server, using: publicKey)
|
|
||||||
promise.catch(on: OpenGroupAPIV2.workQueue) { error in
|
|
||||||
// A 401 means that we didn't provide a (valid) auth token for a route
|
|
||||||
// that required one. We use this as an indication that the token we're
|
|
||||||
// using has expired. Note that a 403 has a different meaning; it means
|
|
||||||
// that we provided a valid token but it doesn't have a high enough
|
|
||||||
// permission level for the route in question.
|
|
||||||
if case OnionRequestAPI.Error.httpRequestFailedAtDestination(let statusCode, _, _) = error, statusCode == 401 {
|
|
||||||
let storage = SNMessagingKitConfiguration.shared.storage
|
|
||||||
|
|
||||||
storage.writeSync { transaction in
|
|
||||||
storage.removeAuthToken(for: room, on: request.server, using: transaction)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return promise
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to sign the request with the new auth
|
// Attempt to sign the request with the new auth
|
||||||
guard let signedRequest: URLRequest = sign(urlRequest, with: publicKey) else {
|
guard let signedRequest: URLRequest = sign(urlRequest, with: publicKey) else {
|
||||||
return Promise(error: Error.signingFailed)
|
return Promise(error: Error.signingFailed)
|
||||||
|
@ -679,7 +692,8 @@ public final class OpenGroupAPIV2: NSObject {
|
||||||
isAuthRequired: false
|
isAuthRequired: false
|
||||||
)
|
)
|
||||||
|
|
||||||
return send(request).map(on: OpenGroupAPIV2.workQueue) { data in
|
return legacySend(request).map(on: OpenGroupAPIV2.workQueue) { _, maybeData in
|
||||||
|
guard let data: Data = maybeData else { throw Error.parsingFailed }
|
||||||
let response = try data.decoded(as: AuthTokenResponse.self, customError: Error.parsingFailed)
|
let response = try data.decoded(as: AuthTokenResponse.self, customError: Error.parsingFailed)
|
||||||
let symmetricKey = try AESGCM.generateSymmetricKey(x25519PublicKey: response.challenge.ephemeralPublicKey, x25519PrivateKey: userKeyPair.privateKey)
|
let symmetricKey = try AESGCM.generateSymmetricKey(x25519PublicKey: response.challenge.ephemeralPublicKey, x25519PrivateKey: userKeyPair.privateKey)
|
||||||
|
|
||||||
|
@ -711,7 +725,7 @@ public final class OpenGroupAPIV2: NSObject {
|
||||||
isAuthRequired: false
|
isAuthRequired: false
|
||||||
)
|
)
|
||||||
|
|
||||||
return send(request).map(on: OpenGroupAPIV2.workQueue) { _ in authToken }
|
return legacySend(request).map(on: OpenGroupAPIV2.workQueue) { _ in authToken }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Should be called when leaving a group.
|
/// Should be called when leaving a group.
|
||||||
|
@ -723,7 +737,7 @@ public final class OpenGroupAPIV2: NSObject {
|
||||||
endpoint: .legacyAuthToken(legacyAuth: true)
|
endpoint: .legacyAuthToken(legacyAuth: true)
|
||||||
)
|
)
|
||||||
|
|
||||||
return send(request).map(on: OpenGroupAPIV2.workQueue) { _ in
|
return legacySend(request).map(on: OpenGroupAPIV2.workQueue) { _ in
|
||||||
let storage = SNMessagingKitConfiguration.shared.storage
|
let storage = SNMessagingKitConfiguration.shared.storage
|
||||||
|
|
||||||
storage.write { transaction in
|
storage.write { transaction in
|
||||||
|
@ -796,8 +810,9 @@ public final class OpenGroupAPIV2: NSObject {
|
||||||
isAuthRequired: false
|
isAuthRequired: false
|
||||||
)
|
)
|
||||||
|
|
||||||
return send(request)
|
return legacySend(request)
|
||||||
.then(on: OpenGroupAPIV2.workQueue) { data -> Promise<LegacyCompactPollResponse> in
|
.then(on: OpenGroupAPIV2.workQueue) { _, maybeData -> Promise<LegacyCompactPollResponse> in
|
||||||
|
guard let data: Data = maybeData else { throw Error.parsingFailed }
|
||||||
let response: LegacyCompactPollResponse = try data.decoded(as: LegacyCompactPollResponse.self, customError: Error.parsingFailed)
|
let response: LegacyCompactPollResponse = try data.decoded(as: LegacyCompactPollResponse.self, customError: Error.parsingFailed)
|
||||||
|
|
||||||
return when(
|
return when(
|
||||||
|
@ -853,8 +868,9 @@ public final class OpenGroupAPIV2: NSObject {
|
||||||
isAuthRequired: false
|
isAuthRequired: false
|
||||||
)
|
)
|
||||||
|
|
||||||
return send(request)
|
return legacySend(request)
|
||||||
.map(on: OpenGroupAPIV2.workQueue) { data in
|
.map(on: OpenGroupAPIV2.workQueue) { _, maybeData in
|
||||||
|
guard let data: Data = maybeData else { throw Error.parsingFailed }
|
||||||
let response: LegacyRoomsResponse = try data.decoded(as: LegacyRoomsResponse.self, customError: Error.parsingFailed)
|
let response: LegacyRoomsResponse = try data.decoded(as: LegacyRoomsResponse.self, customError: Error.parsingFailed)
|
||||||
|
|
||||||
return response.rooms
|
return response.rooms
|
||||||
|
@ -869,8 +885,9 @@ public final class OpenGroupAPIV2: NSObject {
|
||||||
isAuthRequired: false
|
isAuthRequired: false
|
||||||
)
|
)
|
||||||
|
|
||||||
return send(request)
|
return legacySend(request)
|
||||||
.map(on: OpenGroupAPIV2.workQueue) { data in
|
.map(on: OpenGroupAPIV2.workQueue) { _, maybeData in
|
||||||
|
guard let data: Data = maybeData else { throw Error.parsingFailed }
|
||||||
let response: LegacyGetInfoResponse = try data.decoded(as: LegacyGetInfoResponse.self, customError: Error.parsingFailed)
|
let response: LegacyGetInfoResponse = try data.decoded(as: LegacyGetInfoResponse.self, customError: Error.parsingFailed)
|
||||||
|
|
||||||
return response.room
|
return response.room
|
||||||
|
@ -907,7 +924,8 @@ public final class OpenGroupAPIV2: NSObject {
|
||||||
isAuthRequired: false
|
isAuthRequired: false
|
||||||
)
|
)
|
||||||
|
|
||||||
let promise: Promise<Data> = send(request).map(on: OpenGroupAPIV2.workQueue) { data in
|
let promise: Promise<Data> = legacySend(request).map(on: OpenGroupAPIV2.workQueue) { _, maybeData in
|
||||||
|
guard let data: Data = maybeData else { throw Error.parsingFailed }
|
||||||
let response: LegacyFileDownloadResponse = try data.decoded(as: LegacyFileDownloadResponse.self, customError: Error.parsingFailed)
|
let response: LegacyFileDownloadResponse = try data.decoded(as: LegacyFileDownloadResponse.self, customError: Error.parsingFailed)
|
||||||
|
|
||||||
if server == defaultServer {
|
if server == defaultServer {
|
||||||
|
@ -931,8 +949,9 @@ public final class OpenGroupAPIV2: NSObject {
|
||||||
endpoint: .legacyMemberCount(legacyAuth: true)
|
endpoint: .legacyMemberCount(legacyAuth: true)
|
||||||
)
|
)
|
||||||
|
|
||||||
return send(request)
|
return legacySend(request)
|
||||||
.map(on: OpenGroupAPIV2.workQueue) { data in
|
.map(on: OpenGroupAPIV2.workQueue) { _, maybeData in
|
||||||
|
guard let data: Data = maybeData else { throw Error.parsingFailed }
|
||||||
let response: MemberCountResponse = try data.decoded(as: MemberCountResponse.self, customError: Error.parsingFailed)
|
let response: MemberCountResponse = try data.decoded(as: MemberCountResponse.self, customError: Error.parsingFailed)
|
||||||
|
|
||||||
let storage = SNMessagingKitConfiguration.shared.storage
|
let storage = SNMessagingKitConfiguration.shared.storage
|
||||||
|
@ -946,7 +965,7 @@ public final class OpenGroupAPIV2: NSObject {
|
||||||
|
|
||||||
// MARK: - Legacy File Storage
|
// MARK: - Legacy File Storage
|
||||||
|
|
||||||
public static func upload(_ file: Data, to room: String, on server: String) -> Promise<UInt64> {
|
public static func legacyUpload(_ file: Data, to room: String, on server: String) -> Promise<UInt64> {
|
||||||
let requestBody: FileUploadBody = FileUploadBody(file: file.base64EncodedString())
|
let requestBody: FileUploadBody = FileUploadBody(file: file.base64EncodedString())
|
||||||
|
|
||||||
guard let body: Data = try? JSONEncoder().encode(requestBody) else {
|
guard let body: Data = try? JSONEncoder().encode(requestBody) else {
|
||||||
|
@ -955,17 +974,19 @@ public final class OpenGroupAPIV2: NSObject {
|
||||||
|
|
||||||
let request = Request(method: .post, server: server, room: room, endpoint: .legacyFiles, body: body)
|
let request = Request(method: .post, server: server, room: room, endpoint: .legacyFiles, body: body)
|
||||||
|
|
||||||
return send(request).map(on: OpenGroupAPIV2.workQueue) { data in
|
return legacySend(request).map(on: OpenGroupAPIV2.workQueue) { _, maybeData in
|
||||||
|
guard let data: Data = maybeData else { throw Error.parsingFailed }
|
||||||
let response: LegacyFileUploadResponse = try data.decoded(as: LegacyFileUploadResponse.self, customError: Error.parsingFailed)
|
let response: LegacyFileUploadResponse = try data.decoded(as: LegacyFileUploadResponse.self, customError: Error.parsingFailed)
|
||||||
|
|
||||||
return response.fileId
|
return response.fileId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func download(_ file: UInt64, from room: String, on server: String) -> Promise<Data> {
|
public static func legacyDownload(_ file: UInt64, from room: String, on server: String) -> Promise<Data> {
|
||||||
let request = Request(server: server, room: room, endpoint: .legacyFile(file))
|
let request = Request(server: server, room: room, endpoint: .legacyFile(file))
|
||||||
|
|
||||||
return send(request).map(on: OpenGroupAPIV2.workQueue) { data in
|
return legacySend(request).map(on: OpenGroupAPIV2.workQueue) { _, maybeData in
|
||||||
|
guard let data: Data = maybeData else { throw Error.parsingFailed }
|
||||||
let response: LegacyFileDownloadResponse = try data.decoded(as: LegacyFileDownloadResponse.self, customError: Error.parsingFailed)
|
let response: LegacyFileDownloadResponse = try data.decoded(as: LegacyFileDownloadResponse.self, customError: Error.parsingFailed)
|
||||||
|
|
||||||
return response.data
|
return response.data
|
||||||
|
@ -981,7 +1002,8 @@ public final class OpenGroupAPIV2: NSObject {
|
||||||
}
|
}
|
||||||
let request = Request(method: .post, server: server, room: room, endpoint: .legacyMessages, body: body)
|
let request = Request(method: .post, server: server, room: room, endpoint: .legacyMessages, body: body)
|
||||||
|
|
||||||
return send(request).map(on: OpenGroupAPIV2.workQueue) { data in
|
return legacySend(request).map(on: OpenGroupAPIV2.workQueue) { _, maybeData in
|
||||||
|
guard let data: Data = maybeData else { throw Error.parsingFailed }
|
||||||
let message: OpenGroupMessageV2 = try data.decoded(as: OpenGroupMessageV2.self, customError: Error.parsingFailed)
|
let message: OpenGroupMessageV2 = try data.decoded(as: OpenGroupMessageV2.self, customError: Error.parsingFailed)
|
||||||
Storage.shared.write { transaction in
|
Storage.shared.write { transaction in
|
||||||
Storage.shared.addReceivedMessageTimestamp(message.sentTimestamp, using: transaction)
|
Storage.shared.addReceivedMessageTimestamp(message.sentTimestamp, using: transaction)
|
||||||
|
@ -1001,7 +1023,8 @@ public final class OpenGroupAPIV2: NSObject {
|
||||||
].compactMapValues { $0 }
|
].compactMapValues { $0 }
|
||||||
)
|
)
|
||||||
|
|
||||||
return send(request).then(on: OpenGroupAPIV2.workQueue) { data -> Promise<[OpenGroupMessageV2]> in
|
return legacySend(request).then(on: OpenGroupAPIV2.workQueue) { _, maybeData -> Promise<[OpenGroupMessageV2]> in
|
||||||
|
guard let data: Data = maybeData else { throw Error.parsingFailed }
|
||||||
let messages: [OpenGroupMessageV2] = try data.decoded(as: [OpenGroupMessageV2].self, customError: Error.parsingFailed)
|
let messages: [OpenGroupMessageV2] = try data.decoded(as: [OpenGroupMessageV2].self, customError: Error.parsingFailed)
|
||||||
|
|
||||||
return legacyProcess(messages: messages, for: room, on: server)
|
return legacyProcess(messages: messages, for: room, on: server)
|
||||||
|
@ -1010,7 +1033,7 @@ public final class OpenGroupAPIV2: NSObject {
|
||||||
|
|
||||||
// MARK: - Legacy Message Deletion
|
// MARK: - Legacy Message Deletion
|
||||||
|
|
||||||
public static func deleteMessage(with serverID: Int64, from room: String, on server: String) -> Promise<Void> {
|
public static func legacyDeleteMessage(with serverID: Int64, from room: String, on server: String) -> Promise<Void> {
|
||||||
let request: Request = Request(
|
let request: Request = Request(
|
||||||
method: .delete,
|
method: .delete,
|
||||||
server: server,
|
server: server,
|
||||||
|
@ -1018,10 +1041,10 @@ public final class OpenGroupAPIV2: NSObject {
|
||||||
endpoint: .legacyMessagesForServer(serverID)
|
endpoint: .legacyMessagesForServer(serverID)
|
||||||
)
|
)
|
||||||
|
|
||||||
return send(request).map(on: OpenGroupAPIV2.workQueue) { _ in }
|
return legacySend(request).map(on: OpenGroupAPIV2.workQueue) { _ in }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func getDeletedMessages(for room: String, on server: String) -> Promise<[Deletion]> {
|
public static func legacyGetDeletedMessages(for room: String, on server: String) -> Promise<[Deletion]> {
|
||||||
let storage = SNMessagingKitConfiguration.shared.storage
|
let storage = SNMessagingKitConfiguration.shared.storage
|
||||||
|
|
||||||
let request: Request = Request(
|
let request: Request = Request(
|
||||||
|
@ -1033,7 +1056,8 @@ public final class OpenGroupAPIV2: NSObject {
|
||||||
].compactMapValues { $0 }
|
].compactMapValues { $0 }
|
||||||
)
|
)
|
||||||
|
|
||||||
return send(request).then(on: OpenGroupAPIV2.workQueue) { data -> Promise<[Deletion]> in
|
return legacySend(request).then(on: OpenGroupAPIV2.workQueue) { _, maybeData -> Promise<[Deletion]> in
|
||||||
|
guard let data: Data = maybeData else { throw Error.parsingFailed }
|
||||||
let response: DeletedMessagesResponse = try data.decoded(as: DeletedMessagesResponse.self, customError: Error.parsingFailed)
|
let response: DeletedMessagesResponse = try data.decoded(as: DeletedMessagesResponse.self, customError: Error.parsingFailed)
|
||||||
|
|
||||||
return process(deletions: response.deletions, for: room, on: server)
|
return process(deletions: response.deletions, for: room, on: server)
|
||||||
|
@ -1042,15 +1066,16 @@ public final class OpenGroupAPIV2: NSObject {
|
||||||
|
|
||||||
// MARK: - Legacy Moderation
|
// MARK: - Legacy Moderation
|
||||||
|
|
||||||
public static func getModerators(for room: String, on server: String) -> Promise<[String]> {
|
public static func legacyGetModerators(for room: String, on server: String) -> Promise<[String]> {
|
||||||
let request: Request = Request(
|
let request: Request = Request(
|
||||||
server: server,
|
server: server,
|
||||||
room: room,
|
room: room,
|
||||||
endpoint: .legacyModerators
|
endpoint: .legacyModerators
|
||||||
)
|
)
|
||||||
|
|
||||||
return send(request)
|
return legacySend(request)
|
||||||
.map(on: OpenGroupAPIV2.workQueue) { data in
|
.map(on: OpenGroupAPIV2.workQueue) { _, maybeData in
|
||||||
|
guard let data: Data = maybeData else { throw Error.parsingFailed }
|
||||||
let response: ModeratorsResponse = try data.decoded(as: ModeratorsResponse.self, customError: Error.parsingFailed)
|
let response: ModeratorsResponse = try data.decoded(as: ModeratorsResponse.self, customError: Error.parsingFailed)
|
||||||
|
|
||||||
if var x = self.moderators[server] {
|
if var x = self.moderators[server] {
|
||||||
|
@ -1065,7 +1090,7 @@ public final class OpenGroupAPIV2: NSObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func ban(_ publicKey: String, from room: String, on server: String) -> Promise<Void> {
|
public static func legacyBan(_ publicKey: String, from room: String, on server: String) -> Promise<Void> {
|
||||||
let requestBody: PublicKeyBody = PublicKeyBody(publicKey: getUserHexEncodedPublicKey())
|
let requestBody: PublicKeyBody = PublicKeyBody(publicKey: getUserHexEncodedPublicKey())
|
||||||
|
|
||||||
guard let body: Data = try? JSONEncoder().encode(requestBody) else {
|
guard let body: Data = try? JSONEncoder().encode(requestBody) else {
|
||||||
|
@ -1080,10 +1105,10 @@ public final class OpenGroupAPIV2: NSObject {
|
||||||
body: body
|
body: body
|
||||||
)
|
)
|
||||||
|
|
||||||
return send(request).map(on: OpenGroupAPIV2.workQueue) { _ in }
|
return legacySend(request).map(on: OpenGroupAPIV2.workQueue) { _ in }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func banAndDeleteAllMessages(_ publicKey: String, from room: String, on server: String) -> Promise<Void> {
|
public static func legacyBanAndDeleteAllMessages(_ publicKey: String, from room: String, on server: String) -> Promise<Void> {
|
||||||
let requestBody: PublicKeyBody = PublicKeyBody(publicKey: getUserHexEncodedPublicKey())
|
let requestBody: PublicKeyBody = PublicKeyBody(publicKey: getUserHexEncodedPublicKey())
|
||||||
|
|
||||||
guard let body: Data = try? JSONEncoder().encode(requestBody) else {
|
guard let body: Data = try? JSONEncoder().encode(requestBody) else {
|
||||||
|
@ -1098,10 +1123,10 @@ public final class OpenGroupAPIV2: NSObject {
|
||||||
body: body
|
body: body
|
||||||
)
|
)
|
||||||
|
|
||||||
return send(request).map(on: OpenGroupAPIV2.workQueue) { _ in }
|
return legacySend(request).map(on: OpenGroupAPIV2.workQueue) { _ in }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func unban(_ publicKey: String, from room: String, on server: String) -> Promise<Void> {
|
public static func legacyUnban(_ publicKey: String, from room: String, on server: String) -> Promise<Void> {
|
||||||
let request: Request = Request(
|
let request: Request = Request(
|
||||||
method: .delete,
|
method: .delete,
|
||||||
server: server,
|
server: server,
|
||||||
|
@ -1109,7 +1134,7 @@ public final class OpenGroupAPIV2: NSObject {
|
||||||
endpoint: .legacyBlockListIndividual(publicKey)
|
endpoint: .legacyBlockListIndividual(publicKey)
|
||||||
)
|
)
|
||||||
|
|
||||||
return send(request).map(on: OpenGroupAPIV2.workQueue) { _ in }
|
return legacySend(request).map(on: OpenGroupAPIV2.workQueue) { _ in }
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Processing
|
// MARK: - Processing
|
||||||
|
@ -1164,4 +1189,58 @@ public final class OpenGroupAPIV2: NSObject {
|
||||||
|
|
||||||
return Promise.value(deletions)
|
return Promise.value(deletions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Legacy Convenience
|
||||||
|
|
||||||
|
private static func legacySend(_ request: Request, through api: OnionRequestAPIType.Type = LegacyOnionRequestAPI.self) -> Promise<(OnionRequestAPI.ResponseInfo, Data?)> {
|
||||||
|
guard let url: URL = request.url else { return Promise(error: Error.invalidURL) }
|
||||||
|
|
||||||
|
var urlRequest: URLRequest = URLRequest(url: url)
|
||||||
|
urlRequest.httpMethod = request.method.rawValue
|
||||||
|
urlRequest.allHTTPHeaderFields = request.headers
|
||||||
|
.setting(.room, request.room) // TODO: Is this needed anymore? Add at the request level?.
|
||||||
|
.toHTTPHeaders()
|
||||||
|
urlRequest.httpBody = request.body
|
||||||
|
|
||||||
|
if request.useOnionRouting {
|
||||||
|
guard let publicKey = SNMessagingKitConfiguration.shared.storage.getOpenGroupPublicKey(for: request.server) else {
|
||||||
|
return Promise(error: Error.noPublicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.isAuthRequired {
|
||||||
|
// Because legacy auth happens on a per-room basis, we need to have a room to
|
||||||
|
// make an authenticated request
|
||||||
|
guard let room = request.room else {
|
||||||
|
return api.sendOnionRequest(urlRequest, to: request.server, target: "/loki/v3/lsrpc", using: publicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
return legacyGetAuthToken(for: room, on: request.server)
|
||||||
|
.then(on: OpenGroupAPIV2.workQueue) { authToken -> Promise<(OnionRequestAPI.ResponseInfo, Data?)> in
|
||||||
|
urlRequest.setValue(authToken, forHTTPHeaderField: Header.authorization.rawValue)
|
||||||
|
|
||||||
|
let promise = api.sendOnionRequest(urlRequest, to: request.server, target: "/loki/v3/lsrpc", using: publicKey)
|
||||||
|
promise.catch(on: OpenGroupAPIV2.workQueue) { error in
|
||||||
|
// A 401 means that we didn't provide a (valid) auth token for a route
|
||||||
|
// that required one. We use this as an indication that the token we're
|
||||||
|
// using has expired. Note that a 403 has a different meaning; it means
|
||||||
|
// that we provided a valid token but it doesn't have a high enough
|
||||||
|
// permission level for the route in question.
|
||||||
|
if case OnionRequestAPI.Error.httpRequestFailedAtDestination(let statusCode, _, _) = error, statusCode == 401 {
|
||||||
|
let storage = SNMessagingKitConfiguration.shared.storage
|
||||||
|
|
||||||
|
storage.writeSync { transaction in
|
||||||
|
storage.removeAuthToken(for: room, on: request.server, using: transaction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return api.sendOnionRequest(urlRequest, to: request.server, target: "/loki/v3/lsrpc", using: publicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
preconditionFailure("It's currently not allowed to send non onion routed requests.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,7 +79,7 @@ public final class OpenGroupManagerV2 : NSObject {
|
||||||
// }
|
// }
|
||||||
|
|
||||||
OpenGroupAPIV2.room(for: room, on: server)
|
OpenGroupAPIV2.room(for: room, on: server)
|
||||||
.done(on: DispatchQueue.global(qos: .userInitiated)) { room in
|
.done(on: DispatchQueue.global(qos: .userInitiated)) { _, room in
|
||||||
// Create the open group model and the thread
|
// Create the open group model and the thread
|
||||||
let openGroup: OpenGroupV2 = OpenGroupV2(
|
let openGroup: OpenGroupV2 = OpenGroupV2(
|
||||||
server: server,
|
server: server,
|
||||||
|
|
|
@ -216,6 +216,7 @@ public final class MessageSender : NSObject {
|
||||||
handleFailure(with: error, using: transaction)
|
handleFailure(with: error, using: transaction)
|
||||||
return promise
|
return promise
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send the result
|
// Send the result
|
||||||
let base64EncodedData = wrappedMessage.base64EncodedString()
|
let base64EncodedData = wrappedMessage.base64EncodedString()
|
||||||
let timestamp = UInt64(Int64(message.sentTimestamp!) + SnodeAPI.clockOffset)
|
let timestamp = UInt64(Int64(message.sentTimestamp!) + SnodeAPI.clockOffset)
|
||||||
|
@ -280,6 +281,7 @@ public final class MessageSender : NSObject {
|
||||||
let (promise, seal) = Promise<Void>.pending()
|
let (promise, seal) = Promise<Void>.pending()
|
||||||
let storage = SNMessagingKitConfiguration.shared.storage
|
let storage = SNMessagingKitConfiguration.shared.storage
|
||||||
let transaction = transaction as! YapDatabaseReadWriteTransaction
|
let transaction = transaction as! YapDatabaseReadWriteTransaction
|
||||||
|
|
||||||
// Set the timestamp, sender and recipient
|
// Set the timestamp, sender and recipient
|
||||||
if message.sentTimestamp == nil { // Visible messages will already have their sent timestamp set
|
if message.sentTimestamp == nil { // Visible messages will already have their sent timestamp set
|
||||||
message.sentTimestamp = NSDate.millisecondTimestamp()
|
message.sentTimestamp = NSDate.millisecondTimestamp()
|
||||||
|
@ -371,37 +373,30 @@ public final class MessageSender : NSObject {
|
||||||
preconditionFailure()
|
preconditionFailure()
|
||||||
}
|
}
|
||||||
|
|
||||||
// let openGroupMessage = OpenGroupMessageV2(serverID: nil, sender: nil, sentTimestamp: message.sentTimestamp!,
|
OpenGroupAPIV2
|
||||||
// base64EncodedData: plaintext.base64EncodedString(), base64EncodedSignature: nil)
|
.send(
|
||||||
|
plaintext,
|
||||||
|
to: room,
|
||||||
|
on: server,
|
||||||
|
whisperTo: whisperTo,
|
||||||
|
whisperMods: whisperMods,
|
||||||
|
with: openGroupV2.publicKey
|
||||||
|
)
|
||||||
|
.done(on: DispatchQueue.global(qos: .userInitiated)) { responseInfo, data in
|
||||||
|
message.openGroupServerMessageID = given(data.seqNo) { UInt64($0) }
|
||||||
|
|
||||||
//OpenGroupAPIV2.send(openGroupMessage, to: room, on: server, with: openGroupV2.publicKey)
|
Storage.shared.write { transaction in
|
||||||
return promise // TODO: Remove!!!
|
MessageSender.handleSuccessfulMessageSend(message, to: destination, serverTimestamp: UInt64(floor(data.posted)), using: transaction)
|
||||||
// OpenGroupAPIV2
|
seal.fulfill(())
|
||||||
// .send(
|
}
|
||||||
// plaintext,
|
}
|
||||||
// to: room,
|
.catch(on: DispatchQueue.global(qos: .userInitiated)) { error in
|
||||||
// on: server,
|
storage.write(with: { transaction in
|
||||||
// whisperTo: whisperTo,
|
handleFailure(with: error, using: transaction as! YapDatabaseReadWriteTransaction)
|
||||||
// whisperMods: whisperMods,
|
}, completion: { })
|
||||||
// with: openGroupV2.publicKey
|
}
|
||||||
// )
|
|
||||||
// .done(on: DispatchQueue.global(qos: .userInitiated)) { response in
|
return promise
|
||||||
// print("RAWR")
|
|
||||||
//// message.openGroupServerMessageID = given(response.serverID) { UInt64($0) }
|
|
||||||
//// storage.write(with: { transaction in
|
|
||||||
//// Storage.shared.addReceivedMessageTimestamp(message.sentTimestamp, using: transaction)
|
|
||||||
////
|
|
||||||
//// MessageSender.handleSuccessfulMessageSend(message, to: destination, serverTimestamp: response.sentTimestamp, using: transaction)
|
|
||||||
//// seal.fulfill(())
|
|
||||||
//// }, completion: { })
|
|
||||||
// }
|
|
||||||
// .catch(on: DispatchQueue.global(qos: .userInitiated)) { error in
|
|
||||||
// storage.write(with: { transaction in
|
|
||||||
// handleFailure(with: error, using: transaction as! YapDatabaseReadWriteTransaction)
|
|
||||||
// }, completion: { })
|
|
||||||
// }
|
|
||||||
// // Return
|
|
||||||
// return promise
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Success & Failure Handling
|
// MARK: Success & Failure Handling
|
||||||
|
|
|
@ -51,14 +51,17 @@ public final class PushNotificationAPI : NSObject {
|
||||||
request.httpBody = body
|
request.httpBody = body
|
||||||
|
|
||||||
let promise: Promise<Void> = attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) {
|
let promise: Promise<Void> = attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) {
|
||||||
OnionRequestAPI.sendOnionRequest(request, to: server, target: "/loki/v2/lsrpc", using: serverPublicKey).map2 { response in
|
// TODO: Update this to use the V4 union requests once supported
|
||||||
guard let response: UnregisterResponse = try? response.decoded(as: UnregisterResponse.self) else {
|
LegacyOnionRequestAPI.sendOnionRequest(request, to: server, target: "/loki/v2/lsrpc", using: serverPublicKey)
|
||||||
return SNLog("Couldn't unregister from push notifications.")
|
.map2 { json in try JSONSerialization.data(withJSONObject: json, options: []) }
|
||||||
|
.map2 { response in
|
||||||
|
guard let response: UnregisterResponse = try? response.decoded(as: UnregisterResponse.self) else {
|
||||||
|
return SNLog("Couldn't unregister from push notifications.")
|
||||||
|
}
|
||||||
|
guard response.code != 0 else {
|
||||||
|
return SNLog("Couldn't unregister from push notifications due to error: \(response.message ?? "nil").")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
guard response.code != 0 else {
|
|
||||||
return SNLog("Couldn't unregister from push notifications due to error: \(response.message ?? "nil").")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
promise.catch2 { error in
|
promise.catch2 { error in
|
||||||
SNLog("Couldn't unregister from push notifications.")
|
SNLog("Couldn't unregister from push notifications.")
|
||||||
|
@ -99,18 +102,21 @@ public final class PushNotificationAPI : NSObject {
|
||||||
request.httpBody = body
|
request.httpBody = body
|
||||||
|
|
||||||
let promise: Promise<Void> = attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) {
|
let promise: Promise<Void> = attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) {
|
||||||
OnionRequestAPI.sendOnionRequest(request, to: server, target: "/loki/v2/lsrpc", using: serverPublicKey).map2 { response in
|
// TODO: Update this to use the V4 union requests once supported
|
||||||
guard let response: RegisterResponse = try? response.decoded(as: RegisterResponse.self) else {
|
LegacyOnionRequestAPI.sendOnionRequest(request, to: server, target: "/loki/v2/lsrpc", using: serverPublicKey)
|
||||||
return SNLog("Couldn't register device token.")
|
.map2 { json in try JSONSerialization.data(withJSONObject: json, options: []) }
|
||||||
|
.map2 { response in
|
||||||
|
guard let response: RegisterResponse = try? response.decoded(as: RegisterResponse.self) else {
|
||||||
|
return SNLog("Couldn't register device token.")
|
||||||
|
}
|
||||||
|
guard response.code != 0 else {
|
||||||
|
return SNLog("Couldn't register device token due to error: \(response.message ?? "nil").")
|
||||||
|
}
|
||||||
|
|
||||||
|
userDefaults[.deviceToken] = hexEncodedToken
|
||||||
|
userDefaults[.lastDeviceTokenUpload] = now
|
||||||
|
userDefaults[.isUsingFullAPNs] = true
|
||||||
}
|
}
|
||||||
guard response.code != 0 else {
|
|
||||||
return SNLog("Couldn't register device token due to error: \(response.message ?? "nil").")
|
|
||||||
}
|
|
||||||
|
|
||||||
userDefaults[.deviceToken] = hexEncodedToken
|
|
||||||
userDefaults[.lastDeviceTokenUpload] = now
|
|
||||||
userDefaults[.isUsingFullAPNs] = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
promise.catch2 { error in
|
promise.catch2 { error in
|
||||||
SNLog("Couldn't register device token.")
|
SNLog("Couldn't register device token.")
|
||||||
|
@ -144,14 +150,17 @@ public final class PushNotificationAPI : NSObject {
|
||||||
request.httpBody = body
|
request.httpBody = body
|
||||||
|
|
||||||
let promise: Promise<Void> = attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) {
|
let promise: Promise<Void> = attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) {
|
||||||
OnionRequestAPI.sendOnionRequest(request, to: server, target: "/loki/v2/lsrpc", using: serverPublicKey).map2 { response in
|
// TODO: Update this to use the V4 union requests once supported
|
||||||
guard let response: RegisterResponse = try? response.decoded(as: RegisterResponse.self) else {
|
LegacyOnionRequestAPI.sendOnionRequest(request, to: server, target: "/loki/v2/lsrpc", using: serverPublicKey)
|
||||||
return SNLog("Couldn't subscribe/unsubscribe for closed group: \(closedGroupPublicKey).")
|
.map2 { json in try JSONSerialization.data(withJSONObject: json, options: []) }
|
||||||
|
.map2 { response in
|
||||||
|
guard let response: RegisterResponse = try? response.decoded(as: RegisterResponse.self) else {
|
||||||
|
return SNLog("Couldn't subscribe/unsubscribe for closed group: \(closedGroupPublicKey).")
|
||||||
|
}
|
||||||
|
guard response.code != 0 else {
|
||||||
|
return SNLog("Couldn't subscribe/unsubscribe for closed group: \(closedGroupPublicKey) due to error: \(response.message ?? "nil").")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
guard response.code != 0 else {
|
|
||||||
return SNLog("Couldn't subscribe/unsubscribe for closed group: \(closedGroupPublicKey) due to error: \(response.message ?? "nil").")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
promise.catch2 { error in
|
promise.catch2 { error in
|
||||||
SNLog("Couldn't subscribe/unsubscribe for closed group: \(closedGroupPublicKey).")
|
SNLog("Couldn't subscribe/unsubscribe for closed group: \(closedGroupPublicKey).")
|
||||||
|
|
|
@ -47,25 +47,22 @@ public final class OpenGroupPollerV2 : NSObject {
|
||||||
let (promise, seal) = Promise<Void>.pending()
|
let (promise, seal) = Promise<Void>.pending()
|
||||||
promise.retainUntilComplete()
|
promise.retainUntilComplete()
|
||||||
|
|
||||||
// TODO: Update to use the non-legacy version
|
OpenGroupAPIV2.poll(server)
|
||||||
// OpenGroupAPIV2.compactPoll(server)
|
.done(on: OpenGroupAPIV2.workQueue) { [weak self] _ in
|
||||||
OpenGroupAPIV2.legacyCompactPoll(server)
|
self?.isPolling = false
|
||||||
.done(on: OpenGroupAPIV2.workQueue) { [weak self] response in
|
// TODO: Handle response
|
||||||
guard let self = self else { return }
|
|
||||||
self.isPolling = false
|
|
||||||
response.results.forEach { self.handleCompactPollBody($0, isBackgroundPoll: isBackgroundPoll) }
|
|
||||||
seal.fulfill(())
|
seal.fulfill(())
|
||||||
}
|
}
|
||||||
.catch(on: OpenGroupAPIV2.workQueue) { error in
|
.catch(on: OpenGroupAPIV2.workQueue) { [weak self] error in
|
||||||
SNLog("Open group polling failed due to error: \(error).")
|
SNLog("Open group polling failed due to error: \(error).")
|
||||||
self.isPolling = false
|
self?.isPolling = false
|
||||||
seal.fulfill(()) // The promise is just used to keep track of when we're done
|
seal.fulfill(()) // The promise is just used to keep track of when we're done
|
||||||
}
|
}
|
||||||
|
|
||||||
return promise
|
return promise
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleCompactPollBody(_ body: OpenGroupAPIV2.CompactPollResponse.Result, isBackgroundPoll: Bool) {
|
private func handleCompactPollBody(_ body: OpenGroupAPIV2.LegacyCompactPollResponse.Result, isBackgroundPoll: Bool) {
|
||||||
let storage = SNMessagingKitConfiguration.shared.storage
|
let storage = SNMessagingKitConfiguration.shared.storage
|
||||||
// - Messages
|
// - Messages
|
||||||
// Sorting the messages by server ID before importing them fixes an issue where messages that quote older messages can't find those older messages
|
// Sorting the messages by server ID before importing them fixes an issue where messages that quote older messages can't find those older messages
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import PromiseKit
|
import PromiseKit
|
||||||
|
import SessionSnodeKit
|
||||||
|
|
||||||
extension Promise where T == Data {
|
extension Promise where T == Data {
|
||||||
func decoded<R: Decodable>(as type: R.Type, on queue: DispatchQueue? = nil, error: Error? = nil) -> Promise<R> {
|
func decoded<R: Decodable>(as type: R.Type, on queue: DispatchQueue? = nil, error: Error? = nil) -> Promise<R> {
|
||||||
|
@ -10,3 +11,15 @@ extension Promise where T == Data {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Promise where T == (OnionRequestAPI.ResponseInfo, Data?) {
|
||||||
|
func decoded<R: Decodable>(as type: R.Type, on queue: DispatchQueue? = nil, error: Error? = nil) -> Promise<(OnionRequestAPI.ResponseInfo, R)> {
|
||||||
|
self.map(on: queue) { responseInfo, maybeData -> (OnionRequestAPI.ResponseInfo, R) in
|
||||||
|
guard let data: Data = maybeData else {
|
||||||
|
throw OpenGroupAPIV2.Error.parsingFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
return (responseInfo, try data.decoded(as: type, customError: error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
455
SessionSnodeKit/LegacyOnionRequestAPI.swift
Normal file
455
SessionSnodeKit/LegacyOnionRequestAPI.swift
Normal file
|
@ -0,0 +1,455 @@
|
||||||
|
import CryptoSwift
|
||||||
|
import PromiseKit
|
||||||
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
|
/// See the "Onion Requests" section of [The Session Whitepaper](https://arxiv.org/pdf/2002.04609.pdf) for more information.
|
||||||
|
public enum LegacyOnionRequestAPI: OnionRequestAPIType {
|
||||||
|
private static var buildPathsPromise: Promise<[Path]>? = nil
|
||||||
|
/// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
|
||||||
|
private static var pathFailureCount: [Path:UInt] = [:]
|
||||||
|
/// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
|
||||||
|
private static var snodeFailureCount: [Snode:UInt] = [:]
|
||||||
|
/// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
|
||||||
|
public static var guardSnodes: Set<Snode> = []
|
||||||
|
public static var paths: [Path] = [] // Not a set to ensure we consistently show the same path to the user
|
||||||
|
// MARK: Settings
|
||||||
|
public static let maxRequestSize = 10_000_000 // 10 MB
|
||||||
|
/// The number of snodes (including the guard snode) in a path.
|
||||||
|
private static let pathSize: UInt = 3
|
||||||
|
/// The number of times a path can fail before it's replaced.
|
||||||
|
private static let pathFailureThreshold: UInt = 3
|
||||||
|
/// The number of times a snode can fail before it's replaced.
|
||||||
|
private static let snodeFailureThreshold: UInt = 3
|
||||||
|
/// The number of paths to maintain.
|
||||||
|
public static let targetPathCount: UInt = 2
|
||||||
|
|
||||||
|
/// The number of guard snodes required to maintain `targetPathCount` paths.
|
||||||
|
private static var targetGuardSnodeCount: UInt { return targetPathCount } // One per path
|
||||||
|
|
||||||
|
// MARK: Error
|
||||||
|
public enum Error : LocalizedError {
|
||||||
|
case httpRequestFailedAtDestination(statusCode: UInt, json: JSON, destination: OnionRequestAPI.Destination)
|
||||||
|
case insufficientSnodes
|
||||||
|
case invalidURL
|
||||||
|
case missingSnodeVersion
|
||||||
|
case snodePublicKeySetMissing
|
||||||
|
case unsupportedSnodeVersion(String)
|
||||||
|
|
||||||
|
public var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case .httpRequestFailedAtDestination(let statusCode, _, let destination):
|
||||||
|
if statusCode == 429 {
|
||||||
|
return "Rate limited."
|
||||||
|
} else {
|
||||||
|
return "HTTP request failed at destination (\(destination)) with status code: \(statusCode)."
|
||||||
|
}
|
||||||
|
case .insufficientSnodes: return "Couldn't find enough Service Nodes to build a path."
|
||||||
|
case .invalidURL: return "Invalid URL"
|
||||||
|
case .missingSnodeVersion: return "Missing Service Node version."
|
||||||
|
case .snodePublicKeySetMissing: return "Missing Service Node public key set."
|
||||||
|
case .unsupportedSnodeVersion(let version): return "Unsupported Service Node version: \(version)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Path
|
||||||
|
public typealias Path = [Snode]
|
||||||
|
|
||||||
|
// MARK: Onion Building Result
|
||||||
|
private typealias OnionBuildingResult = (guardSnode: Snode, finalEncryptionResult: AESGCM.EncryptionResult, destinationSymmetricKey: Data)
|
||||||
|
|
||||||
|
// MARK: Private API
|
||||||
|
/// Tests the given snode. The returned promise errors out if the snode is faulty; the promise is fulfilled otherwise.
|
||||||
|
private static func testSnode(_ snode: Snode) -> Promise<Void> {
|
||||||
|
let (promise, seal) = Promise<Void>.pending()
|
||||||
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
|
let url = "\(snode.address):\(snode.port)/get_stats/v1"
|
||||||
|
let timeout: TimeInterval = 3 // Use a shorter timeout for testing
|
||||||
|
HTTP.execute(.get, url, timeout: timeout).done2 { json in
|
||||||
|
guard let version = json["version"] as? String else { return seal.reject(Error.missingSnodeVersion) }
|
||||||
|
if version >= "2.0.7" {
|
||||||
|
seal.fulfill(())
|
||||||
|
} else {
|
||||||
|
SNLog("Unsupported snode version: \(version).")
|
||||||
|
seal.reject(Error.unsupportedSnodeVersion(version))
|
||||||
|
}
|
||||||
|
}.catch2 { error in
|
||||||
|
seal.reject(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finds `targetGuardSnodeCount` guard snodes to use for path building. The returned promise errors out with `Error.insufficientSnodes`
|
||||||
|
/// if not enough (reliable) snodes are available.
|
||||||
|
private static func getGuardSnodes(reusing reusableGuardSnodes: [Snode]) -> Promise<Set<Snode>> {
|
||||||
|
if guardSnodes.count >= targetGuardSnodeCount {
|
||||||
|
return Promise<Set<Snode>> { $0.fulfill(guardSnodes) }
|
||||||
|
} else {
|
||||||
|
SNLog("Populating guard snode cache.")
|
||||||
|
var unusedSnodes = SnodeAPI.snodePool.subtracting(reusableGuardSnodes) // Sync on LokiAPI.workQueue
|
||||||
|
let reusableGuardSnodeCount = UInt(reusableGuardSnodes.count)
|
||||||
|
guard unusedSnodes.count >= (targetGuardSnodeCount - reusableGuardSnodeCount) else { return Promise(error: Error.insufficientSnodes) }
|
||||||
|
func getGuardSnode() -> Promise<Snode> {
|
||||||
|
// randomElement() uses the system's default random generator, which is cryptographically secure
|
||||||
|
guard let candidate = unusedSnodes.randomElement() else { return Promise<Snode> { $0.reject(Error.insufficientSnodes) } }
|
||||||
|
unusedSnodes.remove(candidate) // All used snodes should be unique
|
||||||
|
SNLog("Testing guard snode: \(candidate).")
|
||||||
|
// Loop until a reliable guard snode is found
|
||||||
|
return testSnode(candidate).map2 { candidate }.recover(on: DispatchQueue.main) { _ in
|
||||||
|
withDelay(0.1, completionQueue: Threading.workQueue) { getGuardSnode() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let promises = (0..<(targetGuardSnodeCount - reusableGuardSnodeCount)).map { _ in getGuardSnode() }
|
||||||
|
return when(fulfilled: promises).map2 { guardSnodes in
|
||||||
|
let guardSnodesAsSet = Set(guardSnodes + reusableGuardSnodes)
|
||||||
|
OnionRequestAPI.guardSnodes = guardSnodesAsSet
|
||||||
|
return guardSnodesAsSet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds and returns `targetPathCount` paths. The returned promise errors out with `Error.insufficientSnodes`
|
||||||
|
/// if not enough (reliable) snodes are available.
|
||||||
|
@discardableResult
|
||||||
|
private static func buildPaths(reusing reusablePaths: [Path]) -> Promise<[Path]> {
|
||||||
|
if let existingBuildPathsPromise = buildPathsPromise { return existingBuildPathsPromise }
|
||||||
|
SNLog("Building onion request paths.")
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
NotificationCenter.default.post(name: .buildingPaths, object: nil)
|
||||||
|
}
|
||||||
|
let reusableGuardSnodes = reusablePaths.map { $0[0] }
|
||||||
|
let promise: Promise<[Path]> = getGuardSnodes(reusing: reusableGuardSnodes).map2 { guardSnodes -> [Path] in
|
||||||
|
var unusedSnodes = SnodeAPI.snodePool.subtracting(guardSnodes).subtracting(reusablePaths.flatMap { $0 })
|
||||||
|
let reusableGuardSnodeCount = UInt(reusableGuardSnodes.count)
|
||||||
|
let pathSnodeCount = (targetGuardSnodeCount - reusableGuardSnodeCount) * pathSize - (targetGuardSnodeCount - reusableGuardSnodeCount)
|
||||||
|
guard unusedSnodes.count >= pathSnodeCount else { throw Error.insufficientSnodes }
|
||||||
|
// Don't test path snodes as this would reveal the user's IP to them
|
||||||
|
return guardSnodes.subtracting(reusableGuardSnodes).map { guardSnode in
|
||||||
|
let result = [ guardSnode ] + (0..<(pathSize - 1)).map { _ in
|
||||||
|
// randomElement() uses the system's default random generator, which is cryptographically secure
|
||||||
|
let pathSnode = unusedSnodes.randomElement()! // Safe because of the pathSnodeCount check above
|
||||||
|
unusedSnodes.remove(pathSnode) // All used snodes should be unique
|
||||||
|
return pathSnode
|
||||||
|
}
|
||||||
|
SNLog("Built new onion request path: \(result.prettifiedDescription).")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}.map2 { paths in
|
||||||
|
OnionRequestAPI.paths = paths + reusablePaths
|
||||||
|
SNSnodeKitConfiguration.shared.storage.writeSync { transaction in
|
||||||
|
SNLog("Persisting onion request paths to database.")
|
||||||
|
SNSnodeKitConfiguration.shared.storage.setOnionRequestPaths(to: paths, using: transaction)
|
||||||
|
}
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
NotificationCenter.default.post(name: .pathsBuilt, object: nil)
|
||||||
|
}
|
||||||
|
return paths
|
||||||
|
}
|
||||||
|
promise.done2 { _ in buildPathsPromise = nil }
|
||||||
|
promise.catch2 { _ in buildPathsPromise = nil }
|
||||||
|
buildPathsPromise = promise
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a `Path` to be used for building an onion request. Builds new paths as needed.
|
||||||
|
private static func getPath(excluding snode: Snode?) -> Promise<Path> {
|
||||||
|
guard pathSize >= 1 else { preconditionFailure("Can't build path of size zero.") }
|
||||||
|
var paths = OnionRequestAPI.paths
|
||||||
|
if paths.isEmpty {
|
||||||
|
paths = SNSnodeKitConfiguration.shared.storage.getOnionRequestPaths()
|
||||||
|
OnionRequestAPI.paths = paths
|
||||||
|
if !paths.isEmpty {
|
||||||
|
guardSnodes.formUnion([ paths[0][0] ])
|
||||||
|
if paths.count >= 2 {
|
||||||
|
guardSnodes.formUnion([ paths[1][0] ])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// randomElement() uses the system's default random generator, which is cryptographically secure
|
||||||
|
if paths.count >= targetPathCount {
|
||||||
|
if let snode = snode {
|
||||||
|
return Promise { $0.fulfill(paths.filter { !$0.contains(snode) }.randomElement()!) }
|
||||||
|
} else {
|
||||||
|
return Promise { $0.fulfill(paths.randomElement()!) }
|
||||||
|
}
|
||||||
|
} else if !paths.isEmpty {
|
||||||
|
if let snode = snode {
|
||||||
|
if let path = paths.first(where: { !$0.contains(snode) }) {
|
||||||
|
buildPaths(reusing: paths) // Re-build paths in the background
|
||||||
|
return Promise { $0.fulfill(path) }
|
||||||
|
} else {
|
||||||
|
return buildPaths(reusing: paths).map2 { paths in
|
||||||
|
return paths.filter { !$0.contains(snode) }.randomElement()!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
buildPaths(reusing: paths) // Re-build paths in the background
|
||||||
|
return Promise { $0.fulfill(paths.randomElement()!) }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return buildPaths(reusing: []).map2 { paths in
|
||||||
|
if let snode = snode {
|
||||||
|
return paths.filter { !$0.contains(snode) }.randomElement()!
|
||||||
|
} else {
|
||||||
|
return paths.randomElement()!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func dropGuardSnode(_ snode: Snode) {
|
||||||
|
#if DEBUG
|
||||||
|
dispatchPrecondition(condition: .onQueue(Threading.workQueue))
|
||||||
|
#endif
|
||||||
|
guardSnodes = guardSnodes.filter { $0 != snode }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func drop(_ snode: Snode) throws {
|
||||||
|
#if DEBUG
|
||||||
|
dispatchPrecondition(condition: .onQueue(Threading.workQueue))
|
||||||
|
#endif
|
||||||
|
// We repair the path here because we can do it sync. In the case where we drop a whole
|
||||||
|
// path we leave the re-building up to getPath(excluding:) because re-building the path
|
||||||
|
// in that case is async.
|
||||||
|
LegacyOnionRequestAPI.snodeFailureCount[snode] = 0
|
||||||
|
var oldPaths = paths
|
||||||
|
guard let pathIndex = oldPaths.firstIndex(where: { $0.contains(snode) }) else { return }
|
||||||
|
var path = oldPaths[pathIndex]
|
||||||
|
guard let snodeIndex = path.firstIndex(of: snode) else { return }
|
||||||
|
path.remove(at: snodeIndex)
|
||||||
|
let unusedSnodes = SnodeAPI.snodePool.subtracting(oldPaths.flatMap { $0 })
|
||||||
|
guard !unusedSnodes.isEmpty else { throw Error.insufficientSnodes }
|
||||||
|
// randomElement() uses the system's default random generator, which is cryptographically secure
|
||||||
|
path.append(unusedSnodes.randomElement()!)
|
||||||
|
// Don't test the new snode as this would reveal the user's IP
|
||||||
|
oldPaths.remove(at: pathIndex)
|
||||||
|
let newPaths = oldPaths + [ path ]
|
||||||
|
paths = newPaths
|
||||||
|
SNSnodeKitConfiguration.shared.storage.writeSync { transaction in
|
||||||
|
SNLog("Persisting onion request paths to database.")
|
||||||
|
SNSnodeKitConfiguration.shared.storage.setOnionRequestPaths(to: newPaths, using: transaction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func drop(_ path: Path) {
|
||||||
|
#if DEBUG
|
||||||
|
dispatchPrecondition(condition: .onQueue(Threading.workQueue))
|
||||||
|
#endif
|
||||||
|
LegacyOnionRequestAPI.pathFailureCount[path] = 0
|
||||||
|
var paths = LegacyOnionRequestAPI.paths
|
||||||
|
guard let pathIndex = paths.firstIndex(of: path) else { return }
|
||||||
|
paths.remove(at: pathIndex)
|
||||||
|
OnionRequestAPI.paths = paths
|
||||||
|
SNSnodeKitConfiguration.shared.storage.writeSync { transaction in
|
||||||
|
if !paths.isEmpty {
|
||||||
|
SNLog("Persisting onion request paths to database.")
|
||||||
|
SNSnodeKitConfiguration.shared.storage.setOnionRequestPaths(to: paths, using: transaction)
|
||||||
|
} else {
|
||||||
|
SNLog("Clearing onion request paths.")
|
||||||
|
SNSnodeKitConfiguration.shared.storage.setOnionRequestPaths(to: [], using: transaction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds an onion around `payload` and returns the result.
|
||||||
|
private static func buildOnion(around payload: JSON, targetedAt destination: OnionRequestAPI.Destination) -> Promise<OnionBuildingResult> {
|
||||||
|
var guardSnode: Snode!
|
||||||
|
var targetSnodeSymmetricKey: Data! // Needed by invoke(_:on:with:) to decrypt the response sent back by the destination
|
||||||
|
var encryptionResult: AESGCM.EncryptionResult!
|
||||||
|
var snodeToExclude: Snode?
|
||||||
|
if case .snode(let snode) = destination { snodeToExclude = snode }
|
||||||
|
return getPath(excluding: snodeToExclude).then2 { path -> Promise<AESGCM.EncryptionResult> in
|
||||||
|
guardSnode = path.first!
|
||||||
|
// Encrypt in reverse order, i.e. the destination first
|
||||||
|
return OnionRequestAPI.encrypt(payload, for: destination).then2 { r -> Promise<AESGCM.EncryptionResult> in
|
||||||
|
targetSnodeSymmetricKey = r.symmetricKey
|
||||||
|
// Recursively encrypt the layers of the onion (again in reverse order)
|
||||||
|
encryptionResult = r
|
||||||
|
var path = path
|
||||||
|
var rhs = destination
|
||||||
|
func addLayer() -> Promise<AESGCM.EncryptionResult> {
|
||||||
|
if path.isEmpty {
|
||||||
|
return Promise<AESGCM.EncryptionResult> { $0.fulfill(encryptionResult) }
|
||||||
|
} else {
|
||||||
|
let lhs = OnionRequestAPI.Destination.snode(path.removeLast())
|
||||||
|
return OnionRequestAPI.encryptHop(from: lhs, to: rhs, using: encryptionResult).then2 { r -> Promise<AESGCM.EncryptionResult> in
|
||||||
|
encryptionResult = r
|
||||||
|
rhs = lhs
|
||||||
|
return addLayer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return addLayer()
|
||||||
|
}
|
||||||
|
}.map2 { _ in (guardSnode, encryptionResult, targetSnodeSymmetricKey) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Public API
|
||||||
|
/// Sends an onion request to `snode`. Builds new paths as needed.
|
||||||
|
public static func sendOnionRequest(to snode: Snode, invoking method: Snode.Method, with parameters: JSON, associatedWith publicKey: String? = nil) -> Promise<JSON> {
|
||||||
|
let payload: JSON = [ "method" : method.rawValue, "params" : parameters ]
|
||||||
|
return sendOnionRequest(with: payload, to: OnionRequestAPI.Destination.snode(snode)).recover2 { error -> Promise<JSON> in
|
||||||
|
guard case OnionRequestAPI.Error.httpRequestFailedAtDestination(let statusCode, let json, _) = error else { throw error }
|
||||||
|
throw SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode, associatedWith: publicKey) ?? error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sends an onion request to `server`. Builds new paths as needed.
|
||||||
|
public static func sendOnionRequest(_ request: URLRequest, to server: String, target: String = "/loki/v3/lsrpc", using x25519PublicKey: String) -> Promise<(OnionRequestAPI.ResponseInfo, Data?)> {
|
||||||
|
var rawHeaders = request.allHTTPHeaderFields ?? [:]
|
||||||
|
rawHeaders.removeValue(forKey: "User-Agent")
|
||||||
|
var headers: JSON = rawHeaders.mapValues { value in
|
||||||
|
switch value.lowercased() {
|
||||||
|
case "true": return true
|
||||||
|
case "false": return false
|
||||||
|
default: return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
guard let url = request.url, let host = request.url?.host else { return Promise(error: Error.invalidURL) }
|
||||||
|
var endpoint = url.path.removingPrefix("/")
|
||||||
|
if let query = url.query { endpoint += "?\(query)" }
|
||||||
|
let scheme = url.scheme
|
||||||
|
let port = given(url.port) { UInt16($0) }
|
||||||
|
let bodyAsString: String
|
||||||
|
|
||||||
|
if let body: Data = request.httpBody {
|
||||||
|
headers["Content-Type"] = "application/json" // Assume data is JSON
|
||||||
|
bodyAsString = (String(data: body, encoding: .utf8) ?? "null")
|
||||||
|
}
|
||||||
|
else if let inputStream: InputStream = request.httpBodyStream, let body: Data = try? Data(from: inputStream) {
|
||||||
|
headers["Content-Type"] = request.allHTTPHeaderFields!["Content-Type"]
|
||||||
|
bodyAsString = "{ \"fileUpload\" : \"\(String(data: body.base64EncodedData(), encoding: .utf8) ?? "null")\" }"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
bodyAsString = "null"
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload: JSON = [
|
||||||
|
"body" : bodyAsString,
|
||||||
|
"endpoint" : endpoint,
|
||||||
|
"method" : request.httpMethod!,
|
||||||
|
"headers" : headers
|
||||||
|
]
|
||||||
|
let destination = OnionRequestAPI.Destination.server(host: host, target: target, x25519PublicKey: x25519PublicKey, scheme: scheme, port: port)
|
||||||
|
let promise = sendOnionRequest(with: payload, to: destination)
|
||||||
|
.map { (json: JSON) -> (OnionRequestAPI.ResponseInfo, Data?) in
|
||||||
|
guard let data = try? JSONSerialization.data(withJSONObject: json, options: []) else { throw HTTP.Error.invalidJSON }
|
||||||
|
|
||||||
|
return (OnionRequestAPI.ResponseInfo(code: 200, headers: [:]), data)
|
||||||
|
}
|
||||||
|
promise.catch2 { error in
|
||||||
|
SNLog("Couldn't reach server: \(url) due to error: \(error).")
|
||||||
|
}
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func sendOnionRequest(with payload: JSON, to destination: OnionRequestAPI.Destination) -> Promise<JSON> {
|
||||||
|
let (promise, seal) = Promise<JSON>.pending()
|
||||||
|
var guardSnode: Snode?
|
||||||
|
Threading.workQueue.async { // Avoid race conditions on `guardSnodes` and `paths`
|
||||||
|
buildOnion(around: payload, targetedAt: destination).done2 { intermediate in
|
||||||
|
guardSnode = intermediate.guardSnode
|
||||||
|
let url = "\(guardSnode!.address):\(guardSnode!.port)/onion_req/v2"
|
||||||
|
let finalEncryptionResult = intermediate.finalEncryptionResult
|
||||||
|
let onion = finalEncryptionResult.ciphertext
|
||||||
|
if case OnionRequestAPI.Destination.server = destination, Double(onion.count) > 0.75 * Double(maxRequestSize) {
|
||||||
|
SNLog("Approaching request size limit: ~\(onion.count) bytes.")
|
||||||
|
}
|
||||||
|
let parameters: JSON = [
|
||||||
|
"ephemeral_key" : finalEncryptionResult.ephemeralPublicKey.toHexString()
|
||||||
|
]
|
||||||
|
let body: Data
|
||||||
|
do {
|
||||||
|
body = try OnionRequestAPI.encode(ciphertext: onion, json: parameters)
|
||||||
|
} catch {
|
||||||
|
return seal.reject(error)
|
||||||
|
}
|
||||||
|
let destinationSymmetricKey = intermediate.destinationSymmetricKey
|
||||||
|
HTTP.execute(.post, url, body: body).done2 { json in
|
||||||
|
guard let base64EncodedIVAndCiphertext = json["result"] as? String,
|
||||||
|
let ivAndCiphertext = Data(base64Encoded: base64EncodedIVAndCiphertext), ivAndCiphertext.count >= AESGCM.ivSize else { return seal.reject(HTTP.Error.invalidJSON) }
|
||||||
|
do {
|
||||||
|
let data = try AESGCM.decrypt(ivAndCiphertext, with: destinationSymmetricKey)
|
||||||
|
guard let json = try JSONSerialization.jsonObject(with: data, options: [ .fragmentsAllowed ]) as? JSON,
|
||||||
|
let statusCode = json["status_code"] as? Int ?? json["status"] as? Int else { return seal.reject(HTTP.Error.invalidJSON) }
|
||||||
|
if statusCode == 406 { // Clock out of sync
|
||||||
|
SNLog("The user's clock is out of sync with the service node network.")
|
||||||
|
seal.reject(SnodeAPI.Error.clockOutOfSync)
|
||||||
|
} else if let bodyAsString = json["body"] as? String {
|
||||||
|
guard let bodyAsData = bodyAsString.data(using: .utf8),
|
||||||
|
let body = try JSONSerialization.jsonObject(with: bodyAsData, options: [ .fragmentsAllowed ]) as? JSON else { return seal.reject(HTTP.Error.invalidJSON) }
|
||||||
|
if let timestamp = body["t"] as? Int64 {
|
||||||
|
let offset = timestamp - Int64(NSDate.millisecondTimestamp())
|
||||||
|
SnodeAPI.clockOffset = offset
|
||||||
|
}
|
||||||
|
guard 200...299 ~= statusCode else {
|
||||||
|
return seal.reject(Error.httpRequestFailedAtDestination(statusCode: UInt(statusCode), json: body, destination: destination))
|
||||||
|
}
|
||||||
|
seal.fulfill(body)
|
||||||
|
} else {
|
||||||
|
guard 200...299 ~= statusCode else {
|
||||||
|
return seal.reject(Error.httpRequestFailedAtDestination(statusCode: UInt(statusCode), json: json, destination: destination))
|
||||||
|
}
|
||||||
|
seal.fulfill(json)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
seal.reject(error)
|
||||||
|
}
|
||||||
|
}.catch2 { error in
|
||||||
|
seal.reject(error)
|
||||||
|
}
|
||||||
|
}.catch2 { error in
|
||||||
|
seal.reject(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
promise.catch2 { error in // Must be invoked on Threading.workQueue
|
||||||
|
guard case HTTP.Error.httpRequestFailed(let statusCode, let json) = error, let guardSnode = guardSnode else { return }
|
||||||
|
let path = paths.first { $0.contains(guardSnode) }
|
||||||
|
func handleUnspecificError() {
|
||||||
|
guard let path = path else { return }
|
||||||
|
var pathFailureCount = LegacyOnionRequestAPI.pathFailureCount[path] ?? 0
|
||||||
|
pathFailureCount += 1
|
||||||
|
if pathFailureCount >= pathFailureThreshold {
|
||||||
|
dropGuardSnode(guardSnode)
|
||||||
|
path.forEach { snode in
|
||||||
|
SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode) // Intentionally don't throw
|
||||||
|
}
|
||||||
|
drop(path)
|
||||||
|
} else {
|
||||||
|
LegacyOnionRequestAPI.pathFailureCount[path] = pathFailureCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let prefix = "Next node not found: "
|
||||||
|
if let message = json?["result"] as? String, message.hasPrefix(prefix) {
|
||||||
|
let ed25519PublicKey = message[message.index(message.startIndex, offsetBy: prefix.count)..<message.endIndex]
|
||||||
|
if let path = path, let snode = path.first(where: { $0.publicKeySet.ed25519Key == ed25519PublicKey }) {
|
||||||
|
var snodeFailureCount = LegacyOnionRequestAPI.snodeFailureCount[snode] ?? 0
|
||||||
|
snodeFailureCount += 1
|
||||||
|
if snodeFailureCount >= snodeFailureThreshold {
|
||||||
|
SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode) // Intentionally don't throw
|
||||||
|
do {
|
||||||
|
try drop(snode)
|
||||||
|
} catch {
|
||||||
|
handleUnspecificError()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LegacyOnionRequestAPI.snodeFailureCount[snode] = snodeFailureCount
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
} else if let message = json?["result"] as? String, message == "Loki Server error" {
|
||||||
|
// Do nothing
|
||||||
|
} else if case .server(let host, _, _, _, _) = destination, host == "116.203.70.33" && statusCode == 0 {
|
||||||
|
// FIXME: Temporary thing to kick out nodes that can't talk to the V2 OGS yet
|
||||||
|
handleUnspecificError()
|
||||||
|
} else if statusCode == 0 { // Timeout
|
||||||
|
// Do nothing
|
||||||
|
} else {
|
||||||
|
handleUnspecificError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,30 +14,63 @@ internal extension OnionRequestAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encrypts `payload` for `destination` and returns the result. Use this to build the core of an onion request.
|
/// Encrypts `payload` for `destination` and returns the result. Use this to build the core of an onion request.
|
||||||
|
static func encrypt(_ payload: String, for destination: Destination) -> Promise<AESGCM.EncryptionResult> {
|
||||||
|
let (promise, seal) = Promise<AESGCM.EncryptionResult>.pending()
|
||||||
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
|
do {
|
||||||
|
guard let data = payload.data(using: .utf8) else {
|
||||||
|
throw Error.invalidRequestInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = try encrypt(data, for: destination)
|
||||||
|
seal.fulfill(result)
|
||||||
|
}
|
||||||
|
catch (let error) {
|
||||||
|
seal.reject(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
|
||||||
static func encrypt(_ payload: JSON, for destination: Destination) -> Promise<AESGCM.EncryptionResult> {
|
static func encrypt(_ payload: JSON, for destination: Destination) -> Promise<AESGCM.EncryptionResult> {
|
||||||
let (promise, seal) = Promise<AESGCM.EncryptionResult>.pending()
|
let (promise, seal) = Promise<AESGCM.EncryptionResult>.pending()
|
||||||
DispatchQueue.global(qos: .userInitiated).async {
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
do {
|
do {
|
||||||
guard JSONSerialization.isValidJSONObject(payload) else { return seal.reject(HTTP.Error.invalidJSON) }
|
guard JSONSerialization.isValidJSONObject(payload) else { return seal.reject(HTTP.Error.invalidJSON) }
|
||||||
|
|
||||||
// Wrapping isn't needed for file server or open group onion requests
|
// Wrapping isn't needed for file server or open group onion requests
|
||||||
switch destination {
|
switch destination {
|
||||||
case .snode(let snode):
|
case .snode:
|
||||||
let snodeX25519PublicKey = snode.publicKeySet.x25519Key
|
let payloadAsData = try JSONSerialization.data(withJSONObject: payload, options: [ .fragmentsAllowed ])
|
||||||
let payloadAsData = try JSONSerialization.data(withJSONObject: payload, options: [ .fragmentsAllowed ])
|
let data = try encode(ciphertext: payloadAsData, json: [ "headers" : "" ])
|
||||||
let plaintext = try encode(ciphertext: payloadAsData, json: [ "headers" : "" ])
|
let result = try encrypt(data, for: destination)
|
||||||
let result = try AESGCM.encrypt(plaintext, for: snodeX25519PublicKey)
|
seal.fulfill(result)
|
||||||
seal.fulfill(result)
|
|
||||||
case .server(_, _, let serverX25519PublicKey, _, _):
|
case .server:
|
||||||
let plaintext = try JSONSerialization.data(withJSONObject: payload, options: [ .fragmentsAllowed ])
|
let data = try JSONSerialization.data(withJSONObject: payload, options: [ .fragmentsAllowed ])
|
||||||
let result = try AESGCM.encrypt(plaintext, for: serverX25519PublicKey)
|
let result = try encrypt(data, for: destination)
|
||||||
seal.fulfill(result)
|
seal.fulfill(result)
|
||||||
}
|
}
|
||||||
} catch (let error) {
|
}
|
||||||
|
catch (let error) {
|
||||||
seal.reject(error)
|
seal.reject(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return promise
|
return promise
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func encrypt(_ payload: Data, for destination: Destination) throws -> AESGCM.EncryptionResult {
|
||||||
|
switch destination {
|
||||||
|
case .snode(let snode):
|
||||||
|
let snodeX25519PublicKey = snode.publicKeySet.x25519Key
|
||||||
|
return try AESGCM.encrypt(payload, for: snodeX25519PublicKey)
|
||||||
|
|
||||||
|
case .server(_, _, let serverX25519PublicKey, _, _):
|
||||||
|
return try AESGCM.encrypt(payload, for: serverX25519PublicKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Encrypts the previous encryption result (i.e. that of the hop after this one) for this hop. Use this to build the layers of an onion request.
|
/// Encrypts the previous encryption result (i.e. that of the hop after this one) for this hop. Use this to build the layers of an onion request.
|
||||||
static func encryptHop(from lhs: Destination, to rhs: Destination, using previousEncryptionResult: AESGCM.EncryptionResult) -> Promise<AESGCM.EncryptionResult> {
|
static func encryptHop(from lhs: Destination, to rhs: Destination, using previousEncryptionResult: AESGCM.EncryptionResult) -> Promise<AESGCM.EncryptionResult> {
|
||||||
|
|
|
@ -3,8 +3,18 @@ import CryptoSwift
|
||||||
import PromiseKit
|
import PromiseKit
|
||||||
import SessionUtilitiesKit
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
|
public protocol OnionRequestAPIType {
|
||||||
|
static func sendOnionRequest(_ request: URLRequest, to server: String, target: String, using x25519PublicKey: String) -> Promise<(OnionRequestAPI.ResponseInfo, Data?)>
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension OnionRequestAPIType {
|
||||||
|
static func sendOnionRequest(_ request: URLRequest, to server: String, using x25519PublicKey: String) -> Promise<(OnionRequestAPI.ResponseInfo, Data?)> {
|
||||||
|
sendOnionRequest(request, to: server, target: "/oxen/v4/lsrpc", using: x25519PublicKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// See the "Onion Requests" section of [The Session Whitepaper](https://arxiv.org/pdf/2002.04609.pdf) for more information.
|
/// See the "Onion Requests" section of [The Session Whitepaper](https://arxiv.org/pdf/2002.04609.pdf) for more information.
|
||||||
public enum OnionRequestAPI {
|
public enum OnionRequestAPI: OnionRequestAPIType {
|
||||||
private static var buildPathsPromise: Promise<[Path]>? = nil
|
private static var buildPathsPromise: Promise<[Path]>? = nil
|
||||||
/// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
|
/// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
|
||||||
private static var pathFailureCount: [Path:UInt] = [:]
|
private static var pathFailureCount: [Path:UInt] = [:]
|
||||||
|
@ -49,6 +59,7 @@ public enum OnionRequestAPI {
|
||||||
case missingSnodeVersion
|
case missingSnodeVersion
|
||||||
case snodePublicKeySetMissing
|
case snodePublicKeySetMissing
|
||||||
case unsupportedSnodeVersion(String)
|
case unsupportedSnodeVersion(String)
|
||||||
|
case invalidRequestInfo
|
||||||
|
|
||||||
public var errorDescription: String? {
|
public var errorDescription: String? {
|
||||||
switch self {
|
switch self {
|
||||||
|
@ -63,9 +74,22 @@ public enum OnionRequestAPI {
|
||||||
case .missingSnodeVersion: return "Missing Service Node version."
|
case .missingSnodeVersion: return "Missing Service Node version."
|
||||||
case .snodePublicKeySetMissing: return "Missing Service Node public key set."
|
case .snodePublicKeySetMissing: return "Missing Service Node public key set."
|
||||||
case .unsupportedSnodeVersion(let version): return "Unsupported Service Node version: \(version)."
|
case .unsupportedSnodeVersion(let version): return "Unsupported Service Node version: \(version)."
|
||||||
|
case .invalidRequestInfo: return "Invalid Request Info"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: RequestInfo
|
||||||
|
private struct RequestInfo: Codable {
|
||||||
|
let method: String
|
||||||
|
let endpoint: String
|
||||||
|
let headers: [String: String]
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ResponseInfo: Codable {
|
||||||
|
let code: Int
|
||||||
|
let headers: [String: String]
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: Path
|
// MARK: Path
|
||||||
public typealias Path = [Snode]
|
public typealias Path = [Snode]
|
||||||
|
@ -268,7 +292,7 @@ public enum OnionRequestAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds an onion around `payload` and returns the result.
|
/// Builds an onion around `payload` and returns the result.
|
||||||
private static func buildOnion(around payload: JSON, targetedAt destination: Destination) -> Promise<OnionBuildingResult> {
|
private static func buildOnion(around payload: String, targetedAt destination: Destination) -> Promise<OnionBuildingResult> {
|
||||||
var guardSnode: Snode!
|
var guardSnode: Snode!
|
||||||
var targetSnodeSymmetricKey: Data! // Needed by invoke(_:on:with:) to decrypt the response sent back by the destination
|
var targetSnodeSymmetricKey: Data! // Needed by invoke(_:on:with:) to decrypt the response sent back by the destination
|
||||||
var encryptionResult: AESGCM.EncryptionResult!
|
var encryptionResult: AESGCM.EncryptionResult!
|
||||||
|
@ -301,57 +325,67 @@ public enum OnionRequestAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Public API
|
// MARK: Public API
|
||||||
/// Sends an onion request to `snode`. Builds new paths as needed.
|
// /// Sends an onion request to `snode`. Builds new paths as needed.
|
||||||
public static func sendOnionRequest(to snode: Snode, invoking method: Snode.Method, with parameters: JSON, associatedWith publicKey: String? = nil) -> Promise<Data> {
|
// public static func sendOnionRequest(to snode: Snode, invoking method: Snode.Method, with parameters: JSON, associatedWith publicKey: String? = nil) -> Promise<Data> {
|
||||||
let payload: JSON = [ "method" : method.rawValue, "params" : parameters ]
|
// let payload: JSON = [ "method" : method.rawValue, "params" : parameters ]
|
||||||
return sendOnionRequest(with: payload, to: Destination.snode(snode)).recover2 { error -> Promise<Data> in
|
// return sendOnionRequest(with: payload, to: Destination.snode(snode)).recover2 { error -> Promise<Data> in
|
||||||
guard case OnionRequestAPI.Error.httpRequestFailedAtDestination(let statusCode, let json, _) = error else { throw error }
|
// guard case OnionRequestAPI.Error.httpRequestFailedAtDestination(let statusCode, let json, _) = error else { throw error }
|
||||||
throw SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode, associatedWith: publicKey) ?? error
|
// throw SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode, associatedWith: publicKey) ?? error
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
/// Sends an onion request to `server`. Builds new paths as needed.
|
/// Sends an onion request to `server`. Builds new paths as needed.
|
||||||
public static func sendOnionRequest(_ request: URLRequest, to server: String, target: String = "/loki/v3/lsrpc", using x25519PublicKey: String) -> Promise<Data> {
|
public static func sendOnionRequest(_ request: URLRequest, to server: String, target: String = "/oxen/v4/lsrpc", using x25519PublicKey: String) -> Promise<(ResponseInfo, Data?)> {
|
||||||
|
guard server == "https://chat.lokinet.dev" else { // TODO: Remove this
|
||||||
|
return LegacyOnionRequestAPI.sendOnionRequest(request, to: server, target: "/loki/v3/lsrpc", using: x25519PublicKey)
|
||||||
|
}
|
||||||
guard let url = request.url, let host = request.url?.host else { return Promise(error: Error.invalidURL) }
|
guard let url = request.url, let host = request.url?.host else { return Promise(error: Error.invalidURL) }
|
||||||
|
|
||||||
var headers: JSON = (request.allHTTPHeaderFields ?? [:])
|
|
||||||
.mapValues { value -> Any in
|
|
||||||
switch value.lowercased() {
|
|
||||||
case "true": return true
|
|
||||||
case "false": return false
|
|
||||||
default: return value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.removingValue(forKey: "User-Agent")
|
|
||||||
|
|
||||||
// Note: We need to remove the leading forward slash unless we are explicitly hitting a legacy
|
// Note: We need to remove the leading forward slash unless we are explicitly hitting a legacy
|
||||||
// endpoint (in which case we need it to ensure the request signing works correctly
|
// endpoint (in which case we need it to ensure the request signing works correctly
|
||||||
// TODO: Confirm the 'removingPrefix' isn't going to break the request signing on non-legacy endpoints
|
// TODO: Confirm the 'removingPrefix' isn't going to break the request signing on non-legacy endpoints.
|
||||||
let endpoint: String = url.path
|
let endpoint: String = url.path
|
||||||
// .removingPrefix("/", if: !url.path.starts(with: "/legacy"))
|
|
||||||
.appending(url.query.map { value in "?\(value)" })
|
.appending(url.query.map { value in "?\(value)" })
|
||||||
let scheme: String? = url.scheme
|
let scheme: String? = url.scheme
|
||||||
let port: UInt16? = url.port.map { UInt16($0) }
|
let port: UInt16? = url.port.map { UInt16($0) }
|
||||||
let bodyAsString: String
|
|
||||||
|
let requestInfo: RequestInfo = RequestInfo(
|
||||||
|
method: (request.httpMethod ?? "GET"), // Default (if nil) is 'GET'
|
||||||
|
endpoint: endpoint,
|
||||||
|
headers: (request.allHTTPHeaderFields ?? [:])
|
||||||
|
.setting(
|
||||||
|
"Content-Type",
|
||||||
|
// TODO: Determine what 'Content-Type' 'httpBodyStream' should have???
|
||||||
|
(request.httpBody == nil && request.httpBodyStream == nil ? nil :
|
||||||
|
((request.allHTTPHeaderFields ?? [:])["Content-Type"] ?? "application/json") // Default to JSON if not defined
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.removingValue(forKey: "User-Agent")
|
||||||
|
)
|
||||||
|
|
||||||
|
guard let requestInfoData: Data = try? JSONEncoder().encode(requestInfo), let requestInfoString: String = String(data: requestInfoData, encoding: .ascii) else {
|
||||||
|
return Promise(error: Error.invalidRequestInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload: String
|
||||||
|
|
||||||
if let body: Data = request.httpBody {
|
if let body: Data = request.httpBody {
|
||||||
headers["Content-Type"] = "application/json" // Assume data is JSON
|
guard let bodyString: String = String(data: body, encoding: .ascii) else {
|
||||||
bodyAsString = (String(data: body, encoding: .utf8) ?? "null")
|
return Promise(error: Error.invalidRequestInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = "l\(requestInfoString.count):\(requestInfoString)\(bodyString.count):\(bodyString)e"
|
||||||
}
|
}
|
||||||
else if let inputStream: InputStream = request.httpBodyStream, let body: Data = try? Data(from: inputStream) {
|
else if let inputStream: InputStream = request.httpBodyStream, let body: Data = try? Data(from: inputStream), let bodyString: String = String(data: body, encoding: .ascii) {
|
||||||
headers["Content-Type"] = request.allHTTPHeaderFields!["Content-Type"]
|
// TODO: Handle this properly
|
||||||
bodyAsString = "{ \"fileUpload\" : \"\(String(data: body.base64EncodedData(), encoding: .utf8) ?? "null")\" }"
|
// headers["Content-Type"] = request.allHTTPHeaderFields!["Content-Type"]
|
||||||
|
// bodyAsString = "{ \"fileUpload\" : \"\(String(data: body.base64EncodedData(), encoding: .utf8) ?? "null")\" }"
|
||||||
|
payload = "l\(requestInfoString.count):\(requestInfoString)\(bodyString.count):\(bodyString)e"
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
bodyAsString = "null"
|
payload = "l\(requestInfoString.count):\(requestInfoString)e"
|
||||||
}
|
}
|
||||||
|
|
||||||
let payload: JSON = [
|
|
||||||
"body" : bodyAsString,
|
|
||||||
"endpoint" : endpoint,
|
|
||||||
"method" : (request.httpMethod ?? "GET"), // Default (if nil) is 'GET'
|
|
||||||
"headers" : headers
|
|
||||||
]
|
|
||||||
let destination = Destination.server(host: host, target: target, x25519PublicKey: x25519PublicKey, scheme: scheme, port: port)
|
let destination = Destination.server(host: host, target: target, x25519PublicKey: x25519PublicKey, scheme: scheme, port: port)
|
||||||
let promise = sendOnionRequest(with: payload, to: destination)
|
let promise = sendOnionRequest(with: payload, to: destination)
|
||||||
promise.catch2 { error in
|
promise.catch2 { error in
|
||||||
|
@ -360,8 +394,8 @@ public enum OnionRequestAPI {
|
||||||
return promise
|
return promise
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func sendOnionRequest(with payload: JSON, to destination: Destination) -> Promise<Data> {
|
public static func sendOnionRequest(with payload: String, to destination: Destination) -> Promise<(ResponseInfo, Data?)> {
|
||||||
let (promise, seal) = Promise<Data>.pending()
|
let (promise, seal) = Promise<(ResponseInfo, Data?)>.pending()
|
||||||
var guardSnode: Snode?
|
var guardSnode: Snode?
|
||||||
Threading.workQueue.async { // Avoid race conditions on `guardSnodes` and `paths`
|
Threading.workQueue.async { // Avoid race conditions on `guardSnodes` and `paths`
|
||||||
buildOnion(around: payload, targetedAt: destination).done2 { intermediate in
|
buildOnion(around: payload, targetedAt: destination).done2 { intermediate in
|
||||||
|
@ -383,68 +417,78 @@ public enum OnionRequestAPI {
|
||||||
}
|
}
|
||||||
let destinationSymmetricKey = intermediate.destinationSymmetricKey
|
let destinationSymmetricKey = intermediate.destinationSymmetricKey
|
||||||
|
|
||||||
HTTP.execute(.post, url, body: body)
|
HTTP.updatedExecute(.post, url, body: body)
|
||||||
.done2 { json in
|
.done2 { responseData in
|
||||||
guard let base64EncodedIVAndCiphertext = json["result"] as? String, let ivAndCiphertext = Data(base64Encoded: base64EncodedIVAndCiphertext), ivAndCiphertext.count >= AESGCM.ivSize else {
|
guard responseData.count >= AESGCM.ivSize else { return seal.reject(HTTP.Error.invalidResponse) }
|
||||||
return seal.reject(HTTP.Error.invalidJSON)
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let data = try AESGCM.decrypt(ivAndCiphertext, with: destinationSymmetricKey)
|
let data: Data = try AESGCM.decrypt(responseData, with: destinationSymmetricKey)
|
||||||
|
|
||||||
// The JSON data can be either an array or an object so can't cast to 'JSON' here
|
// The data will be in the form of `l123:jsone` or `l123:json456:bodye` so we need to break the data into
|
||||||
// TODO: Would be nice to ditch this 'JSONSerialization' behaviour if we can
|
// parts to properly process it
|
||||||
guard let jsonObject = try? JSONSerialization.jsonObject(with: data, options: [ .fragmentsAllowed ]) else {
|
guard let responseString: String = String(data: data, encoding: .ascii), responseString.starts(with: "l") else {
|
||||||
return seal.reject(HTTP.Error.invalidJSON)
|
return seal.reject(HTTP.Error.invalidResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: How do we now handle this case when the `status_code` is out of sync now that the value isn't provided?
|
let stringParts: [String.SubSequence] = responseString.split(separator: ":")
|
||||||
// TODO: Upgrade to V4?
|
|
||||||
var customStatusCode: Int = 200
|
|
||||||
|
|
||||||
if let json: JSON = jsonObject as? JSON, let bodyStatusCode: Int = (((json["status_code"] as? Int) ?? json["status"] as? Int) ?? json["code"] as? Int) {
|
guard stringParts.count > 1, let infoLength: Int = Int(stringParts[0].suffix(from: stringParts[0].index(stringParts[0].startIndex, offsetBy: 1))) else {
|
||||||
guard bodyStatusCode != 406 else {
|
return seal.reject(HTTP.Error.invalidResponse)
|
||||||
SNLog("The user's clock is out of sync with the service node network.")
|
|
||||||
return seal.reject(SnodeAPI.Error.clockOutOfSync)
|
|
||||||
}
|
|
||||||
|
|
||||||
customStatusCode = bodyStatusCode
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let json: JSON = jsonObject as? JSON, let bodyAsString: String = json["body"] as? String {
|
let infoStringStartIndex: String.Index = responseString.index(responseString.startIndex, offsetBy: "l\(infoLength):".count)
|
||||||
guard let bodyAsData = bodyAsString.data(using: .utf8), let body = try JSONSerialization.jsonObject(with: bodyAsData, options: [ .fragmentsAllowed ]) as? JSON else {
|
let infoStringEndIndex: String.Index = responseString.index(infoStringStartIndex, offsetBy: infoLength)
|
||||||
return seal.reject(HTTP.Error.invalidJSON)
|
let infoString: String = String(responseString[infoStringStartIndex..<infoStringEndIndex])
|
||||||
}
|
|
||||||
|
guard let infoStringData: Data = infoString.data(using: .utf8), let responseInfo: ResponseInfo = try? JSONDecoder().decode(ResponseInfo.self, from: infoStringData) else {
|
||||||
if let timestamp = body["t"] as? Int64 {
|
return seal.reject(HTTP.Error.invalidResponse)
|
||||||
let offset = timestamp - Int64(NSDate.millisecondTimestamp())
|
}
|
||||||
SnodeAPI.clockOffset = offset
|
|
||||||
}
|
// Custom handle a clock out of sync error
|
||||||
|
guard responseInfo.code != 406 else {
|
||||||
guard 200...299 ~= customStatusCode else {
|
SNLog("The user's clock is out of sync with the service node network.")
|
||||||
return seal.reject(
|
return seal.reject(SnodeAPI.Error.clockOutOfSync)
|
||||||
Error.httpRequestFailedAtDestination(
|
|
||||||
statusCode: UInt(customStatusCode),
|
|
||||||
json: body,
|
|
||||||
destination: destination
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return seal.fulfill(data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
guard 200...299 ~= customStatusCode else {
|
// Handle error status codes
|
||||||
|
guard 200...299 ~= responseInfo.code else {
|
||||||
return seal.reject(
|
return seal.reject(
|
||||||
Error.httpRequestFailedAtDestination(
|
Error.httpRequestFailedAtDestination(
|
||||||
statusCode: UInt(customStatusCode),
|
statusCode: UInt(responseInfo.code),
|
||||||
json: json,
|
json: [:], // TODO: Remove the 'json' value??
|
||||||
destination: destination
|
destination: destination
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
seal.fulfill(data)
|
// If there is no data in the response then just return the ResponseInfo
|
||||||
|
guard responseString.count > "l\(infoLength)\(infoString)e".count else {
|
||||||
|
return seal.fulfill((responseInfo, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Is this going to be done anymore...???
|
||||||
|
// if let timestamp = body["t"] as? Int64 {
|
||||||
|
// let offset = timestamp - Int64(NSDate.millisecondTimestamp())
|
||||||
|
// SnodeAPI.clockOffset = offset
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Extract the response data as well
|
||||||
|
let dataString: String = String(responseString.suffix(from: infoStringEndIndex))
|
||||||
|
let dataStringParts: [String.SubSequence] = dataString.split(separator: ":")
|
||||||
|
|
||||||
|
guard dataStringParts.count > 1, let finalDataLength: Int = Int(dataStringParts[0]) else {
|
||||||
|
return seal.reject(HTTP.Error.invalidResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
let finalDataStringStartIndex: String.Index = responseString.index(infoStringEndIndex, offsetBy: "\(finalDataLength):".count)
|
||||||
|
let finalDataStringEndIndex: String.Index = responseString.index(finalDataStringStartIndex, offsetBy: finalDataLength)
|
||||||
|
let finalDataString: String = String(responseString[finalDataStringStartIndex..<finalDataStringEndIndex])
|
||||||
|
|
||||||
|
guard let finalData: Data = finalDataString.data(using: .ascii) else {
|
||||||
|
return seal.reject(HTTP.Error.invalidResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
return seal.fulfill((responseInfo, finalData))
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
seal.reject(error)
|
seal.reject(error)
|
||||||
|
|
|
@ -131,7 +131,8 @@ public final class SnodeAPI : NSObject {
|
||||||
// MARK: Internal API
|
// MARK: Internal API
|
||||||
internal static func invoke(_ method: Snode.Method, on snode: Snode, associatedWith publicKey: String? = nil, parameters: JSON) -> RawResponsePromise {
|
internal static func invoke(_ method: Snode.Method, on snode: Snode, associatedWith publicKey: String? = nil, parameters: JSON) -> RawResponsePromise {
|
||||||
if Features.useOnionRequests {
|
if Features.useOnionRequests {
|
||||||
return OnionRequestAPI.sendOnionRequest(to: snode, invoking: method, with: parameters, associatedWith: publicKey).map2 { $0 as Any }
|
// TODO: Ensure this should use the Legact request?
|
||||||
|
return LegacyOnionRequestAPI.sendOnionRequest(to: snode, invoking: method, with: parameters, associatedWith: publicKey).map2 { $0 as Any }
|
||||||
} else {
|
} else {
|
||||||
let url = "\(snode.address):\(snode.port)/storage_rpc/v1"
|
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
|
return HTTP.execute(.post, url, parameters: parameters).map2 { $0 as Any }.recover2 { error -> Promise<Any> in
|
||||||
|
|
|
@ -80,12 +80,14 @@ public enum HTTP {
|
||||||
case generic
|
case generic
|
||||||
case httpRequestFailed(statusCode: UInt, json: JSON?)
|
case httpRequestFailed(statusCode: UInt, json: JSON?)
|
||||||
case invalidJSON
|
case invalidJSON
|
||||||
|
case invalidResponse
|
||||||
|
|
||||||
public var errorDescription: String? {
|
public var errorDescription: String? {
|
||||||
switch self {
|
switch self {
|
||||||
case .generic: return "An error occurred."
|
case .generic: return "An error occurred."
|
||||||
case .httpRequestFailed(let statusCode, _): return "HTTP request failed with status code: \(statusCode)."
|
case .httpRequestFailed(let statusCode, _): return "HTTP request failed with status code: \(statusCode)."
|
||||||
case .invalidJSON: return "Invalid JSON."
|
case .invalidJSON: return "Invalid JSON."
|
||||||
|
case .invalidResponse: return "Invalid Response"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -156,4 +158,46 @@ public enum HTTP {
|
||||||
task.resume()
|
task.resume()
|
||||||
return promise
|
return promise
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Consilidate the above and this method
|
||||||
|
public static func updatedExecute(_ verb: Verb, _ url: String, body: Data?, timeout: TimeInterval = HTTP.timeout, useSeedNodeURLSession: Bool = false) -> Promise<Data> {
|
||||||
|
var request = URLRequest(url: URL(string: url)!)
|
||||||
|
request.httpMethod = verb.rawValue
|
||||||
|
request.httpBody = body
|
||||||
|
request.timeoutInterval = timeout
|
||||||
|
request.allHTTPHeaderFields?.removeValue(forKey: "User-Agent")
|
||||||
|
request.setValue("WhatsApp", forHTTPHeaderField: "User-Agent") // Set a fake value
|
||||||
|
request.setValue("en-us", forHTTPHeaderField: "Accept-Language") // Set a fake value
|
||||||
|
let (promise, seal) = Promise<Data>.pending()
|
||||||
|
let urlSession = useSeedNodeURLSession ? seedNodeURLSession : snodeURLSession
|
||||||
|
let task = urlSession.dataTask(with: request) { data, response, error in
|
||||||
|
guard let data = data, let response = response as? HTTPURLResponse else {
|
||||||
|
if let error = error {
|
||||||
|
SNLog("\(verb.rawValue) request to \(url) failed due to error: \(error).")
|
||||||
|
} else {
|
||||||
|
SNLog("\(verb.rawValue) request to \(url) failed.")
|
||||||
|
}
|
||||||
|
// Override the actual error so that we can correctly catch failed requests in sendOnionRequest(invoking:on:with:)
|
||||||
|
return seal.reject(Error.httpRequestFailed(statusCode: 0, json: nil))
|
||||||
|
}
|
||||||
|
if let error = error {
|
||||||
|
SNLog("\(verb.rawValue) request to \(url) failed due to error: \(error).")
|
||||||
|
// Override the actual error so that we can correctly catch failed requests in sendOnionRequest(invoking:on:with:)
|
||||||
|
return seal.reject(Error.httpRequestFailed(statusCode: 0, json: nil))
|
||||||
|
}
|
||||||
|
let statusCode = UInt(response.statusCode)
|
||||||
|
|
||||||
|
guard 200...299 ~= statusCode else {
|
||||||
|
// let jsonDescription = json?.prettifiedDescription ?? "no debugging info provided"
|
||||||
|
// SNLog("\(verb.rawValue) request to \(url) failed with status code: \(statusCode) (\(jsonDescription)).")
|
||||||
|
// return seal.reject(Error.httpRequestFailed(statusCode: statusCode, json: json))
|
||||||
|
// TODO: Provide error from backend here
|
||||||
|
return seal.reject(Error.httpRequestFailed(statusCode: statusCode, json: [:]))
|
||||||
|
}
|
||||||
|
|
||||||
|
seal.fulfill(data)
|
||||||
|
}
|
||||||
|
task.resume()
|
||||||
|
return promise
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,7 +48,8 @@ extension MessageSender {
|
||||||
AttachmentUploadJob.upload(
|
AttachmentUploadJob.upload(
|
||||||
stream,
|
stream,
|
||||||
using: { data in
|
using: { data in
|
||||||
OpenGroupAPIV2.upload(
|
// TODO: Update to non-legacy version
|
||||||
|
OpenGroupAPIV2.legacyUpload(
|
||||||
data,
|
data,
|
||||||
to: v2OpenGroup.room,
|
to: v2OpenGroup.room,
|
||||||
on: v2OpenGroup.server
|
on: v2OpenGroup.server
|
||||||
|
@ -94,7 +95,8 @@ extension MessageSender {
|
||||||
AttachmentUploadJob.upload(
|
AttachmentUploadJob.upload(
|
||||||
stream,
|
stream,
|
||||||
using: { data in
|
using: { data in
|
||||||
OpenGroupAPIV2.upload(
|
// TODO: Update to non-legacy version
|
||||||
|
OpenGroupAPIV2.legacyUpload(
|
||||||
data,
|
data,
|
||||||
to: v2OpenGroup.room,
|
to: v2OpenGroup.room,
|
||||||
on: v2OpenGroup.server
|
on: v2OpenGroup.server
|
||||||
|
|
Loading…
Reference in a new issue