mirror of
https://github.com/Jackett/Jackett.git
synced 2025-09-17 17:34:09 +02:00
410 lines
15 KiB
C#
410 lines
15 KiB
C#
using Newtonsoft.Json.Linq;
|
|
using NLog;
|
|
using NLog.Config;
|
|
using NLog.Targets;
|
|
using NLog.Windows.Forms;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Specialized;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading.Tasks;
|
|
using System.Web;
|
|
using System.Windows.Forms;
|
|
|
|
namespace Jackett
|
|
{
|
|
public class WebServer
|
|
{
|
|
public static bool IsWindows { get { return Environment.OSVersion.Platform == PlatformID.Win32NT; } }
|
|
}
|
|
/*
|
|
public const int DefaultPort = 9117;
|
|
public static int Port = DefaultPort;
|
|
public static bool ListenPublic = true;
|
|
|
|
public static Server ServerInstance { get; private set; }
|
|
public static bool IsFirstRun { get; private set; }
|
|
public static Logger LoggerInstance { get; private set; }
|
|
public static bool IsWindows { get { return Environment.OSVersion.Platform == PlatformID.Win32NT; } }
|
|
public static string AppConfigDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Jackett");
|
|
|
|
HttpListener listener;
|
|
IndexerManager indexerManager;
|
|
WebApi webApi;
|
|
SonarrApi sonarrApi;
|
|
|
|
|
|
public WebServer()
|
|
{
|
|
// Allow all SSL.. sucks I know but mono on linux is having problems without it..
|
|
ServicePointManager.ServerCertificateValidationCallback += (sender, cert, chain, sslPolicyErrors) => true;
|
|
|
|
ReadServerSettingsFile();
|
|
LoadApiKey();
|
|
|
|
|
|
indexerManager = new IndexerManager();
|
|
sonarrApi = new SonarrApi();
|
|
webApi = new WebApi(indexerManager, sonarrApi);
|
|
|
|
}
|
|
|
|
|
|
public static void SetupLogging()
|
|
{
|
|
var logConfig = new LoggingConfiguration();
|
|
|
|
var logFile = new FileTarget();
|
|
logConfig.AddTarget("file", logFile);
|
|
logFile.Layout = "${longdate} ${level} ${message} \n ${exception:format=ToString}\n";
|
|
logFile.FileName = Path.Combine(AppConfigDirectory, "log.txt");
|
|
logFile.ArchiveFileName = "log.{#####}.txt";
|
|
logFile.ArchiveAboveSize = 500000;
|
|
logFile.MaxArchiveFiles = 1;
|
|
logFile.KeepFileOpen = false;
|
|
logFile.ArchiveNumbering = ArchiveNumberingMode.DateAndSequence;
|
|
var logFileRule = new LoggingRule("*", LogLevel.Debug, logFile);
|
|
logConfig.LoggingRules.Add(logFileRule);
|
|
|
|
if (WebServer.IsWindows)
|
|
{
|
|
#if !__MonoCS__
|
|
var logAlert = new MessageBoxTarget();
|
|
logConfig.AddTarget("alert", logAlert);
|
|
logAlert.Layout = "${message}";
|
|
logAlert.Caption = "Alert";
|
|
var logAlertRule = new LoggingRule("*", LogLevel.Fatal, logAlert);
|
|
logConfig.LoggingRules.Add(logAlertRule);
|
|
#endif
|
|
}
|
|
|
|
var logConsole = new ConsoleTarget();
|
|
logConfig.AddTarget("console", logConsole);
|
|
logConsole.Layout = "${longdate} ${level} ${message} ${exception:format=ToString}";
|
|
var logConsoleRule = new LoggingRule("*", LogLevel.Debug, logConsole);
|
|
logConfig.LoggingRules.Add(logConsoleRule);
|
|
|
|
LogManager.Configuration = logConfig;
|
|
LoggerInstance = LogManager.GetCurrentClassLogger();
|
|
}
|
|
|
|
void LoadApiKey()
|
|
{
|
|
var apiKeyFile = Path.Combine(WebServer.AppConfigDirectory, "api_key.txt");
|
|
if (File.Exists(apiKeyFile))
|
|
ApiKey.CurrentKey = File.ReadAllText(apiKeyFile).Trim();
|
|
else
|
|
{
|
|
ApiKey.CurrentKey = ApiKey.Generate();
|
|
File.WriteAllText(apiKeyFile, ApiKey.CurrentKey);
|
|
}
|
|
}
|
|
|
|
static void CreateOrMigrateSettings()
|
|
{
|
|
try
|
|
{
|
|
if (!Directory.Exists(AppConfigDirectory))
|
|
{
|
|
IsFirstRun = true;
|
|
Directory.CreateDirectory(AppConfigDirectory);
|
|
}
|
|
Console.WriteLine("App config/log directory: " + AppConfigDirectory);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show("Could not create settings directory. " + ex.Message);
|
|
Application.Exit();
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
string oldDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Jackett");
|
|
if (Directory.Exists(oldDir) && !Directory.Exists(AppConfigDirectory))
|
|
{
|
|
Directory.Move(oldDir, AppConfigDirectory);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine("ERROR could not migrate settings directory " + ex);
|
|
}
|
|
}
|
|
|
|
|
|
public async Task Start()
|
|
{
|
|
CreateOrMigrateSettings();
|
|
WebServer.LoggerInstance.Info("Starting HTTP server on port " + Port + " listening " + (ListenPublic ? "publicly" : "privately"));
|
|
|
|
try
|
|
{
|
|
listener = new HttpListener();
|
|
|
|
if (ListenPublic)
|
|
{
|
|
listener.Prefixes.Add(string.Format("http://*:{0}/", Port));
|
|
}
|
|
else
|
|
{
|
|
listener.Prefixes.Add(string.Format("http://127.0.0.1:{0}/", Port));
|
|
}
|
|
|
|
listener.Start();
|
|
webApi.server = this;
|
|
}
|
|
catch (HttpListenerException ex)
|
|
{
|
|
if (ex.ErrorCode == 5)
|
|
{
|
|
var errorStr = "App must be ran as admin for permission to use port "
|
|
+ Port + Environment.NewLine + "Restart app with admin privileges?";
|
|
if (WebServer.IsWindows)
|
|
{
|
|
var dialogResult = MessageBox.Show(errorStr, "Error", MessageBoxButtons.YesNo);
|
|
if (dialogResult == DialogResult.No)
|
|
{
|
|
Application.Exit();
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
// WebServer.RestartAsAdmin();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
WebServer.LoggerInstance.Fatal("Failed to start HTTP WebServer. " + ex.Message, ex);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
WebServer.LoggerInstance.Error(ex, "Error starting HTTP server: " + ex.Message);
|
|
return;
|
|
}
|
|
|
|
WebServer.LoggerInstance.Info("Server started on port " + Port);
|
|
WebServer.LoggerInstance.Info("Accepting only requests from local system: " + (!ListenPublic));
|
|
|
|
while (true)
|
|
{
|
|
Exception error = null;
|
|
try
|
|
{
|
|
error = null;
|
|
var context = await listener.GetContextAsync();
|
|
ProcessHttpRequest(context);
|
|
}
|
|
catch (ObjectDisposedException ex)
|
|
{
|
|
WebServer.LoggerInstance.Error(ex, "Critical error, HTTP listener was destroyed");
|
|
Process.GetCurrentProcess().Kill();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
error = ex;
|
|
WebServer.LoggerInstance.Error(ex, "Error processing HTTP request");
|
|
}
|
|
|
|
if (error != null)
|
|
await Task.Delay(TimeSpan.FromSeconds(5));
|
|
}
|
|
}
|
|
|
|
|
|
static void ReadSettingsFile()
|
|
{
|
|
var path = Path.Combine(AppConfigDirectory, "config.json");
|
|
if (!File.Exists(path))
|
|
{
|
|
JObject f = new JObject();
|
|
f.Add("port", WebServer.DefaultPort);
|
|
f.Add("public", true);
|
|
File.WriteAllText(path, f.ToString());
|
|
}
|
|
|
|
var configJson = JObject.Parse(File.ReadAllText(path));
|
|
int port = (int)configJson.GetValue("port");
|
|
WebServer.Port = port;
|
|
|
|
WebServer.ListenPublic = (bool)configJson.GetValue("public");
|
|
|
|
Console.WriteLine("Config file path: " + path);
|
|
}
|
|
|
|
public void Stop()
|
|
{
|
|
listener.Stop();
|
|
listener.Abort();
|
|
}
|
|
|
|
async void ProcessHttpRequest(HttpListenerContext context)
|
|
{
|
|
WebServer.LoggerInstance.Trace("Received request: " + context.Request.Url.ToString());
|
|
Exception exception = null;
|
|
try
|
|
{
|
|
if (await webApi.HandleRequest(context))
|
|
{
|
|
|
|
}
|
|
else if (context.Request.Url.AbsolutePath.StartsWith("/api/"))
|
|
{
|
|
await ProcessTorznab(context);
|
|
}
|
|
else
|
|
{
|
|
var responseBytes = Encoding.UTF8.GetBytes("Invalid request");
|
|
await context.Response.OutputStream.WriteAsync(responseBytes, 0, responseBytes.Length);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
exception = ex;
|
|
WebServer.LoggerInstance.Error(ex, ex.Message + ex.ToString());
|
|
}
|
|
|
|
if (exception != null)
|
|
{
|
|
try
|
|
{
|
|
var errorBytes = Encoding.UTF8.GetBytes(exception.Message);
|
|
await context.Response.OutputStream.WriteAsync(errorBytes, 0, errorBytes.Length);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
}
|
|
|
|
try
|
|
{
|
|
context.Response.Close();
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
|
|
}
|
|
|
|
async Task ProcessTorznab(HttpListenerContext context)
|
|
{
|
|
|
|
var query = HttpUtility.ParseQueryString(context.Request.Url.Query);
|
|
var inputStream = context.Request.InputStream;
|
|
|
|
var indexerId = context.Request.Url.Segments[2].TrimEnd('/').ToLower();
|
|
var indexer = indexerManager.GetIndexer(indexerId);
|
|
|
|
if (context.Request.Url.Segments.Length > 4 && context.Request.Url.Segments[3] == "download/")
|
|
{
|
|
var downloadSegment = HttpServerUtility.UrlTokenDecode(context.Request.Url.Segments[4].TrimEnd('/'));
|
|
var downloadLink = Encoding.UTF8.GetString(downloadSegment);
|
|
var downloadBytes = await indexer.Download(new Uri(downloadLink));
|
|
await context.Response.OutputStream.WriteAsync(downloadBytes, 0, downloadBytes.Length);
|
|
return;
|
|
}
|
|
|
|
var torznabQuery = TorznabQuery.FromHttpQuery(query);
|
|
|
|
if (torznabQuery.RageID != 0)
|
|
torznabQuery.ShowTitles = await sonarrApi.GetShowTitle(torznabQuery.RageID);
|
|
else if (!string.IsNullOrEmpty(torznabQuery.SearchTerm))
|
|
torznabQuery.ShowTitles = new string[] { torznabQuery.SearchTerm };
|
|
|
|
var releases = await indexer.PerformQuery(torznabQuery);
|
|
|
|
WebServer.LoggerInstance.Debug(string.Format("Found {0} releases from {1}", releases.Length, indexer.DisplayName));
|
|
|
|
var severUrl = string.Format("{0}://{1}:{2}/", context.Request.Url.Scheme, context.Request.Url.Host, context.Request.Url.Port);
|
|
|
|
var resultPage = new ResultPage(new ChannelInfo
|
|
{
|
|
Title = indexer.DisplayName,
|
|
Description = indexer.DisplayDescription,
|
|
Link = indexer.SiteLink,
|
|
ImageUrl = new Uri(severUrl + "logos/" + indexerId + ".png"),
|
|
ImageTitle = indexer.DisplayName,
|
|
ImageLink = indexer.SiteLink,
|
|
ImageDescription = indexer.DisplayName
|
|
});
|
|
|
|
// add Jackett proxy to download links...
|
|
foreach (var release in releases)
|
|
{
|
|
if (release.Link == null || release.Link.Scheme == "magnet")
|
|
continue;
|
|
var originalLink = release.Link;
|
|
var encodedLink = HttpServerUtility.UrlTokenEncode(Encoding.UTF8.GetBytes(originalLink.ToString())) + "/download.torrent";
|
|
var proxyLink = string.Format("{0}api/{1}/download/{2}", severUrl, indexerId, encodedLink);
|
|
release.Link = new Uri(proxyLink);
|
|
}
|
|
|
|
resultPage.Releases.AddRange(releases);
|
|
|
|
var xml = resultPage.ToXml(new Uri(severUrl));
|
|
|
|
var responseBytes = Encoding.UTF8.GetBytes(xml);
|
|
context.Response.ContentEncoding = Encoding.UTF8;
|
|
context.Response.ContentLength64 = responseBytes.LongLength;
|
|
context.Response.ContentType = "application/rss+xml";
|
|
await context.Response.OutputStream.WriteAsync(responseBytes, 0, responseBytes.Length);
|
|
|
|
}
|
|
|
|
public static JObject ReadServerSettingsFile()
|
|
{
|
|
var path = ServerConfigFile;
|
|
JObject jsonReply = new JObject();
|
|
if (File.Exists(path))
|
|
{
|
|
jsonReply = JObject.Parse(File.ReadAllText(path));
|
|
Port = (int)jsonReply["port"];
|
|
ListenPublic = (bool)jsonReply["public"];
|
|
}
|
|
else
|
|
{
|
|
jsonReply["port"] = Port;
|
|
jsonReply["public"] = ListenPublic;
|
|
}
|
|
return jsonReply;
|
|
}
|
|
|
|
public static Task<int> ApplyPortConfiguration(JToken json)
|
|
{
|
|
JObject jsonObject = (JObject)json;
|
|
JToken jJackettPort = jsonObject.GetValue("port");
|
|
int jackettPort;
|
|
if (!ServerUtil.IsPort(jJackettPort.ToString()))
|
|
throw new CustomException("The value entered is not a valid port");
|
|
else
|
|
jackettPort = int.Parse(jJackettPort.ToString());
|
|
|
|
if (jackettPort == Port)
|
|
throw new CustomException("The current port is the same as the one being used now.");
|
|
else if (ServerUtil.RestrictedPorts.Contains(jackettPort))
|
|
throw new CustomException("This port is not allowed due to it not being safe.");
|
|
SaveSettings(jackettPort);
|
|
|
|
return Task.FromResult(jackettPort);
|
|
}
|
|
|
|
private static string ServerConfigFile = Path.Combine(WebServer.AppConfigDirectory, "config.json");
|
|
|
|
private static void SaveSettings(int jacketPort)
|
|
{
|
|
JObject json = new JObject();
|
|
json["port"] = jacketPort;
|
|
json["public"] = ListenPublic;
|
|
File.WriteAllText(ServerConfigFile, json.ToString());
|
|
}
|
|
|
|
}*/
|
|
}
|