diff --git a/src/NzbDrone.Common/Http/BasicNetworkCredential.cs b/src/NzbDrone.Common/Http/BasicNetworkCredential.cs new file mode 100644 index 000000000..26710f766 --- /dev/null +++ b/src/NzbDrone.Common/Http/BasicNetworkCredential.cs @@ -0,0 +1,12 @@ +using System.Net; + +namespace NzbDrone.Common.Http +{ + public class BasicNetworkCredential : NetworkCredential + { + public BasicNetworkCredential(string user, string pass) + : base(user, pass) + { + } + } +} diff --git a/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs b/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs index 0a33085a1..9e7509851 100644 --- a/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs +++ b/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs @@ -4,9 +4,9 @@ using System.Net.Http; using System.Net.Security; using System.Net.Sockets; +using System.Text; using System.Threading; using System.Threading.Tasks; -using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http.Proxy; @@ -26,6 +26,7 @@ public class ManagedHttpDispatcher : IHttpDispatcher private readonly ICertificateValidationService _certificateValidationService; private readonly IUserAgentBuilder _userAgentBuilder; private readonly ICached _httpClientCache; + private readonly ICached _credentialCache; public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider, ICreateManagedWebProxy createManagedWebProxy, @@ -39,6 +40,7 @@ public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider, _userAgentBuilder = userAgentBuilder; _httpClientCache = cacheManager.GetCache(typeof(ManagedHttpDispatcher)); + _credentialCache = cacheManager.GetCache(typeof(ManagedHttpDispatcher), "credentialcache"); } public HttpResponse GetResponse(HttpRequest request, CookieContainer cookies) @@ -64,6 +66,26 @@ public HttpResponse GetResponse(HttpRequest request, CookieContainer cookies) cts.CancelAfter(TimeSpan.FromSeconds(100)); } + if (request.Credentials != null) + { + if (request.Credentials is BasicNetworkCredential bc) + { + // Manually set header to avoid initial challenge response + var authInfo = bc.UserName + ":" + bc.Password; + authInfo = Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes(authInfo)); + requestMessage.Headers.Add("Authorization", "Basic " + authInfo); + } + else if (request.Credentials is NetworkCredential nc) + { + var creds = GetCredentialCache(); + foreach (var authtype in new[] { "Basic", "Digest" }) + { + creds.Remove((Uri)request.Url, authtype); + creds.Add((Uri)request.Url, authtype, nc); + } + } + } + if (request.ContentData != null) { requestMessage.Content = new ByteArrayContent(request.ContentData); @@ -120,6 +142,8 @@ protected virtual System.Net.Http.HttpClient CreateHttpClient(HttpProxySettings AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Brotli, UseCookies = false, // sic - we don't want to use a shared cookie container AllowAutoRedirect = false, + Credentials = GetCredentialCache(), + PreAuthenticate = true, MaxConnectionsPerServer = 12, ConnectCallback = onConnect, SslOptions = new SslClientAuthenticationOptions @@ -204,6 +228,11 @@ private void AddContentHeader(HttpRequestMessage request, string header, string headers.Add(header, value); } + private CredentialCache GetCredentialCache() + { + return _credentialCache.Get("credentialCache", () => new CredentialCache()); + } + 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. diff --git a/src/NzbDrone.Common/Http/HttpRequest.cs b/src/NzbDrone.Common/Http/HttpRequest.cs index a911081da..1232219f6 100644 --- a/src/NzbDrone.Common/Http/HttpRequest.cs +++ b/src/NzbDrone.Common/Http/HttpRequest.cs @@ -38,6 +38,7 @@ public HttpRequest(string url, HttpAccept httpAccept = null) public HttpHeader Headers { get; set; } public byte[] ContentData { get; set; } public string ContentSummary { get; set; } + public ICredentials Credentials { get; set; } public bool SuppressHttpError { get; set; } public IEnumerable SuppressHttpErrorStatusCodes { get; set; } public bool UseSimplifiedUserAgent { get; set; } @@ -89,12 +90,5 @@ public void SetContent(string data) var encoding = HttpHeader.GetEncodingFromContentType(Headers.ContentType); ContentData = encoding.GetBytes(data); } - - public void AddBasicAuthentication(string username, string password) - { - var authInfo = Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes($"{username}:{password}")); - - Headers.Set("Authorization", "Basic " + authInfo); - } } } diff --git a/src/NzbDrone.Common/Http/HttpRequestBuilder.cs b/src/NzbDrone.Common/Http/HttpRequestBuilder.cs index a07778f99..32fafd56f 100644 --- a/src/NzbDrone.Common/Http/HttpRequestBuilder.cs +++ b/src/NzbDrone.Common/Http/HttpRequestBuilder.cs @@ -26,10 +26,9 @@ public class HttpRequestBuilder public bool ConnectionKeepAlive { get; set; } public TimeSpan RateLimit { get; set; } public bool LogResponseContent { get; set; } - public NetworkCredential NetworkCredential { get; set; } + public ICredentials NetworkCredential { get; set; } public Dictionary Cookies { get; private set; } public List FormData { get; private set; } - public Action PostProcess { get; set; } public HttpRequestBuilder(string baseUrl) @@ -109,13 +108,7 @@ protected virtual void Apply(HttpRequest request) request.ConnectionKeepAlive = ConnectionKeepAlive; request.RateLimit = RateLimit; request.LogResponseContent = LogResponseContent; - - if (NetworkCredential != null) - { - var authInfo = NetworkCredential.UserName + ":" + NetworkCredential.Password; - authInfo = Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes(authInfo)); - request.Headers.Set("Authorization", "Basic " + authInfo); - } + request.Credentials = NetworkCredential; foreach (var header in Headers) { diff --git a/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenProxy.cs b/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenProxy.cs index b78a66df5..b695ef1da 100644 --- a/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenProxy.cs @@ -73,7 +73,7 @@ private T ProcessRequest(HadoukenSettings settings, string method, params obj baseUrl = HttpUri.CombinePath(baseUrl, "api"); var requestBuilder = new JsonRpcRequestBuilder(baseUrl, method, parameters); requestBuilder.LogResponseContent = true; - requestBuilder.NetworkCredential = new NetworkCredential(settings.Username, settings.Password); + requestBuilder.NetworkCredential = new BasicNetworkCredential(settings.Username, settings.Password); requestBuilder.Headers.Add("Accept-Encoding", "gzip,deflate"); var httpRequest = requestBuilder.Build(); diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs index 092325bc9..846802380 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs @@ -229,7 +229,7 @@ private T ProcessRequest(NzbgetSettings settings, string method, params objec var requestBuilder = new JsonRpcRequestBuilder(baseUrl, method, parameters); requestBuilder.LogResponseContent = true; - requestBuilder.NetworkCredential = new NetworkCredential(settings.Username, settings.Password); + requestBuilder.NetworkCredential = new BasicNetworkCredential(settings.Username, settings.Password); var httpRequest = requestBuilder.Build(); diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs index c094edfb9..b89248cff 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs @@ -293,7 +293,7 @@ private HttpRequestBuilder BuildRequest(QBittorrentSettings settings) var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase) { LogResponseContent = true, - NetworkCredential = new NetworkCredential(settings.Username, settings.Password) + NetworkCredential = new BasicNetworkCredential(settings.Username, settings.Password) }; return requestBuilder; } diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs index 58aaf9010..ea889ccfb 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs @@ -335,7 +335,7 @@ private HttpRequestBuilder BuildRequest(QBittorrentSettings settings) var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase) { LogResponseContent = true, - NetworkCredential = new NetworkCredential(settings.Username, settings.Password) + NetworkCredential = new BasicNetworkCredential(settings.Username, settings.Password) }; return requestBuilder; } diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs index b30d14497..1172b600a 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs @@ -200,7 +200,7 @@ private HttpRequestBuilder BuildRequest(TransmissionSettings settings) .Accept(HttpAccept.Json); requestBuilder.LogResponseContent = true; - requestBuilder.NetworkCredential = new NetworkCredential(settings.Username, settings.Password); + requestBuilder.NetworkCredential = new BasicNetworkCredential(settings.Username, settings.Password); requestBuilder.AllowAutoRedirect = false; return requestBuilder; diff --git a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentProxy.cs b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentProxy.cs index 252cca7a1..9ced7181b 100644 --- a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentProxy.cs @@ -196,7 +196,7 @@ private HttpRequestBuilder BuildRequest(UTorrentSettings settings) .Accept(HttpAccept.Json); requestBuilder.LogResponseContent = true; - requestBuilder.NetworkCredential = new NetworkCredential(settings.Username, settings.Password); + requestBuilder.NetworkCredential = new BasicNetworkCredential(settings.Username, settings.Password); return requestBuilder; } diff --git a/src/NzbDrone.Core/Notifications/PushBullet/PushBulletProxy.cs b/src/NzbDrone.Core/Notifications/PushBullet/PushBulletProxy.cs index ec543f528..495e7fe93 100644 --- a/src/NzbDrone.Core/Notifications/PushBullet/PushBulletProxy.cs +++ b/src/NzbDrone.Core/Notifications/PushBullet/PushBulletProxy.cs @@ -102,7 +102,7 @@ public List GetDevices(PushBulletSettings settings) var request = requestBuilder.Build(); request.Method = HttpMethod.Get; - request.AddBasicAuthentication(settings.ApiKey, string.Empty); + request.Credentials = new BasicNetworkCredential(settings.ApiKey, string.Empty); var response = _httpClient.Execute(request); @@ -198,7 +198,7 @@ private void SendNotification(string title, string message, HttpRequestBuilder r var request = requestBuilder.Build(); - request.AddBasicAuthentication(settings.ApiKey, string.Empty); + request.Credentials = new BasicNetworkCredential(settings.ApiKey, string.Empty); _httpClient.Execute(request); } diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookProxy.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookProxy.cs index d196b5579..23a7fbdc8 100755 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookProxy.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookProxy.cs @@ -40,7 +40,7 @@ public void SendWebhook(WebhookPayload body, WebhookSettings settings) if (settings.Username.IsNotNullOrWhiteSpace() || settings.Password.IsNotNullOrWhiteSpace()) { - request.AddBasicAuthentication(settings.Username, settings.Password); + request.Credentials = new BasicNetworkCredential(settings.Username, settings.Password); } _httpClient.Execute(request); diff --git a/src/NzbDrone.Core/Notifications/Xbmc/XbmcJsonApiProxy.cs b/src/NzbDrone.Core/Notifications/Xbmc/XbmcJsonApiProxy.cs index 3e69370be..603a7fcf2 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/XbmcJsonApiProxy.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/XbmcJsonApiProxy.cs @@ -84,7 +84,7 @@ private string ProcessRequest(XbmcSettings settings, string method, params objec if (!settings.Username.IsNullOrWhiteSpace()) { - request.AddBasicAuthentication(settings.Username, settings.Password); + request.Credentials = new BasicNetworkCredential(settings.Username, settings.Password); } var response = _httpClient.Execute(request);