Files
sherlock/cmd/gssh-mcp/client.go
T
amacocian 22ca6b3dc7
CI / build (push) Failing after 0s
Setup gssh-mcp
2026-05-30 14:11:01 +02:00

94 lines
2.7 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).
type gsshAPI struct {
baseURL string
token 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
}