session-ios/Session/Utilities/IP2Country.swift

73 lines
3.3 KiB
Swift

final class IP2Country {
var countryNamesCache: [String:String] = [:]
private static let workQueue = DispatchQueue(label: "IP2Country.workQueue", qos: .utility) // It's important that this is a serial queue
static var isInitialized = false
// 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", 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", withExtension: nil)!
let data = try! Data(contentsOf: url)
return try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as! [String:[String]]
}()
// MARK: Lifecycle
static let shared = IP2Country()
private init() {
NotificationCenter.default.addObserver(self, selector: #selector(populateCacheIfNeededAsync), name: .pathsBuilt, object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
// MARK: Implementation
private func cacheCountry(for ip: String) -> String {
if let result = countryNamesCache[ip] { return result }
let ipAsInt = IPv4.toInt(ip)
guard let ipv4TableIndex = given(ipv4Table["network"]!.firstIndex(where: { $0 > ipAsInt }), { $0 - 1 }) else { return "Unknown Country" } // Relies on the array being sorted
let countryID = ipv4Table["registered_country_geoname_id"]![ipv4TableIndex]
guard let countryNamesTableIndex = countryNamesTable["geoname_id"]!.firstIndex(of: String(countryID)) else { return "Unknown Country" }
let result = countryNamesTable["country_name"]![countryNamesTableIndex]
countryNamesCache[ip] = result
return result
}
@objc func populateCacheIfNeededAsync() {
// This has to be sync since the `countryNamesCache` dict doesn't like async access
IP2Country.workQueue.sync {
let _ = self.populateCacheIfNeeded()
}
}
func populateCacheIfNeeded() -> Bool {
if OnionRequestAPI.paths.isEmpty {
OnionRequestAPI.paths = Storage.shared.getOnionRequestPaths()
}
let paths = OnionRequestAPI.paths
guard !paths.isEmpty else { return false }
let pathToDisplay = paths.first!
pathToDisplay.forEach { snode in
let _ = self.cacheCountry(for: snode.ip) // Preload if needed
}
DispatchQueue.main.async {
IP2Country.isInitialized = true
NotificationCenter.default.post(name: .onionRequestPathCountriesLoaded, object: nil)
}
SNLog("Finished preloading onion request path countries.")
return true
}
}