github.com/stolowski/snapd@v0.0.0-20210407085831-115137ce5a22/store/store_asserts.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2014-2020 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 // Package store has support to use the Ubuntu Store for querying and downloading of snaps, and the related services. 21 package store 22 23 import ( 24 "context" 25 "encoding/json" 26 "fmt" 27 "io" 28 "net/http" 29 "net/url" 30 "path" 31 "strconv" 32 33 "github.com/snapcore/snapd/asserts" 34 "github.com/snapcore/snapd/httputil" 35 "github.com/snapcore/snapd/overlord/auth" 36 ) 37 38 func (s *Store) assertionsEndpointURL(p string, query url.Values) *url.URL { 39 defBaseURL := s.cfg.StoreBaseURL 40 // can be overridden separately! 41 if s.cfg.AssertionsBaseURL != nil { 42 defBaseURL = s.cfg.AssertionsBaseURL 43 } 44 return endpointURL(s.baseURL(defBaseURL), path.Join(assertionsPath, p), query) 45 } 46 47 type assertionSvcError struct { 48 // v1 error fields 49 // XXX: remove once switched to v2 API request. 50 Status int `json:"status"` 51 Type string `json:"type"` 52 Title string `json:"title"` 53 Detail string `json:"detail"` 54 55 // v2 error list - the only field included in v2 error response. 56 // XXX: there is an overlap with searchV2Results (and partially with 57 // errorListEntry), we could share the definition. 58 ErrorList []struct { 59 Code string `json:"code"` 60 Message string `json:"message"` 61 } `json:"error-list"` 62 } 63 64 func (e *assertionSvcError) isNotFound() bool { 65 return (len(e.ErrorList) > 0 && e.ErrorList[0].Code == "not-found" /* v2 error */) || e.Status == 404 66 } 67 68 func (e *assertionSvcError) toError() error { 69 // is it v2 error? 70 if len(e.ErrorList) > 0 { 71 return fmt.Errorf("assertion service error: %q", e.ErrorList[0].Message) 72 } 73 // v1 error 74 return fmt.Errorf("assertion service error: [%s] %q", e.Title, e.Detail) 75 } 76 77 // Assertion retrieves the assertion for the given type and primary key. 78 func (s *Store) Assertion(assertType *asserts.AssertionType, primaryKey []string, user *auth.UserState) (asserts.Assertion, error) { 79 v := url.Values{} 80 v.Set("max-format", strconv.Itoa(assertType.MaxSupportedFormat())) 81 u := s.assertionsEndpointURL(path.Join(assertType.Name, path.Join(primaryKey...)), v) 82 83 var asrt asserts.Assertion 84 85 err := s.downloadAssertions(u, func(r io.Reader) error { 86 // decode assertion 87 dec := asserts.NewDecoder(r) 88 var e error 89 asrt, e = dec.Decode() 90 return e 91 }, func(svcErr *assertionSvcError) error { 92 // error-list indicates v2 error response. 93 if svcErr.isNotFound() { 94 // best-effort 95 headers, _ := asserts.HeadersFromPrimaryKey(assertType, primaryKey) 96 return &asserts.NotFoundError{ 97 Type: assertType, 98 Headers: headers, 99 } 100 } 101 // default error 102 return nil 103 }, "fetch assertion", user) 104 if err != nil { 105 return nil, err 106 } 107 return asrt, nil 108 } 109 110 // SeqFormingAssertion retrieves the sequence-forming assertion for the given 111 // type (currently validation-set only). For sequence <= 0 we query for the 112 // latest sequence, otherwise the latest revision of the given sequence is 113 // requested. 114 func (s *Store) SeqFormingAssertion(assertType *asserts.AssertionType, sequenceKey []string, sequence int, user *auth.UserState) (asserts.Assertion, error) { 115 if !assertType.SequenceForming() { 116 return nil, fmt.Errorf("internal error: requested non sequence-forming assertion type %q", assertType.Name) 117 } 118 v := url.Values{} 119 v.Set("max-format", strconv.Itoa(assertType.MaxSupportedFormat())) 120 121 hasSequenceNumber := sequence > 0 122 if hasSequenceNumber { 123 // full primary key passed, query specific sequence number. 124 v.Set("sequence", fmt.Sprintf("%d", sequence)) 125 } else { 126 // query for the latest sequence. 127 v.Set("sequence", "latest") 128 } 129 u := s.assertionsEndpointURL(path.Join(assertType.Name, path.Join(sequenceKey...)), v) 130 131 var asrt asserts.Assertion 132 133 err := s.downloadAssertions(u, func(r io.Reader) error { 134 // decode assertion 135 dec := asserts.NewDecoder(r) 136 var e error 137 asrt, e = dec.Decode() 138 return e 139 }, func(svcErr *assertionSvcError) error { 140 // error-list indicates v2 error response. 141 if svcErr.isNotFound() { 142 // XXX: this re-implements asserts.HeadersFromPrimaryKey() but is 143 // more relaxed about key length, making sequence optional. Should 144 // we make it a helper on its own in store for the not-found-error 145 // handling? 146 if len(sequenceKey) != len(assertType.PrimaryKey)-1 { 147 return fmt.Errorf("sequence key has wrong length for %q assertion", assertType.Name) 148 } 149 headers := make(map[string]string) 150 for i, keyVal := range sequenceKey { 151 name := assertType.PrimaryKey[i] 152 if keyVal == "" { 153 return fmt.Errorf("sequence key %q header cannot be empty", name) 154 } 155 headers[name] = keyVal 156 } 157 if hasSequenceNumber { 158 headers[assertType.PrimaryKey[len(assertType.PrimaryKey)-1]] = fmt.Sprintf("%d", sequence) 159 } 160 return &asserts.NotFoundError{ 161 Type: assertType, 162 Headers: headers, 163 } 164 } 165 // default error 166 return nil 167 }, "fetch assertion", user) 168 if err != nil { 169 return nil, err 170 } 171 return asrt, nil 172 } 173 174 func (s *Store) downloadAssertions(u *url.URL, decodeBody func(io.Reader) error, handleSvcErr func(*assertionSvcError) error, what string, user *auth.UserState) error { 175 reqOptions := &requestOptions{ 176 Method: "GET", 177 URL: u, 178 Accept: asserts.MediaType, 179 } 180 181 resp, err := httputil.RetryRequest(reqOptions.URL.String(), func() (*http.Response, error) { 182 return s.doRequest(context.TODO(), s.client, reqOptions, user) 183 }, func(resp *http.Response) error { 184 var e error 185 if resp.StatusCode == 200 { 186 e = decodeBody(resp.Body) 187 } else { 188 contentType := resp.Header.Get("Content-Type") 189 if contentType == jsonContentType || contentType == "application/problem+json" { 190 var svcErr assertionSvcError 191 dec := json.NewDecoder(resp.Body) 192 if e = dec.Decode(&svcErr); e != nil { 193 return fmt.Errorf("cannot decode assertion service error with HTTP status code %d: %v", resp.StatusCode, e) 194 } 195 if handleSvcErr != nil { 196 if e := handleSvcErr(&svcErr); e != nil { 197 return e 198 } 199 } 200 // default error handling 201 return svcErr.toError() 202 } 203 } 204 return e 205 }, defaultRetryStrategy) 206 207 if err != nil { 208 return err 209 } 210 211 if resp.StatusCode != 200 { 212 return respToError(resp, what) 213 } 214 215 return nil 216 } 217 218 // DownloadAssertions download the assertion streams at the given URLs 219 // and adds their assertions to the given asserts.Batch. 220 func (s *Store) DownloadAssertions(streamURLs []string, b *asserts.Batch, user *auth.UserState) error { 221 for _, ustr := range streamURLs { 222 u, err := url.Parse(ustr) 223 if err != nil { 224 return fmt.Errorf("invalid assertions stream URL: %v", err) 225 } 226 227 err = s.downloadAssertions(u, func(r io.Reader) error { 228 // decode stream 229 _, e := b.AddStream(r) 230 return e 231 }, nil, "download assertion stream", user) 232 if err != nil { 233 return err 234 } 235 236 } 237 return nil 238 }