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