This commit is contained in:
kaso17
2018-07-02 14:44:43 +02:00
33 changed files with 3355 additions and 46 deletions

View File

@@ -23,8 +23,8 @@ DefaultDirName={pf}\{#MyAppName}
DefaultGroupName={#MyAppName}
DisableProgramGroupPage=yes
OutputBaseFilename={#MyOutputFilename}
SetupIconFile=src\Jackett.Console\jackett.ico
UninstallDisplayIcon={commonappdata}\Jackett\JackettConsole.exe
SetupIconFile=src\Jackett.Tray\jackett.ico
UninstallDisplayIcon={commonappdata}\Jackett\{#MyAppExeName}
VersionInfoVersion={#MyAppVersion}
UninstallDisplayName={#MyAppName}
Compression=lzma
@@ -47,16 +47,16 @@ Name: "{group}\{#MyAppName}"; Filename: "{commonappdata}\Jackett\{#MyAppExeName}
Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"
Name: "{commondesktop}\{#MyAppName}"; Filename: "{commonappdata}\Jackett\{#MyAppExeName}"; Tasks: desktopicon
[Run]
Filename: "{commonappdata}\Jackett\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
[Run]
Filename: "{commonappdata}\Jackett\JackettConsole.exe"; Parameters: "--Uninstall"; Flags: waituntilterminated runhidden;
Filename: "{commonappdata}\Jackett\JackettConsole.exe"; Parameters: "--ReserveUrls"; Flags: waituntilterminated runhidden;
Filename: "{commonappdata}\Jackett\JackettConsole.exe"; Parameters: "--Install"; Flags: waituntilterminated runhidden; Tasks: windowsService
Filename: "{commonappdata}\Jackett\JackettConsole.exe"; Parameters: "--Start"; Flags: waituntilterminated runhidden; Tasks: windowsService
Filename: "{commonappdata}\Jackett\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
[UninstallRun]
Filename: "{commonappdata}\Jackett\JackettConsole.exe"; Parameters: "--Uninstall"; Flags: waituntilterminated skipifdoesntexist runhidden
Filename: "{sys}\taskkill.exe"; Parameters: "/f /im {#MyAppExeName}"; Flags: waituntilterminated skipifdoesntexist runhidden
Filename: "{sys}\taskkill.exe"; Parameters: "/f /im JackettConsole.exe"; Flags: waituntilterminated skipifdoesntexist runhidden

View File

@@ -6,7 +6,7 @@
//////////////////////////////////////////////////////////////////////
var target = Argument("target", "Default");
var configuration = Argument("configuration", "Release");
var configuration = Argument("configuration", "Debug");
//////////////////////////////////////////////////////////////////////
// PREPARATION
@@ -36,8 +36,8 @@ Task("Clean")
.IsDependentOn("Info")
.Does(() =>
{
CleanDirectories("./src/**/obj" + configuration);
CleanDirectories("./src/**/bin" + configuration);
CleanDirectories("./src/**/obj");
CleanDirectories("./src/**/bin");
CleanDirectories("./BuildOutput");
CleanDirectories("./" + artifactsDirName);
CleanDirectories("./" + testResultsDirName);
@@ -149,15 +149,8 @@ Task("Package-Files-Full-Framework-Mono")
.IsDependentOn("Check-Packaging-Platform")
.Does(() =>
{
var cygMonoBuildPath = RelativeWinPathToCygPath(monoBuildFullFramework);
var tarFileName = "Jackett.Binaries.Mono.tar";
var tarArguments = @"-cvf " + cygMonoBuildPath + "/" + tarFileName + " -C " + cygMonoBuildPath + " Jackett --mode ='755'";
var gzipArguments = @"-k " + cygMonoBuildPath + "/" + tarFileName;
RunCygwinCommand("Tar", tarArguments);
RunCygwinCommand("Gzip", gzipArguments);
MoveFile($"{monoBuildFullFramework}/{tarFileName}.gz", $"./{artifactsDirName}/{tarFileName}.gz");
Gzip(monoBuildFullFramework, $"./{artifactsDirName}", "Jackett", "Jackett.Binaries.Mono.tar.gz");
Information(@"Full Framework Mono Binaries Gzip Completed");
});
Task("Package-Full-Framework")
@@ -169,8 +162,117 @@ Task("Package-Full-Framework")
Information("Full Framework Packaging Completed");
});
Task("Kestrel-Full-Framework")
.IsDependentOn("Package-Full-Framework")
.Does(() =>
{
CleanDirectories("./src/**/obj");
CleanDirectories("./src/**/bin");
//Patch csproj to net461 until off Owin (net452 can't handle a nestandard library)
var trayCsproj = File("./src/Jackett.Tray/Jackett.Tray.csproj");
XmlPoke(trayCsproj, "*[name()='Project']/*[name()='PropertyGroup']/*[name()='TargetFrameworkVersion']", "v4.6.1");
var serviceCsproj = File("./src/Jackett.Service/Jackett.Service.csproj");
XmlPoke(serviceCsproj, "*[name()='Project']/*[name()='PropertyGroup']/*[name()='TargetFrameworkVersion']", "v4.6.1");
NuGetRestore("./src/Jackett.sln");
var buildSettings = new MSBuildSettings()
.SetConfiguration(configuration)
.UseToolVersion(MSBuildToolVersion.VS2017);
MSBuild("./src/Jackett.sln", buildSettings);
});
Task("Experimental-Kestrel-Windows-Full-Framework")
.IsDependentOn("Kestrel-Full-Framework")
.Does(() =>
{
string serverProjectPath = "./src/Jackett.Server/Jackett.Server.csproj";
string buildOutputPath = "./BuildOutput/Experimental/net461/win7-x86/Jackett";
DotNetCorePublish(serverProjectPath, "net461", "win7-x86");
CopyFiles("./src/Jackett.Service/bin/" + configuration + "/JackettService.*", buildOutputPath);
CopyFiles("./src/Jackett.Tray/bin/" + configuration + "/JackettTray.*", buildOutputPath);
CopyFiles("./src/Jackett.Updater/bin/" + configuration + "/net461" + "/JackettUpdater.*", buildOutputPath); //builds against multiple frameworks
Zip("./BuildOutput/Experimental/net461/win7-x86", $"./{artifactsDirName}/Experimental.Jackett.Binaries.Windows.zip");
//InnoSetup
string sourceFolder = MakeAbsolute(Directory(buildOutputPath)).ToString();
InnoSetupSettings settings = new InnoSetupSettings();
settings.OutputDirectory = workingDir + "/" + artifactsDirName;
settings.Defines = new Dictionary<string, string>
{
{ "MyFileForVersion", sourceFolder + "/Jackett.Common.dll" },
{ "MySourceFolder", sourceFolder },
{ "MyOutputFilename", "Experimental.Jackett.Installer.Windows" },
};
InnoSetup("./Installer.iss", settings);
});
Task("Experimental-Kestrel-Mono-Full-Framework")
.IsDependentOn("Kestrel-Full-Framework")
.Does(() =>
{
string serverProjectPath = "./src/Jackett.Server/Jackett.Server.csproj";
string buildOutputPath = "./BuildOutput/Experimental/net461/linux-x64/Jackett";
DotNetCorePublish(serverProjectPath, "net461", "linux-x64");
CopyFiles("./src/Jackett.Updater/bin/" + configuration + "/net461" + "/JackettUpdater.*", buildOutputPath); //builds against multiple frameworks
//There is an issue with Mono 5.8 (fixed in Mono 5.12) where its expecting to use its own patched version of System.Net.Http.dll, instead of the version supplied in folder
//https://github.com/dotnet/corefx/issues/19914
//https://bugzilla.xamarin.com/show_bug.cgi?id=60315
//The workaround is to delete System.Net.Http.dll and patch the .exe.config file
DeleteFile(buildOutputPath + "/System.Net.Http.dll");
var configFile = File(buildOutputPath + "/JackettConsole.exe.config");
XmlPoke(configFile, "configuration/runtime/*[name()='assemblyBinding']/*[name()='dependentAssembly']/*[name()='assemblyIdentity'][@name='System.Net.Http']/../*[name()='bindingRedirect']/@newVersion", "4.0.0.0");
Gzip("./BuildOutput/Experimental/net461/linux-x64", $"./{artifactsDirName}", "Jackett", "Experimental.Jackett.Binaries.Mono.tar.gz");
});
Task("Experimental-DotNetCore")
.IsDependentOn("Kestrel-Full-Framework")
.Does(() =>
{
string serverProjectPath = "./src/Jackett.Server/Jackett.Server.csproj";
DotNetCorePublish(serverProjectPath, "netcoreapp2.1", "win-x86");
DotNetCorePublish(serverProjectPath, "netcoreapp2.1", "linux-x64");
DotNetCorePublish(serverProjectPath, "netcoreapp2.1", "osx-x64");
Zip("./BuildOutput/Experimental/netcoreapp2.1/win-x86", $"./{artifactsDirName}/Experimental.netcoreapp.win-x86.zip");
Zip("./BuildOutput/Experimental/netcoreapp2.1/osx-x64", $"./{artifactsDirName}/Experimental.netcoreapp.osx-x64.zip");
Gzip("./BuildOutput/Experimental/netcoreapp2.1/linux-x64", $"./{artifactsDirName}", "Jackett", "Experimental.netcoreapp.linux-x64.tar.gz");
});
Task("Experimental")
.IsDependentOn("Experimental-Kestrel-Windows-Full-Framework")
.IsDependentOn("Experimental-Kestrel-Mono-Full-Framework")
//.IsDependentOn("Experimental-DotNetCore")
.Does(() =>
{
//Unpatch csproj because it's annoying in source control (remove once off Owin)
var trayCsproj = File("./src/Jackett.Tray/Jackett.Tray.csproj");
XmlPoke(trayCsproj, "*[name()='Project']/*[name()='PropertyGroup']/*[name()='TargetFrameworkVersion']", "v4.5.2");
var serviceCsproj = File("./src/Jackett.Service/Jackett.Service.csproj");
XmlPoke(serviceCsproj, "*[name()='Project']/*[name()='PropertyGroup']/*[name()='TargetFrameworkVersion']", "v4.5.2");
Information("Experimental builds completed");
});
Task("Appveyor-Push-Artifacts")
.IsDependentOn("Package-Full-Framework")
.IsDependentOn("Experimental")
.Does(() =>
{
if (AppVeyor.IsRunningOnAppVeyor)
@@ -276,10 +378,35 @@ private void RunCygwinCommand(string utility, string utilityArguments)
private string RelativeWinPathToCygPath(string relativePath)
{
var cygdriveBase = "/cygdrive/" + workingDir.ToString().Replace(":", "").Replace("\\", "/");
var cygPath = cygdriveBase + relativePath.Replace(".", "");
var cygPath = cygdriveBase + relativePath.TrimStart('.');
return cygPath;
}
private void Gzip(string sourceFolder, string outputDirectory, string tarCdirectoryOption, string outputFileName)
{
var cygSourcePath = RelativeWinPathToCygPath(sourceFolder);
var tarFileName = outputFileName.Remove(outputFileName.Length - 3, 3);
var tarArguments = @"-cvf " + cygSourcePath + "/" + tarFileName + " -C " + cygSourcePath + $" {tarCdirectoryOption} --mode ='755'";
var gzipArguments = @"-k " + cygSourcePath + "/" + tarFileName;
RunCygwinCommand("Tar", tarArguments);
RunCygwinCommand("Gzip", gzipArguments);
MoveFile($"{sourceFolder}/{tarFileName}.gz", $"{outputDirectory}/{tarFileName}.gz");
}
private void DotNetCorePublish(string projectPath, string framework, string runtime)
{
var settings = new DotNetCorePublishSettings
{
Framework = framework,
Runtime = runtime,
OutputDirectory = $"./BuildOutput/Experimental/{framework}/{runtime}/Jackett"
};
DotNetCorePublish(projectPath, settings);
}
//////////////////////////////////////////////////////////////////////
// TASK TARGETS
//////////////////////////////////////////////////////////////////////

View File

@@ -13,7 +13,6 @@ using Jackett.Common.Services.Interfaces;
using Jackett.Common.Utils.Clients;
using NLog;
using NLog.Config;
using NLog.LayoutRenderers;
using NLog.Targets;
namespace Jackett.Common
@@ -179,7 +178,7 @@ namespace Jackett.Common
var logFileName = settings.CustomLogFileName ?? "log.txt";
var logLevel = settings.TracingEnabled ? LogLevel.Debug : LogLevel.Info;
// Add custom date time format renderer as the default is too long
ConfigurationItemFactory.Default.LayoutRenderers.RegisterDefinition("simpledatetime", typeof(SimpleDateTimeRenderer));
ConfigurationItemFactory.Default.LayoutRenderers.RegisterDefinition("simpledatetime", typeof(Utils.LoggingSetup.SimpleDateTimeRenderer));
var logConfig = new LoggingConfiguration();
var logFile = new FileTarget();
@@ -265,13 +264,4 @@ namespace Jackett.Common
ConfigService.SaveConfig(ServerConfig);
}
}
[LayoutRenderer("simpledatetime")]
public class SimpleDateTimeRenderer : LayoutRenderer
{
protected override void Append(StringBuilder builder, LogEventInfo logEvent)
{
builder.Append(DateTime.Now.ToString("MM-dd HH:mm:ss"));
}
}
}

View File

@@ -28,6 +28,7 @@ namespace Jackett.Common.Indexers
class NewpctRelease : ReleaseInfo
{
public string SerieName;
public int? Season;
public int? Episode;
public int? EpisodeTo;
@@ -364,7 +365,7 @@ namespace Jackett.Common.Indexers
Match match = _titleListRegex.Match(title);
if (match.Success)
{
string name = match.Groups[1].Value.Trim(' ', '-');
result.SerieName = match.Groups[1].Value.Trim(' ', '-');
result.Season = int.Parse(match.Groups[4].Success ? match.Groups[4].Value.Trim() : "1");
result.Episode = int.Parse(match.Groups[7].Value.Trim().PadLeft(2, '0'));
result.EpisodeTo = match.Groups[10].Success ? (int?)int.Parse(match.Groups[10].Value.Trim()) : null;
@@ -376,7 +377,7 @@ namespace Jackett.Common.Indexers
string episodeToText = result.EpisodeTo.HasValue ? "_" + seasonText + result.EpisodeTo.ToString().PadLeft(2, '0') : "";
result.Title = string.Format("{0} - Temporada {1} [{2}][Cap.{3}{4}][{5}]",
name, seasonText, quality, episodeText, episodeToText, audioQuality);
result.SerieName, seasonText, quality, episodeText, episodeToText, audioQuality);
}
else
{
@@ -412,7 +413,22 @@ namespace Jackett.Common.Indexers
result.Seeders = 1;
result.Peers = 1;
result.Title = FixedTitle(result, quality);
return result;
}
private string FixedTitle(NewpctRelease release, string quality)
{
var titleParts = new List<string>();
titleParts.Add(release.SerieName);
titleParts.Add("S" + release.Season.ToString().PadLeft(2, '0') + "E" + release.Episode.ToString().PadLeft(2, '0'));
titleParts.Add(quality);
if (release.Title.ToLower().Contains("esp") || release.Title.ToLower().Contains("cast"))
{
titleParts.Add("Spanish");
}
return String.Join(".", titleParts);
}
}
}

View File

@@ -53,6 +53,9 @@ namespace Jackett.Common.Plumbing
// Register the best web client for the platform or the override
switch (_runtimeSettings.ClientOverride)
{
case "httpclientnetcore":
// do nothing, registered by the netcore app
break;
case "httpclient":
RegisterWebClient<HttpWebClient>(builder);
break;

View File

@@ -324,20 +324,21 @@ namespace Jackett.Common.Services
startInfo.Arguments += " --StartTray";
}
if (isWindows)
{
lockService.Signal();
logger.Info("Signal sent to lock service");
Thread.Sleep(2000);
}
logger.Info($"Starting updater: {startInfo.FileName} {startInfo.Arguments}");
var procInfo = Process.Start(startInfo);
logger.Info($"Updater started process id: {procInfo.Id}");
if (!NoRestart)
{
if (isWindows)
{
logger.Info("Signal sent to lock service");
lockService.Signal();
Thread.Sleep(2000);
}
logger.Info("Exiting Jackett..");
//TODO: Remove once off Owin
if (EnvironmentUtil.IsRunningLegacyOwin)
{

View File

@@ -1,8 +1,12 @@
using Jackett.Common.Models.Config;
using Jackett.Common.Services;
using NLog;
using NLog.Config;
using NLog.LayoutRenderers;
using NLog.Targets;
using System;
using System.IO;
using System.Text;
namespace Jackett.Common.Utils
{
@@ -45,5 +49,14 @@ namespace Jackett.Common.Utils
return logConfig;
}
[LayoutRenderer("simpledatetime")]
public class SimpleDateTimeRenderer : LayoutRenderer
{
protected override void Append(StringBuilder builder, LogEventInfo logEvent)
{
builder.Append(DateTime.Now.ToString("MM-dd HH:mm:ss"));
}
}
}
}

View File

@@ -0,0 +1,98 @@
using Jackett.Common.Models.Config;
using Jackett.Common.Services.Interfaces;
using Jackett.Common.Utils;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
using NLog;
using System;
using System.IO;
using System.Net;
using System.Threading.Tasks;
namespace Jackett.Server.Controllers
{
[AllowAnonymous]
[ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)]
[Route("bh/{indexerID}")]
public class BlackholeController : Controller
{
private Logger logger;
private IIndexerManagerService indexerService;
private readonly ServerConfig serverConfig;
private IProtectionService protectionService;
public BlackholeController(IIndexerManagerService i, Logger l, ServerConfig config, IProtectionService ps)
{
logger = l;
indexerService = i;
serverConfig = config;
protectionService = ps;
}
[HttpGet]
public async Task<IActionResult> 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 = WebUtility.UrlDecode(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
System.IO.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);
}
}
}

View File

@@ -0,0 +1,96 @@
using BencodeNET.Parsing;
using Jackett.Common.Models.Config;
using Jackett.Common.Services.Interfaces;
using Jackett.Common.Utils;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.WebUtilities;
using NLog;
using System;
using System.Text;
using System.Threading.Tasks;
namespace Jackett.Server.Controllers
{
[AllowAnonymous]
[ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)]
[Route("dl/{indexerID}")]
public class DownloadController : Controller
{
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<IActionResult> 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 Forbid("This indexer is not configured.");
}
path = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(path));
path = protectionService.UnProtect(path);
if (config.APIKey != jackett_apikey)
return 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);
return Redirect(new Uri(magneturi).ToString());
}
// This will fix torrents where the keys are not sorted, and thereby not supported by Sonarr.
byte[] sortedDownloadBytes = null;
try
{
var parser = new BencodeParser();
var torrentDictionary = parser.Parse(downloadBytes);
sortedDownloadBytes = torrentDictionary.EncodeAsBytes();
}
catch (Exception e)
{
var content = indexer.Encoding.GetString(downloadBytes);
logger.Error(content);
throw new Exception("BencodeParser failed", e);
}
string fileName = StringUtil.MakeValidFileName(file, '_', false) + ".torrent"; // call MakeValidFileName again to avoid any kind of injection attack
return File(sortedDownloadBytes, "application/x-bittorrent", fileName);
}
catch (Exception e)
{
logger.Error(e, "Error downloading " + indexerID + " " + path);
return NotFound();
}
}
}
}

View File

@@ -0,0 +1,178 @@
using Jackett.Common.Indexers;
using Jackett.Common.Models;
using Jackett.Common.Services.Interfaces;
using Jackett.Common.Utils;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NLog;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Jackett.Server.Controllers
{
public interface IIndexerController
{
IIndexerManagerService IndexerService { get; }
IIndexer CurrentIndexer { get; set; }
}
public class RequiresIndexer : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
var controller = context.Controller;
if (!(controller is IIndexerController))
return;
var indexerController = controller as IIndexerController;
var parameters = context.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;
}
public void OnActionExecuted(ActionExecutedContext context)
{
// do something after the action executes
}
}
[Route("api/v2.0/indexers")]
[ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)]
public class IndexerApiController : Controller, IIndexerController
{
public IIndexerManagerService IndexerService { get; private set; }
public IIndexer CurrentIndexer { get; set; }
private Logger logger;
private IServerService serverService;
private ICacheService cacheService;
public IndexerApiController(IIndexerManagerService indexerManagerService, IServerService ss, ICacheService c, Logger logger)
{
IndexerService = indexerManagerService;
serverService = ss;
cacheService = c;
this.logger = logger;
}
[HttpGet]
[TypeFilter(typeof(RequiresIndexer))]
[Route("{indexerId?}/Config")]
public async Task<IActionResult> Config()
{
var config = await CurrentIndexer.GetConfigurationForSetup();
return Ok(config.ToJson(null));
}
[HttpPost]
[Route("{indexerId?}/Config")]
[TypeFilter(typeof(RequiresIndexer))]
public async Task<IActionResult> 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);
}
return new NoContentResult();
}
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]
[Route("{indexerid}/[action]")]
[TypeFilter(typeof(RequiresIndexer))]
public async Task<IActionResult> Test()
{
JToken jsonReply = new JObject();
try
{
await IndexerService.TestIndexer(CurrentIndexer.ID);
CurrentIndexer.LastError = null;
return NoContent();
}
catch (Exception ex)
{
var msg = ex.Message;
if (ex.InnerException != null)
msg += ": " + ex.InnerException.Message;
if (CurrentIndexer != null)
CurrentIndexer.LastError = msg;
throw;
}
}
[HttpDelete]
[TypeFilter(typeof(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(serverService.GetBlackholeDirectory()))
result.BlackholeLink = serverService.ConvertToProxyLink(link, serverUrl, result.TrackerId, "bh", file);
}
}
}
}

View File

@@ -0,0 +1,544 @@
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 Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Routing;
using NLog;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
namespace Jackett.Server.Controllers
{
public class RequiresApiKey : IActionFilter
{
public IServerService serverService;
public RequiresApiKey(IServerService ss)
{
serverService = ss;
}
public void OnActionExecuting(ActionExecutingContext context)
{
var validApiKey = serverService.GetApiKey();
var queryParams = context.HttpContext.Request.Query;
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)
{
context.Result = ResultsController.GetErrorActionResult(context.RouteData, HttpStatusCode.Unauthorized, 100, "Invalid API Key");
}
}
public void OnActionExecuted(ActionExecutedContext context)
{
// do something after the action executes
}
}
public class RequiresConfiguredIndexer : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
var controller = context.Controller;
if (!(controller is IIndexerController))
return;
var indexerController = controller as IIndexerController;
var parameters = context.RouteData.Values;
if (!parameters.ContainsKey("indexerId"))
{
indexerController.CurrentIndexer = null;
context.Result = ResultsController.GetErrorActionResult(context.RouteData, HttpStatusCode.NotFound, 200, "Indexer is not specified");
return;
}
var indexerId = parameters["indexerId"] as string;
if (indexerId.IsNullOrEmptyOrWhitespace())
{
indexerController.CurrentIndexer = null;
context.Result = ResultsController.GetErrorActionResult(context.RouteData, HttpStatusCode.NotFound, 201, "Indexer is not specified (empty value)");
return;
}
var indexerService = indexerController.IndexerService;
var indexer = indexerService.GetIndexer(indexerId);
if (indexer == null)
{
indexerController.CurrentIndexer = null;
context.Result = ResultsController.GetErrorActionResult(context.RouteData, HttpStatusCode.NotFound, 201, "Indexer is not supported");
return;
}
if (!indexer.IsConfigured)
{
indexerController.CurrentIndexer = null;
context.Result = ResultsController.GetErrorActionResult(context.RouteData, HttpStatusCode.NotFound, 201, "Indexer is not configured");
return;
}
indexerController.CurrentIndexer = indexer;
}
public void OnActionExecuted(ActionExecutedContext context)
{
// do something after the action executes
}
}
public class RequiresValidQuery : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
//TODO: Not sure what this is meant to do
//if (context.HttpContext.Response != null)
// return;
var controller = context.Controller;
if (!(controller is IResultController))
{
return;
}
var resultController = controller as IResultController;
var query = context.ActionArguments.First().Value;
var queryType = query.GetType();
var converter = queryType.GetMethod("ToTorznabQuery", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public);
if (converter == null)
{
context.Result = ResultsController.GetErrorActionResult(context.RouteData, HttpStatusCode.BadRequest, 900, "ToTorznabQuery() not found");
}
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))
{
context.Result = ResultsController.GetErrorActionResult(context.RouteData, HttpStatusCode.BadRequest, 201, $"{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 void OnActionExecuted(ActionExecutedContext context)
{
// do something after the action executes
}
}
public interface IResultController : IIndexerController
{
TorznabQuery CurrentQuery { get; set; }
}
[AllowAnonymous]
[ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)]
[Route("api/v2.0/indexers/{indexerId}/results")]
[TypeFilter(typeof(RequiresApiKey))]
[TypeFilter(typeof(RequiresConfiguredIndexer))]
[TypeFilter(typeof(RequiresValidQuery))]
public class ResultsController : Controller, IResultController
{
public IIndexerManagerService IndexerService { get; private set; }
public IIndexer CurrentIndexer { get; set; }
public TorznabQuery CurrentQuery { get; set; }
private Logger logger;
private IServerService serverService;
private ICacheService cacheService;
private Common.Models.Config.ServerConfig serverConfig;
public ResultsController(IIndexerManagerService indexerManagerService, IServerService ss, ICacheService c, Logger logger, Common.Models.Config.ServerConfig sConfig)
{
IndexerService = indexerManagerService;
serverService = ss;
cacheService = c;
this.logger = logger;
serverConfig = sConfig;
}
[Route("")]
[HttpGet]
public async Task<IActionResult> Results([FromQuery] ApiSearch requestt)
{
//TODO: Better way to parse querystring
ApiSearch request = new ApiSearch();
foreach (var t in Request.Query)
{
if (t.Key == "Tracker[]")
{
request.Tracker = t.Value.ToString().Split(',');
}
if (t.Key == "Category[]")
{
request.Category = t.Value.ToString().Split(',').Select(Int32.Parse).ToArray();
}
if (t.Key == "query")
{
request.Query = t.Value.ToString();
}
}
var manualResult = new ManualSearchResult();
var trackers = IndexerService.GetAllIndexers().ToList().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.Any()).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 Json(manualResult);
}
[Route("[action]/{ignored?}")]
[HttpGet]
public async Task<IActionResult> Torznab([FromQuery]TorznabRequest request)
{
if (string.Equals(CurrentQuery.QueryType, "caps", StringComparison.InvariantCultureIgnoreCase))
{
return Content(CurrentIndexer.TorznabCaps.ToXml(), "application/rss+xml", Encoding.UTF8);
}
// 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.HttpContext.Connection.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 Content(xdoc.Declaration.ToString() + Environment.NewLine + xdoc.ToString(), "application/xml", Encoding.UTF8);
}
if (CurrentQuery.ImdbID != null)
{
if (!string.IsNullOrEmpty(CurrentQuery.SearchTerm))
{
logger.Warn($"A search request from {Request.HttpContext.Connection.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.HttpContext.Connection.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.HttpContext.Connection.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");
}
}
try
{
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 Content(xml, "application/rss+xml", Encoding.UTF8);
}
catch (Exception ex)
{
logger.Error(ex);
return GetErrorXML(900, ex.ToString());
}
}
[Route("[action]/{ignored?}")]
public IActionResult GetErrorXML(int code, string description)
{
return Content(CreateErrorXML(code, description), "application/xml", Encoding.UTF8);
}
public static string CreateErrorXML(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)
)
);
return xdoc.Declaration + Environment.NewLine + xdoc;
}
public static IActionResult GetErrorActionResult(RouteData routeData, HttpStatusCode status, int torznabCode, string description)
{
bool isTorznab = routeData.Values["action"].ToString().Equals("torznab", StringComparison.OrdinalIgnoreCase);
if (isTorznab)
{
ContentResult contentResult = new ContentResult
{
Content = CreateErrorXML(torznabCode, description),
ContentType = "application/xml",
StatusCode = 200
};
return contentResult;
}
else
{
switch (status)
{
case HttpStatusCode.Unauthorized:
return new UnauthorizedResult();
case HttpStatusCode.NotFound:
return new NotFoundObjectResult(description);
case HttpStatusCode.BadRequest:
return new BadRequestObjectResult(description);
default:
return new ContentResult
{
Content = description,
StatusCode = (int)status
};
}
}
}
[Route("[action]/{ignored?}")]
[HttpGet]
public async Task<TorrentPotatoResponse> Potato([FromQuery]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;
}
[Route("[action]/{ignored?}")]
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 (!string.IsNullOrWhiteSpace(serverConfig.BlackholeDir))
{
if (result.Link != null)
result.BlackholeLink = serverService.ConvertToProxyLink(link, serverUrl, result.TrackerId, "bh", file);
else if (result.MagnetUri != null)
result.BlackholeLink = serverService.ConvertToProxyLink(result.MagnetUri, serverUrl, result.TrackerId, "bh", file);
}
}
}
}
}

View File

@@ -0,0 +1,194 @@
using Jackett.Common.Models;
using Jackett.Common.Models.Config;
using Jackett.Common.Services.Interfaces;
using Jackett.Common.Utils;
using Microsoft.AspNetCore.Mvc;
using NLog;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
namespace Jackett.Server.Controllers
{
[Route("api/v2.0/server/[action]")]
[ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)]
public class ServerConfigurationController : Controller
{
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 IActionResult 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);
}
return new NoContentResult();
}
[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 IActionResult 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 = serverService.BasePath();
configService.SaveConfig(serverConfig);
Helper.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
{
var consoleExePath = System.Reflection.Assembly.GetExecutingAssembly().CodeBase.Replace(".dll", ".exe");
processService.StartProcessAndLog(consoleExePath, "--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);
}
}
Thread.Sleep(500);
Helper.RestartWebHost();
}
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();
return Json(serverConfig);
}
[HttpGet]
public List<CachedLog> Logs()
{
return logCache.Logs;
}
}
}

View File

@@ -0,0 +1,105 @@
using Jackett.Common.Models.Config;
using Jackett.Common.Services.Interfaces;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NLog;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
namespace Jackett.Server.Controllers
{
[Route("UI/[action]")]
[ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)]
public class WebUIController : Controller
{
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;
}
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> Login()
{
if (string.IsNullOrEmpty(serverConfig.AdminPassword))
{
await MakeUserAuthenticated();
}
if (User.Identity.IsAuthenticated)
{
return Redirect("Dashboard");
}
return new PhysicalFileResult(config.GetContentFolder() + "/login.html", "text/html"); ;
}
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return Redirect("Login");
}
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> Dashboard([FromForm] string password)
{
if (password != null && securityService.HashPassword(password) == serverConfig.AdminPassword)
{
await MakeUserAuthenticated();
}
return Redirect("Dashboard");
}
[HttpGet]
public IActionResult Dashboard()
{
bool logout = HttpContext.Request.Query.Where(x => String.Equals(x.Key, "logout", StringComparison.OrdinalIgnoreCase)
&& String.Equals(x.Value, "true", StringComparison.OrdinalIgnoreCase)).Any();
if (logout)
{
return Redirect("Logout");
}
return new PhysicalFileResult(config.GetContentFolder() + "/index.html", "text/html");
}
//TODO: Move this to security service once off Mono
private async Task MakeUserAuthenticated()
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, "Jackett", ClaimValueTypes.String)
};
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
new AuthenticationProperties
{
ExpiresUtc = DateTime.UtcNow.AddMinutes(20),
IsPersistent = false,
AllowRefresh = true
});
}
}
}

View File

@@ -0,0 +1,160 @@
using Autofac;
using AutoMapper;
using Jackett.Common.Models;
using Jackett.Common.Models.Config;
using Jackett.Common.Services.Interfaces;
using Jackett.Common.Utils.Clients;
using Microsoft.AspNetCore.Hosting;
using NLog;
using System.Linq;
using System.Text;
namespace Jackett.Server
{
public static class Helper
{
public static IContainer ApplicationContainer { get; set; }
public static IApplicationLifetime applicationLifetime;
private static bool _automapperInitialised = false;
public static void Initialize()
{
if (_automapperInitialised == false)
{
//Automapper only likes being initialized once per app domain.
//Since we can restart Jackett from the command line it's possible that we'll build the container more than once. (tests do this too)
InitAutomapper();
_automapperInitialised = true;
}
//Load the indexers
ServerService.Initalize();
//Kicks off the update checker
ServerService.Start();
}
public static void RestartWebHost()
{
Logger.Info("Restart of the web application host (not process) initiated");
Program.isWebHostRestart = true;
applicationLifetime.StopApplication();
}
public static void StopWebHost()
{
Logger.Info("Jackett is being stopped");
applicationLifetime.StopApplication();
}
public static IConfigurationService ConfigService
{
get
{
return ApplicationContainer.Resolve<IConfigurationService>();
}
}
public static IServerService ServerService
{
get
{
return ApplicationContainer.Resolve<IServerService>();
}
}
public static IServiceConfigService ServiceConfigService
{
get
{
return ApplicationContainer.Resolve<IServiceConfigService>();
}
}
public static ServerConfig ServerConfiguration
{
get
{
return ApplicationContainer.Resolve<ServerConfig>();
}
}
public static Logger Logger
{
get
{
return ApplicationContainer.Resolve<Logger>();
}
}
private static void InitAutomapper()
{
Mapper.Initialize(cfg =>
{
cfg.CreateMap<WebClientByteResult, WebClientStringResult>().ForMember(x => x.Content, opt => opt.Ignore()).AfterMap((be, str) =>
{
var encoding = be.Request.Encoding ?? Encoding.UTF8;
str.Content = encoding.GetString(be.Content);
});
cfg.CreateMap<WebClientStringResult, WebClientByteResult>().ForMember(x => x.Content, opt => opt.Ignore()).AfterMap((str, be) =>
{
if (!string.IsNullOrEmpty(str.Content))
{
var encoding = str.Request.Encoding ?? Encoding.UTF8;
be.Content = encoding.GetBytes(str.Content);
}
});
cfg.CreateMap<WebClientStringResult, WebClientStringResult>();
cfg.CreateMap<WebClientByteResult, WebClientByteResult>();
cfg.CreateMap<ReleaseInfo, ReleaseInfo>();
cfg.CreateMap<ReleaseInfo, TrackerCacheResult>().AfterMap((r, t) =>
{
if (r.Category != null)
{
t.CategoryDesc = string.Join(", ", r.Category.Select(x => TorznabCatType.GetCatDesc(x)).Where(x => !string.IsNullOrEmpty(x)));
}
else
{
t.CategoryDesc = "";
}
});
});
}
public static void SetupLogging(ContainerBuilder builder)
{
Logger logger = LogManager.GetCurrentClassLogger();
if (builder != null)
{
builder.RegisterInstance(logger).SingleInstance();
}
}
public static void SetLogLevel(LogLevel level)
{
foreach (var rule in LogManager.Configuration.LoggingRules)
{
if (level == LogLevel.Debug)
{
if (!rule.Levels.Contains(LogLevel.Debug))
{
rule.EnableLoggingForLevel(LogLevel.Debug);
}
}
else
{
if (rule.Levels.Contains(LogLevel.Debug))
{
rule.DisableLoggingForLevel(LogLevel.Debug);
}
}
}
LogManager.ReconfigExistingLoggers();
}
}
}

View File

@@ -0,0 +1,313 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
using com.LandonKey.SocksWebProxy;
using com.LandonKey.SocksWebProxy.Proxy;
using CloudFlareUtilities;
using Jackett.Common.Models.Config;
using Jackett.Common.Services.Interfaces;
using NLog;
using Jackett.Common.Helpers;
namespace Jackett.Common.Utils.Clients
{
// custom HttpWebClient based WebClient for netcore (due to changed custom certificate validation API)
public class HttpWebClientNetCore : WebClient
{
static protected Dictionary<string, ICollection<string>> trustedCertificates = new Dictionary<string, ICollection<string>>();
static protected string webProxyUrl;
static protected IWebProxy webProxy;
static public void InitProxy(ServerConfig serverConfig)
{
// dispose old SocksWebProxy
if (webProxy != null && webProxy is SocksWebProxy)
{
((SocksWebProxy)webProxy).Dispose();
webProxy = null;
}
webProxyUrl = serverConfig.GetProxyUrl();
if (!string.IsNullOrWhiteSpace(webProxyUrl))
{
if (serverConfig.ProxyType != ProxyType.Http)
{
var addresses = Dns.GetHostAddressesAsync(serverConfig.ProxyUrl).Result;
var socksConfig = new ProxyConfig
{
SocksAddress = addresses.FirstOrDefault(),
Username = serverConfig.ProxyUsername,
Password = serverConfig.ProxyPassword,
Version = serverConfig.ProxyType == ProxyType.Socks4 ?
ProxyConfig.SocksVersion.Four :
ProxyConfig.SocksVersion.Five
};
if (serverConfig.ProxyPort.HasValue)
{
socksConfig.SocksPort = serverConfig.ProxyPort.Value;
}
webProxy = new SocksWebProxy(socksConfig, false);
}
else
{
NetworkCredential creds = null;
if (!serverConfig.ProxyIsAnonymous)
{
var username = serverConfig.ProxyUsername;
var password = serverConfig.ProxyPassword;
creds = new NetworkCredential(username, password);
}
webProxy = new WebProxy(webProxyUrl)
{
BypassProxyOnLocal = false,
Credentials = creds
};
}
}
}
public HttpWebClientNetCore(IProcessService p, Logger l, IConfigurationService c, ServerConfig sc)
: base(p: p,
l: l,
c: c,
sc: sc)
{
if (webProxyUrl == null)
InitProxy(sc);
}
// Called everytime the ServerConfig changes
public override void OnNext(ServerConfig value)
{
var newProxyUrl = serverConfig.GetProxyUrl();
if (webProxyUrl != newProxyUrl) // if proxy URL changed
InitProxy(serverConfig);
}
override public void Init()
{
ServicePointManager.DefaultConnectionLimit = 1000;
if (serverConfig.RuntimeSettings.IgnoreSslErrors == true)
{
logger.Info(string.Format("HttpWebClient: Disabling certificate validation"));
ServicePointManager.ServerCertificateValidationCallback += (sender, certificate, chain, sslPolicyErrors) => { return true; };
}
}
override protected async Task<WebClientByteResult> Run(WebRequest webRequest)
{
ServicePointManager.SecurityProtocol = (SecurityProtocolType)192 | (SecurityProtocolType)768 | (SecurityProtocolType)3072;
var cookies = new CookieContainer();
if (!string.IsNullOrEmpty(webRequest.Cookies))
{
var uri = new Uri(webRequest.Url);
var cookieUrl = new Uri(uri.Scheme + "://" + uri.Host); // don't include the path, Scheme is needed for mono compatibility
foreach (var c in webRequest.Cookies.Split(';'))
{
try
{
cookies.SetCookies(cookieUrl, c.Trim());
}
catch (CookieException ex)
{
logger.Info("(Non-critical) Problem loading cookie {0}, {1}, {2}", uri, c, ex.Message);
}
}
}
using (ClearanceHandler clearanceHandlr = new ClearanceHandler())
{
clearanceHandlr.MaxRetries = 30;
using (HttpClientHandler clientHandlr = new HttpClientHandler
{
CookieContainer = cookies,
AllowAutoRedirect = false, // Do not use this - Bugs ahoy! Lost cookies and more.
UseCookies = true,
Proxy = webProxy,
UseProxy = (webProxy != null),
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
})
{
// custom certificate validation handler (netcore version)
clientHandlr.ServerCertificateCustomValidationCallback = (request, certificate, chain, sslPolicyErrors) =>
{
var hash = certificate.GetCertHashString();
ICollection<string> hosts;
trustedCertificates.TryGetValue(hash, out hosts);
if (hosts != null)
{
if (hosts.Contains(request.RequestUri.Host))
return true;
}
return sslPolicyErrors == SslPolicyErrors.None;
};
clearanceHandlr.InnerHandler = clientHandlr;
using (var client = new HttpClient(clearanceHandlr))
{
if (webRequest.EmulateBrowser == true)
client.DefaultRequestHeaders.Add("User-Agent", BrowserUtil.ChromeUserAgent);
else
client.DefaultRequestHeaders.Add("User-Agent", "Jackett/" + configService.GetVersion());
HttpResponseMessage response = null;
using (var request = new HttpRequestMessage())
{
request.Headers.ExpectContinue = false;
request.RequestUri = new Uri(webRequest.Url);
if (webRequest.Headers != null)
{
foreach (var header in webRequest.Headers)
{
if (header.Key != "Content-Type")
{
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
}
}
if (!string.IsNullOrEmpty(webRequest.Referer))
request.Headers.Referrer = new Uri(webRequest.Referer);
if (!string.IsNullOrEmpty(webRequest.RawBody))
{
var type = webRequest.Headers.Where(h => h.Key == "Content-Type").Cast<KeyValuePair<string, string>?>().FirstOrDefault();
if (type.HasValue)
{
var str = new StringContent(webRequest.RawBody);
str.Headers.Remove("Content-Type");
str.Headers.Add("Content-Type", type.Value.Value);
request.Content = str;
}
else
request.Content = new StringContent(webRequest.RawBody);
request.Method = HttpMethod.Post;
}
else if (webRequest.Type == RequestType.POST)
{
if (webRequest.PostData != null)
request.Content = new FormUrlEncodedContent(webRequest.PostData);
request.Method = HttpMethod.Post;
}
else
{
request.Method = HttpMethod.Get;
}
using (response = await client.SendAsync(request))
{
var result = new WebClientByteResult
{
Content = await response.Content.ReadAsByteArrayAsync()
};
foreach (var header in response.Headers)
{
IEnumerable<string> value = header.Value;
result.Headers[header.Key.ToLowerInvariant()] = value.ToArray();
}
// some cloudflare clients are using a refresh header
// Pull it out manually
if (response.StatusCode == HttpStatusCode.ServiceUnavailable && response.Headers.Contains("Refresh"))
{
var refreshHeaders = response.Headers.GetValues("Refresh");
var redirval = "";
var redirtime = 0;
if (refreshHeaders != null)
{
foreach (var value in refreshHeaders)
{
var start = value.IndexOf("=");
var end = value.IndexOf(";");
var len = value.Length;
if (start > -1)
{
redirval = value.Substring(start + 1);
result.RedirectingTo = redirval;
// normally we don't want a serviceunavailable (503) to be a redirect, but that's the nature
// of this cloudflare approach..don't want to alter BaseWebResult.IsRedirect because normally
// it shoudln't include service unavailable..only if we have this redirect header.
response.StatusCode = System.Net.HttpStatusCode.Redirect;
redirtime = Int32.Parse(value.Substring(0, end));
System.Threading.Thread.Sleep(redirtime * 1000);
}
}
}
}
if (response.Headers.Location != null)
{
result.RedirectingTo = response.Headers.Location.ToString();
}
// Mono won't add the baseurl to relative redirects.
// e.g. a "Location: /index.php" header will result in the Uri "file:///index.php"
// See issue #1200
if (result.RedirectingTo != null && result.RedirectingTo.StartsWith("file://"))
{
// URL decoding apparently is needed to, without it e.g. Demonoid download is broken
// TODO: is it always needed (not just for relative redirects)?
var newRedirectingTo = WebUtilityHelpers.UrlDecode(result.RedirectingTo, webRequest.Encoding);
newRedirectingTo = newRedirectingTo.Replace("file://", request.RequestUri.Scheme + "://" + request.RequestUri.Host);
logger.Debug("[MONO relative redirect bug] Rewriting relative redirect URL from " + result.RedirectingTo + " to " + newRedirectingTo);
result.RedirectingTo = newRedirectingTo;
}
result.Status = response.StatusCode;
// Compatiblity issue between the cookie format and httpclient
// Pull it out manually ignoring the expiry date then set it manually
// http://stackoverflow.com/questions/14681144/httpclient-not-storing-cookies-in-cookiecontainer
IEnumerable<string> cookieHeaders;
var responseCookies = new List<Tuple<string, string>>();
if (response.Headers.TryGetValues("set-cookie", out cookieHeaders))
{
foreach (var value in cookieHeaders)
{
var nameSplit = value.IndexOf('=');
if (nameSplit > -1)
{
responseCookies.Add(new Tuple<string, string>(value.Substring(0, nameSplit), value.Substring(0, value.IndexOf(';') == -1 ? value.Length : (value.IndexOf(';'))) + ";"));
}
}
var cookieBuilder = new StringBuilder();
foreach (var cookieGroup in responseCookies.GroupBy(c => c.Item1))
{
cookieBuilder.AppendFormat("{0} ", cookieGroup.Last().Item2);
}
result.Cookies = cookieBuilder.ToString().Trim();
}
ServerUtil.ResureRedirectIsFullyQualified(webRequest, result);
return result;
}
}
}
}
}
}
override public void AddTrustedCertificate(string host, string hash)
{
hash = hash.ToUpper();
ICollection<string> hosts;
trustedCertificates.TryGetValue(hash.ToUpper(), out hosts);
if (hosts == null)
{
hosts = new HashSet<string>();
trustedCertificates[hash] = hosts;
}
hosts.Add(host);
}
}
}

View File

@@ -0,0 +1,163 @@
using Jackett.Common.Models.Config;
using Jackett.Common.Services.Interfaces;
using Jackett.Common.Utils;
using Jackett.Server.Services;
using NLog;
using System;
using System.Linq;
using System.Runtime.InteropServices;
namespace Jackett.Server
{
public static class Initialisation
{
public static void ProcessSettings(RuntimeSettings runtimeSettings, Logger logger)
{
if (runtimeSettings.ClientOverride != "httpclient" && runtimeSettings.ClientOverride != "httpclient2" && runtimeSettings.ClientOverride != "httpclientnetcore")
{
logger.Error($"Client override ({runtimeSettings.ClientOverride}) has been deprecated, please remove it from your start arguments");
Environment.Exit(1);
}
if (runtimeSettings.DoSSLFix != null)
{
logger.Error("SSLFix has been deprecated, please remove it from your start arguments");
Environment.Exit(1);
}
if (runtimeSettings.LogRequests)
{
logger.Info("Logging enabled.");
}
if (runtimeSettings.TracingEnabled)
{
logger.Info("Tracing enabled.");
}
if (runtimeSettings.IgnoreSslErrors == true)
{
logger.Error($"The IgnoreSslErrors option has been deprecated, please remove it from your start arguments");
}
if (!string.IsNullOrWhiteSpace(runtimeSettings.CustomDataFolder))
{
logger.Info("Jackett Data will be stored in: " + runtimeSettings.CustomDataFolder);
}
if (runtimeSettings.ProxyConnection != null)
{
logger.Info("Proxy enabled. " + runtimeSettings.ProxyConnection);
}
}
public static void ProcessWindowsSpecificArgs(ConsoleOptions consoleOptions, IProcessService processService, ServerConfig serverConfig, Logger logger)
{
IServiceConfigService serviceConfigService = new ServiceConfigService();
IServerService serverService = new ServerService(null, processService, null, null, logger, null, null, null, serverConfig);
/* ====== Actions ===== */
// Install service
if (consoleOptions.Install)
{
logger.Info("Initiating Jackett service install");
serviceConfigService.Install();
Environment.Exit(1);
}
// Uninstall service
if (consoleOptions.Uninstall)
{
logger.Info("Initiating Jackett service uninstall");
serverService.ReserveUrls(false);
serviceConfigService.Uninstall();
Environment.Exit(1);
}
// Start Service
if (consoleOptions.StartService)
{
if (!serviceConfigService.ServiceRunning())
{
logger.Info("Initiating Jackett service start");
serviceConfigService.Start();
}
Environment.Exit(1);
}
// Stop Service
if (consoleOptions.StopService)
{
if (serviceConfigService.ServiceRunning())
{
logger.Info("Initiating Jackett service stop");
serviceConfigService.Stop();
}
Environment.Exit(1);
}
// Reserve urls
if (consoleOptions.ReserveUrls)
{
logger.Info("Initiating ReserveUrls");
serverService.ReserveUrls(true);
Environment.Exit(1);
}
}
public static void ProcessConsoleOverrides(ConsoleOptions consoleOptions, IProcessService processService, ServerConfig serverConfig, IConfigurationService configurationService, Logger logger)
{
IServerService serverService = new ServerService(null, processService, null, null, logger, null, null, null, serverConfig);
// Override port
if (consoleOptions.Port != 0)
{
Int32.TryParse(serverConfig.Port.ToString(), out Int32 configPort);
if (configPort != consoleOptions.Port)
{
logger.Info("Overriding port to " + consoleOptions.Port);
serverConfig.Port = consoleOptions.Port;
bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
if (isWindows)
{
if (ServerUtil.IsUserAdministrator())
{
serverService.ReserveUrls(true);
}
else
{
logger.Error("Unable to switch ports when not running as administrator");
Environment.Exit(1);
}
}
configurationService.SaveConfig(serverConfig);
}
}
// Override listen public
if (consoleOptions.ListenPublic || consoleOptions.ListenPrivate)
{
if (serverConfig.AllowExternal != consoleOptions.ListenPublic)
{
logger.Info("Overriding external access to " + consoleOptions.ListenPublic);
serverConfig.AllowExternal = consoleOptions.ListenPublic;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
if (ServerUtil.IsUserAdministrator())
{
serverService.ReserveUrls(true);
}
else
{
logger.Error("Unable to switch to public listening without admin rights.");
Environment.Exit(1);
}
}
configurationService.SaveConfig(serverConfig);
}
}
}
}
}

View File

@@ -0,0 +1,59 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
<ApplicationIcon>jackett.ico</ApplicationIcon>
<AssemblyName>JackettConsole</AssemblyName>
<OutputType>Exe</OutputType>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'netcoreapp2.1'">
<RuntimeIdentifiers>win-x86;linux-x64;osx-x64</RuntimeIdentifiers>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'net461'">
<RuntimeIdentifiers>win7-x86;linux-x64</RuntimeIdentifiers>
</PropertyGroup>
<!-- Conditionally obtain references for the .NET Core App 2.1 target -->
<ItemGroup Condition=" '$(TargetFramework)' == 'netcoreapp2.1' ">
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="4.5.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Autofac" Version="4.8.1" />
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="4.2.2" />
<PackageReference Include="AutoMapper" Version="6.2.2" />
<PackageReference Include="CommandLineParser" Version="2.2.1" />
<PackageReference Include="Microsoft.AspNetCore" Version="2.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="2.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.1.0" />
<PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Rewrite" Version="2.1.0" />
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Physical" Version="2.1.0" />
<PackageReference Include="NLog" Version="4.5.6" />
<PackageReference Include="NLog.Web.AspNetCore" Version="4.5.4" />
<PackageReference Include="System.ServiceProcess.ServiceController" Version="4.5.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.5.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Jackett.Common\Jackett.Common.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="..\..\README.md" Visible="false">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="..\..\LICENSE" Visible="false">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="..\..\Upstart.config" Visible="false">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,84 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Jackett.Common;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NLog;
namespace Jackett.Server.Middleware
{
public class CustomExceptionHandler
{
private readonly RequestDelegate _next;
private Logger logger;
public CustomExceptionHandler(RequestDelegate next, Logger l)
{
_next = next;
logger = l;
}
public async Task Invoke(HttpContext httpContext)
{
try
{
await _next(httpContext);
}
catch (Exception ex)
{
try
{
string msg = "";
var json = new JObject();
logger.Error(ex);
var message = ex.Message;
if (ex.InnerException != null)
{
message += ": " + ex.InnerException.Message;
}
msg = message;
if (ex is ExceptionWithConfigData)
{
json["config"] = ((ExceptionWithConfigData)ex).ConfigData.ToJson(null, false);
}
json["result"] = "error";
json["error"] = msg;
json["stacktrace"] = ex.StackTrace;
if (ex.InnerException != null)
{
json["innerstacktrace"] = ex.InnerException.StackTrace;
}
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
httpContext.Response.ContentType = "application/json";
await httpContext.Response.WriteAsync(json.ToString());
return;
}
catch (Exception ex2)
{
logger.Error(ex2, "An exception was thrown attempting to execute the custom exception error handler.");
}
await _next(httpContext);
}
}
}
// Extension method used to add the middleware to the HTTP request pipeline.
public static class CustomExceptionHandlerExtensions
{
public static IApplicationBuilder UseCustomExceptionHandler(this IApplicationBuilder builder)
{
return builder.UseMiddleware<CustomExceptionHandler>();
}
}
}

View File

@@ -0,0 +1,26 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Rewrite;
using Microsoft.Net.Http.Headers;
using System;
namespace Jackett.Server.Middleware
{
public class RedirectRules
{
public static void RedirectToDashboard(RewriteContext context)
{
HttpRequest request = context.HttpContext.Request;
if (request.Path == null || string.IsNullOrWhiteSpace(request.Path.ToString()) || request.Path.ToString() == "/"
|| request.Path.ToString().Equals("/index.html", StringComparison.OrdinalIgnoreCase))
{
// 301 is the status code of permanent redirect
var redir = Helper.ServerService.BasePath() + "/UI/Dashboard";
var response = context.HttpContext.Response;
response.StatusCode = StatusCodes.Status301MovedPermanently;
context.Result = RuleResult.EndResponse;
response.Headers[HeaderNames.Location] = redir;
}
}
}
}

View File

@@ -0,0 +1,25 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Rewrite;
using System;
namespace Jackett.Server.Middleware
{
public class RewriteRules
{
public static void RewriteBasePath(RewriteContext context)
{
var request = context.HttpContext.Request;
string serverBasePath = Helper.ServerService.BasePath() ?? string.Empty;
if (request.Path != null && request.Path.HasValue && serverBasePath.Length > 0
&& (request.Path.Value.StartsWith(serverBasePath + "/", StringComparison.Ordinal)
|| request.Path.Value.Equals(serverBasePath, StringComparison.Ordinal)))
{
string path = request.Path.Value.Substring(serverBasePath.Length);
path = string.IsNullOrEmpty(path) ? "/" : path;
request.Path = new PathString(path);
}
}
}
}

View File

@@ -0,0 +1,184 @@
using CommandLine;
using CommandLine.Text;
using Jackett.Common.Models.Config;
using Jackett.Common.Services;
using Jackett.Common.Services.Interfaces;
using Jackett.Common.Utils;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using NLog;
using NLog.Web;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
namespace Jackett.Server
{
public static class Program
{
public static IConfiguration Configuration { get; set; }
private static RuntimeSettings Settings { get; set; }
public static bool isWebHostRestart = false;
public static void Main(string[] args)
{
AppDomain.CurrentDomain.ProcessExit += CurrentDomain_ProcessExit;
var commandLineParser = new Parser(settings => settings.CaseSensitive = false);
var optionsResult = commandLineParser.ParseArguments<ConsoleOptions>(args);
var runtimeDictionary = new Dictionary<string, string>();
ConsoleOptions consoleOptions = new ConsoleOptions();
optionsResult.WithNotParsed(errors =>
{
var text = HelpText.AutoBuild(optionsResult);
text.Copyright = " ";
text.Heading = "Jackett v" + EnvironmentUtil.JackettVersion;
Console.WriteLine(text);
Environment.Exit(1);
return;
});
optionsResult.WithParsed(options =>
{
if (string.IsNullOrEmpty(options.Client))
{
//TODO: Remove libcurl once off owin
bool runningOnDotNetCore = RuntimeInformation.FrameworkDescription.IndexOf("Core", StringComparison.OrdinalIgnoreCase) >= 0;
if (runningOnDotNetCore)
{
options.Client = "httpclientnetcore";
}
else
{
options.Client = "httpclient";
}
}
Settings = options.ToRunTimeSettings();
consoleOptions = options;
runtimeDictionary = GetValues(Settings);
});
LogManager.Configuration = LoggingSetup.GetLoggingConfiguration(Settings);
Logger logger = LogManager.GetCurrentClassLogger();
logger.Info("Starting Jackett v" + EnvironmentUtil.JackettVersion);
// create PID file early
if (!string.IsNullOrWhiteSpace(Settings.PIDFile))
{
try
{
var proc = Process.GetCurrentProcess();
File.WriteAllText(Settings.PIDFile, proc.Id.ToString());
}
catch (Exception e)
{
logger.Error(e, "Error while creating the PID file");
}
}
Initialisation.ProcessSettings(Settings, logger);
ISerializeService serializeService = new SerializeService();
IProcessService processService = new ProcessService(logger);
IConfigurationService configurationService = new ConfigurationService(serializeService, processService, logger, Settings);
if (consoleOptions.Install || consoleOptions.Uninstall || consoleOptions.StartService || consoleOptions.StopService || consoleOptions.ReserveUrls)
{
bool isWindows = Environment.OSVersion.Platform == PlatformID.Win32NT;
if (isWindows)
{
ServerConfig serverConfig = configurationService.BuildServerConfig(Settings);
Initialisation.ProcessWindowsSpecificArgs(consoleOptions, processService, serverConfig, logger);
}
else
{
logger.Error($"ReserveUrls and service arguments only apply to Windows, please remove them from your start arguments");
Environment.Exit(1);
}
}
var builder = new ConfigurationBuilder();
builder.AddInMemoryCollection(runtimeDictionary);
Configuration = builder.Build();
do
{
if (!isWebHostRestart)
{
if (consoleOptions.Port != 0 || consoleOptions.ListenPublic || consoleOptions.ListenPrivate)
{
ServerConfig serverConfiguration = configurationService.BuildServerConfig(Settings);
Initialisation.ProcessConsoleOverrides(consoleOptions, processService, serverConfiguration, configurationService, logger);
}
}
ServerConfig serverConfig = configurationService.BuildServerConfig(Settings);
Int32.TryParse(serverConfig.Port.ToString(), out Int32 configPort);
string[] url = serverConfig.GetListenAddresses(serverConfig.AllowExternal).Take(1).ToArray(); //Kestrel doesn't need 127.0.0.1 and localhost to be registered, remove once off OWIN
isWebHostRestart = false;
try
{
CreateWebHostBuilder(args, url).Build().Run();
}
catch (Exception ex)
{
if (ex.InnerException is Microsoft.AspNetCore.Connections.AddressInUseException)
{
logger.Error("Address already in use: Most likely Jackett is already running. " + ex.Message);
Environment.Exit(1);
}
logger.Error(ex);
throw;
}
} while (isWebHostRestart);
}
public static Dictionary<string, string> GetValues(object obj)
{
return obj
.GetType()
.GetProperties()
.ToDictionary(p => "RuntimeSettings:" + p.Name, p => p.GetValue(obj) == null ? null : p.GetValue(obj).ToString());
}
private static void CurrentDomain_ProcessExit(object sender, EventArgs e)
{
try
{
if (Settings != null && !string.IsNullOrWhiteSpace(Settings.PIDFile))
{
var PIDFile = Settings.PIDFile;
if (File.Exists(PIDFile))
{
Console.WriteLine("Deleting PID file " + PIDFile);
File.Delete(PIDFile);
}
LogManager.Shutdown();
}
}
catch (Exception ex)
{
Console.Error.WriteLine(ex.ToString(), "Error while deleting the PID file");
}
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args, string[] urls) =>
WebHost.CreateDefaultBuilder(args)
.UseConfiguration(Configuration)
.UseUrls(urls)
.PreferHostingUrls(true)
.UseStartup<Startup>()
.UseNLog();
}
}

View File

@@ -0,0 +1,211 @@
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using Jackett.Common;
using Jackett.Common.Models.Config;
using Jackett.Common.Services.Interfaces;
using Jackett.Common.Utils;
using Microsoft.AspNetCore.DataProtection;
namespace Jackett.Server.Services
{
public class ProtectionService : IProtectionService
{
DataProtectionScope PROTECTION_SCOPE = DataProtectionScope.LocalMachine;
private const string JACKETT_KEY = "JACKETT_KEY";
const string APPLICATION_KEY = "Dvz66r3n8vhTGip2/quiw5ISyM37f7L2iOdupzdKmzkvXGhAgQiWK+6F+4qpxjPVNks1qO7LdWuVqRlzgLzeW8mChC6JnBMUS1Fin4N2nS9lh4XPuCZ1che75xO92Nk2vyXUo9KSFG1hvEszAuLfG2Mcg1r0sVyVXd2gQDU/TbY=";
private byte[] _instanceKey;
IDataProtector _protector = null;
public ProtectionService(ServerConfig config, IDataProtectionProvider provider = null)
{
if (Environment.OSVersion.Platform == PlatformID.Unix)
{
// We should not be running as root and will only have access to the local store.
PROTECTION_SCOPE = DataProtectionScope.CurrentUser;
}
_instanceKey = Encoding.UTF8.GetBytes(config.InstanceId);
if (provider != null)
{
var jackettKey = Environment.GetEnvironmentVariable(JACKETT_KEY);
string purpose = string.IsNullOrEmpty(jackettKey) ? APPLICATION_KEY : jackettKey.ToString();
_protector = provider.CreateProtector(purpose);
}
}
public string Protect(string plainText)
{
if (string.IsNullOrEmpty(plainText))
return string.Empty;
return _protector.Protect(plainText);
}
public string UnProtect(string plainText)
{
if (string.IsNullOrEmpty(plainText))
return string.Empty;
return _protector.Unprotect(plainText);
}
public string LegacyProtect(string plainText)
{
var jackettKey = Environment.GetEnvironmentVariable(JACKETT_KEY);
if (jackettKey == null)
{
return ProtectDefaultMethod(plainText);
}
else
{
return ProtectUsingKey(plainText, jackettKey);
}
}
public string LegacyUnProtect(string plainText)
{
var jackettKey = Environment.GetEnvironmentVariable(JACKETT_KEY);
if (jackettKey == null)
{
return UnProtectDefaultMethod(plainText);
}
else
{
return UnProtectUsingKey(plainText, jackettKey);
}
}
private string ProtectDefaultMethod(string plainText)
{
if (string.IsNullOrEmpty(plainText))
return string.Empty;
var plainBytes = Encoding.UTF8.GetBytes(plainText);
var appKey = Convert.FromBase64String(APPLICATION_KEY);
var instanceKey = _instanceKey;
var entropy = new byte[appKey.Length + instanceKey.Length];
Buffer.BlockCopy(instanceKey, 0, entropy, 0, instanceKey.Length);
Buffer.BlockCopy(appKey, 0, entropy, instanceKey.Length, appKey.Length);
var protectedBytes = ProtectedData.Protect(plainBytes, entropy, PROTECTION_SCOPE);
using (MemoryStream ms = new MemoryStream())
{
using (RijndaelManaged AES = new RijndaelManaged())
{
AES.KeySize = 256;
AES.BlockSize = 128;
var key = new Rfc2898DeriveBytes(instanceKey, instanceKey.Reverse().ToArray(), 64);
AES.Key = key.GetBytes(AES.KeySize / 8);
AES.IV = key.GetBytes(AES.BlockSize / 8);
AES.Mode = CipherMode.CBC;
using (var cs = new CryptoStream(ms, AES.CreateEncryptor(), CryptoStreamMode.Write))
{
cs.Write(protectedBytes, 0, protectedBytes.Length);
cs.Close();
}
protectedBytes = ms.ToArray();
}
}
return Convert.ToBase64String(protectedBytes);
}
private string UnProtectDefaultMethod(string plainText)
{
if (string.IsNullOrEmpty(plainText))
return string.Empty;
var protectedBytes = Convert.FromBase64String(plainText);
var instanceKey = _instanceKey;
using (MemoryStream ms = new MemoryStream())
{
using (RijndaelManaged AES = new RijndaelManaged())
{
AES.KeySize = 256;
AES.BlockSize = 128;
var key = new Rfc2898DeriveBytes(instanceKey, instanceKey.Reverse().ToArray(), 64);
AES.Key = key.GetBytes(AES.KeySize / 8);
AES.IV = key.GetBytes(AES.BlockSize / 8);
AES.Mode = CipherMode.CBC;
using (var cs = new CryptoStream(ms, AES.CreateDecryptor(), CryptoStreamMode.Write))
{
cs.Write(protectedBytes, 0, protectedBytes.Length);
cs.Close();
}
protectedBytes = ms.ToArray();
}
}
var appKey = Convert.FromBase64String(APPLICATION_KEY);
var entropy = new byte[appKey.Length + instanceKey.Length];
Buffer.BlockCopy(instanceKey, 0, entropy, 0, instanceKey.Length);
Buffer.BlockCopy(appKey, 0, entropy, instanceKey.Length, appKey.Length);
var unprotectedBytes = ProtectedData.Unprotect(protectedBytes, entropy, PROTECTION_SCOPE);
return Encoding.UTF8.GetString(unprotectedBytes);
}
private string ProtectUsingKey(string plainText, string key)
{
return StringCipher.Encrypt(plainText, key);
}
private string UnProtectUsingKey(string plainText, string key)
{
return StringCipher.Decrypt(plainText, key);
}
public void Protect<T>(T obj)
{
var type = obj.GetType();
foreach (var property in type.GetProperties(BindingFlags.SetProperty | BindingFlags.GetProperty | BindingFlags.Public))
{
if (property.GetCustomAttributes(typeof(JackettProtectedAttribute), false).Count() > 0)
{
var value = property.GetValue(obj);
if (value is string)
{
var protectedString = Protect(value as string);
property.SetValue(obj, protectedString);
}
}
}
}
public void UnProtect<T>(T obj)
{
var type = obj.GetType();
foreach (var property in type.GetProperties(BindingFlags.SetProperty | BindingFlags.GetProperty | BindingFlags.Public))
{
if (property.GetCustomAttributes(typeof(JackettProtectedAttribute), false).Count() > 0)
{
var value = property.GetValue(obj);
if (value is string)
{
var unprotectedString = UnProtect(value as string);
property.SetValue(obj, unprotectedString);
}
}
}
}
}
}

View File

@@ -0,0 +1,74 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using Jackett.Common.Models.Config;
using Jackett.Common.Services.Interfaces;
namespace Jackett.Server.Services
{
class SecuityService : ISecuityService
{
private const string COOKIENAME = "JACKETT";
private ServerConfig _serverConfig;
public SecuityService(ServerConfig sc)
{
_serverConfig = sc;
}
public string HashPassword(string input)
{
if (input == null)
return null;
// Append key as salt
input += _serverConfig.APIKey;
UnicodeEncoding UE = new UnicodeEncoding();
byte[] hashValue;
byte[] message = UE.GetBytes(input);
SHA512Managed hashString = new SHA512Managed();
string hex = "";
hashValue = hashString.ComputeHash(message);
foreach (byte x in hashValue)
{
hex += String.Format("{0:x2}", x);
}
return hex;
}
public void Login(HttpResponseMessage response)
{
// Login
response.Headers.Add("Set-Cookie", COOKIENAME + "=" + _serverConfig.AdminPassword + "; path=/");
}
public void Logout(HttpResponseMessage response)
{
// Logout
response.Headers.Add("Set-Cookie", COOKIENAME + "=; path=/");
}
public bool CheckAuthorised(HttpRequestMessage request)
{
if (string.IsNullOrEmpty(_serverConfig.AdminPassword))
return true;
try
{
var cookie = request.Headers.GetValues(COOKIENAME).FirstOrDefault();
if (cookie != null)
{
return cookie == _serverConfig.AdminPassword;
}
}
catch { }
return false;
}
}
}

View File

@@ -0,0 +1,341 @@
using Jackett.Common.Models.Config;
using Jackett.Common.Services.Interfaces;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;
using NLog;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
namespace Jackett.Server.Services
{
public class ServerService : IServerService
{
private IIndexerManagerService indexerService;
private IProcessService processService;
private ISerializeService serializeService;
private IConfigurationService configService;
private Logger logger;
private Common.Utils.Clients.WebClient client;
private IUpdateService updater;
private List<string> _notices = new List<string>();
private ServerConfig config;
private IProtectionService _protectionService;
public ServerService(IIndexerManagerService i, IProcessService p, ISerializeService s, IConfigurationService c, Logger l, Common.Utils.Clients.WebClient w, IUpdateService u, IProtectionService protectionService, ServerConfig serverConfig)
{
indexerService = i;
processService = p;
serializeService = s;
configService = c;
logger = l;
client = w;
updater = u;
config = serverConfig;
_protectionService = protectionService;
}
public List<string> notices
{
get
{
return _notices;
}
}
public Uri ConvertToProxyLink(Uri link, string serverUrl, string indexerId, string action = "dl", string file = "t")
{
if (link == null || (link.IsAbsoluteUri && link.Scheme == "magnet" && action != "bh")) // no need to convert a magnet link to a proxy link unless it's a blackhole link
return link;
var encryptedLink = _protectionService.Protect(link.ToString());
var encodedLink = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(encryptedLink));
string urlEncodedFile = WebUtility.UrlEncode(file);
var proxyLink = string.Format("{0}{1}/{2}/?jackett_apikey={3}&path={4}&file={5}", serverUrl, action, indexerId, config.APIKey, encodedLink, urlEncodedFile);
return new Uri(proxyLink);
}
public string BasePath()
{
if (config.BasePathOverride == null || config.BasePathOverride == "")
{
return "";
}
var path = config.BasePathOverride;
if (path.EndsWith("/"))
{
path = path.TrimEnd('/');
}
if (!path.StartsWith("/"))
{
path = "/" + path;
}
return path;
}
public void Initalize()
{
try
{
var x = Environment.OSVersion;
var runtimedir = RuntimeEnvironment.GetRuntimeDirectory();
logger.Info("Environment version: " + Environment.Version.ToString() + " (" + runtimedir + ")");
logger.Info("OS version: " + Environment.OSVersion.ToString() + (Environment.Is64BitOperatingSystem ? " (64bit OS)" : "") + (Environment.Is64BitProcess ? " (64bit process)" : ""));
try
{
ThreadPool.GetMaxThreads(out int workerThreads, out int completionPortThreads);
logger.Info("ThreadPool MaxThreads: " + workerThreads + " workerThreads, " + completionPortThreads + " completionPortThreads");
}
catch (Exception e)
{
logger.Error("Error while getting MaxThreads details: " + e);
}
logger.Info("App config/log directory: " + configService.GetAppDataFolder());
try
{
var issuefile = "/etc/issue";
if (File.Exists(issuefile))
{
using (StreamReader reader = new StreamReader(issuefile))
{
string firstLine = reader.ReadLine();
if (firstLine != null)
logger.Info("issue: " + firstLine);
}
}
}
catch (Exception e)
{
logger.Error(e, "Error while reading the issue file");
}
bool runningOnDotNetCore = RuntimeInformation.FrameworkDescription.IndexOf("Core", StringComparison.OrdinalIgnoreCase) >= 0;
Type monotype = Type.GetType("Mono.Runtime");
if (monotype != null && !runningOnDotNetCore)
{
MethodInfo displayName = monotype.GetMethod("GetDisplayName", BindingFlags.NonPublic | BindingFlags.Static);
var monoVersion = "unknown";
if (displayName != null)
monoVersion = displayName.Invoke(null, null).ToString();
logger.Info("mono version: " + monoVersion);
var monoVersionO = new Version(monoVersion.Split(' ')[0]);
if (monoVersionO.Major < 5 || (monoVersionO.Major == 5 && monoVersionO.Minor < 8))
{
//Hard minimum of 5.8
//5.4 throws a SIGABRT, looks related to this which was fixed in 5.8 https://bugzilla.xamarin.com/show_bug.cgi?id=60625
logger.Error("Your mono version is too old. Please update to the latest version from http://www.mono-project.com/download/");
Environment.Exit(2);
}
if (monoVersionO.Major < 5 || (monoVersionO.Major == 5 && monoVersionO.Minor < 8))
{
string notice = "A minimum Mono version of 5.8 is required. Please update to the latest version from http://www.mono-project.com/download/";
_notices.Add(notice);
logger.Error(notice);
}
try
{
// Check for mono-devel
// Is there any better way which doesn't involve a hard cashes?
var mono_devel_file = Path.Combine(runtimedir, "mono-api-info.exe");
if (!File.Exists(mono_devel_file))
{
var notice = "It looks like the mono-devel package is not installed, please make sure it's installed to avoid crashes.";
_notices.Add(notice);
logger.Error(notice);
}
}
catch (Exception e)
{
logger.Error(e, "Error while checking for mono-devel");
}
try
{
// Check for ca-certificates-mono
var mono_cert_file = Path.Combine(runtimedir, "cert-sync.exe");
if (!File.Exists(mono_cert_file))
{
var notice = "The ca-certificates-mono package is not installed, HTTPS trackers won't work. Please install it.";
_notices.Add(notice);
logger.Error(notice);
}
}
catch (Exception e)
{
logger.Error(e, "Error while checking for ca-certificates-mono");
}
try
{
Encoding.GetEncoding("windows-1255");
}
catch (NotSupportedException e)
{
logger.Debug(e);
logger.Error(e.Message + " Most likely the mono-locale-extras package is not installed.");
Environment.Exit(2);
}
// check if the certificate store was initialized using Mono.Security.X509.X509StoreManager.TrustedRootCertificates.Count
try
{
var monoSecurity = Assembly.Load("Mono.Security");
Type monoX509StoreManager = monoSecurity.GetType("Mono.Security.X509.X509StoreManager");
if (monoX509StoreManager != null)
{
var TrustedRootCertificatesProperty = monoX509StoreManager.GetProperty("TrustedRootCertificates");
var TrustedRootCertificates = (ICollection)TrustedRootCertificatesProperty.GetValue(null);
logger.Info("TrustedRootCertificates count: " + TrustedRootCertificates.Count);
if (TrustedRootCertificates.Count == 0)
{
var CACertificatesFiles = new string[] {
"/etc/ssl/certs/ca-certificates.crt", // Debian based
"/etc/pki/tls/certs/ca-bundle.c", // RedHat based
"/etc/ssl/ca-bundle.pem", // SUSE
};
var notice = "The mono certificate store is not initialized.<br/>\n";
var logSpacer = " ";
var CACertificatesFile = CACertificatesFiles.Where(f => File.Exists(f)).FirstOrDefault();
var CommandRoot = "curl -sS https://curl.haxx.se/ca/cacert.pem | cert-sync /dev/stdin";
var CommandUser = "curl -sS https://curl.haxx.se/ca/cacert.pem | cert-sync --user /dev/stdin";
if (CACertificatesFile != null)
{
CommandRoot = "cert-sync " + CACertificatesFile;
CommandUser = "cert-sync --user " + CACertificatesFile;
}
notice += logSpacer + "Please run the following command as root:<br/>\n";
notice += logSpacer + "<pre>" + CommandRoot + "</pre><br/>\n";
notice += logSpacer + "If you don't have root access or you're running MacOS, please run the following command as the jackett user (" + Environment.UserName + "):<br/>\n";
notice += logSpacer + "<pre>" + CommandUser + "</pre>";
_notices.Add(notice);
logger.Error(Regex.Replace(notice, "<.*?>", String.Empty));
}
}
}
catch (Exception e)
{
logger.Error(e, "Error while chekcing the mono certificate store");
}
}
}
catch (Exception e)
{
logger.Error("Error while getting environment details: " + e);
}
try
{
if (Environment.UserName == "root")
{
var notice = "Jackett is running with root privileges. You should run Jackett as an unprivileged user.";
_notices.Add(notice);
logger.Error(notice);
}
}
catch (Exception e)
{
logger.Error(e, "Error while checking the username");
}
CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("en-US");
// Load indexers
indexerService.InitIndexers(configService.GetCardigannDefinitionsFolders());
client.Init();
updater.CleanupTempDir();
}
public void Start()
{
updater.StartUpdateChecker();
}
public void ReserveUrls(bool doInstall = true)
{
logger.Debug("Unreserving Urls");
config.GetListenAddresses(false).ToList().ForEach(u => RunNetSh(string.Format("http delete urlacl {0}", u)));
config.GetListenAddresses(true).ToList().ForEach(u => RunNetSh(string.Format("http delete urlacl {0}", u)));
if (doInstall)
{
logger.Debug("Reserving Urls");
config.GetListenAddresses(true).ToList().ForEach(u => RunNetSh(string.Format("http add urlacl {0} sddl=D:(A;;GX;;;S-1-1-0)", u)));
logger.Debug("Urls reserved");
}
}
private void RunNetSh(string args)
{
processService.StartProcessAndLog("netsh.exe", args);
}
public void Stop()
{
// Only needed for Owin
}
public string GetServerUrl(Object obj)
{
string serverUrl = "";
if (obj is HttpRequest request)
{
var scheme = request.Scheme;
var port = request.HttpContext.Request.Host.Port;
// Check for protocol headers added by reverse proxys
// X-Forwarded-Proto: A de facto standard for identifying the originating protocol of an HTTP request
var X_Forwarded_Proto = request.Headers.Where(x => x.Key == "X-Forwarded-Proto").Select(x => x.Value).FirstOrDefault();
if (X_Forwarded_Proto.Count > 0)
{
scheme = X_Forwarded_Proto.First();
}
// Front-End-Https: Non-standard header field used by Microsoft applications and load-balancers
else if (request.Headers.Where(x => x.Key == "Front-End-Https" && x.Value.FirstOrDefault() == "on").Any())
{
scheme = "https";
}
//default to 443 if the Host header doesn't contain the port (needed for reverse proxy setups)
if (scheme == "https" && !request.HttpContext.Request.Host.Value.Contains(":"))
{
port = 443;
}
serverUrl = string.Format("{0}://{1}:{2}{3}/", scheme, request.HttpContext.Request.Host.Host, port, BasePath());
}
return serverUrl;
}
public string GetBlackholeDirectory()
{
return config.BlackholeDir;
}
public string GetApiKey()
{
return config.APIKey;
}
}
}

View File

@@ -0,0 +1,121 @@
using NLog;
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.ServiceProcess;
using Jackett.Common.Services.Interfaces;
using System.Reflection;
using Jackett.Common.Services;
namespace Jackett.Server.Services
{
public class ServiceConfigService : IServiceConfigService
{
private const string NAME = "Jackett";
private const string DESCRIPTION = "API Support for your favorite torrent trackers";
private const string SERVICEEXE = "JackettService.exe";
private IProcessService processService;
private Logger logger;
public ServiceConfigService()
{
logger = LogManager.GetCurrentClassLogger();
processService = new ProcessService(logger);
}
public bool ServiceExists()
{
return GetService(NAME) != null;
}
public bool ServiceRunning()
{
var service = GetService(NAME);
if (service == null)
return false;
return service.Status == ServiceControllerStatus.Running;
}
public void Start()
{
var service = GetService(NAME);
service.Start();
}
public void Stop()
{
var service = GetService(NAME);
service.Stop();
}
public ServiceController GetService(string serviceName)
{
return ServiceController.GetServices().FirstOrDefault(c => String.Equals(c.ServiceName, serviceName, StringComparison.InvariantCultureIgnoreCase));
}
public void Install()
{
if (ServiceExists())
{
logger.Warn("The service is already installed!");
}
else
{
string applicationFolder = Path.GetDirectoryName(new Uri(Assembly.GetExecutingAssembly().CodeBase).LocalPath);
var exePath = Path.Combine(applicationFolder, SERVICEEXE);
if (!File.Exists(exePath) && Debugger.IsAttached)
{
exePath = Path.Combine(applicationFolder, "..\\..\\..\\Jackett.Service\\bin\\Debug", SERVICEEXE);
}
string arg = $"create {NAME} start= auto binpath= \"{exePath}\" DisplayName= {NAME}";
processService.StartProcessAndLog("sc.exe", arg, true);
processService.StartProcessAndLog("sc.exe", $"description {NAME} \"{DESCRIPTION}\"", true);
}
}
public void Uninstall()
{
RemoveService();
processService.StartProcessAndLog("sc.exe", $"delete {NAME}", true);
logger.Info("The service was uninstalled.");
}
public void RemoveService()
{
var service = GetService(NAME);
if(service == null)
{
logger.Warn("The service is already uninstalled");
return;
}
if (service.Status != ServiceControllerStatus.Stopped)
{
service.Stop();
service.WaitForStatus(ServiceControllerStatus.Stopped, TimeSpan.FromSeconds(60));
service.Refresh();
if (service.Status == ServiceControllerStatus.Stopped)
{
logger.Info("Service stopped.");
}
else
{
logger.Error("Failed to stop the service");
}
}
else
{
logger.Warn("The service was already stopped");
}
}
}
}

View File

@@ -0,0 +1,134 @@
using Autofac;
using Autofac.Extensions.DependencyInjection;
using Jackett.Common.Models.Config;
using Jackett.Common.Plumbing;
using Jackett.Common.Services.Interfaces;
using Jackett.Common.Utils.Clients;
using Jackett.Server.Middleware;
using Jackett.Server.Services;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.AspNetCore.Rewrite;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Newtonsoft.Json.Serialization;
using System;
using System.IO;
using System.Text;
namespace Jackett.Server
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddResponseCompression();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme,
options =>
{
options.LoginPath = new PathString("/UI/Login");
options.AccessDeniedPath = new PathString("/UI/Login");
options.LogoutPath = new PathString("/UI/Logout");
options.Cookie.Name = "Jackett";
});
services.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
})
.AddJsonOptions(options =>
{
options.SerializerSettings.ContractResolver = new DefaultContractResolver(); //Web app uses Pascal Case JSON
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
RuntimeSettings runtimeSettings = new RuntimeSettings();
Configuration.GetSection("RuntimeSettings").Bind(runtimeSettings);
DirectoryInfo dataProtectionFolder = new DirectoryInfo(Path.Combine(runtimeSettings.DataFolder, "DataProtection"));
services.AddDataProtection()
.PersistKeysToFileSystem(dataProtectionFolder)
.SetApplicationName("Jackett");
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
var builder = new ContainerBuilder();
Helper.SetupLogging(builder);
builder.Populate(services);
builder.RegisterModule(new JackettModule(runtimeSettings));
builder.RegisterType<SecuityService>().As<ISecuityService>();
builder.RegisterType<ServerService>().As<IServerService>();
builder.RegisterType<ProtectionService>().As<IProtectionService>();
builder.RegisterType<ServiceConfigService>().As<IServiceConfigService>();
if (runtimeSettings.ClientOverride == "httpclientnetcore")
builder.RegisterType<HttpWebClientNetCore>().As<WebClient>();
IContainer container = builder.Build();
Helper.ApplicationContainer = container;
Helper.Initialize();
return new AutofacServiceProvider(container);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime applicationLifetime)
{
applicationLifetime.ApplicationStopping.Register(OnShutdown);
Helper.applicationLifetime = applicationLifetime;
app.UseResponseCompression();
app.UseDeveloperExceptionPage();
app.UseCustomExceptionHandler();
var rewriteOptions = new RewriteOptions()
.Add(RewriteRules.RewriteBasePath)
.AddRewrite(@"^torznab\/([\w-]*)", "api/v2.0/indexers/$1/results/torznab", skipRemainingRules: true) //legacy torznab route
.AddRewrite(@"^potato\/([\w-]*)", "api/v2.0/indexers/$1/results/potato", skipRemainingRules: true) //legacy potato route
.Add(RedirectRules.RedirectToDashboard);
app.UseRewriter(rewriteOptions);
app.UseFileServer(new FileServerOptions
{
FileProvider = new PhysicalFileProvider(Helper.ConfigService.GetContentFolder()),
RequestPath = "",
EnableDefaultFiles = true,
EnableDirectoryBrowsing = false
});
app.UseAuthentication();
app.UseMvc();
}
private void OnShutdown()
{
//this code is called when the application stops
}
}
}

View File

@@ -0,0 +1,10 @@
{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}

View File

@@ -0,0 +1,15 @@
{
"Logging": {
"IncludeScopes": false,
"Debug": {
"LogLevel": {
"Default": "Warning"
}
},
"Console": {
"LogLevel": {
"Default": "Warning"
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

View File

@@ -1,37 +1,46 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net452</TargetFramework>
<TargetFramework>net461</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Indexers\**" />
<EmbeddedResource Remove="Indexers\**" />
<None Remove="Indexers\**" />
</ItemGroup>
<ItemGroup>
<None Remove="Util\Invalid-RSS.xml" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Util\Invalid-RSS.xml" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Autofac" Version="4.8.1" />
<PackageReference Include="FluentAssertions" Version="5.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.6.0" />
<PackageReference Include="MSTest.TestAdapter" Version="1.2.0" />
<PackageReference Include="MSTest.TestFramework" Version="1.2.0" />
<PackageReference Include="FluentAssertions" Version="5.4.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.2" />
<PackageReference Include="MSTest.TestAdapter" Version="1.3.2" />
<PackageReference Include="MSTest.TestFramework" Version="1.3.2" />
<PackageReference Include="NUnit" Version="3.10.1" />
<PackageReference Include="NUnit.ConsoleRunner" Version="3.8.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.10.0" />
</ItemGroup>
<ItemGroup>
<Folder Include="Properties\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Jackett.Common\Jackett.Common.csproj" />
<ProjectReference Include="..\Jackett\Jackett.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.Web" />
</ItemGroup>
</Project>

View File

@@ -18,7 +18,7 @@ namespace Jackett.Test
{
var builder = new ContainerBuilder();
builder.RegisterModule(new JackettModule(new RuntimeSettings()));
builder.RegisterModule<WebApi2Module>();
builder.RegisterType<Jackett.Services.ProtectionService>().As<IProtectionService>();
builder.RegisterType<TestWebClient>().As<WebClient>().SingleInstance();
builder.RegisterInstance(LogManager.GetCurrentClassLogger()).SingleInstance();
builder.RegisterType<TestIndexerManagerServiceHelper>().As<IIndexerManagerService>().SingleInstance();

View File

@@ -306,7 +306,15 @@ namespace Jackett.Updater
UseShellExecute = true
};
if (!isWindows)
if (isWindows)
{
//User didn't initiate the update from Windows service and wasn't running Jackett via the tray, must have started from the console
startInfo.Arguments = $"/K {startInfo.FileName} {startInfo.Arguments}";
startInfo.FileName = "cmd.exe";
startInfo.CreateNoWindow = false;
startInfo.WindowStyle = ProcessWindowStyle.Normal;
}
else
{
startInfo.Arguments = startInfo.FileName + " " + startInfo.Arguments;
startInfo.FileName = "mono";

View File

@@ -21,7 +21,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jackett.Service", "Jackett.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jackett.Tray", "Jackett.Tray\Jackett.Tray.csproj", "{FF9025B1-EC14-4AA9-8081-9F69C5E35B63}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jackett.Updater", "Jackett.Updater\Jackett.Updater.csproj", "{A61E311A-6F8B-4497-B5E4-2EA8994C7BD8}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jackett.Updater", "Jackett.Updater\Jackett.Updater.csproj", "{A61E311A-6F8B-4497-B5E4-2EA8994C7BD8}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jackett.Test", "Jackett.Test\Jackett.Test.csproj", "{FA22C904-9F5D-4D3C-9122-3E33652E7373}"
EndProject
@@ -39,6 +39,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".NET Core", ".NET Core", "{
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Executables", "Executables", "{AA50F785-12B8-4669-8D4F-EAFB49258E60}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jackett.Server", "Jackett.Server\Jackett.Server.csproj", "{84182782-EDBC-4342-ADA6-72B7694D0862}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -81,6 +83,10 @@ Global
{6B854A1B-9A90-49C0-BC37-9A35C75BCA73}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6B854A1B-9A90-49C0-BC37-9A35C75BCA73}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6B854A1B-9A90-49C0-BC37-9A35C75BCA73}.Release|Any CPU.Build.0 = Release|Any CPU
{84182782-EDBC-4342-ADA6-72B7694D0862}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{84182782-EDBC-4342-ADA6-72B7694D0862}.Debug|Any CPU.Build.0 = Debug|Any CPU
{84182782-EDBC-4342-ADA6-72B7694D0862}.Release|Any CPU.ActiveCfg = Release|Any CPU
{84182782-EDBC-4342-ADA6-72B7694D0862}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -98,6 +104,7 @@ Global
{6B854A1B-9A90-49C0-BC37-9A35C75BCA73} = {2FA9B879-5882-4B39-8D34-9EBCB82B4F2B}
{FF8B9A1B-AE7E-4F14-9C37-DA861D034738} = {AA50F785-12B8-4669-8D4F-EAFB49258E60}
{6A06EC9B-AF21-4DE8-9B50-BC7E3C2C78B9} = {AA50F785-12B8-4669-8D4F-EAFB49258E60}
{84182782-EDBC-4342-ADA6-72B7694D0862} = {6A06EC9B-AF21-4DE8-9B50-BC7E3C2C78B9}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {54BC4102-8B85-49C1-BA12-257D941D1B97}