mirror of
				https://github.com/jellyfin/jellyfin.git
				synced 2025-10-31 10:37:22 -04:00 
			
		
		
		
	Kill HttpListenerHost
This commit is contained in:
		
							parent
							
								
									6ff372a550
								
							
						
					
					
						commit
						571d0570f5
					
				| @ -96,12 +96,12 @@ using MediaBrowser.Providers.Manager; | |||||||
| using MediaBrowser.Providers.Plugins.TheTvdb; | using MediaBrowser.Providers.Plugins.TheTvdb; | ||||||
| using MediaBrowser.Providers.Subtitles; | using MediaBrowser.Providers.Subtitles; | ||||||
| using MediaBrowser.XbmcMetadata.Providers; | using MediaBrowser.XbmcMetadata.Providers; | ||||||
| using Microsoft.AspNetCore.Http; |  | ||||||
| using Microsoft.AspNetCore.Mvc; | using Microsoft.AspNetCore.Mvc; | ||||||
| using Microsoft.Extensions.DependencyInjection; | using Microsoft.Extensions.DependencyInjection; | ||||||
| using Microsoft.Extensions.Logging; | using Microsoft.Extensions.Logging; | ||||||
| using Prometheus.DotNetRuntime; | using Prometheus.DotNetRuntime; | ||||||
| using OperatingSystem = MediaBrowser.Common.System.OperatingSystem; | using OperatingSystem = MediaBrowser.Common.System.OperatingSystem; | ||||||
|  | using WebSocketManager = Emby.Server.Implementations.HttpServer.WebSocketManager; | ||||||
| 
 | 
 | ||||||
| namespace Emby.Server.Implementations | namespace Emby.Server.Implementations | ||||||
| { | { | ||||||
| @ -122,9 +122,11 @@ namespace Emby.Server.Implementations | |||||||
| 
 | 
 | ||||||
|         private IMediaEncoder _mediaEncoder; |         private IMediaEncoder _mediaEncoder; | ||||||
|         private ISessionManager _sessionManager; |         private ISessionManager _sessionManager; | ||||||
|         private IHttpServer _httpServer; |         private IWebSocketManager _webSocketManager; | ||||||
|         private IHttpClient _httpClient; |         private IHttpClient _httpClient; | ||||||
| 
 | 
 | ||||||
|  |         private string[] _urlPrefixes; | ||||||
|  | 
 | ||||||
|         /// <summary> |         /// <summary> | ||||||
|         /// Gets a value indicating whether this instance can self restart. |         /// Gets a value indicating whether this instance can self restart. | ||||||
|         /// </summary> |         /// </summary> | ||||||
| @ -444,7 +446,6 @@ namespace Emby.Server.Implementations | |||||||
|             Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed); |             Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed); | ||||||
| 
 | 
 | ||||||
|             Logger.LogInformation("Core startup complete"); |             Logger.LogInformation("Core startup complete"); | ||||||
|             _httpServer.GlobalResponse = null; |  | ||||||
| 
 | 
 | ||||||
|             stopWatch.Restart(); |             stopWatch.Restart(); | ||||||
|             await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false); |             await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false); | ||||||
| @ -500,9 +501,6 @@ namespace Emby.Server.Implementations | |||||||
|             RegisterServices(); |             RegisterServices(); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         public Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next) |  | ||||||
|             => _httpServer.RequestHandler(context, next); |  | ||||||
| 
 |  | ||||||
|         /// <summary> |         /// <summary> | ||||||
|         /// Registers services/resources with the service collection that will be available via DI. |         /// Registers services/resources with the service collection that will be available via DI. | ||||||
|         /// </summary> |         /// </summary> | ||||||
| @ -577,7 +575,7 @@ namespace Emby.Server.Implementations | |||||||
| 
 | 
 | ||||||
|             ServiceCollection.AddSingleton<ISearchEngine, SearchEngine>(); |             ServiceCollection.AddSingleton<ISearchEngine, SearchEngine>(); | ||||||
| 
 | 
 | ||||||
|             ServiceCollection.AddSingleton<IHttpServer, HttpListenerHost>(); |             ServiceCollection.AddSingleton<IWebSocketManager, WebSocketManager>(); | ||||||
| 
 | 
 | ||||||
|             ServiceCollection.AddSingleton<IImageProcessor, ImageProcessor>(); |             ServiceCollection.AddSingleton<IImageProcessor, ImageProcessor>(); | ||||||
| 
 | 
 | ||||||
| @ -650,7 +648,7 @@ namespace Emby.Server.Implementations | |||||||
| 
 | 
 | ||||||
|             _mediaEncoder = Resolve<IMediaEncoder>(); |             _mediaEncoder = Resolve<IMediaEncoder>(); | ||||||
|             _sessionManager = Resolve<ISessionManager>(); |             _sessionManager = Resolve<ISessionManager>(); | ||||||
|             _httpServer = Resolve<IHttpServer>(); |             _webSocketManager = Resolve<IWebSocketManager>(); | ||||||
|             _httpClient = Resolve<IHttpClient>(); |             _httpClient = Resolve<IHttpClient>(); | ||||||
| 
 | 
 | ||||||
|             ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize(); |             ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize(); | ||||||
| @ -771,7 +769,8 @@ namespace Emby.Server.Implementations | |||||||
|                         .Where(i => i != null) |                         .Where(i => i != null) | ||||||
|                         .ToArray(); |                         .ToArray(); | ||||||
| 
 | 
 | ||||||
|             _httpServer.Init(GetExports<IWebSocketListener>(), GetUrlPrefixes()); |             _urlPrefixes = GetUrlPrefixes().ToArray(); | ||||||
|  |             _webSocketManager.Init(GetExports<IWebSocketListener>()); | ||||||
| 
 | 
 | ||||||
|             Resolve<ILibraryManager>().AddParts( |             Resolve<ILibraryManager>().AddParts( | ||||||
|                 GetExports<IResolverIgnoreRule>(), |                 GetExports<IResolverIgnoreRule>(), | ||||||
| @ -937,7 +936,7 @@ namespace Emby.Server.Implementations | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (!_httpServer.UrlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase)) |             if (!_urlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase)) | ||||||
|             { |             { | ||||||
|                 requiresRestart = true; |                 requiresRestart = true; | ||||||
|             } |             } | ||||||
|  | |||||||
| @ -15,7 +15,7 @@ namespace Emby.Server.Implementations | |||||||
|         public static Dictionary<string, string> DefaultConfiguration => new Dictionary<string, string> |         public static Dictionary<string, string> DefaultConfiguration => new Dictionary<string, string> | ||||||
|         { |         { | ||||||
|             { HostWebClientKey, bool.TrueString }, |             { HostWebClientKey, bool.TrueString }, | ||||||
|             { HttpListenerHost.DefaultRedirectKey, "web/index.html" }, |             { DefaultRedirectKey, "web/index.html" }, | ||||||
|             { FfmpegProbeSizeKey, "1G" }, |             { FfmpegProbeSizeKey, "1G" }, | ||||||
|             { FfmpegAnalyzeDurationKey, "200M" }, |             { FfmpegAnalyzeDurationKey, "200M" }, | ||||||
|             { PlaylistsAllowDuplicatesKey, bool.TrueString }, |             { PlaylistsAllowDuplicatesKey, bool.TrueString }, | ||||||
|  | |||||||
| @ -1,315 +0,0 @@ | |||||||
| #pragma warning disable CS1591 |  | ||||||
| 
 |  | ||||||
| using System; |  | ||||||
| using System.Collections.Generic; |  | ||||||
| using System.Linq; |  | ||||||
| using System.Net.WebSockets; |  | ||||||
| using System.Threading.Tasks; |  | ||||||
| using Jellyfin.Data.Events; |  | ||||||
| using MediaBrowser.Common.Extensions; |  | ||||||
| using MediaBrowser.Common.Net; |  | ||||||
| using MediaBrowser.Controller.Configuration; |  | ||||||
| using MediaBrowser.Controller.Net; |  | ||||||
| using MediaBrowser.Model.Globalization; |  | ||||||
| using Microsoft.AspNetCore.Http; |  | ||||||
| using Microsoft.Extensions.Configuration; |  | ||||||
| using Microsoft.Extensions.Logging; |  | ||||||
| using Microsoft.Extensions.Primitives; |  | ||||||
| 
 |  | ||||||
| namespace Emby.Server.Implementations.HttpServer |  | ||||||
| { |  | ||||||
|     public class HttpListenerHost : IHttpServer |  | ||||||
|     { |  | ||||||
|         /// <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<HttpListenerHost> _logger; |  | ||||||
|         private readonly ILoggerFactory _loggerFactory; |  | ||||||
|         private readonly IServerConfigurationManager _config; |  | ||||||
|         private readonly INetworkManager _networkManager; |  | ||||||
|         private readonly string _defaultRedirectPath; |  | ||||||
|         private readonly string _baseUrlPrefix; |  | ||||||
| 
 |  | ||||||
|         private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>(); |  | ||||||
|         private bool _disposed = false; |  | ||||||
| 
 |  | ||||||
|         public HttpListenerHost( |  | ||||||
|             ILogger<HttpListenerHost> logger, |  | ||||||
|             IServerConfigurationManager config, |  | ||||||
|             IConfiguration configuration, |  | ||||||
|             INetworkManager networkManager, |  | ||||||
|             ILocalizationManager localizationManager, |  | ||||||
|             ILoggerFactory loggerFactory) |  | ||||||
|         { |  | ||||||
|             _logger = logger; |  | ||||||
|             _config = config; |  | ||||||
|             _defaultRedirectPath = configuration[DefaultRedirectKey]; |  | ||||||
|             _baseUrlPrefix = _config.Configuration.BaseUrl; |  | ||||||
|             _networkManager = networkManager; |  | ||||||
|             _loggerFactory = loggerFactory; |  | ||||||
| 
 |  | ||||||
|             Instance = this; |  | ||||||
|             GlobalResponse = localizationManager.GetLocalizedString("StartupEmbyServerIsLoading"); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected; |  | ||||||
| 
 |  | ||||||
|         public static HttpListenerHost Instance { get; protected set; } |  | ||||||
| 
 |  | ||||||
|         public string[] UrlPrefixes { get; private set; } |  | ||||||
| 
 |  | ||||||
|         public string GlobalResponse { get; set; } |  | ||||||
| 
 |  | ||||||
|         private static string NormalizeConfiguredLocalAddress(string address) |  | ||||||
|         { |  | ||||||
|             var add = address.AsSpan().Trim('/'); |  | ||||||
|             int index = add.IndexOf('/'); |  | ||||||
|             if (index != -1) |  | ||||||
|             { |  | ||||||
|                 add = add.Slice(index + 1); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             return add.TrimStart('/').ToString(); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         private bool ValidateHost(string host) |  | ||||||
|         { |  | ||||||
|             var hosts = _config |  | ||||||
|                 .Configuration |  | ||||||
|                 .LocalNetworkAddresses |  | ||||||
|                 .Select(NormalizeConfiguredLocalAddress) |  | ||||||
|                 .ToList(); |  | ||||||
| 
 |  | ||||||
|             if (hosts.Count == 0) |  | ||||||
|             { |  | ||||||
|                 return true; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             host ??= string.Empty; |  | ||||||
| 
 |  | ||||||
|             if (_networkManager.IsInPrivateAddressSpace(host)) |  | ||||||
|             { |  | ||||||
|                 hosts.Add("localhost"); |  | ||||||
|                 hosts.Add("127.0.0.1"); |  | ||||||
| 
 |  | ||||||
|                 return hosts.Any(i => host.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             return true; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         private bool ValidateRequest(string remoteIp, bool isLocal) |  | ||||||
|         { |  | ||||||
|             if (isLocal) |  | ||||||
|             { |  | ||||||
|                 return true; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if (_config.Configuration.EnableRemoteAccess) |  | ||||||
|             { |  | ||||||
|                 var addressFilter = _config.Configuration.RemoteIPFilter.Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(); |  | ||||||
| 
 |  | ||||||
|                 if (addressFilter.Length > 0 && !_networkManager.IsInLocalNetwork(remoteIp)) |  | ||||||
|                 { |  | ||||||
|                     if (_config.Configuration.IsRemoteIPFilterBlacklist) |  | ||||||
|                     { |  | ||||||
|                         return !_networkManager.IsAddressInSubnets(remoteIp, addressFilter); |  | ||||||
|                     } |  | ||||||
|                     else |  | ||||||
|                     { |  | ||||||
|                         return _networkManager.IsAddressInSubnets(remoteIp, addressFilter); |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             else |  | ||||||
|             { |  | ||||||
|                 if (!_networkManager.IsInLocalNetwork(remoteIp)) |  | ||||||
|                 { |  | ||||||
|                     return false; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             return true; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         public Task RequestHandler(HttpContext context, Func<Task> next) |  | ||||||
|         { |  | ||||||
|             if (context.WebSockets.IsWebSocketRequest) |  | ||||||
|             { |  | ||||||
|                 return WebSocketRequestHandler(context); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             return HttpRequestHandler(context, next); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         /// <summary> |  | ||||||
|         /// Overridable method that can be used to implement a custom handler. |  | ||||||
|         /// </summary> |  | ||||||
|         private async Task HttpRequestHandler(HttpContext httpContext, Func<Task> next) |  | ||||||
|         { |  | ||||||
|             var cancellationToken = httpContext.RequestAborted; |  | ||||||
|             var httpRes = httpContext.Response; |  | ||||||
|             var host = httpContext.Request.Host.ToString(); |  | ||||||
|             var localPath = httpContext.Request.Path.ToString(); |  | ||||||
|             string remoteIp = httpContext.Request.RemoteIp(); |  | ||||||
| 
 |  | ||||||
|             if (_disposed) |  | ||||||
|             { |  | ||||||
|                 httpRes.StatusCode = 503; |  | ||||||
|                 httpRes.ContentType = "text/plain"; |  | ||||||
|                 await httpRes.WriteAsync("Server shutting down", cancellationToken).ConfigureAwait(false); |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if (!ValidateHost(host)) |  | ||||||
|             { |  | ||||||
|                 httpRes.StatusCode = 400; |  | ||||||
|                 httpRes.ContentType = "text/plain"; |  | ||||||
|                 await httpRes.WriteAsync("Invalid host", cancellationToken).ConfigureAwait(false); |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if (!ValidateRequest(remoteIp, httpContext.Request.IsLocal())) |  | ||||||
|             { |  | ||||||
|                 httpRes.StatusCode = 403; |  | ||||||
|                 httpRes.ContentType = "text/plain"; |  | ||||||
|                 await httpRes.WriteAsync("Forbidden", cancellationToken).ConfigureAwait(false); |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if (string.Equals(httpContext.Request.Method, "OPTIONS", StringComparison.OrdinalIgnoreCase)) |  | ||||||
|             { |  | ||||||
|                 httpRes.StatusCode = 200; |  | ||||||
|                 foreach (var (key, value) in GetDefaultCorsHeaders(httpContext)) |  | ||||||
|                 { |  | ||||||
|                     httpRes.Headers.Add(key, value); |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 httpRes.ContentType = "text/plain"; |  | ||||||
|                 await httpRes.WriteAsync(string.Empty, cancellationToken).ConfigureAwait(false); |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if (string.Equals(localPath, _baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase) |  | ||||||
|                 || string.Equals(localPath, _baseUrlPrefix, StringComparison.OrdinalIgnoreCase) |  | ||||||
|                 || string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase) |  | ||||||
|                 || string.IsNullOrEmpty(localPath) |  | ||||||
|                 || !localPath.StartsWith(_baseUrlPrefix, StringComparison.OrdinalIgnoreCase)) |  | ||||||
|             { |  | ||||||
|                 // Always redirect back to the default path if the base prefix is invalid or missing |  | ||||||
|                 _logger.LogDebug("Normalizing a URL at {0}", localPath); |  | ||||||
|                 httpRes.Redirect(_baseUrlPrefix + "/" + _defaultRedirectPath); |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if (!string.IsNullOrEmpty(GlobalResponse)) |  | ||||||
|             { |  | ||||||
|                 // We don't want the address pings in ApplicationHost to fail |  | ||||||
|                 if (localPath.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) == -1) |  | ||||||
|                 { |  | ||||||
|                     httpRes.StatusCode = 503; |  | ||||||
|                     httpRes.ContentType = "text/html"; |  | ||||||
|                     await httpRes.WriteAsync(GlobalResponse, cancellationToken).ConfigureAwait(false); |  | ||||||
|                     return; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             await next().ConfigureAwait(false); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         private async Task WebSocketRequestHandler(HttpContext context) |  | ||||||
|         { |  | ||||||
|             if (_disposed) |  | ||||||
|             { |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             try |  | ||||||
|             { |  | ||||||
|                 _logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress); |  | ||||||
| 
 |  | ||||||
|                 WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false); |  | ||||||
| 
 |  | ||||||
|                 using var connection = new WebSocketConnection( |  | ||||||
|                     _loggerFactory.CreateLogger<WebSocketConnection>(), |  | ||||||
|                     webSocket, |  | ||||||
|                     context.Connection.RemoteIpAddress, |  | ||||||
|                     context.Request.Query) |  | ||||||
|                 { |  | ||||||
|                     OnReceive = ProcessWebSocketMessageReceived |  | ||||||
|                 }; |  | ||||||
| 
 |  | ||||||
|                 WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection)); |  | ||||||
| 
 |  | ||||||
|                 await connection.ProcessAsync().ConfigureAwait(false); |  | ||||||
|                 _logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress); |  | ||||||
|             } |  | ||||||
|             catch (Exception ex) // Otherwise ASP.Net will ignore the exception |  | ||||||
|             { |  | ||||||
|                 _logger.LogError(ex, "WS {IP} WebSocketRequestHandler error", context.Connection.RemoteIpAddress); |  | ||||||
|                 if (!context.Response.HasStarted) |  | ||||||
|                 { |  | ||||||
|                     context.Response.StatusCode = 500; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         /// <inheritdoc /> |  | ||||||
|         public IDictionary<string, string> GetDefaultCorsHeaders(HttpContext httpContext) |  | ||||||
|         { |  | ||||||
|             var origin = httpContext.Request.Headers["Origin"]; |  | ||||||
|             if (origin == StringValues.Empty) |  | ||||||
|             { |  | ||||||
|                 origin = httpContext.Request.Headers["Host"]; |  | ||||||
|                 if (origin == StringValues.Empty) |  | ||||||
|                 { |  | ||||||
|                     origin = "*"; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             var headers = new Dictionary<string, string>(); |  | ||||||
|             headers.Add("Access-Control-Allow-Origin", origin); |  | ||||||
|             headers.Add("Access-Control-Allow-Credentials", "true"); |  | ||||||
|             headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS"); |  | ||||||
|             headers.Add("Access-Control-Allow-Headers", "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization, Cookie"); |  | ||||||
|             return headers; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         /// <summary> |  | ||||||
|         /// Adds the rest handlers. |  | ||||||
|         /// </summary> |  | ||||||
|         /// <param name="listeners">The web socket listeners.</param> |  | ||||||
|         /// <param name="urlPrefixes">The URL prefixes. See <see cref="UrlPrefixes"/>.</param> |  | ||||||
|         public void Init(IEnumerable<IWebSocketListener> listeners, IEnumerable<string> urlPrefixes) |  | ||||||
|         { |  | ||||||
|             _webSocketListeners = listeners.ToArray(); |  | ||||||
|             UrlPrefixes = urlPrefixes.ToArray(); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         /// <summary> |  | ||||||
|         /// Processes the web socket message received. |  | ||||||
|         /// </summary> |  | ||||||
|         /// <param name="result">The result.</param> |  | ||||||
|         private Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result) |  | ||||||
|         { |  | ||||||
|             if (_disposed) |  | ||||||
|             { |  | ||||||
|                 return Task.CompletedTask; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             IEnumerable<Task> GetTasks() |  | ||||||
|             { |  | ||||||
|                 foreach (var x in _webSocketListeners) |  | ||||||
|                 { |  | ||||||
|                     yield return x.ProcessMessageAsync(result); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             return Task.WhenAll(GetTasks()); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										102
									
								
								Emby.Server.Implementations/HttpServer/WebSocketManager.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								Emby.Server.Implementations/HttpServer/WebSocketManager.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,102 @@ | |||||||
|  | #pragma warning disable CS1591 | ||||||
|  | 
 | ||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.Linq; | ||||||
|  | using System.Net.WebSockets; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  | using Jellyfin.Data.Events; | ||||||
|  | using MediaBrowser.Controller.Net; | ||||||
|  | using Microsoft.AspNetCore.Http; | ||||||
|  | using Microsoft.Extensions.Logging; | ||||||
|  | 
 | ||||||
|  | namespace Emby.Server.Implementations.HttpServer | ||||||
|  | { | ||||||
|  |     public class WebSocketManager : IWebSocketManager | ||||||
|  |     { | ||||||
|  |         private readonly ILogger<WebSocketManager> _logger; | ||||||
|  |         private readonly ILoggerFactory _loggerFactory; | ||||||
|  | 
 | ||||||
|  |         private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>(); | ||||||
|  |         private bool _disposed = false; | ||||||
|  | 
 | ||||||
|  |         public WebSocketManager( | ||||||
|  |             ILogger<WebSocketManager> logger, | ||||||
|  |             ILoggerFactory loggerFactory) | ||||||
|  |         { | ||||||
|  |             _logger = logger; | ||||||
|  |             _loggerFactory = loggerFactory; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected; | ||||||
|  | 
 | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         public async Task WebSocketRequestHandler(HttpContext context) | ||||||
|  |         { | ||||||
|  |             if (_disposed) | ||||||
|  |             { | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 _logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress); | ||||||
|  | 
 | ||||||
|  |                 WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false); | ||||||
|  | 
 | ||||||
|  |                 using var connection = new WebSocketConnection( | ||||||
|  |                     _loggerFactory.CreateLogger<WebSocketConnection>(), | ||||||
|  |                     webSocket, | ||||||
|  |                     context.Connection.RemoteIpAddress, | ||||||
|  |                     context.Request.Query) | ||||||
|  |                 { | ||||||
|  |                     OnReceive = ProcessWebSocketMessageReceived | ||||||
|  |                 }; | ||||||
|  | 
 | ||||||
|  |                 WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection)); | ||||||
|  | 
 | ||||||
|  |                 await connection.ProcessAsync().ConfigureAwait(false); | ||||||
|  |                 _logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress); | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) // Otherwise ASP.Net will ignore the exception | ||||||
|  |             { | ||||||
|  |                 _logger.LogError(ex, "WS {IP} WebSocketRequestHandler error", context.Connection.RemoteIpAddress); | ||||||
|  |                 if (!context.Response.HasStarted) | ||||||
|  |                 { | ||||||
|  |                     context.Response.StatusCode = 500; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         /// <summary> | ||||||
|  |         /// Adds the rest handlers. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="listeners">The web socket listeners.</param> | ||||||
|  |         public void Init(IEnumerable<IWebSocketListener> listeners) | ||||||
|  |         { | ||||||
|  |             _webSocketListeners = listeners.ToArray(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         /// <summary> | ||||||
|  |         /// Processes the web socket message received. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="result">The result.</param> | ||||||
|  |         private Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result) | ||||||
|  |         { | ||||||
|  |             if (_disposed) | ||||||
|  |             { | ||||||
|  |                 return Task.CompletedTask; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             IEnumerable<Task> GetTasks() | ||||||
|  |             { | ||||||
|  |                 foreach (var x in _webSocketListeners) | ||||||
|  |                 { | ||||||
|  |                     yield return x.ProcessMessageAsync(result); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return Task.WhenAll(GetTasks()); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -44,7 +44,7 @@ namespace Emby.Server.Implementations.Session | |||||||
|         private readonly ILogger<SessionWebSocketListener> _logger; |         private readonly ILogger<SessionWebSocketListener> _logger; | ||||||
|         private readonly ILoggerFactory _loggerFactory; |         private readonly ILoggerFactory _loggerFactory; | ||||||
| 
 | 
 | ||||||
|         private readonly IHttpServer _httpServer; |         private readonly IWebSocketManager _webSocketManager; | ||||||
| 
 | 
 | ||||||
|         /// <summary> |         /// <summary> | ||||||
|         /// The KeepAlive cancellation token. |         /// The KeepAlive cancellation token. | ||||||
| @ -72,19 +72,19 @@ namespace Emby.Server.Implementations.Session | |||||||
|         /// <param name="logger">The logger.</param> |         /// <param name="logger">The logger.</param> | ||||||
|         /// <param name="sessionManager">The session manager.</param> |         /// <param name="sessionManager">The session manager.</param> | ||||||
|         /// <param name="loggerFactory">The logger factory.</param> |         /// <param name="loggerFactory">The logger factory.</param> | ||||||
|         /// <param name="httpServer">The HTTP server.</param> |         /// <param name="webSocketManager">The HTTP server.</param> | ||||||
|         public SessionWebSocketListener( |         public SessionWebSocketListener( | ||||||
|             ILogger<SessionWebSocketListener> logger, |             ILogger<SessionWebSocketListener> logger, | ||||||
|             ISessionManager sessionManager, |             ISessionManager sessionManager, | ||||||
|             ILoggerFactory loggerFactory, |             ILoggerFactory loggerFactory, | ||||||
|             IHttpServer httpServer) |             IWebSocketManager webSocketManager) | ||||||
|         { |         { | ||||||
|             _logger = logger; |             _logger = logger; | ||||||
|             _sessionManager = sessionManager; |             _sessionManager = sessionManager; | ||||||
|             _loggerFactory = loggerFactory; |             _loggerFactory = loggerFactory; | ||||||
|             _httpServer = httpServer; |             _webSocketManager = webSocketManager; | ||||||
| 
 | 
 | ||||||
|             httpServer.WebSocketConnected += OnServerManagerWebSocketConnected; |             webSocketManager.WebSocketConnected += OnServerManagerWebSocketConnected; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         private async void OnServerManagerWebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e) |         private async void OnServerManagerWebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e) | ||||||
| @ -121,7 +121,7 @@ namespace Emby.Server.Implementations.Session | |||||||
|         /// <inheritdoc /> |         /// <inheritdoc /> | ||||||
|         public void Dispose() |         public void Dispose() | ||||||
|         { |         { | ||||||
|             _httpServer.WebSocketConnected -= OnServerManagerWebSocketConnected; |             _webSocketManager.WebSocketConnected -= OnServerManagerWebSocketConnected; | ||||||
|             StopKeepAlive(); |             StopKeepAlive(); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,3 +1,4 @@ | |||||||
|  | using Jellyfin.Server.Middleware; | ||||||
| using MediaBrowser.Controller.Configuration; | using MediaBrowser.Controller.Configuration; | ||||||
| using Microsoft.AspNetCore.Builder; | using Microsoft.AspNetCore.Builder; | ||||||
| 
 | 
 | ||||||
| @ -46,5 +47,65 @@ namespace Jellyfin.Server.Extensions | |||||||
|                     c.RoutePrefix = $"{baseUrl}api-docs/redoc"; |                     c.RoutePrefix = $"{baseUrl}api-docs/redoc"; | ||||||
|                 }); |                 }); | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         /// <summary> | ||||||
|  |         /// Adds IP based access validation to the application pipeline. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="appBuilder">The application builder.</param> | ||||||
|  |         /// <returns>The updated application builder.</returns> | ||||||
|  |         public static IApplicationBuilder UseIpBasedAccessValidation(this IApplicationBuilder appBuilder) | ||||||
|  |         { | ||||||
|  |             return appBuilder.UseMiddleware<IpBasedAccessValidationMiddleware>(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         /// <summary> | ||||||
|  |         /// Adds LAN based access filtering to the application pipeline. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="appBuilder">The application builder.</param> | ||||||
|  |         /// <returns>The updated application builder.</returns> | ||||||
|  |         public static IApplicationBuilder UseLanFiltering(this IApplicationBuilder appBuilder) | ||||||
|  |         { | ||||||
|  |             return appBuilder.UseMiddleware<LanFilteringMiddleware>(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         /// <summary> | ||||||
|  |         /// Adds CORS OPTIONS request handling to the application pipeline. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="appBuilder">The application builder.</param> | ||||||
|  |         /// <returns>The updated application builder.</returns> | ||||||
|  |         public static IApplicationBuilder UseCorsOptionsResponse(this IApplicationBuilder appBuilder) | ||||||
|  |         { | ||||||
|  |             return appBuilder.UseMiddleware<CorsOptionsResponseMiddleware>(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         /// <summary> | ||||||
|  |         /// Adds base url redirection to the application pipeline. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="appBuilder">The application builder.</param> | ||||||
|  |         /// <returns>The updated application builder.</returns> | ||||||
|  |         public static IApplicationBuilder UseBaseUrlRedirection(this IApplicationBuilder appBuilder) | ||||||
|  |         { | ||||||
|  |             return appBuilder.UseMiddleware<BaseUrlRedirectionMiddleware>(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         /// <summary> | ||||||
|  |         /// Adds a custom message during server startup to the application pipeline. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="appBuilder">The application builder.</param> | ||||||
|  |         /// <returns>The updated application builder.</returns> | ||||||
|  |         public static IApplicationBuilder UseServerStartupMessage(this IApplicationBuilder appBuilder) | ||||||
|  |         { | ||||||
|  |             return appBuilder.UseMiddleware<ServerStartupMessageMiddleware>(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         /// <summary> | ||||||
|  |         /// Adds a WebSocket request handler to the application pipeline. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="appBuilder">The application builder.</param> | ||||||
|  |         /// <returns>The updated application builder.</returns> | ||||||
|  |         public static IApplicationBuilder UseWebSocketHandler(this IApplicationBuilder appBuilder) | ||||||
|  |         { | ||||||
|  |             return appBuilder.UseMiddleware<WebSocketHandlerMiddleware>(); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										62
									
								
								Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,62 @@ | |||||||
|  | using System; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  | using MediaBrowser.Controller.Configuration; | ||||||
|  | using Microsoft.AspNetCore.Http; | ||||||
|  | using Microsoft.Extensions.Configuration; | ||||||
|  | using Microsoft.Extensions.Logging; | ||||||
|  | using ConfigurationExtensions = MediaBrowser.Controller.Extensions.ConfigurationExtensions; | ||||||
|  | 
 | ||||||
|  | namespace Jellyfin.Server.Middleware | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// Redirect requests without baseurl prefix to the baseurl prefixed URL. | ||||||
|  |     /// </summary> | ||||||
|  |     public class BaseUrlRedirectionMiddleware | ||||||
|  |     { | ||||||
|  |         private readonly RequestDelegate _next; | ||||||
|  |         private readonly ILogger<BaseUrlRedirectionMiddleware> _logger; | ||||||
|  |         private readonly IConfiguration _configuration; | ||||||
|  | 
 | ||||||
|  |         /// <summary> | ||||||
|  |         /// Initializes a new instance of the <see cref="BaseUrlRedirectionMiddleware"/> class. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="next">The next delegate in the pipeline.</param> | ||||||
|  |         /// <param name="logger">The logger.</param> | ||||||
|  |         /// <param name="configuration">The application configuration.</param> | ||||||
|  |         public BaseUrlRedirectionMiddleware( | ||||||
|  |             RequestDelegate next, | ||||||
|  |             ILogger<BaseUrlRedirectionMiddleware> logger, | ||||||
|  |             IConfiguration configuration) | ||||||
|  |         { | ||||||
|  |             _next = next; | ||||||
|  |             _logger = logger; | ||||||
|  |             _configuration = configuration; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         /// <summary> | ||||||
|  |         /// Executes the middleware action. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="httpContext">The current HTTP context.</param> | ||||||
|  |         /// <param name="serverConfigurationManager">The server configuration manager.</param> | ||||||
|  |         /// <returns>The async task.</returns> | ||||||
|  |         public async Task Invoke(HttpContext httpContext, IServerConfigurationManager serverConfigurationManager) | ||||||
|  |         { | ||||||
|  |             var localPath = httpContext.Request.Path.ToString(); | ||||||
|  |             var baseUrlPrefix = serverConfigurationManager.Configuration.BaseUrl; | ||||||
|  | 
 | ||||||
|  |             if (string.Equals(localPath, baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase) | ||||||
|  |                 || string.Equals(localPath, baseUrlPrefix, StringComparison.OrdinalIgnoreCase) | ||||||
|  |                 || string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase) | ||||||
|  |                 || string.IsNullOrEmpty(localPath) | ||||||
|  |                 || !localPath.StartsWith(baseUrlPrefix, StringComparison.OrdinalIgnoreCase)) | ||||||
|  |             { | ||||||
|  |                 // Always redirect back to the default path if the base prefix is invalid or missing | ||||||
|  |                 _logger.LogDebug("Normalizing an URL at {LocalPath}", localPath); | ||||||
|  |                 httpContext.Response.Redirect(baseUrlPrefix + "/" + _configuration[ConfigurationExtensions.DefaultRedirectKey]); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             await _next(httpContext).ConfigureAwait(false); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										69
									
								
								Jellyfin.Server/Middleware/CorsOptionsResponseMiddleware.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								Jellyfin.Server/Middleware/CorsOptionsResponseMiddleware.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,69 @@ | |||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.Net.Mime; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  | using Microsoft.AspNetCore.Http; | ||||||
|  | using Microsoft.Extensions.Primitives; | ||||||
|  | 
 | ||||||
|  | namespace Jellyfin.Server.Middleware | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// Middleware for handling OPTIONS requests. | ||||||
|  |     /// </summary> | ||||||
|  |     public class CorsOptionsResponseMiddleware | ||||||
|  |     { | ||||||
|  |         private readonly RequestDelegate _next; | ||||||
|  | 
 | ||||||
|  |         /// <summary> | ||||||
|  |         /// Initializes a new instance of the <see cref="CorsOptionsResponseMiddleware"/> class. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="next">The next delegate in the pipeline.</param> | ||||||
|  |         public CorsOptionsResponseMiddleware(RequestDelegate next) | ||||||
|  |         { | ||||||
|  |             _next = next; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         /// <summary> | ||||||
|  |         /// Executes the middleware action. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="httpContext">The current HTTP context.</param> | ||||||
|  |         /// <returns>The async task.</returns> | ||||||
|  |         public async Task Invoke(HttpContext httpContext) | ||||||
|  |         { | ||||||
|  |             if (string.Equals(httpContext.Request.Method, HttpMethods.Options, StringComparison.OrdinalIgnoreCase)) | ||||||
|  |             { | ||||||
|  |                 httpContext.Response.StatusCode = 200; | ||||||
|  |                 foreach (var (key, value) in GetDefaultCorsHeaders(httpContext)) | ||||||
|  |                 { | ||||||
|  |                     httpContext.Response.Headers.Add(key, value); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 httpContext.Response.ContentType = MediaTypeNames.Text.Plain; | ||||||
|  |                 await httpContext.Response.WriteAsync(string.Empty, httpContext.RequestAborted).ConfigureAwait(false); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             await _next(httpContext).ConfigureAwait(false); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         private static IDictionary<string, string> GetDefaultCorsHeaders(HttpContext httpContext) | ||||||
|  |         { | ||||||
|  |             var origin = httpContext.Request.Headers["Origin"]; | ||||||
|  |             if (origin == StringValues.Empty) | ||||||
|  |             { | ||||||
|  |                 origin = httpContext.Request.Headers["Host"]; | ||||||
|  |                 if (origin == StringValues.Empty) | ||||||
|  |                 { | ||||||
|  |                     origin = "*"; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             var headers = new Dictionary<string, string>(); | ||||||
|  |             headers.Add("Access-Control-Allow-Origin", origin); | ||||||
|  |             headers.Add("Access-Control-Allow-Credentials", "true"); | ||||||
|  |             headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS"); | ||||||
|  |             headers.Add("Access-Control-Allow-Headers", "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization, Cookie"); | ||||||
|  |             return headers; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -0,0 +1,76 @@ | |||||||
|  | using System.Linq; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  | using MediaBrowser.Common.Extensions; | ||||||
|  | using MediaBrowser.Common.Net; | ||||||
|  | using MediaBrowser.Controller.Configuration; | ||||||
|  | using Microsoft.AspNetCore.Http; | ||||||
|  | 
 | ||||||
|  | namespace Jellyfin.Server.Middleware | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// Validates the IP of requests coming from local networks wrt. remote access. | ||||||
|  |     /// </summary> | ||||||
|  |     public class IpBasedAccessValidationMiddleware | ||||||
|  |     { | ||||||
|  |         private readonly RequestDelegate _next; | ||||||
|  | 
 | ||||||
|  |         /// <summary> | ||||||
|  |         /// Initializes a new instance of the <see cref="IpBasedAccessValidationMiddleware"/> class. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="next">The next delegate in the pipeline.</param> | ||||||
|  |         public IpBasedAccessValidationMiddleware(RequestDelegate next) | ||||||
|  |         { | ||||||
|  |             _next = next; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         /// <summary> | ||||||
|  |         /// Executes the middleware action. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="httpContext">The current HTTP context.</param> | ||||||
|  |         /// <param name="networkManager">The network manager.</param> | ||||||
|  |         /// <param name="serverConfigurationManager">The server configuration manager.</param> | ||||||
|  |         /// <returns>The async task.</returns> | ||||||
|  |         public async Task Invoke(HttpContext httpContext, INetworkManager networkManager, IServerConfigurationManager serverConfigurationManager) | ||||||
|  |         { | ||||||
|  |             if (httpContext.Request.IsLocal()) | ||||||
|  |             { | ||||||
|  |                 await _next(httpContext).ConfigureAwait(false); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             var remoteIp = httpContext.Request.RemoteIp(); | ||||||
|  | 
 | ||||||
|  |             if (serverConfigurationManager.Configuration.EnableRemoteAccess) | ||||||
|  |             { | ||||||
|  |                 var addressFilter = serverConfigurationManager.Configuration.RemoteIPFilter.Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(); | ||||||
|  | 
 | ||||||
|  |                 if (addressFilter.Length > 0 && !networkManager.IsInLocalNetwork(remoteIp)) | ||||||
|  |                 { | ||||||
|  |                     if (serverConfigurationManager.Configuration.IsRemoteIPFilterBlacklist) | ||||||
|  |                     { | ||||||
|  |                         if (networkManager.IsAddressInSubnets(remoteIp, addressFilter)) | ||||||
|  |                         { | ||||||
|  |                             return; | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     else | ||||||
|  |                     { | ||||||
|  |                         if (!networkManager.IsAddressInSubnets(remoteIp, addressFilter)) | ||||||
|  |                         { | ||||||
|  |                             return; | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             else | ||||||
|  |             { | ||||||
|  |                 if (!networkManager.IsInLocalNetwork(remoteIp)) | ||||||
|  |                 { | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             await _next(httpContext).ConfigureAwait(false); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										76
									
								
								Jellyfin.Server/Middleware/LanFilteringMiddleware.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								Jellyfin.Server/Middleware/LanFilteringMiddleware.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,76 @@ | |||||||
|  | using System; | ||||||
|  | using System.Linq; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  | using MediaBrowser.Common.Net; | ||||||
|  | using MediaBrowser.Controller.Configuration; | ||||||
|  | using Microsoft.AspNetCore.Http; | ||||||
|  | 
 | ||||||
|  | namespace Jellyfin.Server.Middleware | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// Validates the LAN host IP based on application configuration. | ||||||
|  |     /// </summary> | ||||||
|  |     public class LanFilteringMiddleware | ||||||
|  |     { | ||||||
|  |         private readonly RequestDelegate _next; | ||||||
|  | 
 | ||||||
|  |         /// <summary> | ||||||
|  |         /// Initializes a new instance of the <see cref="LanFilteringMiddleware"/> class. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="next">The next delegate in the pipeline.</param> | ||||||
|  |         public LanFilteringMiddleware(RequestDelegate next) | ||||||
|  |         { | ||||||
|  |             _next = next; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         /// <summary> | ||||||
|  |         /// Executes the middleware action. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="httpContext">The current HTTP context.</param> | ||||||
|  |         /// <param name="networkManager">The network manager.</param> | ||||||
|  |         /// <param name="serverConfigurationManager">The server configuration manager.</param> | ||||||
|  |         /// <returns>The async task.</returns> | ||||||
|  |         public async Task Invoke(HttpContext httpContext, INetworkManager networkManager, IServerConfigurationManager serverConfigurationManager) | ||||||
|  |         { | ||||||
|  |             var currentHost = httpContext.Request.Host.ToString(); | ||||||
|  |             var hosts = serverConfigurationManager | ||||||
|  |                 .Configuration | ||||||
|  |                 .LocalNetworkAddresses | ||||||
|  |                 .Select(NormalizeConfiguredLocalAddress) | ||||||
|  |                 .ToList(); | ||||||
|  | 
 | ||||||
|  |             if (hosts.Count == 0) | ||||||
|  |             { | ||||||
|  |                 await _next(httpContext).ConfigureAwait(false); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             currentHost ??= string.Empty; | ||||||
|  | 
 | ||||||
|  |             if (networkManager.IsInPrivateAddressSpace(currentHost)) | ||||||
|  |             { | ||||||
|  |                 hosts.Add("localhost"); | ||||||
|  |                 hosts.Add("127.0.0.1"); | ||||||
|  | 
 | ||||||
|  |                 if (hosts.All(i => currentHost.IndexOf(i, StringComparison.OrdinalIgnoreCase) == -1)) | ||||||
|  |                 { | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             await _next(httpContext).ConfigureAwait(false); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         private static string NormalizeConfiguredLocalAddress(string address) | ||||||
|  |         { | ||||||
|  |             var add = address.AsSpan().Trim('/'); | ||||||
|  |             int index = add.IndexOf('/'); | ||||||
|  |             if (index != -1) | ||||||
|  |             { | ||||||
|  |                 add = add.Slice(index + 1); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return add.TrimStart('/').ToString(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										38
									
								
								Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,38 @@ | |||||||
|  | using System.Net.Mime; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  | using MediaBrowser.Model.Globalization; | ||||||
|  | using Microsoft.AspNetCore.Http; | ||||||
|  | 
 | ||||||
|  | namespace Jellyfin.Server.Middleware | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// Shows a custom message during server startup. | ||||||
|  |     /// </summary> | ||||||
|  |     public class ServerStartupMessageMiddleware | ||||||
|  |     { | ||||||
|  |         private readonly RequestDelegate _next; | ||||||
|  | 
 | ||||||
|  |         /// <summary> | ||||||
|  |         /// Initializes a new instance of the <see cref="ServerStartupMessageMiddleware"/> class. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="next">The next delegate in the pipeline.</param> | ||||||
|  |         public ServerStartupMessageMiddleware(RequestDelegate next) | ||||||
|  |         { | ||||||
|  |             _next = next; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         /// <summary> | ||||||
|  |         /// Executes the middleware action. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="httpContext">The current HTTP context.</param> | ||||||
|  |         /// <param name="localizationManager">The localization manager.</param> | ||||||
|  |         /// <returns>The async task.</returns> | ||||||
|  |         public async Task Invoke(HttpContext httpContext, ILocalizationManager localizationManager) | ||||||
|  |         { | ||||||
|  |             var message = localizationManager.GetLocalizedString("StartupEmbyServerIsLoading"); | ||||||
|  |             httpContext.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; | ||||||
|  |             httpContext.Response.ContentType = MediaTypeNames.Text.Html; | ||||||
|  |             await httpContext.Response.WriteAsync(message, httpContext.RequestAborted).ConfigureAwait(false); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										40
									
								
								Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | |||||||
|  | using System.Threading.Tasks; | ||||||
|  | using MediaBrowser.Controller.Net; | ||||||
|  | using Microsoft.AspNetCore.Http; | ||||||
|  | 
 | ||||||
|  | namespace Jellyfin.Server.Middleware | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// Handles WebSocket requests. | ||||||
|  |     /// </summary> | ||||||
|  |     public class WebSocketHandlerMiddleware | ||||||
|  |     { | ||||||
|  |         private readonly RequestDelegate _next; | ||||||
|  | 
 | ||||||
|  |         /// <summary> | ||||||
|  |         /// Initializes a new instance of the <see cref="WebSocketHandlerMiddleware"/> class. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="next">The next delegate in the pipeline.</param> | ||||||
|  |         public WebSocketHandlerMiddleware(RequestDelegate next) | ||||||
|  |         { | ||||||
|  |             _next = next; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         /// <summary> | ||||||
|  |         /// Executes the middleware action. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="httpContext">The current HTTP context.</param> | ||||||
|  |         /// <param name="webSocketManager">The WebSocket connection manager.</param> | ||||||
|  |         /// <returns>The async task.</returns> | ||||||
|  |         public async Task Invoke(HttpContext httpContext, IWebSocketManager webSocketManager) | ||||||
|  |         { | ||||||
|  |             if (!httpContext.WebSockets.IsWebSocketRequest) | ||||||
|  |             { | ||||||
|  |                 await _next(httpContext).ConfigureAwait(false); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             await webSocketManager.WebSocketRequestHandler(httpContext).ConfigureAwait(false); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -11,7 +11,6 @@ using System.Threading; | |||||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||||
| using CommandLine; | using CommandLine; | ||||||
| using Emby.Server.Implementations; | using Emby.Server.Implementations; | ||||||
| using Emby.Server.Implementations.HttpServer; |  | ||||||
| using Emby.Server.Implementations.IO; | using Emby.Server.Implementations.IO; | ||||||
| using Emby.Server.Implementations.Networking; | using Emby.Server.Implementations.Networking; | ||||||
| using Jellyfin.Api.Controllers; | using Jellyfin.Api.Controllers; | ||||||
| @ -28,6 +27,7 @@ using Microsoft.Extensions.Logging.Abstractions; | |||||||
| using Serilog; | using Serilog; | ||||||
| using Serilog.Extensions.Logging; | using Serilog.Extensions.Logging; | ||||||
| using SQLitePCL; | using SQLitePCL; | ||||||
|  | using ConfigurationExtensions = MediaBrowser.Controller.Extensions.ConfigurationExtensions; | ||||||
| using ILogger = Microsoft.Extensions.Logging.ILogger; | using ILogger = Microsoft.Extensions.Logging.ILogger; | ||||||
| 
 | 
 | ||||||
| namespace Jellyfin.Server | namespace Jellyfin.Server | ||||||
| @ -594,7 +594,7 @@ namespace Jellyfin.Server | |||||||
|             var inMemoryDefaultConfig = ConfigurationOptions.DefaultConfiguration; |             var inMemoryDefaultConfig = ConfigurationOptions.DefaultConfiguration; | ||||||
|             if (startupConfig != null && !startupConfig.HostWebClient()) |             if (startupConfig != null && !startupConfig.HostWebClient()) | ||||||
|             { |             { | ||||||
|                 inMemoryDefaultConfig[HttpListenerHost.DefaultRedirectKey] = "api-docs/swagger"; |                 inMemoryDefaultConfig[ConfigurationExtensions.DefaultRedirectKey] = "api-docs/swagger"; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             return config |             return config | ||||||
|  | |||||||
| @ -84,11 +84,9 @@ namespace Jellyfin.Server | |||||||
|         /// </summary> |         /// </summary> | ||||||
|         /// <param name="app">The application builder.</param> |         /// <param name="app">The application builder.</param> | ||||||
|         /// <param name="env">The webhost environment.</param> |         /// <param name="env">The webhost environment.</param> | ||||||
|         /// <param name="serverApplicationHost">The server application host.</param> |  | ||||||
|         public void Configure( |         public void Configure( | ||||||
|             IApplicationBuilder app, |             IApplicationBuilder app, | ||||||
|             IWebHostEnvironment env, |             IWebHostEnvironment env) | ||||||
|             IServerApplicationHost serverApplicationHost) |  | ||||||
|         { |         { | ||||||
|             if (env.IsDevelopment()) |             if (env.IsDevelopment()) | ||||||
|             { |             { | ||||||
| @ -120,7 +118,11 @@ namespace Jellyfin.Server | |||||||
|                 app.UseHttpMetrics(); |                 app.UseHttpMetrics(); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             app.Use(serverApplicationHost.ExecuteHttpHandlerAsync); |             app.UseLanFiltering(); | ||||||
|  |             app.UseIpBasedAccessValidation(); | ||||||
|  |             app.UseCorsOptionsResponse(); | ||||||
|  |             app.UseBaseUrlRedirection(); | ||||||
|  |             app.UseWebSocketHandler(); | ||||||
| 
 | 
 | ||||||
|             app.UseEndpoints(endpoints => |             app.UseEndpoints(endpoints => | ||||||
|             { |             { | ||||||
| @ -131,6 +133,8 @@ namespace Jellyfin.Server | |||||||
|                 } |                 } | ||||||
|             }); |             }); | ||||||
| 
 | 
 | ||||||
|  |             app.UseServerStartupMessage(); | ||||||
|  | 
 | ||||||
|             // Add type descriptor for legacy datetime parsing. |             // Add type descriptor for legacy datetime parsing. | ||||||
|             TypeDescriptor.AddAttributes(typeof(DateTime?), new TypeConverterAttribute(typeof(DateTimeTypeConverter))); |             TypeDescriptor.AddAttributes(typeof(DateTime?), new TypeConverterAttribute(typeof(DateTimeTypeConverter))); | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -8,6 +8,12 @@ namespace MediaBrowser.Controller.Extensions | |||||||
|     /// </summary> |     /// </summary> | ||||||
|     public static class ConfigurationExtensions |     public static class ConfigurationExtensions | ||||||
|     { |     { | ||||||
|  |         /// <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 = "DefaultRedirectPath"; | ||||||
|  | 
 | ||||||
|         /// <summary> |         /// <summary> | ||||||
|         /// The key for a setting that indicates whether the application should host web client content. |         /// The key for a setting that indicates whether the application should host web client content. | ||||||
|         /// </summary> |         /// </summary> | ||||||
|  | |||||||
| @ -117,8 +117,7 @@ namespace MediaBrowser.Controller | |||||||
|         IEnumerable<WakeOnLanInfo> GetWakeOnLanInfo(); |         IEnumerable<WakeOnLanInfo> GetWakeOnLanInfo(); | ||||||
| 
 | 
 | ||||||
|         string ExpandVirtualPath(string path); |         string ExpandVirtualPath(string path); | ||||||
|         string ReverseVirtualPath(string path); |  | ||||||
| 
 | 
 | ||||||
|         Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next); |         string ReverseVirtualPath(string path); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,50 +0,0 @@ | |||||||
| using System; |  | ||||||
| using System.Collections.Generic; |  | ||||||
| using System.Threading.Tasks; |  | ||||||
| using Jellyfin.Data.Events; |  | ||||||
| using Microsoft.AspNetCore.Http; |  | ||||||
| 
 |  | ||||||
| namespace MediaBrowser.Controller.Net |  | ||||||
| { |  | ||||||
|     /// <summary> |  | ||||||
|     /// Interface IHttpServer. |  | ||||||
|     /// </summary> |  | ||||||
|     public interface IHttpServer |  | ||||||
|     { |  | ||||||
|         /// <summary> |  | ||||||
|         /// Gets the URL prefix. |  | ||||||
|         /// </summary> |  | ||||||
|         /// <value>The URL prefix.</value> |  | ||||||
|         string[] UrlPrefixes { get; } |  | ||||||
| 
 |  | ||||||
|         /// <summary> |  | ||||||
|         /// Occurs when [web socket connected]. |  | ||||||
|         /// </summary> |  | ||||||
|         event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected; |  | ||||||
| 
 |  | ||||||
|         /// <summary> |  | ||||||
|         /// Inits this instance. |  | ||||||
|         /// </summary> |  | ||||||
|         void Init(IEnumerable<IWebSocketListener> listener, IEnumerable<string> urlPrefixes); |  | ||||||
| 
 |  | ||||||
|         /// <summary> |  | ||||||
|         /// If set, all requests will respond with this message. |  | ||||||
|         /// </summary> |  | ||||||
|         string GlobalResponse { get; set; } |  | ||||||
| 
 |  | ||||||
|         /// <summary> |  | ||||||
|         /// The HTTP request handler. |  | ||||||
|         /// </summary> |  | ||||||
|         /// <param name="context">The current HTTP context.</param> |  | ||||||
|         /// <param name="next">The next middleware in the ASP.NET pipeline.</param> |  | ||||||
|         /// <returns>The task.</returns> |  | ||||||
|         Task RequestHandler(HttpContext context, Func<Task> next); |  | ||||||
| 
 |  | ||||||
|         /// <summary> |  | ||||||
|         /// Get the default CORS headers. |  | ||||||
|         /// </summary> |  | ||||||
|         /// <param name="httpContext">The HTTP context of the current request.</param> |  | ||||||
|         /// <returns>The default CORS headers for the context.</returns> |  | ||||||
|         IDictionary<string, string> GetDefaultCorsHeaders(HttpContext httpContext); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										32
									
								
								MediaBrowser.Controller/Net/IWebSocketManager.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								MediaBrowser.Controller/Net/IWebSocketManager.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,32 @@ | |||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  | using Jellyfin.Data.Events; | ||||||
|  | using Microsoft.AspNetCore.Http; | ||||||
|  | 
 | ||||||
|  | namespace MediaBrowser.Controller.Net | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// Interface IHttpServer. | ||||||
|  |     /// </summary> | ||||||
|  |     public interface IWebSocketManager | ||||||
|  |     { | ||||||
|  |         /// <summary> | ||||||
|  |         /// Occurs when [web socket connected]. | ||||||
|  |         /// </summary> | ||||||
|  |         event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected; | ||||||
|  | 
 | ||||||
|  |         /// <summary> | ||||||
|  |         /// Inits this instance. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="listeners">The websocket listeners.</param> | ||||||
|  |         void Init(IEnumerable<IWebSocketListener> listeners); | ||||||
|  | 
 | ||||||
|  |         /// <summary> | ||||||
|  |         /// The HTTP request handler. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="context">The current HTTP context.</param> | ||||||
|  |         /// <returns>The task.</returns> | ||||||
|  |         Task WebSocketRequestHandler(HttpContext context); | ||||||
|  |     } | ||||||
|  | } | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user