206 lines
5.1 KiB
Go
206 lines
5.1 KiB
Go
package tunnelstore
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/cloudflare/cloudflared/logger"
|
|
"github.com/google/uuid"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
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")
|
|
)
|
|
|
|
type Tunnel struct {
|
|
ID string `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:"uuid"`
|
|
}
|
|
|
|
type Client interface {
|
|
CreateTunnel(name string, tunnelSecret []byte) (*Tunnel, error)
|
|
GetTunnel(tunnelID string) (*Tunnel, error)
|
|
DeleteTunnel(tunnelID string) error
|
|
ListTunnels() ([]Tunnel, error)
|
|
CleanupConnections(tunnelID string) error
|
|
}
|
|
|
|
type RESTClient struct {
|
|
baseURL string
|
|
authToken string
|
|
client http.Client
|
|
logger logger.Service
|
|
}
|
|
|
|
var _ Client = (*RESTClient)(nil)
|
|
|
|
func NewRESTClient(baseURL string, accountTag string, authToken string, logger logger.Service) *RESTClient {
|
|
if strings.HasSuffix(baseURL, "/") {
|
|
baseURL = baseURL[:len(baseURL)-1]
|
|
}
|
|
url := fmt.Sprintf("%s/accounts/%s/tunnels", baseURL, accountTag)
|
|
return &RESTClient{
|
|
baseURL: url,
|
|
authToken: authToken,
|
|
client: http.Client{
|
|
Transport: &http.Transport{
|
|
TLSHandshakeTimeout: defaultTimeout,
|
|
ResponseHeaderTimeout: defaultTimeout,
|
|
},
|
|
Timeout: defaultTimeout,
|
|
},
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
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")
|
|
}
|
|
body, err := json.Marshal(&newTunnel{
|
|
Name: name,
|
|
TunnelSecret: tunnelSecret,
|
|
})
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "Failed to serialize new tunnel request")
|
|
}
|
|
|
|
resp, err := r.sendRequest("POST", "", bytes.NewBuffer(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, statusCodeToError("create tunnel", resp)
|
|
}
|
|
|
|
func (r *RESTClient) GetTunnel(tunnelID string) (*Tunnel, error) {
|
|
resp, err := r.sendRequest("GET", tunnelID, 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, statusCodeToError("get tunnel", resp)
|
|
}
|
|
|
|
func (r *RESTClient) DeleteTunnel(tunnelID string) error {
|
|
resp, err := r.sendRequest("DELETE", tunnelID, nil)
|
|
if err != nil {
|
|
return errors.Wrap(err, "REST request failed")
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
return statusCodeToError("delete tunnel", resp)
|
|
}
|
|
|
|
func (r *RESTClient) ListTunnels() ([]Tunnel, error) {
|
|
resp, err := r.sendRequest("GET", "", nil)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "REST request failed")
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode == http.StatusOK {
|
|
var tunnels []Tunnel
|
|
if err := json.NewDecoder(resp.Body).Decode(&tunnels); err != nil {
|
|
return nil, errors.Wrap(err, "failed to decode response")
|
|
}
|
|
return tunnels, nil
|
|
}
|
|
|
|
return nil, statusCodeToError("list tunnels", resp)
|
|
}
|
|
|
|
func (r *RESTClient) CleanupConnections(tunnelID string) error {
|
|
resp, err := r.sendRequest("DELETE", fmt.Sprintf("%s/connections", tunnelID), nil)
|
|
if err != nil {
|
|
return errors.Wrap(err, "REST request failed")
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
return statusCodeToError("cleanup connections", resp)
|
|
}
|
|
|
|
func (r *RESTClient) resolve(target string) string {
|
|
if target != "" {
|
|
return r.baseURL + "/" + target
|
|
}
|
|
return r.baseURL
|
|
}
|
|
|
|
func (r *RESTClient) sendRequest(method string, target string, body io.Reader) (*http.Response, error) {
|
|
url := r.resolve(target)
|
|
r.logger.Debugf("%s %s", method, url)
|
|
req, err := http.NewRequest(method, url, body)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "can't create %s request", method)
|
|
}
|
|
if body != nil {
|
|
req.Header.Set("Content-Type", jsonContentType)
|
|
}
|
|
req.Header.Add("X-Auth-User-Service-Key", r.authToken)
|
|
return r.client.Do(req)
|
|
}
|
|
|
|
func unmarshalTunnel(reader io.Reader) (*Tunnel, error) {
|
|
var tunnel Tunnel
|
|
if err := json.NewDecoder(reader).Decode(&tunnel); err != nil {
|
|
return nil, errors.Wrap(err, "failed to decode response")
|
|
}
|
|
return &tunnel, nil
|
|
}
|
|
|
|
func statusCodeToError(op string, resp *http.Response) error {
|
|
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))
|
|
}
|