cloudflare/cloudflared

Public

mirrored from https://github.com/cloudflare/cloudflaredAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
2021.12.4

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

cfapi/base_client.go

186lines · modeblame

6822e4f8Nuno Diegues4 years ago1package cfapi
2
3import (
4"bytes"
5"encoding/json"
6"fmt"
7"io"
8"net/http"
9"net/url"
10"strings"
11"time"
12
13"github.com/pkg/errors"
14"github.com/rs/zerolog"
15"golang.org/x/net/http2"
16)
17
18const (
19defaultTimeout = 15 * time.Second
20jsonContentType = "application/json"
21)
22
23var (
24ErrUnauthorized = errors.New("unauthorized")
25ErrBadRequest = errors.New("incorrect request parameters")
26ErrNotFound = errors.New("not found")
27ErrAPINoSuccess = errors.New("API call failed")
28)
29
30type RESTClient struct {
31baseEndpoints *baseEndpoints
32authToken string
33userAgent string
34client http.Client
35log *zerolog.Logger
36}
37
38type baseEndpoints struct {
39accountLevel url.URL
40zoneLevel url.URL
41accountRoutes url.URL
42accountVnets url.URL
43}
44
45var _ Client = (*RESTClient)(nil)
46
47func NewRESTClient(baseURL, accountTag, zoneTag, authToken, userAgent string, log *zerolog.Logger) (*RESTClient, error) {
48if strings.HasSuffix(baseURL, "/") {
49baseURL = baseURL[:len(baseURL)-1]
50}
51accountLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/tunnels", baseURL, accountTag))
52if err != nil {
53return nil, errors.Wrap(err, "failed to create account level endpoint")
54}
55accountRoutesEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/teamnet/routes", baseURL, accountTag))
56if err != nil {
57return nil, errors.Wrap(err, "failed to create route account-level endpoint")
58}
59accountVnetsEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/teamnet/virtual_networks", baseURL, accountTag))
60if err != nil {
61return nil, errors.Wrap(err, "failed to create virtual network account-level endpoint")
62}
63zoneLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/zones/%s/tunnels", baseURL, zoneTag))
64if err != nil {
65return nil, errors.Wrap(err, "failed to create account level endpoint")
66}
67httpTransport := http.Transport{
68TLSHandshakeTimeout: defaultTimeout,
69ResponseHeaderTimeout: defaultTimeout,
70}
71http2.ConfigureTransport(&httpTransport)
72return &RESTClient{
73baseEndpoints: &baseEndpoints{
74accountLevel: *accountLevelEndpoint,
75zoneLevel: *zoneLevelEndpoint,
76accountRoutes: *accountRoutesEndpoint,
77accountVnets: *accountVnetsEndpoint,
78},
79authToken: authToken,
80userAgent: userAgent,
81client: http.Client{
82Transport: &httpTransport,
83Timeout: defaultTimeout,
84},
85log: log,
86}, nil
87}
88
89func (r *RESTClient) sendRequest(method string, url url.URL, body interface{}) (*http.Response, error) {
90var bodyReader io.Reader
91if body != nil {
92if bodyBytes, err := json.Marshal(body); err != nil {
93return nil, errors.Wrap(err, "failed to serialize json body")
94} else {
95bodyReader = bytes.NewBuffer(bodyBytes)
96}
97}
98
99req, err := http.NewRequest(method, url.String(), bodyReader)
100if err != nil {
101return nil, errors.Wrapf(err, "can't create %s request", method)
102}
103req.Header.Set("User-Agent", r.userAgent)
104if bodyReader != nil {
105req.Header.Set("Content-Type", jsonContentType)
106}
107req.Header.Add("X-Auth-User-Service-Key", r.authToken)
108req.Header.Add("Accept", "application/json;version=1")
109return r.client.Do(req)
110}
111
112func parseResponse(reader io.Reader, data interface{}) error {
113// Schema for Tunnelstore responses in the v1 API.
114// Roughly, it's a wrapper around a particular result that adds failures/errors/etc
115var result response
116// First, parse the wrapper and check the API call succeeded
117if err := json.NewDecoder(reader).Decode(&result); err != nil {
118return errors.Wrap(err, "failed to decode response")
119}
120if err := result.checkErrors(); err != nil {
121return err
122}
123if !result.Success {
124return ErrAPINoSuccess
125}
126// At this point we know the API call succeeded, so, parse out the inner
127// result into the datatype provided as a parameter.
128if err := json.Unmarshal(result.Result, &data); err != nil {
129return errors.Wrap(err, "the Cloudflare API response was an unexpected type")
130}
131return nil
132}
133
134type response struct {
135Success bool `json:"success,omitempty"`
136Errors []apiErr `json:"errors,omitempty"`
137Messages []string `json:"messages,omitempty"`
138Result json.RawMessage `json:"result,omitempty"`
139}
140
141func (r *response) checkErrors() error {
142if len(r.Errors) == 0 {
143return nil
144}
145if len(r.Errors) == 1 {
146return r.Errors[0]
147}
148var messages string
149for _, e := range r.Errors {
150messages += fmt.Sprintf("%s; ", e)
151}
152return fmt.Errorf("API errors: %s", messages)
153}
154
155type apiErr struct {
156Code json.Number `json:"code,omitempty"`
157Message string `json:"message,omitempty"`
158}
159
160func (e apiErr) Error() string {
161return fmt.Sprintf("code: %v, reason: %s", e.Code, e.Message)
162}
163
164func (r *RESTClient) statusCodeToError(op string, resp *http.Response) error {
165if resp.Header.Get("Content-Type") == "application/json" {
166var errorsResp response
167if json.NewDecoder(resp.Body).Decode(&errorsResp) == nil {
168if err := errorsResp.checkErrors(); err != nil {
169return errors.Errorf("Failed to %s: %s", op, err)
170}
171}
172}
173
174switch resp.StatusCode {
175case http.StatusOK:
176return nil
177case http.StatusBadRequest:
178return ErrBadRequest
179case http.StatusUnauthorized, http.StatusForbidden:
180return ErrUnauthorized
181case http.StatusNotFound:
182return ErrNotFound
183}
184return errors.Errorf("API call to %s failed with status %d: %s", op,
185resp.StatusCode, http.StatusText(resp.StatusCode))
186}