|
|
|
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
import GRDB
|
|
|
|
import SessionUtilitiesKit
|
|
|
|
|
|
|
|
public struct Snode: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible, Hashable, CustomStringConvertible {
|
|
|
|
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]
|
|
|
|
)
|
|
|
|
|
|
|
|
public typealias Columns = CodingKeys
|
|
|
|
public enum CodingKeys: String, CodingKey, ColumnExpression {
|
|
|
|
case address = "public_ip"
|
|
|
|
case port = "storage_port"
|
|
|
|
case ed25519PublicKey = "pubkey_ed25519"
|
|
|
|
case x25519PublicKey = "pubkey_x25519"
|
|
|
|
}
|
|
|
|
|
|
|
|
public let address: String
|
|
|
|
public let port: UInt16
|
|
|
|
public let ed25519PublicKey: String
|
|
|
|
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 var snodeSet: QueryInterfaceRequest<SnodeSet> {
|
|
|
|
request(for: Snode.snodeSet)
|
|
|
|
}
|
|
|
|
|
|
|
|
public var description: String { return "\(address):\(port)" }
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - Decoder
|
|
|
|
|
|
|
|
extension Snode {
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
|
|
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
|
|
|
|
|
|
|
|
do {
|
|
|
|
let address: String = try container.decode(String.self, forKey: .address)
|
|
|
|
|
|
|
|
guard address != "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)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
catch {
|
|
|
|
SNLog("Failed to parse snode: \(error.localizedDescription).")
|
|
|
|
throw HTTPError.invalidJSON
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - GRDB Interactions
|
|
|
|
|
|
|
|
internal extension Snode {
|
|
|
|
static func fetchSet(_ db: Database, publicKey: String) throws -> Set<Snode> {
|
|
|
|
return try Snode
|
|
|
|
.joining(
|
|
|
|
required: Snode.snodeSet
|
|
|
|
.filter(SnodeSet.Columns.key == publicKey)
|
|
|
|
.order(SnodeSet.Columns.nodeIndex)
|
|
|
|
)
|
|
|
|
.fetchSet(db)
|
|
|
|
}
|
|
|
|
|
|
|
|
static func fetchAllOnionRequestPaths(_ db: Database) throws -> [[Snode]] {
|
|
|
|
struct ResultWrapper: Decodable, FetchableRecord {
|
|
|
|
let key: String
|
|
|
|
let nodeIndex: Int
|
|
|
|
let address: String
|
|
|
|
let port: UInt16
|
|
|
|
let snode: Snode
|
|
|
|
}
|
|
|
|
|
|
|
|
return try SnodeSet
|
|
|
|
.filter(SnodeSet.Columns.key.like("\(SnodeSet.onionRequestPathPrefix)%"))
|
|
|
|
.order(
|
|
|
|
SnodeSet.Columns.nodeIndex,
|
|
|
|
SnodeSet.Columns.key
|
|
|
|
)
|
|
|
|
.including(required: SnodeSet.node)
|
|
|
|
.asRequest(of: ResultWrapper.self)
|
|
|
|
.fetchAll(db)
|
|
|
|
.reduce(into: [:]) { prev, next in // Reducing will lose the 'key' sorting
|
|
|
|
prev[next.key] = (prev[next.key] ?? []).appending(next.snode)
|
|
|
|
}
|
|
|
|
.asArray()
|
|
|
|
.sorted(by: { lhs, rhs in lhs.key < rhs.key })
|
|
|
|
.compactMap { _, nodes in !nodes.isEmpty ? nodes : nil } // Exclude empty sets
|
|
|
|
}
|
|
|
|
|
|
|
|
static func clearOnionRequestPaths(_ db: Database) throws {
|
|
|
|
try SnodeSet
|
|
|
|
.filter(SnodeSet.Columns.key.like("\(SnodeSet.onionRequestPathPrefix)%"))
|
|
|
|
.deleteAll(db)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
internal extension Collection where Element == Snode {
|
|
|
|
/// This method is used to save Swarms
|
|
|
|
func save(_ db: Database, key: String) throws {
|
|
|
|
try self.enumerated().forEach { nodeIndex, node in
|
|
|
|
try node.save(db)
|
|
|
|
|
|
|
|
try SnodeSet(
|
|
|
|
key: key,
|
|
|
|
nodeIndex: nodeIndex,
|
|
|
|
address: node.address,
|
|
|
|
port: node.port
|
|
|
|
).save(db)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
internal extension Collection where Element == [Snode] {
|
|
|
|
/// This method is used to save onion reuqest paths
|
|
|
|
func save(_ db: Database) throws {
|
|
|
|
try self.enumerated().forEach { pathIndex, path in
|
|
|
|
try path.save(db, key: "\(SnodeSet.onionRequestPathPrefix)\(pathIndex)")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|