From 3ee58979ec714f749fcb393e597dcad56a2bc4e0 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Wed, 5 Nov 2014 16:20:00 -0800 Subject: [PATCH] New: CORS support on the API (for developing against the API) --- .../Extensions/AccessControlHeaders.cs | 12 ++++ .../Extensions/Pipelines/CorsPipeline.cs | 47 +++++++++++++++ src/NzbDrone.Api/NzbDrone.Api.csproj | 2 + .../Client/ClientBase.cs | 1 - src/NzbDrone.Integration.Test/CorsFixture.cs | 57 +++++++++++++++++++ .../IntegrationTest.cs | 5 +- .../NzbDrone.Integration.Test.csproj | 1 + 7 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 src/NzbDrone.Api/Extensions/AccessControlHeaders.cs create mode 100644 src/NzbDrone.Api/Extensions/Pipelines/CorsPipeline.cs create mode 100644 src/NzbDrone.Integration.Test/CorsFixture.cs diff --git a/src/NzbDrone.Api/Extensions/AccessControlHeaders.cs b/src/NzbDrone.Api/Extensions/AccessControlHeaders.cs new file mode 100644 index 000000000..5a32395cb --- /dev/null +++ b/src/NzbDrone.Api/Extensions/AccessControlHeaders.cs @@ -0,0 +1,12 @@ +namespace NzbDrone.Api.Extensions +{ + public static class AccessControlHeaders + { + public const string RequestMethod = "Access-Control-Request-Method"; + public const string RequestHeaders = "Access-Control-Request-Headers"; + + public const string AllowOrigin = "Access-Control-Allow-Origin"; + public const string AllowMethods = "Access-Control-Allow-Methods"; + public const string AllowHeaders = "Access-Control-Allow-Headers"; + } +} diff --git a/src/NzbDrone.Api/Extensions/Pipelines/CorsPipeline.cs b/src/NzbDrone.Api/Extensions/Pipelines/CorsPipeline.cs new file mode 100644 index 000000000..028be1dd5 --- /dev/null +++ b/src/NzbDrone.Api/Extensions/Pipelines/CorsPipeline.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Nancy; +using Nancy.Bootstrapper; + +namespace NzbDrone.Api.Extensions.Pipelines +{ + public class CorsPipeline : IRegisterNancyPipeline + { + public void Register(IPipelines pipelines) + { + pipelines.AfterRequest.AddItemToEndOfPipeline(Handle); + } + + private void Handle(NancyContext context) + { + if (context == null || context.Response.Headers.ContainsKey(AccessControlHeaders.AllowOrigin)) + { + return; + } + + ApplyResponseHeaders(context.Response, context.Request); + } + + private static void ApplyResponseHeaders(Response response, Request request) + { + var allowedMethods = "GET, OPTIONS, PATCH, POST, PUT, DELETE"; + + if (response.Headers.ContainsKey("Allow")) + { + allowedMethods = response.Headers["Allow"]; + } + + var requestedHeaders = String.Join(", ", request.Headers[AccessControlHeaders.RequestHeaders]); + + response.Headers.Add(AccessControlHeaders.AllowOrigin, "*"); + response.Headers.Add(AccessControlHeaders.AllowMethods, allowedMethods); + + if (request.Headers[AccessControlHeaders.RequestHeaders].Any()) + { + response.Headers.Add(AccessControlHeaders.AllowHeaders, requestedHeaders); + } + } + } +} diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index 36d1be281..aa4e9dbc3 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -96,6 +96,8 @@ + + diff --git a/src/NzbDrone.Integration.Test/Client/ClientBase.cs b/src/NzbDrone.Integration.Test/Client/ClientBase.cs index 68c738009..5d09a7373 100644 --- a/src/NzbDrone.Integration.Test/Client/ClientBase.cs +++ b/src/NzbDrone.Integration.Test/Client/ClientBase.cs @@ -45,7 +45,6 @@ public PagingResource GetPaged(int pageNumber, int pageSize, string s request.AddParameter("sortKey", sortKey); request.AddParameter("sortDir", sortDir); return Get>(request); - } public TResource Post(TResource body, HttpStatusCode statusCode = HttpStatusCode.Created) diff --git a/src/NzbDrone.Integration.Test/CorsFixture.cs b/src/NzbDrone.Integration.Test/CorsFixture.cs new file mode 100644 index 000000000..2d9d8ac4f --- /dev/null +++ b/src/NzbDrone.Integration.Test/CorsFixture.cs @@ -0,0 +1,57 @@ +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Api.Extensions; +using RestSharp; + +namespace NzbDrone.Integration.Test +{ + [TestFixture] + public class CorsFixture : IntegrationTest + { + private RestRequest BuildRequest() + { + var request = new RestRequest("series"); + request.AddHeader(AccessControlHeaders.RequestMethod, "POST"); + + return request; + } + + [Test] + public void should_not_have_allow_headers_in_response_when_not_included_in_the_request() + { + var request = BuildRequest(); + var response = RestClient.Get(request); + + response.Headers.Should().NotContain(h => h.Name == AccessControlHeaders.AllowHeaders); + } + + [Test] + public void should_have_allow_headers_in_response_when_included_in_the_request() + { + var request = BuildRequest(); + request.AddHeader(AccessControlHeaders.RequestHeaders, "X-Test"); + + var response = RestClient.Get(request); + + response.Headers.Should().Contain(h => h.Name == AccessControlHeaders.AllowHeaders); + } + + [Test] + public void should_have_allow_origin_in_response() + { + var request = BuildRequest(); + var response = RestClient.Get(request); + + response.Headers.Should().Contain(h => h.Name == AccessControlHeaders.AllowOrigin); + } + + [Test] + public void should_have_allow_methods_in_response() + { + var request = BuildRequest(); + var response = RestClient.Get(request); + + response.Headers.Should().Contain(h => h.Name == AccessControlHeaders.AllowMethods); + } + } +} diff --git a/src/NzbDrone.Integration.Test/IntegrationTest.cs b/src/NzbDrone.Integration.Test/IntegrationTest.cs index 3a1b16446..82ba4a148 100644 --- a/src/NzbDrone.Integration.Test/IntegrationTest.cs +++ b/src/NzbDrone.Integration.Test/IntegrationTest.cs @@ -63,7 +63,6 @@ public IntegrationTest() } [TestFixtureSetUp] - //[SetUp] public void SmokeTestSetup() { _runner = new NzbDroneRunner(); @@ -87,6 +86,8 @@ public void SmokeTestSetup() private void InitRestClients() { RestClient = new RestClient(RootUrl + "api/"); + RestClient.AddDefaultHeader("Authentication", _runner.ApiKey); + Series = new SeriesClient(RestClient, _runner.ApiKey); Releases = new ReleaseClient(RestClient, _runner.ApiKey); RootFolders = new ClientBase(RestClient, _runner.ApiKey); @@ -99,7 +100,6 @@ private void InitRestClients() } [TestFixtureTearDown] - //[TearDown] public void SmokeTestTearDown() { _runner.KillAll(); @@ -153,6 +153,5 @@ protected void ConnectSignalR() _signalrConnection.Received += json => _signalRReceived.Add(Json.Deserialize(json)); ; } - } } diff --git a/src/NzbDrone.Integration.Test/NzbDrone.Integration.Test.csproj b/src/NzbDrone.Integration.Test/NzbDrone.Integration.Test.csproj index 1c4bfc2bb..e22605994 100644 --- a/src/NzbDrone.Integration.Test/NzbDrone.Integration.Test.csproj +++ b/src/NzbDrone.Integration.Test/NzbDrone.Integration.Test.csproj @@ -109,6 +109,7 @@ +