using System; using System.Collections.Generic; using System.Net; using Newtonsoft.Json.Linq; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Common.Serializer; namespace NzbDrone.Core.Download.Clients.Transmission { public interface ITransmissionProxy { List GetTorrents(TransmissionSettings settings); void AddTorrentFromUrl(string torrentUrl, string downloadDirectory, TransmissionSettings settings); void AddTorrentFromData(byte[] torrentData, string downloadDirectory, TransmissionSettings settings); void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, TransmissionSettings settings); TransmissionConfig GetConfig(TransmissionSettings settings); string GetProtocolVersion(TransmissionSettings settings); string GetClientVersion(TransmissionSettings settings); void RemoveTorrent(string hash, bool removeData, TransmissionSettings settings); void MoveTorrentToTopInQueue(string hashString, TransmissionSettings settings); } public class TransmissionProxy : ITransmissionProxy { private readonly IHttpClient _httpClient; private readonly 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) { var result = GetTorrentStatus(settings); var torrents = ((JArray)result.Arguments["torrents"]).ToObject>(); return torrents; } public void AddTorrentFromUrl(string torrentUrl, string downloadDirectory, TransmissionSettings settings) { var arguments = new Dictionary(); arguments.Add("filename", torrentUrl); arguments.Add("paused", settings.AddPaused); if (!downloadDirectory.IsNullOrWhiteSpace()) { arguments.Add("download-dir", downloadDirectory); } ProcessRequest("torrent-add", arguments, settings); } public void AddTorrentFromData(byte[] torrentData, string downloadDirectory, TransmissionSettings settings) { var arguments = new Dictionary(); arguments.Add("metainfo", Convert.ToBase64String(torrentData)); arguments.Add("paused", settings.AddPaused); if (!downloadDirectory.IsNullOrWhiteSpace()) { arguments.Add("download-dir", downloadDirectory); } ProcessRequest("torrent-add", arguments, settings); } public void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, TransmissionSettings settings) { if (seedConfiguration == null) { return; } var arguments = new Dictionary(); arguments.Add("ids", new[] { hash }); if (seedConfiguration.Ratio != null) { arguments.Add("seedRatioLimit", seedConfiguration.Ratio.Value); arguments.Add("seedRatioMode", 1); } if (seedConfiguration.SeedTime != null) { arguments.Add("seedIdleLimit", Convert.ToInt32(seedConfiguration.SeedTime.Value.TotalMinutes)); arguments.Add("seedIdleMode", 1); } ProcessRequest("torrent-set", arguments, settings); } public string GetProtocolVersion(TransmissionSettings settings) { var config = GetConfig(settings); return config.RpcVersion; } public string GetClientVersion(TransmissionSettings settings) { var config = GetConfig(settings); return config.Version; } public TransmissionConfig GetConfig(TransmissionSettings settings) { // Gets the transmission version. var result = GetSessionVariables(settings); return Json.Deserialize(result.Arguments.ToJson()); } public void RemoveTorrent(string hashString, bool removeData, TransmissionSettings settings) { var arguments = new Dictionary(); arguments.Add("ids", new string[] { hashString }); arguments.Add("delete-local-data", removeData); ProcessRequest("torrent-remove", arguments, settings); } public void MoveTorrentToTopInQueue(string hashString, TransmissionSettings settings) { var arguments = new Dictionary(); arguments.Add("ids", new string[] { hashString }); ProcessRequest("queue-move-top", arguments, settings); } private TransmissionResponse GetSessionVariables(TransmissionSettings settings) { // Retrieve transmission information such as the default download directory, bandwidth throttling and seed ratio. return ProcessRequest("session-get", null, settings); } private TransmissionResponse GetSessionStatistics(TransmissionSettings settings) { return ProcessRequest("session-stats", null, settings); } private TransmissionResponse GetTorrentStatus(TransmissionSettings settings) { return GetTorrentStatus(null, settings); } private TransmissionResponse GetTorrentStatus(IEnumerable hashStrings, TransmissionSettings settings) { var fields = new string[] { "id", "hashString", // Unique torrent ID. Use this instead of the client id? "name", "downloadDir", "totalSize", "leftUntilDone", "isFinished", "eta", "status", "secondsDownloading", "secondsSeeding", "errorString", "uploadedEver", "downloadedEver", "seedRatioLimit", "seedRatioMode", "seedIdleLimit", "seedIdleMode", "fileCount" }; var arguments = new Dictionary(); arguments.Add("fields", fields); if (hashStrings != null) { arguments.Add("ids", hashStrings); } var result = ProcessRequest("torrent-get", arguments, settings); return result; } private HttpRequestBuilder BuildRequest(TransmissionSettings settings) { var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase) .Resource("rpc") .Accept(HttpAccept.Json); requestBuilder.LogResponseContent = true; requestBuilder.NetworkCredential = new BasicNetworkCredential(settings.Username, settings.Password); requestBuilder.AllowAutoRedirect = false; 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) { _authSessionIDCache.Remove(authKey); var authLoginRequest = BuildRequest(settings).Build(); authLoginRequest.SuppressHttpError = true; var response = _httpClient.Execute(authLoginRequest); if (response.StatusCode == HttpStatusCode.MovedPermanently) { 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."); } } else { throw new DownloadClientAuthenticationException("Failed to authenticate with Transmission."); } _logger.Debug("Transmission authentication succeeded."); _authSessionIDCache.Set(authKey, sessionId); } requestBuilder.SetHeader("X-Transmission-Session-Id", sessionId); } public TransmissionResponse ProcessRequest(string action, object arguments, TransmissionSettings settings) { try { var requestBuilder = BuildRequest(settings); requestBuilder.Headers.ContentType = "application/json"; requestBuilder.SuppressHttpError = true; AuthenticateClient(requestBuilder, settings); var request = requestBuilder.Post().Build(); var data = new Dictionary(); data.Add("method", action); if (arguments != null) { data.Add("arguments", arguments); } request.SetContent(data.ToJson()); request.ContentSummary = string.Format("{0}(...)", action); var response = _httpClient.Execute(request); if (response.StatusCode == HttpStatusCode.Conflict) { AuthenticateClient(requestBuilder, settings, true); request = requestBuilder.Post().Build(); request.SetContent(data.ToJson()); request.ContentSummary = string.Format("{0}(...)", action); response = _httpClient.Execute(request); } else if (response.StatusCode == HttpStatusCode.Unauthorized) { throw new DownloadClientAuthenticationException("User authentication failed."); } var transmissionResponse = Json.Deserialize(response.Content); if (transmissionResponse == null) { throw new TransmissionException("Unexpected response"); } else if (transmissionResponse.Result != "success") { throw new TransmissionException(transmissionResponse.Result); } return transmissionResponse; } catch (HttpException ex) { throw new DownloadClientException("Unable to connect to Transmission, please check your settings", ex); } catch (WebException ex) { if (ex.Status == WebExceptionStatus.TrustFailure) { throw new DownloadClientUnavailableException("Unable to connect to Transmission, certificate validation failed.", ex); } throw new DownloadClientUnavailableException("Unable to connect to Transmission, please check your settings", ex); } } } }