package client import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "strings" "time" ) // UnauthenticatedClient is a minimal HTTP client for public Dragonchain services // that require no HMAC credentials (e.g. the proof-measure service). It mirrors // Client's body marshaling, status handling, and response decoding, but sends no // Authorization/Dragonchain/Timestamp headers. type UnauthenticatedClient struct { baseURL string httpClient *http.Client } // NewUnauthenticatedClient builds an UnauthenticatedClient with the default // HTTP client (30s timeout). func NewUnauthenticatedClient(baseURL string) *UnauthenticatedClient { return NewUnauthenticatedClientWithHTTPClient(baseURL, nil) } // NewUnauthenticatedClientWithHTTPClient is like NewUnauthenticatedClient but // routes requests through the caller-supplied *http.Client (e.g. an SSRF-guarded // transport). A nil hc falls back to the package default. func NewUnauthenticatedClientWithHTTPClient(baseURL string, hc *http.Client) *UnauthenticatedClient { if hc == nil { hc = &http.Client{Timeout: 30 * time.Second} } return &UnauthenticatedClient{ baseURL: strings.TrimSuffix(baseURL, "/"), httpClient: hc, } } // Endpoint returns the configured base URL. func (c *UnauthenticatedClient) Endpoint() string { return c.baseURL } func (c *UnauthenticatedClient) doRequest(ctx context.Context, method, path, contentType string, body any, response any) error { var bodyBytes []byte var err error if body != nil { if b, ok := body.([]byte); ok { bodyBytes = b } else { bodyBytes, err = json.Marshal(body) if err != nil { return fmt.Errorf("failed to marshal request body: %w", err) } contentType = "application/json" } } fullURL := c.baseURL + path req, err := http.NewRequestWithContext(ctx, method, fullURL, bytes.NewBuffer(bodyBytes)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } if contentType != "" { req.Header.Set("Content-Type", contentType) } req.Header.Set("Accept", "application/json") resp, err := c.httpClient.Do(req) if err != nil { return fmt.Errorf("failed to execute request: %w", err) } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("failed to read response body: %w", err) } if resp.StatusCode >= 400 { return fmt.Errorf("API error (status %d): %s", resp.StatusCode, strings.TrimSpace(string(respBody))) } if response != nil && len(respBody) > 0 { if err := json.Unmarshal(respBody, response); err != nil { return fmt.Errorf("failed to unmarshal response: %w", err) } } return nil } // Get performs an unauthenticated GET and decodes the JSON response. func (c *UnauthenticatedClient) Get(ctx context.Context, path string, response any) error { return c.doRequest(ctx, http.MethodGet, path, "", nil, response) } // Post performs an unauthenticated POST and decodes the JSON response. func (c *UnauthenticatedClient) Post(ctx context.Context, path, contentType string, body any, response any) error { return c.doRequest(ctx, http.MethodPost, path, contentType, body, response) }