|
|
|
@ -2,10 +2,18 @@
|
|
|
|
|
final class IP2Country {
|
|
|
|
|
var countryNamesCache: [String:String] = [:]
|
|
|
|
|
|
|
|
|
|
private lazy var ipv4Table: [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:[String]]
|
|
|
|
|
return try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as! [String:[Int]]
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
private lazy var countryNamesTable: [String:[String]] = {
|
|
|
|
@ -14,10 +22,6 @@ final class IP2Country {
|
|
|
|
|
return try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as! [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: Lifecycle
|
|
|
|
|
static let shared = IP2Country()
|
|
|
|
|
|
|
|
|
@ -28,29 +32,17 @@ final class IP2Country {
|
|
|
|
|
deinit {
|
|
|
|
|
NotificationCenter.default.removeObserver(self)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: Implementation
|
|
|
|
|
private func cacheCountry(for ip: String) -> String {
|
|
|
|
|
var truncatedIP = ip
|
|
|
|
|
func getCountryInternal() -> String {
|
|
|
|
|
if let country = countryNamesCache[ip] { return country }
|
|
|
|
|
if let ipv4TableIndex = ipv4Table["network"]!.firstIndex(where: { $0.starts(with: truncatedIP) }) {
|
|
|
|
|
let countryID = ipv4Table["registered_country_geoname_id"]![ipv4TableIndex]
|
|
|
|
|
if let countryNamesTableIndex = countryNamesTable["geoname_id"]!.firstIndex(of: countryID) {
|
|
|
|
|
let country = countryNamesTable["country_name"]![countryNamesTableIndex]
|
|
|
|
|
countryNamesCache[ip] = country
|
|
|
|
|
return country
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if truncatedIP.contains(".") && !truncatedIP.hasSuffix(".") { // The fuzziest we want to go is xxx.x
|
|
|
|
|
truncatedIP.removeLast()
|
|
|
|
|
if truncatedIP.hasSuffix(".") { truncatedIP.removeLast() }
|
|
|
|
|
return getCountryInternal()
|
|
|
|
|
} else {
|
|
|
|
|
return "Unknown Country"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return getCountryInternal()
|
|
|
|
|
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() {
|
|
|
|
|