shark expose: put a local port on the internet through Hammerhead
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
61143d107c
15 files changed
+1538
No files matched your search
@@ -0,0 +1,157 @@
|
||||
// Package api is the small slice of Hammerhead's HTTP API that `shark expose` uses.
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Client talks to Hammerhead with an API key (the "user token" from Access in the dashboard).
|
||||
type Client struct {
|
||||
Base string
|
||||
Token string
|
||||
HTTP *http.Client
|
||||
}
|
||||
|
||||
func New(base, token string) *Client {
|
||||
return &Client{Base: strings.TrimRight(base, "/"), Token: token, HTTP: &http.Client{Timeout: 20 * time.Second}}
|
||||
}
|
||||
|
||||
// Error is an error Hammerhead answered with.
|
||||
type Error struct {
|
||||
Status int
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *Error) Error() string { return e.Message }
|
||||
|
||||
// IsStatus reports whether err is a Hammerhead error with this HTTP status.
|
||||
func IsStatus(err error, status int) bool {
|
||||
var e *Error
|
||||
return errors.As(err, &e) && e.Status == status
|
||||
}
|
||||
|
||||
// Request is what to expose. Zero values mean "let Hammerhead choose".
|
||||
type Request struct {
|
||||
Proto string `json:"proto"` // http, https or tcp
|
||||
Name string `json:"name,omitempty"`
|
||||
TargetHost string `json:"targetHost,omitempty"`
|
||||
TargetPort int `json:"targetPort"`
|
||||
TTLSeconds int `json:"ttlSeconds,omitempty"`
|
||||
Port int `json:"port,omitempty"`
|
||||
Hostname string `json:"hostname,omitempty"`
|
||||
EdgeID string `json:"edgeId,omitempty"`
|
||||
Auth *Auth `json:"auth,omitempty"`
|
||||
}
|
||||
|
||||
// Auth is the login in front of a website.
|
||||
type Auth struct {
|
||||
Password string `json:"password,omitempty"`
|
||||
Pin string `json:"pin,omitempty"`
|
||||
SSO bool `json:"sso,omitempty"`
|
||||
}
|
||||
|
||||
// Exposure is an ephemeral site and port as Hammerhead describes it.
|
||||
type Exposure struct {
|
||||
ID string `json:"id"`
|
||||
Proto string `json:"proto"`
|
||||
URL string `json:"url"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Hostname string `json:"hostname"`
|
||||
SiteToken string `json:"siteToken"`
|
||||
Hammerhead string `json:"hammerhead"`
|
||||
Protected bool `json:"protected"`
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
Live bool `json:"live"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// Address is what visitors use: the URL for websites, host:port for tcp.
|
||||
func (e Exposure) Address() string {
|
||||
if e.URL != "" {
|
||||
return e.URL
|
||||
}
|
||||
return fmt.Sprintf("tcp://%s:%d", e.Host, e.Port)
|
||||
}
|
||||
|
||||
func (c *Client) do(ctx context.Context, method, path string, in, out any) error {
|
||||
var body io.Reader
|
||||
if in != nil {
|
||||
b, err := json.Marshal(in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body = bytes.NewReader(b)
|
||||
}
|
||||
u, err := url.JoinPath(c.Base, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, method, u, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+c.Token)
|
||||
if in != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
resp, err := c.HTTP.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
var e struct{ Error string }
|
||||
json.Unmarshal(raw, &e)
|
||||
if e.Error == "" {
|
||||
e.Error = fmt.Sprintf("%s %s: %s", method, path, resp.Status)
|
||||
}
|
||||
if resp.StatusCode == http.StatusUnauthorized {
|
||||
e.Error = "Hammerhead did not accept the token (" + e.Error + ")"
|
||||
}
|
||||
return &Error{Status: resp.StatusCode, Message: e.Error}
|
||||
}
|
||||
if out != nil {
|
||||
return json.Unmarshal(raw, out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Create(ctx context.Context, r Request) (Exposure, error) {
|
||||
var e Exposure
|
||||
err := c.do(ctx, "POST", "/api/orgs/-/expose", r, &e)
|
||||
return e, err
|
||||
}
|
||||
|
||||
// Renew pushes the deadline out by ttl (0 = the server's default).
|
||||
func (c *Client) Renew(ctx context.Context, id string, ttl time.Duration) (Exposure, error) {
|
||||
var e Exposure
|
||||
err := c.do(ctx, "POST", "/api/orgs/-/expose/"+id+"/renew", map[string]int{"ttlSeconds": int(ttl.Seconds())}, &e)
|
||||
return e, err
|
||||
}
|
||||
|
||||
func (c *Client) Get(ctx context.Context, id string) (Exposure, error) {
|
||||
var e Exposure
|
||||
err := c.do(ctx, "GET", "/api/orgs/-/expose/"+id, nil, &e)
|
||||
return e, err
|
||||
}
|
||||
|
||||
func (c *Client) List(ctx context.Context) ([]Exposure, error) {
|
||||
var e []Exposure
|
||||
err := c.do(ctx, "GET", "/api/orgs/-/expose", nil, &e)
|
||||
return e, err
|
||||
}
|
||||
|
||||
func (c *Client) Delete(ctx context.Context, id string) error {
|
||||
return c.do(ctx, "DELETE", "/api/orgs/-/expose/"+id, nil, nil)
|
||||
}
|
||||
Reference in new issue
Block a user