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.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.Http.Authentication; using NzbDrone.SignalR; using Readarr.Api.V1.System; using Readarr.Http; using Readarr.Http.Authentication; using Readarr.Http.ErrorManagement; using Readarr.Http.Frontend; using Readarr.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("Readarr.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.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("v1", new OpenApiInfo { Version = "1.0.0", Title = "Readarr", Description = "Readarr API docs", License = new OpenApiLicense { Name = "GPL-3.0", Url = new Uri("https://github.com/Readarr/Readarr/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:8787" } } } }); c.AddSecurityDefinition("apikey", apikeyQuery); c.AddSecurityRequirement(new OpenApiSecurityRequirement { { apikeyQuery, Array.Empty() } }); }); services .AddSignalR() .AddJsonProtocol(options => { options.PayloadSerializerOptions = STJson.GetSerializerSettings(); }); services.AddDataProtection() .PersistKeysToFileSystem(new DirectoryInfo(Configuration["dataProtectionFolder"])); services.AddSingleton(); services.AddAuthorization(options => { 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(); } public void Configure(IApplicationBuilder app, IStartupContext startupContext, Lazy mainDatabaseFactory, Lazy logDatabaseFactory, Lazy cacheDatabaseFactory, DatabaseTarget dbTarget, ISingleInstancePolicy singleInstancePolicy, InitializeLogger initializeLogger, ReconfigureLogging reconfigureLogging, IAppFolderFactory appFolderFactory, IProvidePidFile pidFileProvider, IConfigFileProvider configFileProvider, IRuntimeInfo runtimeInfo, IFirewallAdapter firewallAdapter, IEventAggregator eventAggregator, ReadarrErrorPipeline 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; _ = cacheDatabaseFactory.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.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/v1/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(); } } } }