github.com/bugraaydogar/snapd@v0.0.0-20210315170335-8c70bb858939/daemon/api_download.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2019 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 daemon 21 22 import ( 23 "context" 24 "crypto/hmac" 25 "crypto/sha256" 26 "encoding/base64" 27 "encoding/json" 28 "errors" 29 "fmt" 30 "net/http" 31 "path/filepath" 32 "regexp" 33 "strconv" 34 "time" 35 36 "github.com/snapcore/snapd/logger" 37 "github.com/snapcore/snapd/overlord/auth" 38 "github.com/snapcore/snapd/overlord/state" 39 "github.com/snapcore/snapd/randutil" 40 "github.com/snapcore/snapd/snap" 41 "github.com/snapcore/snapd/store" 42 ) 43 44 var snapDownloadCmd = &Command{ 45 Path: "/v2/download", 46 PolkitOK: "io.snapcraft.snapd.manage", 47 POST: postSnapDownload, 48 } 49 50 var validRangeRegexp = regexp.MustCompile(`^\s*bytes=(\d+)-\s*$`) 51 52 // SnapDownloadAction is used to request a snap download 53 type snapDownloadAction struct { 54 SnapName string `json:"snap-name"` 55 snapRevisionOptions 56 57 // HeaderPeek if set requests a peek at the header without the 58 // body being returned. 59 HeaderPeek bool `json:"header-peek"` 60 61 ResumeToken string `json:"resume-token"` 62 resumePosition int64 63 } 64 65 var ( 66 errDownloadNameRequired = errors.New("download operation requires one snap name") 67 errDownloadHeaderPeekResume = errors.New("cannot request header-only peek when resuming") 68 errDownloadResumeNoToken = errors.New("cannot resume without a token") 69 ) 70 71 func (action *snapDownloadAction) validate() error { 72 if action.SnapName == "" { 73 return errDownloadNameRequired 74 } 75 if action.HeaderPeek && (action.resumePosition > 0 || action.ResumeToken != "") { 76 return errDownloadHeaderPeekResume 77 } 78 if action.resumePosition > 0 && action.ResumeToken == "" { 79 return errDownloadResumeNoToken 80 } 81 return action.snapRevisionOptions.validate() 82 } 83 84 func postSnapDownload(c *Command, r *http.Request, user *auth.UserState) Response { 85 var action snapDownloadAction 86 decoder := json.NewDecoder(r.Body) 87 if err := decoder.Decode(&action); err != nil { 88 return BadRequest("cannot decode request body into download operation: %v", err) 89 } 90 if decoder.More() { 91 return BadRequest("extra content found after download operation") 92 } 93 if rangestr := r.Header.Get("Range"); rangestr != "" { 94 // "An origin server MUST ignore a Range header field 95 // that contains a range unit it does not understand." 96 subs := validRangeRegexp.FindStringSubmatch(rangestr) 97 if len(subs) == 2 { 98 n, err := strconv.ParseInt(subs[1], 10, 64) 99 if err == nil { 100 action.resumePosition = n 101 } 102 } 103 } 104 if err := action.validate(); err != nil { 105 return BadRequest(err.Error()) 106 } 107 108 return streamOneSnap(c, action, user) 109 } 110 111 func streamOneSnap(c *Command, action snapDownloadAction, user *auth.UserState) Response { 112 secret, err := downloadTokensSecret(c) 113 if err != nil { 114 return InternalError(err.Error()) 115 } 116 theStore := getStore(c) 117 118 var ss *snapStream 119 if action.ResumeToken == "" { 120 var info *snap.Info 121 actions := []*store.SnapAction{{ 122 Action: "download", 123 InstanceName: action.SnapName, 124 Revision: action.Revision, 125 CohortKey: action.CohortKey, 126 Channel: action.Channel, 127 }} 128 results, _, err := theStore.SnapAction(context.TODO(), nil, actions, nil, user, nil) 129 if err != nil { 130 return errToResponse(err, []string{action.SnapName}, InternalError, "cannot download snap: %v") 131 } 132 if len(results) != 1 { 133 return InternalError("internal error: unexpected number %v of results for a single download", len(results)) 134 } 135 info = results[0].Info 136 137 ss, err = newSnapStream(action.SnapName, info, secret) 138 if err != nil { 139 return InternalError(err.Error()) 140 } 141 } else { 142 var err error 143 ss, err = newResumingSnapStream(action.SnapName, action.ResumeToken, secret) 144 if err != nil { 145 return BadRequest(err.Error()) 146 } 147 ss.resume = action.resumePosition 148 } 149 150 if !action.HeaderPeek { 151 stream, status, err := theStore.DownloadStream(context.TODO(), action.SnapName, ss.Info, action.resumePosition, user) 152 if err != nil { 153 return InternalError(err.Error()) 154 } 155 ss.stream = stream 156 if status != 206 { 157 // store/cdn has no partial content (valid 158 // reply per RFC) 159 logger.Debugf("store refused our range request") 160 ss.resume = 0 161 } 162 } 163 164 return ss 165 } 166 167 func newSnapStream(snapName string, info *snap.Info, secret []byte) (*snapStream, error) { 168 dlInfo := &info.DownloadInfo 169 fname := filepath.Base(info.MountFile()) 170 tokenJSON := downloadTokenJSON{ 171 SnapName: snapName, 172 Filename: fname, 173 Info: dlInfo, 174 } 175 tokStr, err := sealDownloadToken(&tokenJSON, secret) 176 if err != nil { 177 return nil, err 178 } 179 return &snapStream{ 180 SnapName: snapName, 181 Filename: fname, 182 Info: dlInfo, 183 Token: tokStr, 184 }, nil 185 } 186 187 func newResumingSnapStream(snapName string, tokStr string, secret []byte) (*snapStream, error) { 188 d, err := unsealDownloadToken(tokStr, secret) 189 if err != nil { 190 return nil, err 191 } 192 if d.SnapName != snapName { 193 return nil, fmt.Errorf("resume snap name does not match original snap name") 194 } 195 return &snapStream{ 196 SnapName: snapName, 197 Filename: d.Filename, 198 Info: d.Info, 199 Token: tokStr, 200 }, nil 201 } 202 203 type downloadTokenJSON struct { 204 SnapName string `json:"snap-name"` 205 Filename string `json:"filename"` 206 Info *snap.DownloadInfo `json:"dl-info"` 207 } 208 209 func sealDownloadToken(d *downloadTokenJSON, secret []byte) (string, error) { 210 b, err := json.Marshal(d) 211 if err != nil { 212 return "", err 213 } 214 mac := hmac.New(sha256.New, secret) 215 mac.Write(b) 216 // append the HMAC hash to b to build the full raw token tok 217 tok := mac.Sum(b) 218 return base64.RawURLEncoding.EncodeToString(tok), nil 219 } 220 221 var errInvalidDownloadToken = errors.New("download token is invalid") 222 223 func unsealDownloadToken(tokStr string, secret []byte) (*downloadTokenJSON, error) { 224 tok, err := base64.RawURLEncoding.DecodeString(tokStr) 225 if err != nil { 226 return nil, errInvalidDownloadToken 227 } 228 sz := len(tok) 229 if sz < sha256.Size { 230 return nil, errInvalidDownloadToken 231 } 232 h := tok[sz-sha256.Size:] 233 b := tok[:sz-sha256.Size] 234 mac := hmac.New(sha256.New, secret) 235 mac.Write(b) 236 if !hmac.Equal(h, mac.Sum(nil)) { 237 return nil, errInvalidDownloadToken 238 } 239 var d downloadTokenJSON 240 if err := json.Unmarshal(b, &d); err != nil { 241 return nil, err 242 } 243 return &d, nil 244 } 245 246 func downloadTokensSecret(c *Command) (secret []byte, err error) { 247 st := c.d.overlord.State() 248 st.Lock() 249 defer st.Unlock() 250 const k = "api-download-tokens-secret" 251 err = st.Get(k, &secret) 252 if err == nil { 253 return secret, nil 254 } 255 if err != nil && err != state.ErrNoState { 256 return nil, err 257 } 258 secret, err = randutil.CryptoTokenBytes(32) 259 if err != nil { 260 return nil, err 261 } 262 st.Set(k, secret) 263 st.Set(k+"-time", time.Now().UTC()) 264 return secret, nil 265 }