Add support for city lookup

This commit is contained in:
Martin Polden 2016-04-17 11:28:47 +02:00
parent 8027c55fdf
commit 8f71576357
7 changed files with 77 additions and 11 deletions

6
.gitignore vendored
View File

@ -1,4 +1,2 @@
bin/ /GeoLite2-Country.mmdb
pkg/ /GeoLite2-City.mmdb
src/
/GeoLite2-Country.mmdb

View File

@ -15,5 +15,6 @@ deps:
install: install:
go 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-Country.mmdb.gz | gunzip > GeoLite2-Country.mmdb
curl -s http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz | gunzip > GeoLite2-City.mmdb

View File

@ -32,6 +32,7 @@ type API struct {
type Response struct { type Response struct {
IP net.IP `json:"ip"` IP net.IP `json:"ip"`
Country string `json:"country,omitempty"` Country string `json:"country,omitempty"`
City string `json:"city,omitempty"`
Hostname string `json:"hostname,omitempty"` Hostname string `json:"hostname,omitempty"`
} }
@ -73,6 +74,10 @@ func (a *API) newResponse(r *http.Request) (Response, error) {
if err != nil { if err != nil {
log.Print(err) log.Print(err)
} }
city, err := a.oracle.LookupCity(ip)
if err != nil {
log.Print(err)
}
hostnames, err := a.oracle.LookupAddr(ip.String()) hostnames, err := a.oracle.LookupAddr(ip.String())
if err != nil { if err != nil {
log.Print(err) log.Print(err)
@ -80,6 +85,7 @@ func (a *API) newResponse(r *http.Request) (Response, error) {
return Response{ return Response{
IP: ip, IP: ip,
Country: country, Country: country,
City: city,
Hostname: strings.Join(hostnames, " "), Hostname: strings.Join(hostnames, " "),
}, nil }, nil
} }
@ -102,6 +108,15 @@ func (a *API) CLICountryHandler(w http.ResponseWriter, r *http.Request) *appErro
return nil 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 { func (a *API) JSONHandler(w http.ResponseWriter, r *http.Request) *appError {
response, err := a.newResponse(r) response, err := a.newResponse(r)
if err != nil { if err != nil {
@ -223,6 +238,7 @@ func (a *API) Handlers() http.Handler {
r.Handle("/", appHandler(a.CLIHandler)).Methods("GET").MatcherFunc(cliMatcher) r.Handle("/", appHandler(a.CLIHandler)).Methods("GET").MatcherFunc(cliMatcher)
r.Handle("/ip", 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("/country", appHandler(a.CLICountryHandler)).Methods("GET").MatcherFunc(cliMatcher)
r.Handle("/city", appHandler(a.CLICityHandler)).Methods("GET").MatcherFunc(cliMatcher)
// Browser // Browser
r.Handle("/", appHandler(a.DefaultHandler)).Methods("GET") r.Handle("/", appHandler(a.DefaultHandler)).Methods("GET")

View File

@ -13,9 +13,11 @@ type mockOracle struct{}
func (r *mockOracle) LookupAddr(string) ([]string, error) { return []string{"localhost"}, nil } 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) 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) LookupPort(net.IP, uint64) error { return nil }
func (r *mockOracle) IsLookupAddrEnabled() bool { return true } func (r *mockOracle) IsLookupAddrEnabled() bool { return true }
func (r *mockOracle) IsLookupCountryEnabled() 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 (r *mockOracle) IsLookupPortEnabled() bool { return true }
func newTestAPI() *API { func newTestAPI() *API {
@ -60,6 +62,7 @@ func TestClIHandlers(t *testing.T) {
{s.URL, "127.0.0.1\n", 200}, {s.URL, "127.0.0.1\n", 200},
{s.URL + "/ip", "127.0.0.1\n", 200}, {s.URL + "/ip", "127.0.0.1\n", 200},
{s.URL + "/country", "Elbonia\n", 200}, {s.URL + "/country", "Elbonia\n", 200},
{s.URL + "/city", "Bornyasherk\n", 200},
{s.URL + "/foo", "404 page not found", 404}, {s.URL + "/foo", "404 page not found", 404},
} }
@ -86,7 +89,7 @@ func TestJSONHandlers(t *testing.T) {
out string out string
status int 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 + "/port/31337", `{"ip":"127.0.0.1","port":31337,"reachable":true}`, 200},
{s.URL + "/foo", `{"error":"404 page not found"}`, 404}, {s.URL + "/foo", `{"error":"404 page not found"}`, 404},
} }

View File

@ -11,18 +11,22 @@ import (
type Oracle interface { type Oracle interface {
LookupAddr(string) ([]string, error) LookupAddr(string) ([]string, error)
LookupCountry(net.IP) (string, error) LookupCountry(net.IP) (string, error)
LookupCity(net.IP) (string, error)
LookupPort(net.IP, uint64) error LookupPort(net.IP, uint64) error
IsLookupAddrEnabled() bool IsLookupAddrEnabled() bool
IsLookupCountryEnabled() bool IsLookupCountryEnabled() bool
IsLookupCityEnabled() bool
IsLookupPortEnabled() bool IsLookupPortEnabled() bool
} }
type DefaultOracle struct { type DefaultOracle struct {
lookupAddr func(string) ([]string, error) lookupAddr func(string) ([]string, error)
lookupCountry func(net.IP) (string, error) lookupCountry func(net.IP) (string, error)
lookupCity func(net.IP) (string, error)
lookupPort func(net.IP, uint64) error lookupPort func(net.IP, uint64) error
lookupAddrEnabled bool lookupAddrEnabled bool
lookupCountryEnabled bool lookupCountryEnabled bool
lookupCityEnabled bool
lookupPortEnabled bool lookupPortEnabled bool
} }
@ -30,6 +34,7 @@ func NewOracle() *DefaultOracle {
return &DefaultOracle{ return &DefaultOracle{
lookupAddr: func(string) ([]string, error) { return nil, nil }, lookupAddr: func(string) ([]string, error) { return nil, nil },
lookupCountry: func(net.IP) (string, error) { return "", 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 }, 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) 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 { func (r *DefaultOracle) LookupPort(ip net.IP, port uint64) error {
return r.lookupPort(ip, port) return r.lookupPort(ip, port)
} }
@ -63,6 +72,18 @@ func (r *DefaultOracle) EnableLookupCountry(filepath string) error {
return nil 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() { func (r *DefaultOracle) EnableLookupPort() {
r.lookupPort = lookupPort r.lookupPort = lookupPort
r.lookupPortEnabled = true r.lookupPortEnabled = true
@ -70,6 +91,7 @@ func (r *DefaultOracle) EnableLookupPort() {
func (r *DefaultOracle) IsLookupAddrEnabled() bool { return r.lookupAddrEnabled } func (r *DefaultOracle) IsLookupAddrEnabled() bool { return r.lookupAddrEnabled }
func (r *DefaultOracle) IsLookupCountryEnabled() bool { return r.lookupCountryEnabled } 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 (r *DefaultOracle) IsLookupPortEnabled() bool { return r.lookupPortEnabled }
func lookupPort(ip net.IP, port uint64) error { 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) 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)
}

View File

@ -66,6 +66,13 @@ $ fetch -qo- http://ifconfig.co
$ http ifconfig.co/country $ http ifconfig.co/country
{{ .Country }} {{ .Country }}
</pre> </pre>
{{ end }}
{{ if .IsLookupCityEnabled }}
<p>City lookup:</p>
<pre>
$ http ifconfig.co/city
{{ .City }}
</pre>
{{ end }} {{ end }}
</div> </div>
<div class="pure-u-1 pure-u-md-1-2"> <div class="pure-u-1 pure-u-md-1-2">
@ -78,7 +85,8 @@ Content-Type: application/json
Date: Fri, 15 Apr 2016 17:26:53 GMT Date: Fri, 15 Apr 2016 17:26:53 GMT
{ {{ if .IsLookupCountryEnabled }} { {{ if .IsLookupCountryEnabled }}
"country": "{{ .Country }}",{{ end }}{{ if .IsLookupAddrEnabled }} "country": "{{ .Country }}",{{ end }}{{ if .IsLookupCityEnabled }}
"city": "{{ .City }}",{{ end }}{{ if .IsLookupAddrEnabled }}
"hostname": "{{ .Hostname }}",{{ end }} "hostname": "{{ .Hostname }}",{{ end }}
"ip": "{{ .IP }}" "ip": "{{ .IP }}"
} }

15
main.go
View File

@ -11,7 +11,8 @@ import (
func main() { func main() {
var opts struct { 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"` 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"` CORS bool `short:"x" long:"cors" description:"Allow requests from other domains"`
ReverseLookup bool `short:"r" long:"reverse-lookup" description:"Perform reverse hostname lookups"` ReverseLookup bool `short:"r" long:"reverse-lookup" description:"Perform reverse hostname lookups"`
@ -32,9 +33,15 @@ func main() {
log.Println("Enabling port lookup") log.Println("Enabling port lookup")
oracle.EnableLookupPort() oracle.EnableLookupPort()
} }
if opts.DBPath != "" { if opts.CountryDBPath != "" {
log.Printf("Enabling country lookup (using database: %s)", opts.DBPath) log.Printf("Enabling country lookup (using database: %s)", opts.CountryDBPath)
if err := oracle.EnableLookupCountry(opts.DBPath); err != nil { 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) log.Fatal(err)
} }
} }