From b14899b724404c641859a6358d7ae01568421b6b Mon Sep 17 00:00:00 2001 From: Tim Heckman Date: Wed, 27 Feb 2019 22:54:42 -0800 Subject: [PATCH] Update client to support latest API behaviors / data; v0.5.0 This change updates the package so that it continues to work with the ipdata.co API. This also introduces breaking changes in the interest of simplifying the code and reducing the API surface area. There were some changes made to the dara structure returned over the JSON API from ipdata.co, as well as how the authentication credentials are provided to the API itself. This change primarily was executed to support the new format and authentication mechanism. Specifically, some string fields were converted to Objects within the ipdata.co API to provide more rich data. Things like the timezone, the currency, and the risk/threats associated with that IP. The API also stopped accepted authentication tokens via HTTP headers, and now requires them via URL query parameters. In addition to that, the idea of using one struct type for the JSON communication and another for consumers of this package to use has been backed-out. The original implementation used a `RawIP` struct for communication to JSON, and then would convert it to an `IP` struct while doing things like parsing timestamps in to `time.Time`, timezones in to `*time.Location`, and converting URLs to `*url.URL`. Instead of assuming everyone would want the URL to be a `*url.URL`, or that they really need to parse the timestamp, we now only deal with the serialization and deserialization from the API. We leave it up to consumers to parse the data in to a different type if they choose to do so. So the `IP` struct was removed, and the `RawIP` struct was renamed to `IP`. Also, all of the `RawXXX` functions and methods were removed. Signed-off-by: Tim Heckman --- .travis.yml | 6 +- Makefile | 8 +- client.go | 39 +--- client_test.go | 276 ++++++++----------------- ipdata.go | 89 +------- ipdata_test.go | 540 ++++++++++--------------------------------------- types.go | 120 ++++++----- types_test.go | 10 +- 8 files changed, 283 insertions(+), 805 deletions(-) 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") - } -}