diff --git a/src/NzbDrone.Common/Http/BasicNetworkCredential.cs b/src/NzbDrone.Common/Http/BasicNetworkCredential.cs new file mode 100644 index 0000000000..26710f766f --- /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 0a33085a19..9e75098517 100644 --- a/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs +++ b/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs @@ -4,9 +4,9 @@ using System.Net; 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 @@ namespace NzbDrone.Common.Http.Dispatchers 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 @@ namespace NzbDrone.Common.Http.Dispatchers _userAgentBuilder = userAgentBuilder; _httpClientCache = cacheManager.GetCache(typeof(ManagedHttpDispatcher)); + _credentialCache = cacheManager.GetCache(typeof(ManagedHttpDispatcher), "credentialcache"); } public HttpResponse GetResponse(HttpRequest request, CookieContainer cookies) @@ -64,6 +66,26 @@ namespace NzbDrone.Common.Http.Dispatchers 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 @@ namespace NzbDrone.Common.Http.Dispatchers 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 @@ namespace NzbDrone.Common.Http.Dispatchers 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 a911081da2..1232219f62 100644 --- a/src/NzbDrone.Common/Http/HttpRequest.cs +++ b/src/NzbDrone.Common/Http/HttpRequest.cs @@ -38,6 +38,7 @@ namespace NzbDrone.Common.Http 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 @@ namespace NzbDrone.Common.Http 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 a07778f997..32fafd56f9 100644 --- a/src/NzbDrone.Common/Http/HttpRequestBuilder.cs +++ b/src/NzbDrone.Common/Http/HttpRequestBuilder.cs @@ -26,10 +26,9 @@ namespace NzbDrone.Common.Http 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 @@ namespace NzbDrone.Common.Http 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 b78a66df5b..b695ef1da7 100644 --- a/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenProxy.cs @@ -73,7 +73,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken 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 092325bc99..8468023800 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs @@ -229,7 +229,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget 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 c094edfb9e..b89248cffe 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs @@ -293,7 +293,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent 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 58aaf90101..ea889ccfb5 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs @@ -335,7 +335,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent 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 b30d14497a..1172b600aa 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs @@ -200,7 +200,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission .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 252cca7a1f..9ced7181b2 100644 --- a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentProxy.cs @@ -196,7 +196,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent .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 ec543f5288..495e7fe930 100644 --- a/src/NzbDrone.Core/Notifications/PushBullet/PushBulletProxy.cs +++ b/src/NzbDrone.Core/Notifications/PushBullet/PushBulletProxy.cs @@ -102,7 +102,7 @@ namespace NzbDrone.Core.Notifications.PushBullet 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 @@ namespace NzbDrone.Core.Notifications.PushBullet 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 d196b55791..23a7fbdc8c 100755 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookProxy.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookProxy.cs @@ -40,7 +40,7 @@ namespace NzbDrone.Core.Notifications.Webhook 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 3e69370bef..603a7fcf27 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/XbmcJsonApiProxy.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/XbmcJsonApiProxy.cs @@ -84,7 +84,7 @@ namespace NzbDrone.Core.Notifications.Xbmc if (!settings.Username.IsNullOrWhiteSpace()) { - request.AddBasicAuthentication(settings.Username, settings.Password); + request.Credentials = new BasicNetworkCredential(settings.Username, settings.Password); } var response = _httpClient.Execute(request);