Fixed: Use our own HttpClient for rTorrent RPC requests

pull/1461/head
ta264 3 years ago
parent 43f1d77b9f
commit 258847a83b

@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Xml.Linq;
using NLog;
using NzbDrone.Common.Instrumentation;
namespace NzbDrone.Common.Http
{
public class XmlRpcRequestBuilder : HttpRequestBuilder
{
public static string XmlRpcContentType = "text/xml";
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(XmlRpcRequestBuilder));
public string XmlMethod { get; private set; }
public List<object> XmlParameters { get; private set; }
public XmlRpcRequestBuilder(string baseUrl)
: base(baseUrl)
{
Method = HttpMethod.Post;
XmlParameters = new List<object>();
}
public XmlRpcRequestBuilder(bool useHttps, string host, int port, string urlBase = null)
: this(BuildBaseUrl(useHttps, host, port, urlBase))
{
}
public override HttpRequestBuilder Clone()
{
var clone = base.Clone() as XmlRpcRequestBuilder;
clone.XmlParameters = new List<object>(XmlParameters);
return clone;
}
public XmlRpcRequestBuilder Call(string method, params object[] parameters)
{
var clone = Clone() as XmlRpcRequestBuilder;
clone.XmlMethod = method;
clone.XmlParameters = parameters.ToList();
return clone;
}
protected override void Apply(HttpRequest request)
{
base.Apply(request);
request.Headers.ContentType = XmlRpcContentType;
var methodCallElements = new List<XElement> { new XElement("methodName", XmlMethod) };
if (XmlParameters.Any())
{
var argElements = XmlParameters.Select(x => new XElement("param", ConvertParameter(x))).ToList();
var paramsElement = new XElement("params", argElements);
methodCallElements.Add(paramsElement);
}
var message = new XDocument(
new XDeclaration("1.0", "utf-8", "yes"),
new XElement("methodCall", methodCallElements));
var body = message.ToString();
Logger.Debug($"Executing remote method: {XmlMethod}");
Logger.Trace($"methodCall {XmlMethod} body:\n{body}");
request.SetContent(body);
}
private static XElement ConvertParameter(object value)
{
XElement data;
if (value is string s)
{
data = new XElement("string", s);
}
else if (value is List<string> l)
{
data = new XElement("array", new XElement("data", l.Select(x => new XElement("value", new XElement("string", x)))));
}
else if (value is int i)
{
data = new XElement("int", i);
}
else if (value is byte[] bytes)
{
data = new XElement("base64", Convert.ToBase64String(bytes));
}
else
{
throw new InvalidOperationException($"Unhandled argument type {value.GetType().Name}");
}
return new XElement("value", data);
}
}
}

@ -132,6 +132,12 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
continue; continue;
} }
// Ignore torrents with an empty path
if (torrent.Path.IsNullOrWhiteSpace())
{
continue;
}
if (torrent.Path.StartsWith(".")) if (torrent.Path.StartsWith("."))
{ {
throw new DownloadClientException("Download paths must be absolute. Please specify variable \"directory\" in rTorrent."); throw new DownloadClientException("Download paths must be absolute. Please specify variable \"directory\" in rTorrent.");

@ -0,0 +1,28 @@
using System.Xml.Linq;
using System.Xml.XPath;
using NzbDrone.Core.Download.Extensions;
namespace NzbDrone.Core.Download.Clients.RTorrent
{
public class RTorrentFault
{
public RTorrentFault(XElement element)
{
foreach (var e in element.XPathSelectElements("./value/struct/member"))
{
var name = e.ElementAsString("name");
if (name == "faultCode")
{
FaultCode = e.Element("value").GetIntValue();
}
else if (name == "faultString")
{
FaultString = e.Element("value").GetStringValue();
}
}
}
public int FaultCode { get; set; }
public string FaultString { get; set; }
}
}

@ -1,10 +1,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Net; using System.Net;
using CookComputing.XmlRpc; using System.Xml.Linq;
using NLog; using System.Xml.XPath;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Download.Extensions;
namespace NzbDrone.Core.Download.Clients.RTorrent namespace NzbDrone.Core.Download.Clients.RTorrent
{ {
@ -21,64 +23,26 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
void PushTorrentUniqueView(string hash, string view, RTorrentSettings settings); void PushTorrentUniqueView(string hash, string view, RTorrentSettings settings);
} }
public interface IRTorrent : IXmlRpcProxy
{
[XmlRpcMethod("d.multicall2")]
object[] TorrentMulticall(params string[] parameters);
[XmlRpcMethod("load.normal")]
int LoadNormal(string target, string data, params string[] commands);
[XmlRpcMethod("load.start")]
int LoadStart(string target, string data, params string[] commands);
[XmlRpcMethod("load.raw")]
int LoadRaw(string target, byte[] data, params string[] commands);
[XmlRpcMethod("load.raw_start")]
int LoadRawStart(string target, byte[] data, params string[] commands);
[XmlRpcMethod("d.erase")]
int Remove(string hash);
[XmlRpcMethod("d.name")]
string GetName(string hash);
[XmlRpcMethod("d.custom1.set")]
string SetLabel(string hash, string label);
[XmlRpcMethod("d.views.push_back_unique")]
int PushUniqueView(string hash, string view);
[XmlRpcMethod("system.client_version")]
string GetVersion();
}
public class RTorrentProxy : IRTorrentProxy public class RTorrentProxy : IRTorrentProxy
{ {
private readonly Logger _logger; private readonly IHttpClient _httpClient;
public RTorrentProxy(Logger logger) public RTorrentProxy(IHttpClient httpClient)
{ {
_logger = logger; _httpClient = httpClient;
} }
public string GetVersion(RTorrentSettings settings) public string GetVersion(RTorrentSettings settings)
{ {
_logger.Debug("Executing remote method: system.client_version"); var document = ExecuteRequest(settings, "system.client_version");
var client = BuildClient(settings);
var version = ExecuteRequest(() => client.GetVersion());
return version; return document.Descendants("string").FirstOrDefault()?.Value ?? "0.0.0";
} }
public List<RTorrentTorrent> GetTorrents(RTorrentSettings settings) public List<RTorrentTorrent> GetTorrents(RTorrentSettings settings)
{ {
_logger.Debug("Executing remote method: d.multicall2"); var document = ExecuteRequest(settings,
"d.multicall2",
var client = BuildClient(settings);
var ret = ExecuteRequest(() => client.TorrentMulticall(
"", "",
"", "",
"d.name=", // string "d.name=", // string
@ -91,53 +55,35 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
"d.ratio=", // long "d.ratio=", // long
"d.is_open=", // long "d.is_open=", // long
"d.is_active=", // long "d.is_active=", // long
"d.complete=", // long "d.complete=", //long
"d.timestamp.finished=")); // long (unix timestamp) "d.timestamp.finished="); // long (unix timestamp)
var items = new List<RTorrentTorrent>(); var torrents = document.XPathSelectElement("./methodResponse/params/param/value/array/data")
?.Elements()
foreach (object[] torrent in ret) .Select(x => new RTorrentTorrent(x))
{ .ToList()
var labelDecoded = System.Web.HttpUtility.UrlDecode((string)torrent[3]); ?? new List<RTorrentTorrent>();
var item = new RTorrentTorrent();
item.Name = (string)torrent[0];
item.Hash = (string)torrent[1];
item.Path = (string)torrent[2];
item.Category = labelDecoded;
item.TotalSize = (long)torrent[4];
item.RemainingSize = (long)torrent[5];
item.DownRate = (long)torrent[6];
item.Ratio = (long)torrent[7];
item.IsOpen = Convert.ToBoolean((long)torrent[8]);
item.IsActive = Convert.ToBoolean((long)torrent[9]);
item.IsFinished = Convert.ToBoolean((long)torrent[10]);
item.FinishedTime = (long)torrent[11];
items.Add(item);
}
return items; return torrents;
} }
public void AddTorrentFromUrl(string torrentUrl, string label, RTorrentPriority priority, string directory, RTorrentSettings settings) public void AddTorrentFromUrl(string torrentUrl, string label, RTorrentPriority priority, string directory, RTorrentSettings settings)
{ {
var client = BuildClient(settings); var args = new List<object> { "", torrentUrl };
var response = ExecuteRequest(() => args.AddRange(GetCommands(label, priority, directory));
{
XDocument response;
if (settings.AddStopped) if (settings.AddStopped)
{ {
_logger.Debug("Executing remote method: load.normal"); response = ExecuteRequest(settings, "load.normal", args.ToArray());
return client.LoadNormal("", torrentUrl, GetCommands(label, priority, directory));
} }
else else
{ {
_logger.Debug("Executing remote method: load.start"); response = ExecuteRequest(settings, "load.start", args.ToArray());
return client.LoadStart("", torrentUrl, GetCommands(label, priority, directory));
} }
});
if (response != 0) if (response.GetIntResponse() != 0)
{ {
throw new DownloadClientException("Could not add torrent: {0}.", torrentUrl); throw new DownloadClientException("Could not add torrent: {0}.", torrentUrl);
} }
@ -145,22 +91,21 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
public void AddTorrentFromFile(string fileName, byte[] fileContent, string label, RTorrentPriority priority, string directory, RTorrentSettings settings) public void AddTorrentFromFile(string fileName, byte[] fileContent, string label, RTorrentPriority priority, string directory, RTorrentSettings settings)
{ {
var client = BuildClient(settings); var args = new List<object> { "", fileContent };
var response = ExecuteRequest(() => args.AddRange(GetCommands(label, priority, directory));
{
XDocument response;
if (settings.AddStopped) if (settings.AddStopped)
{ {
_logger.Debug("Executing remote method: load.raw"); response = ExecuteRequest(settings, "load.raw", args.ToArray());
return client.LoadRaw("", fileContent, GetCommands(label, priority, directory));
} }
else else
{ {
_logger.Debug("Executing remote method: load.raw_start"); response = ExecuteRequest(settings, "load.raw_start", args.ToArray());
return client.LoadRawStart("", fileContent, GetCommands(label, priority, directory));
} }
});
if (response != 0) if (response.GetIntResponse() != 0)
{ {
throw new DownloadClientException("Could not add torrent: {0}.", fileName); throw new DownloadClientException("Could not add torrent: {0}.", fileName);
} }
@ -168,12 +113,9 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
public void SetTorrentLabel(string hash, string label, RTorrentSettings settings) public void SetTorrentLabel(string hash, string label, RTorrentSettings settings)
{ {
_logger.Debug("Executing remote method: d.custom1.set"); var response = ExecuteRequest(settings, "d.custom1.set", hash, label);
var client = BuildClient(settings);
var response = ExecuteRequest(() => client.SetLabel(hash, label));
if (response != label) if (response.GetStringResponse() != label)
{ {
throw new DownloadClientException("Could not set label to {1} for torrent: {0}.", hash, label); throw new DownloadClientException("Could not set label to {1} for torrent: {0}.", hash, label);
} }
@ -181,11 +123,9 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
public void PushTorrentUniqueView(string hash, string view, RTorrentSettings settings) public void PushTorrentUniqueView(string hash, string view, RTorrentSettings settings)
{ {
_logger.Debug("Executing remote method: d.views.push_back_unique"); var response = ExecuteRequest(settings, "d.views.push_back_unique", hash, view);
var client = BuildClient(settings); if (response.GetIntResponse() != 0)
var response = ExecuteRequest(() => client.PushUniqueView(hash, view));
if (response != 0)
{ {
throw new DownloadClientException("Could not push unique view {0} for torrent: {1}.", view, hash); throw new DownloadClientException("Could not push unique view {0} for torrent: {1}.", view, hash);
} }
@ -193,12 +133,9 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
public void RemoveTorrent(string hash, RTorrentSettings settings) public void RemoveTorrent(string hash, RTorrentSettings settings)
{ {
_logger.Debug("Executing remote method: d.erase"); var response = ExecuteRequest(settings, "d.erase", hash);
var client = BuildClient(settings);
var response = ExecuteRequest(() => client.Remove(hash));
if (response != 0) if (response.GetIntResponse() != 0)
{ {
throw new DownloadClientException("Could not remove torrent: {0}.", hash); throw new DownloadClientException("Could not remove torrent: {0}.", hash);
} }
@ -206,13 +143,10 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
public bool HasHashTorrent(string hash, RTorrentSettings settings) public bool HasHashTorrent(string hash, RTorrentSettings settings)
{ {
_logger.Debug("Executing remote method: d.name");
var client = BuildClient(settings);
try try
{ {
var name = ExecuteRequest(() => client.GetName(hash)); var response = ExecuteRequest(settings, "d.name", hash);
var name = response.GetStringResponse();
if (name.IsNullOrWhiteSpace()) if (name.IsNullOrWhiteSpace())
{ {
@ -251,45 +185,34 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
return result.ToArray(); return result.ToArray();
} }
private IRTorrent BuildClient(RTorrentSettings settings) private XDocument ExecuteRequest(RTorrentSettings settings, string methodName, params object[] args)
{ {
var client = XmlRpcProxyGen.Create<IRTorrent>(); var requestBuilder = new XmlRpcRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase)
{
client.Url = string.Format(@"{0}://{1}:{2}/{3}", LogResponseContent = true,
settings.UseSsl ? "https" : "http", };
settings.Host,
settings.Port,
settings.UrlBase);
client.EnableCompression = true;
if (!settings.Username.IsNullOrWhiteSpace()) if (!settings.Username.IsNullOrWhiteSpace())
{ {
client.Credentials = new BasicNetworkCredential(settings.Username, settings.Password); requestBuilder.NetworkCredential = new NetworkCredential(settings.Username, settings.Password);
} }
return client; var request = requestBuilder.Call(methodName, args).Build();
}
private T ExecuteRequest<T>(Func<T> task) var response = _httpClient.Execute(request);
{
try var doc = XDocument.Parse(response.Content);
{
return task(); var faultElement = doc.XPathSelectElement("./methodResponse/fault");
}
catch (XmlRpcServerException ex) if (faultElement != null)
{
throw new DownloadClientException("Unable to connect to rTorrent, please check your settings", ex);
}
catch (WebException ex)
{
if (ex.Status == WebExceptionStatus.TrustFailure)
{ {
throw new DownloadClientUnavailableException("Unable to connect to rTorrent, certificate validation failed.", ex); var fault = new RTorrentFault(faultElement);
}
throw new DownloadClientUnavailableException("Unable to connect to rTorrent, please check your settings", ex); throw new DownloadClientException($"rTorrent returned error code {fault.FaultCode}: {fault.FaultString}");
} }
return doc;
} }
} }
} }

@ -1,7 +1,35 @@
namespace NzbDrone.Core.Download.Clients.RTorrent using System;
using System.Linq;
using System.Web;
using System.Xml.Linq;
using NzbDrone.Core.Download.Extensions;
namespace NzbDrone.Core.Download.Clients.RTorrent
{ {
public class RTorrentTorrent public class RTorrentTorrent
{ {
public RTorrentTorrent()
{
}
public RTorrentTorrent(XElement element)
{
var data = element.Descendants("value").ToList();
Name = data[0].GetStringValue();
Hash = data[1].GetStringValue();
Path = data[2].GetStringValue();
Category = HttpUtility.UrlDecode(data[3].GetStringValue());
TotalSize = data[4].GetLongValue();
RemainingSize = data[5].GetLongValue();
DownRate = data[6].GetLongValue();
Ratio = data[7].GetLongValue();
IsOpen = Convert.ToBoolean(data[8].GetLongValue());
IsActive = Convert.ToBoolean(data[9].GetLongValue());
IsFinished = Convert.ToBoolean(data[10].GetLongValue());
FinishedTime = data[11].GetLongValue();
}
public string Name { get; set; } public string Name { get; set; }
public string Hash { get; set; } public string Hash { get; set; }
public string Path { get; set; } public string Path { get; set; }

@ -0,0 +1,55 @@
using System.Linq;
using System.Xml.Linq;
using System.Xml.XPath;
namespace NzbDrone.Core.Download.Extensions
{
internal static class XmlExtensions
{
public static string GetStringValue(this XElement element)
{
return element.ElementAsString("string");
}
public static long GetLongValue(this XElement element)
{
return element.ElementAsLong("i8");
}
public static int GetIntValue(this XElement element)
{
return element.ElementAsInt("i4");
}
public static string ElementAsString(this XElement element, XName name, bool trim = false)
{
var el = element.Element(name);
return string.IsNullOrWhiteSpace(el?.Value)
? null
: (trim ? el.Value.Trim() : el.Value);
}
public static long ElementAsLong(this XElement element, XName name)
{
var el = element.Element(name);
return long.TryParse(el?.Value, out long value) ? value : default;
}
public static int ElementAsInt(this XElement element, XName name)
{
var el = element.Element(name);
return int.TryParse(el?.Value, out int value) ? value : default(int);
}
public static int GetIntResponse(this XDocument document)
{
return document.XPathSelectElement("./methodResponse/params/param/value").GetIntValue();
}
public static string GetStringResponse(this XDocument document)
{
return document.XPathSelectElement("./methodResponse/params/param/value").GetStringValue();
}
}
}
Loading…
Cancel
Save