Skip to content

Instantly share code, notes, and snippets.

@ajturner
Created November 22, 2011 00:54
Show Gist options
  • Select an option

  • Save ajturner/1384539 to your computer and use it in GitHub Desktop.

Select an option

Save ajturner/1384539 to your computer and use it in GitHub Desktop.
Geolocate an IP Address with HostIP
#--
# Copyright (c) 2006 Andrew Turner <[email protected]>
#
# Based on Geocoder - Copyright (c) 2006 Paul Smith <[email protected]>
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
#
# = Geolocation -- Geolocation library for Ruby
#
# Example:
# ip = "192.168.0.1"
# geoloc = Geolocation::HostIP.new
# location = geoloc.geolocate ip
require 'cgi'
require 'net/http'
require 'rexml/document'
require 'timeout'
module Geolocation
STATE_ABBR = {
'AL' => 'Alabama',
'AK' => 'Alaska',
'AS' => 'America Samoa',
'AZ' => 'Arizona',
'AR' => 'Arkansas',
'CA' => 'California',
'CO' => 'Colorado',
'CT' => 'Connecticut',
'DE' => 'Delaware',
'DC' => 'District of Columbia',
'FM' => 'Micronesia1',
'FL' => 'Florida',
'GA' => 'Georgia',
'GU' => 'Guam',
'HI' => 'Hawaii',
'ID' => 'Idaho',
'IL' => 'Illinois',
'IN' => 'Indiana',
'IA' => 'Iowa',
'KS' => 'Kansas',
'KY' => 'Kentucky',
'LA' => 'Louisiana',
'ME' => 'Maine',
'MH' => 'Islands1',
'MD' => 'Maryland',
'MA' => 'Massachusetts',
'MI' => 'Michigan',
'MN' => 'Minnesota',
'MS' => 'Mississippi',
'MO' => 'Missouri',
'MT' => 'Montana',
'NE' => 'Nebraska',
'NV' => 'Nevada',
'NH' => 'New Hampshire',
'NJ' => 'New Jersey',
'NM' => 'New Mexico',
'NY' => 'New York',
'NC' => 'North Carolina',
'ND' => 'North Dakota',
'OH' => 'Ohio',
'OK' => 'Oklahoma',
'OR' => 'Oregon',
'PW' => 'Palau',
'PA' => 'Pennsylvania',
'PR' => 'Puerto Rico',
'RI' => 'Rhode Island',
'SC' => 'South Carolina',
'SD' => 'South Dakota',
'TN' => 'Tennessee',
'TX' => 'Texas',
'UT' => 'Utah',
'VT' => 'Vermont',
'VI' => 'Virgin Island',
'VA' => 'Virginia',
'WA' => 'Washington',
'WV' => 'West Virginia',
'WI' => 'Wisconsin',
'WY' => 'Wyoming'
}
class BlankIPString < Exception; end
class GeolocationError < Exception; end
FIELDS = [ ["latitude", "Latitude"],
["longitude", "Longitude"],
["address", "Address"],
["city", "City"],
["region", "region"],
["zip", "ZIP Code"] ].freeze
class Base
# +ip+ is a string of the IP-Address to Geolocate
def geolocate ip, *args
options = { :timeout => nil }
options.update(args.pop) if args.last.is_a?(Hash)
@options = options
if ip.nil? or ip.empty?
raise BlankIPString
end
ip = String ip
results = parse request(ip)
create_response results
end
def create_response results
Response.new results
end
# Makes an HTTP GET request on URL and returns the body
# of the response
def get url, timeout=5
url = URI.parse url
http = Net::HTTP.new url.host, url.port
res = Timeout::timeout(timeout) {
http.get url.request_uri
}
res.body
end
def request ip
get url(ip), @options[:timeout]
end
end
class HostIP < Base
include REXML
def initialize
fields.each { |field| field = "" }
end
private
# return array of results
def parse xml
# Create a new REXML::Document object from the raw XML text
xml = Document.new xml
if xml.root.elements["/HostipLookupResultSet/gml:featureMember/Hostip/countryAbbrev"].get_text.value == "XX"
msgs = []
raise GeolocationError, "Error in geolocation"
else
results = []
result = Result.new
# add fields
result.city, result.region = xml.root.elements["/HostipLookupResultSet/gml:featureMember/Hostip/gml:name"].get_text.value.split(",",2)
result.country = xml.root.elements["/HostipLookupResultSet/gml:featureMember/Hostip/countryName"].get_text.value.capitalize
result.longitude, result.latitude = xml.root.elements["/HostipLookupResultSet/gml:featureMember/Hostip/ipLocation/gml:pointProperty/gml:Point/gml:coordinates"].get_text.value.split(",",2)
#convert MI to Michigan, et al.
result.region = STATE_ABBR.[](result.region.strip!)
results << result
return results
end
end
def fields
%w| latitude longitude address city region zip country |
end
def attributes
%w| precision, warning |
end
def is_error? document
document.root.name == "Error"
end
def url ip
"http://api.hostip.info?ip=#{CGI.escape ip}"
end
end
SERVICES = { :hostip => HostIP }.freeze
class Result < Struct.new :latitude, :longitude, :address, :city,
:region, :zip, :country, :precision,
:warning
alias :lat :latitude
alias :lng :longitude
end
# A Response is a representation of the entire response from the
# geolocation service, which may include multiple results,
# as well as warnings and errors
class Response < Array
def initialize results
results.each do |result|
self << result
end
end
# Geolocation was a success if one result in the result
# set is retured and there is no warning attribute in that result
def success?
size == 1 and self[0].warning.nil?
end
def bullseye?
success?
end
# Returns latitude in degrees decimal
def latitude
self[0].latitude if bullseye?
end
# Returns longitude in degrees decimal
def longitude
self[0].longitude if bullseye?
end
# Returns normalized street address, capitalized
def address
self[0].address if bullseye?
end
# Returns normalized city name, capitalized
def city
self[0].city if bullseye?
end
# Returns normalized two-letter USPS region abbreviation
def region
self[0].region if bullseye?
end
alias_method :array_zip, :zip
# Returns normalized ZIP Code, or postal code
def zip
self[0].zip if bullseye?
end
# Returns two-letter country code abbreviation
def country
self[0].country if bullseye?
end
attr_reader :warning, :precision
alias :lat :latitude
alias :lng :longitude
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment