github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/store/store_download_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2014-2020 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package store_test 21 22 import ( 23 "bytes" 24 "context" 25 "crypto" 26 "fmt" 27 "io" 28 "io/ioutil" 29 "net/http" 30 "net/http/httptest" 31 "net/url" 32 "os" 33 "path/filepath" 34 "time" 35 36 "golang.org/x/crypto/sha3" 37 . "gopkg.in/check.v1" 38 "gopkg.in/retry.v1" 39 40 "github.com/snapcore/snapd/dirs" 41 "github.com/snapcore/snapd/httputil" 42 "github.com/snapcore/snapd/osutil" 43 "github.com/snapcore/snapd/overlord/auth" 44 "github.com/snapcore/snapd/progress" 45 "github.com/snapcore/snapd/snap" 46 "github.com/snapcore/snapd/store" 47 "github.com/snapcore/snapd/testutil" 48 ) 49 50 type storeDownloadSuite struct { 51 baseStoreSuite 52 53 store *store.Store 54 55 localUser *auth.UserState 56 57 mockXDelta *testutil.MockCmd 58 } 59 60 var _ = Suite(&storeDownloadSuite{}) 61 62 func (s *storeDownloadSuite) SetUpTest(c *C) { 63 s.baseStoreSuite.SetUpTest(c) 64 65 c.Assert(os.MkdirAll(dirs.SnapMountDir, 0755), IsNil) 66 67 s.store = store.New(nil, nil) 68 69 s.localUser = &auth.UserState{ 70 ID: 11, 71 Username: "test-user", 72 Macaroon: "snapd-macaroon", 73 } 74 75 s.mockXDelta = testutil.MockCommand(c, "xdelta3", "") 76 s.AddCleanup(s.mockXDelta.Restore) 77 78 store.MockDownloadRetryStrategy(&s.BaseTest, retry.LimitCount(5, retry.LimitTime(1*time.Second, 79 retry.Exponential{ 80 Initial: 1 * time.Millisecond, 81 Factor: 1, 82 }, 83 ))) 84 } 85 86 func (s *storeDownloadSuite) TestDownloadOK(c *C) { 87 expectedContent := []byte("I was downloaded") 88 89 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 { 90 c.Check(url, Equals, "anon-url") 91 w.Write(expectedContent) 92 return nil 93 }) 94 defer restore() 95 96 snap := &snap.Info{} 97 snap.RealName = "foo" 98 snap.AnonDownloadURL = "anon-url" 99 snap.DownloadURL = "AUTH-URL" 100 snap.Size = int64(len(expectedContent)) 101 102 path := filepath.Join(c.MkDir(), "downloaded-file") 103 err := s.store.Download(s.ctx, "foo", path, &snap.DownloadInfo, nil, nil, nil) 104 c.Assert(err, IsNil) 105 defer os.Remove(path) 106 107 c.Assert(path, testutil.FileEquals, expectedContent) 108 } 109 110 func (s *storeDownloadSuite) TestDownloadRangeRequest(c *C) { 111 partialContentStr := "partial content " 112 missingContentStr := "was downloaded" 113 expectedContentStr := partialContentStr + missingContentStr 114 115 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 { 116 c.Check(resume, Equals, int64(len(partialContentStr))) 117 c.Check(url, Equals, "anon-url") 118 w.Write([]byte(missingContentStr)) 119 return nil 120 }) 121 defer restore() 122 123 snap := &snap.Info{} 124 snap.RealName = "foo" 125 snap.AnonDownloadURL = "anon-url" 126 snap.DownloadURL = "AUTH-URL" 127 snap.Sha3_384 = "abcdabcd" 128 snap.Size = int64(len(expectedContentStr)) 129 130 targetFn := filepath.Join(c.MkDir(), "foo_1.0_all.snap") 131 err := ioutil.WriteFile(targetFn+".partial", []byte(partialContentStr), 0644) 132 c.Assert(err, IsNil) 133 134 err = s.store.Download(s.ctx, "foo", targetFn, &snap.DownloadInfo, nil, nil, nil) 135 c.Assert(err, IsNil) 136 137 c.Assert(targetFn, testutil.FileEquals, expectedContentStr) 138 } 139 140 func (s *storeDownloadSuite) TestResumeOfCompleted(c *C) { 141 expectedContentStr := "nothing downloaded" 142 143 snap := &snap.Info{} 144 snap.RealName = "foo" 145 snap.AnonDownloadURL = "anon-url" 146 snap.DownloadURL = "AUTH-URL" 147 snap.Sha3_384 = fmt.Sprintf("%x", sha3.Sum384([]byte(expectedContentStr))) 148 snap.Size = int64(len(expectedContentStr)) 149 150 targetFn := filepath.Join(c.MkDir(), "foo_1.0_all.snap") 151 err := ioutil.WriteFile(targetFn+".partial", []byte(expectedContentStr), 0644) 152 c.Assert(err, IsNil) 153 154 err = s.store.Download(s.ctx, "foo", targetFn, &snap.DownloadInfo, nil, nil, nil) 155 c.Assert(err, IsNil) 156 157 c.Assert(targetFn, testutil.FileEquals, expectedContentStr) 158 } 159 160 func (s *storeDownloadSuite) TestDownloadEOFHandlesResumeHashCorrectly(c *C) { 161 n := 0 162 var mockServer *httptest.Server 163 164 // our mock download content 165 buf := make([]byte, 50000) 166 for i := range buf { 167 buf[i] = 'x' 168 } 169 h := crypto.SHA3_384.New() 170 io.Copy(h, bytes.NewBuffer(buf)) 171 172 // raise an EOF shortly before the end 173 mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 174 n++ 175 if n < 2 { 176 w.Header().Add("Content-Length", fmt.Sprintf("%d", len(buf))) 177 w.Write(buf[0 : len(buf)-5]) 178 mockServer.CloseClientConnections() 179 return 180 } 181 if len(r.Header["Range"]) > 0 { 182 w.WriteHeader(206) 183 } 184 w.Write(buf[len(buf)-5:]) 185 })) 186 187 c.Assert(mockServer, NotNil) 188 defer mockServer.Close() 189 190 snap := &snap.Info{} 191 snap.RealName = "foo" 192 snap.AnonDownloadURL = mockServer.URL 193 snap.DownloadURL = "AUTH-URL" 194 snap.Sha3_384 = fmt.Sprintf("%x", h.Sum(nil)) 195 snap.Size = 50000 196 197 targetFn := filepath.Join(c.MkDir(), "foo_1.0_all.snap") 198 err := s.store.Download(s.ctx, "foo", targetFn, &snap.DownloadInfo, nil, nil, nil) 199 c.Assert(err, IsNil) 200 c.Assert(targetFn, testutil.FileEquals, buf) 201 c.Assert(s.logbuf.String(), Matches, "(?s).*Retrying .* attempt 2, .*") 202 } 203 204 func (s *storeDownloadSuite) TestDownloadRetryHashErrorIsFullyRetried(c *C) { 205 n := 0 206 var mockServer *httptest.Server 207 208 // our mock download content 209 buf := make([]byte, 50000) 210 for i := range buf { 211 buf[i] = 'x' 212 } 213 h := crypto.SHA3_384.New() 214 io.Copy(h, bytes.NewBuffer(buf)) 215 216 // raise an EOF shortly before the end and send the WRONG content next 217 mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 218 n++ 219 switch n { 220 case 1: 221 w.Header().Add("Content-Length", fmt.Sprintf("%d", len(buf))) 222 w.Write(buf[0 : len(buf)-5]) 223 mockServer.CloseClientConnections() 224 case 2: 225 io.WriteString(w, "yyyyy") 226 case 3: 227 w.Write(buf) 228 } 229 })) 230 231 c.Assert(mockServer, NotNil) 232 defer mockServer.Close() 233 234 snap := &snap.Info{} 235 snap.RealName = "foo" 236 snap.AnonDownloadURL = mockServer.URL 237 snap.DownloadURL = "AUTH-URL" 238 snap.Sha3_384 = fmt.Sprintf("%x", h.Sum(nil)) 239 snap.Size = 50000 240 241 targetFn := filepath.Join(c.MkDir(), "foo_1.0_all.snap") 242 err := s.store.Download(s.ctx, "foo", targetFn, &snap.DownloadInfo, nil, nil, nil) 243 c.Assert(err, IsNil) 244 245 c.Assert(targetFn, testutil.FileEquals, buf) 246 247 c.Assert(s.logbuf.String(), Matches, "(?s).*Retrying .* attempt 2, .*") 248 } 249 250 func (s *storeDownloadSuite) TestResumeOfCompletedRetriedOnHashFailure(c *C) { 251 var mockServer *httptest.Server 252 253 // our mock download content 254 buf := make([]byte, 50000) 255 badbuf := make([]byte, 50000) 256 for i := range buf { 257 buf[i] = 'x' 258 badbuf[i] = 'y' 259 } 260 h := crypto.SHA3_384.New() 261 io.Copy(h, bytes.NewBuffer(buf)) 262 263 mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 264 w.Write(buf) 265 })) 266 267 c.Assert(mockServer, NotNil) 268 defer mockServer.Close() 269 270 snap := &snap.Info{} 271 snap.RealName = "foo" 272 snap.AnonDownloadURL = mockServer.URL 273 snap.DownloadURL = "AUTH-URL" 274 snap.Sha3_384 = fmt.Sprintf("%x", h.Sum(nil)) 275 snap.Size = 50000 276 277 targetFn := filepath.Join(c.MkDir(), "foo_1.0_all.snap") 278 c.Assert(ioutil.WriteFile(targetFn+".partial", badbuf, 0644), IsNil) 279 err := s.store.Download(s.ctx, "foo", targetFn, &snap.DownloadInfo, nil, nil, nil) 280 c.Assert(err, IsNil) 281 282 c.Assert(targetFn, testutil.FileEquals, buf) 283 284 c.Assert(s.logbuf.String(), Matches, "(?s).*sha3-384 mismatch.*") 285 } 286 287 func (s *storeDownloadSuite) TestResumeOfTooMuchDataWorks(c *C) { 288 var mockServer *httptest.Server 289 290 // our mock download content 291 snapContent := "snap-content" 292 // the partial file has too much data 293 tooMuchLocalData := "way-way-way-too-much-snap-content" 294 295 h := crypto.SHA3_384.New() 296 io.Copy(h, bytes.NewBufferString(snapContent)) 297 298 n := 0 299 mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 300 n++ 301 w.Write([]byte(snapContent)) 302 })) 303 c.Assert(mockServer, NotNil) 304 defer mockServer.Close() 305 306 snap := &snap.Info{} 307 snap.RealName = "foo" 308 snap.AnonDownloadURL = mockServer.URL 309 snap.DownloadURL = "AUTH-URL" 310 snap.Sha3_384 = fmt.Sprintf("%x", h.Sum(nil)) 311 snap.Size = int64(len(snapContent)) 312 313 targetFn := filepath.Join(c.MkDir(), "foo_1.0_all.snap") 314 c.Assert(ioutil.WriteFile(targetFn+".partial", []byte(tooMuchLocalData), 0644), IsNil) 315 err := s.store.Download(s.ctx, "foo", targetFn, &snap.DownloadInfo, nil, nil, nil) 316 c.Assert(err, IsNil) 317 c.Assert(n, Equals, 1) 318 319 c.Assert(targetFn, testutil.FileEquals, snapContent) 320 321 c.Assert(s.logbuf.String(), Matches, "(?s).*sha3-384 mismatch.*") 322 } 323 324 func (s *storeDownloadSuite) TestDownloadRetryHashErrorIsFullyRetriedOnlyOnce(c *C) { 325 n := 0 326 var mockServer *httptest.Server 327 328 mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 329 n++ 330 io.WriteString(w, "something invalid") 331 })) 332 333 c.Assert(mockServer, NotNil) 334 defer mockServer.Close() 335 336 snap := &snap.Info{} 337 snap.RealName = "foo" 338 snap.AnonDownloadURL = mockServer.URL 339 snap.DownloadURL = "AUTH-URL" 340 snap.Sha3_384 = "invalid-hash" 341 snap.Size = int64(len("something invalid")) 342 343 targetFn := filepath.Join(c.MkDir(), "foo_1.0_all.snap") 344 err := s.store.Download(s.ctx, "foo", targetFn, &snap.DownloadInfo, nil, nil, nil) 345 346 _, ok := err.(store.HashError) 347 c.Assert(ok, Equals, true) 348 // ensure we only retried once (as these downloads might be big) 349 c.Assert(n, Equals, 2) 350 } 351 352 func (s *storeDownloadSuite) TestDownloadRangeRequestRetryOnHashError(c *C) { 353 expectedContentStr := "file was downloaded from scratch" 354 partialContentStr := "partial content " 355 356 n := 0 357 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 { 358 n++ 359 if n == 1 { 360 // force sha3 error on first download 361 c.Check(resume, Equals, int64(len(partialContentStr))) 362 return store.NewHashError("foo", "1234", "5678") 363 } 364 w.Write([]byte(expectedContentStr)) 365 return nil 366 }) 367 defer restore() 368 369 snap := &snap.Info{} 370 snap.RealName = "foo" 371 snap.AnonDownloadURL = "anon-url" 372 snap.DownloadURL = "AUTH-URL" 373 snap.Sha3_384 = "" 374 snap.Size = int64(len(expectedContentStr)) 375 376 targetFn := filepath.Join(c.MkDir(), "foo_1.0_all.snap") 377 err := ioutil.WriteFile(targetFn+".partial", []byte(partialContentStr), 0644) 378 c.Assert(err, IsNil) 379 380 err = s.store.Download(s.ctx, "foo", targetFn, &snap.DownloadInfo, nil, nil, nil) 381 c.Assert(err, IsNil) 382 c.Assert(n, Equals, 2) 383 384 c.Assert(targetFn, testutil.FileEquals, expectedContentStr) 385 } 386 387 func (s *storeDownloadSuite) TestDownloadRangeRequestFailOnHashError(c *C) { 388 partialContentStr := "partial content " 389 390 n := 0 391 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 { 392 n++ 393 return store.NewHashError("foo", "1234", "5678") 394 }) 395 defer restore() 396 397 snap := &snap.Info{} 398 snap.RealName = "foo" 399 snap.AnonDownloadURL = "anon-url" 400 snap.DownloadURL = "AUTH-URL" 401 snap.Sha3_384 = "" 402 snap.Size = int64(len(partialContentStr) + 1) 403 404 targetFn := filepath.Join(c.MkDir(), "foo_1.0_all.snap") 405 err := ioutil.WriteFile(targetFn+".partial", []byte(partialContentStr), 0644) 406 c.Assert(err, IsNil) 407 408 err = s.store.Download(s.ctx, "foo", targetFn, &snap.DownloadInfo, nil, nil, nil) 409 c.Assert(err, NotNil) 410 c.Assert(err, ErrorMatches, `sha3-384 mismatch for "foo": got 1234 but expected 5678`) 411 c.Assert(n, Equals, 2) 412 } 413 414 func (s *storeDownloadSuite) TestAuthenticatedDownloadDoesNotUseAnonURL(c *C) { 415 expectedContent := []byte("I was downloaded") 416 restore := store.MockDownload(func(ctx context.Context, name, sha3, url string, user *auth.UserState, _ *store.Store, w io.ReadWriteSeeker, resume int64, pbar progress.Meter, dlOpts *store.DownloadOptions) error { 417 // check user is pass and auth url is used 418 c.Check(user, Equals, s.user) 419 c.Check(url, Equals, "AUTH-URL") 420 421 w.Write(expectedContent) 422 return nil 423 }) 424 defer restore() 425 426 snap := &snap.Info{} 427 snap.RealName = "foo" 428 snap.AnonDownloadURL = "anon-url" 429 snap.DownloadURL = "AUTH-URL" 430 snap.Size = int64(len(expectedContent)) 431 432 path := filepath.Join(c.MkDir(), "downloaded-file") 433 err := s.store.Download(s.ctx, "foo", path, &snap.DownloadInfo, nil, s.user, nil) 434 c.Assert(err, IsNil) 435 defer os.Remove(path) 436 437 c.Assert(path, testutil.FileEquals, expectedContent) 438 } 439 440 func (s *storeDownloadSuite) TestAuthenticatedDeviceDoesNotUseAnonURL(c *C) { 441 expectedContent := []byte("I was downloaded") 442 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 { 443 // check auth url is used 444 c.Check(url, Equals, "AUTH-URL") 445 446 w.Write(expectedContent) 447 return nil 448 }) 449 defer restore() 450 451 snap := &snap.Info{} 452 snap.RealName = "foo" 453 snap.AnonDownloadURL = "anon-url" 454 snap.DownloadURL = "AUTH-URL" 455 snap.Size = int64(len(expectedContent)) 456 457 dauthCtx := &testDauthContext{c: c, device: s.device} 458 sto := store.New(&store.Config{}, dauthCtx) 459 460 path := filepath.Join(c.MkDir(), "downloaded-file") 461 err := sto.Download(s.ctx, "foo", path, &snap.DownloadInfo, nil, nil, nil) 462 c.Assert(err, IsNil) 463 defer os.Remove(path) 464 465 c.Assert(path, testutil.FileEquals, expectedContent) 466 } 467 468 func (s *storeDownloadSuite) TestLocalUserDownloadUsesAnonURL(c *C) { 469 expectedContentStr := "I was downloaded" 470 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 { 471 c.Check(url, Equals, "anon-url") 472 473 w.Write([]byte(expectedContentStr)) 474 return nil 475 }) 476 defer restore() 477 478 snap := &snap.Info{} 479 snap.RealName = "foo" 480 snap.AnonDownloadURL = "anon-url" 481 snap.DownloadURL = "AUTH-URL" 482 snap.Size = int64(len(expectedContentStr)) 483 484 path := filepath.Join(c.MkDir(), "downloaded-file") 485 err := s.store.Download(s.ctx, "foo", path, &snap.DownloadInfo, nil, s.localUser, nil) 486 c.Assert(err, IsNil) 487 defer os.Remove(path) 488 489 c.Assert(path, testutil.FileEquals, expectedContentStr) 490 } 491 492 func (s *storeDownloadSuite) TestDownloadFails(c *C) { 493 var tmpfile *os.File 494 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 { 495 tmpfile = w.(*os.File) 496 return fmt.Errorf("uh, it failed") 497 }) 498 defer restore() 499 500 snap := &snap.Info{} 501 snap.RealName = "foo" 502 snap.AnonDownloadURL = "anon-url" 503 snap.DownloadURL = "AUTH-URL" 504 snap.Size = 1 505 // simulate a failed download 506 path := filepath.Join(c.MkDir(), "downloaded-file") 507 err := s.store.Download(s.ctx, "foo", path, &snap.DownloadInfo, nil, nil, nil) 508 c.Assert(err, ErrorMatches, "uh, it failed") 509 // ... and ensure that the tempfile is removed 510 c.Assert(osutil.FileExists(tmpfile.Name()), Equals, false) 511 // ... and not because it succeeded either 512 c.Assert(osutil.FileExists(path), Equals, false) 513 } 514 515 func (s *storeDownloadSuite) TestDownloadFailsLeavePartial(c *C) { 516 var tmpfile *os.File 517 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 { 518 tmpfile = w.(*os.File) 519 w.Write([]byte{'X'}) // so it's not empty 520 return fmt.Errorf("uh, it failed") 521 }) 522 defer restore() 523 524 snap := &snap.Info{} 525 snap.RealName = "foo" 526 snap.AnonDownloadURL = "anon-url" 527 snap.DownloadURL = "AUTH-URL" 528 snap.Size = 1 529 // simulate a failed download 530 path := filepath.Join(c.MkDir(), "downloaded-file") 531 err := s.store.Download(s.ctx, "foo", path, &snap.DownloadInfo, nil, nil, &store.DownloadOptions{LeavePartialOnError: true}) 532 c.Assert(err, ErrorMatches, "uh, it failed") 533 // ... and ensure that the tempfile is *NOT* removed 534 c.Assert(osutil.FileExists(tmpfile.Name()), Equals, true) 535 // ... but the target path isn't there 536 c.Assert(osutil.FileExists(path), Equals, false) 537 } 538 539 func (s *storeDownloadSuite) TestDownloadFailsDoesNotLeavePartialIfEmpty(c *C) { 540 var tmpfile *os.File 541 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 { 542 tmpfile = w.(*os.File) 543 // no write, so the partial is empty 544 return fmt.Errorf("uh, it failed") 545 }) 546 defer restore() 547 548 snap := &snap.Info{} 549 snap.RealName = "foo" 550 snap.AnonDownloadURL = "anon-url" 551 snap.DownloadURL = "AUTH-URL" 552 snap.Size = 1 553 // simulate a failed download 554 path := filepath.Join(c.MkDir(), "downloaded-file") 555 err := s.store.Download(s.ctx, "foo", path, &snap.DownloadInfo, nil, nil, &store.DownloadOptions{LeavePartialOnError: true}) 556 c.Assert(err, ErrorMatches, "uh, it failed") 557 // ... and ensure that the tempfile *is* removed 558 c.Assert(osutil.FileExists(tmpfile.Name()), Equals, false) 559 // ... and the target path isn't there 560 c.Assert(osutil.FileExists(path), Equals, false) 561 } 562 563 func (s *storeDownloadSuite) TestDownloadSyncFails(c *C) { 564 var tmpfile *os.File 565 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 { 566 tmpfile = w.(*os.File) 567 w.Write([]byte("sync will fail")) 568 err := tmpfile.Close() 569 c.Assert(err, IsNil) 570 return nil 571 }) 572 defer restore() 573 574 snap := &snap.Info{} 575 snap.RealName = "foo" 576 snap.AnonDownloadURL = "anon-url" 577 snap.DownloadURL = "AUTH-URL" 578 snap.Size = int64(len("sync will fail")) 579 580 // simulate a failed sync 581 path := filepath.Join(c.MkDir(), "downloaded-file") 582 err := s.store.Download(s.ctx, "foo", path, &snap.DownloadInfo, nil, nil, nil) 583 c.Assert(err, ErrorMatches, `(sync|fsync:) .*`) 584 // ... and ensure that the tempfile is removed 585 c.Assert(osutil.FileExists(tmpfile.Name()), Equals, false) 586 // ... because it's been renamed to the target path already 587 c.Assert(osutil.FileExists(path), Equals, true) 588 } 589 590 var downloadDeltaTests = []struct { 591 info snap.DownloadInfo 592 authenticated bool 593 deviceSession bool 594 useLocalUser bool 595 format string 596 expectedURL string 597 expectError bool 598 }{{ 599 // An unauthenticated request downloads the anonymous delta url. 600 info: snap.DownloadInfo{ 601 Sha3_384: "sha3", 602 Deltas: []snap.DeltaInfo{ 603 {AnonDownloadURL: "anon-delta-url", Format: "xdelta3", FromRevision: 24, ToRevision: 26}, 604 }, 605 }, 606 authenticated: false, 607 deviceSession: false, 608 format: "xdelta3", 609 expectedURL: "anon-delta-url", 610 expectError: false, 611 }, { 612 // An authenticated request downloads the authenticated delta url. 613 info: snap.DownloadInfo{ 614 Sha3_384: "sha3", 615 Deltas: []snap.DeltaInfo{ 616 {AnonDownloadURL: "anon-delta-url", DownloadURL: "auth-delta-url", Format: "xdelta3", FromRevision: 24, ToRevision: 26}, 617 }, 618 }, 619 authenticated: true, 620 deviceSession: false, 621 useLocalUser: false, 622 format: "xdelta3", 623 expectedURL: "auth-delta-url", 624 expectError: false, 625 }, { 626 // A device-authenticated request downloads the authenticated delta url. 627 info: snap.DownloadInfo{ 628 Sha3_384: "sha3", 629 Deltas: []snap.DeltaInfo{ 630 {AnonDownloadURL: "anon-delta-url", DownloadURL: "auth-delta-url", Format: "xdelta3", FromRevision: 24, ToRevision: 26}, 631 }, 632 }, 633 authenticated: false, 634 deviceSession: true, 635 useLocalUser: false, 636 format: "xdelta3", 637 expectedURL: "auth-delta-url", 638 expectError: false, 639 }, { 640 // A local authenticated request downloads the anonymous delta url. 641 info: snap.DownloadInfo{ 642 Sha3_384: "sha3", 643 Deltas: []snap.DeltaInfo{ 644 {AnonDownloadURL: "anon-delta-url", Format: "xdelta3", FromRevision: 24, ToRevision: 26}, 645 }, 646 }, 647 authenticated: true, 648 deviceSession: false, 649 useLocalUser: true, 650 format: "xdelta3", 651 expectedURL: "anon-delta-url", 652 expectError: false, 653 }, { 654 // An error is returned if more than one matching delta is returned by the store, 655 // though this may be handled in the future. 656 info: snap.DownloadInfo{ 657 Sha3_384: "sha3", 658 Deltas: []snap.DeltaInfo{ 659 {DownloadURL: "xdelta3-delta-url", Format: "xdelta3", FromRevision: 24, ToRevision: 25}, 660 {DownloadURL: "bsdiff-delta-url", Format: "xdelta3", FromRevision: 25, ToRevision: 26}, 661 }, 662 }, 663 authenticated: false, 664 deviceSession: false, 665 format: "xdelta3", 666 expectedURL: "", 667 expectError: true, 668 }, { 669 // If the supported format isn't available, an error is returned. 670 info: snap.DownloadInfo{ 671 Sha3_384: "sha3", 672 Deltas: []snap.DeltaInfo{ 673 {DownloadURL: "xdelta3-delta-url", Format: "xdelta3", FromRevision: 24, ToRevision: 26}, 674 {DownloadURL: "ydelta-delta-url", Format: "ydelta", FromRevision: 24, ToRevision: 26}, 675 }, 676 }, 677 authenticated: false, 678 deviceSession: false, 679 format: "bsdiff", 680 expectedURL: "", 681 expectError: true, 682 }} 683 684 func (s *storeDownloadSuite) TestDownloadDelta(c *C) { 685 origUseDeltas := os.Getenv("SNAPD_USE_DELTAS_EXPERIMENTAL") 686 defer os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", origUseDeltas) 687 c.Assert(os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", "1"), IsNil) 688 689 dauthCtx := &testDauthContext{c: c} 690 sto := store.New(nil, dauthCtx) 691 692 for _, testCase := range downloadDeltaTests { 693 sto.SetDeltaFormat(testCase.format) 694 restore := store.MockDownload(func(ctx context.Context, name, sha3, url string, user *auth.UserState, _ *store.Store, w io.ReadWriteSeeker, resume int64, pbar progress.Meter, dlOpts *store.DownloadOptions) error { 695 c.Check(dlOpts, DeepEquals, &store.DownloadOptions{IsAutoRefresh: true}) 696 expectedUser := s.user 697 if testCase.useLocalUser { 698 expectedUser = s.localUser 699 } 700 if !testCase.authenticated { 701 expectedUser = nil 702 } 703 c.Check(user, Equals, expectedUser) 704 c.Check(url, Equals, testCase.expectedURL) 705 w.Write([]byte("I was downloaded")) 706 return nil 707 }) 708 defer restore() 709 710 w, err := ioutil.TempFile("", "") 711 c.Assert(err, IsNil) 712 defer os.Remove(w.Name()) 713 714 dauthCtx.device = nil 715 if testCase.deviceSession { 716 dauthCtx.device = s.device 717 } 718 719 authedUser := s.user 720 if testCase.useLocalUser { 721 authedUser = s.localUser 722 } 723 if !testCase.authenticated { 724 authedUser = nil 725 } 726 727 err = sto.DownloadDelta("snapname", &testCase.info, w, nil, authedUser, &store.DownloadOptions{IsAutoRefresh: true}) 728 729 if testCase.expectError { 730 c.Assert(err, NotNil) 731 } else { 732 c.Assert(err, IsNil) 733 c.Assert(w.Name(), testutil.FileEquals, "I was downloaded") 734 } 735 } 736 } 737 738 var applyDeltaTests = []struct { 739 deltaInfo snap.DeltaInfo 740 currentRevision uint 741 error string 742 }{{ 743 // A supported delta format can be applied. 744 deltaInfo: snap.DeltaInfo{Format: "xdelta3", FromRevision: 24, ToRevision: 26}, 745 currentRevision: 24, 746 error: "", 747 }, { 748 // An error is returned if the expected current snap does not exist on disk. 749 deltaInfo: snap.DeltaInfo{Format: "xdelta3", FromRevision: 24, ToRevision: 26}, 750 currentRevision: 23, 751 error: "snap \"foo\" revision 24 not found", 752 }, { 753 // An error is returned if the format is not supported. 754 deltaInfo: snap.DeltaInfo{Format: "nodelta", FromRevision: 24, ToRevision: 26}, 755 currentRevision: 24, 756 error: "cannot apply unsupported delta format \"nodelta\" (only xdelta3 currently)", 757 }} 758 759 func (s *storeDownloadSuite) TestApplyDelta(c *C) { 760 for _, testCase := range applyDeltaTests { 761 name := "foo" 762 currentSnapName := fmt.Sprintf("%s_%d.snap", name, testCase.currentRevision) 763 currentSnapPath := filepath.Join(dirs.SnapBlobDir, currentSnapName) 764 targetSnapName := fmt.Sprintf("%s_%d.snap", name, testCase.deltaInfo.ToRevision) 765 targetSnapPath := filepath.Join(dirs.SnapBlobDir, targetSnapName) 766 err := os.MkdirAll(filepath.Dir(currentSnapPath), 0755) 767 c.Assert(err, IsNil) 768 err = ioutil.WriteFile(currentSnapPath, nil, 0644) 769 c.Assert(err, IsNil) 770 deltaPath := filepath.Join(dirs.SnapBlobDir, "the.delta") 771 err = ioutil.WriteFile(deltaPath, nil, 0644) 772 c.Assert(err, IsNil) 773 // When testing a case where the call to the external 774 // xdelta3 is successful, 775 // simulate the resulting .partial. 776 if testCase.error == "" { 777 err = ioutil.WriteFile(targetSnapPath+".partial", nil, 0644) 778 c.Assert(err, IsNil) 779 } 780 781 // make a fresh store object to circumvent the caching of xdelta3 info 782 // between test cases 783 sto := &store.Store{} 784 err = store.ApplyDelta(sto, name, deltaPath, &testCase.deltaInfo, targetSnapPath, "") 785 786 if testCase.error == "" { 787 c.Assert(err, IsNil) 788 c.Assert(s.mockXDelta.Calls(), DeepEquals, [][]string{ 789 // since we don't cache xdelta3 in this test, we always check if 790 // xdelta3 config is successful before using xdelta3 (and at 791 // that point cache xdelta3 and don't call config again) 792 {"xdelta3", "config"}, 793 {"xdelta3", "-d", "-s", currentSnapPath, deltaPath, targetSnapPath + ".partial"}, 794 }) 795 c.Assert(osutil.FileExists(targetSnapPath+".partial"), Equals, false) 796 st, err := os.Stat(targetSnapPath) 797 c.Assert(err, IsNil) 798 c.Check(st.Mode(), Equals, os.FileMode(0600)) 799 c.Assert(os.Remove(targetSnapPath), IsNil) 800 } else { 801 c.Assert(err, NotNil) 802 c.Assert(err.Error()[0:len(testCase.error)], Equals, testCase.error) 803 c.Assert(osutil.FileExists(targetSnapPath+".partial"), Equals, false) 804 c.Assert(osutil.FileExists(targetSnapPath), Equals, false) 805 } 806 c.Assert(os.Remove(currentSnapPath), IsNil) 807 c.Assert(os.Remove(deltaPath), IsNil) 808 } 809 } 810 811 type cacheObserver struct { 812 inCache map[string]bool 813 814 gets []string 815 puts []string 816 } 817 818 func (co *cacheObserver) Get(cacheKey, targetPath string) error { 819 co.gets = append(co.gets, fmt.Sprintf("%s:%s", cacheKey, targetPath)) 820 if !co.inCache[cacheKey] { 821 return fmt.Errorf("cannot find %s in cache", cacheKey) 822 } 823 return nil 824 } 825 func (co *cacheObserver) GetPath(cacheKey string) string { 826 return "" 827 } 828 func (co *cacheObserver) Put(cacheKey, sourcePath string) error { 829 co.puts = append(co.puts, fmt.Sprintf("%s:%s", cacheKey, sourcePath)) 830 return nil 831 } 832 833 func (s *storeDownloadSuite) TestDownloadCacheHit(c *C) { 834 obs := &cacheObserver{inCache: map[string]bool{"the-snaps-sha3_384": true}} 835 restore := s.store.MockCacher(obs) 836 defer restore() 837 838 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 { 839 c.Fatalf("download should not be called when results come from the cache") 840 return nil 841 }) 842 defer restore() 843 844 snap := &snap.Info{} 845 snap.Sha3_384 = "the-snaps-sha3_384" 846 847 path := filepath.Join(c.MkDir(), "downloaded-file") 848 err := s.store.Download(s.ctx, "foo", path, &snap.DownloadInfo, nil, nil, nil) 849 c.Assert(err, IsNil) 850 851 c.Check(obs.gets, DeepEquals, []string{fmt.Sprintf("%s:%s", snap.Sha3_384, path)}) 852 c.Check(obs.puts, IsNil) 853 } 854 855 func (s *storeDownloadSuite) TestDownloadCacheMiss(c *C) { 856 obs := &cacheObserver{inCache: map[string]bool{}} 857 restore := s.store.MockCacher(obs) 858 defer restore() 859 860 downloadWasCalled := false 861 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 { 862 downloadWasCalled = true 863 return nil 864 }) 865 defer restore() 866 867 snap := &snap.Info{} 868 snap.Sha3_384 = "the-snaps-sha3_384" 869 870 path := filepath.Join(c.MkDir(), "downloaded-file") 871 err := s.store.Download(s.ctx, "foo", path, &snap.DownloadInfo, nil, nil, nil) 872 c.Assert(err, IsNil) 873 c.Check(downloadWasCalled, Equals, true) 874 875 c.Check(obs.gets, DeepEquals, []string{fmt.Sprintf("the-snaps-sha3_384:%s", path)}) 876 c.Check(obs.puts, DeepEquals, []string{fmt.Sprintf("the-snaps-sha3_384:%s", path)}) 877 } 878 879 func (s *storeDownloadSuite) TestDownloadStreamOK(c *C) { 880 expectedContent := []byte("I was downloaded") 881 restore := store.MockDoDownloadReq(func(ctx context.Context, url *url.URL, cdnHeader string, resume int64, s *store.Store, user *auth.UserState) (*http.Response, error) { 882 c.Check(url.String(), Equals, "http://anon-url") 883 r := &http.Response{ 884 Body: ioutil.NopCloser(bytes.NewReader(expectedContent[resume:])), 885 } 886 if resume > 0 { 887 r.StatusCode = 206 888 } else { 889 r.StatusCode = 200 890 } 891 return r, nil 892 }) 893 defer restore() 894 895 snap := &snap.Info{} 896 snap.RealName = "foo" 897 snap.AnonDownloadURL = "http://anon-url" 898 snap.DownloadURL = "AUTH-URL" 899 snap.Size = int64(len(expectedContent)) 900 901 stream, status, err := s.store.DownloadStream(context.TODO(), "foo", &snap.DownloadInfo, 0, nil) 902 c.Assert(err, IsNil) 903 c.Assert(status, Equals, 200) 904 905 buf := new(bytes.Buffer) 906 buf.ReadFrom(stream) 907 c.Check(buf.String(), Equals, string(expectedContent)) 908 909 stream, status, err = s.store.DownloadStream(context.TODO(), "foo", &snap.DownloadInfo, 2, nil) 910 c.Assert(err, IsNil) 911 c.Check(status, Equals, 206) 912 913 buf = new(bytes.Buffer) 914 buf.ReadFrom(stream) 915 c.Check(buf.String(), Equals, string(expectedContent[2:])) 916 } 917 918 func (s *storeDownloadSuite) TestDownloadStreamCachedOK(c *C) { 919 expectedContent := []byte("I was NOT downloaded") 920 defer store.MockDoDownloadReq(func(context.Context, *url.URL, string, int64, *store.Store, *auth.UserState) (*http.Response, error) { 921 c.Fatalf("should not be here") 922 return nil, nil 923 })() 924 925 c.Assert(os.MkdirAll(dirs.SnapDownloadCacheDir, 0700), IsNil) 926 c.Assert(ioutil.WriteFile(filepath.Join(dirs.SnapDownloadCacheDir, "sha3_384-of-foo"), expectedContent, 0600), IsNil) 927 928 cache := store.NewCacheManager(dirs.SnapDownloadCacheDir, 1) 929 defer s.store.MockCacher(cache)() 930 931 snap := &snap.Info{} 932 snap.RealName = "foo" 933 snap.AnonDownloadURL = "http://anon-url" 934 snap.DownloadURL = "AUTH-URL" 935 snap.Size = int64(len(expectedContent)) 936 snap.Sha3_384 = "sha3_384-of-foo" 937 938 stream, status, err := s.store.DownloadStream(context.TODO(), "foo", &snap.DownloadInfo, 0, nil) 939 c.Check(err, IsNil) 940 c.Check(status, Equals, 200) 941 942 buf := new(bytes.Buffer) 943 buf.ReadFrom(stream) 944 c.Check(buf.String(), Equals, string(expectedContent)) 945 946 stream, status, err = s.store.DownloadStream(context.TODO(), "foo", &snap.DownloadInfo, 2, nil) 947 c.Assert(err, IsNil) 948 c.Check(status, Equals, 206) 949 950 buf = new(bytes.Buffer) 951 buf.ReadFrom(stream) 952 c.Check(buf.String(), Equals, string(expectedContent[2:])) 953 } 954 955 func (s *storeDownloadSuite) TestDownloadTimeout(c *C) { 956 var mockServer *httptest.Server 957 958 restore := store.MockDownloadSpeedParams(1*time.Second, 32768) 959 defer restore() 960 961 // our mock download content 962 buf := make([]byte, 65535) 963 964 h := crypto.SHA3_384.New() 965 io.Copy(h, bytes.NewBuffer(buf)) 966 967 quit := make(chan bool) 968 mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 969 w.Header().Add("Content-Length", fmt.Sprintf("%d", len(buf))) 970 w.WriteHeader(200) 971 972 // push enough data to fill in internal buffers, so that download code 973 // hits io.Copy over the body and gets stuck there, and not immediately 974 // on doRequest. 975 w.Write(buf[:20000]) 976 977 // block the handler 978 select { 979 case <-quit: 980 case <-time.After(10 * time.Second): 981 c.Fatalf("unexpected server timeout") 982 } 983 mockServer.CloseClientConnections() 984 })) 985 986 c.Assert(mockServer, NotNil) 987 988 snap := &snap.Info{} 989 snap.RealName = "foo" 990 snap.AnonDownloadURL = mockServer.URL 991 snap.DownloadURL = "AUTH-URL" 992 snap.Sha3_384 = fmt.Sprintf("%x", h.Sum(nil)) 993 snap.Size = 50000 994 995 targetFn := filepath.Join(c.MkDir(), "foo_1.0_all.snap") 996 err := s.store.Download(s.ctx, "foo", targetFn, &snap.DownloadInfo, nil, nil, nil) 997 ok, speed := store.IsTransferSpeedError(err) 998 c.Assert(ok, Equals, true) 999 // in reality speed can be 0, but here it's an extra sanity check. 1000 c.Check(speed > 1, Equals, true) 1001 c.Check(speed < 32768, Equals, true) 1002 close(quit) 1003 defer mockServer.Close() 1004 } 1005 1006 func (s *storeDownloadSuite) TestTransferSpeedMonitoringWriterHappy(c *C) { 1007 origCtx := context.TODO() 1008 w, ctx := store.NewTransferSpeedMonitoringWriterAndContext(origCtx, 50*time.Millisecond, 1) 1009 1010 data := []byte{0, 0, 0, 0, 0} 1011 quit := w.Monitor() 1012 1013 // write a few bytes every ~5ms, this should satisfy >=1 speed in 50ms 1014 // measure windows defined above; 100 iterations ensures we hit a few 1015 // measurement windows. 1016 for i := 0; i < 100; i++ { 1017 n, err := w.Write(data) 1018 c.Assert(err, IsNil) 1019 c.Assert(n, Equals, len(data)) 1020 time.Sleep(5 * time.Millisecond) 1021 } 1022 close(quit) 1023 c.Check(store.Cancelled(ctx), Equals, false) 1024 c.Check(w.Err(), IsNil) 1025 1026 // we should hit at least 100*5/50 = 10 measurement windows 1027 c.Assert(w.MeasuredWindowsCount() >= 10, Equals, true, Commentf("%d", w.MeasuredWindowsCount())) 1028 } 1029 1030 func (s *storeDownloadSuite) TestTransferSpeedMonitoringWriterUnhappy(c *C) { 1031 origCtx := context.TODO() 1032 w, ctx := store.NewTransferSpeedMonitoringWriterAndContext(origCtx, 50*time.Millisecond, 1000) 1033 1034 data := []byte{0} 1035 quit := w.Monitor() 1036 1037 // write just one byte every ~5ms, this will trigger download timeout 1038 // since the writer expects 1000 bytes per 50ms as defined above. 1039 for i := 0; i < 100; i++ { 1040 n, err := w.Write(data) 1041 c.Assert(err, IsNil) 1042 c.Assert(n, Equals, len(data)) 1043 time.Sleep(5 * time.Millisecond) 1044 } 1045 close(quit) 1046 c.Check(store.Cancelled(ctx), Equals, true) 1047 terr, _ := store.IsTransferSpeedError(w.Err()) 1048 c.Assert(terr, Equals, true) 1049 c.Check(w.Err(), ErrorMatches, "download too slow: .* bytes/sec") 1050 } 1051 1052 func (s *storeDownloadSuite) TestDownloadTimeoutOnHeaders(c *C) { 1053 restore := httputil.MockResponseHeaderTimeout(250 * time.Millisecond) 1054 defer restore() 1055 1056 var mockServer *httptest.Server 1057 1058 quit := make(chan bool) 1059 mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1060 // block the handler, do not send response headers. 1061 select { 1062 case <-quit: 1063 case <-time.After(30 * time.Second): 1064 // we expect to hit ResponseHeaderTimeout first 1065 c.Fatalf("unexpected") 1066 } 1067 mockServer.CloseClientConnections() 1068 })) 1069 c.Assert(mockServer, NotNil) 1070 defer mockServer.Close() 1071 1072 snap := &snap.Info{} 1073 snap.RealName = "foo" 1074 snap.AnonDownloadURL = mockServer.URL 1075 snap.DownloadURL = "AUTH-URL" 1076 snap.Sha3_384 = "1234" 1077 snap.Size = 50000 1078 1079 targetFn := filepath.Join(c.MkDir(), "foo_1.0_all.snap") 1080 err := s.store.Download(s.ctx, "foo", targetFn, &snap.DownloadInfo, nil, nil, nil) 1081 close(quit) 1082 c.Assert(err, ErrorMatches, `.*net/http: timeout awaiting response headers`) 1083 }