Kavita/API/Startup.cs
Joe Milazzo 3659bf8a7c
Release Shakeout 4 (#1600)
* Fixed a bug where bulk selection on series detail wouldn't allow you to select the whole card, only the checkbox.

* Refactored the implementation of MarkChaptersAsRead to streamline it.

* Fixed a bug where volume cards weren't properly updating their read state based on events from backend.

* Added [ScannerService] to more loggers

* Fixed invite user flow

* Fixed broken edit user flow

* Fixed calling device service on unauthenticated screens causing redirection

* Fixed reset password via email not working when success message was sent back

* Fixed broken white theme on book reader

* Small tweaks to white theme

* More fixes

* Adjusted AutomaticRetries

* When an auth change occures, reset the devices in service so devices don't leak between profiles

* Fixed a bug where sendTo for series wasn't properly taking into account specials (on series detail page)

* Drop down how long series detail caches for to prevent signalr updates from refreshing the UI

* Close submenus when hovering over other items, not just other submenus

* Fixed a bug where scanning for themes would always report theme didn't exist

* Added Hangfire.db back in

* Fixed a bad build
2022-10-21 17:21:43 -07:00

358 lines
13 KiB
C#

using System;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
using API.Data;
using API.Entities;
using API.Entities.Enums;
using API.Extensions;
using API.Logging;
using API.Middleware;
using API.Services;
using API.Services.HostedServices;
using API.Services.Tasks;
using API.SignalR;
using Hangfire;
using Hangfire.MemoryStorage;
using Hangfire.Storage.SQLite;
using Kavita.Common;
using Kavita.Common.EnvironmentInfo;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.ResponseCompression;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
using Microsoft.OpenApi.Models;
using Serilog;
using TaskScheduler = API.Services.TaskScheduler;
namespace API;
public class Startup
{
private readonly IConfiguration _config;
private readonly IWebHostEnvironment _env;
public Startup(IConfiguration config, IWebHostEnvironment env)
{
_config = config;
_env = env;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddApplicationServices(_config, _env);
services.AddControllers(options =>
{
options.CacheProfiles.Add("Images",
new CacheProfile()
{
Duration = 60,
Location = ResponseCacheLocation.None,
NoStore = false
});
options.CacheProfiles.Add("Hour",
new CacheProfile()
{
Duration = 60 * 60,
Location = ResponseCacheLocation.None,
NoStore = false
});
options.CacheProfiles.Add("10Minute",
new CacheProfile()
{
Duration = 60 * 10,
Location = ResponseCacheLocation.None,
NoStore = false
});
options.CacheProfiles.Add("5Minute",
new CacheProfile()
{
Duration = 60 * 5,
Location = ResponseCacheLocation.None,
});
// Instant is a very quick cache, because we can't bust based on the query params, but rather body
options.CacheProfiles.Add("Instant",
new CacheProfile()
{
Duration = 30,
Location = ResponseCacheLocation.None,
});
});
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.All;
foreach(var proxy in _config.GetSection("KnownProxies").AsEnumerable().Where(c => c.Value != null)) {
options.KnownProxies.Add(IPAddress.Parse(proxy.Value));
}
});
services.AddCors();
services.AddIdentityServices(_config);
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo()
{
Description = "Kavita provides a set of APIs that are authenticated by JWT. JWT token can be copied from local storage.",
Title = "Kavita API",
Version = "v1",
});
var filePath = Path.Combine(AppContext.BaseDirectory, "API.xml");
c.IncludeXmlComments(filePath, true);
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme {
In = ParameterLocation.Header,
Description = "Please insert JWT with Bearer into field",
Name = "Authorization",
Type = SecuritySchemeType.ApiKey
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement {
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
Array.Empty<string>()
}
});
c.AddServer(new OpenApiServer()
{
Description = "Custom Url",
Url = "/"
});
c.AddServer(new OpenApiServer()
{
Description = "Local Server",
Url = "http://localhost:5000/",
});
c.AddServer(new OpenApiServer()
{
Url = "https://demo.kavitareader.com/",
Description = "Kavita Demo"
});
c.AddServer(new OpenApiServer()
{
Url = "http://" + GetLocalIpAddress() + ":5000/",
Description = "Local IP"
});
});
services.AddResponseCompression(options =>
{
options.Providers.Add<BrotliCompressionProvider>();
options.Providers.Add<GzipCompressionProvider>();
options.MimeTypes =
ResponseCompressionDefaults.MimeTypes.Concat(
new[] { "image/jpeg", "image/jpg" });
options.EnableForHttps = true;
});
services.Configure<BrotliCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.Fastest;
});
services.AddResponseCaching();
services.AddHangfire(configuration => configuration
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseSQLiteStorage("config/Hangfire.db")); // UseSQLiteStorage - SQLite has some issues around resuming jobs when aborted
// Add the processing server as IHostedService
services.AddHangfireServer(options =>
{
options.Queues = new[] {TaskScheduler.ScanQueue, TaskScheduler.DefaultQueue};
});
// Add IHostedService for startup tasks
// Any services that should be bootstrapped go here
services.AddHostedService<StartupTasksHostedService>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IBackgroundJobClient backgroundJobs, IWebHostEnvironment env,
IHostApplicationLifetime applicationLifetime, IServiceProvider serviceProvider, ICacheService cacheService,
IDirectoryService directoryService, IUnitOfWork unitOfWork, IBackupService backupService, IImageService imageService)
{
// Apply Migrations
try
{
Task.Run(async () =>
{
// Apply all migrations on startup
var logger = serviceProvider.GetRequiredService<ILogger<Program>>();
var userManager = serviceProvider.GetRequiredService<UserManager<AppUser>>();
var themeService = serviceProvider.GetRequiredService<IThemeService>();
var dataContext = serviceProvider.GetRequiredService<DataContext>();
var readingListService = serviceProvider.GetRequiredService<IReadingListService>();
// Only run this if we are upgrading
await MigrateChangePasswordRoles.Migrate(unitOfWork, userManager);
await MigrateRemoveExtraThemes.Migrate(unitOfWork, themeService);
// only needed for v0.5.4 and v0.6.0
await MigrateNormalizedEverything.Migrate(unitOfWork, dataContext, logger);
// v0.6.0
await MigrateChangeRestrictionRoles.Migrate(unitOfWork, userManager, logger);
await MigrateReadingListAgeRating.Migrate(unitOfWork, dataContext, readingListService, logger);
// Update the version in the DB after all migrations are run
var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion);
installVersion.Value = BuildInfo.Version.ToString();
unitOfWork.SettingsRepository.Update(installVersion);
await unitOfWork.CommitAsync();
}).GetAwaiter()
.GetResult();
}
catch (Exception ex)
{
var logger = serviceProvider.GetRequiredService<ILogger<Program>>();
logger.LogCritical(ex, "An error occurred during migration");
}
app.UseMiddleware<ExceptionMiddleware>();
Task.Run(async () =>
{
var allowSwaggerUi = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync())
.EnableSwaggerUi;
if (env.IsDevelopment() || allowSwaggerUi)
{
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Kavita API " + BuildInfo.Version);
});
}
});
if (env.IsDevelopment())
{
app.UseHangfireDashboard();
}
app.UseResponseCompression();
app.UseForwardedHeaders();
app.UseRouting();
// Ordering is important. Cors, authentication, authorization
if (env.IsDevelopment())
{
app.UseCors(policy => policy
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials() // For SignalR token query param
.WithOrigins("http://localhost:4200", $"http://{GetLocalIpAddress()}:4200")
.WithExposedHeaders("Content-Disposition", "Pagination"));
}
app.UseResponseCaching();
app.UseAuthentication();
app.UseAuthorization();
app.UseDefaultFiles();
app.UseStaticFiles(new StaticFileOptions
{
ContentTypeProvider = new FileExtensionContentTypeProvider(),
HttpsCompression = HttpsCompressionMode.Compress,
OnPrepareResponse = ctx =>
{
ctx.Context.Response.Headers[HeaderNames.CacheControl] = "public,max-age=" + TimeSpan.FromHours(24);
}
});
app.UseSerilogRequestLogging(opts
=>
{
opts.EnrichDiagnosticContext = LogEnricher.EnrichFromRequest;
});
app.Use(async (context, next) =>
{
context.Response.Headers[HeaderNames.Vary] =
new[] { "Accept-Encoding" };
// Don't let the site be iframed outside the same origin (clickjacking)
context.Response.Headers.XFrameOptions = "SAMEORIGIN";
// Setup CSP to ensure we load assets only from these origins
context.Response.Headers.Add("Content-Security-Policy", "frame-ancestors 'none';");
await next();
});
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHub<MessageHub>("hubs/messages");
endpoints.MapHub<LogHub>("hubs/logs");
endpoints.MapHangfireDashboard();
endpoints.MapFallbackToController("Index", "Fallback");
});
applicationLifetime.ApplicationStopping.Register(OnShutdown);
applicationLifetime.ApplicationStarted.Register(() =>
{
try
{
var logger = serviceProvider.GetRequiredService<ILogger<Startup>>();
logger.LogInformation("Kavita - v{Version}", BuildInfo.Version);
}
catch (Exception)
{
/* Swallow Exception */
}
Console.WriteLine($"Kavita - v{BuildInfo.Version}");
});
}
private static void OnShutdown()
{
Console.WriteLine("Server is shutting down. Please allow a few seconds to stop any background jobs...");
TaskScheduler.Client.Dispose();
System.Threading.Thread.Sleep(1000);
Console.WriteLine("You may now close the application window.");
}
private static string GetLocalIpAddress()
{
using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, 0);
socket.Connect("8.8.8.8", 65530);
if (socket.LocalEndPoint is IPEndPoint endPoint) return endPoint.Address.ToString();
throw new KavitaException("No network adapters with an IPv4 address in the system!");
}
}