Skip to content

Commit ed994ff

Browse files
13rac1claude
andcommitted
Extract internal/httpclient package from config and tlsct
Move HTTP client utilities (WrapLogging, WrapCounting, RetryTransport, NewHTTPClient, NewHTTPClientWithTransport, NewAttestationClient) into a dedicated httpclient package. The config package now contains only configuration constants and loading logic — no HTTP client construction. The tlsct package retains only Certificate Transparency verification. Add httpclient.Do() which wraps client.Do() and annotates errors with the configured Client.Timeout value, ensuring timeout errors always show the configured limit even when http.Client.Timeout fires above the transport chain. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent cbfd9f3 commit ed994ff

31 files changed

Lines changed: 682 additions & 835 deletions

internal/attestation/nvidia.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import (
1616

1717
"golang.org/x/sync/singleflight"
1818

19-
"github.com/13rac1/teep/internal/tlsct"
19+
"github.com/13rac1/teep/internal/httpclient"
2020
"github.com/MicahParks/keyfunc/v3"
2121
"github.com/golang-jwt/jwt/v5"
2222
)
@@ -150,7 +150,7 @@ func (v *NVIDIAVerifier) Shutdown() {
150150
// Reference Integrity Manifest values.
151151
func (v *NVIDIAVerifier) VerifyNRAS(ctx context.Context, eatPayload string, client *http.Client, opts ...jwt.ParserOption) *NvidiaVerifyResult {
152152
if client == nil {
153-
client = tlsct.NewHTTPClient(30 * time.Second)
153+
client = httpclient.NewHTTPClient(30 * time.Second)
154154
}
155155

156156
req, err := http.NewRequestWithContext(ctx, http.MethodPost, v.nrasURL, strings.NewReader(eatPayload))
@@ -163,7 +163,7 @@ func (v *NVIDIAVerifier) VerifyNRAS(ctx context.Context, eatPayload string, clie
163163
req.Header.Set("Content-Type", "application/json")
164164
req.Header.Set("Accept", "application/json")
165165

166-
resp, err := client.Do(req)
166+
resp, err := httpclient.Do(client, req)
167167
if err != nil {
168168
return &NvidiaVerifyResult{
169169
Format: "JWT",
@@ -260,13 +260,13 @@ func (v *NVIDIAVerifier) fetchAndCacheJWKS(ctx context.Context, jwksURL string,
260260
v.mu.RUnlock()
261261

262262
if client == nil {
263-
client = tlsct.NewHTTPClient(30 * time.Second)
263+
client = httpclient.NewHTTPClient(30 * time.Second)
264264
}
265265
req, err := http.NewRequestWithContext(ctx, http.MethodGet, jwksURL, http.NoBody)
266266
if err != nil {
267267
return nil, fmt.Errorf("build JWKS request for %s: %w", jwksURL, err)
268268
}
269-
resp, err := client.Do(req)
269+
resp, err := httpclient.Do(client, req)
270270
if err != nil {
271271
return nil, fmt.Errorf("fetch JWKS from %s: %w", jwksURL, err)
272272
}

internal/attestation/poc.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"strings"
1818
"time"
1919

20+
"github.com/13rac1/teep/internal/httpclient"
2021
"github.com/13rac1/teep/internal/jsonstrict"
2122
)
2223

@@ -400,7 +401,7 @@ func (c *PoCClient) postJSON(ctx context.Context, baseURL, path string, payload
400401
}
401402
req.Header.Set("Content-Type", "application/json")
402403

403-
resp, err := c.client.Do(req)
404+
resp, err := httpclient.Do(c.client, req)
404405
if err != nil {
405406
return nil, err
406407
}

internal/attestation/rekor.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"sync"
2020
"unicode/utf8"
2121

22+
"github.com/13rac1/teep/internal/httpclient"
2223
"github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer"
2324
"github.com/transparency-dev/merkle/proof"
2425
"github.com/transparency-dev/merkle/rfc6962"
@@ -289,7 +290,7 @@ func (rc *RekorClient) fetchRekorUUIDs(ctx context.Context, digest string) ([]st
289290
}
290291
req.Header.Set("Content-Type", "application/json")
291292

292-
resp, err := rc.httpClient.Do(req)
293+
resp, err := httpclient.Do(rc.httpClient, req)
293294
if err != nil {
294295
return nil, err
295296
}
@@ -324,7 +325,7 @@ func (rc *RekorClient) fetchRekorEntry(ctx context.Context, uuid string) (*rekor
324325
}
325326
req.Header.Set("Content-Type", "application/json")
326327

327-
resp, err := rc.httpClient.Do(req)
328+
resp, err := httpclient.Do(rc.httpClient, req)
328329
if err != nil {
329330
return nil, err
330331
}

internal/attestation/tdx.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"net/http"
1414
"time"
1515

16+
"github.com/13rac1/teep/internal/httpclient"
1617
tdxabi "github.com/google/go-tdx-guest/abi"
1718
"github.com/google/go-tdx-guest/pcs"
1819
pb "github.com/google/go-tdx-guest/proto/tdx"
@@ -36,7 +37,7 @@ func (g *clientHTTPSGetter) GetContext(ctx context.Context, url string) (header
3637
if err != nil {
3738
return nil, nil, err
3839
}
39-
resp, err := g.client.Do(req)
40+
resp, err := httpclient.Do(g.client, req)
4041
if err != nil {
4142
return nil, nil, err
4243
}

internal/config/config.go

Lines changed: 0 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import (
1515
"fmt"
1616
"log/slog"
1717
"net"
18-
"net/http"
1918
"os"
2019
"strconv"
2120
"strings"
@@ -24,7 +23,6 @@ import (
2423
"github.com/BurntSushi/toml"
2524

2625
"github.com/13rac1/teep/internal/attestation"
27-
"github.com/13rac1/teep/internal/tlsct"
2826
)
2927

3028
const (
@@ -670,87 +668,3 @@ func RedactKey(key string) string {
670668
return key[:4] + "****"
671669
}
672670

673-
// RetryTransport retries requests on 5xx responses and network errors. For
674-
// requests with a body it requires req.GetBody to be set so the body can be
675-
// reset between attempts (http.NewRequestWithContext sets GetBody automatically
676-
// when passed a *bytes.Reader). If a retry is needed and GetBody is nil for a
677-
// request with a body, the last error is returned immediately rather than
678-
// sending an empty body. GET requests have no body and are always retried
679-
// unconditionally.
680-
//
681-
// Base must be non-nil.
682-
//
683-
// All attestation endpoints retried by this transport are effectively
684-
// idempotent reads. Do not use for POST endpoints with side effects (nonce
685-
// consumption, billing) without explicit consideration.
686-
type RetryTransport struct {
687-
Base http.RoundTripper
688-
MaxAttempts int // 0 → default 3
689-
MaxDelay time.Duration // 0 → default 4s
690-
}
691-
692-
// RoundTrip executes the request, retrying on 5xx and network errors.
693-
func (t *RetryTransport) RoundTrip(req *http.Request) (*http.Response, error) {
694-
maxAttempts := t.MaxAttempts
695-
if maxAttempts <= 0 {
696-
maxAttempts = 3
697-
}
698-
maxDelay := t.MaxDelay
699-
if maxDelay <= 0 {
700-
maxDelay = 4 * time.Second
701-
}
702-
hasBody := req.Body != nil && req.Body != http.NoBody
703-
var lastErr error
704-
for attempt := range maxAttempts {
705-
if attempt > 0 {
706-
if hasBody && req.GetBody == nil {
707-
// Body was consumed on the first attempt and cannot be reset.
708-
return nil, lastErr
709-
}
710-
exp := min(attempt-1, 30) // cap to avoid int64 overflow; 2^30s >> any realistic maxDelay
711-
timer := time.NewTimer(min(time.Duration(1<<exp)*time.Second, maxDelay))
712-
select {
713-
case <-req.Context().Done():
714-
timer.Stop()
715-
return nil, req.Context().Err()
716-
case <-timer.C:
717-
}
718-
slog.WarnContext(req.Context(), "retrying after error",
719-
"host", req.URL.Host, "path", req.URL.Path, "attempt", attempt+1, "err", lastErr)
720-
if req.GetBody != nil {
721-
body, err := req.GetBody()
722-
if err != nil {
723-
return nil, err
724-
}
725-
req.Body = body
726-
}
727-
}
728-
resp, err := t.Base.RoundTrip(req)
729-
if err != nil {
730-
lastErr = err
731-
continue
732-
}
733-
if resp.StatusCode >= 500 {
734-
resp.Body.Close()
735-
lastErr = fmt.Errorf("HTTP %d from %s", resp.StatusCode, req.URL.Host)
736-
continue
737-
}
738-
return resp, nil
739-
}
740-
return nil, lastErr
741-
}
742-
743-
// NewAttestationClient returns an *http.Client with a 30-second timeout and
744-
// tuned transport, suitable for fetching attestation data from TEE provider
745-
// endpoints. The default MaxIdleConnsPerHost (2) is too low for providers
746-
// that serve multiple models from the same host. In offline mode, CT checks
747-
// are disabled to avoid external CT log list downloads.
748-
func NewAttestationClient(offline bool) *http.Client {
749-
client := tlsct.NewHTTPClientWithTransport(AttestationTimeout, &http.Transport{
750-
MaxIdleConnsPerHost: 10,
751-
IdleConnTimeout: 90 * time.Second,
752-
}, !offline)
753-
client.Transport = tlsct.WrapLogging(client.Transport, AttestationTimeout)
754-
client.Transport = &RetryTransport{Base: client.Transport}
755-
return client
756-
}

0 commit comments

Comments
 (0)