Files
snapd/daemon/api_download.go

266 lines
7.1 KiB
Go
Raw Permalink Normal View History

// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2019 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package daemon
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/http"
"path/filepath"
daemon, store: support resuming downloads This implements a first pass at the snapd side of resumable-via-snapd downloads. There's a little dance the client needs to do to find out what it's going to download: right now snapd replies with the sha3, and the client should pass that to snapd when wanting to resume. It's not prefixed but probably should be, so we can switch to using hmac without having to add another blob field in the api. It goes like this: first, get the hash and other info, $ sudo curl -D- --unix-socket /run/snapd.socket -d '{"snap-name":"http","no-body":true}' http://localhost/v2/download HTTP/1.1 200 OK Content-Disposition: attachment; filename=http_29.snap Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:07:30 GMT Content-Length: 0 then you ask for the whole thing, $ sudo curl -s -D- -o http_29.snap --unix-socket /run/snapd.socket -d '{"snap-name":"http","resume-stamp":"bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea"}' http://localhost/v2/download HTTP/1.1 200 OK Content-Disposition: attachment; filename=http_29.snap Content-Length: 2572288 Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:10:41 GMT then you truncate the file $ sudo truncate -s 1MB http_29.snap and ask to continue (note in 16.04's curl at least the -C option doesn't work well with POSTs -- it tries to continue the POST? I have no idea), $ sudo curl -s -D- -H Range:bytes=1000000- -o http_29.snap --unix-socket /run/snapd.socket -d '{"snap-name":"http","resume-stamp":"bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea"}' http://localhost/v2/download HTTP/1.1 206 Partial Content Content-Disposition: attachment; filename=http_29.snap Content-Length: 1572288 Content-Range: bytes 1000000-2572287/2572288 Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:16:31 GMT (also note this change only has snapd supporting ranges like this one, ie "bytes=<start>-"; everything else results in a 200). I haven't had time to write unit tests for this, other than for some of the store bits which are better covered than before (but need more). Notably the 200 vs 206 thing I describe below is NOT covered. I've tested it "live" using curl and httpie, and it's sensible and works, and can resume downloading a snap that's in the cache. Right now the store isn't ever replying with a 206 when I ask for a range, so I haven't been able to end-to-end that path, but it's not anything I'm doing wrong AFAICT; the store also doesn't 206 when curl or httpie talk to it directly. I think this is a bug in the store, but it's also correct behaviour AFAIK (from reading the RFC). The client will need to check whether a ranged request comes back with a 206 or a 200 to know whether it's actually resuming or not. NOTE the store itself (`downloadImpl`) doesn't support this behaviour; this needs fixing as currently it'll be downloading the whole thing twice as soon as it tries to resume once and gets the full content instead (the sha3 won't match). I tried to dive into fixing this but it's waaay too late.
2020-02-01 01:31:15 +00:00
"regexp"
"strconv"
"time"
daemon, store: support resuming downloads This implements a first pass at the snapd side of resumable-via-snapd downloads. There's a little dance the client needs to do to find out what it's going to download: right now snapd replies with the sha3, and the client should pass that to snapd when wanting to resume. It's not prefixed but probably should be, so we can switch to using hmac without having to add another blob field in the api. It goes like this: first, get the hash and other info, $ sudo curl -D- --unix-socket /run/snapd.socket -d '{"snap-name":"http","no-body":true}' http://localhost/v2/download HTTP/1.1 200 OK Content-Disposition: attachment; filename=http_29.snap Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:07:30 GMT Content-Length: 0 then you ask for the whole thing, $ sudo curl -s -D- -o http_29.snap --unix-socket /run/snapd.socket -d '{"snap-name":"http","resume-stamp":"bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea"}' http://localhost/v2/download HTTP/1.1 200 OK Content-Disposition: attachment; filename=http_29.snap Content-Length: 2572288 Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:10:41 GMT then you truncate the file $ sudo truncate -s 1MB http_29.snap and ask to continue (note in 16.04's curl at least the -C option doesn't work well with POSTs -- it tries to continue the POST? I have no idea), $ sudo curl -s -D- -H Range:bytes=1000000- -o http_29.snap --unix-socket /run/snapd.socket -d '{"snap-name":"http","resume-stamp":"bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea"}' http://localhost/v2/download HTTP/1.1 206 Partial Content Content-Disposition: attachment; filename=http_29.snap Content-Length: 1572288 Content-Range: bytes 1000000-2572287/2572288 Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:16:31 GMT (also note this change only has snapd supporting ranges like this one, ie "bytes=<start>-"; everything else results in a 200). I haven't had time to write unit tests for this, other than for some of the store bits which are better covered than before (but need more). Notably the 200 vs 206 thing I describe below is NOT covered. I've tested it "live" using curl and httpie, and it's sensible and works, and can resume downloading a snap that's in the cache. Right now the store isn't ever replying with a 206 when I ask for a range, so I haven't been able to end-to-end that path, but it's not anything I'm doing wrong AFAICT; the store also doesn't 206 when curl or httpie talk to it directly. I think this is a bug in the store, but it's also correct behaviour AFAIK (from reading the RFC). The client will need to check whether a ranged request comes back with a 206 or a 200 to know whether it's actually resuming or not. NOTE the store itself (`downloadImpl`) doesn't support this behaviour; this needs fixing as currently it'll be downloading the whole thing twice as soon as it tries to resume once and gets the full content instead (the sha3 won't match). I tried to dive into fixing this but it's waaay too late.
2020-02-01 01:31:15 +00:00
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/overlord/auth"
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/randutil"
daemon, store: support resuming downloads This implements a first pass at the snapd side of resumable-via-snapd downloads. There's a little dance the client needs to do to find out what it's going to download: right now snapd replies with the sha3, and the client should pass that to snapd when wanting to resume. It's not prefixed but probably should be, so we can switch to using hmac without having to add another blob field in the api. It goes like this: first, get the hash and other info, $ sudo curl -D- --unix-socket /run/snapd.socket -d '{"snap-name":"http","no-body":true}' http://localhost/v2/download HTTP/1.1 200 OK Content-Disposition: attachment; filename=http_29.snap Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:07:30 GMT Content-Length: 0 then you ask for the whole thing, $ sudo curl -s -D- -o http_29.snap --unix-socket /run/snapd.socket -d '{"snap-name":"http","resume-stamp":"bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea"}' http://localhost/v2/download HTTP/1.1 200 OK Content-Disposition: attachment; filename=http_29.snap Content-Length: 2572288 Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:10:41 GMT then you truncate the file $ sudo truncate -s 1MB http_29.snap and ask to continue (note in 16.04's curl at least the -C option doesn't work well with POSTs -- it tries to continue the POST? I have no idea), $ sudo curl -s -D- -H Range:bytes=1000000- -o http_29.snap --unix-socket /run/snapd.socket -d '{"snap-name":"http","resume-stamp":"bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea"}' http://localhost/v2/download HTTP/1.1 206 Partial Content Content-Disposition: attachment; filename=http_29.snap Content-Length: 1572288 Content-Range: bytes 1000000-2572287/2572288 Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:16:31 GMT (also note this change only has snapd supporting ranges like this one, ie "bytes=<start>-"; everything else results in a 200). I haven't had time to write unit tests for this, other than for some of the store bits which are better covered than before (but need more). Notably the 200 vs 206 thing I describe below is NOT covered. I've tested it "live" using curl and httpie, and it's sensible and works, and can resume downloading a snap that's in the cache. Right now the store isn't ever replying with a 206 when I ask for a range, so I haven't been able to end-to-end that path, but it's not anything I'm doing wrong AFAICT; the store also doesn't 206 when curl or httpie talk to it directly. I think this is a bug in the store, but it's also correct behaviour AFAIK (from reading the RFC). The client will need to check whether a ranged request comes back with a 206 or a 200 to know whether it's actually resuming or not. NOTE the store itself (`downloadImpl`) doesn't support this behaviour; this needs fixing as currently it'll be downloading the whole thing twice as soon as it tries to resume once and gets the full content instead (the sha3 won't match). I tried to dive into fixing this but it's waaay too late.
2020-02-01 01:31:15 +00:00
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/store"
)
var snapDownloadCmd = &Command{
Path: "/v2/download",
POST: postSnapDownload,
WriteAccess: authenticatedAccess{Polkit: polkitActionManage},
}
var validRangeRegexp = regexp.MustCompile(`^\s*bytes=(\d+)-\s*$`)
daemon, store: support resuming downloads This implements a first pass at the snapd side of resumable-via-snapd downloads. There's a little dance the client needs to do to find out what it's going to download: right now snapd replies with the sha3, and the client should pass that to snapd when wanting to resume. It's not prefixed but probably should be, so we can switch to using hmac without having to add another blob field in the api. It goes like this: first, get the hash and other info, $ sudo curl -D- --unix-socket /run/snapd.socket -d '{"snap-name":"http","no-body":true}' http://localhost/v2/download HTTP/1.1 200 OK Content-Disposition: attachment; filename=http_29.snap Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:07:30 GMT Content-Length: 0 then you ask for the whole thing, $ sudo curl -s -D- -o http_29.snap --unix-socket /run/snapd.socket -d '{"snap-name":"http","resume-stamp":"bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea"}' http://localhost/v2/download HTTP/1.1 200 OK Content-Disposition: attachment; filename=http_29.snap Content-Length: 2572288 Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:10:41 GMT then you truncate the file $ sudo truncate -s 1MB http_29.snap and ask to continue (note in 16.04's curl at least the -C option doesn't work well with POSTs -- it tries to continue the POST? I have no idea), $ sudo curl -s -D- -H Range:bytes=1000000- -o http_29.snap --unix-socket /run/snapd.socket -d '{"snap-name":"http","resume-stamp":"bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea"}' http://localhost/v2/download HTTP/1.1 206 Partial Content Content-Disposition: attachment; filename=http_29.snap Content-Length: 1572288 Content-Range: bytes 1000000-2572287/2572288 Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:16:31 GMT (also note this change only has snapd supporting ranges like this one, ie "bytes=<start>-"; everything else results in a 200). I haven't had time to write unit tests for this, other than for some of the store bits which are better covered than before (but need more). Notably the 200 vs 206 thing I describe below is NOT covered. I've tested it "live" using curl and httpie, and it's sensible and works, and can resume downloading a snap that's in the cache. Right now the store isn't ever replying with a 206 when I ask for a range, so I haven't been able to end-to-end that path, but it's not anything I'm doing wrong AFAICT; the store also doesn't 206 when curl or httpie talk to it directly. I think this is a bug in the store, but it's also correct behaviour AFAIK (from reading the RFC). The client will need to check whether a ranged request comes back with a 206 or a 200 to know whether it's actually resuming or not. NOTE the store itself (`downloadImpl`) doesn't support this behaviour; this needs fixing as currently it'll be downloading the whole thing twice as soon as it tries to resume once and gets the full content instead (the sha3 won't match). I tried to dive into fixing this but it's waaay too late.
2020-02-01 01:31:15 +00:00
// SnapDownloadAction is used to request a snap download
type snapDownloadAction struct {
daemon, store: support resuming downloads This implements a first pass at the snapd side of resumable-via-snapd downloads. There's a little dance the client needs to do to find out what it's going to download: right now snapd replies with the sha3, and the client should pass that to snapd when wanting to resume. It's not prefixed but probably should be, so we can switch to using hmac without having to add another blob field in the api. It goes like this: first, get the hash and other info, $ sudo curl -D- --unix-socket /run/snapd.socket -d '{"snap-name":"http","no-body":true}' http://localhost/v2/download HTTP/1.1 200 OK Content-Disposition: attachment; filename=http_29.snap Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:07:30 GMT Content-Length: 0 then you ask for the whole thing, $ sudo curl -s -D- -o http_29.snap --unix-socket /run/snapd.socket -d '{"snap-name":"http","resume-stamp":"bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea"}' http://localhost/v2/download HTTP/1.1 200 OK Content-Disposition: attachment; filename=http_29.snap Content-Length: 2572288 Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:10:41 GMT then you truncate the file $ sudo truncate -s 1MB http_29.snap and ask to continue (note in 16.04's curl at least the -C option doesn't work well with POSTs -- it tries to continue the POST? I have no idea), $ sudo curl -s -D- -H Range:bytes=1000000- -o http_29.snap --unix-socket /run/snapd.socket -d '{"snap-name":"http","resume-stamp":"bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea"}' http://localhost/v2/download HTTP/1.1 206 Partial Content Content-Disposition: attachment; filename=http_29.snap Content-Length: 1572288 Content-Range: bytes 1000000-2572287/2572288 Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:16:31 GMT (also note this change only has snapd supporting ranges like this one, ie "bytes=<start>-"; everything else results in a 200). I haven't had time to write unit tests for this, other than for some of the store bits which are better covered than before (but need more). Notably the 200 vs 206 thing I describe below is NOT covered. I've tested it "live" using curl and httpie, and it's sensible and works, and can resume downloading a snap that's in the cache. Right now the store isn't ever replying with a 206 when I ask for a range, so I haven't been able to end-to-end that path, but it's not anything I'm doing wrong AFAICT; the store also doesn't 206 when curl or httpie talk to it directly. I think this is a bug in the store, but it's also correct behaviour AFAIK (from reading the RFC). The client will need to check whether a ranged request comes back with a 206 or a 200 to know whether it's actually resuming or not. NOTE the store itself (`downloadImpl`) doesn't support this behaviour; this needs fixing as currently it'll be downloading the whole thing twice as soon as it tries to resume once and gets the full content instead (the sha3 won't match). I tried to dive into fixing this but it's waaay too late.
2020-02-01 01:31:15 +00:00
SnapName string `json:"snap-name"`
snapRevisionOptions
// HeaderPeek if set requests a peek at the header without the
// body being returned.
HeaderPeek bool `json:"header-peek"`
ResumeToken string `json:"resume-token"`
daemon, store: support resuming downloads This implements a first pass at the snapd side of resumable-via-snapd downloads. There's a little dance the client needs to do to find out what it's going to download: right now snapd replies with the sha3, and the client should pass that to snapd when wanting to resume. It's not prefixed but probably should be, so we can switch to using hmac without having to add another blob field in the api. It goes like this: first, get the hash and other info, $ sudo curl -D- --unix-socket /run/snapd.socket -d '{"snap-name":"http","no-body":true}' http://localhost/v2/download HTTP/1.1 200 OK Content-Disposition: attachment; filename=http_29.snap Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:07:30 GMT Content-Length: 0 then you ask for the whole thing, $ sudo curl -s -D- -o http_29.snap --unix-socket /run/snapd.socket -d '{"snap-name":"http","resume-stamp":"bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea"}' http://localhost/v2/download HTTP/1.1 200 OK Content-Disposition: attachment; filename=http_29.snap Content-Length: 2572288 Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:10:41 GMT then you truncate the file $ sudo truncate -s 1MB http_29.snap and ask to continue (note in 16.04's curl at least the -C option doesn't work well with POSTs -- it tries to continue the POST? I have no idea), $ sudo curl -s -D- -H Range:bytes=1000000- -o http_29.snap --unix-socket /run/snapd.socket -d '{"snap-name":"http","resume-stamp":"bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea"}' http://localhost/v2/download HTTP/1.1 206 Partial Content Content-Disposition: attachment; filename=http_29.snap Content-Length: 1572288 Content-Range: bytes 1000000-2572287/2572288 Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:16:31 GMT (also note this change only has snapd supporting ranges like this one, ie "bytes=<start>-"; everything else results in a 200). I haven't had time to write unit tests for this, other than for some of the store bits which are better covered than before (but need more). Notably the 200 vs 206 thing I describe below is NOT covered. I've tested it "live" using curl and httpie, and it's sensible and works, and can resume downloading a snap that's in the cache. Right now the store isn't ever replying with a 206 when I ask for a range, so I haven't been able to end-to-end that path, but it's not anything I'm doing wrong AFAICT; the store also doesn't 206 when curl or httpie talk to it directly. I think this is a bug in the store, but it's also correct behaviour AFAIK (from reading the RFC). The client will need to check whether a ranged request comes back with a 206 or a 200 to know whether it's actually resuming or not. NOTE the store itself (`downloadImpl`) doesn't support this behaviour; this needs fixing as currently it'll be downloading the whole thing twice as soon as it tries to resume once and gets the full content instead (the sha3 won't match). I tried to dive into fixing this but it's waaay too late.
2020-02-01 01:31:15 +00:00
resumePosition int64
}
daemon, store: support resuming downloads This implements a first pass at the snapd side of resumable-via-snapd downloads. There's a little dance the client needs to do to find out what it's going to download: right now snapd replies with the sha3, and the client should pass that to snapd when wanting to resume. It's not prefixed but probably should be, so we can switch to using hmac without having to add another blob field in the api. It goes like this: first, get the hash and other info, $ sudo curl -D- --unix-socket /run/snapd.socket -d '{"snap-name":"http","no-body":true}' http://localhost/v2/download HTTP/1.1 200 OK Content-Disposition: attachment; filename=http_29.snap Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:07:30 GMT Content-Length: 0 then you ask for the whole thing, $ sudo curl -s -D- -o http_29.snap --unix-socket /run/snapd.socket -d '{"snap-name":"http","resume-stamp":"bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea"}' http://localhost/v2/download HTTP/1.1 200 OK Content-Disposition: attachment; filename=http_29.snap Content-Length: 2572288 Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:10:41 GMT then you truncate the file $ sudo truncate -s 1MB http_29.snap and ask to continue (note in 16.04's curl at least the -C option doesn't work well with POSTs -- it tries to continue the POST? I have no idea), $ sudo curl -s -D- -H Range:bytes=1000000- -o http_29.snap --unix-socket /run/snapd.socket -d '{"snap-name":"http","resume-stamp":"bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea"}' http://localhost/v2/download HTTP/1.1 206 Partial Content Content-Disposition: attachment; filename=http_29.snap Content-Length: 1572288 Content-Range: bytes 1000000-2572287/2572288 Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:16:31 GMT (also note this change only has snapd supporting ranges like this one, ie "bytes=<start>-"; everything else results in a 200). I haven't had time to write unit tests for this, other than for some of the store bits which are better covered than before (but need more). Notably the 200 vs 206 thing I describe below is NOT covered. I've tested it "live" using curl and httpie, and it's sensible and works, and can resume downloading a snap that's in the cache. Right now the store isn't ever replying with a 206 when I ask for a range, so I haven't been able to end-to-end that path, but it's not anything I'm doing wrong AFAICT; the store also doesn't 206 when curl or httpie talk to it directly. I think this is a bug in the store, but it's also correct behaviour AFAIK (from reading the RFC). The client will need to check whether a ranged request comes back with a 206 or a 200 to know whether it's actually resuming or not. NOTE the store itself (`downloadImpl`) doesn't support this behaviour; this needs fixing as currently it'll be downloading the whole thing twice as soon as it tries to resume once and gets the full content instead (the sha3 won't match). I tried to dive into fixing this but it's waaay too late.
2020-02-01 01:31:15 +00:00
var (
errDownloadNameRequired = errors.New("download operation requires one snap name")
errDownloadHeaderPeekResume = errors.New("cannot request header-only peek when resuming")
errDownloadResumeNoToken = errors.New("cannot resume without a token")
daemon, store: support resuming downloads This implements a first pass at the snapd side of resumable-via-snapd downloads. There's a little dance the client needs to do to find out what it's going to download: right now snapd replies with the sha3, and the client should pass that to snapd when wanting to resume. It's not prefixed but probably should be, so we can switch to using hmac without having to add another blob field in the api. It goes like this: first, get the hash and other info, $ sudo curl -D- --unix-socket /run/snapd.socket -d '{"snap-name":"http","no-body":true}' http://localhost/v2/download HTTP/1.1 200 OK Content-Disposition: attachment; filename=http_29.snap Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:07:30 GMT Content-Length: 0 then you ask for the whole thing, $ sudo curl -s -D- -o http_29.snap --unix-socket /run/snapd.socket -d '{"snap-name":"http","resume-stamp":"bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea"}' http://localhost/v2/download HTTP/1.1 200 OK Content-Disposition: attachment; filename=http_29.snap Content-Length: 2572288 Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:10:41 GMT then you truncate the file $ sudo truncate -s 1MB http_29.snap and ask to continue (note in 16.04's curl at least the -C option doesn't work well with POSTs -- it tries to continue the POST? I have no idea), $ sudo curl -s -D- -H Range:bytes=1000000- -o http_29.snap --unix-socket /run/snapd.socket -d '{"snap-name":"http","resume-stamp":"bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea"}' http://localhost/v2/download HTTP/1.1 206 Partial Content Content-Disposition: attachment; filename=http_29.snap Content-Length: 1572288 Content-Range: bytes 1000000-2572287/2572288 Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:16:31 GMT (also note this change only has snapd supporting ranges like this one, ie "bytes=<start>-"; everything else results in a 200). I haven't had time to write unit tests for this, other than for some of the store bits which are better covered than before (but need more). Notably the 200 vs 206 thing I describe below is NOT covered. I've tested it "live" using curl and httpie, and it's sensible and works, and can resume downloading a snap that's in the cache. Right now the store isn't ever replying with a 206 when I ask for a range, so I haven't been able to end-to-end that path, but it's not anything I'm doing wrong AFAICT; the store also doesn't 206 when curl or httpie talk to it directly. I think this is a bug in the store, but it's also correct behaviour AFAIK (from reading the RFC). The client will need to check whether a ranged request comes back with a 206 or a 200 to know whether it's actually resuming or not. NOTE the store itself (`downloadImpl`) doesn't support this behaviour; this needs fixing as currently it'll be downloading the whole thing twice as soon as it tries to resume once and gets the full content instead (the sha3 won't match). I tried to dive into fixing this but it's waaay too late.
2020-02-01 01:31:15 +00:00
)
func (action *snapDownloadAction) validate() error {
if action.SnapName == "" {
return errDownloadNameRequired
}
if action.HeaderPeek && (action.resumePosition > 0 || action.ResumeToken != "") {
return errDownloadHeaderPeekResume
daemon, store: support resuming downloads This implements a first pass at the snapd side of resumable-via-snapd downloads. There's a little dance the client needs to do to find out what it's going to download: right now snapd replies with the sha3, and the client should pass that to snapd when wanting to resume. It's not prefixed but probably should be, so we can switch to using hmac without having to add another blob field in the api. It goes like this: first, get the hash and other info, $ sudo curl -D- --unix-socket /run/snapd.socket -d '{"snap-name":"http","no-body":true}' http://localhost/v2/download HTTP/1.1 200 OK Content-Disposition: attachment; filename=http_29.snap Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:07:30 GMT Content-Length: 0 then you ask for the whole thing, $ sudo curl -s -D- -o http_29.snap --unix-socket /run/snapd.socket -d '{"snap-name":"http","resume-stamp":"bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea"}' http://localhost/v2/download HTTP/1.1 200 OK Content-Disposition: attachment; filename=http_29.snap Content-Length: 2572288 Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:10:41 GMT then you truncate the file $ sudo truncate -s 1MB http_29.snap and ask to continue (note in 16.04's curl at least the -C option doesn't work well with POSTs -- it tries to continue the POST? I have no idea), $ sudo curl -s -D- -H Range:bytes=1000000- -o http_29.snap --unix-socket /run/snapd.socket -d '{"snap-name":"http","resume-stamp":"bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea"}' http://localhost/v2/download HTTP/1.1 206 Partial Content Content-Disposition: attachment; filename=http_29.snap Content-Length: 1572288 Content-Range: bytes 1000000-2572287/2572288 Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:16:31 GMT (also note this change only has snapd supporting ranges like this one, ie "bytes=<start>-"; everything else results in a 200). I haven't had time to write unit tests for this, other than for some of the store bits which are better covered than before (but need more). Notably the 200 vs 206 thing I describe below is NOT covered. I've tested it "live" using curl and httpie, and it's sensible and works, and can resume downloading a snap that's in the cache. Right now the store isn't ever replying with a 206 when I ask for a range, so I haven't been able to end-to-end that path, but it's not anything I'm doing wrong AFAICT; the store also doesn't 206 when curl or httpie talk to it directly. I think this is a bug in the store, but it's also correct behaviour AFAIK (from reading the RFC). The client will need to check whether a ranged request comes back with a 206 or a 200 to know whether it's actually resuming or not. NOTE the store itself (`downloadImpl`) doesn't support this behaviour; this needs fixing as currently it'll be downloading the whole thing twice as soon as it tries to resume once and gets the full content instead (the sha3 won't match). I tried to dive into fixing this but it's waaay too late.
2020-02-01 01:31:15 +00:00
}
if action.resumePosition > 0 && action.ResumeToken == "" {
return errDownloadResumeNoToken
daemon, store: support resuming downloads This implements a first pass at the snapd side of resumable-via-snapd downloads. There's a little dance the client needs to do to find out what it's going to download: right now snapd replies with the sha3, and the client should pass that to snapd when wanting to resume. It's not prefixed but probably should be, so we can switch to using hmac without having to add another blob field in the api. It goes like this: first, get the hash and other info, $ sudo curl -D- --unix-socket /run/snapd.socket -d '{"snap-name":"http","no-body":true}' http://localhost/v2/download HTTP/1.1 200 OK Content-Disposition: attachment; filename=http_29.snap Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:07:30 GMT Content-Length: 0 then you ask for the whole thing, $ sudo curl -s -D- -o http_29.snap --unix-socket /run/snapd.socket -d '{"snap-name":"http","resume-stamp":"bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea"}' http://localhost/v2/download HTTP/1.1 200 OK Content-Disposition: attachment; filename=http_29.snap Content-Length: 2572288 Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:10:41 GMT then you truncate the file $ sudo truncate -s 1MB http_29.snap and ask to continue (note in 16.04's curl at least the -C option doesn't work well with POSTs -- it tries to continue the POST? I have no idea), $ sudo curl -s -D- -H Range:bytes=1000000- -o http_29.snap --unix-socket /run/snapd.socket -d '{"snap-name":"http","resume-stamp":"bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea"}' http://localhost/v2/download HTTP/1.1 206 Partial Content Content-Disposition: attachment; filename=http_29.snap Content-Length: 1572288 Content-Range: bytes 1000000-2572287/2572288 Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:16:31 GMT (also note this change only has snapd supporting ranges like this one, ie "bytes=<start>-"; everything else results in a 200). I haven't had time to write unit tests for this, other than for some of the store bits which are better covered than before (but need more). Notably the 200 vs 206 thing I describe below is NOT covered. I've tested it "live" using curl and httpie, and it's sensible and works, and can resume downloading a snap that's in the cache. Right now the store isn't ever replying with a 206 when I ask for a range, so I haven't been able to end-to-end that path, but it's not anything I'm doing wrong AFAICT; the store also doesn't 206 when curl or httpie talk to it directly. I think this is a bug in the store, but it's also correct behaviour AFAIK (from reading the RFC). The client will need to check whether a ranged request comes back with a 206 or a 200 to know whether it's actually resuming or not. NOTE the store itself (`downloadImpl`) doesn't support this behaviour; this needs fixing as currently it'll be downloading the whole thing twice as soon as it tries to resume once and gets the full content instead (the sha3 won't match). I tried to dive into fixing this but it's waaay too late.
2020-02-01 01:31:15 +00:00
}
return action.snapRevisionOptions.validate()
}
func postSnapDownload(c *Command, r *http.Request, user *auth.UserState) Response {
var action snapDownloadAction
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&action); err != nil {
return BadRequest("cannot decode request body into download operation: %v", err)
}
if decoder.More() {
return BadRequest("extra content found after download operation")
}
daemon, store: support resuming downloads This implements a first pass at the snapd side of resumable-via-snapd downloads. There's a little dance the client needs to do to find out what it's going to download: right now snapd replies with the sha3, and the client should pass that to snapd when wanting to resume. It's not prefixed but probably should be, so we can switch to using hmac without having to add another blob field in the api. It goes like this: first, get the hash and other info, $ sudo curl -D- --unix-socket /run/snapd.socket -d '{"snap-name":"http","no-body":true}' http://localhost/v2/download HTTP/1.1 200 OK Content-Disposition: attachment; filename=http_29.snap Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:07:30 GMT Content-Length: 0 then you ask for the whole thing, $ sudo curl -s -D- -o http_29.snap --unix-socket /run/snapd.socket -d '{"snap-name":"http","resume-stamp":"bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea"}' http://localhost/v2/download HTTP/1.1 200 OK Content-Disposition: attachment; filename=http_29.snap Content-Length: 2572288 Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:10:41 GMT then you truncate the file $ sudo truncate -s 1MB http_29.snap and ask to continue (note in 16.04's curl at least the -C option doesn't work well with POSTs -- it tries to continue the POST? I have no idea), $ sudo curl -s -D- -H Range:bytes=1000000- -o http_29.snap --unix-socket /run/snapd.socket -d '{"snap-name":"http","resume-stamp":"bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea"}' http://localhost/v2/download HTTP/1.1 206 Partial Content Content-Disposition: attachment; filename=http_29.snap Content-Length: 1572288 Content-Range: bytes 1000000-2572287/2572288 Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:16:31 GMT (also note this change only has snapd supporting ranges like this one, ie "bytes=<start>-"; everything else results in a 200). I haven't had time to write unit tests for this, other than for some of the store bits which are better covered than before (but need more). Notably the 200 vs 206 thing I describe below is NOT covered. I've tested it "live" using curl and httpie, and it's sensible and works, and can resume downloading a snap that's in the cache. Right now the store isn't ever replying with a 206 when I ask for a range, so I haven't been able to end-to-end that path, but it's not anything I'm doing wrong AFAICT; the store also doesn't 206 when curl or httpie talk to it directly. I think this is a bug in the store, but it's also correct behaviour AFAIK (from reading the RFC). The client will need to check whether a ranged request comes back with a 206 or a 200 to know whether it's actually resuming or not. NOTE the store itself (`downloadImpl`) doesn't support this behaviour; this needs fixing as currently it'll be downloading the whole thing twice as soon as it tries to resume once and gets the full content instead (the sha3 won't match). I tried to dive into fixing this but it's waaay too late.
2020-02-01 01:31:15 +00:00
if rangestr := r.Header.Get("Range"); rangestr != "" {
// "An origin server MUST ignore a Range header field
// that contains a range unit it does not understand."
subs := validRangeRegexp.FindStringSubmatch(rangestr)
if len(subs) == 2 {
n, err := strconv.ParseInt(subs[1], 10, 64)
if err == nil {
action.resumePosition = n
}
}
}
if err := action.validate(); err != nil {
return BadRequest(err.Error())
}
daemon, cmd/snapd: propagate context (#14130) * daemon: establish a cancelation chain for incoming API requests Establish a cancelation chain for incoming API requests, to ensure orderly shutdown. This prevents a situation in which an API request, such as notices wait can block snapd shtudown for a long time. Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> * daemon: return 500 when the request context gets canceled Request's can be canceled based on the code actually issuing a cancel on the associted context, hence an Internal Server Error seems more appropriate. Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> * o/snapstate: leave TODOs about using caller provided context Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> * daemon: pass down request context where possible Pass the context from the API request further down. Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> * daemon: set context in snap instruction for many-snap operation Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> * daemon: pass context as an explicit parameter to request handlers Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> * daemon: pass context Thanks to @ZeyadYasser Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> * daemon: comment on Start() taking a context. Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> * daemon: add unit tests targeting context passed to Start() Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> * daemon: drop unit test for hijacked context The test isn't very useful. Another option to trigger this would be to call Stop() without a prior call to Start(), but this segfaults on d.standbyOpinions.Stop(), so it'c clear this needs a followup fix or callign Stop() this way isn't supported. Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> --------- Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com>
2024-06-28 14:54:52 +02:00
return streamOneSnap(r.Context(), c, action, user)
}
daemon, cmd/snapd: propagate context (#14130) * daemon: establish a cancelation chain for incoming API requests Establish a cancelation chain for incoming API requests, to ensure orderly shutdown. This prevents a situation in which an API request, such as notices wait can block snapd shtudown for a long time. Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> * daemon: return 500 when the request context gets canceled Request's can be canceled based on the code actually issuing a cancel on the associted context, hence an Internal Server Error seems more appropriate. Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> * o/snapstate: leave TODOs about using caller provided context Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> * daemon: pass down request context where possible Pass the context from the API request further down. Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> * daemon: set context in snap instruction for many-snap operation Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> * daemon: pass context as an explicit parameter to request handlers Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> * daemon: pass context Thanks to @ZeyadYasser Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> * daemon: comment on Start() taking a context. Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> * daemon: add unit tests targeting context passed to Start() Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> * daemon: drop unit test for hijacked context The test isn't very useful. Another option to trigger this would be to call Stop() without a prior call to Start(), but this segfaults on d.standbyOpinions.Stop(), so it'c clear this needs a followup fix or callign Stop() this way isn't supported. Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> --------- Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com>
2024-06-28 14:54:52 +02:00
func streamOneSnap(ctx context.Context, c *Command, action snapDownloadAction, user *auth.UserState) Response {
secret, err := downloadTokensSecret(c.d)
2020-02-13 10:38:52 +01:00
if err != nil {
return InternalError(err.Error())
}
2021-03-13 20:28:22 +01:00
theStore := storeFrom(c.d)
daemon, store: support resuming downloads This implements a first pass at the snapd side of resumable-via-snapd downloads. There's a little dance the client needs to do to find out what it's going to download: right now snapd replies with the sha3, and the client should pass that to snapd when wanting to resume. It's not prefixed but probably should be, so we can switch to using hmac without having to add another blob field in the api. It goes like this: first, get the hash and other info, $ sudo curl -D- --unix-socket /run/snapd.socket -d '{"snap-name":"http","no-body":true}' http://localhost/v2/download HTTP/1.1 200 OK Content-Disposition: attachment; filename=http_29.snap Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:07:30 GMT Content-Length: 0 then you ask for the whole thing, $ sudo curl -s -D- -o http_29.snap --unix-socket /run/snapd.socket -d '{"snap-name":"http","resume-stamp":"bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea"}' http://localhost/v2/download HTTP/1.1 200 OK Content-Disposition: attachment; filename=http_29.snap Content-Length: 2572288 Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:10:41 GMT then you truncate the file $ sudo truncate -s 1MB http_29.snap and ask to continue (note in 16.04's curl at least the -C option doesn't work well with POSTs -- it tries to continue the POST? I have no idea), $ sudo curl -s -D- -H Range:bytes=1000000- -o http_29.snap --unix-socket /run/snapd.socket -d '{"snap-name":"http","resume-stamp":"bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea"}' http://localhost/v2/download HTTP/1.1 206 Partial Content Content-Disposition: attachment; filename=http_29.snap Content-Length: 1572288 Content-Range: bytes 1000000-2572287/2572288 Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:16:31 GMT (also note this change only has snapd supporting ranges like this one, ie "bytes=<start>-"; everything else results in a 200). I haven't had time to write unit tests for this, other than for some of the store bits which are better covered than before (but need more). Notably the 200 vs 206 thing I describe below is NOT covered. I've tested it "live" using curl and httpie, and it's sensible and works, and can resume downloading a snap that's in the cache. Right now the store isn't ever replying with a 206 when I ask for a range, so I haven't been able to end-to-end that path, but it's not anything I'm doing wrong AFAICT; the store also doesn't 206 when curl or httpie talk to it directly. I think this is a bug in the store, but it's also correct behaviour AFAIK (from reading the RFC). The client will need to check whether a ranged request comes back with a 206 or a 200 to know whether it's actually resuming or not. NOTE the store itself (`downloadImpl`) doesn't support this behaviour; this needs fixing as currently it'll be downloading the whole thing twice as soon as it tries to resume once and gets the full content instead (the sha3 won't match). I tried to dive into fixing this but it's waaay too late.
2020-02-01 01:31:15 +00:00
var ss *snapStream
if action.ResumeToken == "" {
var info *snap.Info
actions := []*store.SnapAction{{
Action: "download",
InstanceName: action.SnapName,
Revision: action.Revision,
CohortKey: action.CohortKey,
Channel: action.Channel,
}}
daemon, cmd/snapd: propagate context (#14130) * daemon: establish a cancelation chain for incoming API requests Establish a cancelation chain for incoming API requests, to ensure orderly shutdown. This prevents a situation in which an API request, such as notices wait can block snapd shtudown for a long time. Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> * daemon: return 500 when the request context gets canceled Request's can be canceled based on the code actually issuing a cancel on the associted context, hence an Internal Server Error seems more appropriate. Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> * o/snapstate: leave TODOs about using caller provided context Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> * daemon: pass down request context where possible Pass the context from the API request further down. Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> * daemon: set context in snap instruction for many-snap operation Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> * daemon: pass context as an explicit parameter to request handlers Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> * daemon: pass context Thanks to @ZeyadYasser Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> * daemon: comment on Start() taking a context. Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> * daemon: add unit tests targeting context passed to Start() Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> * daemon: drop unit test for hijacked context The test isn't very useful. Another option to trigger this would be to call Stop() without a prior call to Start(), but this segfaults on d.standbyOpinions.Stop(), so it'c clear this needs a followup fix or callign Stop() this way isn't supported. Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> --------- Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com>
2024-06-28 14:54:52 +02:00
results, _, err := theStore.SnapAction(ctx, nil, actions, nil, user, nil)
if err != nil {
return errToResponse(err, []string{action.SnapName}, InternalError, "cannot download snap: %v")
}
2020-02-17 12:04:10 +01:00
if len(results) != 1 {
return InternalError("internal error: unexpected number %v of results for a single download", len(results))
}
2020-02-17 12:04:10 +01:00
info = results[0].Info
ss, err = newSnapStream(action.SnapName, info, secret)
daemon, store: support resuming downloads This implements a first pass at the snapd side of resumable-via-snapd downloads. There's a little dance the client needs to do to find out what it's going to download: right now snapd replies with the sha3, and the client should pass that to snapd when wanting to resume. It's not prefixed but probably should be, so we can switch to using hmac without having to add another blob field in the api. It goes like this: first, get the hash and other info, $ sudo curl -D- --unix-socket /run/snapd.socket -d '{"snap-name":"http","no-body":true}' http://localhost/v2/download HTTP/1.1 200 OK Content-Disposition: attachment; filename=http_29.snap Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:07:30 GMT Content-Length: 0 then you ask for the whole thing, $ sudo curl -s -D- -o http_29.snap --unix-socket /run/snapd.socket -d '{"snap-name":"http","resume-stamp":"bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea"}' http://localhost/v2/download HTTP/1.1 200 OK Content-Disposition: attachment; filename=http_29.snap Content-Length: 2572288 Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:10:41 GMT then you truncate the file $ sudo truncate -s 1MB http_29.snap and ask to continue (note in 16.04's curl at least the -C option doesn't work well with POSTs -- it tries to continue the POST? I have no idea), $ sudo curl -s -D- -H Range:bytes=1000000- -o http_29.snap --unix-socket /run/snapd.socket -d '{"snap-name":"http","resume-stamp":"bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea"}' http://localhost/v2/download HTTP/1.1 206 Partial Content Content-Disposition: attachment; filename=http_29.snap Content-Length: 1572288 Content-Range: bytes 1000000-2572287/2572288 Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:16:31 GMT (also note this change only has snapd supporting ranges like this one, ie "bytes=<start>-"; everything else results in a 200). I haven't had time to write unit tests for this, other than for some of the store bits which are better covered than before (but need more). Notably the 200 vs 206 thing I describe below is NOT covered. I've tested it "live" using curl and httpie, and it's sensible and works, and can resume downloading a snap that's in the cache. Right now the store isn't ever replying with a 206 when I ask for a range, so I haven't been able to end-to-end that path, but it's not anything I'm doing wrong AFAICT; the store also doesn't 206 when curl or httpie talk to it directly. I think this is a bug in the store, but it's also correct behaviour AFAIK (from reading the RFC). The client will need to check whether a ranged request comes back with a 206 or a 200 to know whether it's actually resuming or not. NOTE the store itself (`downloadImpl`) doesn't support this behaviour; this needs fixing as currently it'll be downloading the whole thing twice as soon as it tries to resume once and gets the full content instead (the sha3 won't match). I tried to dive into fixing this but it's waaay too late.
2020-02-01 01:31:15 +00:00
if err != nil {
return InternalError(err.Error())
}
} else {
var err error
ss, err = newResumingSnapStream(action.SnapName, action.ResumeToken, secret)
if err != nil {
return BadRequest(err.Error())
}
ss.resume = action.resumePosition
}
if !action.HeaderPeek {
daemon, cmd/snapd: propagate context (#14130) * daemon: establish a cancelation chain for incoming API requests Establish a cancelation chain for incoming API requests, to ensure orderly shutdown. This prevents a situation in which an API request, such as notices wait can block snapd shtudown for a long time. Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> * daemon: return 500 when the request context gets canceled Request's can be canceled based on the code actually issuing a cancel on the associted context, hence an Internal Server Error seems more appropriate. Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> * o/snapstate: leave TODOs about using caller provided context Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> * daemon: pass down request context where possible Pass the context from the API request further down. Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> * daemon: set context in snap instruction for many-snap operation Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> * daemon: pass context as an explicit parameter to request handlers Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> * daemon: pass context Thanks to @ZeyadYasser Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> * daemon: comment on Start() taking a context. Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> * daemon: add unit tests targeting context passed to Start() Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> * daemon: drop unit test for hijacked context The test isn't very useful. Another option to trigger this would be to call Stop() without a prior call to Start(), but this segfaults on d.standbyOpinions.Stop(), so it'c clear this needs a followup fix or callign Stop() this way isn't supported. Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com> --------- Signed-off-by: Maciej Borzecki <maciej.borzecki@canonical.com>
2024-06-28 14:54:52 +02:00
stream, status, err := theStore.DownloadStream(ctx, action.SnapName, ss.Info, action.resumePosition, user)
if err != nil {
return InternalError(err.Error())
}
ss.stream = stream
if status != 206 {
2020-02-13 10:38:52 +01:00
// store/cdn has no partial content (valid
// reply per RFC)
daemon, store: support resuming downloads This implements a first pass at the snapd side of resumable-via-snapd downloads. There's a little dance the client needs to do to find out what it's going to download: right now snapd replies with the sha3, and the client should pass that to snapd when wanting to resume. It's not prefixed but probably should be, so we can switch to using hmac without having to add another blob field in the api. It goes like this: first, get the hash and other info, $ sudo curl -D- --unix-socket /run/snapd.socket -d '{"snap-name":"http","no-body":true}' http://localhost/v2/download HTTP/1.1 200 OK Content-Disposition: attachment; filename=http_29.snap Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:07:30 GMT Content-Length: 0 then you ask for the whole thing, $ sudo curl -s -D- -o http_29.snap --unix-socket /run/snapd.socket -d '{"snap-name":"http","resume-stamp":"bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea"}' http://localhost/v2/download HTTP/1.1 200 OK Content-Disposition: attachment; filename=http_29.snap Content-Length: 2572288 Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:10:41 GMT then you truncate the file $ sudo truncate -s 1MB http_29.snap and ask to continue (note in 16.04's curl at least the -C option doesn't work well with POSTs -- it tries to continue the POST? I have no idea), $ sudo curl -s -D- -H Range:bytes=1000000- -o http_29.snap --unix-socket /run/snapd.socket -d '{"snap-name":"http","resume-stamp":"bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea"}' http://localhost/v2/download HTTP/1.1 206 Partial Content Content-Disposition: attachment; filename=http_29.snap Content-Length: 1572288 Content-Range: bytes 1000000-2572287/2572288 Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:16:31 GMT (also note this change only has snapd supporting ranges like this one, ie "bytes=<start>-"; everything else results in a 200). I haven't had time to write unit tests for this, other than for some of the store bits which are better covered than before (but need more). Notably the 200 vs 206 thing I describe below is NOT covered. I've tested it "live" using curl and httpie, and it's sensible and works, and can resume downloading a snap that's in the cache. Right now the store isn't ever replying with a 206 when I ask for a range, so I haven't been able to end-to-end that path, but it's not anything I'm doing wrong AFAICT; the store also doesn't 206 when curl or httpie talk to it directly. I think this is a bug in the store, but it's also correct behaviour AFAIK (from reading the RFC). The client will need to check whether a ranged request comes back with a 206 or a 200 to know whether it's actually resuming or not. NOTE the store itself (`downloadImpl`) doesn't support this behaviour; this needs fixing as currently it'll be downloading the whole thing twice as soon as it tries to resume once and gets the full content instead (the sha3 won't match). I tried to dive into fixing this but it's waaay too late.
2020-02-01 01:31:15 +00:00
logger.Debugf("store refused our range request")
ss.resume = 0
daemon, store: support resuming downloads This implements a first pass at the snapd side of resumable-via-snapd downloads. There's a little dance the client needs to do to find out what it's going to download: right now snapd replies with the sha3, and the client should pass that to snapd when wanting to resume. It's not prefixed but probably should be, so we can switch to using hmac without having to add another blob field in the api. It goes like this: first, get the hash and other info, $ sudo curl -D- --unix-socket /run/snapd.socket -d '{"snap-name":"http","no-body":true}' http://localhost/v2/download HTTP/1.1 200 OK Content-Disposition: attachment; filename=http_29.snap Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:07:30 GMT Content-Length: 0 then you ask for the whole thing, $ sudo curl -s -D- -o http_29.snap --unix-socket /run/snapd.socket -d '{"snap-name":"http","resume-stamp":"bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea"}' http://localhost/v2/download HTTP/1.1 200 OK Content-Disposition: attachment; filename=http_29.snap Content-Length: 2572288 Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:10:41 GMT then you truncate the file $ sudo truncate -s 1MB http_29.snap and ask to continue (note in 16.04's curl at least the -C option doesn't work well with POSTs -- it tries to continue the POST? I have no idea), $ sudo curl -s -D- -H Range:bytes=1000000- -o http_29.snap --unix-socket /run/snapd.socket -d '{"snap-name":"http","resume-stamp":"bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea"}' http://localhost/v2/download HTTP/1.1 206 Partial Content Content-Disposition: attachment; filename=http_29.snap Content-Length: 1572288 Content-Range: bytes 1000000-2572287/2572288 Content-Type: application/octet-stream Snap-Length: 2572288 Snap-Sha3-384: bdbcbdac7e8701228b01381b4774419465b4bfbed489f0104f44e1ad691b983bfe05b1c60dd46f3c802b0cc3b3c800ea Date: Sat, 01 Feb 2020 02:16:31 GMT (also note this change only has snapd supporting ranges like this one, ie "bytes=<start>-"; everything else results in a 200). I haven't had time to write unit tests for this, other than for some of the store bits which are better covered than before (but need more). Notably the 200 vs 206 thing I describe below is NOT covered. I've tested it "live" using curl and httpie, and it's sensible and works, and can resume downloading a snap that's in the cache. Right now the store isn't ever replying with a 206 when I ask for a range, so I haven't been able to end-to-end that path, but it's not anything I'm doing wrong AFAICT; the store also doesn't 206 when curl or httpie talk to it directly. I think this is a bug in the store, but it's also correct behaviour AFAIK (from reading the RFC). The client will need to check whether a ranged request comes back with a 206 or a 200 to know whether it's actually resuming or not. NOTE the store itself (`downloadImpl`) doesn't support this behaviour; this needs fixing as currently it'll be downloading the whole thing twice as soon as it tries to resume once and gets the full content instead (the sha3 won't match). I tried to dive into fixing this but it's waaay too late.
2020-02-01 01:31:15 +00:00
}
}
return ss
}
func newSnapStream(snapName string, info *snap.Info, secret []byte) (*snapStream, error) {
dlInfo := &info.DownloadInfo
fname := filepath.Base(info.MountFile())
tokenJSON := downloadTokenJSON{
SnapName: snapName,
Filename: fname,
Info: dlInfo,
}
tokStr, err := sealDownloadToken(&tokenJSON, secret)
if err != nil {
return nil, err
}
return &snapStream{
SnapName: snapName,
Filename: fname,
Info: dlInfo,
Token: tokStr,
}, nil
}
func newResumingSnapStream(snapName string, tokStr string, secret []byte) (*snapStream, error) {
d, err := unsealDownloadToken(tokStr, secret)
if err != nil {
return nil, err
}
if d.SnapName != snapName {
return nil, fmt.Errorf("resume snap name does not match original snap name")
}
return &snapStream{
SnapName: snapName,
Filename: d.Filename,
Info: d.Info,
Token: tokStr,
}, nil
}
type downloadTokenJSON struct {
SnapName string `json:"snap-name"`
Filename string `json:"filename"`
Info *snap.DownloadInfo `json:"dl-info"`
}
func sealDownloadToken(d *downloadTokenJSON, secret []byte) (string, error) {
b, err := json.Marshal(d)
if err != nil {
return "", err
}
mac := hmac.New(sha256.New, secret)
mac.Write(b)
// append the HMAC hash to b to build the full raw token tok
tok := mac.Sum(b)
return base64.RawURLEncoding.EncodeToString(tok), nil
}
var errInvalidDownloadToken = errors.New("download token is invalid")
func unsealDownloadToken(tokStr string, secret []byte) (*downloadTokenJSON, error) {
tok, err := base64.RawURLEncoding.DecodeString(tokStr)
if err != nil {
return nil, errInvalidDownloadToken
}
sz := len(tok)
if sz < sha256.Size {
return nil, errInvalidDownloadToken
}
h := tok[sz-sha256.Size:]
b := tok[:sz-sha256.Size]
mac := hmac.New(sha256.New, secret)
mac.Write(b)
if !hmac.Equal(h, mac.Sum(nil)) {
return nil, errInvalidDownloadToken
}
var d downloadTokenJSON
if err := json.Unmarshal(b, &d); err != nil {
return nil, err
}
return &d, nil
}
func downloadTokensSecret(d *Daemon) (secret []byte, err error) {
st := d.overlord.State()
st.Lock()
defer st.Unlock()
const k = "api-download-tokens-secret"
err = st.Get(k, &secret)
if err == nil {
return secret, nil
}
if err != nil && !errors.Is(err, state.ErrNoState) {
return nil, err
}
secret, err = randutil.CryptoTokenBytes(32)
if err != nil {
return nil, err
}
st.Set(k, secret)
st.Set(k+"-time", time.Now().UTC())
return secret, nil
}