From 57fdbe6e08ddfb14c6fa3910c3884b05fa600c12 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 20 Sep 2013 00:31:02 -0700 Subject: [PATCH] Added API key authentication --- .../Authentication/EnableBasicAuthInNancy.cs | 5 +- .../EnableStatelessAuthInNancy.cs | 50 +++++++++++++++++++ .../Frontend/Mappers/IndexHtmlMapper.cs | 25 +++++++++- .../Mappers/StaticResourceMapperBase.cs | 3 -- NzbDrone.Api/NancyBootstrapper.cs | 1 + NzbDrone.Api/NzbDrone.Api.csproj | 1 + .../Configuration/ConfigFileProvider.cs | 9 ++++ .../{backbone.ajax.js => jquery.ajax.js} | 8 ++- UI/ServerStatus.js | 6 ++- UI/app.js | 15 +++--- UI/index.html | 6 +++ 11 files changed, 114 insertions(+), 15 deletions(-) create mode 100644 NzbDrone.Api/Authentication/EnableStatelessAuthInNancy.cs rename UI/Mixins/{backbone.ajax.js => jquery.ajax.js} (78%) diff --git a/NzbDrone.Api/Authentication/EnableBasicAuthInNancy.cs b/NzbDrone.Api/Authentication/EnableBasicAuthInNancy.cs index 48ae43b2a..ab0bd9f60 100644 --- a/NzbDrone.Api/Authentication/EnableBasicAuthInNancy.cs +++ b/NzbDrone.Api/Authentication/EnableBasicAuthInNancy.cs @@ -27,7 +27,10 @@ namespace NzbDrone.Api.Authentication private Response RequiresAuthentication(NancyContext context) { Response response = null; - if (context.CurrentUser == null && _authenticationService.Enabled) + + if (!context.Request.Path.StartsWith("/api/") && + 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..4ad2e2995 --- /dev/null +++ b/NzbDrone.Api/Authentication/EnableStatelessAuthInNancy.cs @@ -0,0 +1,50 @@ +using System.Linq; +using Nancy; +using Nancy.Bootstrapper; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Configuration; + +namespace NzbDrone.Api.Authentication +{ + public interface IEnableStatelessAuthInNancy + { + void Register(IPipelines pipelines); + } + + public class EnableStatelessAuthInNancy : IEnableStatelessAuthInNancy + { + 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; + var apiKey = context.Request.Headers["ApiKey"].FirstOrDefault(); + + if (!RuntimeInfo.IsProduction && + (context.Request.UserHostAddress.Equals("localhost") || + context.Request.UserHostAddress.Equals("127.0.0.1") || + context.Request.UserHostAddress.Equals("::1"))) + { + return response; + } + + if (context.Request.Path.StartsWith("/api/") && + (apiKey == null || !apiKey.Equals(_configFileProvider.ApiKey))) + { + response = new Response { StatusCode = HttpStatusCode.Unauthorized }; + } + + return response; + } + } +} \ No newline at end of file diff --git a/NzbDrone.Api/Frontend/Mappers/IndexHtmlMapper.cs b/NzbDrone.Api/Frontend/Mappers/IndexHtmlMapper.cs index b15167619..51f263a8f 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"); } @@ -30,7 +38,21 @@ namespace NzbDrone.Api.Frontend.Mappers public override Response GetResponse(string resourceUrl) { + string content; var response = base.GetResponse(resourceUrl); + var stream = new MemoryStream(); + + response.Contents.Invoke(stream); + stream.Position = 0; + + using (var reader = new StreamReader(stream)) + { + content = reader.ReadToEnd(); + } + + content = content.Replace("API_KEY", _configFileProvider.ApiKey); + + response = new StreamResponse(() => StringToStream(content), response.ContentType); response.Headers["X-UA-Compatible"] = "IE=edge"; return response; @@ -51,6 +73,5 @@ namespace NzbDrone.Api.Frontend.Mappers 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..e9464ebd5 100644 --- a/NzbDrone.Api/NancyBootstrapper.cs +++ b/NzbDrone.Api/NancyBootstrapper.cs @@ -31,6 +31,7 @@ namespace NzbDrone.Api container.Resolve().Register(); container.Resolve().Register(pipelines); + 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..257e03887 100644 --- a/NzbDrone.Api/NzbDrone.Api.csproj +++ b/NzbDrone.Api/NzbDrone.Api.csproj @@ -74,6 +74,7 @@ Properties\SharedAssemblyInfo.cs + diff --git a/NzbDrone.Core/Configuration/ConfigFileProvider.cs b/NzbDrone.Core/Configuration/ConfigFileProvider.cs index a25e85b7c..da0fa5569 100644 --- a/NzbDrone.Core/Configuration/ConfigFileProvider.cs +++ b/NzbDrone.Core/Configuration/ConfigFileProvider.cs @@ -26,6 +26,7 @@ namespace NzbDrone.Core.Configuration string Password { get; } string LogLevel { get; } string Branch { get; } + string ApiKey { get; } bool Torrent { get; } } @@ -95,6 +96,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); } diff --git a/UI/Mixins/backbone.ajax.js b/UI/Mixins/jquery.ajax.js similarity index 78% rename from UI/Mixins/backbone.ajax.js rename to UI/Mixins/jquery.ajax.js index 0544b2e28..0f7abf8d5 100644 --- a/UI/Mixins/backbone.ajax.js +++ b/UI/Mixins/jquery.ajax.js @@ -20,9 +20,15 @@ define(function () { delete xhr.data; } + if (xhr) { + if (!xhr.headers) { + xhr.headers = {}; + } + + xhr.headers["ApiKey"] = window.NzbDrone.ApiKey; + } return original.apply(this, arguments); }; }; - }); diff --git a/UI/ServerStatus.js b/UI/ServerStatus.js index 1a5da687b..020713f5b 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: { + ApiKey: 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..a5aa6ba57 100644 --- a/UI/index.html +++ b/UI/index.html @@ -60,6 +60,12 @@ + + +