199 lines
4.4 KiB
Go
199 lines
4.4 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type requestOptions struct {
|
|
BaseURL string
|
|
Path string
|
|
Body []byte
|
|
Token string
|
|
NoAuth bool
|
|
Timeout time.Duration
|
|
}
|
|
|
|
type responseData struct {
|
|
StatusCode int
|
|
Body []byte
|
|
}
|
|
|
|
func normalizeBaseURL(raw string) (string, error) {
|
|
if raw == "" {
|
|
raw = defaultBaseURL
|
|
}
|
|
|
|
parsed, err := url.Parse(raw)
|
|
if err != nil {
|
|
return "", fmt.Errorf("parse base url: %w", err)
|
|
}
|
|
if parsed.Scheme != "http" && parsed.Scheme != "https" {
|
|
return "", fmt.Errorf("base url must start with http:// or https://")
|
|
}
|
|
if parsed.Host == "" {
|
|
return "", fmt.Errorf("base url must include a host")
|
|
}
|
|
|
|
return strings.TrimRight(parsed.String(), "/"), nil
|
|
}
|
|
|
|
func endpointURL(baseURL, path string) (string, error) {
|
|
base, err := normalizeBaseURL(baseURL)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if path == "" {
|
|
return "", fmt.Errorf("endpoint path is required")
|
|
}
|
|
if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") {
|
|
return path, nil
|
|
}
|
|
if !strings.HasPrefix(path, "/") {
|
|
path = "/" + path
|
|
}
|
|
return base + path, nil
|
|
}
|
|
|
|
func postJSON(ctx context.Context, opts requestOptions) (responseData, error) {
|
|
target, err := endpointURL(opts.BaseURL, opts.Path)
|
|
if err != nil {
|
|
return responseData{}, err
|
|
}
|
|
if len(opts.Body) == 0 {
|
|
opts.Body = []byte("{}")
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, target, bytes.NewReader(opts.Body))
|
|
if err != nil {
|
|
return responseData{}, fmt.Errorf("create request: %w", err)
|
|
}
|
|
req.Header.Set("Accept", "application/json")
|
|
req.Header.Set("Content-Type", "application/json")
|
|
if !opts.NoAuth && opts.Token != "" {
|
|
req.Header.Set("Authorization", "Bearer "+opts.Token)
|
|
}
|
|
|
|
timeout := opts.Timeout
|
|
if timeout <= 0 {
|
|
timeout = 30 * time.Second
|
|
}
|
|
client := &http.Client{Timeout: timeout}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return responseData{}, fmt.Errorf("http post %s: %w", target, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return responseData{}, fmt.Errorf("read response: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return responseData{StatusCode: resp.StatusCode, Body: body}, httpStatusError{StatusCode: resp.StatusCode, Body: body}
|
|
}
|
|
|
|
return responseData{StatusCode: resp.StatusCode, Body: body}, nil
|
|
}
|
|
|
|
type httpStatusError struct {
|
|
StatusCode int
|
|
Body []byte
|
|
}
|
|
|
|
func (e httpStatusError) Error() string {
|
|
body := strings.TrimSpace(string(e.Body))
|
|
if body == "" {
|
|
return fmt.Sprintf("request failed with status %d", e.StatusCode)
|
|
}
|
|
return fmt.Sprintf("request failed with status %d: %s", e.StatusCode, body)
|
|
}
|
|
|
|
func readJSONData(value string) ([]byte, error) {
|
|
if value == "" {
|
|
return []byte("{}"), nil
|
|
}
|
|
|
|
var data []byte
|
|
var err error
|
|
switch {
|
|
case value == "-":
|
|
data, err = io.ReadAll(os.Stdin)
|
|
case strings.HasPrefix(value, "@"):
|
|
path := strings.TrimPrefix(value, "@")
|
|
if path == "" {
|
|
return nil, fmt.Errorf("missing file after @")
|
|
}
|
|
cleanPath, err := filepath.Abs(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("resolve json file: %w", err)
|
|
}
|
|
data, err = os.ReadFile(cleanPath)
|
|
default:
|
|
data = []byte(value)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read json body: %w", err)
|
|
}
|
|
|
|
data = bytes.TrimSpace(data)
|
|
if len(data) == 0 {
|
|
data = []byte("{}")
|
|
}
|
|
if !json.Valid(data) {
|
|
return nil, fmt.Errorf("request body must be valid JSON")
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
|
|
func writeJSONResponse(w io.Writer, body []byte, raw bool) error {
|
|
body = bytes.TrimSpace(body)
|
|
if len(body) == 0 {
|
|
_, err := fmt.Fprintln(w, "{}")
|
|
return err
|
|
}
|
|
if raw {
|
|
_, err := w.Write(append(body, '\n'))
|
|
return err
|
|
}
|
|
|
|
var out bytes.Buffer
|
|
if err := json.Indent(&out, body, "", " "); err != nil {
|
|
_, writeErr := w.Write(append(body, '\n'))
|
|
return writeErr
|
|
}
|
|
out.WriteByte('\n')
|
|
_, err := w.Write(out.Bytes())
|
|
return err
|
|
}
|
|
|
|
func tokenFromResponse(body []byte) (string, error) {
|
|
var decoded map[string]any
|
|
if err := json.Unmarshal(body, &decoded); err != nil {
|
|
return "", fmt.Errorf("login response is not JSON: %w", err)
|
|
}
|
|
if token, ok := decoded["token"].(string); ok && token != "" {
|
|
return token, nil
|
|
}
|
|
|
|
if data, ok := decoded["data"].(map[string]any); ok {
|
|
if token, ok := data["token"].(string); ok && token != "" {
|
|
return token, nil
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("login response does not contain token")
|
|
}
|