TUN-5482: Refactor tunnelstore client related packages for more coherent package

This commit is contained in:
Nuno Diegues 2021-12-27 14:56:50 +00:00
parent 834c0eaeed
commit 6822e4f8ab
27 changed files with 1056 additions and 1025 deletions

186
cfapi/base_client.go Normal file
View File

@ -0,0 +1,186 @@
package cfapi
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"golang.org/x/net/http2"
)
const (
defaultTimeout = 15 * time.Second
jsonContentType = "application/json"
)
var (
ErrUnauthorized = errors.New("unauthorized")
ErrBadRequest = errors.New("incorrect request parameters")
ErrNotFound = errors.New("not found")
ErrAPINoSuccess = errors.New("API call failed")
)
type RESTClient struct {
baseEndpoints *baseEndpoints
authToken string
userAgent string
client http.Client
log *zerolog.Logger
}
type baseEndpoints struct {
accountLevel url.URL
zoneLevel url.URL
accountRoutes url.URL
accountVnets url.URL
}
var _ Client = (*RESTClient)(nil)
func NewRESTClient(baseURL, accountTag, zoneTag, authToken, userAgent string, log *zerolog.Logger) (*RESTClient, error) {
if strings.HasSuffix(baseURL, "/") {
baseURL = baseURL[:len(baseURL)-1]
}
accountLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/tunnels", baseURL, accountTag))
if err != nil {
return nil, errors.Wrap(err, "failed to create account level endpoint")
}
accountRoutesEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/teamnet/routes", baseURL, accountTag))
if err != nil {
return nil, errors.Wrap(err, "failed to create route account-level endpoint")
}
accountVnetsEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/teamnet/virtual_networks", baseURL, accountTag))
if err != nil {
return nil, errors.Wrap(err, "failed to create virtual network account-level endpoint")
}
zoneLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/zones/%s/tunnels", baseURL, zoneTag))
if err != nil {
return nil, errors.Wrap(err, "failed to create account level endpoint")
}
httpTransport := http.Transport{
TLSHandshakeTimeout: defaultTimeout,
ResponseHeaderTimeout: defaultTimeout,
}
http2.ConfigureTransport(&httpTransport)
return &RESTClient{
baseEndpoints: &baseEndpoints{
accountLevel: *accountLevelEndpoint,
zoneLevel: *zoneLevelEndpoint,
accountRoutes: *accountRoutesEndpoint,
accountVnets: *accountVnetsEndpoint,
},
authToken: authToken,
userAgent: userAgent,
client: http.Client{
Transport: &httpTransport,
Timeout: defaultTimeout,
},
log: log,
}, nil
}
func (r *RESTClient) sendRequest(method string, url url.URL, body interface{}) (*http.Response, error) {
var bodyReader io.Reader
if body != nil {
if bodyBytes, err := json.Marshal(body); err != nil {
return nil, errors.Wrap(err, "failed to serialize json body")
} else {
bodyReader = bytes.NewBuffer(bodyBytes)
}
}
req, err := http.NewRequest(method, url.String(), bodyReader)
if err != nil {
return nil, errors.Wrapf(err, "can't create %s request", method)
}
req.Header.Set("User-Agent", r.userAgent)
if bodyReader != nil {
req.Header.Set("Content-Type", jsonContentType)
}
req.Header.Add("X-Auth-User-Service-Key", r.authToken)
req.Header.Add("Accept", "application/json;version=1")
return r.client.Do(req)
}
func parseResponse(reader io.Reader, data interface{}) error {
// Schema for Tunnelstore responses in the v1 API.
// Roughly, it's a wrapper around a particular result that adds failures/errors/etc
var result response
// First, parse the wrapper and check the API call succeeded
if err := json.NewDecoder(reader).Decode(&result); err != nil {
return errors.Wrap(err, "failed to decode response")
}
if err := result.checkErrors(); err != nil {
return err
}
if !result.Success {
return ErrAPINoSuccess
}
// At this point we know the API call succeeded, so, parse out the inner
// result into the datatype provided as a parameter.
if err := json.Unmarshal(result.Result, &data); err != nil {
return errors.Wrap(err, "the Cloudflare API response was an unexpected type")
}
return nil
}
type response struct {
Success bool `json:"success,omitempty"`
Errors []apiErr `json:"errors,omitempty"`
Messages []string `json:"messages,omitempty"`
Result json.RawMessage `json:"result,omitempty"`
}
func (r *response) checkErrors() error {
if len(r.Errors) == 0 {
return nil
}
if len(r.Errors) == 1 {
return r.Errors[0]
}
var messages string
for _, e := range r.Errors {
messages += fmt.Sprintf("%s; ", e)
}
return fmt.Errorf("API errors: %s", messages)
}
type apiErr struct {
Code json.Number `json:"code,omitempty"`
Message string `json:"message,omitempty"`
}
func (e apiErr) Error() string {
return fmt.Sprintf("code: %v, reason: %s", e.Code, e.Message)
}
func (r *RESTClient) statusCodeToError(op string, resp *http.Response) error {
if resp.Header.Get("Content-Type") == "application/json" {
var errorsResp response
if json.NewDecoder(resp.Body).Decode(&errorsResp) == nil {
if err := errorsResp.checkErrors(); err != nil {
return errors.Errorf("Failed to %s: %s", op, err)
}
}
}
switch resp.StatusCode {
case http.StatusOK:
return nil
case http.StatusBadRequest:
return ErrBadRequest
case http.StatusUnauthorized, http.StatusForbidden:
return ErrUnauthorized
case http.StatusNotFound:
return ErrNotFound
}
return errors.Errorf("API call to %s failed with status %d: %s", op,
resp.StatusCode, http.StatusText(resp.StatusCode))
}

39
cfapi/client.go Normal file
View File

@ -0,0 +1,39 @@
package cfapi
import (
"github.com/google/uuid"
)
type TunnelClient interface {
CreateTunnel(name string, tunnelSecret []byte) (*Tunnel, error)
GetTunnel(tunnelID uuid.UUID) (*Tunnel, error)
DeleteTunnel(tunnelID uuid.UUID) error
ListTunnels(filter *TunnelFilter) ([]*Tunnel, error)
ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error)
CleanupConnections(tunnelID uuid.UUID, params *CleanupParams) error
}
type HostnameClient interface {
RouteTunnel(tunnelID uuid.UUID, route HostnameRoute) (HostnameRouteResult, error)
}
type IPRouteClient interface {
ListRoutes(filter *IpRouteFilter) ([]*DetailedRoute, error)
AddRoute(newRoute NewRoute) (Route, error)
DeleteRoute(params DeleteRouteParams) error
GetByIP(params GetRouteByIpParams) (DetailedRoute, error)
}
type VnetClient interface {
CreateVirtualNetwork(newVnet NewVirtualNetwork) (VirtualNetwork, error)
ListVirtualNetworks(filter *VnetFilter) ([]*VirtualNetwork, error)
DeleteVirtualNetwork(id uuid.UUID) error
UpdateVirtualNetwork(id uuid.UUID, updates UpdateVirtualNetwork) error
}
type Client interface {
TunnelClient
HostnameClient
IPRouteClient
VnetClient
}

192
cfapi/hostname.go Normal file
View File

@ -0,0 +1,192 @@
package cfapi
import (
"encoding/json"
"fmt"
"io"
"net/http"
"path"
"github.com/google/uuid"
"github.com/pkg/errors"
)
type Change = string
const (
ChangeNew = "new"
ChangeUpdated = "updated"
ChangeUnchanged = "unchanged"
)
// HostnameRoute represents a record type that can route to a tunnel
type HostnameRoute interface {
json.Marshaler
RecordType() string
UnmarshalResult(body io.Reader) (HostnameRouteResult, error)
String() string
}
type HostnameRouteResult interface {
// SuccessSummary explains what will route to this tunnel when it's provisioned successfully
SuccessSummary() string
}
type DNSRoute struct {
userHostname string
overwriteExisting bool
}
type DNSRouteResult struct {
route *DNSRoute
CName Change `json:"cname"`
Name string `json:"name"`
}
func NewDNSRoute(userHostname string, overwriteExisting bool) HostnameRoute {
return &DNSRoute{
userHostname: userHostname,
overwriteExisting: overwriteExisting,
}
}
func (dr *DNSRoute) MarshalJSON() ([]byte, error) {
s := struct {
Type string `json:"type"`
UserHostname string `json:"user_hostname"`
OverwriteExisting bool `json:"overwrite_existing"`
}{
Type: dr.RecordType(),
UserHostname: dr.userHostname,
OverwriteExisting: dr.overwriteExisting,
}
return json.Marshal(&s)
}
func (dr *DNSRoute) UnmarshalResult(body io.Reader) (HostnameRouteResult, error) {
var result DNSRouteResult
err := parseResponse(body, &result)
result.route = dr
return &result, err
}
func (dr *DNSRoute) RecordType() string {
return "dns"
}
func (dr *DNSRoute) String() string {
return fmt.Sprintf("%s %s", dr.RecordType(), dr.userHostname)
}
func (res *DNSRouteResult) SuccessSummary() string {
var msgFmt string
switch res.CName {
case ChangeNew:
msgFmt = "Added CNAME %s which will route to this tunnel"
case ChangeUpdated: // this is not currently returned by tunnelsore
msgFmt = "%s updated to route to your tunnel"
case ChangeUnchanged:
msgFmt = "%s is already configured to route to your tunnel"
}
return fmt.Sprintf(msgFmt, res.hostname())
}
// hostname yields the resulting name for the DNS route; if that is not available from Cloudflare API, then the
// requested name is returned instead (should not be the common path, it is just a fall-back).
func (res *DNSRouteResult) hostname() string {
if res.Name != "" {
return res.Name
}
return res.route.userHostname
}
type LBRoute struct {
lbName string
lbPool string
}
type LBRouteResult struct {
route *LBRoute
LoadBalancer Change `json:"load_balancer"`
Pool Change `json:"pool"`
}
func NewLBRoute(lbName, lbPool string) HostnameRoute {
return &LBRoute{
lbName: lbName,
lbPool: lbPool,
}
}
func (lr *LBRoute) MarshalJSON() ([]byte, error) {
s := struct {
Type string `json:"type"`
LBName string `json:"lb_name"`
LBPool string `json:"lb_pool"`
}{
Type: lr.RecordType(),
LBName: lr.lbName,
LBPool: lr.lbPool,
}
return json.Marshal(&s)
}
func (lr *LBRoute) RecordType() string {
return "lb"
}
func (lb *LBRoute) String() string {
return fmt.Sprintf("%s %s %s", lb.RecordType(), lb.lbName, lb.lbPool)
}
func (lr *LBRoute) UnmarshalResult(body io.Reader) (HostnameRouteResult, error) {
var result LBRouteResult
err := parseResponse(body, &result)
result.route = lr
return &result, err
}
func (res *LBRouteResult) SuccessSummary() string {
var msg string
switch res.LoadBalancer + "," + res.Pool {
case "new,new":
msg = "Created load balancer %s and added a new pool %s with this tunnel as an origin"
case "new,updated":
msg = "Created load balancer %s with an existing pool %s which was updated to use this tunnel as an origin"
case "new,unchanged":
msg = "Created load balancer %s with an existing pool %s which already has this tunnel as an origin"
case "updated,new":
msg = "Added new pool %[2]s with this tunnel as an origin to load balancer %[1]s"
case "updated,updated":
msg = "Updated pool %[2]s to use this tunnel as an origin and added it to load balancer %[1]s"
case "updated,unchanged":
msg = "Added pool %[2]s, which already has this tunnel as an origin, to load balancer %[1]s"
case "unchanged,updated":
msg = "Added this tunnel as an origin in pool %[2]s which is already used by load balancer %[1]s"
case "unchanged,unchanged":
msg = "Load balancer %s already uses pool %s which has this tunnel as an origin"
case "unchanged,new":
// this state is not possible
fallthrough
default:
msg = "Something went wrong: failed to modify load balancer %s with pool %s; please check traffic manager configuration in the dashboard"
}
return fmt.Sprintf(msg, res.route.lbName, res.route.lbPool)
}
func (r *RESTClient) RouteTunnel(tunnelID uuid.UUID, route HostnameRoute) (HostnameRouteResult, error) {
endpoint := r.baseEndpoints.zoneLevel
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/routes", tunnelID))
resp, err := r.sendRequest("PUT", endpoint, route)
if err != nil {
return nil, errors.Wrap(err, "REST request failed")
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
return route.UnmarshalResult(resp.Body)
}
return nil, r.statusCodeToError("add route", resp)
}

View File

@ -1,17 +1,9 @@
package tunnelstore
package cfapi
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"net"
"reflect"
"strings"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
@ -105,125 +97,3 @@ func TestLBRouteResultSuccessSummary(t *testing.T) {
assert.Equal(t, tt.expected, actual, "case %d", i+1)
}
}
func Test_parseListTunnels(t *testing.T) {
type args struct {
body string
}
tests := []struct {
name string
args args
want []*Tunnel
wantErr bool
}{
{
name: "empty list",
args: args{body: `{"success": true, "result": []}`},
want: []*Tunnel{},
},
{
name: "success is false",
args: args{body: `{"success": false, "result": []}`},
wantErr: true,
},
{
name: "errors are present",
args: args{body: `{"errors": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}], "result": []}`},
wantErr: true,
},
{
name: "invalid response",
args: args{body: `abc`},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
body := ioutil.NopCloser(bytes.NewReader([]byte(tt.args.body)))
got, err := parseListTunnels(body)
if (err != nil) != tt.wantErr {
t.Errorf("parseListTunnels() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("parseListTunnels() = %v, want %v", got, tt.want)
}
})
}
}
func Test_unmarshalTunnel(t *testing.T) {
type args struct {
reader io.Reader
}
tests := []struct {
name string
args args
want *Tunnel
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := unmarshalTunnel(tt.args.reader)
if (err != nil) != tt.wantErr {
t.Errorf("unmarshalTunnel() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("unmarshalTunnel() = %v, want %v", got, tt.want)
}
})
}
}
func TestUnmarshalTunnelOk(t *testing.T) {
jsonBody := `{"success": true, "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}`
expected := Tunnel{
ID: uuid.Nil,
Name: "test",
CreatedAt: time.Time{},
Connections: []Connection{},
}
actual, err := unmarshalTunnel(bytes.NewReader([]byte(jsonBody)))
assert.NoError(t, err)
assert.Equal(t, &expected, actual)
}
func TestUnmarshalTunnelErr(t *testing.T) {
tests := []string{
`abc`,
`{"success": true, "result": abc}`,
`{"success": false, "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}}`,
`{"errors": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}], "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}}`,
}
for i, test := range tests {
_, err := unmarshalTunnel(bytes.NewReader([]byte(test)))
assert.Error(t, err, fmt.Sprintf("Test #%v failed", i))
}
}
func TestUnmarshalConnections(t *testing.T) {
jsonBody := `{"success":true,"messages":[],"errors":[],"result":[{"id":"d4041254-91e3-4deb-bd94-b46e11680b1e","features":["ha-origin"],"version":"2021.2.5","arch":"darwin_amd64","conns":[{"colo_name":"LIS","id":"ac2286e5-c708-4588-a6a0-ba6b51940019","is_pending_reconnect":false,"origin_ip":"148.38.28.2","opened_at":"0001-01-01T00:00:00Z"}],"run_at":"0001-01-01T00:00:00Z"}]}`
expected := ActiveClient{
ID: uuid.MustParse("d4041254-91e3-4deb-bd94-b46e11680b1e"),
Features: []string{"ha-origin"},
Version: "2021.2.5",
Arch: "darwin_amd64",
RunAt: time.Time{},
Connections: []Connection{{
ID: uuid.MustParse("ac2286e5-c708-4588-a6a0-ba6b51940019"),
ColoName: "LIS",
IsPendingReconnect: false,
OriginIP: net.ParseIP("148.38.28.2"),
OpenedAt: time.Time{},
}},
}
actual, err := parseConnectionsDetails(bytes.NewReader([]byte(jsonBody)))
assert.NoError(t, err)
assert.Equal(t, []*ActiveClient{&expected}, actual)
}

View File

@ -1,9 +1,13 @@
package teamnet
package cfapi
import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/url"
"path"
"time"
"github.com/google/uuid"
@ -133,3 +137,104 @@ type GetRouteByIpParams struct {
// Optional field. If unset, backend will assume the default vnet for the account.
VNetID *uuid.UUID
}
// ListRoutes calls the Tunnelstore GET endpoint for all routes under an account.
func (r *RESTClient) ListRoutes(filter *IpRouteFilter) ([]*DetailedRoute, error) {
endpoint := r.baseEndpoints.accountRoutes
endpoint.RawQuery = filter.Encode()
resp, err := r.sendRequest("GET", endpoint, nil)
if err != nil {
return nil, errors.Wrap(err, "REST request failed")
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
return parseListDetailedRoutes(resp.Body)
}
return nil, r.statusCodeToError("list routes", resp)
}
// AddRoute calls the Tunnelstore POST endpoint for a given route.
func (r *RESTClient) AddRoute(newRoute NewRoute) (Route, error) {
endpoint := r.baseEndpoints.accountRoutes
endpoint.Path = path.Join(endpoint.Path, "network", url.PathEscape(newRoute.Network.String()))
resp, err := r.sendRequest("POST", endpoint, newRoute)
if err != nil {
return Route{}, errors.Wrap(err, "REST request failed")
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
return parseRoute(resp.Body)
}
return Route{}, r.statusCodeToError("add route", resp)
}
// DeleteRoute calls the Tunnelstore DELETE endpoint for a given route.
func (r *RESTClient) DeleteRoute(params DeleteRouteParams) error {
endpoint := r.baseEndpoints.accountRoutes
endpoint.Path = path.Join(endpoint.Path, "network", url.PathEscape(params.Network.String()))
setVnetParam(&endpoint, params.VNetID)
resp, err := r.sendRequest("DELETE", endpoint, nil)
if err != nil {
return errors.Wrap(err, "REST request failed")
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
_, err := parseRoute(resp.Body)
return err
}
return r.statusCodeToError("delete route", resp)
}
// GetByIP checks which route will proxy a given IP.
func (r *RESTClient) GetByIP(params GetRouteByIpParams) (DetailedRoute, error) {
endpoint := r.baseEndpoints.accountRoutes
endpoint.Path = path.Join(endpoint.Path, "ip", url.PathEscape(params.Ip.String()))
setVnetParam(&endpoint, params.VNetID)
resp, err := r.sendRequest("GET", endpoint, nil)
if err != nil {
return DetailedRoute{}, errors.Wrap(err, "REST request failed")
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
return parseDetailedRoute(resp.Body)
}
return DetailedRoute{}, r.statusCodeToError("get route by IP", resp)
}
func parseListDetailedRoutes(body io.ReadCloser) ([]*DetailedRoute, error) {
var routes []*DetailedRoute
err := parseResponse(body, &routes)
return routes, err
}
func parseRoute(body io.ReadCloser) (Route, error) {
var route Route
err := parseResponse(body, &route)
return route, err
}
func parseDetailedRoute(body io.ReadCloser) (DetailedRoute, error) {
var route DetailedRoute
err := parseResponse(body, &route)
return route, err
}
// setVnetParam overwrites the URL's query parameters with a query param to scope the HostnameRoute action to a certain
// virtual network (if one is provided).
func setVnetParam(endpoint *url.URL, vnetID *uuid.UUID) {
queryParams := url.Values{}
if vnetID != nil {
queryParams.Set("virtual_network_id", vnetID.String())
}
endpoint.RawQuery = queryParams.Encode()
}

View File

@ -1,4 +1,4 @@
package teamnet
package cfapi
import (
"fmt"
@ -13,90 +13,90 @@ import (
)
var (
filterDeleted = cli.BoolFlag{
filterIpRouteDeleted = cli.BoolFlag{
Name: "filter-is-deleted",
Usage: "If false (default), only show non-deleted routes. If true, only show deleted routes.",
}
filterTunnelID = cli.StringFlag{
filterIpRouteTunnelID = cli.StringFlag{
Name: "filter-tunnel-id",
Usage: "Show only routes with the given tunnel ID.",
}
filterSubset = cli.StringFlag{
filterSubsetIpRoute = cli.StringFlag{
Name: "filter-network-is-subset-of",
Aliases: []string{"nsub"},
Usage: "Show only routes whose network is a subset of the given network.",
}
filterSuperset = cli.StringFlag{
filterSupersetIpRoute = cli.StringFlag{
Name: "filter-network-is-superset-of",
Aliases: []string{"nsup"},
Usage: "Show only routes whose network is a superset of the given network.",
}
filterComment = cli.StringFlag{
filterIpRouteComment = cli.StringFlag{
Name: "filter-comment-is",
Usage: "Show only routes with this comment.",
}
filterVnet = cli.StringFlag{
filterIpRouteByVnet = cli.StringFlag{
Name: "filter-virtual-network-id",
Usage: "Show only routes that are attached to the given virtual network ID.",
}
// Flags contains all filter flags.
FilterFlags = []cli.Flag{
&filterDeleted,
&filterTunnelID,
&filterSubset,
&filterSuperset,
&filterComment,
&filterVnet,
IpRouteFilterFlags = []cli.Flag{
&filterIpRouteDeleted,
&filterIpRouteTunnelID,
&filterSubsetIpRoute,
&filterSupersetIpRoute,
&filterIpRouteComment,
&filterIpRouteByVnet,
}
)
// Filter which routes get queried.
type Filter struct {
// IpRouteFilter which routes get queried.
type IpRouteFilter struct {
queryParams url.Values
}
// NewFromCLI parses CLI flags to discover which filters should get applied.
func NewFromCLI(c *cli.Context) (*Filter, error) {
f := &Filter{
// NewIpRouteFilterFromCLI parses CLI flags to discover which filters should get applied.
func NewIpRouteFilterFromCLI(c *cli.Context) (*IpRouteFilter, error) {
f := &IpRouteFilter{
queryParams: url.Values{},
}
// Set deletion filter
if flag := filterDeleted.Name; c.IsSet(flag) && c.Bool(flag) {
if flag := filterIpRouteDeleted.Name; c.IsSet(flag) && c.Bool(flag) {
f.deleted()
} else {
f.notDeleted()
}
if subset, err := cidrFromFlag(c, filterSubset); err != nil {
if subset, err := cidrFromFlag(c, filterSubsetIpRoute); err != nil {
return nil, err
} else if subset != nil {
f.networkIsSupersetOf(*subset)
}
if superset, err := cidrFromFlag(c, filterSuperset); err != nil {
if superset, err := cidrFromFlag(c, filterSupersetIpRoute); err != nil {
return nil, err
} else if superset != nil {
f.networkIsSupersetOf(*superset)
}
if comment := c.String(filterComment.Name); comment != "" {
if comment := c.String(filterIpRouteComment.Name); comment != "" {
f.commentIs(comment)
}
if tunnelID := c.String(filterTunnelID.Name); tunnelID != "" {
if tunnelID := c.String(filterIpRouteTunnelID.Name); tunnelID != "" {
u, err := uuid.Parse(tunnelID)
if err != nil {
return nil, errors.Wrapf(err, "Couldn't parse UUID from %s", filterTunnelID.Name)
return nil, errors.Wrapf(err, "Couldn't parse UUID from %s", filterIpRouteTunnelID.Name)
}
f.tunnelID(u)
}
if vnetId := c.String(filterVnet.Name); vnetId != "" {
if vnetId := c.String(filterIpRouteByVnet.Name); vnetId != "" {
u, err := uuid.Parse(vnetId)
if err != nil {
return nil, errors.Wrapf(err, "Couldn't parse UUID from %s", filterVnet.Name)
return nil, errors.Wrapf(err, "Couldn't parse UUID from %s", filterIpRouteByVnet.Name)
}
f.vnetID(u)
}
@ -124,42 +124,42 @@ func cidrFromFlag(c *cli.Context, flag cli.StringFlag) (*net.IPNet, error) {
return subset, nil
}
func (f *Filter) commentIs(comment string) {
func (f *IpRouteFilter) commentIs(comment string) {
f.queryParams.Set("comment", comment)
}
func (f *Filter) notDeleted() {
func (f *IpRouteFilter) notDeleted() {
f.queryParams.Set("is_deleted", "false")
}
func (f *Filter) deleted() {
func (f *IpRouteFilter) deleted() {
f.queryParams.Set("is_deleted", "true")
}
func (f *Filter) networkIsSubsetOf(superset net.IPNet) {
func (f *IpRouteFilter) networkIsSubsetOf(superset net.IPNet) {
f.queryParams.Set("network_subset", superset.String())
}
func (f *Filter) networkIsSupersetOf(subset net.IPNet) {
func (f *IpRouteFilter) networkIsSupersetOf(subset net.IPNet) {
f.queryParams.Set("network_superset", subset.String())
}
func (f *Filter) existedAt(existedAt time.Time) {
func (f *IpRouteFilter) existedAt(existedAt time.Time) {
f.queryParams.Set("existed_at", existedAt.Format(time.RFC3339))
}
func (f *Filter) tunnelID(id uuid.UUID) {
func (f *IpRouteFilter) tunnelID(id uuid.UUID) {
f.queryParams.Set("tunnel_id", id.String())
}
func (f *Filter) vnetID(id uuid.UUID) {
func (f *IpRouteFilter) vnetID(id uuid.UUID) {
f.queryParams.Set("virtual_network_id", id.String())
}
func (f *Filter) MaxFetchSize(max uint) {
func (f *IpRouteFilter) MaxFetchSize(max uint) {
f.queryParams.Set("per_page", strconv.Itoa(int(max)))
}
func (f Filter) Encode() string {
func (f IpRouteFilter) Encode() string {
return f.queryParams.Encode()
}

View File

@ -1,4 +1,4 @@
package teamnet
package cfapi
import (
"encoding/json"

183
cfapi/tunnel.go Normal file
View File

@ -0,0 +1,183 @@
package cfapi
import (
"fmt"
"io"
"net"
"net/http"
"net/url"
"path"
"time"
"github.com/google/uuid"
"github.com/pkg/errors"
)
var ErrTunnelNameConflict = errors.New("tunnel with name already exists")
type Tunnel struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
CreatedAt time.Time `json:"created_at"`
DeletedAt time.Time `json:"deleted_at"`
Connections []Connection `json:"connections"`
}
type Connection struct {
ColoName string `json:"colo_name"`
ID uuid.UUID `json:"id"`
IsPendingReconnect bool `json:"is_pending_reconnect"`
OriginIP net.IP `json:"origin_ip"`
OpenedAt time.Time `json:"opened_at"`
}
type ActiveClient struct {
ID uuid.UUID `json:"id"`
Features []string `json:"features"`
Version string `json:"version"`
Arch string `json:"arch"`
RunAt time.Time `json:"run_at"`
Connections []Connection `json:"conns"`
}
type newTunnel struct {
Name string `json:"name"`
TunnelSecret []byte `json:"tunnel_secret"`
}
type CleanupParams struct {
queryParams url.Values
}
func NewCleanupParams() *CleanupParams {
return &CleanupParams{
queryParams: url.Values{},
}
}
func (cp *CleanupParams) ForClient(clientID uuid.UUID) {
cp.queryParams.Set("client_id", clientID.String())
}
func (cp CleanupParams) encode() string {
return cp.queryParams.Encode()
}
func (r *RESTClient) CreateTunnel(name string, tunnelSecret []byte) (*Tunnel, error) {
if name == "" {
return nil, errors.New("tunnel name required")
}
if _, err := uuid.Parse(name); err == nil {
return nil, errors.New("you cannot use UUIDs as tunnel names")
}
body := &newTunnel{
Name: name,
TunnelSecret: tunnelSecret,
}
resp, err := r.sendRequest("POST", r.baseEndpoints.accountLevel, body)
if err != nil {
return nil, errors.Wrap(err, "REST request failed")
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusOK:
return unmarshalTunnel(resp.Body)
case http.StatusConflict:
return nil, ErrTunnelNameConflict
}
return nil, r.statusCodeToError("create tunnel", resp)
}
func (r *RESTClient) GetTunnel(tunnelID uuid.UUID) (*Tunnel, error) {
endpoint := r.baseEndpoints.accountLevel
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", tunnelID))
resp, err := r.sendRequest("GET", endpoint, nil)
if err != nil {
return nil, errors.Wrap(err, "REST request failed")
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
return unmarshalTunnel(resp.Body)
}
return nil, r.statusCodeToError("get tunnel", resp)
}
func (r *RESTClient) DeleteTunnel(tunnelID uuid.UUID) error {
endpoint := r.baseEndpoints.accountLevel
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", tunnelID))
resp, err := r.sendRequest("DELETE", endpoint, nil)
if err != nil {
return errors.Wrap(err, "REST request failed")
}
defer resp.Body.Close()
return r.statusCodeToError("delete tunnel", resp)
}
func (r *RESTClient) ListTunnels(filter *TunnelFilter) ([]*Tunnel, error) {
endpoint := r.baseEndpoints.accountLevel
endpoint.RawQuery = filter.encode()
resp, err := r.sendRequest("GET", endpoint, nil)
if err != nil {
return nil, errors.Wrap(err, "REST request failed")
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
return parseListTunnels(resp.Body)
}
return nil, r.statusCodeToError("list tunnels", resp)
}
func parseListTunnels(body io.ReadCloser) ([]*Tunnel, error) {
var tunnels []*Tunnel
err := parseResponse(body, &tunnels)
return tunnels, err
}
func (r *RESTClient) ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error) {
endpoint := r.baseEndpoints.accountLevel
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/connections", tunnelID))
resp, err := r.sendRequest("GET", endpoint, nil)
if err != nil {
return nil, errors.Wrap(err, "REST request failed")
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
return parseConnectionsDetails(resp.Body)
}
return nil, r.statusCodeToError("list connection details", resp)
}
func parseConnectionsDetails(reader io.Reader) ([]*ActiveClient, error) {
var clients []*ActiveClient
err := parseResponse(reader, &clients)
return clients, err
}
func (r *RESTClient) CleanupConnections(tunnelID uuid.UUID, params *CleanupParams) error {
endpoint := r.baseEndpoints.accountLevel
endpoint.RawQuery = params.encode()
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/connections", tunnelID))
resp, err := r.sendRequest("DELETE", endpoint, nil)
if err != nil {
return errors.Wrap(err, "REST request failed")
}
defer resp.Body.Close()
return r.statusCodeToError("cleanup connections", resp)
}
func unmarshalTunnel(reader io.Reader) (*Tunnel, error) {
var tunnel Tunnel
err := parseResponse(reader, &tunnel)
return &tunnel, err
}

View File

@ -1,4 +1,4 @@
package tunnelstore
package cfapi
import (
"net/url"
@ -12,44 +12,44 @@ const (
TimeLayout = time.RFC3339
)
type Filter struct {
type TunnelFilter struct {
queryParams url.Values
}
func NewFilter() *Filter {
return &Filter{
func NewTunnelFilter() *TunnelFilter {
return &TunnelFilter{
queryParams: url.Values{},
}
}
func (f *Filter) ByName(name string) {
func (f *TunnelFilter) ByName(name string) {
f.queryParams.Set("name", name)
}
func (f *Filter) ByNamePrefix(namePrefix string) {
func (f *TunnelFilter) ByNamePrefix(namePrefix string) {
f.queryParams.Set("name_prefix", namePrefix)
}
func (f *Filter) ExcludeNameWithPrefix(excludePrefix string) {
func (f *TunnelFilter) ExcludeNameWithPrefix(excludePrefix string) {
f.queryParams.Set("exclude_prefix", excludePrefix)
}
func (f *Filter) NoDeleted() {
func (f *TunnelFilter) NoDeleted() {
f.queryParams.Set("is_deleted", "false")
}
func (f *Filter) ByExistedAt(existedAt time.Time) {
func (f *TunnelFilter) ByExistedAt(existedAt time.Time) {
f.queryParams.Set("existed_at", existedAt.Format(TimeLayout))
}
func (f *Filter) ByTunnelID(tunnelID uuid.UUID) {
func (f *TunnelFilter) ByTunnelID(tunnelID uuid.UUID) {
f.queryParams.Set("uuid", tunnelID.String())
}
func (f *Filter) MaxFetchSize(max uint) {
func (f *TunnelFilter) MaxFetchSize(max uint) {
f.queryParams.Set("per_page", strconv.Itoa(int(max)))
}
func (f Filter) encode() string {
func (f TunnelFilter) encode() string {
return f.queryParams.Encode()
}

149
cfapi/tunnel_test.go Normal file
View File

@ -0,0 +1,149 @@
package cfapi
import (
"bytes"
"fmt"
"io/ioutil"
"net"
"reflect"
"strings"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
var loc, _ = time.LoadLocation("UTC")
func Test_parseListTunnels(t *testing.T) {
type args struct {
body string
}
tests := []struct {
name string
args args
want []*Tunnel
wantErr bool
}{
{
name: "empty list",
args: args{body: `{"success": true, "result": []}`},
want: []*Tunnel{},
},
{
name: "success is false",
args: args{body: `{"success": false, "result": []}`},
wantErr: true,
},
{
name: "errors are present",
args: args{body: `{"errors": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}], "result": []}`},
wantErr: true,
},
{
name: "invalid response",
args: args{body: `abc`},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
body := ioutil.NopCloser(bytes.NewReader([]byte(tt.args.body)))
got, err := parseListTunnels(body)
if (err != nil) != tt.wantErr {
t.Errorf("parseListTunnels() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("parseListTunnels() = %v, want %v", got, tt.want)
}
})
}
}
func Test_unmarshalTunnel(t *testing.T) {
type args struct {
body string
}
tests := []struct {
name string
args args
want *Tunnel
wantErr bool
}{
{
name: "empty list",
args: args{body: `{"success": true, "result": {"id":"b34cc7ce-925b-46ee-bc23-4cb5c18d8292","created_at":"2021-07-29T13:46:14.090955Z","deleted_at":"2021-07-29T14:07:27.559047Z","name":"qt-bIWWN7D662ogh61pCPfu5s2XgqFY1OyV","account_id":6946212,"account_tag":"5ab4e9dfbd435d24068829fda0077963","conns_active_at":null,"conns_inactive_at":"2021-07-29T13:47:22.548482Z","tun_type":"cfd_tunnel","metadata":{"qtid":"a6fJROgkXutNruBGaJjD"}}}`},
want: &Tunnel{
ID: uuid.MustParse("b34cc7ce-925b-46ee-bc23-4cb5c18d8292"),
Name: "qt-bIWWN7D662ogh61pCPfu5s2XgqFY1OyV",
CreatedAt: time.Date(2021, 07, 29, 13, 46, 14, 90955000, loc),
DeletedAt: time.Date(2021, 07, 29, 14, 7, 27, 559047000, loc),
Connections: nil,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := unmarshalTunnel(strings.NewReader(tt.args.body))
if (err != nil) != tt.wantErr {
t.Errorf("unmarshalTunnel() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("unmarshalTunnel() = %v, want %v", got, tt.want)
}
})
}
}
func TestUnmarshalTunnelOk(t *testing.T) {
jsonBody := `{"success": true, "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}`
expected := Tunnel{
ID: uuid.Nil,
Name: "test",
CreatedAt: time.Time{},
Connections: []Connection{},
}
actual, err := unmarshalTunnel(bytes.NewReader([]byte(jsonBody)))
assert.NoError(t, err)
assert.Equal(t, &expected, actual)
}
func TestUnmarshalTunnelErr(t *testing.T) {
tests := []string{
`abc`,
`{"success": true, "result": abc}`,
`{"success": false, "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}}`,
`{"errors": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}], "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}}`,
}
for i, test := range tests {
_, err := unmarshalTunnel(bytes.NewReader([]byte(test)))
assert.Error(t, err, fmt.Sprintf("Test #%v failed", i))
}
}
func TestUnmarshalConnections(t *testing.T) {
jsonBody := `{"success":true,"messages":[],"errors":[],"result":[{"id":"d4041254-91e3-4deb-bd94-b46e11680b1e","features":["ha-origin"],"version":"2021.2.5","arch":"darwin_amd64","conns":[{"colo_name":"LIS","id":"ac2286e5-c708-4588-a6a0-ba6b51940019","is_pending_reconnect":false,"origin_ip":"148.38.28.2","opened_at":"0001-01-01T00:00:00Z"}],"run_at":"0001-01-01T00:00:00Z"}]}`
expected := ActiveClient{
ID: uuid.MustParse("d4041254-91e3-4deb-bd94-b46e11680b1e"),
Features: []string{"ha-origin"},
Version: "2021.2.5",
Arch: "darwin_amd64",
RunAt: time.Time{},
Connections: []Connection{{
ID: uuid.MustParse("ac2286e5-c708-4588-a6a0-ba6b51940019"),
ColoName: "LIS",
IsPendingReconnect: false,
OriginIP: net.ParseIP("148.38.28.2"),
OpenedAt: time.Time{},
}},
}
actual, err := parseConnectionsDetails(bytes.NewReader([]byte(jsonBody)))
assert.NoError(t, err)
assert.Equal(t, []*ActiveClient{&expected}, actual)
}

View File

@ -1,21 +1,59 @@
package tunnelstore
package cfapi
import (
"fmt"
"io"
"net/http"
"net/url"
"path"
"strconv"
"time"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/cloudflare/cloudflared/vnet"
)
func (r *RESTClient) CreateVirtualNetwork(newVnet vnet.NewVirtualNetwork) (vnet.VirtualNetwork, error) {
type NewVirtualNetwork struct {
Name string `json:"name"`
Comment string `json:"comment"`
IsDefault bool `json:"is_default"`
}
type VirtualNetwork struct {
ID uuid.UUID `json:"id"`
Comment string `json:"comment"`
Name string `json:"name"`
IsDefault bool `json:"is_default_network"`
CreatedAt time.Time `json:"created_at"`
DeletedAt time.Time `json:"deleted_at"`
}
type UpdateVirtualNetwork struct {
Name *string `json:"name,omitempty"`
Comment *string `json:"comment,omitempty"`
IsDefault *bool `json:"is_default_network,omitempty"`
}
func (virtualNetwork VirtualNetwork) TableString() string {
deletedColumn := "-"
if !virtualNetwork.DeletedAt.IsZero() {
deletedColumn = virtualNetwork.DeletedAt.Format(time.RFC3339)
}
return fmt.Sprintf(
"%s\t%s\t%s\t%s\t%s\t%s\t",
virtualNetwork.ID,
virtualNetwork.Name,
strconv.FormatBool(virtualNetwork.IsDefault),
virtualNetwork.Comment,
virtualNetwork.CreatedAt.Format(time.RFC3339),
deletedColumn,
)
}
func (r *RESTClient) CreateVirtualNetwork(newVnet NewVirtualNetwork) (VirtualNetwork, error) {
resp, err := r.sendRequest("POST", r.baseEndpoints.accountVnets, newVnet)
if err != nil {
return vnet.VirtualNetwork{}, errors.Wrap(err, "REST request failed")
return VirtualNetwork{}, errors.Wrap(err, "REST request failed")
}
defer resp.Body.Close()
@ -23,10 +61,10 @@ func (r *RESTClient) CreateVirtualNetwork(newVnet vnet.NewVirtualNetwork) (vnet.
return parseVnet(resp.Body)
}
return vnet.VirtualNetwork{}, r.statusCodeToError("add virtual network", resp)
return VirtualNetwork{}, r.statusCodeToError("add virtual network", resp)
}
func (r *RESTClient) ListVirtualNetworks(filter *vnet.Filter) ([]*vnet.VirtualNetwork, error) {
func (r *RESTClient) ListVirtualNetworks(filter *VnetFilter) ([]*VirtualNetwork, error) {
endpoint := r.baseEndpoints.accountVnets
endpoint.RawQuery = filter.Encode()
resp, err := r.sendRequest("GET", endpoint, nil)
@ -59,7 +97,7 @@ func (r *RESTClient) DeleteVirtualNetwork(id uuid.UUID) error {
return r.statusCodeToError("delete virtual network", resp)
}
func (r *RESTClient) UpdateVirtualNetwork(id uuid.UUID, updates vnet.UpdateVirtualNetwork) error {
func (r *RESTClient) UpdateVirtualNetwork(id uuid.UUID, updates UpdateVirtualNetwork) error {
endpoint := r.baseEndpoints.accountVnets
endpoint.Path = path.Join(endpoint.Path, url.PathEscape(id.String()))
resp, err := r.sendRequest("PATCH", endpoint, updates)
@ -76,14 +114,14 @@ func (r *RESTClient) UpdateVirtualNetwork(id uuid.UUID, updates vnet.UpdateVirtu
return r.statusCodeToError("update virtual network", resp)
}
func parseListVnets(body io.ReadCloser) ([]*vnet.VirtualNetwork, error) {
var vnets []*vnet.VirtualNetwork
func parseListVnets(body io.ReadCloser) ([]*VirtualNetwork, error) {
var vnets []*VirtualNetwork
err := parseResponse(body, &vnets)
return vnets, err
}
func parseVnet(body io.ReadCloser) (vnet.VirtualNetwork, error) {
var vnet vnet.VirtualNetwork
func parseVnet(body io.ReadCloser) (VirtualNetwork, error) {
var vnet VirtualNetwork
err := parseResponse(body, &vnet)
return vnet, err
}

View File

@ -1,4 +1,4 @@
package vnet
package cfapi
import (
"net/url"
@ -10,68 +10,68 @@ import (
)
var (
filterId = cli.StringFlag{
filterVnetId = cli.StringFlag{
Name: "id",
Usage: "List virtual networks with the given `ID`",
}
filterName = cli.StringFlag{
filterVnetByName = cli.StringFlag{
Name: "name",
Usage: "List virtual networks with the given `NAME`",
}
filterDefault = cli.BoolFlag{
filterDefaultVnet = cli.BoolFlag{
Name: "is-default",
Usage: "If true, lists the virtual network that is the default one. If false, lists all non-default virtual networks for the account. If absent, all are included in the results regardless of their default status.",
}
filterDeleted = cli.BoolFlag{
filterDeletedVnet = cli.BoolFlag{
Name: "show-deleted",
Usage: "If false (default), only show non-deleted virtual networks. If true, only show deleted virtual networks.",
}
FilterFlags = []cli.Flag{
&filterId,
&filterName,
&filterDefault,
&filterDeleted,
VnetFilterFlags = []cli.Flag{
&filterVnetId,
&filterVnetByName,
&filterDefaultVnet,
&filterDeletedVnet,
}
)
// Filter which virtual networks get queried.
type Filter struct {
// VnetFilter which virtual networks get queried.
type VnetFilter struct {
queryParams url.Values
}
func NewFilter() *Filter {
return &Filter{
func NewVnetFilter() *VnetFilter {
return &VnetFilter{
queryParams: url.Values{},
}
}
func (f *Filter) ById(vnetId uuid.UUID) {
func (f *VnetFilter) ById(vnetId uuid.UUID) {
f.queryParams.Set("id", vnetId.String())
}
func (f *Filter) ByName(name string) {
func (f *VnetFilter) ByName(name string) {
f.queryParams.Set("name", name)
}
func (f *Filter) ByDefaultStatus(isDefault bool) {
func (f *VnetFilter) ByDefaultStatus(isDefault bool) {
f.queryParams.Set("is_default", strconv.FormatBool(isDefault))
}
func (f *Filter) WithDeleted(isDeleted bool) {
func (f *VnetFilter) WithDeleted(isDeleted bool) {
f.queryParams.Set("is_deleted", strconv.FormatBool(isDeleted))
}
func (f *Filter) MaxFetchSize(max uint) {
func (f *VnetFilter) MaxFetchSize(max uint) {
f.queryParams.Set("per_page", strconv.Itoa(int(max)))
}
func (f Filter) Encode() string {
func (f VnetFilter) Encode() string {
return f.queryParams.Encode()
}
// NewFromCLI parses CLI flags to discover which filters should get applied to list virtual networks.
func NewFromCLI(c *cli.Context) (*Filter, error) {
f := NewFilter()
func NewFromCLI(c *cli.Context) (*VnetFilter, error) {
f := NewVnetFilter()
if id := c.String("id"); id != "" {
vnetId, err := uuid.Parse(id)

View File

@ -1,4 +1,4 @@
package vnet
package cfapi
import (
"encoding/json"

View File

@ -21,6 +21,7 @@ import (
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
"github.com/cloudflare/cloudflared/cfapi"
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/proxydns"
@ -35,7 +36,6 @@ import (
"github.com/cloudflare/cloudflared/signal"
"github.com/cloudflare/cloudflared/tlsconfig"
"github.com/cloudflare/cloudflared/tunneldns"
"github.com/cloudflare/cloudflared/tunnelstore"
)
const (
@ -212,12 +212,12 @@ func runClassicTunnel(sc *subcommandContext) error {
return StartServer(sc.c, version, nil, sc.log, sc.isUIEnabled)
}
func routeFromFlag(c *cli.Context) (route tunnelstore.Route, ok bool) {
func routeFromFlag(c *cli.Context) (route cfapi.HostnameRoute, ok bool) {
if hostname := c.String("hostname"); hostname != "" {
if lbPool := c.String("lb-pool"); lbPool != "" {
return tunnelstore.NewLBRoute(hostname, lbPool), true
return cfapi.NewLBRoute(hostname, lbPool), true
}
return tunnelstore.NewDNSRoute(hostname, c.Bool(overwriteDNSFlagName)), true
return cfapi.NewDNSRoute(hostname, c.Bool(overwriteDNSFlagName)), true
}
return nil, false
}

View File

@ -5,12 +5,12 @@ import (
"github.com/google/uuid"
"github.com/cloudflare/cloudflared/tunnelstore"
"github.com/cloudflare/cloudflared/cfapi"
)
type Info struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
CreatedAt time.Time `json:"createdAt"`
Connectors []*tunnelstore.ActiveClient `json:"conns"`
ID uuid.UUID `json:"id"`
Name string `json:"name"`
CreatedAt time.Time `json:"createdAt"`
Connectors []*cfapi.ActiveClient `json:"conns"`
}

View File

@ -14,9 +14,9 @@ import (
"github.com/urfave/cli/v2"
"github.com/cloudflare/cloudflared/certutil"
"github.com/cloudflare/cloudflared/cfapi"
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/tunnelstore"
)
type errInvalidJSONCredential struct {
@ -37,7 +37,7 @@ type subcommandContext struct {
fs fileSystem
// These fields should be accessed using their respective Getter
tunnelstoreClient tunnelstore.Client
tunnelstoreClient cfapi.Client
userCredential *userCredential
}
@ -68,7 +68,7 @@ type userCredential struct {
certPath string
}
func (sc *subcommandContext) client() (tunnelstore.Client, error) {
func (sc *subcommandContext) client() (cfapi.Client, error) {
if sc.tunnelstoreClient != nil {
return sc.tunnelstoreClient, nil
}
@ -77,7 +77,7 @@ func (sc *subcommandContext) client() (tunnelstore.Client, error) {
return nil, err
}
userAgent := fmt.Sprintf("cloudflared/%s", version)
client, err := tunnelstore.NewRESTClient(
client, err := cfapi.NewRESTClient(
sc.c.String("api-url"),
credential.cert.AccountID,
credential.cert.ZoneID,
@ -149,7 +149,7 @@ func (sc *subcommandContext) readTunnelCredentials(credFinder CredFinder) (conne
return credentials, nil
}
func (sc *subcommandContext) create(name string, credentialsFilePath string, secret string) (*tunnelstore.Tunnel, error) {
func (sc *subcommandContext) create(name string, credentialsFilePath string, secret string) (*cfapi.Tunnel, error) {
client, err := sc.client()
if err != nil {
return nil, errors.Wrap(err, "couldn't create client to talk to Cloudflare Tunnel backend")
@ -224,7 +224,7 @@ func (sc *subcommandContext) create(name string, credentialsFilePath string, sec
return tunnel, nil
}
func (sc *subcommandContext) list(filter *tunnelstore.Filter) ([]*tunnelstore.Tunnel, error) {
func (sc *subcommandContext) list(filter *cfapi.TunnelFilter) ([]*cfapi.Tunnel, error) {
client, err := sc.client()
if err != nil {
return nil, err
@ -251,7 +251,7 @@ func (sc *subcommandContext) delete(tunnelIDs []uuid.UUID) error {
return fmt.Errorf("Tunnel %s has already been deleted", tunnel.ID)
}
if forceFlagSet {
if err := client.CleanupConnections(tunnel.ID, tunnelstore.NewCleanupParams()); err != nil {
if err := client.CleanupConnections(tunnel.ID, cfapi.NewCleanupParams()); err != nil {
return errors.Wrapf(err, "Error cleaning up connections for tunnel %s", tunnel.ID)
}
}
@ -311,7 +311,7 @@ func (sc *subcommandContext) run(tunnelID uuid.UUID) error {
}
func (sc *subcommandContext) cleanupConnections(tunnelIDs []uuid.UUID) error {
params := tunnelstore.NewCleanupParams()
params := cfapi.NewCleanupParams()
extraLog := ""
if connector := sc.c.String("connector-id"); connector != "" {
connectorID, err := uuid.Parse(connector)
@ -335,7 +335,7 @@ func (sc *subcommandContext) cleanupConnections(tunnelIDs []uuid.UUID) error {
return nil
}
func (sc *subcommandContext) route(tunnelID uuid.UUID, r tunnelstore.Route) (tunnelstore.RouteResult, error) {
func (sc *subcommandContext) route(tunnelID uuid.UUID, r cfapi.HostnameRoute) (cfapi.HostnameRouteResult, error) {
client, err := sc.client()
if err != nil {
return nil, err
@ -345,8 +345,8 @@ func (sc *subcommandContext) route(tunnelID uuid.UUID, r tunnelstore.Route) (tun
}
// Query Tunnelstore to find the active tunnel with the given name.
func (sc *subcommandContext) tunnelActive(name string) (*tunnelstore.Tunnel, bool, error) {
filter := tunnelstore.NewFilter()
func (sc *subcommandContext) tunnelActive(name string) (*cfapi.Tunnel, bool, error) {
filter := cfapi.NewTunnelFilter()
filter.NoDeleted()
filter.ByName(name)
tunnels, err := sc.list(filter)
@ -391,7 +391,7 @@ func (sc *subcommandContext) findIDs(inputs []string) ([]uuid.UUID, error) {
uuids, names := splitUuids(inputs)
for _, name := range names {
filter := tunnelstore.NewFilter()
filter := cfapi.NewTunnelFilter()
filter.NoDeleted()
filter.ByName(name)

View File

@ -3,12 +3,12 @@ package tunnel
import (
"github.com/pkg/errors"
"github.com/cloudflare/cloudflared/teamnet"
"github.com/cloudflare/cloudflared/cfapi"
)
const noClientMsg = "error while creating backend client"
func (sc *subcommandContext) listRoutes(filter *teamnet.Filter) ([]*teamnet.DetailedRoute, error) {
func (sc *subcommandContext) listRoutes(filter *cfapi.IpRouteFilter) ([]*cfapi.DetailedRoute, error) {
client, err := sc.client()
if err != nil {
return nil, errors.Wrap(err, noClientMsg)
@ -16,15 +16,15 @@ func (sc *subcommandContext) listRoutes(filter *teamnet.Filter) ([]*teamnet.Deta
return client.ListRoutes(filter)
}
func (sc *subcommandContext) addRoute(newRoute teamnet.NewRoute) (teamnet.Route, error) {
func (sc *subcommandContext) addRoute(newRoute cfapi.NewRoute) (cfapi.Route, error) {
client, err := sc.client()
if err != nil {
return teamnet.Route{}, errors.Wrap(err, noClientMsg)
return cfapi.Route{}, errors.Wrap(err, noClientMsg)
}
return client.AddRoute(newRoute)
}
func (sc *subcommandContext) deleteRoute(params teamnet.DeleteRouteParams) error {
func (sc *subcommandContext) deleteRoute(params cfapi.DeleteRouteParams) error {
client, err := sc.client()
if err != nil {
return errors.Wrap(err, noClientMsg)
@ -32,10 +32,10 @@ func (sc *subcommandContext) deleteRoute(params teamnet.DeleteRouteParams) error
return client.DeleteRoute(params)
}
func (sc *subcommandContext) getRouteByIP(params teamnet.GetRouteByIpParams) (teamnet.DetailedRoute, error) {
func (sc *subcommandContext) getRouteByIP(params cfapi.GetRouteByIpParams) (cfapi.DetailedRoute, error) {
client, err := sc.client()
if err != nil {
return teamnet.DetailedRoute{}, errors.Wrap(err, noClientMsg)
return cfapi.DetailedRoute{}, errors.Wrap(err, noClientMsg)
}
return client.GetByIP(params)
}

View File

@ -13,8 +13,8 @@ import (
"github.com/rs/zerolog"
"github.com/urfave/cli/v2"
"github.com/cloudflare/cloudflared/cfapi"
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/tunnelstore"
)
type mockFileSystem struct {
@ -36,7 +36,7 @@ func Test_subcommandContext_findCredentials(t *testing.T) {
log *zerolog.Logger
isUIEnabled bool
fs fileSystem
tunnelstoreClient tunnelstore.Client
tunnelstoreClient cfapi.Client
userCredential *userCredential
}
type args struct {
@ -187,13 +187,13 @@ func Test_subcommandContext_findCredentials(t *testing.T) {
}
type deleteMockTunnelStore struct {
tunnelstore.Client
cfapi.Client
mockTunnels map[uuid.UUID]mockTunnelBehaviour
deletedTunnelIDs []uuid.UUID
}
type mockTunnelBehaviour struct {
tunnel tunnelstore.Tunnel
tunnel cfapi.Tunnel
deleteErr error
cleanupErr error
}
@ -209,7 +209,7 @@ func newDeleteMockTunnelStore(tunnels ...mockTunnelBehaviour) *deleteMockTunnelS
}
}
func (d *deleteMockTunnelStore) GetTunnel(tunnelID uuid.UUID) (*tunnelstore.Tunnel, error) {
func (d *deleteMockTunnelStore) GetTunnel(tunnelID uuid.UUID) (*cfapi.Tunnel, error) {
tunnel, ok := d.mockTunnels[tunnelID]
if !ok {
return nil, fmt.Errorf("Couldn't find tunnel: %v", tunnelID)
@ -233,7 +233,7 @@ func (d *deleteMockTunnelStore) DeleteTunnel(tunnelID uuid.UUID) error {
return nil
}
func (d *deleteMockTunnelStore) CleanupConnections(tunnelID uuid.UUID, _ *tunnelstore.CleanupParams) error {
func (d *deleteMockTunnelStore) CleanupConnections(tunnelID uuid.UUID, _ *cfapi.CleanupParams) error {
tunnel, ok := d.mockTunnels[tunnelID]
if !ok {
return fmt.Errorf("Couldn't find tunnel: %v", tunnelID)
@ -284,10 +284,10 @@ func Test_subcommandContext_Delete(t *testing.T) {
}(),
tunnelstoreClient: newDeleteMockTunnelStore(
mockTunnelBehaviour{
tunnel: tunnelstore.Tunnel{ID: tunnelID1},
tunnel: cfapi.Tunnel{ID: tunnelID1},
},
mockTunnelBehaviour{
tunnel: tunnelstore.Tunnel{ID: tunnelID2},
tunnel: cfapi.Tunnel{ID: tunnelID2},
},
),
},

View File

@ -4,18 +4,18 @@ import (
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/cloudflare/cloudflared/vnet"
"github.com/cloudflare/cloudflared/cfapi"
)
func (sc *subcommandContext) addVirtualNetwork(newVnet vnet.NewVirtualNetwork) (vnet.VirtualNetwork, error) {
func (sc *subcommandContext) addVirtualNetwork(newVnet cfapi.NewVirtualNetwork) (cfapi.VirtualNetwork, error) {
client, err := sc.client()
if err != nil {
return vnet.VirtualNetwork{}, errors.Wrap(err, noClientMsg)
return cfapi.VirtualNetwork{}, errors.Wrap(err, noClientMsg)
}
return client.CreateVirtualNetwork(newVnet)
}
func (sc *subcommandContext) listVirtualNetworks(filter *vnet.Filter) ([]*vnet.VirtualNetwork, error) {
func (sc *subcommandContext) listVirtualNetworks(filter *cfapi.VnetFilter) ([]*cfapi.VirtualNetwork, error) {
client, err := sc.client()
if err != nil {
return nil, errors.Wrap(err, noClientMsg)
@ -31,7 +31,7 @@ func (sc *subcommandContext) deleteVirtualNetwork(vnetId uuid.UUID) error {
return client.DeleteVirtualNetwork(vnetId)
}
func (sc *subcommandContext) updateVirtualNetwork(vnetId uuid.UUID, updates vnet.UpdateVirtualNetwork) error {
func (sc *subcommandContext) updateVirtualNetwork(vnetId uuid.UUID, updates cfapi.UpdateVirtualNetwork) error {
client, err := sc.client()
if err != nil {
return errors.Wrap(err, noClientMsg)

View File

@ -21,11 +21,11 @@ import (
"golang.org/x/net/idna"
yaml "gopkg.in/yaml.v2"
"github.com/cloudflare/cloudflared/cfapi"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
"github.com/cloudflare/cloudflared/config"
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/tunnelstore"
)
const (
@ -64,8 +64,8 @@ var (
Name: "when",
Aliases: []string{"w"},
Usage: "List tunnels that are active at the given `TIME` in RFC3339 format",
Layout: tunnelstore.TimeLayout,
DefaultText: fmt.Sprintf("current time, %s", time.Now().Format(tunnelstore.TimeLayout)),
Layout: cfapi.TimeLayout,
DefaultText: fmt.Sprintf("current time, %s", time.Now().Format(cfapi.TimeLayout)),
}
listIDFlag = &cli.StringFlag{
Name: "id",
@ -260,7 +260,7 @@ func listCommand(c *cli.Context) error {
warningChecker := updater.StartWarningCheck(c)
defer warningChecker.LogWarningIfAny(sc.log)
filter := tunnelstore.NewFilter()
filter := cfapi.NewTunnelFilter()
if !c.Bool("show-deleted") {
filter.NoDeleted()
}
@ -335,7 +335,7 @@ func listCommand(c *cli.Context) error {
return nil
}
func formatAndPrintTunnelList(tunnels []*tunnelstore.Tunnel, showRecentlyDisconnected bool) {
func formatAndPrintTunnelList(tunnels []*cfapi.Tunnel, showRecentlyDisconnected bool) {
writer := tabWriter()
defer writer.Flush()
@ -357,7 +357,7 @@ func formatAndPrintTunnelList(tunnels []*tunnelstore.Tunnel, showRecentlyDisconn
}
}
func fmtConnections(connections []tunnelstore.Connection, showRecentlyDisconnected bool) string {
func fmtConnections(connections []cfapi.Connection, showRecentlyDisconnected bool) string {
// Count connections per colo
numConnsPerColo := make(map[string]uint, len(connections))
@ -477,8 +477,8 @@ func tunnelInfo(c *cli.Context) error {
return nil
}
func getTunnel(sc *subcommandContext, tunnelID uuid.UUID) (*tunnelstore.Tunnel, error) {
filter := tunnelstore.NewFilter()
func getTunnel(sc *subcommandContext, tunnelID uuid.UUID) (*cfapi.Tunnel, error) {
filter := cfapi.NewTunnelFilter()
filter.ByTunnelID(tunnelID)
tunnels, err := sc.list(filter)
if err != nil {
@ -711,7 +711,7 @@ Further information about managing Cloudflare WARP traffic to your tunnel is ava
{
Name: "dns",
Action: cliutil.ConfiguredAction(routeDnsCommand),
Usage: "Route a hostname by creating a DNS CNAME record to a tunnel",
Usage: "HostnameRoute a hostname by creating a DNS CNAME record to a tunnel",
UsageText: "cloudflared tunnel route dns [TUNNEL] [HOSTNAME]",
Description: `Creates a DNS CNAME record hostname that points to the tunnel.`,
Flags: []cli.Flag{overwriteDNSFlag},
@ -728,7 +728,7 @@ Further information about managing Cloudflare WARP traffic to your tunnel is ava
}
}
func dnsRouteFromArg(c *cli.Context, overwriteExisting bool) (tunnelstore.Route, error) {
func dnsRouteFromArg(c *cli.Context, overwriteExisting bool) (cfapi.HostnameRoute, error) {
const (
userHostnameIndex = 1
expectedNArgs = 2
@ -742,10 +742,10 @@ func dnsRouteFromArg(c *cli.Context, overwriteExisting bool) (tunnelstore.Route,
} else if !validateHostname(userHostname, true) {
return nil, errors.Errorf("%s is not a valid hostname", userHostname)
}
return tunnelstore.NewDNSRoute(userHostname, overwriteExisting), nil
return cfapi.NewDNSRoute(userHostname, overwriteExisting), nil
}
func lbRouteFromArg(c *cli.Context) (tunnelstore.Route, error) {
func lbRouteFromArg(c *cli.Context) (cfapi.HostnameRoute, error) {
const (
lbNameIndex = 1
lbPoolIndex = 2
@ -768,7 +768,7 @@ func lbRouteFromArg(c *cli.Context) (tunnelstore.Route, error) {
return nil, errors.Errorf("%s is not a valid pool name", lbPool)
}
return tunnelstore.NewLBRoute(lbName, lbPool), nil
return cfapi.NewLBRoute(lbName, lbPool), nil
}
var nameRegex = regexp.MustCompile("^[_a-zA-Z0-9][-_.a-zA-Z0-9]*$")
@ -815,7 +815,7 @@ func routeCommand(c *cli.Context, routeType string) error {
if err != nil {
return err
}
var route tunnelstore.Route
var route cfapi.HostnameRoute
switch routeType {
case "dns":
route, err = dnsRouteFromArg(c, c.Bool(overwriteDNSFlagName))

View File

@ -8,12 +8,12 @@ import (
homedir "github.com/mitchellh/go-homedir"
"github.com/stretchr/testify/assert"
"github.com/cloudflare/cloudflared/tunnelstore"
"github.com/cloudflare/cloudflared/cfapi"
)
func Test_fmtConnections(t *testing.T) {
type args struct {
connections []tunnelstore.Connection
connections []cfapi.Connection
}
tests := []struct {
name string
@ -23,14 +23,14 @@ func Test_fmtConnections(t *testing.T) {
{
name: "empty",
args: args{
connections: []tunnelstore.Connection{},
connections: []cfapi.Connection{},
},
want: "",
},
{
name: "trivial",
args: args{
connections: []tunnelstore.Connection{
connections: []cfapi.Connection{
{
ColoName: "DFW",
ID: uuid.MustParse("ea550130-57fd-4463-aab1-752822231ddd"),
@ -42,7 +42,7 @@ func Test_fmtConnections(t *testing.T) {
{
name: "with a pending reconnect",
args: args{
connections: []tunnelstore.Connection{
connections: []cfapi.Connection{
{
ColoName: "DFW",
ID: uuid.MustParse("ea550130-57fd-4463-aab1-752822231ddd"),
@ -55,7 +55,7 @@ func Test_fmtConnections(t *testing.T) {
{
name: "many colos",
args: args{
connections: []tunnelstore.Connection{
connections: []cfapi.Connection{
{
ColoName: "YRV",
ID: uuid.MustParse("ea550130-57fd-4463-aab1-752822231ddd"),

View File

@ -8,12 +8,11 @@ import (
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"github.com/cloudflare/cloudflared/cfapi"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
"github.com/cloudflare/cloudflared/teamnet"
"github.com/urfave/cli/v2"
)
var (
@ -92,7 +91,7 @@ to tell which virtual network whose routing table you want to use.`,
func showRoutesFlags() []cli.Flag {
flags := make([]cli.Flag, 0)
flags = append(flags, teamnet.FilterFlags...)
flags = append(flags, cfapi.IpRouteFilterFlags...)
flags = append(flags, outputFormatFlag)
return flags
}
@ -103,7 +102,7 @@ func showRoutesCommand(c *cli.Context) error {
return err
}
filter, err := teamnet.NewFromCLI(c)
filter, err := cfapi.NewIpRouteFilterFromCLI(c)
if err != nil {
return errors.Wrap(err, "invalid config for routing filters")
}
@ -168,7 +167,7 @@ func addRouteCommand(c *cli.Context) error {
vnetId = &id
}
_, err = sc.addRoute(teamnet.NewRoute{
_, err = sc.addRoute(cfapi.NewRoute{
Comment: comment,
Network: *network,
TunnelID: tunnelID,
@ -199,7 +198,7 @@ func deleteRouteCommand(c *cli.Context) error {
return errors.New("Invalid network CIDR")
}
params := teamnet.DeleteRouteParams{
params := cfapi.DeleteRouteParams{
Network: *network,
}
@ -233,7 +232,7 @@ func getRouteByIPCommand(c *cli.Context) error {
return fmt.Errorf("Invalid IP %s", ipInput)
}
params := teamnet.GetRouteByIpParams{
params := cfapi.GetRouteByIpParams{
Ip: ip,
}
@ -252,12 +251,12 @@ func getRouteByIPCommand(c *cli.Context) error {
if route.IsZero() {
fmt.Printf("No route matches the IP %s\n", ip)
} else {
formatAndPrintRouteList([]*teamnet.DetailedRoute{&route})
formatAndPrintRouteList([]*cfapi.DetailedRoute{&route})
}
return nil
}
func formatAndPrintRouteList(routes []*teamnet.DetailedRoute) {
func formatAndPrintRouteList(routes []*cfapi.DetailedRoute) {
const (
minWidth = 0
tabWidth = 8

View File

@ -9,9 +9,9 @@ import (
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"github.com/cloudflare/cloudflared/cfapi"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
"github.com/cloudflare/cloudflared/vnet"
)
var (
@ -102,7 +102,7 @@ default or update an existing one to become the default.`,
func listVirtualNetworksFlags() []cli.Flag {
flags := make([]cli.Flag, 0)
flags = append(flags, vnet.FilterFlags...)
flags = append(flags, cfapi.VnetFilterFlags...)
flags = append(flags, outputFormatFlag)
return flags
}
@ -128,7 +128,7 @@ func addVirtualNetworkCommand(c *cli.Context) error {
comment = args.Get(1)
}
newVnet := vnet.NewVirtualNetwork{
newVnet := cfapi.NewVirtualNetwork{
Name: name,
Comment: comment,
IsDefault: c.Bool(makeDefaultFlag.Name),
@ -160,7 +160,7 @@ func listVirtualNetworksCommand(c *cli.Context) error {
warningChecker := updater.StartWarningCheck(c)
defer warningChecker.LogWarningIfAny(sc.log)
filter, err := vnet.NewFromCLI(c)
filter, err := cfapi.NewFromCLI(c)
if err != nil {
return errors.Wrap(err, "invalid flags for filtering virtual networks")
}
@ -220,7 +220,7 @@ func updateVirtualNetworkCommand(c *cli.Context) error {
return err
}
updates := vnet.UpdateVirtualNetwork{}
updates := cfapi.UpdateVirtualNetwork{}
if c.IsSet(newNameFlag.Name) {
newName := c.String(newNameFlag.Name)
@ -248,7 +248,7 @@ func getVnetId(sc *subcommandContext, input string) (uuid.UUID, error) {
return val, nil
}
filter := vnet.NewFilter()
filter := cfapi.NewVnetFilter()
filter.WithDeleted(false)
filter.ByName(input)
@ -264,7 +264,7 @@ func getVnetId(sc *subcommandContext, input string) (uuid.UUID, error) {
return vnets[0].ID, nil
}
func formatAndPrintVnetsList(vnets []*vnet.VirtualNetwork) {
func formatAndPrintVnetsList(vnets []*cfapi.VirtualNetwork) {
const (
minWidth = 0
tabWidth = 8

View File

@ -1,25 +0,0 @@
package tunnelstore
import (
"net/url"
"github.com/google/uuid"
)
type CleanupParams struct {
queryParams url.Values
}
func NewCleanupParams() *CleanupParams {
return &CleanupParams{
queryParams: url.Values{},
}
}
func (cp *CleanupParams) ForClient(clientID uuid.UUID) {
cp.queryParams.Set("client_id", clientID.String())
}
func (cp CleanupParams) encode() string {
return cp.queryParams.Encode()
}

View File

@ -1,545 +0,0 @@
package tunnelstore
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/url"
"path"
"strings"
"time"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"golang.org/x/net/http2"
"github.com/cloudflare/cloudflared/teamnet"
"github.com/cloudflare/cloudflared/vnet"
)
const (
defaultTimeout = 15 * time.Second
jsonContentType = "application/json"
)
var (
ErrTunnelNameConflict = errors.New("tunnel with name already exists")
ErrUnauthorized = errors.New("unauthorized")
ErrBadRequest = errors.New("incorrect request parameters")
ErrNotFound = errors.New("not found")
ErrAPINoSuccess = errors.New("API call failed")
)
type Tunnel struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
CreatedAt time.Time `json:"created_at"`
DeletedAt time.Time `json:"deleted_at"`
Connections []Connection `json:"connections"`
}
type Connection struct {
ColoName string `json:"colo_name"`
ID uuid.UUID `json:"id"`
IsPendingReconnect bool `json:"is_pending_reconnect"`
OriginIP net.IP `json:"origin_ip"`
OpenedAt time.Time `json:"opened_at"`
}
type ActiveClient struct {
ID uuid.UUID `json:"id"`
Features []string `json:"features"`
Version string `json:"version"`
Arch string `json:"arch"`
RunAt time.Time `json:"run_at"`
Connections []Connection `json:"conns"`
}
type Change = string
const (
ChangeNew = "new"
ChangeUpdated = "updated"
ChangeUnchanged = "unchanged"
)
// Route represents a record type that can route to a tunnel
type Route interface {
json.Marshaler
RecordType() string
UnmarshalResult(body io.Reader) (RouteResult, error)
String() string
}
type RouteResult interface {
// SuccessSummary explains what will route to this tunnel when it's provisioned successfully
SuccessSummary() string
}
type DNSRoute struct {
userHostname string
overwriteExisting bool
}
type DNSRouteResult struct {
route *DNSRoute
CName Change `json:"cname"`
Name string `json:"name"`
}
func NewDNSRoute(userHostname string, overwriteExisting bool) Route {
return &DNSRoute{
userHostname: userHostname,
overwriteExisting: overwriteExisting,
}
}
func (dr *DNSRoute) MarshalJSON() ([]byte, error) {
s := struct {
Type string `json:"type"`
UserHostname string `json:"user_hostname"`
OverwriteExisting bool `json:"overwrite_existing"`
}{
Type: dr.RecordType(),
UserHostname: dr.userHostname,
OverwriteExisting: dr.overwriteExisting,
}
return json.Marshal(&s)
}
func (dr *DNSRoute) UnmarshalResult(body io.Reader) (RouteResult, error) {
var result DNSRouteResult
err := parseResponse(body, &result)
result.route = dr
return &result, err
}
func (dr *DNSRoute) RecordType() string {
return "dns"
}
func (dr *DNSRoute) String() string {
return fmt.Sprintf("%s %s", dr.RecordType(), dr.userHostname)
}
func (res *DNSRouteResult) SuccessSummary() string {
var msgFmt string
switch res.CName {
case ChangeNew:
msgFmt = "Added CNAME %s which will route to this tunnel"
case ChangeUpdated: // this is not currently returned by tunnelsore
msgFmt = "%s updated to route to your tunnel"
case ChangeUnchanged:
msgFmt = "%s is already configured to route to your tunnel"
}
return fmt.Sprintf(msgFmt, res.hostname())
}
// hostname yields the resulting name for the DNS route; if that is not available from Cloudflare API, then the
// requested name is returned instead (should not be the common path, it is just a fall-back).
func (res *DNSRouteResult) hostname() string {
if res.Name != "" {
return res.Name
}
return res.route.userHostname
}
type LBRoute struct {
lbName string
lbPool string
}
type LBRouteResult struct {
route *LBRoute
LoadBalancer Change `json:"load_balancer"`
Pool Change `json:"pool"`
}
func NewLBRoute(lbName, lbPool string) Route {
return &LBRoute{
lbName: lbName,
lbPool: lbPool,
}
}
func (lr *LBRoute) MarshalJSON() ([]byte, error) {
s := struct {
Type string `json:"type"`
LBName string `json:"lb_name"`
LBPool string `json:"lb_pool"`
}{
Type: lr.RecordType(),
LBName: lr.lbName,
LBPool: lr.lbPool,
}
return json.Marshal(&s)
}
func (lr *LBRoute) RecordType() string {
return "lb"
}
func (lb *LBRoute) String() string {
return fmt.Sprintf("%s %s %s", lb.RecordType(), lb.lbName, lb.lbPool)
}
func (lr *LBRoute) UnmarshalResult(body io.Reader) (RouteResult, error) {
var result LBRouteResult
err := parseResponse(body, &result)
result.route = lr
return &result, err
}
func (res *LBRouteResult) SuccessSummary() string {
var msg string
switch res.LoadBalancer + "," + res.Pool {
case "new,new":
msg = "Created load balancer %s and added a new pool %s with this tunnel as an origin"
case "new,updated":
msg = "Created load balancer %s with an existing pool %s which was updated to use this tunnel as an origin"
case "new,unchanged":
msg = "Created load balancer %s with an existing pool %s which already has this tunnel as an origin"
case "updated,new":
msg = "Added new pool %[2]s with this tunnel as an origin to load balancer %[1]s"
case "updated,updated":
msg = "Updated pool %[2]s to use this tunnel as an origin and added it to load balancer %[1]s"
case "updated,unchanged":
msg = "Added pool %[2]s, which already has this tunnel as an origin, to load balancer %[1]s"
case "unchanged,updated":
msg = "Added this tunnel as an origin in pool %[2]s which is already used by load balancer %[1]s"
case "unchanged,unchanged":
msg = "Load balancer %s already uses pool %s which has this tunnel as an origin"
case "unchanged,new":
// this state is not possible
fallthrough
default:
msg = "Something went wrong: failed to modify load balancer %s with pool %s; please check traffic manager configuration in the dashboard"
}
return fmt.Sprintf(msg, res.route.lbName, res.route.lbPool)
}
type Client interface {
// Named Tunnels endpoints
CreateTunnel(name string, tunnelSecret []byte) (*Tunnel, error)
GetTunnel(tunnelID uuid.UUID) (*Tunnel, error)
DeleteTunnel(tunnelID uuid.UUID) error
ListTunnels(filter *Filter) ([]*Tunnel, error)
ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error)
CleanupConnections(tunnelID uuid.UUID, params *CleanupParams) error
RouteTunnel(tunnelID uuid.UUID, route Route) (RouteResult, error)
// Teamnet endpoints
ListRoutes(filter *teamnet.Filter) ([]*teamnet.DetailedRoute, error)
AddRoute(newRoute teamnet.NewRoute) (teamnet.Route, error)
DeleteRoute(params teamnet.DeleteRouteParams) error
GetByIP(params teamnet.GetRouteByIpParams) (teamnet.DetailedRoute, error)
// Virtual Networks endpoints
CreateVirtualNetwork(newVnet vnet.NewVirtualNetwork) (vnet.VirtualNetwork, error)
ListVirtualNetworks(filter *vnet.Filter) ([]*vnet.VirtualNetwork, error)
DeleteVirtualNetwork(id uuid.UUID) error
UpdateVirtualNetwork(id uuid.UUID, updates vnet.UpdateVirtualNetwork) error
}
type RESTClient struct {
baseEndpoints *baseEndpoints
authToken string
userAgent string
client http.Client
log *zerolog.Logger
}
type baseEndpoints struct {
accountLevel url.URL
zoneLevel url.URL
accountRoutes url.URL
accountVnets url.URL
}
var _ Client = (*RESTClient)(nil)
func NewRESTClient(baseURL, accountTag, zoneTag, authToken, userAgent string, log *zerolog.Logger) (*RESTClient, error) {
if strings.HasSuffix(baseURL, "/") {
baseURL = baseURL[:len(baseURL)-1]
}
accountLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/tunnels", baseURL, accountTag))
if err != nil {
return nil, errors.Wrap(err, "failed to create account level endpoint")
}
accountRoutesEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/teamnet/routes", baseURL, accountTag))
if err != nil {
return nil, errors.Wrap(err, "failed to create route account-level endpoint")
}
accountVnetsEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/teamnet/virtual_networks", baseURL, accountTag))
if err != nil {
return nil, errors.Wrap(err, "failed to create virtual network account-level endpoint")
}
zoneLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/zones/%s/tunnels", baseURL, zoneTag))
if err != nil {
return nil, errors.Wrap(err, "failed to create account level endpoint")
}
httpTransport := http.Transport{
TLSHandshakeTimeout: defaultTimeout,
ResponseHeaderTimeout: defaultTimeout,
}
http2.ConfigureTransport(&httpTransport)
return &RESTClient{
baseEndpoints: &baseEndpoints{
accountLevel: *accountLevelEndpoint,
zoneLevel: *zoneLevelEndpoint,
accountRoutes: *accountRoutesEndpoint,
accountVnets: *accountVnetsEndpoint,
},
authToken: authToken,
userAgent: userAgent,
client: http.Client{
Transport: &httpTransport,
Timeout: defaultTimeout,
},
log: log,
}, nil
}
type newTunnel struct {
Name string `json:"name"`
TunnelSecret []byte `json:"tunnel_secret"`
}
func (r *RESTClient) CreateTunnel(name string, tunnelSecret []byte) (*Tunnel, error) {
if name == "" {
return nil, errors.New("tunnel name required")
}
if _, err := uuid.Parse(name); err == nil {
return nil, errors.New("you cannot use UUIDs as tunnel names")
}
body := &newTunnel{
Name: name,
TunnelSecret: tunnelSecret,
}
resp, err := r.sendRequest("POST", r.baseEndpoints.accountLevel, body)
if err != nil {
return nil, errors.Wrap(err, "REST request failed")
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusOK:
return unmarshalTunnel(resp.Body)
case http.StatusConflict:
return nil, ErrTunnelNameConflict
}
return nil, r.statusCodeToError("create tunnel", resp)
}
func (r *RESTClient) GetTunnel(tunnelID uuid.UUID) (*Tunnel, error) {
endpoint := r.baseEndpoints.accountLevel
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", tunnelID))
resp, err := r.sendRequest("GET", endpoint, nil)
if err != nil {
return nil, errors.Wrap(err, "REST request failed")
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
return unmarshalTunnel(resp.Body)
}
return nil, r.statusCodeToError("get tunnel", resp)
}
func (r *RESTClient) DeleteTunnel(tunnelID uuid.UUID) error {
endpoint := r.baseEndpoints.accountLevel
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", tunnelID))
resp, err := r.sendRequest("DELETE", endpoint, nil)
if err != nil {
return errors.Wrap(err, "REST request failed")
}
defer resp.Body.Close()
return r.statusCodeToError("delete tunnel", resp)
}
func (r *RESTClient) ListTunnels(filter *Filter) ([]*Tunnel, error) {
endpoint := r.baseEndpoints.accountLevel
endpoint.RawQuery = filter.encode()
resp, err := r.sendRequest("GET", endpoint, nil)
if err != nil {
return nil, errors.Wrap(err, "REST request failed")
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
return parseListTunnels(resp.Body)
}
return nil, r.statusCodeToError("list tunnels", resp)
}
func parseListTunnels(body io.ReadCloser) ([]*Tunnel, error) {
var tunnels []*Tunnel
err := parseResponse(body, &tunnels)
return tunnels, err
}
func (r *RESTClient) ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error) {
endpoint := r.baseEndpoints.accountLevel
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/connections", tunnelID))
resp, err := r.sendRequest("GET", endpoint, nil)
if err != nil {
return nil, errors.Wrap(err, "REST request failed")
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
return parseConnectionsDetails(resp.Body)
}
return nil, r.statusCodeToError("list connection details", resp)
}
func parseConnectionsDetails(reader io.Reader) ([]*ActiveClient, error) {
var clients []*ActiveClient
err := parseResponse(reader, &clients)
return clients, err
}
func (r *RESTClient) CleanupConnections(tunnelID uuid.UUID, params *CleanupParams) error {
endpoint := r.baseEndpoints.accountLevel
endpoint.RawQuery = params.encode()
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/connections", tunnelID))
resp, err := r.sendRequest("DELETE", endpoint, nil)
if err != nil {
return errors.Wrap(err, "REST request failed")
}
defer resp.Body.Close()
return r.statusCodeToError("cleanup connections", resp)
}
func (r *RESTClient) RouteTunnel(tunnelID uuid.UUID, route Route) (RouteResult, error) {
endpoint := r.baseEndpoints.zoneLevel
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/routes", tunnelID))
resp, err := r.sendRequest("PUT", endpoint, route)
if err != nil {
return nil, errors.Wrap(err, "REST request failed")
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
return route.UnmarshalResult(resp.Body)
}
return nil, r.statusCodeToError("add route", resp)
}
func (r *RESTClient) sendRequest(method string, url url.URL, body interface{}) (*http.Response, error) {
var bodyReader io.Reader
if body != nil {
if bodyBytes, err := json.Marshal(body); err != nil {
return nil, errors.Wrap(err, "failed to serialize json body")
} else {
bodyReader = bytes.NewBuffer(bodyBytes)
}
}
req, err := http.NewRequest(method, url.String(), bodyReader)
if err != nil {
return nil, errors.Wrapf(err, "can't create %s request", method)
}
req.Header.Set("User-Agent", r.userAgent)
if bodyReader != nil {
req.Header.Set("Content-Type", jsonContentType)
}
req.Header.Add("X-Auth-User-Service-Key", r.authToken)
req.Header.Add("Accept", "application/json;version=1")
return r.client.Do(req)
}
func parseResponse(reader io.Reader, data interface{}) error {
// Schema for Tunnelstore responses in the v1 API.
// Roughly, it's a wrapper around a particular result that adds failures/errors/etc
var result response
// First, parse the wrapper and check the API call succeeded
if err := json.NewDecoder(reader).Decode(&result); err != nil {
return errors.Wrap(err, "failed to decode response")
}
if err := result.checkErrors(); err != nil {
return err
}
if !result.Success {
return ErrAPINoSuccess
}
// At this point we know the API call succeeded, so, parse out the inner
// result into the datatype provided as a parameter.
if err := json.Unmarshal(result.Result, &data); err != nil {
return errors.Wrap(err, "the Cloudflare API response was an unexpected type")
}
return nil
}
func unmarshalTunnel(reader io.Reader) (*Tunnel, error) {
var tunnel Tunnel
err := parseResponse(reader, &tunnel)
return &tunnel, err
}
type response struct {
Success bool `json:"success,omitempty"`
Errors []apiErr `json:"errors,omitempty"`
Messages []string `json:"messages,omitempty"`
Result json.RawMessage `json:"result,omitempty"`
}
func (r *response) checkErrors() error {
if len(r.Errors) == 0 {
return nil
}
if len(r.Errors) == 1 {
return r.Errors[0]
}
var messages string
for _, e := range r.Errors {
messages += fmt.Sprintf("%s; ", e)
}
return fmt.Errorf("API errors: %s", messages)
}
type apiErr struct {
Code json.Number `json:"code,omitempty"`
Message string `json:"message,omitempty"`
}
func (e apiErr) Error() string {
return fmt.Sprintf("code: %v, reason: %s", e.Code, e.Message)
}
func (r *RESTClient) statusCodeToError(op string, resp *http.Response) error {
if resp.Header.Get("Content-Type") == "application/json" {
var errorsResp response
if json.NewDecoder(resp.Body).Decode(&errorsResp) == nil {
if err := errorsResp.checkErrors(); err != nil {
return errors.Errorf("Failed to %s: %s", op, err)
}
}
}
switch resp.StatusCode {
case http.StatusOK:
return nil
case http.StatusBadRequest:
return ErrBadRequest
case http.StatusUnauthorized, http.StatusForbidden:
return ErrUnauthorized
case http.StatusNotFound:
return ErrNotFound
}
return errors.Errorf("API call to %s failed with status %d: %s", op,
resp.StatusCode, http.StatusText(resp.StatusCode))
}

View File

@ -1,114 +0,0 @@
package tunnelstore
import (
"io"
"net/http"
"net/url"
"path"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/cloudflare/cloudflared/teamnet"
)
// ListRoutes calls the Tunnelstore GET endpoint for all routes under an account.
func (r *RESTClient) ListRoutes(filter *teamnet.Filter) ([]*teamnet.DetailedRoute, error) {
endpoint := r.baseEndpoints.accountRoutes
endpoint.RawQuery = filter.Encode()
resp, err := r.sendRequest("GET", endpoint, nil)
if err != nil {
return nil, errors.Wrap(err, "REST request failed")
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
return parseListDetailedRoutes(resp.Body)
}
return nil, r.statusCodeToError("list routes", resp)
}
// AddRoute calls the Tunnelstore POST endpoint for a given route.
func (r *RESTClient) AddRoute(newRoute teamnet.NewRoute) (teamnet.Route, error) {
endpoint := r.baseEndpoints.accountRoutes
endpoint.Path = path.Join(endpoint.Path, "network", url.PathEscape(newRoute.Network.String()))
resp, err := r.sendRequest("POST", endpoint, newRoute)
if err != nil {
return teamnet.Route{}, errors.Wrap(err, "REST request failed")
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
return parseRoute(resp.Body)
}
return teamnet.Route{}, r.statusCodeToError("add route", resp)
}
// DeleteRoute calls the Tunnelstore DELETE endpoint for a given route.
func (r *RESTClient) DeleteRoute(params teamnet.DeleteRouteParams) error {
endpoint := r.baseEndpoints.accountRoutes
endpoint.Path = path.Join(endpoint.Path, "network", url.PathEscape(params.Network.String()))
setVnetParam(&endpoint, params.VNetID)
resp, err := r.sendRequest("DELETE", endpoint, nil)
if err != nil {
return errors.Wrap(err, "REST request failed")
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
_, err := parseRoute(resp.Body)
return err
}
return r.statusCodeToError("delete route", resp)
}
// GetByIP checks which route will proxy a given IP.
func (r *RESTClient) GetByIP(params teamnet.GetRouteByIpParams) (teamnet.DetailedRoute, error) {
endpoint := r.baseEndpoints.accountRoutes
endpoint.Path = path.Join(endpoint.Path, "ip", url.PathEscape(params.Ip.String()))
setVnetParam(&endpoint, params.VNetID)
resp, err := r.sendRequest("GET", endpoint, nil)
if err != nil {
return teamnet.DetailedRoute{}, errors.Wrap(err, "REST request failed")
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
return parseDetailedRoute(resp.Body)
}
return teamnet.DetailedRoute{}, r.statusCodeToError("get route by IP", resp)
}
func parseListDetailedRoutes(body io.ReadCloser) ([]*teamnet.DetailedRoute, error) {
var routes []*teamnet.DetailedRoute
err := parseResponse(body, &routes)
return routes, err
}
func parseRoute(body io.ReadCloser) (teamnet.Route, error) {
var route teamnet.Route
err := parseResponse(body, &route)
return route, err
}
func parseDetailedRoute(body io.ReadCloser) (teamnet.DetailedRoute, error) {
var route teamnet.DetailedRoute
err := parseResponse(body, &route)
return route, err
}
// setVnetParam overwrites the URL's query parameters with a query param to scope the Route action to a certain
// virtual network (if one is provided).
func setVnetParam(endpoint *url.URL, vnetID *uuid.UUID) {
queryParams := url.Values{}
if vnetID != nil {
queryParams.Set("virtual_network_id", vnetID.String())
}
endpoint.RawQuery = queryParams.Encode()
}

View File

@ -1,46 +0,0 @@
package vnet
import (
"fmt"
"strconv"
"time"
"github.com/google/uuid"
)
type NewVirtualNetwork struct {
Name string `json:"name"`
Comment string `json:"comment"`
IsDefault bool `json:"is_default"`
}
type VirtualNetwork struct {
ID uuid.UUID `json:"id"`
Comment string `json:"comment"`
Name string `json:"name"`
IsDefault bool `json:"is_default_network"`
CreatedAt time.Time `json:"created_at"`
DeletedAt time.Time `json:"deleted_at"`
}
type UpdateVirtualNetwork struct {
Name *string `json:"name,omitempty"`
Comment *string `json:"comment,omitempty"`
IsDefault *bool `json:"is_default_network,omitempty"`
}
func (virtualNetwork VirtualNetwork) TableString() string {
deletedColumn := "-"
if !virtualNetwork.DeletedAt.IsZero() {
deletedColumn = virtualNetwork.DeletedAt.Format(time.RFC3339)
}
return fmt.Sprintf(
"%s\t%s\t%s\t%s\t%s\t%s\t",
virtualNetwork.ID,
virtualNetwork.Name,
strconv.FormatBool(virtualNetwork.IsDefault),
virtualNetwork.Comment,
virtualNetwork.CreatedAt.Format(time.RFC3339),
deletedColumn,
)
}