[WIP] Working on the libQuic onion requests

pull/960/head
Morgan Pretty 1 year ago
parent 756e256d9a
commit 8ef1c24215

@ -1 +1 @@
Subproject commit 0b48055f5f00e15a2fae41fa846f8c9acc2628a7
Subproject commit 6dab3b99208b9be410952174e72cb38bb0dedb27

@ -571,7 +571,7 @@
FD245C6B2850667400B966DD /* VisibleMessage+Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C300A5B12554AF9800555489 /* VisibleMessage+Profile.swift */; };
FD245C6C2850669200B966DD /* MessageReceiveJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A31225574F5200338F3E /* MessageReceiveJob.swift */; };
FD245C6D285066A400B966DD /* NotifyPushServerJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A32E2557549C00338F3E /* NotifyPushServerJob.swift */; };
FD29598B2A43BB8100888A17 /* GetStatsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD29598A2A43BB8100888A17 /* GetStatsResponse.swift */; };
FD29598B2A43BB8100888A17 /* GetInfoResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD29598A2A43BB8100888A17 /* GetInfoResponse.swift */; };
FD29598D2A43BC0B00888A17 /* Version.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD29598C2A43BC0B00888A17 /* Version.swift */; };
FD2959902A43BE5F00888A17 /* VersionSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD29598F2A43BE5F00888A17 /* VersionSpec.swift */; };
FD2959922A4417A900888A17 /* PreparedSendData.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2959912A4417A900888A17 /* PreparedSendData.swift */; };
@ -717,6 +717,7 @@
FD7F74602BAAA4C7006DDFD8 /* libSessionUtil.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FD9BDDF82A5D2294005F1EBC /* libSessionUtil.a */; };
FD7F74632BAAA4CA006DDFD8 /* libSessionUtil.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FD9BDDF82A5D2294005F1EBC /* libSessionUtil.a */; };
FD7F74672BAAAC26006DDFD8 /* GetSwarmResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7F74662BAAAC26006DDFD8 /* GetSwarmResponse.swift */; };
FD7F746A2BAB8A6D006DDFD8 /* LibSession+Networking.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7F74692BAB8A6D006DDFD8 /* LibSession+Networking.swift */; };
FD83B9B327CF200A005E1583 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; platformFilter = ios; };
FD83B9BB27CF20AF005E1583 /* SessionIdSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9BA27CF20AF005E1583 /* SessionIdSpec.swift */; };
FD83B9BF27CF2294005E1583 /* TestConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9BD27CF2243005E1583 /* TestConstants.swift */; };
@ -1718,7 +1719,7 @@
FD23EA6028ED0B260058676E /* CombineExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombineExtensions.swift; sourceTree = "<group>"; };
FD245C612850664300B966DD /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = "<group>"; };
FD28A4F527EAD44C00FF65E7 /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = "<group>"; };
FD29598A2A43BB8100888A17 /* GetStatsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetStatsResponse.swift; sourceTree = "<group>"; };
FD29598A2A43BB8100888A17 /* GetInfoResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetInfoResponse.swift; sourceTree = "<group>"; };
FD29598C2A43BC0B00888A17 /* Version.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Version.swift; sourceTree = "<group>"; };
FD29598F2A43BE5F00888A17 /* VersionSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionSpec.swift; sourceTree = "<group>"; };
FD2959912A4417A900888A17 /* PreparedSendData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreparedSendData.swift; sourceTree = "<group>"; };
@ -1854,6 +1855,7 @@
FD7F745C2BAAA38B006DDFD8 /* LibSessionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibSessionError.swift; sourceTree = "<group>"; };
FD7F745E2BAAA3B4006DDFD8 /* TypeConversion+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TypeConversion+Utilities.swift"; sourceTree = "<group>"; };
FD7F74662BAAAC26006DDFD8 /* GetSwarmResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetSwarmResponse.swift; sourceTree = "<group>"; };
FD7F74692BAB8A6D006DDFD8 /* LibSession+Networking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LibSession+Networking.swift"; sourceTree = "<group>"; };
FD83B9AF27CF200A005E1583 /* SessionUtilitiesKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SessionUtilitiesKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
FD83B9BA27CF20AF005E1583 /* SessionIdSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionIdSpec.swift; sourceTree = "<group>"; };
FD83B9BD27CF2243005E1583 /* TestConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestConstants.swift; sourceTree = "<group>"; };
@ -3337,6 +3339,7 @@
FDF8488F29405C13007DCAE5 /* Types */,
FDF8488C29405C04007DCAE5 /* Jobs */,
FDF8489229405C1B007DCAE5 /* Networking */,
FD7F74682BAB8A5D006DDFD8 /* SessionUtil */,
C3C2A5CD255385F300C340D1 /* Utilities */,
C3C2A5B9255385ED00C340D1 /* Configuration.swift */,
);
@ -4105,6 +4108,14 @@
path = Utilities;
sourceTree = "<group>";
};
FD7F74682BAB8A5D006DDFD8 /* SessionUtil */ = {
isa = PBXGroup;
children = (
FD7F74692BAB8A6D006DDFD8 /* LibSession+Networking.swift */,
);
path = SessionUtil;
sourceTree = "<group>";
};
FD83B9B027CF200A005E1583 /* SessionUtilitiesKitTests */ = {
isa = PBXGroup;
children = (
@ -4529,7 +4540,7 @@
FDF848A629405C5A007DCAE5 /* ONSResolveRequest.swift */,
FDF8489E29405C5A007DCAE5 /* ONSResolveResponse.swift */,
FDF8489C29405C5A007DCAE5 /* GetServiceNodesRequest.swift */,
FD29598A2A43BB8100888A17 /* GetStatsResponse.swift */,
FD29598A2A43BB8100888A17 /* GetInfoResponse.swift */,
);
path = Models;
sourceTree = "<group>";
@ -5783,6 +5794,7 @@
FDF848C329405C5A007DCAE5 /* DeleteMessagesRequest.swift in Sources */,
FDF8489129405C13007DCAE5 /* SnodeAPINamespace.swift in Sources */,
C3C2A5E02553860B00C340D1 /* Threading.swift in Sources */,
FD7F746A2BAB8A6D006DDFD8 /* LibSession+Networking.swift in Sources */,
FDF848C529405C5B007DCAE5 /* GetSwarmRequest.swift in Sources */,
FDF848D729405C5B007DCAE5 /* SnodeBatchRequest.swift in Sources */,
C3C2A5C0255385EE00C340D1 /* Snode.swift in Sources */,
@ -5812,7 +5824,7 @@
FDF848CC29405C5B007DCAE5 /* SnodeReceivedMessage.swift in Sources */,
FDF848C129405C5A007DCAE5 /* UpdateExpiryRequest.swift in Sources */,
FDF848C729405C5B007DCAE5 /* SendMessageResponse.swift in Sources */,
FD29598B2A43BB8100888A17 /* GetStatsResponse.swift in Sources */,
FD29598B2A43BB8100888A17 /* GetInfoResponse.swift in Sources */,
FDF848CA29405C5B007DCAE5 /* DeleteAllBeforeRequest.swift in Sources */,
FDF848D229405C5B007DCAE5 /* LegacyGetMessagesRequest.swift in Sources */,
FDF848CB29405C5B007DCAE5 /* SnodePoolResponse.swift in Sources */,

@ -532,68 +532,3 @@ public extension LibSession {
return String(cString: cFullUrl)
}
}
public extension LibSession {
static func addNetworkLogger() {
network_add_logger({ logPtr, msgLen in
guard let log: String = String(pointer: logPtr, length: msgLen, encoding: .utf8) else {
print("[quic:info] Null log")
return
}
print(log.trimmingCharacters(in: .whitespacesAndNewlines))
})
}
static func sendRequest(
ed25519SecretKey: [UInt8]?,
targetPubkey: String,
targetIp: String,
targetPort: UInt16,
endpoint: String,
payload: String,
callback: @escaping (Bool, Int16, Data?) -> Void
) {
class CWrapper {
let callback: (Bool, Int16, Data?) -> Void
public init(_ callback: @escaping (Bool, Int16, Data?) -> Void) {
self.callback = callback
}
}
let callbackWrapper: CWrapper = CWrapper(callback)
let cWrapperPtr: UnsafeMutableRawPointer = Unmanaged.passRetained(callbackWrapper).toOpaque()
let cEd25519SecretKey: [UInt8] = ed25519SecretKey!
let cRemoteAddress: remote_address = remote_address(
pubkey: targetPubkey.toLibSession(),
ip: targetIp.toLibSession(),
port: targetPort
)
let cEndpoint: [CChar] = endpoint.cArray
let cPayload: [CChar] = payload.cArray
do {
try CExceptionHelper.performSafely {
network_send_request(
cEd25519SecretKey,
cRemoteAddress,
cEndpoint,
cEndpoint.count,
cPayload,
cPayload.count,
{ success, statusCode, dataPtr, dataLen, ctx in
let data: Data? = dataPtr.map { Data(bytes: $0, count: dataLen) }
Unmanaged<CWrapper>.fromOpaque(ctx!).takeRetainedValue().callback(success, statusCode, data)
},
cWrapperPtr
)
}
}
catch {
print("RAWR \(error)")
callback(false, -1, nil)
}
}
}

@ -1,4 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import GRDB
@ -8,36 +10,28 @@ public struct Snode: Codable, FetchableRecord, PersistableRecord, TableRecord, C
public static var databaseTableName: String { "snode" }
static let snodeSet = hasMany(SnodeSet.self)
static let snodeSetForeignKey = ForeignKey(
[Columns.address, Columns.port],
to: [SnodeSet.Columns.address, SnodeSet.Columns.port]
[Columns.ip, Columns.lmqPort],
to: [SnodeSet.Columns.ip, SnodeSet.Columns.lmqPort]
)
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression {
case address = "public_ip"
case port = "storage_port"
case ed25519PublicKey = "pubkey_ed25519"
case ip = "public_ip"
case lmqPort = "storage_lmq_port"
case x25519PublicKey = "pubkey_x25519"
case ed25519PublicKey = "pubkey_ed25519"
}
public let address: String
public let port: UInt16
public let ed25519PublicKey: String
public let ip: String
public let lmqPort: UInt16
public let x25519PublicKey: String
public var ip: String {
guard let range = address.range(of: "https://"), range.lowerBound == address.startIndex else {
return address
}
return String(address[range.upperBound..<address.endIndex])
}
public let ed25519PublicKey: String
public var snodeSet: QueryInterfaceRequest<SnodeSet> {
request(for: Snode.snodeSet)
}
public var description: String { return "\(address):\(port)" }
public var description: String { return "\(ip):\(lmqPort)" }
}
// MARK: - Decoder
@ -47,15 +41,18 @@ extension Snode {
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
do {
let address: String = try container.decode(String.self, forKey: .address)
// Strip the scheme from the IP (if included)
let ip: String = (try container.decode(String.self, forKey: .ip))
.replacingOccurrences(of: "http://", with: "")
.replacingOccurrences(of: "https://", with: "")
guard address != "0.0.0.0" else { throw SnodeAPIError.invalidIP }
guard !ip.isEmpty && ip != "0.0.0.0" else { throw SnodeAPIError.invalidIP }
self = Snode(
address: (address.starts(with: "https://") ? address : "https://\(address)"),
port: try container.decode(UInt16.self, forKey: .port),
ed25519PublicKey: try container.decode(String.self, forKey: .ed25519PublicKey),
x25519PublicKey: try container.decode(String.self, forKey: .x25519PublicKey)
ip: ip,
lmqPort: try container.decode(UInt16.self, forKey: .lmqPort),
x25519PublicKey: try container.decode(String.self, forKey: .x25519PublicKey),
ed25519PublicKey: try container.decode(String.self, forKey: .ed25519PublicKey)
)
}
catch {
@ -82,8 +79,8 @@ internal extension Snode {
struct ResultWrapper: Decodable, FetchableRecord {
let key: String
let nodeIndex: Int
let address: String
let port: UInt16
let ip: String
let lmqPort: UInt16
let snode: Snode
}
@ -113,7 +110,7 @@ internal extension Snode {
internal extension Collection where Element == Snode {
/// This method is used to save Swarms
/// This method is used to save Swarms and paths
func save(_ db: Database, key: String) throws {
try self.enumerated().forEach { nodeIndex, node in
try node.save(db)
@ -121,15 +118,15 @@ internal extension Collection where Element == Snode {
try SnodeSet(
key: key,
nodeIndex: nodeIndex,
address: node.address,
port: node.port
ip: node.ip,
lmqPort: node.lmqPort
).save(db)
}
}
}
internal extension Collection where Element == [Snode] {
/// This method is used to save onion reuqest paths
/// This method is used to save onion request paths
func save(_ db: Database) throws {
try self.enumerated().forEach { pathIndex, path in
try path.save(db, key: "\(SnodeSet.onionRequestPathPrefix)\(pathIndex)")

@ -43,10 +43,10 @@ public struct SnodeReceivedMessageInfo: Codable, FetchableRecord, MutablePersist
public extension SnodeReceivedMessageInfo {
private static func key(for snode: Snode, publicKey: String, namespace: SnodeAPI.Namespace) -> String {
guard namespace != .default else {
return "\(snode.address):\(snode.port).\(publicKey)"
return "\(snode.ip):\(snode.lmqPort).\(publicKey)"
}
return "\(snode.address):\(snode.port).\(publicKey).\(namespace.rawValue)"
return "\(snode.ip):\(snode.lmqPort).\(publicKey).\(namespace.rawValue)"
}
init(

@ -13,14 +13,14 @@ public struct SnodeSet: Codable, FetchableRecord, EncodableRecord, PersistableRe
public enum CodingKeys: String, CodingKey, ColumnExpression {
case key
case nodeIndex
case address
case port
case ip
case lmqPort
}
public let key: String
public let nodeIndex: Int
public let address: String
public let port: UInt16
public let ip: String
public let lmqPort: UInt16
public var node: QueryInterfaceRequest<Snode> {
request(for: SnodeSet.node)

@ -0,0 +1,30 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import SessionUtilitiesKit
extension SnodeAPI {
public class GetInfoResponse: SnodeResponse {
private enum CodingKeys: String, CodingKey {
case versionString = "version"
}
let versionString: String?
var version: Version? { versionString.map { Version.from($0) } }
// MARK: - Initialization
required init(from decoder: Decoder) throws {
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
versionString = (try container.decode([Int]?.self, forKey: .versionString))?
.map { "\($0)" }
.joined(separator: ".")
try super.init(from: decoder)
}
}
}

@ -17,15 +17,15 @@ extension SnodeAPI {
public struct Fields: Encodable {
enum CodingKeys: String, CodingKey {
case publicIp = "public_ip"
case storagePort = "storage_port"
case pubkeyEd25519 = "pubkey_ed25519"
case pubkeyX25519 = "pubkey_x25519"
case storageLmqPort = "storage_lmq_port"
}
let publicIp: Bool
let storagePort: Bool
let pubkeyEd25519: Bool
let pubkeyX25519: Bool
let storageLmqPort: Bool
}
}
}

@ -1,16 +0,0 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import SessionUtilitiesKit
extension SnodeAPI {
public struct GetStatsResponse: Codable {
private enum CodingKeys: String, CodingKey {
case versionString = "version"
}
let versionString: String?
var version: Version? { versionString.map { Version.from($0) } }
}
}

@ -0,0 +1,93 @@
// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import SessionUtilitiesKit
public class GetSwarmResponse: SnodeResponse {
private enum CodingKeys: String, CodingKey {
case swarm
case internalSnodes = "snodes"
}
fileprivate struct _Snode: Codable {
public enum CodingKeys: String, CodingKey {
case ip
case lmqPort = "port_omq"
case x25519PublicKey = "pubkey_x25519"
case ed25519PublicKey = "pubkey_ed25519"
}
/// The IPv4 address of the service node.
let ip: String
/// The storage server port where OxenMQ is listening.
let lmqPort: UInt16
/// This is the X25519 pubkey key of this service node, used for encrypting onion requests and for establishing an encrypted connection to the storage server's OxenMQ port.
let x25519PublicKey: String
/// The Ed25519 public key of this service node. This is the public key the service node uses wherever a signature is required (such as when signing recursive requests).
let ed25519PublicKey: String
}
/// Contains the target swarm ID, encoded as a hex string. (This ID is a unsigned, 64-bit value and cannot be reliably transported unencoded through JSON)
internal let swarm: String
/// An array containing the list of service nodes in the target swarm.
private let internalSnodes: [Failable<_Snode>]
public var snodes: Set<Snode> {
internalSnodes
.compactMap { $0.value }
.map { responseSnode in
Snode(
ip: responseSnode.ip,
lmqPort: responseSnode.lmqPort,
x25519PublicKey: responseSnode.x25519PublicKey,
ed25519PublicKey: responseSnode.ed25519PublicKey
)
}
.asSet()
}
// MARK: - Initialization
required init(from decoder: Decoder) throws {
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
swarm = try container.decode(String.self, forKey: .swarm)
internalSnodes = try container.decode([Failable<_Snode>].self, forKey: .internalSnodes)
try super.init(from: decoder)
}
}
// MARK: - Decoder
extension GetSwarmResponse._Snode {
public init(from decoder: Decoder) throws {
let container: KeyedDecodingContainer<GetSwarmResponse._Snode.CodingKeys> = try decoder.container(keyedBy: GetSwarmResponse._Snode.CodingKeys.self)
do {
// Strip the http from the IP (if included)
let ip: String = (try container.decode(String.self, forKey: .ip))
.replacingOccurrences(of: "http://", with: "")
.replacingOccurrences(of: "https://", with: "")
guard !ip.isEmpty && ip != "0.0.0.0" else { throw SnodeAPIError.invalidIP }
self = GetSwarmResponse._Snode(
ip: ip,
lmqPort: try container.decode(UInt16.self, forKey: .lmqPort),
x25519PublicKey: try container.decode(String.self, forKey: .x25519PublicKey),
ed25519PublicKey: try container.decode(String.self, forKey: .ed25519PublicKey)
)
}
catch {
SNLog("Failed to parse snode: \(error.localizedDescription).")
throw HTTPError.invalidJSON
}
}
}

@ -79,10 +79,15 @@ public enum OnionRequestAPI {
private static func testSnode(_ snode: Snode, using dependencies: Dependencies) -> AnyPublisher<Void, Error> {
let url = "\(snode.address):\(snode.port)/get_stats/v1"
let timeout: TimeInterval = 3 // Use a shorter timeout for testing
return HTTP.execute(.get, url, timeout: timeout)
.decoded(as: SnodeAPI.GetStatsResponse.self, using: dependencies)
.tryMap { response -> Void in
return LibSession
.sendRequest(
ed25519SecretKey: Identity.fetchUserEd25519KeyPair()?.secretKey,
snode: snode,
endpoint: SnodeAPI.Endpoint.getInfo.rawValue
)
.decoded(as: SnodeAPI.GetInfoResponse.self, using: dependencies)
.tryMap { _, response -> Void in
guard let version: Version = response.version else { throw OnionRequestAPIError.missingSnodeVersion }
guard version >= Version(major: 2, minor: 0, patch: 7) else {
SNLog("Unsupported snode version: \(version.stringValue).")
@ -513,117 +518,28 @@ public enum OnionRequestAPI {
timeout: TimeInterval = HTTP.defaultTimeout,
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<(ResponseInfoType, Data?), Error> {
var guardSnode: Snode?
return buildOnion(around: payload, targetedAt: destination, using: dependencies)
.flatMap { intermediate -> AnyPublisher<(ResponseInfoType, Data?), Error> in
guardSnode = intermediate.guardSnode
let url = "\(guardSnode!.address):\(guardSnode!.port)/onion_req/v2"
let finalEncryptionResult = intermediate.finalEncryptionResult
let onion = finalEncryptionResult.ciphertext
if case OnionRequestAPIDestination.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 destinationSymmetricKey = intermediate.destinationSymmetricKey
let snodeToExclude: Snode? = {
switch destination {
case .snode(let snode): return snode
default: return nil
}
}()
return getPath(excluding: snodeToExclude, using: dependencies)
.tryFlatMap { path -> AnyPublisher<(ResponseInfoType, Data?), Error> in
guard let guardSnode: Snode = path.first else { throw OnionRequestAPIError.insufficientSnodes }
// TODO: Replace 'json' with a codable typed
return encode(ciphertext: onion, json: parameters)
.flatMap { body in HTTP.execute(.post, url, body: body, timeout: timeout) }
.flatMap { responseData in
handleResponse(
responseData: responseData,
destinationSymmetricKey: destinationSymmetricKey,
version: version,
destination: destination
)
}
.eraseToAnyPublisher()
return LibSession
.sendOnionRequest(
path: path,
ed25519SecretKey: Identity.fetchUserEd25519KeyPair()?.secretKey,
to: destination,
payload: payload//Data()//body
)
}
.handleEvents(
receiveCompletion: { result in
switch result {
case .finished: break
case .failure(let error):
guard let guardSnode: Snode = guardSnode else {
return SNLog("Request failed with no guardSnode.")
}
guard case HTTPError.httpRequestFailed(let statusCode, let data) = error else { return }
let path = paths.first { $0.contains(guardSnode) }
func handleUnspecificError() {
guard let path = path else { return }
var pathFailureCount: UInt = (OnionRequestAPI.pathFailureCount.wrappedValue[path] ?? 0)
pathFailureCount += 1
if pathFailureCount >= pathFailureThreshold {
dropGuardSnode(guardSnode)
path.forEach { snode in
SnodeAPI.handleError(withStatusCode: statusCode, data: data, forSnode: snode) // Intentionally don't throw
}
drop(path)
}
else {
OnionRequestAPI.pathFailureCount.mutate { $0[path] = pathFailureCount }
}
}
let prefix = "Next node not found: "
let json: JSON?
if let data: Data = data, let processedJson = try? JSONSerialization.jsonObject(with: data, options: [ .fragmentsAllowed ]) as? JSON {
json = processedJson
}
else if let data: Data = data, let result: String = String(data: data, encoding: .utf8) {
json = [ "result": result ]
}
else {
json = nil
}
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.ed25519PublicKey == ed25519PublicKey }) {
var snodeFailureCount: UInt = (OnionRequestAPI.snodeFailureCount.wrappedValue[snode] ?? 0)
snodeFailureCount += 1
if snodeFailureCount >= snodeFailureThreshold {
SnodeAPI.handleError(withStatusCode: statusCode, data: data, forSnode: snode) // Intentionally don't throw
do {
try drop(snode)
}
catch {
handleUnspecificError()
}
}
else {
OnionRequestAPI.snodeFailureCount
.mutate { $0[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()
}
}
}
)
.eraseToAnyPublisher()

@ -47,15 +47,27 @@ public final class SnodeAPI {
private static let maxRetryCount: Int = 8
private static let minSwarmSnodeCount: Int = 3
private static let seedNodePool: Set<String> = {
private static let seedNodePool: Set<Snode> = {
guard !Features.useTestnet else {
return [ "http://public.loki.foundation:38157" ]
// public.loki.foundation
return [
Snode(
ip: "144.76.164.202",
lmqPort: 20200,
x25519PublicKey: "",
ed25519PublicKey: "1f000f09a7b07828dcb72af7cd16857050c10c02bd58afb0e38111fb6cda1fef"
)
]
}
return [
"https://seed1.getsession.org:4432",
"https://seed2.getsession.org:4432",
"https://seed3.getsession.org:4432"
// seed2.getsession.org
Snode(
ip: "144.76.164.202",
lmqPort: 20203,
x25519PublicKey: "",
ed25519PublicKey: "1f003f0b6544c1050c9a052deafdb8cd1b4d2fbbf1dfb9d80f47ee2a0c316112"
),
]
}()
private static let snodeFailureThreshold: Int = 3
@ -308,9 +320,10 @@ public final class SnodeAPI {
.retry(4)
.eraseToAnyPublisher()
}
.map { _, responseData in parseSnodes(from: responseData) }
.decoded(as: GetSwarmResponse.self, using: dependencies)
.map { _, response in response.snodes }
.handleEvents(
receiveOutput: { swarm in setSwarm(to: swarm, for: publicKey) }
receiveOutput: { snodes in setSwarm(to: snodes, for: publicKey) }
)
.eraseToAnyPublisher()
}
@ -1097,37 +1110,28 @@ public final class SnodeAPI {
private static func getSnodePoolFromSeedNode(
using dependencies: Dependencies
) -> AnyPublisher<Set<Snode>, Error> {
let request: SnodeRequest = SnodeRequest(
endpoint: .jsonGetNServiceNodes,
body: GetServiceNodesRequest(
activeOnly: true,
limit: 256,
fields: GetServiceNodesRequest.Fields(
publicIp: true,
storagePort: true,
pubkeyEd25519: true,
pubkeyX25519: true
)
)
)
guard let target: String = seedNodePool.randomElement() else {
guard let targetSeedNode: Snode = seedNodePool.randomElement() else {
return Fail(error: SnodeAPIError.snodePoolUpdatingFailed)
.eraseToAnyPublisher()
}
guard let payload: Data = try? JSONEncoder().encode(request) else {
return Fail(error: HTTPError.invalidJSON)
.eraseToAnyPublisher()
}
SNLog("Populating snode pool using seed node: \(target).")
SNLog("Populating snode pool using seed node: \(targetSeedNode).")
return HTTP
.execute(
.post,
"\(target)/json_rpc",
body: payload,
useSeedNodeURLSession: true
return LibSession
.sendRequest(
ed25519SecretKey: Identity.fetchUserEd25519KeyPair()?.secretKey,
snode: targetSeedNode,
endpoint: SnodeAPI.Endpoint.jsonGetNServiceNodes.rawValue,
payload: GetServiceNodesRequest(
activeOnly: true,
limit: 256,
fields: GetServiceNodesRequest.Fields(
publicIp: true,
pubkeyEd25519: true,
pubkeyX25519: true,
storageLmqPort: true
)
)
)
.decoded(as: SnodePoolResponse.self, using: dependencies)
.mapError { error in
@ -1136,7 +1140,7 @@ public final class SnodeAPI {
default: return error
}
}
.map { snodePool -> Set<Snode> in
.map { _, snodePool -> Set<Snode> in
snodePool.result
.serviceNodeStates
.compactMap { $0.value }
@ -1146,8 +1150,8 @@ public final class SnodeAPI {
.handleEvents(
receiveCompletion: { result in
switch result {
case .finished: SNLog("Got snode pool from seed node: \(target).")
case .failure: SNLog("Failed to contact seed node at: \(target).")
case .finished: SNLog("Got snode pool from seed node: \(targetSeedNode).")
case .failure: SNLog("Failed to contact seed node at: \(targetSeedNode).")
}
}
)
@ -1184,9 +1188,9 @@ public final class SnodeAPI {
limit: nil,
fields: GetServiceNodesRequest.Fields(
publicIp: true,
storagePort: true,
pubkeyEd25519: true,
pubkeyX25519: true
pubkeyX25519: true,
storageLmqPort: true
)
)
)
@ -1227,7 +1231,6 @@ public final class SnodeAPI {
}
.eraseToAnyPublisher()
}
public static var otherReuquestCallback: ((Snode, Data) -> Void)?
private static func send<T: Encodable>(
request: SnodeRequest<T>,
@ -1241,33 +1244,30 @@ public final class SnodeAPI {
}
guard Features.useOnionRequests else {
return HTTP
.execute(
.post,
"\(snode.address):\(snode.port)/storage_rpc/v1",
body: payload
return LibSession
.sendRequest(
ed25519SecretKey: Identity.fetchUserEd25519KeyPair()?.secretKey,
snode: snode,
endpoint: request.endpoint.rawValue,
payload: request.body
)
.map { response in (HTTP.ResponseInfo(code: -1, headers: [:]), response) }
.mapError { error in
switch error {
case HTTPError.httpRequestFailed(let statusCode, let data):
return (SnodeAPI.handleError(withStatusCode: statusCode, data: data, forSnode: snode, associatedWith: publicKey) ?? error)
return (SnodeAPI.handleError(withStatusCode: statusCode, data: data, forSnode: snode, associatedWith: publicKey, using: dependencies) ?? error)
default: return error
}
}
.eraseToAnyPublisher()
}
if let callback = otherReuquestCallback {
callback(snode, payload)
}
return dependencies.network
.send(.onionRequest(payload, to: snode))
.mapError { error in
switch error {
case HTTPError.httpRequestFailed(let statusCode, let data):
return (SnodeAPI.handleError(withStatusCode: statusCode, data: data, forSnode: snode, associatedWith: publicKey) ?? error)
return (SnodeAPI.handleError(withStatusCode: statusCode, data: data, forSnode: snode, associatedWith: publicKey, using: dependencies) ?? error)
default: return error
}
@ -1342,7 +1342,8 @@ public final class SnodeAPI {
withStatusCode statusCode: UInt,
data: Data?,
forSnode snode: Snode,
associatedWith publicKey: String? = nil
associatedWith publicKey: String? = nil,
using dependencies: Dependencies
) -> Error? {
func handleBadSnode() {
let oldFailureCount = (SnodeAPI.snodeFailureCount.wrappedValue[snode] ?? 0)

@ -0,0 +1,241 @@
// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Combine
import SessionUtil
import SessionUtilitiesKit
// MARK: - LibSession
public extension LibSession {
private static func sendRequest(
ed25519SecretKey: [UInt8],
targetPubkey: String,
targetIp: String,
targetPort: UInt16,
endpoint: String,
payload: [UInt8]?,
callback: @escaping (Bool, Bool, Int16, Data?) -> Void
) {
class CWrapper {
let callback: (Bool, Bool, Int16, Data?) -> Void
public init(_ callback: @escaping (Bool, Bool, Int16, Data?) -> Void) {
self.callback = callback
}
}
let callbackWrapper: CWrapper = CWrapper(callback)
let cWrapperPtr: UnsafeMutableRawPointer = Unmanaged.passRetained(callbackWrapper).toOpaque()
let cRemoteAddress: remote_address = remote_address(
pubkey: targetPubkey.toLibSession(),
ip: targetIp.toLibSession(),
port: targetPort
)
let cEndpoint: [CChar] = endpoint.cArray
let cPayload: [UInt8] = (payload ?? [])
network_send_request(
ed25519SecretKey,
cRemoteAddress,
cEndpoint,
cEndpoint.count,
cPayload,
cPayload.count,
{ success, timeout, statusCode, dataPtr, dataLen, ctx in
let data: Data? = dataPtr.map { Data(bytes: $0, count: dataLen) }
Unmanaged<CWrapper>.fromOpaque(ctx!).takeRetainedValue().callback(success, timeout, statusCode, data)
},
cWrapperPtr
)
}
private static func sendOnionRequest(
path: [Snode],
ed25519SecretKey: [UInt8],
to destination: OnionRequestAPIDestination,
payload: [UInt8]?,
callback: @escaping (Bool, Bool, Int16, Data?) -> Void
) {
class CWrapper {
let callback: (Bool, Bool, Int16, Data?) -> Void
public init(_ callback: @escaping (Bool, Bool, Int16, Data?) -> Void) {
self.callback = callback
}
}
let callbackWrapper: CWrapper = CWrapper(callback)
let cWrapperPtr: UnsafeMutableRawPointer = Unmanaged.passRetained(callbackWrapper).toOpaque()
let cPayload: [UInt8] = (payload ?? [])
var x25519Pubkeys: [UnsafePointer<CChar>?] = path.map { $0.x25519PublicKey.cArray }.unsafeCopy()
var ed25519Pubkeys: [UnsafePointer<CChar>?] = path.map { $0.ed25519PublicKey.cArray }.unsafeCopy()
let cNodes: UnsafePointer<onion_request_service_node>? = path
.enumerated()
.map { index, snode in
onion_request_service_node(
ip: snode.ip.toLibSession(),
lmq_port: snode.lmqPort,
x25519_pubkey_hex: x25519Pubkeys[index],
ed25519_pubkey_hex: ed25519Pubkeys[index],
failure_count: 0
)
}
.unsafeCopy()
let cOnionPath: onion_request_path = onion_request_path(
nodes: cNodes,
nodes_count: path.count,
failure_count: 0
)
switch destination {
case .snode(let snode):
let cX25519Pubkey: UnsafePointer<CChar>? = snode.x25519PublicKey.cArray.unsafeCopy()
let cEd25519Pubkey: UnsafePointer<CChar>? = snode.ed25519PublicKey.cArray.unsafeCopy()
network_send_onion_request_to_snode_destination(
cOnionPath,
ed25519SecretKey,
onion_request_service_node(
ip: snode.ip.toLibSession(),
lmq_port: snode.lmqPort,
x25519_pubkey_hex: cX25519Pubkey,
ed25519_pubkey_hex: cEd25519Pubkey,
failure_count: 0
),
cPayload,
cPayload.count,
{ success, timeout, statusCode, dataPtr, dataLen, ctx in
let data: Data? = dataPtr.map { Data(bytes: $0, count: dataLen) }
Unmanaged<CWrapper>.fromOpaque(ctx!).takeRetainedValue().callback(success, timeout, statusCode, data)
},
cWrapperPtr
)
case .server(let host, let target, let x25519PublicKey, let scheme, let port):
let cMethod: [CChar] = "GET".cArray
let targetScheme: String = (scheme ?? "https")
network_send_onion_request_to_server_destination(
cOnionPath,
ed25519SecretKey,
cMethod,
host.cArray,
target.cArray,
targetScheme.cArray,
x25519PublicKey.cArray,
(port ?? (targetScheme == "https" ? 443 : 80)),
nil,
nil,
0,
cPayload,
cPayload.count,
{ success, timeout, statusCode, dataPtr, dataLen, ctx in
let data: Data? = dataPtr.map { Data(bytes: $0, count: dataLen) }
Unmanaged<CWrapper>.fromOpaque(ctx!).takeRetainedValue().callback(success, timeout, statusCode, data)
},
cWrapperPtr
)
}
}
private static func sendRequest(
ed25519SecretKey: [UInt8]?,
snode: Snode,
endpoint: String,
payloadBytes: [UInt8]?
) -> AnyPublisher<(ResponseInfoType, Data?), Error> {
return Deferred {
Future { resolver in
guard let ed25519SecretKey: [UInt8] = ed25519SecretKey else {
return resolver(Result.failure(SnodeAPIError.missingSecretKey))
}
LibSession.sendRequest(
ed25519SecretKey: ed25519SecretKey,
targetPubkey: snode.ed25519PublicKey,
targetIp: snode.ip,
targetPort: snode.lmqPort,
endpoint: endpoint,//.rawValue,
payload: payloadBytes,
callback: { success, timeout, statusCode, data in
switch SnodeAPIError(success: success, timeout: timeout, statusCode: statusCode, data: data) {
case .some(let error): resolver(Result.failure(error))
case .none: resolver(Result.success((HTTP.ResponseInfo(code: Int(statusCode), headers: [:]), data)))
}
}
)
}
}.eraseToAnyPublisher()
}
static func sendRequest(
ed25519SecretKey: [UInt8]?,
snode: Snode,
endpoint: String
) -> AnyPublisher<(ResponseInfoType, Data?), Error> {
return sendRequest(ed25519SecretKey: ed25519SecretKey, snode: snode, endpoint: endpoint, payloadBytes: nil)
}
static func sendRequest<T: Encodable>(
ed25519SecretKey: [UInt8]?,
snode: Snode,
endpoint: String,
payload: T
) -> AnyPublisher<(ResponseInfoType, Data?), Error> {
let payloadBytes: [UInt8]
switch payload {
case let data as Data: payloadBytes = Array(data)
case let bytes as [UInt8]: payloadBytes = bytes
default:
guard let encodedPayload: Data = try? JSONEncoder().encode(payload) else {
return Fail(error: SnodeAPIError.invalidPayload).eraseToAnyPublisher()
}
payloadBytes = Array(encodedPayload)
}
return sendRequest(ed25519SecretKey: ed25519SecretKey, snode: snode, endpoint: endpoint, payloadBytes: payloadBytes)
}
static func sendOnionRequest<T: Encodable>(
path: [Snode],
ed25519SecretKey: [UInt8]?,
to destination: OnionRequestAPIDestination,
payload: T
) -> AnyPublisher<(ResponseInfoType, Data?), Error> {
let payloadBytes: [UInt8]
switch payload {
case let data as Data: payloadBytes = Array(data)
case let bytes as [UInt8]: payloadBytes = bytes
default:
guard let encodedPayload: Data = try? JSONEncoder().encode(payload) else {
return Fail(error: SnodeAPIError.invalidPayload).eraseToAnyPublisher()
}
payloadBytes = Array(encodedPayload)
}
return Deferred {
Future { resolver in
guard let ed25519SecretKey: [UInt8] = ed25519SecretKey else {
return resolver(Result.failure(SnodeAPIError.missingSecretKey))
}
LibSession.sendOnionRequest(
path: path,
ed25519SecretKey: ed25519SecretKey,
to: destination,
payload: payloadBytes,
callback: { success, timeout, statusCode, data in
switch SnodeAPIError(success: success, timeout: timeout, statusCode: statusCode, data: data) {
case .some(let error): resolver(Result.failure(error))
case .none: resolver(Result.success((HTTP.ResponseInfo(code: Int(statusCode), headers: [:]), data)))
}
}
)
}
}.eraseToAnyPublisher()
}
}

@ -8,7 +8,7 @@ public enum OnionRequestAPIDestination: CustomStringConvertible, Codable {
public var description: String {
switch self {
case .snode(let snode): return "Service node \(snode.ip):\(snode.port)"
case .snode(let snode): return "Service node \(snode.ip):\(snode.lmqPort)"
case .server(let host, _, _, _, _): return host
}
}

@ -17,7 +17,7 @@ public extension SnodeAPI {
case sequence = "sequence"
case getInfo = "info"
case getSwarm = "get_snodes_for_pubkey"
case getSwarm = "get_swarm"
case jsonRPCCall = "json_rpc"
case oxenDaemonRPCCall = "oxend_request"

@ -3,6 +3,7 @@
// stringlint:disable
import Foundation
import SessionUtilitiesKit
public enum SnodeAPIError: LocalizedError {
case generic
@ -20,6 +21,15 @@ public enum SnodeAPIError: LocalizedError {
case decryptionFailed
case hashingFailed
case validationFailed
// Quic
case invalidPayload
case missingSecretKey
case requestFailed(error: String, rawData: Data?)
case timeout
case unreachable
case unassociatedPubkey
case unknown
public var errorDescription: String? {
switch self {
@ -38,6 +48,46 @@ public enum SnodeAPIError: LocalizedError {
case .decryptionFailed: return "Couldn't decrypt ONS name."
case .hashingFailed: return "Couldn't compute ONS name hash."
case .validationFailed: return "ONS name validation failed."
// Quic
case .invalidPayload: return "Invalid payload."
case .missingSecretKey: return "Missing secret key."
case .requestFailed(let error, _): return error
case .timeout: return "The request timed out."
case .unreachable: return "The service node is unreachable."
case .unassociatedPubkey: return "The service node is no longer associated with the public key."
case .unknown: return "An unknown error occurred."
}
}
}
public extension SnodeAPIError {
init?(success: Bool, timeout: Bool, statusCode: Int16, data: Data?) {
guard !success || statusCode < 200 || statusCode > 299 else { return nil }
guard !timeout else {
self = .timeout
return
}
// Handle status codes with specific meanings
switch (statusCode, data.map { String(data: $0, encoding: .utf8) }) {
/// A snode will return a `406` but onion requests v4 seems to return `425` so handle both
case (406, _), (425, _):
SNLog("The user's clock is out of sync with the service node network.")
self = .clockOutOfSync
case (401, _):
SNLog("Failed to verify the signature.")
self = .signatureVerificationFailed
case (421, _):
self = .unassociatedPubkey
case (500, _), (502, _), (503, _):
self = .unreachable
case (_, .none): self = .unknown
case (_, .some(let responseString)): self = .requestFailed(error: responseString, rawData: data)
}
}
}

@ -16,6 +16,14 @@ public extension Collection {
_ = copy.initialize(from: self)
return copy
}
/// This creates an UnsafePointer to access data in memory directly. This result pointer provides no automated
/// memory management so after use you are responsible for handling the life cycle and need to call `deallocate()`.
func unsafeCopy() -> UnsafePointer<Element>? {
let copy = UnsafeMutableBufferPointer<Element>.allocate(capacity: self.underestimatedCount)
_ = copy.initialize(from: self)
return UnsafePointer(copy.baseAddress)
}
}
public extension Collection where Element == [CChar] {

@ -13,19 +13,6 @@ public enum Configuration {
SNMessagingKit.configure()
SNSnodeKit.configure()
SNUIKit.configure()
let secKey = Identity.fetchUserEd25519KeyPair()?.secretKey
SnodeAPI.otherReuquestCallback = { snode, payload in
SessionUtil.sendRequest(
ed25519SecretKey: secKey,
targetPubkey: snode.x25519PublicKey,
targetIp: snode.ip,
targetPort: snode.port,
endpoint: "/storage_rpc/v1",
payload: payload
) { success, statusCode, data in
print("RAWR")
}
}
}
}

Loading…
Cancel
Save