diff --git a/.travis.yml b/.travis.yml index 4c1fd43..f8abdfc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,16 +1,13 @@ language: go go: - - 1.3 - - 1.4 - - tip + - 1.11 + - 1.12 before_install: - sudo apt-get install libgeoip-dev bzr install: - - mkdir -p $TRAVIS_BUILD_DIR/db - - curl http://geodns.bitnames.com/geoip/GeoLiteCity.dat.gz | gzip -cd > $TRAVIS_BUILD_DIR/db/GeoLiteCity.dat - go get gopkg.in/check.v1 script: diff --git a/db/download b/db/download deleted file mode 100755 index bbe8341..0000000 --- a/db/download +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env perl -use strict; -use v5.12.0; -use LWP::Simple qw(mirror); -use File::Basename qw(basename); - -my @files = qw( - http://geolite.maxmind.com/download/geoip/database/GeoLiteCountry/GeoIP.dat.gz - http://geolite.maxmind.com/download/geoip/database/GeoIPv6.dat.gz - http://geolite.maxmind.com/download/geoip/database/GeoLiteCity.dat.gz - http://geolite.maxmind.com/download/geoip/database/GeoLiteCityv6-beta/GeoLiteCityv6.dat.gz - http://download.maxmind.com/download/geoip/database/asnum/GeoIPASNum.dat.gz - http://download.maxmind.com/download/geoip/database/asnum/GeoIPASNumv6.dat.gz -); - -for my $url (@files) { - my $file = basename($url); - my ($dat_name) = ($file =~ m/(.*)\.gz/); - my $rv = mirror($url, $file); - if ($rv == 200) { - system("gzip -cd $file > $dat_name"); - } - elsif ($rv == 304) { - # already updated - } - else { - say "$url:", $rv; - } -} diff --git a/geoip.go b/geoip.go index 4d264fd..c49a91e 100644 --- a/geoip.go +++ b/geoip.go @@ -2,7 +2,7 @@ package geoip /* -#cgo pkg-config: geoip +#cgo pkg-config: geoip #include #include #include @@ -15,6 +15,7 @@ import "C" import ( "fmt" "log" + "net" "os" "runtime" "sync" @@ -139,6 +140,13 @@ func OpenType(dbType int) (*GeoIP, error) { return OpenTypeFlag(dbType, GEOIP_MEMORY_CACHE) } +// DatabaseType returns the database type. +func (gi *GeoIP) DatabaseType() int { + gi.mu.Lock() + defer gi.mu.Unlock() + return int(gi.db.databaseType) +} + // Takes an IPv4 address string and returns the organization name for that IP. // Requires the GeoIP organization database. func (gi *GeoIP) GetOrg(ip string) string { @@ -161,10 +169,11 @@ func (gi *GeoIP) GetName(ip string) (name string, netmask int) { defer C.free(unsafe.Pointer(cip)) cname := C.GeoIP_name_by_addr(gi.db, cip) + netmask = int(C.GeoIP_last_netmask(gi.db)) + if cname != nil { name = C.GoString(cname) defer C.free(unsafe.Pointer(cname)) - netmask = int(C.GeoIP_last_netmask(gi.db)) return } return @@ -183,27 +192,76 @@ type GeoIPRecord struct { AreaCode int CharSet int ContinentCode string + Netmask int +} + +// CityResult holds the result of looking up an IP in a City type database. The +// lookup may be from either an IPv4 or IPv6 database. +type CityResult struct { + Record *GeoIPRecord + Netmask int +} + +// LookupIPv4City looks up the IP in the database. The database must be an IPv4 +// City database. +// +// This is the same as GetRecord() except it also provides information when the +// IP is not in the database. Specifically, the netmask. The netmask is useful +// for iterating the database. +func (gi *GeoIP) LookupIPv4City(ipString string) (CityResult, error) { + return gi.lookupIPv4City(ipString) } // Returns the "City Record" for an IP address. Requires the GeoCity(Lite) // database - http://www.maxmind.com/en/city -func (gi *GeoIP) GetRecord(ip string) *GeoIPRecord { - if gi.db == nil { - return nil +func (gi *GeoIP) GetRecord(ipString string) *GeoIPRecord { + // We could return whether there was an error, but I'm trying to not change + // the API. + result, _ := gi.lookupIPv4City(ipString) + return result.Record +} + +func (gi *GeoIP) lookupIPv4City(ipString string) (CityResult, error) { + if gi == nil || gi.db == nil { + return CityResult{}, fmt.Errorf("database is not loaded") } - cip := C.CString(ip) + if gi.db.databaseType != GEOIP_CITY_EDITION_REV0 && + gi.db.databaseType != GEOIP_CITY_EDITION_REV1 { + return CityResult{}, fmt.Errorf("invalid database type") + } + + ip := net.ParseIP(ipString) + if ip == nil { + return CityResult{}, fmt.Errorf("invalid IP address") + } + + // It's only valid to look up IPv4 IPs this way. + if ip := ip.To4(); ip == nil { + return CityResult{}, fmt.Errorf("IPv6 IP given for IPv4-only lookup") + } + + cip := C.CString(ipString) defer C.free(unsafe.Pointer(cip)) gi.mu.Lock() record := C.GeoIP_record_by_addr(gi.db, cip) + netmask := int(gi.db.netmask) gi.mu.Unlock() if record == nil { - return nil + return CityResult{Netmask: netmask}, nil } - // defer C.free(unsafe.Pointer(record)) + defer C.GeoIPRecord_delete(record) + + return CityResult{ + Record: gi.convertRecord(record), + Netmask: netmask, + }, nil +} + +func (gi *GeoIP) convertRecord(record *C.GeoIPRecord) *GeoIPRecord { rec := new(GeoIPRecord) rec.CountryCode = C.GoString(record.country_code) rec.CountryCode3 = C.GoString(record.country_code3) @@ -226,9 +284,53 @@ func (gi *GeoIP) GetRecord(ip string) *GeoIPRecord { rec.AreaCode = int(record.area_code) } + rec.Netmask = int(record.netmask) + return rec } +// LookupIPv6City returns the city record for an IPv6 IP. This is only +// valid for the GeoLite City IPv6 database. +func (gi *GeoIP) LookupIPv6City(ipString string) (CityResult, error) { + if gi == nil || gi.db == nil { + return CityResult{}, fmt.Errorf("database is not loaded") + } + + if gi.db.databaseType != GEOIP_CITY_EDITION_REV0_V6 && + gi.db.databaseType != GEOIP_CITY_EDITION_REV1_V6 { + return CityResult{}, fmt.Errorf("invalid database type") + } + + ip := net.ParseIP(ipString) + if ip == nil { + return CityResult{}, fmt.Errorf("invalid IP address") + } + + // It's only valid to look up IPv6 IPs this way. + if ip := ip.To4(); ip != nil { + return CityResult{}, fmt.Errorf("IPv4 IP given for IPv6-only lookup") + } + + cip := C.CString(ipString) + defer C.free(unsafe.Pointer(cip)) + + gi.mu.Lock() + record := C.GeoIP_record_by_addr_v6(gi.db, cip) + netmask := int(gi.db.netmask) + gi.mu.Unlock() + + if record == nil { + return CityResult{Netmask: netmask}, nil + } + + defer C.GeoIPRecord_delete(record) + + return CityResult{ + Record: gi.convertRecord(record), + Netmask: netmask, + }, nil +} + // Returns the country code and region code for an IP address. Requires // the GeoIP Region database. func (gi *GeoIP) GetRegion(ip string) (string, string) { @@ -274,6 +376,25 @@ func GetRegionName(countryCode, regionCode string) string { return regionName } +// Returns the time zone given a country code and region code +func GetTimeZone(countryCode, regionCode string) string { + cc := C.CString(countryCode) + defer C.free(unsafe.Pointer(cc)) + + rc := C.CString(regionCode) + defer C.free(unsafe.Pointer(rc)) + + tz := C.GeoIP_time_zone_by_country_and_region(cc, rc) + if tz == nil { + return "" + } + + // it's a static string constant, don't free this + timeZone := C.GoString(tz) + + return timeZone +} + // Same as GetName() but for IPv6 addresses. func (gi *GeoIP) GetNameV6(ip string) (name string, netmask int) { if gi.db == nil { @@ -287,10 +408,11 @@ func (gi *GeoIP) GetNameV6(ip string) (name string, netmask int) { defer C.free(unsafe.Pointer(cip)) cname := C.GeoIP_name_by_addr_v6(gi.db, cip) + netmask = int(C.GeoIP_last_netmask(gi.db)) + if cname != nil { name = C.GoString(cname) defer C.free(unsafe.Pointer(cname)) - netmask = int(C.GeoIP_last_netmask(gi.db)) return } return @@ -338,3 +460,37 @@ func (gi *GeoIP) GetCountry_v6(ip string) (cc string, netmask int) { } return } + +// GetCountryNameFromCode returns the country name for the 2 letter country +// code. If there is no name for the specified code, the empty string will +// be returned. +func (gi *GeoIP) GetCountryNameFromCode(code string) string { + if gi.db == nil { + return "" + } + + gi.mu.Lock() + defer gi.mu.Unlock() + + ccode := C.CString(code) + defer C.free(unsafe.Pointer(ccode)) + + cid := C.GeoIP_id_by_code(ccode) + cname := C.GeoIP_country_name_by_id(gi.db, cid) + + return C.GoString(cname) +} + +// EnableTeredo enables Teredo support. It is on by default. +func (gi *GeoIP) EnableTeredo() { + gi.mu.Lock() + defer gi.mu.Unlock() + C.GeoIP_enable_teredo(gi.db, C.int(1)) +} + +// DisableTeredo disable Teredo support. It is on by default. +func (gi *GeoIP) DisableTeredo() { + gi.mu.Lock() + defer gi.mu.Unlock() + C.GeoIP_enable_teredo(gi.db, C.int(0)) +} diff --git a/geoip_test.go b/geoip_test.go index c8695c1..7d91acb 100644 --- a/geoip_test.go +++ b/geoip_test.go @@ -27,10 +27,15 @@ func (s *GeoIPSuite) Testv4(c *C) { country, netmask := gi.GetCountry("64.17.254.216") c.Check(country, Equals, "US") c.Check(netmask, Equals, 17) + c.Check(gi.GetCountryNameFromCode("US"), Equals, "United States") country, netmask = gi.GetCountry("222.230.136.0") c.Check(country, Equals, "JP") c.Check(netmask, Equals, 16) + c.Check(gi.GetCountryNameFromCode("JP"), Equals, "Japan") + + c.Check(gi.GetCountryNameFromCode(""), Equals, "") + c.Check(gi.GetCountryNameFromCode("invalid"), Equals, "") } func (s *GeoIPSuite) TestOpenType(c *C) { @@ -84,20 +89,133 @@ func (s *GeoIPSuite) Testv4Record(c *C) { MetroCode: 807, CharSet: 1, ContinentCode: "NA", + Netmask: 28, + }, + ) +} + +func (s *GeoIPSuite) TestGetRecordInvalidIP(c *C) { + gi, err := Open("test-db/GeoIPCity.dat") + if gi == nil || err != nil { + c.Fatalf("Could not open GeoIP database: %s", err) + return + } + + c.Check(gi, NotNil) + + record := gi.GetRecord("blah") + c.Check(record, IsNil) +} + +func (s *GeoIPSuite) TestLookupIPv4CityRecordFound(c *C) { + gi, err := Open("test-db/GeoIPCity.dat") + if gi == nil || err != nil { + c.Fatalf("Could not open GeoIP database: %s", err) + return + } + + c.Check(gi, NotNil) + + result, err := gi.LookupIPv4City("66.92.181.240") + c.Check(err, IsNil) + + c.Check(result.Netmask, Equals, 28) + c.Check(result.Record, NotNil) + + c.Check( + *result.Record, + Equals, + GeoIPRecord{ + CountryCode: "US", + CountryCode3: "USA", + CountryName: "United States", + Region: "CA", + City: "Fremont", + PostalCode: "94538", + Latitude: 37.5079, + Longitude: -121.96, + AreaCode: 510, + MetroCode: 807, + CharSet: 1, + ContinentCode: "NA", + Netmask: 28, }, ) } +func (s *GeoIPSuite) TestLookupIPv4CityRecordNotFound(c *C) { + gi, err := Open("test-db/GeoIPCity.dat") + if gi == nil || err != nil { + c.Fatalf("Could not open GeoIP database: %s", err) + return + } + + c.Check(gi, NotNil) + + result, err := gi.LookupIPv4City("0.0.0.0") + c.Check(err, IsNil) + + c.Check(result.Netmask, Equals, 5) + c.Check(result.Record, IsNil) +} + +func (s *GeoIPSuite) TestLookupIPv6CityRecordFound(c *C) { + gi, err := Open("test-db/GeoLiteCityv6.dat") + if gi == nil || err != nil { + c.Fatalf("Could not open GeoIP database: %s", err) + return + } + + c.Check(gi, NotNil) + + result, err := gi.LookupIPv6City("2001:200::") + c.Check(err, IsNil) + + c.Check(result.Netmask, Equals, 32) + c.Check(result.Record, NotNil) + + c.Check( + *result.Record, + Equals, + GeoIPRecord{ + CountryCode: "JP", + CountryCode3: "JPN", + CountryName: "Japan", + Latitude: 36, + Longitude: 138, + CharSet: 1, + ContinentCode: "AS", + Netmask: 32, + }, + ) +} + +func (s *GeoIPSuite) TestLookupIPv6CityRecordNotFound(c *C) { + gi, err := Open("test-db/GeoLiteCityv6.dat") + if gi == nil || err != nil { + c.Fatalf("Could not open GeoIP database: %s", err) + return + } + + c.Check(gi, NotNil) + + result, err := gi.LookupIPv6City("2a02:cf::") + c.Check(err, IsNil) + + c.Check(result.Netmask, Equals, 17) + c.Check(result.Record, IsNil) +} + func (s *GeoIPSuite) Benchmark_GetRecord(c *C) { - gi, err := Open("db/GeoLiteCity.dat") + gi, err := Open("test-db/GeoIPCity.dat") if gi == nil || err != nil { fmt.Printf("Could not open GeoIP database: %s\n", err) return } for i := 0; i < c.N; i++ { - record := gi.GetRecord("207.171.7.51") + record := gi.GetRecord("89.92.212.32") if record == nil { panic("") } diff --git a/test-db/GeoLiteCityv6.dat b/test-db/GeoLiteCityv6.dat new file mode 100644 index 0000000..647b6ed Binary files /dev/null and b/test-db/GeoLiteCityv6.dat differ