mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2025-04-24 13:57:11 -04:00
New: Use ASP.NET Core instead of Nancy
(cherry picked from commit 58ddbcd77e17ef95ecfad4b746084ee9326116f3)
This commit is contained in:
parent
7d494f9743
commit
dbdc527f2e
122 changed files with 2177 additions and 2838 deletions
|
@ -139,7 +139,8 @@ export function executeCommandHelper( payload, dispatch) {
|
|||
const promise = createAjaxRequest({
|
||||
url: '/command',
|
||||
method: 'POST',
|
||||
data: JSON.stringify(payload)
|
||||
data: JSON.stringify(payload),
|
||||
dataType: 'json'
|
||||
}).request;
|
||||
|
||||
return promise.then((data) => {
|
||||
|
|
|
@ -53,7 +53,8 @@ export const actionHandlers = handleThunks({
|
|||
const promise = createAjaxRequest({
|
||||
url: '/tag',
|
||||
method: 'POST',
|
||||
data: JSON.stringify(payload.tag)
|
||||
data: JSON.stringify(payload.tag),
|
||||
dataType: 'json'
|
||||
}).request;
|
||||
|
||||
promise.done((data) => {
|
||||
|
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NzbDrone.Common.Serializer
|
||||
{
|
||||
|
@ -15,15 +16,19 @@ namespace NzbDrone.Common.Serializer
|
|||
|
||||
public static JsonSerializerOptions GetSerializerSettings()
|
||||
{
|
||||
var serializerSettings = new JsonSerializerOptions
|
||||
{
|
||||
AllowTrailingCommas = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = true
|
||||
};
|
||||
var settings = new JsonSerializerOptions();
|
||||
ApplySerializerSettings(settings);
|
||||
return settings;
|
||||
}
|
||||
|
||||
public static void ApplySerializerSettings(JsonSerializerOptions serializerSettings)
|
||||
{
|
||||
serializerSettings.AllowTrailingCommas = true;
|
||||
serializerSettings.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
|
||||
serializerSettings.PropertyNameCaseInsensitive = true;
|
||||
serializerSettings.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase;
|
||||
serializerSettings.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
|
||||
serializerSettings.WriteIndented = true;
|
||||
|
||||
serializerSettings.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, true));
|
||||
serializerSettings.Converters.Add(new STJVersionConverter());
|
||||
|
@ -31,8 +36,6 @@ namespace NzbDrone.Common.Serializer
|
|||
serializerSettings.Converters.Add(new STJTimeSpanConverter());
|
||||
serializerSettings.Converters.Add(new STJUtcConverter());
|
||||
serializerSettings.Converters.Add(new DictionaryStringObjectConverter());
|
||||
|
||||
return serializerSettings;
|
||||
}
|
||||
|
||||
public static T Deserialize<T>(string json)
|
||||
|
@ -85,5 +88,15 @@ namespace NzbDrone.Common.Serializer
|
|||
JsonSerializer.Serialize(writer, (object)model, options);
|
||||
}
|
||||
}
|
||||
|
||||
public static Task SerializeAsync<TModel>(TModel model, Stream outputStream, JsonSerializerOptions options = null)
|
||||
{
|
||||
if (options == null)
|
||||
{
|
||||
options = SerializerSettings;
|
||||
}
|
||||
|
||||
return JsonSerializer.SerializeAsync(outputStream, (object)model, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ namespace NzbDrone.Core.IndexerSearch
|
|||
{
|
||||
public class NewznabRequest
|
||||
{
|
||||
public int id { get; set; }
|
||||
public string t { get; set; }
|
||||
public string q { get; set; }
|
||||
public string cat { get; set; }
|
||||
|
|
|
@ -5,7 +5,7 @@ using NLog;
|
|||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Core.Configuration;
|
||||
|
||||
namespace Prowlarr.Host.AccessControl
|
||||
namespace NzbDrone.Host.AccessControl
|
||||
{
|
||||
public interface IFirewallAdapter
|
||||
{
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
using System.Collections.Generic;
|
||||
using Nancy.Bootstrapper;
|
||||
using NzbDrone.Common.Composition;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.SignalR;
|
||||
using Prowlarr.Http;
|
||||
|
||||
namespace Prowlarr.Host
|
||||
{
|
||||
|
@ -28,8 +26,6 @@ namespace Prowlarr.Host
|
|||
{
|
||||
AutoRegisterImplementations<MessageHub>();
|
||||
|
||||
Container.Register<INancyBootstrapper, ProwlarrBootstrapper>();
|
||||
|
||||
if (OsInfo.IsWindows)
|
||||
{
|
||||
Container.Register<INzbDroneServiceFactory, NzbDroneServiceFactory>();
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
<OutputType>Library</OutputType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Owin" Version="5.0.4" />
|
||||
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.0" />
|
||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Host.AccessControl;
|
||||
|
||||
namespace Prowlarr.Host.AccessControl
|
||||
{
|
||||
|
|
26
src/NzbDrone.Host/WebHost/ControllerActivator.cs
Normal file
26
src/NzbDrone.Host/WebHost/ControllerActivator.cs
Normal file
|
@ -0,0 +1,26 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using NzbDrone.Common.Composition;
|
||||
|
||||
namespace NzbDrone.Host
|
||||
{
|
||||
public class ControllerActivator : IControllerActivator
|
||||
{
|
||||
private readonly IContainer _container;
|
||||
|
||||
public ControllerActivator(IContainer container)
|
||||
{
|
||||
_container = container;
|
||||
}
|
||||
|
||||
public object Create(ControllerContext context)
|
||||
{
|
||||
return _container.Resolve(context.ActionDescriptor.ControllerTypeInfo.AsType());
|
||||
}
|
||||
|
||||
public void Release(ControllerContext context, object controller)
|
||||
{
|
||||
// Nothing to do
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
using Microsoft.AspNetCore.Builder;
|
||||
|
||||
namespace Prowlarr.Host.Middleware
|
||||
{
|
||||
public interface IAspNetCoreMiddleware
|
||||
{
|
||||
int Order { get; }
|
||||
void Attach(IApplicationBuilder appBuilder);
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
using Microsoft.AspNetCore.Builder;
|
||||
using Nancy.Bootstrapper;
|
||||
using Nancy.Owin;
|
||||
|
||||
namespace Prowlarr.Host.Middleware
|
||||
{
|
||||
public class NancyMiddleware : IAspNetCoreMiddleware
|
||||
{
|
||||
private readonly INancyBootstrapper _nancyBootstrapper;
|
||||
|
||||
public int Order => 2;
|
||||
|
||||
public NancyMiddleware(INancyBootstrapper nancyBootstrapper)
|
||||
{
|
||||
_nancyBootstrapper = nancyBootstrapper;
|
||||
}
|
||||
|
||||
public void Attach(IApplicationBuilder appBuilder)
|
||||
{
|
||||
var options = new NancyOptions
|
||||
{
|
||||
Bootstrapper = _nancyBootstrapper,
|
||||
PerformPassThrough = context => context.Request.Path.StartsWith("/signalr")
|
||||
};
|
||||
|
||||
appBuilder.UseOwin(x => x.UseNancy(options));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
using System;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Composition;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.SignalR;
|
||||
|
||||
namespace Prowlarr.Host.Middleware
|
||||
{
|
||||
public class SignalRMiddleware : IAspNetCoreMiddleware
|
||||
{
|
||||
private readonly IContainer _container;
|
||||
private readonly Logger _logger;
|
||||
private static string API_KEY;
|
||||
private static string URL_BASE;
|
||||
public int Order => 1;
|
||||
|
||||
public SignalRMiddleware(IContainer container,
|
||||
IConfigFileProvider configFileProvider,
|
||||
Logger logger)
|
||||
{
|
||||
_container = container;
|
||||
_logger = logger;
|
||||
API_KEY = configFileProvider.ApiKey;
|
||||
URL_BASE = configFileProvider.UrlBase;
|
||||
}
|
||||
|
||||
public void Attach(IApplicationBuilder appBuilder)
|
||||
{
|
||||
appBuilder.UseWebSockets();
|
||||
|
||||
appBuilder.Use(async (context, next) =>
|
||||
{
|
||||
if (context.Request.Path.StartsWithSegments("/signalr") &&
|
||||
!context.Request.Path.Value.EndsWith("/negotiate"))
|
||||
{
|
||||
if (!context.Request.Query.ContainsKey("access_token") ||
|
||||
context.Request.Query["access_token"] != API_KEY)
|
||||
{
|
||||
context.Response.StatusCode = 401;
|
||||
await context.Response.WriteAsync("Unauthorized");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await next();
|
||||
}
|
||||
catch (OperationCanceledException e)
|
||||
{
|
||||
// Demote the exception to trace logging so users don't worry (as much).
|
||||
_logger.Trace(e);
|
||||
}
|
||||
});
|
||||
|
||||
appBuilder.UseEndpoints(x =>
|
||||
{
|
||||
x.MapHub<MessageHub>(URL_BASE + "/signalr/messages");
|
||||
});
|
||||
|
||||
// This is a side effect of haing multiple IoC containers, TinyIoC and whatever
|
||||
// Kestrel/SignalR is using. Ideally we'd have one IoC container, but that's non-trivial with TinyIoC
|
||||
// TODO: Use a single IoC container if supported for TinyIoC or if we switch to another system (ie Autofac).
|
||||
var hubContext = appBuilder.ApplicationServices.GetService<IHubContext<MessageHub>>();
|
||||
_container.Register(hubContext);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,42 +4,60 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Routing.Internal;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NLog;
|
||||
using NLog.Extensions.Logging;
|
||||
using NzbDrone.Common.Composition;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Exceptions;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using Prowlarr.Host.AccessControl;
|
||||
using Prowlarr.Host.Middleware;
|
||||
using NzbDrone.Host;
|
||||
using NzbDrone.Host.AccessControl;
|
||||
using NzbDrone.SignalR;
|
||||
using Prowlarr.Api.V1.System;
|
||||
using Prowlarr.Http;
|
||||
using Prowlarr.Http.Authentication;
|
||||
using Prowlarr.Http.ErrorManagement;
|
||||
using Prowlarr.Http.Frontend;
|
||||
using Prowlarr.Http.Middleware;
|
||||
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
|
||||
|
||||
namespace Prowlarr.Host
|
||||
{
|
||||
public class WebHostController : IHostController
|
||||
{
|
||||
private readonly IContainer _container;
|
||||
private readonly IRuntimeInfo _runtimeInfo;
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
private readonly IFirewallAdapter _firewallAdapter;
|
||||
private readonly IEnumerable<IAspNetCoreMiddleware> _middlewares;
|
||||
private readonly ProwlarrErrorPipeline _errorHandler;
|
||||
private readonly Logger _logger;
|
||||
private IWebHost _host;
|
||||
|
||||
public WebHostController(IRuntimeInfo runtimeInfo,
|
||||
public WebHostController(IContainer container,
|
||||
IRuntimeInfo runtimeInfo,
|
||||
IConfigFileProvider configFileProvider,
|
||||
IFirewallAdapter firewallAdapter,
|
||||
IEnumerable<IAspNetCoreMiddleware> middlewares,
|
||||
ProwlarrErrorPipeline errorHandler,
|
||||
Logger logger)
|
||||
{
|
||||
_container = container;
|
||||
_runtimeInfo = runtimeInfo;
|
||||
_configFileProvider = configFileProvider;
|
||||
_firewallAdapter = firewallAdapter;
|
||||
_middlewares = middlewares;
|
||||
_errorHandler = errorHandler;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
|
@ -105,24 +123,125 @@ namespace Prowlarr.Host
|
|||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
// So that we can resolve containers with our TinyIoC services
|
||||
services.AddSingleton(_container);
|
||||
services.AddSingleton<IControllerActivator, ControllerActivator>();
|
||||
|
||||
// Bits used in our custom middleware
|
||||
services.AddSingleton(_container.Resolve<ProwlarrErrorPipeline>());
|
||||
services.AddSingleton(_container.Resolve<ICacheableSpecification>());
|
||||
|
||||
// Used in authentication
|
||||
services.AddSingleton(_container.Resolve<IAuthenticationService>());
|
||||
|
||||
services.AddRouting(options => options.LowercaseUrls = true);
|
||||
|
||||
services.AddResponseCompression();
|
||||
|
||||
services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy(VersionedApiControllerAttribute.API_CORS_POLICY,
|
||||
builder =>
|
||||
builder.AllowAnyOrigin()
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader());
|
||||
|
||||
options.AddPolicy("AllowGet",
|
||||
builder =>
|
||||
builder.AllowAnyOrigin()
|
||||
.WithMethods("GET", "OPTIONS")
|
||||
.AllowAnyHeader());
|
||||
});
|
||||
|
||||
services
|
||||
.AddControllers(options =>
|
||||
{
|
||||
options.ReturnHttpNotAcceptable = true;
|
||||
})
|
||||
.AddApplicationPart(typeof(SystemController).Assembly)
|
||||
.AddApplicationPart(typeof(StaticResourceController).Assembly)
|
||||
.AddJsonOptions(options =>
|
||||
{
|
||||
STJson.ApplySerializerSettings(options.JsonSerializerOptions);
|
||||
});
|
||||
|
||||
services
|
||||
.AddSignalR()
|
||||
.AddJsonProtocol(options =>
|
||||
{
|
||||
options.PayloadSerializerOptions = STJson.GetSerializerSettings();
|
||||
});
|
||||
|
||||
services.AddAuthorization(options =>
|
||||
{
|
||||
options.AddPolicy("UI", policy =>
|
||||
{
|
||||
policy.AuthenticationSchemes.Add(_configFileProvider.AuthenticationMethod.ToString());
|
||||
policy.RequireAuthenticatedUser();
|
||||
});
|
||||
|
||||
options.AddPolicy("SignalR", policy =>
|
||||
{
|
||||
policy.AuthenticationSchemes.Add("SignalR");
|
||||
policy.RequireAuthenticatedUser();
|
||||
});
|
||||
|
||||
// Require auth on everything except those marked [AllowAnonymous]
|
||||
options.FallbackPolicy = new AuthorizationPolicyBuilder("API")
|
||||
.RequireAuthenticatedUser()
|
||||
.Build();
|
||||
});
|
||||
|
||||
services.AddAppAuthentication(_configFileProvider);
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseRouting();
|
||||
app.Properties["host.AppName"] = BuildInfo.AppName;
|
||||
app.UsePathBase(_configFileProvider.UrlBase);
|
||||
|
||||
foreach (var middleWare in _middlewares.OrderBy(c => c.Order))
|
||||
app.UseMiddleware<LoggingMiddleware>();
|
||||
app.UsePathBase(new PathString(_configFileProvider.UrlBase));
|
||||
app.UseExceptionHandler(new ExceptionHandlerOptions
|
||||
{
|
||||
_logger.Debug("Attaching {0} to host", middleWare.GetType().Name);
|
||||
middleWare.Attach(app);
|
||||
}
|
||||
AllowStatusCode404Response = true,
|
||||
ExceptionHandler = _errorHandler.HandleException
|
||||
});
|
||||
|
||||
app.UseRouting();
|
||||
app.UseCors();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.UseResponseCompression();
|
||||
app.Properties["host.AppName"] = BuildInfo.AppName;
|
||||
|
||||
app.UseMiddleware<VersionMiddleware>();
|
||||
app.UseMiddleware<UrlBaseMiddleware>(_configFileProvider.UrlBase);
|
||||
app.UseMiddleware<CacheHeaderMiddleware>();
|
||||
app.UseMiddleware<IfModifiedMiddleware>();
|
||||
|
||||
app.Use((context, next) =>
|
||||
{
|
||||
if (context.Request.Path.StartsWithSegments("/api/v1/command", StringComparison.CurrentCultureIgnoreCase))
|
||||
{
|
||||
context.Request.EnableBuffering();
|
||||
}
|
||||
|
||||
return next();
|
||||
});
|
||||
|
||||
app.UseWebSockets();
|
||||
|
||||
app.UseEndpoints(x =>
|
||||
{
|
||||
x.MapHub<MessageHub>("/signalr/messages").RequireAuthorization("SignalR");
|
||||
x.MapControllers();
|
||||
});
|
||||
|
||||
// This is a side effect of haing multiple IoC containers, TinyIoC and whatever
|
||||
// Kestrel/SignalR is using. Ideally we'd have one IoC container, but that's non-trivial with TinyIoC
|
||||
// TODO: Use a single IoC container if supported for TinyIoC or if we switch to another system (ie Autofac).
|
||||
_container.Register(app.ApplicationServices);
|
||||
_container.Register(app.ApplicationServices.GetService<IHubContext<MessageHub>>());
|
||||
_container.Register(app.ApplicationServices.GetService<IActionDescriptorCollectionProvider>());
|
||||
_container.Register(app.ApplicationServices.GetService<EndpointDataSource>());
|
||||
_container.Register(app.ApplicationServices.GetService<DfaGraphWriter>());
|
||||
})
|
||||
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||
.Build();
|
||||
|
|
|
@ -51,11 +51,7 @@ namespace NzbDrone.Integration.Test.Client
|
|||
throw response.ErrorException;
|
||||
}
|
||||
|
||||
// cache control header gets reordered on net core
|
||||
((string)response.Headers.Single(c => c.Name == "Cache-Control").Value).Split(',').Select(x => x.Trim())
|
||||
.Should().BeEquivalentTo("no-store, must-revalidate, no-cache, max-age=0".Split(',').Select(x => x.Trim()));
|
||||
response.Headers.Single(c => c.Name == "Pragma").Value.Should().Be("no-cache");
|
||||
response.Headers.Single(c => c.Name == "Expires").Value.Should().Be("0");
|
||||
AssertDisableCache(response);
|
||||
|
||||
response.ErrorMessage.Should().BeNullOrWhiteSpace();
|
||||
|
||||
|
@ -71,6 +67,16 @@ namespace NzbDrone.Integration.Test.Client
|
|||
|
||||
return Json.Deserialize<T>(content);
|
||||
}
|
||||
|
||||
private static void AssertDisableCache(IRestResponse response)
|
||||
{
|
||||
// cache control header gets reordered on net core
|
||||
var headers = response.Headers;
|
||||
((string)headers.Single(c => c.Name == "Cache-Control").Value).Split(',').Select(x => x.Trim())
|
||||
.Should().BeEquivalentTo("no-store, no-cache".Split(',').Select(x => x.Trim()));
|
||||
headers.Single(c => c.Name == "Pragma").Value.Should().Be("no-cache");
|
||||
headers.Single(c => c.Name == "Expires").Value.Should().Be("-1");
|
||||
}
|
||||
}
|
||||
|
||||
public class ClientBase<TResource> : ClientBase
|
||||
|
|
|
@ -11,6 +11,7 @@ namespace NzbDrone.Integration.Test
|
|||
private RestRequest BuildGet(string route = "indexer")
|
||||
{
|
||||
var request = new RestRequest(route, Method.GET);
|
||||
request.AddHeader("Origin", "http://a.different.domain");
|
||||
request.AddHeader(AccessControlHeaders.RequestMethod, "POST");
|
||||
|
||||
return request;
|
||||
|
@ -19,6 +20,8 @@ namespace NzbDrone.Integration.Test
|
|||
private RestRequest BuildOptions(string route = "indexer")
|
||||
{
|
||||
var request = new RestRequest(route, Method.OPTIONS);
|
||||
request.AddHeader("Origin", "http://a.different.domain");
|
||||
request.AddHeader(AccessControlHeaders.RequestMethod, "POST");
|
||||
|
||||
return request;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System.Net;
|
||||
using System.Net;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using RestSharp;
|
||||
|
@ -33,8 +33,6 @@ namespace NzbDrone.Integration.Test
|
|||
[TestCase("application/junk")]
|
||||
public void should_get_unacceptable_with_accept_header(string header)
|
||||
{
|
||||
IgnoreOnMonoVersions("5.12", "5.14");
|
||||
|
||||
var request = new RestRequest("system/status")
|
||||
{
|
||||
RequestFormat = DataFormat.None
|
||||
|
|
|
@ -11,8 +11,6 @@ namespace NzbDrone.Integration.Test
|
|||
[Test]
|
||||
public void should_log_on_error()
|
||||
{
|
||||
IgnoreOnMonoVersions("5.12", "5.14");
|
||||
|
||||
var config = HostConfig.Get(1);
|
||||
config.LogLevel = "Trace";
|
||||
HostConfig.Put(config);
|
||||
|
|
|
@ -131,22 +131,6 @@ namespace NzbDrone.Integration.Test
|
|||
}
|
||||
}
|
||||
|
||||
protected void IgnoreOnMonoVersions(params string[] version_strings)
|
||||
{
|
||||
if (!PlatformInfo.IsMono)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var current = PlatformInfo.GetVersion();
|
||||
var versions = version_strings.Select(x => new Version(x)).ToList();
|
||||
|
||||
if (versions.Any(x => x.Major == current.Major && x.Minor == current.Minor))
|
||||
{
|
||||
throw new IgnoreException($"Ignored on mono {PlatformInfo.GetVersion()}");
|
||||
}
|
||||
}
|
||||
|
||||
public string GetTempDirectory(params string[] args)
|
||||
{
|
||||
var path = Path.Combine(TempDirectory, Path.Combine(args));
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.IO;
|
||||
using FluentAssertions;
|
||||
using Mono.Posix;
|
||||
using Mono.Unix;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Mono.Disk;
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
using NzbDrone.Core.Applications;
|
||||
using Prowlarr.Http;
|
||||
|
||||
namespace Prowlarr.Api.V1.Application
|
||||
{
|
||||
public class ApplicationModule : ProviderModuleBase<ApplicationResource, IApplication, ApplicationDefinition>
|
||||
[V1ApiController("applications")]
|
||||
public class ApplicationController : ProviderControllerBase<ApplicationResource, IApplication, ApplicationDefinition>
|
||||
{
|
||||
public static readonly ApplicationResourceMapper ResourceMapper = new ApplicationResourceMapper();
|
||||
|
||||
public ApplicationModule(ApplicationFactory applicationsFactory)
|
||||
public ApplicationController(ApplicationFactory applicationsFactory)
|
||||
: base(applicationsFactory, "applications", ResourceMapper)
|
||||
{
|
||||
}
|
|
@ -1,27 +1,32 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Common;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Common.TPL;
|
||||
using NzbDrone.Core.Datastore.Events;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.ProgressMessaging;
|
||||
using NzbDrone.Http.REST.Attributes;
|
||||
using NzbDrone.SignalR;
|
||||
using Prowlarr.Http;
|
||||
using Prowlarr.Http.Extensions;
|
||||
using Prowlarr.Http.REST;
|
||||
using Prowlarr.Http.Validation;
|
||||
|
||||
namespace Prowlarr.Api.V1.Commands
|
||||
{
|
||||
public class CommandModule : ProwlarrRestModuleWithSignalR<CommandResource, CommandModel>, IHandle<CommandUpdatedEvent>
|
||||
[V1ApiController]
|
||||
public class CommandController : RestControllerWithSignalR<CommandResource, CommandModel>, IHandle<CommandUpdatedEvent>
|
||||
{
|
||||
private readonly IManageCommandQueue _commandQueueManager;
|
||||
private readonly IServiceFactory _serviceFactory;
|
||||
private readonly Debouncer _debouncer;
|
||||
private readonly Dictionary<int, CommandResource> _pendingUpdates;
|
||||
|
||||
public CommandModule(IManageCommandQueue commandQueueManager,
|
||||
public CommandController(IManageCommandQueue commandQueueManager,
|
||||
IBroadcastSignalRMessage signalRBroadcaster,
|
||||
IServiceFactory serviceFactory)
|
||||
: base(signalRBroadcaster)
|
||||
|
@ -32,45 +37,49 @@ namespace Prowlarr.Api.V1.Commands
|
|||
_debouncer = new Debouncer(SendUpdates, TimeSpan.FromSeconds(0.1));
|
||||
_pendingUpdates = new Dictionary<int, CommandResource>();
|
||||
|
||||
GetResourceById = GetCommand;
|
||||
CreateResource = StartCommand;
|
||||
GetResourceAll = GetStartedCommands;
|
||||
DeleteResource = CancelCommand;
|
||||
|
||||
PostValidator.RuleFor(c => c.Name).NotBlank();
|
||||
}
|
||||
|
||||
private CommandResource GetCommand(int id)
|
||||
public override CommandResource GetResourceById(int id)
|
||||
{
|
||||
return _commandQueueManager.Get(id).ToResource();
|
||||
}
|
||||
|
||||
private int StartCommand(CommandResource commandResource)
|
||||
[RestPostById]
|
||||
public ActionResult<CommandResource> StartCommand(CommandResource commandResource)
|
||||
{
|
||||
var commandType =
|
||||
_serviceFactory.GetImplementations(typeof(Command))
|
||||
.Single(c => c.Name.Replace("Command", "")
|
||||
.Equals(commandResource.Name, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
dynamic command = Request.Body.FromJson(commandType);
|
||||
Request.Body.Seek(0, SeekOrigin.Begin);
|
||||
using var reader = new StreamReader(Request.Body);
|
||||
var body = reader.ReadToEnd();
|
||||
|
||||
dynamic command = STJson.Deserialize(body, commandType);
|
||||
|
||||
command.Trigger = CommandTrigger.Manual;
|
||||
command.SuppressMessages = !command.SendUpdatesToClient;
|
||||
command.SendUpdatesToClient = true;
|
||||
|
||||
var trackedCommand = _commandQueueManager.Push(command, CommandPriority.Normal, CommandTrigger.Manual);
|
||||
return trackedCommand.Id;
|
||||
return Created(trackedCommand.Id);
|
||||
}
|
||||
|
||||
private List<CommandResource> GetStartedCommands()
|
||||
[HttpGet]
|
||||
public List<CommandResource> GetStartedCommands()
|
||||
{
|
||||
return _commandQueueManager.All().ToResource();
|
||||
}
|
||||
|
||||
private void CancelCommand(int id)
|
||||
[RestDeleteById]
|
||||
public void CancelCommand(int id)
|
||||
{
|
||||
_commandQueueManager.Cancel(id);
|
||||
}
|
||||
|
||||
[NonAction]
|
||||
public void Handle(CommandUpdatedEvent message)
|
||||
{
|
||||
if (message.Command.Body.SendUpdatesToClient)
|
48
src/Prowlarr.Api.V1/Config/ConfigController.cs
Normal file
48
src/Prowlarr.Api.V1/Config/ConfigController.cs
Normal file
|
@ -0,0 +1,48 @@
|
|||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Http.REST.Attributes;
|
||||
using Prowlarr.Http.REST;
|
||||
|
||||
namespace Prowlarr.Api.V1.Config
|
||||
{
|
||||
public abstract class ConfigController<TResource> : RestController<TResource>
|
||||
where TResource : RestResource, new()
|
||||
{
|
||||
protected readonly IConfigService _configService;
|
||||
|
||||
protected ConfigController(IConfigService configService)
|
||||
{
|
||||
_configService = configService;
|
||||
}
|
||||
|
||||
public override TResource GetResourceById(int id)
|
||||
{
|
||||
return GetConfig();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public TResource GetConfig()
|
||||
{
|
||||
var resource = ToResource(_configService);
|
||||
resource.Id = 1;
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
[RestPutById]
|
||||
public virtual ActionResult<TResource> SaveConfig(TResource resource)
|
||||
{
|
||||
var dictionary = resource.GetType()
|
||||
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
|
||||
.ToDictionary(prop => prop.Name, prop => prop.GetValue(resource, null));
|
||||
|
||||
_configService.SaveConfigDictionary(dictionary);
|
||||
|
||||
return Accepted(resource.Id);
|
||||
}
|
||||
|
||||
protected abstract TResource ToResource(IConfigService model);
|
||||
}
|
||||
}
|
39
src/Prowlarr.Api.V1/Config/DevelopmentConfigController.cs
Normal file
39
src/Prowlarr.Api.V1/Config/DevelopmentConfigController.cs
Normal file
|
@ -0,0 +1,39 @@
|
|||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using Prowlarr.Api.V1.Tags;
|
||||
using Prowlarr.Http;
|
||||
|
||||
namespace Prowlarr.Api.V1.Config
|
||||
{
|
||||
[V1ApiController("config/development")]
|
||||
public class DevelopmentConfigController : ConfigController<DevelopmentConfigResource>
|
||||
{
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
|
||||
public DevelopmentConfigController(IConfigFileProvider configFileProvider,
|
||||
IConfigService configService)
|
||||
: base(configService)
|
||||
{
|
||||
_configFileProvider = configFileProvider;
|
||||
}
|
||||
|
||||
public override ActionResult<DevelopmentConfigResource> SaveConfig(DevelopmentConfigResource resource)
|
||||
{
|
||||
var dictionary = resource.GetType()
|
||||
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
|
||||
.ToDictionary(prop => prop.Name, prop => prop.GetValue(resource, null));
|
||||
|
||||
_configFileProvider.SaveConfigDictionary(dictionary);
|
||||
_configService.SaveConfigDictionary(dictionary);
|
||||
|
||||
return Accepted(resource.Id);
|
||||
}
|
||||
|
||||
protected override DevelopmentConfigResource ToResource(IConfigService model)
|
||||
{
|
||||
return DevelopmentConfigResourceMapper.ToResource(_configFileProvider, model);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using NzbDrone.Core.Authentication;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Validation.Paths;
|
||||
using Prowlarr.Http;
|
||||
|
||||
namespace Prowlarr.Api.V1.Config
|
||||
{
|
||||
public class DevelopmentConfigModule : ProwlarrRestModule<DevelopmentConfigResource>
|
||||
{
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
private readonly IConfigService _configService;
|
||||
|
||||
public DevelopmentConfigModule(IConfigFileProvider configFileProvider,
|
||||
IConfigService configService)
|
||||
: base("/config/development")
|
||||
{
|
||||
_configFileProvider = configFileProvider;
|
||||
_configService = configService;
|
||||
|
||||
GetResourceSingle = GetDevelopmentConfig;
|
||||
GetResourceById = GetDevelopmentConfig;
|
||||
UpdateResource = SaveDevelopmentConfig;
|
||||
}
|
||||
|
||||
private DevelopmentConfigResource GetDevelopmentConfig()
|
||||
{
|
||||
var resource = DevelopmentConfigResourceMapper.ToResource(_configFileProvider, _configService);
|
||||
resource.Id = 1;
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
private DevelopmentConfigResource GetDevelopmentConfig(int id)
|
||||
{
|
||||
return GetDevelopmentConfig();
|
||||
}
|
||||
|
||||
private void SaveDevelopmentConfig(DevelopmentConfigResource resource)
|
||||
{
|
||||
var dictionary = resource.GetType()
|
||||
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
|
||||
.ToDictionary(prop => prop.Name, prop => prop.GetValue(resource, null));
|
||||
|
||||
_configFileProvider.SaveConfigDictionary(dictionary);
|
||||
_configService.SaveConfigDictionary(dictionary);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,10 +1,12 @@
|
|||
using NzbDrone.Core.Configuration;
|
||||
using Prowlarr.Http;
|
||||
|
||||
namespace Prowlarr.Api.V1.Config
|
||||
{
|
||||
public class DownloadClientConfigModule : ProwlarrConfigModule<DownloadClientConfigResource>
|
||||
[V1ApiController("config/downloadclient")]
|
||||
public class DownloadClientConfigController : ConfigController<DownloadClientConfigResource>
|
||||
{
|
||||
public DownloadClientConfigModule(IConfigService configService)
|
||||
public DownloadClientConfigController(IConfigService configService)
|
||||
: base(configService)
|
||||
{
|
||||
}
|
|
@ -3,36 +3,35 @@ using System.Linq;
|
|||
using System.Reflection;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using FluentValidation;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Authentication;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Update;
|
||||
using NzbDrone.Core.Validation;
|
||||
using NzbDrone.Core.Validation.Paths;
|
||||
using NzbDrone.Http.REST.Attributes;
|
||||
using Prowlarr.Http;
|
||||
using Prowlarr.Http.REST;
|
||||
|
||||
namespace Prowlarr.Api.V1.Config
|
||||
{
|
||||
public class HostConfigModule : ProwlarrRestModule<HostConfigResource>
|
||||
[V1ApiController("config/host")]
|
||||
public class HostConfigController : RestController<HostConfigResource>
|
||||
{
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
private readonly IConfigService _configService;
|
||||
private readonly IUserService _userService;
|
||||
|
||||
public HostConfigModule(IConfigFileProvider configFileProvider,
|
||||
IConfigService configService,
|
||||
IUserService userService,
|
||||
FileExistsValidator fileExistsValidator)
|
||||
: base("/config/host")
|
||||
public HostConfigController(IConfigFileProvider configFileProvider,
|
||||
IConfigService configService,
|
||||
IUserService userService,
|
||||
FileExistsValidator fileExistsValidator)
|
||||
{
|
||||
_configFileProvider = configFileProvider;
|
||||
_configService = configService;
|
||||
_userService = userService;
|
||||
|
||||
GetResourceSingle = GetHostConfig;
|
||||
GetResourceById = GetHostConfig;
|
||||
UpdateResource = SaveHostConfig;
|
||||
|
||||
SharedValidator.RuleFor(c => c.BindAddress)
|
||||
.ValidIp4Address()
|
||||
.NotListenAllIp4Address()
|
||||
|
@ -79,7 +78,13 @@ namespace Prowlarr.Api.V1.Config
|
|||
return cert != null;
|
||||
}
|
||||
|
||||
private HostConfigResource GetHostConfig()
|
||||
public override HostConfigResource GetResourceById(int id)
|
||||
{
|
||||
return GetHostConfig();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public HostConfigResource GetHostConfig()
|
||||
{
|
||||
var resource = HostConfigResourceMapper.ToResource(_configFileProvider, _configService);
|
||||
resource.Id = 1;
|
||||
|
@ -94,12 +99,8 @@ namespace Prowlarr.Api.V1.Config
|
|||
return resource;
|
||||
}
|
||||
|
||||
private HostConfigResource GetHostConfig(int id)
|
||||
{
|
||||
return GetHostConfig();
|
||||
}
|
||||
|
||||
private void SaveHostConfig(HostConfigResource resource)
|
||||
[RestPutById]
|
||||
public ActionResult<HostConfigResource> SaveHostConfig(HostConfigResource resource)
|
||||
{
|
||||
var dictionary = resource.GetType()
|
||||
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
|
||||
|
@ -112,6 +113,8 @@ namespace Prowlarr.Api.V1.Config
|
|||
{
|
||||
_userService.Upsert(resource.Username, resource.Password);
|
||||
}
|
||||
|
||||
return Accepted(resource.Id);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,12 +1,14 @@
|
|||
using FluentValidation;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using Prowlarr.Http;
|
||||
using Prowlarr.Http.Validation;
|
||||
|
||||
namespace Prowlarr.Api.V1.Config
|
||||
{
|
||||
public class IndexerConfigModule : ProwlarrConfigModule<IndexerConfigResource>
|
||||
[V1ApiController("config/indexer")]
|
||||
public class IndexerConfigController : ConfigController<IndexerConfigResource>
|
||||
{
|
||||
public IndexerConfigModule(IConfigService configService)
|
||||
public IndexerConfigController(IConfigService configService)
|
||||
: base(configService)
|
||||
{
|
||||
SharedValidator.RuleFor(c => c.MinimumAge)
|
|
@ -3,12 +3,14 @@ using NzbDrone.Common.EnvironmentInfo;
|
|||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Validation;
|
||||
using NzbDrone.Core.Validation.Paths;
|
||||
using Prowlarr.Http;
|
||||
|
||||
namespace Prowlarr.Api.V1.Config
|
||||
{
|
||||
public class MediaManagementConfigModule : ProwlarrConfigModule<MediaManagementConfigResource>
|
||||
[V1ApiController("config/mediamanagement")]
|
||||
public class MediaManagementConfigController : ConfigController<MediaManagementConfigResource>
|
||||
{
|
||||
public MediaManagementConfigModule(IConfigService configService, PathExistsValidator pathExistsValidator, FileChmodValidator fileChmodValidator)
|
||||
public MediaManagementConfigController(IConfigService configService, PathExistsValidator pathExistsValidator, FileChmodValidator fileChmodValidator)
|
||||
: base(configService)
|
||||
{
|
||||
SharedValidator.RuleFor(c => c.RecycleBinCleanupDays).GreaterThanOrEqualTo(0);
|
|
@ -1,53 +0,0 @@
|
|||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using Prowlarr.Http;
|
||||
using Prowlarr.Http.REST;
|
||||
|
||||
namespace Prowlarr.Api.V1.Config
|
||||
{
|
||||
public abstract class ProwlarrConfigModule<TResource> : ProwlarrRestModule<TResource>
|
||||
where TResource : RestResource, new()
|
||||
{
|
||||
private readonly IConfigService _configService;
|
||||
|
||||
protected ProwlarrConfigModule(IConfigService configService)
|
||||
: this(new TResource().ResourceName.Replace("config", ""), configService)
|
||||
{
|
||||
}
|
||||
|
||||
protected ProwlarrConfigModule(string resource, IConfigService configService)
|
||||
: base("config/" + resource.Trim('/'))
|
||||
{
|
||||
_configService = configService;
|
||||
|
||||
GetResourceSingle = GetConfig;
|
||||
GetResourceById = GetConfig;
|
||||
UpdateResource = SaveConfig;
|
||||
}
|
||||
|
||||
private TResource GetConfig()
|
||||
{
|
||||
var resource = ToResource(_configService);
|
||||
resource.Id = 1;
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
protected abstract TResource ToResource(IConfigService model);
|
||||
|
||||
private TResource GetConfig(int id)
|
||||
{
|
||||
return GetConfig();
|
||||
}
|
||||
|
||||
private void SaveConfig(TResource resource)
|
||||
{
|
||||
var dictionary = resource.GetType()
|
||||
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
|
||||
.ToDictionary(prop => prop.Name, prop => prop.GetValue(resource, null));
|
||||
|
||||
_configService.SaveConfigDictionary(dictionary);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,10 +1,12 @@
|
|||
using NzbDrone.Core.Configuration;
|
||||
using Prowlarr.Http;
|
||||
|
||||
namespace Prowlarr.Api.V1.Config
|
||||
{
|
||||
public class UiConfigModule : ProwlarrConfigModule<UiConfigResource>
|
||||
[V1ApiController("config/ui")]
|
||||
public class UiConfigController : ConfigController<UiConfigResource>
|
||||
{
|
||||
public UiConfigModule(IConfigService configService)
|
||||
public UiConfigController(IConfigService configService)
|
||||
: base(configService)
|
||||
{
|
||||
}
|
52
src/Prowlarr.Api.V1/CustomFilters/CustomFilterController.cs
Normal file
52
src/Prowlarr.Api.V1/CustomFilters/CustomFilterController.cs
Normal file
|
@ -0,0 +1,52 @@
|
|||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Core.CustomFilters;
|
||||
using NzbDrone.Http.REST.Attributes;
|
||||
using Prowlarr.Http;
|
||||
using Prowlarr.Http.REST;
|
||||
|
||||
namespace Prowlarr.Api.V1.CustomFilters
|
||||
{
|
||||
[V1ApiController]
|
||||
public class CustomFilterController : RestController<CustomFilterResource>
|
||||
{
|
||||
private readonly ICustomFilterService _customFilterService;
|
||||
|
||||
public CustomFilterController(ICustomFilterService customFilterService)
|
||||
{
|
||||
_customFilterService = customFilterService;
|
||||
}
|
||||
|
||||
public override CustomFilterResource GetResourceById(int id)
|
||||
{
|
||||
return _customFilterService.Get(id).ToResource();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public List<CustomFilterResource> GetCustomFilters()
|
||||
{
|
||||
return _customFilterService.All().ToResource();
|
||||
}
|
||||
|
||||
[RestPostById]
|
||||
public ActionResult<CustomFilterResource> AddCustomFilter(CustomFilterResource resource)
|
||||
{
|
||||
var customFilter = _customFilterService.Add(resource.ToModel());
|
||||
|
||||
return Created(customFilter.Id);
|
||||
}
|
||||
|
||||
[RestPutById]
|
||||
public ActionResult<CustomFilterResource> UpdateCustomFilter(CustomFilterResource resource)
|
||||
{
|
||||
_customFilterService.Update(resource.ToModel());
|
||||
return Accepted(resource.Id);
|
||||
}
|
||||
|
||||
[RestDeleteById]
|
||||
public void DeleteCustomResource(int id)
|
||||
{
|
||||
_customFilterService.Delete(id);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.CustomFilters;
|
||||
using Prowlarr.Http;
|
||||
|
||||
namespace Prowlarr.Api.V1.CustomFilters
|
||||
{
|
||||
public class CustomFilterModule : ProwlarrRestModule<CustomFilterResource>
|
||||
{
|
||||
private readonly ICustomFilterService _customFilterService;
|
||||
|
||||
public CustomFilterModule(ICustomFilterService customFilterService)
|
||||
{
|
||||
_customFilterService = customFilterService;
|
||||
|
||||
GetResourceById = GetCustomFilter;
|
||||
GetResourceAll = GetCustomFilters;
|
||||
CreateResource = AddCustomFilter;
|
||||
UpdateResource = UpdateCustomFilter;
|
||||
DeleteResource = DeleteCustomResource;
|
||||
}
|
||||
|
||||
private CustomFilterResource GetCustomFilter(int id)
|
||||
{
|
||||
return _customFilterService.Get(id).ToResource();
|
||||
}
|
||||
|
||||
private List<CustomFilterResource> GetCustomFilters()
|
||||
{
|
||||
return _customFilterService.All().ToResource();
|
||||
}
|
||||
|
||||
private int AddCustomFilter(CustomFilterResource resource)
|
||||
{
|
||||
var customFilter = _customFilterService.Add(resource.ToModel());
|
||||
|
||||
return customFilter.Id;
|
||||
}
|
||||
|
||||
private void UpdateCustomFilter(CustomFilterResource resource)
|
||||
{
|
||||
_customFilterService.Update(resource.ToModel());
|
||||
}
|
||||
|
||||
private void DeleteCustomResource(int id)
|
||||
{
|
||||
_customFilterService.Delete(id);
|
||||
}
|
||||
}
|
||||
}
|
41
src/Prowlarr.Api.V1/FileSystem/FileSystemController.cs
Normal file
41
src/Prowlarr.Api.V1/FileSystem/FileSystemController.cs
Normal file
|
@ -0,0 +1,41 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Common.Disk;
|
||||
using Prowlarr.Http;
|
||||
|
||||
namespace Prowlarr.Api.V1.FileSystem
|
||||
{
|
||||
[V1ApiController]
|
||||
public class FileSystemController : Controller
|
||||
{
|
||||
private readonly IFileSystemLookupService _fileSystemLookupService;
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
|
||||
public FileSystemController(IFileSystemLookupService fileSystemLookupService,
|
||||
IDiskProvider diskProvider)
|
||||
{
|
||||
_fileSystemLookupService = fileSystemLookupService;
|
||||
_diskProvider = diskProvider;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult GetContents(string path, bool includeFiles = false, bool allowFoldersWithoutTrailingSlashes = false)
|
||||
{
|
||||
return Ok(_fileSystemLookupService.LookupContents(path, includeFiles, allowFoldersWithoutTrailingSlashes));
|
||||
}
|
||||
|
||||
[HttpGet("type")]
|
||||
public object GetEntityType(string path)
|
||||
{
|
||||
if (_diskProvider.FileExists(path))
|
||||
{
|
||||
return new { type = "file" };
|
||||
}
|
||||
|
||||
//Return folder even if it doesn't exist on disk to avoid leaking anything from the UI about the underlying system
|
||||
return new { type = "folder" };
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Nancy;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using Prowlarr.Http.Extensions;
|
||||
|
||||
namespace Prowlarr.Api.V1.FileSystem
|
||||
{
|
||||
public class FileSystemModule : ProwlarrV1Module
|
||||
{
|
||||
private readonly IFileSystemLookupService _fileSystemLookupService;
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
|
||||
public FileSystemModule(IFileSystemLookupService fileSystemLookupService,
|
||||
IDiskProvider diskProvider)
|
||||
: base("/filesystem")
|
||||
{
|
||||
_fileSystemLookupService = fileSystemLookupService;
|
||||
_diskProvider = diskProvider;
|
||||
Get("/", x => GetContents());
|
||||
Get("/type", x => GetEntityType());
|
||||
}
|
||||
|
||||
private object GetContents()
|
||||
{
|
||||
var pathQuery = Request.Query.path;
|
||||
var includeFiles = Request.GetBooleanQueryParameter("includeFiles");
|
||||
var allowFoldersWithoutTrailingSlashes = Request.GetBooleanQueryParameter("allowFoldersWithoutTrailingSlashes");
|
||||
|
||||
return _fileSystemLookupService.LookupContents((string)pathQuery.Value, includeFiles, allowFoldersWithoutTrailingSlashes);
|
||||
}
|
||||
|
||||
private object GetEntityType()
|
||||
{
|
||||
var pathQuery = Request.Query.path;
|
||||
var path = (string)pathQuery.Value;
|
||||
|
||||
if (_diskProvider.FileExists(path))
|
||||
{
|
||||
return new { type = "file" };
|
||||
}
|
||||
|
||||
//Return folder even if it doesn't exist on disk to avoid leaking anything from the UI about the underlying system
|
||||
return new { type = "folder" };
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,29 +1,39 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Core.Datastore.Events;
|
||||
using NzbDrone.Core.HealthCheck;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.SignalR;
|
||||
using Prowlarr.Http;
|
||||
using Prowlarr.Http.REST;
|
||||
|
||||
namespace Prowlarr.Api.V1.Health
|
||||
{
|
||||
public class HealthModule : ProwlarrRestModuleWithSignalR<HealthResource, HealthCheck>,
|
||||
[V1ApiController]
|
||||
public class HealthController : RestControllerWithSignalR<HealthResource, HealthCheck>,
|
||||
IHandle<HealthCheckCompleteEvent>
|
||||
{
|
||||
private readonly IHealthCheckService _healthCheckService;
|
||||
|
||||
public HealthModule(IBroadcastSignalRMessage signalRBroadcaster, IHealthCheckService healthCheckService)
|
||||
public HealthController(IBroadcastSignalRMessage signalRBroadcaster, IHealthCheckService healthCheckService)
|
||||
: base(signalRBroadcaster)
|
||||
{
|
||||
_healthCheckService = healthCheckService;
|
||||
GetResourceAll = GetHealth;
|
||||
}
|
||||
|
||||
private List<HealthResource> GetHealth()
|
||||
public override HealthResource GetResourceById(int id)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public List<HealthResource> GetHealth()
|
||||
{
|
||||
return _healthCheckService.Results().ToResource();
|
||||
}
|
||||
|
||||
[NonAction]
|
||||
public void Handle(HealthCheckCompleteEvent message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Sync);
|
65
src/Prowlarr.Api.V1/History/HistoryController.cs
Normal file
65
src/Prowlarr.Api.V1/History/HistoryController.cs
Normal file
|
@ -0,0 +1,65 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.History;
|
||||
using Prowlarr.Http;
|
||||
using Prowlarr.Http.Extensions;
|
||||
|
||||
namespace Prowlarr.Api.V1.History
|
||||
{
|
||||
[V1ApiController]
|
||||
public class HistoryController : Controller
|
||||
{
|
||||
private readonly IHistoryService _historyService;
|
||||
|
||||
public HistoryController(IHistoryService historyService)
|
||||
{
|
||||
_historyService = historyService;
|
||||
}
|
||||
|
||||
protected HistoryResource MapToResource(NzbDrone.Core.History.History model)
|
||||
{
|
||||
var resource = model.ToResource();
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public PagingResource<HistoryResource> GetHistory()
|
||||
{
|
||||
var pagingResource = Request.ReadPagingResourceFromRequest<HistoryResource>();
|
||||
var pagingSpec = pagingResource.MapToPagingSpec<HistoryResource, NzbDrone.Core.History.History>("date", SortDirection.Descending);
|
||||
|
||||
var eventTypeFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "eventType");
|
||||
var downloadIdFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "downloadId");
|
||||
|
||||
if (eventTypeFilter != null)
|
||||
{
|
||||
var filterValue = (HistoryEventType)Convert.ToInt32(eventTypeFilter.Value);
|
||||
pagingSpec.FilterExpressions.Add(v => v.EventType == filterValue);
|
||||
}
|
||||
|
||||
if (downloadIdFilter != null)
|
||||
{
|
||||
var downloadId = downloadIdFilter.Value;
|
||||
pagingSpec.FilterExpressions.Add(h => h.DownloadId == downloadId);
|
||||
}
|
||||
|
||||
return pagingSpec.ApplyToPage(_historyService.Paged, h => MapToResource(h));
|
||||
}
|
||||
|
||||
[HttpGet("since")]
|
||||
public List<HistoryResource> GetHistorySince(DateTime date, HistoryEventType? eventType = null)
|
||||
{
|
||||
return _historyService.Since(date, eventType).Select(h => MapToResource(h)).ToList();
|
||||
}
|
||||
|
||||
[HttpGet("indexer")]
|
||||
public List<HistoryResource> GetIndexerHistory(int indexerId, HistoryEventType? eventType = null)
|
||||
{
|
||||
return _historyService.GetByIndexerId(indexerId, eventType).Select(h => MapToResource(h)).ToList();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,100 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Nancy;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.History;
|
||||
using Prowlarr.Http;
|
||||
using Prowlarr.Http.Extensions;
|
||||
using Prowlarr.Http.REST;
|
||||
|
||||
namespace Prowlarr.Api.V1.History
|
||||
{
|
||||
public class HistoryModule : ProwlarrRestModule<HistoryResource>
|
||||
{
|
||||
private readonly IHistoryService _historyService;
|
||||
|
||||
public HistoryModule(IHistoryService historyService)
|
||||
{
|
||||
_historyService = historyService;
|
||||
GetResourcePaged = GetHistory;
|
||||
|
||||
Get("/since", x => GetHistorySince());
|
||||
Get("/indexer", x => GetIndexerHistory());
|
||||
}
|
||||
|
||||
protected HistoryResource MapToResource(NzbDrone.Core.History.History model, bool includeMovie)
|
||||
{
|
||||
var resource = model.ToResource();
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
private PagingResource<HistoryResource> GetHistory(PagingResource<HistoryResource> pagingResource)
|
||||
{
|
||||
var pagingSpec = pagingResource.MapToPagingSpec<HistoryResource, NzbDrone.Core.History.History>("date", SortDirection.Descending);
|
||||
var includeMovie = Request.GetBooleanQueryParameter("includeMovie");
|
||||
|
||||
var eventTypeFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "eventType");
|
||||
var downloadIdFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "downloadId");
|
||||
|
||||
if (eventTypeFilter != null)
|
||||
{
|
||||
var filterValue = (HistoryEventType)Convert.ToInt32(eventTypeFilter.Value);
|
||||
pagingSpec.FilterExpressions.Add(v => v.EventType == filterValue);
|
||||
}
|
||||
|
||||
if (downloadIdFilter != null)
|
||||
{
|
||||
var downloadId = downloadIdFilter.Value;
|
||||
pagingSpec.FilterExpressions.Add(h => h.DownloadId == downloadId);
|
||||
}
|
||||
|
||||
return ApplyToPage(_historyService.Paged, pagingSpec, h => MapToResource(h, includeMovie));
|
||||
}
|
||||
|
||||
private List<HistoryResource> GetHistorySince()
|
||||
{
|
||||
var queryDate = Request.Query.Date;
|
||||
var queryEventType = Request.Query.EventType;
|
||||
|
||||
if (!queryDate.HasValue)
|
||||
{
|
||||
throw new BadRequestException("date is missing");
|
||||
}
|
||||
|
||||
DateTime date = DateTime.Parse(queryDate.Value);
|
||||
HistoryEventType? eventType = null;
|
||||
var includeMovie = Request.GetBooleanQueryParameter("includeMovie");
|
||||
|
||||
if (queryEventType.HasValue)
|
||||
{
|
||||
eventType = (HistoryEventType)Convert.ToInt32(queryEventType.Value);
|
||||
}
|
||||
|
||||
return _historyService.Since(date, eventType).Select(h => MapToResource(h, includeMovie)).ToList();
|
||||
}
|
||||
|
||||
private List<HistoryResource> GetIndexerHistory()
|
||||
{
|
||||
var queryIndexerId = Request.Query.IndexerId;
|
||||
var queryEventType = Request.Query.EventType;
|
||||
|
||||
if (!queryIndexerId.HasValue)
|
||||
{
|
||||
throw new BadRequestException("indexerId is missing");
|
||||
}
|
||||
|
||||
int indexerId = Convert.ToInt32(queryIndexerId.Value);
|
||||
HistoryEventType? eventType = null;
|
||||
var includeIndexer = Request.GetBooleanQueryParameter("includeIndexer");
|
||||
|
||||
if (queryEventType.HasValue)
|
||||
{
|
||||
eventType = (HistoryEventType)Convert.ToInt32(queryEventType.Value);
|
||||
}
|
||||
|
||||
return _historyService.GetByIndexerId(indexerId, eventType).Select(h => MapToResource(h, includeIndexer)).ToList();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,42 +2,32 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using Nancy;
|
||||
using Nancy.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.IndexerSearch;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Http.Extensions;
|
||||
using Prowlarr.Http;
|
||||
using Prowlarr.Http.Extensions;
|
||||
using Prowlarr.Http.REST;
|
||||
|
||||
namespace Prowlarr.Api.V1.Indexers
|
||||
{
|
||||
public class IndexerModule : ProviderModuleBase<IndexerResource, IIndexer, IndexerDefinition>
|
||||
[V1ApiController]
|
||||
public class IndexerController : ProviderControllerBase<IndexerResource, IIndexer, IndexerDefinition>
|
||||
{
|
||||
private IIndexerFactory _indexerFactory { get; set; }
|
||||
private ISearchForNzb _nzbSearchService { get; set; }
|
||||
private IDownloadMappingService _downloadMappingService { get; set; }
|
||||
private IDownloadService _downloadService { get; set; }
|
||||
|
||||
public IndexerModule(IndexerFactory indexerFactory, ISearchForNzb nzbSearchService, IDownloadMappingService downloadMappingService, IDownloadService downloadService, IndexerResourceMapper resourceMapper)
|
||||
public IndexerController(IndexerFactory indexerFactory, ISearchForNzb nzbSearchService, IDownloadMappingService downloadMappingService, IDownloadService downloadService, IndexerResourceMapper resourceMapper)
|
||||
: base(indexerFactory, "indexer", resourceMapper)
|
||||
{
|
||||
_indexerFactory = indexerFactory;
|
||||
_nzbSearchService = nzbSearchService;
|
||||
_downloadMappingService = downloadMappingService;
|
||||
_downloadService = downloadService;
|
||||
|
||||
Get("{id}/newznab", x =>
|
||||
{
|
||||
var request = this.Bind<NewznabRequest>();
|
||||
return GetNewznabResponse(request);
|
||||
});
|
||||
Get("{id}/download", x =>
|
||||
{
|
||||
return GetDownload(x.id);
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Validate(IndexerDefinition definition, bool includeWarnings)
|
||||
|
@ -50,10 +40,11 @@ namespace Prowlarr.Api.V1.Indexers
|
|||
base.Validate(definition, includeWarnings);
|
||||
}
|
||||
|
||||
private object GetNewznabResponse(NewznabRequest request)
|
||||
[HttpGet("{id:int}/newznab")]
|
||||
public IActionResult GetNewznabResponse(int id, [FromQuery] NewznabRequest request)
|
||||
{
|
||||
var requestType = request.t;
|
||||
request.source = UserAgentParser.ParseSource(Request.Headers.UserAgent);
|
||||
request.source = UserAgentParser.ParseSource(Request.Headers["User-Agent"]);
|
||||
request.server = Request.GetServerUrl();
|
||||
|
||||
if (requestType.IsNullOrWhiteSpace())
|
||||
|
@ -61,7 +52,7 @@ namespace Prowlarr.Api.V1.Indexers
|
|||
throw new BadRequestException("Missing Function Parameter");
|
||||
}
|
||||
|
||||
var indexer = _indexerFactory.Get(request.id);
|
||||
var indexer = _indexerFactory.Get(id);
|
||||
|
||||
if (indexer == null)
|
||||
{
|
||||
|
@ -73,32 +64,26 @@ namespace Prowlarr.Api.V1.Indexers
|
|||
switch (requestType)
|
||||
{
|
||||
case "caps":
|
||||
Response response = indexerInstance.GetCapabilities().ToXml();
|
||||
response.ContentType = "application/rss+xml";
|
||||
return response;
|
||||
return Content(indexerInstance.GetCapabilities().ToXml(), "application/rss+xml");
|
||||
case "search":
|
||||
case "tvsearch":
|
||||
case "music":
|
||||
case "book":
|
||||
case "movie":
|
||||
var results = _nzbSearchService.Search(request, new List<int> { indexer.Id }, false);
|
||||
|
||||
Response searchResponse = results.ToXml(indexerInstance.Protocol);
|
||||
searchResponse.ContentType = "application/rss+xml";
|
||||
return searchResponse;
|
||||
return Content(results.ToXml(indexerInstance.Protocol), "application/rss+xml");
|
||||
default:
|
||||
throw new BadRequestException("Function Not Available");
|
||||
}
|
||||
}
|
||||
|
||||
private object GetDownload(int id)
|
||||
[HttpGet("{id:int}/download")]
|
||||
public object GetDownload(int id, string link, string file)
|
||||
{
|
||||
var indexerDef = _indexerFactory.Get(id);
|
||||
var indexer = _indexerFactory.GetInstance(indexerDef);
|
||||
var link = Request.Query.Link;
|
||||
var file = Request.Query.File;
|
||||
|
||||
if (!link.HasValue || !file.HasValue)
|
||||
if (link.IsNullOrWhiteSpace() || file.IsNullOrWhiteSpace())
|
||||
{
|
||||
throw new BadRequestException("Invalid Prowlarr link");
|
||||
}
|
||||
|
@ -110,15 +95,15 @@ namespace Prowlarr.Api.V1.Indexers
|
|||
throw new NotFoundException("Indexer Not Found");
|
||||
}
|
||||
|
||||
var source = UserAgentParser.ParseSource(Request.Headers.UserAgent);
|
||||
var source = UserAgentParser.ParseSource(Request.Headers["User-Agent"]);
|
||||
|
||||
var unprotectedlLink = _downloadMappingService.ConvertToNormalLink((string)link.Value);
|
||||
var unprotectedlLink = _downloadMappingService.ConvertToNormalLink(link);
|
||||
|
||||
// If Indexer is set to download via Redirect then just redirect to the link
|
||||
if (indexer.SupportsRedirect && indexerDef.Redirect)
|
||||
{
|
||||
_downloadService.RecordRedirect(unprotectedlLink, id, source, file);
|
||||
return Response.AsRedirect(unprotectedlLink, Nancy.Responses.RedirectResponse.RedirectType.Permanent);
|
||||
return RedirectPermanent(unprotectedlLink);
|
||||
}
|
||||
|
||||
var downloadBytes = Array.Empty<byte>();
|
||||
|
@ -135,14 +120,14 @@ namespace Prowlarr.Api.V1.Indexers
|
|||
&& downloadBytes[6] == 0x3a)
|
||||
{
|
||||
var magnetUrl = Encoding.UTF8.GetString(downloadBytes);
|
||||
return Response.AsRedirect(magnetUrl, Nancy.Responses.RedirectResponse.RedirectType.Permanent);
|
||||
return RedirectPermanent(magnetUrl);
|
||||
}
|
||||
|
||||
var contentType = indexer.Protocol == DownloadProtocol.Torrent ? "application/x-bittorrent" : "application/x-nzb";
|
||||
var extension = indexer.Protocol == DownloadProtocol.Torrent ? "torrent" : "nzb";
|
||||
var filename = $"{file}.{extension}";
|
||||
|
||||
return Response.FromByteArray(downloadBytes, contentType).AsAttachment(filename, contentType);
|
||||
return File(downloadBytes, contentType, filename);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using Prowlarr.Http;
|
||||
|
||||
namespace NzbDrone.Api.V1.Indexers
|
||||
{
|
||||
[V1ApiController("indexer/categories")]
|
||||
public class IndexerDefaultCategoriesController : Controller
|
||||
{
|
||||
[HttpGet]
|
||||
public IndexerCategory[] GetAll()
|
||||
{
|
||||
return NewznabStandardCategory.ParentCats;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
using NzbDrone.Core.Indexers;
|
||||
using Prowlarr.Api.V1;
|
||||
|
||||
namespace NzbDrone.Api.V1.Indexers
|
||||
{
|
||||
public class IndexerDefaultCategoriesModule : ProwlarrV1Module
|
||||
{
|
||||
public IndexerDefaultCategoriesModule()
|
||||
: base("/indexer/categories")
|
||||
{
|
||||
Get("/", movie => GetAll());
|
||||
}
|
||||
|
||||
private IndexerCategory[] GetAll()
|
||||
{
|
||||
return NewznabStandardCategory.ParentCats;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,32 +1,30 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Nancy;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using Prowlarr.Http;
|
||||
using Prowlarr.Http.Extensions;
|
||||
|
||||
namespace Prowlarr.Api.V1.Indexers
|
||||
{
|
||||
public class IndexerEditorModule : ProwlarrV1Module
|
||||
[V1ApiController("indexer/editor")]
|
||||
public class IndexerEditorController : Controller
|
||||
{
|
||||
private readonly IIndexerFactory _indexerService;
|
||||
private readonly IManageCommandQueue _commandQueueManager;
|
||||
private readonly IndexerResourceMapper _resourceMapper;
|
||||
|
||||
public IndexerEditorModule(IIndexerFactory indexerService, IManageCommandQueue commandQueueManager, IndexerResourceMapper resourceMapper)
|
||||
: base("/indexer/editor")
|
||||
public IndexerEditorController(IIndexerFactory indexerService, IManageCommandQueue commandQueueManager, IndexerResourceMapper resourceMapper)
|
||||
{
|
||||
_indexerService = indexerService;
|
||||
_commandQueueManager = commandQueueManager;
|
||||
_resourceMapper = resourceMapper;
|
||||
|
||||
Put("/", movie => SaveAll());
|
||||
Delete("/", movie => DeleteIndexers());
|
||||
}
|
||||
|
||||
private object SaveAll()
|
||||
[HttpPut]
|
||||
public IActionResult SaveAll(IndexerEditorResource resource)
|
||||
{
|
||||
var resource = Request.Body.FromJson<IndexerEditorResource>();
|
||||
var indexersToUpdate = _indexerService.All().Where(x => resource.IndexerIds.Contains(x.Id));
|
||||
|
||||
foreach (var indexer in indexersToUpdate)
|
||||
|
@ -65,13 +63,12 @@ namespace Prowlarr.Api.V1.Indexers
|
|||
_indexerService.SetProviderCharacteristics(definition);
|
||||
}
|
||||
|
||||
return ResponseWithCode(_resourceMapper.ToResource(indexers), HttpStatusCode.Accepted);
|
||||
return Accepted(_resourceMapper.ToResource(indexers));
|
||||
}
|
||||
|
||||
private object DeleteIndexers()
|
||||
[HttpDelete]
|
||||
public object DeleteIndexers(IndexerEditorResource resource)
|
||||
{
|
||||
var resource = Request.Body.FromJson<IndexerEditorResource>();
|
||||
|
||||
_indexerService.DeleteIndexers(resource.IndexerIds);
|
||||
|
||||
return new object();
|
|
@ -1,19 +1,17 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using Prowlarr.Http;
|
||||
|
||||
namespace Prowlarr.Api.V1.Indexers
|
||||
{
|
||||
public class IndexerFlagModule : ProwlarrRestModule<IndexerFlagResource>
|
||||
[V1ApiController]
|
||||
public class IndexerFlagController : Controller
|
||||
{
|
||||
public IndexerFlagModule()
|
||||
{
|
||||
GetResourceAll = GetAll;
|
||||
}
|
||||
|
||||
private List<IndexerFlagResource> GetAll()
|
||||
[HttpGet]
|
||||
public List<IndexerFlagResource> GetAll()
|
||||
{
|
||||
return Enum.GetValues(typeof(IndexerFlags)).Cast<IndexerFlags>().Select(f => new IndexerFlagResource
|
||||
{
|
|
@ -1,28 +1,21 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Core.IndexerStats;
|
||||
using Prowlarr.Http;
|
||||
|
||||
namespace Prowlarr.Api.V1.Indexers
|
||||
{
|
||||
public class IndexerStatsModule : ProwlarrRestModule<IndexerStatsResource>
|
||||
[V1ApiController]
|
||||
public class IndexerStatsController : Controller
|
||||
{
|
||||
private readonly IIndexerStatisticsService _indexerStatisticsService;
|
||||
|
||||
public IndexerStatsModule(IIndexerStatisticsService indexerStatisticsService)
|
||||
public IndexerStatsController(IIndexerStatisticsService indexerStatisticsService)
|
||||
{
|
||||
_indexerStatisticsService = indexerStatisticsService;
|
||||
|
||||
Get("/", x =>
|
||||
{
|
||||
return GetAll();
|
||||
});
|
||||
}
|
||||
|
||||
private IndexerStatsResource GetAll()
|
||||
[HttpGet]
|
||||
public IndexerStatsResource GetAll()
|
||||
{
|
||||
var indexerResource = new IndexerStatsResource
|
||||
{
|
|
@ -1,31 +1,40 @@
|
|||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Core.Datastore.Events;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.ThingiProvider.Events;
|
||||
using NzbDrone.SignalR;
|
||||
using Prowlarr.Http;
|
||||
using Prowlarr.Http.REST;
|
||||
using NotImplementedException = System.NotImplementedException;
|
||||
|
||||
namespace Prowlarr.Api.V1.Indexers
|
||||
{
|
||||
public class IndexerStatusModule : ProwlarrRestModuleWithSignalR<IndexerStatusResource, IndexerStatus>,
|
||||
[V1ApiController]
|
||||
public class IndexerStatusController : RestControllerWithSignalR<IndexerStatusResource, IndexerStatus>,
|
||||
IHandle<ProviderStatusChangedEvent<IIndexer>>
|
||||
{
|
||||
private readonly IIndexerStatusService _indexerStatusService;
|
||||
|
||||
public IndexerStatusModule(IBroadcastSignalRMessage signalRBroadcaster, IIndexerStatusService indexerStatusService)
|
||||
public IndexerStatusController(IBroadcastSignalRMessage signalRBroadcaster, IIndexerStatusService indexerStatusService)
|
||||
: base(signalRBroadcaster)
|
||||
{
|
||||
_indexerStatusService = indexerStatusService;
|
||||
|
||||
GetResourceAll = GetAll;
|
||||
}
|
||||
|
||||
private List<IndexerStatusResource> GetAll()
|
||||
public override IndexerStatusResource GetResourceById(int id)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public List<IndexerStatusResource> GetAll()
|
||||
{
|
||||
return _indexerStatusService.GetBlockedProviders().ToResource();
|
||||
}
|
||||
|
||||
[NonAction]
|
||||
public void Handle(ProviderStatusChangedEvent<IIndexer> message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Sync);
|
|
@ -1,19 +1,16 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Core.Languages;
|
||||
using Prowlarr.Http;
|
||||
using Prowlarr.Http.REST;
|
||||
|
||||
namespace Prowlarr.Api.V1.Languages
|
||||
{
|
||||
public class LanguageModule : ProwlarrRestModule<LanguageResource>
|
||||
[V1ApiController()]
|
||||
public class LanguageController : RestController<LanguageResource>
|
||||
{
|
||||
public LanguageModule()
|
||||
{
|
||||
GetResourceAll = GetAll;
|
||||
GetResourceById = GetById;
|
||||
}
|
||||
|
||||
private LanguageResource GetById(int id)
|
||||
public override LanguageResource GetResourceById(int id)
|
||||
{
|
||||
var language = (Language)id;
|
||||
|
||||
|
@ -24,7 +21,8 @@ namespace Prowlarr.Api.V1.Languages
|
|||
};
|
||||
}
|
||||
|
||||
private List<LanguageResource> GetAll()
|
||||
[HttpGet]
|
||||
public List<LanguageResource> GetAll()
|
||||
{
|
||||
return Language.All.Select(l => new LanguageResource
|
||||
{
|
|
@ -1,21 +1,22 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json;
|
||||
using NzbDrone.Core.Localization;
|
||||
using Prowlarr.Http;
|
||||
|
||||
namespace Prowlarr.Api.V1.Localization
|
||||
{
|
||||
public class LocalizationModule : ProwlarrRestModule<LocalizationResource>
|
||||
[V1ApiController]
|
||||
public class LocalizationController : Controller
|
||||
{
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
public LocalizationModule(ILocalizationService localizationService)
|
||||
public LocalizationController(ILocalizationService localizationService)
|
||||
{
|
||||
_localizationService = localizationService;
|
||||
|
||||
Get("/", x => GetLocalizationDictionary());
|
||||
}
|
||||
|
||||
private string GetLocalizationDictionary()
|
||||
[HttpGet]
|
||||
public string GetLocalizationDictionary()
|
||||
{
|
||||
// We don't want camel case for transation strings, create new serializer settings
|
||||
var serializerSettings = new JsonSerializerSettings
|
|
@ -1,21 +1,25 @@
|
|||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Core.Instrumentation;
|
||||
using Prowlarr.Http;
|
||||
using Prowlarr.Http.Extensions;
|
||||
|
||||
namespace Prowlarr.Api.V1.Logs
|
||||
{
|
||||
public class LogModule : ProwlarrRestModule<LogResource>
|
||||
[V1ApiController]
|
||||
public class LogController : Controller
|
||||
{
|
||||
private readonly ILogService _logService;
|
||||
|
||||
public LogModule(ILogService logService)
|
||||
public LogController(ILogService logService)
|
||||
{
|
||||
_logService = logService;
|
||||
GetResourcePaged = GetLogs;
|
||||
}
|
||||
|
||||
private PagingResource<LogResource> GetLogs(PagingResource<LogResource> pagingResource)
|
||||
[HttpGet]
|
||||
public PagingResource<LogResource> GetLogs()
|
||||
{
|
||||
var pagingResource = Request.ReadPagingResourceFromRequest<LogResource>();
|
||||
var pageSpec = pagingResource.MapToPagingSpec<LogResource, Log>();
|
||||
|
||||
if (pageSpec.SortKey == "time")
|
||||
|
@ -50,7 +54,7 @@ namespace Prowlarr.Api.V1.Logs
|
|||
}
|
||||
}
|
||||
|
||||
var response = ApplyToPage(_logService.Paged, pageSpec, LogResourceMapper.ToResource);
|
||||
var response = pageSpec.ApplyToPage(_logService.Paged, LogResourceMapper.ToResource);
|
||||
|
||||
if (pageSpec.SortKey == "id")
|
||||
{
|
|
@ -1,18 +1,20 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using Prowlarr.Http;
|
||||
|
||||
namespace Prowlarr.Api.V1.Logs
|
||||
{
|
||||
public class LogFileModule : LogFileModuleBase
|
||||
[V1ApiController("log/file")]
|
||||
public class LogFileController : LogFileControllerBase
|
||||
{
|
||||
private readonly IAppFolderInfo _appFolderInfo;
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
|
||||
public LogFileModule(IAppFolderInfo appFolderInfo,
|
||||
public LogFileController(IAppFolderInfo appFolderInfo,
|
||||
IDiskProvider diskProvider,
|
||||
IConfigFileProvider configFileProvider)
|
||||
: base(diskProvider, configFileProvider, "")
|
|
@ -1,35 +1,32 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Nancy;
|
||||
using Nancy.Responses;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using Prowlarr.Http;
|
||||
|
||||
namespace Prowlarr.Api.V1.Logs
|
||||
{
|
||||
public abstract class LogFileModuleBase : ProwlarrRestModule<LogFileResource>
|
||||
public abstract class LogFileControllerBase : Controller
|
||||
{
|
||||
protected const string LOGFILE_ROUTE = @"/(?<filename>[-.a-zA-Z0-9]+?\.txt)";
|
||||
protected string _resource;
|
||||
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
|
||||
public LogFileModuleBase(IDiskProvider diskProvider,
|
||||
public LogFileControllerBase(IDiskProvider diskProvider,
|
||||
IConfigFileProvider configFileProvider,
|
||||
string route)
|
||||
: base("log/file" + route)
|
||||
string resource)
|
||||
{
|
||||
_diskProvider = diskProvider;
|
||||
_configFileProvider = configFileProvider;
|
||||
GetResourceAll = GetLogFilesResponse;
|
||||
|
||||
Get(LOGFILE_ROUTE, options => GetLogFileResponse(options.filename));
|
||||
_resource = resource;
|
||||
}
|
||||
|
||||
private List<LogFileResource> GetLogFilesResponse()
|
||||
[HttpGet]
|
||||
public List<LogFileResource> GetLogFilesResponse()
|
||||
{
|
||||
var result = new List<LogFileResource>();
|
||||
|
||||
|
@ -45,7 +42,7 @@ namespace Prowlarr.Api.V1.Logs
|
|||
Id = i + 1,
|
||||
Filename = filename,
|
||||
LastWriteTime = _diskProvider.FileGetLastWrite(file),
|
||||
ContentsUrl = string.Format("{0}/api/v1/{1}/{2}", _configFileProvider.UrlBase, Resource, filename),
|
||||
ContentsUrl = string.Format("{0}/api/v1/{1}/{2}", _configFileProvider.UrlBase, _resource, filename),
|
||||
DownloadUrl = string.Format("{0}/{1}/{2}", _configFileProvider.UrlBase, DownloadUrlRoot, filename)
|
||||
});
|
||||
}
|
||||
|
@ -53,7 +50,8 @@ namespace Prowlarr.Api.V1.Logs
|
|||
return result.OrderByDescending(l => l.LastWriteTime).ToList();
|
||||
}
|
||||
|
||||
private object GetLogFileResponse(string filename)
|
||||
[HttpGet(@"{filename:regex([[-.a-zA-Z0-9]]+?\.txt)}")]
|
||||
public IActionResult GetLogFileResponse(string filename)
|
||||
{
|
||||
LogManager.Flush();
|
||||
|
||||
|
@ -61,12 +59,10 @@ namespace Prowlarr.Api.V1.Logs
|
|||
|
||||
if (!_diskProvider.FileExists(filePath))
|
||||
{
|
||||
return new NotFoundResponse();
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var data = _diskProvider.ReadAllText(filePath);
|
||||
|
||||
return new TextResponse(data);
|
||||
return PhysicalFile(filePath, "text/plain");
|
||||
}
|
||||
|
||||
protected abstract IEnumerable<string> GetLogFiles();
|
|
@ -1,4 +1,4 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
@ -6,18 +6,20 @@ using NzbDrone.Common.Disk;
|
|||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using Prowlarr.Http;
|
||||
|
||||
namespace Prowlarr.Api.V1.Logs
|
||||
{
|
||||
public class UpdateLogFileModule : LogFileModuleBase
|
||||
[V1ApiController("log/file/update")]
|
||||
public class UpdateLogFileController : LogFileControllerBase
|
||||
{
|
||||
private readonly IAppFolderInfo _appFolderInfo;
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
|
||||
public UpdateLogFileModule(IAppFolderInfo appFolderInfo,
|
||||
public UpdateLogFileController(IAppFolderInfo appFolderInfo,
|
||||
IDiskProvider diskProvider,
|
||||
IConfigFileProvider configFileProvider)
|
||||
: base(diskProvider, configFileProvider, "/update")
|
||||
: base(diskProvider, configFileProvider, "update")
|
||||
{
|
||||
_appFolderInfo = appFolderInfo;
|
||||
_diskProvider = diskProvider;
|
|
@ -1,12 +1,14 @@
|
|||
using NzbDrone.Core.Notifications;
|
||||
using Prowlarr.Http;
|
||||
|
||||
namespace Prowlarr.Api.V1.Notifications
|
||||
{
|
||||
public class NotificationModule : ProviderModuleBase<NotificationResource, INotification, NotificationDefinition>
|
||||
[V1ApiController]
|
||||
public class NotificationController : ProviderControllerBase<NotificationResource, INotification, NotificationDefinition>
|
||||
{
|
||||
public static readonly NotificationResourceMapper ResourceMapper = new NotificationResourceMapper();
|
||||
|
||||
public NotificationModule(NotificationFactory notificationFactory)
|
||||
public NotificationController(NotificationFactory notificationFactory)
|
||||
: base(notificationFactory, "notification", ResourceMapper)
|
||||
{
|
||||
}
|
|
@ -2,16 +2,17 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using Nancy;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Validation;
|
||||
using Prowlarr.Http;
|
||||
using NzbDrone.Http.REST.Attributes;
|
||||
using Prowlarr.Http.Extensions;
|
||||
using Prowlarr.Http.REST;
|
||||
|
||||
namespace Prowlarr.Api.V1
|
||||
{
|
||||
public abstract class ProviderModuleBase<TProviderResource, TProvider, TProviderDefinition> : ProwlarrRestModule<TProviderResource>
|
||||
public abstract class ProviderControllerBase<TProviderResource, TProvider, TProviderDefinition> : RestController<TProviderResource>
|
||||
where TProviderDefinition : ProviderDefinition, new()
|
||||
where TProvider : IProvider
|
||||
where TProviderResource : ProviderResource<TProviderResource>, new()
|
||||
|
@ -19,23 +20,11 @@ namespace Prowlarr.Api.V1
|
|||
protected readonly IProviderFactory<TProvider, TProviderDefinition> _providerFactory;
|
||||
protected readonly ProviderResourceMapper<TProviderResource, TProviderDefinition> _resourceMapper;
|
||||
|
||||
protected ProviderModuleBase(IProviderFactory<TProvider, TProviderDefinition> providerFactory, string resource, ProviderResourceMapper<TProviderResource, TProviderDefinition> resourceMapper)
|
||||
: base(resource)
|
||||
protected ProviderControllerBase(IProviderFactory<TProvider, TProviderDefinition> providerFactory, string resource, ProviderResourceMapper<TProviderResource, TProviderDefinition> resourceMapper)
|
||||
{
|
||||
_providerFactory = providerFactory;
|
||||
_resourceMapper = resourceMapper;
|
||||
|
||||
Get("schema", x => GetTemplates());
|
||||
Post("test", x => Test(ReadResourceFromRequest(true)));
|
||||
Post("testall", x => TestAll());
|
||||
Post("action/{action}", x => RequestAction(x.action, ReadResourceFromRequest(true, true)));
|
||||
|
||||
GetResourceAll = GetAll;
|
||||
GetResourceById = GetProviderById;
|
||||
CreateResource = CreateProvider;
|
||||
UpdateResource = UpdateProvider;
|
||||
DeleteResource = DeleteProvider;
|
||||
|
||||
SharedValidator.RuleFor(c => c.Name).NotEmpty();
|
||||
SharedValidator.RuleFor(c => c.Name).Must((v, c) => !_providerFactory.All().Any(p => p.Name == c && p.Id != v.Id)).WithMessage("Should be unique");
|
||||
SharedValidator.RuleFor(c => c.Implementation).NotEmpty();
|
||||
|
@ -44,7 +33,7 @@ namespace Prowlarr.Api.V1
|
|||
PostValidator.RuleFor(c => c.Fields).NotNull();
|
||||
}
|
||||
|
||||
private TProviderResource GetProviderById(int id)
|
||||
public override TProviderResource GetResourceById(int id)
|
||||
{
|
||||
var definition = _providerFactory.Get(id);
|
||||
_providerFactory.SetProviderCharacteristics(definition);
|
||||
|
@ -52,7 +41,8 @@ namespace Prowlarr.Api.V1
|
|||
return _resourceMapper.ToResource(definition);
|
||||
}
|
||||
|
||||
private List<TProviderResource> GetAll()
|
||||
[HttpGet]
|
||||
public List<TProviderResource> GetAll()
|
||||
{
|
||||
var providerDefinitions = _providerFactory.All().OrderBy(p => p.ImplementationName);
|
||||
|
||||
|
@ -68,7 +58,8 @@ namespace Prowlarr.Api.V1
|
|||
return result.OrderBy(p => p.Name).ToList();
|
||||
}
|
||||
|
||||
private int CreateProvider(TProviderResource providerResource)
|
||||
[RestPostById]
|
||||
public ActionResult<TProviderResource> CreateProvider(TProviderResource providerResource)
|
||||
{
|
||||
var providerDefinition = GetDefinition(providerResource, false);
|
||||
|
||||
|
@ -79,10 +70,11 @@ namespace Prowlarr.Api.V1
|
|||
|
||||
providerDefinition = _providerFactory.Create(providerDefinition);
|
||||
|
||||
return providerDefinition.Id;
|
||||
return Created(providerDefinition.Id);
|
||||
}
|
||||
|
||||
private void UpdateProvider(TProviderResource providerResource)
|
||||
[RestPutById]
|
||||
public ActionResult<TProviderResource> UpdateProvider(TProviderResource providerResource)
|
||||
{
|
||||
var providerDefinition = GetDefinition(providerResource, false);
|
||||
var forceSave = Request.GetBooleanQueryParameter("forceSave");
|
||||
|
@ -94,6 +86,8 @@ namespace Prowlarr.Api.V1
|
|||
}
|
||||
|
||||
_providerFactory.Update(providerDefinition);
|
||||
|
||||
return Accepted(providerResource.Id);
|
||||
}
|
||||
|
||||
private TProviderDefinition GetDefinition(TProviderResource providerResource, bool includeWarnings = false, bool validate = true)
|
||||
|
@ -108,12 +102,15 @@ namespace Prowlarr.Api.V1
|
|||
return definition;
|
||||
}
|
||||
|
||||
private void DeleteProvider(int id)
|
||||
[RestDeleteById]
|
||||
public object DeleteProvider(int id)
|
||||
{
|
||||
_providerFactory.Delete(id);
|
||||
return new object();
|
||||
}
|
||||
|
||||
protected virtual object GetTemplates()
|
||||
[HttpGet("schema")]
|
||||
public virtual List<TProviderResource> GetTemplates()
|
||||
{
|
||||
var defaultDefinitions = _providerFactory.GetDefaultDefinitions().OrderBy(p => p.ImplementationName).ToList();
|
||||
|
||||
|
@ -134,7 +131,9 @@ namespace Prowlarr.Api.V1
|
|||
return result;
|
||||
}
|
||||
|
||||
private object Test(TProviderResource providerResource)
|
||||
[SkipValidation(true, false)]
|
||||
[HttpPost("test")]
|
||||
public object Test([FromBody] TProviderResource providerResource)
|
||||
{
|
||||
var providerDefinition = GetDefinition(providerResource, true);
|
||||
|
||||
|
@ -143,7 +142,8 @@ namespace Prowlarr.Api.V1
|
|||
return "{}";
|
||||
}
|
||||
|
||||
private object TestAll()
|
||||
[HttpPost("testall")]
|
||||
public IActionResult TestAll()
|
||||
{
|
||||
var providerDefinitions = _providerFactory.All()
|
||||
.Where(c => c.Settings.Validate().IsValid && c.Enable)
|
||||
|
@ -161,19 +161,20 @@ namespace Prowlarr.Api.V1
|
|||
});
|
||||
}
|
||||
|
||||
return ResponseWithCode(result, result.Any(c => !c.IsValid) ? HttpStatusCode.BadRequest : HttpStatusCode.OK);
|
||||
return result.Any(c => !c.IsValid) ? BadRequest(result) : Ok(result);
|
||||
}
|
||||
|
||||
private object RequestAction(string action, TProviderResource providerResource)
|
||||
[SkipValidation]
|
||||
[HttpPost("action/{name}")]
|
||||
public IActionResult RequestAction(string name, [FromBody] TProviderResource resource)
|
||||
{
|
||||
var providerDefinition = GetDefinition(providerResource, true, false);
|
||||
var providerDefinition = GetDefinition(resource, true, false);
|
||||
|
||||
var query = ((IDictionary<string, object>)Request.Query.ToDictionary()).ToDictionary(k => k.Key, k => k.Value.ToString());
|
||||
var query = Request.Query.ToDictionary(x => x.Key, x => x.Value.ToString());
|
||||
|
||||
var data = _providerFactory.RequestAction(providerDefinition, action, query);
|
||||
Response resp = data.ToJson();
|
||||
resp.ContentType = "application/json";
|
||||
return resp;
|
||||
var data = _providerFactory.RequestAction(providerDefinition, name, query);
|
||||
|
||||
return Content(data.ToJson(), "application/json");
|
||||
}
|
||||
|
||||
protected virtual void Validate(TProviderDefinition definition, bool includeWarnings)
|
|
@ -5,9 +5,6 @@
|
|||
<ItemGroup>
|
||||
<PackageReference Include="FluentValidation" Version="8.6.2" />
|
||||
<PackageReference Include="Ical.Net" Version="4.1.11" />
|
||||
<PackageReference Include="Nancy" Version="2.0.0" />
|
||||
<PackageReference Include="Nancy.Authentication.Basic" Version="2.0.0" />
|
||||
<PackageReference Include="Nancy.Authentication.Forms" Version="2.0.0" />
|
||||
<PackageReference Include="NLog" Version="4.7.7" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
using Prowlarr.Http;
|
||||
|
||||
namespace Prowlarr.Api.V1
|
||||
{
|
||||
public abstract class ProwlarrV3FeedModule : ProwlarrModule
|
||||
{
|
||||
protected ProwlarrV3FeedModule(string resource)
|
||||
: base("/feed/v1/" + resource.Trim('/'))
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
using Prowlarr.Http;
|
||||
|
||||
namespace Prowlarr.Api.V1
|
||||
{
|
||||
public abstract class ProwlarrV1Module : ProwlarrModule
|
||||
{
|
||||
protected ProwlarrV1Module(string resource)
|
||||
: base("/api/v1/" + resource.Trim('/'))
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,47 +1,42 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using Nancy.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Exceptions;
|
||||
using NzbDrone.Core.IndexerSearch;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using Prowlarr.Http;
|
||||
using Prowlarr.Http.Extensions;
|
||||
|
||||
namespace Prowlarr.Api.V1.Search
|
||||
{
|
||||
public class SearchModule : ProwlarrRestModule<SearchResource>
|
||||
[V1ApiController]
|
||||
public class SearchController : Controller
|
||||
{
|
||||
private readonly ISearchForNzb _nzbSearhService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public SearchModule(ISearchForNzb nzbSearhService, Logger logger)
|
||||
public SearchController(ISearchForNzb nzbSearhService, Logger logger)
|
||||
{
|
||||
_nzbSearhService = nzbSearhService;
|
||||
_logger = logger;
|
||||
|
||||
GetResourceAll = GetAll;
|
||||
}
|
||||
|
||||
private List<SearchResource> GetAll()
|
||||
[HttpGet]
|
||||
public List<SearchResource> GetAll(string query, [FromQuery] List<int> indexerIds, [FromQuery] List<int> categories)
|
||||
{
|
||||
var request = this.Bind<SearchRequest>();
|
||||
|
||||
if (request.Query.IsNotNullOrWhiteSpace())
|
||||
if (query.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
var indexerIds = request.IndexerIds ?? new List<int>();
|
||||
var categories = request.Categories ?? new List<int>();
|
||||
|
||||
if (indexerIds.Count > 0)
|
||||
if (indexerIds.Any())
|
||||
{
|
||||
return GetSearchReleases(request.Query, indexerIds, categories);
|
||||
return GetSearchReleases(query, indexerIds, categories);
|
||||
}
|
||||
else
|
||||
{
|
||||
return GetSearchReleases(request.Query, null, categories);
|
||||
return GetSearchReleases(query, null, categories);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace Prowlarr.Api.V1.Search
|
||||
{
|
||||
public class SearchRequest
|
||||
{
|
||||
public List<int> IndexerIds { get; set; }
|
||||
public string Query { get; set; }
|
||||
public List<int> Categories { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,17 +1,20 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Common.Crypto;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Backup;
|
||||
using NzbDrone.Http.REST.Attributes;
|
||||
using Prowlarr.Http;
|
||||
using Prowlarr.Http.REST;
|
||||
|
||||
namespace Prowlarr.Api.V1.System.Backup
|
||||
{
|
||||
public class BackupModule : ProwlarrRestModule<BackupResource>
|
||||
[V1ApiController("system/backup")]
|
||||
public class BackupController : Controller
|
||||
{
|
||||
private readonly IBackupService _backupService;
|
||||
private readonly IAppFolderInfo _appFolderInfo;
|
||||
|
@ -19,21 +22,16 @@ namespace Prowlarr.Api.V1.System.Backup
|
|||
|
||||
private static readonly List<string> ValidExtensions = new List<string> { ".zip", ".db", ".xml" };
|
||||
|
||||
public BackupModule(IBackupService backupService,
|
||||
public BackupController(IBackupService backupService,
|
||||
IAppFolderInfo appFolderInfo,
|
||||
IDiskProvider diskProvider)
|
||||
: base("system/backup")
|
||||
{
|
||||
_backupService = backupService;
|
||||
_appFolderInfo = appFolderInfo;
|
||||
_diskProvider = diskProvider;
|
||||
GetResourceAll = GetBackupFiles;
|
||||
DeleteResource = DeleteBackup;
|
||||
|
||||
Post(@"/restore/(?<id>[\d]{1,10})", x => Restore((int)x.Id));
|
||||
Post("/restore/upload", x => UploadAndRestore());
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public List<BackupResource> GetBackupFiles()
|
||||
{
|
||||
var backups = _backupService.GetBackups();
|
||||
|
@ -50,7 +48,8 @@ namespace Prowlarr.Api.V1.System.Backup
|
|||
.ToList();
|
||||
}
|
||||
|
||||
private void DeleteBackup(int id)
|
||||
[RestDeleteById]
|
||||
public void DeleteBackup(int id)
|
||||
{
|
||||
var backup = GetBackup(id);
|
||||
var path = GetBackupPath(backup);
|
||||
|
@ -63,6 +62,7 @@ namespace Prowlarr.Api.V1.System.Backup
|
|||
_diskProvider.DeleteFile(path);
|
||||
}
|
||||
|
||||
[HttpPost("restore/{id:int}")]
|
||||
public object Restore(int id)
|
||||
{
|
||||
var backup = GetBackup(id);
|
||||
|
@ -82,9 +82,10 @@ namespace Prowlarr.Api.V1.System.Backup
|
|||
};
|
||||
}
|
||||
|
||||
[HttpPost("restore/upload")]
|
||||
public object UploadAndRestore()
|
||||
{
|
||||
var files = Context.Request.Files.ToList();
|
||||
var files = Request.Form.Files;
|
||||
|
||||
if (files.Empty())
|
||||
{
|
||||
|
@ -92,7 +93,7 @@ namespace Prowlarr.Api.V1.System.Backup
|
|||
}
|
||||
|
||||
var file = files.First();
|
||||
var extension = Path.GetExtension(file.Name);
|
||||
var extension = Path.GetExtension(file.FileName);
|
||||
|
||||
if (!ValidExtensions.Contains(extension))
|
||||
{
|
||||
|
@ -101,7 +102,7 @@ namespace Prowlarr.Api.V1.System.Backup
|
|||
|
||||
var path = Path.Combine(_appFolderInfo.TempFolder, $"prowlarr_backup_restore{extension}");
|
||||
|
||||
_diskProvider.SaveStream(file.Value, path);
|
||||
_diskProvider.SaveStream(file.OpenReadStream(), path);
|
||||
_backupService.Restore(path);
|
||||
|
||||
// Cleanup restored file
|
|
@ -1,52 +1,60 @@
|
|||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Nancy.Routing;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Routing.Internal;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Lifecycle;
|
||||
using Prowlarr.Http;
|
||||
using Prowlarr.Http.Validation;
|
||||
|
||||
namespace Prowlarr.Api.V1.System
|
||||
{
|
||||
public class SystemModule : ProwlarrV1Module
|
||||
[V1ApiController]
|
||||
public class SystemController : Controller
|
||||
{
|
||||
private readonly IAppFolderInfo _appFolderInfo;
|
||||
private readonly IRuntimeInfo _runtimeInfo;
|
||||
private readonly IPlatformInfo _platformInfo;
|
||||
private readonly IOsInfo _osInfo;
|
||||
private readonly IRouteCacheProvider _routeCacheProvider;
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
private readonly IMainDatabase _database;
|
||||
private readonly ILifecycleService _lifecycleService;
|
||||
private readonly IDeploymentInfoProvider _deploymentInfoProvider;
|
||||
private readonly EndpointDataSource _endpointData;
|
||||
private readonly DfaGraphWriter _graphWriter;
|
||||
private readonly DuplicateEndpointDetector _detector;
|
||||
|
||||
public SystemModule(IAppFolderInfo appFolderInfo,
|
||||
IRuntimeInfo runtimeInfo,
|
||||
IPlatformInfo platformInfo,
|
||||
IOsInfo osInfo,
|
||||
IRouteCacheProvider routeCacheProvider,
|
||||
IConfigFileProvider configFileProvider,
|
||||
IMainDatabase database,
|
||||
ILifecycleService lifecycleService,
|
||||
IDeploymentInfoProvider deploymentInfoProvider)
|
||||
: base("system")
|
||||
public SystemController(IAppFolderInfo appFolderInfo,
|
||||
IRuntimeInfo runtimeInfo,
|
||||
IPlatformInfo platformInfo,
|
||||
IOsInfo osInfo,
|
||||
IConfigFileProvider configFileProvider,
|
||||
IMainDatabase database,
|
||||
ILifecycleService lifecycleService,
|
||||
IDeploymentInfoProvider deploymentInfoProvider,
|
||||
EndpointDataSource endpoints,
|
||||
DfaGraphWriter graphWriter,
|
||||
DuplicateEndpointDetector detector)
|
||||
{
|
||||
_appFolderInfo = appFolderInfo;
|
||||
_runtimeInfo = runtimeInfo;
|
||||
_platformInfo = platformInfo;
|
||||
_osInfo = osInfo;
|
||||
_routeCacheProvider = routeCacheProvider;
|
||||
_configFileProvider = configFileProvider;
|
||||
_database = database;
|
||||
_lifecycleService = lifecycleService;
|
||||
_deploymentInfoProvider = deploymentInfoProvider;
|
||||
Get("/status", x => GetStatus());
|
||||
Get("/routes", x => GetRoutes());
|
||||
Post("/shutdown", x => Shutdown());
|
||||
Post("/restart", x => Restart());
|
||||
_endpointData = endpoints;
|
||||
_graphWriter = graphWriter;
|
||||
_detector = detector;
|
||||
}
|
||||
|
||||
private object GetStatus()
|
||||
[HttpGet("status")]
|
||||
public object GetStatus()
|
||||
{
|
||||
return new
|
||||
{
|
||||
|
@ -82,18 +90,32 @@ namespace Prowlarr.Api.V1.System
|
|||
};
|
||||
}
|
||||
|
||||
private object GetRoutes()
|
||||
[HttpGet("routes")]
|
||||
public IActionResult GetRoutes()
|
||||
{
|
||||
return _routeCacheProvider.GetCache().Values;
|
||||
using (var sw = new StringWriter())
|
||||
{
|
||||
_graphWriter.Write(_endpointData, sw);
|
||||
var graph = sw.ToString();
|
||||
return Content(graph, "text/plain");
|
||||
}
|
||||
}
|
||||
|
||||
private object Shutdown()
|
||||
[HttpGet("routes/duplicate")]
|
||||
public object DuplicateRoutes()
|
||||
{
|
||||
return _detector.GetDuplicateEndpoints(_endpointData);
|
||||
}
|
||||
|
||||
[HttpPost("shutdown")]
|
||||
public object Shutdown()
|
||||
{
|
||||
Task.Factory.StartNew(() => _lifecycleService.Shutdown());
|
||||
return new { ShuttingDown = true };
|
||||
}
|
||||
|
||||
private object Restart()
|
||||
[HttpPost("restart")]
|
||||
public object Restart()
|
||||
{
|
||||
Task.Factory.StartNew(() => _lifecycleService.Restart());
|
||||
return new { Restarting = true };
|
|
@ -1,27 +1,29 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Datastore.Events;
|
||||
using NzbDrone.Core.Jobs;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.SignalR;
|
||||
using Prowlarr.Http;
|
||||
using Prowlarr.Http.REST;
|
||||
|
||||
namespace Prowlarr.Api.V1.System.Tasks
|
||||
{
|
||||
public class TaskModule : ProwlarrRestModuleWithSignalR<TaskResource, ScheduledTask>, IHandle<CommandExecutedEvent>
|
||||
[V1ApiController("system/task")]
|
||||
public class TaskController : RestControllerWithSignalR<TaskResource, ScheduledTask>, IHandle<CommandExecutedEvent>
|
||||
{
|
||||
private readonly ITaskManager _taskManager;
|
||||
|
||||
public TaskModule(ITaskManager taskManager, IBroadcastSignalRMessage broadcastSignalRMessage)
|
||||
: base(broadcastSignalRMessage, "system/task")
|
||||
public TaskController(ITaskManager taskManager, IBroadcastSignalRMessage broadcastSignalRMessage)
|
||||
: base(broadcastSignalRMessage)
|
||||
{
|
||||
_taskManager = taskManager;
|
||||
GetResourceAll = GetAll;
|
||||
GetResourceById = GetTask;
|
||||
}
|
||||
|
||||
private List<TaskResource> GetAll()
|
||||
[HttpGet]
|
||||
public List<TaskResource> GetAll()
|
||||
{
|
||||
return _taskManager.GetAll()
|
||||
.Select(ConvertToResource)
|
||||
|
@ -29,7 +31,7 @@ namespace Prowlarr.Api.V1.System.Tasks
|
|||
.ToList();
|
||||
}
|
||||
|
||||
private TaskResource GetTask(int id)
|
||||
public override TaskResource GetResourceById(int id)
|
||||
{
|
||||
var task = _taskManager.GetAll()
|
||||
.SingleOrDefault(t => t.Id == id);
|
||||
|
@ -58,6 +60,7 @@ namespace Prowlarr.Api.V1.System.Tasks
|
|||
};
|
||||
}
|
||||
|
||||
[NonAction]
|
||||
public void Handle(CommandExecutedEvent message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Sync);
|
|
@ -1,54 +1,59 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Core.Datastore.Events;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Tags;
|
||||
using NzbDrone.Http.REST.Attributes;
|
||||
using NzbDrone.SignalR;
|
||||
using Prowlarr.Http;
|
||||
using Prowlarr.Http.REST;
|
||||
|
||||
namespace Prowlarr.Api.V1.Tags
|
||||
{
|
||||
public class TagModule : ProwlarrRestModuleWithSignalR<TagResource, Tag>, IHandle<TagsUpdatedEvent>
|
||||
[V1ApiController]
|
||||
public class TagController : RestControllerWithSignalR<TagResource, Tag>, IHandle<TagsUpdatedEvent>
|
||||
{
|
||||
private readonly ITagService _tagService;
|
||||
|
||||
public TagModule(IBroadcastSignalRMessage signalRBroadcaster,
|
||||
public TagController(IBroadcastSignalRMessage signalRBroadcaster,
|
||||
ITagService tagService)
|
||||
: base(signalRBroadcaster)
|
||||
{
|
||||
_tagService = tagService;
|
||||
|
||||
GetResourceById = GetById;
|
||||
GetResourceAll = GetAll;
|
||||
CreateResource = Create;
|
||||
UpdateResource = Update;
|
||||
DeleteResource = DeleteTag;
|
||||
}
|
||||
|
||||
private TagResource GetById(int id)
|
||||
public override TagResource GetResourceById(int id)
|
||||
{
|
||||
return _tagService.GetTag(id).ToResource();
|
||||
}
|
||||
|
||||
private List<TagResource> GetAll()
|
||||
[HttpGet]
|
||||
public List<TagResource> GetAll()
|
||||
{
|
||||
return _tagService.All().ToResource();
|
||||
}
|
||||
|
||||
private int Create(TagResource resource)
|
||||
[RestPostById]
|
||||
public ActionResult<TagResource> Create(TagResource resource)
|
||||
{
|
||||
return _tagService.Add(resource.ToModel()).Id;
|
||||
return Created(_tagService.Add(resource.ToModel()).Id);
|
||||
}
|
||||
|
||||
private void Update(TagResource resource)
|
||||
[RestPutById]
|
||||
public ActionResult<TagResource> Update(TagResource resource)
|
||||
{
|
||||
_tagService.Update(resource.ToModel());
|
||||
return Accepted(resource.Id);
|
||||
}
|
||||
|
||||
private void DeleteTag(int id)
|
||||
[RestDeleteById]
|
||||
public void DeleteTag(int id)
|
||||
{
|
||||
_tagService.Delete(id);
|
||||
}
|
||||
|
||||
[NonAction]
|
||||
public void Handle(TagsUpdatedEvent message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Sync);
|
|
@ -1,28 +1,28 @@
|
|||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Core.Tags;
|
||||
using Prowlarr.Http;
|
||||
using Prowlarr.Http.REST;
|
||||
|
||||
namespace Prowlarr.Api.V1.Tags
|
||||
{
|
||||
public class TagDetailsModule : ProwlarrRestModule<TagDetailsResource>
|
||||
[V1ApiController("tag/detail")]
|
||||
public class TagDetailsController : RestController<TagDetailsResource>
|
||||
{
|
||||
private readonly ITagService _tagService;
|
||||
|
||||
public TagDetailsModule(ITagService tagService)
|
||||
: base("/tag/detail")
|
||||
public TagDetailsController(ITagService tagService)
|
||||
{
|
||||
_tagService = tagService;
|
||||
|
||||
GetResourceById = GetById;
|
||||
GetResourceAll = GetAll;
|
||||
}
|
||||
|
||||
private TagDetailsResource GetById(int id)
|
||||
public override TagDetailsResource GetResourceById(int id)
|
||||
{
|
||||
return _tagService.Details(id).ToResource();
|
||||
}
|
||||
|
||||
private List<TagDetailsResource> GetAll()
|
||||
[HttpGet]
|
||||
public List<TagDetailsResource> GetAll()
|
||||
{
|
||||
return _tagService.Details().ToResource();
|
||||
}
|
|
@ -1,22 +1,24 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Core.Update;
|
||||
using Prowlarr.Http;
|
||||
|
||||
namespace Prowlarr.Api.V1.Update
|
||||
{
|
||||
public class UpdateModule : ProwlarrRestModule<UpdateResource>
|
||||
[V1ApiController]
|
||||
public class UpdateController : Controller
|
||||
{
|
||||
private readonly IRecentUpdateProvider _recentUpdateProvider;
|
||||
|
||||
public UpdateModule(IRecentUpdateProvider recentUpdateProvider)
|
||||
public UpdateController(IRecentUpdateProvider recentUpdateProvider)
|
||||
{
|
||||
_recentUpdateProvider = recentUpdateProvider;
|
||||
GetResourceAll = GetRecentUpdates;
|
||||
}
|
||||
|
||||
private List<UpdateResource> GetRecentUpdates()
|
||||
[HttpGet]
|
||||
public List<UpdateResource> GetRecentUpdates()
|
||||
{
|
||||
var resources = _recentUpdateProvider.GetRecentUpdatePackages()
|
||||
.OrderByDescending(u => u.Version)
|
|
@ -0,0 +1,89 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Prowlarr.Http.Authentication
|
||||
{
|
||||
public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions
|
||||
{
|
||||
public const string DefaultScheme = "API Key";
|
||||
public string Scheme => DefaultScheme;
|
||||
public string AuthenticationType = DefaultScheme;
|
||||
|
||||
public string HeaderName { get; set; }
|
||||
public string QueryName { get; set; }
|
||||
public string ApiKey { get; set; }
|
||||
}
|
||||
|
||||
public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
|
||||
{
|
||||
public ApiKeyAuthenticationHandler(IOptionsMonitor<ApiKeyAuthenticationOptions> options,
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder,
|
||||
ISystemClock clock)
|
||||
: base(options, logger, encoder, clock)
|
||||
{
|
||||
}
|
||||
|
||||
private string ParseApiKey()
|
||||
{
|
||||
// Try query parameter
|
||||
if (Request.Query.TryGetValue(Options.QueryName, out var value))
|
||||
{
|
||||
return value.FirstOrDefault();
|
||||
}
|
||||
|
||||
// No ApiKey query parameter found try headers
|
||||
if (Request.Headers.TryGetValue(Options.HeaderName, out var headerValue))
|
||||
{
|
||||
return headerValue.FirstOrDefault();
|
||||
}
|
||||
|
||||
return Request.Headers["Authorization"].FirstOrDefault()?.Replace("Bearer ", "");
|
||||
}
|
||||
|
||||
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
var providedApiKey = ParseApiKey();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(providedApiKey))
|
||||
{
|
||||
return Task.FromResult(AuthenticateResult.NoResult());
|
||||
}
|
||||
|
||||
if (Options.ApiKey == providedApiKey)
|
||||
{
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new Claim("ApiKey", "true")
|
||||
};
|
||||
|
||||
var identity = new ClaimsIdentity(claims, Options.AuthenticationType);
|
||||
var identities = new List<ClaimsIdentity> { identity };
|
||||
var principal = new ClaimsPrincipal(identities);
|
||||
var ticket = new AuthenticationTicket(principal, Options.Scheme);
|
||||
|
||||
return Task.FromResult(AuthenticateResult.Success(ticket));
|
||||
}
|
||||
|
||||
return Task.FromResult(AuthenticateResult.NoResult());
|
||||
}
|
||||
|
||||
protected override Task HandleChallengeAsync(AuthenticationProperties properties)
|
||||
{
|
||||
Response.StatusCode = 401;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected override Task HandleForbiddenAsync(AuthenticationProperties properties)
|
||||
{
|
||||
Response.StatusCode = 403;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
using System;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NzbDrone.Core.Authentication;
|
||||
using NzbDrone.Core.Configuration;
|
||||
|
||||
namespace Prowlarr.Http.Authentication
|
||||
{
|
||||
public static class AuthenticationBuilderExtensions
|
||||
{
|
||||
public static AuthenticationBuilder AddApiKey(this AuthenticationBuilder authenticationBuilder, string name, Action<ApiKeyAuthenticationOptions> options)
|
||||
{
|
||||
return authenticationBuilder.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>(name, options);
|
||||
}
|
||||
|
||||
public static AuthenticationBuilder AddBasicAuthentication(this AuthenticationBuilder authenticationBuilder)
|
||||
{
|
||||
return authenticationBuilder.AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>(AuthenticationType.Basic.ToString(), options => { });
|
||||
}
|
||||
|
||||
public static AuthenticationBuilder AddNoAuthentication(this AuthenticationBuilder authenticationBuilder)
|
||||
{
|
||||
return authenticationBuilder.AddScheme<AuthenticationSchemeOptions, NoAuthenticationHandler>(AuthenticationType.None.ToString(), options => { });
|
||||
}
|
||||
|
||||
public static AuthenticationBuilder AddAppAuthentication(this IServiceCollection services, IConfigFileProvider config)
|
||||
{
|
||||
var authBuilder = services.AddAuthentication(config.AuthenticationMethod.ToString());
|
||||
|
||||
if (config.AuthenticationMethod == AuthenticationType.Basic)
|
||||
{
|
||||
authBuilder.AddBasicAuthentication();
|
||||
}
|
||||
else if (config.AuthenticationMethod == AuthenticationType.Forms)
|
||||
{
|
||||
authBuilder.AddCookie(AuthenticationType.Forms.ToString(), options =>
|
||||
{
|
||||
options.AccessDeniedPath = "/login?loginFailed=true";
|
||||
options.LoginPath = "/login";
|
||||
options.ExpireTimeSpan = TimeSpan.FromDays(7);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
authBuilder.AddNoAuthentication();
|
||||
}
|
||||
|
||||
authBuilder.AddApiKey("API", options =>
|
||||
{
|
||||
options.HeaderName = "X-Api-Key";
|
||||
options.QueryName = "apikey";
|
||||
options.ApiKey = config.ApiKey;
|
||||
});
|
||||
|
||||
authBuilder.AddApiKey("SignalR", options =>
|
||||
{
|
||||
options.HeaderName = "X-Api-Key";
|
||||
options.QueryName = "access_token";
|
||||
options.ApiKey = config.ApiKey;
|
||||
});
|
||||
|
||||
return authBuilder;
|
||||
}
|
||||
}
|
||||
}
|
58
src/Prowlarr.Http/Authentication/AuthenticationController.cs
Normal file
58
src/Prowlarr.Http/Authentication/AuthenticationController.cs
Normal file
|
@ -0,0 +1,58 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Core.Configuration;
|
||||
|
||||
namespace Prowlarr.Http.Authentication
|
||||
{
|
||||
[AllowAnonymous]
|
||||
[ApiController]
|
||||
public class AuthenticationController : Controller
|
||||
{
|
||||
private readonly IAuthenticationService _authService;
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
|
||||
public AuthenticationController(IAuthenticationService authService, IConfigFileProvider configFileProvider)
|
||||
{
|
||||
_authService = authService;
|
||||
_configFileProvider = configFileProvider;
|
||||
}
|
||||
|
||||
[HttpPost("login")]
|
||||
public async Task<IActionResult> Login([FromForm] LoginResource resource, [FromQuery] string returnUrl = null)
|
||||
{
|
||||
var user = _authService.Login(HttpContext.Request, resource.Username, resource.Password);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
return Redirect($"~/login?returnUrl={returnUrl}&loginFailed=true");
|
||||
}
|
||||
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new Claim("user", user.Username),
|
||||
new Claim("identifier", user.Identifier.ToString()),
|
||||
new Claim("UiAuth", "true")
|
||||
};
|
||||
|
||||
var authProperties = new AuthenticationProperties
|
||||
{
|
||||
IsPersistent = resource.RememberMe == "on"
|
||||
};
|
||||
await HttpContext.SignInAsync(new ClaimsPrincipal(new ClaimsIdentity(claims, "Cookies", "user", "identifier")), authProperties);
|
||||
|
||||
return Redirect("/");
|
||||
}
|
||||
|
||||
[HttpGet("logout")]
|
||||
public async Task<IActionResult> Logout()
|
||||
{
|
||||
_authService.Logout(HttpContext);
|
||||
await HttpContext.SignOutAsync();
|
||||
return Redirect("/");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
using System;
|
||||
using Nancy;
|
||||
using Nancy.Authentication.Forms;
|
||||
using Nancy.Extensions;
|
||||
using Nancy.ModelBinding;
|
||||
using NzbDrone.Core.Configuration;
|
||||
|
||||
namespace Prowlarr.Http.Authentication
|
||||
{
|
||||
public class AuthenticationModule : NancyModule
|
||||
{
|
||||
private readonly IAuthenticationService _authService;
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
|
||||
public AuthenticationModule(IAuthenticationService authService, IConfigFileProvider configFileProvider)
|
||||
{
|
||||
_authService = authService;
|
||||
_configFileProvider = configFileProvider;
|
||||
Post("/login", x => Login(this.Bind<LoginResource>()));
|
||||
Get("/logout", x => Logout());
|
||||
}
|
||||
|
||||
private Response Login(LoginResource resource)
|
||||
{
|
||||
var user = _authService.Login(Context, resource.Username, resource.Password);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
var returnUrl = (string)Request.Query.returnUrl;
|
||||
return Context.GetRedirect($"~/login?returnUrl={returnUrl}&loginFailed=true");
|
||||
}
|
||||
|
||||
DateTime? expiry = null;
|
||||
|
||||
if (resource.RememberMe)
|
||||
{
|
||||
expiry = DateTime.UtcNow.AddDays(7);
|
||||
}
|
||||
|
||||
return this.LoginAndRedirect(user.Identifier, expiry, _configFileProvider.UrlBase + "/");
|
||||
}
|
||||
|
||||
private Response Logout()
|
||||
{
|
||||
_authService.Logout(Context);
|
||||
|
||||
return this.LogoutAndRedirect(_configFileProvider.UrlBase + "/");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,26 +1,16 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Principal;
|
||||
using Nancy;
|
||||
using Nancy.Authentication.Basic;
|
||||
using Nancy.Authentication.Forms;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Authentication;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using Prowlarr.Http.Extensions;
|
||||
|
||||
namespace Prowlarr.Http.Authentication
|
||||
{
|
||||
public interface IAuthenticationService : IUserValidator, IUserMapper
|
||||
public interface IAuthenticationService
|
||||
{
|
||||
void SetContext(NancyContext context);
|
||||
|
||||
void LogUnauthorized(NancyContext context);
|
||||
User Login(NancyContext context, string username, string password);
|
||||
void Logout(NancyContext context);
|
||||
bool IsAuthenticated(NancyContext context);
|
||||
void LogUnauthorized(HttpRequest context);
|
||||
User Login(HttpRequest request, string username, string password);
|
||||
void Logout(HttpContext context);
|
||||
}
|
||||
|
||||
public class AuthenticationService : IAuthenticationService
|
||||
|
@ -32,9 +22,6 @@ namespace Prowlarr.Http.Authentication
|
|||
private static string API_KEY;
|
||||
private static AuthenticationType AUTH_METHOD;
|
||||
|
||||
[ThreadStatic]
|
||||
private static NancyContext _context;
|
||||
|
||||
public AuthenticationService(IConfigFileProvider configFileProvider, IUserService userService)
|
||||
{
|
||||
_userService = userService;
|
||||
|
@ -42,13 +29,7 @@ namespace Prowlarr.Http.Authentication
|
|||
AUTH_METHOD = configFileProvider.AuthenticationMethod;
|
||||
}
|
||||
|
||||
public void SetContext(NancyContext context)
|
||||
{
|
||||
// Validate and GetUserIdentifier don't have access to the NancyContext so get it from the pipeline earlier
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public User Login(NancyContext context, string username, string password)
|
||||
public User Login(HttpRequest request, string username, string password)
|
||||
{
|
||||
if (AUTH_METHOD == AuthenticationType.None)
|
||||
{
|
||||
|
@ -59,174 +40,50 @@ namespace Prowlarr.Http.Authentication
|
|||
|
||||
if (user != null)
|
||||
{
|
||||
LogSuccess(context, username);
|
||||
LogSuccess(request, username);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
LogFailure(context, username);
|
||||
LogFailure(request, username);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void Logout(NancyContext context)
|
||||
public void Logout(HttpContext context)
|
||||
{
|
||||
if (AUTH_METHOD == AuthenticationType.None)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.CurrentUser != null)
|
||||
if (context.User != null)
|
||||
{
|
||||
LogLogout(context, context.CurrentUser.Identity.Name);
|
||||
LogLogout(context.Request, context.User.Identity.Name);
|
||||
}
|
||||
}
|
||||
|
||||
public ClaimsPrincipal Validate(string username, string password)
|
||||
public void LogUnauthorized(HttpRequest context)
|
||||
{
|
||||
if (AUTH_METHOD == AuthenticationType.None)
|
||||
{
|
||||
return new ClaimsPrincipal(new GenericIdentity(AnonymousUser));
|
||||
}
|
||||
|
||||
var user = _userService.FindUser(username, password);
|
||||
|
||||
if (user != null)
|
||||
{
|
||||
if (AUTH_METHOD != AuthenticationType.Basic)
|
||||
{
|
||||
// Don't log success for basic auth
|
||||
LogSuccess(_context, username);
|
||||
}
|
||||
|
||||
return new ClaimsPrincipal(new GenericIdentity(user.Username));
|
||||
}
|
||||
|
||||
LogFailure(_context, username);
|
||||
|
||||
return null;
|
||||
_authLogger.Info("Auth-Unauthorized ip {0} url '{1}'", context.GetRemoteIP(), context.Path);
|
||||
}
|
||||
|
||||
public ClaimsPrincipal GetUserFromIdentifier(Guid identifier, NancyContext context)
|
||||
{
|
||||
if (AUTH_METHOD == AuthenticationType.None)
|
||||
{
|
||||
return new ClaimsPrincipal(new GenericIdentity(AnonymousUser));
|
||||
}
|
||||
|
||||
var user = _userService.FindUser(identifier);
|
||||
|
||||
if (user != null)
|
||||
{
|
||||
return new ClaimsPrincipal(new GenericIdentity(user.Username));
|
||||
}
|
||||
|
||||
LogInvalidated(_context);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public bool IsAuthenticated(NancyContext context)
|
||||
{
|
||||
var apiKey = GetApiKey(context);
|
||||
|
||||
if (context.Request.IsApiRequest())
|
||||
{
|
||||
return ValidApiKey(apiKey);
|
||||
}
|
||||
|
||||
if (AUTH_METHOD == AuthenticationType.None)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (context.Request.IsFeedRequest())
|
||||
{
|
||||
if (ValidUser(context) || ValidApiKey(apiKey))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (context.Request.IsLoginRequest())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (context.Request.IsContentRequest())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ValidUser(context))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool ValidUser(NancyContext context)
|
||||
{
|
||||
if (context.CurrentUser != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool ValidApiKey(string apiKey)
|
||||
{
|
||||
if (API_KEY.Equals(apiKey))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private string GetApiKey(NancyContext context)
|
||||
{
|
||||
var apiKeyHeader = context.Request.Headers["X-Api-Key"].FirstOrDefault();
|
||||
var apiKeyQueryString = context.Request.Query["ApiKey"];
|
||||
|
||||
if (!apiKeyHeader.IsNullOrWhiteSpace())
|
||||
{
|
||||
return apiKeyHeader;
|
||||
}
|
||||
|
||||
if (apiKeyQueryString.HasValue)
|
||||
{
|
||||
return apiKeyQueryString.Value;
|
||||
}
|
||||
|
||||
return context.Request.Headers.Authorization;
|
||||
}
|
||||
|
||||
public void LogUnauthorized(NancyContext context)
|
||||
{
|
||||
_authLogger.Info("Auth-Unauthorized ip {0} url '{1}'", context.GetRemoteIP(), context.Request.Url.ToString());
|
||||
}
|
||||
|
||||
private void LogInvalidated(NancyContext context)
|
||||
private void LogInvalidated(HttpRequest context)
|
||||
{
|
||||
_authLogger.Info("Auth-Invalidated ip {0}", context.GetRemoteIP());
|
||||
}
|
||||
|
||||
private void LogFailure(NancyContext context, string username)
|
||||
private void LogFailure(HttpRequest context, string username)
|
||||
{
|
||||
_authLogger.Warn("Auth-Failure ip {0} username '{1}'", context.GetRemoteIP(), username);
|
||||
}
|
||||
|
||||
private void LogSuccess(NancyContext context, string username)
|
||||
private void LogSuccess(HttpRequest context, string username)
|
||||
{
|
||||
_authLogger.Info("Auth-Success ip {0} username '{1}'", context.GetRemoteIP(), username);
|
||||
}
|
||||
|
||||
private void LogLogout(NancyContext context, string username)
|
||||
private void LogLogout(HttpRequest context, string username)
|
||||
{
|
||||
_authLogger.Info("Auth-Logout ip {0} username '{1}'", context.GetRemoteIP(), username);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
|
||||
namespace Prowlarr.Http.Authentication
|
||||
{
|
||||
public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
|
||||
{
|
||||
private readonly IAuthenticationService _authService;
|
||||
|
||||
public BasicAuthenticationHandler(IAuthenticationService authService,
|
||||
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder,
|
||||
ISystemClock clock)
|
||||
: base(options, logger, encoder, clock)
|
||||
{
|
||||
_authService = authService;
|
||||
}
|
||||
|
||||
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
if (!Request.Headers.ContainsKey("Authorization"))
|
||||
{
|
||||
return Task.FromResult(AuthenticateResult.Fail("Authorization header missing."));
|
||||
}
|
||||
|
||||
// Get authorization key
|
||||
var authorizationHeader = Request.Headers["Authorization"].ToString();
|
||||
var authHeaderRegex = new Regex(@"Basic (.*)");
|
||||
|
||||
if (!authHeaderRegex.IsMatch(authorizationHeader))
|
||||
{
|
||||
return Task.FromResult(AuthenticateResult.Fail("Authorization code not formatted properly."));
|
||||
}
|
||||
|
||||
var authBase64 = Encoding.UTF8.GetString(Convert.FromBase64String(authHeaderRegex.Replace(authorizationHeader, "$1")));
|
||||
var authSplit = authBase64.Split(':', 2);
|
||||
var authUsername = authSplit[0];
|
||||
var authPassword = authSplit.Length > 1 ? authSplit[1] : throw new Exception("Unable to get password");
|
||||
|
||||
var user = _authService.Login(Request, authUsername, authPassword);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
return Task.FromResult(AuthenticateResult.Fail("The username or password is not correct."));
|
||||
}
|
||||
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new Claim("user", user.Username),
|
||||
new Claim("identifier", user.Identifier.ToString()),
|
||||
new Claim("UiAuth", "true")
|
||||
};
|
||||
|
||||
var identity = new ClaimsIdentity(claims, "Basic", "user", "identifier");
|
||||
var principal = new ClaimsPrincipal(identity);
|
||||
var ticket = new AuthenticationTicket(principal, "Basic");
|
||||
|
||||
return Task.FromResult(AuthenticateResult.Success(ticket));
|
||||
}
|
||||
|
||||
protected override Task HandleChallengeAsync(AuthenticationProperties properties)
|
||||
{
|
||||
Response.Headers.Add("WWW-Authenticate", $"Basic realm=\"{BuildInfo.AppName}\"");
|
||||
Response.StatusCode = 401;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected override Task HandleForbiddenAsync(AuthenticationProperties properties)
|
||||
{
|
||||
Response.StatusCode = 403;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,142 +0,0 @@
|
|||
using System;
|
||||
using System.Text;
|
||||
using Nancy;
|
||||
using Nancy.Authentication.Basic;
|
||||
using Nancy.Authentication.Forms;
|
||||
using Nancy.Bootstrapper;
|
||||
using Nancy.Cookies;
|
||||
using Nancy.Cryptography;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Authentication;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using Prowlarr.Http.Extensions;
|
||||
using Prowlarr.Http.Extensions.Pipelines;
|
||||
|
||||
namespace Prowlarr.Http.Authentication
|
||||
{
|
||||
public class EnableAuthInNancy : IRegisterNancyPipeline
|
||||
{
|
||||
private readonly IAuthenticationService _authenticationService;
|
||||
private readonly IConfigService _configService;
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
private FormsAuthenticationConfiguration _formsAuthConfig;
|
||||
|
||||
public EnableAuthInNancy(IAuthenticationService authenticationService,
|
||||
IConfigService configService,
|
||||
IConfigFileProvider configFileProvider)
|
||||
{
|
||||
_authenticationService = authenticationService;
|
||||
_configService = configService;
|
||||
_configFileProvider = configFileProvider;
|
||||
}
|
||||
|
||||
public int Order => 10;
|
||||
|
||||
public void Register(IPipelines pipelines)
|
||||
{
|
||||
if (_configFileProvider.AuthenticationMethod == AuthenticationType.Forms)
|
||||
{
|
||||
RegisterFormsAuth(pipelines);
|
||||
pipelines.AfterRequest.AddItemToEndOfPipeline((Action<NancyContext>)SlidingAuthenticationForFormsAuth);
|
||||
}
|
||||
else if (_configFileProvider.AuthenticationMethod == AuthenticationType.Basic)
|
||||
{
|
||||
pipelines.EnableBasicAuthentication(new BasicAuthenticationConfiguration(_authenticationService, "Prowlarr"));
|
||||
pipelines.BeforeRequest.AddItemToStartOfPipeline(CaptureContext);
|
||||
}
|
||||
|
||||
pipelines.BeforeRequest.AddItemToEndOfPipeline((Func<NancyContext, Response>)RequiresAuthentication);
|
||||
pipelines.AfterRequest.AddItemToEndOfPipeline((Action<NancyContext>)RemoveLoginHooksForApiCalls);
|
||||
}
|
||||
|
||||
private Response CaptureContext(NancyContext context)
|
||||
{
|
||||
_authenticationService.SetContext(context);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Response RequiresAuthentication(NancyContext context)
|
||||
{
|
||||
Response response = null;
|
||||
|
||||
if (!_authenticationService.IsAuthenticated(context))
|
||||
{
|
||||
_authenticationService.LogUnauthorized(context);
|
||||
response = new Response { StatusCode = HttpStatusCode.Unauthorized };
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private void RegisterFormsAuth(IPipelines pipelines)
|
||||
{
|
||||
FormsAuthentication.FormsAuthenticationCookieName = "ProwlarrAuth";
|
||||
|
||||
var cryptographyConfiguration = new CryptographyConfiguration(
|
||||
new AesEncryptionProvider(new PassphraseKeyGenerator(_configService.RijndaelPassphrase, Encoding.ASCII.GetBytes(_configService.RijndaelSalt))),
|
||||
new DefaultHmacProvider(new PassphraseKeyGenerator(_configService.HmacPassphrase, Encoding.ASCII.GetBytes(_configService.HmacSalt))));
|
||||
|
||||
_formsAuthConfig = new FormsAuthenticationConfiguration
|
||||
{
|
||||
RedirectUrl = _configFileProvider.UrlBase + "/login",
|
||||
UserMapper = _authenticationService,
|
||||
Path = GetCookiePath(),
|
||||
CryptographyConfiguration = cryptographyConfiguration
|
||||
};
|
||||
|
||||
FormsAuthentication.Enable(pipelines, _formsAuthConfig);
|
||||
}
|
||||
|
||||
private void RemoveLoginHooksForApiCalls(NancyContext context)
|
||||
{
|
||||
if (context.Request.IsApiRequest())
|
||||
{
|
||||
if ((context.Response.StatusCode == HttpStatusCode.SeeOther &&
|
||||
context.Response.Headers["Location"].StartsWith($"{_configFileProvider.UrlBase}/login", StringComparison.InvariantCultureIgnoreCase)) ||
|
||||
context.Response.StatusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
context.Response = new { Error = "Unauthorized" }.AsResponse(context, HttpStatusCode.Unauthorized);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SlidingAuthenticationForFormsAuth(NancyContext context)
|
||||
{
|
||||
if (context.CurrentUser == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var formsAuthCookieName = FormsAuthentication.FormsAuthenticationCookieName;
|
||||
|
||||
if (!context.Request.Path.Equals("/logout") &&
|
||||
context.Request.Cookies.ContainsKey(formsAuthCookieName))
|
||||
{
|
||||
var formsAuthCookieValue = context.Request.Cookies[formsAuthCookieName];
|
||||
|
||||
if (FormsAuthentication.DecryptAndValidateAuthenticationCookie(formsAuthCookieValue, _formsAuthConfig).IsNotNullOrWhiteSpace())
|
||||
{
|
||||
var formsAuthCookie = new NancyCookie(formsAuthCookieName, formsAuthCookieValue, true, false, DateTime.UtcNow.AddDays(7))
|
||||
{
|
||||
Path = GetCookiePath()
|
||||
};
|
||||
|
||||
context.Response.WithCookie(formsAuthCookie);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string GetCookiePath()
|
||||
{
|
||||
var urlBase = _configFileProvider.UrlBase;
|
||||
|
||||
if (urlBase.IsNullOrWhiteSpace())
|
||||
{
|
||||
return "/";
|
||||
}
|
||||
|
||||
return urlBase;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,6 +4,6 @@
|
|||
{
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
public bool RememberMe { get; set; }
|
||||
public string RememberMe { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
37
src/Prowlarr.Http/Authentication/NoAuthenticationHandler.cs
Normal file
37
src/Prowlarr.Http/Authentication/NoAuthenticationHandler.cs
Normal file
|
@ -0,0 +1,37 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Prowlarr.Http.Authentication
|
||||
{
|
||||
public class NoAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
|
||||
{
|
||||
public NoAuthenticationHandler(IAuthenticationService authService,
|
||||
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder,
|
||||
ISystemClock clock)
|
||||
: base(options, logger, encoder, clock)
|
||||
{
|
||||
}
|
||||
|
||||
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new Claim("user", "Anonymous"),
|
||||
new Claim("UiAuth", "true")
|
||||
};
|
||||
|
||||
var identity = new ClaimsIdentity(claims, "NoAuth", "user", "identifier");
|
||||
var principal = new ClaimsPrincipal(identity);
|
||||
var ticket = new AuthenticationTicket(principal, "NoAuth");
|
||||
|
||||
return Task.FromResult(AuthenticateResult.Success(ticket));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
using Nancy;
|
||||
using Nancy.ErrorHandling;
|
||||
using Prowlarr.Http.Extensions;
|
||||
|
||||
namespace Prowlarr.Http.ErrorManagement
|
||||
{
|
||||
public class ErrorHandler : IStatusCodeHandler
|
||||
{
|
||||
public bool HandlesStatusCode(HttpStatusCode statusCode, NancyContext context)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Handle(HttpStatusCode statusCode, NancyContext context)
|
||||
{
|
||||
if (statusCode == HttpStatusCode.SeeOther || statusCode == HttpStatusCode.MovedPermanently || statusCode == HttpStatusCode.OK)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (statusCode == HttpStatusCode.Continue)
|
||||
{
|
||||
context.Response = new Response { StatusCode = statusCode };
|
||||
return;
|
||||
}
|
||||
|
||||
if (statusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.Response.ContentType == "text/html" || context.Response.ContentType == "text/plain")
|
||||
{
|
||||
context.Response = new ErrorModel
|
||||
{
|
||||
Message = statusCode.ToString()
|
||||
}.AsResponse(context, statusCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,8 @@
|
|||
using Prowlarr.Http.Exceptions;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using Prowlarr.Http.Exceptions;
|
||||
|
||||
namespace Prowlarr.Http.ErrorManagement
|
||||
{
|
||||
|
@ -17,5 +21,12 @@ namespace Prowlarr.Http.ErrorManagement
|
|||
public ErrorModel()
|
||||
{
|
||||
}
|
||||
|
||||
public Task WriteToResponse(HttpResponse response, HttpStatusCode statusCode = HttpStatusCode.InternalServerError)
|
||||
{
|
||||
response.StatusCode = (int)statusCode;
|
||||
response.ContentType = "application/json";
|
||||
return STJson.SerializeAsync(this, response.Body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
using System;
|
||||
using System.Data.SQLite;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using FluentValidation;
|
||||
using Nancy;
|
||||
using Nancy.Extensions;
|
||||
using Nancy.IO;
|
||||
using Microsoft.AspNetCore.Diagnostics;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Exceptions;
|
||||
using Prowlarr.Http.Exceptions;
|
||||
using Prowlarr.Http.Extensions;
|
||||
using HttpStatusCode = Nancy.HttpStatusCode;
|
||||
|
||||
namespace Prowlarr.Http.ErrorManagement
|
||||
{
|
||||
|
@ -22,63 +21,81 @@ namespace Prowlarr.Http.ErrorManagement
|
|||
_logger = logger;
|
||||
}
|
||||
|
||||
public Response HandleException(NancyContext context, Exception exception)
|
||||
public async Task HandleException(HttpContext context)
|
||||
{
|
||||
_logger.Trace("Handling Exception");
|
||||
|
||||
var response = context.Response;
|
||||
var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
|
||||
var exception = exceptionHandlerPathFeature?.Error;
|
||||
|
||||
_logger.Warn(exception);
|
||||
|
||||
var statusCode = HttpStatusCode.InternalServerError;
|
||||
var errorModel = new ErrorModel
|
||||
{
|
||||
Message = exception.Message,
|
||||
Description = exception.ToString()
|
||||
};
|
||||
|
||||
if (exception is ApiException apiException)
|
||||
{
|
||||
_logger.Warn(apiException, "API Error:\n{0}", apiException.Message);
|
||||
var body = RequestStream.FromStream(context.Request.Body).AsString();
|
||||
_logger.Trace("Request body:\n{0}", body);
|
||||
|
||||
return apiException.ToErrorResponse(context);
|
||||
/* var body = RequestStream.FromStream(context.Request.Body).AsString();
|
||||
_logger.Trace("Request body:\n{0}", body);*/
|
||||
|
||||
errorModel = new ErrorModel(apiException);
|
||||
statusCode = apiException.StatusCode;
|
||||
}
|
||||
|
||||
if (exception is ValidationException validationException)
|
||||
else if (exception is ValidationException validationException)
|
||||
{
|
||||
_logger.Warn("Invalid request {0}", validationException.Message);
|
||||
|
||||
return validationException.Errors.AsResponse(context, HttpStatusCode.BadRequest);
|
||||
response.StatusCode = (int)HttpStatusCode.BadRequest;
|
||||
response.ContentType = "application/json";
|
||||
await response.WriteAsync(STJson.ToJson(validationException.Errors));
|
||||
return;
|
||||
}
|
||||
|
||||
if (exception is NzbDroneClientException clientException)
|
||||
else if (exception is NzbDroneClientException clientException)
|
||||
{
|
||||
return new ErrorModel
|
||||
errorModel = new ErrorModel
|
||||
{
|
||||
Message = exception.Message,
|
||||
Description = exception.ToString()
|
||||
}.AsResponse(context, (HttpStatusCode)clientException.StatusCode);
|
||||
};
|
||||
statusCode = clientException.StatusCode;
|
||||
}
|
||||
|
||||
if (exception is ModelNotFoundException notFoundException)
|
||||
else if (exception is ModelNotFoundException notFoundException)
|
||||
{
|
||||
return new ErrorModel
|
||||
errorModel = new ErrorModel
|
||||
{
|
||||
Message = exception.Message,
|
||||
Description = exception.ToString()
|
||||
}.AsResponse(context, HttpStatusCode.NotFound);
|
||||
};
|
||||
statusCode = HttpStatusCode.NotFound;
|
||||
}
|
||||
|
||||
if (exception is ModelConflictException conflictException)
|
||||
else if (exception is ModelConflictException conflictException)
|
||||
{
|
||||
return new ErrorModel
|
||||
_logger.Error(exception, "DB error");
|
||||
errorModel = new ErrorModel
|
||||
{
|
||||
Message = exception.Message,
|
||||
Description = exception.ToString()
|
||||
}.AsResponse(context, HttpStatusCode.Conflict);
|
||||
};
|
||||
statusCode = HttpStatusCode.Conflict;
|
||||
}
|
||||
|
||||
if (exception is SQLiteException sqLiteException)
|
||||
else if (exception is SQLiteException sqLiteException)
|
||||
{
|
||||
if (context.Request.Method == "PUT" || context.Request.Method == "POST")
|
||||
{
|
||||
if (sqLiteException.Message.Contains("constraint failed"))
|
||||
{
|
||||
return new ErrorModel
|
||||
errorModel = new ErrorModel
|
||||
{
|
||||
Message = exception.Message,
|
||||
}.AsResponse(context, HttpStatusCode.Conflict);
|
||||
};
|
||||
statusCode = HttpStatusCode.Conflict;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -87,11 +104,7 @@ namespace Prowlarr.Http.ErrorManagement
|
|||
|
||||
_logger.Fatal(exception, "Request Failed. {0} {1}", context.Request.Method, context.Request.Path);
|
||||
|
||||
return new ErrorModel
|
||||
{
|
||||
Message = exception.Message,
|
||||
Description = exception.ToString()
|
||||
}.AsResponse(context, HttpStatusCode.InternalServerError);
|
||||
await errorModel.WriteToResponse(response, statusCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
using System;
|
||||
using Nancy;
|
||||
using Nancy.Responses;
|
||||
using Prowlarr.Http.ErrorManagement;
|
||||
using Prowlarr.Http.Extensions;
|
||||
using System.Net;
|
||||
|
||||
namespace Prowlarr.Http.Exceptions
|
||||
{
|
||||
|
@ -19,11 +16,6 @@ namespace Prowlarr.Http.Exceptions
|
|||
Content = content;
|
||||
}
|
||||
|
||||
public JsonResponse<ErrorModel> ToErrorResponse(NancyContext context)
|
||||
{
|
||||
return new ErrorModel(this).AsResponse(context, StatusCode);
|
||||
}
|
||||
|
||||
private static string GetMessage(HttpStatusCode statusCode, object content)
|
||||
{
|
||||
var result = statusCode.ToString();
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using Nancy;
|
||||
using Nancy.Responses.Negotiation;
|
||||
using NzbDrone.Common.Serializer;
|
||||
|
||||
namespace Prowlarr.Http.Extensions
|
||||
{
|
||||
public class NancyJsonSerializer : ISerializer
|
||||
{
|
||||
protected readonly JsonSerializerOptions _serializerSettings;
|
||||
|
||||
public NancyJsonSerializer()
|
||||
{
|
||||
_serializerSettings = STJson.GetSerializerSettings();
|
||||
}
|
||||
|
||||
public bool CanSerialize(MediaRange contentType)
|
||||
{
|
||||
return contentType == "application/json";
|
||||
}
|
||||
|
||||
public void Serialize<TModel>(MediaRange contentType, TModel model, Stream outputStream)
|
||||
{
|
||||
STJson.Serialize(model, outputStream, _serializerSettings);
|
||||
}
|
||||
|
||||
public IEnumerable<string> Extensions { get; private set; }
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
using System;
|
||||
using Nancy;
|
||||
using Nancy.Bootstrapper;
|
||||
using Prowlarr.Http.Frontend;
|
||||
|
||||
namespace Prowlarr.Http.Extensions.Pipelines
|
||||
{
|
||||
public class CacheHeaderPipeline : IRegisterNancyPipeline
|
||||
{
|
||||
private readonly ICacheableSpecification _cacheableSpecification;
|
||||
|
||||
public CacheHeaderPipeline(ICacheableSpecification cacheableSpecification)
|
||||
{
|
||||
_cacheableSpecification = cacheableSpecification;
|
||||
}
|
||||
|
||||
public int Order => 0;
|
||||
|
||||
public void Register(IPipelines pipelines)
|
||||
{
|
||||
pipelines.AfterRequest.AddItemToStartOfPipeline((Action<NancyContext>)Handle);
|
||||
}
|
||||
|
||||
private void Handle(NancyContext context)
|
||||
{
|
||||
if (context.Request.Method == "OPTIONS")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_cacheableSpecification.IsCacheable(context))
|
||||
{
|
||||
context.Response.Headers.EnableCache();
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Response.Headers.DisableCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
using System.Linq;
|
||||
using Nancy;
|
||||
using Nancy.Bootstrapper;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
||||
namespace Prowlarr.Http.Extensions.Pipelines
|
||||
{
|
||||
public class CorsPipeline : IRegisterNancyPipeline
|
||||
{
|
||||
public int Order => 0;
|
||||
|
||||
public void Register(IPipelines pipelines)
|
||||
{
|
||||
pipelines.BeforeRequest.AddItemToEndOfPipeline(HandleRequest);
|
||||
pipelines.AfterRequest.AddItemToEndOfPipeline(HandleResponse);
|
||||
}
|
||||
|
||||
private Response HandleRequest(NancyContext context)
|
||||
{
|
||||
if (context == null || context.Request.Method != "OPTIONS")
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var response = new Response()
|
||||
.WithStatusCode(HttpStatusCode.OK)
|
||||
.WithContentType("");
|
||||
ApplyResponseHeaders(response, context.Request);
|
||||
return response;
|
||||
}
|
||||
|
||||
private void HandleResponse(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)
|
||||
{
|
||||
if (request.IsApiRequest())
|
||||
{
|
||||
// Allow Cross-Origin access to the API since it's protected with the apikey, and nothing else.
|
||||
ApplyCorsResponseHeaders(response, request, "*", "GET, OPTIONS, PATCH, POST, PUT, DELETE");
|
||||
}
|
||||
else if (request.IsSharedContentRequest())
|
||||
{
|
||||
// Allow Cross-Origin access to specific shared content such as mediacovers and images.
|
||||
ApplyCorsResponseHeaders(response, request, "*", "GET, OPTIONS");
|
||||
}
|
||||
|
||||
// Disallow Cross-Origin access for any other route.
|
||||
}
|
||||
|
||||
private static void ApplyCorsResponseHeaders(Response response, Request request, string allowOrigin, string allowedMethods)
|
||||
{
|
||||
response.Headers.Add(AccessControlHeaders.AllowOrigin, allowOrigin);
|
||||
|
||||
if (request.Method == "OPTIONS")
|
||||
{
|
||||
if (response.Headers.ContainsKey("Allow"))
|
||||
{
|
||||
allowedMethods = response.Headers["Allow"];
|
||||
}
|
||||
|
||||
response.Headers.Add(AccessControlHeaders.AllowMethods, allowedMethods);
|
||||
|
||||
if (request.Headers[AccessControlHeaders.RequestHeaders].Any())
|
||||
{
|
||||
var requestedHeaders = request.Headers[AccessControlHeaders.RequestHeaders].Join(", ");
|
||||
|
||||
response.Headers.Add(AccessControlHeaders.AllowHeaders, requestedHeaders);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,104 +0,0 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using Nancy;
|
||||
using Nancy.Bootstrapper;
|
||||
using NLog;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
|
||||
namespace Prowlarr.Http.Extensions.Pipelines
|
||||
{
|
||||
public class GzipCompressionPipeline : IRegisterNancyPipeline
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
|
||||
public int Order => 0;
|
||||
|
||||
private readonly Action<Action<Stream>, Stream> _writeGZipStream;
|
||||
|
||||
public GzipCompressionPipeline(Logger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
|
||||
// On Mono GZipStream/DeflateStream leaks memory if an exception is thrown, use an intermediate buffer in that case.
|
||||
_writeGZipStream = PlatformInfo.IsMono ? WriteGZipStreamMono : (Action<Action<Stream>, Stream>)WriteGZipStream;
|
||||
}
|
||||
|
||||
public void Register(IPipelines pipelines)
|
||||
{
|
||||
pipelines.AfterRequest.AddItemToEndOfPipeline(CompressResponse);
|
||||
}
|
||||
|
||||
private void CompressResponse(NancyContext context)
|
||||
{
|
||||
var request = context.Request;
|
||||
var response = context.Response;
|
||||
|
||||
try
|
||||
{
|
||||
if (
|
||||
response.Contents != Response.NoBody
|
||||
&& !response.ContentType.Contains("image")
|
||||
&& !response.ContentType.Contains("font")
|
||||
&& request.Headers.AcceptEncoding.Any(x => x.Contains("gzip"))
|
||||
&& !AlreadyGzipEncoded(response)
|
||||
&& !ContentLengthIsTooSmall(response))
|
||||
{
|
||||
var contents = response.Contents;
|
||||
|
||||
response.Headers["Content-Encoding"] = "gzip";
|
||||
response.Contents = responseStream => _writeGZipStream(contents, responseStream);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Unable to gzip response");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteGZipStreamMono(Action<Stream> innerContent, Stream targetStream)
|
||||
{
|
||||
using (var membuffer = new MemoryStream())
|
||||
{
|
||||
WriteGZipStream(innerContent, membuffer);
|
||||
membuffer.Position = 0;
|
||||
membuffer.CopyTo(targetStream);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteGZipStream(Action<Stream> innerContent, Stream targetStream)
|
||||
{
|
||||
using (var gzip = new GZipStream(targetStream, CompressionMode.Compress, true))
|
||||
using (var buffered = new BufferedStream(gzip, 8192))
|
||||
{
|
||||
innerContent.Invoke(buffered);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ContentLengthIsTooSmall(Response response)
|
||||
{
|
||||
var contentLength = response.Headers.TryGetValue("Content-Length", out var value) ? value : null;
|
||||
|
||||
if (contentLength != null && long.Parse(contentLength) < 1024)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool AlreadyGzipEncoded(Response response)
|
||||
{
|
||||
var contentEncoding = response.Headers.TryGetValue("Content-Encoding", out var value) ? value : null;
|
||||
|
||||
if (contentEncoding == "gzip")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
using Nancy.Bootstrapper;
|
||||
|
||||
namespace Prowlarr.Http.Extensions.Pipelines
|
||||
{
|
||||
public interface IRegisterNancyPipeline
|
||||
{
|
||||
int Order { get; }
|
||||
|
||||
void Register(IPipelines pipelines);
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
using System;
|
||||
using Nancy;
|
||||
using Nancy.Bootstrapper;
|
||||
using Prowlarr.Http.Frontend;
|
||||
|
||||
namespace Prowlarr.Http.Extensions.Pipelines
|
||||
{
|
||||
public class IfModifiedPipeline : IRegisterNancyPipeline
|
||||
{
|
||||
private readonly ICacheableSpecification _cacheableSpecification;
|
||||
|
||||
public IfModifiedPipeline(ICacheableSpecification cacheableSpecification)
|
||||
{
|
||||
_cacheableSpecification = cacheableSpecification;
|
||||
}
|
||||
|
||||
public int Order => 0;
|
||||
|
||||
public void Register(IPipelines pipelines)
|
||||
{
|
||||
pipelines.BeforeRequest.AddItemToStartOfPipeline((Func<NancyContext, Response>)Handle);
|
||||
}
|
||||
|
||||
private Response Handle(NancyContext context)
|
||||
{
|
||||
if (_cacheableSpecification.IsCacheable(context) && context.Request.Headers.IfModifiedSince.HasValue)
|
||||
{
|
||||
var response = new Response { ContentType = MimeTypes.GetMimeType(context.Request.Path), StatusCode = HttpStatusCode.NotModified };
|
||||
response.Headers.EnableCache();
|
||||
return response;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
using System;
|
||||
using Nancy;
|
||||
using Nancy.Bootstrapper;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
|
||||
namespace Prowlarr.Http.Extensions.Pipelines
|
||||
{
|
||||
public class ProwlarrVersionPipeline : IRegisterNancyPipeline
|
||||
{
|
||||
public int Order => 0;
|
||||
|
||||
public void Register(IPipelines pipelines)
|
||||
{
|
||||
pipelines.AfterRequest.AddItemToStartOfPipeline((Action<NancyContext>)Handle);
|
||||
}
|
||||
|
||||
private void Handle(NancyContext context)
|
||||
{
|
||||
if (!context.Response.Headers.ContainsKey("X-ApplicationVersion"))
|
||||
{
|
||||
context.Response.Headers.Add("X-ApplicationVersion", BuildInfo.Version.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,104 +0,0 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using Nancy;
|
||||
using Nancy.Bootstrapper;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using Prowlarr.Http.ErrorManagement;
|
||||
using Prowlarr.Http.Extensions;
|
||||
using Prowlarr.Http.Extensions.Pipelines;
|
||||
|
||||
namespace NzbDrone.Api.Extensions.Pipelines
|
||||
{
|
||||
public class RequestLoggingPipeline : IRegisterNancyPipeline
|
||||
{
|
||||
private static readonly Logger _loggerHttp = LogManager.GetLogger("Http");
|
||||
private static readonly Logger _loggerApi = LogManager.GetLogger("Api");
|
||||
|
||||
private static int _requestSequenceID;
|
||||
|
||||
private readonly ProwlarrErrorPipeline _errorPipeline;
|
||||
|
||||
public RequestLoggingPipeline(ProwlarrErrorPipeline errorPipeline)
|
||||
{
|
||||
_errorPipeline = errorPipeline;
|
||||
}
|
||||
|
||||
public int Order => 100;
|
||||
|
||||
public void Register(IPipelines pipelines)
|
||||
{
|
||||
pipelines.BeforeRequest.AddItemToStartOfPipeline(LogStart);
|
||||
pipelines.AfterRequest.AddItemToEndOfPipeline(LogEnd);
|
||||
pipelines.OnError.AddItemToEndOfPipeline(LogError);
|
||||
}
|
||||
|
||||
private Response LogStart(NancyContext context)
|
||||
{
|
||||
var id = Interlocked.Increment(ref _requestSequenceID);
|
||||
|
||||
context.Items["ApiRequestSequenceID"] = id;
|
||||
context.Items["ApiRequestStartTime"] = DateTime.UtcNow;
|
||||
|
||||
var reqPath = GetRequestPathAndQuery(context.Request);
|
||||
|
||||
_loggerHttp.Trace("Req: {0} [{1}] {2} (from {3})", id, context.Request.Method, reqPath, GetOrigin(context));
|
||||
return null;
|
||||
}
|
||||
|
||||
private void LogEnd(NancyContext context)
|
||||
{
|
||||
var id = (int)context.Items["ApiRequestSequenceID"];
|
||||
var startTime = (DateTime)context.Items["ApiRequestStartTime"];
|
||||
|
||||
var endTime = DateTime.UtcNow;
|
||||
var duration = endTime - startTime;
|
||||
|
||||
var reqPath = GetRequestPathAndQuery(context.Request);
|
||||
|
||||
_loggerHttp.Trace("Res: {0} [{1}] {2}: {3}.{4} ({5} ms)", id, context.Request.Method, reqPath, (int)context.Response.StatusCode, context.Response.StatusCode, (int)duration.TotalMilliseconds);
|
||||
|
||||
if (context.Request.IsApiRequest())
|
||||
{
|
||||
_loggerApi.Debug("[{0}] {1}: {2}.{3} ({4} ms)", context.Request.Method, reqPath, (int)context.Response.StatusCode, context.Response.StatusCode, (int)duration.TotalMilliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
private Response LogError(NancyContext context, Exception exception)
|
||||
{
|
||||
var response = _errorPipeline.HandleException(context, exception);
|
||||
|
||||
context.Response = response;
|
||||
|
||||
LogEnd(context);
|
||||
|
||||
context.Response = null;
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private static string GetRequestPathAndQuery(Request request)
|
||||
{
|
||||
if (request.Url.Query.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
return string.Concat(request.Url.Path, request.Url.Query);
|
||||
}
|
||||
else
|
||||
{
|
||||
return request.Url.Path;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetOrigin(NancyContext context)
|
||||
{
|
||||
if (context.Request.Headers.UserAgent.IsNullOrWhiteSpace())
|
||||
{
|
||||
return context.GetRemoteIP();
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"{context.GetRemoteIP()} {context.Request.Headers.UserAgent}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
using System;
|
||||
using Nancy;
|
||||
using Nancy.Bootstrapper;
|
||||
using Nancy.Responses;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Configuration;
|
||||
|
||||
namespace Prowlarr.Http.Extensions.Pipelines
|
||||
{
|
||||
public class UrlBasePipeline : IRegisterNancyPipeline
|
||||
{
|
||||
private readonly string _urlBase;
|
||||
|
||||
public UrlBasePipeline(IConfigFileProvider configFileProvider)
|
||||
{
|
||||
_urlBase = configFileProvider.UrlBase;
|
||||
}
|
||||
|
||||
public int Order => 99;
|
||||
|
||||
public void Register(IPipelines pipelines)
|
||||
{
|
||||
if (_urlBase.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
pipelines.BeforeRequest.AddItemToStartOfPipeline((Func<NancyContext, Response>)Handle);
|
||||
}
|
||||
}
|
||||
|
||||
private Response Handle(NancyContext context)
|
||||
{
|
||||
var basePath = context.Request.Url.BasePath;
|
||||
|
||||
if (basePath.IsNullOrWhiteSpace())
|
||||
{
|
||||
return new RedirectResponse($"{_urlBase}{context.Request.Path}{context.Request.Url.Query}");
|
||||
}
|
||||
|
||||
if (_urlBase != basePath)
|
||||
{
|
||||
return new NotFoundResponse();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Nancy;
|
||||
using Nancy.Responses;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Serializer;
|
||||
|
||||
namespace Prowlarr.Http.Extensions
|
||||
{
|
||||
public static class ReqResExtensions
|
||||
{
|
||||
private static readonly NancyJsonSerializer NancySerializer = new NancyJsonSerializer();
|
||||
|
||||
public static readonly string LastModified = BuildInfo.BuildDateTime.ToString("r");
|
||||
|
||||
public static T FromJson<T>(this Stream body)
|
||||
where T : class, new()
|
||||
{
|
||||
return FromJson<T>(body, typeof(T));
|
||||
}
|
||||
|
||||
public static T FromJson<T>(this Stream body, Type type)
|
||||
{
|
||||
return (T)FromJson(body, type);
|
||||
}
|
||||
|
||||
public static object FromJson(this Stream body, Type type)
|
||||
{
|
||||
body.Position = 0;
|
||||
return STJson.Deserialize(body, type);
|
||||
}
|
||||
|
||||
public static JsonResponse<TModel> AsResponse<TModel>(this TModel model, NancyContext context, HttpStatusCode statusCode = HttpStatusCode.OK)
|
||||
{
|
||||
var response = new JsonResponse<TModel>(model, NancySerializer, context.Environment) { StatusCode = statusCode };
|
||||
response.Headers.DisableCache();
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public static IDictionary<string, string> DisableCache(this IDictionary<string, string> headers)
|
||||
{
|
||||
headers["Cache-Control"] = "no-cache, no-store, must-revalidate, max-age=0";
|
||||
headers["Pragma"] = "no-cache";
|
||||
headers["Expires"] = "0";
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
public static IDictionary<string, string> EnableCache(this IDictionary<string, string> headers)
|
||||
{
|
||||
headers["Cache-Control"] = "max-age=31536000 , public";
|
||||
headers["Expires"] = "Sat, 29 Jun 2020 00:00:00 GMT";
|
||||
headers["Last-Modified"] = LastModified;
|
||||
headers["Age"] = "193266";
|
||||
|
||||
return headers;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,107 +1,137 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using Nancy;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Exceptions;
|
||||
|
||||
namespace Prowlarr.Http.Extensions
|
||||
{
|
||||
public static class RequestExtensions
|
||||
{
|
||||
public static bool IsApiRequest(this Request request)
|
||||
public static bool IsApiRequest(this HttpRequest request)
|
||||
{
|
||||
return request.Path.StartsWith("/api/", StringComparison.InvariantCultureIgnoreCase);
|
||||
return request.Path.StartsWithSegments("/api", StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
public static bool IsFeedRequest(this Request request)
|
||||
{
|
||||
return request.Path.StartsWith("/feed/", 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");
|
||||
}
|
||||
|
||||
public static bool IsLoginRequest(this Request request)
|
||||
{
|
||||
return request.Path.Equals("/login", StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
public static bool IsContentRequest(this Request request)
|
||||
{
|
||||
return request.Path.StartsWith("/Content/", StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
public static bool IsSharedContentRequest(this Request request)
|
||||
{
|
||||
return request.Path.StartsWith("/MediaCover/", StringComparison.InvariantCultureIgnoreCase) ||
|
||||
request.Path.StartsWith("/Content/Images/", StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
public static bool GetBooleanQueryParameter(this Request request, string parameter, bool defaultValue = false)
|
||||
public static bool GetBooleanQueryParameter(this HttpRequest request, string parameter, bool defaultValue = false)
|
||||
{
|
||||
var parameterValue = request.Query[parameter];
|
||||
|
||||
if (parameterValue.HasValue)
|
||||
if (parameterValue.Any())
|
||||
{
|
||||
return bool.Parse(parameterValue.Value);
|
||||
return bool.Parse(parameterValue.ToString());
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public static int GetIntegerQueryParameter(this Request request, string parameter, int defaultValue = 0)
|
||||
public static PagingResource<TResource> ReadPagingResourceFromRequest<TResource>(this HttpRequest request)
|
||||
{
|
||||
var parameterValue = request.Query[parameter];
|
||||
|
||||
if (parameterValue.HasValue)
|
||||
if (!int.TryParse(request.Query["PageSize"].ToString(), out var pageSize))
|
||||
{
|
||||
return int.Parse(parameterValue.Value);
|
||||
pageSize = 10;
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public static int? GetNullableIntegerQueryParameter(this Request request, string parameter, int? defaultValue = null)
|
||||
{
|
||||
var parameterValue = request.Query[parameter];
|
||||
|
||||
if (parameterValue.HasValue)
|
||||
if (!int.TryParse(request.Query["Page"].ToString(), out var page))
|
||||
{
|
||||
return int.Parse(parameterValue.Value);
|
||||
page = 1;
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
var pagingResource = new PagingResource<TResource>
|
||||
{
|
||||
PageSize = pageSize,
|
||||
Page = page,
|
||||
Filters = new List<PagingResourceFilter>()
|
||||
};
|
||||
|
||||
if (request.Query["SortKey"].Any())
|
||||
{
|
||||
var sortKey = request.Query["SortKey"].ToString();
|
||||
|
||||
pagingResource.SortKey = sortKey;
|
||||
|
||||
if (request.Query["SortDirection"].Any())
|
||||
{
|
||||
pagingResource.SortDirection = request.Query["SortDirection"].ToString()
|
||||
.Equals("ascending", StringComparison.InvariantCultureIgnoreCase)
|
||||
? SortDirection.Ascending
|
||||
: SortDirection.Descending;
|
||||
}
|
||||
}
|
||||
|
||||
// For backwards compatibility with v2
|
||||
if (request.Query["FilterKey"].Any())
|
||||
{
|
||||
var filter = new PagingResourceFilter
|
||||
{
|
||||
Key = request.Query["FilterKey"].ToString()
|
||||
};
|
||||
|
||||
if (request.Query["FilterValue"].Any())
|
||||
{
|
||||
filter.Value = request.Query["FilterValue"].ToString();
|
||||
}
|
||||
|
||||
pagingResource.Filters.Add(filter);
|
||||
}
|
||||
|
||||
// v3 uses filters in key=value format
|
||||
foreach (var pair in request.Query)
|
||||
{
|
||||
pagingResource.Filters.Add(new PagingResourceFilter
|
||||
{
|
||||
Key = pair.Key,
|
||||
Value = pair.Value.ToString()
|
||||
});
|
||||
}
|
||||
|
||||
return pagingResource;
|
||||
}
|
||||
|
||||
public static string GetRemoteIP(this NancyContext context)
|
||||
public static PagingResource<TResource> ApplyToPage<TResource, TModel>(this PagingSpec<TModel> pagingSpec, Func<PagingSpec<TModel>, PagingSpec<TModel>> function, Converter<TModel, TResource> mapper)
|
||||
{
|
||||
if (context == null || context.Request == null)
|
||||
pagingSpec = function(pagingSpec);
|
||||
|
||||
return new PagingResource<TResource>
|
||||
{
|
||||
Page = pagingSpec.Page,
|
||||
PageSize = pagingSpec.PageSize,
|
||||
SortDirection = pagingSpec.SortDirection,
|
||||
SortKey = pagingSpec.SortKey,
|
||||
TotalRecords = pagingSpec.TotalRecords,
|
||||
Records = pagingSpec.Records.ConvertAll(mapper)
|
||||
};
|
||||
}
|
||||
|
||||
public static string GetRemoteIP(this HttpContext context)
|
||||
{
|
||||
return context?.Request?.GetRemoteIP() ?? "Unknown";
|
||||
}
|
||||
|
||||
public static string GetRemoteIP(this HttpRequest request)
|
||||
{
|
||||
if (request == null)
|
||||
{
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
var remoteAddress = context.Request.UserHostAddress;
|
||||
IPAddress remoteIP;
|
||||
var remoteIP = request.HttpContext.Connection.RemoteIpAddress;
|
||||
var remoteAddress = remoteIP.ToString();
|
||||
|
||||
// Only check if forwarded by a local network reverse proxy
|
||||
if (IPAddress.TryParse(remoteAddress, out remoteIP) && remoteIP.IsLocalAddress())
|
||||
if (remoteIP.IsLocalAddress())
|
||||
{
|
||||
var realIPHeader = context.Request.Headers["X-Real-IP"];
|
||||
var realIPHeader = request.Headers["X-Real-IP"];
|
||||
if (realIPHeader.Any())
|
||||
{
|
||||
return realIPHeader.First().ToString();
|
||||
}
|
||||
|
||||
var forwardedForHeader = context.Request.Headers["X-Forwarded-For"];
|
||||
var forwardedForHeader = request.Headers["X-Forwarded-For"];
|
||||
if (forwardedForHeader.Any())
|
||||
{
|
||||
// Get the first address that was forwarded by a local IP to prevent remote clients faking another proxy
|
||||
|
@ -125,16 +155,16 @@ namespace Prowlarr.Http.Extensions
|
|||
return remoteAddress;
|
||||
}
|
||||
|
||||
public static string GetServerUrl(this Request request)
|
||||
public static string GetServerUrl(this HttpRequest request)
|
||||
{
|
||||
var scheme = request.Url.Scheme;
|
||||
var port = request.Url.Port;
|
||||
var scheme = request.Scheme;
|
||||
var port = request.HttpContext.Connection.LocalPort;
|
||||
|
||||
// Check for protocol headers added by reverse proxys
|
||||
// X-Forwarded-Proto: A de facto standard for identifying the originating protocol of an HTTP request
|
||||
var xForwardedProto = request.Headers.Where(x => x.Key == "X-Forwarded-Proto").Select(x => x.Value).FirstOrDefault();
|
||||
|
||||
if (xForwardedProto != null)
|
||||
if (xForwardedProto.Any())
|
||||
{
|
||||
scheme = xForwardedProto.First();
|
||||
}
|
||||
|
@ -146,12 +176,25 @@ namespace Prowlarr.Http.Extensions
|
|||
}
|
||||
|
||||
//default to 443 if the Host header doesn't contain the port (needed for reverse proxy setups)
|
||||
if (scheme == "https" && !request.Url.HostName.Contains(":"))
|
||||
if (scheme == "https" && !request.Host.Port.HasValue)
|
||||
{
|
||||
port = 443;
|
||||
}
|
||||
|
||||
return $"{scheme}://{request.Url.HostName}:{port}";
|
||||
return $"{scheme}://{request.Host.Host}:{port}";
|
||||
}
|
||||
|
||||
public static void DisableCache(this IHeaderDictionary headers)
|
||||
{
|
||||
headers["Cache-Control"] = "no-cache, no-store";
|
||||
headers["Expires"] = "-1";
|
||||
headers["Pragma"] = "no-cache";
|
||||
}
|
||||
|
||||
public static void EnableCache(this IHeaderDictionary headers)
|
||||
{
|
||||
headers["Cache-Control"] = "max-age=31536000, public";
|
||||
headers["Last-Modified"] = BuildInfo.BuildDateTime.ToString("r");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
using System.IO;
|
||||
using Nancy;
|
||||
|
||||
namespace NzbDrone.Http.Extensions
|
||||
{
|
||||
public static class ResponseExtensions
|
||||
{
|
||||
public static Response FromByteArray(this IResponseFormatter formatter, byte[] body, string contentType = null)
|
||||
{
|
||||
return new ByteArrayResponse(body, contentType);
|
||||
}
|
||||
}
|
||||
|
||||
public class ByteArrayResponse : Response
|
||||
{
|
||||
public ByteArrayResponse(byte[] body, string contentType = null)
|
||||
{
|
||||
this.ContentType = contentType ?? "application/octet-stream";
|
||||
|
||||
this.Contents = stream =>
|
||||
{
|
||||
using (var writer = new BinaryWriter(stream))
|
||||
{
|
||||
writer.Write(body);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
using System;
|
||||
using Nancy;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
||||
namespace Prowlarr.Http.Frontend
|
||||
{
|
||||
public interface ICacheableSpecification
|
||||
{
|
||||
bool IsCacheable(NancyContext context);
|
||||
}
|
||||
|
||||
public class CacheableSpecification : ICacheableSpecification
|
||||
{
|
||||
public bool IsCacheable(NancyContext context)
|
||||
{
|
||||
if (!RuntimeInfo.IsProduction)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (((DynamicDictionary)context.Request.Query).ContainsKey("h"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (context.Request.Path.StartsWith("/api", StringComparison.CurrentCultureIgnoreCase))
|
||||
{
|
||||
if (context.Request.Path.ContainsIgnoreCase("/MediaCover"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (context.Request.Path.StartsWith("/signalr", StringComparison.CurrentCultureIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (context.Request.Path.EndsWith("index.js"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (context.Request.Path.EndsWith("initialize.js"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (context.Request.Path.StartsWith("/feed", StringComparison.CurrentCultureIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (context.Request.Path.StartsWith("/log", StringComparison.CurrentCultureIgnoreCase) &&
|
||||
context.Request.Path.EndsWith(".txt", StringComparison.CurrentCultureIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (context.Response != null)
|
||||
{
|
||||
if (context.Response.ContentType.Contains("text/html"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
using System.IO;
|
||||
using System.Text;
|
||||
using Nancy;
|
||||
using Nancy.Responses;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Common;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Core.Analytics;
|
||||
|
@ -9,7 +8,9 @@ using NzbDrone.Core.Configuration;
|
|||
|
||||
namespace Prowlarr.Http.Frontend
|
||||
{
|
||||
public class InitializeJsModule : NancyModule
|
||||
[Authorize(Policy = "UI")]
|
||||
[ApiController]
|
||||
public class InitializeJsController : Controller
|
||||
{
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
private readonly IAnalyticsService _analyticsService;
|
||||
|
@ -18,35 +19,21 @@ namespace Prowlarr.Http.Frontend
|
|||
private static string _urlBase;
|
||||
private string _generatedContent;
|
||||
|
||||
public InitializeJsModule(IConfigFileProvider configFileProvider,
|
||||
IAnalyticsService analyticsService)
|
||||
public InitializeJsController(IConfigFileProvider configFileProvider,
|
||||
IAnalyticsService analyticsService)
|
||||
{
|
||||
_configFileProvider = configFileProvider;
|
||||
_analyticsService = analyticsService;
|
||||
|
||||
_apiKey = configFileProvider.ApiKey;
|
||||
_urlBase = configFileProvider.UrlBase;
|
||||
|
||||
Get("/initialize.js", x => Index());
|
||||
}
|
||||
|
||||
private Response Index()
|
||||
[HttpGet("/initialize.js")]
|
||||
public IActionResult Index()
|
||||
{
|
||||
// TODO: Move away from window.Sonarr and prefetch the information returned here when starting the UI
|
||||
return new StreamResponse(GetContentStream, "application/javascript");
|
||||
}
|
||||
|
||||
private Stream GetContentStream()
|
||||
{
|
||||
var text = GetContent();
|
||||
|
||||
var stream = new MemoryStream();
|
||||
var writer = new StreamWriter(stream);
|
||||
writer.Write(text);
|
||||
writer.Flush();
|
||||
stream.Position = 0;
|
||||
|
||||
return stream;
|
||||
// TODO: Move away from window.Prowlarr and prefetch the information returned here when starting the UI
|
||||
return Content(GetContent(), "application/javascript");
|
||||
}
|
||||
|
||||
private string GetContent()
|
|
@ -1,7 +1,6 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using Nancy;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
|
@ -40,13 +39,14 @@ namespace Prowlarr.Http.Frontend.Mappers
|
|||
return stream;
|
||||
}
|
||||
|
||||
public override Response GetResponse(string resourceUrl)
|
||||
/*
|
||||
public override IActionResult GetResponse(string resourceUrl)
|
||||
{
|
||||
var response = base.GetResponse(resourceUrl);
|
||||
response.Headers["X-UA-Compatible"] = "IE=edge";
|
||||
|
||||
return response;
|
||||
}
|
||||
}*/
|
||||
|
||||
protected string GetHtmlText()
|
||||
{
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using Nancy;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Prowlarr.Http.Frontend.Mappers
|
||||
{
|
||||
|
@ -6,6 +6,6 @@ namespace Prowlarr.Http.Frontend.Mappers
|
|||
{
|
||||
string Map(string resourceUrl);
|
||||
bool CanHandle(string resourceUrl);
|
||||
Response GetResponse(string resourceUrl);
|
||||
IActionResult GetResponse(string resourceUrl);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using Nancy;
|
||||
using Nancy.Responses;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
|
@ -13,14 +13,14 @@ namespace Prowlarr.Http.Frontend.Mappers
|
|||
private readonly IDiskProvider _diskProvider;
|
||||
private readonly Logger _logger;
|
||||
private readonly StringComparison _caseSensitive;
|
||||
|
||||
private static readonly NotFoundResponse NotFoundResponse = new NotFoundResponse();
|
||||
private readonly IContentTypeProvider _mimeTypeProvider;
|
||||
|
||||
protected StaticResourceMapperBase(IDiskProvider diskProvider, Logger logger)
|
||||
{
|
||||
_diskProvider = diskProvider;
|
||||
_logger = logger;
|
||||
|
||||
_mimeTypeProvider = new FileExtensionContentTypeProvider();
|
||||
_caseSensitive = RuntimeInfo.IsProduction ? DiskProviderBase.PathStringComparison : StringComparison.OrdinalIgnoreCase;
|
||||
}
|
||||
|
||||
|
@ -28,19 +28,23 @@ namespace Prowlarr.Http.Frontend.Mappers
|
|||
|
||||
public abstract bool CanHandle(string resourceUrl);
|
||||
|
||||
public virtual Response GetResponse(string resourceUrl)
|
||||
public virtual IActionResult GetResponse(string resourceUrl)
|
||||
{
|
||||
var filePath = Map(resourceUrl);
|
||||
|
||||
if (_diskProvider.FileExists(filePath, _caseSensitive))
|
||||
{
|
||||
var response = new StreamResponse(() => GetContentStream(filePath), MimeTypes.GetMimeType(filePath));
|
||||
return new MaterialisingResponse(response);
|
||||
if (!_mimeTypeProvider.TryGetContentType(filePath, out var contentType))
|
||||
{
|
||||
contentType = "application/octet-stream";
|
||||
}
|
||||
|
||||
return new FileStreamResult(GetContentStream(filePath), contentType);
|
||||
}
|
||||
|
||||
_logger.Warn("File {0} not found", filePath);
|
||||
|
||||
return NotFoundResponse;
|
||||
return null;
|
||||
}
|
||||
|
||||
protected virtual Stream GetContentStream(string filePath)
|
||||
|
|
73
src/Prowlarr.Http/Frontend/StaticResourceController.cs
Normal file
73
src/Prowlarr.Http/Frontend/StaticResourceController.cs
Normal file
|
@ -0,0 +1,73 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NLog;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using Prowlarr.Http.Frontend.Mappers;
|
||||
|
||||
namespace Prowlarr.Http.Frontend
|
||||
{
|
||||
[Authorize(Policy="UI")]
|
||||
[ApiController]
|
||||
public class StaticResourceController : Controller
|
||||
{
|
||||
private readonly string _urlBase;
|
||||
private readonly string _loginPath;
|
||||
private readonly IEnumerable<IMapHttpRequestsToDisk> _requestMappers;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public StaticResourceController(IConfigFileProvider configFileProvider,
|
||||
IAppFolderInfo appFolderInfo,
|
||||
IEnumerable<IMapHttpRequestsToDisk> requestMappers,
|
||||
Logger logger)
|
||||
{
|
||||
_urlBase = configFileProvider.UrlBase.Trim('/');
|
||||
_requestMappers = requestMappers;
|
||||
_logger = logger;
|
||||
|
||||
_loginPath = Path.Combine(appFolderInfo.StartUpFolder, configFileProvider.UiFolder, "login.html");
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpGet("login")]
|
||||
public IActionResult LoginPage()
|
||||
{
|
||||
return PhysicalFile(_loginPath, "text/html");
|
||||
}
|
||||
|
||||
[EnableCors("AllowGet")]
|
||||
[AllowAnonymous]
|
||||
[HttpGet("/content/{**path:regex(^(?!api/).*)}")]
|
||||
public IActionResult IndexContent([FromRoute] string path)
|
||||
{
|
||||
return MapResource("Content/" + path);
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
[HttpGet("/{**path:regex(^(?!api/).*)}")]
|
||||
public IActionResult Index([FromRoute] string path)
|
||||
{
|
||||
return MapResource(path);
|
||||
}
|
||||
|
||||
private IActionResult MapResource(string path)
|
||||
{
|
||||
path = "/" + (path ?? "");
|
||||
|
||||
var mapper = _requestMappers.SingleOrDefault(m => m.CanHandle(path));
|
||||
|
||||
if (mapper != null)
|
||||
{
|
||||
return mapper.GetResponse(path) ?? NotFound();
|
||||
}
|
||||
|
||||
_logger.Warn("Couldn't find handler for {0}", path);
|
||||
|
||||
return NotFound();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Nancy;
|
||||
using NLog;
|
||||
using Prowlarr.Http.Frontend.Mappers;
|
||||
|
||||
namespace Prowlarr.Http.Frontend
|
||||
{
|
||||
public class StaticResourceModule : NancyModule
|
||||
{
|
||||
private readonly IEnumerable<IMapHttpRequestsToDisk> _requestMappers;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public StaticResourceModule(IEnumerable<IMapHttpRequestsToDisk> requestMappers, Logger logger)
|
||||
{
|
||||
_requestMappers = requestMappers;
|
||||
_logger = logger;
|
||||
|
||||
Get("/{resource*}", x => Index());
|
||||
Get("/", x => Index());
|
||||
}
|
||||
|
||||
private Response Index()
|
||||
{
|
||||
var path = Request.Url.Path;
|
||||
|
||||
if (
|
||||
string.IsNullOrWhiteSpace(path) ||
|
||||
path.StartsWith("/api", StringComparison.CurrentCultureIgnoreCase) ||
|
||||
path.StartsWith("/signalr", StringComparison.CurrentCultureIgnoreCase))
|
||||
{
|
||||
return new NotFoundResponse();
|
||||
}
|
||||
|
||||
var mapper = _requestMappers.SingleOrDefault(m => m.CanHandle(path));
|
||||
|
||||
if (mapper != null)
|
||||
{
|
||||
return mapper.GetResponse(path);
|
||||
}
|
||||
|
||||
_logger.Warn("Couldn't find handler for {0}", path);
|
||||
|
||||
return new NotFoundResponse();
|
||||
}
|
||||
}
|
||||
}
|
35
src/Prowlarr.Http/Middleware/CacheHeaderMiddleware.cs
Normal file
35
src/Prowlarr.Http/Middleware/CacheHeaderMiddleware.cs
Normal file
|
@ -0,0 +1,35 @@
|
|||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Prowlarr.Http.Extensions;
|
||||
|
||||
namespace Prowlarr.Http.Middleware
|
||||
{
|
||||
public class CacheHeaderMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ICacheableSpecification _cacheableSpecification;
|
||||
|
||||
public CacheHeaderMiddleware(RequestDelegate next, ICacheableSpecification cacheableSpecification)
|
||||
{
|
||||
_next = next;
|
||||
_cacheableSpecification = cacheableSpecification;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
if (context.Request.Method != "OPTIONS")
|
||||
{
|
||||
if (_cacheableSpecification.IsCacheable(context))
|
||||
{
|
||||
context.Response.Headers.EnableCache();
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Response.Headers.DisableCache();
|
||||
}
|
||||
}
|
||||
|
||||
await _next(context);
|
||||
}
|
||||
}
|
||||
}
|
74
src/Prowlarr.Http/Middleware/CacheableSpecification.cs
Normal file
74
src/Prowlarr.Http/Middleware/CacheableSpecification.cs
Normal file
|
@ -0,0 +1,74 @@
|
|||
using System;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
||||
namespace Prowlarr.Http.Middleware
|
||||
{
|
||||
public interface ICacheableSpecification
|
||||
{
|
||||
bool IsCacheable(HttpContext context);
|
||||
}
|
||||
|
||||
public class CacheableSpecification : ICacheableSpecification
|
||||
{
|
||||
public bool IsCacheable(HttpContext context)
|
||||
{
|
||||
if (!RuntimeInfo.IsProduction)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (context.Request.Query.ContainsKey("h"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (context.Request.Path.StartsWithSegments("/api", StringComparison.CurrentCultureIgnoreCase))
|
||||
{
|
||||
if (context.Request.Path.ToString().ContainsIgnoreCase("/MediaCover"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (context.Request.Path.StartsWithSegments("/signalr", StringComparison.CurrentCultureIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (context.Request.Path.Equals("/index.js"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (context.Request.Path.Equals("/initialize.js"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (context.Request.Path.StartsWithSegments("/feed", StringComparison.CurrentCultureIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (context.Request.Path.StartsWithSegments("/log", StringComparison.CurrentCultureIgnoreCase) &&
|
||||
context.Request.Path.ToString().EndsWith(".txt", StringComparison.CurrentCultureIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (context.Response != null)
|
||||
{
|
||||
if (context.Response.ContentType?.Contains("text/html") ?? false || context.Response.StatusCode >= 400)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
43
src/Prowlarr.Http/Middleware/IfModifiedMiddleware.cs
Normal file
43
src/Prowlarr.Http/Middleware/IfModifiedMiddleware.cs
Normal file
|
@ -0,0 +1,43 @@
|
|||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
using Prowlarr.Http.Extensions;
|
||||
|
||||
namespace Prowlarr.Http.Middleware
|
||||
{
|
||||
public class IfModifiedMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ICacheableSpecification _cacheableSpecification;
|
||||
private readonly IContentTypeProvider _mimeTypeProvider;
|
||||
|
||||
public IfModifiedMiddleware(RequestDelegate next, ICacheableSpecification cacheableSpecification)
|
||||
{
|
||||
_next = next;
|
||||
_cacheableSpecification = cacheableSpecification;
|
||||
|
||||
_mimeTypeProvider = new FileExtensionContentTypeProvider();
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
if (_cacheableSpecification.IsCacheable(context) && context.Request.Headers["IfModifiedSince"].Any())
|
||||
{
|
||||
context.Response.StatusCode = 304;
|
||||
context.Response.Headers.EnableCache();
|
||||
|
||||
if (!_mimeTypeProvider.TryGetContentType(context.Request.Path.ToString(), out var mimeType))
|
||||
{
|
||||
mimeType = "application/octet-stream";
|
||||
}
|
||||
|
||||
context.Response.ContentType = mimeType;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await _next(context);
|
||||
}
|
||||
}
|
||||
}
|
92
src/Prowlarr.Http/Middleware/LoggingMiddleware.cs
Normal file
92
src/Prowlarr.Http/Middleware/LoggingMiddleware.cs
Normal file
|
@ -0,0 +1,92 @@
|
|||
using System;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using Prowlarr.Http.ErrorManagement;
|
||||
using Prowlarr.Http.Extensions;
|
||||
|
||||
namespace Prowlarr.Http.Middleware
|
||||
{
|
||||
public class LoggingMiddleware
|
||||
{
|
||||
private static readonly Logger _loggerHttp = LogManager.GetLogger("Http");
|
||||
private static readonly Logger _loggerApi = LogManager.GetLogger("Api");
|
||||
private static int _requestSequenceID;
|
||||
|
||||
private readonly ProwlarrErrorPipeline _errorHandler;
|
||||
private readonly RequestDelegate _next;
|
||||
|
||||
public LoggingMiddleware(RequestDelegate next,
|
||||
ProwlarrErrorPipeline errorHandler)
|
||||
{
|
||||
_next = next;
|
||||
_errorHandler = errorHandler;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
LogStart(context);
|
||||
|
||||
await _next(context);
|
||||
|
||||
LogEnd(context);
|
||||
}
|
||||
|
||||
private void LogStart(HttpContext context)
|
||||
{
|
||||
var id = Interlocked.Increment(ref _requestSequenceID);
|
||||
|
||||
context.Items["ApiRequestSequenceID"] = id;
|
||||
context.Items["ApiRequestStartTime"] = DateTime.UtcNow;
|
||||
|
||||
var reqPath = GetRequestPathAndQuery(context.Request);
|
||||
|
||||
_loggerHttp.Trace("Req: {0} [{1}] {2} (from {3})", id, context.Request.Method, reqPath, GetOrigin(context));
|
||||
}
|
||||
|
||||
private void LogEnd(HttpContext context)
|
||||
{
|
||||
var id = (int)context.Items["ApiRequestSequenceID"];
|
||||
var startTime = (DateTime)context.Items["ApiRequestStartTime"];
|
||||
|
||||
var endTime = DateTime.UtcNow;
|
||||
var duration = endTime - startTime;
|
||||
|
||||
var reqPath = GetRequestPathAndQuery(context.Request);
|
||||
|
||||
_loggerHttp.Trace("Res: {0} [{1}] {2}: {3}.{4} ({5} ms)", id, context.Request.Method, reqPath, context.Response.StatusCode, (HttpStatusCode)context.Response.StatusCode, (int)duration.TotalMilliseconds);
|
||||
|
||||
if (context.Request.IsApiRequest())
|
||||
{
|
||||
_loggerApi.Debug("[{0}] {1}: {2}.{3} ({4} ms)", context.Request.Method, reqPath, context.Response.StatusCode, (HttpStatusCode)context.Response.StatusCode, (int)duration.TotalMilliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetRequestPathAndQuery(HttpRequest request)
|
||||
{
|
||||
if (request.QueryString.Value.IsNotNullOrWhiteSpace() && request.QueryString.Value != "?")
|
||||
{
|
||||
return string.Concat(request.Path, request.QueryString);
|
||||
}
|
||||
else
|
||||
{
|
||||
return request.Path;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetOrigin(HttpContext context)
|
||||
{
|
||||
if (context.Request.Headers["UserAgent"].ToString().IsNullOrWhiteSpace())
|
||||
{
|
||||
return context.GetRemoteIP();
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"{context.GetRemoteIP()} {context.Request.Headers["UserAgent"]}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
29
src/Prowlarr.Http/Middleware/UrlBaseMiddleware.cs
Normal file
29
src/Prowlarr.Http/Middleware/UrlBaseMiddleware.cs
Normal file
|
@ -0,0 +1,29 @@
|
|||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
||||
namespace Prowlarr.Http.Middleware
|
||||
{
|
||||
public class UrlBaseMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly string _urlBase;
|
||||
|
||||
public UrlBaseMiddleware(RequestDelegate next, string urlBase)
|
||||
{
|
||||
_next = next;
|
||||
_urlBase = urlBase;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
if (_urlBase.IsNotNullOrWhiteSpace() && context.Request.PathBase.Value.IsNullOrWhiteSpace())
|
||||
{
|
||||
context.Response.Redirect($"{_urlBase}{context.Request.Path}{context.Request.QueryString}");
|
||||
return;
|
||||
}
|
||||
|
||||
await _next(context);
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue