From 25d481d5d9974ec317af34ec0e907de8feb64842 Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Sun, 28 Feb 2016 17:34:24 +0100 Subject: [PATCH] Migrated all Download client proxies from RestSharp to HttpClient. --- src/NzbDrone.Common/Http/JsonRpcResponse.cs | 3 +- .../NzbVortexTests/NzbVortexFixture.cs | 17 +- .../Download/Clients/Deluge/DelugeProxy.cs | 209 ++++++------ .../Download/Clients/Deluge/DelugeResponse.cs | 11 - .../Download/Clients/NzbVortex/NzbVortex.cs | 10 +- .../Clients/NzbVortex/NzbVortexFiles.cs | 10 - .../Clients/NzbVortex/NzbVortexProxy.cs | 257 +++++++------- .../Clients/NzbVortex/NzbVortexQueue.cs | 11 - .../Responses/NzbVortexFilesResponse.cs | 9 + .../Responses/NzbVortexQueueResponse.cs | 11 + .../Download/Clients/Nzbget/JsonRequest.cs | 21 -- .../Download/Clients/Nzbget/NzbgetProxy.cs | 119 +++---- .../Download/Clients/Sabnzbd/SabnzbdProxy.cs | 132 ++++---- .../Clients/Transmission/TransmissionProxy.cs | 127 +++---- .../qBittorrent/DigestAuthenticator.cs | 22 -- .../Clients/qBittorrent/QBittorrentProxy.cs | 315 ++++++++++-------- .../Clients/uTorrent/UTorrentProxy.cs | 260 +++++++-------- src/NzbDrone.Core/NzbDrone.Core.csproj | 17 +- 18 files changed, 728 insertions(+), 833 deletions(-) delete mode 100644 src/NzbDrone.Core/Download/Clients/Deluge/DelugeResponse.cs delete mode 100644 src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexFiles.cs delete mode 100644 src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexQueue.cs create mode 100644 src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexFilesResponse.cs create mode 100644 src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexQueueResponse.cs delete mode 100644 src/NzbDrone.Core/Download/Clients/Nzbget/JsonRequest.cs delete mode 100644 src/NzbDrone.Core/Download/Clients/qBittorrent/DigestAuthenticator.cs diff --git a/src/NzbDrone.Common/Http/JsonRpcResponse.cs b/src/NzbDrone.Common/Http/JsonRpcResponse.cs index 3d7b329b3..4f12ea779 100644 --- a/src/NzbDrone.Common/Http/JsonRpcResponse.cs +++ b/src/NzbDrone.Common/Http/JsonRpcResponse.cs @@ -1,4 +1,5 @@ using System; +using Newtonsoft.Json.Linq; namespace NzbDrone.Common.Http { @@ -6,6 +7,6 @@ public class JsonRpcResponse { public string Id { get; set; } public T Result { get; set; } - public object Error { get; set; } + public JToken Error { get; set; } } } diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbVortexTests/NzbVortexFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbVortexTests/NzbVortexFixture.cs index f3fa6c279..ccdaba3f1 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbVortexTests/NzbVortexFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbVortexTests/NzbVortexFixture.cs @@ -88,10 +88,7 @@ protected virtual void GivenQueue(NzbVortexQueueItem queue) Mocker.GetMock() .Setup(s => s.GetQueue(It.IsAny(), It.IsAny())) - .Returns(new NzbVortexQueue - { - Items = list - }); + .Returns(list); } [Test] @@ -244,7 +241,7 @@ public void should_get_files_if_completed_download_is_not_in_a_job_folder() Mocker.GetMock() .Setup(s => s.GetFiles(It.IsAny(), It.IsAny())) - .Returns(new NzbVortexFiles{ Files = new List { new NzbVortexFile { FileName = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE.mkv" } } }); + .Returns(new List { new NzbVortexFile { FileName = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE.mkv" } }); _completed.State = NzbVortexStateType.Done; GivenQueue(_completed); @@ -263,11 +260,11 @@ public void should_be_warning_if_more_than_one_file_is_not_in_a_job_folder() Mocker.GetMock() .Setup(s => s.GetFiles(It.IsAny(), It.IsAny())) - .Returns(new NzbVortexFiles { Files = new List - { - new NzbVortexFile { FileName = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE.mkv" }, - new NzbVortexFile { FileName = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE.nfo" } - } }); + .Returns(new List + { + new NzbVortexFile { FileName = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE.mkv" }, + new NzbVortexFile { FileName = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE.nfo" } + }); _completed.State = NzbVortexStateType.Done; GivenQueue(_completed); diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeProxy.cs b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeProxy.cs index 1bf315c1c..c64f55c3e 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeProxy.cs @@ -4,9 +4,10 @@ using System.Net; using Newtonsoft.Json.Linq; using NLog; +using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; -using NzbDrone.Core.Rest; -using RestSharp; +using NzbDrone.Common.Http; +using NzbDrone.Common.Serializer; namespace NzbDrone.Core.Download.Clients.Deluge { @@ -33,30 +34,31 @@ public class DelugeProxy : IDelugeProxy { private static readonly string[] requiredProperties = new string[] { "hash", "name", "state", "progress", "eta", "message", "is_finished", "save_path", "total_size", "total_done", "time_added", "active_time", "ratio", "is_auto_managed", "stop_at_ratio", "remove_at_ratio", "stop_ratio" }; + private readonly IHttpClient _httpClient; private readonly Logger _logger; - private string _authPassword; - private CookieContainer _authCookieContainer; + private readonly ICached> _authCookieCache; - private static int _callId; - - public DelugeProxy(Logger logger) + public DelugeProxy(ICacheManager cacheManager, IHttpClient httpClient, Logger logger) { + _httpClient = httpClient; _logger = logger; + + _authCookieCache = cacheManager.GetCache>(GetType(), "authCookies"); } public string GetVersion(DelugeSettings settings) { var response = ProcessRequest(settings, "daemon.info"); - return response.Result; + return response; } public Dictionary GetConfig(DelugeSettings settings) { var response = ProcessRequest>(settings, "core.get_config"); - return response.Result; + return response; } public DelugeTorrent[] GetTorrents(DelugeSettings settings) @@ -67,7 +69,7 @@ public DelugeTorrent[] GetTorrents(DelugeSettings settings) //var response = ProcessRequest>(settings, "core.get_torrents_status", filter, new String[0]); var response = ProcessRequest(settings, "web.update_ui", requiredProperties, filter); - return GetTorrents(response.Result); + return GetTorrents(response); } public DelugeTorrent[] GetTorrentsByLabel(string label, DelugeSettings settings) @@ -78,28 +80,28 @@ public DelugeTorrent[] GetTorrentsByLabel(string label, DelugeSettings settings) //var response = ProcessRequest>(settings, "core.get_torrents_status", filter, new String[0]); var response = ProcessRequest(settings, "web.update_ui", requiredProperties, filter); - return GetTorrents(response.Result); + return GetTorrents(response); } public string AddTorrentFromMagnet(string magnetLink, DelugeSettings settings) { var response = ProcessRequest(settings, "core.add_torrent_magnet", magnetLink, new JObject()); - return response.Result; + return response; } public string AddTorrentFromFile(string filename, byte[] fileContent, DelugeSettings settings) { var response = ProcessRequest(settings, "core.add_torrent_file", filename, Convert.ToBase64String(fileContent), new JObject()); - return response.Result; + return response; } public bool RemoveTorrent(string hashString, bool removeData, DelugeSettings settings) { var response = ProcessRequest(settings, "core.remove_torrent", hashString, removeData); - return response.Result; + return response; } public void MoveTorrentToTopInQueue(string hash, DelugeSettings settings) @@ -111,21 +113,21 @@ public string[] GetAvailablePlugins(DelugeSettings settings) { var response = ProcessRequest(settings, "core.get_available_plugins"); - return response.Result; + return response; } public string[] GetEnabledPlugins(DelugeSettings settings) { var response = ProcessRequest(settings, "core.get_enabled_plugins"); - return response.Result; + return response; } public string[] GetAvailableLabels(DelugeSettings settings) { var response = ProcessRequest(settings, "label.get_labels"); - return response.Result; + return response; } public void SetTorrentConfiguration(string hash, string key, object value, DelugeSettings settings) @@ -143,7 +145,7 @@ public void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration var ratioArguments = new Dictionary(); ratioArguments.Add("stop_ratio", seedConfiguration.Ratio.Value); - ProcessRequest(settings, "core.set_torrent_options", new string[]{hash}, ratioArguments); + ProcessRequest(settings, "core.set_torrent_options", new string[] { hash }, ratioArguments); } } @@ -157,134 +159,122 @@ public void SetLabel(string hash, string label, DelugeSettings settings) ProcessRequest(settings, "label.set_torrent", hash, label); } - protected DelugeResponse ProcessRequest(DelugeSettings settings, string action, params object[] arguments) + private JsonRpcRequestBuilder BuildRequest(DelugeSettings settings) { - var client = BuildClient(settings); + string url = HttpRequestBuilder.BuildBaseUrl(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase); - DelugeResponse response; + var builder = new JsonRpcRequestBuilder(url); + + builder.Resource("json"); + builder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15); - try - { - response = ProcessRequest(client, action, arguments); - } - catch (WebException ex) - { - if (ex.Status == WebExceptionStatus.Timeout) - { - _logger.Debug("Deluge timeout during request, daemon connection may have been broken. Attempting to reconnect."); - response = new DelugeResponse(); - response.Error = new DelugeError(); - response.Error.Code = 2; - } - else - { - throw; - } - } + AuthenticateClient(builder, settings); + + return builder; + } + + protected TResult ProcessRequest(DelugeSettings settings, string method, params object[] arguments) + { + var requestBuilder = BuildRequest(settings); + + var response = ProcessRequest(requestBuilder, method, arguments); if (response.Error != null) { - if (response.Error.Code == 1 || response.Error.Code == 2) + var error = response.Error.ToObject(); + if (error.Code == 1 || error.Code == 2) { - AuthenticateClient(client); + AuthenticateClient(requestBuilder, settings, true); - response = ProcessRequest(client, action, arguments); + response = ProcessRequest(requestBuilder, method, arguments); if (response.Error == null) { - return response; + return response.Result; } + error = response.Error.ToObject(); - throw new DownloadClientAuthenticationException(response.Error.Message); + throw new DownloadClientAuthenticationException(error.Message); } - throw new DelugeException(response.Error.Message, response.Error.Code); + throw new DelugeException(error.Message, error.Code); } - return response; + return response.Result; } - - private DelugeResponse ProcessRequest(IRestClient client, string action, object[] arguments) + + private JsonRpcResponse ProcessRequest(JsonRpcRequestBuilder requestBuilder, string method, params object[] arguments) { - var request = new RestRequest(Method.POST); - request.Resource = "json"; - request.RequestFormat = DataFormat.Json; - request.AddHeader("Accept-Encoding", "gzip,deflate"); + var request = requestBuilder.Call(method, arguments).Build(); - var data = new Dictionary(); - data.Add("id", GetCallId()); - data.Add("method", action); - - if (arguments != null) + HttpResponse response; + try { - data.Add("params", arguments); + response = _httpClient.Execute(request); + + return Json.Deserialize>(response.Content); + } + catch (HttpException ex) + { + if (ex.Response.StatusCode == HttpStatusCode.RequestTimeout) + { + _logger.Debug("Deluge timeout during request, daemon connection may have been broken. Attempting to reconnect."); + return new JsonRpcResponse() + { + Error = JToken.Parse("{ Code = 2 }") + }; + } + else + { + throw new DownloadClientException("Unable to connect to Deluge, please check your settings", ex); + } } - - request.AddBody(data); - - _logger.Debug("Url: {0} Action: {1}", client.BuildUri(request), action); - var response = client.ExecuteAndValidate>(request); - - return response; } - private IRestClient BuildClient(DelugeSettings settings) + private void AuthenticateClient(JsonRpcRequestBuilder requestBuilder, DelugeSettings settings, bool reauthenticate = false) { - var protocol = settings.UseSsl ? "https" : "http"; + var authKey = string.Format("{0}:{1}", requestBuilder.BaseUrl, settings.Password); - string url; - if (!settings.UrlBase.IsNullOrWhiteSpace()) + var cookies = _authCookieCache.Find(authKey); + + if (cookies == null || reauthenticate) { - url = string.Format(@"{0}://{1}:{2}/{3}", protocol, settings.Host, settings.Port, settings.UrlBase.Trim('/')); + _authCookieCache.Remove(authKey); + + var authLoginRequest = requestBuilder.Call("auth.login", settings.Password).Build(); + var response = _httpClient.Execute(authLoginRequest); + var result = Json.Deserialize>(response.Content); + if (!result.Result) + { + _logger.Debug("Deluge authentication failed."); + throw new DownloadClientAuthenticationException("Failed to authenticate with Deluge."); + } + _logger.Debug("Deluge authentication succeeded."); + + cookies = response.GetCookies(); + + _authCookieCache.Set(authKey, cookies); + + requestBuilder.SetCookies(cookies); + + ConnectDaemon(requestBuilder); } else { - url = string.Format(@"{0}://{1}:{2}", protocol, settings.Host, settings.Port); + requestBuilder.SetCookies(cookies); } - - var restClient = RestClientFactory.BuildClient(url); - restClient.Timeout = 15000; - - if (_authPassword != settings.Password || _authCookieContainer == null) - { - _authPassword = settings.Password; - AuthenticateClient(restClient); - } - else - { - restClient.CookieContainer = _authCookieContainer; - } - - return restClient; } - private void AuthenticateClient(IRestClient restClient) + private void ConnectDaemon(JsonRpcRequestBuilder requestBuilder) { - restClient.CookieContainer = new CookieContainer(); - - var result = ProcessRequest(restClient, "auth.login", new object[] { _authPassword }); - - if (!result.Result) - { - _logger.Debug("Deluge authentication failed."); - throw new DownloadClientAuthenticationException("Failed to authenticate with Deluge."); - } - _logger.Debug("Deluge authentication succeeded."); - _authCookieContainer = restClient.CookieContainer; - - ConnectDaemon(restClient); - } - - private void ConnectDaemon(IRestClient restClient) - { - var resultConnected = ProcessRequest(restClient, "web.connected", new object[0]); + var resultConnected = ProcessRequest(requestBuilder, "web.connected"); if (resultConnected.Result) { return; } - var resultHosts = ProcessRequest>(restClient, "web.get_hosts", new object[0]); + var resultHosts = ProcessRequest>(requestBuilder, "web.get_hosts"); if (resultHosts.Result != null) { @@ -293,7 +283,7 @@ private void ConnectDaemon(IRestClient restClient) if (connection != null) { - ProcessRequest(restClient, "web.connect", new object[] { connection[0] }); + ProcessRequest(requestBuilder, "web.connect", new object[] { connection[0] }); } else { @@ -302,11 +292,6 @@ private void ConnectDaemon(IRestClient restClient) } } - private int GetCallId() - { - return System.Threading.Interlocked.Increment(ref _callId); - } - private DelugeTorrent[] GetTorrents(DelugeUpdateUIResult result) { if (result.Torrents == null) diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeResponse.cs b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeResponse.cs deleted file mode 100644 index 3c4fe26da..000000000 --- a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeResponse.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace NzbDrone.Core.Download.Clients.Deluge -{ - public class DelugeResponse - { - public int Id { get; set; } - public TResult Result { get; set; } - public DelugeError Error { get; set; } - } -} diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs index eef799364..3d8017d57 100644 --- a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs @@ -53,7 +53,7 @@ public override string Name public override IEnumerable GetItems() { - NzbVortexQueue vortexQueue; + List vortexQueue; try { @@ -67,7 +67,7 @@ public override IEnumerable GetItems() var queueItems = new List(); - foreach (var vortexQueueItem in vortexQueue.Items) + foreach (var vortexQueueItem in vortexQueue) { var queueItem = new DownloadClientItem(); @@ -132,7 +132,7 @@ public override void RemoveItem(string downloadId, bool deleteData) else { var queue = _proxy.GetQueue(30, Settings); - var queueItem = queue.Items.FirstOrDefault(c => c.AddUUID == downloadId); + var queueItem = queue.FirstOrDefault(c => c.AddUUID == downloadId); if (queueItem != null) { @@ -249,7 +249,7 @@ private OsPath GetOutputPath(NzbVortexQueueItem vortexQueueItem, DownloadClientI var filesResponse = _proxy.GetFiles(vortexQueueItem.Id, Settings); - if (filesResponse.Files.Count > 1) + if (filesResponse.Count > 1) { var message = string.Format("Download contains multiple files and is not in a job folder: {0}", outputPath); @@ -259,7 +259,7 @@ private OsPath GetOutputPath(NzbVortexQueueItem vortexQueueItem, DownloadClientI _logger.Debug(message); } - return new OsPath(Path.Combine(outputPath.FullPath, filesResponse.Files.First().FileName)); + return new OsPath(Path.Combine(outputPath.FullPath, filesResponse.First().FileName)); } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexFiles.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexFiles.cs deleted file mode 100644 index ff9150001..000000000 --- a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexFiles.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace NzbDrone.Core.Download.Clients.NzbVortex -{ - public class NzbVortexFiles - { - public List Files { get; set; } - } -} diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexProxy.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexProxy.cs index ade58c8c7..45898c2d3 100644 --- a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexProxy.cs @@ -1,15 +1,12 @@ using System; -using System.CodeDom; using System.Collections.Generic; -using System.Security.Cryptography; -using Newtonsoft.Json.Linq; +using Newtonsoft.Json; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; using NzbDrone.Common.Serializer; -using NzbDrone.Core.Rest; using NzbDrone.Core.Download.Clients.NzbVortex.Responses; -using RestSharp; namespace NzbDrone.Core.Download.Clients.NzbVortex { @@ -20,216 +17,188 @@ public interface INzbVortexProxy NzbVortexVersionResponse GetVersion(NzbVortexSettings settings); NzbVortexApiVersionResponse GetApiVersion(NzbVortexSettings settings); List GetGroups(NzbVortexSettings settings); - NzbVortexQueue GetQueue(int doneLimit, NzbVortexSettings settings); - NzbVortexFiles GetFiles(int id, NzbVortexSettings settings); + List GetQueue(int doneLimit, NzbVortexSettings settings); + List GetFiles(int id, NzbVortexSettings settings); } public class NzbVortexProxy : INzbVortexProxy { - private readonly ICached _authCache; + private readonly IHttpClient _httpClient; private readonly Logger _logger; - public NzbVortexProxy(ICacheManager cacheManager, Logger logger) + private readonly ICached _authSessionIdCache; + + public NzbVortexProxy(IHttpClient httpClient, ICacheManager cacheManager, Logger logger) { - _authCache = cacheManager.GetCache(GetType(), "authCache"); + _httpClient = httpClient; _logger = logger; + + _authSessionIdCache = cacheManager.GetCache(GetType(), "authCache"); } public string DownloadNzb(byte[] nzbData, string filename, int priority, NzbVortexSettings settings) { - var request = BuildRequest("nzb/add", Method.POST, true, settings); - - request.AddFile("name", nzbData, filename, "application/x-nzb"); - request.AddQueryParameter("priority", priority.ToString()); - + var requestBuilder = BuildRequest(settings).Resource("nzb/add") + .Post() + .AddQueryParam("priority", priority.ToString()); + if (settings.TvCategory.IsNotNullOrWhiteSpace()) { - request.AddQueryParameter("groupname", settings.TvCategory); + requestBuilder.AddQueryParam("groupname", settings.TvCategory); } + + requestBuilder.AddFormUpload("name", filename, nzbData, "application/x-nzb"); - var response = ProcessRequest(request, settings); + var response = ProcessRequest(requestBuilder, true, settings); return response.Id; } public void Remove(int id, bool deleteData, NzbVortexSettings settings) { - var request = BuildRequest(string.Format("nzb/{0}/cancel", id), Method.GET, true, settings); + var requestBuilder = BuildRequest(settings).Resource(string.Format("nzb/{0}/{1}", id, deleteData ? "cancelDelete" : "cancel")); - if (deleteData) - { - request.Resource += "Delete"; - } - - ProcessRequest(request, settings); + ProcessRequest(requestBuilder, true, settings); } public NzbVortexVersionResponse GetVersion(NzbVortexSettings settings) { - var request = BuildRequest("app/appversion", Method.GET, false, settings); - var response = ProcessRequest(request, settings); + var requestBuilder = BuildRequest(settings).Resource("app/appversion"); + + var response = ProcessRequest(requestBuilder, false, settings); return response; } public NzbVortexApiVersionResponse GetApiVersion(NzbVortexSettings settings) { - var request = BuildRequest("app/apilevel", Method.GET, false, settings); - var response = ProcessRequest(request, settings); + var requestBuilder = BuildRequest(settings).Resource("app/apilevel"); + + var response = ProcessRequest(requestBuilder, false, settings); return response; } public List GetGroups(NzbVortexSettings settings) { - var request = BuildRequest("group", Method.GET, true, settings); - var response = ProcessRequest(request, settings); + var request = BuildRequest(settings).Resource("group"); + var response = ProcessRequest(request, true, settings); return response.Groups; } - public NzbVortexQueue GetQueue(int doneLimit, NzbVortexSettings settings) + public List GetQueue(int doneLimit, NzbVortexSettings settings) { - var request = BuildRequest("nzb", Method.GET, true, settings); + var requestBuilder = BuildRequest(settings).Resource("nzb"); + if (settings.TvCategory.IsNotNullOrWhiteSpace()) { - request.AddQueryParameter("groupName", settings.TvCategory); + requestBuilder.AddQueryParam("groupName", settings.TvCategory); } - request.AddQueryParameter("limitDone", doneLimit.ToString()); + requestBuilder.AddQueryParam("limitDone", doneLimit.ToString()); - var response = ProcessRequest(request, settings); + var response = ProcessRequest(requestBuilder, true, settings); - return response; + return response.Items; } - public NzbVortexFiles GetFiles(int id, NzbVortexSettings settings) + public List GetFiles(int id, NzbVortexSettings settings) { - var request = BuildRequest(string.Format("file/{0}", id), Method.GET, true, settings); - var response = ProcessRequest(request, settings); + var requestBuilder = BuildRequest(settings).Resource(string.Format("file/{0}", id)); - return response; + var response = ProcessRequest(requestBuilder, true, settings); + + return response.Files; + } + + private HttpRequestBuilder BuildRequest(NzbVortexSettings settings) + { + return new HttpRequestBuilder(true, settings.Host, settings.Port, "api"); } - private string GetSessionId(bool force, NzbVortexSettings settings) + private T ProcessRequest(HttpRequestBuilder requestBuilder, bool requiresAuthentication, NzbVortexSettings settings) + where T : NzbVortexResponseBase, new() { - var authCacheKey = string.Format("{0}_{1}_{2}", settings.Host, settings.Port, settings.ApiKey); - - if (force) - { - _authCache.Remove(authCacheKey); - } - - var sessionId = _authCache.Get(authCacheKey, () => Authenticate(settings)); - - return sessionId; - } - - private string Authenticate(NzbVortexSettings settings) - { - var nonce = GetNonce(settings); - var cnonce = Guid.NewGuid().ToString(); - var hashString = string.Format("{0}:{1}:{2}", nonce, cnonce, settings.ApiKey); - var sha256 = hashString.SHA256Hash(); - var base64 = Convert.ToBase64String(sha256.HexToByteArray()); - var request = BuildRequest("auth/login", Method.GET, false, settings); - - request.AddQueryParameter("nonce", nonce); - request.AddQueryParameter("cnonce", cnonce); - request.AddQueryParameter("hash", base64); - - var response = ProcessRequest(request, settings); - var result = Json.Deserialize(response); - - if (result.LoginResult == NzbVortexLoginResultType.Failed) - { - throw new NzbVortexAuthenticationException("Authentication failed, check your API Key"); - } - - return result.SessionId; - } - - private string GetNonce(NzbVortexSettings settings) - { - var request = BuildRequest("auth/nonce", Method.GET, false, settings); - - return ProcessRequest(request, settings).AuthNonce; - } - - private IRestClient BuildClient(NzbVortexSettings settings) - { - var url = string.Format(@"https://{0}:{1}/api", settings.Host, settings.Port); - - return RestClientFactory.BuildClient(url); - } - - private IRestRequest BuildRequest(string resource, Method method, bool requiresAuthentication, NzbVortexSettings settings) - { - var request = new RestRequest(resource, method); - if (requiresAuthentication) { - request.AddQueryParameter("sessionid", GetSessionId(false, settings)); + AuthenticateClient(requestBuilder, settings); } - return request; - } - - private T ProcessRequest(IRestRequest request, NzbVortexSettings settings) where T : new() - { - return Json.Deserialize(ProcessRequest(request, settings)); - } - - private string ProcessRequest(IRestRequest request, NzbVortexSettings settings) - { - var client = BuildClient(settings); - + HttpResponse response = null; try { - return ProcessRequest(client, request).Content; - } - catch (NzbVortexNotLoggedInException) - { - _logger.Warn("Not logged in response received, reauthenticating and retrying"); - request.AddQueryParameter("sessionid", GetSessionId(true, settings)); + response = _httpClient.Execute(requestBuilder.Build()); - return ProcessRequest(client, request).Content; - } - } + var result = Json.Deserialize(response.Content); - private IRestResponse ProcessRequest(IRestClient client, IRestRequest request) - { - _logger.Debug("URL: {0}/{1}", client.BaseUrl, request.Resource); - var response = client.Execute(request); - - _logger.Trace("Response: {0}", response.Content); - CheckForError(response); - - return response; - } - - private void CheckForError(IRestResponse response) - { - if (response.ResponseStatus != ResponseStatus.Completed) - { - throw new DownloadClientException("Unable to connect to NZBVortex, please check your settings", response.ErrorException); - } - - NzbVortexResponseBase result; - - if (Json.TryDeserialize(response.Content, out result)) - { if (result.Result == NzbVortexResultType.NotLoggedIn) { - throw new NzbVortexNotLoggedInException(); + _logger.Debug("Not logged in response received, reauthenticating and retrying"); + AuthenticateClient(requestBuilder, settings, true); + + response = _httpClient.Execute(requestBuilder.Build()); + + result = Json.Deserialize(response.Content); + + if (result.Result == NzbVortexResultType.NotLoggedIn) + { + throw new DownloadClientException("Unable to connect to remain authenticated to NzbVortex"); + } } + + return result; + } + catch (JsonException ex) + { + throw new DownloadClientException("NzbVortex response could not be processed {0}: {1}", ex.Message, response.Content); + } + catch (HttpException ex) + { + throw new DownloadClientException("Unable to connect to NZBVortex, please check your settings", ex); + } + } + + private void AuthenticateClient(HttpRequestBuilder requestBuilder, NzbVortexSettings settings, bool reauthenticate = false) + { + var authKey = string.Format("{0}:{1}", requestBuilder.BaseUrl, settings.ApiKey); + + var sessionId = _authSessionIdCache.Find(authKey); + + if (sessionId == null || reauthenticate) + { + _authSessionIdCache.Remove(authKey); + + var nonceRequest = BuildRequest(settings).Resource("auth/nonce").Build(); + var nonceResponse = _httpClient.Execute(nonceRequest); + + var nonce = Json.Deserialize(nonceResponse.Content).AuthNonce; + + var cnonce = Guid.NewGuid().ToString(); + + var hashString = string.Format("{0}:{1}:{2}", nonce, cnonce, settings.ApiKey); + var hash = Convert.ToBase64String(hashString.SHA256Hash().HexToByteArray()); + + var authRequest = BuildRequest(settings).Resource("auth/login") + .AddQueryParam("nonce", nonce) + .AddQueryParam("cnonce", cnonce) + .AddQueryParam("hash", hash) + .Build(); + var authResponse = _httpClient.Execute(authRequest); + var authResult = Json.Deserialize(authResponse.Content); + + if (authResult.LoginResult == NzbVortexLoginResultType.Failed) + { + throw new NzbVortexAuthenticationException("Authentication failed, check your API Key"); + } + + sessionId = authResult.SessionId; + + _authSessionIdCache.Set(authKey, sessionId); } - else - { - throw new DownloadClientException("Response could not be processed: {0}", response.Content); - } + requestBuilder.AddQueryParam("sessionid", sessionId); } } } diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexQueue.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexQueue.cs deleted file mode 100644 index 6d4e2f59c..000000000 --- a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexQueue.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace NzbDrone.Core.Download.Clients.NzbVortex -{ - public class NzbVortexQueue - { - [JsonProperty(PropertyName = "nzbs")] - public List Items { get; set; } - } -} diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexFilesResponse.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexFilesResponse.cs new file mode 100644 index 000000000..abe2f76cb --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexFilesResponse.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace NzbDrone.Core.Download.Clients.NzbVortex.Responses +{ + public class NzbVortexFilesResponse : NzbVortexResponseBase + { + public List Files { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexQueueResponse.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexQueueResponse.cs new file mode 100644 index 000000000..2f5adb87f --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexQueueResponse.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace NzbDrone.Core.Download.Clients.NzbVortex.Responses +{ + public class NzbVortexQueueResponse : NzbVortexResponseBase + { + [JsonProperty(PropertyName = "nzbs")] + public List Items { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/JsonRequest.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/JsonRequest.cs deleted file mode 100644 index a7eaf4804..000000000 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/JsonRequest.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; - -namespace NzbDrone.Core.Download.Clients.Nzbget -{ - public class JsonRequest - { - public string Method { get; set; } - public object[] Params { get; set; } - - public JsonRequest(string method) - { - Method = method; - } - - public JsonRequest(string method, object[] @params) - { - Method = method; - Params = @params; - } - } -} diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs index c933d58c2..3fa1d521b 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs @@ -2,9 +2,9 @@ using System.Collections.Generic; using System.Linq; using NLog; +using NzbDrone.Common.Http; using NzbDrone.Common.Serializer; -using NzbDrone.Core.Rest; -using RestSharp; +using System.Net; namespace NzbDrone.Core.Download.Clients.Nzbget { @@ -22,22 +22,20 @@ public interface INzbgetProxy public class NzbgetProxy : INzbgetProxy { + private readonly IHttpClient _httpClient; private readonly Logger _logger; - public NzbgetProxy(Logger logger) + public NzbgetProxy(IHttpClient httpClient, Logger logger) { + _httpClient = httpClient; _logger = logger; } public string DownloadNzb(byte[] nzbData, string title, string category, int priority, NzbgetSettings settings) { - var parameters = new object[] { title, category, priority, false, Convert.ToBase64String(nzbData) }; - var request = BuildRequest(new JsonRequest("append", parameters)); + var response = ProcessRequest(settings, "append", title, category, priority, false, nzbData); - var response = Json.Deserialize>(ProcessRequest(request, settings)); - _logger.Trace("Response: [{0}]", response.Result); - - if (!response.Result) + if (!response) { return null; } @@ -63,37 +61,27 @@ public string DownloadNzb(byte[] nzbData, string title, string category, int pri public NzbgetGlobalStatus GetGlobalStatus(NzbgetSettings settings) { - var request = BuildRequest(new JsonRequest("status")); - - return Json.Deserialize>(ProcessRequest(request, settings)).Result; + return ProcessRequest(settings, "status"); } public List GetQueue(NzbgetSettings settings) { - var request = BuildRequest(new JsonRequest("listgroups")); - - return Json.Deserialize>>(ProcessRequest(request, settings)).Result; + return ProcessRequest>(settings, "listgroups"); } public List GetHistory(NzbgetSettings settings) { - var request = BuildRequest(new JsonRequest("history")); - - return Json.Deserialize>>(ProcessRequest(request, settings)).Result; + return ProcessRequest>(settings, "history"); } public string GetVersion(NzbgetSettings settings) { - var request = BuildRequest(new JsonRequest("version")); - - return Json.Deserialize>(ProcessRequest(request, settings)).Result; + return ProcessRequest(settings, "version"); } public Dictionary GetConfig(NzbgetSettings settings) { - var request = BuildRequest(new JsonRequest("config")); - - return Json.Deserialize>>(ProcessRequest(request, settings)).Result.ToDictionary(v => v.Name, v => v.Value); + return ProcessRequest>(settings, "config").ToDictionary(v => v.Name, v => v.Value); } @@ -160,68 +148,43 @@ public void RetryDownload(string id, NzbgetSettings settings) private bool EditQueue(string command, int offset, string editText, int id, NzbgetSettings settings) { - var parameters = new object[] { command, offset, editText, id }; - var request = BuildRequest(new JsonRequest("editqueue", parameters)); - var response = Json.Deserialize>(ProcessRequest(request, settings)); - - return response.Result; + return ProcessRequest(settings, "editqueue", command, offset, editText, id); } - private string ProcessRequest(IRestRequest restRequest, NzbgetSettings settings) + private T ProcessRequest(NzbgetSettings settings, string method, params object[] parameters) { - var client = BuildClient(settings); - var response = client.Execute(restRequest); + var baseUrl = HttpRequestBuilder.BuildBaseUrl(settings.UseSsl, settings.Host, settings.Port, "jsonrpc"); + + var builder = new JsonRpcRequestBuilder(baseUrl, method, parameters); + builder.NetworkCredential = new NetworkCredential(settings.Username, settings.Password); + + var httpRequest = builder.Build(); + + HttpResponse response; + try + { + response = _httpClient.Execute(httpRequest); + } + catch (HttpException ex) + { + if (ex.Response.StatusCode == HttpStatusCode.Unauthorized) + { + throw new DownloadClientException("Authentication failed for NzbGet, please check your settings", ex); + } + + throw new DownloadClientException("Unable to connect to NzbGet. " + ex.Message, ex); + } + _logger.Trace("Response: {0}", response.Content); - CheckForError(response); - - return response.Content; - } - - private IRestClient BuildClient(NzbgetSettings settings) - { - var protocol = settings.UseSsl ? "https" : "http"; - - var url = string.Format("{0}://{1}:{2}/jsonrpc", - protocol, - settings.Host, - settings.Port); - - _logger.Debug("Url: " + url); - - var client = RestClientFactory.BuildClient(url); - client.Authenticator = new HttpBasicAuthenticator(settings.Username, settings.Password); - - return client; - } - - private IRestRequest BuildRequest(JsonRequest jsonRequest) - { - var request = new RestRequest(Method.POST); - - request.JsonSerializer = new JsonNetSerializer(); - request.RequestFormat = DataFormat.Json; - request.AddBody(jsonRequest); - - return request; - } - - private void CheckForError(IRestResponse response) - { - if (response.ErrorException != null) - { - throw new DownloadClientException("Unable to connect to NzbGet. " + response.ErrorException.Message, response.ErrorException); - } - - if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) - { - throw new DownloadClientException("Authentication failed for NzbGet, please check your settings", response.ErrorException); - } - - var result = Json.Deserialize(response.Content); + var result = Json.Deserialize>(response.Content); if (result.Error != null) + { throw new DownloadClientException("Error response received from nzbget: {0}", result.Error.ToString()); + } + + return result.Result; } } } diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs index 14e58d5d9..ec6cb123d 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs @@ -3,9 +3,8 @@ using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Common.Serializer; -using NzbDrone.Core.Rest; using NzbDrone.Core.Download.Clients.Sabnzbd.Responses; -using RestSharp; +using NzbDrone.Common.Http; namespace NzbDrone.Core.Download.Clients.Sabnzbd { @@ -13,7 +12,6 @@ public interface ISabnzbdProxy { SabnzbdAddResponse DownloadNzb(byte[] nzbData, string filename, string category, int priority, SabnzbdSettings settings); void RemoveFrom(string source, string id,bool deleteData, SabnzbdSettings settings); - string ProcessRequest(IRestRequest restRequest, string action, SabnzbdSettings settings); string GetVersion(SabnzbdSettings settings); SabnzbdConfig GetConfig(SabnzbdSettings settings); SabnzbdQueue GetQueue(int start, int limit, SabnzbdSettings settings); @@ -23,23 +21,27 @@ public interface ISabnzbdProxy public class SabnzbdProxy : ISabnzbdProxy { + private readonly IHttpClient _httpClient; private readonly Logger _logger; - public SabnzbdProxy(Logger logger) + public SabnzbdProxy(IHttpClient httpClient, Logger logger) { + _httpClient = httpClient; _logger = logger; } public SabnzbdAddResponse DownloadNzb(byte[] nzbData, string filename, string category, int priority, SabnzbdSettings settings) { - var request = new RestRequest(Method.POST); - var action = string.Format("mode=addfile&cat={0}&priority={1}", Uri.EscapeDataString(category), priority); + var request = BuildRequest("addfile", settings).Post(); - request.AddFile("name", nzbData, filename, "application/x-nzb"); + request.AddQueryParam("cat", category); + request.AddQueryParam("priority", priority); + + request.AddFormUpload("name", filename, nzbData, "application/x-nzb"); SabnzbdAddResponse response; - if (!Json.TryDeserialize(ProcessRequest(request, action, settings), out response)) + if (!Json.TryDeserialize(ProcessRequest(request, settings), out response)) { response = new SabnzbdAddResponse(); response.Status = true; @@ -50,32 +52,21 @@ public SabnzbdAddResponse DownloadNzb(byte[] nzbData, string filename, string ca public void RemoveFrom(string source, string id, bool deleteData, SabnzbdSettings settings) { - var request = new RestRequest(); + var request = BuildRequest(source, settings); + request.AddQueryParam("name", "delete"); + request.AddQueryParam("del_files", deleteData ? 1 : 0); + request.AddQueryParam("value", id); - var action = string.Format("mode={0}&name=delete&del_files={1}&value={2}", source, deleteData ? 1 : 0, id); - - ProcessRequest(request, action, settings); - } - - public string ProcessRequest(IRestRequest restRequest, string action, SabnzbdSettings settings) - { - var client = BuildClient(action, settings); - var response = client.Execute(restRequest); - _logger.Trace("Response: {0}", response.Content); - - CheckForError(response); - - return response.Content; + ProcessRequest(request, settings); } public string GetVersion(SabnzbdSettings settings) { - var request = new RestRequest(); - var action = "mode=version"; + var request = BuildRequest("version", settings); SabnzbdVersionResponse response; - if (!Json.TryDeserialize(ProcessRequest(request, action, settings), out response)) + if (!Json.TryDeserialize(ProcessRequest(request, settings), out response)) { response = new SabnzbdVersionResponse(); } @@ -85,45 +76,48 @@ public string GetVersion(SabnzbdSettings settings) public SabnzbdConfig GetConfig(SabnzbdSettings settings) { - var request = new RestRequest(); - var action = "mode=get_config"; + var request = BuildRequest("get_config", settings); - var response = Json.Deserialize(ProcessRequest(request, action, settings)); + var response = Json.Deserialize(ProcessRequest(request, settings)); return response.Config; } public SabnzbdQueue GetQueue(int start, int limit, SabnzbdSettings settings) { - var request = new RestRequest(); - var action = string.Format("mode=queue&start={0}&limit={1}", start, limit); + var request = BuildRequest("queue", settings); + request.AddQueryParam("start", start); + request.AddQueryParam("limit", limit); + + var response = ProcessRequest(request, settings); - var response = ProcessRequest(request, action, settings); return Json.Deserialize(JObject.Parse(response).SelectToken("queue").ToString()); } public SabnzbdHistory GetHistory(int start, int limit, string category, SabnzbdSettings settings) { - var request = new RestRequest(); - var action = string.Format("mode=history&start={0}&limit={1}", start, limit); + var request = BuildRequest("history", settings); + request.AddQueryParam("start", start); + request.AddQueryParam("limit", limit); if (category.IsNotNullOrWhiteSpace()) { - action += "&category=" + category; + request.AddQueryParam("category", category); } - var response = ProcessRequest(request, action, settings); + var response = ProcessRequest(request, settings); + return Json.Deserialize(JObject.Parse(response).SelectToken("history").ToString()); } public string RetryDownload(string id, SabnzbdSettings settings) { - var request = new RestRequest(); - var action = string.Format("mode=retry&value={0}", id); + var request = BuildRequest("retry", settings); + request.AddQueryParam("value", id); SabnzbdRetryResponse response; - if (!Json.TryDeserialize(ProcessRequest(request, action, settings), out response)) + if (!Json.TryDeserialize(ProcessRequest(request, settings), out response)) { response = new SabnzbdRetryResponse(); response.Status = true; @@ -132,33 +126,57 @@ public string RetryDownload(string id, SabnzbdSettings settings) return response.Id; } - private IRestClient BuildClient(string action, SabnzbdSettings settings) + private HttpRequestBuilder BuildRequest(string mode, SabnzbdSettings settings) { - var protocol = settings.UseSsl ? "https" : "http"; + var baseUrl = string.Format(@"{0}://{1}:{2}/api", + settings.UseSsl ? "https" : "http", + settings.Host, + settings.Port); - var authentication = settings.ApiKey.IsNullOrWhiteSpace() ? - string.Format("ma_username={0}&ma_password={1}", settings.Username, Uri.EscapeDataString(settings.Password)) : - string.Format("apikey={0}", settings.ApiKey); + var requestBuilder = new HttpRequestBuilder(baseUrl) + .Accept(HttpAccept.Json) + .AddQueryParam("mode", mode); - var url = string.Format(@"{0}://{1}:{2}/api?{3}&{4}&output=json", - protocol, - settings.Host, - settings.Port, - action, - authentication); + if (settings.ApiKey.IsNotNullOrWhiteSpace()) + { + requestBuilder.AddSuffixQueryParam("apikey", settings.ApiKey); + } + else + { + requestBuilder.AddSuffixQueryParam("ma_username", settings.Username); + requestBuilder.AddSuffixQueryParam("ma_password", settings.Password); + } + requestBuilder.AddSuffixQueryParam("output", "json"); - _logger.Debug("Url: " + url); - - return RestClientFactory.BuildClient(url); + return requestBuilder; } - private void CheckForError(IRestResponse response) + private string ProcessRequest(HttpRequestBuilder requestBuilder, SabnzbdSettings settings) { - if (response.ResponseStatus != ResponseStatus.Completed) + var httpRequest = requestBuilder.Build(); + + HttpResponse response; + + _logger.Debug("Url: {0}", httpRequest.Url); + + try { - throw new DownloadClientException("Unable to connect to SABnzbd, please check your settings", response.ErrorException); + response = _httpClient.Execute(httpRequest); + } + catch (HttpException ex) + { + throw new DownloadClientException("Unable to connect to SABnzbd, please check your settings", ex); } + _logger.Trace("Response: {0}", response.Content); + + CheckForError(response); + + return response.Content; + } + + private void CheckForError(HttpResponse response) + { SabnzbdJsonError result; if (!Json.TryDeserialize(response.Content, out result)) @@ -181,7 +199,9 @@ private void CheckForError(IRestResponse response) } if (result.Failed) + { throw new DownloadClientException("Error response received from SABnzbd: {0}", result.Error); + } } } } diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs index 0bc00da5a..dc7a66f40 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs @@ -5,8 +5,10 @@ using NzbDrone.Common.Extensions; using NzbDrone.Core.Rest; using NLog; -using RestSharp; using Newtonsoft.Json.Linq; +using NzbDrone.Common.Http; +using NzbDrone.Common.Cache; +using NzbDrone.Common.Serializer; namespace NzbDrone.Core.Download.Clients.Transmission { @@ -23,13 +25,18 @@ public interface ITransmissionProxy } public class TransmissionProxy: ITransmissionProxy - { + { + private readonly IHttpClient _httpClient; private readonly Logger _logger; - private string _sessionId; - public TransmissionProxy(Logger logger) + private ICached _authSessionIDCache; + + public TransmissionProxy(ICacheManager cacheManager, IHttpClient httpClient, Logger logger) { + _httpClient = httpClient; _logger = logger; + + _authSessionIDCache = cacheManager.GetCache(GetType(), "authSessionID"); } public List GetTorrents(TransmissionSettings settings) @@ -167,56 +174,69 @@ private TransmissionResponse GetTorrentStatus(IEnumerable hashStrings, T return result; } - protected string GetSessionId(IRestClient client, TransmissionSettings settings) + private HttpRequestBuilder BuildRequest(TransmissionSettings settings) { - var request = new RestRequest(); - request.RequestFormat = DataFormat.Json; + var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase) + .Resource("rpc") + .Accept(HttpAccept.Json); - _logger.Debug("Url: {0} GetSessionId", client.BuildUri(request)); - var restResponse = client.Execute(request); + requestBuilder.NetworkCredential = new NetworkCredential(settings.Username, settings.Password); + requestBuilder.AllowAutoRedirect = false; - if (restResponse.StatusCode == HttpStatusCode.MovedPermanently) + return requestBuilder; + } + + private void AuthenticateClient(HttpRequestBuilder requestBuilder, TransmissionSettings settings, bool reauthenticate = false) + { + var authKey = string.Format("{0}:{1}", requestBuilder.BaseUrl, settings.Password); + + var sessionId = _authSessionIDCache.Find(authKey); + + if (sessionId == null || reauthenticate) { - var uri = new Uri(restResponse.ResponseUri, (string)restResponse.GetHeaderValue("Location")); + _authSessionIDCache.Remove(authKey); - throw new DownloadClientException("Remote site redirected to " + uri); - } + var authLoginRequest = BuildRequest(settings).Build(); + authLoginRequest.SuppressHttpError = true; - // We expect the StatusCode = Conflict, coz that will provide us with a new session id. - switch (restResponse.StatusCode) - { - case HttpStatusCode.Conflict: + var response = _httpClient.Execute(authLoginRequest); + if (response.StatusCode == HttpStatusCode.MovedPermanently) { - var sessionId = restResponse.Headers.SingleOrDefault(o => o.Name == "X-Transmission-Session-Id"); + var url = response.Headers.GetSingleValue("Location"); + + throw new DownloadClientException("Remote site redirected to " + url); + } + else if (response.StatusCode == HttpStatusCode.Conflict) + { + sessionId = response.Headers.GetSingleValue("X-Transmission-Session-Id"); if (sessionId == null) { throw new DownloadClientException("Remote host did not return a Session Id."); } - - return (string)sessionId.Value; } - case HttpStatusCode.Unauthorized: - throw new DownloadClientAuthenticationException("User authentication failed."); + else + { + throw new DownloadClientAuthenticationException("Failed to authenticate with Transmission."); + } + + _logger.Debug("Transmission authentication succeeded."); + + _authSessionIDCache.Set(authKey, sessionId); } - restResponse.ValidateResponse(client); - - throw new DownloadClientException("Remote host did not return a Session Id."); + requestBuilder.SetHeader("X-Transmission-Session-Id", sessionId); } public TransmissionResponse ProcessRequest(string action, object arguments, TransmissionSettings settings) { - var client = BuildClient(settings); + var requestBuilder = BuildRequest(settings); + requestBuilder.Headers.ContentType = "application/json"; + requestBuilder.SuppressHttpError = true; - if (string.IsNullOrWhiteSpace(_sessionId)) - { - _sessionId = GetSessionId(client, settings); - } + AuthenticateClient(requestBuilder, settings); - var request = new RestRequest(Method.POST); - request.RequestFormat = DataFormat.Json; - request.AddHeader("X-Transmission-Session-Id", _sessionId); + var request = requestBuilder.Post().Build(); var data = new Dictionary(); data.Add("method", action); @@ -226,23 +246,27 @@ public TransmissionResponse ProcessRequest(string action, object arguments, Tran data.Add("arguments", arguments); } - request.AddBody(data); + request.SetContent(data.ToJson()); + request.ContentSummary = string.Format("{0}(...)", action); - _logger.Debug("Url: {0} Action: {1}", client.BuildUri(request), action); - var restResponse = client.Execute(request); - - if (restResponse.StatusCode == HttpStatusCode.Conflict) + var response = _httpClient.Execute(request); + if (response.StatusCode == HttpStatusCode.Conflict) { - _sessionId = GetSessionId(client, settings); - request.Parameters.First(o => o.Name == "X-Transmission-Session-Id").Value = _sessionId; - restResponse = client.Execute(request); + AuthenticateClient(requestBuilder, settings, true); + + request = requestBuilder.Post().Build(); + + request.SetContent(data.ToJson()); + request.ContentSummary = string.Format("{0}(...)", action); + + response = _httpClient.Execute(request); } - else if (restResponse.StatusCode == HttpStatusCode.Unauthorized) + else if (response.StatusCode == HttpStatusCode.Unauthorized) { throw new DownloadClientAuthenticationException("User authentication failed."); } - var transmissionResponse = restResponse.Read(client); + var transmissionResponse = Json.Deserialize(response.Content); if (transmissionResponse == null) { @@ -255,22 +279,5 @@ public TransmissionResponse ProcessRequest(string action, object arguments, Tran return transmissionResponse; } - - private IRestClient BuildClient(TransmissionSettings settings) - { - var protocol = settings.UseSsl ? "https" : "http"; - - var url = string.Format(@"{0}://{1}:{2}/{3}/rpc", protocol, settings.Host, settings.Port, settings.UrlBase.Trim('/')); - - var restClient = RestClientFactory.BuildClient(url); - restClient.FollowRedirects = false; - - if (!settings.Username.IsNullOrWhiteSpace()) - { - restClient.Authenticator = new HttpBasicAuthenticator(settings.Username, settings.Password); - } - - return restClient; - } } } diff --git a/src/NzbDrone.Core/Download/Clients/qBittorrent/DigestAuthenticator.cs b/src/NzbDrone.Core/Download/Clients/qBittorrent/DigestAuthenticator.cs deleted file mode 100644 index 6668d2661..000000000 --- a/src/NzbDrone.Core/Download/Clients/qBittorrent/DigestAuthenticator.cs +++ /dev/null @@ -1,22 +0,0 @@ -using RestSharp; -using System.Net; - -namespace NzbDrone.Core.Download.Clients.QBittorrent -{ - public class DigestAuthenticator : IAuthenticator - { - private readonly string _user; - private readonly string _pass; - - public DigestAuthenticator(string user, string pass) - { - _user = user; - _pass = pass; - } - - public void Authenticate(IRestClient client, IRestRequest request) - { - request.Credentials = new NetworkCredential(_user, _pass); - } - } -} diff --git a/src/NzbDrone.Core/Download/Clients/qBittorrent/QBittorrentProxy.cs b/src/NzbDrone.Core/Download/Clients/qBittorrent/QBittorrentProxy.cs index 510054689..44436688f 100644 --- a/src/NzbDrone.Core/Download/Clients/qBittorrent/QBittorrentProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/qBittorrent/QBittorrentProxy.cs @@ -1,12 +1,10 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Net; using NLog; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Rest; -using RestSharp; using NzbDrone.Common.Cache; +using NzbDrone.Common.Http; +using NzbDrone.Common.Serializer; namespace NzbDrone.Core.Download.Clients.QBittorrent { @@ -28,165 +26,192 @@ public interface IQBittorrentProxy public class QBittorrentProxy : IQBittorrentProxy { + private readonly IHttpClient _httpClient; private readonly Logger _logger; - private readonly CookieContainer _cookieContainer; - private readonly ICached _logins; - private readonly TimeSpan _loginTimeout = TimeSpan.FromSeconds(10); + private readonly ICached> _authCookieCache; - public QBittorrentProxy(ICacheManager cacheManager, Logger logger) + public QBittorrentProxy(IHttpClient httpClient, ICacheManager cacheManager, Logger logger) { + _httpClient = httpClient; _logger = logger; - _cookieContainer = new CookieContainer(); - _logins = cacheManager.GetCache(GetType(), "logins"); + + _authCookieCache = cacheManager.GetCache>(GetType(), "authCookies"); } public int GetVersion(QBittorrentSettings settings) { - var request = new RestRequest("/version/api", Method.GET); - - var client = BuildClient(settings); - var response = ProcessRequest(client, request, settings); - response.ValidateResponse(client); - return Convert.ToInt32(response.Content); - } - - public Dictionary GetConfig(QBittorrentSettings settings) - { - var request = new RestRequest("/query/preferences", Method.GET); - request.RequestFormat = DataFormat.Json; - - var client = BuildClient(settings); - var response = ProcessRequest(client, request, settings); - response.ValidateResponse(client); - return response.Read>(client); - } - - public List GetTorrents(QBittorrentSettings settings) - { - var request = new RestRequest("/query/torrents", Method.GET); - request.RequestFormat = DataFormat.Json; - request.AddParameter("label", settings.TvCategory); - - var client = BuildClient(settings); - var response = ProcessRequest(client, request, settings); - response.ValidateResponse(client); - return response.Read>(client); - } - - public void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings) - { - var request = new RestRequest("/command/download", Method.POST); - request.AddParameter("urls", torrentUrl); - - var client = BuildClient(settings); - var response = ProcessRequest(client, request, settings); - response.ValidateResponse(client); - } - - public void AddTorrentFromFile(string fileName, Byte[] fileContent, QBittorrentSettings settings) - { - var request = new RestRequest("/command/upload", Method.POST); - request.AddFile("torrents", fileContent, fileName); - - var client = BuildClient(settings); - var response = ProcessRequest(client, request, settings); - response.ValidateResponse(client); - } - - public void RemoveTorrent(string hash, Boolean removeData, QBittorrentSettings settings) - { - var cmd = removeData ? "/command/deletePerm" : "/command/delete"; - var request = new RestRequest(cmd, Method.POST); - request.AddParameter("hashes", hash); - - var client = BuildClient(settings); - var response = ProcessRequest(client, request, settings); - response.ValidateResponse(client); - } - - public void SetTorrentLabel(string hash, string label, QBittorrentSettings settings) - { - var request = new RestRequest("/command/setLabel", Method.POST); - request.AddParameter("hashes", hash); - request.AddParameter("label", label); - - var client = BuildClient(settings); - var response = ProcessRequest(client, request, settings); - response.ValidateResponse(client); - } - - public void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings) - { - var request = new RestRequest("/command/topPrio", Method.POST); - request.AddParameter("hashes", hash); - - var client = BuildClient(settings); - var response = ProcessRequest(client, request, settings); - - // qBittorrent rejects all Prio commands with 403: Forbidden if Options -> BitTorrent -> Torrent Queueing is not enabled - if (response.StatusCode == HttpStatusCode.Forbidden) - { - return; - } - - response.ValidateResponse(client); - } - - private IRestResponse ProcessRequest(IRestClient client, IRestRequest request, QBittorrentSettings settings) - { - var response = client.Execute(request); - - if (response.StatusCode == HttpStatusCode.Forbidden) - { - _logger.Info("Authentication required, logging in."); - - var loggedIn = _logins.Get(settings.Username + settings.Password, () => Login(client, settings), _loginTimeout); - - if (!loggedIn) - { - throw new DownloadClientAuthenticationException("Failed to authenticate"); - } - - // success! retry the original request - response = client.Execute(request); - } + var request = BuildRequest(settings).Resource("/version/api"); + var response = ProcessRequest(request, settings); return response; } - private bool Login(IRestClient client, QBittorrentSettings settings) + public Dictionary GetConfig(QBittorrentSettings settings) { - var request = new RestRequest("/login", Method.POST); - request.AddParameter("username", settings.Username); - request.AddParameter("password", settings.Password); + var request = BuildRequest(settings).Resource("/query/preferences"); + var response = ProcessRequest>(request, settings); - var response = client.Execute(request); - - if (response.StatusCode != HttpStatusCode.OK) - { - _logger.Warn("Login failed with {0}.", response.StatusCode); - return false; - } - - if (response.Content != "Ok.") // returns "Fails." on bad login - { - _logger.Warn("Login failed, incorrect username or password."); - return false; - } - - response.ValidateResponse(client); - return true; + return response; } - private IRestClient BuildClient(QBittorrentSettings settings) + public List GetTorrents(QBittorrentSettings settings) { - var protocol = settings.UseSsl ? "https" : "http"; - var url = String.Format(@"{0}://{1}:{2}", protocol, settings.Host, settings.Port); - var client = RestClientFactory.BuildClient(url); + var request = BuildRequest(settings).Resource("/query/torrents") + .AddQueryParam("label", settings.TvCategory); - client.Authenticator = new DigestAuthenticator(settings.Username, settings.Password); - client.CookieContainer = _cookieContainer; - return client; + var response = ProcessRequest>(request, settings); + + return response; + } + + public void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings) + { + var request = BuildRequest(settings).Resource("/command/download") + .Post() + .AddQueryParam("urls", torrentUrl); + + ProcessRequest(request, settings); + } + + public void AddTorrentFromFile(string fileName, Byte[] fileContent, QBittorrentSettings settings) + { + var request = BuildRequest(settings).Resource("/command/upload") + .Post() + .AddFormUpload("torrents", fileName, fileContent); + + ProcessRequest(request, settings); + } + + public void RemoveTorrent(string hash, Boolean removeData, QBittorrentSettings settings) + { + var request = BuildRequest(settings).Resource(removeData ? "/command/deletePerm" : "/command/delete") + .Post() + .AddFormParameter("hashes", hash); + + ProcessRequest(request, settings); + } + + public void SetTorrentLabel(string hash, string label, QBittorrentSettings settings) + { + var request = BuildRequest(settings).Resource("/command/setLabel") + .Post() + .AddFormParameter("hashes", hash) + .AddFormParameter("label", label); + + ProcessRequest(request, settings); + } + + public void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings) + { + var request = BuildRequest(settings).Resource("/command/topPrio") + .Post() + .AddFormParameter("hashes", hash); + + try + { + var response = ProcessRequest(request, settings); + } + catch (DownloadClientException ex) + { + // qBittorrent rejects all Prio commands with 403: Forbidden if Options -> BitTorrent -> Torrent Queueing is not enabled +#warning FIXME: so wouldn't the reauthenticate logic trigger on Forbidden? + if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.Forbidden) + { + return; + } + + throw; + } + + } + + private HttpRequestBuilder BuildRequest(QBittorrentSettings settings) + { + var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port); + requestBuilder.NetworkCredential = new NetworkCredential(settings.Username, settings.Password); + + return requestBuilder; + } + + private TResult ProcessRequest(HttpRequestBuilder requestBuilder, QBittorrentSettings settings) + where TResult : new() + { + AuthenticateClient(requestBuilder, settings); + + var request = requestBuilder.Build(); + + HttpResponse response; + try + { + response = _httpClient.Execute(request); + } + catch (HttpException ex) + { + if (ex.Response.StatusCode == HttpStatusCode.Forbidden) + { + _logger.Debug("Authentication required, logging in."); + + AuthenticateClient(requestBuilder, settings, true); + + request = requestBuilder.Build(); + + response = _httpClient.Execute(request); + } + else + { + throw new DownloadClientException("Failed to connect to download client", ex); + } + } + + return Json.Deserialize(response.Content); + } + + private void AuthenticateClient(HttpRequestBuilder requestBuilder, QBittorrentSettings settings, bool reauthenticate = false) + { + var authKey = string.Format("{0}:{1}", requestBuilder.BaseUrl, settings.Password); + + var cookies = _authCookieCache.Find(authKey); + + if (cookies == null || reauthenticate) + { + _authCookieCache.Remove(authKey); + + var authLoginRequest = BuildRequest(settings).Resource("/login") + .Post() + .AddFormParameter("username", settings.Username) + .AddFormParameter("password", settings.Password) + .Build(); + + HttpResponse response; + try + { + response = _httpClient.Execute(authLoginRequest); + } + catch (HttpException ex) + { + _logger.Debug("qbitTorrent authentication failed."); + if (ex.Response.StatusCode == HttpStatusCode.Forbidden) + { + throw new DownloadClientAuthenticationException("Failed to authenticate with qbitTorrent.", ex); + } + + throw; + } + + if (response.Content != "Ok.") // returns "Fails." on bad login + { + _logger.Debug("qbitTorrent authentication failed."); + throw new DownloadClientAuthenticationException("Failed to authenticate with qbitTorrent."); + } + + _logger.Debug("qbitTorrent authentication succeeded."); + + cookies = response.GetCookies(); + + _authCookieCache.Set(authKey, cookies); + } + + requestBuilder.SetCookies(cookies); } } } diff --git a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentProxy.cs b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentProxy.cs index 6c89fcb23..ed759e8cf 100644 --- a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentProxy.cs @@ -3,9 +3,11 @@ using System.Linq; using System.Net; using NLog; +using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Common.Serializer; using NzbDrone.Core.Rest; -using RestSharp; namespace NzbDrone.Core.Download.Clients.UTorrent { @@ -26,32 +28,37 @@ public interface IUTorrentProxy public class UTorrentProxy : IUTorrentProxy { + private readonly IHttpClient _httpClient; private readonly Logger _logger; - private readonly CookieContainer _cookieContainer; - private string _authToken; - public UTorrentProxy(Logger logger) + private readonly ICached> _authCookieCache; + private readonly ICached _authTokenCache; + + public UTorrentProxy(ICacheManager cacheManager, IHttpClient httpClient, Logger logger) { + _httpClient = httpClient; _logger = logger; - _cookieContainer = new CookieContainer(); + + _authCookieCache = cacheManager.GetCache>(GetType(), "authCookies"); + _authTokenCache = cacheManager.GetCache(GetType(), "authTokens"); } public int GetVersion(UTorrentSettings settings) { - var arguments = new Dictionary(); - arguments.Add("action", "getsettings"); + var requestBuilder = BuildRequest(settings) + .AddQueryParam("action", "getsettings"); - var result = ProcessRequest(arguments, settings); + var result = ProcessRequest(requestBuilder, settings); return result.Build; } public Dictionary GetConfig(UTorrentSettings settings) { - var arguments = new Dictionary(); - arguments.Add("action", "getsettings"); + var requestBuilder = BuildRequest(settings) + .AddQueryParam("action", "getsettings"); - var result = ProcessRequest(arguments, settings); + var result = ProcessRequest(requestBuilder, settings); var configuration = new Dictionary(); @@ -65,196 +72,175 @@ public Dictionary GetConfig(UTorrentSettings settings) public List GetTorrents(UTorrentSettings settings) { - var arguments = new Dictionary(); - arguments.Add("list", 1); + var requestBuilder = BuildRequest(settings) + .AddQueryParam("list", 1); - var result = ProcessRequest(arguments, settings); + var result = ProcessRequest(requestBuilder, settings); return result.Torrents; } public void AddTorrentFromUrl(string torrentUrl, UTorrentSettings settings) { - var arguments = new Dictionary(); - arguments.Add("action", "add-url"); - arguments.Add("s", torrentUrl); + var requestBuilder = BuildRequest(settings) + .AddQueryParam("action", "add-url") + .AddQueryParam("s", torrentUrl); - ProcessRequest(arguments, settings); + ProcessRequest(requestBuilder, settings); } public void AddTorrentFromFile(string fileName, byte[] fileContent, UTorrentSettings settings) { - var arguments = new Dictionary(); - arguments.Add("action", "add-file"); - arguments.Add("path", string.Empty); + var requestBuilder = BuildRequest(settings) + .Post() + .AddQueryParam("action", "add-file") + .AddQueryParam("path", string.Empty) + .AddFormUpload("torrent_file", fileName, fileContent, @"application/octet-stream"); - var client = BuildClient(settings); - - // add-file should use POST unlike all other methods which are GET - var request = new RestRequest(Method.POST); - request.RequestFormat = DataFormat.Json; - request.Resource = "/gui/"; - request.AddParameter("token", _authToken, ParameterType.QueryString); - - foreach (var argument in arguments) - { - request.AddParameter(argument.Key, argument.Value, ParameterType.QueryString); - } - - request.AddFile("torrent_file", fileContent, fileName, @"application/octet-stream"); - - ProcessRequest(request, client); + ProcessRequest(requestBuilder, settings); } public void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, UTorrentSettings settings) { - var arguments = new List>(); - arguments.Add("action", "setprops"); - arguments.Add("hash", hash); - - arguments.Add("s", "seed_override"); - arguments.Add("v", 1); + var requestBuilder = BuildRequest(settings) + .AddQueryParam("action", "setprops") + .AddQueryParam("hash", hash); + + requestBuilder.AddQueryParam("s", "seed_override") + .AddQueryParam("v", 1); if (seedConfiguration.Ratio != null) { - arguments.Add("s","seed_ratio"); - arguments.Add("v", Convert.ToInt32(seedConfiguration.Ratio.Value * 1000)); + requestBuilder.AddQueryParam("s", "seed_ratio") + .AddQueryParam("v", Convert.ToInt32(seedConfiguration.Ratio.Value * 1000)); } if (seedConfiguration.SeedTime != null) { - arguments.Add("s", "seed_time"); - arguments.Add("v", Convert.ToInt32(seedConfiguration.SeedTime.Value.TotalSeconds)); + requestBuilder.AddQueryParam("s", "seed_time") + .AddQueryParam("v", Convert.ToInt32(seedConfiguration.SeedTime.Value.TotalSeconds)); } - ProcessRequest(arguments, settings); + ProcessRequest(requestBuilder, settings); } public void RemoveTorrent(string hash, bool removeData, UTorrentSettings settings) { - var arguments = new Dictionary(); + var requestBuilder = BuildRequest(settings) + .AddQueryParam("action", removeData ? "removedata" : "remove") + .AddQueryParam("hash", hash); - if (removeData) - { - arguments.Add("action", "removedata"); - } - else - { - arguments.Add("action", "remove"); - } - - arguments.Add("hash", hash); - - ProcessRequest(arguments, settings); + ProcessRequest(requestBuilder, settings); } public void SetTorrentLabel(string hash, string label, UTorrentSettings settings) { - var arguments = new Dictionary(); - arguments.Add("action", "setprops"); - arguments.Add("hash", hash); + var requestBuilder = BuildRequest(settings) + .AddQueryParam("action", "setprops") + .AddQueryParam("hash", hash); - arguments.Add("s", "label"); - arguments.Add("v", label); + requestBuilder.AddQueryParam("s", "label") + .AddQueryParam("v", label); - ProcessRequest(arguments, settings); + ProcessRequest(requestBuilder, settings); } public void MoveTorrentToTopInQueue(string hash, UTorrentSettings settings) { - var arguments = new Dictionary(); - arguments.Add("action", "queuetop"); - arguments.Add("hash", hash); + var requestBuilder = BuildRequest(settings) + .AddQueryParam("action", "queuetop") + .AddQueryParam("hash", hash); - ProcessRequest(arguments, settings); + ProcessRequest(requestBuilder, settings); } - public UTorrentResponse ProcessRequest(IEnumerable> arguments, UTorrentSettings settings) + private HttpRequestBuilder BuildRequest(UTorrentSettings settings) { - var client = BuildClient(settings); + var requestBuilder = new HttpRequestBuilder(false, settings.Host, settings.Port) + .Resource("/gui/") + .Accept(HttpAccept.Json); - var request = new RestRequest(Method.GET); - request.RequestFormat = DataFormat.Json; - request.Resource = "/gui/"; - request.AddParameter("token", _authToken, ParameterType.QueryString); + requestBuilder.NetworkCredential = new NetworkCredential(settings.Username, settings.Password); - foreach (var argument in arguments) - { - request.AddParameter(argument.Key, argument.Value, ParameterType.QueryString); - } - - return ProcessRequest(request, client); + return requestBuilder; } - private UTorrentResponse ProcessRequest(IRestRequest request, IRestClient client) + public UTorrentResponse ProcessRequest(HttpRequestBuilder requestBuilder, UTorrentSettings settings) { - _logger.Debug("Url: {0}", client.BuildUri(request)); - var clientResponse = client.Execute(request); + AuthenticateClient(requestBuilder, settings); - if (clientResponse.StatusCode == HttpStatusCode.BadRequest) + var request = requestBuilder.Build(); + + HttpResponse response; + try { - // Token has expired. If the settings were incorrect or the API is disabled we'd have gotten an error 400 during GetAuthToken - _logger.Debug("uTorrent authentication token error."); - - _authToken = GetAuthToken(client); - - request.Parameters.First(v => v.Name == "token").Value = _authToken; - clientResponse = client.Execute(request); + response = _httpClient.Execute(request); } - else if (clientResponse.StatusCode == HttpStatusCode.Unauthorized) + catch (HttpException ex) { - throw new DownloadClientAuthenticationException("Failed to authenticate"); + if (ex.Response.StatusCode == HttpStatusCode.BadRequest || ex.Response.StatusCode == HttpStatusCode.Unauthorized) + { + _logger.Debug("Authentication required, logging in."); + + AuthenticateClient(requestBuilder, settings, true); + + request = requestBuilder.Build(); + + response = _httpClient.Execute(request); + } + else + { + throw new DownloadClientException("Unable to connect to Deluge, please check your settings", ex); + } } - var uTorrentResult = clientResponse.Read(client); - - return uTorrentResult; + return Json.Deserialize(response.Content); } - private string GetAuthToken(IRestClient client) + private void AuthenticateClient(HttpRequestBuilder requestBuilder, UTorrentSettings settings, bool reauthenticate = false) { - var request = new RestRequest(); - request.RequestFormat = DataFormat.Json; - request.Resource = "/gui/token.html"; + var authKey = string.Format("{0}:{1}", requestBuilder.BaseUrl, settings.Password); - _logger.Debug("Url: {0}", client.BuildUri(request)); - var response = client.Execute(request); + var cookies = _authCookieCache.Find(authKey); + var authToken = _authTokenCache.Find(authKey); - if (response.StatusCode == HttpStatusCode.Unauthorized) + if (cookies == null || authToken == null || reauthenticate) { - throw new DownloadClientAuthenticationException("Failed to authenticate"); + _authCookieCache.Remove(authKey); + _authTokenCache.Remove(authKey); + + var authLoginRequest = BuildRequest(settings).Resource("/gui/token.html").Build(); + + HttpResponse response; + try + { + response = _httpClient.Execute(authLoginRequest); + _logger.Debug("uTorrent authentication succeeded."); + + var xmlDoc = new System.Xml.XmlDocument(); + xmlDoc.LoadXml(response.Content); + + authToken = xmlDoc.FirstChild.FirstChild.InnerText; + } + catch (HttpException ex) + { + if (ex.Response.StatusCode == HttpStatusCode.Unauthorized) + { + _logger.Debug("uTorrent authentication failed."); + throw new DownloadClientAuthenticationException("Failed to authenticate with uTorrent."); + } + + throw; + } + + cookies = response.GetCookies(); + + _authCookieCache.Set(authKey, cookies); + _authTokenCache.Set(authKey, authToken); } - response.ValidateResponse(client); - - var xmlDoc = new System.Xml.XmlDocument(); - xmlDoc.LoadXml(response.Content); - - var authToken = xmlDoc.FirstChild.FirstChild.InnerText; - - _logger.Debug("uTorrent AuthToken={0}", authToken); - - return authToken; - } - - private IRestClient BuildClient(UTorrentSettings settings) - { - var url = string.Format(@"http://{0}:{1}", - settings.Host, - settings.Port); - - var restClient = RestClientFactory.BuildClient(url); - - restClient.Authenticator = new HttpBasicAuthenticator(settings.Username, settings.Password); - restClient.CookieContainer = _cookieContainer; - - if (_authToken.IsNullOrWhiteSpace()) - { - // µTorrent requires a token and cookie for authentication. The cookie is set automatically when getting the token. - _authToken = GetAuthToken(restClient); - } - - return restClient; + requestBuilder.SetCookies(cookies); + requestBuilder.AddQueryParam("token", authToken, true); } } } diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 2a6372013..5e1539363 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -336,7 +336,6 @@ - @@ -346,7 +345,6 @@ - @@ -370,8 +368,6 @@ - - @@ -381,20 +377,21 @@ + + - - - - - - + + + + +