-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathipdata.go
More file actions
199 lines (176 loc) · 5.57 KB
/
Copy pathipdata.go
File metadata and controls
199 lines (176 loc) · 5.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
// Package ipdata is the official Go client for the ipdata.info IP geolocation,
// ASN, and threat-intelligence API. See https://ipdata.info.
package ipdata
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"time"
)
const (
// DefaultBaseURL is the free tier host (50 req/min, no key).
DefaultBaseURL = "https://ipdata.info"
// ProBaseURL is the paid host used automatically when an API key is set.
ProBaseURL = "https://pro.ipdata.info"
version = "0.1.0"
userAgent = "ipdata-go/" + version
)
// Client is an ipdata.info API client. The zero value is not usable; call New.
type Client struct {
apiKey string
baseURL string
http *http.Client
}
// Option configures a Client.
type Option func(*Client)
// WithAPIKey sets the API key (sent as the X-Api-Key header) and, unless a base
// URL was set explicitly, switches the default host to the pro tier.
func WithAPIKey(key string) Option { return func(c *Client) { c.apiKey = key } }
// WithBaseURL overrides the API host (no trailing slash).
func WithBaseURL(u string) Option { return func(c *Client) { c.baseURL = u } }
// WithHTTPClient supplies a custom *http.Client (e.g. for a proxy or timeout).
func WithHTTPClient(h *http.Client) Option { return func(c *Client) { c.http = h } }
// New builds a Client. Without options it targets the free tier.
func New(opts ...Option) *Client {
c := &Client{http: &http.Client{Timeout: 10 * time.Second}}
for _, o := range opts {
o(c)
}
if c.baseURL == "" {
if c.apiKey != "" {
c.baseURL = ProBaseURL
} else {
c.baseURL = DefaultBaseURL
}
}
return c
}
// Lookup returns the full record for ip. Pass "" for the caller's own IP.
func (c *Client) Lookup(ctx context.Context, ip string) (*IPInfo, error) {
var out IPInfo
if err := c.get(ctx, "/json/"+url.PathEscape(ip), &out); err != nil {
return nil, err
}
return &out, nil
}
// Geo returns the compact geo subset for ip.
func (c *Client) Geo(ctx context.Context, ip string) (*GeoInfo, error) {
var out GeoInfo
if err := c.get(ctx, "/api/v1/"+url.PathEscape(ip)+"/geo", &out); err != nil {
return nil, err
}
return &out, nil
}
// ASN returns the compact ASN subset for ip.
func (c *Client) ASN(ctx context.Context, ip string) (*ASNBrief, error) {
var out ASNBrief
if err := c.get(ctx, "/api/v1/"+url.PathEscape(ip)+"/asn", &out); err != nil {
return nil, err
}
return &out, nil
}
// Batch looks up many IPs at once. Requires an API key (paid tier).
func (c *Client) Batch(ctx context.Context, ips []string) ([]IPInfo, error) {
body, err := json.Marshal(ips)
if err != nil {
return nil, err
}
var out []IPInfo
if err := c.do(ctx, http.MethodPost, "/api/v1/batch", bytes.NewReader(body), &out); err != nil {
return nil, err
}
return out, nil
}
// ASNDetail returns the detailed ASN record (prefixes, peering) as a raw map.
func (c *Client) ASNDetail(ctx context.Context, number int) (map[string]any, error) {
var out map[string]any
if err := c.get(ctx, "/api/v1/asn/"+strconv.Itoa(number), &out); err != nil {
return nil, err
}
return out, nil
}
// ASNWhoisHistory returns the ASN whois history as a raw map.
func (c *Client) ASNWhoisHistory(ctx context.Context, number int) (map[string]any, error) {
var out map[string]any
if err := c.get(ctx, "/api/v1/asn/"+strconv.Itoa(number)+"/whois-history", &out); err != nil {
return nil, err
}
return out, nil
}
// ASNChanges returns the ASN change feed as a raw value.
func (c *Client) ASNChanges(ctx context.Context) (any, error) {
var out any
if err := c.get(ctx, "/api/v1/asn-changes", &out); err != nil {
return nil, err
}
return out, nil
}
// ThreatDomain looks up a domain in the threat-intel store.
func (c *Client) ThreatDomain(ctx context.Context, domain string) (*ThreatMatch, error) {
return c.threat(ctx, "/api/v1/threat/domain/"+url.PathEscape(domain))
}
// ThreatHash looks up a file hash (md5/sha1/sha256).
func (c *Client) ThreatHash(ctx context.Context, hash string) (*ThreatMatch, error) {
return c.threat(ctx, "/api/v1/threat/hash/"+url.PathEscape(hash))
}
// ThreatURL looks up a URL (server falls back to its domain on a miss).
func (c *Client) ThreatURL(ctx context.Context, u string) (*ThreatMatch, error) {
return c.threat(ctx, "/api/v1/threat/url?u="+url.QueryEscape(u))
}
func (c *Client) threat(ctx context.Context, path string) (*ThreatMatch, error) {
var out ThreatMatch
if err := c.get(ctx, path, &out); err != nil {
return nil, err
}
return &out, nil
}
func (c *Client) get(ctx context.Context, path string, out any) error {
return c.do(ctx, http.MethodGet, path, nil, out)
}
func (c *Client) do(ctx context.Context, method, path string, body io.Reader, out any) error {
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, body)
if err != nil {
return err
}
req.Header.Set("User-Agent", userAgent)
req.Header.Set("Accept", "application/json")
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
if c.apiKey != "" {
req.Header.Set("X-Api-Key", c.apiKey)
}
resp, err := c.http.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return &APIError{Status: resp.StatusCode, Message: apiErrMessage(data)}
}
if out == nil {
return nil
}
if err := json.Unmarshal(data, out); err != nil {
return fmt.Errorf("ipdata: decode response: %w", err)
}
return nil
}
func apiErrMessage(data []byte) string {
var e struct {
Error string `json:"error"`
}
if json.Unmarshal(data, &e) == nil && e.Error != "" {
return e.Error
}
return string(data)
}