mirror of
https://github.com/Jackett/Jackett.git
synced 2025-09-17 17:34:09 +02:00
Copy controllers untouced from Jackett project
This commit is contained in:
99
src/Jackett.Server/Controllers/BlackholeController.cs
Normal file
99
src/Jackett.Server/Controllers/BlackholeController.cs
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using NLog;
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Web;
|
||||||
|
using System.Web.Http;
|
||||||
|
using Jackett.Common.Models.Config;
|
||||||
|
using Jackett.Common.Services.Interfaces;
|
||||||
|
using Jackett.Common.Utils;
|
||||||
|
|
||||||
|
namespace Jackett.Controllers
|
||||||
|
{
|
||||||
|
[AllowAnonymous]
|
||||||
|
[JackettAPINoCache]
|
||||||
|
public class BlackholeController : ApiController
|
||||||
|
{
|
||||||
|
private Logger logger;
|
||||||
|
private IIndexerManagerService indexerService;
|
||||||
|
private readonly ServerConfig serverConfig;
|
||||||
|
IProtectionService protectionService;
|
||||||
|
|
||||||
|
public BlackholeController(IIndexerManagerService i, Logger l, ServerConfig config, IProtectionService ps)
|
||||||
|
{
|
||||||
|
logger = l;
|
||||||
|
indexerService = i;
|
||||||
|
serverConfig = config;
|
||||||
|
|
||||||
|
protectionService = ps;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IHttpActionResult> Blackhole(string indexerID, string path, string jackett_apikey, string file)
|
||||||
|
{
|
||||||
|
|
||||||
|
var jsonReply = new JObject();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var indexer = indexerService.GetWebIndexer(indexerID);
|
||||||
|
if (!indexer.IsConfigured)
|
||||||
|
{
|
||||||
|
logger.Warn(string.Format("Rejected a request to {0} which is unconfigured.", indexer.DisplayName));
|
||||||
|
throw new Exception("This indexer is not configured.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serverConfig.APIKey != jackett_apikey)
|
||||||
|
throw new Exception("Incorrect API key");
|
||||||
|
|
||||||
|
path = Encoding.UTF8.GetString(HttpServerUtility.UrlTokenDecode(path));
|
||||||
|
path = protectionService.UnProtect(path);
|
||||||
|
var remoteFile = new Uri(path, UriKind.RelativeOrAbsolute);
|
||||||
|
var fileExtension = ".torrent";
|
||||||
|
var downloadBytes = await indexer.Download(remoteFile);
|
||||||
|
|
||||||
|
// handle magnet URLs
|
||||||
|
if (downloadBytes.Length >= 7
|
||||||
|
&& downloadBytes[0] == 0x6d // m
|
||||||
|
&& downloadBytes[1] == 0x61 // a
|
||||||
|
&& downloadBytes[2] == 0x67 // g
|
||||||
|
&& downloadBytes[3] == 0x6e // n
|
||||||
|
&& downloadBytes[4] == 0x65 // e
|
||||||
|
&& downloadBytes[5] == 0x74 // t
|
||||||
|
&& downloadBytes[6] == 0x3a // :
|
||||||
|
)
|
||||||
|
{
|
||||||
|
fileExtension = ".magnet";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(serverConfig.BlackholeDir))
|
||||||
|
{
|
||||||
|
throw new Exception("Blackhole directory not set!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Directory.Exists(serverConfig.BlackholeDir))
|
||||||
|
{
|
||||||
|
throw new Exception("Blackhole directory does not exist: " + serverConfig.BlackholeDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileName = DateTime.Now.Ticks.ToString() + "-" + StringUtil.MakeValidFileName(indexer.DisplayName, '_', false);
|
||||||
|
if (string.IsNullOrWhiteSpace(file))
|
||||||
|
fileName += fileExtension;
|
||||||
|
else
|
||||||
|
fileName += "-"+StringUtil.MakeValidFileName(file + fileExtension, '_', false); // call MakeValidFileName() again to avoid any possibility of path traversal attacks
|
||||||
|
|
||||||
|
File.WriteAllBytes(Path.Combine(serverConfig.BlackholeDir, fileName), downloadBytes);
|
||||||
|
jsonReply["result"] = "success";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.Error(ex, "Error downloading to blackhole " + indexerID + " " + path);
|
||||||
|
jsonReply["result"] = "error";
|
||||||
|
jsonReply["error"] = ex.Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Json(jsonReply);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
94
src/Jackett.Server/Controllers/DownloadController.cs
Normal file
94
src/Jackett.Server/Controllers/DownloadController.cs
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Web;
|
||||||
|
using System.Web.Http;
|
||||||
|
using BencodeNET.Parsing;
|
||||||
|
using Jackett.Common.Models.Config;
|
||||||
|
using Jackett.Common.Services.Interfaces;
|
||||||
|
using Jackett.Common.Utils;
|
||||||
|
using NLog;
|
||||||
|
|
||||||
|
namespace Jackett.Controllers
|
||||||
|
{
|
||||||
|
[AllowAnonymous]
|
||||||
|
[JackettAPINoCache]
|
||||||
|
public class DownloadController : ApiController
|
||||||
|
{
|
||||||
|
private ServerConfig config;
|
||||||
|
private Logger logger;
|
||||||
|
private IIndexerManagerService indexerService;
|
||||||
|
private IProtectionService protectionService;
|
||||||
|
|
||||||
|
public DownloadController(IIndexerManagerService i, Logger l, IProtectionService ps, ServerConfig serverConfig)
|
||||||
|
{
|
||||||
|
config = serverConfig;
|
||||||
|
logger = l;
|
||||||
|
indexerService = i;
|
||||||
|
protectionService = ps;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<HttpResponseMessage> Download(string indexerID, string path, string jackett_apikey, string file)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var indexer = indexerService.GetWebIndexer(indexerID);
|
||||||
|
|
||||||
|
if (!indexer.IsConfigured)
|
||||||
|
{
|
||||||
|
logger.Warn(string.Format("Rejected a request to {0} which is unconfigured.", indexer.DisplayName));
|
||||||
|
return Request.CreateResponse(HttpStatusCode.Forbidden, "This indexer is not configured.");
|
||||||
|
}
|
||||||
|
|
||||||
|
path = Encoding.UTF8.GetString(HttpServerUtility.UrlTokenDecode(path));
|
||||||
|
path = protectionService.UnProtect(path);
|
||||||
|
|
||||||
|
if (config.APIKey != jackett_apikey)
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
|
||||||
|
|
||||||
|
var target = new Uri(path, UriKind.RelativeOrAbsolute);
|
||||||
|
var downloadBytes = await indexer.Download(target);
|
||||||
|
|
||||||
|
// handle magnet URLs
|
||||||
|
if (downloadBytes.Length >= 7
|
||||||
|
&& downloadBytes[0] == 0x6d // m
|
||||||
|
&& downloadBytes[1] == 0x61 // a
|
||||||
|
&& downloadBytes[2] == 0x67 // g
|
||||||
|
&& downloadBytes[3] == 0x6e // n
|
||||||
|
&& downloadBytes[4] == 0x65 // e
|
||||||
|
&& downloadBytes[5] == 0x74 // t
|
||||||
|
&& downloadBytes[6] == 0x3a // :
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var magneturi = Encoding.UTF8.GetString(downloadBytes);
|
||||||
|
var response = Request.CreateResponse(HttpStatusCode.Moved);
|
||||||
|
response.Headers.Location = new Uri(magneturi);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This will fix torrents where the keys are not sorted, and thereby not supported by Sonarr.
|
||||||
|
var parser = new BencodeParser();
|
||||||
|
var torrentDictionary = parser.Parse(downloadBytes);
|
||||||
|
byte[] sortedDownloadBytes = torrentDictionary.EncodeAsBytes();
|
||||||
|
|
||||||
|
var result = new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
result.Content = new ByteArrayContent(sortedDownloadBytes);
|
||||||
|
result.Content.Headers.ContentType = new MediaTypeHeaderValue("application/x-bittorrent");
|
||||||
|
result.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
|
||||||
|
{
|
||||||
|
FileName = StringUtil.MakeValidFileName(file, '_', false) + ".torrent" // call MakeValidFileName again to avoid any kind of injection attack
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
logger.Error(e, "Error downloading " + indexerID + " " + path);
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.NotFound);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
173
src/Jackett.Server/Controllers/IndexerApiController.cs
Normal file
173
src/Jackett.Server/Controllers/IndexerApiController.cs
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Web.Http;
|
||||||
|
using System.Web.Http.Controllers;
|
||||||
|
using System.Web.Http.Filters;
|
||||||
|
using Jackett.Common;
|
||||||
|
using Jackett.Common.Indexers;
|
||||||
|
using Jackett.Common.Models;
|
||||||
|
using Jackett.Common.Services.Interfaces;
|
||||||
|
using Jackett.Common.Utils;
|
||||||
|
using Jackett.Utils;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using NLog;
|
||||||
|
|
||||||
|
namespace Jackett.Controllers
|
||||||
|
{
|
||||||
|
public interface IIndexerController
|
||||||
|
{
|
||||||
|
IIndexerManagerService IndexerService { get; }
|
||||||
|
IIndexer CurrentIndexer { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RequiresIndexerAttribute : ActionFilterAttribute
|
||||||
|
{
|
||||||
|
public override void OnActionExecuting(HttpActionContext actionContext)
|
||||||
|
{
|
||||||
|
base.OnActionExecuting(actionContext);
|
||||||
|
|
||||||
|
var controller = actionContext.ControllerContext.Controller;
|
||||||
|
if (!(controller is IIndexerController))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var indexerController = controller as IIndexerController;
|
||||||
|
|
||||||
|
var parameters = actionContext.RequestContext.RouteData.Values;
|
||||||
|
|
||||||
|
if (!parameters.ContainsKey("indexerId"))
|
||||||
|
{
|
||||||
|
indexerController.CurrentIndexer = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var indexerId = parameters["indexerId"] as string;
|
||||||
|
if (indexerId.IsNullOrEmptyOrWhitespace())
|
||||||
|
return;
|
||||||
|
|
||||||
|
var indexerService = indexerController.IndexerService;
|
||||||
|
var indexer = indexerService.GetIndexer(indexerId);
|
||||||
|
indexerController.CurrentIndexer = indexer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[RoutePrefix("api/v2.0/indexers")]
|
||||||
|
[JackettAuthorized]
|
||||||
|
[JackettAPINoCache]
|
||||||
|
public class IndexerApiController : ApiController, IIndexerController
|
||||||
|
{
|
||||||
|
public IIndexerManagerService IndexerService { get; private set; }
|
||||||
|
public IIndexer CurrentIndexer { get; set; }
|
||||||
|
|
||||||
|
public IndexerApiController(IIndexerManagerService indexerManagerService, IServerService ss, ICacheService c, Logger logger)
|
||||||
|
{
|
||||||
|
IndexerService = indexerManagerService;
|
||||||
|
serverService = ss;
|
||||||
|
cacheService = c;
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[RequiresIndexer]
|
||||||
|
public async Task<IHttpActionResult> Config()
|
||||||
|
{
|
||||||
|
var config = await CurrentIndexer.GetConfigurationForSetup();
|
||||||
|
return Ok(config.ToJson(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[ActionName("Config")]
|
||||||
|
[RequiresIndexer]
|
||||||
|
public async Task UpdateConfig([FromBody]Common.Models.DTO.ConfigItem[] config)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// HACK
|
||||||
|
var jsonString = JsonConvert.SerializeObject(config);
|
||||||
|
var json = JToken.Parse(jsonString);
|
||||||
|
|
||||||
|
var configurationResult = await CurrentIndexer.ApplyConfiguration(json);
|
||||||
|
|
||||||
|
if (configurationResult == IndexerConfigurationStatus.RequiresTesting)
|
||||||
|
await IndexerService.TestIndexer(CurrentIndexer.ID);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
var baseIndexer = CurrentIndexer as BaseIndexer;
|
||||||
|
if (null != baseIndexer)
|
||||||
|
baseIndexer.ResetBaseConfig();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("")]
|
||||||
|
public IEnumerable<Common.Models.DTO.Indexer> Indexers()
|
||||||
|
{
|
||||||
|
var dto = IndexerService.GetAllIndexers().Select(i => new Common.Models.DTO.Indexer(i));
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[RequiresIndexer]
|
||||||
|
public async Task Test()
|
||||||
|
{
|
||||||
|
JToken jsonReply = new JObject();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await IndexerService.TestIndexer(CurrentIndexer.ID);
|
||||||
|
CurrentIndexer.LastError = null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var msg = ex.Message;
|
||||||
|
if (ex.InnerException != null)
|
||||||
|
msg += ": " + ex.InnerException.Message;
|
||||||
|
|
||||||
|
if (CurrentIndexer != null)
|
||||||
|
CurrentIndexer.LastError = msg;
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete]
|
||||||
|
[RequiresIndexer]
|
||||||
|
[Route("{indexerId}")]
|
||||||
|
public void Delete()
|
||||||
|
{
|
||||||
|
IndexerService.DeleteIndexer(CurrentIndexer.ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// This should go to ServerConfigurationController
|
||||||
|
[Route("Cache")]
|
||||||
|
[HttpGet]
|
||||||
|
public List<TrackerCacheResult> Cache()
|
||||||
|
{
|
||||||
|
var results = cacheService.GetCachedResults();
|
||||||
|
ConfigureCacheResults(results);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ConfigureCacheResults(IEnumerable<TrackerCacheResult> results)
|
||||||
|
{
|
||||||
|
var serverUrl = serverService.GetServerUrl(Request);
|
||||||
|
foreach (var result in results)
|
||||||
|
{
|
||||||
|
var link = result.Link;
|
||||||
|
var file = StringUtil.MakeValidFileName(result.Title, '_', false);
|
||||||
|
result.Link = serverService.ConvertToProxyLink(link, serverUrl, result.TrackerId, "dl", file);
|
||||||
|
if (result.Link != null && result.Link.Scheme != "magnet" && !string.IsNullOrWhiteSpace(Engine.ServerConfig.BlackholeDir))
|
||||||
|
result.BlackholeLink = serverService.ConvertToProxyLink(link, serverUrl, result.TrackerId, "bh", file);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Logger logger;
|
||||||
|
private IServerService serverService;
|
||||||
|
private ICacheService cacheService;
|
||||||
|
}
|
||||||
|
}
|
452
src/Jackett.Server/Controllers/ResultsController.cs
Normal file
452
src/Jackett.Server/Controllers/ResultsController.cs
Normal file
@@ -0,0 +1,452 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Web.Http;
|
||||||
|
using System.Web.Http.Controllers;
|
||||||
|
using System.Web.Http.Filters;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
using Jackett.Common;
|
||||||
|
using Jackett.Common.Indexers;
|
||||||
|
using Jackett.Common.Indexers.Meta;
|
||||||
|
using Jackett.Common.Models;
|
||||||
|
using Jackett.Common.Models.DTO;
|
||||||
|
using Jackett.Common.Services.Interfaces;
|
||||||
|
using Jackett.Common.Utils;
|
||||||
|
using NLog;
|
||||||
|
|
||||||
|
namespace Jackett.Controllers
|
||||||
|
{
|
||||||
|
public class RequiresApiKeyAttribute : AuthorizationFilterAttribute
|
||||||
|
{
|
||||||
|
public override void OnAuthorization(HttpActionContext actionContext)
|
||||||
|
{
|
||||||
|
var validApiKey = Engine.ServerConfig.APIKey;
|
||||||
|
var queryParams = actionContext.Request.GetQueryNameValuePairs();
|
||||||
|
var queryApiKey = queryParams.Where(x => x.Key == "apikey" || x.Key == "passkey").Select(x => x.Value).FirstOrDefault();
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
if (Debugger.IsAttached)
|
||||||
|
return;
|
||||||
|
#endif
|
||||||
|
if (queryApiKey != validApiKey)
|
||||||
|
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RequiresConfiguredIndexerAttribute : ActionFilterAttribute
|
||||||
|
{
|
||||||
|
public override void OnActionExecuting(HttpActionContext actionContext)
|
||||||
|
{
|
||||||
|
var controller = actionContext.ControllerContext.Controller;
|
||||||
|
if (!(controller is IIndexerController))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var indexerController = controller as IIndexerController;
|
||||||
|
|
||||||
|
var parameters = actionContext.RequestContext.RouteData.Values;
|
||||||
|
|
||||||
|
if (!parameters.ContainsKey("indexerId"))
|
||||||
|
{
|
||||||
|
indexerController.CurrentIndexer = null;
|
||||||
|
actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Unauthorized, "Invalid parameter");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var indexerId = parameters["indexerId"] as string;
|
||||||
|
if (indexerId.IsNullOrEmptyOrWhitespace())
|
||||||
|
{
|
||||||
|
indexerController.CurrentIndexer = null;
|
||||||
|
actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Unauthorized, "Invalid parameter");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var indexerService = indexerController.IndexerService;
|
||||||
|
var indexer = indexerService.GetIndexer(indexerId);
|
||||||
|
|
||||||
|
if (indexer == null)
|
||||||
|
{
|
||||||
|
indexerController.CurrentIndexer = null;
|
||||||
|
actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Unauthorized, "Invalid parameter");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!indexer.IsConfigured)
|
||||||
|
{
|
||||||
|
indexerController.CurrentIndexer = null;
|
||||||
|
actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Unauthorized, "Indexer is not configured");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
indexerController.CurrentIndexer = indexer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RequiresValidQueryAttribute : RequiresConfiguredIndexerAttribute
|
||||||
|
{
|
||||||
|
public override void OnActionExecuting(HttpActionContext actionContext)
|
||||||
|
{
|
||||||
|
base.OnActionExecuting(actionContext);
|
||||||
|
if (actionContext.Response != null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var controller = actionContext.ControllerContext.Controller;
|
||||||
|
if (!(controller is IResultController))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var resultController = controller as IResultController;
|
||||||
|
|
||||||
|
var query = actionContext.ActionArguments.First().Value;
|
||||||
|
var queryType = query.GetType();
|
||||||
|
var converter = queryType.GetMethod("ToTorznabQuery", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public);
|
||||||
|
if (converter == null)
|
||||||
|
actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, "");
|
||||||
|
var converted = converter.Invoke(null, new object[] { query });
|
||||||
|
var torznabQuery = converted as TorznabQuery;
|
||||||
|
resultController.CurrentQuery = torznabQuery;
|
||||||
|
|
||||||
|
if (queryType == typeof(ApiSearch)) // Skip CanHandleQuery() check for manual search (CurrentIndexer isn't used during manul search)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!resultController.CurrentIndexer.CanHandleQuery(resultController.CurrentQuery))
|
||||||
|
actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, $"{resultController.CurrentIndexer.ID} does not support the requested query. Please check the capabilities (t=caps) and make sure the search mode and categories are supported.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class JsonResponseAttribute : ActionFilterAttribute
|
||||||
|
{
|
||||||
|
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
|
||||||
|
{
|
||||||
|
base.OnActionExecuted(actionExecutedContext);
|
||||||
|
|
||||||
|
if (actionExecutedContext.Exception != null)
|
||||||
|
throw new Exception("Error while executing request", actionExecutedContext.Exception);
|
||||||
|
|
||||||
|
var content = actionExecutedContext.Response.Content as ObjectContent;
|
||||||
|
actionExecutedContext.Response.Content = new JsonContent(content.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IResultController : IIndexerController
|
||||||
|
{
|
||||||
|
TorznabQuery CurrentQuery { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[AllowAnonymous]
|
||||||
|
[JackettAPINoCache]
|
||||||
|
[RoutePrefix("api/v2.0/indexers")]
|
||||||
|
[RequiresApiKey]
|
||||||
|
[RequiresValidQuery]
|
||||||
|
public class ResultsController : ApiController, IResultController
|
||||||
|
{
|
||||||
|
public IIndexerManagerService IndexerService { get; private set; }
|
||||||
|
public IIndexer CurrentIndexer { get; set; }
|
||||||
|
public TorznabQuery CurrentQuery { get; set; }
|
||||||
|
|
||||||
|
public ResultsController(IIndexerManagerService indexerManagerService, IServerService ss, ICacheService c, Logger logger)
|
||||||
|
{
|
||||||
|
IndexerService = indexerManagerService;
|
||||||
|
serverService = ss;
|
||||||
|
cacheService = c;
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<ManualSearchResult> Results([FromUri]ApiSearch request)
|
||||||
|
{
|
||||||
|
var manualResult = new ManualSearchResult();
|
||||||
|
var trackers = IndexerService.GetAllIndexers().Where(t => t.IsConfigured);
|
||||||
|
if (request.Tracker != null)
|
||||||
|
trackers = trackers.Where(t => request.Tracker.Contains(t.ID));
|
||||||
|
trackers = trackers.Where(t => t.CanHandleQuery(CurrentQuery));
|
||||||
|
|
||||||
|
var tasks = trackers.ToList().Select(t => t.ResultsForQuery(CurrentQuery)).ToList();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var aggregateTask = Task.WhenAll(tasks);
|
||||||
|
await aggregateTask;
|
||||||
|
}
|
||||||
|
catch (AggregateException aex)
|
||||||
|
{
|
||||||
|
foreach (var ex in aex.InnerExceptions)
|
||||||
|
{
|
||||||
|
logger.Error(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.Error(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
manualResult.Indexers = tasks.Select(t =>
|
||||||
|
{
|
||||||
|
var resultIndexer = new ManualSearchResultIndexer();
|
||||||
|
IIndexer indexer = null;
|
||||||
|
if (t.Status == TaskStatus.RanToCompletion)
|
||||||
|
{
|
||||||
|
resultIndexer.Status = ManualSearchResultIndexerStatus.OK;
|
||||||
|
resultIndexer.Results = t.Result.Releases.Count();
|
||||||
|
resultIndexer.Error = null;
|
||||||
|
indexer = t.Result.Indexer;
|
||||||
|
}
|
||||||
|
else if (t.Exception.InnerException is IndexerException)
|
||||||
|
{
|
||||||
|
resultIndexer.Status = ManualSearchResultIndexerStatus.Error;
|
||||||
|
resultIndexer.Results = 0;
|
||||||
|
resultIndexer.Error = ((IndexerException)t.Exception.InnerException).ToString();
|
||||||
|
indexer = ((IndexerException)t.Exception.InnerException).Indexer;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
resultIndexer.Status = ManualSearchResultIndexerStatus.Unknown;
|
||||||
|
resultIndexer.Results = 0;
|
||||||
|
resultIndexer.Error = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (indexer != null)
|
||||||
|
{
|
||||||
|
resultIndexer.ID = indexer.ID;
|
||||||
|
resultIndexer.Name = indexer.DisplayName;
|
||||||
|
}
|
||||||
|
return resultIndexer;
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
manualResult.Results = tasks.Where(t => t.Status == TaskStatus.RanToCompletion).Where(t => t.Result.Releases.Count() > 0).SelectMany(t =>
|
||||||
|
{
|
||||||
|
var searchResults = t.Result.Releases;
|
||||||
|
var indexer = t.Result.Indexer;
|
||||||
|
cacheService.CacheRssResults(indexer, searchResults);
|
||||||
|
|
||||||
|
return searchResults.Select(result =>
|
||||||
|
{
|
||||||
|
var item = AutoMapper.Mapper.Map<TrackerCacheResult>(result);
|
||||||
|
item.Tracker = indexer.DisplayName;
|
||||||
|
item.TrackerId = indexer.ID;
|
||||||
|
item.Peers = item.Peers - item.Seeders; // Use peers as leechers
|
||||||
|
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
}).OrderByDescending(d => d.PublishDate).ToList();
|
||||||
|
|
||||||
|
ConfigureCacheResults(manualResult.Results);
|
||||||
|
|
||||||
|
logger.Info(string.Format("Manual search for \"{0}\" on {1} with {2} results.", CurrentQuery.SanitizedSearchTerm, string.Join(", ", manualResult.Indexers.Select(i => i.ID)), manualResult.Results.Count()));
|
||||||
|
return manualResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IHttpActionResult> Torznab([FromUri]Common.Models.DTO.TorznabRequest request)
|
||||||
|
{
|
||||||
|
if (string.Equals(CurrentQuery.QueryType, "caps", StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
return ResponseMessage(new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(CurrentIndexer.TorznabCaps.ToXml(), Encoding.UTF8, "application/xml")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// indexers - returns a list of all included indexers (meta indexers only)
|
||||||
|
if (string.Equals(CurrentQuery.QueryType, "indexers", StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
if (!(CurrentIndexer is BaseMetaIndexer)) // shouldn't be needed because CanHandleQuery should return false
|
||||||
|
{
|
||||||
|
logger.Warn($"A search request with t=indexers from {Request.GetOwinContext().Request.RemoteIpAddress} was made but the indexer {CurrentIndexer.DisplayName} isn't a meta indexer.");
|
||||||
|
return GetErrorXML(203, "Function Not Available: this isn't a meta indexer");
|
||||||
|
}
|
||||||
|
var CurrentBaseMetaIndexer = (BaseMetaIndexer)CurrentIndexer;
|
||||||
|
var indexers = CurrentBaseMetaIndexer.Indexers;
|
||||||
|
if (string.Equals(request.configured, "true", StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
indexers = indexers.Where(i => i.IsConfigured);
|
||||||
|
else if (string.Equals(request.configured, "false", StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
indexers = indexers.Where(i => !i.IsConfigured);
|
||||||
|
|
||||||
|
var xdoc = new XDocument(
|
||||||
|
new XDeclaration("1.0", "UTF-8", null),
|
||||||
|
new XElement("indexers",
|
||||||
|
from i in indexers
|
||||||
|
select new XElement("indexer",
|
||||||
|
new XAttribute("id", i.ID),
|
||||||
|
new XAttribute("configured", i.IsConfigured),
|
||||||
|
new XElement("title", i.DisplayName),
|
||||||
|
new XElement("description", i.DisplayDescription),
|
||||||
|
new XElement("link", i.SiteLink),
|
||||||
|
new XElement("language", i.Language),
|
||||||
|
new XElement("type", i.Type),
|
||||||
|
i.TorznabCaps.GetXDocument().FirstNode
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return ResponseMessage(new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(xdoc.Declaration.ToString() + Environment.NewLine + xdoc.ToString(), Encoding.UTF8, "application/xml")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CurrentQuery.ImdbID != null)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(CurrentQuery.SearchTerm))
|
||||||
|
{
|
||||||
|
logger.Warn($"A search request from {Request.GetOwinContext().Request.RemoteIpAddress} was made containing q and imdbid.");
|
||||||
|
return GetErrorXML(201, "Incorrect parameter: please specify either imdbid or q");
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentQuery.ImdbID = ParseUtil.GetFullImdbID(CurrentQuery.ImdbID); // normalize ImdbID
|
||||||
|
if (CurrentQuery.ImdbID == null)
|
||||||
|
{
|
||||||
|
logger.Warn($"A search request from {Request.GetOwinContext().Request.RemoteIpAddress} was made with an invalid imdbid.");
|
||||||
|
return GetErrorXML(201, "Incorrect parameter: invalid imdbid format");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CurrentIndexer.TorznabCaps.SupportsImdbSearch)
|
||||||
|
{
|
||||||
|
logger.Warn($"A search request with imdbid from {Request.GetOwinContext().Request.RemoteIpAddress} was made but the indexer {CurrentIndexer.DisplayName} doesn't support it.");
|
||||||
|
return GetErrorXML(203, "Function Not Available: imdbid is not supported by this indexer");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await CurrentIndexer.ResultsForQuery(CurrentQuery);
|
||||||
|
|
||||||
|
// Some trackers do not support multiple category filtering so filter the releases that match manually.
|
||||||
|
int? newItemCount = null;
|
||||||
|
|
||||||
|
// Cache non query results
|
||||||
|
if (string.IsNullOrEmpty(CurrentQuery.SanitizedSearchTerm))
|
||||||
|
{
|
||||||
|
newItemCount = cacheService.GetNewItemCount(CurrentIndexer, result.Releases);
|
||||||
|
cacheService.CacheRssResults(CurrentIndexer, result.Releases);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log info
|
||||||
|
var logBuilder = new StringBuilder();
|
||||||
|
if (newItemCount != null)
|
||||||
|
{
|
||||||
|
logBuilder.AppendFormat("Found {0} ({1} new) releases from {2}", result.Releases.Count(), newItemCount, CurrentIndexer.DisplayName);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logBuilder.AppendFormat("Found {0} releases from {1}", result.Releases.Count(), CurrentIndexer.DisplayName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(CurrentQuery.SanitizedSearchTerm))
|
||||||
|
{
|
||||||
|
logBuilder.AppendFormat(" for: {0}", CurrentQuery.GetQueryString());
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info(logBuilder.ToString());
|
||||||
|
|
||||||
|
var serverUrl = serverService.GetServerUrl(Request);
|
||||||
|
var resultPage = new ResultPage(new ChannelInfo
|
||||||
|
{
|
||||||
|
Title = CurrentIndexer.DisplayName,
|
||||||
|
Description = CurrentIndexer.DisplayDescription,
|
||||||
|
Link = new Uri(CurrentIndexer.SiteLink),
|
||||||
|
ImageUrl = new Uri(serverUrl + "logos/" + CurrentIndexer.ID + ".png"),
|
||||||
|
ImageTitle = CurrentIndexer.DisplayName,
|
||||||
|
ImageLink = new Uri(CurrentIndexer.SiteLink),
|
||||||
|
ImageDescription = CurrentIndexer.DisplayName
|
||||||
|
});
|
||||||
|
|
||||||
|
var proxiedReleases = result.Releases.Select(r => AutoMapper.Mapper.Map<ReleaseInfo>(r)).Select(r =>
|
||||||
|
{
|
||||||
|
r.Link = serverService.ConvertToProxyLink(r.Link, serverUrl, r.Origin.ID, "dl", r.Title);
|
||||||
|
return r;
|
||||||
|
});
|
||||||
|
|
||||||
|
resultPage.Releases = proxiedReleases.ToList();
|
||||||
|
|
||||||
|
var xml = resultPage.ToXml(new Uri(serverUrl));
|
||||||
|
// Force the return as XML
|
||||||
|
return ResponseMessage(new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(xml, Encoding.UTF8, "application/rss+xml")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public IHttpActionResult GetErrorXML(int code, string description)
|
||||||
|
{
|
||||||
|
var xdoc = new XDocument(
|
||||||
|
new XDeclaration("1.0", "UTF-8", null),
|
||||||
|
new XElement("error",
|
||||||
|
new XAttribute("code", code.ToString()),
|
||||||
|
new XAttribute("description", description)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
var xml = xdoc.Declaration.ToString() + Environment.NewLine + xdoc.ToString();
|
||||||
|
|
||||||
|
return ResponseMessage(new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(xml, Encoding.UTF8, "application/xml")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[JsonResponse]
|
||||||
|
public async Task<TorrentPotatoResponse> Potato([FromUri]TorrentPotatoRequest request)
|
||||||
|
{
|
||||||
|
var result = await CurrentIndexer.ResultsForQuery(CurrentQuery);
|
||||||
|
|
||||||
|
// Cache non query results
|
||||||
|
if (string.IsNullOrEmpty(CurrentQuery.SanitizedSearchTerm))
|
||||||
|
cacheService.CacheRssResults(CurrentIndexer, result.Releases);
|
||||||
|
|
||||||
|
// Log info
|
||||||
|
if (string.IsNullOrWhiteSpace(CurrentQuery.SanitizedSearchTerm))
|
||||||
|
logger.Info($"Found {result.Releases.Count()} torrentpotato releases from {CurrentIndexer.DisplayName}");
|
||||||
|
else
|
||||||
|
logger.Info($"Found {result.Releases.Count()} torrentpotato releases from {CurrentIndexer.DisplayName} for: {CurrentQuery.GetQueryString()}");
|
||||||
|
|
||||||
|
var serverUrl = serverService.GetServerUrl(Request);
|
||||||
|
var potatoReleases = result.Releases.Where(r => r.Link != null || r.MagnetUri != null).Select(r =>
|
||||||
|
{
|
||||||
|
var release = AutoMapper.Mapper.Map<ReleaseInfo>(r);
|
||||||
|
release.Link = serverService.ConvertToProxyLink(release.Link, serverUrl, CurrentIndexer.ID, "dl", release.Title);
|
||||||
|
var item = new TorrentPotatoResponseItem()
|
||||||
|
{
|
||||||
|
release_name = release.Title + "[" + CurrentIndexer.DisplayName + "]", // Suffix the indexer so we can see which tracker we are using in CPS as it just says torrentpotato >.>
|
||||||
|
torrent_id = release.Guid.ToString(),
|
||||||
|
details_url = release.Comments.ToString(),
|
||||||
|
download_url = (release.Link != null ? release.Link.ToString() : release.MagnetUri.ToString()),
|
||||||
|
imdb_id = release.Imdb.HasValue ? "tt" + release.Imdb : null,
|
||||||
|
freeleech = (release.DownloadVolumeFactor == 0 ? true : false),
|
||||||
|
type = "movie",
|
||||||
|
size = (long)release.Size / (1024 * 1024), // This is in MB
|
||||||
|
leechers = (release.Peers ?? -1) - (release.Seeders ?? 0),
|
||||||
|
seeders = release.Seeders ?? -1,
|
||||||
|
publish_date = r.PublishDate == DateTime.MinValue ? null : release.PublishDate.ToUniversalTime().ToString("s")
|
||||||
|
};
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
|
||||||
|
var potatoResponse = new TorrentPotatoResponse()
|
||||||
|
{
|
||||||
|
results = potatoReleases.ToList()
|
||||||
|
};
|
||||||
|
|
||||||
|
return potatoResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ConfigureCacheResults(IEnumerable<TrackerCacheResult> results)
|
||||||
|
{
|
||||||
|
var serverUrl = serverService.GetServerUrl(Request);
|
||||||
|
foreach (var result in results)
|
||||||
|
{
|
||||||
|
var link = result.Link;
|
||||||
|
var file = StringUtil.MakeValidFileName(result.Title, '_', false);
|
||||||
|
result.Link = serverService.ConvertToProxyLink(link, serverUrl, result.TrackerId, "dl", file);
|
||||||
|
if (result.Link != null && result.Link.Scheme != "magnet" && !string.IsNullOrWhiteSpace(Engine.ServerConfig.BlackholeDir))
|
||||||
|
result.BlackholeLink = serverService.ConvertToProxyLink(link, serverUrl, result.TrackerId, "bh", file);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Logger logger;
|
||||||
|
private IServerService serverService;
|
||||||
|
private ICacheService cacheService;
|
||||||
|
}
|
||||||
|
}
|
202
src/Jackett.Server/Controllers/ServerConfigurationController.cs
Normal file
202
src/Jackett.Server/Controllers/ServerConfigurationController.cs
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Web.Http;
|
||||||
|
using Jackett.Common;
|
||||||
|
using Jackett.Common.Models;
|
||||||
|
using Jackett.Common.Models.Config;
|
||||||
|
using Jackett.Common.Services.Interfaces;
|
||||||
|
using Jackett.Common.Utils;
|
||||||
|
using Jackett.Utils;
|
||||||
|
using NLog;
|
||||||
|
|
||||||
|
namespace Jackett.Controllers
|
||||||
|
{
|
||||||
|
[RoutePrefix("api/v2.0/server")]
|
||||||
|
[JackettAuthorized]
|
||||||
|
[JackettAPINoCache]
|
||||||
|
public class ServerConfigurationController : ApiController
|
||||||
|
{
|
||||||
|
private readonly IConfigurationService configService;
|
||||||
|
private ServerConfig serverConfig;
|
||||||
|
private IServerService serverService;
|
||||||
|
private IProcessService processService;
|
||||||
|
private IIndexerManagerService indexerService;
|
||||||
|
private ISecuityService securityService;
|
||||||
|
private IUpdateService updater;
|
||||||
|
private ILogCacheService logCache;
|
||||||
|
private Logger logger;
|
||||||
|
|
||||||
|
public ServerConfigurationController(IConfigurationService c, IServerService s, IProcessService p, IIndexerManagerService i, ISecuityService ss, IUpdateService u, ILogCacheService lc, Logger l, ServerConfig sc)
|
||||||
|
{
|
||||||
|
configService = c;
|
||||||
|
serverConfig = sc;
|
||||||
|
serverService = s;
|
||||||
|
processService = p;
|
||||||
|
indexerService = i;
|
||||||
|
securityService = ss;
|
||||||
|
updater = u;
|
||||||
|
logCache = lc;
|
||||||
|
logger = l;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public void AdminPassword([FromBody]string password)
|
||||||
|
{
|
||||||
|
var oldPassword = serverConfig.AdminPassword;
|
||||||
|
if (string.IsNullOrEmpty(password))
|
||||||
|
password = null;
|
||||||
|
|
||||||
|
if (oldPassword != password)
|
||||||
|
{
|
||||||
|
serverConfig.AdminPassword = securityService.HashPassword(password);
|
||||||
|
configService.SaveConfig(serverConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public void Update()
|
||||||
|
{
|
||||||
|
updater.CheckForUpdatesNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public Common.Models.DTO.ServerConfig Config()
|
||||||
|
{
|
||||||
|
|
||||||
|
var dto = new Common.Models.DTO.ServerConfig(serverService.notices, serverConfig, configService.GetVersion());
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ActionName("Config")]
|
||||||
|
[HttpPost]
|
||||||
|
public void UpdateConfig([FromBody]Common.Models.DTO.ServerConfig config)
|
||||||
|
{
|
||||||
|
var originalPort = serverConfig.Port;
|
||||||
|
var originalAllowExternal = serverConfig.AllowExternal;
|
||||||
|
int port = config.port;
|
||||||
|
bool external = config.external;
|
||||||
|
string saveDir = config.blackholedir;
|
||||||
|
bool updateDisabled = config.updatedisabled;
|
||||||
|
bool preRelease = config.prerelease;
|
||||||
|
bool logging = config.logging;
|
||||||
|
string basePathOverride = config.basepathoverride;
|
||||||
|
if (basePathOverride != null)
|
||||||
|
{
|
||||||
|
basePathOverride = basePathOverride.TrimEnd('/');
|
||||||
|
if (!string.IsNullOrWhiteSpace(basePathOverride) && !basePathOverride.StartsWith("/"))
|
||||||
|
throw new Exception("The Base Path Override must start with a /");
|
||||||
|
}
|
||||||
|
|
||||||
|
string omdbApiKey = config.omdbkey;
|
||||||
|
|
||||||
|
serverConfig.UpdateDisabled = updateDisabled;
|
||||||
|
serverConfig.UpdatePrerelease = preRelease;
|
||||||
|
serverConfig.BasePathOverride = basePathOverride;
|
||||||
|
serverConfig.RuntimeSettings.BasePath = Engine.Server.BasePath();
|
||||||
|
configService.SaveConfig(serverConfig);
|
||||||
|
|
||||||
|
Engine.SetLogLevel(logging ? LogLevel.Debug : LogLevel.Info);
|
||||||
|
serverConfig.RuntimeSettings.TracingEnabled = logging;
|
||||||
|
|
||||||
|
if (omdbApiKey != serverConfig.OmdbApiKey)
|
||||||
|
{
|
||||||
|
serverConfig.OmdbApiKey = omdbApiKey;
|
||||||
|
configService.SaveConfig(serverConfig);
|
||||||
|
// HACK
|
||||||
|
indexerService.InitAggregateIndexer();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.proxy_type != serverConfig.ProxyType ||
|
||||||
|
config.proxy_url != serverConfig.ProxyUrl ||
|
||||||
|
config.proxy_port != serverConfig.ProxyPort ||
|
||||||
|
config.proxy_username != serverConfig.ProxyUsername ||
|
||||||
|
config.proxy_password != serverConfig.ProxyPassword)
|
||||||
|
{
|
||||||
|
if (config.proxy_port < 1 || config.proxy_port > 65535)
|
||||||
|
throw new Exception("The port you have selected is invalid, it must be below 65535.");
|
||||||
|
|
||||||
|
serverConfig.ProxyUrl = config.proxy_url;
|
||||||
|
serverConfig.ProxyType = config.proxy_type;
|
||||||
|
serverConfig.ProxyPort = config.proxy_port;
|
||||||
|
serverConfig.ProxyUsername = config.proxy_username;
|
||||||
|
serverConfig.ProxyPassword = config.proxy_password;
|
||||||
|
configService.SaveConfig(serverConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (port != serverConfig.Port || external != serverConfig.AllowExternal)
|
||||||
|
{
|
||||||
|
|
||||||
|
if (ServerUtil.RestrictedPorts.Contains(port))
|
||||||
|
throw new Exception("The port you have selected is restricted, try a different one.");
|
||||||
|
|
||||||
|
if (port < 1 || port > 65535)
|
||||||
|
throw new Exception("The port you have selected is invalid, it must be below 65535.");
|
||||||
|
|
||||||
|
// Save port to the config so it can be picked up by the if needed when running as admin below.
|
||||||
|
serverConfig.AllowExternal = external;
|
||||||
|
serverConfig.Port = port;
|
||||||
|
configService.SaveConfig(serverConfig);
|
||||||
|
|
||||||
|
// On Windows change the url reservations
|
||||||
|
if (System.Environment.OSVersion.Platform != PlatformID.Unix)
|
||||||
|
{
|
||||||
|
if (!ServerUtil.IsUserAdministrator())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
processService.StartProcessAndLog(System.Windows.Forms.Application.ExecutablePath, "--ReserveUrls", true);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
serverConfig.Port = originalPort;
|
||||||
|
serverConfig.AllowExternal = originalAllowExternal;
|
||||||
|
configService.SaveConfig(serverConfig);
|
||||||
|
|
||||||
|
throw new Exception("Failed to acquire admin permissions to reserve the new port.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
serverService.ReserveUrls(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(new Thread(() =>
|
||||||
|
{
|
||||||
|
Thread.Sleep(500);
|
||||||
|
serverService.Stop();
|
||||||
|
Engine.BuildContainer(serverConfig.RuntimeSettings, new WebApi2Module());
|
||||||
|
Engine.Server.Initalize();
|
||||||
|
Engine.Server.Start();
|
||||||
|
})).Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (saveDir != serverConfig.BlackholeDir)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(saveDir))
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(saveDir))
|
||||||
|
{
|
||||||
|
throw new Exception("Blackhole directory does not exist");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
serverConfig.BlackholeDir = saveDir;
|
||||||
|
configService.SaveConfig(serverConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
serverConfig.ConfigChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public List<CachedLog> Logs()
|
||||||
|
{
|
||||||
|
return logCache.Logs;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
91
src/Jackett.Server/Controllers/UIController.cs
Normal file
91
src/Jackett.Server/Controllers/UIController.cs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Web.Http;
|
||||||
|
using Jackett.Common.Models.Config;
|
||||||
|
using Jackett.Common.Services.Interfaces;
|
||||||
|
using Jackett.Utils;
|
||||||
|
using MimeMapping;
|
||||||
|
using NLog;
|
||||||
|
|
||||||
|
namespace Jackett.Controllers
|
||||||
|
{
|
||||||
|
[RoutePrefix("UI")]
|
||||||
|
[JackettAuthorized]
|
||||||
|
[JackettAPINoCache]
|
||||||
|
public class WebUIController : ApiController
|
||||||
|
{
|
||||||
|
private IConfigurationService config;
|
||||||
|
private ServerConfig serverConfig;
|
||||||
|
private ISecuityService securityService;
|
||||||
|
private Logger logger;
|
||||||
|
|
||||||
|
public WebUIController(IConfigurationService config, ISecuityService ss, ServerConfig s, Logger l)
|
||||||
|
{
|
||||||
|
this.config = config;
|
||||||
|
serverConfig = s;
|
||||||
|
securityService = ss;
|
||||||
|
logger = l;
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpResponseMessage GetFile(string path)
|
||||||
|
{
|
||||||
|
var result = new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
var mappedPath = Path.Combine(config.GetContentFolder(), path);
|
||||||
|
var stream = new FileStream(mappedPath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
|
result.Content = new StreamContent(stream);
|
||||||
|
result.Content.Headers.ContentType = new MediaTypeHeaderValue(MimeUtility.GetMimeMapping(mappedPath));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public IHttpActionResult Logout()
|
||||||
|
{
|
||||||
|
var ctx = Request.GetOwinContext();
|
||||||
|
var authManager = ctx.Authentication;
|
||||||
|
authManager.SignOut("ApplicationCookie");
|
||||||
|
return Redirect("UI/Dashboard");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[HttpPost]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<HttpResponseMessage> Dashboard()
|
||||||
|
{
|
||||||
|
if (Request.RequestUri.Query != null && Request.RequestUri.Query.Contains("logout"))
|
||||||
|
{
|
||||||
|
var file = GetFile("login.html");
|
||||||
|
securityService.Logout(file);
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (securityService.CheckAuthorised(Request))
|
||||||
|
{
|
||||||
|
return GetFile("index.html");
|
||||||
|
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var formData = await Request.Content.ReadAsFormDataAsync();
|
||||||
|
|
||||||
|
if (formData != null && securityService.HashPassword(formData["password"]) == serverConfig.AdminPassword)
|
||||||
|
{
|
||||||
|
var file = GetFile("index.html");
|
||||||
|
securityService.Login(file);
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return GetFile("login.html");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user