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 }