123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238 |
- 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 TunnelWithToken struct {
- Tunnel
- Token string `json:"token"`
- }
- 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 managementRequest struct {
- Resources []string `json:"resources"`
- }
- 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) (*TunnelWithToken, 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:
- var tunnel TunnelWithToken
- if serdeErr := parseResponse(resp.Body, &tunnel); serdeErr != nil {
- return nil, serdeErr
- }
- return &tunnel, nil
- 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) 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)
- }
- 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)
- }
- func (r *RESTClient) DeleteTunnel(tunnelID uuid.UUID, cascade bool) error {
- endpoint := r.baseEndpoints.accountLevel
- endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", tunnelID))
- // Cascade will delete all tunnel dependencies (connections, routes, etc.) that
- // are linked to the deleted tunnel.
- if cascade {
- endpoint.RawQuery = "cascade=true"
- }
- 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) {
- fetchFn := func(page int) (*http.Response, error) {
- endpoint := r.baseEndpoints.accountLevel
- filter.Page(page)
- endpoint.RawQuery = filter.encode()
- rsp, err := r.sendRequest("GET", endpoint, nil)
- if err != nil {
- return nil, errors.Wrap(err, "REST request failed")
- }
- if rsp.StatusCode != http.StatusOK {
- rsp.Body.Close()
- return nil, r.statusCodeToError("list tunnels", rsp)
- }
- return rsp, nil
- }
- return fetchExhaustively[Tunnel](fetchFn)
- }
- 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
- }
|