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 }