cloudflare/cloudflared

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
2026.2.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

cfapi/base_client.go

247lines · 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}
051b2cf3João Oliveirinha4 years ago51accountLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/cfd_tunnel", baseURL, accountTag))
6822e4f8Nuno Diegues4 years ago52if 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}
19106cd6João Oliveirinha3 years ago107req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.authToken))
6822e4f8Nuno Diegues4 years ago108req.Header.Add("Accept", "application/json;version=1")
109return r.client.Do(req)
110}
111
86476e62GoncaloGarcia2 years ago112func parseResponseEnvelope(reader io.Reader) (*response, error) {
6822e4f8Nuno Diegues4 years ago113// 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 {
86476e62GoncaloGarcia2 years ago118return nil, errors.Wrap(err, "failed to decode response")
6822e4f8Nuno Diegues4 years ago119}
120if err := result.checkErrors(); err != nil {
86476e62GoncaloGarcia2 years ago121return nil, err
6822e4f8Nuno Diegues4 years ago122}
123if !result.Success {
86476e62GoncaloGarcia2 years ago124return nil, ErrAPINoSuccess
6822e4f8Nuno Diegues4 years ago125}
86476e62GoncaloGarcia2 years ago126
127return &result, nil
128}
129
130func parseResponse(reader io.Reader, data interface{}) error {
131result, err := parseResponseEnvelope(reader)
132if err != nil {
133return err
134}
135
136return parseResponseBody(result, data)
137}
138
139func parseResponseBody(result *response, data interface{}) error {
6822e4f8Nuno Diegues4 years ago140// At this point we know the API call succeeded, so, parse out the inner
141// result into the datatype provided as a parameter.
142if err := json.Unmarshal(result.Result, &data); err != nil {
143return errors.Wrap(err, "the Cloudflare API response was an unexpected type")
144}
145return nil
146}
147
86476e62GoncaloGarcia2 years ago148func fetchExhaustively[T any](requestFn func(int) (*http.Response, error)) ([]*T, error) {
149page := 0
150var fullResponse []*T
151
152for {
153page += 1
154envelope, parsedBody, err := fetchPage[T](requestFn, page)
155
156if err != nil {
157return nil, errors.Wrap(err, fmt.Sprintf("Error Parsing page %d", page))
158}
159
160fullResponse = append(fullResponse, parsedBody...)
161if envelope.Pagination.Count < envelope.Pagination.PerPage || len(fullResponse) >= envelope.Pagination.TotalCount {
162break
163}
164
165}
166return fullResponse, nil
167}
168
169func fetchPage[T any](requestFn func(int) (*http.Response, error), page int) (*response, []*T, error) {
170pageResp, err := requestFn(page)
171if err != nil {
172return nil, nil, errors.Wrap(err, "REST request failed")
173}
174defer pageResp.Body.Close()
175if pageResp.StatusCode == http.StatusOK {
176envelope, err := parseResponseEnvelope(pageResp.Body)
177if err != nil {
178return nil, nil, err
179}
180var parsedRspBody []*T
181return envelope, parsedRspBody, parseResponseBody(envelope, &parsedRspBody)
182
183}
184return nil, nil, errors.New(fmt.Sprintf("Failed to fetch page. Server returned: %d", pageResp.StatusCode))
185}
186
6822e4f8Nuno Diegues4 years ago187type response struct {
86476e62GoncaloGarcia2 years ago188Success bool `json:"success,omitempty"`
189Errors []apiErr `json:"errors,omitempty"`
190Messages []string `json:"messages,omitempty"`
191Result json.RawMessage `json:"result,omitempty"`
192Pagination Pagination `json:"result_info,omitempty"`
193}
194
195type Pagination struct {
196Count int `json:"count,omitempty"`
197Page int `json:"page,omitempty"`
198PerPage int `json:"per_page,omitempty"`
199TotalCount int `json:"total_count,omitempty"`
6822e4f8Nuno Diegues4 years ago200}
201
202func (r *response) checkErrors() error {
203if len(r.Errors) == 0 {
204return nil
205}
206if len(r.Errors) == 1 {
207return r.Errors[0]
208}
209var messages string
210for _, e := range r.Errors {
211messages += fmt.Sprintf("%s; ", e)
212}
213return fmt.Errorf("API errors: %s", messages)
214}
215
216type apiErr struct {
217Code json.Number `json:"code,omitempty"`
218Message string `json:"message,omitempty"`
219}
220
221func (e apiErr) Error() string {
222return fmt.Sprintf("code: %v, reason: %s", e.Code, e.Message)
223}
224
225func (r *RESTClient) statusCodeToError(op string, resp *http.Response) error {
226if resp.Header.Get("Content-Type") == "application/json" {
227var errorsResp response
228if json.NewDecoder(resp.Body).Decode(&errorsResp) == nil {
229if err := errorsResp.checkErrors(); err != nil {
230return errors.Errorf("Failed to %s: %s", op, err)
231}
232}
233}
234
235switch resp.StatusCode {
236case http.StatusOK:
237return nil
238case http.StatusBadRequest:
239return ErrBadRequest
240case http.StatusUnauthorized, http.StatusForbidden:
241return ErrUnauthorized
242case http.StatusNotFound:
243return ErrNotFound
244}
245return errors.Errorf("API call to %s failed with status %d: %s", op,
246resp.StatusCode, http.StatusText(resp.StatusCode))
247}