124 lines
3.7 KiB
Go
124 lines
3.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
)
|
|
|
|
// ErrNotFound is returned wrapped when Gitea answers 404. Lets tool
|
|
// handlers special-case "this resource just doesn't exist" (e.g. an
|
|
// uninitialised wiki) and return an empty result instead of bubbling
|
|
// up a confusing HTTP error.
|
|
var ErrNotFound = errors.New("gitea: not found")
|
|
|
|
// giteaAPI is a thin wrapper around the few Gitea REST endpoints we
|
|
// use. We don't pull in code.gitea.io/sdk/gitea because its client
|
|
// only knows how to send `Authorization: token <PAT>`, not the
|
|
// `Authorization: Bearer <OAuth>` that Gitea's OAuth2 server issues.
|
|
//
|
|
// token is a context-aware getter: it triggers sherlock's lazy OAuth
|
|
// flow on first use, then returns the current (background-renewed)
|
|
// access token. Threading ctx lets a first-call browser flow be
|
|
// cancelled with the request, and the error surfaces auth failures to
|
|
// the tool caller.
|
|
type giteaAPI struct {
|
|
baseURL string
|
|
token func(context.Context) (string, error)
|
|
client *http.Client
|
|
}
|
|
|
|
// get does a GET against path (with optional query) and JSON-decodes
|
|
// into into.
|
|
func (g *giteaAPI) 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
|
|
}
|
|
tok, err := g.token(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+tok)
|
|
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)
|
|
}
|
|
|
|
// getRaw returns the raw body (up to maxBytes; anything beyond is
|
|
// silently dropped). Useful for endpoints that return plain text or
|
|
// binary (e.g. job logs).
|
|
func (g *giteaAPI) getRaw(ctx context.Context, path string, query url.Values, maxBytes int64) ([]byte, bool, 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 nil, false, err
|
|
}
|
|
tok, err := g.token(ctx)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+tok)
|
|
req.Header.Set("Accept", "*/*")
|
|
resp, err := g.client.Do(req)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
if err := checkStatus(resp, req); err != nil {
|
|
return nil, false, err
|
|
}
|
|
body, err := io.ReadAll(io.LimitReader(resp.Body, maxBytes+1))
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
truncated := false
|
|
if int64(len(body)) > maxBytes {
|
|
body = body[:maxBytes]
|
|
truncated = true
|
|
}
|
|
return body, truncated, nil
|
|
}
|
|
|
|
func checkStatus(resp *http.Response, req *http.Request) error {
|
|
if resp.StatusCode == http.StatusUnauthorized {
|
|
return fmt.Errorf("gitea: 401 (token rejected by %s; try `sherlock logout gitea` and retry)", req.URL.Host)
|
|
}
|
|
if resp.StatusCode == http.StatusNotFound {
|
|
// Wrap so callers can errors.Is(err, ErrNotFound).
|
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
|
return fmt.Errorf("%w: %s %s: %s", ErrNotFound, req.Method, req.URL.Path, strings.TrimSpace(string(body)))
|
|
}
|
|
if resp.StatusCode >= 400 {
|
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
|
return fmt.Errorf("gitea: %s %s: HTTP %d: %s", req.Method, req.URL.Path, resp.StatusCode, strings.TrimSpace(string(body)))
|
|
}
|
|
return nil
|
|
}
|