Merge branch 'api-key' into develop

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

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

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

@ -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)

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

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

@ -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<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 EpisodeClient(IRestClient restClient)
: base(restClient, "episodes")
public EpisodeClient(IRestClient restClient, string apiKey)
: base(restClient, apiKey, "episodes")
{
}

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

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

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

@ -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<RootFolderResource>(RestClient);
Commands = new ClientBase<CommandResource>(RestClient);
History = new ClientBase<HistoryResource>(RestClient);
Indexers = new IndexerClient(RestClient);
Episodes = new EpisodeClient(RestClient);
NamingConfig = new ClientBase<NamingConfigResource>(RestClient, "config/naming");
Series = new SeriesClient(RestClient, _runner.ApiKey);
Releases = new ReleaseClient(RestClient, _runner.ApiKey);
RootFolders = new ClientBase<RootFolderResource>(RestClient, _runner.ApiKey);
Commands = new ClientBase<CommandResource>(RestClient, _runner.ApiKey);
History = new ClientBase<HistoryResource>(RestClient, _runner.ApiKey);
Indexers = new IndexerClient(RestClient, _runner.ApiKey);
Episodes = new EpisodeClient(RestClient, _runner.ApiKey);
NamingConfig = new ClientBase<NamingConfigResource>(RestClient, _runner.ApiKey, "config/naming");
}
//[TestFixtureTearDown]

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

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

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

@ -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'
},

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

Loading…
Cancel
Save