parent
da60543c72
commit
a080bf1c6c
@ -0,0 +1,101 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Core.Configuration;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Authentication
|
||||||
|
{
|
||||||
|
public interface IProtectionService
|
||||||
|
{
|
||||||
|
string Protect(string plainText);
|
||||||
|
string UnProtect(string plainText);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ProtectionService : IProtectionService
|
||||||
|
{
|
||||||
|
private readonly IConfigFileProvider _configService;
|
||||||
|
|
||||||
|
public ProtectionService(IConfigFileProvider configService)
|
||||||
|
{
|
||||||
|
_configService = configService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Protect(string text)
|
||||||
|
{
|
||||||
|
var key = Encoding.UTF8.GetBytes(_configService.ApiKey);
|
||||||
|
|
||||||
|
using (var aesAlg = Aes.Create())
|
||||||
|
{
|
||||||
|
using (var encryptor = aesAlg.CreateEncryptor(key, aesAlg.IV))
|
||||||
|
{
|
||||||
|
using (var msEncrypt = new MemoryStream())
|
||||||
|
{
|
||||||
|
using (var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
|
||||||
|
using (var swEncrypt = new StreamWriter(csEncrypt))
|
||||||
|
{
|
||||||
|
swEncrypt.Write(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
var iv = aesAlg.IV;
|
||||||
|
|
||||||
|
var decryptedContent = msEncrypt.ToArray();
|
||||||
|
|
||||||
|
var result = new byte[iv.Length + decryptedContent.Length];
|
||||||
|
|
||||||
|
Buffer.BlockCopy(iv, 0, result, 0, iv.Length);
|
||||||
|
Buffer.BlockCopy(decryptedContent, 0, result, iv.Length, decryptedContent.Length);
|
||||||
|
|
||||||
|
return Convert.ToBase64String(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string UnProtect(string value)
|
||||||
|
{
|
||||||
|
if (value.IsNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
value = value.Replace(" ", "+");
|
||||||
|
var fullCipher = Convert.FromBase64String(value);
|
||||||
|
|
||||||
|
var iv = new byte[16];
|
||||||
|
var cipher = new byte[fullCipher.Length - iv.Length];
|
||||||
|
|
||||||
|
Buffer.BlockCopy(fullCipher, 0, iv, 0, iv.Length);
|
||||||
|
Buffer.BlockCopy(fullCipher, iv.Length, cipher, 0, fullCipher.Length - iv.Length);
|
||||||
|
var key = Encoding.UTF8.GetBytes(_configService.ApiKey);
|
||||||
|
|
||||||
|
using (var aesAlg = Aes.Create())
|
||||||
|
{
|
||||||
|
using (var decryptor = aesAlg.CreateDecryptor(key, iv))
|
||||||
|
{
|
||||||
|
string result;
|
||||||
|
using (var msDecrypt = new MemoryStream(cipher))
|
||||||
|
{
|
||||||
|
using (var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
|
||||||
|
{
|
||||||
|
using (var srDecrypt = new StreamReader(csDecrypt))
|
||||||
|
{
|
||||||
|
result = srDecrypt.ReadToEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.AspNetCore.WebUtilities;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Core.Authentication;
|
||||||
|
using NzbDrone.Core.Configuration;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Indexers
|
||||||
|
{
|
||||||
|
public interface IDownloadMappingService
|
||||||
|
{
|
||||||
|
Uri ConvertToProxyLink(Uri link, string serverUrl, int indexerId, string file = "t");
|
||||||
|
string ConvertToNormalLink(string link);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DownloadMappingService : IDownloadMappingService
|
||||||
|
{
|
||||||
|
private readonly IProtectionService _protectionService;
|
||||||
|
private readonly IConfigFileProvider _configFileProvider;
|
||||||
|
|
||||||
|
public DownloadMappingService(IProtectionService protectionService, IConfigFileProvider configFileProvider)
|
||||||
|
{
|
||||||
|
_protectionService = protectionService;
|
||||||
|
_configFileProvider = configFileProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Uri ConvertToProxyLink(Uri link, string serverUrl, int indexerId, string file = "t")
|
||||||
|
{
|
||||||
|
var urlBase = _configFileProvider.UrlBase;
|
||||||
|
|
||||||
|
if (urlBase.IsNotNullOrWhiteSpace() && !urlBase.StartsWith("/"))
|
||||||
|
{
|
||||||
|
urlBase = "/" + urlBase;
|
||||||
|
}
|
||||||
|
|
||||||
|
var encryptedLink = _protectionService.Protect(link.ToString());
|
||||||
|
var encodedLink = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(encryptedLink));
|
||||||
|
var urlEncodedFile = WebUtility.UrlEncode(file);
|
||||||
|
var proxyLink = $"{serverUrl}{urlBase}/api/v1/indexer/{indexerId}/download?apikey={_configFileProvider.ApiKey}&link={encodedLink}&file={urlEncodedFile}";
|
||||||
|
return new Uri(proxyLink);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ConvertToNormalLink(string link)
|
||||||
|
{
|
||||||
|
var encodedLink = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(link));
|
||||||
|
var decryptedLink = _protectionService.UnProtect(encodedLink);
|
||||||
|
|
||||||
|
return decryptedLink;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,86 @@
|
|||||||
|
using System;
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Common.EnsureThat;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Common.TPL;
|
||||||
|
using NzbDrone.Core.Exceptions;
|
||||||
|
using NzbDrone.Core.Indexers;
|
||||||
|
using NzbDrone.Core.Indexers.Events;
|
||||||
|
using NzbDrone.Core.Messaging.Events;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Indexers
|
||||||
|
{
|
||||||
|
public interface IDownloadService
|
||||||
|
{
|
||||||
|
byte[] DownloadReport(string link, int indexerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DownloadService : IDownloadService
|
||||||
|
{
|
||||||
|
private readonly IIndexerFactory _indexerFactory;
|
||||||
|
private readonly IIndexerStatusService _indexerStatusService;
|
||||||
|
private readonly IRateLimitService _rateLimitService;
|
||||||
|
private readonly IEventAggregator _eventAggregator;
|
||||||
|
private readonly Logger _logger;
|
||||||
|
|
||||||
|
public DownloadService(IIndexerFactory indexerFactory,
|
||||||
|
IIndexerStatusService indexerStatusService,
|
||||||
|
IRateLimitService rateLimitService,
|
||||||
|
IEventAggregator eventAggregator,
|
||||||
|
Logger logger)
|
||||||
|
{
|
||||||
|
_indexerFactory = indexerFactory;
|
||||||
|
_indexerStatusService = indexerStatusService;
|
||||||
|
_rateLimitService = rateLimitService;
|
||||||
|
_eventAggregator = eventAggregator;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] DownloadReport(string link, int indexerId)
|
||||||
|
{
|
||||||
|
var url = new HttpUri(link);
|
||||||
|
|
||||||
|
// Limit grabs to 2 per second.
|
||||||
|
if (link.IsNotNullOrWhiteSpace() && !link.StartsWith("magnet:"))
|
||||||
|
{
|
||||||
|
_rateLimitService.WaitAndPulse(url.Host, TimeSpan.FromSeconds(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
var indexer = _indexerFactory.GetInstance(_indexerFactory.Get(indexerId));
|
||||||
|
bool success;
|
||||||
|
var downloadedBytes = Array.Empty<byte>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
downloadedBytes = indexer.Download(url);
|
||||||
|
_indexerStatusService.RecordSuccess(indexerId);
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
catch (ReleaseUnavailableException)
|
||||||
|
{
|
||||||
|
_logger.Trace("Release {0} no longer available on indexer.", link);
|
||||||
|
_eventAggregator.PublishEvent(new IndexerDownloadEvent(indexerId, false));
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (ReleaseDownloadException ex)
|
||||||
|
{
|
||||||
|
var http429 = ex.InnerException as TooManyRequestsException;
|
||||||
|
if (http429 != null)
|
||||||
|
{
|
||||||
|
_indexerStatusService.RecordFailure(indexerId, http429.RetryAfter);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_indexerStatusService.RecordFailure(indexerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
_eventAggregator.PublishEvent(new IndexerDownloadEvent(indexerId, false));
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
_eventAggregator.PublishEvent(new IndexerDownloadEvent(indexerId, success));
|
||||||
|
return downloadedBytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using NzbDrone.Common.Messaging;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Indexers.Events
|
||||||
|
{
|
||||||
|
public class IndexerDownloadEvent : IEvent
|
||||||
|
{
|
||||||
|
public int IndexerId { get; set; }
|
||||||
|
public bool Successful { get; set; }
|
||||||
|
|
||||||
|
public IndexerDownloadEvent(int indexerId, bool successful)
|
||||||
|
{
|
||||||
|
IndexerId = indexerId;
|
||||||
|
Successful = successful;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
using NzbDrone.Common.Messaging;
|
using NzbDrone.Common.Messaging;
|
||||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Indexers
|
namespace NzbDrone.Core.Indexers.Events
|
||||||
{
|
{
|
||||||
public class IndexerQueryEvent : IEvent
|
public class IndexerQueryEvent : IEvent
|
||||||
{
|
{
|
@ -0,0 +1,29 @@
|
|||||||
|
using System.IO;
|
||||||
|
using Nancy;
|
||||||
|
|
||||||
|
namespace NzbDrone.Http.Extensions
|
||||||
|
{
|
||||||
|
public static class ResponseExtensions
|
||||||
|
{
|
||||||
|
public static Response FromByteArray(this IResponseFormatter formatter, byte[] body, string contentType = null)
|
||||||
|
{
|
||||||
|
return new ByteArrayResponse(body, contentType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ByteArrayResponse : Response
|
||||||
|
{
|
||||||
|
public ByteArrayResponse(byte[] body, string contentType = null)
|
||||||
|
{
|
||||||
|
this.ContentType = contentType ?? "application/octet-stream";
|
||||||
|
|
||||||
|
this.Contents = stream =>
|
||||||
|
{
|
||||||
|
using (var writer = new BinaryWriter(stream))
|
||||||
|
{
|
||||||
|
writer.Write(body);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue