mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-03-06 10:16:18 +00:00
Merge branch 'master' into buffer
This commit is contained in:
@@ -32,12 +32,12 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
/// <summary>
|
||||
/// The _options
|
||||
/// The _options.
|
||||
/// </summary>
|
||||
private readonly IDictionary<string, string> _options = new Dictionary<string, string>();
|
||||
|
||||
/// <summary>
|
||||
/// The _requested ranges
|
||||
/// The _requested ranges.
|
||||
/// </summary>
|
||||
private List<KeyValuePair<long, long?>> _requestedRanges;
|
||||
|
||||
|
||||
@@ -6,14 +6,16 @@ using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Sockets;
|
||||
using System.Net.WebSockets;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Server.Implementations.Net;
|
||||
using Emby.Server.Implementations.Services;
|
||||
using Emby.Server.Implementations.SocketSharp;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Authentication;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Events;
|
||||
@@ -21,15 +23,17 @@ using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Model.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using ServiceStack.Text.Jsv;
|
||||
|
||||
namespace Emby.Server.Implementations.HttpServer
|
||||
{
|
||||
public class HttpListenerHost : IHttpServer, IDisposable
|
||||
public class HttpListenerHost : IHttpServer
|
||||
{
|
||||
/// <summary>
|
||||
/// The key for a setting that specifies the default redirect path
|
||||
@@ -37,18 +41,18 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// </summary>
|
||||
public const string DefaultRedirectKey = "HttpListenerHost:DefaultRedirectPath";
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private readonly ILogger<HttpListenerHost> _logger;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly INetworkManager _networkManager;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IXmlSerializer _xmlSerializer;
|
||||
private readonly IHttpListener _socketListener;
|
||||
private readonly Func<Type, Func<string, object>> _funcParseFn;
|
||||
private readonly string _defaultRedirectPath;
|
||||
private readonly string _baseUrlPrefix;
|
||||
|
||||
private readonly Dictionary<Type, Type> _serviceOperationsMap = new Dictionary<Type, Type>();
|
||||
private readonly List<IWebSocketConnection> _webSocketConnections = new List<IWebSocketConnection>();
|
||||
private readonly IHostEnvironment _hostEnvironment;
|
||||
|
||||
private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
|
||||
@@ -62,10 +66,10 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
INetworkManager networkManager,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IXmlSerializer xmlSerializer,
|
||||
IHttpListener socketListener,
|
||||
ILocalizationManager localizationManager,
|
||||
ServiceController serviceController,
|
||||
IHostEnvironment hostEnvironment)
|
||||
IHostEnvironment hostEnvironment,
|
||||
ILoggerFactory loggerFactory)
|
||||
{
|
||||
_appHost = applicationHost;
|
||||
_logger = logger;
|
||||
@@ -75,11 +79,9 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
_networkManager = networkManager;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_xmlSerializer = xmlSerializer;
|
||||
_socketListener = socketListener;
|
||||
ServiceController = serviceController;
|
||||
|
||||
_socketListener.WebSocketConnected = OnWebSocketConnected;
|
||||
_hostEnvironment = hostEnvironment;
|
||||
_loggerFactory = loggerFactory;
|
||||
|
||||
_funcParseFn = t => s => JsvReader.GetParseFn(t)(s);
|
||||
|
||||
@@ -171,38 +173,6 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
return attributes;
|
||||
}
|
||||
|
||||
private void OnWebSocketConnected(WebSocketConnectEventArgs e)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var connection = new WebSocketConnection(e.WebSocket, e.Endpoint, _jsonSerializer, _logger)
|
||||
{
|
||||
OnReceive = ProcessWebSocketMessageReceived,
|
||||
Url = e.Url,
|
||||
QueryString = e.QueryString
|
||||
};
|
||||
|
||||
connection.Closed += OnConnectionClosed;
|
||||
|
||||
lock (_webSocketConnections)
|
||||
{
|
||||
_webSocketConnections.Add(connection);
|
||||
}
|
||||
|
||||
WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection));
|
||||
}
|
||||
|
||||
private void OnConnectionClosed(object sender, EventArgs e)
|
||||
{
|
||||
lock (_webSocketConnections)
|
||||
{
|
||||
_webSocketConnections.Remove((IWebSocketConnection)sender);
|
||||
}
|
||||
}
|
||||
|
||||
private static Exception GetActualException(Exception ex)
|
||||
{
|
||||
if (ex is AggregateException agg)
|
||||
@@ -230,7 +200,8 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
switch (ex)
|
||||
{
|
||||
case ArgumentException _: return 400;
|
||||
case SecurityException _: return 401;
|
||||
case AuthenticationException _: return 401;
|
||||
case SecurityException _: return 403;
|
||||
case DirectoryNotFoundException _:
|
||||
case FileNotFoundException _:
|
||||
case ResourceNotFoundException _: return 404;
|
||||
@@ -239,81 +210,46 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ErrorHandler(Exception ex, IRequest httpReq, bool logExceptionStackTrace, string urlToLog)
|
||||
private async Task ErrorHandler(Exception ex, IRequest httpReq, int statusCode, string urlToLog, bool ignoreStackTrace)
|
||||
{
|
||||
try
|
||||
if (ignoreStackTrace)
|
||||
{
|
||||
ex = GetActualException(ex);
|
||||
|
||||
if (logExceptionStackTrace)
|
||||
{
|
||||
_logger.LogError(ex, "Error processing request. URL: {Url}", urlToLog);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("Error processing request: {Message}. URL: {Url}", ex.Message.TrimEnd('.'), urlToLog);
|
||||
}
|
||||
|
||||
var httpRes = httpReq.Response;
|
||||
|
||||
if (httpRes.HasStarted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var statusCode = GetStatusCode(ex);
|
||||
httpRes.StatusCode = statusCode;
|
||||
|
||||
var errContent = NormalizeExceptionMessage(ex.Message);
|
||||
httpRes.ContentType = "text/plain";
|
||||
httpRes.ContentLength = errContent.Length;
|
||||
await httpRes.WriteAsync(errContent).ConfigureAwait(false);
|
||||
_logger.LogError("Error processing request: {Message}. URL: {Url}", ex.Message.TrimEnd('.'), urlToLog);
|
||||
}
|
||||
catch (Exception errorEx)
|
||||
else
|
||||
{
|
||||
_logger.LogError(errorEx, "Error this.ProcessRequest(context)(Exception while writing error to the response). URL: {Url}", urlToLog);
|
||||
_logger.LogError(ex, "Error processing request. URL: {Url}", urlToLog);
|
||||
}
|
||||
|
||||
var httpRes = httpReq.Response;
|
||||
|
||||
if (httpRes.HasStarted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
httpRes.StatusCode = statusCode;
|
||||
|
||||
var errContent = _hostEnvironment.IsDevelopment()
|
||||
? (NormalizeExceptionMessage(ex) ?? string.Empty)
|
||||
: "Error processing request.";
|
||||
httpRes.ContentType = "text/plain";
|
||||
httpRes.ContentLength = errContent.Length;
|
||||
await httpRes.WriteAsync(errContent).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private string NormalizeExceptionMessage(string msg)
|
||||
private string NormalizeExceptionMessage(Exception ex)
|
||||
{
|
||||
if (msg == null)
|
||||
// Do not expose the exception message for AuthenticationException
|
||||
if (ex is AuthenticationException)
|
||||
{
|
||||
return string.Empty;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Strip any information we don't want to reveal
|
||||
|
||||
msg = msg.Replace(_config.ApplicationPaths.ProgramSystemPath, string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
msg = msg.Replace(_config.ApplicationPaths.ProgramDataPath, string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shut down the Web Service
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
List<IWebSocketConnection> connections;
|
||||
|
||||
lock (_webSocketConnections)
|
||||
{
|
||||
connections = _webSocketConnections.ToList();
|
||||
_webSocketConnections.Clear();
|
||||
}
|
||||
|
||||
foreach (var connection in connections)
|
||||
{
|
||||
try
|
||||
{
|
||||
connection.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error disposing connection");
|
||||
}
|
||||
}
|
||||
return ex.Message
|
||||
?.Replace(_config.ApplicationPaths.ProgramSystemPath, string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace(_config.ApplicationPaths.ProgramDataPath, string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static string RemoveQueryStringByKey(string url, string key)
|
||||
@@ -425,33 +361,52 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate a connection from a remote IP address to a URL to see if a redirection to HTTPS is required.
|
||||
/// </summary>
|
||||
/// <returns>True if the request is valid, or false if the request is not valid and an HTTPS redirect is required.</returns>
|
||||
private bool ValidateSsl(string remoteIp, string urlString)
|
||||
{
|
||||
if (_config.Configuration.RequireHttps && _appHost.EnableHttps && !_config.Configuration.IsBehindProxy)
|
||||
if (_config.Configuration.RequireHttps
|
||||
&& _appHost.ListenWithHttps
|
||||
&& !urlString.Contains("https://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (urlString.IndexOf("https://", StringComparison.OrdinalIgnoreCase) == -1)
|
||||
// These are hacks, but if these ever occur on ipv6 in the local network they could be incorrectly redirected
|
||||
if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1
|
||||
|| urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
// These are hacks, but if these ever occur on ipv6 in the local network they could be incorrectly redirected
|
||||
if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1
|
||||
|| urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!_networkManager.IsInLocalNetwork(remoteIp))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!_networkManager.IsInLocalNetwork(remoteIp))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task RequestHandler(HttpContext context)
|
||||
{
|
||||
if (context.WebSockets.IsWebSocketRequest)
|
||||
{
|
||||
return WebSocketRequestHandler(context);
|
||||
}
|
||||
|
||||
var request = context.Request;
|
||||
var response = context.Response;
|
||||
var localPath = context.Request.Path.ToString();
|
||||
|
||||
var req = new WebSocketSharpRequest(request, response, request.Path);
|
||||
return RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overridable method that can be used to implement a custom handler.
|
||||
/// </summary>
|
||||
public async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken)
|
||||
private async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken)
|
||||
{
|
||||
var stopWatch = new Stopwatch();
|
||||
stopWatch.Start();
|
||||
@@ -494,9 +449,11 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
httpRes.StatusCode = 200;
|
||||
httpRes.Headers.Add("Access-Control-Allow-Origin", "*");
|
||||
httpRes.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
|
||||
httpRes.Headers.Add("Access-Control-Allow-Headers", "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization");
|
||||
foreach(var (key, value) in GetDefaultCorsHeaders(httpReq))
|
||||
{
|
||||
httpRes.Headers.Add(key, value);
|
||||
}
|
||||
|
||||
httpRes.ContentType = "text/plain";
|
||||
await httpRes.WriteAsync(string.Empty, cancellationToken).ConfigureAwait(false);
|
||||
return;
|
||||
@@ -536,22 +493,50 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception requestEx)
|
||||
{
|
||||
// Do not handle exceptions manually when in development mode
|
||||
// The framework-defined development exception page will be returned instead
|
||||
if (_hostEnvironment.IsDevelopment())
|
||||
try
|
||||
{
|
||||
throw;
|
||||
}
|
||||
var requestInnerEx = GetActualException(requestEx);
|
||||
var statusCode = GetStatusCode(requestInnerEx);
|
||||
|
||||
bool ignoreStackTrace =
|
||||
ex is SocketException
|
||||
|| ex is IOException
|
||||
|| ex is OperationCanceledException
|
||||
|| ex is SecurityException
|
||||
|| ex is FileNotFoundException;
|
||||
await ErrorHandler(ex, httpReq, !ignoreStackTrace, urlToLog).ConfigureAwait(false);
|
||||
foreach (var (key, value) in GetDefaultCorsHeaders(httpReq))
|
||||
{
|
||||
if (!httpRes.Headers.ContainsKey(key))
|
||||
{
|
||||
httpRes.Headers.Add(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
bool ignoreStackTrace =
|
||||
requestInnerEx is SocketException
|
||||
|| requestInnerEx is IOException
|
||||
|| requestInnerEx is OperationCanceledException
|
||||
|| requestInnerEx is SecurityException
|
||||
|| requestInnerEx is AuthenticationException
|
||||
|| requestInnerEx is FileNotFoundException;
|
||||
|
||||
// Do not handle 500 server exceptions manually when in development mode.
|
||||
// Instead, re-throw the exception so it can be handled by the DeveloperExceptionPageMiddleware.
|
||||
// However, do not use the DeveloperExceptionPageMiddleware when the stack trace should be ignored,
|
||||
// because it will log the stack trace when it handles the exception.
|
||||
if (statusCode == 500 && !ignoreStackTrace && _hostEnvironment.IsDevelopment())
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
await ErrorHandler(requestInnerEx, httpReq, statusCode, urlToLog, ignoreStackTrace).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception handlerException)
|
||||
{
|
||||
var aggregateEx = new AggregateException("Error while handling request exception", requestEx, handlerException);
|
||||
_logger.LogError(aggregateEx, "Error while handling exception in response to {Url}", urlToLog);
|
||||
|
||||
if (_hostEnvironment.IsDevelopment())
|
||||
{
|
||||
throw aggregateEx;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -569,6 +554,68 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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>
|
||||
/// Get the default CORS headers.
|
||||
/// </summary>
|
||||
/// <param name="req"></param>
|
||||
/// <returns></returns>
|
||||
public IDictionary<string, string> GetDefaultCorsHeaders(IRequest req)
|
||||
{
|
||||
var origin = req.Headers["Origin"];
|
||||
if (origin == StringValues.Empty)
|
||||
{
|
||||
origin = req.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;
|
||||
}
|
||||
|
||||
// Entry point for HttpListener
|
||||
public ServiceHandler GetServiceHandler(IHttpRequest httpReq)
|
||||
{
|
||||
@@ -615,7 +662,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
|
||||
ResponseFilters = new Action<IRequest, HttpResponse, object>[]
|
||||
{
|
||||
new ResponseFilter(_logger).FilterResponse
|
||||
new ResponseFilter(this, _logger).FilterResponse
|
||||
};
|
||||
}
|
||||
|
||||
@@ -676,11 +723,6 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
return _jsonSerializer.DeserializeFromStreamAsync(stream, type);
|
||||
}
|
||||
|
||||
public Task ProcessWebSocketRequest(HttpContext context)
|
||||
{
|
||||
return _socketListener.ProcessWebSocketRequest(context);
|
||||
}
|
||||
|
||||
private string NormalizeEmbyRoutePath(string path)
|
||||
{
|
||||
_logger.LogDebug("Normalizing /emby route");
|
||||
@@ -699,28 +741,6 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
return _baseUrlPrefix + NormalizeUrlPath(path);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes the web socket message received.
|
||||
/// </summary>
|
||||
@@ -732,8 +752,6 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Websocket message received: {0}", result.MessageType);
|
||||
|
||||
IEnumerable<Task> GetTasks()
|
||||
{
|
||||
foreach (var x in _webSocketListeners)
|
||||
|
||||
@@ -28,10 +28,16 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// </summary>
|
||||
public class HttpResultFactory : IHttpResultFactory
|
||||
{
|
||||
// Last-Modified and If-Modified-Since must follow strict date format,
|
||||
// see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since
|
||||
private const string HttpDateFormat = "ddd, dd MMM yyyy HH:mm:ss \"GMT\"";
|
||||
// We specifically use en-US culture because both day of week and month names require it
|
||||
private static readonly CultureInfo _enUSculture = new CultureInfo("en-US", false);
|
||||
|
||||
/// <summary>
|
||||
/// The logger.
|
||||
/// </summary>
|
||||
private readonly ILogger _logger;
|
||||
private readonly ILogger<HttpResultFactory> _logger;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IStreamHelper _streamHelper;
|
||||
@@ -44,12 +50,13 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
_fileSystem = fileSystem;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_streamHelper = streamHelper;
|
||||
_logger = loggerfactory.CreateLogger("HttpResultFactory");
|
||||
_logger = loggerfactory.CreateLogger<HttpResultFactory>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the result.
|
||||
/// </summary>
|
||||
/// <param name="requestContext">The request context.</param>
|
||||
/// <param name="content">The content.</param>
|
||||
/// <param name="contentType">Type of the content.</param>
|
||||
/// <param name="responseHeaders">The response headers.</param>
|
||||
@@ -249,16 +256,20 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
{
|
||||
var acceptEncoding = request.Headers[HeaderNames.AcceptEncoding].ToString();
|
||||
|
||||
if (string.IsNullOrEmpty(acceptEncoding))
|
||||
if (!string.IsNullOrEmpty(acceptEncoding))
|
||||
{
|
||||
//if (_brotliCompressor != null && acceptEncoding.IndexOf("br", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
// if (_brotliCompressor != null && acceptEncoding.IndexOf("br", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
// return "br";
|
||||
|
||||
if (acceptEncoding.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
if (acceptEncoding.Contains("deflate", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "deflate";
|
||||
}
|
||||
|
||||
if (acceptEncoding.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
if (acceptEncoding.Contains("gzip", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "gzip";
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -420,7 +431,11 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
|
||||
if (!noCache)
|
||||
{
|
||||
DateTime.TryParse(requestContext.Headers[HeaderNames.IfModifiedSince], out var ifModifiedSinceHeader);
|
||||
if (!DateTime.TryParseExact(requestContext.Headers[HeaderNames.IfModifiedSince], HttpDateFormat, _enUSculture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var ifModifiedSinceHeader))
|
||||
{
|
||||
_logger.LogDebug("Failed to parse If-Modified-Since header date: {0}", requestContext.Headers[HeaderNames.IfModifiedSince]);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (IsNotModified(ifModifiedSinceHeader, options.CacheDuration, options.DateLastModified))
|
||||
{
|
||||
@@ -631,7 +646,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
|
||||
if (lastModifiedDate.HasValue)
|
||||
{
|
||||
responseHeaders[HeaderNames.LastModified] = lastModifiedDate.Value.ToString(CultureInfo.InvariantCulture);
|
||||
responseHeaders[HeaderNames.LastModified] = lastModifiedDate.Value.ToUniversalTime().ToString(HttpDateFormat, _enUSculture);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -680,7 +695,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that
|
||||
/// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that.
|
||||
/// </summary>
|
||||
/// <param name="date">The date.</param>
|
||||
/// <returns>DateTime.</returns>
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Server.Implementations.Net;
|
||||
using MediaBrowser.Model.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Emby.Server.Implementations.HttpServer
|
||||
{
|
||||
public interface IHttpListener : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the error handler.
|
||||
/// </summary>
|
||||
/// <value>The error handler.</value>
|
||||
Func<Exception, IRequest, bool, bool, Task> ErrorHandler { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the request handler.
|
||||
/// </summary>
|
||||
/// <value>The request handler.</value>
|
||||
Func<IHttpRequest, string, string, string, CancellationToken, Task> RequestHandler { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the web socket handler.
|
||||
/// </summary>
|
||||
/// <value>The web socket handler.</value>
|
||||
Action<WebSocketConnectEventArgs> WebSocketConnected { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Stops this instance.
|
||||
/// </summary>
|
||||
Task Stop();
|
||||
|
||||
Task ProcessWebSocketRequest(HttpContext ctx);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -13,14 +14,17 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// </summary>
|
||||
public class ResponseFilter
|
||||
{
|
||||
private readonly IHttpServer _server;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ResponseFilter"/> class.
|
||||
/// </summary>
|
||||
/// <param name="server">The HTTP server.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public ResponseFilter(ILogger logger)
|
||||
public ResponseFilter(IHttpServer server, ILogger logger)
|
||||
{
|
||||
_server = server;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -32,10 +36,16 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// <param name="dto">The dto.</param>
|
||||
public void FilterResponse(IRequest req, HttpResponse res, object dto)
|
||||
{
|
||||
foreach(var (key, value) in _server.GetDefaultCorsHeaders(req))
|
||||
{
|
||||
res.Headers.Add(key, value);
|
||||
}
|
||||
// Try to prevent compatibility view
|
||||
res.Headers.Add("Access-Control-Allow-Headers", "Accept, Accept-Language, Authorization, Cache-Control, Content-Disposition, Content-Encoding, Content-Language, Content-Length, Content-MD5, Content-Range, Content-Type, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, X-Emby-Authorization");
|
||||
res.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
|
||||
res.Headers.Add("Access-Control-Allow-Origin", "*");
|
||||
res.Headers["Access-Control-Allow-Headers"] = ("Accept, Accept-Language, Authorization, Cache-Control, " +
|
||||
"Content-Disposition, Content-Encoding, Content-Language, Content-Length, Content-MD5, Content-Range, " +
|
||||
"Content-Type, Cookie, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, " +
|
||||
"Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, " +
|
||||
"X-Emby-Authorization");
|
||||
|
||||
if (dto is Exception exception)
|
||||
{
|
||||
@@ -82,6 +92,10 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
{
|
||||
return null;
|
||||
}
|
||||
else if (inString.Length == 0)
|
||||
{
|
||||
return inString;
|
||||
}
|
||||
|
||||
var newString = new StringBuilder(inString.Length);
|
||||
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Emby.Server.Implementations.SocketSharp;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Authentication;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Security;
|
||||
using MediaBrowser.Controller.Session;
|
||||
@@ -44,7 +46,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
|
||||
public User Authenticate(HttpRequest request, IAuthenticationAttributes authAttributes)
|
||||
{
|
||||
var req = new WebSocketSharpRequest(request, null, request.Path, _logger);
|
||||
var req = new WebSocketSharpRequest(request, null, request.Path);
|
||||
var user = ValidateUser(req, authAttributes);
|
||||
return user;
|
||||
}
|
||||
@@ -68,7 +70,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
|
||||
if (user == null && auth.UserId != Guid.Empty)
|
||||
{
|
||||
throw new SecurityException("User with Id " + auth.UserId + " not found");
|
||||
throw new AuthenticationException("User with Id " + auth.UserId + " not found");
|
||||
}
|
||||
|
||||
if (user != null)
|
||||
@@ -89,7 +91,8 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
!string.IsNullOrEmpty(auth.Client) &&
|
||||
!string.IsNullOrEmpty(auth.Device))
|
||||
{
|
||||
_sessionManager.LogSessionActivity(auth.Client,
|
||||
_sessionManager.LogSessionActivity(
|
||||
auth.Client,
|
||||
auth.Version,
|
||||
auth.DeviceId,
|
||||
auth.Device,
|
||||
@@ -103,35 +106,26 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
private void ValidateUserAccess(
|
||||
User user,
|
||||
IRequest request,
|
||||
IAuthenticationAttributes authAttribtues,
|
||||
IAuthenticationAttributes authAttributes,
|
||||
AuthorizationInfo auth)
|
||||
{
|
||||
if (user.Policy.IsDisabled)
|
||||
if (user.HasPermission(PermissionKind.IsDisabled))
|
||||
{
|
||||
throw new SecurityException("User account has been disabled.")
|
||||
{
|
||||
SecurityExceptionType = SecurityExceptionType.Unauthenticated
|
||||
};
|
||||
throw new SecurityException("User account has been disabled.");
|
||||
}
|
||||
|
||||
if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(request.RemoteIp))
|
||||
if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !_networkManager.IsInLocalNetwork(request.RemoteIp))
|
||||
{
|
||||
throw new SecurityException("User account has been disabled.")
|
||||
{
|
||||
SecurityExceptionType = SecurityExceptionType.Unauthenticated
|
||||
};
|
||||
throw new SecurityException("User account has been disabled.");
|
||||
}
|
||||
|
||||
if (!user.Policy.IsAdministrator
|
||||
&& !authAttribtues.EscapeParentalControl
|
||||
if (!user.HasPermission(PermissionKind.IsAdministrator)
|
||||
&& !authAttributes.EscapeParentalControl
|
||||
&& !user.IsParentalScheduleAllowed())
|
||||
{
|
||||
request.Response.Headers.Add("X-Application-Error-Code", "ParentalControl");
|
||||
|
||||
throw new SecurityException("This user account is not allowed access at this time.")
|
||||
{
|
||||
SecurityExceptionType = SecurityExceptionType.ParentalControl
|
||||
};
|
||||
throw new SecurityException("This user account is not allowed access at this time.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,6 +140,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (authAttribtues.AllowLocalOnly && request.IsLocal)
|
||||
{
|
||||
return true;
|
||||
@@ -188,34 +183,25 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
{
|
||||
if (roles.Contains("admin", StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
if (user == null || !user.Policy.IsAdministrator)
|
||||
if (user == null || !user.HasPermission(PermissionKind.IsAdministrator))
|
||||
{
|
||||
throw new SecurityException("User does not have admin access.")
|
||||
{
|
||||
SecurityExceptionType = SecurityExceptionType.Unauthenticated
|
||||
};
|
||||
throw new SecurityException("User does not have admin access.");
|
||||
}
|
||||
}
|
||||
|
||||
if (roles.Contains("delete", StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
if (user == null || !user.Policy.EnableContentDeletion)
|
||||
if (user == null || !user.HasPermission(PermissionKind.EnableContentDeletion))
|
||||
{
|
||||
throw new SecurityException("User does not have delete access.")
|
||||
{
|
||||
SecurityExceptionType = SecurityExceptionType.Unauthenticated
|
||||
};
|
||||
throw new SecurityException("User does not have delete access.");
|
||||
}
|
||||
}
|
||||
|
||||
if (roles.Contains("download", StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
if (user == null || !user.Policy.EnableContentDownloading)
|
||||
if (user == null || !user.HasPermission(PermissionKind.EnableContentDownloading))
|
||||
{
|
||||
throw new SecurityException("User does not have download access.")
|
||||
{
|
||||
SecurityExceptionType = SecurityExceptionType.Unauthenticated
|
||||
};
|
||||
throw new SecurityException("User does not have download access.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -230,17 +216,17 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
{
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
throw new SecurityException("Access token is required.");
|
||||
throw new AuthenticationException("Access token is required.");
|
||||
}
|
||||
|
||||
var info = GetTokenInfo(request);
|
||||
|
||||
if (info == null)
|
||||
{
|
||||
throw new SecurityException("Access token is invalid or expired.");
|
||||
throw new AuthenticationException("Access token is invalid or expired.");
|
||||
}
|
||||
|
||||
//if (!string.IsNullOrEmpty(info.UserId))
|
||||
// if (!string.IsNullOrEmpty(info.UserId))
|
||||
//{
|
||||
// var user = _userManager.GetUserById(info.UserId);
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
{
|
||||
token = httpReq.Headers["X-MediaBrowser-Token"];
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
token = httpReq.QueryString["api_key"];
|
||||
@@ -116,7 +117,6 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
{
|
||||
info.Device = tokenInfo.DeviceName;
|
||||
}
|
||||
|
||||
else if (!string.Equals(info.Device, tokenInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (allowTokenInfoUpdate)
|
||||
@@ -149,9 +149,9 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
{
|
||||
info.User = _userManager.GetUserById(tokenInfo.UserId);
|
||||
|
||||
if (info.User != null && !string.Equals(info.User.Name, tokenInfo.UserName, StringComparison.OrdinalIgnoreCase))
|
||||
if (info.User != null && !string.Equals(info.User.Username, tokenInfo.UserName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
tokenInfo.UserName = info.User.Name;
|
||||
tokenInfo.UserName = info.User.Username;
|
||||
updateToken = true;
|
||||
}
|
||||
}
|
||||
@@ -161,6 +161,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
_authRepo.Update(tokenInfo);
|
||||
}
|
||||
}
|
||||
|
||||
httpReq.Items["OriginalAuthenticationInfo"] = tokenInfo;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Security;
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
using System;
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.IO.Pipelines;
|
||||
using System.Net;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Server.Implementations.Net;
|
||||
using MediaBrowser.Common.Json;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using UtfUnknown;
|
||||
|
||||
namespace Emby.Server.Implementations.HttpServer
|
||||
{
|
||||
@@ -21,72 +24,53 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// <summary>
|
||||
/// The logger.
|
||||
/// </summary>
|
||||
private readonly ILogger _logger;
|
||||
private readonly ILogger<WebSocketConnection> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// The json serializer.
|
||||
/// The json serializer options.
|
||||
/// </summary>
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
|
||||
/// <summary>
|
||||
/// The socket.
|
||||
/// </summary>
|
||||
private readonly IWebSocket _socket;
|
||||
private readonly WebSocket _socket;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WebSocketConnection" /> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="socket">The socket.</param>
|
||||
/// <param name="remoteEndPoint">The remote end point.</param>
|
||||
/// <param name="jsonSerializer">The json serializer.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <exception cref="ArgumentNullException">socket</exception>
|
||||
public WebSocketConnection(IWebSocket socket, string remoteEndPoint, IJsonSerializer jsonSerializer, ILogger logger)
|
||||
/// <param name="query">The query.</param>
|
||||
public WebSocketConnection(
|
||||
ILogger<WebSocketConnection> logger,
|
||||
WebSocket socket,
|
||||
IPAddress? remoteEndPoint,
|
||||
IQueryCollection query)
|
||||
{
|
||||
if (socket == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(socket));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(remoteEndPoint))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(remoteEndPoint));
|
||||
}
|
||||
|
||||
if (jsonSerializer == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(jsonSerializer));
|
||||
}
|
||||
|
||||
if (logger == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
Id = Guid.NewGuid();
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_socket = socket;
|
||||
_socket.OnReceiveBytes = OnReceiveInternal;
|
||||
|
||||
RemoteEndPoint = remoteEndPoint;
|
||||
_logger = logger;
|
||||
_socket = socket;
|
||||
RemoteEndPoint = remoteEndPoint;
|
||||
QueryString = query;
|
||||
|
||||
socket.Closed += OnSocketClosed;
|
||||
_jsonOptions = JsonDefaults.GetOptions();
|
||||
LastActivityDate = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<EventArgs> Closed;
|
||||
public event EventHandler<EventArgs>? Closed;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the remote end point.
|
||||
/// </summary>
|
||||
public string RemoteEndPoint { get; private set; }
|
||||
public IPAddress? RemoteEndPoint { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the receive action.
|
||||
/// </summary>
|
||||
/// <value>The receive action.</value>
|
||||
public Func<WebSocketMessageInfo, Task> OnReceive { get; set; }
|
||||
public Func<WebSocketMessageInfo, Task>? OnReceive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the last activity date.
|
||||
@@ -94,23 +78,14 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// <value>The last activity date.</value>
|
||||
public DateTime LastActivityDate { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the id.
|
||||
/// </summary>
|
||||
/// <value>The id.</value>
|
||||
public Guid Id { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the URL.
|
||||
/// </summary>
|
||||
/// <value>The URL.</value>
|
||||
public string Url { get; set; }
|
||||
/// <inheritdoc />
|
||||
public DateTime LastKeepAliveDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the query string.
|
||||
/// </summary>
|
||||
/// <value>The query string.</value>
|
||||
public IQueryCollection QueryString { get; set; }
|
||||
public IQueryCollection QueryString { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the state.
|
||||
@@ -118,70 +93,6 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// <value>The state.</value>
|
||||
public WebSocketState State => _socket.State;
|
||||
|
||||
void OnSocketClosed(object sender, EventArgs e)
|
||||
{
|
||||
Closed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when [receive].
|
||||
/// </summary>
|
||||
/// <param name="bytes">The bytes.</param>
|
||||
private void OnReceiveInternal(byte[] bytes)
|
||||
{
|
||||
LastActivityDate = DateTime.UtcNow;
|
||||
|
||||
if (OnReceive == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var charset = CharsetDetector.DetectFromBytes(bytes).Detected?.EncodingName;
|
||||
|
||||
if (string.Equals(charset, "utf-8", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
OnReceiveInternal(Encoding.UTF8.GetString(bytes, 0, bytes.Length));
|
||||
}
|
||||
else
|
||||
{
|
||||
OnReceiveInternal(Encoding.ASCII.GetString(bytes, 0, bytes.Length));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnReceiveInternal(string message)
|
||||
{
|
||||
LastActivityDate = DateTime.UtcNow;
|
||||
|
||||
if (!message.StartsWith("{", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// This info is useful sometimes but also clogs up the log
|
||||
_logger.LogDebug("Received web socket message that is not a json structure: {message}", message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (OnReceive == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var stub = (WebSocketMessage<object>)_jsonSerializer.DeserializeFromString(message, typeof(WebSocketMessage<object>));
|
||||
|
||||
var info = new WebSocketMessageInfo
|
||||
{
|
||||
MessageType = stub.MessageType,
|
||||
Data = stub.Data?.ToString(),
|
||||
Connection = this
|
||||
};
|
||||
|
||||
OnReceive(info);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error processing web socket message");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message asynchronously.
|
||||
/// </summary>
|
||||
@@ -189,48 +100,146 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
/// <exception cref="ArgumentNullException">message</exception>
|
||||
public Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken)
|
||||
{
|
||||
if (message == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(message));
|
||||
}
|
||||
|
||||
var json = _jsonSerializer.SerializeToString(message);
|
||||
|
||||
return SendAsync(json, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The buffer.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task SendAsync(byte[] buffer, CancellationToken cancellationToken)
|
||||
{
|
||||
if (buffer == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(buffer));
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
return _socket.SendAsync(buffer, true, cancellationToken);
|
||||
var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions);
|
||||
return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SendAsync(string text, CancellationToken cancellationToken)
|
||||
public async Task ProcessAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
var pipe = new Pipe();
|
||||
var writer = pipe.Writer;
|
||||
|
||||
ValueWebSocketReceiveResult receiveresult;
|
||||
do
|
||||
{
|
||||
throw new ArgumentNullException(nameof(text));
|
||||
// Allocate at least 512 bytes from the PipeWriter
|
||||
Memory<byte> memory = writer.GetMemory(512);
|
||||
try
|
||||
{
|
||||
receiveresult = await _socket.ReceiveAsync(memory, cancellationToken);
|
||||
}
|
||||
catch (WebSocketException ex)
|
||||
{
|
||||
_logger.LogWarning("WS {IP} error receiving data: {Message}", RemoteEndPoint, ex.Message);
|
||||
break;
|
||||
}
|
||||
|
||||
int bytesRead = receiveresult.Count;
|
||||
if (bytesRead == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Tell the PipeWriter how much was read from the Socket
|
||||
writer.Advance(bytesRead);
|
||||
|
||||
// Make the data available to the PipeReader
|
||||
FlushResult flushResult = await writer.FlushAsync();
|
||||
if (flushResult.IsCompleted)
|
||||
{
|
||||
// The PipeReader stopped reading
|
||||
break;
|
||||
}
|
||||
|
||||
LastActivityDate = DateTime.UtcNow;
|
||||
|
||||
if (receiveresult.EndOfMessage)
|
||||
{
|
||||
await ProcessInternal(pipe.Reader).ConfigureAwait(false);
|
||||
}
|
||||
} while (
|
||||
(_socket.State == WebSocketState.Open || _socket.State == WebSocketState.Connecting)
|
||||
&& receiveresult.MessageType != WebSocketMessageType.Close);
|
||||
|
||||
Closed?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
if (_socket.State == WebSocketState.Open
|
||||
|| _socket.State == WebSocketState.CloseReceived
|
||||
|| _socket.State == WebSocketState.CloseSent)
|
||||
{
|
||||
await _socket.CloseAsync(
|
||||
WebSocketCloseStatus.NormalClosure,
|
||||
string.Empty,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessInternal(PipeReader reader)
|
||||
{
|
||||
ReadResult result = await reader.ReadAsync().ConfigureAwait(false);
|
||||
ReadOnlySequence<byte> buffer = result.Buffer;
|
||||
|
||||
if (OnReceive == null)
|
||||
{
|
||||
// Tell the PipeReader how much of the buffer we have consumed
|
||||
reader.AdvanceTo(buffer.End);
|
||||
return;
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
WebSocketMessage<object> stub;
|
||||
try
|
||||
{
|
||||
|
||||
return _socket.SendAsync(text, true, cancellationToken);
|
||||
if (buffer.IsSingleSegment)
|
||||
{
|
||||
stub = JsonSerializer.Deserialize<WebSocketMessage<object>>(buffer.FirstSpan, _jsonOptions);
|
||||
}
|
||||
else
|
||||
{
|
||||
var buf = ArrayPool<byte>.Shared.Rent(Convert.ToInt32(buffer.Length));
|
||||
try
|
||||
{
|
||||
buffer.CopyTo(buf);
|
||||
stub = JsonSerializer.Deserialize<WebSocketMessage<object>>(buf, _jsonOptions);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
// Tell the PipeReader how much of the buffer we have consumed
|
||||
reader.AdvanceTo(buffer.End);
|
||||
_logger.LogError(ex, "Error processing web socket message");
|
||||
return;
|
||||
}
|
||||
|
||||
// Tell the PipeReader how much of the buffer we have consumed
|
||||
reader.AdvanceTo(buffer.End);
|
||||
|
||||
_logger.LogDebug("WS {IP} received message: {@Message}", RemoteEndPoint, stub);
|
||||
|
||||
var info = new WebSocketMessageInfo
|
||||
{
|
||||
MessageType = stub.MessageType,
|
||||
Data = stub.Data?.ToString(), // Data can be null
|
||||
Connection = this
|
||||
};
|
||||
|
||||
if (info.MessageType.Equals("KeepAlive", StringComparison.Ordinal))
|
||||
{
|
||||
await SendKeepAliveResponse();
|
||||
}
|
||||
else
|
||||
{
|
||||
await OnReceive(info).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private Task SendKeepAliveResponse()
|
||||
{
|
||||
LastKeepAliveDate = DateTime.UtcNow;
|
||||
return SendAsync(
|
||||
new WebSocketMessage<string>
|
||||
{
|
||||
MessageId = Guid.NewGuid(),
|
||||
MessageType = "KeepAlive"
|
||||
}, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
Reference in New Issue
Block a user