2017-02-07 21:33:23 +00:00
|
|
|
package rest
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"strconv"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
clientVersion = "2.0.0"
|
|
|
|
defaultEndpoint = "https://api.nsone.net/v1/"
|
|
|
|
defaultUserAgent = "go-ns1/" + clientVersion
|
|
|
|
|
|
|
|
headerAuth = "X-NSONE-Key"
|
|
|
|
headerRateLimit = "X-Ratelimit-Limit"
|
|
|
|
headerRateRemaining = "X-Ratelimit-Remaining"
|
|
|
|
headerRatePeriod = "X-Ratelimit-Period"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Doer is a single method interface that allows a user to extend/augment an http.Client instance.
|
|
|
|
// Note: http.Client satisfies the Doer interface.
|
|
|
|
type Doer interface {
|
|
|
|
Do(*http.Request) (*http.Response, error)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Client manages communication with the NS1 Rest API.
|
|
|
|
type Client struct {
|
|
|
|
// httpClient handles all rest api communication,
|
|
|
|
// and expects an *http.Client.
|
|
|
|
httpClient Doer
|
|
|
|
|
|
|
|
// NS1 rest endpoint, overrides default if given.
|
|
|
|
Endpoint *url.URL
|
|
|
|
|
|
|
|
// NS1 api key (value for http request header 'X-NSONE-Key').
|
|
|
|
APIKey string
|
|
|
|
|
|
|
|
// NS1 go rest user agent (value for http request header 'User-Agent').
|
|
|
|
UserAgent string
|
|
|
|
|
|
|
|
// Func to call after response is returned in Do
|
|
|
|
RateLimitFunc func(RateLimit)
|
|
|
|
|
|
|
|
// From the excellent github-go client.
|
|
|
|
common service // Reuse a single struct instead of allocating one for each service on the heap.
|
|
|
|
|
|
|
|
// Services used for communicating with different components of the NS1 API.
|
|
|
|
APIKeys *APIKeysService
|
|
|
|
DataFeeds *DataFeedsService
|
|
|
|
DataSources *DataSourcesService
|
|
|
|
Jobs *JobsService
|
|
|
|
Notifications *NotificationsService
|
|
|
|
Records *RecordsService
|
|
|
|
Settings *SettingsService
|
|
|
|
Teams *TeamsService
|
|
|
|
Users *UsersService
|
|
|
|
Warnings *WarningsService
|
|
|
|
Zones *ZonesService
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewClient constructs and returns a reference to an instantiated Client.
|
|
|
|
func NewClient(httpClient Doer, options ...func(*Client)) *Client {
|
|
|
|
endpoint, _ := url.Parse(defaultEndpoint)
|
|
|
|
|
|
|
|
if httpClient == nil {
|
|
|
|
httpClient = http.DefaultClient
|
|
|
|
}
|
|
|
|
|
|
|
|
c := &Client{
|
|
|
|
httpClient: httpClient,
|
|
|
|
Endpoint: endpoint,
|
|
|
|
RateLimitFunc: defaultRateLimitFunc,
|
|
|
|
UserAgent: defaultUserAgent,
|
|
|
|
}
|
|
|
|
|
|
|
|
c.common.client = c
|
|
|
|
c.APIKeys = (*APIKeysService)(&c.common)
|
|
|
|
c.DataFeeds = (*DataFeedsService)(&c.common)
|
|
|
|
c.DataSources = (*DataSourcesService)(&c.common)
|
|
|
|
c.Jobs = (*JobsService)(&c.common)
|
|
|
|
c.Notifications = (*NotificationsService)(&c.common)
|
|
|
|
c.Records = (*RecordsService)(&c.common)
|
|
|
|
c.Settings = (*SettingsService)(&c.common)
|
|
|
|
c.Teams = (*TeamsService)(&c.common)
|
|
|
|
c.Users = (*UsersService)(&c.common)
|
|
|
|
c.Warnings = (*WarningsService)(&c.common)
|
|
|
|
c.Zones = (*ZonesService)(&c.common)
|
|
|
|
|
|
|
|
for _, option := range options {
|
|
|
|
option(c)
|
|
|
|
}
|
|
|
|
return c
|
|
|
|
}
|
|
|
|
|
|
|
|
type service struct {
|
|
|
|
client *Client
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetHTTPClient sets a Client instances' httpClient.
|
|
|
|
func SetHTTPClient(httpClient Doer) func(*Client) {
|
|
|
|
return func(c *Client) { c.httpClient = httpClient }
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetAPIKey sets a Client instances' APIKey.
|
|
|
|
func SetAPIKey(key string) func(*Client) {
|
|
|
|
return func(c *Client) { c.APIKey = key }
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetEndpoint sets a Client instances' Endpoint.
|
|
|
|
func SetEndpoint(endpoint string) func(*Client) {
|
|
|
|
return func(c *Client) { c.Endpoint, _ = url.Parse(endpoint) }
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetUserAgent sets a Client instances' user agent.
|
|
|
|
func SetUserAgent(ua string) func(*Client) {
|
|
|
|
return func(c *Client) { c.UserAgent = ua }
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetRateLimitFunc sets a Client instances' RateLimitFunc.
|
|
|
|
func SetRateLimitFunc(ratefunc func(rl RateLimit)) func(*Client) {
|
|
|
|
return func(c *Client) { c.RateLimitFunc = ratefunc }
|
|
|
|
}
|
|
|
|
|
|
|
|
// Do satisfies the Doer interface.
|
|
|
|
func (c Client) Do(req *http.Request, v interface{}) (*http.Response, error) {
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
err = CheckResponse(resp)
|
|
|
|
if err != nil {
|
|
|
|
return resp, err
|
|
|
|
}
|
|
|
|
|
|
|
|
rl := parseRate(resp)
|
|
|
|
c.RateLimitFunc(rl)
|
|
|
|
|
|
|
|
if v != nil {
|
|
|
|
// Try to unmarshal body into given type using streaming decoder.
|
|
|
|
if err := json.NewDecoder(resp.Body).Decode(&v); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return resp, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewRequest constructs and returns a http.Request.
|
|
|
|
func (c *Client) NewRequest(method, path string, body interface{}) (*http.Request, error) {
|
|
|
|
rel, err := url.Parse(path)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
uri := c.Endpoint.ResolveReference(rel)
|
|
|
|
|
|
|
|
// Encode body as json
|
|
|
|
buf := new(bytes.Buffer)
|
|
|
|
if body != nil {
|
|
|
|
err := json.NewEncoder(buf).Encode(body)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
req, err := http.NewRequest(method, uri.String(), buf)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
req.Header.Add(headerAuth, c.APIKey)
|
|
|
|
req.Header.Add("User-Agent", c.UserAgent)
|
|
|
|
return req, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Response wraps stdlib http response.
|
|
|
|
type Response struct {
|
|
|
|
*http.Response
|
|
|
|
}
|
|
|
|
|
|
|
|
// Error contains all http responses outside the 2xx range.
|
|
|
|
type Error struct {
|
|
|
|
Resp *http.Response
|
|
|
|
Message string
|
|
|
|
}
|
|
|
|
|
|
|
|
// Satisfy std lib error interface.
|
|
|
|
func (re *Error) Error() string {
|
|
|
|
return fmt.Sprintf("%v %v: %d %v", re.Resp.Request.Method, re.Resp.Request.URL, re.Resp.StatusCode, re.Message)
|
|
|
|
}
|
|
|
|
|
|
|
|
// CheckResponse handles parsing of rest api errors. Returns nil if no error.
|
|
|
|
func CheckResponse(resp *http.Response) error {
|
|
|
|
if c := resp.StatusCode; c >= 200 && c <= 299 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
restErr := &Error{Resp: resp}
|
|
|
|
|
|
|
|
b, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if len(b) == 0 {
|
|
|
|
return restErr
|
|
|
|
}
|
|
|
|
|
|
|
|
err = json.Unmarshal(b, restErr)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return restErr
|
|
|
|
}
|
|
|
|
|
|
|
|
// RateLimitFunc is rate limiting strategy for the Client instance.
|
|
|
|
type RateLimitFunc func(RateLimit)
|
|
|
|
|
|
|
|
// RateLimit stores X-Ratelimit-* headers
|
|
|
|
type RateLimit struct {
|
|
|
|
Limit int
|
|
|
|
Remaining int
|
|
|
|
Period int
|
|
|
|
}
|
|
|
|
|
|
|
|
var defaultRateLimitFunc = func(rl RateLimit) {}
|
|
|
|
|
|
|
|
// PercentageLeft returns the ratio of Remaining to Limit as a percentage
|
|
|
|
func (rl RateLimit) PercentageLeft() int {
|
|
|
|
return rl.Remaining * 100 / rl.Limit
|
|
|
|
}
|
|
|
|
|
|
|
|
// WaitTime returns the time.Duration ratio of Period to Limit
|
|
|
|
func (rl RateLimit) WaitTime() time.Duration {
|
|
|
|
return (time.Second * time.Duration(rl.Period)) / time.Duration(rl.Limit)
|
|
|
|
}
|
|
|
|
|
|
|
|
// WaitTimeRemaining returns the time.Duration ratio of Period to Remaining
|
|
|
|
func (rl RateLimit) WaitTimeRemaining() time.Duration {
|
|
|
|
return (time.Second * time.Duration(rl.Period)) / time.Duration(rl.Remaining)
|
|
|
|
}
|
|
|
|
|
|
|
|
// RateLimitStrategySleep sets RateLimitFunc to sleep by WaitTimeRemaining
|
|
|
|
func (c *Client) RateLimitStrategySleep() {
|
|
|
|
c.RateLimitFunc = func(rl RateLimit) {
|
|
|
|
remaining := rl.WaitTimeRemaining()
|
|
|
|
time.Sleep(remaining)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// parseRate parses rate related headers from http response.
|
|
|
|
func parseRate(resp *http.Response) RateLimit {
|
|
|
|
var rl RateLimit
|
|
|
|
|
|
|
|
if limit := resp.Header.Get(headerRateLimit); limit != "" {
|
|
|
|
rl.Limit, _ = strconv.Atoi(limit)
|
|
|
|
}
|
|
|
|
if remaining := resp.Header.Get(headerRateRemaining); remaining != "" {
|
|
|
|
rl.Remaining, _ = strconv.Atoi(remaining)
|
|
|
|
}
|
|
|
|
if period := resp.Header.Get(headerRatePeriod); period != "" {
|
|
|
|
rl.Period, _ = strconv.Atoi(period)
|
|
|
|
}
|
|
|
|
|
|
|
|
return rl
|
|
|
|
}
|
2017-04-11 15:10:46 +00:00
|
|
|
|
|
|
|
// SetTimeParam sets a url timestamp query param given the parameters name.
|
|
|
|
func SetTimeParam(key string, t time.Time) func(*url.Values) {
|
|
|
|
return func(v *url.Values) { v.Set(key, strconv.Itoa(int(t.Unix()))) }
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetBoolParam sets a url boolean query param given the parameters name.
|
|
|
|
func SetBoolParam(key string, b bool) func(*url.Values) {
|
|
|
|
return func(v *url.Values) { v.Set(key, strconv.FormatBool(b)) }
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetStringParam sets a url string query param given the parameters name.
|
|
|
|
func SetStringParam(key, val string) func(*url.Values) {
|
|
|
|
return func(v *url.Values) { v.Set(key, val) }
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetIntParam sets a url integer query param given the parameters name.
|
|
|
|
func SetIntParam(key string, val int) func(*url.Values) {
|
|
|
|
return func(v *url.Values) { v.Set(key, strconv.Itoa(val)) }
|
|
|
|
}
|