cloudflare/cloudflared

Public

mirrored fromhttps://github.com/cloudflare/cloudflaredAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
4d95ab73f584a5a6439803648f013031ca1dd4ac

Branches

Tags

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

Clone

HTTPS

Download ZIP

cfapi/base_client.go

243lines · modecode

1package 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 (
19 defaultTimeout = 15 * time.Second
20 jsonContentType = "application/json"
21)
22
23var (
24 ErrUnauthorized = errors.New("unauthorized")
25 ErrBadRequest = errors.New("incorrect request parameters")
26 ErrNotFound = errors.New("not found")
27 ErrAPINoSuccess = errors.New("API call failed")
28)
29
30type RESTClient struct {
31 baseEndpoints *baseEndpoints
32 authToken string
33 userAgent string
34 client http.Client
35 log *zerolog.Logger
36}
37
38type baseEndpoints struct {
39 accountLevel url.URL
40 zoneLevel url.URL
41 accountRoutes url.URL
42 accountVnets url.URL
43}
44
45var _ Client = (*RESTClient)(nil)
46
47func NewRESTClient(baseURL, accountTag, zoneTag, authToken, userAgent string, log *zerolog.Logger) (*RESTClient, error) {
48 baseURL = strings.TrimSuffix(baseURL, "/")
49 accountLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/cfd_tunnel", baseURL, accountTag))
50 if err != nil {
51 return nil, errors.Wrap(err, "failed to create account level endpoint")
52 }
53 accountRoutesEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/teamnet/routes", baseURL, accountTag))
54 if err != nil {
55 return nil, errors.Wrap(err, "failed to create route account-level endpoint")
56 }
57 accountVnetsEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/teamnet/virtual_networks", baseURL, accountTag))
58 if err != nil {
59 return nil, errors.Wrap(err, "failed to create virtual network account-level endpoint")
60 }
61 zoneLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/zones/%s/tunnels", baseURL, zoneTag))
62 if err != nil {
63 return nil, errors.Wrap(err, "failed to create account level endpoint")
64 }
65 httpTransport := http.Transport{
66 TLSHandshakeTimeout: defaultTimeout,
67 ResponseHeaderTimeout: defaultTimeout,
68 }
69 _ = http2.ConfigureTransport(&httpTransport)
70 return &RESTClient{
71 baseEndpoints: &baseEndpoints{
72 accountLevel: *accountLevelEndpoint,
73 zoneLevel: *zoneLevelEndpoint,
74 accountRoutes: *accountRoutesEndpoint,
75 accountVnets: *accountVnetsEndpoint,
76 },
77 authToken: authToken,
78 userAgent: userAgent,
79 client: http.Client{
80 Transport: &httpTransport,
81 Timeout: defaultTimeout,
82 },
83 log: log,
84 }, nil
85}
86
87func (r *RESTClient) sendRequest(method string, url url.URL, body interface{}) (*http.Response, error) {
88 var bodyReader io.Reader
89 if body != nil {
90 if bodyBytes, err := json.Marshal(body); err != nil {
91 return nil, errors.Wrap(err, "failed to serialize json body")
92 } else {
93 bodyReader = bytes.NewBuffer(bodyBytes)
94 }
95 }
96
97 req, err := http.NewRequest(method, url.String(), bodyReader)
98 if err != nil {
99 return nil, errors.Wrapf(err, "can't create %s request", method)
100 }
101 req.Header.Set("User-Agent", r.userAgent)
102 if bodyReader != nil {
103 req.Header.Set("Content-Type", jsonContentType)
104 }
105 req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.authToken))
106 req.Header.Add("Accept", "application/json;version=1")
107 return r.client.Do(req)
108}
109
110func parseResponseEnvelope(reader io.Reader) (*response, error) {
111 // Schema for Tunnelstore responses in the v1 API.
112 // Roughly, it's a wrapper around a particular result that adds failures/errors/etc
113 var result response
114 // First, parse the wrapper and check the API call succeeded
115 if err := json.NewDecoder(reader).Decode(&result); err != nil {
116 return nil, errors.Wrap(err, "failed to decode response")
117 }
118 if err := result.checkErrors(); err != nil {
119 return nil, err
120 }
121 if !result.Success {
122 return nil, ErrAPINoSuccess
123 }
124
125 return &result, nil
126}
127
128func parseResponse(reader io.Reader, data interface{}) error {
129 result, err := parseResponseEnvelope(reader)
130 if err != nil {
131 return err
132 }
133
134 return parseResponseBody(result, data)
135}
136
137func parseResponseBody(result *response, data interface{}) error {
138 // At this point we know the API call succeeded, so, parse out the inner
139 // result into the datatype provided as a parameter.
140 if err := json.Unmarshal(result.Result, &data); err != nil {
141 return errors.Wrap(err, "the Cloudflare API response was an unexpected type")
142 }
143 return nil
144}
145
146func fetchExhaustively[T any](requestFn func(int) (*http.Response, error)) ([]*T, error) {
147 page := 0
148 var fullResponse []*T
149
150 for {
151 page += 1
152 envelope, parsedBody, err := fetchPage[T](requestFn, page)
153
154 if err != nil {
155 return nil, errors.Wrap(err, fmt.Sprintf("Error Parsing page %d", page))
156 }
157
158 fullResponse = append(fullResponse, parsedBody...)
159 if envelope.Pagination.Count < envelope.Pagination.PerPage || len(fullResponse) >= envelope.Pagination.TotalCount {
160 break
161 }
162 }
163 return fullResponse, nil
164}
165
166func fetchPage[T any](requestFn func(int) (*http.Response, error), page int) (*response, []*T, error) {
167 pageResp, err := requestFn(page)
168 if err != nil {
169 return nil, nil, errors.Wrap(err, "REST request failed")
170 }
171 defer pageResp.Body.Close()
172 if pageResp.StatusCode == http.StatusOK {
173 envelope, err := parseResponseEnvelope(pageResp.Body)
174 if err != nil {
175 return nil, nil, err
176 }
177 var parsedRspBody []*T
178 return envelope, parsedRspBody, parseResponseBody(envelope, &parsedRspBody)
179 }
180 return nil, nil, errors.New(fmt.Sprintf("Failed to fetch page. Server returned: %d", pageResp.StatusCode))
181}
182
183type response struct {
184 Success bool `json:"success,omitempty"`
185 Errors []apiError `json:"errors,omitempty"`
186 Messages []string `json:"messages,omitempty"`
187 Result json.RawMessage `json:"result,omitempty"`
188 Pagination Pagination `json:"result_info,omitempty"`
189}
190
191type Pagination struct {
192 Count int `json:"count,omitempty"`
193 Page int `json:"page,omitempty"`
194 PerPage int `json:"per_page,omitempty"`
195 TotalCount int `json:"total_count,omitempty"`
196}
197
198func (r *response) checkErrors() error {
199 if len(r.Errors) == 0 {
200 return nil
201 }
202 if len(r.Errors) == 1 {
203 return r.Errors[0]
204 }
205 var messagesBuilder strings.Builder
206 for _, e := range r.Errors {
207 messagesBuilder.WriteString(fmt.Sprintf("%s; ", e))
208 }
209 return fmt.Errorf("API errors: %s", messagesBuilder.String())
210}
211
212type apiError struct {
213 Code json.Number `json:"code,omitempty"`
214 Message string `json:"message,omitempty"`
215}
216
217func (e apiError) Error() string {
218 return fmt.Sprintf("code: %v, reason: %s", e.Code, e.Message)
219}
220
221func (r *RESTClient) statusCodeToError(op string, resp *http.Response) error {
222 if resp.Header.Get("Content-Type") == "application/json" {
223 var errorsResp response
224 if json.NewDecoder(resp.Body).Decode(&errorsResp) == nil {
225 if err := errorsResp.checkErrors(); err != nil {
226 return errors.Errorf("Failed to %s: %s", op, err)
227 }
228 }
229 }
230
231 switch resp.StatusCode {
232 case http.StatusOK:
233 return nil
234 case http.StatusBadRequest:
235 return ErrBadRequest
236 case http.StatusUnauthorized, http.StatusForbidden:
237 return ErrUnauthorized
238 case http.StatusNotFound:
239 return ErrNotFound
240 }
241 return errors.Errorf("API call to %s failed with status %d: %s", op,
242 resp.StatusCode, http.StatusText(resp.StatusCode))
243}
244