(cherry picked from commit 58ddbcd77e17ef95ecfad4b746084ee9326116f3)pull/26/head
parent
7d494f9743
commit
dbdc527f2e
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +1,14 @@
|
|||||||
using NzbDrone.Core.Applications;
|
using NzbDrone.Core.Applications;
|
||||||
|
using Prowlarr.Http;
|
||||||
|
|
||||||
namespace Prowlarr.Api.V1.Application
|
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 static readonly ApplicationResourceMapper ResourceMapper = new ApplicationResourceMapper();
|
||||||
|
|
||||||
public ApplicationModule(ApplicationFactory applicationsFactory)
|
public ApplicationController(ApplicationFactory applicationsFactory)
|
||||||
: base(applicationsFactory, "applications", ResourceMapper)
|
: base(applicationsFactory, "applications", ResourceMapper)
|
||||||
{
|
{
|
||||||
}
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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 NzbDrone.Core.Configuration;
|
||||||
|
using Prowlarr.Http;
|
||||||
|
|
||||||
namespace Prowlarr.Api.V1.Config
|
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)
|
: base(configService)
|
||||||
{
|
{
|
||||||
}
|
}
|
@ -1,12 +1,14 @@
|
|||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
|
using Prowlarr.Http;
|
||||||
using Prowlarr.Http.Validation;
|
using Prowlarr.Http.Validation;
|
||||||
|
|
||||||
namespace Prowlarr.Api.V1.Config
|
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)
|
: base(configService)
|
||||||
{
|
{
|
||||||
SharedValidator.RuleFor(c => c.MinimumAge)
|
SharedValidator.RuleFor(c => c.MinimumAge)
|
@ -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 NzbDrone.Core.Configuration;
|
||||||
|
using Prowlarr.Http;
|
||||||
|
|
||||||
namespace Prowlarr.Api.V1.Config
|
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)
|
: base(configService)
|
||||||
{
|
{
|
||||||
}
|
}
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 System.Collections.Generic;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using NzbDrone.Core.Datastore.Events;
|
using NzbDrone.Core.Datastore.Events;
|
||||||
using NzbDrone.Core.HealthCheck;
|
using NzbDrone.Core.HealthCheck;
|
||||||
using NzbDrone.Core.Messaging.Events;
|
using NzbDrone.Core.Messaging.Events;
|
||||||
using NzbDrone.SignalR;
|
using NzbDrone.SignalR;
|
||||||
using Prowlarr.Http;
|
using Prowlarr.Http;
|
||||||
|
using Prowlarr.Http.REST;
|
||||||
|
|
||||||
namespace Prowlarr.Api.V1.Health
|
namespace Prowlarr.Api.V1.Health
|
||||||
{
|
{
|
||||||
public class HealthModule : ProwlarrRestModuleWithSignalR<HealthResource, HealthCheck>,
|
[V1ApiController]
|
||||||
|
public class HealthController : RestControllerWithSignalR<HealthResource, HealthCheck>,
|
||||||
IHandle<HealthCheckCompleteEvent>
|
IHandle<HealthCheckCompleteEvent>
|
||||||
{
|
{
|
||||||
private readonly IHealthCheckService _healthCheckService;
|
private readonly IHealthCheckService _healthCheckService;
|
||||||
|
|
||||||
public HealthModule(IBroadcastSignalRMessage signalRBroadcaster, IHealthCheckService healthCheckService)
|
public HealthController(IBroadcastSignalRMessage signalRBroadcaster, IHealthCheckService healthCheckService)
|
||||||
: base(signalRBroadcaster)
|
: base(signalRBroadcaster)
|
||||||
{
|
{
|
||||||
_healthCheckService = healthCheckService;
|
_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();
|
return _healthCheckService.Results().ToResource();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[NonAction]
|
||||||
public void Handle(HealthCheckCompleteEvent message)
|
public void Handle(HealthCheckCompleteEvent message)
|
||||||
{
|
{
|
||||||
BroadcastResourceChange(ModelAction.Sync);
|
BroadcastResourceChange(ModelAction.Sync);
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,19 +1,17 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
using Prowlarr.Http;
|
using Prowlarr.Http;
|
||||||
|
|
||||||
namespace Prowlarr.Api.V1.Indexers
|
namespace Prowlarr.Api.V1.Indexers
|
||||||
{
|
{
|
||||||
public class IndexerFlagModule : ProwlarrRestModule<IndexerFlagResource>
|
[V1ApiController]
|
||||||
|
public class IndexerFlagController : Controller
|
||||||
{
|
{
|
||||||
public IndexerFlagModule()
|
[HttpGet]
|
||||||
{
|
public List<IndexerFlagResource> GetAll()
|
||||||
GetResourceAll = GetAll;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<IndexerFlagResource> GetAll()
|
|
||||||
{
|
{
|
||||||
return Enum.GetValues(typeof(IndexerFlags)).Cast<IndexerFlags>().Select(f => new IndexerFlagResource
|
return Enum.GetValues(typeof(IndexerFlags)).Cast<IndexerFlags>().Select(f => new IndexerFlagResource
|
||||||
{
|
{
|
@ -1,28 +1,21 @@
|
|||||||
using System;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using NzbDrone.Core.IndexerStats;
|
using NzbDrone.Core.IndexerStats;
|
||||||
using Prowlarr.Http;
|
using Prowlarr.Http;
|
||||||
|
|
||||||
namespace Prowlarr.Api.V1.Indexers
|
namespace Prowlarr.Api.V1.Indexers
|
||||||
{
|
{
|
||||||
public class IndexerStatsModule : ProwlarrRestModule<IndexerStatsResource>
|
[V1ApiController]
|
||||||
|
public class IndexerStatsController : Controller
|
||||||
{
|
{
|
||||||
private readonly IIndexerStatisticsService _indexerStatisticsService;
|
private readonly IIndexerStatisticsService _indexerStatisticsService;
|
||||||
|
|
||||||
public IndexerStatsModule(IIndexerStatisticsService indexerStatisticsService)
|
public IndexerStatsController(IIndexerStatisticsService indexerStatisticsService)
|
||||||
{
|
{
|
||||||
_indexerStatisticsService = indexerStatisticsService;
|
_indexerStatisticsService = indexerStatisticsService;
|
||||||
|
|
||||||
Get("/", x =>
|
|
||||||
{
|
|
||||||
return GetAll();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private IndexerStatsResource GetAll()
|
[HttpGet]
|
||||||
|
public IndexerStatsResource GetAll()
|
||||||
{
|
{
|
||||||
var indexerResource = new IndexerStatsResource
|
var indexerResource = new IndexerStatsResource
|
||||||
{
|
{
|
@ -1,31 +1,40 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using NzbDrone.Core.Datastore.Events;
|
using NzbDrone.Core.Datastore.Events;
|
||||||
using NzbDrone.Core.Indexers;
|
using NzbDrone.Core.Indexers;
|
||||||
using NzbDrone.Core.Messaging.Events;
|
using NzbDrone.Core.Messaging.Events;
|
||||||
using NzbDrone.Core.ThingiProvider.Events;
|
using NzbDrone.Core.ThingiProvider.Events;
|
||||||
using NzbDrone.SignalR;
|
using NzbDrone.SignalR;
|
||||||
using Prowlarr.Http;
|
using Prowlarr.Http;
|
||||||
|
using Prowlarr.Http.REST;
|
||||||
|
using NotImplementedException = System.NotImplementedException;
|
||||||
|
|
||||||
namespace Prowlarr.Api.V1.Indexers
|
namespace Prowlarr.Api.V1.Indexers
|
||||||
{
|
{
|
||||||
public class IndexerStatusModule : ProwlarrRestModuleWithSignalR<IndexerStatusResource, IndexerStatus>,
|
[V1ApiController]
|
||||||
|
public class IndexerStatusController : RestControllerWithSignalR<IndexerStatusResource, IndexerStatus>,
|
||||||
IHandle<ProviderStatusChangedEvent<IIndexer>>
|
IHandle<ProviderStatusChangedEvent<IIndexer>>
|
||||||
{
|
{
|
||||||
private readonly IIndexerStatusService _indexerStatusService;
|
private readonly IIndexerStatusService _indexerStatusService;
|
||||||
|
|
||||||
public IndexerStatusModule(IBroadcastSignalRMessage signalRBroadcaster, IIndexerStatusService indexerStatusService)
|
public IndexerStatusController(IBroadcastSignalRMessage signalRBroadcaster, IIndexerStatusService indexerStatusService)
|
||||||
: base(signalRBroadcaster)
|
: base(signalRBroadcaster)
|
||||||
{
|
{
|
||||||
_indexerStatusService = indexerStatusService;
|
_indexerStatusService = indexerStatusService;
|
||||||
|
}
|
||||||
|
|
||||||
GetResourceAll = GetAll;
|
public override IndexerStatusResource GetResourceById(int id)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<IndexerStatusResource> GetAll()
|
[HttpGet]
|
||||||
|
public List<IndexerStatusResource> GetAll()
|
||||||
{
|
{
|
||||||
return _indexerStatusService.GetBlockedProviders().ToResource();
|
return _indexerStatusService.GetBlockedProviders().ToResource();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[NonAction]
|
||||||
public void Handle(ProviderStatusChangedEvent<IIndexer> message)
|
public void Handle(ProviderStatusChangedEvent<IIndexer> message)
|
||||||
{
|
{
|
||||||
BroadcastResourceChange(ModelAction.Sync);
|
BroadcastResourceChange(ModelAction.Sync);
|
@ -1,21 +1,22 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using NzbDrone.Core.Localization;
|
using NzbDrone.Core.Localization;
|
||||||
using Prowlarr.Http;
|
using Prowlarr.Http;
|
||||||
|
|
||||||
namespace Prowlarr.Api.V1.Localization
|
namespace Prowlarr.Api.V1.Localization
|
||||||
{
|
{
|
||||||
public class LocalizationModule : ProwlarrRestModule<LocalizationResource>
|
[V1ApiController]
|
||||||
|
public class LocalizationController : Controller
|
||||||
{
|
{
|
||||||
private readonly ILocalizationService _localizationService;
|
private readonly ILocalizationService _localizationService;
|
||||||
|
|
||||||
public LocalizationModule(ILocalizationService localizationService)
|
public LocalizationController(ILocalizationService localizationService)
|
||||||
{
|
{
|
||||||
_localizationService = 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
|
// We don't want camel case for transation strings, create new serializer settings
|
||||||
var serializerSettings = new JsonSerializerSettings
|
var serializerSettings = new JsonSerializerSettings
|
@ -1,12 +1,14 @@
|
|||||||
using NzbDrone.Core.Notifications;
|
using NzbDrone.Core.Notifications;
|
||||||
|
using Prowlarr.Http;
|
||||||
|
|
||||||
namespace Prowlarr.Api.V1.Notifications
|
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 static readonly NotificationResourceMapper ResourceMapper = new NotificationResourceMapper();
|
||||||
|
|
||||||
public NotificationModule(NotificationFactory notificationFactory)
|
public NotificationController(NotificationFactory notificationFactory)
|
||||||
: base(notificationFactory, "notification", ResourceMapper)
|
: base(notificationFactory, "notification", ResourceMapper)
|
||||||
{
|
{
|
||||||
}
|
}
|
@ -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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using Nancy.ModelBinding;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Core.Exceptions;
|
using NzbDrone.Core.Exceptions;
|
||||||
using NzbDrone.Core.IndexerSearch;
|
using NzbDrone.Core.IndexerSearch;
|
||||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
using Prowlarr.Http;
|
using Prowlarr.Http;
|
||||||
using Prowlarr.Http.Extensions;
|
using Prowlarr.Http.Extensions;
|
||||||
|
|
||||||
namespace Prowlarr.Api.V1.Search
|
namespace Prowlarr.Api.V1.Search
|
||||||
{
|
{
|
||||||
public class SearchModule : ProwlarrRestModule<SearchResource>
|
[V1ApiController]
|
||||||
|
public class SearchController : Controller
|
||||||
{
|
{
|
||||||
private readonly ISearchForNzb _nzbSearhService;
|
private readonly ISearchForNzb _nzbSearhService;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
|
|
||||||
public SearchModule(ISearchForNzb nzbSearhService, Logger logger)
|
public SearchController(ISearchForNzb nzbSearhService, Logger logger)
|
||||||
{
|
{
|
||||||
_nzbSearhService = nzbSearhService;
|
_nzbSearhService = nzbSearhService;
|
||||||
_logger = logger;
|
_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 (query.IsNotNullOrWhiteSpace())
|
||||||
|
|
||||||
if (request.Query.IsNotNullOrWhiteSpace())
|
|
||||||
{
|
{
|
||||||
var indexerIds = request.IndexerIds ?? new List<int>();
|
if (indexerIds.Any())
|
||||||
var categories = request.Categories ?? new List<int>();
|
|
||||||
|
|
||||||
if (indexerIds.Count > 0)
|
|
||||||
{
|
{
|
||||||
return GetSearchReleases(request.Query, indexerIds, categories);
|
return GetSearchReleases(query, indexerIds, categories);
|
||||||
}
|
}
|
||||||
else
|
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,54 +1,59 @@
|
|||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using NzbDrone.Core.Datastore.Events;
|
using NzbDrone.Core.Datastore.Events;
|
||||||
using NzbDrone.Core.Messaging.Events;
|
using NzbDrone.Core.Messaging.Events;
|
||||||
using NzbDrone.Core.Tags;
|
using NzbDrone.Core.Tags;
|
||||||
|
using NzbDrone.Http.REST.Attributes;
|
||||||
using NzbDrone.SignalR;
|
using NzbDrone.SignalR;
|
||||||
using Prowlarr.Http;
|
using Prowlarr.Http;
|
||||||
|
using Prowlarr.Http.REST;
|
||||||
|
|
||||||
namespace Prowlarr.Api.V1.Tags
|
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;
|
private readonly ITagService _tagService;
|
||||||
|
|
||||||
public TagModule(IBroadcastSignalRMessage signalRBroadcaster,
|
public TagController(IBroadcastSignalRMessage signalRBroadcaster,
|
||||||
ITagService tagService)
|
ITagService tagService)
|
||||||
: base(signalRBroadcaster)
|
: base(signalRBroadcaster)
|
||||||
{
|
{
|
||||||
_tagService = tagService;
|
_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();
|
return _tagService.GetTag(id).ToResource();
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<TagResource> GetAll()
|
[HttpGet]
|
||||||
|
public List<TagResource> GetAll()
|
||||||
{
|
{
|
||||||
return _tagService.All().ToResource();
|
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());
|
_tagService.Update(resource.ToModel());
|
||||||
|
return Accepted(resource.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DeleteTag(int id)
|
[RestDeleteById]
|
||||||
|
public void DeleteTag(int id)
|
||||||
{
|
{
|
||||||
_tagService.Delete(id);
|
_tagService.Delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[NonAction]
|
||||||
public void Handle(TagsUpdatedEvent message)
|
public void Handle(TagsUpdatedEvent message)
|
||||||
{
|
{
|
||||||
BroadcastResourceChange(ModelAction.Sync);
|
BroadcastResourceChange(ModelAction.Sync);
|
@ -1,28 +1,28 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using NzbDrone.Core.Tags;
|
using NzbDrone.Core.Tags;
|
||||||
using Prowlarr.Http;
|
using Prowlarr.Http;
|
||||||
|
using Prowlarr.Http.REST;
|
||||||
|
|
||||||
namespace Prowlarr.Api.V1.Tags
|
namespace Prowlarr.Api.V1.Tags
|
||||||
{
|
{
|
||||||
public class TagDetailsModule : ProwlarrRestModule<TagDetailsResource>
|
[V1ApiController("tag/detail")]
|
||||||
|
public class TagDetailsController : RestController<TagDetailsResource>
|
||||||
{
|
{
|
||||||
private readonly ITagService _tagService;
|
private readonly ITagService _tagService;
|
||||||
|
|
||||||
public TagDetailsModule(ITagService tagService)
|
public TagDetailsController(ITagService tagService)
|
||||||
: base("/tag/detail")
|
|
||||||
{
|
{
|
||||||
_tagService = tagService;
|
_tagService = tagService;
|
||||||
|
|
||||||
GetResourceById = GetById;
|
|
||||||
GetResourceAll = GetAll;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private TagDetailsResource GetById(int id)
|
public override TagDetailsResource GetResourceById(int id)
|
||||||
{
|
{
|
||||||
return _tagService.Details(id).ToResource();
|
return _tagService.Details(id).ToResource();
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<TagDetailsResource> GetAll()
|
[HttpGet]
|
||||||
|
public List<TagDetailsResource> GetAll()
|
||||||
{
|
{
|
||||||
return _tagService.Details().ToResource();
|
return _tagService.Details().ToResource();
|
||||||
}
|
}
|
@ -1,22 +1,24 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Core.Update;
|
using NzbDrone.Core.Update;
|
||||||
using Prowlarr.Http;
|
using Prowlarr.Http;
|
||||||
|
|
||||||
namespace Prowlarr.Api.V1.Update
|
namespace Prowlarr.Api.V1.Update
|
||||||
{
|
{
|
||||||
public class UpdateModule : ProwlarrRestModule<UpdateResource>
|
[V1ApiController]
|
||||||
|
public class UpdateController : Controller
|
||||||
{
|
{
|
||||||
private readonly IRecentUpdateProvider _recentUpdateProvider;
|
private readonly IRecentUpdateProvider _recentUpdateProvider;
|
||||||
|
|
||||||
public UpdateModule(IRecentUpdateProvider recentUpdateProvider)
|
public UpdateController(IRecentUpdateProvider recentUpdateProvider)
|
||||||
{
|
{
|
||||||
_recentUpdateProvider = recentUpdateProvider;
|
_recentUpdateProvider = recentUpdateProvider;
|
||||||
GetResourceAll = GetRecentUpdates;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<UpdateResource> GetRecentUpdates()
|
[HttpGet]
|
||||||
|
public List<UpdateResource> GetRecentUpdates()
|
||||||
{
|
{
|
||||||
var resources = _recentUpdateProvider.GetRecentUpdatePackages()
|
var resources = _recentUpdateProvider.GetRecentUpdatePackages()
|
||||||
.OrderByDescending(u => u.Version)
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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 + "/");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,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,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,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,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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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"]}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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…
Reference in new issue