diff options
Diffstat (limited to 'Jellyfin.Server')
| -rw-r--r-- | Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs | 27 | ||||
| -rw-r--r-- | Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs | 90 | ||||
| -rw-r--r-- | Jellyfin.Server/Jellyfin.Server.csproj | 6 | ||||
| -rw-r--r-- | Jellyfin.Server/Program.cs | 104 | ||||
| -rw-r--r-- | Jellyfin.Server/Resources/Configuration/logging.json | 3 | ||||
| -rw-r--r-- | Jellyfin.Server/Startup.cs | 81 | ||||
| -rw-r--r-- | Jellyfin.Server/StartupOptions.cs | 18 |
7 files changed, 311 insertions, 18 deletions
diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs new file mode 100644 index 000000000..db06eb455 --- /dev/null +++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Builder; + +namespace Jellyfin.Server.Extensions +{ + /// <summary> + /// Extensions for adding API specific functionality to the application pipeline. + /// </summary> + public static class ApiApplicationBuilderExtensions + { + /// <summary> + /// Adds swagger and swagger UI to the application pipeline. + /// </summary> + /// <param name="applicationBuilder">The application builder.</param> + /// <returns>The updated application builder.</returns> + public static IApplicationBuilder UseJellyfinApiSwagger(this IApplicationBuilder applicationBuilder) + { + applicationBuilder.UseSwagger(); + + // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), + // specifying the Swagger JSON endpoint. + return applicationBuilder.UseSwaggerUI(c => + { + c.SwaggerEndpoint("/swagger/v1/swagger.json", "Jellyfin API V1"); + }); + } + } +} diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs new file mode 100644 index 000000000..dd4f9cd23 --- /dev/null +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -0,0 +1,90 @@ +using Jellyfin.Api; +using Jellyfin.Api.Auth; +using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy; +using Jellyfin.Api.Auth.RequiresElevationPolicy; +using Jellyfin.Api.Constants; +using Jellyfin.Api.Controllers; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OpenApi.Models; + +namespace Jellyfin.Server.Extensions +{ + /// <summary> + /// API specific extensions for the service collection. + /// </summary> + public static class ApiServiceCollectionExtensions + { + /// <summary> + /// Adds jellyfin API authorization policies to the DI container. + /// </summary> + /// <param name="serviceCollection">The service collection.</param> + /// <returns>The updated service collection.</returns> + public static IServiceCollection AddJellyfinApiAuthorization(this IServiceCollection serviceCollection) + { + serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupOrElevatedHandler>(); + serviceCollection.AddSingleton<IAuthorizationHandler, RequiresElevationHandler>(); + return serviceCollection.AddAuthorizationCore(options => + { + options.AddPolicy( + Policies.RequiresElevation, + policy => + { + policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); + policy.AddRequirements(new RequiresElevationRequirement()); + }); + options.AddPolicy( + Policies.FirstTimeSetupOrElevated, + policy => + { + policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); + policy.AddRequirements(new FirstTimeSetupOrElevatedRequirement()); + }); + }); + } + + /// <summary> + /// Adds custom legacy authentication to the service collection. + /// </summary> + /// <param name="serviceCollection">The service collection.</param> + /// <returns>The updated service collection.</returns> + public static AuthenticationBuilder AddCustomAuthentication(this IServiceCollection serviceCollection) + { + return serviceCollection.AddAuthentication(AuthenticationSchemes.CustomAuthentication) + .AddScheme<AuthenticationSchemeOptions, CustomAuthenticationHandler>(AuthenticationSchemes.CustomAuthentication, null); + } + + /// <summary> + /// Extension method for adding the jellyfin API to the service collection. + /// </summary> + /// <param name="serviceCollection">The service collection.</param> + /// <param name="baseUrl">The base url for the API.</param> + /// <returns>The MVC builder.</returns> + public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, string baseUrl) + { + return serviceCollection.AddMvc(opts => + { + opts.UseGeneralRoutePrefix(baseUrl); + }) + + // Clear app parts to avoid other assemblies being picked up + .ConfigureApplicationPartManager(a => a.ApplicationParts.Clear()) + .AddApplicationPart(typeof(StartupController).Assembly) + .AddControllersAsServices(); + } + + /// <summary> + /// Adds Swagger to the service collection. + /// </summary> + /// <param name="serviceCollection">The service collection.</param> + /// <returns>The updated service collection.</returns> + public static IServiceCollection AddJellyfinApiSwagger(this IServiceCollection serviceCollection) + { + return serviceCollection.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo { Title = "Jellyfin API", Version = "v1" }); + }); + } + } +} diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 8afeb8750..110028176 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -10,6 +10,7 @@ <PropertyGroup> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> @@ -20,6 +21,10 @@ <EmbeddedResource Include="Resources/Configuration/*" /> </ItemGroup> + <ItemGroup> + <FrameworkReference Include="Microsoft.AspNetCore.App" /> + </ItemGroup> + <!-- Code analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.7" /> @@ -41,6 +46,7 @@ <PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" /> <PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" /> <PackageReference Include="Serilog.Sinks.File" Version="4.1.0" /> + <PackageReference Include="Serilog.Sinks.Graylog" Version="2.1.1" /> <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.0.2" /> <PackageReference Include="SQLitePCLRaw.provider.sqlite3.netstandard11" Version="1.1.14" /> </ItemGroup> diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index bdf3689f1..5ac005b40 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Linq; using System.Net; @@ -18,9 +19,12 @@ using Jellyfin.Drawing.Skia; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Model.Globalization; +using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Serilog; using Serilog.Extensions.Logging; using SQLitePCL; @@ -35,7 +39,7 @@ namespace Jellyfin.Server { private static readonly CancellationTokenSource _tokenSource = new CancellationTokenSource(); private static readonly ILoggerFactory _loggerFactory = new SerilogLoggerFactory(); - private static ILogger _logger; + private static ILogger _logger = NullLogger.Instance; private static bool _restartOnShutdown; /// <summary> @@ -86,6 +90,12 @@ namespace Jellyfin.Server { var stopWatch = new Stopwatch(); stopWatch.Start(); + + // Log all uncaught exceptions to std error + static void UnhandledExceptionToConsole(object sender, UnhandledExceptionEventArgs e) => + Console.Error.WriteLine("Unhandled Exception\n" + e.ExceptionObject.ToString()); + AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionToConsole; + ServerApplicationPaths appPaths = CreateApplicationPaths(options); // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager @@ -97,6 +107,8 @@ namespace Jellyfin.Server _logger = _loggerFactory.CreateLogger("Main"); + // Log uncaught exceptions to the logging instead of std error + AppDomain.CurrentDomain.UnhandledException -= UnhandledExceptionToConsole; AppDomain.CurrentDomain.UnhandledException += (sender, e) => _logger.LogCritical((Exception)e.ExceptionObject, "Unhandled Exception"); @@ -129,7 +141,7 @@ namespace Jellyfin.Server _logger.LogInformation( "Jellyfin version: {Version}", - Assembly.GetEntryAssembly().GetName().Version.ToString(3)); + Assembly.GetEntryAssembly()!.GetName().Version!.ToString(3)); ApplicationHost.LogEnvironmentInfo(_logger, appPaths); @@ -157,7 +169,24 @@ namespace Jellyfin.Server appConfig); try { - await appHost.InitAsync(new ServiceCollection()).ConfigureAwait(false); + ServiceCollection serviceCollection = new ServiceCollection(); + await appHost.InitAsync(serviceCollection).ConfigureAwait(false); + + var host = CreateWebHostBuilder(appHost, serviceCollection).Build(); + + // A bit hacky to re-use service provider since ASP.NET doesn't allow a custom service collection. + appHost.ServiceProvider = host.Services; + appHost.FindParts(); + + try + { + await host.StartAsync().ConfigureAwait(false); + } + catch + { + _logger.LogError("Kestrel failed to start! This is most likely due to an invalid address or port bind - correct your bind configuration in system.xml and try again."); + throw; + } appHost.ImageProcessor.ImageEncoder = GetImageEncoder(appPaths, appHost.LocalizationManager); @@ -189,6 +218,55 @@ namespace Jellyfin.Server } } + private static IWebHostBuilder CreateWebHostBuilder(ApplicationHost appHost, IServiceCollection serviceCollection) + { + return new WebHostBuilder() + .UseKestrel(options => + { + var addresses = appHost.ServerConfigurationManager + .Configuration + .LocalNetworkAddresses + .Select(appHost.NormalizeConfiguredLocalAddress) + .Where(i => i != null) + .ToList(); + if (addresses.Any()) + { + foreach (var address in addresses) + { + _logger.LogInformation("Kestrel listening on {ipaddr}", address); + options.Listen(address, appHost.HttpPort); + + if (appHost.EnableHttps && appHost.Certificate != null) + { + options.Listen( + address, + appHost.HttpsPort, + listenOptions => listenOptions.UseHttps(appHost.Certificate)); + } + } + } + else + { + _logger.LogInformation("Kestrel listening on all interfaces"); + options.ListenAnyIP(appHost.HttpPort); + + if (appHost.EnableHttps && appHost.Certificate != null) + { + options.ListenAnyIP( + appHost.HttpsPort, + listenOptions => listenOptions.UseHttps(appHost.Certificate)); + } + } + }) + .UseContentRoot(appHost.ContentRoot) + .ConfigureServices(services => + { + // Merge the external ServiceCollection into ASP.NET DI + services.TryAdd(serviceCollection); + }) + .UseStartup<Startup>(); + } + /// <summary> /// Create the data, config and log paths from the variety of inputs(command line args, /// environment variables) or decide on what default to use. For Windows it's %AppPath% @@ -354,16 +432,25 @@ namespace Jellyfin.Server private static async Task<IConfiguration> CreateConfiguration(IApplicationPaths appPaths) { + const string ResourcePath = "Jellyfin.Server.Resources.Configuration.logging.json"; string configPath = Path.Combine(appPaths.ConfigurationDirectoryPath, "logging.json"); if (!File.Exists(configPath)) { // For some reason the csproj name is used instead of the assembly name - using (Stream rscstr = typeof(Program).Assembly - .GetManifestResourceStream("Jellyfin.Server.Resources.Configuration.logging.json")) - using (Stream fstr = File.Open(configPath, FileMode.CreateNew)) + using (Stream? resource = typeof(Program).Assembly.GetManifestResourceStream(ResourcePath)) { - await rscstr.CopyToAsync(fstr).ConfigureAwait(false); + if (resource == null) + { + throw new InvalidOperationException( + string.Format( + CultureInfo.InvariantCulture, + "Invalid resource path: '{0}'", + ResourcePath)); + } + + using Stream dst = File.Open(configPath, FileMode.CreateNew); + await resource.CopyToAsync(dst).ConfigureAwait(false); } } @@ -426,7 +513,7 @@ namespace Jellyfin.Server { _logger.LogInformation("Starting new instance"); - string module = options.RestartPath; + var module = options.RestartPath; if (string.IsNullOrWhiteSpace(module)) { @@ -434,7 +521,6 @@ namespace Jellyfin.Server } string commandLineArgsString; - if (options.RestartArgs != null) { commandLineArgsString = options.RestartArgs ?? string.Empty; diff --git a/Jellyfin.Server/Resources/Configuration/logging.json b/Jellyfin.Server/Resources/Configuration/logging.json index d16991277..e85ef05af 100644 --- a/Jellyfin.Server/Resources/Configuration/logging.json +++ b/Jellyfin.Server/Resources/Configuration/logging.json @@ -17,6 +17,9 @@ "Args": { "path": "%JELLYFIN_LOG_DIR%//log_.log", "rollingInterval": "Day", + "retainedFileCountLimit": 3, + "rollOnFileSizeLimit": true, + "fileSizeLimitBytes": 100000000, "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {Message}{NewLine}{Exception}" } } diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs new file mode 100644 index 000000000..3ee5fb8b5 --- /dev/null +++ b/Jellyfin.Server/Startup.cs @@ -0,0 +1,81 @@ +using Jellyfin.Server.Extensions; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Jellyfin.Server +{ + /// <summary> + /// Startup configuration for the Kestrel webhost. + /// </summary> + public class Startup + { + private readonly IServerConfigurationManager _serverConfigurationManager; + + /// <summary> + /// Initializes a new instance of the <see cref="Startup" /> class. + /// </summary> + /// <param name="serverConfigurationManager">The server configuration manager.</param> + public Startup(IServerConfigurationManager serverConfigurationManager) + { + _serverConfigurationManager = serverConfigurationManager; + } + + /// <summary> + /// Configures the service collection for the webhost. + /// </summary> + /// <param name="services">The service collection.</param> + public void ConfigureServices(IServiceCollection services) + { + services.AddResponseCompression(); + services.AddHttpContextAccessor(); + services.AddJellyfinApi(_serverConfigurationManager.Configuration.BaseUrl.TrimStart('/')); + + services.AddJellyfinApiSwagger(); + + // configure custom legacy authentication + services.AddCustomAuthentication(); + + services.AddJellyfinApiAuthorization(); + } + + /// <summary> + /// Configures the app builder for the webhost. + /// </summary> + /// <param name="app">The application builder.</param> + /// <param name="env">The webhost environment.</param> + /// <param name="serverApplicationHost">The server application host.</param> + public void Configure( + IApplicationBuilder app, + IWebHostEnvironment env, + IServerApplicationHost serverApplicationHost) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseWebSockets(); + + app.UseResponseCompression(); + + // TODO app.UseMiddleware<WebSocketMiddleware>(); + app.Use(serverApplicationHost.ExecuteWebsocketHandlerAsync); + + // TODO use when old API is removed: app.UseAuthentication(); + app.UseJellyfinApiSwagger(); + app.UseRouting(); + app.UseAuthorization(); + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + + app.Use(serverApplicationHost.ExecuteHttpHandlerAsync); + } + } +} diff --git a/Jellyfin.Server/StartupOptions.cs b/Jellyfin.Server/StartupOptions.cs index bb0adaf63..1fb1c5af8 100644 --- a/Jellyfin.Server/StartupOptions.cs +++ b/Jellyfin.Server/StartupOptions.cs @@ -13,39 +13,39 @@ namespace Jellyfin.Server /// </summary> /// <value>The path to the data directory.</value> [Option('d', "datadir", Required = false, HelpText = "Path to use for the data folder (database files, etc.).")] - public string DataDir { get; set; } + public string? DataDir { get; set; } /// <summary> /// Gets or sets the path to the web directory. /// </summary> /// <value>The path to the web directory.</value> [Option('w', "webdir", Required = false, HelpText = "Path to the Jellyfin web UI resources.")] - public string WebDir { get; set; } + public string? WebDir { get; set; } /// <summary> /// Gets or sets the path to the cache directory. /// </summary> /// <value>The path to the cache directory.</value> [Option('C', "cachedir", Required = false, HelpText = "Path to use for caching.")] - public string CacheDir { get; set; } + public string? CacheDir { get; set; } /// <summary> /// Gets or sets the path to the config directory. /// </summary> /// <value>The path to the config directory.</value> [Option('c', "configdir", Required = false, HelpText = "Path to use for configuration data (user settings and pictures).")] - public string ConfigDir { get; set; } + public string? ConfigDir { get; set; } /// <summary> /// Gets or sets the path to the log directory. /// </summary> /// <value>The path to the log directory.</value> [Option('l', "logdir", Required = false, HelpText = "Path to use for writing log files.")] - public string LogDir { get; set; } + public string? LogDir { get; set; } /// <inheritdoc /> [Option("ffmpeg", Required = false, HelpText = "Path to external FFmpeg executable to use in place of default found in PATH.")] - public string FFmpegPath { get; set; } + public string? FFmpegPath { get; set; } /// <inheritdoc /> [Option("service", Required = false, HelpText = "Run as headless service.")] @@ -57,14 +57,14 @@ namespace Jellyfin.Server /// <inheritdoc /> [Option("package-name", Required = false, HelpText = "Used when packaging Jellyfin (example, synology).")] - public string PackageName { get; set; } + public string? PackageName { get; set; } /// <inheritdoc /> [Option("restartpath", Required = false, HelpText = "Path to restart script.")] - public string RestartPath { get; set; } + public string? RestartPath { get; set; } /// <inheritdoc /> [Option("restartargs", Required = false, HelpText = "Arguments for restart script.")] - public string RestartArgs { get; set; } + public string? RestartArgs { get; set; } } } |
