78 lines
2.3 KiB
Go
78 lines
2.3 KiB
Go
|
|
// Package tlog is the attest-reconciler's HTTP client for the Transparency Log
|
||
|
|
// (ADR-0005). Mirrors admin-api/internal/tlog.HTTPSubmitter byte-for-byte so
|
||
|
|
// resubmits from the reconciler collide with the admin-api's original log
|
||
|
|
// entry — the server's idempotency key covers both paths.
|
||
|
|
package tlog
|
||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"context"
|
||
|
|
"encoding/json"
|
||
|
|
"errors"
|
||
|
|
"fmt"
|
||
|
|
"io"
|
||
|
|
"net/http"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"basicstack.de/proofedmesh/services/attest-reconciler/internal/attest"
|
||
|
|
)
|
||
|
|
|
||
|
|
type HTTPSubmitter struct {
|
||
|
|
BaseURL string
|
||
|
|
Client *http.Client
|
||
|
|
TokenSource attest.TokenSource
|
||
|
|
}
|
||
|
|
|
||
|
|
type submitRequest struct {
|
||
|
|
Envelope attest.Envelope `json:"envelope"`
|
||
|
|
ContentHash string `json:"content_hash"`
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s HTTPSubmitter) Submit(ctx context.Context, env attest.Envelope, contentHash string) (attest.LogReceipt, error) {
|
||
|
|
if s.BaseURL == "" {
|
||
|
|
return attest.LogReceipt{}, errors.New("tlog.HTTPSubmitter: BaseURL is empty")
|
||
|
|
}
|
||
|
|
cli := s.Client
|
||
|
|
if cli == nil {
|
||
|
|
cli = &http.Client{Timeout: 10 * time.Second}
|
||
|
|
}
|
||
|
|
body, err := json.Marshal(submitRequest{Envelope: env, ContentHash: contentHash})
|
||
|
|
if err != nil {
|
||
|
|
return attest.LogReceipt{}, fmt.Errorf("marshal: %w", err)
|
||
|
|
}
|
||
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.BaseURL+"/api/v1/log/entries", bytes.NewReader(body))
|
||
|
|
if err != nil {
|
||
|
|
return attest.LogReceipt{}, err
|
||
|
|
}
|
||
|
|
req.Header.Set("Content-Type", "application/json")
|
||
|
|
if s.TokenSource != nil {
|
||
|
|
tok, err := s.TokenSource.Token(ctx)
|
||
|
|
if err != nil {
|
||
|
|
return attest.LogReceipt{}, fmt.Errorf("token: %w", err)
|
||
|
|
}
|
||
|
|
req.Header.Set("Authorization", "Bearer "+tok)
|
||
|
|
}
|
||
|
|
resp, err := cli.Do(req)
|
||
|
|
if err != nil {
|
||
|
|
return attest.LogReceipt{}, err
|
||
|
|
}
|
||
|
|
defer resp.Body.Close()
|
||
|
|
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||
|
|
switch resp.StatusCode {
|
||
|
|
case http.StatusOK, http.StatusCreated:
|
||
|
|
default:
|
||
|
|
return attest.LogReceipt{}, fmt.Errorf("tlog: status %d: %s", resp.StatusCode, string(raw))
|
||
|
|
}
|
||
|
|
var out attest.LogReceipt
|
||
|
|
if err := json.Unmarshal(raw, &out); err != nil {
|
||
|
|
return attest.LogReceipt{}, fmt.Errorf("decode: %w", err)
|
||
|
|
}
|
||
|
|
if out.ContentHash != contentHash {
|
||
|
|
return out, fmt.Errorf("tlog response content_hash %q != sent %q", out.ContentHash, contentHash)
|
||
|
|
}
|
||
|
|
if out.LogIndex < 0 {
|
||
|
|
return out, fmt.Errorf("tlog response has negative log_index %d", out.LogIndex)
|
||
|
|
}
|
||
|
|
return out, nil
|
||
|
|
}
|