fixes
This commit is contained in:
1 parent
f30adf05af
commit
8824af8699
29 files changed
+1201
-115
No files matched your search
@@ -0,0 +1,140 @@
|
||||
// Package geoip resolves IP addresses against local MaxMind (GeoLite2 / GeoIP2)
|
||||
// databases so the UI can show where an issue's client came from. Lookups are
|
||||
// purely local; no IP ever leaves the process.
|
||||
package geoip
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"strings"
|
||||
|
||||
geoip2 "github.com/oschwald/geoip2-golang/v2"
|
||||
)
|
||||
|
||||
// Lookup holds the opened databases. Either reader may be nil; a nil *Lookup
|
||||
// means no database is configured.
|
||||
type Lookup struct {
|
||||
city *geoip2.Reader
|
||||
asn *geoip2.Reader
|
||||
}
|
||||
|
||||
// Result is the JSON shape returned to the UI. Fields the databases don't
|
||||
// know are omitted.
|
||||
type Result struct {
|
||||
IP string `json:"ip"`
|
||||
Configured bool `json:"configured"`
|
||||
// Scope is set for addresses that can't be geolocated: "private",
|
||||
// "loopback", "link-local" or "unspecified".
|
||||
Scope string `json:"scope,omitempty"`
|
||||
|
||||
Country string `json:"country,omitempty"`
|
||||
CountryCode string `json:"countryCode,omitempty"`
|
||||
InEU bool `json:"inEu,omitempty"`
|
||||
Region string `json:"region,omitempty"`
|
||||
City string `json:"city,omitempty"`
|
||||
Postal string `json:"postal,omitempty"`
|
||||
Timezone string `json:"timezone,omitempty"`
|
||||
Latitude *float64 `json:"latitude,omitempty"`
|
||||
Longitude *float64 `json:"longitude,omitempty"`
|
||||
Network string `json:"network,omitempty"`
|
||||
ASN uint `json:"asn,omitempty"`
|
||||
ASOrg string `json:"asOrg,omitempty"`
|
||||
}
|
||||
|
||||
// Open opens the given database files; empty paths are skipped. It returns nil
|
||||
// (and no error) when both are empty.
|
||||
func Open(cityPath, asnPath string) (*Lookup, error) {
|
||||
if cityPath == "" && asnPath == "" {
|
||||
return nil, nil
|
||||
}
|
||||
l := &Lookup{}
|
||||
var err error
|
||||
if cityPath != "" {
|
||||
if l.city, err = geoip2.Open(cityPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if asnPath != "" {
|
||||
if l.asn, err = geoip2.Open(asnPath); err != nil {
|
||||
l.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// Close releases the memory-mapped databases.
|
||||
func (l *Lookup) Close() {
|
||||
if l == nil {
|
||||
return
|
||||
}
|
||||
if l.city != nil {
|
||||
l.city.Close()
|
||||
}
|
||||
if l.asn != nil {
|
||||
l.asn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// Lookup resolves ip. It never fails: an unparsable or unroutable address just
|
||||
// yields a Result without geo fields.
|
||||
func (l *Lookup) Lookup(ip netip.Addr) Result {
|
||||
ip = ip.Unmap()
|
||||
res := Result{IP: ip.String(), Configured: l != nil}
|
||||
switch {
|
||||
case ip.IsLoopback():
|
||||
res.Scope = "loopback"
|
||||
case ip.IsPrivate():
|
||||
res.Scope = "private"
|
||||
case ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast():
|
||||
res.Scope = "link-local"
|
||||
case ip.IsUnspecified():
|
||||
res.Scope = "unspecified"
|
||||
}
|
||||
if l == nil || res.Scope != "" {
|
||||
return res
|
||||
}
|
||||
if l.city != nil {
|
||||
if c, err := l.city.City(ip); err == nil && c.HasData() {
|
||||
res.Country = c.Country.Names.English
|
||||
res.CountryCode = c.Country.ISOCode
|
||||
res.InEU = c.Country.IsInEuropeanUnion
|
||||
res.City = c.City.Names.English
|
||||
res.Postal = c.Postal.Code
|
||||
res.Timezone = c.Location.TimeZone
|
||||
res.Latitude, res.Longitude = c.Location.Latitude, c.Location.Longitude
|
||||
if n := len(c.Subdivisions); n > 0 {
|
||||
res.Region = c.Subdivisions[0].Names.English
|
||||
}
|
||||
if c.Traits.Network.IsValid() {
|
||||
res.Network = c.Traits.Network.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
if l.asn != nil {
|
||||
if a, err := l.asn.ASN(ip); err == nil && a.HasData() {
|
||||
res.ASN, res.ASOrg = a.AutonomousSystemNumber, a.AutonomousSystemOrganization
|
||||
if res.Network == "" && a.Network.IsValid() {
|
||||
res.Network = a.Network.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Handler serves GET /api/ip/{ip}. Without a configured database it still
|
||||
// answers (Configured=false) so the UI can explain how to enable it.
|
||||
func (l *Lookup) Handler(w http.ResponseWriter, r *http.Request) {
|
||||
ip, err := netip.ParseAddr(strings.TrimSpace(r.PathValue("ip")))
|
||||
if err != nil {
|
||||
http.Error(w, "invalid ip address", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Cache-Control", "private, max-age=3600")
|
||||
if err := json.NewEncoder(w).Encode(l.Lookup(ip)); err != nil {
|
||||
log.Printf("geoip: encode: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package geoip
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLookupWithoutDB(t *testing.T) {
|
||||
var l *Lookup
|
||||
if r := l.Lookup(netip.MustParseAddr("8.8.8.8")); r.Configured || r.Country != "" {
|
||||
t.Fatalf("unexpected result %+v", r)
|
||||
}
|
||||
if r := l.Lookup(netip.MustParseAddr("10.1.2.3")); r.Scope != "private" {
|
||||
t.Fatalf("want private scope, got %+v", r)
|
||||
}
|
||||
if r := l.Lookup(netip.MustParseAddr("::ffff:127.0.0.1")); r.Scope != "loopback" || r.IP != "127.0.0.1" {
|
||||
t.Fatalf("want unmapped loopback, got %+v", r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenEmpty(t *testing.T) {
|
||||
if l, err := Open("", ""); l != nil || err != nil {
|
||||
t.Fatalf("want nil,nil got %v,%v", l, err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user