154 lines
4.5 KiB
Go
154 lines
4.5 KiB
Go
// Package client is the REST client used by every CLI subcommand (and the
|
|
// attach client for its HTTP-side needs).
|
|
package client
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"termd/internal/api"
|
|
)
|
|
|
|
// Client talks to a termd daemon.
|
|
type Client struct {
|
|
base string // http://host:port, no trailing slash
|
|
token string
|
|
hc *http.Client
|
|
}
|
|
|
|
// New builds a client for addr, which is "host:port", a full http(s) URL, or
|
|
// "unix:///path/to.sock".
|
|
func New(addr, token string) *Client {
|
|
hc := &http.Client{}
|
|
base := addr
|
|
if path, ok := strings.CutPrefix(addr, "unix://"); ok {
|
|
hc.Transport = &http.Transport{
|
|
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
|
var d net.Dialer
|
|
return d.DialContext(ctx, "unix", path)
|
|
},
|
|
}
|
|
base = "http://termd" // dummy host; the dialer ignores it
|
|
} else if !strings.HasPrefix(addr, "http://") && !strings.HasPrefix(addr, "https://") {
|
|
base = "http://" + addr
|
|
}
|
|
return &Client{base: strings.TrimRight(base, "/"), token: token, hc: hc}
|
|
}
|
|
|
|
// BaseURL returns the HTTP base URL (ws dials derive from it).
|
|
func (c *Client) BaseURL() string { return c.base }
|
|
|
|
// Token returns the bearer token, empty if none.
|
|
func (c *Client) Token() string { return c.token }
|
|
|
|
// HTTPClient returns the underlying HTTP client (carries the unix-socket
|
|
// transport when one is configured).
|
|
func (c *Client) HTTPClient() *http.Client { return c.hc }
|
|
|
|
func (c *Client) do(ctx context.Context, method, path string, body, out any) error {
|
|
var rd io.Reader
|
|
if body != nil {
|
|
b, err := json.Marshal(body)
|
|
if err != nil {
|
|
return fmt.Errorf("encoding request: %w", err)
|
|
}
|
|
rd = bytes.NewReader(b)
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, method, c.base+path, rd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if body != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
if c.token != "" {
|
|
req.Header.Set("Authorization", "Bearer "+c.token)
|
|
}
|
|
resp, err := c.hc.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode >= 400 {
|
|
var apiErr api.Error
|
|
data, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
|
if json.Unmarshal(data, &apiErr) == nil && apiErr.Error != "" {
|
|
msg := apiErr.Error
|
|
if apiErr.Modes != nil {
|
|
msg += fmt.Sprintf(" (modes: mouse=%s encoding=%s)", apiErr.Modes.Mouse, apiErr.Modes.MouseEncoding)
|
|
}
|
|
return fmt.Errorf("%s: %s", resp.Status, msg)
|
|
}
|
|
return fmt.Errorf("%s: %s", resp.Status, strings.TrimSpace(string(data)))
|
|
}
|
|
if out != nil {
|
|
if err := json.NewDecoder(resp.Body).Decode(out); err != nil {
|
|
return fmt.Errorf("decoding response: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) Create(ctx context.Context, req api.CreateSessionRequest) (api.SessionInfo, error) {
|
|
var info api.SessionInfo
|
|
err := c.do(ctx, http.MethodPost, "/sessions", req, &info)
|
|
return info, err
|
|
}
|
|
|
|
func (c *Client) List(ctx context.Context) (api.SessionList, error) {
|
|
var list api.SessionList
|
|
err := c.do(ctx, http.MethodGet, "/sessions", nil, &list)
|
|
return list, err
|
|
}
|
|
|
|
func (c *Client) Info(ctx context.Context, id string) (api.SessionInfo, error) {
|
|
var info api.SessionInfo
|
|
err := c.do(ctx, http.MethodGet, "/sessions/"+url.PathEscape(id), nil, &info)
|
|
return info, err
|
|
}
|
|
|
|
func (c *Client) Delete(ctx context.Context, id, signal string) error {
|
|
path := "/sessions/" + url.PathEscape(id)
|
|
if signal != "" {
|
|
path += "?signal=" + url.QueryEscape(signal)
|
|
}
|
|
return c.do(ctx, http.MethodDelete, path, nil, nil)
|
|
}
|
|
|
|
func (c *Client) Resize(ctx context.Context, id string, cols, rows int) error {
|
|
return c.do(ctx, http.MethodPost, "/sessions/"+url.PathEscape(id)+"/resize",
|
|
api.ResizeRequest{Cols: cols, Rows: rows}, nil)
|
|
}
|
|
|
|
func (c *Client) Input(ctx context.Context, id string, items []api.InputItem) (api.InputResponse, error) {
|
|
var resp api.InputResponse
|
|
err := c.do(ctx, http.MethodPost, "/sessions/"+url.PathEscape(id)+"/input",
|
|
api.InputRequest{Input: items}, &resp)
|
|
return resp, err
|
|
}
|
|
|
|
func (c *Client) Mouse(ctx context.Context, id string, req api.MouseRequest) error {
|
|
return c.do(ctx, http.MethodPost, "/sessions/"+url.PathEscape(id)+"/mouse", req, nil)
|
|
}
|
|
|
|
func (c *Client) Screen(ctx context.Context, id string, raw bool) (api.Screen, error) {
|
|
path := "/sessions/" + url.PathEscape(id) + "/screen"
|
|
if raw {
|
|
path += "?format=raw"
|
|
}
|
|
var scr api.Screen
|
|
err := c.do(ctx, http.MethodGet, path, nil, &scr)
|
|
return scr, err
|
|
}
|
|
|
|
// AttachURL returns the websocket URL for a session.
|
|
func (c *Client) AttachURL(id string) string {
|
|
return c.base + "/sessions/" + url.PathEscape(id) + "/attach"
|
|
}
|