You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
session-ios/Session/Utilities/IP2Country.swift

107 lines
4.2 KiB
Swift

import Foundation
import GRDB
import SessionSnodeKit
import SessionUtilitiesKit
final class IP2Country: Hashable {
static var isInitialized: Bool = false
static let shared: IP2Country = IP2Country()
private let instanceIdentifier: UUID = UUID()
private let dependencies: Dependencies
public var countryNamesCache: Atomic<[String: String]> = Atomic([:])
// MARK: - Tables
/// This table has two columns: the "network" column and the "registered_country_geoname_id" column. The network column contains
/// the **lower** bound of an IP range and the "registered_country_geoname_id" column contains the ID of the country corresponding
/// to that range. We look up an IP by finding the first index in the network column where the value is greater than the IP we're looking
/// up (converted to an integer). The IP we're looking up must then be in the range **before** that range.
private lazy var ipv4Table: [String: [Int]] = {
let url = Bundle.main.url(
forResource: "GeoLite2-Country-Blocks-IPv4", // stringlint:disable
withExtension: nil
)!
let data = try! Data(contentsOf: url)
return try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as! [String: [Int]]
}()
private lazy var countryNamesTable: [String: [String]] = {
let url = Bundle.main.url(
forResource: "GeoLite2-Country-Locations-English", // stringlint:disable
withExtension: nil
)!
let data = try! Data(contentsOf: url)
return try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as! [String: [String]]
}()
// MARK: - Initialization
private init(using dependencies: Dependencies = Dependencies()) {
self.dependencies = dependencies
dependencies.addFeatureObserver(self, for: .networkLayers, events: [.pathsBuilt]) { [weak self] _, _ in
self?.populateCacheIfNeededAsync()
}
}
deinit {
dependencies.removeFeatureObserver(self)
}
// MARK: - Implementation
@discardableResult private func cacheCountry(for ip: String, inCache cache: inout [String: String]) -> String {
if let result: String = cache[ip] { return result }
let ipAsInt: Int = IPv4.toInt(ip)
guard
let ipv4TableIndex: Int = ipv4Table["network"]? // stringlint:disable
.firstIndex(where: { $0 > ipAsInt })
.map({ $0 - 1 }),
let countryID: Int = ipv4Table["registered_country_geoname_id"]?[ipv4TableIndex], // stringlint:disable
let countryNamesTableIndex = countryNamesTable["geoname_id"]? // stringlint:disable
.firstIndex(of: String(countryID)),
let result: String = countryNamesTable["country_name"]?[countryNamesTableIndex] // stringlint:disable
else {
return "Unknown Country" // Relies on the array being sorted
}
cache[ip] = result
return result
}
@objc func populateCacheIfNeededAsync() {
DispatchQueue.global(qos: .utility).async { [weak self] in
self?.populateCacheIfNeeded()
}
}
@discardableResult func populateCacheIfNeeded(using dependencies: Dependencies = Dependencies()) -> Bool {
guard let pathToDisplay: [Snode] = dependencies[cache: .onionRequestAPI].paths.first else { return false }
countryNamesCache.mutate { [weak self] cache in
pathToDisplay.forEach { snode in
self?.cacheCountry(for: snode.ip, inCache: &cache) // Preload if needed
}
}
DispatchQueue.main.async {
IP2Country.isInitialized = true
dependencies.notifyObservers(for: .networkLayers, with: .onionRequestPathCountriesLoaded)
}
SNLog("Finished preloading onion request path countries.")
return true
}
// MARK: - Conformance
static func == (lhs: IP2Country, rhs: IP2Country) -> Bool {
return (lhs.instanceIdentifier == rhs.instanceIdentifier)
}
func hash(into hasher: inout Hasher) {
instanceIdentifier.hash(into: &hasher)
}
}