diff --git a/Source/AaxDecrypter/AudiobookDownloadBase.cs b/Source/AaxDecrypter/AudiobookDownloadBase.cs index d6529b39..f2347e94 100644 --- a/Source/AaxDecrypter/AudiobookDownloadBase.cs +++ b/Source/AaxDecrypter/AudiobookDownloadBase.cs @@ -52,7 +52,7 @@ namespace AaxDecrypter // delete file after validation is complete FileUtility.SaferDelete(OutputFileName); - } + } public abstract Task CancelAsync(); @@ -72,7 +72,7 @@ namespace AaxDecrypter => RetrievedNarrators?.Invoke(this, narrators); protected void OnRetrievedCoverArt(byte[] coverArt) => RetrievedCoverArt?.Invoke(this, coverArt); - protected void OnDecryptProgressUpdate(DownloadProgress downloadProgress) + protected void OnDecryptProgressUpdate(DownloadProgress downloadProgress) => DecryptProgressUpdate?.Invoke(this, downloadProgress); protected void OnDecryptTimeRemaining(TimeSpan timeRemaining) => DecryptTimeRemaining?.Invoke(this, timeRemaining); @@ -111,8 +111,8 @@ namespace AaxDecrypter { FileUtility.SaferDelete(jsonDownloadState); - if (DownloadOptions.AudibleKey is not null && - DownloadOptions.AudibleIV is not null && + if (DownloadOptions.AudibleKey is not null && + DownloadOptions.AudibleIV is not null && DownloadOptions.RetainEncryptedFile) { string aaxPath = Path.ChangeExtension(TempFilePath, ".aax"); @@ -156,12 +156,7 @@ namespace AaxDecrypter private NetworkFileStreamPersister NewNetworkFilePersister() { - var headers = new System.Net.WebHeaderCollection - { - { "User-Agent", DownloadOptions.UserAgent } - }; - - var networkFileStream = new NetworkFileStream(TempFilePath, new Uri(DownloadOptions.DownloadUrl), 0, headers); + var networkFileStream = new NetworkFileStream(TempFilePath, new Uri(DownloadOptions.DownloadUrl), 0, new() { { "User-Agent", DownloadOptions.UserAgent } }); return new NetworkFileStreamPersister(networkFileStream, jsonDownloadState); } } diff --git a/Source/AaxDecrypter/NetworkFileStream.cs b/Source/AaxDecrypter/NetworkFileStream.cs index d03e44f8..0b7b1d79 100644 --- a/Source/AaxDecrypter/NetworkFileStream.cs +++ b/Source/AaxDecrypter/NetworkFileStream.cs @@ -1,35 +1,16 @@ using Dinah.Core; using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; +using System.Net.Http; using System.Threading; +using System.Threading.Tasks; namespace AaxDecrypter { - /// - /// A for a single Uri. - /// - public class SingleUriCookieContainer : CookieContainer - { - private Uri baseAddress; - public Uri Uri - { - get => baseAddress; - set - { - baseAddress = new UriBuilder(value.Scheme, value.Host).Uri; - } - } - - public CookieCollection GetCookies() - { - return GetCookies(Uri); - } - } - /// /// A resumable, simultaneous file downloader and reader. /// @@ -51,17 +32,11 @@ namespace AaxDecrypter [JsonProperty(Required = Required.Always)] public Uri Uri { get; private set; } - /// - /// All cookies set by caller or by the remote server. - /// - [JsonProperty(Required = Required.Always)] - public SingleUriCookieContainer CookieContainer { get; } - /// /// Http headers to be sent to the server with the request. /// [JsonProperty(Required = Required.Always)] - public WebHeaderCollection RequestHeaders { get; private set; } + public Dictionary RequestHeaders { get; private set; } /// /// The position in that has been written and flushed to disk. @@ -75,17 +50,16 @@ namespace AaxDecrypter [JsonProperty(Required = Required.Always)] public long ContentLength { get; private set; } + [JsonIgnore] + public bool IsCancelled { get; private set; } + #endregion #region Private Properties - private HttpWebRequest HttpRequest { get; set; } private FileStream _writeFile { get; } private FileStream _readFile { get; } - private Stream _networkStream { get; set; } - private bool hasBegunDownloading { get; set; } - public bool IsCancelled { get; private set; } - private EventWaitHandle downloadEnded { get; set; } - private EventWaitHandle downloadedPiece { get; set; } + private EventWaitHandle _downloadedPiece { get; set; } + private Task _backgroundDownloadTask { get; set; } #endregion @@ -110,7 +84,7 @@ namespace AaxDecrypter /// The position in to begin downloading. /// Http headers to be sent to the server with the . /// A with cookies to send with the . It will also be populated with any cookies set by the server. - public NetworkFileStream(string saveFilePath, Uri uri, long writePosition = 0, WebHeaderCollection requestHeaders = null, SingleUriCookieContainer cookies = null) + public NetworkFileStream(string saveFilePath, Uri uri, long writePosition = 0, Dictionary requestHeaders = null) { ArgumentValidator.EnsureNotNullOrWhiteSpace(saveFilePath, nameof(saveFilePath)); ArgumentValidator.EnsureNotNullOrWhiteSpace(uri?.AbsoluteUri, nameof(uri)); @@ -122,8 +96,7 @@ namespace AaxDecrypter SaveFilePath = saveFilePath; Uri = uri; WritePosition = writePosition; - RequestHeaders = requestHeaders ?? new WebHeaderCollection(); - CookieContainer = cookies ?? new SingleUriCookieContainer { Uri = uri }; + RequestHeaders = requestHeaders ?? new(); _writeFile = new FileStream(SaveFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite) { @@ -144,7 +117,7 @@ namespace AaxDecrypter /// private void Update() { - RequestHeaders = HttpRequest.Headers; + RequestHeaders["Range"] = $"bytes={WritePosition}-"; try { Updated?.Invoke(this, EventArgs.Empty); @@ -165,37 +138,31 @@ namespace AaxDecrypter if (uriToSameFile.Host != Uri.Host) throw new ArgumentException($"New uri to the same file must have the same host.\r\n Old Host :{Uri.Host}\r\nNew Host: {uriToSameFile.Host}"); - if (hasBegunDownloading) + if (_backgroundDownloadTask is not null) throw new InvalidOperationException("Cannot change Uri after download has started."); Uri = uriToSameFile; - HttpRequest = WebRequest.CreateHttp(Uri); - - HttpRequest.CookieContainer = CookieContainer; - HttpRequest.Headers = RequestHeaders; - //If NetworkFileStream is resuming, Header will already contain a range. - HttpRequest.Headers.Remove("Range"); - HttpRequest.AddRange(WritePosition); + RequestHeaders["Range"] = $"bytes={WritePosition}-"; } /// /// Begins downloading to in a background thread. /// - private void BeginDownloading() + private Task BeginDownloading() { - downloadEnded = new EventWaitHandle(false, EventResetMode.ManualReset); - if (ContentLength != 0 && WritePosition == ContentLength) - { - hasBegunDownloading = true; - downloadEnded.Set(); - return; - } + return Task.CompletedTask; if (ContentLength != 0 && WritePosition > ContentLength) throw new WebException($"Specified write position (0x{WritePosition:X10}) is larger than {nameof(ContentLength)} (0x{ContentLength:X10})."); - var response = HttpRequest.GetResponse() as HttpWebResponse; + + var request = new HttpRequestMessage(HttpMethod.Get, Uri); + + foreach (var header in RequestHeaders) + request.Headers.Add(header.Key, header.Value); + + var response = new HttpClient().Send(request, HttpCompletionOption.ResponseHeadersRead); if (response.StatusCode != HttpStatusCode.PartialContent) throw new WebException($"Server at {Uri.Host} responded with unexpected status code: {response.StatusCode}."); @@ -203,24 +170,19 @@ namespace AaxDecrypter //Content length is the length of the range request, and it is only equal //to the complete file length if requesting Range: bytes=0- if (WritePosition == 0) - ContentLength = response.ContentLength; + ContentLength = response.Content.Headers.ContentLength.GetValueOrDefault(); - _networkStream = response.GetResponseStream(); - downloadedPiece = new EventWaitHandle(false, EventResetMode.AutoReset); + var networkStream = response.Content.ReadAsStream(); + _downloadedPiece = new EventWaitHandle(false, EventResetMode.AutoReset); //Download the file in the background. - new Thread(() => DownloadFile()) - { IsBackground = true } - .Start(); - - hasBegunDownloading = true; - return; + return Task.Run(() => DownloadFile(networkStream)); } /// /// Download to . /// - private void DownloadFile() + private void DownloadFile(Stream networkStream) { var downloadPosition = WritePosition; var nextFlush = downloadPosition + DATA_FLUSH_SZ; @@ -231,7 +193,7 @@ namespace AaxDecrypter int bytesRead; do { - bytesRead = _networkStream.Read(buff, 0, DOWNLOAD_BUFF_SZ); + bytesRead = networkStream.Read(buff, 0, DOWNLOAD_BUFF_SZ); _writeFile.Write(buff, 0, bytesRead); downloadPosition += bytesRead; @@ -242,15 +204,12 @@ namespace AaxDecrypter WritePosition = downloadPosition; Update(); nextFlush = downloadPosition + DATA_FLUSH_SZ; - downloadedPiece.Set(); + _downloadedPiece.Set(); } } while (downloadPosition < ContentLength && !IsCancelled && bytesRead > 0); - _writeFile.Close(); - _networkStream.Close(); WritePosition = downloadPosition; - Update(); if (!IsCancelled && WritePosition < ContentLength) throw new WebException($"Downloaded size (0x{WritePosition:X10}) is less than {nameof(ContentLength)} (0x{ContentLength:X10})."); @@ -264,8 +223,10 @@ namespace AaxDecrypter } finally { - downloadedPiece.Set(); - downloadEnded.Set(); + networkStream.Close(); + _writeFile.Close(); + _downloadedPiece.Set(); + Update(); } } @@ -274,96 +235,7 @@ namespace AaxDecrypter #region Json Connverters public static JsonSerializerSettings GetJsonSerializerSettings() - { - var settings = new JsonSerializerSettings(); - settings.Converters.Add(new CookieContainerConverter()); - settings.Converters.Add(new WebHeaderCollectionConverter()); - return settings; - } - - internal class CookieContainerConverter : JsonConverter - { - public override bool CanConvert(Type objectType) - => objectType == typeof(SingleUriCookieContainer); - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - var jObj = JObject.Load(reader); - - var result = new SingleUriCookieContainer() - { - Uri = new Uri(jObj["Uri"].Value()), - Capacity = jObj["Capacity"].Value(), - MaxCookieSize = jObj["MaxCookieSize"].Value(), - PerDomainCapacity = jObj["PerDomainCapacity"].Value() - }; - - var cookieList = jObj["Cookies"].ToList(); - - foreach (var cookie in cookieList) - { - result.Add( - new Cookie - { - Comment = cookie["Comment"].Value(), - HttpOnly = cookie["HttpOnly"].Value(), - Discard = cookie["Discard"].Value(), - Domain = cookie["Domain"].Value(), - Expired = cookie["Expired"].Value(), - Expires = cookie["Expires"].Value(), - Name = cookie["Name"].Value(), - Path = cookie["Path"].Value(), - Port = cookie["Port"].Value(), - Secure = cookie["Secure"].Value(), - Value = cookie["Value"].Value(), - Version = cookie["Version"].Value(), - }); - } - - return result; - } - - public override bool CanWrite => true; - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - var cookies = value as SingleUriCookieContainer; - var obj = (JObject)JToken.FromObject(value); - var container = cookies.GetCookies(); - var propertyNames = container.Select(c => JToken.FromObject(c)); - obj.AddFirst(new JProperty("Cookies", new JArray(propertyNames))); - obj.WriteTo(writer); - } - } - - internal class WebHeaderCollectionConverter : JsonConverter - { - public override bool CanConvert(Type objectType) - => objectType == typeof(WebHeaderCollection); - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - var jObj = JObject.Load(reader); - var result = new WebHeaderCollection(); - - foreach (var kvp in jObj) - result.Add(kvp.Key, kvp.Value.Value()); - - return result; - } - - public override bool CanWrite => true; - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - var jObj = new JObject(); - var type = value.GetType(); - var headers = value as WebHeaderCollection; - var jHeaders = headers.AllKeys.Select(k => new JProperty(k, headers[k])); - jObj.Add(jHeaders); - jObj.WriteTo(writer); - } - } + => new JsonSerializerSettings(); #endregion @@ -383,8 +255,7 @@ namespace AaxDecrypter { get { - if (!hasBegunDownloading) - BeginDownloading(); + _backgroundDownloadTask ??= BeginDownloading(); return ContentLength; } } @@ -401,15 +272,14 @@ namespace AaxDecrypter [JsonIgnore] public override int WriteTimeout { get => base.WriteTimeout; set => base.WriteTimeout = value; } - public override void Flush() => throw new NotImplementedException(); - public override void SetLength(long value) => throw new NotImplementedException(); - public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException(); + public override void Flush() => throw new InvalidOperationException(); + public override void SetLength(long value) => throw new InvalidOperationException(); + public override void Write(byte[] buffer, int offset, int count) => throw new InvalidOperationException(); public override int Read(byte[] buffer, int offset, int count) { - if (!hasBegunDownloading) - BeginDownloading(); - + _backgroundDownloadTask ??= BeginDownloading(); + var toRead = Math.Min(count, Length - Position); WaitToPosition(Position + toRead); return _readFile.Read(buffer, offset, count); @@ -435,31 +305,27 @@ namespace AaxDecrypter private void WaitToPosition(long requiredPosition) { while (WritePosition < requiredPosition - && hasBegunDownloading - && !IsCancelled - && !downloadEnded.WaitOne(0)) + && _backgroundDownloadTask?.IsCompleted is false + && !IsCancelled) { - downloadedPiece.WaitOne(100); + _downloadedPiece.WaitOne(100); } } public override void Close() { IsCancelled = true; - - while (downloadEnded is not null && !downloadEnded.WaitOne(100)) ; + _backgroundDownloadTask?.Wait(); _readFile.Close(); _writeFile.Close(); - _networkStream?.Close(); Update(); } #endregion ~NetworkFileStream() { - downloadEnded?.Close(); - downloadedPiece?.Close(); + _downloadedPiece?.Close(); } } }