mirror of
https://github.com/Jackett/Jackett.git
synced 2025-09-17 17:34:09 +02:00
Merge branch 'master' of https://github.com/Jackett/Jackett
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
|
153
build.cake
153
build.cake
@@ -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
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
@@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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)
|
||||
{
|
||||
|
@@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
98
src/Jackett.Server/Controllers/BlackholeController.cs
Normal file
98
src/Jackett.Server/Controllers/BlackholeController.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
96
src/Jackett.Server/Controllers/DownloadController.cs
Normal file
96
src/Jackett.Server/Controllers/DownloadController.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
178
src/Jackett.Server/Controllers/IndexerApiController.cs
Normal file
178
src/Jackett.Server/Controllers/IndexerApiController.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
544
src/Jackett.Server/Controllers/ResultsController.cs
Normal file
544
src/Jackett.Server/Controllers/ResultsController.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
194
src/Jackett.Server/Controllers/ServerConfigurationController.cs
Normal file
194
src/Jackett.Server/Controllers/ServerConfigurationController.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
105
src/Jackett.Server/Controllers/UIController.cs
Normal file
105
src/Jackett.Server/Controllers/UIController.cs
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
160
src/Jackett.Server/Helper.cs
Normal file
160
src/Jackett.Server/Helper.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
313
src/Jackett.Server/HttpWebClientNetCore.cs
Normal file
313
src/Jackett.Server/HttpWebClientNetCore.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
163
src/Jackett.Server/Initialisation.cs
Normal file
163
src/Jackett.Server/Initialisation.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
59
src/Jackett.Server/Jackett.Server.csproj
Normal file
59
src/Jackett.Server/Jackett.Server.csproj
Normal 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>
|
84
src/Jackett.Server/Middleware/CustomExceptionHandler.cs
Normal file
84
src/Jackett.Server/Middleware/CustomExceptionHandler.cs
Normal 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>();
|
||||
}
|
||||
}
|
||||
}
|
26
src/Jackett.Server/Middleware/RedirectRules.cs
Normal file
26
src/Jackett.Server/Middleware/RedirectRules.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
25
src/Jackett.Server/Middleware/RewriteRules.cs
Normal file
25
src/Jackett.Server/Middleware/RewriteRules.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
184
src/Jackett.Server/Program.cs
Normal file
184
src/Jackett.Server/Program.cs
Normal 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();
|
||||
}
|
||||
}
|
211
src/Jackett.Server/Services/ProtectionService.cs
Normal file
211
src/Jackett.Server/Services/ProtectionService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
74
src/Jackett.Server/Services/SecuityService.cs
Normal file
74
src/Jackett.Server/Services/SecuityService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
341
src/Jackett.Server/Services/ServerService.cs
Normal file
341
src/Jackett.Server/Services/ServerService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
121
src/Jackett.Server/Services/ServiceConfigService.cs
Normal file
121
src/Jackett.Server/Services/ServiceConfigService.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
134
src/Jackett.Server/Startup.cs
Normal file
134
src/Jackett.Server/Startup.cs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
10
src/Jackett.Server/appsettings.Development.json
Normal file
10
src/Jackett.Server/appsettings.Development.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"Logging": {
|
||||
"IncludeScopes": false,
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
"System": "Information",
|
||||
"Microsoft": "Information"
|
||||
}
|
||||
}
|
||||
}
|
15
src/Jackett.Server/appsettings.json
Normal file
15
src/Jackett.Server/appsettings.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"Logging": {
|
||||
"IncludeScopes": false,
|
||||
"Debug": {
|
||||
"LogLevel": {
|
||||
"Default": "Warning"
|
||||
}
|
||||
},
|
||||
"Console": {
|
||||
"LogLevel": {
|
||||
"Default": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
BIN
src/Jackett.Server/jackett.ico
Normal file
BIN
src/Jackett.Server/jackett.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 298 KiB |
@@ -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>
|
@@ -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();
|
||||
|
@@ -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";
|
||||
|
@@ -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}
|
||||
|
Reference in New Issue
Block a user