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 }