Files
go-sip/internal/configread/client.go
T

108 lines
3.3 KiB
Go

package configread
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"mime"
"net/http"
"net/url"
"strings"
)
const dispatcherIDHeader = "X-DISPATCHER-id"
const dispatcherSecretHeader = "X-DISPATCHER-SECRET-KEY"
const configReadPath = "/internal/v1/dispatcher"
type Client struct {
baseURL string
dispatcherID string
secret string
httpClient *http.Client
}
type HTTPError struct {
StatusCode int
Code string
}
func (e *HTTPError) Error() string {
if e.Code == "" {
return fmt.Sprintf("configuration service returned HTTP %d", e.StatusCode)
}
return fmt.Sprintf("configuration service returned HTTP %d (%s)", e.StatusCode, e.Code)
}
func NewClient(baseURL, dispatcherID, secret string, httpClient *http.Client) (*Client, error) {
parsed, err := url.ParseRequestURI(baseURL)
if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" {
return nil, errors.New("configuration service URL must be an absolute HTTP(S) URL without credentials, query, or fragment")
}
if strings.TrimSpace(dispatcherID) == "" || strings.TrimSpace(secret) == "" {
return nil, errors.New("dispatcher ID and secret are required")
}
if strings.ContainsAny(dispatcherID+secret, "\r\n") {
return nil, errors.New("dispatcher credentials contain invalid header characters")
}
if httpClient == nil {
httpClient = http.DefaultClient
}
return &Client{baseURL: strings.TrimRight(baseURL, "/"), dispatcherID: dispatcherID, secret: secret, httpClient: httpClient}, nil
}
func (c *Client) DispatcherID() string { return c.dispatcherID }
func (c *Client) get(ctx context.Context, path string) (json.RawMessage, error) {
relative, err := url.Parse(path)
if err != nil {
return nil, fmt.Errorf("parse configuration path: %w", err)
}
base, err := url.Parse(c.baseURL)
if err != nil {
return nil, fmt.Errorf("parse configuration base URL: %w", err)
}
base = base.JoinPath(relative.Path)
base.RawQuery = relative.RawQuery
request, err := http.NewRequestWithContext(ctx, http.MethodGet, base.String(), nil)
if err != nil {
return nil, fmt.Errorf("build configuration request: %w", err)
}
request.Header.Set("Accept", "application/json")
request.Header.Set(dispatcherIDHeader, c.dispatcherID)
request.Header.Set(dispatcherSecretHeader, c.secret)
response, err := c.httpClient.Do(request)
if err != nil {
return nil, fmt.Errorf("request configuration service: %w", err)
}
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("read configuration response: %w", err)
}
if response.StatusCode != http.StatusOK {
return nil, &HTTPError{StatusCode: response.StatusCode, Code: responseErrorCode(body)}
}
mediaType, _, err := mime.ParseMediaType(response.Header.Get("Content-Type"))
if err != nil || mediaType != "application/json" {
return nil, errors.New("configuration service response must use application/json")
}
if !json.Valid(body) {
return nil, errors.New("configuration service returned invalid JSON")
}
return json.RawMessage(body), nil
}
func responseErrorCode(body []byte) string {
var response struct {
Error struct {
Code string `json:"code"`
} `json:"error"`
}
if err := json.Unmarshal(body, &response); err != nil {
return ""
}
return response.Error.Code
}