1package ipinfo
2
3import (
4	"context"
5	"encoding/json"
6	"fmt"
7	"io"
8	"io/ioutil"
9	"net/http"
10	"net/url"
11	"strings"
12)
13
14const (
15	defaultBaseURL   = "https://ipinfo.io/"
16	defaultUserAgent = "IPinfoClient/Go/2.6.0"
17)
18
19// A Client is the main handler to communicate with the IPinfo API.
20type Client struct {
21	// HTTP client used to communicate with the API.
22	client *http.Client
23
24	// Base URL for API requests. BaseURL should always be specified with a
25	// trailing slash.
26	BaseURL *url.URL
27
28	// User agent used when communicating with the IPinfo API.
29	UserAgent string
30
31	// Cache interface implementation to prevent API quota overuse for
32	// identical requests.
33	Cache *Cache
34
35	// The API token used for authorization for more data and higher limits.
36	Token string
37}
38
39// NewClient returns a new IPinfo API client.
40//
41// If `httpClient` is nil, `http.DefaultClient` will be used.
42//
43// If `cache` is nil, no cache is automatically assigned. You may set one later
44// at any time with `client.SetCache`.
45//
46// If `token` is empty, the API will be queried without any token. You may set
47// one later at any time with `client.SetToken`.
48func NewClient(
49	httpClient *http.Client,
50	cache *Cache,
51	token string,
52) *Client {
53	if httpClient == nil {
54		httpClient = http.DefaultClient
55	}
56
57	baseURL, _ := url.Parse(defaultBaseURL)
58	return &Client{
59		client:    httpClient,
60		BaseURL:   baseURL,
61		UserAgent: defaultUserAgent,
62		Cache:     cache,
63		Token:     token,
64	}
65}
66
67// `newRequest` creates an API request. A relative URL can be provided in
68// urlStr, in which case it is resolved relative to the BaseURL of the Client.
69// Relative URLs should always be specified without a preceding slash.
70func (c *Client) newRequest(
71	ctx context.Context,
72	method string,
73	urlStr string,
74	body io.Reader,
75) (*http.Request, error) {
76	if ctx == nil {
77		ctx = context.Background()
78	}
79
80	u := new(url.URL)
81
82	// get final URL path.
83	if rel, err := url.Parse(urlStr); err == nil {
84		u = c.BaseURL.ResolveReference(rel)
85	} else if strings.ContainsRune(urlStr, ':') {
86		// IPv6 strings fail to parse as URLs, so let's add it as a URL Path.
87		*u = *c.BaseURL
88		u.Path += urlStr
89	} else {
90		return nil, err
91	}
92
93	// get `http` package request object.
94	req, err := http.NewRequestWithContext(ctx, method, u.String(), body)
95	if err != nil {
96		return nil, err
97	}
98
99	// set common headers.
100	req.Header.Set("Accept", "application/json")
101	if c.UserAgent != "" {
102		req.Header.Set("User-Agent", c.UserAgent)
103	}
104	if c.Token != "" {
105		req.Header.Set("Authorization", "Bearer "+c.Token)
106	}
107
108	return req, nil
109}
110
111// `do` sends an API request and returns the API response. The API response is
112// JSON decoded and stored in the value pointed to by v, or returned as an
113// error if an API error has occurred. If v implements the io.Writer interface,
114// the raw response body will be written to v, without attempting to first
115// decode it.
116func (c *Client) do(
117	req *http.Request,
118	v interface{},
119) (*http.Response, error) {
120	resp, err := c.client.Do(req)
121	if err != nil {
122		return nil, err
123	}
124	defer resp.Body.Close()
125
126	err = checkResponse(resp)
127	if err != nil {
128		// even though there was an error, we still return the response
129		// in case the caller wants to inspect it further
130		return resp, err
131	}
132
133	if v != nil {
134		if w, ok := v.(io.Writer); ok {
135			io.Copy(w, resp.Body)
136		} else {
137			err = json.NewDecoder(resp.Body).Decode(v)
138			if err == io.EOF {
139				// ignore EOF errors caused by empty response body
140				err = nil
141			}
142		}
143	}
144
145	return resp, err
146}
147
148// An ErrorResponse reports an error caused by an API request.
149type ErrorResponse struct {
150	// HTTP response that caused this error
151	Response *http.Response
152
153	// Error structure returned by the IPinfo Core API.
154	Status string `json:"status"`
155	Err    struct {
156		Title   string `json:"title"`
157		Message string `json:"message"`
158	} `json:"error"`
159}
160
161func (r *ErrorResponse) Error() string {
162	return fmt.Sprintf("%v %v: %d %v",
163		r.Response.Request.Method, r.Response.Request.URL,
164		r.Response.StatusCode, r.Err)
165}
166
167// `checkResponse` checks the API response for errors, and returns them if
168// present. A response is considered an error if it has a status code outside
169// the 200 range.
170func checkResponse(r *http.Response) error {
171	if c := r.StatusCode; 200 <= c && c <= 299 {
172		return nil
173	}
174	errorResponse := &ErrorResponse{Response: r}
175	data, err := ioutil.ReadAll(r.Body)
176	if err == nil && data != nil {
177		json.Unmarshal(data, errorResponse)
178	}
179	return errorResponse
180}
181
182/* SetCache */
183
184// SetCache assigns a cache to the package-level client.
185func SetCache(cache *Cache) {
186	DefaultClient.SetCache(cache)
187}
188
189// SetCache assigns a cache to the client `c`.
190func (c *Client) SetCache(cache *Cache) {
191	c.Cache = cache
192}
193
194/* SetToken */
195
196// SetToken assigns a token to the package-level client.
197func SetToken(token string) {
198	DefaultClient.SetToken(token)
199}
200
201// SetToken assigns a token to the client `c`.
202func (c *Client) SetToken(token string) {
203	c.Token = token
204}
205