2021-12-27 14:56:50 +00:00
|
|
|
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"`
|
|
|
|
}
|
|
|
|
|
2022-02-21 11:49:13 +00:00
|
|
|
type TunnelWithToken struct {
|
|
|
|
Tunnel
|
|
|
|
Token string `json:"token"`
|
|
|
|
}
|
|
|
|
|
2021-12-27 14:56:50 +00:00
|
|
|
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"`
|
|
|
|
}
|
|
|
|
|
2023-04-12 16:43:38 +00:00
|
|
|
type managementRequest struct {
|
|
|
|
Resources []string `json:"resources"`
|
|
|
|
}
|
|
|
|
|
2021-12-27 14:56:50 +00:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
|
2022-02-21 11:49:13 +00:00
|
|
|
func (r *RESTClient) CreateTunnel(name string, tunnelSecret []byte) (*TunnelWithToken, error) {
|
2021-12-27 14:56:50 +00:00
|
|
|
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:
|
2022-02-21 11:49:13 +00:00
|
|
|
var tunnel TunnelWithToken
|
2024-01-05 07:57:30 +00:00
|
|
|
if serdeErr := parseResponse(resp.Body, &tunnel); serdeErr != nil {
|
2022-02-21 11:49:13 +00:00
|
|
|
return nil, serdeErr
|
|
|
|
}
|
|
|
|
return &tunnel, nil
|
2021-12-27 14:56:50 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2022-03-22 12:46:07 +00:00
|
|
|
func (r *RESTClient) GetTunnelToken(tunnelID uuid.UUID) (token string, err error) {
|
|
|
|
endpoint := r.baseEndpoints.accountLevel
|
|
|
|
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/token", tunnelID))
|
|
|
|
resp, err := r.sendRequest("GET", endpoint, nil)
|
|
|
|
if err != nil {
|
|
|
|
return "", errors.Wrap(err, "REST request failed")
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
if resp.StatusCode == http.StatusOK {
|
|
|
|
err = parseResponse(resp.Body, &token)
|
|
|
|
return token, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return "", r.statusCodeToError("get tunnel token", resp)
|
|
|
|
}
|
|
|
|
|
2023-04-12 16:43:38 +00:00
|
|
|
func (r *RESTClient) GetManagementToken(tunnelID uuid.UUID) (token string, err error) {
|
|
|
|
endpoint := r.baseEndpoints.accountLevel
|
|
|
|
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/management", tunnelID))
|
|
|
|
|
|
|
|
body := &managementRequest{
|
|
|
|
Resources: []string{"logs"},
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := r.sendRequest("POST", endpoint, body)
|
|
|
|
if err != nil {
|
|
|
|
return "", errors.Wrap(err, "REST request failed")
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
if resp.StatusCode == http.StatusOK {
|
|
|
|
err = parseResponse(resp.Body, &token)
|
|
|
|
return token, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return "", r.statusCodeToError("get tunnel token", resp)
|
|
|
|
}
|
|
|
|
|
2023-09-20 10:10:50 +00:00
|
|
|
func (r *RESTClient) DeleteTunnel(tunnelID uuid.UUID, cascade bool) error {
|
2021-12-27 14:56:50 +00:00
|
|
|
endpoint := r.baseEndpoints.accountLevel
|
|
|
|
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", tunnelID))
|
2023-09-20 10:10:50 +00:00
|
|
|
// Cascade will delete all tunnel dependencies (connections, routes, etc.) that
|
|
|
|
// are linked to the deleted tunnel.
|
|
|
|
if cascade {
|
|
|
|
endpoint.RawQuery = "cascade=true"
|
|
|
|
}
|
2021-12-27 14:56:50 +00:00
|
|
|
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
|
|
|
|
}
|