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