123 lines
3.9 KiB
Ruby
123 lines
3.9 KiB
Ruby
class MaxMindManager < BaseManager
|
|
|
|
def initialize(options={})
|
|
super(options)
|
|
end
|
|
|
|
|
|
# Returns a hash with location information. Fields are nil if they can't be figured.
|
|
# This is a class method because it doesn't need to be in a transaction.
|
|
def self.lookup(ip_address)
|
|
|
|
city = state = country = nil
|
|
|
|
unless ip_address.nil? || ip_address !~ /^\d+\.\d+\.\d+\.\d+$/
|
|
ActiveRecord::Base.connection_pool.with_connection do |connection|
|
|
pg_conn = connection.instance_variable_get("@connection")
|
|
ip_as_int = ip_address_to_int(ip_address)
|
|
pg_conn.exec("SELECT country, region, city FROM max_mind WHERE ip_bottom <= $1 AND ip_top >= $2", [ip_as_int, ip_as_int]) do |result|
|
|
if !result.nil? && result.ntuples > 0
|
|
country = result.getvalue(0, 0)
|
|
state = result[0]['region']
|
|
city = result[0]['city']
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
{
|
|
:city => city,
|
|
:state => state,
|
|
:country => country
|
|
}
|
|
|
|
end
|
|
|
|
def self.countries()
|
|
ActiveRecord::Base.connection_pool.with_connection do |connection|
|
|
pg_conn = connection.instance_variable_get("@connection")
|
|
pg_conn.exec("SELECT DISTINCT country FROM max_mind ORDER BY country ASC").map do |tuple|
|
|
tuple["country"]
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
def self.regions(country)
|
|
ActiveRecord::Base.connection_pool.with_connection do |connection|
|
|
pg_conn = connection.instance_variable_get("@connection")
|
|
pg_conn.exec("SELECT DISTINCT region FROM max_mind WHERE country = $1 ORDER BY region ASC", [country]).map do |tuple|
|
|
tuple["region"]
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
def self.cities(country, region)
|
|
ActiveRecord::Base.connection_pool.with_connection do |connection|
|
|
pg_conn = connection.instance_variable_get("@connection")
|
|
pg_conn.exec("SELECT DISTINCT city FROM max_mind WHERE country = $1 AND region = $2 ORDER BY city ASC", [country, region]).map do |tuple|
|
|
tuple["city"]
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
|
|
|
|
# Do this in a transaction so there's no partial import.
|
|
def import_database(filename)
|
|
clear_table
|
|
# Format:
|
|
# startIpNum,endIpNum,country,region,city,postalCode,latitude,longitude,dmaCode,areaCode
|
|
it = 0
|
|
CSV.foreach(filename, col_sep: ",", encoding: "ISO-8859-1") do |split_line|
|
|
next if split_line[0] == 'startIpNum' # Ignore the legend line at the top
|
|
it += 1
|
|
if it > 100000
|
|
return
|
|
end
|
|
unless split_line.length < 10
|
|
@pg_conn.exec("INSERT INTO max_mind (ip_bottom, ip_top, country, region, city) VALUES ($1, $2, $3, $4, $5)",
|
|
[
|
|
self.class.ip_address_to_int(split_line[0]),
|
|
self.class.ip_address_to_int(split_line[1]),
|
|
split_line[2],
|
|
split_line[3],
|
|
split_line[4]
|
|
]).clear
|
|
end
|
|
end
|
|
end
|
|
|
|
# Note that there's one big country, and then two cities in each region.
|
|
def create_phony_database()
|
|
clear_table
|
|
(0..255).each do |top_octet|
|
|
@pg_conn.exec("INSERT INTO max_mind (ip_bottom, ip_top, country, region, city) VALUES ($1, $2, $3, $4, $5)",
|
|
[
|
|
self.class.ip_address_to_int("#{top_octet}.0.0.0"),
|
|
self.class.ip_address_to_int("#{top_octet}.255.255.255"),
|
|
"United States",
|
|
"Region #{(top_octet / 2).floor}",
|
|
"City #{top_octet}"
|
|
]).clear
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
# Make an IP address fit in a signed int. Just divide it by 2, as the least significant part
|
|
# just can't possibly matter. We can verify this if needed. My guess is the entire bottom octet is
|
|
# actually irrelevant
|
|
def self.ip_address_to_int(ip)
|
|
ip.split('.').inject(0) {|total,value| (total << 8 ) + value.to_i} / 2
|
|
end
|
|
|
|
def clear_table
|
|
@pg_conn.exec("DELETE FROM max_mind").clear
|
|
end
|
|
|
|
end
|
|
|