diff --git a/NzbDrone.Api/Authentication/EnableBasicAuthInNancy.cs b/NzbDrone.Api/Authentication/EnableBasicAuthInNancy.cs index 48ae43b2a..a6994caf3 100644 --- a/NzbDrone.Api/Authentication/EnableBasicAuthInNancy.cs +++ b/NzbDrone.Api/Authentication/EnableBasicAuthInNancy.cs @@ -1,15 +1,12 @@ using Nancy; using Nancy.Authentication.Basic; using Nancy.Bootstrapper; +using NzbDrone.Api.Extensions; +using NzbDrone.Api.Extensions.Pipelines; namespace NzbDrone.Api.Authentication { - public interface IEnableBasicAuthInNancy - { - void Register(IPipelines pipelines); - } - - public class EnableBasicAuthInNancy : IEnableBasicAuthInNancy + public class EnableBasicAuthInNancy : IRegisterNancyPipeline { private readonly IAuthenticationService _authenticationService; @@ -27,7 +24,10 @@ namespace NzbDrone.Api.Authentication private Response RequiresAuthentication(NancyContext context) { Response response = null; - if (context.CurrentUser == null && _authenticationService.Enabled) + + if (!context.Request.IsApiRequest() && + context.CurrentUser == null && + _authenticationService.Enabled) { response = new Response { StatusCode = HttpStatusCode.Unauthorized }; } diff --git a/NzbDrone.Api/Authentication/EnableStatelessAuthInNancy.cs b/NzbDrone.Api/Authentication/EnableStatelessAuthInNancy.cs new file mode 100644 index 000000000..68d737387 --- /dev/null +++ b/NzbDrone.Api/Authentication/EnableStatelessAuthInNancy.cs @@ -0,0 +1,46 @@ +using System; +using System.Linq; +using Nancy; +using Nancy.Bootstrapper; +using NzbDrone.Api.Extensions; +using NzbDrone.Api.Extensions.Pipelines; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Configuration; + +namespace NzbDrone.Api.Authentication +{ + public class EnableStatelessAuthInNancy : IRegisterNancyPipeline + { + private readonly IConfigFileProvider _configFileProvider; + + public EnableStatelessAuthInNancy(IConfigFileProvider configFileProvider) + { + _configFileProvider = configFileProvider; + } + + public void Register(IPipelines pipelines) + { + pipelines.BeforeRequest.AddItemToEndOfPipeline(ValidateApiKey); + } + + public Response ValidateApiKey(NancyContext context) + { + Response response = null; + + if (!RuntimeInfo.IsProduction && context.Request.IsLocalRequest()) + { + return response; + } + + var apiKey = context.Request.Headers.Authorization; + + if (context.Request.IsApiRequest() && + (String.IsNullOrWhiteSpace(apiKey) || !apiKey.Equals(_configFileProvider.ApiKey))) + { + response = new Response { StatusCode = HttpStatusCode.Unauthorized }; + } + + return response; + } + } +} \ No newline at end of file diff --git a/NzbDrone.Api/Extensions/RequestExtensions.cs b/NzbDrone.Api/Extensions/RequestExtensions.cs new file mode 100644 index 000000000..3d0329522 --- /dev/null +++ b/NzbDrone.Api/Extensions/RequestExtensions.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Nancy; + +namespace NzbDrone.Api.Extensions +{ + public static class RequestExtensions + { + public static bool IsApiRequest(this Request request) + { + return request.Path.StartsWith("/api/", StringComparison.InvariantCultureIgnoreCase); + } + + public static bool IsSignalRRequest(this Request request) + { + return request.Path.StartsWith("/signalr/", StringComparison.InvariantCultureIgnoreCase); + } + + public static bool IsLocalRequest(this Request request) + { + return (request.UserHostAddress.Equals("localhost") || + request.UserHostAddress.Equals("127.0.0.1") || + request.UserHostAddress.Equals("::1")); + } + } +} diff --git a/NzbDrone.Api/Frontend/Mappers/IndexHtmlMapper.cs b/NzbDrone.Api/Frontend/Mappers/IndexHtmlMapper.cs index b15167619..ae950aae0 100644 --- a/NzbDrone.Api/Frontend/Mappers/IndexHtmlMapper.cs +++ b/NzbDrone.Api/Frontend/Mappers/IndexHtmlMapper.cs @@ -1,20 +1,28 @@ +using System; using System.IO; using Nancy; +using Nancy.Responses; using NLog; using NzbDrone.Common; using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Configuration; namespace NzbDrone.Api.Frontend.Mappers { public class IndexHtmlMapper : StaticResourceMapperBase { private readonly IDiskProvider _diskProvider; + private readonly IConfigFileProvider _configFileProvider; private readonly string _indexPath; - public IndexHtmlMapper(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, Logger logger) + public IndexHtmlMapper(IAppFolderInfo appFolderInfo, + IDiskProvider diskProvider, + IConfigFileProvider configFileProvider, + Logger logger) : base(diskProvider, logger) { _diskProvider = diskProvider; + _configFileProvider = configFileProvider; _indexPath = Path.Combine(appFolderInfo.StartUpFolder, "UI", "index.html"); } @@ -48,9 +56,9 @@ namespace NzbDrone.Api.Frontend.Mappers text = text.Replace(".css", ".css?v=" + BuildInfo.Version); text = text.Replace(".js", ".js?v=" + BuildInfo.Version); + text = text.Replace("API_KEY", _configFileProvider.ApiKey); return text; } - } } \ No newline at end of file diff --git a/NzbDrone.Api/Frontend/Mappers/StaticResourceMapperBase.cs b/NzbDrone.Api/Frontend/Mappers/StaticResourceMapperBase.cs index eae69b9de..4cc42f49f 100644 --- a/NzbDrone.Api/Frontend/Mappers/StaticResourceMapperBase.cs +++ b/NzbDrone.Api/Frontend/Mappers/StaticResourceMapperBase.cs @@ -24,13 +24,10 @@ namespace NzbDrone.Api.Frontend.Mappers { _caseSensitive = true; } - - } protected abstract string Map(string resourceUrl); - public abstract bool CanHandle(string resourceUrl); public virtual Response GetResponse(string resourceUrl) diff --git a/NzbDrone.Api/NancyBootstrapper.cs b/NzbDrone.Api/NancyBootstrapper.cs index 29535d272..bee581921 100644 --- a/NzbDrone.Api/NancyBootstrapper.cs +++ b/NzbDrone.Api/NancyBootstrapper.cs @@ -30,7 +30,6 @@ namespace NzbDrone.Api RegisterPipelines(pipelines); container.Resolve().Register(); - container.Resolve().Register(pipelines); container.Resolve().PublishEvent(new ApplicationStartedEvent()); ApplicationPipelines.OnError.AddItemToEndOfPipeline(container.Resolve().HandleException); diff --git a/NzbDrone.Api/NzbDrone.Api.csproj b/NzbDrone.Api/NzbDrone.Api.csproj index 331591808..b88c2277d 100644 --- a/NzbDrone.Api/NzbDrone.Api.csproj +++ b/NzbDrone.Api/NzbDrone.Api.csproj @@ -74,6 +74,7 @@ Properties\SharedAssemblyInfo.cs + @@ -97,6 +98,7 @@ + diff --git a/NzbDrone.Core/Configuration/ConfigFileProvider.cs b/NzbDrone.Core/Configuration/ConfigFileProvider.cs index 4a1e93518..400f03a56 100644 --- a/NzbDrone.Core/Configuration/ConfigFileProvider.cs +++ b/NzbDrone.Core/Configuration/ConfigFileProvider.cs @@ -28,13 +28,14 @@ namespace NzbDrone.Core.Configuration string Password { get; } string LogLevel { get; } string Branch { get; } + string ApiKey { get; } bool Torrent { get; } string SslCertHash { get; } } public class ConfigFileProvider : IConfigFileProvider { - private const string CONFIG_ELEMENT_NAME = "Config"; + public const string CONFIG_ELEMENT_NAME = "Config"; private readonly IEventAggregator _eventAggregator; private readonly ICached _cache; @@ -108,6 +109,14 @@ namespace NzbDrone.Core.Configuration get { return GetValueBoolean("LaunchBrowser", true); } } + public string ApiKey + { + get + { + return GetValue("ApiKey", Guid.NewGuid().ToString().Replace("-", "")); + } + } + public bool Torrent { get { return GetValueBoolean("Torrent", false, persist: false); } @@ -223,6 +232,8 @@ namespace NzbDrone.Core.Configuration var xDoc = new XDocument(new XDeclaration("1.0", "utf-8", "yes")); xDoc.Add(new XElement(CONFIG_ELEMENT_NAME)); xDoc.Save(_configFile); + + SaveConfigDictionary(GetConfigDictionary()); } } diff --git a/NzbDrone.Integration.Test/Client/ClientBase.cs b/NzbDrone.Integration.Test/Client/ClientBase.cs index 20e35117f..9cd0b4b7a 100644 --- a/NzbDrone.Integration.Test/Client/ClientBase.cs +++ b/NzbDrone.Integration.Test/Client/ClientBase.cs @@ -14,10 +14,10 @@ namespace NzbDrone.Integration.Test.Client { private readonly IRestClient _restClient; private readonly string _resource; - + private readonly string _apiKey; private readonly Logger _logger; - public ClientBase(IRestClient restClient, string resource = null) + public ClientBase(IRestClient restClient, string apiKey, string resource = null) { if (resource == null) { @@ -26,6 +26,7 @@ namespace NzbDrone.Integration.Test.Client _restClient = restClient; _resource = resource; + _apiKey = apiKey; _logger = LogManager.GetLogger("REST"); } @@ -88,10 +89,14 @@ namespace NzbDrone.Integration.Test.Client public RestRequest BuildRequest(string command = "") { - return new RestRequest(_resource + "/" + command.Trim('/')) + var request = new RestRequest(_resource + "/" + command.Trim('/')) { - RequestFormat = DataFormat.Json + RequestFormat = DataFormat.Json, }; + + request.AddHeader("Authorization", _apiKey); + + return request; } public T Get(IRestRequest request, HttpStatusCode statusCode = HttpStatusCode.OK) where T : class, new() diff --git a/NzbDrone.Integration.Test/Client/EpisodeClient.cs b/NzbDrone.Integration.Test/Client/EpisodeClient.cs index 8cc89af4e..437226c2f 100644 --- a/NzbDrone.Integration.Test/Client/EpisodeClient.cs +++ b/NzbDrone.Integration.Test/Client/EpisodeClient.cs @@ -6,8 +6,8 @@ namespace NzbDrone.Integration.Test.Client { public class EpisodeClient : ClientBase { - public EpisodeClient(IRestClient restClient) - : base(restClient, "episodes") + public EpisodeClient(IRestClient restClient, string apiKey) + : base(restClient, apiKey, "episodes") { } diff --git a/NzbDrone.Integration.Test/Client/IndexerClient.cs b/NzbDrone.Integration.Test/Client/IndexerClient.cs index 44f5d4c77..9d6f9b974 100644 --- a/NzbDrone.Integration.Test/Client/IndexerClient.cs +++ b/NzbDrone.Integration.Test/Client/IndexerClient.cs @@ -5,12 +5,9 @@ namespace NzbDrone.Integration.Test.Client { public class IndexerClient : ClientBase { - public IndexerClient(IRestClient restClient) - : base(restClient) + public IndexerClient(IRestClient restClient, string apiKey) + : base(restClient, apiKey) { } - - - } } \ No newline at end of file diff --git a/NzbDrone.Integration.Test/Client/ReleaseClient.cs b/NzbDrone.Integration.Test/Client/ReleaseClient.cs index fba274856..46a6db839 100644 --- a/NzbDrone.Integration.Test/Client/ReleaseClient.cs +++ b/NzbDrone.Integration.Test/Client/ReleaseClient.cs @@ -5,12 +5,9 @@ namespace NzbDrone.Integration.Test.Client { public class ReleaseClient : ClientBase { - public ReleaseClient(IRestClient restClient) - : base(restClient) + public ReleaseClient(IRestClient restClient, string apiKey) + : base(restClient, apiKey) { } - - - } } diff --git a/NzbDrone.Integration.Test/Client/SeriesClient.cs b/NzbDrone.Integration.Test/Client/SeriesClient.cs index 3dc88c969..1f0a572f9 100644 --- a/NzbDrone.Integration.Test/Client/SeriesClient.cs +++ b/NzbDrone.Integration.Test/Client/SeriesClient.cs @@ -7,8 +7,8 @@ namespace NzbDrone.Integration.Test.Client { public class SeriesClient : ClientBase { - public SeriesClient(IRestClient restClient) - : base(restClient) + public SeriesClient(IRestClient restClient, string apiKey) + : base(restClient, apiKey) { } @@ -27,14 +27,11 @@ namespace NzbDrone.Integration.Test.Client } - public class SystemInfoClient : ClientBase { - public SystemInfoClient(IRestClient restClient) - : base(restClient) + public SystemInfoClient(IRestClient restClient, string apiKey) + : base(restClient, apiKey) { } - - } } diff --git a/NzbDrone.Integration.Test/IntegrationTest.cs b/NzbDrone.Integration.Test/IntegrationTest.cs index a0b5b3bcd..f8556a58d 100644 --- a/NzbDrone.Integration.Test/IntegrationTest.cs +++ b/NzbDrone.Integration.Test/IntegrationTest.cs @@ -47,22 +47,21 @@ namespace NzbDrone.Integration.Test _runner = new NzbDroneRunner(); _runner.KillAll(); - InitRestClients(); - _runner.Start(); + InitRestClients(); } private void InitRestClients() { RestClient = new RestClient("http://localhost:8989/api"); - Series = new SeriesClient(RestClient); - Releases = new ReleaseClient(RestClient); - RootFolders = new ClientBase(RestClient); - Commands = new ClientBase(RestClient); - History = new ClientBase(RestClient); - Indexers = new IndexerClient(RestClient); - Episodes = new EpisodeClient(RestClient); - NamingConfig = new ClientBase(RestClient, "config/naming"); + Series = new SeriesClient(RestClient, _runner.ApiKey); + Releases = new ReleaseClient(RestClient, _runner.ApiKey); + RootFolders = new ClientBase(RestClient, _runner.ApiKey); + Commands = new ClientBase(RestClient, _runner.ApiKey); + History = new ClientBase(RestClient, _runner.ApiKey); + Indexers = new IndexerClient(RestClient, _runner.ApiKey); + Episodes = new EpisodeClient(RestClient, _runner.ApiKey); + NamingConfig = new ClientBase(RestClient, _runner.ApiKey, "config/naming"); } //[TestFixtureTearDown] diff --git a/NzbDrone.Integration.Test/NzbDroneRunner.cs b/NzbDrone.Integration.Test/NzbDroneRunner.cs index b4bb8fa85..375fdc0b9 100644 --- a/NzbDrone.Integration.Test/NzbDroneRunner.cs +++ b/NzbDrone.Integration.Test/NzbDroneRunner.cs @@ -1,11 +1,14 @@ using System; using System.Diagnostics; using System.IO; +using System.Linq; using System.Threading; +using System.Xml.Linq; using NUnit.Framework; using NzbDrone.Common; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Processes; +using NzbDrone.Core.Configuration; using RestSharp; namespace NzbDrone.Integration.Test @@ -16,16 +19,18 @@ namespace NzbDrone.Integration.Test private readonly IRestClient _restClient; private Process _nzbDroneProcess; + public string AppData { get; private set; } + public string ApiKey { get; private set; } + public NzbDroneRunner(int port = 8989) { _processProvider = new ProcessProvider(); _restClient = new RestClient("http://localhost:8989/api"); } - public void Start() { - AppDate = Path.Combine(Directory.GetCurrentDirectory(), "_intg_" + DateTime.Now.Ticks); + AppData = Path.Combine(Directory.GetCurrentDirectory(), "_intg_" + DateTime.Now.Ticks); var nzbdroneConsoleExe = "NzbDrone.Console.exe"; @@ -34,7 +39,6 @@ namespace NzbDrone.Integration.Test nzbdroneConsoleExe = "NzbDrone.exe"; } - if (BuildInfo.IsDebug) { @@ -54,8 +58,12 @@ namespace NzbDrone.Integration.Test Assert.Fail("Process has exited"); } + SetApiKey(); + + var request = new RestRequest("system/status"); + request.AddHeader("Authorization", ApiKey); - var statusCall = _restClient.Get(new RestRequest("system/status")); + var statusCall = _restClient.Get(request); if (statusCall.ResponseStatus == ResponseStatus.Completed) { @@ -77,7 +85,7 @@ namespace NzbDrone.Integration.Test private void Start(string outputNzbdroneConsoleExe) { - var args = "-nobrowser -data=\"" + AppDate + "\""; + var args = "-nobrowser -data=\"" + AppData + "\""; _nzbDroneProcess = _processProvider.Start(outputNzbdroneConsoleExe, args, OnOutputDataReceived, OnOutputDataReceived); } @@ -92,7 +100,16 @@ namespace NzbDrone.Integration.Test } } + private void SetApiKey() + { + var configFile = Path.Combine(AppData, "config.xml"); + + if (!String.IsNullOrWhiteSpace(ApiKey)) return; + if (!File.Exists(configFile)) return; - public string AppDate { get; private set; } + var xDoc = XDocument.Load(configFile); + var config = xDoc.Descendants(ConfigFileProvider.CONFIG_ELEMENT_NAME).Single(); + ApiKey = config.Descendants("ApiKey").Single().Value; + } } } \ No newline at end of file diff --git a/UI/Mixins/backbone.ajax.js b/UI/Mixins/jquery.ajax.js similarity index 81% rename from UI/Mixins/backbone.ajax.js rename to UI/Mixins/jquery.ajax.js index 0544b2e28..e05a7d8fb 100644 --- a/UI/Mixins/backbone.ajax.js +++ b/UI/Mixins/jquery.ajax.js @@ -20,9 +20,12 @@ define(function () { delete xhr.data; } + if (xhr) { + xhr.headers = xhr.headers || {}; + xhr.headers['Authorization'] = window.NzbDrone.ApiKey; + } return original.apply(this, arguments); }; }; - }); diff --git a/UI/ServerStatus.js b/UI/ServerStatus.js index 1a5da687b..589d45fc6 100644 --- a/UI/ServerStatus.js +++ b/UI/ServerStatus.js @@ -1,10 +1,12 @@ -window.NzbDrone = {}; window.NzbDrone.ApiRoot = '/api'; var statusText = $.ajax({ type : 'GET', url : window.NzbDrone.ApiRoot + '/system/status', - async: false + async: false, + headers: { + Authorization: window.NzbDrone.ApiKey + } }).responseText; window.NzbDrone.ServerStatus = JSON.parse(statusText); diff --git a/UI/app.js b/UI/app.js index 280a16d39..8f64f4790 100644 --- a/UI/app.js +++ b/UI/app.js @@ -33,12 +33,19 @@ require.config({ $: { exports: '$', - init: function () { + deps : + [ + 'Mixins/jquery.ajax' + ], + + init: function (AjaxMixin) { require( [ 'jQuery/ToTheTop', 'Instrumentation/ErrorHandler' ]); + + AjaxMixin.apply($); } }, @@ -75,14 +82,10 @@ require.config({ backbone: { deps : [ - 'Mixins/backbone.ajax', 'underscore', '$' ], - exports: 'Backbone', - init : function (AjaxMixin) { - AjaxMixin.apply(Backbone); - } + exports: 'Backbone' }, diff --git a/UI/index.html b/UI/index.html index 178c5beb2..c2b0897e7 100644 --- a/UI/index.html +++ b/UI/index.html @@ -60,6 +60,12 @@ + + +