Skip to content

Commit 1ebcde4

Browse files
committed
Add support for Core bundle
1 parent 4441fd1 commit 1ebcde4

File tree

3 files changed

+282
-2
lines changed

3 files changed

+282
-2
lines changed

example/lookup-core/main.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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+
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\n", info.Geo.Region)
26+
fmt.Printf("Country: %s (%s)\n", info.Geo.Country, info.Geo.CountryCode)
27+
fmt.Printf("Location: %f, %f\n", info.Geo.Latitude, info.Geo.Longitude)
28+
fmt.Printf("Timezone: %s\n", info.Geo.Timezone)
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+
}
35+
fmt.Printf("Anycast: %v\n", info.IsAnycast)
36+
fmt.Printf("Hosting: %v\n", info.IsHosting)
37+
}

ipinfo/core_new.go

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

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)