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_geo 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.lookup_isp(ip_address) isp = 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 isp FROM max_mind_isp WHERE ip_bottom <= $1 AND ip_top >= $2", [ip_as_int, ip_as_int]) do |result| if !result.nil? && result.ntuples > 0 isp = result.getvalue(0, 0) end end end end return isp 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_geo 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_geo 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_geo WHERE country = $1 AND region = $2 ORDER BY city ASC", [country, region]).map do |tuple| tuple["city"] end end end def self.isps(country) ActiveRecord::Base.connection_pool.with_connection do |connection| pg_conn = connection.instance_variable_get("@connection") pg_conn.exec("SELECT DISTINCT isp FROM max_mind_isp WHERE country = $1 ORDER BY isp ASC", [country]).map do |tuple| tuple["isp"] end end end # Note that there's one big country, and then two cities in each region. def create_phony_database() clear_location_table (0..255).each do |top_octet| @pg_conn.exec("INSERT INTO max_mind_geo (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"), "US", "Region #{(top_octet / 2).floor}", "City #{top_octet}" ]).clear end clear_isp_table (0..255).each do |top_octet| @pg_conn.exec("INSERT INTO max_mind_isp (ip_bottom, ip_top, isp, country) VALUES ($1, $2, $3, $4)", [ self.class.ip_address_to_int("#{top_octet}.0.0.0"), self.class.ip_address_to_int("#{top_octet}.255.255.255"), "ISP #{top_octet}", "US" ]).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_location_table @pg_conn.exec("DELETE FROM max_mind_geo").clear end def clear_isp_table @pg_conn.exec("DELETE FROM max_mind_isp").clear end end