diff --git a/.travis.yml b/.travis.yml index f432e0a..db56f99 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,8 +5,10 @@ language: go go: - tip -- 1.10beta1 -- 1.9.2 +- 1.11.x +- 1.12.x +env: + - GO111MODULE=auto sudo: false notifications: email: diff --git a/Makefile b/Makefile index 422ef78..53fd042 100644 --- a/Makefile +++ b/Makefile @@ -5,10 +5,14 @@ test: vet lint megacheck tests prebuild: - go get -v -u github.com/golang/dep/cmd/dep github.com/golang/lint/golint honnef.co/go/tools/cmd/megacheck + go get -v -u github.com/golang/dep/cmd/dep \ + golang.org/x/lint/golint \ + honnef.co/go/tools/cmd/megacheck \ + golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow vet: - go vet -shadow ./... + go vet ./... + go vet -vettool=$(shell which shadow) ./... lint: golint -set_exit_status diff --git a/client.go b/client.go index 0e851dc..a116654 100644 --- a/client.go +++ b/client.go @@ -9,6 +9,7 @@ import ( "io/ioutil" "net" "net/http" + "net/url" "runtime" "time" @@ -16,14 +17,14 @@ import ( ) // Version is the package version -const Version = "0.4.1" +const Version = "0.5.0" // fqpn is the Fully Qualified Package Name for use in the client's User-Agent const fqpn = "github.com/theckman/go-ipdata" const ( - apiEndpoint = "https://api.ipdata.co/" - apiAuthHeader = "api-key" + apiEndpoint = "https://api.ipdata.co/" + apiAuthParam = "api-key" ) var userAgent = fmt.Sprintf( @@ -62,7 +63,7 @@ func (c Client) Request(ip string) (*http.Response, error) { // action request resp, err := c.c.Do(req) if err != nil { - return nil, errors.Wrapf(err, "http request to %q failed", req.URL.String()) + return nil, errors.Wrapf(err, "http request to %q failed", req.URL.Scheme+"://"+req.URL.Host+req.URL.Path) } switch resp.StatusCode { @@ -82,7 +83,7 @@ func (c Client) Request(ip string) (*http.Response, error) { if resp.StatusCode == http.StatusTooManyRequests { rerr := rateErr{r: true, m: string(body)} - return nil, errors.Wrapf(rerr, "request for %q failed (ratelimited)") + return nil, errors.Wrapf(rerr, "request for %q failed (ratelimited)", req.URL.String()) } return nil, errors.Errorf("request for %q failed: %s", ip, string(body)) @@ -92,27 +93,6 @@ func (c Client) Request(ip string) (*http.Response, error) { } } -// LookupRaw takes an IP address to look up the details for. An empty string -// means you want the information about the current node's public IP address. -// -// This method is a little more performant than Lookup as it does not convert -// the RawIP struct to an IP struct. -func (c Client) LookupRaw(ip string) (RawIP, error) { - resp, err := c.Request(ip) - if err != nil { - return RawIP{}, err - } - - defer resp.Body.Close() - - rip, err := DecodeRawIP(resp.Body) - if err != nil { - return RawIP{}, err - } - - return rip, nil -} - // Lookup takes an IP address to look up the details for. An empty string means // you want the information about the current node's pubilc IP address. func (c Client) Lookup(ip string) (IP, error) { @@ -131,8 +111,8 @@ func (c Client) Lookup(ip string) (IP, error) { return pip, nil } -func newRequest(url, apiKey string) (*http.Request, error) { - req, err := http.NewRequest(http.MethodGet, url, nil) +func newRequest(urlStr, apiKey string) (*http.Request, error) { + req, err := http.NewRequest(http.MethodGet, urlStr, nil) if err != nil { return nil, err } @@ -141,7 +121,8 @@ func newRequest(url, apiKey string) (*http.Request, error) { // set the api key header (if set) if len(apiKey) > 0 { - req.Header.Set(apiAuthHeader, apiKey) + q := url.Values{apiAuthParam: []string{apiKey}} + req.URL.RawQuery = q.Encode() } return req, nil diff --git a/client_test.go b/client_test.go index 2a0ceaf..741b2da 100644 --- a/client_test.go +++ b/client_test.go @@ -5,11 +5,11 @@ package ipdata import ( + "fmt" "io" "io/ioutil" "net" "net/http" - "net/url" "strings" "testing" "time" @@ -24,15 +24,33 @@ func testHTTPServer(addr string) (net.Listener, *http.Server, error) { mux := http.NewServeMux() - // 200 response code - mux.HandleFunc("/76.14.47.42", func(w http.ResponseWriter, r *http.Request) { - if r.Header.Get("api-key") != "testAPIkey" { - w.WriteHeader(http.StatusUnauthorized) - io.WriteString(w, "API key does not exist.") - return + amw := func(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "failed to parse form: %v", err) + return + } + + if r.FormValue("api-key") != "testAPIkey" { + w.WriteHeader(http.StatusUnauthorized) + io.WriteString(w, "API key does not exist.") + return + } + + next(w, r) } + } + + // 200 response code + mux.HandleFunc("/76.14.47.42", amw(func(w http.ResponseWriter, r *http.Request) { io.WriteString(w, testJSONValid) - }) + })) + + // 200 response code -- invalid JSON + mux.HandleFunc("/76.14.42.42", amw(func(w http.ResponseWriter, r *http.Request) { + io.WriteString(w, "{") + })) // 400 response code mux.HandleFunc("/192.168.0.1", func(w http.ResponseWriter, r *http.Request) { @@ -114,6 +132,8 @@ func TestNewClient(t *testing.T) { } } +const tjFlagURL = "https://ipdata.co/flags/us.png" + func Test_client_Lookup(t *testing.T) { ln, srvr, err := testHTTPServer("") if err != nil { @@ -123,16 +143,10 @@ func Test_client_Lookup(t *testing.T) { defer ln.Close() defer srvr.Close() - tjFlagURL, err := url.Parse("https://ipdata.co/flags/us.png") if err != nil { t.Fatalf("failed to parse URL: %s", err) } - loc, err := time.LoadLocation("America/Los_Angeles") - if err != nil { - t.Fatalf("failed to load location: %s", err) - } - c := Client{ c: newHTTPClient(), e: "http://" + ln.Addr().String() + "/", @@ -145,6 +159,11 @@ func Test_client_Lookup(t *testing.T) { o IP e string }{ + { + name: "invalid_json", + i: "76.14.42.42", + e: "failed to parse JSON: unexpected EOF", + }, { name: "private_ipv4", i: "192.168.0.1", @@ -164,23 +183,45 @@ func Test_client_Lookup(t *testing.T) { name: "valid_address", i: "76.14.47.42", o: IP{ - IP: net.ParseIP("76.14.47.42"), - ASN: "AS11404", - Organization: "vanoppen.biz LLC", - City: "San Francisco", - Region: "California", - Postal: "94132", - CountryName: "United States", - CountryCode: "US", - Flag: tjFlagURL, - ContinentName: "North America", - ContinentCode: "NA", - Latitude: 37.723, - Longitude: -122.4842, - CallingCode: "1", - Currency: "USD", - CurrencySymbol: "$", - TimeZone: loc, + IP: "76.14.47.42", + ASN: "AS11404", + Organization: "vanoppen.biz LLC", + City: "San Francisco", + Region: "California", + Postal: "94132", + CountryName: "United States", + CountryCode: "US", + Flag: tjFlagURL, + EmojiUnicode: `"U+1F1FA U+1F1F8"`, + ContinentName: "North America", + ContinentCode: "NA", + Latitude: 37.723, + Longitude: -122.4842, + CallingCode: "1", + Languages: []Language{}, + Currency: &Currency{ + Name: "US Dollar", + Code: "USD", + Symbol: "$", + Native: "$", + Plural: "US dollars", + }, + TimeZone: &TimeZone{ + Name: "America/Los_Angeles", + Abbreviation: "PST", + Offset: "-0800", + IsDST: false, + CurrentTime: "2019-02-27T15:00:32.745936-08:00", + }, + Threat: &Threat{ + IsTOR: false, + IsProxy: false, + IsAnonymous: false, + IsKnownAttacker: false, + IsKnownAbuser: false, + IsThreat: true, + IsBogon: false, + }, }, }, } @@ -210,7 +251,7 @@ func Test_client_Lookup(t *testing.T) { t.Fatalf("Lookup(%q) unexpected error: %s", tt.i, err) } - if a, b := ip.IP.String(), tt.o.IP.String(); a != b { + if a, b := ip.IP, tt.o.IP; a != b { t.Errorf("ip.IP = %q, want %q", a, b) } @@ -242,7 +283,7 @@ func Test_client_Lookup(t *testing.T) { t.Errorf("ip.CountryCode = %q, want %q", ip.CountryCode, tt.o.CountryCode) } - if a, b := ip.Flag.String(), tt.o.Flag.String(); a != b { + if a, b := ip.Flag, tt.o.Flag; a != b { t.Errorf("ip.Flag = %q, want %q", a, b) } @@ -266,173 +307,16 @@ func Test_client_Lookup(t *testing.T) { t.Errorf("ip.CallingCode = %q, want %q", ip.CallingCode, tt.o.CallingCode) } - if ip.Currency != tt.o.Currency { - t.Errorf("ip.Currency = %q, want %q", ip.Currency, tt.o.Currency) - } - - if ip.CurrencySymbol != tt.o.CurrencySymbol { - t.Errorf("ip.CurrencySymbol = %q, want %q", ip.CurrencySymbol, tt.o.CurrencySymbol) - } - - if a, b := ip.TimeZone.String(), tt.o.TimeZone.String(); a != b { - t.Errorf("ip.TimeZone = %q, want %q", a, b) - } - }) - } -} - -func Test_client_LookupRaw(t *testing.T) { - ln, srvr, err := testHTTPServer("") - if err != nil { - t.Fatalf(`testHTTPServer("") returned unexpected error: %s`, err) - } - - defer ln.Close() - defer srvr.Close() - - c := Client{ - c: newHTTPClient(), - e: "http://" + ln.Addr().String() + "/", - k: "testAPIkey", - } - - tests := []struct { - name string - i string - o RawIP - e string - }{ - { - name: "private_ipv4", - i: "192.168.0.1", - e: "192.168.0.1 is a private IP address", - }, - { - name: "invalid_ip", - i: "bacon", - e: "bacon does not appear to be an IPv4 or IPv6 address", - }, - { - name: "rate_limited", - i: "8.8.8.8", - e: "You have exceeded your free tier limit of 1500 requests. Register for a paid plan at https://ipdata.co to make more requests.", - }, - { - name: "valid_address", - i: "76.14.47.42", - o: RawIP{ - IP: "76.14.47.42", - ASN: "AS11404", - Organization: "vanoppen.biz LLC", - City: "San Francisco", - Region: "California", - Postal: "94132", - CountryName: "United States", - CountryCode: "US", - Flag: "https://ipdata.co/flags/us.png", - ContinentName: "North America", - ContinentCode: "NA", - Latitude: 37.723, - Longitude: -122.4842, - CallingCode: "1", - Currency: "USD", - CurrencySymbol: "$", - TimeZone: "America/Los_Angeles", - }, - }, - } - - for _, tt := range tests { - tt := tt - - t.Run(tt.name, func(t *testing.T) { - var ip RawIP - var err error - - ip, err = c.LookupRaw(tt.i) - - if len(tt.e) > 0 { - if err == nil { - t.Fatal("error expected but was nil") - } - - if !strings.Contains(err.Error(), tt.e) { - t.Fatalf("error message %q not found in error: %s", tt.e, err) - } - - return - } - - if err != nil { - t.Fatalf("LookupRaw(%q) unexpected error: %s", tt.i, err) - } - - if ip.IP != tt.o.IP { - t.Errorf("ip.IP = %q, want %q", ip.IP, tt.o.IP) - } - - if ip.ASN != tt.o.ASN { - t.Errorf("ip.ASN = %q, want %q", ip.ASN, tt.o.ASN) - } - - if ip.Organization != tt.o.Organization { - t.Errorf("ip.Organization = %q, want %q", ip.Organization, tt.o.Organization) - } - - if ip.City != tt.o.City { - t.Errorf("ip.City = %q, want %q", ip.City, tt.o.City) - } - - if ip.Region != tt.o.Region { - t.Errorf("ip.Region = %q, want %q", ip.Region, tt.o.Region) - } - - if ip.Postal != tt.o.Postal { - t.Errorf("ip.Postal = %q, want %q", ip.Postal, tt.o.Postal) - } - - if ip.CountryName != tt.o.CountryName { - t.Errorf("ip.CountryName = %q, want %q", ip.CountryName, tt.o.CountryName) - } - - if ip.CountryCode != tt.o.CountryCode { - t.Errorf("ip.CountryCode = %q, want %q", ip.CountryCode, tt.o.CountryCode) - } - - if ip.Flag != tt.o.Flag { - t.Errorf("ip.Flag = %q, want %q", ip.Flag, tt.o.Flag) - } - - if ip.ContinentName != tt.o.ContinentName { - t.Errorf("ip.ContinentName = %q, want %q", ip.ContinentName, tt.o.ContinentName) - } - - if ip.ContinentCode != tt.o.ContinentCode { - t.Errorf("ip.ContinentCode = %q, want %q", ip.ContinentCode, tt.o.ContinentCode) - } - - if ip.Latitude != tt.o.Latitude { - t.Errorf("ip.Latitude = %f, want %f", ip.Latitude, tt.o.Latitude) - } - - if ip.Longitude != tt.o.Longitude { - t.Errorf("ip.Longitude = %f, want %f", ip.Longitude, tt.o.Longitude) - } - - if ip.CallingCode != tt.o.CallingCode { - t.Errorf("ip.CallingCode = %q, want %q", ip.CallingCode, tt.o.CallingCode) - } - - if ip.Currency != tt.o.Currency { - t.Errorf("ip.Currency = %q, want %q", ip.Currency, tt.o.Currency) + if *ip.Currency != *tt.o.Currency { + t.Errorf("ip.Currency = %#v, want %#v", ip.Currency, tt.o.Currency) } - if ip.CurrencySymbol != tt.o.CurrencySymbol { - t.Errorf("ip.CurrencySymbol = %q, want %q", ip.CurrencySymbol, tt.o.CurrencySymbol) + if a, b := *ip.TimeZone, *tt.o.TimeZone; a != b { + t.Errorf("ip.TimeZone = %#v, want %#v", a, b) } - if ip.TimeZone != tt.o.TimeZone { - t.Errorf("ip.TimeZone = %q, want %q", ip.TimeZone, tt.o.TimeZone) + if a, b := *ip.Threat, *tt.o.Threat; a != b { + t.Errorf("ip.Threat = %#v, want %#v", a, b) } }) } diff --git a/ipdata.go b/ipdata.go index afbbba5..ed313e1 100644 --- a/ipdata.go +++ b/ipdata.go @@ -8,9 +8,6 @@ import ( "encoding/json" "fmt" "io" - "net" - "net/url" - "time" ) // DecodeIP is a function to decode an io.Reader as the JSON representation of @@ -19,91 +16,13 @@ import ( // you'd prefer to work with the raw data from the API, with no transformations // to Go types, use the DecodeRawIP function. func DecodeIP(r io.Reader) (IP, error) { - rip, err := DecodeRawIP(r) - if err != nil { - return IP{}, err - } - - pip, err := ripToIP(rip) - if err != nil { - return IP{}, err - } - - return pip, nil -} - -// DecodeRawIP takes an io.Reader, and tries to parse the JSON document -// representing an IP address from https://ipdata.co. Unlike DecodeIP, this -// function does not convert the response to Go types and keeps them as the form -// given back by the API. -func DecodeRawIP(r io.Reader) (RawIP, error) { dec := json.NewDecoder(r) - rip := RawIP{} - - if err := dec.Decode(&rip); err != nil { - return RawIP{}, fmt.Errorf("failed to parse JSON: %s", err) - } - - return rip, nil -} - -func ripToIP(rip RawIP) (IP, error) { - var err error + ip := IP{} - // parse the country flag URL if one was provided - var flag *url.URL - if len(rip.Flag) > 0 { - flag, err = url.Parse(rip.Flag) - if err != nil { - return IP{}, fmt.Errorf("failed to parse flag %q: %s", rip.Flag, err) - } + if err := dec.Decode(&ip); err != nil { + return IP{}, fmt.Errorf("failed to parse JSON: %s", err) } - // parse the timezone if one was provided - var loc *time.Location - if len(rip.TimeZone) > 0 { - loc, err = time.LoadLocation(rip.TimeZone) - if err != nil { - return IP{}, fmt.Errorf("failed to parse timezone %q: %s", rip.TimeZone, err) - } - } - - // take a RawIP and transpose it with an IP - // this is a copy of the fields on the RawIP - pip := transpose(rip) - - // set the IP address on the new IP struct - pip.IP = net.ParseIP(rip.IP) - - // if we parsed out a flag URL - if flag != nil { - pip.Flag = flag - } - - // if we parsed out a TimeZone location - if loc != nil { - pip.TimeZone = loc - } - - return pip, nil -} - -func transpose(r RawIP) IP { - return IP{ - ASN: r.ASN, - Organization: r.Organization, - City: r.City, - Region: r.Region, - Postal: r.Postal, - CountryName: r.CountryName, - CountryCode: r.CountryCode, - ContinentName: r.ContinentName, - ContinentCode: r.ContinentCode, - Latitude: r.Latitude, - Longitude: r.Longitude, - CallingCode: r.CallingCode, - Currency: r.Currency, - CurrencySymbol: r.CurrencySymbol, - } + return ip, nil } diff --git a/ipdata_test.go b/ipdata_test.go index 84610ff..5bf15ee 100644 --- a/ipdata_test.go +++ b/ipdata_test.go @@ -5,284 +5,15 @@ package ipdata import ( - "net" - "net/url" "strings" "testing" - "time" ) -func Test_transpose(t *testing.T) { - tests := []struct { - i RawIP - o IP - }{ - { - i: RawIP{ - ASN: "AS11404", - Organization: "vanoppen.biz LLC", - City: "San Francisco", - Region: "California", - Postal: "94132", - CountryName: "United States", - CountryCode: "US", - ContinentName: "North America", - ContinentCode: "NA", - Latitude: 37.723, - Longitude: -122.4842, - CallingCode: "1", - Currency: "USD", - CurrencySymbol: "$", - }, - o: IP{ - ASN: "AS11404", - Organization: "vanoppen.biz LLC", - City: "San Francisco", - Region: "California", - Postal: "94132", - CountryName: "United States", - CountryCode: "US", - ContinentName: "North America", - ContinentCode: "NA", - Latitude: 37.723, - Longitude: -122.4842, - CallingCode: "1", - Currency: "USD", - CurrencySymbol: "$", - }, - }, - } - - for _, tt := range tests { - ip := transpose(tt.i) - - if ip.ASN != tt.o.ASN { - t.Errorf("ip.ASN = %q, want %q", ip.ASN, tt.o.ASN) - } - - if ip.Organization != tt.o.Organization { - t.Errorf("ip.Organization = %q, want %q", ip.Organization, tt.o.Organization) - } - - if ip.City != tt.o.City { - t.Errorf("ip.City = %q, want %q", ip.City, tt.o.City) - } - - if ip.Region != tt.o.Region { - t.Errorf("ip.Region = %q, want %q", ip.Region, tt.o.Region) - } - - if ip.Postal != tt.o.Postal { - t.Errorf("ip.Postal = %q, want %q", ip.Postal, tt.o.Postal) - } - - if ip.CountryName != tt.o.CountryName { - t.Errorf("ip.CountryName = %q, want %q", ip.CountryName, tt.o.CountryName) - } - - if ip.CountryCode != tt.o.CountryCode { - t.Errorf("ip.CountryCode = %q, want %q", ip.CountryCode, tt.o.CountryCode) - } - - if ip.ContinentName != tt.o.ContinentName { - t.Errorf("ip.ContinentName = %q, want %q", ip.ContinentName, tt.o.ContinentName) - } - - if ip.ContinentCode != tt.o.ContinentCode { - t.Errorf("ip.ContinentCode = %q, want %q", ip.ContinentCode, tt.o.ContinentCode) - } - - if ip.Latitude != tt.o.Latitude { - t.Errorf("ip.Latitude = %f, want %f", ip.Latitude, tt.o.Latitude) - } - - if ip.Longitude != tt.o.Longitude { - t.Errorf("ip.Longitude = %f, want %f", ip.Longitude, tt.o.Longitude) - } - - if ip.CallingCode != tt.o.CallingCode { - t.Errorf("ip.CallingCode = %q, want %q", ip.CallingCode, tt.o.CallingCode) - } - - if ip.Currency != tt.o.Currency { - t.Errorf("ip.Currency = %q, want %q", ip.Currency, tt.o.Currency) - } - - if ip.CurrencySymbol != tt.o.CurrencySymbol { - t.Errorf("ip.CurrencySymbol = %q, want %q", ip.CurrencySymbol, tt.o.CurrencySymbol) - } - } -} - -func Test_ripToIP(t *testing.T) { - tjFlagURL, err := url.Parse("https://ipdata.co/flags/us.png") - if err != nil { - t.Fatalf("failed to parse URL: %s", err) - } - - loc, err := time.LoadLocation("America/Los_Angeles") - if err != nil { - t.Fatalf("failed to load location: %s", err) - } - - tests := []struct { - name string - i RawIP - o IP - e string - }{ - { - name: "invalid_flag", - i: RawIP{Flag: `http://%ƒail`}, - e: "failed to parse flag", - }, - { - name: "invalid_timezone", - i: RawIP{TimeZone: `http://%ƒail`}, - e: "failed to parse timezone", - }, - { - name: "valid_RawIP", - i: RawIP{ - IP: "76.14.47.42", - ASN: "AS11404", - Organization: "vanoppen.biz LLC", - City: "San Francisco", - Region: "California", - Postal: "94132", - CountryName: "United States", - CountryCode: "US", - Flag: "https://ipdata.co/flags/us.png", - ContinentName: "North America", - ContinentCode: "NA", - Latitude: 37.723, - Longitude: -122.4842, - CallingCode: "1", - Currency: "USD", - CurrencySymbol: "$", - TimeZone: "America/Los_Angeles", - }, - o: IP{ - IP: net.ParseIP("76.14.47.42"), - ASN: "AS11404", - Organization: "vanoppen.biz LLC", - City: "San Francisco", - Region: "California", - Postal: "94132", - CountryName: "United States", - CountryCode: "US", - Flag: tjFlagURL, - ContinentName: "North America", - ContinentCode: "NA", - Latitude: 37.723, - Longitude: -122.4842, - CallingCode: "1", - Currency: "USD", - CurrencySymbol: "$", - TimeZone: loc, - }, - }, - } - - for _, tt := range tests { - tt := tt - - t.Run(tt.name, func(t *testing.T) { - ip, err := ripToIP(tt.i) - - if len(tt.e) > 0 { - if err == nil { - t.Fatal("error expected but was nil") - } - - if !strings.Contains(err.Error(), tt.e) { - t.Fatalf("error message %q not found in error: %s", tt.e, err) - } - - return - - } - - if err != nil { - t.Fatalf("ripToIP(%+v) returned an unexpected error: %s", tt.i, err) - } - - if a, b := ip.IP.String(), tt.o.IP.String(); a != b { - t.Errorf("ip.IP = %q, want %q", a, b) - } - - if ip.ASN != tt.o.ASN { - t.Errorf("ip.ASN = %q, want %q", ip.ASN, tt.o.ASN) - } - - if ip.Organization != tt.o.Organization { - t.Errorf("ip.Organization = %q, want %q", ip.Organization, tt.o.Organization) - } - - if ip.City != tt.o.City { - t.Errorf("ip.City = %q, want %q", ip.City, tt.o.City) - } - - if ip.Region != tt.o.Region { - t.Errorf("ip.Region = %q, want %q", ip.Region, tt.o.Region) - } - - if ip.Postal != tt.o.Postal { - t.Errorf("ip.Postal = %q, want %q", ip.Postal, tt.o.Postal) - } - - if ip.CountryName != tt.o.CountryName { - t.Errorf("ip.CountryName = %q, want %q", ip.CountryName, tt.o.CountryName) - } - - if ip.CountryCode != tt.o.CountryCode { - t.Errorf("ip.CountryCode = %q, want %q", ip.CountryCode, tt.o.CountryCode) - } - - if a, b := ip.Flag.String(), tt.o.Flag.String(); a != b { - t.Errorf("ip.Flag = %q, want %q", a, b) - } - - if ip.ContinentName != tt.o.ContinentName { - t.Errorf("ip.ContinentName = %q, want %q", ip.ContinentName, tt.o.ContinentName) - } - - if ip.ContinentCode != tt.o.ContinentCode { - t.Errorf("ip.ContinentCode = %q, want %q", ip.ContinentCode, tt.o.ContinentCode) - } - - if ip.Latitude != tt.o.Latitude { - t.Errorf("ip.Latitude = %f, want %f", ip.Latitude, tt.o.Latitude) - } - - if ip.Longitude != tt.o.Longitude { - t.Errorf("ip.Longitude = %f, want %f", ip.Longitude, tt.o.Longitude) - } - - if ip.CallingCode != tt.o.CallingCode { - t.Errorf("ip.CallingCode = %q, want %q", ip.CallingCode, tt.o.CallingCode) - } - - if ip.Currency != tt.o.Currency { - t.Errorf("ip.Currency = %q, want %q", ip.Currency, tt.o.Currency) - } - - if ip.CurrencySymbol != tt.o.CurrencySymbol { - t.Errorf("ip.CurrencySymbol = %q, want %q", ip.CurrencySymbol, tt.o.CurrencySymbol) - } - - if a, b := ip.TimeZone.String(), tt.o.TimeZone.String(); a != b { - t.Errorf("ip.TimeZone = %q, want %q", a, b) - } - }) - } -} - -func TestDecodeRawIP(t *testing.T) { +func TestDecodeIP(t *testing.T) { tests := []struct { name string i string - o RawIP + o IP e string }{ { @@ -293,24 +24,47 @@ func TestDecodeRawIP(t *testing.T) { { name: "valid_json", i: testJSONValid, - o: RawIP{ - IP: "76.14.47.42", - ASN: "AS11404", - Organization: "vanoppen.biz LLC", - City: "San Francisco", - Region: "California", - Postal: "94132", - CountryName: "United States", - CountryCode: "US", - Flag: "https://ipdata.co/flags/us.png", - ContinentName: "North America", - ContinentCode: "NA", - Latitude: 37.723, - Longitude: -122.4842, - CallingCode: "1", - Currency: "USD", - CurrencySymbol: "$", - TimeZone: "America/Los_Angeles", + o: IP{ + IP: "76.14.47.42", + ASN: "AS11404", + Organization: "vanoppen.biz LLC", + City: "San Francisco", + Region: "California", + Postal: "94132", + CountryName: "United States", + CountryCode: "US", + Flag: tjFlagURL, + EmojiUnicode: `U+1F1FA U+1F1F8`, + ContinentName: "North America", + ContinentCode: "NA", + Latitude: 37.723, + Longitude: -122.4842, + CallingCode: "1", + IsEU: true, + Languages: []Language{}, + Currency: &Currency{ + Name: "US Dollar", + Code: "USD", + Symbol: "$", + Native: "$", + Plural: "US dollars", + }, + TimeZone: &TimeZone{ + Name: "America/Los_Angeles", + Abbreviation: "PST", + Offset: "-0800", + IsDST: false, + CurrentTime: "2019-02-27T15:00:32.745936-08:00", + }, + Threat: &Threat{ + IsTOR: false, + IsProxy: false, + IsAnonymous: false, + IsKnownAttacker: false, + IsKnownAbuser: false, + IsThreat: true, + IsBogon: false, + }, }, }, } @@ -319,7 +73,7 @@ func TestDecodeRawIP(t *testing.T) { tt := tt t.Run(tt.name, func(t *testing.T) { - ip, err := DecodeRawIP(strings.NewReader(tt.i)) + ip, err := DecodeIP(strings.NewReader(tt.i)) if len(tt.e) > 0 { if err == nil { @@ -337,8 +91,8 @@ func TestDecodeRawIP(t *testing.T) { t.Fatalf("DecodeIP(%+v) returned an unexpected error: %s", tt.i, err) } - if ip.IP != tt.o.IP { - t.Errorf("ip.IP = %q, want %q", ip.IP, tt.o.IP) + if a, b := ip.IP, tt.o.IP; a != b { + t.Errorf("ip.IP = %q, want %q", a, b) } if ip.ASN != tt.o.ASN { @@ -369,8 +123,8 @@ func TestDecodeRawIP(t *testing.T) { t.Errorf("ip.CountryCode = %q, want %q", ip.CountryCode, tt.o.CountryCode) } - if ip.Flag != tt.o.Flag { - t.Errorf("ip.Flag = %q, want %q", ip.Flag, tt.o.Flag) + if a, b := ip.Flag, tt.o.Flag; a != b { + t.Errorf("ip.Flag = %q, want %q", a, b) } if ip.ContinentName != tt.o.ContinentName { @@ -393,161 +147,51 @@ func TestDecodeRawIP(t *testing.T) { t.Errorf("ip.CallingCode = %q, want %q", ip.CallingCode, tt.o.CallingCode) } - if ip.Currency != tt.o.Currency { - t.Errorf("ip.Currency = %q, want %q", ip.Currency, tt.o.Currency) + if ip.IsEU != tt.o.IsEU { + t.Errorf("ip.IsEU = %v, want %v", ip.IsEU, tt.o.IsEU) } - if ip.CurrencySymbol != tt.o.CurrencySymbol { - t.Errorf("ip.CurrencySymbol = %q, want %q", ip.CurrencySymbol, tt.o.CurrencySymbol) + if ip.EmojiUnicode != tt.o.EmojiUnicode { + t.Errorf("ip.EmojiUnicode = %q, want %q", ip.EmojiUnicode, tt.o.EmojiUnicode) } - if ip.TimeZone != tt.o.TimeZone { - t.Errorf("ip.TimeZone = %q, want %q", ip.TimeZone, tt.o.TimeZone) + if a, b := len(ip.Languages), len(tt.o.Languages); a != b { + t.Errorf("len(ip.Languages) = %d, want %d", a, b) } - }) - } -} -func TestDecodeIP(t *testing.T) { - tjFlagURL, err := url.Parse("https://ipdata.co/flags/us.png") - if err != nil { - t.Fatalf("failed to parse URL: %s", err) - } + fn := func(t *testing.T, x, y []Language) { + t.Helper() - loc, err := time.LoadLocation("America/Los_Angeles") - if err != nil { - t.Fatalf("failed to load location: %s", err) - } + for i := range tt.o.Languages { + if i >= len(ip.Languages) { + t.Errorf("ip.Languages[%d] = [not present], want %#v", i, tt.o.Languages[i]) + continue + } - tests := []struct { - name string - i string - o IP - e string - }{ - { - name: "invalid_json", - i: "garbage", - e: "failed to parse JSON:", - }, - { - name: "invalid_field", - i: `{"flag":"http://%ƒail"}`, - e: "failed to parse flag", - }, - { - name: "valid_json", - i: testJSONValid, - o: IP{ - IP: net.ParseIP("76.14.47.42"), - ASN: "AS11404", - Organization: "vanoppen.biz LLC", - City: "San Francisco", - Region: "California", - Postal: "94132", - CountryName: "United States", - CountryCode: "US", - Flag: tjFlagURL, - ContinentName: "North America", - ContinentCode: "NA", - Latitude: 37.723, - Longitude: -122.4842, - CallingCode: "1", - Currency: "USD", - CurrencySymbol: "$", - TimeZone: loc, - }, - }, - } - - for _, tt := range tests { - tt := tt + a, b := ip.Languages[i], tt.o.Languages[i] - t.Run(tt.name, func(t *testing.T) { - ip, err := DecodeIP(strings.NewReader(tt.i)) - - if len(tt.e) > 0 { - if err == nil { - t.Fatal("error expected but was nil") + if a != b { + t.Errorf("ip.Languages[%d] = %#v, want %#v", i, a, b) + } } - - if !strings.Contains(err.Error(), tt.e) { - t.Fatalf("error message %q not found in error: %s", tt.e, err) - } - - return - } - - if err != nil { - t.Fatalf("DecodeIP(%+v) returned an unexpected error: %s", tt.i, err) - } - - if a, b := ip.IP.String(), tt.o.IP.String(); a != b { - t.Errorf("ip.IP = %q, want %q", a, b) - } - - if ip.ASN != tt.o.ASN { - t.Errorf("ip.ASN = %q, want %q", ip.ASN, tt.o.ASN) - } - - if ip.Organization != tt.o.Organization { - t.Errorf("ip.Organization = %q, want %q", ip.Organization, tt.o.Organization) - } - - if ip.City != tt.o.City { - t.Errorf("ip.City = %q, want %q", ip.City, tt.o.City) - } - - if ip.Region != tt.o.Region { - t.Errorf("ip.Region = %q, want %q", ip.Region, tt.o.Region) - } - - if ip.Postal != tt.o.Postal { - t.Errorf("ip.Postal = %q, want %q", ip.Postal, tt.o.Postal) - } - - if ip.CountryName != tt.o.CountryName { - t.Errorf("ip.CountryName = %q, want %q", ip.CountryName, tt.o.CountryName) - } - - if ip.CountryCode != tt.o.CountryCode { - t.Errorf("ip.CountryCode = %q, want %q", ip.CountryCode, tt.o.CountryCode) } - if a, b := ip.Flag.String(), tt.o.Flag.String(); a != b { - t.Errorf("ip.Flag = %q, want %q", a, b) + if len(ip.Languages) >= len(tt.o.Languages) { + fn(t, ip.Languages, tt.o.Languages) + } else { + fn(t, tt.o.Languages, ip.Languages) } - if ip.ContinentName != tt.o.ContinentName { - t.Errorf("ip.ContinentName = %q, want %q", ip.ContinentName, tt.o.ContinentName) + if *ip.Currency != *tt.o.Currency { + t.Errorf("ip.Currency = %#v, want %#v", ip.Currency, tt.o.Currency) } - if ip.ContinentCode != tt.o.ContinentCode { - t.Errorf("ip.ContinentCode = %q, want %q", ip.ContinentCode, tt.o.ContinentCode) + if a, b := *ip.TimeZone, *tt.o.TimeZone; a != b { + t.Errorf("ip.TimeZone = %#v, want %#v", a, b) } - if ip.Latitude != tt.o.Latitude { - t.Errorf("ip.Latitude = %f, want %f", ip.Latitude, tt.o.Latitude) - } - - if ip.Longitude != tt.o.Longitude { - t.Errorf("ip.Longitude = %f, want %f", ip.Longitude, tt.o.Longitude) - } - - if ip.CallingCode != tt.o.CallingCode { - t.Errorf("ip.CallingCode = %q, want %q", ip.CallingCode, tt.o.CallingCode) - } - - if ip.Currency != tt.o.Currency { - t.Errorf("ip.Currency = %q, want %q", ip.Currency, tt.o.Currency) - } - - if ip.CurrencySymbol != tt.o.CurrencySymbol { - t.Errorf("ip.CurrencySymbol = %q, want %q", ip.CurrencySymbol, tt.o.CurrencySymbol) - } - - if a, b := ip.TimeZone.String(), tt.o.TimeZone.String(); a != b { - t.Errorf("ip.TimeZone = %q, want %q", a, b) + if a, b := *ip.Threat, *tt.o.Threat; a != b { + t.Errorf("ip.Threat = %#v, want %#v", a, b) } }) } @@ -566,9 +210,37 @@ var testJSONValid = `{ "asn": "AS11404", "organisation": "vanoppen.biz LLC", "postal": "94132", - "currency": "USD", - "currency_symbol": "$", "calling_code": "1", "flag": "https://ipdata.co/flags/us.png", - "time_zone": "America/Los_Angeles" + "emoji_unicode": "U+1F1FA U+1F1F8", + "is_eu": true, + "languages": [ + { + "name": "English", + "native": "English" + } + ], + "currency": { + "name": "US Dollar", + "code": "USD", + "symbol": "$", + "native": "$", + "plural": "US dollars" + }, + "time_zone": { + "name": "America/Los_Angeles", + "abbr": "PST", + "offset": "-0800", + "is_dst": false, + "current_time": "2019-02-27T15:00:32.745936-08:00" + }, + "threat": { + "is_tor": false, + "is_proxy": false, + "is_anonymous": false, + "is_known_attacker": false, + "is_known_abuser": false, + "is_threat": true, + "is_bogon": false + } }` diff --git a/types.go b/types.go index 1d364f0..4701481 100644 --- a/types.go +++ b/types.go @@ -4,50 +4,9 @@ package ipdata -import ( - "net" - "net/url" - "time" -) - -// IP is the representation of the metadata available from the https://ipdata.co -// API. This struct is meant to be a parsed version of the RawIP struct, where -// fields are replaced by ones with a more useful type. One example is -// converting the TimeZone of RawIP to be a *time.Location. +// IP is a struct that represents the JSON response from the https://ipdata.co +// API. type IP struct { - IP net.IP - ASN string - Organization string - - City string - Region string - Postal string - - CountryName string - CountryCode string - Flag *url.URL - - ContinentName string - ContinentCode string - - Latitude float64 - Longitude float64 - - CallingCode string - - Currency string - CurrencySymbol string - - TimeZone *time.Location -} - -func (ip IP) String() string { - return ip.IP.String() -} - -// RawIP is a struct that represents the raw JSON response from the -// https://ipdata.co API. -type RawIP struct { IP string `json:"ip"` ASN string `json:"asn"` Organization string `json:"organisation"` @@ -58,7 +17,10 @@ type RawIP struct { CountryName string `json:"country_name"` CountryCode string `json:"country_code"` - Flag string `json:"flag"` + + Flag string `json:"flag"` + EmojiFlag string `json:"emoji_flag"` + EmojiUnicode string `json:"emoji_unicode"` ContinentName string `json:"continent_name"` ContinentCode string `json:"continent_code"` @@ -68,12 +30,74 @@ type RawIP struct { CallingCode string `json:"calling_code"` - Currency string `json:"currency"` - CurrencySymbol string `json:"currency_symbol"` + IsEU bool `json:"is_eu"` + + Languages []Language `json:"language,omitempty"` + + Currency *Currency `json:"currency,omitempty"` + + TimeZone *TimeZone `json:"time_zone,omitempty"` - TimeZone string `json:"time_zone"` + Threat *Threat `json:"threat,omitempty"` } -func (ip RawIP) String() string { +func (ip IP) String() string { return ip.IP } + +// Language represents the language object within the JSON response from the +// API. This provides information about the language(s) where that IP resides. +type Language struct { + Name string `json:"name"` + Native string `json:"native"` +} + +// Currency represents the currency object within the JSON response from the +// API. This provides information about the currency where that IP resides. +type Currency struct { + Name string `json:"name"` + Code string `json:"code"` + Symbol string `json:"symbol"` + Native string `json:"native"` + Plural string `json:"plural"` +} + +// TimeZone represents the time_zone object within the JSON response from the +// API. This provides information about the timezone where that IP resides. +type TimeZone struct { + Name string `json:"name"` + Abbreviation string `json:"abbr"` + Offset string `json:"offset"` + IsDST bool `json:"is_dst"` + CurrentTime string `json:"current_time,omitempty"` +} + +// Threat represents the threat object within the JSON response from the API. +// This provides information about what type of threat this IP may be. +type Threat struct { + // IsTOR is true if the IP is associated with a node on the TOR (The Onion + // Router) network + IsTOR bool `json:"is_tor"` + + // IsProxy is true if the IP is associated with bring a proxy + // (HTTP/HTTPS/SSL/SOCKS/CONNECT and transparent proxies) + IsProxy bool `json:"is_proxy"` + + // IsAnonymous is true if either IsTor or IsProxy are true + IsAnonymous bool `json:"is_anonymous"` + + // IsKnownAttacker is true if the IP address is a known source of malicious + // activity (i.e. attacks, malware, botnet activity, etc) + IsKnownAttacker bool `json:"is_known_attacker"` + + // IsKnownAbuser is true if the IP address is a known source of abuse + // (i.e. spam, harvesters, registration bots, and other nuisance bots, etc) + IsKnownAbuser bool `json:"is_known_abuser"` + + // IsThreat is true if either IsKnownAttacker or IsKnownAbuser are true + IsThreat bool `json:"is_threat"` + + // IsBogon is true if this IP address should be within a bogon filter: + // https://en.wikipedia.org/wiki/Bogon_filtering + IsBogon bool `json:"is_bogon"` +} diff --git a/types_test.go b/types_test.go index 41081d9..a9201b2 100644 --- a/types_test.go +++ b/types_test.go @@ -5,20 +5,12 @@ package ipdata import ( - "net" "testing" ) func Test_IP_String(t *testing.T) { - ip := IP{IP: net.ParseIP("8.8.8.8")} + ip := IP{IP: "8.8.8.8"} if ip.String() != "8.8.8.8" { t.Errorf("ip.String() = %q, want %q", ip.String(), "8.8.8.8") } } - -func Test_RawIP_String(t *testing.T) { - rawIP := RawIP{IP: "8.8.8.8"} - if rawIP.String() != "8.8.8.8" { - t.Errorf("rawIP.String() = %q, want %q", rawIP.String(), "8.8.8.8") - } -}