github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/core/snap/assertions.go (about)

     1  // Copyright 2019 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package snap
     5  
     6  import (
     7  	"context"
     8  	"io"
     9  	"net/http"
    10  	"net/url"
    11  	"regexp"
    12  	"time"
    13  
    14  	"github.com/juju/errors"
    15  )
    16  
    17  // LookupAssertions attempts to download an assertion list from the snap store
    18  // proxy located at proxyURL and locate the store ID associated with the
    19  // specified proxyURL.
    20  //
    21  // If the local snap store proxy instance is operating in an air-gapped
    22  // environment, downloading the assertion list from the proxy will not be
    23  // possible and an appropriate error will be returned.
    24  func LookupAssertions(proxyURL string) (assertions, storeID string, err error) {
    25  	u, err := url.Parse(proxyURL)
    26  	if err != nil {
    27  		return "", "", errors.Annotate(err, "proxy URL not valid")
    28  	}
    29  	if u.Scheme != "http" && u.Scheme != "https" {
    30  		return "", "", errors.NotValidf("proxy URL scheme %q", u.Scheme)
    31  	}
    32  
    33  	// Make sure to redact user/pass when including the proxy URL in error messages
    34  	u.User = nil
    35  	noCredsProxyURL := u.String()
    36  
    37  	pathURL, _ := url.Parse("/v2/auth/store/assertions")
    38  	req, _ := http.NewRequest("GET", u.ResolveReference(pathURL).String(), nil)
    39  	ctx, cancelFn := context.WithTimeout(context.Background(), 30*time.Second)
    40  	defer cancelFn()
    41  
    42  	res, err := http.DefaultClient.Do(req.WithContext(ctx))
    43  	if err != nil {
    44  		return "", "", errors.Annotatef(err, "could not contact snap store proxy at %q. If using an air-gapped proxy you must manually provide the assertions file and store ID", noCredsProxyURL)
    45  	}
    46  	defer func() { _ = res.Body.Close() }()
    47  	if res.StatusCode != http.StatusOK {
    48  		return "", "", errors.Annotatef(err, "could not retrieve assertions from proxy at %q; proxy replied with unexpected HTTP status code %d", noCredsProxyURL, res.StatusCode)
    49  	}
    50  
    51  	data, err := io.ReadAll(res.Body)
    52  	if err != nil {
    53  		return "", "", errors.Annotatef(err, "could not read assertions response from proxy at %q", noCredsProxyURL)
    54  	}
    55  	assertions = string(data)
    56  	if storeID, err = findStoreID(assertions, u); err != nil {
    57  		return "", "", errors.Trace(err)
    58  	}
    59  
    60  	return assertions, storeID, nil
    61  }
    62  
    63  var storeInAssertionRE = regexp.MustCompile(`(?is)type: store.*?store: ([a-zA-Z0-9]+).*?url: (https?://[^\s]+)`)
    64  
    65  func findStoreID(assertions string, proxyURL *url.URL) (string, error) {
    66  	var storeID string
    67  	for _, match := range storeInAssertionRE.FindAllStringSubmatch(assertions, -1) {
    68  		if len(match) != 3 {
    69  			continue
    70  		}
    71  
    72  		// Found store assertion but not for the URL provided
    73  		storeURL, err := url.Parse(match[2])
    74  		if err != nil {
    75  			continue
    76  		}
    77  		if storeURL.Host != proxyURL.Host {
    78  			continue
    79  		}
    80  
    81  		// Found same URL but different store ID
    82  		if storeID != "" && match[1] != storeID {
    83  			return "", errors.Errorf("assertions response from proxy at %q is ambiguous as it contains multiple entries with the same proxy URL but different store ID", proxyURL)
    84  		}
    85  
    86  		storeID = match[1]
    87  	}
    88  
    89  	if storeID == "" {
    90  		return "", errors.NotFoundf("store ID in assertions response from proxy at %q", proxyURL)
    91  	}
    92  
    93  	return storeID, nil
    94  }