cloudflare/cloudflared

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
9abcfece66e90761ecd5aceb0a9129dbeb7eb508

Branches

Tags

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

Clone

HTTPS

Download ZIP

cfapi/base_client.go

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