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