From 387f3670410ef61fc3e9d1243a5ffe49614026dd Mon Sep 17 00:00:00 2001 From: ThomasAmpen Date: Thu, 9 Jul 2015 21:38:12 +0200 Subject: [PATCH] Auth(Still disabled), chaing port, stripped special chars in serie name --- src/Jackett/ChromeUnsafePorts.cs | 78 ++++ src/Jackett/ExceptionWithConfigData.cs | 7 + src/Jackett/Indexers/ShowRSS.cs | 176 ++++++++ src/Jackett/Indexers/ThePirateBay.cs | 2 +- src/Jackett/Indexers/Torrentz.cs | 23 +- src/Jackett/Jackett.csproj | 44 +- src/Jackett/Program.cs | 15 + src/Jackett/Security.cs | 39 ++ src/Jackett/Server.cs | 231 ++++++++++ src/Jackett/WebApi.cs | 134 +++++- src/Jackett/WebContent/custom.css | 189 +++++++++ src/Jackett/WebContent/custom.js | 453 ++++++++++++++++++++ src/Jackett/WebContent/index.html | 515 ++--------------------- src/Jackett/WebContent/logos/showrss.png | Bin 0 -> 7560 bytes 14 files changed, 1402 insertions(+), 504 deletions(-) create mode 100644 src/Jackett/ChromeUnsafePorts.cs create mode 100644 src/Jackett/Indexers/ShowRSS.cs create mode 100644 src/Jackett/Security.cs create mode 100644 src/Jackett/WebContent/custom.css create mode 100644 src/Jackett/WebContent/custom.js create mode 100644 src/Jackett/WebContent/logos/showrss.png diff --git a/src/Jackett/ChromeUnsafePorts.cs b/src/Jackett/ChromeUnsafePorts.cs new file mode 100644 index 000000000..c40cdb51c --- /dev/null +++ b/src/Jackett/ChromeUnsafePorts.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Jackett +{ + public class ChromeUnsafePorts + { + public static int[] RestrictedPorts = new int[] { + 1, // tcpmux + 7, // echo + 9, // discard + 11, // systat + 13, // daytime + 15, // netstat + 17, // qotd + 19, // chargen + 20, // ftp data + 21, // ftp access + 22, // ssh + 23, // telnet + 25, // smtp + 37, // time + 42, // name + 43, // nicname + 53, // domain + 77, // priv-rjs + 79, // finger + 87, // ttylink + 95, // supdup + 101, // hostriame + 102, // iso-tsap + 103, // gppitnp + 104, // acr-nema + 109, // pop2 + 110, // pop3 + 111, // sunrpc + 113, // auth + 115, // sftp + 117, // uucp-path + 119, // nntp + 123, // NTP + 135, // loc-srv /epmap + 139, // netbios + 143, // imap2 + 179, // BGP + 389, // ldap + 465, // smtp+ssl + 512, // print / exec + 513, // login + 514, // shell + 515, // printer + 526, // tempo + 530, // courier + 531, // chat + 532, // netnews + 540, // uucp + 556, // remotefs + 563, // nntp+ssl + 587, // stmp? + 601, // ?? + 636, // ldap+ssl + 993, // ldap+ssl + 995, // pop3+ssl + 2049, // nfs + 3659, // apple-sasl / PasswordServer + 4045, // lockd + 6000, // X11 + 6665, // Alternate IRC [Apple addition] + 6666, // Alternate IRC [Apple addition] + 6667, // Standard IRC [Apple addition] + 6668, // Alternate IRC [Apple addition] + 6669, // Alternate IRC [Apple addition]}; + }; + } +} diff --git a/src/Jackett/ExceptionWithConfigData.cs b/src/Jackett/ExceptionWithConfigData.cs index 0585b0db4..f65ecb8cc 100644 --- a/src/Jackett/ExceptionWithConfigData.cs +++ b/src/Jackett/ExceptionWithConfigData.cs @@ -16,4 +16,11 @@ namespace Jackett ConfigData = data; } } + + public class CustomException : Exception + { + public CustomException(string message) + : base(message) + { } + } } diff --git a/src/Jackett/Indexers/ShowRSS.cs b/src/Jackett/Indexers/ShowRSS.cs new file mode 100644 index 000000000..50518f893 --- /dev/null +++ b/src/Jackett/Indexers/ShowRSS.cs @@ -0,0 +1,176 @@ +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using System.Web; +using System.Xml; + +namespace Jackett.Indexers +{ + public class ShowRSS : IndexerInterface + { + public event Action OnSaveConfigurationRequested; + + public event Action OnResultParsingError; + + public string DisplayName + { + get { return "ShowRSS"; } + } + + public string DisplayDescription + { + get { return "showRSS is a service that allows you to keep track of your favorite TV shows"; } + } + + public Uri SiteLink + { + get { return new Uri(DefaultUrl); } + } + + const string DefaultUrl = "http://showrss.info"; + const string searchAllUrl = DefaultUrl + "/feeds/all.rss"; + string BaseUrl; + static string chromeUserAgent = "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36"; + + CookieContainer cookies; + HttpClientHandler handler; + HttpClient client; + + public bool IsConfigured + { + get; + private set; + } + + public ShowRSS() + { + IsConfigured = false; + cookies = new CookieContainer(); + handler = new HttpClientHandler + { + CookieContainer = cookies, + AllowAutoRedirect = true, + UseCookies = true, + }; + client = new HttpClient(handler); + } + + public Task GetConfigurationForSetup() + { + var config = new ConfigurationDataUrl(DefaultUrl); + return Task.FromResult(config); + } + + public async Task ApplyConfiguration(Newtonsoft.Json.Linq.JToken configJson) + { + var config = new ConfigurationDataUrl(DefaultUrl); + config.LoadValuesFromJson(configJson); + + var formattedUrl = config.GetFormattedHostUrl(); + var releases = await PerformQuery(new TorznabQuery(), formattedUrl); + if (releases.Length == 0) + throw new Exception("Could not find releases from this URL"); + + BaseUrl = formattedUrl; + + var configSaveData = new JObject(); + configSaveData["base_url"] = BaseUrl; + + if (OnSaveConfigurationRequested != null) + OnSaveConfigurationRequested(this, configSaveData); + + IsConfigured = true; + } + + public void LoadFromSavedConfiguration(Newtonsoft.Json.Linq.JToken jsonConfig) + { + BaseUrl = (string)jsonConfig["base_url"]; + IsConfigured = true; + } + + public async Task PerformQuery(TorznabQuery query) + { + return await PerformQuery(query, BaseUrl); + } + + public Task Download(Uri link) + { + throw new NotImplementedException(); + } + + private WebClient getWebClient() + { + WebClient wc = new WebClient(); + WebHeaderCollection headers = new WebHeaderCollection(); + headers.Add("User-Agent", chromeUserAgent); + wc.Headers = headers; + return wc; + } + + async Task PerformQuery(TorznabQuery query, string baseUrl) + { + List releases = new List(); + + foreach (var title in query.ShowTitles ?? new string[] { string.Empty }) + { + var searchString = title + " " + query.GetEpisodeSearchString(); + var episodeSearchUrl = string.Format(searchAllUrl); + + XmlDocument xmlDoc = new XmlDocument(); + string xml = string.Empty; + WebClient wc = getWebClient(); + + try + { + using (wc) + { + xml = wc.DownloadString(episodeSearchUrl); + xmlDoc.LoadXml(xml); + } + + ReleaseInfo release; + TorrentzHelper td; + string serie_title; + + foreach (XmlNode node in xmlDoc.GetElementsByTagName("item")) + { + release = new ReleaseInfo(); + + release.MinimumRatio = 1; + release.MinimumSeedTime = 172800; + + serie_title = node.SelectSingleNode("title").InnerText; + release.Title = serie_title; + + release.Comments = new Uri(node.SelectSingleNode("link").InnerText); + release.Category = node.SelectSingleNode("title").InnerText; + var test = node.SelectSingleNode("enclosure"); + release.Guid = new Uri(test.Attributes["url"].Value); + release.PublishDate = DateTime.Parse(node.SelectSingleNode("pubDate").InnerText, CultureInfo.InvariantCulture); + + release.Description = node.SelectSingleNode("description").InnerText; + release.InfoHash = node.SelectSingleNode("description").InnerText; + release.Size = 0; + release.Seeders = 1; + release.Peers = 1; + release.MagnetUri = new Uri(node.SelectSingleNode("link").InnerText); + releases.Add(release); + } + } + catch (Exception ex) + { + OnResultParsingError(this, xml, ex); + throw ex; + } + } + + return releases.ToArray(); + } + } +} diff --git a/src/Jackett/Indexers/ThePirateBay.cs b/src/Jackett/Indexers/ThePirateBay.cs index 511486fb0..715aef264 100644 --- a/src/Jackett/Indexers/ThePirateBay.cs +++ b/src/Jackett/Indexers/ThePirateBay.cs @@ -168,7 +168,7 @@ namespace Jackett.Indexers release.InfoHash = release.MagnetUri.ToString().Split(':')[3].Split('&')[0]; var sizeString = row.ChildElements.ElementAt(4).Cq().Text().Split(' '); - var sizeVal = float.Parse(sizeString[0]); + var sizeVal = float.Parse(sizeString[0], CultureInfo.InvariantCulture); var sizeUnit = sizeString[1]; release.Size = ReleaseInfo.GetBytes(sizeUnit, sizeVal); diff --git a/src/Jackett/Indexers/Torrentz.cs b/src/Jackett/Indexers/Torrentz.cs index c9109055a..78b60edf1 100644 --- a/src/Jackett/Indexers/Torrentz.cs +++ b/src/Jackett/Indexers/Torrentz.cs @@ -122,7 +122,8 @@ namespace Jackett.Indexers } ReleaseInfo release; - TorrentzDescription td; + TorrentzHelper td; + string serie_title; foreach (XmlNode node in xmlDoc.GetElementsByTagName("item")) { @@ -130,20 +131,21 @@ namespace Jackett.Indexers release.MinimumRatio = 1; release.MinimumSeedTime = 172800; - release.Title = node.SelectSingleNode("title").InnerText; + serie_title = node.SelectSingleNode("title").InnerText; + release.Title = serie_title; release.Comments = new Uri(node.SelectSingleNode("link").InnerText); release.Category = node.SelectSingleNode("category").InnerText; release.Guid = new Uri(node.SelectSingleNode("guid").InnerText); release.PublishDate = DateTime.Parse(node.SelectSingleNode("pubDate").InnerText, CultureInfo.InvariantCulture); - td = new TorrentzDescription(node.SelectSingleNode("description").InnerText); + td = new TorrentzHelper(node.SelectSingleNode("description").InnerText); release.Description = td.Description; release.InfoHash = td.hash; release.Size = td.Size; release.Seeders = td.Seeders; release.Peers = td.Peers; - release.MagnetUri = new Uri("https://torrage.com/torrent/" + td.hash.ToUpper() + ".torrent"); + release.MagnetUri = TorrentzHelper.createMagnetLink(td.hash, serie_title); releases.Add(release); } } @@ -176,9 +178,9 @@ namespace Jackett.Indexers } - public class TorrentzDescription + public class TorrentzHelper { - public TorrentzDescription(string description) + public TorrentzHelper(string description) { this.Description = description; if (null == description) @@ -193,6 +195,15 @@ namespace Jackett.Indexers FillProperties(); } + public static Uri createMagnetLink(string hash, string title) + { + string MagnetLink = "magnet:?xt=urn:btih:{0}&dn={1}&tr={2}"; + string Trackers = WebUtility.UrlEncode("udp://tracker.publicbt.com:80&tr=udp://tracker.openbittorrent.com:80&tr=udp://tracker.ccc.de:80&tr=udp://tracker.istole.it:80"); + title = WebUtility.UrlEncode(title); + + return new Uri(string.Format(MagnetLink, hash, title, Trackers)); + } + private void FillProperties() { string description = this.Description; diff --git a/src/Jackett/Jackett.csproj b/src/Jackett/Jackett.csproj index 2c895d48b..fbb0ebdeb 100644 --- a/src/Jackett/Jackett.csproj +++ b/src/Jackett/Jackett.csproj @@ -12,6 +12,7 @@ v4.5.1 512 + false publish\ true Disk @@ -24,7 +25,6 @@ true 0 1.0.0.%2a - false false true @@ -82,6 +82,7 @@ + @@ -98,6 +99,7 @@ + @@ -119,6 +121,7 @@ + @@ -147,14 +150,23 @@ - + PreserveNewest + + PreserveNewest + + + Always + + + Always + - PreserveNewest + Always - PreserveNewest + Always @@ -182,10 +194,10 @@ PreserveNewest - PreserveNewest + Always - PreserveNewest + Always PreserveNewest @@ -203,33 +215,35 @@ PreserveNewest - PreserveNewest + Always - PreserveNewest + Always - PreserveNewest + Always - PreserveNewest + Always - PreserveNewest + Always - PreserveNewest + Always - PreserveNewest + Always + + + Always - PreserveNewest - PreserveNewest + Always diff --git a/src/Jackett/Program.cs b/src/Jackett/Program.cs index 52c0d18d5..c6a8b93cc 100644 --- a/src/Jackett/Program.cs +++ b/src/Jackett/Program.cs @@ -93,6 +93,8 @@ namespace Jackett await ServerInstance.Start(); }); + + try { if (Program.IsWindows) @@ -111,6 +113,19 @@ namespace Jackett Console.WriteLine("Server thread exit"); } + public static void RestartServer() + { + + ServerInstance.Stop(); + ServerInstance = null; + var serverTask = Task.Run(async () => + { + ServerInstance = new Server(); + await ServerInstance.Start(); + }); + Task.WaitAll(serverTask); + } + static void ReadSettingsFile() { var path = Path.Combine(AppConfigDirectory, "config.json"); diff --git a/src/Jackett/Security.cs b/src/Jackett/Security.cs new file mode 100644 index 000000000..4317d217f --- /dev/null +++ b/src/Jackett/Security.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace Jackett +{ + public class Security + { + public static string HashMD5(string value) + { + MD5 md5 = MD5.Create(); + byte[] inputBytes = Encoding.ASCII.GetBytes(value); + byte[] hash = md5.ComputeHash(inputBytes); + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < hash.Length; i++) + { + sb.Append(hash[i].ToString("X2")); + } + return sb.ToString(); + } + + public static string Base64Encode(string plainText) + { + var plainTextBytes = Encoding.UTF8.GetBytes(plainText); + return Convert.ToBase64String(plainTextBytes); + } + + public static string Base64Decode(string base64EncodedData) + { + var base64EncodedBytes = Convert.FromBase64String(base64EncodedData); + return Encoding.UTF8.GetString(base64EncodedBytes); + } + + } +} diff --git a/src/Jackett/Server.cs b/src/Jackett/Server.cs index 3931b8137..9584a47bc 100644 --- a/src/Jackett/Server.cs +++ b/src/Jackett/Server.cs @@ -7,6 +7,7 @@ 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; @@ -19,6 +20,11 @@ namespace Jackett public static int Port = DefaultPort; public static bool ListenPublic = true; + private static bool isAuthenticated = false; + private static bool isAuthEnabled = false; + private static string Username = ""; + private static string Password = ""; + HttpListener listener; IndexerManager indexerManager; WebApi webApi; @@ -30,6 +36,7 @@ namespace Jackett // 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(); @@ -67,7 +74,12 @@ namespace Jackett listener.Prefixes.Add(string.Format("http://127.0.0.1:{0}/", Port)); } + if (isAuthEnabled && HttpListener.IsSupported) + listener.AuthenticationSchemes = AuthenticationSchemes.Basic; + listener.Start(); + + webApi.server = this; } catch (HttpListenerException ex) { @@ -109,6 +121,13 @@ namespace Jackett error = null; var context = await listener.GetContextAsync(); ProcessHttpRequest(context); + + if (isAuthEnabled && !isAuthenticated && HttpListener.IsSupported) + { + IAsyncResult result = listener.BeginGetContext(new AsyncCallback(ListenerCallback), listener); + result.AsyncWaitHandle.WaitOne(); + } + } catch (ObjectDisposedException ex) { @@ -126,6 +145,43 @@ namespace Jackett } } + + private static void ListenerCallback(IAsyncResult ar) + { + try + { + HttpListener listener = (HttpListener)ar.AsyncState; + HttpListenerContext context = listener.EndGetContext(ar); + HttpListenerBasicIdentity identity = (HttpListenerBasicIdentity)context.User.Identity; + + + if (Security.Base64Encode(identity.Name) != Username || Security.Base64Encode(identity.Password) != Password) + context.Response.StatusCode = 401; + + + if (context.Response.StatusCode != 401) + { + context.Response.StatusCode = 200; + context.Response.StatusDescription = "OK"; + context.Response.Headers["StatusDescription"] = "OK"; + isAuthenticated = true; + } + + try + { + context.Response.Close(); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + } + public void Stop() { listener.Stop(); @@ -205,6 +261,15 @@ namespace Jackett else if (!string.IsNullOrEmpty(torznabQuery.SearchTerm)) torznabQuery.ShowTitles = new string[] { torznabQuery.SearchTerm }; + //Replacing non-alphanumeric characters with an empty string + if (torznabQuery.ShowTitles != null) + for (int i = 0; i < torznabQuery.ShowTitles.Length; i++) + { + char[] arr = torznabQuery.ShowTitles[i].ToCharArray(); + arr = Array.FindAll(arr, (c => (char.IsLetterOrDigit(c) || char.IsWhiteSpace(c) || c == '-' || c == '@'))); + torznabQuery.ShowTitles[i] = new string(arr); + } + var releases = await indexer.PerformQuery(torznabQuery); Program.LoggerInstance.Debug(string.Format("Found {0} releases from {1}", releases.Length, indexer.DisplayName)); @@ -245,6 +310,172 @@ namespace Jackett } + private static string ServerConfigFile = Path.Combine(Program.AppConfigDirectory, "config.json"); + + public 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"]; + Username = (string)jsonReply["Username"]; + Password = (string)jsonReply["Password"]; + isAuthEnabled = (!String.IsNullOrEmpty(Username) && !String.IsNullOrEmpty(Password)); + } + else + { + jsonReply["port"] = Port; + jsonReply["public"] = ListenPublic; + jsonReply["Username"] = Username; + jsonReply["Password"] = Password; + } + return jsonReply; + } + + public Task ApplyPortConfiguration(JToken json) + { + JObject jsonObject = (JObject)json; + JToken jJackettPort = jsonObject.GetValue("port"); + int jackettPort; + if (!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 (ChromeUnsafePorts.RestrictedPorts.Contains(jackettPort)) + throw new CustomException("This port is not allowed due to it not being safe."); + SaveSettings(jackettPort); + + return Task.FromResult(jackettPort); + } + + + public async Task ApplyAuthConfiguration(JToken json) + { + JObject jsonObject = (JObject)json; + JToken jUsername = jsonObject.GetValue("username"); + JToken jPassword = jsonObject.GetValue("password"); + + if (String.IsNullOrWhiteSpace(jUsername.ToString())) + throw new CustomException("Your username can not be empty."); + else if (String.IsNullOrWhiteSpace(jPassword.ToString())) + throw new CustomException("Your password can not be empty."); + else + { + Username = Security.Base64Encode(jUsername.ToString()); + Password = Security.Base64Encode(jPassword.ToString()); + + isAuthEnabled = true; + isAuthenticated = false; + SaveAuthSettings(); + listener.AuthenticationSchemes = AuthenticationSchemes.Basic; + } + + } + + async Task ReadPostDataJson(Stream stream) + { + string postData = await new StreamReader(stream).ReadToEndAsync(); + return JObject.Parse(postData); + } + + private void SaveAuthSettings() + { + JObject json = new JObject(); + json["port"] = Port; + json["public"] = ListenPublic; + json["Password"] = Password; + json["Username"] = Username; + File.WriteAllText(ServerConfigFile, json.ToString()); + } + + private void SaveSettings(int jacketPort) + { + JObject json = new JObject(); + json["port"] = jacketPort; + json["public"] = ListenPublic; + json["Password"] = Password; + json["Username"] = Username; + File.WriteAllText(ServerConfigFile, json.ToString()); + } + + public static bool IsPort(string value) + { + if (string.IsNullOrEmpty(value)) + return false; + + Regex numeric = new Regex(@"^[0-9]+$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + if (numeric.IsMatch(value)) + { + try + { + if (Convert.ToInt32(value) < 65536) + return true; + } + catch (OverflowException) + { + } + } + + return false; + } + + + public ConfigurationAuthentication GetConfiguration() + { + var config = new ConfigurationAuthentication(); + config.Username.Value = Security.Base64Decode(Username); + config.Password.Value = Security.Base64Decode(Password); + + return config; + } + + public async Task ApplyConfiguration(JToken configJson) + { + var config = new ConfigurationAuthentication(); + config.LoadValuesFromJson(configJson); + Username = config.Username.Value; + Password = config.Password.Value; + isAuthEnabled = true; + SaveAuthSettings(); + + } + + public async Task RemoveAuthConfig() + { + Username = ""; + Password = ""; + isAuthEnabled = false; + isAuthenticated = false; + SaveAuthSettings(); + } + + + public class ConfigurationAuthentication : ConfigurationData + { + public StringItem Username { get; private set; } + public StringItem Password { get; private set; } + + DisplayItem ApiInfo; + + public ConfigurationAuthentication() + { + Username = new StringItem { Name = "Username", Value = Server.Username }; + Password = new StringItem { Name = "Password", Value = Server.Password }; + } + + public override Item[] GetItems() + { + return new Item[] { Username, Password }; + } + + } } diff --git a/src/Jackett/WebApi.cs b/src/Jackett/WebApi.cs index 8b6a6b543..ca1d4c6a1 100644 --- a/src/Jackett/WebApi.cs +++ b/src/Jackett/WebApi.cs @@ -15,6 +15,8 @@ namespace Jackett { static string WebContentFolder = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "WebContent"); static string[] StaticFiles = Directory.EnumerateFiles(WebContentFolder, "*", SearchOption.AllDirectories).ToArray(); + public Server server; + public enum WebApiMethod { @@ -25,7 +27,13 @@ namespace Jackett DeleteIndexer, GetSonarrConfig, ApplySonarrConfig, - TestSonarr + TestSonarr, + GetJackettConfig, + ApplyJackettConfig, + JackettRestart, + ApplyAuthenticationConfig, + GetAuthenticationConfig, + RemoveAuthenticationConfig, } static Dictionary WebApiMethods = new Dictionary { @@ -35,8 +43,14 @@ namespace Jackett { "test_indexer", WebApiMethod.TestIndexer }, { "delete_indexer", WebApiMethod.DeleteIndexer }, { "get_sonarr_config", WebApiMethod.GetSonarrConfig }, + { "get_jackett_config",WebApiMethod.GetJackettConfig}, + { "apply_jackett_config",WebApiMethod.ApplyJackettConfig}, { "apply_sonarr_config", WebApiMethod.ApplySonarrConfig }, - { "test_sonarr", WebApiMethod.TestSonarr } + { "jackett_restart", WebApiMethod.JackettRestart }, + { "test_sonarr", WebApiMethod.TestSonarr }, + { "apply_authentication_config", WebApiMethod.ApplyAuthenticationConfig}, + { "get_authentication_config", WebApiMethod.GetAuthenticationConfig}, + {"remove_authentication_config", WebApiMethod.RemoveAuthenticationConfig}, }; IndexerManager indexerManager; @@ -123,9 +137,27 @@ namespace Jackett case WebApiMethod.ApplySonarrConfig: handlerTask = HandleApplySonarrConfig; break; + case WebApiMethod.ApplyJackettConfig: + handlerTask = HandleApplyJackettConfig; + break; case WebApiMethod.TestSonarr: handlerTask = HandleTestSonarr; break; + case WebApiMethod.GetJackettConfig: + handlerTask = HandleJackettConfig; + break; + case WebApiMethod.JackettRestart: + handlerTask = HandleJackettRestart; + break; + case WebApiMethod.ApplyAuthenticationConfig: + handlerTask = HandleApplyAuthenticationConfig; + break; + case WebApiMethod.GetAuthenticationConfig: + handlerTask = HandleGetAuthenticationConfig; + break; + case WebApiMethod.RemoveAuthenticationConfig: + handlerTask = HandlerRemoveAuthenticationConfig; + break; default: handlerTask = HandleInvalidApiMethod; break; @@ -134,6 +166,23 @@ namespace Jackett await ReplyWithJson(context, jsonReply, method.ToString()); } + async Task HandlerRemoveAuthenticationConfig(HttpListenerContext context) + { + JToken jsonReply = new JObject(); + try + { + await server.RemoveAuthConfig(); + jsonReply["result"] = "success"; + } + catch (Exception ex) + { + jsonReply["result"] = "error"; + jsonReply["error"] = ex.Message; + } + return jsonReply; + } + + async Task ReplyWithJson(HttpListenerContext context, JToken json, string apiCall) { try @@ -163,6 +212,45 @@ namespace Jackett return jsonReply; } + async Task HandleJackettRestart(HttpListenerContext context) + { + Program.RestartServer(); + return null; + } + + async Task HandleApplyAuthenticationConfig(HttpListenerContext context) + { + JToken jsonReply = new JObject(); + try + { + var postData = await ReadPostDataJson(context.Request.InputStream); + await server.ApplyAuthConfiguration(postData); + jsonReply["result"] = "success"; + } + catch (Exception ex) + { + jsonReply["result"] = "error"; + jsonReply["error"] = ex.Message; + } + return jsonReply; + } + + Task HandleGetAuthenticationConfig(HttpListenerContext context) + { + JObject jsonReply = new JObject(); + try + { + jsonReply["config"] = server.GetConfiguration().ToJson(); + jsonReply["result"] = "success"; + } + catch (Exception ex) + { + jsonReply["result"] = "error"; + jsonReply["error"] = ex.Message; + } + return Task.FromResult(jsonReply); + } + async Task HandleApplySonarrConfig(HttpListenerContext context) { JToken jsonReply = new JObject(); @@ -180,6 +268,47 @@ namespace Jackett return jsonReply; } + async Task HandleApplyJackettConfig(HttpListenerContext context) + { + JToken jsonReply = new JObject(); + + try + { + var postData = await ReadPostDataJson(context.Request.InputStream); + int port = await server.ApplyPortConfiguration(postData); + jsonReply["result"] = "success"; + jsonReply["port"] = port; + } + catch (Exception ex) + { + jsonReply["result"] = "error"; + jsonReply["error"] = ex.Message; + } + return jsonReply; + } + + Task HandleJackettConfig(HttpListenerContext context) + { + JObject jsonReply = new JObject(); + try + { + jsonReply["config"] = server.ReadServerSettingsFile(); + jsonReply["result"] = "success"; + } + catch (CustomException ex) + { + jsonReply["result"] = "error"; + jsonReply["error"] = ex.Message; + } + catch (Exception ex) + { + jsonReply["result"] = "error"; + jsonReply["error"] = ex.Message; + } + return Task.FromResult(jsonReply); + } + + Task HandleGetSonarrConfig(HttpListenerContext context) { JObject jsonReply = new JObject(); @@ -257,6 +386,7 @@ namespace Jackett { jsonReply["result"] = "success"; jsonReply["api_key"] = ApiKey.CurrentKey; + jsonReply["jackett_port"] = Server.Port; JArray items = new JArray(); foreach (var i in indexerManager.Indexers) { diff --git a/src/Jackett/WebContent/custom.css b/src/Jackett/WebContent/custom.css new file mode 100644 index 000000000..fea3cff3d --- /dev/null +++ b/src/Jackett/WebContent/custom.css @@ -0,0 +1,189 @@ + body { + background-image: url("binding_dark.png"); + background-repeat: repeat; + } + + #page { + border-radius: 6px; + background-color: white; + max-width: 900px; + margin: 0 auto; + margin-top: 30px; + padding: 20px; + margin-bottom: 100px; + } + + .container-fluid { + } + + #templates { + display: none; + } + + .card { + background-color: #f9f9f9; + border-radius: 6px; + box-shadow: 1px 1px 5px 2px #cdcdcd; + padding: 10px; + width: 260px; + display: inline-block; + vertical-align: top; + margin: 10px; + } + + .unconfigured-indexer { + height: 170px; + } + + .indexer { + height: 230px; + } + + .add-indexer { + border: 0; + } + + .indexer-logo { + text-align: center; + } + + .indexer-logo > img { + border: 1px solid #828282; + } + + .indexer-name > h3 { + margin-top: 13px; + text-align: center; + } + + .indexer-buttons { + text-align: center; + } + + .indexer-buttons > .btn { + margin-bottom: 10px; + } + + + .indexer-button-test { + width: 60px; + } + + .indexer-add-content { + color: gray; + text-align: center; + } + + .indexer-add-content > .glyphicon { + font-size: 50px; + vertical-align: bottom; + } + + .indexer-add-content > .light-text { + margin-top: 11px; + font-size: 18px; + margin-left: -5px; + } + + + .indexer-host > input { + font-size: 12px; + padding: 2px; + } + + .setup-item-inputstring { + max-width: 260px; + } + + .spinner { + -webkit-animation: spin 2s infinite linear; + -moz-animation: spin 2s infinite linear; + -o-animation: spin 2s infinite linear; + animation: spin 2s infinite linear; + } + + @-moz-keyframes spin { + from { + -moz-transform: rotate(0deg); + } + + to { + -moz-transform: rotate(360deg); + } + } + + @-webkit-keyframes spin { + from { + -webkit-transform: rotate(0deg); + } + + to { + -webkit-transform: rotate(360deg); + } + } + + @keyframes spin { + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } + } + + #setup-indexer-go { + width: 70px; + } + + hr { + border-top-color: #cdcdcd; + } + + .input-area { + } + + .input-area > * { + vertical-align: middle; + } + + .input-area > p { + margin-top: 10px; + } + + .input-header { + font-size: 18px; + width: 140px; + display: inline-block; + } + + .input-right { + width: 300px; + display: inline-block; + font-family: monospace; + } + + #sonarr-warning { + display: none; + } + + #logo { + max-width: 50px; + } + + #header-title { + font-size: 34px; + vertical-align: middle; + padding-left: 15px; + } + + +.floatleft { +float:left +} + +#authentication-status { + display: inline-block; + width: 300px; + margin-left: 4px; +} \ No newline at end of file diff --git a/src/Jackett/WebContent/custom.js b/src/Jackett/WebContent/custom.js new file mode 100644 index 000000000..cd81aadd6 --- /dev/null +++ b/src/Jackett/WebContent/custom.js @@ -0,0 +1,453 @@ + + +reloadIndexers(); +loadJackettSettings(); +loadAuthenticationStatus(); +loadSonarrInfo(); + +function loadSonarrInfo() { + getSonarrConfig(function (data) { + $("#sonarr-host").val(""); + var host, port, apiKey; + for (var i = 0; i < data.config.length; i++) { + if (data.config[i].id == "host") + host = data.config[i].value; + if (data.config[i].id == "port") + port = data.config[i].value; + if (data.config[i].id == "apikey") + apiKey = data.config[i].value; + } + if (!apiKey) + $("#sonarr-warning").show(); + else { + $("#sonarr-warning").hide(); + $("#sonarr-host").val(host + ":" + port); + } + }); +} + +function loadJackettSettings() { + getJackettConfig(function (data) { + console.log(data); + $("#jackett-port").val(data.config.port); + }); +} + +function getJackettConfig(callback) { + var jqxhr = $.get("get_jackett_config", function (data) { + + callback(data); + console.log(data); + }).fail(function () { + doNotify("Error loading Jackett settings, request to Jackett server failed", "danger", "glyphicon glyphicon-alert"); + }); +} + +function loadAuthenticationStatus() +{ + getAuthenticationConfig(function (data) { + var config = data.config; + if (config[0].value && config[1].value) + $("#authentication-status").text("Enabled"); + else + $("#authentication-status").text("Disabled"); + }); +} + +function getSonarrConfig(callback) { + var jqxhr = $.get("get_sonarr_config", function (data) { + callback(data); + }).fail(function () { + doNotify("Error loading Sonarr API configuration, request to Jackett server failed", "danger", "glyphicon glyphicon-alert"); + }); +} + + +$("#sonarr-settings").click(function () { + getSonarrConfig(function (data) { + var config = data.config; + + var configForm = newConfigModal("Sonarr API", config); + + var $goButton = configForm.find(".setup-indexer-go"); + $goButton.click(function () { + var data = getConfigModalJson(configForm); + + var originalBtnText = $goButton.html(); + $goButton.prop('disabled', true); + $goButton.html($('#templates > .spinner')[0].outerHTML); + + var jqxhr = $.post("apply_sonarr_config", JSON.stringify(data), function (data) { + if (data.result == "error") { + if (data.config) { + populateSetupForm(data.indexer, data.name, data.config); + } + doNotify("Configuration failed: " + data.error, "danger", "glyphicon glyphicon-alert"); + } + else { + configForm.modal("hide"); + loadSonarrInfo(); + doNotify("Successfully configured Sonarr API", "success", "glyphicon glyphicon-ok"); + } + }).fail(function () { + doNotify("Request to Jackett server failed", "danger", "glyphicon glyphicon-alert"); + }).always(function () { + $goButton.html(originalBtnText); + $goButton.prop('disabled', false); + }); + }); + + configForm.modal("show"); + + }); +}); + +//$("#Authenticate-jackett").click(function () { +// getAuthenticationConfig(function (data) { +// var config = data.config; +// var configForm = newAuthenticationModal("Authenication", config); + +// var $removeButton = configForm.find(".remove-auth"); +// $removeButton.click(function () { +// var originalBtnText = $goButton.html(); +// var jqxhr = $.post("remove_authentication_config", JSON.stringify(data), function (data) { +// if (data.result == "error") +// doNotify("Authentication configuration failed: " + data.error, "danger", "glyphicon glyphicon-alert"); +// else { +// loadAuthenticationStatus(); +// configForm.modal("hide"); +// doNotify("Successfully removed authentication", "success", "glyphicon glyphicon-ok"); +// } + +// }).fail(function () { +// doNotify("Request to Jackett server failed", "danger", "glyphicon glyphicon-alert"); +// }).always(function () { +// $removeButton.html(originalBtnText); +// $removeButton.prop('disabled', false); +// }); + + +// }); + +// var $goButton = configForm.find(".setup-indexer-go"); +// $goButton.click(function () { +// var data = getConfigModalJson(configForm); +// var originalBtnText = $goButton.html(); +// $goButton.prop('disabled', true); +// $goButton.html($('#templates > .spinner')[0].outerHTML); + +// var jqxhr = $.post("apply_authentication_config", JSON.stringify(data), function (data) { + +// if (data.result == "error") { +// if (data.config) { +// populateSetupForm(data.indexer, data.name, data.config); +// } +// doNotify("Authentication configuration failed: " + data.error, "danger", "glyphicon glyphicon-alert"); +// } +// else { +// loadAuthenticationStatus(); +// restartJackett(); +// configForm.modal("hide"); +// doNotify("Successfully configured authentication", "success", "glyphicon glyphicon-ok"); +// } + + +// }).fail(function () { +// doNotify("Request to Jackett server failed", "danger", "glyphicon glyphicon-alert"); +// }).always(function () { +// $goButton.html(originalBtnText); +// $goButton.prop('disabled', false); +// }); + + +// }); + +// configForm.modal("show"); + +// }); +//}); + +function restartJackett() { + var jqxhr0 = $.post("jackett_restart", null, function (data_restart) { }); + console.log("restart"); + window.setTimeout(function () { + url = window.location.href; + window.location.href = url; + + }, 3000); +} + + +function getAuthenticationConfig(callback) { + var jqxhr = $.get("get_authentication_config", function (data) { + callback(data); + }).fail(function () { + doNotify("Error loading authentication configuration, request to Jackett server failed", "danger", "glyphicon glyphicon-alert"); + }); +} + + +$("#change-jackett-port").click(function () { + var jackett_port = $("#jackett-port").val(); + var jsonObject = JSON.parse('{"port":"'+jackett_port+'"}'); + + var jqxhr = $.post("apply_jackett_config", JSON.stringify(jsonObject), function (data) { + + if (data.result == "error") { + doNotify("Error: " + data.error, "danger", "glyphicon glyphicon-alert"); + return; + } else { + doNotify("The port has been changed. Jackett will now restart...", "success", "glyphicon glyphicon-ok"); + var jqxhr0 = $.post("jackett_restart", null, function (data_restart) { }); + + window.setTimeout(function () { + url = window.location.href; + window.location.href = url.substr(0,url.lastIndexOf(":")+1) + data.port; + + }, 3000); + + } + }).fail(function () { + doNotify("Request to Jackett server failed", "danger", "glyphicon glyphicon-alert"); + }); + }); + + $("#sonarr-test").click(function () { + var jqxhr = $.get("get_indexers", function (data) { + if (data.result == "error") + doNotify("Test failed for Sonarr API\n" + data.error, "danger", "glyphicon glyphicon-alert"); + else + doNotify("Test successful for Sonarr API", "success", "glyphicon glyphicon-ok"); + }).fail(function () { + doNotify("Error testing Sonarr, request to Jackett server failed", "danger", "glyphicon glyphicon-alert"); + }); + }); + + + + + function reloadIndexers() { + $('#indexers').hide(); + $('#indexers > .indexer').remove(); + $('#unconfigured-indexers').empty(); + var jqxhr = $.get("get_indexers", function (data) { + $("#api-key-input").val(data.api_key); + displayIndexers(data.items); + }).fail(function () { + doNotify("Error loading indexers, request to Jackett server failed", "danger", "glyphicon glyphicon-alert"); + }); + } + + function displayIndexers(items) { + var indexerTemplate = Handlebars.compile($("#templates > .configured-indexer")[0].outerHTML); + var unconfiguredIndexerTemplate = Handlebars.compile($("#templates > .unconfigured-indexer")[0].outerHTML); + for (var i = 0; i < items.length; i++) { + var item = items[i]; + item.torznab_host = resolveUrl("/api/" + item.id); + if (item.configured) + $('#indexers').append(indexerTemplate(item)); + else + $('#unconfigured-indexers').append($(unconfiguredIndexerTemplate(item))); + } + + var addIndexerButton = $("#templates > .add-indexer")[0].outerHTML; + $('#indexers').append(addIndexerButton); + + $('#indexers').fadeIn(); + prepareSetupButtons(); + prepareTestButtons(); + prepareDeleteButtons(); + } + + function prepareDeleteButtons() { + $(".indexer-button-delete").each(function (i, btn) { + var $btn = $(btn); + var id = $btn.data("id"); + $btn.click(function () { + var jqxhr = $.post("delete_indexer", JSON.stringify({ indexer: id }), function (data) { + if (data.result == "error") { + doNotify("Delete error for " + id + "\n" + data.error, "danger", "glyphicon glyphicon-alert"); + } + else { + doNotify("Deleted " + id, "success", "glyphicon glyphicon-ok"); + } + }).fail(function () { + doNotify("Error deleting indexer, request to Jackett server error", "danger", "glyphicon glyphicon-alert"); + }).always(function () { + reloadIndexers(); + }); + }); + }); + } + + function prepareSetupButtons() { + $('.indexer-setup').each(function (i, btn) { + var $btn = $(btn); + var id = $btn.data("id"); + $btn.click(function () { + displayIndexerSetup(id); + }); + }); + } + + function prepareTestButtons() { + $(".indexer-button-test").each(function (i, btn) { + var $btn = $(btn); + var id = $btn.data("id"); + $btn.click(function () { + doNotify("Test started for " + id, "info", "glyphicon glyphicon-transfer"); + var jqxhr = $.post("test_indexer", JSON.stringify({ indexer: id }), function (data) { + if (data.result == "error") { + doNotify("Test failed for " + data.name + "\n" + data.error, "danger", "glyphicon glyphicon-alert"); + } + else { + doNotify("Test successful for " + data.name, "success", "glyphicon glyphicon-ok"); + } + }).fail(function () { + doNotify("Error testing indexer, request to Jackett server error", "danger", "glyphicon glyphicon-alert"); + }); + }); + }); + } + + function displayIndexerSetup(id) { + + var jqxhr = $.post("get_config_form", JSON.stringify({ indexer: id }), function (data) { + if (data.result == "error") { + doNotify("Error: " + data.error, "danger", "glyphicon glyphicon-alert"); + return; + } + populateSetupForm(id, data.name, data.config); + + }).fail(function () { + doNotify("Request to Jackett server failed", "danger", "glyphicon glyphicon-alert"); + }); + + $("#select-indexer-modal").modal("hide"); + } + + function populateConfigItems(configForm, config) { + var $formItemContainer = configForm.find(".config-setup-form"); + $formItemContainer.empty(); + var setupItemTemplate = Handlebars.compile($("#templates > .setup-item")[0].outerHTML); + for (var i = 0; i < config.length; i++) { + var item = config[i]; + var setupValueTemplate = Handlebars.compile($("#templates > .setup-item-" + item.type)[0].outerHTML); + item.value_element = setupValueTemplate(item); + $formItemContainer.append(setupItemTemplate(item)); + } + } + + + function newAuthenticationModal(title, config) + { + //config-authentication-modal + var configTemplate = Handlebars.compile($("#templates > .config-authentication-modal")[0].outerHTML); + var configForm = $(configTemplate({ title: title })); + + $("#modals").append(configForm); + + populateConfigItems(configForm, config); + + return configForm; + } + + function newConfigModal(title, config) { + //config-setup-modal + var configTemplate = Handlebars.compile($("#templates > .config-setup-modal")[0].outerHTML); + var configForm = $(configTemplate({ title: title })); + + $("#modals").append(configForm); + + populateConfigItems(configForm, config); + + return configForm; + //modal.remove(); + } + + function getConfigModalJson(configForm) { + var configJson = {}; + configForm.find(".config-setup-form").children().each(function (i, el) { + $el = $(el); + var type = $el.data("type"); + var id = $el.data("id"); + switch (type) { + case "inputstring": + configJson[id] = $el.find(".setup-item-inputstring").val(); + break; + case "inputbool": + configJson[id] = $el.find(".setup-item-checkbox").val(); + break; + } + }); + return configJson; + } + + function populateSetupForm(indexerId, name, config) { + + var configForm = newConfigModal(name, config); + + var $goButton = configForm.find(".setup-indexer-go"); + $goButton.click(function () { + var data = { indexer: indexerId, name: name }; + data.config = getConfigModalJson(configForm); + + var originalBtnText = $goButton.html(); + $goButton.prop('disabled', true); + $goButton.html($('#templates > .spinner')[0].outerHTML); + + var jqxhr = $.post("configure_indexer", JSON.stringify(data), function (data) { + if (data.result == "error") { + if (data.config) { + populateConfigItems(configForm, data.config); + } + doNotify("Configuration failed: " + data.error, "danger", "glyphicon glyphicon-alert"); + } + else { + configForm.modal("hide"); + reloadIndexers(); + doNotify("Successfully configured " + data.name, "success", "glyphicon glyphicon-ok"); + } + }).fail(function () { + doNotify("Request to Jackett server failed", "danger", "glyphicon glyphicon-alert"); + }).always(function () { + $goButton.html(originalBtnText); + $goButton.prop('disabled', false); + }); + }); + + configForm.modal("show"); + } + + function resolveUrl(url) { + var a = document.createElement('a'); + a.href = url; + url = a.href; + return url; + } + + + + function doNotify(message, type, icon) { + $.notify({ + message: message, + icon: icon + }, { + element: 'body', + type: type, + allow_dismiss: true, + z_index: 9000, + mouse_over: 'pause', + placement: { + from: "bottom", + align: "center" + } + }); + } + + function clearNotifications() { + $('[data-notify="container"]').remove(); + } + + $('#test').click(doNotify); + diff --git a/src/Jackett/WebContent/index.html b/src/Jackett/WebContent/index.html index f54b8f533..111e45a61 100644 --- a/src/Jackett/WebContent/index.html +++ b/src/Jackett/WebContent/index.html @@ -13,188 +13,9 @@ + Jackett -
@@ -207,7 +28,7 @@ Sonarr API Host: + +

+ Authentication: +
+

@@ -281,6 +115,25 @@ + +