cloudflare/cloudflared

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
cc1c6d9abc865533e1ebb5a6a387fd3973ff7bc6

Branches

Tags

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

Clone

HTTPS

Download ZIP

cfapi/base_client.go

186lines · 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("X-Auth-User-Service-Key", r.authToken)
108 req.Header.Add("Accept", "application/json;version=1")
109 return 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
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 errors.Wrap(err, "failed to decode response")
119 }
120 if err := result.checkErrors(); err != nil {
121 return err
122 }
123 if !result.Success {
124 return 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.
128 if err := json.Unmarshal(result.Result, &data); err != nil {
129 return errors.Wrap(err, "the Cloudflare API response was an unexpected type")
130 }
131 return nil
132}
133
134type response struct {
135 Success bool `json:"success,omitempty"`
136 Errors []apiErr `json:"errors,omitempty"`
137 Messages []string `json:"messages,omitempty"`
138 Result json.RawMessage `json:"result,omitempty"`
139}
140
141func (r *response) checkErrors() error {
142 if len(r.Errors) == 0 {
143 return nil
144 }
145 if len(r.Errors) == 1 {
146 return r.Errors[0]
147 }
148 var messages string
149 for _, e := range r.Errors {
150 messages += fmt.Sprintf("%s; ", e)
151 }
152 return fmt.Errorf("API errors: %s", messages)
153}
154
155type apiErr struct {
156 Code json.Number `json:"code,omitempty"`
157 Message string `json:"message,omitempty"`
158}
159
160func (e apiErr) Error() string {
161 return fmt.Sprintf("code: %v, reason: %s", e.Code, e.Message)
162}
163
164func (r *RESTClient) statusCodeToError(op string, resp *http.Response) error {
165 if resp.Header.Get("Content-Type") == "application/json" {
166 var errorsResp response
167 if json.NewDecoder(resp.Body).Decode(&errorsResp) == nil {
168 if err := errorsResp.checkErrors(); err != nil {
169 return errors.Errorf("Failed to %s: %s", op, err)
170 }
171 }
172 }
173
174 switch resp.StatusCode {
175 case http.StatusOK:
176 return nil
177 case http.StatusBadRequest:
178 return ErrBadRequest
179 case http.StatusUnauthorized, http.StatusForbidden:
180 return ErrUnauthorized
181 case http.StatusNotFound:
182 return ErrNotFound
183 }
184 return errors.Errorf("API call to %s failed with status %d: %s", op,
185 resp.StatusCode, http.StatusText(resp.StatusCode))
186}
187