diff --git a/.gitignore b/.gitignore index f2fcde0..d5b53a6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,2 @@ -bin/ -pkg/ -src/ -/GeoLite2-Country.mmdb \ No newline at end of file +/GeoLite2-Country.mmdb +/GeoLite2-City.mmdb diff --git a/Makefile b/Makefile index b5907eb..b851e18 100644 --- a/Makefile +++ b/Makefile @@ -15,5 +15,6 @@ deps: install: go install -get-geoip-db: +get-geoip-dbs: curl -s http://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.mmdb.gz | gunzip > GeoLite2-Country.mmdb + curl -s http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz | gunzip > GeoLite2-City.mmdb diff --git a/api/api.go b/api/api.go index 870b783..de75b93 100644 --- a/api/api.go +++ b/api/api.go @@ -32,6 +32,7 @@ type API struct { type Response struct { IP net.IP `json:"ip"` Country string `json:"country,omitempty"` + City string `json:"city,omitempty"` Hostname string `json:"hostname,omitempty"` } @@ -73,6 +74,10 @@ func (a *API) newResponse(r *http.Request) (Response, error) { if err != nil { log.Print(err) } + city, err := a.oracle.LookupCity(ip) + if err != nil { + log.Print(err) + } hostnames, err := a.oracle.LookupAddr(ip.String()) if err != nil { log.Print(err) @@ -80,6 +85,7 @@ func (a *API) newResponse(r *http.Request) (Response, error) { return Response{ IP: ip, Country: country, + City: city, Hostname: strings.Join(hostnames, " "), }, nil } @@ -102,6 +108,15 @@ func (a *API) CLICountryHandler(w http.ResponseWriter, r *http.Request) *appErro return nil } +func (a *API) CLICityHandler(w http.ResponseWriter, r *http.Request) *appError { + response, err := a.newResponse(r) + if err != nil { + return internalServerError(err) + } + io.WriteString(w, response.City+"\n") + return nil +} + func (a *API) JSONHandler(w http.ResponseWriter, r *http.Request) *appError { response, err := a.newResponse(r) if err != nil { @@ -223,6 +238,7 @@ func (a *API) Handlers() http.Handler { r.Handle("/", appHandler(a.CLIHandler)).Methods("GET").MatcherFunc(cliMatcher) r.Handle("/ip", appHandler(a.CLIHandler)).Methods("GET").MatcherFunc(cliMatcher) r.Handle("/country", appHandler(a.CLICountryHandler)).Methods("GET").MatcherFunc(cliMatcher) + r.Handle("/city", appHandler(a.CLICityHandler)).Methods("GET").MatcherFunc(cliMatcher) // Browser r.Handle("/", appHandler(a.DefaultHandler)).Methods("GET") diff --git a/api/api_test.go b/api/api_test.go index 6be3000..245869a 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -13,9 +13,11 @@ type mockOracle struct{} func (r *mockOracle) LookupAddr(string) ([]string, error) { return []string{"localhost"}, nil } func (r *mockOracle) LookupCountry(net.IP) (string, error) { return "Elbonia", nil } +func (r *mockOracle) LookupCity(net.IP) (string, error) { return "Bornyasherk", nil } func (r *mockOracle) LookupPort(net.IP, uint64) error { return nil } func (r *mockOracle) IsLookupAddrEnabled() bool { return true } func (r *mockOracle) IsLookupCountryEnabled() bool { return true } +func (r *mockOracle) IsLookupCityEnabled() bool { return true } func (r *mockOracle) IsLookupPortEnabled() bool { return true } func newTestAPI() *API { @@ -60,6 +62,7 @@ func TestClIHandlers(t *testing.T) { {s.URL, "127.0.0.1\n", 200}, {s.URL + "/ip", "127.0.0.1\n", 200}, {s.URL + "/country", "Elbonia\n", 200}, + {s.URL + "/city", "Bornyasherk\n", 200}, {s.URL + "/foo", "404 page not found", 404}, } @@ -86,7 +89,7 @@ func TestJSONHandlers(t *testing.T) { out string status int }{ - {s.URL, `{"ip":"127.0.0.1","country":"Elbonia","hostname":"localhost"}`, 200}, + {s.URL, `{"ip":"127.0.0.1","country":"Elbonia","city":"Bornyasherk","hostname":"localhost"}`, 200}, {s.URL + "/port/31337", `{"ip":"127.0.0.1","port":31337,"reachable":true}`, 200}, {s.URL + "/foo", `{"error":"404 page not found"}`, 404}, } diff --git a/api/oracle.go b/api/oracle.go index b745049..c923b0c 100644 --- a/api/oracle.go +++ b/api/oracle.go @@ -11,18 +11,22 @@ import ( type Oracle interface { LookupAddr(string) ([]string, error) LookupCountry(net.IP) (string, error) + LookupCity(net.IP) (string, error) LookupPort(net.IP, uint64) error IsLookupAddrEnabled() bool IsLookupCountryEnabled() bool + IsLookupCityEnabled() bool IsLookupPortEnabled() bool } type DefaultOracle struct { lookupAddr func(string) ([]string, error) lookupCountry func(net.IP) (string, error) + lookupCity func(net.IP) (string, error) lookupPort func(net.IP, uint64) error lookupAddrEnabled bool lookupCountryEnabled bool + lookupCityEnabled bool lookupPortEnabled bool } @@ -30,6 +34,7 @@ func NewOracle() *DefaultOracle { return &DefaultOracle{ lookupAddr: func(string) ([]string, error) { return nil, nil }, lookupCountry: func(net.IP) (string, error) { return "", nil }, + lookupCity: func(net.IP) (string, error) { return "", nil }, lookupPort: func(net.IP, uint64) error { return nil }, } } @@ -42,6 +47,10 @@ func (r *DefaultOracle) LookupCountry(ip net.IP) (string, error) { return r.lookupCountry(ip) } +func (r *DefaultOracle) LookupCity(ip net.IP) (string, error) { + return r.lookupCity(ip) +} + func (r *DefaultOracle) LookupPort(ip net.IP, port uint64) error { return r.lookupPort(ip, port) } @@ -63,6 +72,18 @@ func (r *DefaultOracle) EnableLookupCountry(filepath string) error { return nil } +func (r *DefaultOracle) EnableLookupCity(filepath string) error { + db, err := geoip2.Open(filepath) + if err != nil { + return err + } + r.lookupCity = func(ip net.IP) (string, error) { + return lookupCity(db, ip) + } + r.lookupCityEnabled = true + return nil +} + func (r *DefaultOracle) EnableLookupPort() { r.lookupPort = lookupPort r.lookupPortEnabled = true @@ -70,6 +91,7 @@ func (r *DefaultOracle) EnableLookupPort() { func (r *DefaultOracle) IsLookupAddrEnabled() bool { return r.lookupAddrEnabled } func (r *DefaultOracle) IsLookupCountryEnabled() bool { return r.lookupCountryEnabled } +func (r *DefaultOracle) IsLookupCityEnabled() bool { return r.lookupCityEnabled } func (r *DefaultOracle) IsLookupPortEnabled() bool { return r.lookupPortEnabled } func lookupPort(ip net.IP, port uint64) error { @@ -95,3 +117,14 @@ func lookupCountry(db *geoip2.Reader, ip net.IP) (string, error) { } return "Unknown", fmt.Errorf("could not determine country for IP: %s", ip) } + +func lookupCity(db *geoip2.Reader, ip net.IP) (string, error) { + record, err := db.City(ip) + if err != nil { + return "", err + } + if city, exists := record.City.Names["en"]; exists { + return city, nil + } + return "Unknown", fmt.Errorf("could not determine city for IP: %s", ip) +} diff --git a/index.html b/index.html index c2be443..80005eb 100644 --- a/index.html +++ b/index.html @@ -66,6 +66,13 @@ $ fetch -qo- http://ifconfig.co $ http ifconfig.co/country {{ .Country }} +{{ end }} +{{ if .IsLookupCityEnabled }} +

City lookup:

+
+$ http ifconfig.co/city
+{{ .City }}
+        
{{ end }}
@@ -78,7 +85,8 @@ Content-Type: application/json Date: Fri, 15 Apr 2016 17:26:53 GMT { {{ if .IsLookupCountryEnabled }} - "country": "{{ .Country }}",{{ end }}{{ if .IsLookupAddrEnabled }} + "country": "{{ .Country }}",{{ end }}{{ if .IsLookupCityEnabled }} + "city": "{{ .City }}",{{ end }}{{ if .IsLookupAddrEnabled }} "hostname": "{{ .Hostname }}",{{ end }} "ip": "{{ .IP }}" } diff --git a/main.go b/main.go index 46b6e01..66d51ff 100644 --- a/main.go +++ b/main.go @@ -11,7 +11,8 @@ import ( func main() { var opts struct { - DBPath string `short:"f" long:"file" description:"Path to GeoIP database" value-name:"FILE" default:""` + CountryDBPath string `short:"f" long:"country-db" description:"Path to GeoIP country database" value-name:"FILE" default:""` + CityDBPath string `short:"c" long:"city-db" description:"Path to GeoIP city database" value-name:"FILE" default:""` Listen string `short:"l" long:"listen" description:"Listening address" value-name:"ADDR" default:":8080"` CORS bool `short:"x" long:"cors" description:"Allow requests from other domains"` ReverseLookup bool `short:"r" long:"reverse-lookup" description:"Perform reverse hostname lookups"` @@ -32,9 +33,15 @@ func main() { log.Println("Enabling port lookup") oracle.EnableLookupPort() } - if opts.DBPath != "" { - log.Printf("Enabling country lookup (using database: %s)", opts.DBPath) - if err := oracle.EnableLookupCountry(opts.DBPath); err != nil { + if opts.CountryDBPath != "" { + log.Printf("Enabling country lookup (using database: %s)", opts.CountryDBPath) + if err := oracle.EnableLookupCountry(opts.CountryDBPath); err != nil { + log.Fatal(err) + } + } + if opts.CityDBPath != "" { + log.Printf("Enabling city lookup (using database: %s)", opts.CityDBPath) + if err := oracle.EnableLookupCity(opts.CityDBPath); err != nil { log.Fatal(err) } }