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