├── LICENSE.txt ├── README.textile ├── example └── example.go └── libgeo.go /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, Nikola Ranchev 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.textile: -------------------------------------------------------------------------------- 1 | h2. General Description 2 | 3 | p{width:500px}. This is the Go implementation of the "Maxmind":http://www.maxmind.com/app/ip-location GeoIP API. It is incomplete and work in progress the initial goal is support only two of the database types - the City Lite and Country Lite. The only supported method is loading the full db on startup into memory (memory cache). 4 | 5 | h3. Supported Access Methods 6 | 7 | * In Memory (Load(string)) 8 | 9 | h3. Supported Database Formats 10 | 11 | * Country Edition (dbType=1) 12 | * City Edition REV 0 (dbType=6) 13 | * City Edition REV 1 (dbType=2) 14 | 15 | h3. Supported Lookups 16 | 17 | * By IP Address (GetLocationByIP(string)) 18 | * By IP Number (GetLocationByIPNum(uint32)) 19 | 20 | h3. Supported Responses 21 | 22 | * CountryCode string (available in all databases) 23 | * CountryName string (available in all databases) 24 | * City string 25 | * Region string 26 | * PostalCode string 27 | * Latitude float32 28 | * Longitude float32 29 | 30 | h3. To Do 31 | * Implement better error handling (report the error on load and lookups) 32 | * Better returns, country edition has only code and name (perhaps use interfaces) 33 | * Add test cases and benchmarking 34 | * Add support for more database formats 35 | 36 | h3. Build 37 | 38 | make (See Makefile for more details) 39 | 40 | h3. Example 41 | 42 | ./example DBFILE IPADDRESS (i.e. ./example GeoIP.dat 1.1.1.1) 43 | 44 | h3. Usage 45 | 46 | Please see example.go for a complete example of how to use this library. 47 | -------------------------------------------------------------------------------- /example/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/nranchev/go-libGeoIP" 7 | ) 8 | 9 | func main() { 10 | flag.Parse() 11 | 12 | // Check the number of arguments 13 | if flag.NArg() < 2 { 14 | fmt.Printf("usage: main DBFILE IPADDRESS\n") 15 | return 16 | } 17 | 18 | // Set the arguments 19 | dbFile := flag.Arg(0) 20 | ipAddr := flag.Arg(1) 21 | 22 | // Load the database file, exit on failure 23 | gi, err := libgeo.Load(dbFile) 24 | if err != nil { 25 | fmt.Printf("Error: %s\n", err.Error()) 26 | return 27 | } 28 | 29 | // Lookup the IP and display the details if country is found 30 | loc := gi.GetLocationByIP(ipAddr) 31 | if loc != nil { 32 | fmt.Printf("Country: %s (%s)\n", loc.CountryName, loc.CountryCode) 33 | fmt.Printf("City: %s\n", loc.City) 34 | fmt.Printf("Region: %s\n", loc.Region) 35 | fmt.Printf("Postal Code: %s\n", loc.PostalCode) 36 | fmt.Printf("Latitude: %f\n", loc.Latitude) 37 | fmt.Printf("Longitude: %f\n", loc.Longitude) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /libgeo.go: -------------------------------------------------------------------------------- 1 | /** 2 | * libgeo.go 3 | * 4 | * Copyright (c) 2010, Nikola Ranchev 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * - Redistributions of source code must retain the above copyright 10 | * notice, this list of conditions and the following disclaimer. 11 | * - Redistributions in binary form must reproduce the above copyright 12 | * notice, this list of conditions and the following disclaimer in the 13 | * documentation and/or other materials provided with the distribution. 14 | * - Neither the name of the nor the 15 | * names of its contributors may be used to endorse or promote products 16 | * derived from this software without specific prior written permission. 17 | * 18 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | * DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 22 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | */ 29 | 30 | package libgeo 31 | 32 | // Dependencies 33 | import ( 34 | "encoding/binary" 35 | "errors" 36 | "net" 37 | "os" 38 | ) 39 | 40 | // Globals (const arrays that will be initialized inside init()) 41 | var ( 42 | countryCode = []string{ 43 | "--", "AP", "EU", "AD", "AE", "AF", "AG", "AI", "AL", "AM", "AN", "AO", "AQ", "AR", 44 | "AS", "AT", "AU", "AW", "AZ", "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", 45 | "BM", "BN", "BO", "BR", "BS", "BT", "BV", "BW", "BY", "BZ", "CA", "CC", "CD", "CF", 46 | "CG", "CH", "CI", "CK", "CL", "CM", "CN", "CO", "CR", "CU", "CV", "CX", "CY", "CZ", 47 | "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "EH", "ER", "ES", "ET", "FI", 48 | "FJ", "FK", "FM", "FO", "FR", "FX", "GA", "GB", "GD", "GE", "GF", "GH", "GI", "GL", 49 | "GM", "GN", "GP", "GQ", "GR", "GS", "GT", "GU", "GW", "GY", "HK", "HM", "HN", "HR", 50 | "HT", "HU", "ID", "IE", "IL", "IN", "IO", "IQ", "IR", "IS", "IT", "JM", "JO", "JP", 51 | "KE", "KG", "KH", "KI", "KM", "KN", "KP", "KR", "KW", "KY", "KZ", "LA", "LB", "LC", 52 | "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "MG", "MH", "MK", 53 | "ML", "MM", "MN", "MO", "MP", "MQ", "MR", "MS", "MT", "MU", "MV", "MW", "MX", "MY", 54 | "MZ", "NA", "NC", "NE", "NF", "NG", "NI", "NL", "NO", "NP", "NR", "NU", "NZ", "OM", 55 | "PA", "PE", "PF", "PG", "PH", "PK", "PL", "PM", "PN", "PR", "PS", "PT", "PW", "PY", 56 | "QA", "RE", "RO", "RU", "RW", "SA", "SB", "SC", "SD", "SE", "SG", "SH", "SI", "SJ", 57 | "SK", "SL", "SM", "SN", "SO", "SR", "ST", "SV", "SY", "SZ", "TC", "TD", "TF", "TG", 58 | "TH", "TJ", "TK", "TM", "TN", "TO", "TL", "TR", "TT", "TV", "TW", "TZ", "UA", "UG", 59 | "UM", "US", "UY", "UZ", "VA", "VC", "VE", "VG", "VI", "VN", "VU", "WF", "WS", "YE", 60 | "YT", "RS", "ZA", "ZM", "ME", "ZW", "A1", "A2", "O1", "AX", "GG", "IM", "JE", "BL", 61 | "MF", "BQ", "SS", "O1"} 62 | countryName = []string{ 63 | "N/A", "Asia/Pacific Region", "Europe", "Andorra", "United Arab Emirates", 64 | "Afghanistan", "Antigua and Barbuda", "Anguilla", "Albania", "Armenia", 65 | "Netherlands Antilles", "Angola", "Antarctica", "Argentina", "American Samoa", 66 | "Austria", "Australia", "Aruba", "Azerbaijan", "Bosnia and Herzegovina", 67 | "Barbados", "Bangladesh", "Belgium", "Burkina Faso", "Bulgaria", "Bahrain", 68 | "Burundi", "Benin", "Bermuda", "Brunei Darussalam", "Bolivia", "Brazil", "Bahamas", 69 | "Bhutan", "Bouvet Island", "Botswana", "Belarus", "Belize", "Canada", 70 | "Cocos (Keeling) Islands", "Congo, The Democratic Republic of the", 71 | "Central African Republic", "Congo", "Switzerland", "Cote D'Ivoire", 72 | "Cook Islands", "Chile", "Cameroon", "China", "Colombia", "Costa Rica", "Cuba", 73 | "Cape Verde", "Christmas Island", "Cyprus", "Czech Republic", "Germany", 74 | "Djibouti", "Denmark", "Dominica", "Dominican Republic", "Algeria", "Ecuador", 75 | "Estonia", "Egypt", "Western Sahara", "Eritrea", "Spain", "Ethiopia", "Finland", 76 | "Fiji", "Falkland Islands (Malvinas)", "Micronesia, Federated States of", 77 | "Faroe Islands", "France", "France, Metropolitan", "Gabon", "United Kingdom", 78 | "Grenada", "Georgia", "French Guiana", "Ghana", "Gibraltar", "Greenland", "Gambia", 79 | "Guinea", "Guadeloupe", "Equatorial Guinea", "Greece", 80 | "South Georgia and the South Sandwich Islands", "Guatemala", "Guam", 81 | "Guinea-Bissau", "Guyana", "Hong Kong", "Heard Island and McDonald Islands", 82 | "Honduras", "Croatia", "Haiti", "Hungary", "Indonesia", "Ireland", "Israel", "India", 83 | "British Indian Ocean Territory", "Iraq", "Iran, Islamic Republic of", 84 | "Iceland", "Italy", "Jamaica", "Jordan", "Japan", "Kenya", "Kyrgyzstan", "Cambodia", 85 | "Kiribati", "Comoros", "Saint Kitts and Nevis", 86 | "Korea, Democratic People's Republic of", "Korea, Republic of", "Kuwait", 87 | "Cayman Islands", "Kazakhstan", "Lao People's Democratic Republic", "Lebanon", 88 | "Saint Lucia", "Liechtenstein", "Sri Lanka", "Liberia", "Lesotho", "Lithuania", 89 | "Luxembourg", "Latvia", "Libyan Arab Jamahiriya", "Morocco", "Monaco", 90 | "Moldova, Republic of", "Madagascar", "Marshall Islands", 91 | "Macedonia", "Mali", "Myanmar", "Mongolia", 92 | "Macau", "Northern Mariana Islands", "Martinique", "Mauritania", "Montserrat", 93 | "Malta", "Mauritius", "Maldives", "Malawi", "Mexico", "Malaysia", "Mozambique", 94 | "Namibia", "New Caledonia", "Niger", "Norfolk Island", "Nigeria", "Nicaragua", 95 | "Netherlands", "Norway", "Nepal", "Nauru", "Niue", "New Zealand", "Oman", "Panama", 96 | "Peru", "French Polynesia", "Papua New Guinea", "Philippines", "Pakistan", 97 | "Poland", "Saint Pierre and Miquelon", "Pitcairn Islands", "Puerto Rico", 98 | "Palestinian Territory", "Portugal", "Palau", "Paraguay", "Qatar", 99 | "Reunion", "Romania", "Russian Federation", "Rwanda", "Saudi Arabia", 100 | "Solomon Islands", "Seychelles", "Sudan", "Sweden", "Singapore", "Saint Helena", 101 | "Slovenia", "Svalbard and Jan Mayen", "Slovakia", "Sierra Leone", "San Marino", 102 | "Senegal", "Somalia", "Suriname", "Sao Tome and Principe", "El Salvador", 103 | "Syrian Arab Republic", "Swaziland", "Turks and Caicos Islands", "Chad", 104 | "French Southern Territories", "Togo", "Thailand", "Tajikistan", "Tokelau", 105 | "Turkmenistan", "Tunisia", "Tonga", "Timor-Leste", "Turkey", "Trinidad and Tobago", 106 | "Tuvalu", "Taiwan", "Tanzania, United Republic of", "Ukraine", "Uganda", 107 | "United States Minor Outlying Islands", "United States", "Uruguay", "Uzbekistan", 108 | "Holy See (Vatican City State)", "Saint Vincent and the Grenadines", 109 | "Venezuela", "Virgin Islands, British", "Virgin Islands, U.S.", "Vietnam", 110 | "Vanuatu", "Wallis and Futuna", "Samoa", "Yemen", "Mayotte", "Serbia", 111 | "South Africa", "Zambia", "Montenegro", "Zimbabwe", "Anonymous Proxy", 112 | "Satellite Provider", "Other", "Aland Islands", "Guernsey", "Isle of Man", "Jersey", 113 | "Saint Barthelemy", "Saint Martin", "Bonaire, Saint Eustatius and Saba", 114 | "South Sudan", "Other"} 115 | ) 116 | 117 | // Constants 118 | const ( 119 | maxRecordLength = 4 120 | standardRecordLength = 3 121 | countryBegin = 16776960 122 | structureInfoMaxSize = 20 123 | fullRecordLength = 60 124 | segmentRecordLength = 3 125 | 126 | // DB Types 127 | dbCountryEdition = byte(1) 128 | dbCityEditionRev0 = byte(6) 129 | dbCityEditionRev1 = byte(2) 130 | ) 131 | 132 | // These are some structs 133 | type GeoIP struct { 134 | databaseSegment int // No need to make an array of size 1 135 | recordLength int // Set to one of the constants above 136 | dbType byte // Store the database type 137 | data []byte // All of the data from the DB file 138 | } 139 | type Location struct { 140 | CountryCode string // If country ed. only country info is filled 141 | CountryName string // If country ed. only country info is filled 142 | Region string 143 | City string 144 | PostalCode string 145 | Latitude float32 146 | Longitude float32 147 | } 148 | 149 | // Load the database file in memory, detect the db format and setup the GeoIP struct 150 | func Load(filename string) (gi *GeoIP, err error) { 151 | // Try to open the requested file 152 | dbInfo, err := os.Lstat(filename) 153 | if err != nil { 154 | return 155 | } 156 | dbFile, err := os.Open(filename) 157 | if err != nil { 158 | return 159 | } 160 | 161 | // Copy the db into memory 162 | gi = new(GeoIP) 163 | gi.data = make([]byte, dbInfo.Size()) 164 | dbFile.Read(gi.data) 165 | dbFile.Close() 166 | 167 | // Check the database type 168 | gi.dbType = dbCountryEdition // Default the database to country edition 169 | gi.databaseSegment = countryBegin // Default to country DB 170 | gi.recordLength = standardRecordLength // Default to country DB 171 | 172 | // Search for the DB type headers 173 | delim := make([]byte, 3) 174 | for i := 0; i < structureInfoMaxSize; i++ { 175 | delim = gi.data[len(gi.data)-i-3-1 : len(gi.data)-i-1] 176 | if int8(delim[0]) == -1 && int8(delim[1]) == -1 && int8(delim[2]) == -1 { 177 | gi.dbType = gi.data[len(gi.data)-i-1] 178 | // If we detect city edition set the correct segment offset 179 | if gi.dbType == dbCityEditionRev0 || gi.dbType == dbCityEditionRev1 { 180 | buf := make([]byte, segmentRecordLength) 181 | buf = gi.data[len(gi.data)-i-1+1 : len(gi.data)-i-1+4] 182 | gi.databaseSegment = 0 183 | for j := 0; j < segmentRecordLength; j++ { 184 | gi.databaseSegment += (int(buf[j]) << uint8(j*8)) 185 | } 186 | } 187 | break 188 | } 189 | } 190 | 191 | // Support older formats 192 | if gi.dbType >= 106 { 193 | gi.dbType -= 105 194 | } 195 | 196 | // Reject unsupported formats 197 | if gi.dbType != dbCountryEdition && gi.dbType != dbCityEditionRev0 && gi.dbType != dbCityEditionRev1 { 198 | err = errors.New("Unsupported database format") 199 | return 200 | } 201 | 202 | return 203 | } 204 | 205 | // Lookup by IP address and return location 206 | func (gi *GeoIP) GetLocationByIP(ip string) *Location { 207 | return gi.GetLocationByIPNum(addrToNum(ip)) 208 | } 209 | 210 | // Lookup by IP number and return location 211 | func (gi *GeoIP) GetLocationByIPNum(ipNum uint32) *Location { 212 | // Perform the lookup on the database to see if the record is found 213 | offset := gi.lookupByIPNum(ipNum) 214 | 215 | // Check if the country was found 216 | if gi.dbType == dbCountryEdition && offset-countryBegin == 0 || 217 | gi.dbType != dbCountryEdition && gi.databaseSegment == offset { 218 | return nil 219 | } 220 | 221 | // Create a generic location structure 222 | location := new(Location) 223 | 224 | // If the database is country 225 | if gi.dbType == dbCountryEdition { 226 | location.CountryCode = countryCode[offset-countryBegin] 227 | location.CountryName = countryName[offset-countryBegin] 228 | 229 | return location 230 | } 231 | 232 | // Find the max record length 233 | recPointer := offset + (2*gi.recordLength-1)*gi.databaseSegment 234 | recordEnd := recPointer + fullRecordLength 235 | if len(gi.data)-recPointer < fullRecordLength { 236 | recordEnd = len(gi.data) 237 | } 238 | 239 | // Read the country code/name first 240 | location.CountryCode = countryCode[gi.data[recPointer]] 241 | location.CountryName = countryName[gi.data[recPointer]] 242 | readLen := 1 243 | recPointer += 1 244 | 245 | // Get the region 246 | for readLen = 0; gi.data[recPointer+readLen] != '\000' && 247 | recPointer+readLen < recordEnd; readLen++ { 248 | } 249 | if readLen != 0 { 250 | location.Region = string(gi.data[recPointer : recPointer+readLen]) 251 | } 252 | recPointer += readLen + 1 253 | 254 | // Get the city 255 | for readLen = 0; gi.data[recPointer+readLen] != '\000' && 256 | recPointer+readLen < recordEnd; readLen++ { 257 | } 258 | if readLen != 0 { 259 | location.City = string(gi.data[recPointer : recPointer+readLen]) 260 | } 261 | recPointer += readLen + 1 262 | 263 | // Get the postal code 264 | for readLen = 0; gi.data[recPointer+readLen] != '\000' && 265 | recPointer+readLen < recordEnd; readLen++ { 266 | } 267 | if readLen != 0 { 268 | location.PostalCode = string(gi.data[recPointer : recPointer+readLen]) 269 | } 270 | recPointer += readLen + 1 271 | 272 | // Get the latitude 273 | coordinate := float32(0) 274 | for j := 0; j < 3; j++ { 275 | coordinate += float32(int32(gi.data[recPointer+j]) << uint8(j*8)) 276 | } 277 | location.Latitude = float32(coordinate/10000 - 180) 278 | recPointer += 3 279 | 280 | // Get the longitude 281 | coordinate = 0 282 | for j := 0; j < 3; j++ { 283 | coordinate += float32(int32(gi.data[recPointer+j]) << uint8(j*8)) 284 | } 285 | location.Longitude = float32(coordinate/10000 - 180) 286 | 287 | return location 288 | } 289 | 290 | // Read the database and return record position 291 | func (gi *GeoIP) lookupByIPNum(ip uint32) int { 292 | buf := make([]byte, 2*maxRecordLength) 293 | x := make([]int, 2) 294 | offset := 0 295 | for depth := 31; depth >= 0; depth-- { 296 | for i := 0; i < 2*maxRecordLength; i++ { 297 | buf[i] = gi.data[(2*gi.recordLength*offset)+i] 298 | } 299 | for i := 0; i < 2; i++ { 300 | x[i] = 0 301 | for j := 0; j < gi.recordLength; j++ { 302 | var y int = int(buf[i*gi.recordLength+j]) 303 | if y < 0 { 304 | y += 256 305 | } 306 | x[i] += (y << uint(j*8)) 307 | } 308 | } 309 | if (ip & (1 << uint(depth))) > 0 { 310 | if x[1] >= gi.databaseSegment { 311 | return x[1] 312 | } 313 | offset = x[1] 314 | } else { 315 | if x[0] >= gi.databaseSegment { 316 | return x[0] 317 | } 318 | offset = x[0] 319 | } 320 | } 321 | return 0 322 | } 323 | 324 | func addrToNum(ip string) uint32 { 325 | i := net.ParseIP(ip) 326 | if i == nil { 327 | return uint32(0) 328 | } 329 | i = i.To4() 330 | if i == nil { 331 | return uint32(0) 332 | } 333 | return binary.BigEndian.Uint32(i) 334 | } 335 | --------------------------------------------------------------------------------