github.com/fastly/go-fastly/v6@v6.8.0/fastly/client.go (about)

     1  package fastly
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"mime/multipart"
     9  	"net/http"
    10  	"net/url"
    11  	"os"
    12  	"path/filepath"
    13  	"runtime"
    14  	"strconv"
    15  	"strings"
    16  	"sync"
    17  	"time"
    18  
    19  	"github.com/google/go-querystring/query"
    20  	"github.com/google/jsonapi"
    21  	"github.com/hashicorp/go-cleanhttp"
    22  	"github.com/mitchellh/mapstructure"
    23  )
    24  
    25  // APIKeyEnvVar is the name of the environment variable where the Fastly API
    26  // key should be read from.
    27  const APIKeyEnvVar = "FASTLY_API_KEY"
    28  
    29  // APIKeyHeader is the name of the header that contains the Fastly API key.
    30  const APIKeyHeader = "Fastly-Key"
    31  
    32  // EndpointEnvVar is the name of an environment variable that can be used
    33  // to change the URL of API requests.
    34  const EndpointEnvVar = "FASTLY_API_URL"
    35  
    36  // DefaultEndpoint is the default endpoint for Fastly. Since Fastly does not
    37  // support an on-premise solution, this is likely to always be the default.
    38  const DefaultEndpoint = "https://api.fastly.com"
    39  
    40  // RealtimeStatsEndpointEnvVar is the name of an environment variable that can be used
    41  // to change the URL of realtime stats requests.
    42  const RealtimeStatsEndpointEnvVar = "FASTLY_RTS_URL"
    43  
    44  // DefaultRealtimeStatsEndpoint is the realtime stats endpoint for Fastly.
    45  const DefaultRealtimeStatsEndpoint = "https://rt.fastly.com"
    46  
    47  // ProjectURL is the url for this library.
    48  var ProjectURL = "github.com/fastly/go-fastly"
    49  
    50  // ProjectVersion is the version of this library.
    51  var ProjectVersion = "6.8.0"
    52  
    53  // UserAgent is the user agent for this particular client.
    54  var UserAgent = fmt.Sprintf("FastlyGo/%s (+%s; %s)",
    55  	ProjectVersion, ProjectURL, runtime.Version())
    56  
    57  // Client is the main entrypoint to the Fastly golang API library.
    58  type Client struct {
    59  	// Address is the address of Fastly's API endpoint.
    60  	Address string
    61  
    62  	// HTTPClient is the HTTP client to use. If one is not provided, a default
    63  	// client will be used.
    64  	HTTPClient *http.Client
    65  
    66  	// updateLock forces serialization of calls that modify a service.
    67  	// Concurrent modifications have undefined semantics.
    68  	updateLock sync.Mutex
    69  
    70  	// apiKey is the Fastly API key to authenticate requests.
    71  	apiKey string
    72  
    73  	// url is the parsed URL from Address
    74  	url *url.URL
    75  
    76  	// remaining is last observed value of http header Fastly-RateLimit-Remaining
    77  	remaining int
    78  
    79  	// reset is last observed value of http header Fastly-RateLimit-Reset
    80  	reset int64
    81  }
    82  
    83  // RTSClient is the entrypoint to the Fastly's Realtime Stats API.
    84  type RTSClient struct {
    85  	client *Client
    86  }
    87  
    88  // DefaultClient instantiates a new Fastly API client. This function requires
    89  // the environment variable `FASTLY_API_KEY` is set and contains a valid API key
    90  // to authenticate with Fastly.
    91  func DefaultClient() *Client {
    92  	client, err := NewClient(os.Getenv(APIKeyEnvVar))
    93  	if err != nil {
    94  		panic(err)
    95  	}
    96  	return client
    97  }
    98  
    99  // NewClient creates a new API client with the given key and the default API
   100  // endpoint. Because Fastly allows some requests without an API key, this
   101  // function will not error if the API token is not supplied. Attempts to make a
   102  // request that requires an API key will return a 403 response.
   103  func NewClient(key string) (*Client, error) {
   104  	endpoint, ok := os.LookupEnv(EndpointEnvVar)
   105  
   106  	if !ok {
   107  		endpoint = DefaultEndpoint
   108  	}
   109  
   110  	return NewClientForEndpoint(key, endpoint)
   111  }
   112  
   113  // NewClientForEndpoint creates a new API client with the given key and API
   114  // endpoint. Because Fastly allows some requests without an API key, this
   115  // function will not error if the API token is not supplied. Attempts to make a
   116  // request that requires an API key will return a 403 response.
   117  func NewClientForEndpoint(key string, endpoint string) (*Client, error) {
   118  	client := &Client{apiKey: key, Address: endpoint}
   119  	return client.init()
   120  }
   121  
   122  // NewRealtimeStatsClient instantiates a new Fastly API client for the realtime stats.
   123  // This function requires the environment variable `FASTLY_API_KEY` is set and contains
   124  // a valid API key to authenticate with Fastly.
   125  func NewRealtimeStatsClient() *RTSClient {
   126  	endpoint, ok := os.LookupEnv(RealtimeStatsEndpointEnvVar)
   127  
   128  	if !ok {
   129  		endpoint = DefaultRealtimeStatsEndpoint
   130  	}
   131  
   132  	c, err := NewClientForEndpoint(os.Getenv(APIKeyEnvVar), endpoint)
   133  	if err != nil {
   134  		panic(err)
   135  	}
   136  	return &RTSClient{client: c}
   137  }
   138  
   139  // NewRealtimeStatsClientForEndpoint creates an RTSClient from a token and endpoint url.
   140  // `token` is a Fastly API token and `endpoint` is RealtimeStatsEndpoint for the production
   141  // realtime stats API.
   142  func NewRealtimeStatsClientForEndpoint(token, endpoint string) (*RTSClient, error) {
   143  	c, err := NewClientForEndpoint(token, endpoint)
   144  	if err != nil {
   145  		return nil, err
   146  	}
   147  	return &RTSClient{client: c}, nil
   148  }
   149  
   150  func (c *Client) init() (*Client, error) {
   151  	// Until we do a request, we don't know how many are left.
   152  	// Use the default limit as a first guess:
   153  	// https://developer.fastly.com/reference/api/#rate-limiting
   154  	c.remaining = 1000
   155  
   156  	u, err := url.Parse(c.Address)
   157  	if err != nil {
   158  		return nil, err
   159  	}
   160  	c.url = u
   161  
   162  	if c.HTTPClient == nil {
   163  		c.HTTPClient = cleanhttp.DefaultClient()
   164  	}
   165  
   166  	return c, nil
   167  }
   168  
   169  // RateLimitRemaining returns the number of non-read requests left before
   170  // rate limiting causes a 429 Too Many Requests error.
   171  func (c *Client) RateLimitRemaining() int {
   172  	return c.remaining
   173  }
   174  
   175  // RateLimitReset returns the next time the rate limiter's counter will be
   176  // reset.
   177  func (c *Client) RateLimitReset() time.Time {
   178  	return time.Unix(c.reset, 0)
   179  }
   180  
   181  // Get issues an HTTP GET request.
   182  func (c *Client) Get(p string, ro *RequestOptions) (*http.Response, error) {
   183  	if ro == nil {
   184  		ro = new(RequestOptions)
   185  	}
   186  	ro.Parallel = true
   187  	return c.Request("GET", p, ro)
   188  }
   189  
   190  // Head issues an HTTP HEAD request.
   191  func (c *Client) Head(p string, ro *RequestOptions) (*http.Response, error) {
   192  	if ro == nil {
   193  		ro = new(RequestOptions)
   194  	}
   195  	ro.Parallel = true
   196  	return c.Request("HEAD", p, ro)
   197  }
   198  
   199  // Patch issues an HTTP PATCH request.
   200  func (c *Client) Patch(p string, ro *RequestOptions) (*http.Response, error) {
   201  	return c.Request("PATCH", p, ro)
   202  }
   203  
   204  // PatchForm issues an HTTP PUT request with the given interface form-encoded.
   205  func (c *Client) PatchForm(p string, i interface{}, ro *RequestOptions) (*http.Response, error) {
   206  	return c.RequestForm("PATCH", p, i, ro)
   207  }
   208  
   209  // PatchJSON issues an HTTP PUT request with the given interface json-encoded.
   210  func (c *Client) PatchJSON(p string, i interface{}, ro *RequestOptions) (*http.Response, error) {
   211  	return c.RequestJSON("PATCH", p, i, ro)
   212  }
   213  
   214  // PatchJSONAPI issues an HTTP PUT request with the given interface json-encoded.
   215  func (c *Client) PatchJSONAPI(p string, i interface{}, ro *RequestOptions) (*http.Response, error) {
   216  	return c.RequestJSONAPI("PATCH", p, i, ro)
   217  }
   218  
   219  // Post issues an HTTP POST request.
   220  func (c *Client) Post(p string, ro *RequestOptions) (*http.Response, error) {
   221  	return c.Request("POST", p, ro)
   222  }
   223  
   224  // PostForm issues an HTTP POST request with the given interface form-encoded.
   225  func (c *Client) PostForm(p string, i interface{}, ro *RequestOptions) (*http.Response, error) {
   226  	return c.RequestForm("POST", p, i, ro)
   227  }
   228  
   229  // PostJSON issues an HTTP POST request with the given interface json-encoded.
   230  func (c *Client) PostJSON(p string, i interface{}, ro *RequestOptions) (*http.Response, error) {
   231  	return c.RequestJSON("POST", p, i, ro)
   232  }
   233  
   234  // PostJSONAPI issues an HTTP POST request with the given interface json-encoded.
   235  func (c *Client) PostJSONAPI(p string, i interface{}, ro *RequestOptions) (*http.Response, error) {
   236  	return c.RequestJSONAPI("POST", p, i, ro)
   237  }
   238  
   239  // PostJSONAPIBulk issues an HTTP POST request with the given interface json-encoded and bulk requests.
   240  func (c *Client) PostJSONAPIBulk(p string, i interface{}, ro *RequestOptions) (*http.Response, error) {
   241  	return c.RequestJSONAPIBulk("POST", p, i, ro)
   242  }
   243  
   244  // Put issues an HTTP PUT request.
   245  func (c *Client) Put(p string, ro *RequestOptions) (*http.Response, error) {
   246  	return c.Request("PUT", p, ro)
   247  }
   248  
   249  // PutForm issues an HTTP PUT request with the given interface form-encoded.
   250  func (c *Client) PutForm(p string, i interface{}, ro *RequestOptions) (*http.Response, error) {
   251  	return c.RequestForm("PUT", p, i, ro)
   252  }
   253  
   254  // PutFormFile issues an HTTP PUT request (multipart/form-encoded) to put a file to an endpoint.
   255  func (c *Client) PutFormFile(urlPath string, filePath string, fieldName string, ro *RequestOptions) (*http.Response, error) {
   256  	return c.RequestFormFile("PUT", urlPath, filePath, fieldName, ro)
   257  }
   258  
   259  // PutJSON issues an HTTP PUT request with the given interface json-encoded.
   260  func (c *Client) PutJSON(p string, i interface{}, ro *RequestOptions) (*http.Response, error) {
   261  	return c.RequestJSON("PUT", p, i, ro)
   262  }
   263  
   264  // PutJSONAPI issues an HTTP PUT request with the given interface json-encoded.
   265  func (c *Client) PutJSONAPI(p string, i interface{}, ro *RequestOptions) (*http.Response, error) {
   266  	return c.RequestJSONAPI("PUT", p, i, ro)
   267  }
   268  
   269  // Delete issues an HTTP DELETE request.
   270  func (c *Client) Delete(p string, ro *RequestOptions) (*http.Response, error) {
   271  	return c.Request("DELETE", p, ro)
   272  }
   273  
   274  // DeleteJSONAPI issues an HTTP DELETE request with the given interface json-encoded.
   275  func (c *Client) DeleteJSONAPI(p string, i interface{}, ro *RequestOptions) (*http.Response, error) {
   276  	return c.RequestJSONAPI("DELETE", p, i, ro)
   277  }
   278  
   279  // DeleteJSONAPIBulk issues an HTTP DELETE request with the given interface json-encoded and bulk requests.
   280  func (c *Client) DeleteJSONAPIBulk(p string, i interface{}, ro *RequestOptions) (*http.Response, error) {
   281  	return c.RequestJSONAPIBulk("DELETE", p, i, ro)
   282  }
   283  
   284  // Request makes an HTTP request against the HTTPClient using the given verb,
   285  // Path, and request options.
   286  func (c *Client) Request(verb, p string, ro *RequestOptions) (*http.Response, error) {
   287  	req, err := c.RawRequest(verb, p, ro)
   288  	if err != nil {
   289  		return nil, err
   290  	}
   291  
   292  	if ro == nil || !ro.Parallel {
   293  		c.updateLock.Lock()
   294  		defer c.updateLock.Unlock()
   295  
   296  	}
   297  	resp, err := checkResp(c.HTTPClient.Do(req))
   298  	if err != nil {
   299  		return resp, err
   300  	}
   301  
   302  	if verb != "GET" && verb != "HEAD" {
   303  		remaining := resp.Header.Get("Fastly-RateLimit-Remaining")
   304  		if remaining != "" {
   305  			if val, err := strconv.Atoi(remaining); err == nil {
   306  				c.remaining = val
   307  			}
   308  		}
   309  		reset := resp.Header.Get("Fastly-RateLimit-Reset")
   310  		if reset != "" {
   311  			if val, err := strconv.ParseInt(reset, 10, 64); err == nil {
   312  				c.reset = val
   313  			}
   314  		}
   315  	}
   316  
   317  	return resp, nil
   318  }
   319  
   320  // parseHealthCheckHeaders returns the serialised body with the custom health
   321  // check headers appended.
   322  //
   323  // NOTE: The Google query library we use for parsing and encoding the provided
   324  // struct values doesn't support the format `headers=["Foo: Bar"]` and so we
   325  // have to manually construct this format.
   326  func parseHealthCheckHeaders(s string) string {
   327  	headers := []string{}
   328  	result := []string{}
   329  	segs := strings.Split(s, "&")
   330  	for _, s := range segs {
   331  		if strings.HasPrefix(strings.ToLower(s), "headers=") {
   332  			v := strings.Split(s, "=")
   333  			if len(v) == 2 {
   334  				headers = append(headers, fmt.Sprintf("%q", strings.ReplaceAll(v[1], "%3A+", ":")))
   335  			}
   336  		} else {
   337  			result = append(result, s)
   338  		}
   339  	}
   340  	if len(headers) > 0 {
   341  		result = append(result, "headers=%5B"+strings.Join(headers, ",")+"%5D")
   342  	}
   343  	return strings.Join(result, "&")
   344  }
   345  
   346  // RequestForm makes an HTTP request with the given interface being encoded as
   347  // form data.
   348  func (c *Client) RequestForm(verb, p string, i interface{}, ro *RequestOptions) (*http.Response, error) {
   349  	if ro == nil {
   350  		ro = new(RequestOptions)
   351  	}
   352  
   353  	if ro.Headers == nil {
   354  		ro.Headers = make(map[string]string)
   355  	}
   356  	ro.Headers["Content-Type"] = "application/x-www-form-urlencoded"
   357  
   358  	v, err := query.Values(i)
   359  	if err != nil {
   360  		return nil, err
   361  	}
   362  
   363  	body := v.Encode()
   364  	if ro.HealthCheckHeaders {
   365  		body = parseHealthCheckHeaders(body)
   366  	}
   367  
   368  	ro.Body = strings.NewReader(body)
   369  	ro.BodyLength = int64(len(body))
   370  
   371  	return c.Request(verb, p, ro)
   372  }
   373  
   374  // RequestFormFile makes an HTTP request to upload a file to an endpoint.
   375  func (c *Client) RequestFormFile(verb, urlPath string, filePath string, fieldName string, ro *RequestOptions) (*http.Response, error) {
   376  	file, err := os.Open(filepath.Clean(filePath))
   377  	if err != nil {
   378  		return nil, fmt.Errorf("error reading file: %v", err)
   379  	}
   380  	defer file.Close() // #nosec G307
   381  
   382  	var body bytes.Buffer
   383  	writer := multipart.NewWriter(&body)
   384  	part, err := writer.CreateFormFile(fieldName, filepath.Base(filePath))
   385  	if err != nil {
   386  		return nil, fmt.Errorf("error creating multipart form: %v", err)
   387  	}
   388  
   389  	_, err = io.Copy(part, file)
   390  	if err != nil {
   391  		return nil, fmt.Errorf("error copying file to multipart form: %v", err)
   392  	}
   393  
   394  	err = writer.Close()
   395  	if err != nil {
   396  		return nil, fmt.Errorf("error closing multipart form: %v", err)
   397  	}
   398  
   399  	if ro == nil {
   400  		ro = new(RequestOptions)
   401  	}
   402  	if ro.Headers == nil {
   403  		ro.Headers = make(map[string]string)
   404  	}
   405  	ro.Headers["Content-Type"] = writer.FormDataContentType()
   406  	ro.Headers["Accept"] = "application/json"
   407  	ro.Body = &body
   408  	ro.BodyLength = int64(body.Len())
   409  
   410  	return c.Request(verb, urlPath, ro)
   411  }
   412  
   413  func (c *Client) RequestJSON(verb, p string, i interface{}, ro *RequestOptions) (*http.Response, error) {
   414  	if ro == nil {
   415  		ro = new(RequestOptions)
   416  	}
   417  
   418  	if ro.Headers == nil {
   419  		ro.Headers = make(map[string]string)
   420  	}
   421  	ro.Headers["Content-Type"] = "application/json"
   422  	ro.Headers["Accept"] = "application/json"
   423  
   424  	body, err := json.Marshal(i)
   425  	if err != nil {
   426  		return nil, err
   427  	}
   428  
   429  	ro.Body = bytes.NewReader(body)
   430  	ro.BodyLength = int64(len(body))
   431  
   432  	return c.Request(verb, p, ro)
   433  }
   434  
   435  func (c *Client) RequestJSONAPI(verb, p string, i interface{}, ro *RequestOptions) (*http.Response, error) {
   436  	if ro == nil {
   437  		ro = new(RequestOptions)
   438  	}
   439  
   440  	if ro.Headers == nil {
   441  		ro.Headers = make(map[string]string)
   442  	}
   443  	ro.Headers["Content-Type"] = jsonapi.MediaType
   444  	ro.Headers["Accept"] = jsonapi.MediaType
   445  
   446  	if i != nil {
   447  		var buf bytes.Buffer
   448  		if err := jsonapi.MarshalPayload(&buf, i); err != nil {
   449  			return nil, err
   450  		}
   451  
   452  		ro.Body = &buf
   453  		ro.BodyLength = int64(buf.Len())
   454  	}
   455  	return c.Request(verb, p, ro)
   456  }
   457  
   458  func (c *Client) RequestJSONAPIBulk(verb, p string, i interface{}, ro *RequestOptions) (*http.Response, error) {
   459  	if ro == nil {
   460  		ro = new(RequestOptions)
   461  	}
   462  
   463  	if ro.Headers == nil {
   464  		ro.Headers = make(map[string]string)
   465  	}
   466  	ro.Headers["Content-Type"] = jsonapi.MediaType + "; ext=bulk"
   467  	ro.Headers["Accept"] = jsonapi.MediaType + "; ext=bulk"
   468  
   469  	var buf bytes.Buffer
   470  	if err := jsonapi.MarshalPayload(&buf, i); err != nil {
   471  		return nil, err
   472  	}
   473  
   474  	ro.Body = &buf
   475  	ro.BodyLength = int64(buf.Len())
   476  
   477  	return c.Request(verb, p, ro)
   478  }
   479  
   480  // checkResp wraps an HTTP request from the default client and verifies that the
   481  // request was successful. A non-200 request returns an error formatted to
   482  // included any validation problems or otherwise.
   483  func checkResp(resp *http.Response, err error) (*http.Response, error) {
   484  	// If the err is already there, there was an error higher up the chain, so
   485  	// just return that.
   486  	if err != nil {
   487  		return resp, err
   488  	}
   489  
   490  	switch resp.StatusCode {
   491  	case 200, 201, 202, 204, 205, 206:
   492  		return resp, nil
   493  	default:
   494  		return resp, NewHTTPError(resp)
   495  	}
   496  }
   497  
   498  // decodeBodyMap is used to decode an HTTP response body into a mapstructure struct.
   499  func decodeBodyMap(body io.Reader, out interface{}) error {
   500  	var parsed interface{}
   501  	dec := json.NewDecoder(body)
   502  	if err := dec.Decode(&parsed); err != nil {
   503  		return err
   504  	}
   505  
   506  	return decodeMap(parsed, out)
   507  }
   508  
   509  // decodeMap decodes an `in` struct or map to a mapstructure tagged `out`.
   510  // It applies the decoder defaults used throughout go-fastly.
   511  // Note that this uses opposite argument order from Go's copy().
   512  func decodeMap(in interface{}, out interface{}) error {
   513  	decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
   514  		DecodeHook: mapstructure.ComposeDecodeHookFunc(
   515  			mapToHTTPHeaderHookFunc(),
   516  			stringToTimeHookFunc(),
   517  		),
   518  		WeaklyTypedInput: true,
   519  		Result:           out,
   520  	})
   521  	if err != nil {
   522  		return err
   523  	}
   524  	return decoder.Decode(in)
   525  }