Merge branch 'api-key' into develop

Conflicts:
	NzbDrone.Integration.Test/NzbDroneRunner.cs
pull/2/head
Mark McDowall 11 years ago
commit ca3b4cb1d7

@ -1,15 +1,12 @@
using Nancy; using Nancy;
using Nancy.Authentication.Basic; using Nancy.Authentication.Basic;
using Nancy.Bootstrapper; using Nancy.Bootstrapper;
using NzbDrone.Api.Extensions;
using NzbDrone.Api.Extensions.Pipelines;
namespace NzbDrone.Api.Authentication namespace NzbDrone.Api.Authentication
{ {
public interface IEnableBasicAuthInNancy public class EnableBasicAuthInNancy : IRegisterNancyPipeline
{
void Register(IPipelines pipelines);
}
public class EnableBasicAuthInNancy : IEnableBasicAuthInNancy
{ {
private readonly IAuthenticationService _authenticationService; private readonly IAuthenticationService _authenticationService;
@ -27,7 +24,10 @@ namespace NzbDrone.Api.Authentication
private Response RequiresAuthentication(NancyContext context) private Response RequiresAuthentication(NancyContext context)
{ {
Response response = null; Response response = null;
if (context.CurrentUser == null && _authenticationService.Enabled)
if (!context.Request.IsApiRequest() &&
context.CurrentUser == null &&
_authenticationService.Enabled)
{ {
response = new Response { StatusCode = HttpStatusCode.Unauthorized }; response = new Response { StatusCode = HttpStatusCode.Unauthorized };
} }

@ -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;
}
}
}

@ -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"));
}
}
}

@ -1,20 +1,28 @@
using System;
using System.IO; using System.IO;
using Nancy; using Nancy;
using Nancy.Responses;
using NLog; using NLog;
using NzbDrone.Common; using NzbDrone.Common;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration;
namespace NzbDrone.Api.Frontend.Mappers namespace NzbDrone.Api.Frontend.Mappers
{ {
public class IndexHtmlMapper : StaticResourceMapperBase public class IndexHtmlMapper : StaticResourceMapperBase
{ {
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private readonly IConfigFileProvider _configFileProvider;
private readonly string _indexPath; 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) : base(diskProvider, logger)
{ {
_diskProvider = diskProvider; _diskProvider = diskProvider;
_configFileProvider = configFileProvider;
_indexPath = Path.Combine(appFolderInfo.StartUpFolder, "UI", "index.html"); _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(".css", ".css?v=" + BuildInfo.Version);
text = text.Replace(".js", ".js?v=" + BuildInfo.Version); text = text.Replace(".js", ".js?v=" + BuildInfo.Version);
text = text.Replace("API_KEY", _configFileProvider.ApiKey);
return text; return text;
} }
} }
} }

@ -24,13 +24,10 @@ namespace NzbDrone.Api.Frontend.Mappers
{ {
_caseSensitive = true; _caseSensitive = true;
} }
} }
protected abstract string Map(string resourceUrl); protected abstract string Map(string resourceUrl);
public abstract bool CanHandle(string resourceUrl); public abstract bool CanHandle(string resourceUrl);
public virtual Response GetResponse(string resourceUrl) public virtual Response GetResponse(string resourceUrl)

@ -30,7 +30,6 @@ namespace NzbDrone.Api
RegisterPipelines(pipelines); RegisterPipelines(pipelines);
container.Resolve<DatabaseTarget>().Register(); container.Resolve<DatabaseTarget>().Register();
container.Resolve<IEnableBasicAuthInNancy>().Register(pipelines);
container.Resolve<IEventAggregator>().PublishEvent(new ApplicationStartedEvent()); container.Resolve<IEventAggregator>().PublishEvent(new ApplicationStartedEvent());
ApplicationPipelines.OnError.AddItemToEndOfPipeline(container.Resolve<NzbDroneErrorPipeline>().HandleException); ApplicationPipelines.OnError.AddItemToEndOfPipeline(container.Resolve<NzbDroneErrorPipeline>().HandleException);

@ -74,6 +74,7 @@
<Link>Properties\SharedAssemblyInfo.cs</Link> <Link>Properties\SharedAssemblyInfo.cs</Link>
</Compile> </Compile>
<Compile Include="Authentication\AuthenticationService.cs" /> <Compile Include="Authentication\AuthenticationService.cs" />
<Compile Include="Authentication\EnableStatelessAuthInNancy.cs" />
<Compile Include="Authentication\EnableBasicAuthInNancy.cs" /> <Compile Include="Authentication\EnableBasicAuthInNancy.cs" />
<Compile Include="Authentication\NzbDroneUser.cs" /> <Compile Include="Authentication\NzbDroneUser.cs" />
<Compile Include="Calendar\CalendarModule.cs" /> <Compile Include="Calendar\CalendarModule.cs" />
@ -97,6 +98,7 @@
<Compile Include="Extensions\Pipelines\IfModifiedPipeline.cs" /> <Compile Include="Extensions\Pipelines\IfModifiedPipeline.cs" />
<Compile Include="Extensions\Pipelines\IRegisterNancyPipeline.cs" /> <Compile Include="Extensions\Pipelines\IRegisterNancyPipeline.cs" />
<Compile Include="Extensions\NancyJsonSerializer.cs" /> <Compile Include="Extensions\NancyJsonSerializer.cs" />
<Compile Include="Extensions\RequestExtensions.cs" />
<Compile Include="Frontend\IsCacheableSpecification.cs" /> <Compile Include="Frontend\IsCacheableSpecification.cs" />
<Compile Include="Frontend\Mappers\IndexHtmlMapper.cs" /> <Compile Include="Frontend\Mappers\IndexHtmlMapper.cs" />
<Compile Include="Frontend\Mappers\LogFileMapper.cs" /> <Compile Include="Frontend\Mappers\LogFileMapper.cs" />

@ -28,13 +28,14 @@ namespace NzbDrone.Core.Configuration
string Password { get; } string Password { get; }
string LogLevel { get; } string LogLevel { get; }
string Branch { get; } string Branch { get; }
string ApiKey { get; }
bool Torrent { get; } bool Torrent { get; }
string SslCertHash { get; } string SslCertHash { get; }
} }
public class ConfigFileProvider : IConfigFileProvider public class ConfigFileProvider : IConfigFileProvider
{ {
private const string CONFIG_ELEMENT_NAME = "Config"; public const string CONFIG_ELEMENT_NAME = "Config";
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
private readonly ICached<string> _cache; private readonly ICached<string> _cache;
@ -108,6 +109,14 @@ namespace NzbDrone.Core.Configuration
get { return GetValueBoolean("LaunchBrowser", true); } get { return GetValueBoolean("LaunchBrowser", true); }
} }
public string ApiKey
{
get
{
return GetValue("ApiKey", Guid.NewGuid().ToString().Replace("-", ""));
}
}
public bool Torrent public bool Torrent
{ {
get { return GetValueBoolean("Torrent", false, persist: false); } 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")); var xDoc = new XDocument(new XDeclaration("1.0", "utf-8", "yes"));
xDoc.Add(new XElement(CONFIG_ELEMENT_NAME)); xDoc.Add(new XElement(CONFIG_ELEMENT_NAME));
xDoc.Save(_configFile); xDoc.Save(_configFile);
SaveConfigDictionary(GetConfigDictionary());
} }
} }

@ -14,10 +14,10 @@ namespace NzbDrone.Integration.Test.Client
{ {
private readonly IRestClient _restClient; private readonly IRestClient _restClient;
private readonly string _resource; private readonly string _resource;
private readonly string _apiKey;
private readonly Logger _logger; private readonly Logger _logger;
public ClientBase(IRestClient restClient, string resource = null) public ClientBase(IRestClient restClient, string apiKey, string resource = null)
{ {
if (resource == null) if (resource == null)
{ {
@ -26,6 +26,7 @@ namespace NzbDrone.Integration.Test.Client
_restClient = restClient; _restClient = restClient;
_resource = resource; _resource = resource;
_apiKey = apiKey;
_logger = LogManager.GetLogger("REST"); _logger = LogManager.GetLogger("REST");
} }
@ -88,10 +89,14 @@ namespace NzbDrone.Integration.Test.Client
public RestRequest BuildRequest(string command = "") 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<T>(IRestRequest request, HttpStatusCode statusCode = HttpStatusCode.OK) where T : class, new() public T Get<T>(IRestRequest request, HttpStatusCode statusCode = HttpStatusCode.OK) where T : class, new()

@ -6,8 +6,8 @@ namespace NzbDrone.Integration.Test.Client
{ {
public class EpisodeClient : ClientBase<EpisodeResource> public class EpisodeClient : ClientBase<EpisodeResource>
{ {
public EpisodeClient(IRestClient restClient) public EpisodeClient(IRestClient restClient, string apiKey)
: base(restClient, "episodes") : base(restClient, apiKey, "episodes")
{ {
} }

@ -5,12 +5,9 @@ namespace NzbDrone.Integration.Test.Client
{ {
public class IndexerClient : ClientBase<IndexerResource> public class IndexerClient : ClientBase<IndexerResource>
{ {
public IndexerClient(IRestClient restClient) public IndexerClient(IRestClient restClient, string apiKey)
: base(restClient) : base(restClient, apiKey)
{ {
} }
} }
} }

@ -5,12 +5,9 @@ namespace NzbDrone.Integration.Test.Client
{ {
public class ReleaseClient : ClientBase<ReleaseResource> public class ReleaseClient : ClientBase<ReleaseResource>
{ {
public ReleaseClient(IRestClient restClient) public ReleaseClient(IRestClient restClient, string apiKey)
: base(restClient) : base(restClient, apiKey)
{ {
} }
} }
} }

@ -7,8 +7,8 @@ namespace NzbDrone.Integration.Test.Client
{ {
public class SeriesClient : ClientBase<SeriesResource> public class SeriesClient : ClientBase<SeriesResource>
{ {
public SeriesClient(IRestClient restClient) public SeriesClient(IRestClient restClient, string apiKey)
: base(restClient) : base(restClient, apiKey)
{ {
} }
@ -27,14 +27,11 @@ namespace NzbDrone.Integration.Test.Client
} }
public class SystemInfoClient : ClientBase<SeriesResource> public class SystemInfoClient : ClientBase<SeriesResource>
{ {
public SystemInfoClient(IRestClient restClient) public SystemInfoClient(IRestClient restClient, string apiKey)
: base(restClient) : base(restClient, apiKey)
{ {
} }
} }
} }

@ -47,22 +47,21 @@ namespace NzbDrone.Integration.Test
_runner = new NzbDroneRunner(); _runner = new NzbDroneRunner();
_runner.KillAll(); _runner.KillAll();
InitRestClients();
_runner.Start(); _runner.Start();
InitRestClients();
} }
private void InitRestClients() private void InitRestClients()
{ {
RestClient = new RestClient("http://localhost:8989/api"); RestClient = new RestClient("http://localhost:8989/api");
Series = new SeriesClient(RestClient); Series = new SeriesClient(RestClient, _runner.ApiKey);
Releases = new ReleaseClient(RestClient); Releases = new ReleaseClient(RestClient, _runner.ApiKey);
RootFolders = new ClientBase<RootFolderResource>(RestClient); RootFolders = new ClientBase<RootFolderResource>(RestClient, _runner.ApiKey);
Commands = new ClientBase<CommandResource>(RestClient); Commands = new ClientBase<CommandResource>(RestClient, _runner.ApiKey);
History = new ClientBase<HistoryResource>(RestClient); History = new ClientBase<HistoryResource>(RestClient, _runner.ApiKey);
Indexers = new IndexerClient(RestClient); Indexers = new IndexerClient(RestClient, _runner.ApiKey);
Episodes = new EpisodeClient(RestClient); Episodes = new EpisodeClient(RestClient, _runner.ApiKey);
NamingConfig = new ClientBase<NamingConfigResource>(RestClient, "config/naming"); NamingConfig = new ClientBase<NamingConfigResource>(RestClient, _runner.ApiKey, "config/naming");
} }
//[TestFixtureTearDown] //[TestFixtureTearDown]

@ -1,11 +1,14 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq;
using System.Threading; using System.Threading;
using System.Xml.Linq;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common; using NzbDrone.Common;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Processes; using NzbDrone.Common.Processes;
using NzbDrone.Core.Configuration;
using RestSharp; using RestSharp;
namespace NzbDrone.Integration.Test namespace NzbDrone.Integration.Test
@ -16,16 +19,18 @@ namespace NzbDrone.Integration.Test
private readonly IRestClient _restClient; private readonly IRestClient _restClient;
private Process _nzbDroneProcess; private Process _nzbDroneProcess;
public string AppData { get; private set; }
public string ApiKey { get; private set; }
public NzbDroneRunner(int port = 8989) public NzbDroneRunner(int port = 8989)
{ {
_processProvider = new ProcessProvider(); _processProvider = new ProcessProvider();
_restClient = new RestClient("http://localhost:8989/api"); _restClient = new RestClient("http://localhost:8989/api");
} }
public void Start() 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"; var nzbdroneConsoleExe = "NzbDrone.Console.exe";
@ -34,7 +39,6 @@ namespace NzbDrone.Integration.Test
nzbdroneConsoleExe = "NzbDrone.exe"; nzbdroneConsoleExe = "NzbDrone.exe";
} }
if (BuildInfo.IsDebug) if (BuildInfo.IsDebug)
{ {
@ -54,8 +58,12 @@ namespace NzbDrone.Integration.Test
Assert.Fail("Process has exited"); 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) if (statusCall.ResponseStatus == ResponseStatus.Completed)
{ {
@ -77,7 +85,7 @@ namespace NzbDrone.Integration.Test
private void Start(string outputNzbdroneConsoleExe) private void Start(string outputNzbdroneConsoleExe)
{ {
var args = "-nobrowser -data=\"" + AppDate + "\""; var args = "-nobrowser -data=\"" + AppData + "\"";
_nzbDroneProcess = _processProvider.Start(outputNzbdroneConsoleExe, args, OnOutputDataReceived, OnOutputDataReceived); _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;
}
} }
} }

@ -20,9 +20,12 @@ define(function () {
delete xhr.data; delete xhr.data;
} }
if (xhr) {
xhr.headers = xhr.headers || {};
xhr.headers['Authorization'] = window.NzbDrone.ApiKey;
}
return original.apply(this, arguments); return original.apply(this, arguments);
}; };
}; };
}); });

@ -1,10 +1,12 @@
window.NzbDrone = {};
window.NzbDrone.ApiRoot = '/api'; window.NzbDrone.ApiRoot = '/api';
var statusText = $.ajax({ var statusText = $.ajax({
type : 'GET', type : 'GET',
url : window.NzbDrone.ApiRoot + '/system/status', url : window.NzbDrone.ApiRoot + '/system/status',
async: false async: false,
headers: {
Authorization: window.NzbDrone.ApiKey
}
}).responseText; }).responseText;
window.NzbDrone.ServerStatus = JSON.parse(statusText); window.NzbDrone.ServerStatus = JSON.parse(statusText);

@ -33,12 +33,19 @@ require.config({
$: { $: {
exports: '$', exports: '$',
init: function () { deps :
[
'Mixins/jquery.ajax'
],
init: function (AjaxMixin) {
require( require(
[ [
'jQuery/ToTheTop', 'jQuery/ToTheTop',
'Instrumentation/ErrorHandler' 'Instrumentation/ErrorHandler'
]); ]);
AjaxMixin.apply($);
} }
}, },
@ -75,14 +82,10 @@ require.config({
backbone: { backbone: {
deps : deps :
[ [
'Mixins/backbone.ajax',
'underscore', 'underscore',
'$' '$'
], ],
exports: 'Backbone', exports: 'Backbone'
init : function (AjaxMixin) {
AjaxMixin.apply(Backbone);
}
}, },

@ -60,6 +60,12 @@
</div> </div>
</footer> </footer>
</body> </body>
<script type="text/javascript">
window.NzbDrone = window.NzbDrone || {};
window.NzbDrone.ApiKey = 'API_KEY';
</script>
<script src="/polyfills.js"></script> <script src="/polyfills.js"></script>
<script src="/JsLibraries/jquery.js"></script> <script src="/JsLibraries/jquery.js"></script>
<script src="/JsLibraries/messenger.js"></script> <script src="/JsLibraries/messenger.js"></script>

Loading…
Cancel
Save