Skip to content

Commit ec0270e

Browse files
authored
Merge pull request #69 from ipinfo/silvano/eng-500-add-core-bundle-support-in-ipinfogo-library
Add support for Core bundle
2 parents 4441fd1 + 1d57252 commit ec0270e

File tree

3 files changed

+271
-2
lines changed

3 files changed

+271
-2
lines changed

example/lookup-core/main.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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.NewCoreClient(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+
if info.Geo != nil {
23+
fmt.Printf("City: %s\n", info.Geo.City)
24+
fmt.Printf("Region: %s\n", info.Geo.Region)
25+
fmt.Printf("Country: %s (%s)\n", info.Geo.Country, info.Geo.CountryCode)
26+
fmt.Printf("Location: %f, %f\n", info.Geo.Latitude, info.Geo.Longitude)
27+
fmt.Printf("Timezone: %s\n", info.Geo.Timezone)
28+
fmt.Printf("Postal Code: %s\n", info.Geo.PostalCode)
29+
}
30+
if info.AS != nil {
31+
fmt.Printf("ASN: %s\n", info.AS.ASN)
32+
fmt.Printf("AS Name: %s\n", info.AS.Name)
33+
fmt.Printf("AS Domain: %s\n", info.AS.Domain)
34+
fmt.Printf("AS Type: %s\n", info.AS.Type)
35+
}
36+
fmt.Printf("Anonymous: %v\n", info.IsAnonymous)
37+
fmt.Printf("Anycast: %v\n", info.IsAnycast)
38+
fmt.Printf("Hosting: %v\n", info.IsHosting)
39+
fmt.Printf("Mobile: %v\n", info.IsMobile)
40+
fmt.Printf("Satellite: %v\n", info.IsSatellite)
41+
}

ipinfo/core_new.go

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
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+
defaultCoreBaseURL = "https://api.ipinfo.io/lookup/"
16+
)
17+
18+
// CoreClient is a client for the IPinfo Core API.
19+
type CoreClient 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+
// CoreResponse represents the response from the IPinfo Core API /lookup endpoint.
37+
type CoreResponse struct {
38+
IP net.IP `json:"ip"`
39+
Bogon bool `json:"bogon,omitempty"`
40+
Geo *CoreGeo `json:"geo,omitempty"`
41+
AS *CoreAS `json:"as,omitempty"`
42+
IsAnonymous bool `json:"is_anonymous"`
43+
IsAnycast bool `json:"is_anycast"`
44+
IsHosting bool `json:"is_hosting"`
45+
IsMobile bool `json:"is_mobile"`
46+
IsSatellite bool `json:"is_satellite"`
47+
}
48+
49+
// CoreGeo represents the geo object in Core API response.
50+
type CoreGeo struct {
51+
City string `json:"city,omitempty"`
52+
Region string `json:"region,omitempty"`
53+
RegionCode string `json:"region_code,omitempty"`
54+
Country string `json:"country,omitempty"`
55+
CountryCode string `json:"country_code,omitempty"`
56+
Continent string `json:"continent,omitempty"`
57+
ContinentCode string `json:"continent_code,omitempty"`
58+
Latitude float64 `json:"latitude"`
59+
Longitude float64 `json:"longitude"`
60+
Timezone string `json:"timezone,omitempty"`
61+
PostalCode string `json:"postal_code,omitempty"`
62+
63+
// Extended fields using the same country data as legacy Core API
64+
CountryName string `json:"-"`
65+
IsEU bool `json:"-"`
66+
CountryFlag CountryFlag `json:"-"`
67+
CountryFlagURL string `json:"-"`
68+
CountryCurrency CountryCurrency `json:"-"`
69+
ContinentInfo Continent `json:"-"`
70+
}
71+
72+
// CoreAS represents the AS object in Core API response.
73+
type CoreAS struct {
74+
ASN string `json:"asn"`
75+
Name string `json:"name"`
76+
Domain string `json:"domain"`
77+
Type string `json:"type"`
78+
}
79+
80+
func (v *CoreResponse) enrichGeo() {
81+
if v.Geo != nil && v.Geo.CountryCode != "" {
82+
v.Geo.CountryName = GetCountryName(v.Geo.CountryCode)
83+
v.Geo.IsEU = IsEU(v.Geo.CountryCode)
84+
v.Geo.CountryFlag.Emoji = GetCountryFlagEmoji(v.Geo.CountryCode)
85+
v.Geo.CountryFlag.Unicode = GetCountryFlagUnicode(v.Geo.CountryCode)
86+
v.Geo.CountryFlagURL = GetCountryFlagURL(v.Geo.CountryCode)
87+
v.Geo.CountryCurrency.Code = GetCountryCurrencyCode(v.Geo.CountryCode)
88+
v.Geo.CountryCurrency.Symbol = GetCountryCurrencySymbol(v.Geo.CountryCode)
89+
v.Geo.ContinentInfo.Code = GetContinentCode(v.Geo.CountryCode)
90+
v.Geo.ContinentInfo.Name = GetContinentName(v.Geo.CountryCode)
91+
}
92+
}
93+
94+
// NewCoreClient creates a new IPinfo Core API client.
95+
func NewCoreClient(httpClient *http.Client, cache *Cache, token string) *CoreClient {
96+
if httpClient == nil {
97+
httpClient = http.DefaultClient
98+
}
99+
100+
baseURL, _ := url.Parse(defaultCoreBaseURL)
101+
return &CoreClient{
102+
client: httpClient,
103+
BaseURL: baseURL,
104+
UserAgent: defaultUserAgent,
105+
Cache: cache,
106+
Token: token,
107+
}
108+
}
109+
110+
// GetIPInfo returns the Core details for the specified IP.
111+
func (c *CoreClient) GetIPInfo(ip net.IP) (*CoreResponse, error) {
112+
if ip != nil && isBogon(netip.MustParseAddr(ip.String())) {
113+
bogonResponse := new(CoreResponse)
114+
bogonResponse.Bogon = true
115+
bogonResponse.IP = ip
116+
return bogonResponse, nil
117+
}
118+
relUrl := ""
119+
if ip != nil {
120+
relUrl = ip.String()
121+
}
122+
123+
if c.Cache != nil {
124+
if res, err := c.Cache.Get(cacheKey(relUrl)); err == nil {
125+
return res.(*CoreResponse), nil
126+
}
127+
}
128+
129+
req, err := c.newRequest(nil, "GET", relUrl, nil)
130+
if err != nil {
131+
return nil, err
132+
}
133+
134+
res := new(CoreResponse)
135+
if _, err := c.do(req, res); err != nil {
136+
return nil, err
137+
}
138+
139+
res.enrichGeo()
140+
141+
if c.Cache != nil {
142+
if err := c.Cache.Set(cacheKey(relUrl), res); err != nil {
143+
return res, err
144+
}
145+
}
146+
147+
return res, nil
148+
}
149+
150+
func (c *CoreClient) newRequest(ctx context.Context,
151+
method string,
152+
urlStr string,
153+
body io.Reader,
154+
) (*http.Request, error) {
155+
if ctx == nil {
156+
ctx = context.Background()
157+
}
158+
159+
u := new(url.URL)
160+
baseURL := c.BaseURL
161+
if rel, err := url.Parse(urlStr); err == nil {
162+
u = baseURL.ResolveReference(rel)
163+
} else if strings.ContainsRune(urlStr, ':') {
164+
// IPv6 strings fail to parse as URLs, so let's add it as a URL Path.
165+
*u = *baseURL
166+
u.Path += urlStr
167+
} else {
168+
return nil, err
169+
}
170+
171+
// get `http` package request object.
172+
req, err := http.NewRequestWithContext(ctx, method, u.String(), body)
173+
if err != nil {
174+
return nil, err
175+
}
176+
177+
// set common headers.
178+
req.Header.Set("Accept", "application/json")
179+
if c.UserAgent != "" {
180+
req.Header.Set("User-Agent", c.UserAgent)
181+
}
182+
if c.Token != "" {
183+
req.Header.Set("Authorization", "Bearer "+c.Token)
184+
}
185+
186+
return req, nil
187+
}
188+
189+
func (c *CoreClient) do(
190+
req *http.Request,
191+
v interface{},
192+
) (*http.Response, error) {
193+
resp, err := c.client.Do(req)
194+
if err != nil {
195+
return nil, err
196+
}
197+
defer resp.Body.Close()
198+
199+
err = checkResponse(resp)
200+
if err != nil {
201+
// even though there was an error, we still return the response
202+
// in case the caller wants to inspect it further
203+
return resp, err
204+
}
205+
206+
if v != nil {
207+
if w, ok := v.(io.Writer); ok {
208+
io.Copy(w, resp.Body)
209+
} else {
210+
err = json.NewDecoder(resp.Body).Decode(v)
211+
if err == io.EOF {
212+
// ignore EOF errors caused by empty response body
213+
err = nil
214+
}
215+
}
216+
}
217+
218+
return resp, err
219+
}
220+
221+
// GetIPInfoCore returns the Core details for the specified IP.
222+
func GetIPInfoCore(ip net.IP) (*CoreResponse, error) {
223+
return DefaultCoreClient.GetIPInfo(ip)
224+
}

ipinfo/ipinfo.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@ package ipinfo
33
// DefaultClient is the package-level client available to the user.
44
var DefaultClient *Client
55

6-
// Package-level lite client
6+
// Package-level Lite bundle client
77
var DefaultLiteClient *LiteClient
88

9+
// Package-level Core bundle client
10+
var DefaultCoreClient *CoreClient
11+
912
func init() {
10-
// Create two global clients, one for Core and one for Lite API
13+
// Create global clients for legacy, Lite, Core APIs
1114
DefaultClient = NewClient(nil, nil, "")
1215
DefaultLiteClient = NewLiteClient(nil, nil, "")
16+
DefaultCoreClient = NewCoreClient(nil, nil, "")
1317
}

0 commit comments

Comments
 (0)