github.com/stolowski/snapd@v0.0.0-20210407085831-115137ce5a22/store/download_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2018 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_test 21 22 import ( 23 "bytes" 24 "context" 25 "crypto" 26 "errors" 27 "fmt" 28 "io" 29 "io/ioutil" 30 "net/http" 31 "net/http/httptest" 32 "os" 33 "path/filepath" 34 "time" 35 36 "github.com/juju/ratelimit" 37 . "gopkg.in/check.v1" 38 "gopkg.in/retry.v1" 39 40 "github.com/snapcore/snapd/dirs" 41 "github.com/snapcore/snapd/osutil" 42 "github.com/snapcore/snapd/overlord/auth" 43 "github.com/snapcore/snapd/progress" 44 "github.com/snapcore/snapd/release" 45 "github.com/snapcore/snapd/snap" 46 "github.com/snapcore/snapd/store" 47 "github.com/snapcore/snapd/testutil" 48 ) 49 50 type downloadSuite struct { 51 testutil.BaseTest 52 } 53 54 var _ = Suite(&downloadSuite{}) 55 56 func (s *downloadSuite) SetUpTest(c *C) { 57 s.BaseTest.SetUpTest(c) 58 59 store.MockDownloadRetryStrategy(&s.BaseTest, retry.LimitCount(5, retry.Exponential{ 60 Initial: time.Millisecond, 61 Factor: 2.5, 62 })) 63 64 mockXdelta := testutil.MockCommand(c, "xdelta3", "") 65 s.AddCleanup(mockXdelta.Restore) 66 } 67 68 func (s *downloadSuite) TestActualDownload(c *C) { 69 n := 0 70 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 71 c.Check(r.Header.Get("Snap-CDN"), Equals, "") 72 c.Check(r.Header.Get("Snap-Refresh-Reason"), Equals, "") 73 n++ 74 io.WriteString(w, "response-data") 75 })) 76 c.Assert(mockServer, NotNil) 77 defer mockServer.Close() 78 79 theStore := store.New(&store.Config{}, nil) 80 var buf SillyBuffer 81 // keep tests happy 82 sha3 := "" 83 err := store.Download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, &buf, 0, nil, nil) 84 c.Assert(err, IsNil) 85 c.Check(buf.String(), Equals, "response-data") 86 c.Check(n, Equals, 1) 87 } 88 89 func (s *downloadSuite) TestActualDownloadAutoRefresh(c *C) { 90 n := 0 91 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 92 c.Check(r.Header.Get("Snap-Refresh-Reason"), Equals, "scheduled") 93 n++ 94 io.WriteString(w, "response-data") 95 })) 96 c.Assert(mockServer, NotNil) 97 defer mockServer.Close() 98 99 theStore := store.New(&store.Config{}, nil) 100 var buf SillyBuffer 101 // keep tests happy 102 sha3 := "" 103 err := store.Download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, &buf, 0, nil, &store.DownloadOptions{IsAutoRefresh: true}) 104 c.Assert(err, IsNil) 105 c.Check(buf.String(), Equals, "response-data") 106 c.Check(n, Equals, 1) 107 } 108 109 func (s *downloadSuite) TestActualDownloadNoCDN(c *C) { 110 os.Setenv("SNAPPY_STORE_NO_CDN", "1") 111 defer os.Unsetenv("SNAPPY_STORE_NO_CDN") 112 113 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 114 c.Check(r.Header.Get("Snap-CDN"), Equals, "none") 115 io.WriteString(w, "response-data") 116 })) 117 c.Assert(mockServer, NotNil) 118 defer mockServer.Close() 119 120 theStore := store.New(&store.Config{}, nil) 121 var buf SillyBuffer 122 // keep tests happy 123 sha3 := "" 124 err := store.Download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, &buf, 0, nil, nil) 125 c.Assert(err, IsNil) 126 c.Check(buf.String(), Equals, "response-data") 127 } 128 129 func (s *downloadSuite) TestActualDownloadFullCloudInfoFromAuthContext(c *C) { 130 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 131 c.Check(r.Header.Get("Snap-CDN"), Equals, `cloud-name="aws" region="us-east-1" availability-zone="us-east-1c"`) 132 133 io.WriteString(w, "response-data") 134 })) 135 c.Assert(mockServer, NotNil) 136 defer mockServer.Close() 137 138 device := createTestDevice() 139 theStore := store.New(&store.Config{}, &testDauthContext{c: c, device: device, cloudInfo: &auth.CloudInfo{Name: "aws", Region: "us-east-1", AvailabilityZone: "us-east-1c"}}) 140 141 var buf SillyBuffer 142 // keep tests happy 143 sha3 := "" 144 err := store.Download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, &buf, 0, nil, nil) 145 c.Assert(err, IsNil) 146 c.Check(buf.String(), Equals, "response-data") 147 } 148 149 func (s *downloadSuite) TestActualDownloadLessDetailedCloudInfoFromAuthContext(c *C) { 150 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 151 c.Check(r.Header.Get("Snap-CDN"), Equals, `cloud-name="openstack" availability-zone="nova"`) 152 153 io.WriteString(w, "response-data") 154 })) 155 c.Assert(mockServer, NotNil) 156 defer mockServer.Close() 157 158 device := createTestDevice() 159 theStore := store.New(&store.Config{}, &testDauthContext{c: c, device: device, cloudInfo: &auth.CloudInfo{Name: "openstack", Region: "", AvailabilityZone: "nova"}}) 160 161 var buf SillyBuffer 162 // keep tests happy 163 sha3 := "" 164 err := store.Download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, &buf, 0, nil, nil) 165 c.Assert(err, IsNil) 166 c.Check(buf.String(), Equals, "response-data") 167 } 168 169 func (s *downloadSuite) TestDownloadCancellation(c *C) { 170 // the channel used by mock server to request cancellation from the test 171 syncCh := make(chan struct{}) 172 173 n := 0 174 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 175 n++ 176 io.WriteString(w, "foo") 177 syncCh <- struct{}{} 178 io.WriteString(w, "bar") 179 time.Sleep(10 * time.Millisecond) 180 })) 181 c.Assert(mockServer, NotNil) 182 defer mockServer.Close() 183 184 theStore := store.New(&store.Config{}, nil) 185 186 ctx, cancel := context.WithCancel(context.Background()) 187 188 result := make(chan string) 189 go func() { 190 sha3 := "" 191 var buf SillyBuffer 192 err := store.Download(ctx, "foo", sha3, mockServer.URL, nil, theStore, &buf, 0, nil, nil) 193 result <- err.Error() 194 close(result) 195 }() 196 197 <-syncCh 198 cancel() 199 200 err := <-result 201 c.Check(n, Equals, 1) 202 c.Assert(err, Equals, "The download has been cancelled: context canceled") 203 } 204 205 type nopeSeeker struct{ io.ReadWriter } 206 207 func (nopeSeeker) Seek(int64, int) (int64, error) { 208 return -1, errors.New("what is this, quidditch?") 209 } 210 211 func (s *downloadSuite) TestActualDownloadNonPurchased402(c *C) { 212 n := 0 213 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 214 n++ 215 // XXX: the server doesn't behave correctly ATM 216 // but 401 for paid snaps is the unlikely case so far 217 w.WriteHeader(402) 218 })) 219 c.Assert(mockServer, NotNil) 220 defer mockServer.Close() 221 222 theStore := store.New(&store.Config{}, nil) 223 var buf bytes.Buffer 224 err := store.Download(context.TODO(), "foo", "sha3", mockServer.URL, nil, theStore, nopeSeeker{&buf}, -1, nil, nil) 225 c.Assert(err, NotNil) 226 c.Check(err.Error(), Equals, "please buy foo before installing it.") 227 c.Check(n, Equals, 1) 228 } 229 230 func (s *downloadSuite) TestActualDownload404(c *C) { 231 n := 0 232 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 233 n++ 234 w.WriteHeader(404) 235 })) 236 c.Assert(mockServer, NotNil) 237 defer mockServer.Close() 238 239 theStore := store.New(&store.Config{}, nil) 240 var buf SillyBuffer 241 err := store.Download(context.TODO(), "foo", "sha3", mockServer.URL, nil, theStore, &buf, 0, nil, nil) 242 c.Assert(err, NotNil) 243 c.Assert(err, FitsTypeOf, &store.DownloadError{}) 244 c.Check(err.(*store.DownloadError).Code, Equals, 404) 245 c.Check(n, Equals, 1) 246 } 247 248 func (s *downloadSuite) TestActualDownload500(c *C) { 249 n := 0 250 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 251 n++ 252 w.WriteHeader(500) 253 })) 254 c.Assert(mockServer, NotNil) 255 defer mockServer.Close() 256 257 theStore := store.New(&store.Config{}, nil) 258 var buf SillyBuffer 259 err := store.Download(context.TODO(), "foo", "sha3", mockServer.URL, nil, theStore, &buf, 0, nil, nil) 260 c.Assert(err, NotNil) 261 c.Assert(err, FitsTypeOf, &store.DownloadError{}) 262 c.Check(err.(*store.DownloadError).Code, Equals, 500) 263 c.Check(n, Equals, 5) 264 } 265 266 func (s *downloadSuite) TestActualDownload500Once(c *C) { 267 n := 0 268 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 269 n++ 270 if n == 1 { 271 w.WriteHeader(500) 272 } else { 273 io.WriteString(w, "response-data") 274 } 275 })) 276 c.Assert(mockServer, NotNil) 277 defer mockServer.Close() 278 279 theStore := store.New(&store.Config{}, nil) 280 var buf SillyBuffer 281 // keep tests happy 282 sha3 := "" 283 err := store.Download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, &buf, 0, nil, nil) 284 c.Assert(err, IsNil) 285 c.Check(buf.String(), Equals, "response-data") 286 c.Check(n, Equals, 2) 287 } 288 289 // SillyBuffer is a ReadWriteSeeker buffer with a limited size for the tests 290 // (bytes does not implement an ReadWriteSeeker) 291 type SillyBuffer struct { 292 buf [1024]byte 293 pos int64 294 end int64 295 } 296 297 func NewSillyBufferString(s string) *SillyBuffer { 298 sb := &SillyBuffer{ 299 pos: int64(len(s)), 300 end: int64(len(s)), 301 } 302 copy(sb.buf[0:], []byte(s)) 303 return sb 304 } 305 func (sb *SillyBuffer) Read(b []byte) (n int, err error) { 306 if sb.pos >= int64(sb.end) { 307 return 0, io.EOF 308 } 309 n = copy(b, sb.buf[sb.pos:sb.end]) 310 sb.pos += int64(n) 311 return n, nil 312 } 313 func (sb *SillyBuffer) Seek(offset int64, whence int) (int64, error) { 314 if whence != 0 { 315 panic("only io.SeekStart implemented in SillyBuffer") 316 } 317 if offset < 0 || offset > int64(sb.end) { 318 return 0, fmt.Errorf("seek out of bounds: %d", offset) 319 } 320 sb.pos = offset 321 return sb.pos, nil 322 } 323 func (sb *SillyBuffer) Write(p []byte) (n int, err error) { 324 n = copy(sb.buf[sb.pos:], p) 325 sb.pos += int64(n) 326 if sb.pos > sb.end { 327 sb.end = sb.pos 328 } 329 return n, nil 330 } 331 func (sb *SillyBuffer) String() string { 332 return string(sb.buf[0:sb.pos]) 333 } 334 335 func (s *downloadSuite) TestActualDownloadResume(c *C) { 336 n := 0 337 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 338 n++ 339 w.WriteHeader(206) 340 io.WriteString(w, "data") 341 })) 342 c.Assert(mockServer, NotNil) 343 defer mockServer.Close() 344 345 theStore := store.New(&store.Config{}, nil) 346 buf := NewSillyBufferString("some ") 347 // calc the expected hash 348 h := crypto.SHA3_384.New() 349 h.Write([]byte("some data")) 350 sha3 := fmt.Sprintf("%x", h.Sum(nil)) 351 err := store.Download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, buf, int64(len("some ")), nil, nil) 352 c.Check(err, IsNil) 353 c.Check(buf.String(), Equals, "some data") 354 c.Check(n, Equals, 1) 355 } 356 357 func (s *downloadSuite) TestActualDownloadServerNoResumeHandeled(c *C) { 358 n := 0 359 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 360 n++ 361 362 switch n { 363 case 1: 364 c.Check(r.Header["Range"], HasLen, 1) 365 default: 366 c.Fatal("only one request expected") 367 } 368 // server does not do partial content and sends full data instead 369 w.WriteHeader(200) 370 io.WriteString(w, "some data") 371 })) 372 c.Assert(mockServer, NotNil) 373 defer mockServer.Close() 374 375 theStore := store.New(&store.Config{}, nil) 376 buf := NewSillyBufferString("some ") 377 // calc the expected hash 378 h := crypto.SHA3_384.New() 379 h.Write([]byte("some data")) 380 sha3 := fmt.Sprintf("%x", h.Sum(nil)) 381 err := store.Download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, buf, int64(len("some ")), nil, nil) 382 c.Check(err, IsNil) 383 c.Check(buf.String(), Equals, "some data") 384 c.Check(n, Equals, 1) 385 } 386 387 func (s *downloadSuite) TestUseDeltas(c *C) { 388 origPath := os.Getenv("PATH") 389 defer os.Setenv("PATH", origPath) 390 origUseDeltas := os.Getenv("SNAPD_USE_DELTAS_EXPERIMENTAL") 391 defer os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", origUseDeltas) 392 restore := release.MockOnClassic(false) 393 defer restore() 394 altPath := c.MkDir() 395 origSnapMountDir := dirs.SnapMountDir 396 defer func() { dirs.SnapMountDir = origSnapMountDir }() 397 dirs.SnapMountDir = c.MkDir() 398 exeInCorePath := filepath.Join(dirs.SnapMountDir, "/core/current/usr/bin/xdelta3") 399 os.MkdirAll(filepath.Dir(exeInCorePath), 0755) 400 401 scenarios := []struct { 402 env string 403 classic bool 404 exeInHost bool 405 exeInCore bool 406 407 wantDelta bool 408 }{ 409 {env: "", classic: false, exeInHost: false, exeInCore: false, wantDelta: false}, 410 {env: "", classic: false, exeInHost: false, exeInCore: true, wantDelta: true}, 411 {env: "", classic: false, exeInHost: true, exeInCore: false, wantDelta: true}, 412 {env: "", classic: false, exeInHost: true, exeInCore: true, wantDelta: true}, 413 {env: "", classic: true, exeInHost: false, exeInCore: false, wantDelta: false}, 414 {env: "", classic: true, exeInHost: false, exeInCore: true, wantDelta: true}, 415 {env: "", classic: true, exeInHost: true, exeInCore: false, wantDelta: true}, 416 {env: "", classic: true, exeInHost: true, exeInCore: true, wantDelta: true}, 417 418 {env: "0", classic: false, exeInHost: false, exeInCore: false, wantDelta: false}, 419 {env: "0", classic: false, exeInHost: false, exeInCore: true, wantDelta: false}, 420 {env: "0", classic: false, exeInHost: true, exeInCore: false, wantDelta: false}, 421 {env: "0", classic: false, exeInHost: true, exeInCore: true, wantDelta: false}, 422 {env: "0", classic: true, exeInHost: false, exeInCore: false, wantDelta: false}, 423 {env: "0", classic: true, exeInHost: false, exeInCore: true, wantDelta: false}, 424 {env: "0", classic: true, exeInHost: true, exeInCore: false, wantDelta: false}, 425 {env: "0", classic: true, exeInHost: true, exeInCore: true, wantDelta: false}, 426 427 {env: "1", classic: false, exeInHost: false, exeInCore: false, wantDelta: false}, 428 {env: "1", classic: false, exeInHost: false, exeInCore: true, wantDelta: true}, 429 {env: "1", classic: false, exeInHost: true, exeInCore: false, wantDelta: true}, 430 {env: "1", classic: false, exeInHost: true, exeInCore: true, wantDelta: true}, 431 {env: "1", classic: true, exeInHost: false, exeInCore: false, wantDelta: false}, 432 {env: "1", classic: true, exeInHost: false, exeInCore: true, wantDelta: true}, 433 {env: "1", classic: true, exeInHost: true, exeInCore: false, wantDelta: true}, 434 {env: "1", classic: true, exeInHost: true, exeInCore: true, wantDelta: true}, 435 } 436 437 for _, scenario := range scenarios { 438 if scenario.exeInCore { 439 osutil.CopyFile("/bin/true", exeInCorePath, 0) 440 } else { 441 os.Remove(exeInCorePath) 442 } 443 os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", scenario.env) 444 release.MockOnClassic(scenario.classic) 445 if scenario.exeInHost { 446 os.Setenv("PATH", origPath) 447 } else { 448 os.Setenv("PATH", altPath) 449 } 450 451 c.Check(store.UseDeltas(), Equals, scenario.wantDelta, Commentf("%#v", scenario)) 452 } 453 } 454 455 type downloadBehaviour []struct { 456 url string 457 error bool 458 } 459 460 var deltaTests = []struct { 461 downloads downloadBehaviour 462 info snap.DownloadInfo 463 expectedContent string 464 }{{ 465 // The full snap is not downloaded, but rather the delta 466 // is downloaded and applied. 467 downloads: downloadBehaviour{ 468 {url: "delta-url"}, 469 }, 470 info: snap.DownloadInfo{ 471 AnonDownloadURL: "full-snap-url", 472 Deltas: []snap.DeltaInfo{ 473 {AnonDownloadURL: "delta-url", Format: "xdelta3"}, 474 }, 475 }, 476 expectedContent: "snap-content-via-delta", 477 }, { 478 // If there is an error during the delta download, the 479 // full snap is downloaded as per normal. 480 downloads: downloadBehaviour{ 481 {error: true}, 482 {url: "full-snap-url"}, 483 }, 484 info: snap.DownloadInfo{ 485 AnonDownloadURL: "full-snap-url", 486 Deltas: []snap.DeltaInfo{ 487 {AnonDownloadURL: "delta-url", Format: "xdelta3"}, 488 }, 489 }, 490 expectedContent: "full-snap-url-content", 491 }, { 492 // If more than one matching delta is returned by the store 493 // we ignore deltas and do the full download. 494 downloads: downloadBehaviour{ 495 {url: "full-snap-url"}, 496 }, 497 info: snap.DownloadInfo{ 498 AnonDownloadURL: "full-snap-url", 499 Deltas: []snap.DeltaInfo{ 500 {AnonDownloadURL: "delta-url", Format: "xdelta3"}, 501 {AnonDownloadURL: "delta-url-2", Format: "xdelta3"}, 502 }, 503 }, 504 expectedContent: "full-snap-url-content", 505 }} 506 507 func (s *downloadSuite) TestDownloadWithDelta(c *C) { 508 origUseDeltas := os.Getenv("SNAPD_USE_DELTAS_EXPERIMENTAL") 509 defer os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", origUseDeltas) 510 c.Assert(os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", "1"), IsNil) 511 512 for _, testCase := range deltaTests { 513 testCase.info.Size = int64(len(testCase.expectedContent)) 514 downloadIndex := 0 515 restore := store.MockDownload(func(ctx context.Context, name, sha3, url string, user *auth.UserState, s *store.Store, w io.ReadWriteSeeker, resume int64, pbar progress.Meter, dlOpts *store.DownloadOptions) error { 516 if testCase.downloads[downloadIndex].error { 517 downloadIndex++ 518 return errors.New("Bang") 519 } 520 c.Check(url, Equals, testCase.downloads[downloadIndex].url) 521 w.Write([]byte(testCase.downloads[downloadIndex].url + "-content")) 522 downloadIndex++ 523 return nil 524 }) 525 defer restore() 526 restore = store.MockApplyDelta(func(name string, deltaPath string, deltaInfo *snap.DeltaInfo, targetPath string, targetSha3_384 string) error { 527 c.Check(deltaInfo, Equals, &testCase.info.Deltas[0]) 528 err := ioutil.WriteFile(targetPath, []byte("snap-content-via-delta"), 0644) 529 c.Assert(err, IsNil) 530 return nil 531 }) 532 defer restore() 533 534 theStore := store.New(&store.Config{}, nil) 535 path := filepath.Join(c.MkDir(), "subdir", "downloaded-file") 536 err := theStore.Download(context.TODO(), "foo", path, &testCase.info, nil, nil, nil) 537 538 c.Assert(err, IsNil) 539 defer os.Remove(path) 540 c.Assert(path, testutil.FileEquals, testCase.expectedContent) 541 } 542 } 543 544 func (s *downloadSuite) TestActualDownloadRateLimited(c *C) { 545 var ratelimitReaderUsed bool 546 restore := store.MockRatelimitReader(func(r io.Reader, bucket *ratelimit.Bucket) io.Reader { 547 ratelimitReaderUsed = true 548 return r 549 }) 550 defer restore() 551 552 canary := "downloaded data" 553 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 554 fmt.Fprint(w, canary) 555 })) 556 defer ts.Close() 557 558 theStore := store.New(&store.Config{}, nil) 559 var buf SillyBuffer 560 err := store.Download(context.TODO(), "example-name", "", ts.URL, nil, theStore, &buf, 0, nil, &store.DownloadOptions{RateLimit: 1}) 561 c.Assert(err, IsNil) 562 c.Check(buf.String(), Equals, canary) 563 c.Check(ratelimitReaderUsed, Equals, true) 564 }