diff options
| -rw-r--r-- | Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs | 7 | ||||
| -rw-r--r-- | Emby.Server.Implementations/ApplicationHost.cs | 43 | ||||
| -rw-r--r-- | Emby.Server.Implementations/Browser/BrowserLauncher.cs | 35 | ||||
| -rw-r--r-- | Emby.Server.Implementations/ConfigurationOptions.cs | 13 | ||||
| -rw-r--r-- | Emby.Server.Implementations/EntryPoints/StartupWizard.cs | 8 | ||||
| -rw-r--r-- | Emby.Server.Implementations/HttpServer/HttpListenerHost.cs | 13 | ||||
| -rw-r--r-- | Jellyfin.Server/Program.cs | 48 | ||||
| -rw-r--r-- | Jellyfin.Server/Properties/launchSettings.json | 11 | ||||
| -rw-r--r-- | Jellyfin.Server/StartupOptions.cs | 19 | ||||
| -rw-r--r-- | MediaBrowser.Common/Configuration/IApplicationPaths.cs | 9 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs | 15 | ||||
| -rw-r--r-- | MediaBrowser.Controller/IServerApplicationHost.cs | 5 | ||||
| -rw-r--r-- | MediaBrowser.Model/Configuration/ServerConfiguration.cs | 4 | ||||
| -rw-r--r-- | MediaBrowser.WebDashboard/Api/DashboardService.cs | 38 | ||||
| -rw-r--r-- | MediaBrowser.WebDashboard/Api/PackageCreator.cs | 22 |
15 files changed, 203 insertions, 87 deletions
diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs index c3cdcc222..bc4781743 100644 --- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs +++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs @@ -5,7 +5,7 @@ using MediaBrowser.Common.Configuration; namespace Emby.Server.Implementations.AppBase { /// <summary> - /// Provides a base class to hold common application paths used by both the Ui and Server. + /// Provides a base class to hold common application paths used by both the UI and Server. /// This can be subclassed to add application-specific paths. /// </summary> public abstract class BaseApplicationPaths : IApplicationPaths @@ -37,10 +37,7 @@ namespace Emby.Server.Implementations.AppBase /// <value>The program data path.</value> public string ProgramDataPath { get; } - /// <summary> - /// Gets the path to the web UI resources folder. - /// </summary> - /// <value>The web UI resources path.</value> + /// <inheritdoc/> public string WebPath { get; } /// <summary> diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index acae23052..aa22ac8b9 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -236,11 +236,6 @@ namespace Emby.Server.Implementations public int HttpsPort { get; private set; } /// <summary> - /// Gets the content root for the webhost. - /// </summary> - public string ContentRoot { get; private set; } - - /// <summary> /// Gets the server configuration manager. /// </summary> /// <value>The server configuration manager.</value> @@ -612,13 +607,7 @@ namespace Emby.Server.Implementations DiscoverTypes(); - await RegisterResources(serviceCollection, startupConfig).ConfigureAwait(false); - - ContentRoot = ServerConfigurationManager.Configuration.DashboardSourcePath; - if (string.IsNullOrEmpty(ContentRoot)) - { - ContentRoot = ServerConfigurationManager.ApplicationPaths.WebPath; - } + await RegisterServices(serviceCollection, startupConfig).ConfigureAwait(false); } public async Task ExecuteWebsocketHandlerAsync(HttpContext context, Func<Task> next) @@ -649,9 +638,9 @@ namespace Emby.Server.Implementations } /// <summary> - /// Registers resources that classes will depend on + /// Registers services/resources with the service collection that will be available via DI. /// </summary> - protected async Task RegisterResources(IServiceCollection serviceCollection, IConfiguration startupConfig) + protected async Task RegisterServices(IServiceCollection serviceCollection, IConfiguration startupConfig) { serviceCollection.AddMemoryCache(); @@ -769,20 +758,8 @@ namespace Emby.Server.Implementations CertificateInfo = GetCertificateInfo(true); Certificate = GetCertificate(CertificateInfo); - HttpServer = new HttpListenerHost( - this, - LoggerFactory.CreateLogger<HttpListenerHost>(), - ServerConfigurationManager, - startupConfig, - NetworkManager, - JsonSerializer, - XmlSerializer, - CreateHttpListener()) - { - GlobalResponse = LocalizationManager.GetLocalizedString("StartupEmbyServerIsLoading") - }; - - serviceCollection.AddSingleton(HttpServer); + serviceCollection.AddSingleton<IHttpListener, WebSocketSharpListener>(); + serviceCollection.AddSingleton<IHttpServer, HttpListenerHost>(); ImageProcessor = new ImageProcessor(LoggerFactory.CreateLogger<ImageProcessor>(), ServerConfigurationManager.ApplicationPaths, FileSystemManager, ImageEncoder, () => LibraryManager, () => MediaEncoder); serviceCollection.AddSingleton(ImageProcessor); @@ -890,6 +867,14 @@ namespace Emby.Server.Implementations ((LibraryManager)LibraryManager).ItemRepository = ItemRepository; } + /// <summary> + /// Create services registered with the service container that need to be initialized at application startup. + /// </summary> + public void InitializeServices() + { + HttpServer = Resolve<IHttpServer>(); + } + public static void LogEnvironmentInfo(ILogger logger, IApplicationPaths appPaths) { // Distinct these to prevent users from reporting problems that aren't actually problems @@ -1195,8 +1180,6 @@ namespace Emby.Server.Implementations }); } - protected IHttpListener CreateHttpListener() => new WebSocketSharpListener(LoggerFactory.CreateLogger<WebSocketSharpListener>()); - private CertificateInfo GetCertificateInfo(bool generateCertificate) { // Custom cert diff --git a/Emby.Server.Implementations/Browser/BrowserLauncher.cs b/Emby.Server.Implementations/Browser/BrowserLauncher.cs index f5da0d018..96096e142 100644 --- a/Emby.Server.Implementations/Browser/BrowserLauncher.cs +++ b/Emby.Server.Implementations/Browser/BrowserLauncher.cs @@ -1,51 +1,48 @@ using System; using MediaBrowser.Controller; +using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Browser { /// <summary> - /// Class BrowserLauncher. + /// Assists in opening application URLs in an external browser. /// </summary> public static class BrowserLauncher { /// <summary> - /// Opens the dashboard page. + /// Opens the home page of the web client. /// </summary> - /// <param name="page">The page.</param> /// <param name="appHost">The app host.</param> - private static void OpenDashboardPage(string page, IServerApplicationHost appHost) + public static void OpenWebApp(IServerApplicationHost appHost) { - var url = appHost.GetLocalApiUrl("localhost") + "/web/" + page; - - OpenUrl(appHost, url); + TryOpenUrl(appHost, "/web/index.html"); } /// <summary> - /// Opens the web client. + /// Opens the swagger API page. /// </summary> /// <param name="appHost">The app host.</param> - public static void OpenWebApp(IServerApplicationHost appHost) + public static void OpenSwaggerPage(IServerApplicationHost appHost) { - OpenDashboardPage("index.html", appHost); + TryOpenUrl(appHost, "/swagger/index.html"); } /// <summary> - /// Opens the URL. + /// Opens the specified URL in an external browser window. Any exceptions will be logged, but ignored. /// </summary> - /// <param name="appHost">The application host instance.</param> + /// <param name="appHost">The application host.</param> /// <param name="url">The URL.</param> - private static void OpenUrl(IServerApplicationHost appHost, string url) + private static void TryOpenUrl(IServerApplicationHost appHost, string url) { try { - appHost.LaunchUrl(url); - } - catch (NotSupportedException) - { - + string baseUrl = appHost.GetLocalApiUrl("localhost"); + appHost.LaunchUrl(baseUrl + url); } - catch (Exception) + catch (Exception ex) { + var logger = appHost.Resolve<ILogger>(); + logger?.LogError(ex, "Failed to open browser window with URL {URL}", url); } } } diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs index 31fb5ca58..814d4b8b5 100644 --- a/Emby.Server.Implementations/ConfigurationOptions.cs +++ b/Emby.Server.Implementations/ConfigurationOptions.cs @@ -1,13 +1,22 @@ using System.Collections.Generic; +using Emby.Server.Implementations.HttpServer; +using MediaBrowser.Providers.Music; using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; namespace Emby.Server.Implementations { + /// <summary> + /// Static class containing the default configuration options for the web server. + /// </summary> public static class ConfigurationOptions { - public static Dictionary<string, string> Configuration => new Dictionary<string, string> + /// <summary> + /// Gets a new copy of the default configuration options. + /// </summary> + public static Dictionary<string, string> DefaultConfiguration => new Dictionary<string, string> { - { "HttpListenerHost:DefaultRedirectPath", "web/index.html" }, + { NoWebContentKey, bool.FalseString }, + { HttpListenerHost.DefaultRedirectKey, "web/index.html" }, { FfmpegProbeSizeKey, "1G" }, { FfmpegAnalyzeDurationKey, "200M" }, { PlaylistsAllowDuplicatesKey, bool.TrueString } diff --git a/Emby.Server.Implementations/EntryPoints/StartupWizard.cs b/Emby.Server.Implementations/EntryPoints/StartupWizard.cs index 5f2d629fe..bc6b8c956 100644 --- a/Emby.Server.Implementations/EntryPoints/StartupWizard.cs +++ b/Emby.Server.Implementations/EntryPoints/StartupWizard.cs @@ -2,7 +2,9 @@ using System.Threading.Tasks; using Emby.Server.Implementations.Browser; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Plugins; +using Microsoft.Extensions.Configuration; namespace Emby.Server.Implementations.EntryPoints { @@ -36,7 +38,11 @@ namespace Emby.Server.Implementations.EntryPoints return Task.CompletedTask; } - if (!_config.Configuration.IsStartupWizardCompleted) + if (_appHost.Resolve<IConfiguration>().NoWebContent()) + { + BrowserLauncher.OpenSwaggerPage(_appHost); + } + else if (!_config.Configuration.IsStartupWizardCompleted) { BrowserLauncher.OpenWebApp(_appHost); } diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index 93572b8bf..655130fcf 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -17,6 +17,7 @@ using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Events; +using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; @@ -29,6 +30,12 @@ namespace Emby.Server.Implementations.HttpServer { public class HttpListenerHost : IHttpServer, IDisposable { + /// <summary> + /// The key for a setting that specifies the default redirect path + /// to use for requests where the URL base prefix is invalid or missing. + /// </summary> + public const string DefaultRedirectKey = "HttpListenerHost:DefaultRedirectPath"; + private readonly ILogger _logger; private readonly IServerConfigurationManager _config; private readonly INetworkManager _networkManager; @@ -52,12 +59,13 @@ namespace Emby.Server.Implementations.HttpServer INetworkManager networkManager, IJsonSerializer jsonSerializer, IXmlSerializer xmlSerializer, - IHttpListener socketListener) + IHttpListener socketListener, + ILocalizationManager localizationManager) { _appHost = applicationHost; _logger = logger; _config = config; - _defaultRedirectPath = configuration["HttpListenerHost:DefaultRedirectPath"]; + _defaultRedirectPath = configuration[DefaultRedirectKey]; _baseUrlPrefix = _config.Configuration.BaseUrl; _networkManager = networkManager; _jsonSerializer = jsonSerializer; @@ -69,6 +77,7 @@ namespace Emby.Server.Implementations.HttpServer Instance = this; ResponseFilters = Array.Empty<Action<IRequest, HttpResponse, object>>(); + GlobalResponse = localizationManager.GetLocalizedString("StartupEmbyServerIsLoading"); } public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected; diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index e9e852349..67251eb24 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -13,12 +13,13 @@ using System.Threading.Tasks; using CommandLine; using Emby.Drawing; using Emby.Server.Implementations; +using Emby.Server.Implementations.HttpServer; using Emby.Server.Implementations.IO; using Emby.Server.Implementations.Networking; using Jellyfin.Drawing.Skia; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Drawing; -using MediaBrowser.Model.Globalization; +using MediaBrowser.Controller.Extensions; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -112,9 +113,10 @@ namespace Jellyfin.Server // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath); - // Create an instance of the application configuration to use for application startup await InitLoggingConfigFile(appPaths).ConfigureAwait(false); - IConfiguration startupConfig = CreateAppConfiguration(appPaths); + + // Create an instance of the application configuration to use for application startup + IConfiguration startupConfig = CreateAppConfiguration(options, appPaths); // Initialize logging framework InitializeLoggingFramework(startupConfig, appPaths); @@ -188,10 +190,11 @@ namespace Jellyfin.Server ServiceCollection serviceCollection = new ServiceCollection(); await appHost.InitAsync(serviceCollection, startupConfig).ConfigureAwait(false); - var webHost = CreateWebHostBuilder(appHost, serviceCollection, appPaths).Build(); + var webHost = CreateWebHostBuilder(appHost, serviceCollection, options, startupConfig, appPaths).Build(); - // A bit hacky to re-use service provider since ASP.NET doesn't allow a custom service collection. + // Re-use the web host service provider in the app host since ASP.NET doesn't allow a custom service collection. appHost.ServiceProvider = webHost.Services; + appHost.InitializeServices(); appHost.FindParts(); Migrations.MigrationRunner.Run(appHost, _loggerFactory); @@ -233,7 +236,12 @@ namespace Jellyfin.Server } } - private static IWebHostBuilder CreateWebHostBuilder(ApplicationHost appHost, IServiceCollection serviceCollection, IApplicationPaths appPaths) + private static IWebHostBuilder CreateWebHostBuilder( + ApplicationHost appHost, + IServiceCollection serviceCollection, + StartupOptions commandLineOpts, + IConfiguration startupConfig, + IApplicationPaths appPaths) { return new WebHostBuilder() .UseKestrel(options => @@ -273,9 +281,8 @@ namespace Jellyfin.Server } } }) - .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(appPaths)) + .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(commandLineOpts, appPaths, startupConfig)) .UseSerilog() - .UseContentRoot(appHost.ContentRoot) .ConfigureServices(services => { // Merge the external ServiceCollection into ASP.NET DI @@ -398,9 +405,8 @@ namespace Jellyfin.Server // webDir // IF --webdir // ELSE IF $JELLYFIN_WEB_DIR - // ELSE use <bindir>/jellyfin-web + // ELSE <bindir>/jellyfin-web var webDir = options.WebDir; - if (string.IsNullOrEmpty(webDir)) { webDir = Environment.GetEnvironmentVariable("JELLYFIN_WEB_DIR"); @@ -471,21 +477,33 @@ namespace Jellyfin.Server await resource.CopyToAsync(dst).ConfigureAwait(false); } - private static IConfiguration CreateAppConfiguration(IApplicationPaths appPaths) + private static IConfiguration CreateAppConfiguration(StartupOptions commandLineOpts, IApplicationPaths appPaths) { return new ConfigurationBuilder() - .ConfigureAppConfiguration(appPaths) + .ConfigureAppConfiguration(commandLineOpts, appPaths) .Build(); } - private static IConfigurationBuilder ConfigureAppConfiguration(this IConfigurationBuilder config, IApplicationPaths appPaths) + private static IConfigurationBuilder ConfigureAppConfiguration( + this IConfigurationBuilder config, + StartupOptions commandLineOpts, + IApplicationPaths appPaths, + IConfiguration? startupConfig = null) { + // Use the swagger API page as the default redirect path if not hosting the jellyfin-web content + var inMemoryDefaultConfig = ConfigurationOptions.DefaultConfiguration; + if (startupConfig != null && startupConfig.NoWebContent()) + { + inMemoryDefaultConfig[HttpListenerHost.DefaultRedirectKey] = "swagger/index.html"; + } + return config .SetBasePath(appPaths.ConfigurationDirectoryPath) - .AddInMemoryCollection(ConfigurationOptions.Configuration) + .AddInMemoryCollection(inMemoryDefaultConfig) .AddJsonFile(LoggingConfigFileDefault, optional: false, reloadOnChange: true) .AddJsonFile(LoggingConfigFileSystem, optional: true, reloadOnChange: true) - .AddEnvironmentVariables("JELLYFIN_"); + .AddEnvironmentVariables("JELLYFIN_") + .AddInMemoryCollection(commandLineOpts.ConvertToConfig()); } /// <summary> diff --git a/Jellyfin.Server/Properties/launchSettings.json b/Jellyfin.Server/Properties/launchSettings.json new file mode 100644 index 000000000..d68a611c1 --- /dev/null +++ b/Jellyfin.Server/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "Jellyfin.Server": { + "commandName": "Project" + }, + "Jellyfin.Server (nowebcontent)": { + "commandName": "Project", + "commandLineArgs": "--nowebcontent" + } + } +}
\ No newline at end of file diff --git a/Jellyfin.Server/StartupOptions.cs b/Jellyfin.Server/StartupOptions.cs index 1fb1c5af8..ac5df3925 100644 --- a/Jellyfin.Server/StartupOptions.cs +++ b/Jellyfin.Server/StartupOptions.cs @@ -1,5 +1,8 @@ +using System.Collections.Generic; +using System.Globalization; using CommandLine; using Emby.Server.Implementations; +using MediaBrowser.Controller.Extensions; namespace Jellyfin.Server { @@ -16,6 +19,12 @@ namespace Jellyfin.Server public string? DataDir { get; set; } /// <summary> + /// Gets or sets a value indicating whether the server should not host static web content. + /// </summary> + [Option(ConfigurationExtensions.NoWebContentKey, Required = false, HelpText = "Indicates that the web server should not host any static web content.")] + public bool NoWebContent { get; set; } + + /// <summary> /// Gets or sets the path to the web directory. /// </summary> /// <value>The path to the web directory.</value> @@ -66,5 +75,15 @@ namespace Jellyfin.Server /// <inheritdoc /> [Option("restartargs", Required = false, HelpText = "Arguments for restart script.")] public string? RestartArgs { get; set; } + + /// <summary> + /// Gets the command line options as a dictionary that can be used in the .NET configuration system. + /// </summary> + /// <returns>The configuration dictionary.</returns> + public Dictionary<string, string> ConvertToConfig() + => new Dictionary<string, string> + { + { ConfigurationExtensions.NoWebContentKey, NoWebContent.ToString(CultureInfo.InvariantCulture) } + }; } } diff --git a/MediaBrowser.Common/Configuration/IApplicationPaths.cs b/MediaBrowser.Common/Configuration/IApplicationPaths.cs index 5bdea7d8b..870b90796 100644 --- a/MediaBrowser.Common/Configuration/IApplicationPaths.cs +++ b/MediaBrowser.Common/Configuration/IApplicationPaths.cs @@ -1,3 +1,5 @@ +using MediaBrowser.Model.Configuration; + namespace MediaBrowser.Common.Configuration { /// <summary> @@ -12,9 +14,12 @@ namespace MediaBrowser.Common.Configuration string ProgramDataPath { get; } /// <summary> - /// Gets the path to the web UI resources folder + /// Gets the path to the web UI resources folder. /// </summary> - /// <value>The web UI resources path.</value> + /// <remarks> + /// This value is not relevant if the server is configured to not host any static web content. Additionally, + /// the value for <see cref="ServerConfiguration.DashboardSourcePath"/> takes precedence over this one. + /// </remarks> string WebPath { get; } /// <summary> diff --git a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs index 48316499a..900cc6cb5 100644 --- a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs +++ b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs @@ -1,3 +1,4 @@ +using System; using Microsoft.Extensions.Configuration; namespace MediaBrowser.Controller.Extensions @@ -8,6 +9,11 @@ namespace MediaBrowser.Controller.Extensions public static class ConfigurationExtensions { /// <summary> + /// The key for a setting that indicates whether the application should host static web content. + /// </summary> + public const string NoWebContentKey = "nowebcontent"; + + /// <summary> /// The key for the FFmpeg probe size option. /// </summary> public const string FfmpegProbeSizeKey = "FFmpeg:probesize"; @@ -23,6 +29,15 @@ namespace MediaBrowser.Controller.Extensions public const string PlaylistsAllowDuplicatesKey = "playlists:allowDuplicates"; /// <summary> + /// Gets a value indicating whether the application should not host static web content from the <see cref="IConfiguration"/>. + /// </summary> + /// <param name="configuration">The configuration to retrieve the value from.</param> + /// <returns>The parsed config value.</returns> + /// <exception cref="FormatException">The config value is not a valid bool string. See <see cref="bool.Parse(string)"/>.</exception> + public static bool NoWebContent(this IConfiguration configuration) + => configuration.GetValue<bool>(NoWebContentKey); + + /// <summary> /// Gets the FFmpeg probe size from the <see cref="IConfiguration" />. /// </summary> /// <param name="configuration">The configuration to read the setting from.</param> diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index 25f0905eb..608ffc61c 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -82,6 +82,11 @@ namespace MediaBrowser.Controller /// <returns>The local API URL.</returns> string GetLocalApiUrl(IPAddress address); + /// <summary> + /// Open a URL in an external browser window. + /// </summary> + /// <param name="url">The URL to open.</param> + /// <exception cref="NotSupportedException"><see cref="CanLaunchWebBrowser"/> is false.</exception> void LaunchUrl(string url); void EnableLoopback(string appName); diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index dfb563180..3107ec242 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -148,9 +148,9 @@ namespace MediaBrowser.Model.Configuration public bool EnableDashboardResponseCaching { get; set; } /// <summary> - /// Allows the dashboard to be served from a custom path. + /// Gets or sets a custom path to serve the dashboard from. /// </summary> - /// <value>The dashboard source path.</value> + /// <value>The dashboard source path, or null if the default path should be used.</value> public string DashboardSourcePath { get; set; } /// <summary> diff --git a/MediaBrowser.WebDashboard/Api/DashboardService.cs b/MediaBrowser.WebDashboard/Api/DashboardService.cs index 99d8d044f..3e47ce682 100644 --- a/MediaBrowser.WebDashboard/Api/DashboardService.cs +++ b/MediaBrowser.WebDashboard/Api/DashboardService.cs @@ -12,12 +12,14 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Plugins; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Services; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace MediaBrowser.WebDashboard.Api @@ -102,6 +104,7 @@ namespace MediaBrowser.WebDashboard.Api /// <value>The HTTP result factory.</value> private readonly IHttpResultFactory _resultFactory; private readonly IServerApplicationHost _appHost; + private readonly IConfiguration _appConfig; private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IFileSystem _fileSystem; private readonly IResourceFileManager _resourceFileManager; @@ -111,6 +114,7 @@ namespace MediaBrowser.WebDashboard.Api /// </summary> /// <param name="logger">The logger.</param> /// <param name="appHost">The application host.</param> + /// <param name="appConfig">The application configuration.</param> /// <param name="resourceFileManager">The resource file manager.</param> /// <param name="serverConfigurationManager">The server configuration manager.</param> /// <param name="fileSystem">The file system.</param> @@ -118,6 +122,7 @@ namespace MediaBrowser.WebDashboard.Api public DashboardService( ILogger<DashboardService> logger, IServerApplicationHost appHost, + IConfiguration appConfig, IResourceFileManager resourceFileManager, IServerConfigurationManager serverConfigurationManager, IFileSystem fileSystem, @@ -125,10 +130,22 @@ namespace MediaBrowser.WebDashboard.Api { _logger = logger; _appHost = appHost; + _appConfig = appConfig; _resourceFileManager = resourceFileManager; _serverConfigurationManager = serverConfigurationManager; _fileSystem = fileSystem; _resultFactory = resultFactory; + + // Validate web content path + string webContentPath = DashboardUIPath; + bool webContentPathValid = appConfig.NoWebContent() || (Directory.Exists(webContentPath) && Directory.GetFiles(webContentPath).Any()); + if (!webContentPathValid) + { + throw new InvalidOperationException( + "The server is expected to host web content, but the provided content directory is either " + + $"invalid or empty: {webContentPath}. If you do not want to host web content with the server, " + + $"you may set the '{Controller.Extensions.ConfigurationExtensions.NoWebContentKey}' flag."); + } } /// <summary> @@ -138,13 +155,18 @@ namespace MediaBrowser.WebDashboard.Api public IRequest Request { get; set; } /// <summary> - /// Gets the path for the web interface. + /// Gets the path of the directory containing the static web interface content, or null if the server is not + /// hosting the static web content. /// </summary> - /// <value>The path for the web interface.</value> public string DashboardUIPath { get { + if (_appConfig.NoWebContent()) + { + return null; + } + if (!string.IsNullOrEmpty(_serverConfigurationManager.Configuration.DashboardSourcePath)) { return _serverConfigurationManager.Configuration.DashboardSourcePath; @@ -209,7 +231,7 @@ namespace MediaBrowser.WebDashboard.Api return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => Task.FromResult(stream)); } - return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => GetPackageCreator(DashboardUIPath).ModifyHtml("dummy.html", stream, null, _appHost.ApplicationVersionString, null)); + return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => PackageCreator.ModifyHtml(false, stream, null, _appHost.ApplicationVersionString, null)); } throw new ResourceNotFoundException(); @@ -307,6 +329,11 @@ namespace MediaBrowser.WebDashboard.Api /// <returns>System.Object.</returns> public async Task<object> Get(GetDashboardResource request) { + if (_appConfig.NoWebContent() || DashboardUIPath == null) + { + throw new ResourceNotFoundException(); + } + var path = request.ResourceName; var contentType = MimeTypes.GetMimeType(path); @@ -378,6 +405,11 @@ namespace MediaBrowser.WebDashboard.Api public async Task<object> Get(GetDashboardPackage request) { + if (_appConfig.NoWebContent() || DashboardUIPath == null) + { + throw new ResourceNotFoundException(); + } + var mode = request.Mode; var inputPath = string.IsNullOrWhiteSpace(mode) ? diff --git a/MediaBrowser.WebDashboard/Api/PackageCreator.cs b/MediaBrowser.WebDashboard/Api/PackageCreator.cs index ad996c5a9..b7c15a840 100644 --- a/MediaBrowser.WebDashboard/Api/PackageCreator.cs +++ b/MediaBrowser.WebDashboard/Api/PackageCreator.cs @@ -31,7 +31,8 @@ namespace MediaBrowser.WebDashboard.Api if (resourceStream != null && IsCoreHtml(virtualPath)) { - resourceStream = await ModifyHtml(virtualPath, resourceStream, mode, appVersion, localizationCulture).ConfigureAwait(false); + bool isMainIndexPage = string.Equals(virtualPath, "index.html", StringComparison.OrdinalIgnoreCase); + resourceStream = await ModifyHtml(isMainIndexPage, resourceStream, mode, appVersion, localizationCulture).ConfigureAwait(false); } return resourceStream; @@ -47,16 +48,25 @@ namespace MediaBrowser.WebDashboard.Api return string.Equals(Path.GetExtension(path), ".html", StringComparison.OrdinalIgnoreCase); } - // Modifies the HTML by adding common meta tags, css and js. - public async Task<Stream> ModifyHtml( - string path, + /// <summary> + /// Modifies the source HTML stream by adding common meta tags, css and js. + /// </summary> + /// <param name="isMainIndexPage">True if the stream contains content for the main index page.</param> + /// <param name="sourceStream">The stream whose content should be modified.</param> + /// <param name="mode">The client mode ('cordova', 'android', etc).</param> + /// <param name="appVersion">The application version.</param> + /// <param name="localizationCulture">The localization culture.</param> + /// <returns> + /// A task that represents the async operation to read and modify the input stream. + /// The task result contains a stream containing the modified HTML content. + /// </returns> + public static async Task<Stream> ModifyHtml( + bool isMainIndexPage, Stream sourceStream, string mode, string appVersion, string localizationCulture) { - var isMainIndexPage = string.Equals(path, "index.html", StringComparison.OrdinalIgnoreCase); - string html; using (var reader = new StreamReader(sourceStream, Encoding.UTF8)) { |
