client.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. package tunnelstore
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "fmt"
  6. "io"
  7. "net"
  8. "net/http"
  9. "net/url"
  10. "path"
  11. "strings"
  12. "time"
  13. "github.com/cloudflare/cloudflared/teamnet"
  14. "github.com/google/uuid"
  15. "github.com/pkg/errors"
  16. "github.com/rs/zerolog"
  17. )
  18. const (
  19. defaultTimeout = 15 * time.Second
  20. jsonContentType = "application/json"
  21. )
  22. var (
  23. ErrTunnelNameConflict = errors.New("tunnel with name already exists")
  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. type Tunnel struct {
  30. ID uuid.UUID `json:"id"`
  31. Name string `json:"name"`
  32. CreatedAt time.Time `json:"created_at"`
  33. DeletedAt time.Time `json:"deleted_at"`
  34. Connections []Connection `json:"connections"`
  35. }
  36. type Connection struct {
  37. ColoName string `json:"colo_name"`
  38. ID uuid.UUID `json:"id"`
  39. IsPendingReconnect bool `json:"is_pending_reconnect"`
  40. }
  41. type Change = string
  42. const (
  43. ChangeNew = "new"
  44. ChangeUpdated = "updated"
  45. ChangeUnchanged = "unchanged"
  46. )
  47. // Route represents a record type that can route to a tunnel
  48. type Route interface {
  49. json.Marshaler
  50. RecordType() string
  51. UnmarshalResult(body io.Reader) (RouteResult, error)
  52. }
  53. type RouteResult interface {
  54. // SuccessSummary explains what will route to this tunnel when it's provisioned successfully
  55. SuccessSummary() string
  56. }
  57. type DNSRoute struct {
  58. userHostname string
  59. }
  60. type DNSRouteResult struct {
  61. route *DNSRoute
  62. CName Change `json:"cname"`
  63. }
  64. func NewDNSRoute(userHostname string) Route {
  65. return &DNSRoute{
  66. userHostname: userHostname,
  67. }
  68. }
  69. func (dr *DNSRoute) MarshalJSON() ([]byte, error) {
  70. s := struct {
  71. Type string `json:"type"`
  72. UserHostname string `json:"user_hostname"`
  73. }{
  74. Type: dr.RecordType(),
  75. UserHostname: dr.userHostname,
  76. }
  77. return json.Marshal(&s)
  78. }
  79. func (dr *DNSRoute) UnmarshalResult(body io.Reader) (RouteResult, error) {
  80. var result DNSRouteResult
  81. err := parseResponse(body, &result)
  82. result.route = dr
  83. return &result, err
  84. }
  85. func (dr *DNSRoute) RecordType() string {
  86. return "dns"
  87. }
  88. func (res *DNSRouteResult) SuccessSummary() string {
  89. var msgFmt string
  90. switch res.CName {
  91. case ChangeNew:
  92. msgFmt = "Added CNAME %s which will route to this tunnel"
  93. case ChangeUpdated: // this is not currently returned by tunnelsore
  94. msgFmt = "%s updated to route to your tunnel"
  95. case ChangeUnchanged:
  96. msgFmt = "%s is already configured to route to your tunnel"
  97. }
  98. return fmt.Sprintf(msgFmt, res.route.userHostname)
  99. }
  100. type LBRoute struct {
  101. lbName string
  102. lbPool string
  103. }
  104. type LBRouteResult struct {
  105. route *LBRoute
  106. LoadBalancer Change `json:"load_balancer"`
  107. Pool Change `json:"pool"`
  108. }
  109. func NewLBRoute(lbName, lbPool string) Route {
  110. return &LBRoute{
  111. lbName: lbName,
  112. lbPool: lbPool,
  113. }
  114. }
  115. func (lr *LBRoute) MarshalJSON() ([]byte, error) {
  116. s := struct {
  117. Type string `json:"type"`
  118. LBName string `json:"lb_name"`
  119. LBPool string `json:"lb_pool"`
  120. }{
  121. Type: lr.RecordType(),
  122. LBName: lr.lbName,
  123. LBPool: lr.lbPool,
  124. }
  125. return json.Marshal(&s)
  126. }
  127. func (lr *LBRoute) RecordType() string {
  128. return "lb"
  129. }
  130. func (lr *LBRoute) UnmarshalResult(body io.Reader) (RouteResult, error) {
  131. var result LBRouteResult
  132. err := parseResponse(body, &result)
  133. result.route = lr
  134. return &result, err
  135. }
  136. func (res *LBRouteResult) SuccessSummary() string {
  137. var msg string
  138. switch res.LoadBalancer + "," + res.Pool {
  139. case "new,new":
  140. msg = "Created load balancer %s and added a new pool %s with this tunnel as an origin"
  141. case "new,updated":
  142. msg = "Created load balancer %s with an existing pool %s which was updated to use this tunnel as an origin"
  143. case "new,unchanged":
  144. msg = "Created load balancer %s with an existing pool %s which already has this tunnel as an origin"
  145. case "updated,new":
  146. msg = "Added new pool %[2]s with this tunnel as an origin to load balancer %[1]s"
  147. case "updated,updated":
  148. msg = "Updated pool %[2]s to use this tunnel as an origin and added it to load balancer %[1]s"
  149. case "updated,unchanged":
  150. msg = "Added pool %[2]s, which already has this tunnel as an origin, to load balancer %[1]s"
  151. case "unchanged,updated":
  152. msg = "Added this tunnel as an origin in pool %[2]s which is already used by load balancer %[1]s"
  153. case "unchanged,unchanged":
  154. msg = "Load balancer %s already uses pool %s which has this tunnel as an origin"
  155. case "unchanged,new":
  156. // this state is not possible
  157. fallthrough
  158. default:
  159. msg = "Something went wrong: failed to modify load balancer %s with pool %s; please check traffic manager configuration in the dashboard"
  160. }
  161. return fmt.Sprintf(msg, res.route.lbName, res.route.lbPool)
  162. }
  163. type Client interface {
  164. // Named Tunnels endpoints
  165. CreateTunnel(name string, tunnelSecret []byte) (*Tunnel, error)
  166. GetTunnel(tunnelID uuid.UUID) (*Tunnel, error)
  167. DeleteTunnel(tunnelID uuid.UUID) error
  168. ListTunnels(filter *Filter) ([]*Tunnel, error)
  169. CleanupConnections(tunnelID uuid.UUID) error
  170. RouteTunnel(tunnelID uuid.UUID, route Route) (RouteResult, error)
  171. // Teamnet endpoints
  172. ListRoutes(filter *teamnet.Filter) ([]*teamnet.DetailedRoute, error)
  173. AddRoute(newRoute teamnet.NewRoute) (teamnet.Route, error)
  174. DeleteRoute(network net.IPNet) error
  175. GetByIP(ip net.IP) (teamnet.DetailedRoute, error)
  176. }
  177. type RESTClient struct {
  178. baseEndpoints *baseEndpoints
  179. authToken string
  180. userAgent string
  181. client http.Client
  182. log *zerolog.Logger
  183. }
  184. type baseEndpoints struct {
  185. accountLevel url.URL
  186. zoneLevel url.URL
  187. accountRoutes url.URL
  188. }
  189. var _ Client = (*RESTClient)(nil)
  190. func NewRESTClient(baseURL, accountTag, zoneTag, authToken, userAgent string, log *zerolog.Logger) (*RESTClient, error) {
  191. if strings.HasSuffix(baseURL, "/") {
  192. baseURL = baseURL[:len(baseURL)-1]
  193. }
  194. accountLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/tunnels", baseURL, accountTag))
  195. if err != nil {
  196. return nil, errors.Wrap(err, "failed to create account level endpoint")
  197. }
  198. accountRoutesEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/teamnet/routes", baseURL, accountTag))
  199. if err != nil {
  200. return nil, errors.Wrap(err, "failed to create route account-level endpoint")
  201. }
  202. zoneLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/zones/%s/tunnels", baseURL, zoneTag))
  203. if err != nil {
  204. return nil, errors.Wrap(err, "failed to create account level endpoint")
  205. }
  206. return &RESTClient{
  207. baseEndpoints: &baseEndpoints{
  208. accountLevel: *accountLevelEndpoint,
  209. zoneLevel: *zoneLevelEndpoint,
  210. accountRoutes: *accountRoutesEndpoint,
  211. },
  212. authToken: authToken,
  213. userAgent: userAgent,
  214. client: http.Client{
  215. Transport: &http.Transport{
  216. TLSHandshakeTimeout: defaultTimeout,
  217. ResponseHeaderTimeout: defaultTimeout,
  218. },
  219. Timeout: defaultTimeout,
  220. },
  221. log: log,
  222. }, nil
  223. }
  224. type newTunnel struct {
  225. Name string `json:"name"`
  226. TunnelSecret []byte `json:"tunnel_secret"`
  227. }
  228. func (r *RESTClient) CreateTunnel(name string, tunnelSecret []byte) (*Tunnel, error) {
  229. if name == "" {
  230. return nil, errors.New("tunnel name required")
  231. }
  232. if _, err := uuid.Parse(name); err == nil {
  233. return nil, errors.New("you cannot use UUIDs as tunnel names")
  234. }
  235. body := &newTunnel{
  236. Name: name,
  237. TunnelSecret: tunnelSecret,
  238. }
  239. resp, err := r.sendRequest("POST", r.baseEndpoints.accountLevel, body)
  240. if err != nil {
  241. return nil, errors.Wrap(err, "REST request failed")
  242. }
  243. defer resp.Body.Close()
  244. switch resp.StatusCode {
  245. case http.StatusOK:
  246. return unmarshalTunnel(resp.Body)
  247. case http.StatusConflict:
  248. return nil, ErrTunnelNameConflict
  249. }
  250. return nil, r.statusCodeToError("create tunnel", resp)
  251. }
  252. func (r *RESTClient) GetTunnel(tunnelID uuid.UUID) (*Tunnel, error) {
  253. endpoint := r.baseEndpoints.accountLevel
  254. endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", tunnelID))
  255. resp, err := r.sendRequest("GET", endpoint, nil)
  256. if err != nil {
  257. return nil, errors.Wrap(err, "REST request failed")
  258. }
  259. defer resp.Body.Close()
  260. if resp.StatusCode == http.StatusOK {
  261. return unmarshalTunnel(resp.Body)
  262. }
  263. return nil, r.statusCodeToError("get tunnel", resp)
  264. }
  265. func (r *RESTClient) DeleteTunnel(tunnelID uuid.UUID) error {
  266. endpoint := r.baseEndpoints.accountLevel
  267. endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", tunnelID))
  268. resp, err := r.sendRequest("DELETE", endpoint, nil)
  269. if err != nil {
  270. return errors.Wrap(err, "REST request failed")
  271. }
  272. defer resp.Body.Close()
  273. return r.statusCodeToError("delete tunnel", resp)
  274. }
  275. func (r *RESTClient) ListTunnels(filter *Filter) ([]*Tunnel, error) {
  276. endpoint := r.baseEndpoints.accountLevel
  277. endpoint.RawQuery = filter.encode()
  278. resp, err := r.sendRequest("GET", endpoint, nil)
  279. if err != nil {
  280. return nil, errors.Wrap(err, "REST request failed")
  281. }
  282. defer resp.Body.Close()
  283. if resp.StatusCode == http.StatusOK {
  284. return parseListTunnels(resp.Body)
  285. }
  286. return nil, r.statusCodeToError("list tunnels", resp)
  287. }
  288. func parseListTunnels(body io.ReadCloser) ([]*Tunnel, error) {
  289. var tunnels []*Tunnel
  290. err := parseResponse(body, &tunnels)
  291. return tunnels, err
  292. }
  293. func (r *RESTClient) CleanupConnections(tunnelID uuid.UUID) error {
  294. endpoint := r.baseEndpoints.accountLevel
  295. endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/connections", tunnelID))
  296. resp, err := r.sendRequest("DELETE", endpoint, nil)
  297. if err != nil {
  298. return errors.Wrap(err, "REST request failed")
  299. }
  300. defer resp.Body.Close()
  301. return r.statusCodeToError("cleanup connections", resp)
  302. }
  303. func (r *RESTClient) RouteTunnel(tunnelID uuid.UUID, route Route) (RouteResult, error) {
  304. endpoint := r.baseEndpoints.zoneLevel
  305. endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/routes", tunnelID))
  306. resp, err := r.sendRequest("PUT", endpoint, route)
  307. if err != nil {
  308. return nil, errors.Wrap(err, "REST request failed")
  309. }
  310. defer resp.Body.Close()
  311. if resp.StatusCode == http.StatusOK {
  312. return route.UnmarshalResult(resp.Body)
  313. }
  314. return nil, r.statusCodeToError("add route", resp)
  315. }
  316. func (r *RESTClient) sendRequest(method string, url url.URL, body interface{}) (*http.Response, error) {
  317. var bodyReader io.Reader
  318. if body != nil {
  319. if bodyBytes, err := json.Marshal(body); err != nil {
  320. return nil, errors.Wrap(err, "failed to serialize json body")
  321. } else {
  322. bodyReader = bytes.NewBuffer(bodyBytes)
  323. }
  324. }
  325. req, err := http.NewRequest(method, url.String(), bodyReader)
  326. if err != nil {
  327. return nil, errors.Wrapf(err, "can't create %s request", method)
  328. }
  329. req.Header.Set("User-Agent", r.userAgent)
  330. if bodyReader != nil {
  331. req.Header.Set("Content-Type", jsonContentType)
  332. }
  333. req.Header.Add("X-Auth-User-Service-Key", r.authToken)
  334. req.Header.Add("Accept", "application/json;version=1")
  335. return r.client.Do(req)
  336. }
  337. func parseResponse(reader io.Reader, data interface{}) error {
  338. // Schema for Tunnelstore responses in the v1 API.
  339. // Roughly, it's a wrapper around a particular result that adds failures/errors/etc
  340. var result response
  341. // First, parse the wrapper and check the API call succeeded
  342. if err := json.NewDecoder(reader).Decode(&result); err != nil {
  343. return errors.Wrap(err, "failed to decode response")
  344. }
  345. if err := result.checkErrors(); err != nil {
  346. return err
  347. }
  348. if !result.Success {
  349. return ErrAPINoSuccess
  350. }
  351. // At this point we know the API call succeeded, so, parse out the inner
  352. // result into the datatype provided as a parameter.
  353. if err := json.Unmarshal(result.Result, &data); err != nil {
  354. return errors.Wrap(err, "the Cloudflare API response was an unexpected type")
  355. }
  356. return nil
  357. }
  358. func unmarshalTunnel(reader io.Reader) (*Tunnel, error) {
  359. var tunnel Tunnel
  360. err := parseResponse(reader, &tunnel)
  361. return &tunnel, err
  362. }
  363. type response struct {
  364. Success bool `json:"success,omitempty"`
  365. Errors []apiErr `json:"errors,omitempty"`
  366. Messages []string `json:"messages,omitempty"`
  367. Result json.RawMessage `json:"result,omitempty"`
  368. }
  369. func (r *response) checkErrors() error {
  370. if len(r.Errors) == 0 {
  371. return nil
  372. }
  373. if len(r.Errors) == 1 {
  374. return r.Errors[0]
  375. }
  376. var messages string
  377. for _, e := range r.Errors {
  378. messages += fmt.Sprintf("%s; ", e)
  379. }
  380. return fmt.Errorf("API errors: %s", messages)
  381. }
  382. type apiErr struct {
  383. Code json.Number `json:"code,omitempty"`
  384. Message string `json:"message,omitempty"`
  385. }
  386. func (e apiErr) Error() string {
  387. return fmt.Sprintf("code: %v, reason: %s", e.Code, e.Message)
  388. }
  389. func (r *RESTClient) statusCodeToError(op string, resp *http.Response) error {
  390. if resp.Header.Get("Content-Type") == "application/json" {
  391. var errorsResp response
  392. if json.NewDecoder(resp.Body).Decode(&errorsResp) == nil {
  393. if err := errorsResp.checkErrors(); err != nil {
  394. return errors.Errorf("Failed to %s: %s", op, err)
  395. }
  396. }
  397. }
  398. switch resp.StatusCode {
  399. case http.StatusOK:
  400. return nil
  401. case http.StatusBadRequest:
  402. return ErrBadRequest
  403. case http.StatusUnauthorized, http.StatusForbidden:
  404. return ErrUnauthorized
  405. case http.StatusNotFound:
  406. return ErrNotFound
  407. }
  408. return errors.Errorf("API call to %s failed with status %d: %s", op,
  409. resp.StatusCode, http.StatusText(resp.StatusCode))
  410. }