mirror of https://github.com/mpolden/echoip
commit
09a47bea6a
|
@ -10,6 +10,9 @@ import (
|
||||||
"github.com/mpolden/echoip/http"
|
"github.com/mpolden/echoip/http"
|
||||||
"github.com/mpolden/echoip/iputil"
|
"github.com/mpolden/echoip/iputil"
|
||||||
"github.com/mpolden/echoip/iputil/geo"
|
"github.com/mpolden/echoip/iputil/geo"
|
||||||
|
"github.com/mpolden/echoip/iputil/ipstack"
|
||||||
|
parser "github.com/mpolden/echoip/iputil/paser"
|
||||||
|
ipstackApi "github.com/qioalice/ipstack"
|
||||||
)
|
)
|
||||||
|
|
||||||
type multiValueFlag []string
|
type multiValueFlag []string
|
||||||
|
@ -29,6 +32,9 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
var ipstackApiKey string
|
||||||
|
service := flag.String("d", "geoip", "Which database to use, 'ipstack' or 'geoip'")
|
||||||
|
flag.StringVar(&ipstackApiKey, "S", "", "IP Stack API Key")
|
||||||
countryFile := flag.String("f", "", "Path to GeoIP country database")
|
countryFile := flag.String("f", "", "Path to GeoIP country database")
|
||||||
cityFile := flag.String("c", "", "Path to GeoIP city database")
|
cityFile := flag.String("c", "", "Path to GeoIP city database")
|
||||||
asnFile := flag.String("a", "", "Path to GeoIP ASN database")
|
asnFile := flag.String("a", "", "Path to GeoIP ASN database")
|
||||||
|
@ -42,17 +48,31 @@ func main() {
|
||||||
var headers multiValueFlag
|
var headers multiValueFlag
|
||||||
flag.Var(&headers, "H", "Header to trust for remote IP, if present (e.g. X-Real-IP)")
|
flag.Var(&headers, "H", "Header to trust for remote IP, if present (e.g. X-Real-IP)")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if len(flag.Args()) != 0 {
|
if len(flag.Args()) != 0 {
|
||||||
flag.Usage()
|
flag.Usage()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
r, err := geo.Open(*countryFile, *cityFile, *asnFile)
|
var parser parser.Parser
|
||||||
|
if *service == "geoip" {
|
||||||
|
geo, err := geo.Open(*countryFile, *cityFile, *asnFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
parser = &geo
|
||||||
|
}
|
||||||
|
|
||||||
|
if *service == "ipstack" {
|
||||||
|
if err := ipstackApi.Init(ipstackApiKey); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
ips := ipstack.IPStack{}
|
||||||
|
parser = &ips
|
||||||
|
}
|
||||||
|
|
||||||
cache := http.NewCache(*cacheSize)
|
cache := http.NewCache(*cacheSize)
|
||||||
server := http.New(r, cache, *profile)
|
server := http.New(parser, cache, *profile)
|
||||||
server.IPHeaders = headers
|
server.IPHeaders = headers
|
||||||
if _, err := os.Stat(*template); err == nil {
|
if _, err := os.Stat(*template); err == nil {
|
||||||
server.Template = *template
|
server.Template = *template
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -4,5 +4,6 @@ go 1.13
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/oschwald/geoip2-golang v1.5.0
|
github.com/oschwald/geoip2-golang v1.5.0
|
||||||
|
github.com/qioalice/ipstack v1.0.1 // indirect
|
||||||
golang.org/x/sys v0.0.0-20210223212115-eede4237b368 // indirect
|
golang.org/x/sys v0.0.0-20210223212115-eede4237b368 // indirect
|
||||||
)
|
)
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -6,6 +6,8 @@ github.com/oschwald/maxminddb-golang v1.8.0 h1:Uh/DSnGoxsyp/KYbY1AuP0tYEwfs0sCph
|
||||||
github.com/oschwald/maxminddb-golang v1.8.0/go.mod h1:RXZtst0N6+FY/3qCNmZMBApR19cdQj43/NM9VkrNAis=
|
github.com/oschwald/maxminddb-golang v1.8.0/go.mod h1:RXZtst0N6+FY/3qCNmZMBApR19cdQj43/NM9VkrNAis=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/qioalice/ipstack v1.0.1 h1:Ync2O+tR2AH9/TzTg4aeJxd9c1Xz2CeH1joECwnhvms=
|
||||||
|
github.com/qioalice/ipstack v1.0.1/go.mod h1:6eB9LdNCUdUoOsfDB8Pn2GpmD2I+f2k3yR30ceuf/rY=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
|
|
|
@ -168,13 +168,18 @@
|
||||||
</tr>
|
</tr>
|
||||||
{{ end }} {{ end }}
|
{{ end }} {{ end }}
|
||||||
</table>
|
</table>
|
||||||
{{ if .Country }}
|
{{ if .Country }} {{ if .UsingGeoIP }}
|
||||||
<p>
|
<p>
|
||||||
This information is provided from the GeoLite2 database created by
|
This information is provided from the GeoLite2 database created by
|
||||||
MaxMind, available from
|
MaxMind, available from
|
||||||
<a href="https://www.maxmind.com">www.maxmind.com</a>
|
<a href="https://www.maxmind.com">www.maxmind.com</a>
|
||||||
</p>
|
</p>
|
||||||
{{ end }} {{ if .Latitude }}
|
{{ end}} {{ if .UsingIPStack }}
|
||||||
|
<p>
|
||||||
|
This information is provided by
|
||||||
|
<a href="https://www.ipstack.com">www.ipstack.com</a>
|
||||||
|
</p>
|
||||||
|
{{ end}} {{ end }} {{ if .Latitude }}
|
||||||
<div class="pure-u-1 pure-u-md-1-1">
|
<div class="pure-u-1 pure-u-md-1-1">
|
||||||
<h2>Map</h2>
|
<h2>Map</h2>
|
||||||
<iframe
|
<iframe
|
||||||
|
|
|
@ -6,6 +6,8 @@ import (
|
||||||
"hash/fnv"
|
"hash/fnv"
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
parser "github.com/mpolden/echoip/iputil/paser"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Cache struct {
|
type Cache struct {
|
||||||
|
@ -39,7 +41,7 @@ func key(ip net.IP) uint64 {
|
||||||
return h.Sum64()
|
return h.Sum64()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cache) Set(ip net.IP, resp Response) {
|
func (c *Cache) Set(ip net.IP, resp parser.Response) {
|
||||||
if c.capacity == 0 {
|
if c.capacity == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -50,7 +52,7 @@ func (c *Cache) Set(ip net.IP, resp Response) {
|
||||||
if minEvictions > 0 { // At or above capacity. Shrink the cache
|
if minEvictions > 0 { // At or above capacity. Shrink the cache
|
||||||
evicted := 0
|
evicted := 0
|
||||||
for el := c.values.Front(); el != nil && evicted < minEvictions; {
|
for el := c.values.Front(); el != nil && evicted < minEvictions; {
|
||||||
value := el.Value.(Response)
|
value := el.Value.(parser.Response)
|
||||||
delete(c.entries, key(value.IP))
|
delete(c.entries, key(value.IP))
|
||||||
next := el.Next()
|
next := el.Next()
|
||||||
c.values.Remove(el)
|
c.values.Remove(el)
|
||||||
|
@ -66,15 +68,15 @@ func (c *Cache) Set(ip net.IP, resp Response) {
|
||||||
c.entries[k] = c.values.PushBack(resp)
|
c.entries[k] = c.values.PushBack(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cache) Get(ip net.IP) (Response, bool) {
|
func (c *Cache) Get(ip net.IP) (parser.Response, bool) {
|
||||||
k := key(ip)
|
k := key(ip)
|
||||||
c.mu.RLock()
|
c.mu.RLock()
|
||||||
defer c.mu.RUnlock()
|
defer c.mu.RUnlock()
|
||||||
r, ok := c.entries[k]
|
r, ok := c.entries[k]
|
||||||
if !ok {
|
if !ok {
|
||||||
return Response{}, false
|
return parser.Response{}, false
|
||||||
}
|
}
|
||||||
return r.Value.(Response), true
|
return r.Value.(parser.Response), true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cache) Resize(capacity int) error {
|
func (c *Cache) Resize(capacity int) error {
|
||||||
|
|
|
@ -4,6 +4,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
parser "github.com/mpolden/echoip/iputil/paser"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCacheCapacity(t *testing.T) {
|
func TestCacheCapacity(t *testing.T) {
|
||||||
|
@ -19,10 +21,10 @@ func TestCacheCapacity(t *testing.T) {
|
||||||
}
|
}
|
||||||
for i, tt := range tests {
|
for i, tt := range tests {
|
||||||
c := NewCache(tt.capacity)
|
c := NewCache(tt.capacity)
|
||||||
var responses []Response
|
var responses []parser.Response
|
||||||
for i := 0; i < tt.addCount; i++ {
|
for i := 0; i < tt.addCount; i++ {
|
||||||
ip := net.ParseIP(fmt.Sprintf("192.0.2.%d", i))
|
ip := net.ParseIP(fmt.Sprintf("192.0.2.%d", i))
|
||||||
r := Response{IP: ip}
|
r := parser.Response{IP: ip}
|
||||||
responses = append(responses, r)
|
responses = append(responses, r)
|
||||||
c.Set(ip, r)
|
c.Set(ip, r)
|
||||||
}
|
}
|
||||||
|
@ -48,7 +50,7 @@ func TestCacheCapacity(t *testing.T) {
|
||||||
func TestCacheDuplicate(t *testing.T) {
|
func TestCacheDuplicate(t *testing.T) {
|
||||||
c := NewCache(10)
|
c := NewCache(10)
|
||||||
ip := net.ParseIP("192.0.2.1")
|
ip := net.ParseIP("192.0.2.1")
|
||||||
response := Response{IP: ip}
|
response := parser.Response{IP: ip}
|
||||||
c.Set(ip, response)
|
c.Set(ip, response)
|
||||||
c.Set(ip, response)
|
c.Set(ip, response)
|
||||||
want := 1
|
want := 1
|
||||||
|
@ -64,7 +66,7 @@ func TestCacheResize(t *testing.T) {
|
||||||
c := NewCache(10)
|
c := NewCache(10)
|
||||||
for i := 1; i <= 20; i++ {
|
for i := 1; i <= 20; i++ {
|
||||||
ip := net.ParseIP(fmt.Sprintf("192.0.2.%d", i))
|
ip := net.ParseIP(fmt.Sprintf("192.0.2.%d", i))
|
||||||
r := Response{IP: ip}
|
r := parser.Response{IP: ip}
|
||||||
c.Set(ip, r)
|
c.Set(ip, r)
|
||||||
}
|
}
|
||||||
if got, want := len(c.entries), 10; got != want {
|
if got, want := len(c.entries), 10; got != want {
|
||||||
|
@ -79,7 +81,7 @@ func TestCacheResize(t *testing.T) {
|
||||||
if got, want := c.evictions, uint64(0); got != want {
|
if got, want := c.evictions, uint64(0); got != want {
|
||||||
t.Errorf("want %d evictions, got %d", want, got)
|
t.Errorf("want %d evictions, got %d", want, got)
|
||||||
}
|
}
|
||||||
r := Response{IP: net.ParseIP("192.0.2.42")}
|
r := parser.Response{IP: net.ParseIP("192.0.2.42")}
|
||||||
c.Set(r.IP, r)
|
c.Set(r.IP, r)
|
||||||
if got, want := len(c.entries), 5; got != want {
|
if got, want := len(c.entries), 5; got != want {
|
||||||
t.Errorf("want %d entries, got %d", want, got)
|
t.Errorf("want %d entries, got %d", want, got)
|
||||||
|
|
68
http/http.go
68
http/http.go
|
@ -11,11 +11,9 @@ import (
|
||||||
|
|
||||||
"net/http/pprof"
|
"net/http/pprof"
|
||||||
|
|
||||||
"github.com/mpolden/echoip/iputil"
|
parser "github.com/mpolden/echoip/iputil/paser"
|
||||||
"github.com/mpolden/echoip/iputil/geo"
|
|
||||||
"github.com/mpolden/echoip/useragent"
|
"github.com/mpolden/echoip/useragent"
|
||||||
|
|
||||||
"math/big"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -32,39 +30,19 @@ type Server struct {
|
||||||
LookupAddr func(net.IP) (string, error)
|
LookupAddr func(net.IP) (string, error)
|
||||||
LookupPort func(net.IP, uint64) error
|
LookupPort func(net.IP, uint64) error
|
||||||
cache *Cache
|
cache *Cache
|
||||||
gr geo.Reader
|
parser parser.Parser
|
||||||
profile bool
|
profile bool
|
||||||
Sponsor bool
|
Sponsor bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type Response struct {
|
|
||||||
IP net.IP `json:"ip"`
|
|
||||||
IPDecimal *big.Int `json:"ip_decimal"`
|
|
||||||
Country string `json:"country,omitempty"`
|
|
||||||
CountryISO string `json:"country_iso,omitempty"`
|
|
||||||
CountryEU *bool `json:"country_eu,omitempty"`
|
|
||||||
RegionName string `json:"region_name,omitempty"`
|
|
||||||
RegionCode string `json:"region_code,omitempty"`
|
|
||||||
MetroCode uint `json:"metro_code,omitempty"`
|
|
||||||
PostalCode string `json:"zip_code,omitempty"`
|
|
||||||
City string `json:"city,omitempty"`
|
|
||||||
Latitude float64 `json:"latitude,omitempty"`
|
|
||||||
Longitude float64 `json:"longitude,omitempty"`
|
|
||||||
Timezone string `json:"time_zone,omitempty"`
|
|
||||||
ASN string `json:"asn,omitempty"`
|
|
||||||
ASNOrg string `json:"asn_org,omitempty"`
|
|
||||||
Hostname string `json:"hostname,omitempty"`
|
|
||||||
UserAgent *useragent.UserAgent `json:"user_agent,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PortResponse struct {
|
type PortResponse struct {
|
||||||
IP net.IP `json:"ip"`
|
IP net.IP `json:"ip"`
|
||||||
Port uint64 `json:"port"`
|
Port uint64 `json:"port"`
|
||||||
Reachable bool `json:"reachable"`
|
Reachable bool `json:"reachable"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(db geo.Reader, cache *Cache, profile bool) *Server {
|
func New(parser parser.Parser, cache *Cache, profile bool) *Server {
|
||||||
return &Server{cache: cache, gr: db, profile: profile}
|
return &Server{cache: cache, parser: parser, profile: profile}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ipFromForwardedForHeader(v string) string {
|
func ipFromForwardedForHeader(v string) string {
|
||||||
|
@ -122,10 +100,10 @@ func userAgentFromRequest(r *http.Request) *useragent.UserAgent {
|
||||||
return userAgent
|
return userAgent
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) newResponse(r *http.Request) (Response, error) {
|
func (s *Server) newResponse(r *http.Request) (parser.Response, error) {
|
||||||
ip, err := ipFromRequest(s.IPHeaders, r, true)
|
ip, err := ipFromRequest(s.IPHeaders, r, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Response{}, err
|
return parser.Response{}, err
|
||||||
}
|
}
|
||||||
response, ok := s.cache.Get(ip)
|
response, ok := s.cache.Get(ip)
|
||||||
if ok {
|
if ok {
|
||||||
|
@ -133,36 +111,12 @@ func (s *Server) newResponse(r *http.Request) (Response, error) {
|
||||||
response.UserAgent = userAgentFromRequest(r)
|
response.UserAgent = userAgentFromRequest(r)
|
||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
ipDecimal := iputil.ToDecimal(ip)
|
|
||||||
country, _ := s.gr.Country(ip)
|
|
||||||
city, _ := s.gr.City(ip)
|
|
||||||
asn, _ := s.gr.ASN(ip)
|
|
||||||
var hostname string
|
var hostname string
|
||||||
if s.LookupAddr != nil {
|
if s.LookupAddr != nil {
|
||||||
hostname, _ = s.LookupAddr(ip)
|
hostname, _ = s.LookupAddr(ip)
|
||||||
}
|
}
|
||||||
var autonomousSystemNumber string
|
|
||||||
if asn.AutonomousSystemNumber > 0 {
|
response, err = s.parser.Parse(ip, hostname)
|
||||||
autonomousSystemNumber = fmt.Sprintf("AS%d", asn.AutonomousSystemNumber)
|
|
||||||
}
|
|
||||||
response = Response{
|
|
||||||
IP: ip,
|
|
||||||
IPDecimal: ipDecimal,
|
|
||||||
Country: country.Name,
|
|
||||||
CountryISO: country.ISO,
|
|
||||||
CountryEU: country.IsEU,
|
|
||||||
RegionName: city.RegionName,
|
|
||||||
RegionCode: city.RegionCode,
|
|
||||||
MetroCode: city.MetroCode,
|
|
||||||
PostalCode: city.PostalCode,
|
|
||||||
City: city.Name,
|
|
||||||
Latitude: city.Latitude,
|
|
||||||
Longitude: city.Longitude,
|
|
||||||
Timezone: city.Timezone,
|
|
||||||
ASN: autonomousSystemNumber,
|
|
||||||
ASNOrg: asn.AutonomousSystemOrganization,
|
|
||||||
Hostname: hostname,
|
|
||||||
}
|
|
||||||
s.cache.Set(ip, response)
|
s.cache.Set(ip, response)
|
||||||
response.UserAgent = userAgentFromRequest(r)
|
response.UserAgent = userAgentFromRequest(r)
|
||||||
return response, nil
|
return response, nil
|
||||||
|
@ -342,7 +296,7 @@ func (s *Server) DefaultHandler(w http.ResponseWriter, r *http.Request) *appErro
|
||||||
}
|
}
|
||||||
|
|
||||||
var data = struct {
|
var data = struct {
|
||||||
Response
|
parser.Response
|
||||||
Host string
|
Host string
|
||||||
BoxLatTop float64
|
BoxLatTop float64
|
||||||
BoxLatBottom float64
|
BoxLatBottom float64
|
||||||
|
@ -362,6 +316,7 @@ func (s *Server) DefaultHandler(w http.ResponseWriter, r *http.Request) *appErro
|
||||||
s.LookupPort != nil,
|
s.LookupPort != nil,
|
||||||
s.Sponsor,
|
s.Sponsor,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := t.Execute(w, &data); err != nil {
|
if err := t.Execute(w, &data); err != nil {
|
||||||
return internalServerError(err)
|
return internalServerError(err)
|
||||||
}
|
}
|
||||||
|
@ -434,7 +389,8 @@ func (s *Server) Handler() http.Handler {
|
||||||
r.Route("GET", "/", s.CLIHandler).MatcherFunc(cliMatcher)
|
r.Route("GET", "/", s.CLIHandler).MatcherFunc(cliMatcher)
|
||||||
r.Route("GET", "/", s.CLIHandler).Header("Accept", textMediaType)
|
r.Route("GET", "/", s.CLIHandler).Header("Accept", textMediaType)
|
||||||
r.Route("GET", "/ip", s.CLIHandler)
|
r.Route("GET", "/ip", s.CLIHandler)
|
||||||
if !s.gr.IsEmpty() {
|
|
||||||
|
if !s.parser.IsEmpty() {
|
||||||
r.Route("GET", "/country", s.CLICountryHandler)
|
r.Route("GET", "/country", s.CLICountryHandler)
|
||||||
r.Route("GET", "/country-iso", s.CLICountryISOHandler)
|
r.Route("GET", "/country-iso", s.CLICountryISOHandler)
|
||||||
r.Route("GET", "/city", s.CLICityHandler)
|
r.Route("GET", "/city", s.CLICityHandler)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
|
@ -10,7 +11,9 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mpolden/echoip/iputil"
|
||||||
"github.com/mpolden/echoip/iputil/geo"
|
"github.com/mpolden/echoip/iputil/geo"
|
||||||
|
parser "github.com/mpolden/echoip/iputil/paser"
|
||||||
)
|
)
|
||||||
|
|
||||||
func lookupAddr(net.IP) (string, error) { return "localhost", nil }
|
func lookupAddr(net.IP) (string, error) { return "localhost", nil }
|
||||||
|
@ -32,8 +35,39 @@ func (t *testDb) ASN(net.IP) (geo.ASN, error) {
|
||||||
|
|
||||||
func (t *testDb) IsEmpty() bool { return false }
|
func (t *testDb) IsEmpty() bool { return false }
|
||||||
|
|
||||||
|
func (t *testDb) Parse(ip net.IP, hostname string) (parser.Response, error) {
|
||||||
|
ipDecimal := iputil.ToDecimal(ip)
|
||||||
|
country, _ := t.Country(ip)
|
||||||
|
city, _ := t.City(ip)
|
||||||
|
asn, _ := t.ASN(ip)
|
||||||
|
var autonomousSystemNumber string
|
||||||
|
if asn.AutonomousSystemNumber > 0 {
|
||||||
|
autonomousSystemNumber = fmt.Sprintf("AS%d", asn.AutonomousSystemNumber)
|
||||||
|
}
|
||||||
|
return parser.Response{
|
||||||
|
UsingGeoIP: true,
|
||||||
|
UsingIPStack: false,
|
||||||
|
IP: ip,
|
||||||
|
IPDecimal: ipDecimal,
|
||||||
|
Country: country.Name,
|
||||||
|
CountryISO: country.ISO,
|
||||||
|
CountryEU: country.IsEU,
|
||||||
|
RegionName: city.RegionName,
|
||||||
|
RegionCode: city.RegionCode,
|
||||||
|
MetroCode: city.MetroCode,
|
||||||
|
PostalCode: city.PostalCode,
|
||||||
|
City: city.Name,
|
||||||
|
Latitude: city.Latitude,
|
||||||
|
Longitude: city.Longitude,
|
||||||
|
Timezone: city.Timezone,
|
||||||
|
ASN: autonomousSystemNumber,
|
||||||
|
ASNOrg: asn.AutonomousSystemOrganization,
|
||||||
|
Hostname: hostname,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func testServer() *Server {
|
func testServer() *Server {
|
||||||
return &Server{cache: NewCache(100), gr: &testDb{}, LookupAddr: lookupAddr, LookupPort: lookupPort}
|
return &Server{cache: NewCache(100), parser: &testDb{}, LookupAddr: lookupAddr, LookupPort: lookupPort}
|
||||||
}
|
}
|
||||||
|
|
||||||
func httpGet(url string, acceptMediaType string, userAgent string) (string, int, error) {
|
func httpGet(url string, acceptMediaType string, userAgent string) (string, int, error) {
|
||||||
|
@ -116,7 +150,8 @@ func TestDisabledHandlers(t *testing.T) {
|
||||||
server := testServer()
|
server := testServer()
|
||||||
server.LookupPort = nil
|
server.LookupPort = nil
|
||||||
server.LookupAddr = nil
|
server.LookupAddr = nil
|
||||||
server.gr, _ = geo.Open("", "", "")
|
parser, _ := geo.Open("", "", "")
|
||||||
|
server.parser = &parser
|
||||||
s := httptest.NewServer(server.Handler())
|
s := httptest.NewServer(server.Handler())
|
||||||
|
|
||||||
var tests = []struct {
|
var tests = []struct {
|
||||||
|
@ -128,7 +163,7 @@ func TestDisabledHandlers(t *testing.T) {
|
||||||
{s.URL + "/country", "404 page not found", 404},
|
{s.URL + "/country", "404 page not found", 404},
|
||||||
{s.URL + "/country-iso", "404 page not found", 404},
|
{s.URL + "/country-iso", "404 page not found", 404},
|
||||||
{s.URL + "/city", "404 page not found", 404},
|
{s.URL + "/city", "404 page not found", 404},
|
||||||
{s.URL + "/json", "{\n \"ip\": \"127.0.0.1\",\n \"ip_decimal\": 2130706433\n}", 200},
|
{s.URL + "/json", "{\n \"UsingGeoIP\": true,\n \"UsingIPStack\": false,\n \"ip\": \"127.0.0.1\",\n \"ip_decimal\": 2130706433\n}", 200},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
@ -154,7 +189,7 @@ func TestJSONHandlers(t *testing.T) {
|
||||||
out string
|
out string
|
||||||
status int
|
status int
|
||||||
}{
|
}{
|
||||||
{s.URL, "{\n \"ip\": \"127.0.0.1\",\n \"ip_decimal\": 2130706433,\n \"country\": \"Elbonia\",\n \"country_iso\": \"EB\",\n \"country_eu\": false,\n \"region_name\": \"North Elbonia\",\n \"region_code\": \"1234\",\n \"metro_code\": 1234,\n \"zip_code\": \"1234\",\n \"city\": \"Bornyasherk\",\n \"latitude\": 63.416667,\n \"longitude\": 10.416667,\n \"time_zone\": \"Europe/Bornyasherk\",\n \"asn\": \"AS59795\",\n \"asn_org\": \"Hosting4Real\",\n \"hostname\": \"localhost\",\n \"user_agent\": {\n \"product\": \"curl\",\n \"version\": \"7.2.6.0\",\n \"raw_value\": \"curl/7.2.6.0\"\n }\n}", 200},
|
{s.URL, "{\n \"UsingGeoIP\": true,\n \"UsingIPStack\": false,\n \"ip\": \"127.0.0.1\",\n \"ip_decimal\": 2130706433,\n \"country\": \"Elbonia\",\n \"country_iso\": \"EB\",\n \"country_eu\": false,\n \"region_name\": \"North Elbonia\",\n \"region_code\": \"1234\",\n \"metro_code\": 1234,\n \"zip_code\": \"1234\",\n \"city\": \"Bornyasherk\",\n \"latitude\": 63.416667,\n \"longitude\": 10.416667,\n \"time_zone\": \"Europe/Bornyasherk\",\n \"asn\": \"AS59795\",\n \"asn_org\": \"Hosting4Real\",\n \"hostname\": \"localhost\",\n \"user_agent\": {\n \"product\": \"curl\",\n \"version\": \"7.2.6.0\",\n \"raw_value\": \"curl/7.2.6.0\"\n }\n}", 200},
|
||||||
{s.URL + "/port/foo", "{\n \"status\": 400,\n \"error\": \"invalid port: foo\"\n}", 400},
|
{s.URL + "/port/foo", "{\n \"status\": 400,\n \"error\": \"invalid port: foo\"\n}", 400},
|
||||||
{s.URL + "/port/0", "{\n \"status\": 400,\n \"error\": \"invalid port: 0\"\n}", 400},
|
{s.URL + "/port/0", "{\n \"status\": 400,\n \"error\": \"invalid port: 0\"\n}", 400},
|
||||||
{s.URL + "/port/65537", "{\n \"status\": 400,\n \"error\": \"invalid port: 65537\"\n}", 400},
|
{s.URL + "/port/65537", "{\n \"status\": 400,\n \"error\": \"invalid port: 65537\"\n}", 400},
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
package geo
|
package geo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
|
"github.com/mpolden/echoip/iputil"
|
||||||
|
parser "github.com/mpolden/echoip/iputil/paser"
|
||||||
geoip2 "github.com/oschwald/geoip2-golang"
|
geoip2 "github.com/oschwald/geoip2-golang"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -42,30 +45,61 @@ type geoip struct {
|
||||||
asn *geoip2.Reader
|
asn *geoip2.Reader
|
||||||
}
|
}
|
||||||
|
|
||||||
func Open(countryDB, cityDB string, asnDB string) (Reader, error) {
|
func Open(countryDB, cityDB string, asnDB string) (geoip, error) {
|
||||||
var country, city, asn *geoip2.Reader
|
var country, city, asn *geoip2.Reader
|
||||||
if countryDB != "" {
|
if countryDB != "" {
|
||||||
r, err := geoip2.Open(countryDB)
|
r, err := geoip2.Open(countryDB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return geoip{}, err
|
||||||
}
|
}
|
||||||
country = r
|
country = r
|
||||||
}
|
}
|
||||||
if cityDB != "" {
|
if cityDB != "" {
|
||||||
r, err := geoip2.Open(cityDB)
|
r, err := geoip2.Open(cityDB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return geoip{}, err
|
||||||
}
|
}
|
||||||
city = r
|
city = r
|
||||||
}
|
}
|
||||||
if asnDB != "" {
|
if asnDB != "" {
|
||||||
r, err := geoip2.Open(asnDB)
|
r, err := geoip2.Open(asnDB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return geoip{}, err
|
||||||
}
|
}
|
||||||
asn = r
|
asn = r
|
||||||
}
|
}
|
||||||
return &geoip{country: country, city: city, asn: asn}, nil
|
return geoip{country: country, city: city, asn: asn}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *geoip) Parse(ip net.IP, hostname string) (parser.Response, error) {
|
||||||
|
ipDecimal := iputil.ToDecimal(ip)
|
||||||
|
country, _ := g.Country(ip)
|
||||||
|
city, _ := g.City(ip)
|
||||||
|
asn, _ := g.ASN(ip)
|
||||||
|
var autonomousSystemNumber string
|
||||||
|
if asn.AutonomousSystemNumber > 0 {
|
||||||
|
autonomousSystemNumber = fmt.Sprintf("AS%d", asn.AutonomousSystemNumber)
|
||||||
|
}
|
||||||
|
return parser.Response{
|
||||||
|
UsingGeoIP: true,
|
||||||
|
UsingIPStack: false,
|
||||||
|
IP: ip,
|
||||||
|
IPDecimal: ipDecimal,
|
||||||
|
Country: country.Name,
|
||||||
|
CountryISO: country.ISO,
|
||||||
|
CountryEU: country.IsEU,
|
||||||
|
RegionName: city.RegionName,
|
||||||
|
RegionCode: city.RegionCode,
|
||||||
|
MetroCode: city.MetroCode,
|
||||||
|
PostalCode: city.PostalCode,
|
||||||
|
City: city.Name,
|
||||||
|
Latitude: city.Latitude,
|
||||||
|
Longitude: city.Longitude,
|
||||||
|
Timezone: city.Timezone,
|
||||||
|
ASN: autonomousSystemNumber,
|
||||||
|
ASNOrg: asn.AutonomousSystemOrganization,
|
||||||
|
Hostname: hostname,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *geoip) Country(ip net.IP) (Country, error) {
|
func (g *geoip) Country(ip net.IP) (Country, error) {
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
package ipstack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/mpolden/echoip/iputil"
|
||||||
|
parser "github.com/mpolden/echoip/iputil/paser"
|
||||||
|
"github.com/qioalice/ipstack"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IPStack struct {
|
||||||
|
response *ipstack.Response
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ips *IPStack) Parse(ip net.IP, hostname string) (parser.Response, error) {
|
||||||
|
res, err := ipstack.IP(ip.String())
|
||||||
|
ips.response = res
|
||||||
|
if err != nil {
|
||||||
|
return parser.Response{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ipDecimal := iputil.ToDecimal(ip)
|
||||||
|
|
||||||
|
parserResponse := parser.Response{
|
||||||
|
UsingGeoIP: false,
|
||||||
|
UsingIPStack: true,
|
||||||
|
Latitude: float64(res.Latitide),
|
||||||
|
Longitude: float64(res.Longitude),
|
||||||
|
Hostname: hostname,
|
||||||
|
IP: ip,
|
||||||
|
IPDecimal: ipDecimal,
|
||||||
|
Country: res.CountryName,
|
||||||
|
CountryISO: res.CountryCode,
|
||||||
|
RegionName: res.RegionName,
|
||||||
|
RegionCode: res.RegionCode,
|
||||||
|
MetroCode: 0,
|
||||||
|
PostalCode: res.Zip,
|
||||||
|
City: res.City,
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.Timezone != nil {
|
||||||
|
parserResponse.Timezone = res.Timezone.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.Location != nil {
|
||||||
|
parserResponse.CountryEU = &res.Location.IsEU
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.Connection != nil {
|
||||||
|
if res.Connection.ASN > 0 {
|
||||||
|
parserResponse.ASN = fmt.Sprintf("AS%d", res.Connection.ASN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parserResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ips *IPStack) IsEmpty() bool {
|
||||||
|
return false
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/big"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/mpolden/echoip/useragent"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Parser interface {
|
||||||
|
Parse(net.IP, string) (Response, error)
|
||||||
|
IsEmpty() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
UsingGeoIP bool `json:"UsingGeoIP"`
|
||||||
|
UsingIPStack bool `json:"UsingIPStack"`
|
||||||
|
IP net.IP `json:"ip"`
|
||||||
|
IPDecimal *big.Int `json:"ip_decimal"`
|
||||||
|
Country string `json:"country,omitempty"`
|
||||||
|
CountryISO string `json:"country_iso,omitempty"`
|
||||||
|
CountryEU *bool `json:"country_eu,omitempty"`
|
||||||
|
RegionName string `json:"region_name,omitempty"`
|
||||||
|
RegionCode string `json:"region_code,omitempty"`
|
||||||
|
MetroCode uint `json:"metro_code,omitempty"`
|
||||||
|
PostalCode string `json:"zip_code,omitempty"`
|
||||||
|
City string `json:"city,omitempty"`
|
||||||
|
Latitude float64 `json:"latitude,omitempty"`
|
||||||
|
Longitude float64 `json:"longitude,omitempty"`
|
||||||
|
Timezone string `json:"time_zone,omitempty"`
|
||||||
|
ASN string `json:"asn,omitempty"`
|
||||||
|
ASNOrg string `json:"asn_org,omitempty"`
|
||||||
|
Hostname string `json:"hostname,omitempty"`
|
||||||
|
UserAgent *useragent.UserAgent `json:"user_agent,omitempty"`
|
||||||
|
}
|
Loading…
Reference in New Issue