You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Lidarr/src/NzbDrone.Host/WebHost/WebHostController.cs

273 lines
11 KiB

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Lidarr.Api.V1.System;
using Lidarr.Http;
using Lidarr.Http.Authentication;
using Lidarr.Http.ErrorManagement;
using Lidarr.Http.Frontend;
using Lidarr.Http.Middleware;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NLog;
using NLog.Extensions.Logging;
using NzbDrone.Common.Composition;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Exceptions;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Configuration;
using NzbDrone.Host.AccessControl;
using NzbDrone.SignalR;
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
namespace NzbDrone.Host
{
public class WebHostController : IHostController
{
private readonly IContainer _container;
private readonly IRuntimeInfo _runtimeInfo;
private readonly IConfigFileProvider _configFileProvider;
private readonly IFirewallAdapter _firewallAdapter;
private readonly LidarrErrorPipeline _errorHandler;
private readonly Logger _logger;
private IWebHost _host;
public WebHostController(IContainer container,
IRuntimeInfo runtimeInfo,
IConfigFileProvider configFileProvider,
IFirewallAdapter firewallAdapter,
LidarrErrorPipeline errorHandler,
Logger logger)
{
_container = container;
_runtimeInfo = runtimeInfo;
_configFileProvider = configFileProvider;
_firewallAdapter = firewallAdapter;
_errorHandler = errorHandler;
_logger = logger;
}
public void StartServer()
{
if (OsInfo.IsWindows && _runtimeInfo.IsAdmin)
{
_firewallAdapter.MakeAccessible();
}
var bindAddress = _configFileProvider.BindAddress;
var enableSsl = _configFileProvider.EnableSsl;
var sslCertPath = _configFileProvider.SslCertPath;
var urls = new List<string>();
urls.Add(BuildUrl("http", bindAddress, _configFileProvider.Port));
if (enableSsl && sslCertPath.IsNotNullOrWhiteSpace())
{
urls.Add(BuildUrl("https", bindAddress, _configFileProvider.SslPort));
}
_host = new WebHostBuilder()
.UseUrls(urls.ToArray())
.UseKestrel(options =>
{
if (enableSsl && sslCertPath.IsNotNullOrWhiteSpace())
{
options.ConfigureHttpsDefaults(configureOptions =>
{
X509Certificate2 certificate;
try
{
certificate = new X509Certificate2(sslCertPath, _configFileProvider.SslCertPassword, X509KeyStorageFlags.DefaultKeySet);
}
catch (CryptographicException ex)
{
if (ex.HResult == 0x2 || ex.HResult == 0x2006D080)
{
throw new LidarrStartupException(ex, $"The SSL certificate file {sslCertPath} does not exist");
}
throw new LidarrStartupException(ex);
}
configureOptions.ServerCertificate = certificate;
});
}
})
.ConfigureKestrel(serverOptions =>
{
serverOptions.AllowSynchronousIO = true;
serverOptions.Limits.MaxRequestBodySize = null;
})
.ConfigureLogging(logging =>
{
logging.AddProvider(new NLogLoggerProvider());
logging.SetMinimumLevel(LogLevel.Warning);
})
.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<LidarrErrorPipeline>());
services.AddSingleton(_container.Resolve<ICacheableSpecification>());
// Used in authentication
services.AddSingleton(_container.Resolve<IAuthenticationService>());
services.AddRouting(options => options.LowercaseUrls = true);
services.AddResponseCompression();
services.AddCors(options =>
{
options.AddPolicy(VersionedApiControllerAttribute.API_CORS_POLICY,
builder =>
builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
options.AddPolicy("AllowGet",
builder =>
builder.AllowAnyOrigin()
.WithMethods("GET", "OPTIONS")
.AllowAnyHeader());
});
services
.AddControllers(options =>
{
options.ReturnHttpNotAcceptable = true;
})
.AddApplicationPart(typeof(SystemController).Assembly)
.AddApplicationPart(typeof(StaticResourceController).Assembly)
.AddJsonOptions(options =>
{
STJson.ApplySerializerSettings(options.JsonSerializerOptions);
});
services
.AddSignalR()
.AddJsonProtocol(options =>
{
options.PayloadSerializerOptions = STJson.GetSerializerSettings();
});
services.AddAuthorization(options =>
{
options.AddPolicy("UI", policy =>
{
policy.AuthenticationSchemes.Add(_configFileProvider.AuthenticationMethod.ToString());
policy.RequireAuthenticatedUser();
});
options.AddPolicy("SignalR", policy =>
{
policy.AuthenticationSchemes.Add("SignalR");
policy.RequireAuthenticatedUser();
});
// Require auth on everything except those marked [AllowAnonymous]
options.DefaultPolicy = new AuthorizationPolicyBuilder("API")
.RequireAuthenticatedUser()
.Build();
});
services.AddAppAuthentication(_configFileProvider);
})
.Configure(app =>
{
app.UseMiddleware<LoggingMiddleware>();
app.UsePathBase(new PathString(_configFileProvider.UrlBase));
app.UseExceptionHandler(new ExceptionHandlerOptions
{
AllowStatusCode404Response = true,
ExceptionHandler = _errorHandler.HandleException
});
app.UseRouting();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.UseResponseCompression();
app.Properties["host.AppName"] = BuildInfo.AppName;
app.UseMiddleware<VersionMiddleware>();
app.UseMiddleware<UrlBaseMiddleware>(_configFileProvider.UrlBase);
app.UseMiddleware<CacheHeaderMiddleware>();
app.UseMiddleware<IfModifiedMiddleware>();
app.Use((context, next) =>
{
if (context.Request.Path.StartsWithSegments("/api/v1/command", StringComparison.CurrentCultureIgnoreCase))
{
context.Request.EnableBuffering();
}
return next();
});
app.UseWebSockets();
app.UseEndpoints(x =>
{
x.MapHub<MessageHub>("/signalr/messages").RequireAuthorization("SignalR");
x.MapControllers();
});
// This is a side effect of haing multiple IoC containers, TinyIoC and whatever
// Kestrel/SignalR is using. Ideally we'd have one IoC container, but that's non-trivial with TinyIoC
// TODO: Use a single IoC container if supported for TinyIoC or if we switch to another system (ie Autofac).
_container.Register(app.ApplicationServices);
_container.Register(app.ApplicationServices.GetService<IHubContext<MessageHub>>());
_container.Register(app.ApplicationServices.GetService<IActionDescriptorCollectionProvider>());
_container.Register(app.ApplicationServices.GetService<EndpointDataSource>());
_container.Register(app.ApplicationServices.GetService<DfaGraphWriter>());
})
.UseContentRoot(Directory.GetCurrentDirectory())
.Build();
_logger.Info("Listening on the following URLs:");
foreach (var url in urls)
{
_logger.Info(" {0}", url);
}
_host.Start();
}
public async void StopServer()
{
_logger.Info("Attempting to stop OWIN host");
await _host.StopAsync(TimeSpan.FromSeconds(5));
_host.Dispose();
_host = null;
_logger.Info("Host has stopped");
}
private string BuildUrl(string scheme, string bindAddress, int port)
{
return $"{scheme}://{bindAddress}:{port}";
}
}
}