mirror of https://github.com/oxen-io/session-ios
[WIP] Working on the libQuic onion requests
parent
756e256d9a
commit
8ef1c24215
@ -1 +1 @@
|
||||
Subproject commit 0b48055f5f00e15a2fae41fa846f8c9acc2628a7
|
||||
Subproject commit 6dab3b99208b9be410952174e72cb38bb0dedb27
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue