Geolocation via Reverse IP Address Lookup
From wikipedia, “Geolocation is the identification of the real-world geographic location of an internet connected computer, mobile device, website visitor or other.” In other words, geolocation utilizes a mapping of IP address to physical location (country, region, city…). Usually this involves a lookup table (database) of IP addresses. There are a number of paid services (ip2-location, ip-address-location, geo-bytes) that allow you to use their database for IP lookup. There are only a few free ones (host-tip, free-geo-ip). Scott Hanselman has a blog on using host-tip and Google to provide a geolocation service. This blog is inspired by his work on the subject.
I chose to utilize the free-geo-ip service developed by Alexandre Fiori. I appreciate the RESTful API and the clean returned data stream. Data is returned in one of three forms: CSV, XML, or JSON. To query data from the free-geo-ip service you submit a URL indicating the desired return data format and the IP address. For example try these URLs:
- http://freegeoip.appspot.com/csv/97.87.12.242
- http://freegeoip.appspot.com/xml/97.87.12.242
- http://freegeoip.appspot.com/json/97.87.12.242
I selected the XML format. XML is a bit heavier that JSON, but the amount of data being streamed from the service is small.
The XML returned looks like the following:
<?xml version="1.0" encoding="UTF-8"?> <Response> <Status>true</Status> <Ip>97.87.12.242</Ip> <CountryCode>US</CountryCode> <CountryName>United States</CountryName> <RegionCode>WI</RegionCode> <RegionName>Wisconsin</RegionName> <City>Verona</City> <ZipCode>53593</ZipCode> <Latitude>42.9925</Latitude> <Longitude>-89.5679</Longitude> </Response>
Very clean, which makes parsing a easy. The following C# static class wraps this service and provides a typed result object.
public static class IpLocator { public class Location { public bool Status { get; set; } public string IP { get; set; } public string CountryCode { get; set; } public string CountryName { get; set; } public string RegionCode { get; set; } public string RegionName { get; set; } public string City { get; set; } public string ZipCode { get; set; } public double Latitude { get; set; } public double Longitude { get; set; } public override string ToString() { if (Status) { return CountryName + ", " + RegionName + ", " + City + ", " + IP; } else { return "Location Not Found, " + IP; } } } private static Dictionary<string, Location> _cachedIps = new Dictionary<string,Location>(); public static Location Find(string ipAddress) { // Clean up the input parameter by parsing // and recasting it to a string. // IPAddress ip = IPAddress.Parse(ipAddress); string ipString = ip.ToString(); // Check the cache...if found, exit early. // if (_cachedIps.ContainsKey(ipString)) { return _cachedIps[ipString]; } string result; if (ipString == "127.0.0.1") { // This is xml in the following format. // This is used when testing on localhost. // result = @" <?xml version='1.0' encoding='UTF-8'?> <Response> <Status>true</Status> <Ip>97.87.12.242</Ip> <CountryCode>US</CountryCode> <CountryName>United States</CountryName> <RegionCode>WI</RegionCode> <RegionName>Wisconsin</RegionName> <City>Verona</City> <ZipCode>53593</ZipCode> <Latitude>42.9925</Latitude> <Longitude>-89.5679</Longitude> </Response>"; result = result.Replace("t", "").Replace("n", "").Replace("r", ""); } else { // Need to fetch from service // using (WebClient webClient = new WebClient()) { string url = string.Format("http://freegeoip.appspot.com/xml/{0}", ipString); result = webClient.DownloadString(url); } } // Need to parse the information // Location location = new Location(); try { XDocument xmlResponse = XDocument.Parse(result); XElement responseNode = xmlResponse.Element("Response"); location.Status = bool.Parse(responseNode.Element("Status").Value); location.IP = responseNode.Element("Ip").Value; location.CountryCode = responseNode.Element("CountryCode").Value; location.CountryName = responseNode.Element("CountryName").Value; location.RegionCode = responseNode.Element("RegionCode").Value; location.RegionName = responseNode.Element("RegionName").Value; location.City = responseNode.Element("City").Value; location.ZipCode = responseNode.Element("ZipCode").Value; location.Latitude = double.Parse(responseNode.Element("Latitude").Value); location.Longitude = double.Parse(responseNode.Element("Longitude").Value); } catch (Exception ex) { // Looks like the format changed! location = null; } // Cache the result // if (location != null) { _cachedIps.Add(ipString, location); } return location; } }
The IpLocator class contains the definition for the Location class. The Location class exposes properties for each XML node in the returned data stream. The overridden ToString method of the Location class provides a string with the most common information. Even though the call to free-geo-ip should be very fast, I want to minimize the number of fetches. The IpLocator class has a private dictionary that provides a fast local look up. If the IP address is not found, then the free-geo-ip service is called.
The code is pretty straight-forward. The entry point is the static Find method that takes an IP address in string format. Just in-case the format is not consistent, we first parse and then re-serialize the IP address. This is used as the look-up key into the dictionary. If not found, then the WebClient object is used to call and get the response from the free-geo-ip service. This response is parsed.