Skip to content

Commit b6a1b49

Browse files
authored
Merge pull request #70 from ipinfo/silvano/eng-501-add-plus-bundle-support-in-ipinfogo-library
Add support for Plus bundle
2 parents ec0270e + 311da8f commit b6a1b49

File tree

3 files changed

+366
-0
lines changed

3 files changed

+366
-0
lines changed

example/lookup-plus/main.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"net"
7+
"os"
8+
9+
"github.com/ipinfo/go/v2/ipinfo"
10+
)
11+
12+
func main() {
13+
client := ipinfo.NewPlusClient(nil, nil, os.Getenv("IPINFO_TOKEN"))
14+
15+
ip := net.ParseIP("8.8.8.8")
16+
info, err := client.GetIPInfo(ip)
17+
if err != nil {
18+
log.Fatal(err)
19+
}
20+
21+
fmt.Printf("IP: %s\n", info.IP)
22+
fmt.Printf("Hostname: %s\n", info.Hostname)
23+
if info.Geo != nil {
24+
fmt.Printf("City: %s\n", info.Geo.City)
25+
fmt.Printf("Region: %s (%s)\n", info.Geo.Region, info.Geo.RegionCode)
26+
fmt.Printf("Country: %s (%s)\n", info.Geo.Country, info.Geo.CountryCode)
27+
fmt.Printf("Continent: %s (%s)\n", info.Geo.Continent, info.Geo.ContinentCode)
28+
fmt.Printf("Location: %f, %f\n", info.Geo.Latitude, info.Geo.Longitude)
29+
fmt.Printf("Timezone: %s\n", info.Geo.Timezone)
30+
fmt.Printf("Postal Code: %s\n", info.Geo.PostalCode)
31+
}
32+
if info.AS != nil {
33+
fmt.Printf("ASN: %s\n", info.AS.ASN)
34+
fmt.Printf("AS Name: %s\n", info.AS.Name)
35+
fmt.Printf("AS Domain: %s\n", info.AS.Domain)
36+
fmt.Printf("AS Type: %s\n", info.AS.Type)
37+
}
38+
if info.Mobile != nil {
39+
fmt.Printf("Mobile - Name: %s, MCC: %s, MNC: %s\n",
40+
info.Mobile.Name, info.Mobile.MCC, info.Mobile.MNC)
41+
}
42+
if info.Anonymous != nil {
43+
fmt.Printf("Anonymous - Proxy: %v, Relay: %v, Tor: %v, VPN: %v\n",
44+
info.Anonymous.IsProxy, info.Anonymous.IsRelay, info.Anonymous.IsTor, info.Anonymous.IsVPN)
45+
}
46+
fmt.Printf("Anonymous: %v\n", info.IsAnonymous)
47+
fmt.Printf("Anycast: %v\n", info.IsAnycast)
48+
fmt.Printf("Hosting: %v\n", info.IsHosting)
49+
fmt.Printf("Mobile: %v\n", info.IsMobile)
50+
fmt.Printf("Satellite: %v\n", info.IsSatellite)
51+
if info.Abuse != nil {
52+
fmt.Printf("Abuse Contact - Email: %s, Name: %s\n", info.Abuse.Email, info.Abuse.Name)
53+
}
54+
if info.Company != nil {
55+
fmt.Printf("Company - Name: %s, Domain: %s, Type: %s\n",
56+
info.Company.Name, info.Company.Domain, info.Company.Type)
57+
}
58+
if info.Privacy != nil {
59+
fmt.Printf("Privacy - VPN: %v, Proxy: %v, Tor: %v, Relay: %v, Hosting: %v\n",
60+
info.Privacy.VPN, info.Privacy.Proxy, info.Privacy.Tor, info.Privacy.Relay, info.Privacy.Hosting)
61+
}
62+
if info.Domains != nil {
63+
fmt.Printf("Domains - Total: %d\n", info.Domains.Total)
64+
if len(info.Domains.Domains) > 0 {
65+
maxDomains := 3
66+
if len(info.Domains.Domains) < maxDomains {
67+
maxDomains = len(info.Domains.Domains)
68+
}
69+
fmt.Printf("Sample Domains: %v\n", info.Domains.Domains[:maxDomains])
70+
}
71+
}
72+
}

ipinfo/ipinfo.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@ var DefaultLiteClient *LiteClient
99
// Package-level Core bundle client
1010
var DefaultCoreClient *CoreClient
1111

12+
// Package-level Plus bundle client
13+
var DefaultPlusClient *PlusClient
14+
1215
func init() {
1316
// Create global clients for legacy, Lite, Core APIs
1417
DefaultClient = NewClient(nil, nil, "")
1518
DefaultLiteClient = NewLiteClient(nil, nil, "")
1619
DefaultCoreClient = NewCoreClient(nil, nil, "")
20+
DefaultPlusClient = NewPlusClient(nil, nil, "")
1721
}

ipinfo/plus.go

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
package ipinfo
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"io"
7+
"net"
8+
"net/http"
9+
"net/netip"
10+
"net/url"
11+
"strings"
12+
)
13+
14+
const (
15+
defaultPlusBaseURL = "https://api.ipinfo.io/lookup/"
16+
)
17+
18+
// PlusClient is a client for the IPinfo Plus API.
19+
type PlusClient struct {
20+
// HTTP client used to communicate with the API.
21+
client *http.Client
22+
23+
// Base URL for API requests.
24+
BaseURL *url.URL
25+
26+
// User agent used when communicating with the IPinfo API.
27+
UserAgent string
28+
29+
// Cache interface implementation to prevent API quota overuse.
30+
Cache *Cache
31+
32+
// The API token used for authorization.
33+
Token string
34+
}
35+
36+
// Plus represents the response from the IPinfo Plus API /lookup endpoint.
37+
type Plus struct {
38+
IP net.IP `json:"ip"`
39+
Hostname string `json:"hostname,omitempty"`
40+
Bogon bool `json:"bogon,omitempty"`
41+
Geo *PlusGeo `json:"geo,omitempty"`
42+
AS *PlusAS `json:"as,omitempty"`
43+
Mobile *PlusMobile `json:"mobile,omitempty"`
44+
Anonymous *PlusAnonymous `json:"anonymous,omitempty"`
45+
IsAnonymous bool `json:"is_anonymous"`
46+
IsAnycast bool `json:"is_anycast"`
47+
IsHosting bool `json:"is_hosting"`
48+
IsMobile bool `json:"is_mobile"`
49+
IsSatellite bool `json:"is_satellite"`
50+
Abuse *PlusAbuse `json:"abuse,omitempty"`
51+
Company *PlusCompany `json:"company,omitempty"`
52+
Privacy *PlusPrivacy `json:"privacy,omitempty"`
53+
Domains *PlusDomains `json:"domains,omitempty"`
54+
}
55+
56+
// PlusGeo represents the geo object in Plus API response.
57+
type PlusGeo struct {
58+
City string `json:"city,omitempty"`
59+
Region string `json:"region,omitempty"`
60+
RegionCode string `json:"region_code,omitempty"`
61+
Country string `json:"country,omitempty"`
62+
CountryCode string `json:"country_code,omitempty"`
63+
Continent string `json:"continent,omitempty"`
64+
ContinentCode string `json:"continent_code,omitempty"`
65+
Latitude float64 `json:"latitude"`
66+
Longitude float64 `json:"longitude"`
67+
Timezone string `json:"timezone,omitempty"`
68+
PostalCode string `json:"postal_code,omitempty"`
69+
DMACode string `json:"dma_code,omitempty"`
70+
GeonameID string `json:"geoname_id,omitempty"`
71+
Radius int `json:"radius"`
72+
LastChanged string `json:"last_changed,omitempty"`
73+
74+
// Extended fields using the same country data as legacy Core API
75+
CountryName string `json:"-"`
76+
IsEU bool `json:"-"`
77+
CountryFlag CountryFlag `json:"-"`
78+
CountryFlagURL string `json:"-"`
79+
CountryCurrency CountryCurrency `json:"-"`
80+
ContinentInfo Continent `json:"-"`
81+
}
82+
83+
// PlusAS represents the AS object in Plus API response.
84+
type PlusAS struct {
85+
ASN string `json:"asn"`
86+
Name string `json:"name"`
87+
Domain string `json:"domain"`
88+
Type string `json:"type"`
89+
LastChanged string `json:"last_changed,omitempty"`
90+
}
91+
92+
// PlusMobile represents the mobile object in Plus API response.
93+
type PlusMobile struct {
94+
Name string `json:"name,omitempty"`
95+
MCC string `json:"mcc,omitempty"`
96+
MNC string `json:"mnc,omitempty"`
97+
}
98+
99+
// PlusAnonymous represents the anonymous object in Plus API response.
100+
type PlusAnonymous struct {
101+
IsProxy bool `json:"is_proxy"`
102+
IsRelay bool `json:"is_relay"`
103+
IsTor bool `json:"is_tor"`
104+
IsVPN bool `json:"is_vpn"`
105+
Name string `json:"name,omitempty"`
106+
}
107+
108+
// PlusAbuse represents the abuse object in Plus API response.
109+
type PlusAbuse struct {
110+
Address string `json:"address,omitempty"`
111+
Country string `json:"country,omitempty"`
112+
CountryName string `json:"country_name,omitempty"`
113+
Email string `json:"email,omitempty"`
114+
Name string `json:"name,omitempty"`
115+
Network string `json:"network,omitempty"`
116+
Phone string `json:"phone,omitempty"`
117+
}
118+
119+
// PlusCompany represents the company object in Plus API response.
120+
type PlusCompany struct {
121+
Name string `json:"name,omitempty"`
122+
Domain string `json:"domain,omitempty"`
123+
Type string `json:"type,omitempty"`
124+
}
125+
126+
// PlusPrivacy represents the privacy object in Plus API response.
127+
type PlusPrivacy struct {
128+
VPN bool `json:"vpn"`
129+
Proxy bool `json:"proxy"`
130+
Tor bool `json:"tor"`
131+
Relay bool `json:"relay"`
132+
Hosting bool `json:"hosting"`
133+
Service string `json:"service,omitempty"`
134+
}
135+
136+
// PlusDomains represents the domains object in Plus API response.
137+
type PlusDomains struct {
138+
IP string `json:"ip,omitempty"`
139+
Total uint64 `json:"total"`
140+
Domains []string `json:"domains,omitempty"`
141+
}
142+
143+
func (v *Plus) enrichGeo() {
144+
if v.Geo != nil && v.Geo.CountryCode != "" {
145+
v.Geo.CountryName = GetCountryName(v.Geo.CountryCode)
146+
v.Geo.IsEU = IsEU(v.Geo.CountryCode)
147+
v.Geo.CountryFlag.Emoji = GetCountryFlagEmoji(v.Geo.CountryCode)
148+
v.Geo.CountryFlag.Unicode = GetCountryFlagUnicode(v.Geo.CountryCode)
149+
v.Geo.CountryFlagURL = GetCountryFlagURL(v.Geo.CountryCode)
150+
v.Geo.CountryCurrency.Code = GetCountryCurrencyCode(v.Geo.CountryCode)
151+
v.Geo.CountryCurrency.Symbol = GetCountryCurrencySymbol(v.Geo.CountryCode)
152+
v.Geo.ContinentInfo.Code = GetContinentCode(v.Geo.CountryCode)
153+
v.Geo.ContinentInfo.Name = GetContinentName(v.Geo.CountryCode)
154+
}
155+
if v.Abuse != nil && v.Abuse.Country != "" {
156+
v.Abuse.CountryName = GetCountryName(v.Abuse.Country)
157+
}
158+
}
159+
160+
// NewPlusClient creates a new IPinfo Plus API client.
161+
func NewPlusClient(httpClient *http.Client, cache *Cache, token string) *PlusClient {
162+
if httpClient == nil {
163+
httpClient = http.DefaultClient
164+
}
165+
166+
baseURL, _ := url.Parse(defaultPlusBaseURL)
167+
return &PlusClient{
168+
client: httpClient,
169+
BaseURL: baseURL,
170+
UserAgent: defaultUserAgent,
171+
Cache: cache,
172+
Token: token,
173+
}
174+
}
175+
176+
// GetIPInfo returns the Plus details for the specified IP.
177+
func (c *PlusClient) GetIPInfo(ip net.IP) (*Plus, error) {
178+
if ip != nil && isBogon(netip.MustParseAddr(ip.String())) {
179+
bogonResponse := new(Plus)
180+
bogonResponse.Bogon = true
181+
bogonResponse.IP = ip
182+
return bogonResponse, nil
183+
}
184+
relUrl := ""
185+
if ip != nil {
186+
relUrl = ip.String()
187+
}
188+
189+
if c.Cache != nil {
190+
if res, err := c.Cache.Get(cacheKey(relUrl)); err == nil {
191+
return res.(*Plus), nil
192+
}
193+
}
194+
195+
req, err := c.newRequest(nil, "GET", relUrl, nil)
196+
if err != nil {
197+
return nil, err
198+
}
199+
200+
res := new(Plus)
201+
if _, err := c.do(req, res); err != nil {
202+
return nil, err
203+
}
204+
205+
res.enrichGeo()
206+
207+
if c.Cache != nil {
208+
if err := c.Cache.Set(cacheKey(relUrl), res); err != nil {
209+
return res, err
210+
}
211+
}
212+
213+
return res, nil
214+
}
215+
216+
func (c *PlusClient) newRequest(ctx context.Context,
217+
method string,
218+
urlStr string,
219+
body io.Reader,
220+
) (*http.Request, error) {
221+
if ctx == nil {
222+
ctx = context.Background()
223+
}
224+
225+
u := new(url.URL)
226+
baseURL := c.BaseURL
227+
if rel, err := url.Parse(urlStr); err == nil {
228+
u = baseURL.ResolveReference(rel)
229+
} else if strings.ContainsRune(urlStr, ':') {
230+
// IPv6 strings fail to parse as URLs, so let's add it as a URL Path.
231+
*u = *baseURL
232+
u.Path += urlStr
233+
} else {
234+
return nil, err
235+
}
236+
237+
// get `http` package request object.
238+
req, err := http.NewRequestWithContext(ctx, method, u.String(), body)
239+
if err != nil {
240+
return nil, err
241+
}
242+
243+
// set common headers.
244+
req.Header.Set("Accept", "application/json")
245+
if c.UserAgent != "" {
246+
req.Header.Set("User-Agent", c.UserAgent)
247+
}
248+
if c.Token != "" {
249+
req.Header.Set("Authorization", "Bearer "+c.Token)
250+
}
251+
252+
return req, nil
253+
}
254+
255+
func (c *PlusClient) do(
256+
req *http.Request,
257+
v interface{},
258+
) (*http.Response, error) {
259+
resp, err := c.client.Do(req)
260+
if err != nil {
261+
return nil, err
262+
}
263+
defer resp.Body.Close()
264+
265+
err = checkResponse(resp)
266+
if err != nil {
267+
// even though there was an error, we still return the response
268+
// in case the caller wants to inspect it further
269+
return resp, err
270+
}
271+
272+
if v != nil {
273+
if w, ok := v.(io.Writer); ok {
274+
io.Copy(w, resp.Body)
275+
} else {
276+
err = json.NewDecoder(resp.Body).Decode(v)
277+
if err == io.EOF {
278+
// ignore EOF errors caused by empty response body
279+
err = nil
280+
}
281+
}
282+
}
283+
284+
return resp, err
285+
}
286+
287+
// GetIPInfoPlus returns the Plus details for the specified IP.
288+
func GetIPInfoPlus(ip net.IP) (*Plus, error) {
289+
return DefaultPlusClient.GetIPInfo(ip)
290+
}

0 commit comments

Comments
 (0)