98 lines
2.9 KiB
Go
98 lines
2.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
)
|
|
|
|
// ErrUnauthorized is returned when Gssh answers 401. The tool layer
|
|
// surfaces this so the operator knows to `sherlock logout gssh` and
|
|
// re-auth — a refresh attempt by sherlock is also valid but for now
|
|
// we just surface the error verbatim, matching gitea-mcp.
|
|
var ErrUnauthorized = errors.New("gssh: 401 unauthorized")
|
|
|
|
// gsshAPI is a thin wrapper around the small REST surface Gssh
|
|
// exposes alongside its WebSocket routes. The exec WebSocket itself
|
|
// goes through internal/wsdatagram (see tools_ssh.go).
|
|
//
|
|
// token is a getter, not a static string: the sherlock TokenSource
|
|
// renews the short-lived access token in the background and pushes each
|
|
// new value in, so every REST call and WS dial reads the current bearer.
|
|
type gsshAPI struct {
|
|
baseURL string
|
|
token func() string
|
|
client *http.Client
|
|
}
|
|
|
|
func (g *gsshAPI) get(ctx context.Context, path string, query url.Values, into any) error {
|
|
full := g.baseURL + path
|
|
if len(query) > 0 {
|
|
full += "?" + query.Encode()
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, full, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+g.token())
|
|
req.Header.Set("Accept", "application/json")
|
|
dbg("GET %s", full)
|
|
resp, err := g.client.Do(req)
|
|
if err != nil {
|
|
dbg("GET %s -> transport error: %v", full, err)
|
|
return err
|
|
}
|
|
defer func() { _, _ = io.Copy(io.Discard, resp.Body); _ = resp.Body.Close() }()
|
|
if err := checkStatus(resp, req); err != nil {
|
|
dbg("GET %s -> %v", full, err)
|
|
return err
|
|
}
|
|
dbg("GET %s -> %d", full, resp.StatusCode)
|
|
if into == nil {
|
|
return nil
|
|
}
|
|
return json.NewDecoder(resp.Body).Decode(into)
|
|
}
|
|
|
|
func (g *gsshAPI) post(ctx context.Context, path string, into any) error {
|
|
full := g.baseURL + path
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, full, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+g.token())
|
|
req.Header.Set("Accept", "application/json")
|
|
dbg("POST %s", full)
|
|
resp, err := g.client.Do(req)
|
|
if err != nil {
|
|
dbg("POST %s -> transport error: %v", full, err)
|
|
return err
|
|
}
|
|
defer func() { _, _ = io.Copy(io.Discard, resp.Body); _ = resp.Body.Close() }()
|
|
if err := checkStatus(resp, req); err != nil {
|
|
dbg("POST %s -> %v", full, err)
|
|
return err
|
|
}
|
|
dbg("POST %s -> %d", full, resp.StatusCode)
|
|
if into == nil {
|
|
return nil
|
|
}
|
|
return json.NewDecoder(resp.Body).Decode(into)
|
|
}
|
|
|
|
func checkStatus(resp *http.Response, req *http.Request) error {
|
|
if resp.StatusCode == http.StatusUnauthorized {
|
|
return fmt.Errorf("%w (token rejected by %s; try `sherlock logout gssh` and retry)", ErrUnauthorized, req.URL.Host)
|
|
}
|
|
if resp.StatusCode >= 400 {
|
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
|
return fmt.Errorf("gssh: %s %s: HTTP %d: %s", req.Method, req.URL.Path, resp.StatusCode, strings.TrimSpace(string(body)))
|
|
}
|
|
return nil
|
|
}
|