1 // 2 // Copyright (c) 2010-2024 Antmicro 3 // Copyright (c) 2011-2015 Realtime Embedded 4 // 5 // This file is licensed under the MIT License. 6 // Full license text is available in 'licenses/MIT.txt'. 7 // 8 using System; 9 using System.Linq; 10 using System.Net; 11 using System.IO; 12 using Antmicro.Renode.Exceptions; 13 using Antmicro.Renode.Logging; 14 using System.Collections.Generic; 15 using Antmicro.Migrant; 16 using System.IO.Compression; 17 using System.Threading; 18 using System.Security.Cryptography; 19 using System.Text.RegularExpressions; 20 using System.Globalization; 21 using System.ComponentModel; 22 using Antmicro.Renode.Core; 23 using System.Text; 24 using Antmicro.Migrant.Customization; 25 26 namespace Antmicro.Renode.Utilities 27 { 28 public class CachingFileFetcher : IDisposable 29 { CachingFileFetcher()30 public CachingFileFetcher() 31 { 32 fetchedFiles = new Dictionary<string, string>(); 33 progressUpdateThreshold = TimeSpan.FromSeconds(0.25); 34 } 35 GetFetchedFiles()36 public IDictionary<string, string> GetFetchedFiles() 37 { 38 return fetchedFiles.ToDictionary(x => x.Key, x => x.Value); 39 } 40 FetchFromUri(Uri uri)41 public string FetchFromUri(Uri uri) 42 { 43 string fileName; 44 if(!TryFetchFromUri(uri, out fileName)) 45 { 46 throw new RecoverableException("Could not download file from {0}.".FormatWith(uri)); 47 } 48 return fileName; 49 } 50 CancelDownload()51 public void CancelDownload() 52 { 53 if(client != null && client.IsBusy) 54 { 55 client.CancelAsync(); 56 } 57 } 58 TryFetchFromUri(Uri uri, out string fileName)59 public bool TryFetchFromUri(Uri uri, out string fileName) 60 { 61 fileName = null; 62 if(!Monitor.TryEnter(concurrentLock)) 63 { 64 Logger.LogAs(this, LogLevel.Error, "Cannot perform concurrent downloads, aborting..."); 65 return false; 66 } 67 68 try 69 { 70 var disableCaching = Emulator.InCIMode; 71 72 return disableCaching 73 ? TryFetchFromUriInner(uri, out fileName) 74 : TryFetchFromCacheOrUriInner(uri, out fileName); 75 } 76 finally 77 { 78 Monitor.Exit(concurrentLock); 79 } 80 } 81 Dispose()82 public void Dispose() 83 { 84 if(EmulationManager.DisableEmulationFilesCleanup) 85 { 86 return; 87 } 88 89 foreach(var file in fetchedFiles.Keys) 90 { 91 try 92 { 93 File.Delete(file); 94 } 95 catch(Exception) 96 { 97 // nothing we can do 98 } 99 } 100 } 101 TryFetchFromCacheOrUriInner(Uri uri, out string fileName)102 private bool TryFetchFromCacheOrUriInner(Uri uri, out string fileName) 103 { 104 using(var locker = new FileLocker(GetCacheIndexLockLocation())) 105 { 106 if(TryGetFromCache(uri, out fileName)) 107 { 108 fetchedFiles.Add(fileName, uri.ToString()); 109 return true; 110 } 111 112 if(TryFetchFromUriInner(uri, out fileName)) 113 { 114 UpdateInCache(uri, fileName); 115 return true; 116 } 117 } 118 119 fileName = null; 120 return false; 121 } 122 TryFetchFromUriInner(Uri uri, out string fileName)123 private bool TryFetchFromUriInner(Uri uri, out string fileName) 124 { 125 fileName = TemporaryFilesManager.Instance.GetTemporaryFile(Path.GetExtension(uri.AbsoluteUri)); 126 // try download the file a few times times 127 // in order to handle some intermittent 128 // network problems 129 if(!TryDownload(uri, fileName, DownloadAttempts)) 130 { 131 return false; 132 } 133 134 // at this point the file has been successfully downloded; 135 // now verify its size and checksum based on the information 136 // encoded in URI 137 // NOTE: there is no point in redownloading the file as a result 138 // of checksum/size verification failure; we are using TCP/IP 139 // protocol that guarantee a failure-free communication, so 140 // a checksum/size mismatch must be a result of a broken file 141 // at server side or a wrong URI 142 if(TryGetChecksumAndSizeFromUri(uri, out var checksum, out var size)) 143 { 144 if(!VerifySize(fileName, size)) 145 { 146 Logger.Log(LogLevel.Error, "Wrong size of the downloaded file, aborting"); 147 return false; 148 } 149 150 if(!VerifyChecksum(fileName, checksum)) 151 { 152 Logger.Log(LogLevel.Error, "Wrong checksum of the downloaded file, aborting"); 153 return false; 154 } 155 } 156 157 if(uri.ToString().EndsWith(".gz", StringComparison.InvariantCulture)) 158 { 159 fileName = Decompress(fileName); 160 } 161 162 fetchedFiles.Add(fileName, uri.ToString()); 163 164 return true; 165 } 166 TryDownload(Uri uri, string fileName, int attemptsLimit)167 private bool TryDownload(Uri uri, string fileName, int attemptsLimit) 168 { 169 var attempts = 0; 170 do 171 { 172 if(!TryDownloadInner(uri, fileName, out var error)) 173 { 174 if(error == null) 175 { 176 Logger.LogAs(this, LogLevel.Info, "Download cancelled."); 177 return false; 178 } 179 else 180 { 181 var webException = error as WebException; 182 Logger.Log(LogLevel.Error, "Failed to download from {0}, reason: {1} (attempt {2}/{3})", uri, webException != null ? ResolveWebException(webException) : error.Message, attempts + 1, attemptsLimit); 183 } 184 } 185 else 186 { 187 Logger.LogAs(this, LogLevel.Info, "Download done."); 188 return true; 189 } 190 } 191 while (++attempts < attemptsLimit); 192 193 Logger.Log(LogLevel.Error, "Download failed {0} times, aborting.", attempts); 194 return false; 195 } 196 TryDownloadInner(Uri uri, string fileName, out Exception error)197 private bool TryDownloadInner(Uri uri, string fileName, out Exception error) 198 { 199 Exception localError = null; 200 var wasCancelled = false; 201 202 using(var downloadProgressHandler = EmulationManager.Instance.ProgressMonitor.Start(GenerateProgressMessage(uri), false, true)) 203 #pragma warning disable SYSLIB0014 // Even though WebClient is technically obselete, there's no better replacement for our use case 204 using(client = new WebClient()) 205 #pragma warning restore SYSLIB0014 206 { 207 Logger.LogAs(this, LogLevel.Info, "Downloading {0}.", uri); 208 var now = CustomDateTime.Now; 209 var bytesDownloaded = 0L; 210 client.DownloadProgressChanged += (sender, e) => 211 { 212 var newNow = CustomDateTime.Now; 213 214 var period = newNow - now; 215 if (period > progressUpdateThreshold) 216 { 217 downloadProgressHandler.UpdateProgress(e.ProgressPercentage, 218 GenerateProgressMessage(uri, 219 e.BytesReceived, e.TotalBytesToReceive, e.ProgressPercentage, 1.0 * (e.BytesReceived - bytesDownloaded) / period.TotalSeconds)); 220 221 now = newNow; 222 bytesDownloaded = e.BytesReceived; 223 } 224 }; 225 var resetEvent = new ManualResetEvent(false); 226 client.DownloadFileCompleted += delegate (object sender, AsyncCompletedEventArgs e) 227 { 228 localError = e.Error; 229 if (e.Cancelled) 230 { 231 wasCancelled = true; 232 } 233 resetEvent.Set(); 234 }; 235 client.DownloadFileAsync(uri, fileName); 236 resetEvent.WaitOne(); 237 error = localError; 238 } 239 client = null; 240 241 return !wasCancelled && error == null; 242 } 243 Decompress(string fileName)244 private string Decompress(string fileName) 245 { 246 var decompressedFile = TemporaryFilesManager.Instance.GetTemporaryFile(); 247 248 using(var decompressionProgressHandler = EmulationManager.Instance.ProgressMonitor.Start("Decompressing file")) 249 { 250 Logger.Log(LogLevel.Info, "Decompressing file"); 251 using (var gzipStream = new GZipStream(File.OpenRead(fileName), CompressionMode.Decompress)) 252 using (var outputStream = File.OpenWrite(decompressedFile)) 253 { 254 gzipStream.CopyTo(outputStream); 255 } 256 Logger.Log(LogLevel.Info, "Decompression done"); 257 } 258 259 return decompressedFile; 260 } 261 ResolveWebException(WebException e)262 private static string ResolveWebException(WebException e) 263 { 264 string reason; 265 switch(e.Status) 266 { 267 case WebExceptionStatus.ConnectFailure: 268 reason = "unable to connect to the server"; 269 break; 270 271 case WebExceptionStatus.ConnectionClosed: 272 reason = "the connection was prematurely closed"; 273 break; 274 275 case WebExceptionStatus.NameResolutionFailure: 276 reason = "server name resolution error"; 277 break; 278 279 case WebExceptionStatus.ProtocolError: 280 switch(((HttpWebResponse)e.Response).StatusCode) 281 { 282 case HttpStatusCode.NotFound: 283 reason = "file was not found on a server"; 284 break; 285 286 default: 287 reason = string.Format("http protocol status code {0}", (int)((HttpWebResponse)e.Response).StatusCode); 288 break; 289 } 290 break; 291 292 default: 293 reason = e.Status.ToString(); 294 break; 295 } 296 297 return reason; 298 } 299 GenerateProgressMessage(Uri uri, long? bytesDownloaded = null, long? totalBytes = null, int? progressPercentage = null, double? speed = null)300 private string GenerateProgressMessage(Uri uri, long? bytesDownloaded = null, long? totalBytes = null, int? progressPercentage = null, double? speed = null) 301 { 302 var strBldr = new StringBuilder(); 303 strBldr.AppendFormat("Downloading: {0}", uri); 304 if(bytesDownloaded.HasValue && totalBytes.HasValue) 305 { 306 // A workaround for a bug in Mono misreporting TotalBytesToReceive 307 // https://github.com/mono/mono/issues/9808 308 if(totalBytes == -1) 309 { 310 strBldr.AppendFormat("\nProgress: {0}B downloaded", Misc.NormalizeBinary(bytesDownloaded.Value)); 311 } 312 else 313 { 314 strBldr.AppendFormat("\nProgress: {0}% ({1}B/{2}B)", progressPercentage, Misc.NormalizeBinary(bytesDownloaded.Value), Misc.NormalizeBinary(totalBytes.Value)); 315 } 316 } 317 if(speed != null) 318 { 319 double val; 320 string unit; 321 322 Misc.CalculateUnitSuffix(speed.Value, out val, out unit); 323 strBldr.AppendFormat("\nSpeed: {0:F2}{1}/s", val, unit); 324 } 325 return strBldr.Append(".").ToString(); 326 } 327 TryGetFromCache(Uri uri, out string fileName)328 private bool TryGetFromCache(Uri uri, out string fileName) 329 { 330 lock(CacheDirectory) 331 { 332 fileName = null; 333 var index = ReadBinariesIndex(); 334 BinaryEntry entry; 335 if(!index.TryGetValue(uri.ToString(), out entry)) 336 { 337 return false; 338 } 339 var fileToCopy = GetBinaryFileName(entry.Index); 340 if(!VerifyCachedFile(fileToCopy, entry)) 341 { 342 return false; 343 } 344 fileName = TemporaryFilesManager.Instance.GetTemporaryFile(); 345 FileCopier.Copy(GetBinaryFileName(entry.Index), fileName, true); 346 return true; 347 } 348 } 349 VerifyCachedFile(string fileName, BinaryEntry entry)350 private bool VerifyCachedFile(string fileName, BinaryEntry entry) 351 { 352 if(!File.Exists(fileName)) 353 { 354 Logger.LogAs(this, LogLevel.Warning, "Binary {0} found in index but is missing in cache.", fileName); 355 return false; 356 } 357 358 if(entry.Checksum == null) 359 { 360 return true; 361 } 362 363 return VerifySize(fileName, entry.Size) && VerifyChecksum(fileName, entry.Checksum); 364 } 365 VerifySize(string fileName, long expectedSize)366 private bool VerifySize(string fileName, long expectedSize) 367 { 368 var actualSize = new FileInfo(fileName).Length; 369 if(actualSize != expectedSize) 370 { 371 Logger.LogAs(this, LogLevel.Warning, "Size of the file differs: is {0}B, should be {1}B.", actualSize, expectedSize); 372 return false; 373 } 374 return true; 375 } 376 VerifyChecksum(string fileName, byte[] expectedChecksum)377 private bool VerifyChecksum(string fileName, byte[] expectedChecksum) 378 { 379 if(!ConfigurationManager.Instance.Get("file-fetcher", "calculate-checksum", true)) 380 { 381 // with a disabled checksum verification we pretend everything is peachy 382 return true; 383 } 384 385 byte[] checksum; 386 using(var progressHandler = EmulationManager.Instance.ProgressMonitor.Start("Calculating SHA1 checksum...")) 387 { 388 checksum = GetSHA1Checksum(fileName); 389 } 390 if(!checksum.SequenceEqual(expectedChecksum)) 391 { 392 Logger.LogAs(this, LogLevel.Warning, "Checksum of the file differs, is {0}, should be {1}.", ChecksumToText(checksum), ChecksumToText(expectedChecksum)); 393 return false; 394 } 395 return true; 396 } 397 UpdateInCache(Uri uri, string withFile)398 private void UpdateInCache(Uri uri, string withFile) 399 { 400 using(var progressHandler = EmulationManager.Instance.ProgressMonitor.Start("Updating cache")) 401 { 402 lock(CacheDirectory) 403 { 404 var index = ReadBinariesIndex(); 405 BinaryEntry entry; 406 var fileId = 0; 407 if(!index.TryGetValue(uri.ToString(), out entry)) 408 { 409 foreach(var element in index) 410 { 411 fileId = Math.Max(fileId, element.Value.Index) + 1; 412 } 413 } 414 else 415 { 416 fileId = entry.Index; 417 } 418 FileCopier.Copy(withFile, GetBinaryFileName(fileId), true); 419 420 // checksum will be 'null' if the uri pattern does not contain 421 // checksum/size information 422 TryGetChecksumAndSizeFromUri(uri, out var checksum, out var size); 423 index[uri.ToString()] = new BinaryEntry(fileId, size, checksum); 424 WriteBinariesIndex(index); 425 } 426 } 427 } 428 ReadBinariesIndex()429 private Dictionary<string, BinaryEntry> ReadBinariesIndex() 430 { 431 using(var progressHandler = EmulationManager.Instance.ProgressMonitor.Start("Reading cache")) 432 { 433 using(var fStream = GetIndexFileStream()) 434 { 435 if(fStream.Length == 0) 436 { 437 return new Dictionary<string, BinaryEntry>(); 438 } 439 Dictionary<string, BinaryEntry> result; 440 if(Serializer.TryDeserialize<Dictionary<string, BinaryEntry>>(fStream, out result) != DeserializationResult.OK) 441 { 442 Logger.LogAs(this, LogLevel.Warning, "There was an error while loading index file. Cache will be rebuilt."); 443 fStream.Close(); 444 ResetIndex(); 445 return new Dictionary<string, BinaryEntry>(); 446 } 447 return result; 448 } 449 } 450 } 451 WriteBinariesIndex(Dictionary<string, BinaryEntry> index)452 private void WriteBinariesIndex(Dictionary<string, BinaryEntry> index) 453 { 454 using(var progressHandler = EmulationManager.Instance.ProgressMonitor.Start("Writing binaries index")) 455 { 456 using(var fStream = GetIndexFileStream()) 457 { 458 Serializer.Serialize(index, fStream); 459 } 460 } 461 } 462 GetIndexFileStream()463 private FileStream GetIndexFileStream() 464 { 465 return new FileStream(GetCacheIndexLocation(), FileMode.OpenOrCreate); 466 } 467 ResetIndex()468 private void ResetIndex() 469 { 470 File.WriteAllText(GetCacheIndexLocation(), string.Empty); 471 var cacheDir = GetCacheLocation(); 472 if (Directory.Exists(cacheDir)) 473 { 474 Directory.Delete(cacheDir, true); 475 } 476 } 477 GetBinaryFileName(int id)478 private string GetBinaryFileName(int id) 479 { 480 var cacheDir = GetCacheLocation(); 481 if(!Directory.Exists(cacheDir)) 482 { 483 Directory.CreateDirectory(cacheDir); 484 } 485 return Path.Combine(cacheDir, "bin" + id); 486 } 487 ChecksumToText(byte[] checksum)488 private static string ChecksumToText(byte[] checksum) 489 { 490 return checksum.Select(x => x.ToString("x2")).Aggregate((x, y) => x + y); 491 } 492 GetCacheLocation()493 private static string GetCacheLocation() 494 { 495 return Path.Combine(Emulator.UserDirectoryPath, CacheDirectory); 496 } 497 GetCacheIndexLocation()498 private static string GetCacheIndexLocation() 499 { 500 return Path.Combine(Emulator.UserDirectoryPath, CacheIndex); 501 } 502 GetCacheIndexLockLocation()503 private static string GetCacheIndexLockLocation() 504 { 505 return Path.Combine(Emulator.UserDirectoryPath, CacheLock); 506 } 507 GetSHA1Checksum(string fileName)508 private static byte[] GetSHA1Checksum(string fileName) 509 { 510 using(var file = new FileStream(fileName, FileMode.Open)) 511 using(var sha = SHA1.Create()) 512 { 513 sha.Initialize(); 514 return sha.ComputeHash(file); 515 } 516 } 517 TryGetChecksumAndSizeFromUri(Uri uri, out byte[] checksum, out long size)518 private static bool TryGetChecksumAndSizeFromUri(Uri uri, out byte[] checksum, out long size) 519 { 520 size = 0; 521 checksum = null; 522 523 var groups = ChecksumRegex.Match(uri.ToString()).Groups; 524 if(groups.Count != 3) 525 { 526 return false; 527 } 528 529 // regex check above ensures that all the data below is parsable 530 size = long.Parse(groups[1].Value); 531 var checksumAsString = groups[2].Value; 532 checksum = new byte[20]; 533 for(var i = 0; i < checksum.Length; i++) 534 { 535 checksum[i] = byte.Parse(checksumAsString.Substring(2 * i, 2), NumberStyles.HexNumber); 536 } 537 538 return true; 539 } 540 CachingFileFetcher()541 static CachingFileFetcher() 542 { 543 ServicePointManager.ServerCertificateValidationCallback = delegate 544 { 545 return true; 546 }; 547 ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12; 548 } 549 550 private TimeSpan progressUpdateThreshold; 551 private WebClient client; 552 private object concurrentLock = new object(); 553 private const string CacheDirectory = "cached_binaries"; 554 private const string CacheIndex = "binaries_index"; 555 private const string CacheLock = "cache_lock"; 556 private readonly Dictionary<string, string> fetchedFiles; 557 558 private static readonly Serializer Serializer = new Serializer(new Settings(versionTolerance: VersionToleranceLevel.AllowGuidChange, disableTypeStamping: true)); 559 private static readonly Regex ChecksumRegex = new Regex(@"-s_(\d+)-([a-f,0-9]{40})$"); 560 561 private const int DownloadAttempts = 5; 562 563 private class BinaryEntry 564 { BinaryEntry(int index, long size, byte[] checksum)565 public BinaryEntry(int index, long size, byte[] checksum) 566 { 567 this.Index = index; 568 this.Size = size; 569 this.Checksum = checksum; 570 } 571 572 public int Index { get; set; } 573 public long Size { get; set; } 574 public byte[] Checksum { get; set; } 575 } 576 } 577 } 578