From c5b736e422ef563a7359da81f890dcf52383f69f Mon Sep 17 00:00:00 2001 From: ta264 Date: Mon, 22 Nov 2021 21:14:04 +0000 Subject: [PATCH] Use modern HttpClient --- .../Http/HttpClientFixture.cs | 113 +++++-- .../ICertificateValidationService.cs | 11 + .../Http/Dispatchers/IHttpDispatcher.cs | 1 - .../Http/Dispatchers/ManagedHttpDispatcher.cs | 298 +++++++++++------- src/NzbDrone.Common/Http/GZipWebClient.cs | 15 - src/NzbDrone.Common/Http/HttpClient.cs | 101 ++++-- src/NzbDrone.Common/Http/HttpHeader.cs | 23 +- src/NzbDrone.Common/Http/HttpMethod.cs | 14 - src/NzbDrone.Common/Http/HttpRequest.cs | 4 + .../Http/HttpRequestBuilder.cs | 9 +- .../Http/JsonRpcRequestBuilder.cs | 7 +- src/NzbDrone.Core.Test/Framework/CoreTest.cs | 4 +- .../FileListTests/FileListFixture.cs | 3 +- .../IndexerTests/HDBitsTests/HDBitsFixture.cs | 5 +- .../IPTorrentsTests/IPTorrentsFixture.cs | 3 +- .../NewznabTests/NewznabFixture.cs | 5 +- .../IndexerTests/NyaaTests/NyaaFixture.cs | 5 +- .../OmgwtfnzbsTests/OmgwtfnzbsFixture.cs | 5 +- .../IndexerTests/PTPTests/PTPFixture.cs | 7 +- .../IndexerTests/RarbgTests/RarbgFixture.cs | 9 +- .../TorznabTests/TorznabFixture.cs | 7 +- .../Proxies/DiskStationProxyBase.cs | 11 +- .../Proxies/DownloadStationTaskProxyV1.cs | 3 +- .../Proxies/DownloadStationTaskProxyV2.cs | 3 +- .../Download/Clients/Flood/FloodProxy.cs | 9 +- .../ImportLists/TMDb/User/TMDbUserImport.cs | 5 +- .../TMDb/User/TMDbUserRequestGenerator.cs | 5 +- .../Trakt/List/TraktListRequestGenerator.cs | 3 +- .../Popular/TraktPopularRequestGenerator.cs | 3 +- .../Trakt/User/TraktUserRequestGenerator.cs | 3 +- .../Indexers/HDBits/HDBitsRequestGenerator.cs | 3 +- .../Messaging/Commands/CommandExecutor.cs | 5 - .../Notifications/Discord/DiscordProxy.cs | 3 +- .../Notifications/Join/JoinProxy.cs | 3 +- .../Notifications/Mailgun/MailgunProxy.cs | 4 +- .../Plex/PlexTv/PlexTvService.cs | 3 +- .../Plex/Server/PlexServerProxy.cs | 13 +- .../PushBullet/PushBulletProxy.cs | 3 +- .../Notifications/SendGrid/SendGridProxy.cs | 3 +- .../Notifications/Slack/SlackProxy.cs | 3 +- .../Notifications/Trakt/TraktProxy.cs | 7 +- .../Notifications/Webhook/WebhookMethod.cs | 6 +- .../Notifications/Webhook/WebhookProxy.cs | 10 +- .../X509CertificateValidationService.cs | 27 +- src/NzbDrone.Core/TinyTwitter.cs | 67 +--- .../IndexHtmlFixture.cs | 26 +- 46 files changed, 527 insertions(+), 353 deletions(-) create mode 100644 src/NzbDrone.Common/Http/Dispatchers/ICertificateValidationService.cs 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 b6347d65e..c3d268a42 100644 --- a/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs +++ b/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Net; +using System.Net.Http; using System.Threading; using FluentAssertions; using Moq; @@ -15,8 +16,11 @@ using NzbDrone.Common.Http; using NzbDrone.Common.Http.Dispatchers; using NzbDrone.Common.Http.Proxy; using NzbDrone.Common.TPL; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Security; using NzbDrone.Test.Common; using NzbDrone.Test.Common.Categories; +using HttpClient = NzbDrone.Common.Http.HttpClient; namespace NzbDrone.Common.Test.Http { @@ -31,6 +35,8 @@ namespace NzbDrone.Common.Test.Http private string _httpBinHost; private string _httpBinHost2; + private System.Net.Http.HttpClient _httpClient = new (); + [OneTimeSetUp] public void FixtureSetUp() { @@ -38,7 +44,7 @@ namespace NzbDrone.Common.Test.Http var mainHost = "httpbin.servarr.com"; // Use mirrors for tests that use two hosts - var candidates = new[] { "eu.httpbin.org", /* "httpbin.org", */ "www.httpbin.org" }; + var candidates = new[] { "httpbin1.servarr.com" }; // httpbin.org is broken right now, occassionally redirecting to https if it's unavailable. _httpBinHost = mainHost; @@ -46,29 +52,20 @@ namespace NzbDrone.Common.Test.Http TestLogger.Info($"{candidates.Length} TestSites available."); - _httpBinSleep = _httpBinHosts.Length < 2 ? 100 : 10; + _httpBinSleep = 10; } private bool IsTestSiteAvailable(string site) { 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) { @@ -95,10 +92,13 @@ namespace NzbDrone.Common.Test.Http Mocker.GetMock().Setup(c => c.Name).Returns("TestOS"); Mocker.GetMock().Setup(c => c.Version).Returns("9.0.0"); + Mocker.GetMock().SetupGet(x => x.CertificateValidation).Returns(CertificateValidationType.Enabled); + Mocker.SetConstant(Mocker.Resolve()); Mocker.SetConstant(Mocker.Resolve()); Mocker.SetConstant(Mocker.Resolve()); + Mocker.SetConstant(new X509CertificateValidationService(Mocker.GetMock().Object, TestLogger)); Mocker.SetConstant(Mocker.Resolve()); Mocker.SetConstant>(Array.Empty()); Mocker.SetConstant(Mocker.Resolve()); @@ -138,6 +138,28 @@ namespace NzbDrone.Common.Test.Http response.Content.Should().NotBeNullOrWhiteSpace(); } + [TestCase(CertificateValidationType.Enabled)] + [TestCase(CertificateValidationType.DisabledForLocalAddresses)] + public void bad_ssl_should_fail_when_remote_validation_enabled(CertificateValidationType validationType) + { + Mocker.GetMock().SetupGet(x => x.CertificateValidation).Returns(validationType); + var request = new HttpRequest($"https://expired.badssl.com"); + + Assert.Throws(() => Subject.Execute(request)); + ExceptionVerification.ExpectedErrors(2); + } + + [Test] + public void bad_ssl_should_pass_if_remote_validation_disabled() + { + Mocker.GetMock().SetupGet(x => x.CertificateValidation).Returns(CertificateValidationType.Disabled); + + var request = new HttpRequest($"https://expired.badssl.com"); + + Subject.Execute(request); + ExceptionVerification.ExpectedErrors(0); + } + [Test] public void should_execute_typed_get() { @@ -162,15 +184,44 @@ namespace NzbDrone.Common.Test.Http response.Resource.Data.Should().Be(message); } - [TestCase("gzip")] - public void should_execute_get_using_gzip(string compression) + [Test] + public void should_execute_post_with_content_type() { - var request = new HttpRequest($"https://{_httpBinHost}/{compression}"); + var message = "{ my: 1 }"; + + var request = new HttpRequest($"https://{_httpBinHost}/post"); + request.SetContent(message); + request.Headers.ContentType = "application/json"; + + var response = Subject.Post(request); + + response.Resource.Data.Should().Be(message); + } + + [Test] + public void should_execute_get_using_gzip() + { + var request = new HttpRequest($"https://{_httpBinHost}/gzip"); var response = Subject.Get(request); - response.Resource.Headers["Accept-Encoding"].ToString().Should().Be(compression); + response.Resource.Headers["Accept-Encoding"].ToString().Should().Contain("gzip"); + response.Resource.Gzipped.Should().BeTrue(); + response.Resource.Brotli.Should().BeFalse(); + } + + [Test] + public void should_execute_get_using_brotli() + { + var request = new HttpRequest($"https://{_httpBinHost}/brotli"); + + var response = Subject.Get(request); + + response.Resource.Headers["Accept-Encoding"].ToString().Should().Contain("br"); + + response.Resource.Gzipped.Should().BeFalse(); + response.Resource.Brotli.Should().BeTrue(); } [TestCase(HttpStatusCode.Unauthorized)] @@ -337,13 +388,38 @@ namespace NzbDrone.Common.Test.Http { var file = GetTempFilePath(); - Assert.Throws(() => Subject.DownloadFile("https://download.sonarr.tv/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() { @@ -773,6 +849,7 @@ namespace NzbDrone.Common.Test.Http public string Url { get; set; } public string Data { get; set; } public bool Gzipped { get; set; } + public bool Brotli { get; set; } } public class HttpCookieResource diff --git a/src/NzbDrone.Common/Http/Dispatchers/ICertificateValidationService.cs b/src/NzbDrone.Common/Http/Dispatchers/ICertificateValidationService.cs new file mode 100644 index 000000000..187c1fd43 --- /dev/null +++ b/src/NzbDrone.Common/Http/Dispatchers/ICertificateValidationService.cs @@ -0,0 +1,11 @@ +using System.Net.Http; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; + +namespace NzbDrone.Common.Http.Dispatchers +{ + public interface ICertificateValidationService + { + bool ShouldByPassValidationError(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors); + } +} 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 61d024e93..b621a1bd7 100644 --- a/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs +++ b/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs @@ -1,12 +1,13 @@ using System; -using System.Diagnostics; using System.IO; -using System.IO.Compression; using System.Net; -using System.Reflection; +using System.Net.Http; +using System.Net.Security; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; using NLog; -using NLog.Fluent; -using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http.Proxy; @@ -14,200 +15,187 @@ namespace NzbDrone.Common.Http.Dispatchers { public class ManagedHttpDispatcher : IHttpDispatcher { + private const string NO_PROXY_KEY = "no-proxy"; + + private const int connection_establish_timeout = 2000; + private static bool useIPv6 = Socket.OSSupportsIPv6; + private static bool hasResolvedIPv6Availability; + private readonly IHttpProxySettingsProvider _proxySettingsProvider; private readonly ICreateManagedWebProxy _createManagedWebProxy; + private readonly ICertificateValidationService _certificateValidationService; private readonly IUserAgentBuilder _userAgentBuilder; - private readonly IPlatformInfo _platformInfo; + private readonly ICached _httpClientCache; private readonly Logger _logger; - public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider, ICreateManagedWebProxy createManagedWebProxy, IUserAgentBuilder userAgentBuilder, IPlatformInfo platformInfo, Logger logger) + public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider, + ICreateManagedWebProxy createManagedWebProxy, + ICertificateValidationService certificateValidationService, + IUserAgentBuilder userAgentBuilder, + ICacheManager cacheManager, + Logger logger) { _proxySettingsProvider = proxySettingsProvider; _createManagedWebProxy = createManagedWebProxy; + _certificateValidationService = certificateValidationService; _userAgentBuilder = userAgentBuilder; - _platformInfo = platformInfo; _logger = logger; + + _httpClientCache = cacheManager.GetCache(typeof(ManagedHttpDispatcher)); } public HttpResponse GetResponse(HttpRequest request, CookieContainer cookies) { - var webRequest = (HttpWebRequest)WebRequest.Create((Uri)request.Url); + var requestMessage = new HttpRequestMessage(request.Method, (Uri)request.Url); + requestMessage.Headers.UserAgent.ParseAdd(_userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent)); + requestMessage.Headers.ConnectionClose = !request.ConnectionKeepAlive; - // 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.GZip; - - webRequest.Method = request.Method.ToString(); - webRequest.UserAgent = _userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent); - webRequest.KeepAlive = request.ConnectionKeepAlive; - webRequest.AllowAutoRedirect = false; - webRequest.CookieContainer = cookies; - - if (request.RequestTimeout != TimeSpan.Zero) + var cookieHeader = cookies.GetCookieHeader((Uri)request.Url); + if (cookieHeader.IsNotNullOrWhiteSpace()) { - webRequest.Timeout = (int)Math.Ceiling(request.RequestTimeout.TotalMilliseconds); + requestMessage.Headers.Add("Cookie", cookieHeader); } - webRequest.Proxy = GetProxy(request.Url); + using var cts = new CancellationTokenSource(); + if (request.RequestTimeout != TimeSpan.Zero) + { + cts.CancelAfter(request.RequestTimeout); + } + else + { + // The default for System.Net.Http.HttpClient + cts.CancelAfter(TimeSpan.FromSeconds(100)); + } + + if (request.ContentData != null) + { + requestMessage.Content = new ByteArrayContent(request.ContentData); + } 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()) - { - writeStream.Write(request.ContentData, 0, request.ContentData.Length); - } - } - - 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 { - data = responseStream.ToBytes(); + 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 + { + data = responseStream.ToBytes(); + } } 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 stopWatch = Stopwatch.StartNew(); - var uri = new HttpUri(url); - - 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; - } - } - - protected virtual IWebProxy GetProxy(HttpUri uri) - { - IWebProxy proxy = null; - var proxySettings = _proxySettingsProvider.GetProxySettings(uri); + var key = proxySettings?.Key ?? NO_PROXY_KEY; + + return _httpClientCache.Get(key, () => CreateHttpClient(proxySettings)); + } + + protected virtual System.Net.Http.HttpClient CreateHttpClient(HttpProxySettings proxySettings) + { + var handler = new SocketsHttpHandler() + { + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Brotli, + UseCookies = false, // sic - we don't want to use a shared cookie container + AllowAutoRedirect = false, + MaxConnectionsPerServer = 12, + ConnectCallback = onConnect, + SslOptions = new SslClientAuthenticationOptions + { + RemoteCertificateValidationCallback = _certificateValidationService.ShouldByPassValidationError + } + }; + 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); + AddContentHeader(webRequest, "Content-Length", header.Value); break; case "Content-Type": - webRequest.ContentType = header.Value; + AddContentHeader(webRequest, "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 "Range": throw new NotImplementedException(); 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": throw new NotSupportedException("User-Agent other than Radarr not allowed."); @@ -219,5 +207,79 @@ namespace NzbDrone.Common.Http.Dispatchers } } } + + private void AddContentHeader(HttpRequestMessage request, string header, string value) + { + var headers = request.Content?.Headers; + if (headers == null) + { + return; + } + + headers.Remove(header); + headers.Add(header, value); + } + + private static async ValueTask onConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken) + { + // Until .NET supports an implementation of Happy Eyeballs (https://tools.ietf.org/html/rfc8305#section-2), let's make IPv4 fallback work in a simple way. + // This issue is being tracked at https://github.com/dotnet/runtime/issues/26177 and expected to be fixed in .NET 6. + if (useIPv6) + { + try + { + var localToken = cancellationToken; + + if (!hasResolvedIPv6Availability) + { + // to make things move fast, use a very low timeout for the initial ipv6 attempt. + var quickFailCts = new CancellationTokenSource(connection_establish_timeout); + var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, quickFailCts.Token); + + localToken = linkedTokenSource.Token; + } + + return await attemptConnection(AddressFamily.InterNetworkV6, context, localToken); + } + catch + { + // very naively fallback to ipv4 permanently for this execution based on the response of the first connection attempt. + // note that this may cause users to eventually get switched to ipv4 (on a random failure when they are switching networks, for instance) + // but in the interest of keeping this implementation simple, this is acceptable. + useIPv6 = false; + } + finally + { + hasResolvedIPv6Availability = true; + } + } + + // fallback to IPv4. + return await attemptConnection(AddressFamily.InterNetwork, context, cancellationToken); + } + + private static async ValueTask attemptConnection(AddressFamily addressFamily, SocketsHttpConnectionContext context, CancellationToken cancellationToken) + { + // The following socket constructor will create a dual-mode socket on systems where IPV6 is available. + var socket = new Socket(addressFamily, SocketType.Stream, ProtocolType.Tcp) + { + // Turn off Nagle's algorithm since it degrades performance in most HttpClient scenarios. + NoDelay = true + }; + + try + { + await socket.ConnectAsync(context.DnsEndPoint, cancellationToken).ConfigureAwait(false); + + // The stream should take the ownership of the underlying socket, + // closing it when it's disposed. + return new NetworkStream(socket, ownsSocket: true); + } + catch + { + socket.Dispose(); + throw; + } + } } } 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 9ea6f985c..175448e43 100644 --- a/src/NzbDrone.Common/Http/HttpClient.cs +++ b/src/NzbDrone.Common/Http/HttpClient.cs @@ -1,8 +1,10 @@ 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.GetAllCookies()) { - var presistentContainer = _cookieContainerCache.Get("container", () => new CookieContainer()); - var persistentCookies = presistentContainer.GetCookies((Uri)request.Url); - var existingCookies = cookieContainer.GetCookies((Uri)request.Url); + cookie.Expired = true; + } - cookieContainer.Add(persistentCookies); - cookieContainer.Add(existingCookies); - }*/ - } - - 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) { - _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 2794f6dc4..3c6b18a09 100644 --- a/src/NzbDrone.Common/Http/HttpHeader.cs +++ b/src/NzbDrone.Common/Http/HttpHeader.cs @@ -1,14 +1,30 @@ -using System; +using System; using System.Collections; 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 637e97903..a911081da 100644 --- a/src/NzbDrone.Common/Http/HttpRequest.cs +++ b/src/NzbDrone.Common/Http/HttpRequest.cs @@ -1,6 +1,8 @@ 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; @@ -11,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; @@ -49,6 +52,7 @@ namespace NzbDrone.Common.Http public TimeSpan RequestTimeout { get; set; } public TimeSpan RateLimit { get; set; } public string RateLimitKey { get; set; } + public Stream ResponseStream { get; set; } public override string ToString() { diff --git a/src/NzbDrone.Common/Http/HttpRequestBuilder.cs b/src/NzbDrone.Common/Http/HttpRequestBuilder.cs index a8405a15c..a07778f99 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; @@ -35,7 +36,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(); @@ -271,7 +272,7 @@ namespace NzbDrone.Common.Http public virtual HttpRequestBuilder Post() { - Method = HttpMethod.POST; + Method = HttpMethod.Post; return this; } @@ -362,7 +363,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."); } @@ -378,7 +379,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 4c9682957..a83096d80 100644 --- a/src/NzbDrone.Core.Test/Framework/CoreTest.cs +++ b/src/NzbDrone.Core.Test/Framework/CoreTest.cs @@ -11,6 +11,7 @@ using NzbDrone.Common.TPL; using NzbDrone.Core.Configuration; using NzbDrone.Core.Http; using NzbDrone.Core.Parser; +using NzbDrone.Core.Security; using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.Framework @@ -25,7 +26,8 @@ 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 X509CertificateValidationService(Mocker.Resolve(), TestLogger)); + Mocker.SetConstant(new ManagedHttpDispatcher(Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve(), TestLogger)); Mocker.SetConstant(new HttpClient(Array.Empty(), Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve(), TestLogger)); Mocker.SetConstant(new RadarrCloudRequestBuilder()); } diff --git a/src/NzbDrone.Core.Test/IndexerTests/FileListTests/FileListFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/FileListTests/FileListFixture.cs index aa54caf92..8b742f47c 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/HDBitsTests/HDBitsFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/HDBitsTests/HDBitsFixture.cs index 99c716cb8..4bea0d2df 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/HDBitsTests/HDBitsFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/HDBitsTests/HDBitsFixture.cs @@ -1,5 +1,6 @@ -using System; +using System; using System.Linq; +using System.Net.Http; using System.Text; using FluentAssertions; using Moq; @@ -34,7 +35,7 @@ namespace NzbDrone.Core.Test.IndexerTests.HDBitsTests var responseJson = ReadAllText(fileName); Mocker.GetMock() - .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.POST))) + .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.Post))) .Returns(r => new HttpResponse(r, new HttpHeader(), responseJson)); var torrents = 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 e31bfc35b..1e1e2f53a 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabFixture.cs @@ -1,5 +1,6 @@ -using System; +using System; using System.Linq; +using System.Net.Http; using FluentAssertions; using Moq; using NUnit.Framework; @@ -41,7 +42,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 494e21780..992fd7419 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 2e6ff1f83..6836be0f8 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/PTPTests/PTPFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/PTPTests/PTPFixture.cs index 707f0f2a7..76c113f1a 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/PTPTests/PTPFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/PTPTests/PTPFixture.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System.Linq; +using System.Net.Http; using FluentAssertions; using Moq; using NUnit.Framework; @@ -34,11 +35,11 @@ namespace NzbDrone.Core.Test.IndexerTests.PTPTests var responseJson = ReadAllText(fileName); Mocker.GetMock() - .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.POST))) + .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.Post))) .Returns(r => new HttpResponse(r, new HttpHeader(), authStream.ToString())); 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 { ContentType = HttpAccept.Json.Value }, responseJson)); var torrents = Subject.FetchRecent(); diff --git a/src/NzbDrone.Core.Test/IndexerTests/RarbgTests/RarbgFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/RarbgTests/RarbgFixture.cs index 797aa9617..eab77a41d 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/TorznabTests/TorznabFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/TorznabTests/TorznabFixture.cs index 0ded98781..139e0c99c 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(); @@ -101,7 +102,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests var recentFeed = ReadAllText(@"Files/Indexers/Torznab/torznab_animetosho.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 1247c9e83..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.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 261f76e19..fa76c1d0d 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; @@ -25,7 +26,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/TMDb/User/TMDbUserImport.cs b/src/NzbDrone.Core/ImportLists/TMDb/User/TMDbUserImport.cs index 33a8c10c6..6a905b989 100644 --- a/src/NzbDrone.Core/ImportLists/TMDb/User/TMDbUserImport.cs +++ b/src/NzbDrone.Core/ImportLists/TMDb/User/TMDbUserImport.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Net.Http; using NLog; using NzbDrone.Common.Cloud; using NzbDrone.Common.Http; @@ -53,7 +54,7 @@ namespace NzbDrone.Core.ImportLists.TMDb.User .SetSegment("secondaryRoute", "request_token") .AddQueryParam("redirect_to", query["callbackUrl"]); - requestBuilder.Method = HttpMethod.POST; + requestBuilder.Method = HttpMethod.Post; var request = requestBuilder.Build(); @@ -78,7 +79,7 @@ namespace NzbDrone.Core.ImportLists.TMDb.User .SetSegment("secondaryRoute", "access_token") .AddQueryParam("request_token", query["requestToken"]); - requestBuilder.Method = HttpMethod.POST; + requestBuilder.Method = HttpMethod.Post; var request = requestBuilder.Build(); diff --git a/src/NzbDrone.Core/ImportLists/TMDb/User/TMDbUserRequestGenerator.cs b/src/NzbDrone.Core/ImportLists/TMDb/User/TMDbUserRequestGenerator.cs index cd5fd05b9..6838af1b9 100644 --- a/src/NzbDrone.Core/ImportLists/TMDb/User/TMDbUserRequestGenerator.cs +++ b/src/NzbDrone.Core/ImportLists/TMDb/User/TMDbUserRequestGenerator.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using System.Net.Http; using Newtonsoft.Json; using NLog; using NzbDrone.Common.Http; @@ -53,7 +54,7 @@ namespace NzbDrone.Core.ImportLists.TMDb.User requestBuilder.Accept(HttpAccept.Json); - requestBuilder.Method = HttpMethod.GET; + requestBuilder.Method = HttpMethod.Get; var jsonResponse = JsonConvert.DeserializeObject(HttpClient.Execute(requestBuilder.Build()).Content); diff --git a/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListRequestGenerator.cs b/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListRequestGenerator.cs index 3be121a9f..591c852a3 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListRequestGenerator.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListRequestGenerator.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Net.Http; using NzbDrone.Common.Http; using NzbDrone.Core.Notifications.Trakt; @@ -30,7 +31,7 @@ namespace NzbDrone.Core.ImportLists.Trakt.List var listName = Parser.Parser.ToUrlSlug(Settings.Listname.Trim()); link += $"users/{Settings.Username.Trim()}/lists/{listName}/items/movies?limit={Settings.Limit}"; - var request = new ImportListRequest(_traktProxy.BuildTraktRequest(link, HttpMethod.GET, Settings.AccessToken)); + var request = new ImportListRequest(_traktProxy.BuildTraktRequest(link, HttpMethod.Get, Settings.AccessToken)); yield return request; } diff --git a/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularRequestGenerator.cs b/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularRequestGenerator.cs index 6dac3c963..59004deee 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularRequestGenerator.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularRequestGenerator.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Net.Http; using NzbDrone.Common.Http; using NzbDrone.Core.Notifications.Trakt; @@ -71,7 +72,7 @@ namespace NzbDrone.Core.ImportLists.Trakt.Popular link += filtersAndLimit; - var request = new ImportListRequest(_traktProxy.BuildTraktRequest(link, HttpMethod.GET, Settings.AccessToken)); + var request = new ImportListRequest(_traktProxy.BuildTraktRequest(link, HttpMethod.Get, Settings.AccessToken)); yield return request; } diff --git a/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserRequestGenerator.cs b/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserRequestGenerator.cs index af1a94ee7..bc058e34b 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserRequestGenerator.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserRequestGenerator.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Net.Http; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Notifications.Trakt; @@ -42,7 +43,7 @@ namespace NzbDrone.Core.ImportLists.Trakt.User break; } - var request = new ImportListRequest(_traktProxy.BuildTraktRequest(link, HttpMethod.GET, Settings.AccessToken)); + var request = new ImportListRequest(_traktProxy.BuildTraktRequest(link, HttpMethod.Get, Settings.AccessToken)); yield return request; } diff --git a/src/NzbDrone.Core/Indexers/HDBits/HDBitsRequestGenerator.cs b/src/NzbDrone.Core/Indexers/HDBits/HDBitsRequestGenerator.cs index 3c4a2523a..c502e4a83 100644 --- a/src/NzbDrone.Core/Indexers/HDBits/HDBitsRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/HDBits/HDBitsRequestGenerator.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net.Http; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Common.Serializer; @@ -60,7 +61,7 @@ namespace NzbDrone.Core.Indexers.HDBits .Resource("/api/torrents") .Build(); - request.Method = HttpMethod.POST; + request.Method = HttpMethod.Post; const string appJson = "application/json"; request.Headers.Accept = appJson; request.Headers.ContentType = appJson; diff --git a/src/NzbDrone.Core/Messaging/Commands/CommandExecutor.cs b/src/NzbDrone.Core/Messaging/Commands/CommandExecutor.cs index fdd7e18af..e3d06d26d 100644 --- a/src/NzbDrone.Core/Messaging/Commands/CommandExecutor.cs +++ b/src/NzbDrone.Core/Messaging/Commands/CommandExecutor.cs @@ -49,11 +49,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/Join/JoinProxy.cs b/src/NzbDrone.Core/Notifications/Join/JoinProxy.cs index f0c976f83..8c144bea3 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 44828c7f3..f8b970f15 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/Plex/PlexTv/PlexTvService.cs b/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvService.cs index 533f5d866..aadf31388 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvService.cs +++ b/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvService.cs @@ -1,4 +1,5 @@ using System.Linq; +using System.Net.Http; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; @@ -37,7 +38,7 @@ namespace NzbDrone.Core.Notifications.Plex.PlexTv .AddQueryParam("X-Plex-Version", BuildInfo.Version.ToString()) .AddQueryParam("strong", true); - requestBuilder.Method = HttpMethod.POST; + requestBuilder.Method = HttpMethod.Post; var request = requestBuilder.Build(); diff --git a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerProxy.cs b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerProxy.cs index d90eb1365..117dfab71 100644 --- a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerProxy.cs +++ b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerProxy.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using System.Net.Http; using NLog; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; @@ -35,7 +36,7 @@ namespace NzbDrone.Core.Notifications.Plex.Server public List GetMovieSections(PlexServerSettings settings) { - var request = BuildRequest("library/sections", HttpMethod.GET, settings); + var request = BuildRequest("library/sections", HttpMethod.Get, settings); var response = ProcessRequest(request); CheckForError(response); @@ -65,7 +66,7 @@ namespace NzbDrone.Core.Notifications.Plex.Server public void Update(int sectionId, PlexServerSettings settings) { var resource = $"library/sections/{sectionId}/refresh"; - var request = BuildRequest(resource, HttpMethod.GET, settings); + var request = BuildRequest(resource, HttpMethod.Get, settings); var response = ProcessRequest(request); CheckForError(response); @@ -74,7 +75,7 @@ namespace NzbDrone.Core.Notifications.Plex.Server public void UpdateMovie(int metadataId, PlexServerSettings settings) { var resource = $"library/metadata/{metadataId}/refresh"; - var request = BuildRequest(resource, HttpMethod.PUT, settings); + var request = BuildRequest(resource, HttpMethod.Put, settings); var response = ProcessRequest(request); CheckForError(response); @@ -82,7 +83,7 @@ namespace NzbDrone.Core.Notifications.Plex.Server public string Version(PlexServerSettings settings) { - var request = BuildRequest("identity", HttpMethod.GET, settings); + var request = BuildRequest("identity", HttpMethod.Get, settings); var response = ProcessRequest(request); CheckForError(response); @@ -100,7 +101,7 @@ namespace NzbDrone.Core.Notifications.Plex.Server public List Preferences(PlexServerSettings settings) { - var request = BuildRequest(":/prefs", HttpMethod.GET, settings); + var request = BuildRequest(":/prefs", HttpMethod.Get, settings); var response = ProcessRequest(request); CheckForError(response); @@ -120,7 +121,7 @@ namespace NzbDrone.Core.Notifications.Plex.Server { var guid = $"com.plexapp.agents.imdb://{imdbId}?lang={language}"; var resource = $"library/sections/{sectionId}/all?guid={System.Web.HttpUtility.UrlEncode(guid)}"; - var request = BuildRequest(resource, HttpMethod.GET, settings); + var request = BuildRequest(resource, HttpMethod.Get, settings); var response = ProcessRequest(request); CheckForError(response); diff --git a/src/NzbDrone.Core/Notifications/PushBullet/PushBulletProxy.cs b/src/NzbDrone.Core/Notifications/PushBullet/PushBulletProxy.cs index e66aac0ef..ec543f528 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.AddBasicAuthentication(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 a8a44ef11..6a3b41ca9 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.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/Trakt/TraktProxy.cs b/src/NzbDrone.Core/Notifications/Trakt/TraktProxy.cs index cf12fbb01..2120eb341 100644 --- a/src/NzbDrone.Core/Notifications/Trakt/TraktProxy.cs +++ b/src/NzbDrone.Core/Notifications/Trakt/TraktProxy.cs @@ -1,3 +1,4 @@ +using System.Net.Http; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; @@ -35,7 +36,7 @@ namespace NzbDrone.Core.Notifications.Trakt public void AddToCollection(TraktCollectMoviesResource payload, string accessToken) { - var request = BuildTraktRequest("sync/collection", HttpMethod.POST, accessToken); + var request = BuildTraktRequest("sync/collection", HttpMethod.Post, accessToken); request.Headers.ContentType = "application/json"; request.SetContent(payload.ToJson()); @@ -53,7 +54,7 @@ namespace NzbDrone.Core.Notifications.Trakt public void RemoveFromCollection(TraktCollectMoviesResource payload, string accessToken) { - var request = BuildTraktRequest("sync/collection/remove", HttpMethod.POST, accessToken); + var request = BuildTraktRequest("sync/collection/remove", HttpMethod.Post, accessToken); request.Headers.ContentType = "application/json"; request.SetContent(payload.ToJson()); @@ -71,7 +72,7 @@ namespace NzbDrone.Core.Notifications.Trakt public string GetUserName(string accessToken) { - var request = BuildTraktRequest("users/settings", HttpMethod.GET, accessToken); + var request = BuildTraktRequest("users/settings", HttpMethod.Get, accessToken); try { diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookMethod.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookMethod.cs index 5d6e859a6..a1d7b5137 100755 --- 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 489eeb41f..d196b5579 100755 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookProxy.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookProxy.cs @@ -1,3 +1,5 @@ +using System; +using System.Net.Http; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Common.Serializer; @@ -26,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/Security/X509CertificateValidationService.cs b/src/NzbDrone.Core/Security/X509CertificateValidationService.cs index 6d505ca0a..05b9e5239 100644 --- a/src/NzbDrone.Core/Security/X509CertificateValidationService.cs +++ b/src/NzbDrone.Core/Security/X509CertificateValidationService.cs @@ -4,13 +4,12 @@ using System.Net.Security; using System.Security.Cryptography.X509Certificates; using NLog; using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http.Dispatchers; using NzbDrone.Core.Configuration; -using NzbDrone.Core.Lifecycle; -using NzbDrone.Core.Messaging.Events; namespace NzbDrone.Core.Security { - public class X509CertificateValidationService : IHandle + public class X509CertificateValidationService : ICertificateValidationService { private readonly IConfigService _configService; private readonly Logger _logger; @@ -21,19 +20,16 @@ namespace NzbDrone.Core.Security _logger = logger; } - private bool ShouldByPassValidationError(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + public bool ShouldByPassValidationError(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { - var request = sender as HttpWebRequest; - - if (request == null) + if (sender is not SslStream request) { return true; } - var cert2 = certificate as X509Certificate2; - if (cert2 != null && request != null && cert2.SignatureAlgorithm.FriendlyName == "md5RSA") + if (certificate is X509Certificate2 cert2 && cert2.SignatureAlgorithm.FriendlyName == "md5RSA") { - _logger.Error("https://{0} uses the obsolete md5 hash in it's https certificate, if that is your certificate, please (re)create certificate with better algorithm as soon as possible.", request.RequestUri.Authority); + _logger.Error("https://{0} uses the obsolete md5 hash in it's https certificate, if that is your certificate, please (re)create certificate with better algorithm as soon as possible.", request.TargetHostName); } if (sslPolicyErrors == SslPolicyErrors.None) @@ -41,12 +37,12 @@ namespace NzbDrone.Core.Security return true; } - if (request.RequestUri.Host == "localhost" || request.RequestUri.Host == "127.0.0.1") + if (request.TargetHostName == "localhost" || request.TargetHostName == "127.0.0.1") { return true; } - var ipAddresses = GetIPAddresses(request.RequestUri.Host); + var ipAddresses = GetIPAddresses(request.TargetHostName); var certificateValidation = _configService.CertificateValidation; if (certificateValidation == CertificateValidationType.Disabled) @@ -60,7 +56,7 @@ namespace NzbDrone.Core.Security return true; } - _logger.Error("Certificate validation for {0} failed. {1}", request.Address, sslPolicyErrors); + _logger.Error("Certificate validation for {0} failed. {1}", request.TargetHostName, sslPolicyErrors); return false; } @@ -74,10 +70,5 @@ namespace NzbDrone.Core.Security return Dns.GetHostEntry(host).AddressList; } - - public void Handle(ApplicationStartedEvent message) - { - ServicePointManager.ServerCertificateValidationCallback = ShouldByPassValidationError; - } } } diff --git a/src/NzbDrone.Core/TinyTwitter.cs b/src/NzbDrone.Core/TinyTwitter.cs index 9f772095d..acd47fb0c 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 = client.ResponseHeaders; + var headers = response.Headers; - 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"); + headers.CacheControl.NoStore.Should().BeTrue(); + headers.CacheControl.NoCache.Should().BeTrue(); + headers.Pragma.Should().Contain(new NameValueHeaderValue("no-cache")); + + response.Content.Headers.Expires.Should().BeBefore(DateTime.UtcNow); } } }