using System; using System.Collections.Generic; using System.IO; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.OpenApi.Models; using NLog.Extensions.Logging; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Instrumentation; using NzbDrone.Common.Processes; using NzbDrone.Common.Serializer; using NzbDrone.Core.Configuration; using NzbDrone.Core.Datastore; using NzbDrone.Core.Instrumentation; using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Messaging.Events; using NzbDrone.Host.AccessControl; using NzbDrone.SignalR; using Radarr.Api.V3.System; using Radarr.Http; using Radarr.Http.Authentication; using Radarr.Http.ErrorManagement; using Radarr.Http.Frontend; using Radarr.Http.Middleware; using LogLevel = Microsoft.Extensions.Logging.LogLevel; namespace NzbDrone.Host { public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { services.AddLogging(b => { b.ClearProviders(); b.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace); b.AddFilter("Microsoft.AspNetCore", Microsoft.Extensions.Logging.LogLevel.Warning); b.AddFilter("Radarr.Http.Authentication", LogLevel.Information); b.AddFilter("Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager", LogLevel.Error); b.AddNLog(); }); services.Configure(options => { options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; options.KnownNetworks.Clear(); options.KnownProxies.Clear(); }); services.Configure(options => { options.Secure = CookieSecurePolicy.Always; }); services.AddRouting(options => options.LowercaseUrls = true); services.AddResponseCompression(options => options.EnableForHttps = true); 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); }) .AddControllersAsServices(); services.AddSwaggerGen(c => { c.SwaggerDoc("v3", new OpenApiInfo { Version = "3.0.0", Title = "Radarr", Description = "Radarr API docs", License = new OpenApiLicense { Name = "GPL-3.0", Url = new Uri("https://github.com/Radarr/Radarr/blob/develop/LICENSE") } }); var apiKeyHeader = new OpenApiSecurityScheme { Name = "X-Api-Key", Type = SecuritySchemeType.ApiKey, Scheme = "apiKey", Description = "Apikey passed as header", In = ParameterLocation.Header, Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "X-Api-Key" }, }; c.AddSecurityDefinition("X-Api-Key", apiKeyHeader); c.AddSecurityRequirement(new OpenApiSecurityRequirement { { apiKeyHeader, Array.Empty() } }); var apikeyQuery = new OpenApiSecurityScheme { Name = "apikey", Type = SecuritySchemeType.ApiKey, Scheme = "apiKey", Description = "Apikey passed as header", In = ParameterLocation.Query, Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "apikey" }, }; c.AddServer(new OpenApiServer { Url = "{protocol}://{hostpath}", Variables = new Dictionary { { "protocol", new OpenApiServerVariable { Default = "http", Enum = new List { "http", "https" } } }, { "hostpath", new OpenApiServerVariable { Default = "localhost:7878" } } } }); c.AddSecurityDefinition("apikey", apikeyQuery); c.AddSecurityRequirement(new OpenApiSecurityRequirement { { apikeyQuery, Array.Empty() } }); }); services.ConfigureSwaggerGen(options => { options.CustomSchemaIds(x => x.FullName); }); services .AddSignalR() .AddJsonProtocol(options => { options.PayloadSerializerOptions = STJson.GetSerializerSettings(); }); services.AddDataProtection() .PersistKeysToFileSystem(new DirectoryInfo(Configuration["dataProtectionFolder"])); services.AddAuthorization(options => { options.AddPolicy("SignalR", policy => { policy.AuthenticationSchemes.Add("SignalR"); policy.Requirements.Add(new ApiKeyRequirement()); }); // Require auth on everything except those marked [AllowAnonymous] options.FallbackPolicy = new AuthorizationPolicyBuilder("API") .AddRequirements(new ApiKeyRequirement()) .Build(); }); services.AddAppAuthentication(); services.PostConfigure(options => { var builtInFactory = options.InvalidModelStateResponseFactory; options.InvalidModelStateResponseFactory = context => { var loggerFactory = context.HttpContext.RequestServices.GetRequiredService(); var logger = loggerFactory.CreateLogger(context.ActionDescriptor.DisplayName); logger.LogError(STJson.ToJson(context.ModelState)); return builtInFactory(context); }; }); } public void Configure(IApplicationBuilder app, IStartupContext startupContext, Lazy mainDatabaseFactory, Lazy logDatabaseFactory, DatabaseTarget dbTarget, ISingleInstancePolicy singleInstancePolicy, InitializeLogger initializeLogger, ReconfigureLogging reconfigureLogging, IAppFolderFactory appFolderFactory, IProvidePidFile pidFileProvider, IConfigFileProvider configFileProvider, IRuntimeInfo runtimeInfo, IFirewallAdapter firewallAdapter, IEventAggregator eventAggregator, RadarrErrorPipeline errorHandler) { initializeLogger.Initialize(); appFolderFactory.Register(); pidFileProvider.Write(); configFileProvider.EnsureDefaultConfigFile(); reconfigureLogging.Reconfigure(); EnsureSingleInstance(false, startupContext, singleInstancePolicy); // instantiate the databases to initialize/migrate them _ = mainDatabaseFactory.Value; _ = logDatabaseFactory.Value; dbTarget.Register(); if (OsInfo.IsNotWindows) { Console.CancelKeyPress += (sender, eventArgs) => NLog.LogManager.Configuration = null; } eventAggregator.PublishEvent(new ApplicationStartingEvent()); if (OsInfo.IsWindows && runtimeInfo.IsAdmin) { firewallAdapter.MakeAccessible(); } app.UseForwardedHeaders(); app.UseMiddleware(); app.UsePathBase(new PathString(configFileProvider.UrlBase)); app.UseExceptionHandler(new ExceptionHandlerOptions { AllowStatusCode404Response = true, ExceptionHandler = errorHandler.HandleException }); app.UseCookiePolicy(); app.UseRouting(); app.UseCors(); app.UseAuthentication(); app.UseAuthorization(); app.UseResponseCompression(); app.Properties["host.AppName"] = BuildInfo.AppName; app.UseMiddleware(); app.UseMiddleware(configFileProvider.UrlBase); app.UseMiddleware(); app.UseMiddleware(); app.UseMiddleware(); app.UseMiddleware(new List { "/api/v3/command" }); app.UseWebSockets(); // Enable middleware to serve generated Swagger as a JSON endpoint. if (BuildInfo.IsDebug) { app.UseSwagger(c => { c.RouteTemplate = "docs/{documentName}/openapi.json"; }); } app.UseEndpoints(x => { x.MapHub("/signalr/messages").RequireAuthorization("SignalR"); x.MapControllers(); }); } private void EnsureSingleInstance(bool isService, IStartupContext startupContext, ISingleInstancePolicy instancePolicy) { if (startupContext.Flags.Contains(StartupContext.NO_SINGLE_INSTANCE_CHECK)) { return; } if (startupContext.Flags.Contains(StartupContext.TERMINATE)) { instancePolicy.KillAllOtherInstance(); } else if (startupContext.Args.ContainsKey(StartupContext.APPDATA)) { instancePolicy.WarnIfAlreadyRunning(); } else if (isService) { instancePolicy.KillAllOtherInstance(); } else { instancePolicy.PreventStartIfAlreadyRunning(); } } } }