From fbddbc98679f5800ee25b7d127ae1f30b3571c71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Wei=C3=9Fe?= Date: Tue, 11 Jun 2024 16:11:53 +0200 Subject: [PATCH] Dont re-marshal fetched objects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Weiße --- internal/api/fetcher/fetcher.go | 95 +++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 41 deletions(-) diff --git a/internal/api/fetcher/fetcher.go b/internal/api/fetcher/fetcher.go index 2b18f9d15..ec3118c9c 100644 --- a/internal/api/fetcher/fetcher.go +++ b/internal/api/fetcher/fetcher.go @@ -22,6 +22,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "net/http" "net/url" "strings" @@ -40,45 +41,12 @@ func NewHTTPClient() HTTPClient { // Fetch fetches the given apiObject from the public Constellation CDN. // Fetch does not require authentication. func Fetch[T apiObject](ctx context.Context, c HTTPClient, cdnURL string, obj T) (T, error) { - if err := obj.ValidateRequest(); err != nil { - return *new(T), fmt.Errorf("validating request for %T: %w", obj, err) - } - - urlObj, err := url.Parse(cdnURL) + rawObj, err := fetch(ctx, c, cdnURL, obj) if err != nil { - return *new(T), fmt.Errorf("parsing CDN root URL: %w", err) - } - urlObj.Path = obj.JSONPath() - url := urlObj.String() - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody) - if err != nil { - return *new(T), fmt.Errorf("creating request for %T: %w", obj, err) + return *new(T), fmt.Errorf("fetching %T: %w", obj, err) } - resp, err := c.Do(req) - if err != nil { - return *new(T), fmt.Errorf("sending request for %T: %w", obj, err) - } - defer resp.Body.Close() - switch resp.StatusCode { - case http.StatusOK: - case http.StatusNotFound: - return *new(T), &NotFoundError{fmt.Errorf("requesting resource at %s returned status code 404", url)} - default: - return *new(T), fmt.Errorf("unexpected status code %d while requesting resource", resp.StatusCode) - } - - var newObj T - if err := json.NewDecoder(resp.Body).Decode(&newObj); err != nil { - return *new(T), fmt.Errorf("decoding %T: %w", obj, err) - } - - if newObj.Validate() != nil { - return *new(T), fmt.Errorf("received invalid %T: %w", newObj, newObj.Validate()) - } - - return newObj, nil + return parseObject(rawObj, obj) } // FetchAndVerify fetches the given apiObject, checks if it can fetch an accompanying signature and verifies if the signature matches the found object. @@ -86,25 +54,70 @@ func Fetch[T apiObject](ctx context.Context, c HTTPClient, cdnURL string, obj T) // FetchAndVerify uses a generic to return a new object of type T. // Otherwise the caller would have to cast the interface type to a concrete object, which could fail. func FetchAndVerify[T apiObject](ctx context.Context, c HTTPClient, cdnURL string, obj T, cosignVerifier sigstore.Verifier) (T, error) { - fetchedObj, err := Fetch(ctx, c, cdnURL, obj) + rawObj, err := fetch(ctx, c, cdnURL, obj) if err != nil { - return fetchedObj, fmt.Errorf("fetching object: %w", err) + return *new(T), fmt.Errorf("fetching %T: %w", obj, err) } - marshalledObj, err := json.Marshal(fetchedObj) + fetchedObj, err := parseObject(rawObj, obj) if err != nil { - return fetchedObj, fmt.Errorf("marshalling object: %w", err) + return fetchedObj, fmt.Errorf("parsing %T: %w", obj, err) } + signature, err := Fetch(ctx, c, cdnURL, signature{Signed: obj.JSONPath()}) if err != nil { return fetchedObj, fmt.Errorf("fetching signature: %w", err) } - err = cosignVerifier.VerifySignature(marshalledObj, signature.Signature) + err = cosignVerifier.VerifySignature(rawObj, signature.Signature) if err != nil { return fetchedObj, fmt.Errorf("verifying signature: %w", err) } return fetchedObj, nil } +func fetch[T apiObject](ctx context.Context, c HTTPClient, cdnURL string, obj T) ([]byte, error) { + if err := obj.ValidateRequest(); err != nil { + return nil, fmt.Errorf("validating request for %T: %w", obj, err) + } + + urlObj, err := url.Parse(cdnURL) + if err != nil { + return nil, fmt.Errorf("parsing CDN root URL: %w", err) + } + urlObj.Path = obj.JSONPath() + url := urlObj.String() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody) + if err != nil { + return nil, fmt.Errorf("creating request for %T: %w", obj, err) + } + + resp, err := c.Do(req) + if err != nil { + return nil, fmt.Errorf("sending request for %T: %w", obj, err) + } + defer resp.Body.Close() + switch resp.StatusCode { + case http.StatusOK: + case http.StatusNotFound: + return nil, &NotFoundError{fmt.Errorf("requesting resource at %s returned status code 404", url)} + default: + return nil, fmt.Errorf("unexpected status code %d while requesting resource", resp.StatusCode) + } + + return io.ReadAll(resp.Body) +} + +func parseObject[T apiObject](rawObj []byte, obj T) (T, error) { + var newObj T + if err := json.Unmarshal(rawObj, &newObj); err != nil { + return *new(T), fmt.Errorf("decoding %T: %w", obj, err) + } + if newObj.Validate() != nil { + return *new(T), fmt.Errorf("received invalid %T: %w", newObj, newObj.Validate()) + } + return newObj, nil +} + // NotFoundError is an error that is returned when a resource is not found. type NotFoundError struct { err error