From cef15887a4e3cdaddd9860c896aae930fb92a9e1 Mon Sep 17 00:00:00 2001 From: ta264 Date: Wed, 10 Nov 2021 21:59:08 +0000 Subject: [PATCH] Use modern HttpClient (cherry picked from commit 402f8b296f17bf161824ec5ff40d67d036d00d94) --- .../Http/HttpClientFixture.cs | 69 +++++- .../Http/Dispatchers/IHttpDispatcher.cs | 1 - .../Http/Dispatchers/ManagedHttpDispatcher.cs | 208 ++++++++---------- src/NzbDrone.Common/Http/GZipWebClient.cs | 15 -- src/NzbDrone.Common/Http/HttpClient.cs | 103 ++++++--- src/NzbDrone.Common/Http/HttpHeader.cs | 21 ++ src/NzbDrone.Common/Http/HttpMethod.cs | 14 -- src/NzbDrone.Common/Http/HttpRequest.cs | 2 + .../Http/HttpRequestBuilder.cs | 9 +- .../Http/JsonRpcRequestBuilder.cs | 7 +- src/NzbDrone.Core.Test/Framework/CoreTest.cs | 2 +- .../FileListTests/FileListFixture.cs | 3 +- .../GazelleTests/GazelleFixture.cs | 7 +- .../IPTorrentsTests/IPTorrentsFixture.cs | 3 +- .../NewznabTests/NewznabFixture.cs | 3 +- .../IndexerTests/NyaaTests/NyaaFixture.cs | 5 +- .../OmgwtfnzbsTests/OmgwtfnzbsFixture.cs | 5 +- .../IndexerTests/RarbgTests/RarbgFixture.cs | 9 +- .../TorrentleechTests/TorrentleechFixture.cs | 5 +- .../TorznabTests/TorznabFixture.cs | 5 +- .../Proxies/DiskStationProxyBase.cs | 11 +- .../Proxies/DownloadStationTaskProxyV1.cs | 5 +- .../Proxies/DownloadStationTaskProxyV2.cs | 3 +- .../Download/Clients/Flood/FloodProxy.cs | 9 +- .../Goodreads/GoodreadsImportListBase.cs | 3 +- .../Gazelle/GazelleRequestGenerator.cs | 5 +- .../Messaging/Commands/CommandExecutor.cs | 5 - .../Notifications/Discord/DiscordProxy.cs | 3 +- .../Goodreads/GoodreadsNotificationBase.cs | 7 +- .../Notifications/Join/JoinProxy.cs | 3 +- .../Notifications/Mailgun/MailgunProxy.cs | 4 +- .../PushBullet/PushBulletProxy.cs | 3 +- .../Notifications/SendGrid/SendGridProxy.cs | 5 +- .../Notifications/Slack/SlackProxy.cs | 3 +- .../Subsonic/SubsonicServerProxy.cs | 7 +- .../Notifications/Webhook/WebhookMethod.cs | 6 +- .../Notifications/Webhook/WebhookProxy.cs | 11 +- src/NzbDrone.Core/TinyTwitter.cs | 67 +----- .../IndexHtmlFixture.cs | 26 ++- 39 files changed, 368 insertions(+), 314 deletions(-) delete mode 100644 src/NzbDrone.Common/Http/GZipWebClient.cs delete mode 100644 src/NzbDrone.Common/Http/HttpMethod.cs diff --git a/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs b/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs index 200864ede..22d790969 100644 --- a/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs +++ b/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs @@ -17,6 +17,7 @@ using NzbDrone.Common.Http.Proxy; using NzbDrone.Common.TPL; using NzbDrone.Test.Common; using NzbDrone.Test.Common.Categories; +using HttpClient = NzbDrone.Common.Http.HttpClient; namespace NzbDrone.Common.Test.Http { @@ -31,6 +32,8 @@ namespace NzbDrone.Common.Test.Http private string _httpBinHost; private string _httpBinHost2; + private System.Net.Http.HttpClient _httpClient = new (); + [OneTimeSetUp] public void FixtureSetUp() { @@ -53,22 +56,13 @@ namespace NzbDrone.Common.Test.Http { try { - var req = WebRequest.Create($"https://{site}/get") as HttpWebRequest; - var res = req.GetResponse() as HttpWebResponse; + var res = _httpClient.GetAsync($"https://{site}/get").GetAwaiter().GetResult(); if (res.StatusCode != HttpStatusCode.OK) { return false; } - try - { - req = WebRequest.Create($"https://{site}/status/429") as HttpWebRequest; - res = req.GetResponse() as HttpWebResponse; - } - catch (WebException ex) - { - res = ex.Response as HttpWebResponse; - } + res = _httpClient.GetAsync($"https://{site}/status/429").GetAwaiter().GetResult(); if (res == null || res.StatusCode != (HttpStatusCode)429) { @@ -165,7 +159,9 @@ namespace NzbDrone.Common.Test.Http var response = Subject.Get(request); response.Resource.Headers["Accept-Encoding"].ToString().Should().Contain("gzip"); + response.Resource.Gzipped.Should().BeTrue(); + response.Resource.Brotli.Should().BeFalse(); } [Test] @@ -176,6 +172,8 @@ namespace NzbDrone.Common.Test.Http var response = Subject.Get(request); response.Resource.Headers["Accept-Encoding"].ToString().Should().Contain("br"); + + response.Resource.Gzipped.Should().BeFalse(); response.Resource.Brotli.Should().BeTrue(); } @@ -358,13 +356,38 @@ namespace NzbDrone.Common.Test.Http { var file = GetTempFilePath(); - Assert.Throws(() => Subject.DownloadFile("https://download.readarr.com/wrongpath", file)); + Assert.Throws(() => Subject.DownloadFile("https://download.sonarr.tv/wrongpath", file)); File.Exists(file).Should().BeFalse(); ExceptionVerification.ExpectedWarns(1); } + [Test] + public void should_not_write_redirect_content_to_stream() + { + var file = GetTempFilePath(); + + using (var fileStream = new FileStream(file, FileMode.Create)) + { + var request = new HttpRequest($"http://{_httpBinHost}/redirect/1"); + request.AllowAutoRedirect = false; + request.ResponseStream = fileStream; + + var response = Subject.Get(request); + + response.StatusCode.Should().Be(HttpStatusCode.Moved); + } + + ExceptionVerification.ExpectedErrors(1); + + File.Exists(file).Should().BeTrue(); + + var fileInfo = new FileInfo(file); + + fileInfo.Length.Should().Be(0); + } + [Test] public void should_send_cookie() { @@ -784,6 +807,28 @@ namespace NzbDrone.Common.Test.Http { } } + + [Test] + public void should_correctly_use_basic_auth() + { + var request = new HttpRequest($"https://{_httpBinHost}/basic-auth/username/password"); + request.Credentials = new BasicNetworkCredential("username", "password"); + + var response = Subject.Execute(request); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + } + + [Test] + public void should_correctly_use_digest_auth() + { + var request = new HttpRequest($"https://{_httpBinHost}/digest-auth/auth/username/password"); + request.Credentials = new NetworkCredential("username", "password"); + + var response = Subject.Execute(request); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + } } public class HttpBinResource diff --git a/src/NzbDrone.Common/Http/Dispatchers/IHttpDispatcher.cs b/src/NzbDrone.Common/Http/Dispatchers/IHttpDispatcher.cs index b4cae7ff8..8e665ceed 100644 --- a/src/NzbDrone.Common/Http/Dispatchers/IHttpDispatcher.cs +++ b/src/NzbDrone.Common/Http/Dispatchers/IHttpDispatcher.cs @@ -5,6 +5,5 @@ namespace NzbDrone.Common.Http.Dispatchers public interface IHttpDispatcher { HttpResponse GetResponse(HttpRequest request, CookieContainer cookies); - void DownloadFile(string url, string fileName); } } diff --git a/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs b/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs index 60da0aa65..dbedd7fcf 100644 --- a/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs +++ b/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs @@ -1,10 +1,12 @@ using System; -using System.Diagnostics; using System.IO; using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; using System.Text; +using System.Threading; using NLog; -using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http.Proxy; @@ -12,120 +14,113 @@ namespace NzbDrone.Common.Http.Dispatchers { public class ManagedHttpDispatcher : IHttpDispatcher { + private const string NO_PROXY_KEY = "no-proxy"; + private readonly IHttpProxySettingsProvider _proxySettingsProvider; private readonly ICreateManagedWebProxy _createManagedWebProxy; private readonly IUserAgentBuilder _userAgentBuilder; - private readonly IPlatformInfo _platformInfo; + private readonly ICached _httpClientCache; + private readonly ICached _credentialCache; private readonly Logger _logger; - public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider, ICreateManagedWebProxy createManagedWebProxy, IUserAgentBuilder userAgentBuilder, IPlatformInfo platformInfo, Logger logger) + public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider, + ICreateManagedWebProxy createManagedWebProxy, + IUserAgentBuilder userAgentBuilder, + ICacheManager cacheManager, + Logger logger) { _proxySettingsProvider = proxySettingsProvider; _createManagedWebProxy = createManagedWebProxy; _userAgentBuilder = userAgentBuilder; - _platformInfo = platformInfo; _logger = logger; + + _httpClientCache = cacheManager.GetCache(typeof(ManagedHttpDispatcher), "httpclient"); + _credentialCache = cacheManager.GetCache(typeof(ManagedHttpDispatcher), "credentialcache"); } public HttpResponse GetResponse(HttpRequest request, CookieContainer cookies) { - var webRequest = (HttpWebRequest)WebRequest.Create((Uri)request.Url); - - // Deflate is not a standard and could break depending on implementation. - // we should just stick with the more compatible Gzip - //http://stackoverflow.com/questions/8490718/how-to-decompress-stream-deflated-with-java-util-zip-deflater-in-net - webRequest.AutomaticDecompression = DecompressionMethods.Brotli | DecompressionMethods.GZip; + var requestMessage = new HttpRequestMessage(request.Method, (Uri)request.Url); + requestMessage.Headers.UserAgent.ParseAdd(_userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent)); + requestMessage.Headers.ConnectionClose = !request.ConnectionKeepAlive; - webRequest.Method = request.Method.ToString(); - webRequest.UserAgent = _userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent); - webRequest.KeepAlive = request.ConnectionKeepAlive; - webRequest.AllowAutoRedirect = false; - webRequest.CookieContainer = cookies; + var cookieHeader = cookies.GetCookieHeader((Uri)request.Url); + if (cookieHeader.IsNotNullOrWhiteSpace()) + { + requestMessage.Headers.Add("Cookie", cookieHeader); + } if (request.Credentials != null) { - if (request.Credentials is BasicNetworkCredential nc) + if (request.Credentials is BasicNetworkCredential bc) { // Manually set header to avoid initial challenge response - var authInfo = nc.UserName + ":" + nc.Password; + var authInfo = bc.UserName + ":" + bc.Password; authInfo = Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes(authInfo)); - webRequest.Headers.Add("Authorization", "Basic " + authInfo); + requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Basic", authInfo); } - else + else if (request.Credentials is NetworkCredential nc) { - webRequest.PreAuthenticate = true; - webRequest.Credentials = request.Credentials; + var creds = GetCredentialCache(); + creds.Remove((Uri)request.Url, "Digest"); + creds.Add((Uri)request.Url, "Digest", nc); } } + using var cts = new CancellationTokenSource(); if (request.RequestTimeout != TimeSpan.Zero) { - webRequest.Timeout = (int)Math.Ceiling(request.RequestTimeout.TotalMilliseconds); + cts.CancelAfter(request.RequestTimeout); + } + else + { + // The default for System.Net.Http.HttpClient + cts.CancelAfter(TimeSpan.FromSeconds(100)); } - - webRequest.Proxy = GetProxy(request.Url); if (request.Headers != null) { - AddRequestHeaders(webRequest, request.Headers); + AddRequestHeaders(requestMessage, request.Headers); } - HttpWebResponse httpWebResponse; + var httpClient = GetClient(request.Url); + + HttpResponseMessage responseMessage; try { if (request.ContentData != null) { - webRequest.ContentLength = request.ContentData.Length; - using (var writeStream = webRequest.GetRequestStream()) + var content = new ByteArrayContent(request.ContentData); + content.Headers.Remove("Content-Type"); + if (request.Headers.ContentType.IsNotNullOrWhiteSpace()) { - writeStream.Write(request.ContentData, 0, request.ContentData.Length); + content.Headers.Add("Content-Type", request.Headers.ContentType); } + + requestMessage.Content = content; } - httpWebResponse = (HttpWebResponse)webRequest.GetResponse(); + responseMessage = httpClient.Send(requestMessage, cts.Token); } - catch (WebException e) + catch (HttpRequestException e) { - httpWebResponse = (HttpWebResponse)e.Response; - - if (httpWebResponse == null) - { - // The default messages for WebException on mono are pretty horrible. - if (e.Status == WebExceptionStatus.NameResolutionFailure) - { - throw new WebException($"DNS Name Resolution Failure: '{webRequest.RequestUri.Host}'", e.Status); - } - else if (e.ToString().Contains("TLS Support not")) - { - throw new TlsFailureException(webRequest, e); - } - else if (e.ToString().Contains("The authentication or decryption has failed.")) - { - throw new TlsFailureException(webRequest, e); - } - else if (OsInfo.IsNotWindows) - { - throw new WebException($"{e.Message}: '{webRequest.RequestUri}'", e, e.Status, e.Response); - } - else - { - throw; - } - } + _logger.Error(e, "HttpClient error"); + throw; } byte[] data = null; - using (var responseStream = httpWebResponse.GetResponseStream()) + using (var responseStream = responseMessage.Content.ReadAsStream()) { if (responseStream != null && responseStream != Stream.Null) { try { - if (request.ResponseStream != null) + if (request.ResponseStream != null && responseMessage.StatusCode == HttpStatusCode.OK) { // A target ResponseStream was specified, write to that instead. + // But only on the OK status code, since we don't want to write failures and redirects. responseStream.CopyTo(request.ResponseStream); } else @@ -135,102 +130,88 @@ namespace NzbDrone.Common.Http.Dispatchers } catch (Exception ex) { - throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, httpWebResponse); + throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, null); } } } - return new HttpResponse(request, new HttpHeader(httpWebResponse.Headers), data, httpWebResponse.StatusCode); + return new HttpResponse(request, new HttpHeader(responseMessage.Headers), data, responseMessage.StatusCode); } - public void DownloadFile(string url, string fileName) + protected virtual System.Net.Http.HttpClient GetClient(HttpUri uri) { - try - { - var fileInfo = new FileInfo(fileName); - if (fileInfo.Directory != null && !fileInfo.Directory.Exists) - { - fileInfo.Directory.Create(); - } - - _logger.Debug("Downloading [{0}] to [{1}]", url, fileName); + var proxySettings = _proxySettingsProvider.GetProxySettings(uri); - var stopWatch = Stopwatch.StartNew(); - var uri = new HttpUri(url); + var key = proxySettings?.Key ?? NO_PROXY_KEY; - using (var webClient = new GZipWebClient()) - { - webClient.Headers.Add(HttpRequestHeader.UserAgent, _userAgentBuilder.GetUserAgent()); - webClient.Proxy = GetProxy(uri); - webClient.DownloadFile(uri.FullUri, fileName); - stopWatch.Stop(); - _logger.Debug("Downloading Completed. took {0:0}s", stopWatch.Elapsed.Seconds); - } - } - catch (WebException e) - { - _logger.Warn("Failed to get response from: {0} {1}", url, e.Message); - throw; - } - catch (Exception e) - { - _logger.Warn(e, "Failed to get response from: " + url); - throw; - } + return _httpClientCache.Get(key, () => CreateHttpClient(proxySettings)); } - protected virtual IWebProxy GetProxy(HttpUri uri) + protected virtual System.Net.Http.HttpClient CreateHttpClient(HttpProxySettings proxySettings) { - IWebProxy proxy = null; - - var proxySettings = _proxySettingsProvider.GetProxySettings(uri); + var handler = new HttpClientHandler() + { + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Brotli, + UseCookies = false, // sic - we don't want to use a shared cookie container + AllowAutoRedirect = false, + Credentials = GetCredentialCache(), + PreAuthenticate = true + }; if (proxySettings != null) { - proxy = _createManagedWebProxy.GetWebProxy(proxySettings); + handler.Proxy = _createManagedWebProxy.GetWebProxy(proxySettings); } - return proxy; + var client = new System.Net.Http.HttpClient(handler) + { + Timeout = Timeout.InfiniteTimeSpan + }; + + return client; } - protected virtual void AddRequestHeaders(HttpWebRequest webRequest, HttpHeader headers) + protected virtual void AddRequestHeaders(HttpRequestMessage webRequest, HttpHeader headers) { foreach (var header in headers) { switch (header.Key) { case "Accept": - webRequest.Accept = header.Value; + webRequest.Headers.Accept.ParseAdd(header.Value); break; case "Connection": - webRequest.Connection = header.Value; + webRequest.Headers.Connection.Clear(); + webRequest.Headers.Connection.Add(header.Value); break; case "Content-Length": - webRequest.ContentLength = Convert.ToInt64(header.Value); + webRequest.Headers.Add("Content-Length", header.Value); break; case "Content-Type": - webRequest.ContentType = header.Value; + webRequest.Headers.Remove("Content-Type"); + webRequest.Headers.Add("Content-Type", header.Value); break; case "Date": - webRequest.Date = HttpHeader.ParseDateTime(header.Value); + webRequest.Headers.Remove("Date"); + webRequest.Headers.Date = HttpHeader.ParseDateTime(header.Value); break; case "Expect": - webRequest.Expect = header.Value; + webRequest.Headers.Expect.ParseAdd(header.Value); break; case "Host": - webRequest.Host = header.Value; + webRequest.Headers.Host = header.Value; break; case "If-Modified-Since": - webRequest.IfModifiedSince = HttpHeader.ParseDateTime(header.Value); + webRequest.Headers.IfModifiedSince = HttpHeader.ParseDateTime(header.Value); break; case "Referer": - webRequest.Referer = header.Value; + webRequest.Headers.Add("Referer", header.Value); break; case "Transfer-Encoding": - webRequest.TransferEncoding = header.Value; + webRequest.Headers.TransferEncoding.ParseAdd(header.Value); break; case "User-Agent": - webRequest.UserAgent = header.Value; + webRequest.Headers.UserAgent.ParseAdd(header.Value); break; case "Proxy-Connection": throw new NotImplementedException(); @@ -240,5 +221,10 @@ namespace NzbDrone.Common.Http.Dispatchers } } } + + private CredentialCache GetCredentialCache() + { + return _credentialCache.Get("credentialCache", () => new CredentialCache()); + } } } diff --git a/src/NzbDrone.Common/Http/GZipWebClient.cs b/src/NzbDrone.Common/Http/GZipWebClient.cs deleted file mode 100644 index 191bfb10b..000000000 --- a/src/NzbDrone.Common/Http/GZipWebClient.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Net; - -namespace NzbDrone.Common.Http -{ - public class GZipWebClient : WebClient - { - protected override WebRequest GetWebRequest(Uri address) - { - var request = (HttpWebRequest)base.GetWebRequest(address); - request.AutomaticDecompression = DecompressionMethods.GZip; - return request; - } - } -} diff --git a/src/NzbDrone.Common/Http/HttpClient.cs b/src/NzbDrone.Common/Http/HttpClient.cs index 7ca7e9188..416901173 100644 --- a/src/NzbDrone.Common/Http/HttpClient.cs +++ b/src/NzbDrone.Common/Http/HttpClient.cs @@ -1,8 +1,10 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using System.Net; +using System.Net.Http; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.EnvironmentInfo; @@ -119,8 +121,6 @@ namespace NzbDrone.Common.Http var stopWatch = Stopwatch.StartNew(); - PrepareRequestCookies(request, cookieContainer); - var response = _httpDispatcher.GetResponse(request, cookieContainer); HandleResponseCookies(response, cookieContainer); @@ -187,57 +187,98 @@ namespace NzbDrone.Common.Http } } - private void PrepareRequestCookies(HttpRequest request, CookieContainer cookieContainer) + private void HandleResponseCookies(HttpResponse response, CookieContainer container) { - // Don't collect persistnet cookies for intermediate/redirected urls. - /*lock (_cookieContainerCache) + foreach (Cookie cookie in container.GetCookies((Uri)response.Request.Url)) { - var presistentContainer = _cookieContainerCache.Get("container", () => new CookieContainer()); - var persistentCookies = presistentContainer.GetCookies((Uri)request.Url); - var existingCookies = cookieContainer.GetCookies((Uri)request.Url); - - cookieContainer.Add(persistentCookies); - cookieContainer.Add(existingCookies); - }*/ - } + cookie.Expired = true; + } - private void HandleResponseCookies(HttpResponse response, CookieContainer cookieContainer) - { var cookieHeaders = response.GetCookieHeaders(); + if (cookieHeaders.Empty()) { return; } + AddCookiesToContainer(response.Request.Url, cookieHeaders, container); + if (response.Request.StoreResponseCookie) { lock (_cookieContainerCache) { var persistentCookieContainer = _cookieContainerCache.Get("container", () => new CookieContainer()); - foreach (var cookieHeader in cookieHeaders) - { - try - { - persistentCookieContainer.SetCookies((Uri)response.Request.Url, cookieHeader); - } - catch (Exception ex) - { - _logger.Debug(ex, "Invalid cookie in {0}", response.Request.Url); - } - } + AddCookiesToContainer(response.Request.Url, cookieHeaders, persistentCookieContainer); + } + } + } + + private void AddCookiesToContainer(HttpUri url, string[] cookieHeaders, CookieContainer container) + { + foreach (var cookieHeader in cookieHeaders) + { + try + { + container.SetCookies((Uri)url, cookieHeader); + } + catch (Exception ex) + { + _logger.Debug(ex, "Invalid cookie in {0}", url); } } } public void DownloadFile(string url, string fileName, string userAgent = null) { - _httpDispatcher.DownloadFile(url, fileName); + var fileNamePart = fileName + ".part"; + + try + { + var fileInfo = new FileInfo(fileName); + if (fileInfo.Directory != null && !fileInfo.Directory.Exists) + { + fileInfo.Directory.Create(); + } + + _logger.Debug("Downloading [{0}] to [{1}]", url, fileName); + + var stopWatch = Stopwatch.StartNew(); + using (var fileStream = new FileStream(fileNamePart, FileMode.Create, FileAccess.ReadWrite)) + { + var request = new HttpRequest(url); + request.AllowAutoRedirect = true; + request.ResponseStream = fileStream; + var response = Get(request); + + if (response.Headers.ContentType != null && response.Headers.ContentType.Contains("text/html")) + { + throw new HttpException(request, response, "Site responded with html content."); + } + } + + stopWatch.Stop(); + + if (File.Exists(fileName)) + { + File.Delete(fileName); + } + + File.Move(fileNamePart, fileName); + _logger.Debug("Downloading Completed. took {0:0}s", stopWatch.Elapsed.Seconds); + } + finally + { + if (File.Exists(fileNamePart)) + { + File.Delete(fileNamePart); + } + } } public HttpResponse Get(HttpRequest request) { - request.Method = HttpMethod.GET; + request.Method = HttpMethod.Get; return Execute(request); } @@ -251,13 +292,13 @@ namespace NzbDrone.Common.Http public HttpResponse Head(HttpRequest request) { - request.Method = HttpMethod.HEAD; + request.Method = HttpMethod.Head; return Execute(request); } public HttpResponse Post(HttpRequest request) { - request.Method = HttpMethod.POST; + request.Method = HttpMethod.Post; return Execute(request); } diff --git a/src/NzbDrone.Common/Http/HttpHeader.cs b/src/NzbDrone.Common/Http/HttpHeader.cs index 4aee4092f..63fad846d 100644 --- a/src/NzbDrone.Common/Http/HttpHeader.cs +++ b/src/NzbDrone.Common/Http/HttpHeader.cs @@ -4,11 +4,27 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Globalization; using System.Linq; +using System.Net; +using System.Net.Http.Headers; using System.Text; using NzbDrone.Common.Extensions; namespace NzbDrone.Common.Http { + public static class WebHeaderCollectionExtensions + { + public static NameValueCollection ToNameValueCollection(this HttpHeaders headers) + { + var result = new NameValueCollection(); + foreach (var header in headers) + { + result.Add(header.Key, header.Value.ConcatToString(";")); + } + + return result; + } + } + public class HttpHeader : NameValueCollection, IEnumerable>, IEnumerable { public HttpHeader(NameValueCollection headers) @@ -16,6 +32,11 @@ namespace NzbDrone.Common.Http { } + public HttpHeader(HttpHeaders headers) + : base(headers.ToNameValueCollection()) + { + } + public HttpHeader() { } diff --git a/src/NzbDrone.Common/Http/HttpMethod.cs b/src/NzbDrone.Common/Http/HttpMethod.cs deleted file mode 100644 index 8964bbef6..000000000 --- a/src/NzbDrone.Common/Http/HttpMethod.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace NzbDrone.Common.Http -{ - public enum HttpMethod - { - GET, - POST, - PUT, - DELETE, - HEAD, - OPTIONS, - PATCH, - MERGE - } -} diff --git a/src/NzbDrone.Common/Http/HttpRequest.cs b/src/NzbDrone.Common/Http/HttpRequest.cs index a0f5b8e45..c9ef99f62 100644 --- a/src/NzbDrone.Common/Http/HttpRequest.cs +++ b/src/NzbDrone.Common/Http/HttpRequest.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Net; +using System.Net.Http; using System.Text; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; @@ -12,6 +13,7 @@ namespace NzbDrone.Common.Http { public HttpRequest(string url, HttpAccept httpAccept = null) { + Method = HttpMethod.Get; Url = new HttpUri(url); Headers = new HttpHeader(); AllowAutoRedirect = true; diff --git a/src/NzbDrone.Common/Http/HttpRequestBuilder.cs b/src/NzbDrone.Common/Http/HttpRequestBuilder.cs index bdbf3bb1c..32fafd56f 100644 --- a/src/NzbDrone.Common/Http/HttpRequestBuilder.cs +++ b/src/NzbDrone.Common/Http/HttpRequestBuilder.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; +using System.Net.Http; using System.Text; using NzbDrone.Common.Extensions; @@ -34,7 +35,7 @@ namespace NzbDrone.Common.Http { BaseUrl = new HttpUri(baseUrl); ResourceUrl = string.Empty; - Method = HttpMethod.GET; + Method = HttpMethod.Get; QueryParams = new List>(); SuffixQueryParams = new List>(); Segments = new Dictionary(); @@ -264,7 +265,7 @@ namespace NzbDrone.Common.Http public virtual HttpRequestBuilder Post() { - Method = HttpMethod.POST; + Method = HttpMethod.Post; return this; } @@ -355,7 +356,7 @@ namespace NzbDrone.Common.Http public virtual HttpRequestBuilder AddFormParameter(string key, object value) { - if (Method != HttpMethod.POST) + if (Method != HttpMethod.Post) { throw new NotSupportedException("HttpRequest Method must be POST to add FormParameter."); } @@ -371,7 +372,7 @@ namespace NzbDrone.Common.Http public virtual HttpRequestBuilder AddFormUpload(string name, string fileName, byte[] data, string contentType = "application/octet-stream") { - if (Method != HttpMethod.POST) + if (Method != HttpMethod.Post) { throw new NotSupportedException("HttpRequest Method must be POST to add FormUpload."); } diff --git a/src/NzbDrone.Common/Http/JsonRpcRequestBuilder.cs b/src/NzbDrone.Common/Http/JsonRpcRequestBuilder.cs index ae987a23d..06b113e54 100644 --- a/src/NzbDrone.Common/Http/JsonRpcRequestBuilder.cs +++ b/src/NzbDrone.Common/Http/JsonRpcRequestBuilder.cs @@ -1,6 +1,7 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; +using System.Net.Http; using Newtonsoft.Json; using NzbDrone.Common.Serializer; @@ -17,14 +18,14 @@ namespace NzbDrone.Common.Http public JsonRpcRequestBuilder(string baseUrl) : base(baseUrl) { - Method = HttpMethod.POST; + Method = HttpMethod.Post; JsonParameters = new List(); } public JsonRpcRequestBuilder(string baseUrl, string method, IEnumerable parameters) : base(baseUrl) { - Method = HttpMethod.POST; + Method = HttpMethod.Post; JsonMethod = method; JsonParameters = parameters.ToList(); } diff --git a/src/NzbDrone.Core.Test/Framework/CoreTest.cs b/src/NzbDrone.Core.Test/Framework/CoreTest.cs index 420541731..ebb0611b5 100644 --- a/src/NzbDrone.Core.Test/Framework/CoreTest.cs +++ b/src/NzbDrone.Core.Test/Framework/CoreTest.cs @@ -25,7 +25,7 @@ namespace NzbDrone.Core.Test.Framework Mocker.SetConstant(new HttpProxySettingsProvider(Mocker.Resolve())); Mocker.SetConstant(new ManagedWebProxyFactory(Mocker.Resolve())); - Mocker.SetConstant(new ManagedHttpDispatcher(Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve(), TestLogger)); + Mocker.SetConstant(new ManagedHttpDispatcher(Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve(), TestLogger)); Mocker.SetConstant(new HttpClient(new IHttpRequestInterceptor[0], Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve(), TestLogger)); Mocker.SetConstant(new ReadarrCloudRequestBuilder()); Mocker.SetConstant(Mocker.Resolve()); diff --git a/src/NzbDrone.Core.Test/IndexerTests/FileListTests/FileListFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/FileListTests/FileListFixture.cs index cd4da25ee..732825188 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/FileListTests/FileListFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/FileListTests/FileListFixture.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Net.Http; using FluentAssertions; using Moq; using NUnit.Framework; @@ -30,7 +31,7 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests var recentFeed = ReadAllText(@"Files/Indexers/FileList/RecentFeed.json"); Mocker.GetMock() - .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.GET))) + .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.Get))) .Returns(r => new HttpResponse(r, new HttpHeader(), recentFeed)); var releases = Subject.FetchRecent(); diff --git a/src/NzbDrone.Core.Test/IndexerTests/GazelleTests/GazelleFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/GazelleTests/GazelleFixture.cs index eece75042..76a1325ba 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/GazelleTests/GazelleFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/GazelleTests/GazelleFixture.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Net.Http; using FluentAssertions; using Moq; using NUnit.Framework; @@ -35,15 +36,15 @@ namespace NzbDrone.Core.Test.IndexerTests.GazelleTests var indexFeed = ReadAllText(@"Files/Indexers/Gazelle/GazelleIndex.json"); Mocker.GetMock() - .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.GET && v.Url.FullUri.Contains("ajax.php?action=browse")))) + .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.Get && v.Url.FullUri.Contains("ajax.php?action=browse")))) .Returns(r => new HttpResponse(r, new HttpHeader { ContentType = "application/json" }, recentFeed)); Mocker.GetMock() - .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.POST && v.Url.FullUri.Contains("ajax.php?action=index")))) + .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.Post && v.Url.FullUri.Contains("ajax.php?action=index")))) .Returns(r => new HttpResponse(r, new HttpHeader(), indexFeed)); Mocker.GetMock() - .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.POST && v.Url.FullUri.Contains("login.php")))) + .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.Post && v.Url.FullUri.Contains("login.php")))) .Returns(r => new HttpResponse(r, new HttpHeader(), indexFeed)); var releases = Subject.FetchRecent(); diff --git a/src/NzbDrone.Core.Test/IndexerTests/IPTorrentsTests/IPTorrentsFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/IPTorrentsTests/IPTorrentsFixture.cs index cabc82040..14238d93f 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/IPTorrentsTests/IPTorrentsFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/IPTorrentsTests/IPTorrentsFixture.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Net.Http; using FluentAssertions; using Moq; using NUnit.Framework; @@ -88,7 +89,7 @@ namespace NzbDrone.Core.Test.IndexerTests.IPTorrentsTests var recentFeed = ReadAllText(@"Files/Indexers/IPTorrents/IPTorrents.xml"); Mocker.GetMock() - .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.GET))) + .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.Get))) .Returns(r => new HttpResponse(r, new HttpHeader(), recentFeed)); var releases = Subject.FetchRecent(); diff --git a/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabFixture.cs index 3d244f5fe..b0b8ecd8b 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabFixture.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Net; +using System.Net.Http; using FluentAssertions; using Moq; using NUnit.Framework; @@ -43,7 +44,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests var recentFeed = ReadAllText(@"Files/Indexers/Newznab/newznab_nzb_su.xml"); Mocker.GetMock() - .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.GET))) + .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.Get))) .Returns(r => new HttpResponse(r, new HttpHeader(), recentFeed)); var releases = Subject.FetchRecent(); diff --git a/src/NzbDrone.Core.Test/IndexerTests/NyaaTests/NyaaFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/NyaaTests/NyaaFixture.cs index f04f8a571..2d2d957d3 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/NyaaTests/NyaaFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/NyaaTests/NyaaFixture.cs @@ -1,5 +1,6 @@ -using System; +using System; using System.Linq; +using System.Net.Http; using FluentAssertions; using Moq; using NUnit.Framework; @@ -30,7 +31,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NyaaTests var recentFeed = ReadAllText(@"Files/Indexers/Nyaa/Nyaa.xml"); Mocker.GetMock() - .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.GET))) + .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.Get))) .Returns(r => new HttpResponse(r, new HttpHeader(), recentFeed)); var releases = Subject.FetchRecent(); diff --git a/src/NzbDrone.Core.Test/IndexerTests/OmgwtfnzbsTests/OmgwtfnzbsFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/OmgwtfnzbsTests/OmgwtfnzbsFixture.cs index 1b19fd7f1..ade0029bc 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/OmgwtfnzbsTests/OmgwtfnzbsFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/OmgwtfnzbsTests/OmgwtfnzbsFixture.cs @@ -1,5 +1,6 @@ -using System; +using System; using System.Linq; +using System.Net.Http; using FluentAssertions; using Moq; using NUnit.Framework; @@ -33,7 +34,7 @@ namespace NzbDrone.Core.Test.IndexerTests.OmgwtfnzbsTests var recentFeed = ReadAllText(@"Files/Indexers/Omgwtfnzbs/Omgwtfnzbs.xml"); Mocker.GetMock() - .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.GET))) + .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.Get))) .Returns(r => new HttpResponse(r, new HttpHeader(), recentFeed)); var releases = Subject.FetchRecent(); diff --git a/src/NzbDrone.Core.Test/IndexerTests/RarbgTests/RarbgFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/RarbgTests/RarbgFixture.cs index 63405dbec..37ef9f5b6 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/RarbgTests/RarbgFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/RarbgTests/RarbgFixture.cs @@ -1,5 +1,6 @@ -using System; +using System; using System.Linq; +using System.Net.Http; using FluentAssertions; using Moq; using NUnit.Framework; @@ -35,7 +36,7 @@ namespace NzbDrone.Core.Test.IndexerTests.RarbgTests var recentFeed = ReadAllText(@"Files/Indexers/Rarbg/RecentFeed_v2.json"); Mocker.GetMock() - .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.GET))) + .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.Get))) .Returns(r => new HttpResponse(r, new HttpHeader(), recentFeed)); var releases = Subject.FetchRecent(); @@ -62,7 +63,7 @@ namespace NzbDrone.Core.Test.IndexerTests.RarbgTests public void should_parse_error_20_as_empty_results() { Mocker.GetMock() - .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.GET))) + .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.Get))) .Returns(r => new HttpResponse(r, new HttpHeader(), "{ error_code: 20, error: \"some message\" }")); var releases = Subject.FetchRecent(); @@ -74,7 +75,7 @@ namespace NzbDrone.Core.Test.IndexerTests.RarbgTests public void should_warn_on_unknown_error() { Mocker.GetMock() - .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.GET))) + .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.Get))) .Returns(r => new HttpResponse(r, new HttpHeader(), "{ error_code: 25, error: \"some message\" }")); var releases = Subject.FetchRecent(); diff --git a/src/NzbDrone.Core.Test/IndexerTests/TorrentleechTests/TorrentleechFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/TorrentleechTests/TorrentleechFixture.cs index 691abd0a4..59a284276 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/TorrentleechTests/TorrentleechFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/TorrentleechTests/TorrentleechFixture.cs @@ -1,5 +1,6 @@ -using System; +using System; using System.Linq; +using System.Net.Http; using FluentAssertions; using Moq; using NUnit.Framework; @@ -30,7 +31,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorrentleechTests var recentFeed = ReadAllText(@"Files/Indexers/Torrentleech/Torrentleech.xml"); Mocker.GetMock() - .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.GET))) + .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.Get))) .Returns(r => new HttpResponse(r, new HttpHeader(), recentFeed)); var releases = Subject.FetchRecent(); diff --git a/src/NzbDrone.Core.Test/IndexerTests/TorznabTests/TorznabFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/TorznabTests/TorznabFixture.cs index 93ab571d0..3f81cf6b1 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/TorznabTests/TorznabFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/TorznabTests/TorznabFixture.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Net.Http; using FluentAssertions; using Moq; using NUnit.Framework; @@ -42,7 +43,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests var recentFeed = ReadAllText(@"Files/Indexers/Torznab/torznab_hdaccess_net.xml"); Mocker.GetMock() - .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.GET))) + .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.Get))) .Returns(r => new HttpResponse(r, new HttpHeader(), recentFeed)); var releases = Subject.FetchRecent(); @@ -71,7 +72,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests var recentFeed = ReadAllText(@"Files/Indexers/Torznab/torznab_tpb.xml"); Mocker.GetMock() - .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.GET))) + .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.Get))) .Returns(r => new HttpResponse(r, new HttpHeader(), recentFeed)); var releases = Subject.FetchRecent(); diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DiskStationProxyBase.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DiskStationProxyBase.cs index 5dfbea3ac..c1507aa2e 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DiskStationProxyBase.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DiskStationProxyBase.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Net; +using System.Net.Http; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Http; @@ -142,15 +143,19 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation.Proxies return authResponse.Data.SId; } - protected HttpRequestBuilder BuildRequest(DownloadStationSettings settings, string methodName, int apiVersion, HttpMethod httpVerb = HttpMethod.GET) + protected HttpRequestBuilder BuildRequest(DownloadStationSettings settings, string methodName, int apiVersion, HttpMethod httpVerb = null) { + httpVerb ??= HttpMethod.Get; + var info = GetApiInfo(_apiType, settings); return BuildRequest(settings, info, methodName, apiVersion, httpVerb); } - private HttpRequestBuilder BuildRequest(DownloadStationSettings settings, DiskStationApiInfo apiInfo, string methodName, int apiVersion, HttpMethod httpVerb = HttpMethod.GET) + private HttpRequestBuilder BuildRequest(DownloadStationSettings settings, DiskStationApiInfo apiInfo, string methodName, int apiVersion, HttpMethod httpVerb = null) { + httpVerb ??= HttpMethod.Get; + var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port).Resource($"webapi/{apiInfo.Path}"); requestBuilder.Method = httpVerb; requestBuilder.LogResponseContent = true; @@ -163,7 +168,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation.Proxies throw new ArgumentOutOfRangeException(nameof(apiVersion)); } - if (httpVerb == HttpMethod.POST) + if (httpVerb == HttpMethod.Post) { if (apiInfo.NeedsAuthentication) { diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DownloadStationTaskProxyV1.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DownloadStationTaskProxyV1.cs index 8180a70ba..466a8c49c 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DownloadStationTaskProxyV1.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DownloadStationTaskProxyV1.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using System.Net.Http; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; @@ -21,7 +22,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation.Proxies public void AddTaskFromData(byte[] data, string filename, string downloadDirectory, DownloadStationSettings settings) { - var requestBuilder = BuildRequest(settings, "create", 2, HttpMethod.POST); + var requestBuilder = BuildRequest(settings, "create", 2, HttpMethod.Post); if (downloadDirectory.IsNotNullOrWhiteSpace()) { diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DownloadStationTaskProxyV2.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DownloadStationTaskProxyV2.cs index 5e5dc2a02..0812b76d4 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DownloadStationTaskProxyV2.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DownloadStationTaskProxyV2.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Net.Http; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; @@ -22,7 +23,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation.Proxies public void AddTaskFromData(byte[] data, string filename, string downloadDirectory, DownloadStationSettings settings) { - var requestBuilder = BuildRequest(settings, "create", 2, HttpMethod.POST); + var requestBuilder = BuildRequest(settings, "create", 2, HttpMethod.Post); requestBuilder.AddFormParameter("type", "\"file\""); requestBuilder.AddFormParameter("file", "[\"fileData\"]"); diff --git a/src/NzbDrone.Core/Download/Clients/Flood/FloodProxy.cs b/src/NzbDrone.Core/Download/Clients/Flood/FloodProxy.cs index ddebdbfff..06bca878f 100644 --- a/src/NzbDrone.Core/Download/Clients/Flood/FloodProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Flood/FloodProxy.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using System.Net.Http; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Http; @@ -107,7 +108,7 @@ namespace NzbDrone.Core.Download.Clients.Flood { var verifyRequest = BuildRequest(settings).Resource("/auth/verify").Build(); - verifyRequest.Method = HttpMethod.GET; + verifyRequest.Method = HttpMethod.Get; HandleRequest(verifyRequest, settings); } @@ -180,7 +181,7 @@ namespace NzbDrone.Core.Download.Clients.Flood { var getTorrentsRequest = BuildRequest(settings).Resource("/torrents").Build(); - getTorrentsRequest.Method = HttpMethod.GET; + getTorrentsRequest.Method = HttpMethod.Get; return Json.Deserialize(HandleRequest(getTorrentsRequest, settings).Content).Torrents; } @@ -189,7 +190,7 @@ namespace NzbDrone.Core.Download.Clients.Flood { var contentsRequest = BuildRequest(settings).Resource($"/torrents/{hash}/contents").Build(); - contentsRequest.Method = HttpMethod.GET; + contentsRequest.Method = HttpMethod.Get; return Json.Deserialize>(HandleRequest(contentsRequest, settings).Content).ConvertAll(content => content.Path); } @@ -198,7 +199,7 @@ namespace NzbDrone.Core.Download.Clients.Flood { var tagsRequest = BuildRequest(settings).Resource("/torrents/tags").Build(); - tagsRequest.Method = HttpMethod.PATCH; + tagsRequest.Method = HttpMethod.Patch; var body = new Dictionary { diff --git a/src/NzbDrone.Core/ImportLists/Goodreads/GoodreadsImportListBase.cs b/src/NzbDrone.Core/ImportLists/Goodreads/GoodreadsImportListBase.cs index edb68e07e..e8abfb889 100644 --- a/src/NzbDrone.Core/ImportLists/Goodreads/GoodreadsImportListBase.cs +++ b/src/NzbDrone.Core/ImportLists/Goodreads/GoodreadsImportListBase.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; +using System.Net.Http; using System.Web; using System.Xml.Linq; using System.Xml.XPath; @@ -142,7 +143,7 @@ namespace NzbDrone.Core.ImportLists.Goodreads { var request = new Common.Http.HttpRequest(Settings.SigningUrl) { - Method = HttpMethod.POST, + Method = HttpMethod.Post, }; request.Headers.Set("Content-Type", "application/json"); diff --git a/src/NzbDrone.Core/Indexers/Gazelle/GazelleRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Gazelle/GazelleRequestGenerator.cs index bdd27b84d..a64fa6643 100644 --- a/src/NzbDrone.Core/Indexers/Gazelle/GazelleRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/Gazelle/GazelleRequestGenerator.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Net.Http; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; @@ -71,7 +72,7 @@ namespace NzbDrone.Core.Indexers.Gazelle }; indexRequestBuilder.SetCookies(cookies); - indexRequestBuilder.Method = HttpMethod.POST; + indexRequestBuilder.Method = HttpMethod.Post; indexRequestBuilder.Resource("ajax.php?action=index"); var authIndexRequest = indexRequestBuilder @@ -92,7 +93,7 @@ namespace NzbDrone.Core.Indexers.Gazelle LogResponseContent = true }; - requestBuilder.Method = HttpMethod.POST; + requestBuilder.Method = HttpMethod.Post; requestBuilder.Resource("login.php"); requestBuilder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15); diff --git a/src/NzbDrone.Core/Messaging/Commands/CommandExecutor.cs b/src/NzbDrone.Core/Messaging/Commands/CommandExecutor.cs index 18dabae07..bf127486c 100644 --- a/src/NzbDrone.Core/Messaging/Commands/CommandExecutor.cs +++ b/src/NzbDrone.Core/Messaging/Commands/CommandExecutor.cs @@ -47,11 +47,6 @@ namespace NzbDrone.Core.Messaging.Commands } } } - catch (ThreadAbortException ex) - { - _logger.Error(ex, "Thread aborted"); - Thread.ResetAbort(); - } catch (OperationCanceledException) { _logger.Trace("Stopped one command execution pipeline"); diff --git a/src/NzbDrone.Core/Notifications/Discord/DiscordProxy.cs b/src/NzbDrone.Core/Notifications/Discord/DiscordProxy.cs index f3a6be3d2..c066da569 100644 --- a/src/NzbDrone.Core/Notifications/Discord/DiscordProxy.cs +++ b/src/NzbDrone.Core/Notifications/Discord/DiscordProxy.cs @@ -1,3 +1,4 @@ +using System.Net.Http; using NLog; using NzbDrone.Common.Http; using NzbDrone.Common.Serializer; @@ -29,7 +30,7 @@ namespace NzbDrone.Core.Notifications.Discord .Accept(HttpAccept.Json) .Build(); - request.Method = HttpMethod.POST; + request.Method = HttpMethod.Post; request.Headers.ContentType = "application/json"; request.SetContent(payload.ToJson()); diff --git a/src/NzbDrone.Core/Notifications/Goodreads/GoodreadsNotificationBase.cs b/src/NzbDrone.Core/Notifications/Goodreads/GoodreadsNotificationBase.cs index aae715ab4..e892a0741 100644 --- a/src/NzbDrone.Core/Notifications/Goodreads/GoodreadsNotificationBase.cs +++ b/src/NzbDrone.Core/Notifications/Goodreads/GoodreadsNotificationBase.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; +using System.Net.Http; using System.Text; using System.Web; using System.Xml.Linq; @@ -112,11 +113,11 @@ namespace NzbDrone.Core.Notifications.Goodreads // we need the url without the query to sign auth.RequestUrl = request.Url.SetQuery(null).FullUri; - if (builder.Method == HttpMethod.GET) + if (builder.Method == HttpMethod.Get) { auth.Parameters = builder.QueryParams.ToDictionary(x => x.Key, x => x.Value); } - else if (builder.Method == HttpMethod.POST) + else if (builder.Method == HttpMethod.Post) { auth.Parameters = builder.FormData.ToDictionary(x => x.Name, x => Encoding.UTF8.GetString(x.ContentData)); } @@ -172,7 +173,7 @@ namespace NzbDrone.Core.Notifications.Goodreads { var request = new Common.Http.HttpRequest(Settings.SigningUrl) { - Method = HttpMethod.POST, + Method = HttpMethod.Post, }; request.Headers.Set("Content-Type", "application/json"); diff --git a/src/NzbDrone.Core/Notifications/Join/JoinProxy.cs b/src/NzbDrone.Core/Notifications/Join/JoinProxy.cs index bb776b0f4..e0f5f074f 100644 --- a/src/NzbDrone.Core/Notifications/Join/JoinProxy.cs +++ b/src/NzbDrone.Core/Notifications/Join/JoinProxy.cs @@ -1,4 +1,5 @@ using System; +using System.Net.Http; using FluentValidation.Results; using NLog; using NzbDrone.Common.Extensions; @@ -27,7 +28,7 @@ namespace NzbDrone.Core.Notifications.Join public void SendNotification(string title, string message, JoinSettings settings) { - var method = HttpMethod.GET; + var method = HttpMethod.Get; try { diff --git a/src/NzbDrone.Core/Notifications/Mailgun/MailgunProxy.cs b/src/NzbDrone.Core/Notifications/Mailgun/MailgunProxy.cs index dbbbf2af7..ba4bbe18b 100644 --- a/src/NzbDrone.Core/Notifications/Mailgun/MailgunProxy.cs +++ b/src/NzbDrone.Core/Notifications/Mailgun/MailgunProxy.cs @@ -1,7 +1,7 @@ using System.Net; +using System.Net.Http; using NLog; using NzbDrone.Common.Http; -using HttpMethod = NzbDrone.Common.Http.HttpMethod; namespace NzbDrone.Core.Notifications.Mailgun { @@ -27,7 +27,7 @@ namespace NzbDrone.Core.Notifications.Mailgun { try { - var request = BuildRequest(settings, $"{settings.SenderDomain}/messages", HttpMethod.POST, title, message).Build(); + var request = BuildRequest(settings, $"{settings.SenderDomain}/messages", HttpMethod.Post, title, message).Build(); _httpClient.Execute(request); } catch (HttpException ex) diff --git a/src/NzbDrone.Core/Notifications/PushBullet/PushBulletProxy.cs b/src/NzbDrone.Core/Notifications/PushBullet/PushBulletProxy.cs index 2aa226737..1f3cb4ce8 100644 --- a/src/NzbDrone.Core/Notifications/PushBullet/PushBulletProxy.cs +++ b/src/NzbDrone.Core/Notifications/PushBullet/PushBulletProxy.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Net; +using System.Net.Http; using FluentValidation.Results; using NLog; using NzbDrone.Common.Extensions; @@ -100,7 +101,7 @@ namespace NzbDrone.Core.Notifications.PushBullet var request = requestBuilder.Build(); - request.Method = HttpMethod.GET; + request.Method = HttpMethod.Get; request.Credentials = new BasicNetworkCredential(settings.ApiKey, string.Empty); var response = _httpClient.Execute(request); diff --git a/src/NzbDrone.Core/Notifications/SendGrid/SendGridProxy.cs b/src/NzbDrone.Core/Notifications/SendGrid/SendGridProxy.cs index 0db56b4c9..ec57c8636 100644 --- a/src/NzbDrone.Core/Notifications/SendGrid/SendGridProxy.cs +++ b/src/NzbDrone.Core/Notifications/SendGrid/SendGridProxy.cs @@ -1,4 +1,5 @@ -using System.Net; +using System.Net; +using System.Net.Http; using NzbDrone.Common.Http; using NzbDrone.Common.Serializer; @@ -22,7 +23,7 @@ namespace NzbDrone.Core.Notifications.SendGrid { try { - var request = BuildRequest(settings, "mail/send", HttpMethod.POST); + var request = BuildRequest(settings, "mail/send", HttpMethod.Post); var payload = new SendGridPayload { diff --git a/src/NzbDrone.Core/Notifications/Slack/SlackProxy.cs b/src/NzbDrone.Core/Notifications/Slack/SlackProxy.cs index 02075dad9..c8dd12f53 100644 --- a/src/NzbDrone.Core/Notifications/Slack/SlackProxy.cs +++ b/src/NzbDrone.Core/Notifications/Slack/SlackProxy.cs @@ -1,3 +1,4 @@ +using System.Net.Http; using NLog; using NzbDrone.Common.Http; using NzbDrone.Common.Serializer; @@ -29,7 +30,7 @@ namespace NzbDrone.Core.Notifications.Slack .Accept(HttpAccept.Json) .Build(); - request.Method = HttpMethod.POST; + request.Method = HttpMethod.Post; request.Headers.ContentType = "application/json"; request.SetContent(payload.ToJson()); diff --git a/src/NzbDrone.Core/Notifications/Subsonic/SubsonicServerProxy.cs b/src/NzbDrone.Core/Notifications/Subsonic/SubsonicServerProxy.cs index 4acd0ae00..be0104375 100644 --- a/src/NzbDrone.Core/Notifications/Subsonic/SubsonicServerProxy.cs +++ b/src/NzbDrone.Core/Notifications/Subsonic/SubsonicServerProxy.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Net.Http; using System.Xml.Linq; using NLog; using NzbDrone.Common.Extensions; @@ -36,7 +37,7 @@ namespace NzbDrone.Core.Notifications.Subsonic public void Notify(SubsonicSettings settings, string message) { var resource = "addChatMessage"; - var request = GetSubsonicServerRequest(resource, HttpMethod.GET, settings); + var request = GetSubsonicServerRequest(resource, HttpMethod.Get, settings); request.AddQueryParam("message", message); var response = _httpClient.Execute(request.Build()); @@ -48,7 +49,7 @@ namespace NzbDrone.Core.Notifications.Subsonic public void Update(SubsonicSettings settings) { var resource = "startScan"; - var request = GetSubsonicServerRequest(resource, HttpMethod.GET, settings); + var request = GetSubsonicServerRequest(resource, HttpMethod.Get, settings); var response = _httpClient.Execute(request.Build()); _logger.Trace("Update response: {0}", response.Content); @@ -57,7 +58,7 @@ namespace NzbDrone.Core.Notifications.Subsonic public string Version(SubsonicSettings settings) { - var request = GetSubsonicServerRequest("ping", HttpMethod.GET, settings); + var request = GetSubsonicServerRequest("ping", HttpMethod.Get, settings); var response = _httpClient.Execute(request.Build()); _logger.Trace("Version response: {0}", response.Content); diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookMethod.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookMethod.cs index 5d6e859a6..a1d7b5137 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookMethod.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookMethod.cs @@ -1,10 +1,8 @@ -using NzbDrone.Common.Http; - namespace NzbDrone.Core.Notifications.Webhook { public enum WebhookMethod { - POST = HttpMethod.POST, - PUT = HttpMethod.PUT + POST = 1, + PUT = 2 } } diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookProxy.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookProxy.cs index 65ea5f92e..23a7fbdc8 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookProxy.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookProxy.cs @@ -1,4 +1,5 @@ -using System.Net; +using System; +using System.Net.Http; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Common.Serializer; @@ -27,7 +28,13 @@ namespace NzbDrone.Core.Notifications.Webhook .Accept(HttpAccept.Json) .Build(); - request.Method = (HttpMethod)settings.Method; + request.Method = settings.Method switch + { + (int)WebhookMethod.POST => HttpMethod.Post, + (int)WebhookMethod.PUT => HttpMethod.Put, + _ => throw new ArgumentOutOfRangeException($"Invalid Webhook method {settings.Method}") + }; + request.Headers.ContentType = "application/json"; request.SetContent(body.ToJson()); diff --git a/src/NzbDrone.Core/TinyTwitter.cs b/src/NzbDrone.Core/TinyTwitter.cs index a1a1da69c..97eabbfd8 100644 --- a/src/NzbDrone.Core/TinyTwitter.cs +++ b/src/NzbDrone.Core/TinyTwitter.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; +using System.Net.Http; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; @@ -37,7 +38,7 @@ namespace TinyTwitter public void UpdateStatus(string message) { - new RequestBuilder(_oauth, "POST", "https://api.twitter.com/1.1/statuses/update.json") + new RequestBuilder(_oauth, HttpMethod.Post, "https://api.twitter.com/1.1/statuses/update.json") .AddParameter("status", message) .Execute(); } @@ -51,7 +52,7 @@ namespace TinyTwitter **/ public void DirectMessage(string message, string screenName) { - new RequestBuilder(_oauth, "POST", "https://api.twitter.com/1.1/direct_messages/new.json") + new RequestBuilder(_oauth, HttpMethod.Post, "https://api.twitter.com/1.1/direct_messages/new.json") .AddParameter("text", message) .AddParameter("screen_name", screenName) .Execute(); @@ -63,16 +64,18 @@ namespace TinyTwitter private const string SIGNATURE_METHOD = "HMAC-SHA1"; private readonly OAuthInfo _oauth; - private readonly string _method; + private readonly HttpMethod _method; private readonly IDictionary _customParameters; private readonly string _url; + private readonly HttpClient _httpClient; - public RequestBuilder(OAuthInfo oauth, string method, string url) + public RequestBuilder(OAuthInfo oauth, HttpMethod method, string url) { _oauth = oauth; _method = method; _url = url; _customParameters = new Dictionary(); + _httpClient = new (); } public RequestBuilder AddParameter(string name, string value) @@ -92,61 +95,13 @@ namespace TinyTwitter var signature = GenerateSignature(parameters); var headerValue = GenerateAuthorizationHeaderValue(parameters, signature); - var request = (HttpWebRequest)WebRequest.Create(GetRequestUrl()); - request.Method = _method; - request.ContentType = "application/x-www-form-urlencoded"; + var request = new HttpRequestMessage(_method, _url); + request.Content = new FormUrlEncodedContent(_customParameters); request.Headers.Add("Authorization", headerValue); - WriteRequestBody(request); - - // It looks like a bug in HttpWebRequest. It throws random TimeoutExceptions - // after some requests. Abort the request seems to work. More info: - // http://stackoverflow.com/questions/2252762/getrequeststream-throws-timeout-exception-randomly - var response = request.GetResponse(); - - string content; - - using (var stream = response.GetResponseStream()) - { - using (var reader = new StreamReader(stream)) - { - content = reader.ReadToEnd(); - } - } - - request.Abort(); - - return content; - } - - private void WriteRequestBody(HttpWebRequest request) - { - if (_method == "GET") - { - return; - } - - var requestBody = Encoding.ASCII.GetBytes(GetCustomParametersString()); - using (var stream = request.GetRequestStream()) - { - stream.Write(requestBody, 0, requestBody.Length); - } - } - - private string GetRequestUrl() - { - if (_method != "GET" || _customParameters.Count == 0) - { - return _url; - } - - return string.Format("{0}?{1}", _url, GetCustomParametersString()); - } - - private string GetCustomParametersString() - { - return _customParameters.Select(x => string.Format("{0}={1}", x.Key, x.Value)).Join("&"); + var response = _httpClient.Send(request); + return response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); } private string GenerateAuthorizationHeaderValue(IEnumerable> parameters, string signature) diff --git a/src/NzbDrone.Integration.Test/IndexHtmlFixture.cs b/src/NzbDrone.Integration.Test/IndexHtmlFixture.cs index ed732aee8..7f3a087a5 100644 --- a/src/NzbDrone.Integration.Test/IndexHtmlFixture.cs +++ b/src/NzbDrone.Integration.Test/IndexHtmlFixture.cs @@ -1,5 +1,6 @@ -using System.Linq; -using System.Net; +using System; +using System.Net.Http; +using System.Net.Http.Headers; using FluentAssertions; using NUnit.Framework; @@ -8,25 +9,30 @@ namespace NzbDrone.Integration.Test [TestFixture] public class IndexHtmlFixture : IntegrationTest { + private HttpClient _httpClient = new HttpClient(); + [Test] public void should_get_index_html() { - var text = new WebClient().DownloadString(RootUrl); + var request = new HttpRequestMessage(HttpMethod.Get, RootUrl); + var response = _httpClient.Send(request); + var text = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); text.Should().NotBeNullOrWhiteSpace(); } [Test] public void index_should_not_be_cached() { - var client = new WebClient(); - _ = client.DownloadString(RootUrl); + var request = new HttpRequestMessage(HttpMethod.Get, RootUrl); + var response = _httpClient.Send(request); + + var headers = response.Headers; - var headers = client.ResponseHeaders; + headers.CacheControl.NoStore.Should().BeTrue(); + headers.CacheControl.NoCache.Should().BeTrue(); + headers.Pragma.Should().Contain(new NameValueHeaderValue("no-cache")); - headers.Get("Cache-Control").Split(',').Select(x => x.Trim()) - .Should().BeEquivalentTo("no-store, no-cache".Split(',').Select(x => x.Trim())); - headers.Get("Pragma").Should().Be("no-cache"); - headers.Get("Expires").Should().Be("-1"); + response.Content.Headers.Expires.Should().BeBefore(DateTime.UtcNow); } } }