Merge remote-tracking branch 'upstream/master' into http-client-migrate

This commit is contained in:
crobibero
2020-09-03 07:46:52 -06:00
86 changed files with 1593 additions and 5408 deletions

View File

@@ -37,10 +37,10 @@ using Emby.Server.Implementations.LiveTv;
using Emby.Server.Implementations.Localization;
using Emby.Server.Implementations.Net;
using Emby.Server.Implementations.Playlists;
using Emby.Server.Implementations.QuickConnect;
using Emby.Server.Implementations.ScheduledTasks;
using Emby.Server.Implementations.Security;
using Emby.Server.Implementations.Serialization;
using Emby.Server.Implementations.Services;
using Emby.Server.Implementations.Session;
using Emby.Server.Implementations.SyncPlay;
using Emby.Server.Implementations.TV;
@@ -72,6 +72,7 @@ using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.QuickConnect;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session;
@@ -89,7 +90,6 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Services;
using MediaBrowser.Model.System;
using MediaBrowser.Model.Tasks;
using MediaBrowser.Providers.Chapters;
@@ -97,12 +97,12 @@ using MediaBrowser.Providers.Manager;
using MediaBrowser.Providers.Plugins.TheTvdb;
using MediaBrowser.Providers.Subtitles;
using MediaBrowser.XbmcMetadata.Providers;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Prometheus.DotNetRuntime;
using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
using WebSocketManager = Emby.Server.Implementations.HttpServer.WebSocketManager;
namespace Emby.Server.Implementations
{
@@ -123,14 +123,18 @@ namespace Emby.Server.Implementations
private IMediaEncoder _mediaEncoder;
private ISessionManager _sessionManager;
private IHttpServer _httpServer;
private IHttpClientFactory _httpClientFactory;
private IWebSocketManager _webSocketManager;
private string[] _urlPrefixes;
/// <summary>
/// Gets a value indicating whether this instance can self restart.
/// </summary>
public bool CanSelfRestart => _startupOptions.RestartPath != null;
public bool CoreStartupHasCompleted { get; private set; }
public virtual bool CanLaunchWebBrowser
{
get
@@ -445,8 +449,7 @@ namespace Emby.Server.Implementations
Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
Logger.LogInformation("Core startup complete");
_httpServer.GlobalResponse = null;
CoreStartupHasCompleted = true;
stopWatch.Restart();
await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
@@ -501,9 +504,6 @@ namespace Emby.Server.Implementations
RegisterServices();
}
public Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next)
=> _httpServer.RequestHandler(context);
/// <summary>
/// Registers services/resources with the service collection that will be available via DI.
/// </summary>
@@ -541,8 +541,6 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton<IZipClient, ZipClient>();
ServiceCollection.AddSingleton<IHttpResultFactory, HttpResultFactory>();
ServiceCollection.AddSingleton<IServerApplicationHost>(this);
ServiceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
@@ -578,8 +576,7 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton<ISearchEngine, SearchEngine>();
ServiceCollection.AddSingleton<ServiceController>();
ServiceCollection.AddSingleton<IHttpServer, HttpListenerHost>();
ServiceCollection.AddSingleton<IWebSocketManager, WebSocketManager>();
ServiceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
@@ -626,6 +623,7 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton<ISessionContext, SessionContext>();
ServiceCollection.AddSingleton<IAuthService, AuthService>();
ServiceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
ServiceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
@@ -651,8 +649,8 @@ namespace Emby.Server.Implementations
_mediaEncoder = Resolve<IMediaEncoder>();
_sessionManager = Resolve<ISessionManager>();
_httpServer = Resolve<IHttpServer>();
_httpClientFactory = Resolve<IHttpClientFactory>();
_webSocketManager = Resolve<IWebSocketManager>();
((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
@@ -753,7 +751,6 @@ namespace Emby.Server.Implementations
CollectionFolder.XmlSerializer = _xmlSerializer;
CollectionFolder.JsonSerializer = Resolve<IJsonSerializer>();
CollectionFolder.ApplicationHost = this;
AuthenticatedAttribute.AuthService = Resolve<IAuthService>();
}
/// <summary>
@@ -773,7 +770,8 @@ namespace Emby.Server.Implementations
.Where(i => i != null)
.ToArray();
_httpServer.Init(GetExportTypes<IService>(), GetExports<IWebSocketListener>(), GetUrlPrefixes());
_urlPrefixes = GetUrlPrefixes().ToArray();
_webSocketManager.Init(GetExports<IWebSocketListener>());
Resolve<ILibraryManager>().AddParts(
GetExports<IResolverIgnoreRule>(),
@@ -939,7 +937,7 @@ namespace Emby.Server.Implementations
}
}
if (!_httpServer.UrlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
if (!_urlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
{
requiresRestart = true;
}

View File

@@ -15,7 +15,7 @@ namespace Emby.Server.Implementations
public static Dictionary<string, string> DefaultConfiguration => new Dictionary<string, string>
{
{ HostWebClientKey, bool.TrueString },
{ HttpListenerHost.DefaultRedirectKey, "web/index.html" },
{ DefaultRedirectKey, "web/index.html" },
{ FfmpegProbeSizeKey, "1G" },
{ FfmpegAnalyzeDurationKey, "200M" },
{ PlaylistsAllowDuplicatesKey, bool.TrueString },

View File

@@ -1,250 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
namespace Emby.Server.Implementations.HttpServer
{
public class FileWriter : IHttpResult
{
private static readonly CultureInfo UsCulture = CultureInfo.ReadOnly(new CultureInfo("en-US"));
private static readonly string[] _skipLogExtensions = {
".js",
".html",
".css"
};
private readonly IStreamHelper _streamHelper;
private readonly ILogger _logger;
/// <summary>
/// The _options.
/// </summary>
private readonly IDictionary<string, string> _options = new Dictionary<string, string>();
/// <summary>
/// The _requested ranges.
/// </summary>
private List<KeyValuePair<long, long?>> _requestedRanges;
public FileWriter(string path, string contentType, string rangeHeader, ILogger logger, IFileSystem fileSystem, IStreamHelper streamHelper)
{
if (string.IsNullOrEmpty(contentType))
{
throw new ArgumentNullException(nameof(contentType));
}
_streamHelper = streamHelper;
Path = path;
_logger = logger;
RangeHeader = rangeHeader;
Headers[HeaderNames.ContentType] = contentType;
TotalContentLength = fileSystem.GetFileInfo(path).Length;
Headers[HeaderNames.AcceptRanges] = "bytes";
if (string.IsNullOrWhiteSpace(rangeHeader))
{
Headers[HeaderNames.ContentLength] = TotalContentLength.ToString(CultureInfo.InvariantCulture);
StatusCode = HttpStatusCode.OK;
}
else
{
StatusCode = HttpStatusCode.PartialContent;
SetRangeValues();
}
FileShare = FileShare.Read;
Cookies = new List<Cookie>();
}
private string RangeHeader { get; set; }
private bool IsHeadRequest { get; set; }
private long RangeStart { get; set; }
private long RangeEnd { get; set; }
private long RangeLength { get; set; }
public long TotalContentLength { get; set; }
public Action OnComplete { get; set; }
public Action OnError { get; set; }
public List<Cookie> Cookies { get; private set; }
public FileShare FileShare { get; set; }
/// <summary>
/// Gets the options.
/// </summary>
/// <value>The options.</value>
public IDictionary<string, string> Headers => _options;
public string Path { get; set; }
/// <summary>
/// Gets the requested ranges.
/// </summary>
/// <value>The requested ranges.</value>
protected List<KeyValuePair<long, long?>> RequestedRanges
{
get
{
if (_requestedRanges == null)
{
_requestedRanges = new List<KeyValuePair<long, long?>>();
// Example: bytes=0-,32-63
var ranges = RangeHeader.Split('=')[1].Split(',');
foreach (var range in ranges)
{
var vals = range.Split('-');
long start = 0;
long? end = null;
if (!string.IsNullOrEmpty(vals[0]))
{
start = long.Parse(vals[0], UsCulture);
}
if (!string.IsNullOrEmpty(vals[1]))
{
end = long.Parse(vals[1], UsCulture);
}
_requestedRanges.Add(new KeyValuePair<long, long?>(start, end));
}
}
return _requestedRanges;
}
}
public string ContentType { get; set; }
public IRequest RequestContext { get; set; }
public object Response { get; set; }
public int Status { get; set; }
public HttpStatusCode StatusCode
{
get => (HttpStatusCode)Status;
set => Status = (int)value;
}
/// <summary>
/// Sets the range values.
/// </summary>
private void SetRangeValues()
{
var requestedRange = RequestedRanges[0];
// If the requested range is "0-", we can optimize by just doing a stream copy
if (!requestedRange.Value.HasValue)
{
RangeEnd = TotalContentLength - 1;
}
else
{
RangeEnd = requestedRange.Value.Value;
}
RangeStart = requestedRange.Key;
RangeLength = 1 + RangeEnd - RangeStart;
// Content-Length is the length of what we're serving, not the original content
var lengthString = RangeLength.ToString(CultureInfo.InvariantCulture);
Headers[HeaderNames.ContentLength] = lengthString;
var rangeString = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}";
Headers[HeaderNames.ContentRange] = rangeString;
_logger.LogDebug("Setting range response values for {0}. RangeRequest: {1} Content-Length: {2}, Content-Range: {3}", Path, RangeHeader, lengthString, rangeString);
}
public async Task WriteToAsync(HttpResponse response, CancellationToken cancellationToken)
{
try
{
// Headers only
if (IsHeadRequest)
{
return;
}
var path = Path;
var offset = RangeStart;
var count = RangeLength;
if (string.IsNullOrWhiteSpace(RangeHeader) || RangeStart <= 0 && RangeEnd >= TotalContentLength - 1)
{
var extension = System.IO.Path.GetExtension(path);
if (extension == null || !_skipLogExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
{
_logger.LogDebug("Transmit file {0}", path);
}
offset = 0;
count = 0;
}
await TransmitFile(response.Body, path, offset, count, FileShare, cancellationToken).ConfigureAwait(false);
}
finally
{
OnComplete?.Invoke();
}
}
public async Task TransmitFile(Stream stream, string path, long offset, long count, FileShare fileShare, CancellationToken cancellationToken)
{
var fileOptions = FileOptions.SequentialScan;
// use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
fileOptions |= FileOptions.Asynchronous;
}
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, fileShare, IODefaults.FileStreamBufferSize, fileOptions))
{
if (offset > 0)
{
fs.Position = offset;
}
if (count > 0)
{
await _streamHelper.CopyToAsync(fs, stream, count, cancellationToken).ConfigureAwait(false);
}
else
{
await fs.CopyToAsync(stream, IODefaults.CopyToBufferSize, cancellationToken).ConfigureAwait(false);
}
}
}
}
}

View File

@@ -1,766 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
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.Services;
using Emby.Server.Implementations.SocketSharp;
using Jellyfin.Data.Events;
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.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
{
/// <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 IServerApplicationHost _appHost;
private readonly IJsonSerializer _jsonSerializer;
private readonly IXmlSerializer _xmlSerializer;
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 IHostEnvironment _hostEnvironment;
private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
private bool _disposed = false;
public HttpListenerHost(
IServerApplicationHost applicationHost,
ILogger<HttpListenerHost> logger,
IServerConfigurationManager config,
IConfiguration configuration,
INetworkManager networkManager,
IJsonSerializer jsonSerializer,
IXmlSerializer xmlSerializer,
ILocalizationManager localizationManager,
ServiceController serviceController,
IHostEnvironment hostEnvironment,
ILoggerFactory loggerFactory)
{
_appHost = applicationHost;
_logger = logger;
_config = config;
_defaultRedirectPath = configuration[DefaultRedirectKey];
_baseUrlPrefix = _config.Configuration.BaseUrl;
_networkManager = networkManager;
_jsonSerializer = jsonSerializer;
_xmlSerializer = xmlSerializer;
ServiceController = serviceController;
_hostEnvironment = hostEnvironment;
_loggerFactory = loggerFactory;
_funcParseFn = t => s => JsvReader.GetParseFn(t)(s);
Instance = this;
ResponseFilters = Array.Empty<Action<IRequest, HttpResponse, object>>();
GlobalResponse = localizationManager.GetLocalizedString("StartupEmbyServerIsLoading");
}
public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;
public Action<IRequest, HttpResponse, object>[] ResponseFilters { get; set; }
public static HttpListenerHost Instance { get; protected set; }
public string[] UrlPrefixes { get; private set; }
public string GlobalResponse { get; set; }
public ServiceController ServiceController { get; }
public object CreateInstance(Type type)
{
return _appHost.CreateInstance(type);
}
private static string NormalizeUrlPath(string path)
{
if (path.Length > 0 && path[0] == '/')
{
// If the path begins with a leading slash, just return it as-is
return path;
}
else
{
// If the path does not begin with a leading slash, append one for consistency
return "/" + path;
}
}
/// <summary>
/// Applies the request filters. Returns whether or not the request has been handled
/// and no more processing should be done.
/// </summary>
/// <returns></returns>
public void ApplyRequestFilters(IRequest req, HttpResponse res, object requestDto)
{
// Exec all RequestFilter attributes with Priority < 0
var attributes = GetRequestFilterAttributes(requestDto.GetType());
int count = attributes.Count;
int i = 0;
for (; i < count && attributes[i].Priority < 0; i++)
{
var attribute = attributes[i];
attribute.RequestFilter(req, res, requestDto);
}
// Exec remaining RequestFilter attributes with Priority >= 0
for (; i < count && attributes[i].Priority >= 0; i++)
{
var attribute = attributes[i];
attribute.RequestFilter(req, res, requestDto);
}
}
public Type GetServiceTypeByRequest(Type requestType)
{
_serviceOperationsMap.TryGetValue(requestType, out var serviceType);
return serviceType;
}
public void AddServiceInfo(Type serviceType, Type requestType)
{
_serviceOperationsMap[requestType] = serviceType;
}
private List<IHasRequestFilter> GetRequestFilterAttributes(Type requestDtoType)
{
var attributes = requestDtoType.GetCustomAttributes(true).OfType<IHasRequestFilter>().ToList();
var serviceType = GetServiceTypeByRequest(requestDtoType);
if (serviceType != null)
{
attributes.AddRange(serviceType.GetCustomAttributes(true).OfType<IHasRequestFilter>());
}
attributes.Sort((x, y) => x.Priority - y.Priority);
return attributes;
}
private static Exception GetActualException(Exception ex)
{
if (ex is AggregateException agg)
{
var inner = agg.InnerException;
if (inner != null)
{
return GetActualException(inner);
}
else
{
var inners = agg.InnerExceptions;
if (inners.Count > 0)
{
return GetActualException(inners[0]);
}
}
}
return ex;
}
private int GetStatusCode(Exception ex)
{
switch (ex)
{
case ArgumentException _: return 400;
case AuthenticationException _: return 401;
case SecurityException _: return 403;
case DirectoryNotFoundException _:
case FileNotFoundException _:
case ResourceNotFoundException _: return 404;
case MethodNotAllowedException _: return 405;
default: return 500;
}
}
private async Task ErrorHandler(Exception ex, IRequest httpReq, int statusCode, string urlToLog, bool ignoreStackTrace)
{
if (ignoreStackTrace)
{
_logger.LogError("Error processing request: {Message}. URL: {Url}", ex.Message.TrimEnd('.'), urlToLog);
}
else
{
_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(Exception ex)
{
// Do not expose the exception message for AuthenticationException
if (ex is AuthenticationException)
{
return null;
}
// Strip any information we don't want to reveal
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)
{
var uri = new Uri(url);
// this gets all the query string key value pairs as a collection
var newQueryString = QueryHelpers.ParseQuery(uri.Query);
var originalCount = newQueryString.Count;
if (originalCount == 0)
{
return url;
}
// this removes the key if exists
newQueryString.Remove(key);
if (originalCount == newQueryString.Count)
{
return url;
}
// this gets the page path from root without QueryString
string pagePathWithoutQueryString = url.Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries)[0];
return newQueryString.Count > 0
? QueryHelpers.AddQueryString(pagePathWithoutQueryString, newQueryString.ToDictionary(kv => kv.Key, kv => kv.Value.ToString()))
: pagePathWithoutQueryString;
}
private static string GetUrlToLog(string url)
{
url = RemoveQueryStringByKey(url, "api_key");
return url;
}
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;
}
/// <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.ListenWithHttps
&& !urlString.Contains("https://", StringComparison.OrdinalIgnoreCase))
{
// 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;
}
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>
private async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken)
{
var stopWatch = new Stopwatch();
stopWatch.Start();
var httpRes = httpReq.Response;
string urlToLog = GetUrlToLog(urlString);
string remoteIp = httpReq.RemoteIp;
try
{
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, httpReq.IsLocal))
{
httpRes.StatusCode = 403;
httpRes.ContentType = "text/plain";
await httpRes.WriteAsync("Forbidden", cancellationToken).ConfigureAwait(false);
return;
}
if (!ValidateSsl(httpReq.RemoteIp, urlString))
{
RedirectToSecureUrl(httpReq, httpRes, urlString);
return;
}
if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase))
{
httpRes.StatusCode = 200;
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;
}
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;
}
}
var handler = GetServiceHandler(httpReq);
if (handler != null)
{
await handler.ProcessRequestAsync(this, httpReq, httpRes, cancellationToken).ConfigureAwait(false);
}
else
{
throw new FileNotFoundException();
}
}
catch (Exception requestEx)
{
try
{
var requestInnerEx = GetActualException(requestEx);
var statusCode = GetStatusCode(requestInnerEx);
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
{
if (httpRes.StatusCode >= 500)
{
_logger.LogDebug("Sending HTTP Response 500 in response to {Url}", urlToLog);
}
stopWatch.Stop();
var elapsed = stopWatch.Elapsed;
if (elapsed.TotalMilliseconds > 500)
{
_logger.LogWarning("HTTP Response {StatusCode} to {RemoteIp}. Time (slow): {Elapsed:g}. {Url}", httpRes.StatusCode, remoteIp, elapsed, urlToLog);
}
}
}
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;
}
}
}
/// <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)
{
var pathInfo = httpReq.PathInfo;
pathInfo = ServiceHandler.GetSanitizedPathInfo(pathInfo, out string contentType);
var restPath = ServiceController.GetRestPathForRequest(httpReq.HttpMethod, pathInfo);
if (restPath != null)
{
return new ServiceHandler(restPath, contentType);
}
_logger.LogError("Could not find handler for {PathInfo}", pathInfo);
return null;
}
private void RedirectToSecureUrl(IHttpRequest httpReq, HttpResponse httpRes, string url)
{
if (Uri.TryCreate(url, UriKind.Absolute, out Uri uri))
{
var builder = new UriBuilder(uri)
{
Port = _config.Configuration.PublicHttpsPort,
Scheme = "https"
};
url = builder.Uri.ToString();
}
httpRes.Redirect(url);
}
/// <summary>
/// Adds the rest handlers.
/// </summary>
/// <param name="serviceTypes">The service types to register with the <see cref="ServiceController"/>.</param>
/// <param name="listeners">The web socket listeners.</param>
/// <param name="urlPrefixes">The URL prefixes. See <see cref="UrlPrefixes"/>.</param>
public void Init(IEnumerable<Type> serviceTypes, IEnumerable<IWebSocketListener> listeners, IEnumerable<string> urlPrefixes)
{
_webSocketListeners = listeners.ToArray();
UrlPrefixes = urlPrefixes.ToArray();
ServiceController.Init(this, serviceTypes);
ResponseFilters = new Action<IRequest, HttpResponse, object>[]
{
new ResponseFilter(this, _logger).FilterResponse
};
}
public RouteAttribute[] GetRouteAttributes(Type requestType)
{
var routes = requestType.GetTypeInfo().GetCustomAttributes<RouteAttribute>(true).ToList();
var clone = routes.ToList();
foreach (var route in clone)
{
routes.Add(new RouteAttribute(NormalizeCustomRoutePath(route.Path), route.Verbs)
{
Notes = route.Notes,
Priority = route.Priority,
Summary = route.Summary
});
routes.Add(new RouteAttribute(NormalizeEmbyRoutePath(route.Path), route.Verbs)
{
Notes = route.Notes,
Priority = route.Priority,
Summary = route.Summary
});
routes.Add(new RouteAttribute(NormalizeMediaBrowserRoutePath(route.Path), route.Verbs)
{
Notes = route.Notes,
Priority = route.Priority,
Summary = route.Summary
});
}
return routes.ToArray();
}
public Func<string, object> GetParseFn(Type propertyType)
{
return _funcParseFn(propertyType);
}
public void SerializeToJson(object o, Stream stream)
{
_jsonSerializer.SerializeToStream(o, stream);
}
public void SerializeToXml(object o, Stream stream)
{
_xmlSerializer.SerializeToStream(o, stream);
}
public Task<object> DeserializeXml(Type type, Stream stream)
{
return Task.FromResult(_xmlSerializer.DeserializeFromStream(type, stream));
}
public Task<object> DeserializeJson(Type type, Stream stream)
{
return _jsonSerializer.DeserializeFromStreamAsync(stream, type);
}
private string NormalizeEmbyRoutePath(string path)
{
_logger.LogDebug("Normalizing /emby route");
return _baseUrlPrefix + "/emby" + NormalizeUrlPath(path);
}
private string NormalizeMediaBrowserRoutePath(string path)
{
_logger.LogDebug("Normalizing /mediabrowser route");
return _baseUrlPrefix + "/mediabrowser" + NormalizeUrlPath(path);
}
private string NormalizeCustomRoutePath(string path)
{
_logger.LogDebug("Normalizing custom route {0}", path);
return _baseUrlPrefix + NormalizeUrlPath(path);
}
/// <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());
}
}
}

View File

@@ -1,721 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using Emby.Server.Implementations.Services;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Services;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using IRequest = MediaBrowser.Model.Services.IRequest;
using MimeTypes = MediaBrowser.Model.Net.MimeTypes;
namespace Emby.Server.Implementations.HttpServer
{
/// <summary>
/// Class HttpResultFactory.
/// </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<HttpResultFactory> _logger;
private readonly IFileSystem _fileSystem;
private readonly IJsonSerializer _jsonSerializer;
private readonly IStreamHelper _streamHelper;
/// <summary>
/// Initializes a new instance of the <see cref="HttpResultFactory" /> class.
/// </summary>
public HttpResultFactory(ILoggerFactory loggerfactory, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IStreamHelper streamHelper)
{
_fileSystem = fileSystem;
_jsonSerializer = jsonSerializer;
_streamHelper = streamHelper;
_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>
/// <returns>System.Object.</returns>
public object GetResult(IRequest requestContext, byte[] content, string contentType, IDictionary<string, string> responseHeaders = null)
{
return GetHttpResult(requestContext, content, contentType, true, responseHeaders);
}
public object GetResult(string content, string contentType, IDictionary<string, string> responseHeaders = null)
{
return GetHttpResult(null, content, contentType, true, responseHeaders);
}
public object GetResult(IRequest requestContext, Stream content, string contentType, IDictionary<string, string> responseHeaders = null)
{
return GetHttpResult(requestContext, content, contentType, true, responseHeaders);
}
public object GetResult(IRequest requestContext, string content, string contentType, IDictionary<string, string> responseHeaders = null)
{
return GetHttpResult(requestContext, content, contentType, true, responseHeaders);
}
public object GetRedirectResult(string url)
{
var responseHeaders = new Dictionary<string, string>();
responseHeaders[HeaderNames.Location] = url;
var result = new HttpResult(Array.Empty<byte>(), "text/plain", HttpStatusCode.Redirect);
AddResponseHeaders(result, responseHeaders);
return result;
}
/// <summary>
/// Gets the HTTP result.
/// </summary>
private IHasHeaders GetHttpResult(IRequest requestContext, Stream content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null)
{
var result = new StreamWriter(content, contentType);
if (responseHeaders == null)
{
responseHeaders = new Dictionary<string, string>();
}
if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out _))
{
responseHeaders[HeaderNames.Expires] = "0";
}
AddResponseHeaders(result, responseHeaders);
return result;
}
/// <summary>
/// Gets the HTTP result.
/// </summary>
private IHasHeaders GetHttpResult(IRequest requestContext, byte[] content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null)
{
string compressionType = null;
bool isHeadRequest = false;
if (requestContext != null)
{
compressionType = GetCompressionType(requestContext, content, contentType);
isHeadRequest = string.Equals(requestContext.Verb, "head", StringComparison.OrdinalIgnoreCase);
}
IHasHeaders result;
if (string.IsNullOrEmpty(compressionType))
{
var contentLength = content.Length;
if (isHeadRequest)
{
content = Array.Empty<byte>();
}
result = new StreamWriter(content, contentType, contentLength);
}
else
{
result = GetCompressedResult(content, compressionType, responseHeaders, isHeadRequest, contentType);
}
if (responseHeaders == null)
{
responseHeaders = new Dictionary<string, string>();
}
if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _))
{
responseHeaders[HeaderNames.Expires] = "0";
}
AddResponseHeaders(result, responseHeaders);
return result;
}
/// <summary>
/// Gets the HTTP result.
/// </summary>
private IHasHeaders GetHttpResult(IRequest requestContext, string content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null)
{
IHasHeaders result;
var bytes = Encoding.UTF8.GetBytes(content);
var compressionType = requestContext == null ? null : GetCompressionType(requestContext, bytes, contentType);
var isHeadRequest = requestContext == null ? false : string.Equals(requestContext.Verb, "head", StringComparison.OrdinalIgnoreCase);
if (string.IsNullOrEmpty(compressionType))
{
var contentLength = bytes.Length;
if (isHeadRequest)
{
bytes = Array.Empty<byte>();
}
result = new StreamWriter(bytes, contentType, contentLength);
}
else
{
result = GetCompressedResult(bytes, compressionType, responseHeaders, isHeadRequest, contentType);
}
if (responseHeaders == null)
{
responseHeaders = new Dictionary<string, string>();
}
if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _))
{
responseHeaders[HeaderNames.Expires] = "0";
}
AddResponseHeaders(result, responseHeaders);
return result;
}
/// <summary>
/// Gets the optimized result.
/// </summary>
/// <typeparam name="T"></typeparam>
public object GetResult<T>(IRequest requestContext, T result, IDictionary<string, string> responseHeaders = null)
where T : class
{
if (result == null)
{
throw new ArgumentNullException(nameof(result));
}
if (responseHeaders == null)
{
responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
responseHeaders[HeaderNames.Expires] = "0";
return ToOptimizedResultInternal(requestContext, result, responseHeaders);
}
private string GetCompressionType(IRequest request, byte[] content, string responseContentType)
{
if (responseContentType == null)
{
return null;
}
// Per apple docs, hls manifests must be compressed
if (!responseContentType.StartsWith("text/", StringComparison.OrdinalIgnoreCase) &&
responseContentType.IndexOf("json", StringComparison.OrdinalIgnoreCase) == -1 &&
responseContentType.IndexOf("javascript", StringComparison.OrdinalIgnoreCase) == -1 &&
responseContentType.IndexOf("xml", StringComparison.OrdinalIgnoreCase) == -1 &&
responseContentType.IndexOf("application/x-mpegURL", StringComparison.OrdinalIgnoreCase) == -1)
{
return null;
}
if (content.Length < 1024)
{
return null;
}
return GetCompressionType(request);
}
private static string GetCompressionType(IRequest request)
{
var acceptEncoding = request.Headers[HeaderNames.AcceptEncoding].ToString();
if (!string.IsNullOrEmpty(acceptEncoding))
{
// if (_brotliCompressor != null && acceptEncoding.IndexOf("br", StringComparison.OrdinalIgnoreCase) != -1)
// return "br";
if (acceptEncoding.Contains("deflate", StringComparison.OrdinalIgnoreCase))
{
return "deflate";
}
if (acceptEncoding.Contains("gzip", StringComparison.OrdinalIgnoreCase))
{
return "gzip";
}
}
return null;
}
/// <summary>
/// Returns the optimized result for the IRequestContext.
/// Does not use or store results in any cache.
/// </summary>
/// <param name="request"></param>
/// <param name="dto"></param>
/// <returns></returns>
public object ToOptimizedResult<T>(IRequest request, T dto)
{
return ToOptimizedResultInternal(request, dto);
}
private object ToOptimizedResultInternal<T>(IRequest request, T dto, IDictionary<string, string> responseHeaders = null)
{
// TODO: @bond use Span and .Equals
var contentType = request.ResponseContentType?.Split(';')[0].Trim().ToLowerInvariant();
switch (contentType)
{
case "application/xml":
case "text/xml":
case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml
return GetHttpResult(request, SerializeToXmlString(dto), contentType, false, responseHeaders);
case "application/json":
case "text/json":
return GetHttpResult(request, _jsonSerializer.SerializeToString(dto), contentType, false, responseHeaders);
default:
break;
}
var isHeadRequest = string.Equals(request.Verb, "head", StringComparison.OrdinalIgnoreCase);
var ms = new MemoryStream();
var writerFn = RequestHelper.GetResponseWriter(HttpListenerHost.Instance, contentType);
writerFn(dto, ms);
ms.Position = 0;
if (isHeadRequest)
{
using (ms)
{
return GetHttpResult(request, Array.Empty<byte>(), contentType, true, responseHeaders);
}
}
return GetHttpResult(request, ms, contentType, true, responseHeaders);
}
private IHasHeaders GetCompressedResult(
byte[] content,
string requestedCompressionType,
IDictionary<string, string> responseHeaders,
bool isHeadRequest,
string contentType)
{
if (responseHeaders == null)
{
responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
content = Compress(content, requestedCompressionType);
responseHeaders[HeaderNames.ContentEncoding] = requestedCompressionType;
responseHeaders[HeaderNames.Vary] = HeaderNames.AcceptEncoding;
var contentLength = content.Length;
if (isHeadRequest)
{
var result = new StreamWriter(Array.Empty<byte>(), contentType, contentLength);
AddResponseHeaders(result, responseHeaders);
return result;
}
else
{
var result = new StreamWriter(content, contentType, contentLength);
AddResponseHeaders(result, responseHeaders);
return result;
}
}
private byte[] Compress(byte[] bytes, string compressionType)
{
if (string.Equals(compressionType, "deflate", StringComparison.OrdinalIgnoreCase))
{
return Deflate(bytes);
}
if (string.Equals(compressionType, "gzip", StringComparison.OrdinalIgnoreCase))
{
return GZip(bytes);
}
throw new NotSupportedException(compressionType);
}
private static byte[] Deflate(byte[] bytes)
{
// In .NET FX incompat-ville, you can't access compressed bytes without closing DeflateStream
// Which means we must use MemoryStream since you have to use ToArray() on a closed Stream
using (var ms = new MemoryStream())
using (var zipStream = new DeflateStream(ms, CompressionMode.Compress))
{
zipStream.Write(bytes, 0, bytes.Length);
zipStream.Dispose();
return ms.ToArray();
}
}
private static byte[] GZip(byte[] buffer)
{
using (var ms = new MemoryStream())
using (var zipStream = new GZipStream(ms, CompressionMode.Compress))
{
zipStream.Write(buffer, 0, buffer.Length);
zipStream.Dispose();
return ms.ToArray();
}
}
private static string SerializeToXmlString(object from)
{
using (var ms = new MemoryStream())
{
var xwSettings = new XmlWriterSettings();
xwSettings.Encoding = new UTF8Encoding(false);
xwSettings.OmitXmlDeclaration = false;
using (var xw = XmlWriter.Create(ms, xwSettings))
{
var serializer = new DataContractSerializer(from.GetType());
serializer.WriteObject(xw, from);
xw.Flush();
ms.Seek(0, SeekOrigin.Begin);
using (var reader = new StreamReader(ms))
{
return reader.ReadToEnd();
}
}
}
}
/// <summary>
/// Pres the process optimized result.
/// </summary>
private object GetCachedResult(IRequest requestContext, IDictionary<string, string> responseHeaders, StaticResultOptions options)
{
bool noCache = requestContext.Headers[HeaderNames.CacheControl].ToString().IndexOf("no-cache", StringComparison.OrdinalIgnoreCase) != -1;
AddCachingHeaders(responseHeaders, options.CacheDuration, noCache, options.DateLastModified);
if (!noCache)
{
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))
{
AddAgeHeader(responseHeaders, options.DateLastModified);
var result = new HttpResult(Array.Empty<byte>(), options.ContentType ?? "text/html", HttpStatusCode.NotModified);
AddResponseHeaders(result, responseHeaders);
return result;
}
}
return null;
}
public Task<object> GetStaticFileResult(IRequest requestContext,
string path,
FileShare fileShare = FileShare.Read)
{
if (string.IsNullOrEmpty(path))
{
throw new ArgumentNullException(nameof(path));
}
return GetStaticFileResult(requestContext, new StaticFileResultOptions
{
Path = path,
FileShare = fileShare
});
}
public Task<object> GetStaticFileResult(IRequest requestContext, StaticFileResultOptions options)
{
var path = options.Path;
var fileShare = options.FileShare;
if (string.IsNullOrEmpty(path))
{
throw new ArgumentException("Path can't be empty.", nameof(options));
}
if (fileShare != FileShare.Read && fileShare != FileShare.ReadWrite)
{
throw new ArgumentException("FileShare must be either Read or ReadWrite");
}
if (string.IsNullOrEmpty(options.ContentType))
{
options.ContentType = MimeTypes.GetMimeType(path);
}
if (!options.DateLastModified.HasValue)
{
options.DateLastModified = _fileSystem.GetLastWriteTimeUtc(path);
}
options.ContentFactory = () => Task.FromResult(GetFileStream(path, fileShare));
options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
return GetStaticResult(requestContext, options);
}
/// <summary>
/// Gets the file stream.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="fileShare">The file share.</param>
/// <returns>Stream.</returns>
private Stream GetFileStream(string path, FileShare fileShare)
{
return new FileStream(path, FileMode.Open, FileAccess.Read, fileShare);
}
public Task<object> GetStaticResult(IRequest requestContext,
Guid cacheKey,
DateTime? lastDateModified,
TimeSpan? cacheDuration,
string contentType,
Func<Task<Stream>> factoryFn,
IDictionary<string, string> responseHeaders = null,
bool isHeadRequest = false)
{
return GetStaticResult(requestContext, new StaticResultOptions
{
CacheDuration = cacheDuration,
ContentFactory = factoryFn,
ContentType = contentType,
DateLastModified = lastDateModified,
IsHeadRequest = isHeadRequest,
ResponseHeaders = responseHeaders
});
}
public async Task<object> GetStaticResult(IRequest requestContext, StaticResultOptions options)
{
options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var contentType = options.ContentType;
if (!StringValues.IsNullOrEmpty(requestContext.Headers[HeaderNames.IfModifiedSince]))
{
// See if the result is already cached in the browser
var result = GetCachedResult(requestContext, options.ResponseHeaders, options);
if (result != null)
{
return result;
}
}
// TODO: We don't really need the option value
var isHeadRequest = options.IsHeadRequest || string.Equals(requestContext.Verb, "HEAD", StringComparison.OrdinalIgnoreCase);
var factoryFn = options.ContentFactory;
var responseHeaders = options.ResponseHeaders;
AddCachingHeaders(responseHeaders, options.CacheDuration, false, options.DateLastModified);
AddAgeHeader(responseHeaders, options.DateLastModified);
var rangeHeader = requestContext.Headers[HeaderNames.Range];
if (!isHeadRequest && !string.IsNullOrEmpty(options.Path))
{
var hasHeaders = new FileWriter(options.Path, contentType, rangeHeader, _logger, _fileSystem, _streamHelper)
{
OnComplete = options.OnComplete,
OnError = options.OnError,
FileShare = options.FileShare
};
AddResponseHeaders(hasHeaders, options.ResponseHeaders);
return hasHeaders;
}
var stream = await factoryFn().ConfigureAwait(false);
var totalContentLength = options.ContentLength;
if (!totalContentLength.HasValue)
{
try
{
totalContentLength = stream.Length;
}
catch (NotSupportedException)
{
}
}
if (!string.IsNullOrWhiteSpace(rangeHeader) && totalContentLength.HasValue)
{
var hasHeaders = new RangeRequestWriter(rangeHeader, totalContentLength.Value, stream, contentType, isHeadRequest)
{
OnComplete = options.OnComplete
};
AddResponseHeaders(hasHeaders, options.ResponseHeaders);
return hasHeaders;
}
else
{
if (totalContentLength.HasValue)
{
responseHeaders["Content-Length"] = totalContentLength.Value.ToString(CultureInfo.InvariantCulture);
}
if (isHeadRequest)
{
using (stream)
{
return GetHttpResult(requestContext, Array.Empty<byte>(), contentType, true, responseHeaders);
}
}
var hasHeaders = new StreamWriter(stream, contentType)
{
OnComplete = options.OnComplete,
OnError = options.OnError
};
AddResponseHeaders(hasHeaders, options.ResponseHeaders);
return hasHeaders;
}
}
/// <summary>
/// Adds the caching responseHeaders.
/// </summary>
private void AddCachingHeaders(
IDictionary<string, string> responseHeaders,
TimeSpan? cacheDuration,
bool noCache,
DateTime? lastModifiedDate)
{
if (noCache)
{
responseHeaders[HeaderNames.CacheControl] = "no-cache, no-store, must-revalidate";
responseHeaders[HeaderNames.Pragma] = "no-cache, no-store, must-revalidate";
return;
}
if (cacheDuration.HasValue)
{
responseHeaders[HeaderNames.CacheControl] = "public, max-age=" + cacheDuration.Value.TotalSeconds;
}
else
{
responseHeaders[HeaderNames.CacheControl] = "public";
}
if (lastModifiedDate.HasValue)
{
responseHeaders[HeaderNames.LastModified] = lastModifiedDate.Value.ToUniversalTime().ToString(HttpDateFormat, _enUSculture);
}
}
/// <summary>
/// Adds the age header.
/// </summary>
/// <param name="responseHeaders">The responseHeaders.</param>
/// <param name="lastDateModified">The last date modified.</param>
private static void AddAgeHeader(IDictionary<string, string> responseHeaders, DateTime? lastDateModified)
{
if (lastDateModified.HasValue)
{
responseHeaders[HeaderNames.Age] = Convert.ToInt64((DateTime.UtcNow - lastDateModified.Value).TotalSeconds).ToString(CultureInfo.InvariantCulture);
}
}
/// <summary>
/// Determines whether [is not modified] [the specified if modified since].
/// </summary>
/// <param name="ifModifiedSince">If modified since.</param>
/// <param name="cacheDuration">Duration of the cache.</param>
/// <param name="dateModified">The date modified.</param>
/// <returns><c>true</c> if [is not modified] [the specified if modified since]; otherwise, <c>false</c>.</returns>
private bool IsNotModified(DateTime ifModifiedSince, TimeSpan? cacheDuration, DateTime? dateModified)
{
if (dateModified.HasValue)
{
var lastModified = NormalizeDateForComparison(dateModified.Value);
ifModifiedSince = NormalizeDateForComparison(ifModifiedSince);
return lastModified <= ifModifiedSince;
}
if (cacheDuration.HasValue)
{
var cacheExpirationDate = ifModifiedSince.Add(cacheDuration.Value);
if (DateTime.UtcNow < cacheExpirationDate)
{
return true;
}
}
return false;
}
/// <summary>
/// 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>
private static DateTime NormalizeDateForComparison(DateTime date)
{
return new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second, date.Kind);
}
/// <summary>
/// Adds the response headers.
/// </summary>
/// <param name="hasHeaders">The has options.</param>
/// <param name="responseHeaders">The response headers.</param>
private static void AddResponseHeaders(IHasHeaders hasHeaders, IEnumerable<KeyValuePair<string, string>> responseHeaders)
{
foreach (var item in responseHeaders)
{
hasHeaders.Headers[item.Key] = item.Value;
}
}
}
}

View File

@@ -1,212 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.Services;
using Microsoft.Net.Http.Headers;
namespace Emby.Server.Implementations.HttpServer
{
public class RangeRequestWriter : IAsyncStreamWriter, IHttpResult
{
private const int BufferSize = 81920;
private readonly Dictionary<string, string> _options = new Dictionary<string, string>();
private List<KeyValuePair<long, long?>> _requestedRanges;
/// <summary>
/// Initializes a new instance of the <see cref="RangeRequestWriter" /> class.
/// </summary>
/// <param name="rangeHeader">The range header.</param>
/// <param name="contentLength">The content length.</param>
/// <param name="source">The source.</param>
/// <param name="contentType">Type of the content.</param>
/// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
public RangeRequestWriter(string rangeHeader, long contentLength, Stream source, string contentType, bool isHeadRequest)
{
if (string.IsNullOrEmpty(contentType))
{
throw new ArgumentNullException(nameof(contentType));
}
RangeHeader = rangeHeader;
SourceStream = source;
IsHeadRequest = isHeadRequest;
ContentType = contentType;
Headers[HeaderNames.ContentType] = contentType;
Headers[HeaderNames.AcceptRanges] = "bytes";
StatusCode = HttpStatusCode.PartialContent;
SetRangeValues(contentLength);
}
/// <summary>
/// Gets or sets the source stream.
/// </summary>
/// <value>The source stream.</value>
private Stream SourceStream { get; set; }
private string RangeHeader { get; set; }
private bool IsHeadRequest { get; set; }
private long RangeStart { get; set; }
private long RangeEnd { get; set; }
private long RangeLength { get; set; }
private long TotalContentLength { get; set; }
public Action OnComplete { get; set; }
/// <summary>
/// Additional HTTP Headers
/// </summary>
/// <value>The headers.</value>
public IDictionary<string, string> Headers => _options;
/// <summary>
/// Gets the requested ranges.
/// </summary>
/// <value>The requested ranges.</value>
protected List<KeyValuePair<long, long?>> RequestedRanges
{
get
{
if (_requestedRanges == null)
{
_requestedRanges = new List<KeyValuePair<long, long?>>();
// Example: bytes=0-,32-63
var ranges = RangeHeader.Split('=')[1].Split(',');
foreach (var range in ranges)
{
var vals = range.Split('-');
long start = 0;
long? end = null;
if (!string.IsNullOrEmpty(vals[0]))
{
start = long.Parse(vals[0], CultureInfo.InvariantCulture);
}
if (!string.IsNullOrEmpty(vals[1]))
{
end = long.Parse(vals[1], CultureInfo.InvariantCulture);
}
_requestedRanges.Add(new KeyValuePair<long, long?>(start, end));
}
}
return _requestedRanges;
}
}
public string ContentType { get; set; }
public IRequest RequestContext { get; set; }
public object Response { get; set; }
public int Status { get; set; }
public HttpStatusCode StatusCode
{
get => (HttpStatusCode)Status;
set => Status = (int)value;
}
/// <summary>
/// Sets the range values.
/// </summary>
private void SetRangeValues(long contentLength)
{
var requestedRange = RequestedRanges[0];
TotalContentLength = contentLength;
// If the requested range is "0-", we can optimize by just doing a stream copy
if (!requestedRange.Value.HasValue)
{
RangeEnd = TotalContentLength - 1;
}
else
{
RangeEnd = requestedRange.Value.Value;
}
RangeStart = requestedRange.Key;
RangeLength = 1 + RangeEnd - RangeStart;
Headers[HeaderNames.ContentLength] = RangeLength.ToString(CultureInfo.InvariantCulture);
Headers[HeaderNames.ContentRange] = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}";
if (RangeStart > 0 && SourceStream.CanSeek)
{
SourceStream.Position = RangeStart;
}
}
public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken)
{
try
{
// Headers only
if (IsHeadRequest)
{
return;
}
using (var source = SourceStream)
{
// If the requested range is "0-", we can optimize by just doing a stream copy
if (RangeEnd >= TotalContentLength - 1)
{
await source.CopyToAsync(responseStream, BufferSize, cancellationToken).ConfigureAwait(false);
}
else
{
await CopyToInternalAsync(source, responseStream, RangeLength, cancellationToken).ConfigureAwait(false);
}
}
}
finally
{
OnComplete?.Invoke();
}
}
private static async Task CopyToInternalAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken)
{
var array = ArrayPool<byte>.Shared.Rent(BufferSize);
try
{
int bytesRead;
while ((bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false)) != 0)
{
var bytesToCopy = Math.Min(bytesRead, copyLength);
await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToCopy), cancellationToken).ConfigureAwait(false);
copyLength -= bytesToCopy;
if (copyLength <= 0)
{
break;
}
}
}
finally
{
ArrayPool<byte>.Shared.Return(array);
}
}
}
}

View File

@@ -1,113 +0,0 @@
using System;
using System.Globalization;
using System.Text;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
namespace Emby.Server.Implementations.HttpServer
{
/// <summary>
/// Class ResponseFilter.
/// </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(IHttpServer server, ILogger logger)
{
_server = server;
_logger = logger;
}
/// <summary>
/// Filters the response.
/// </summary>
/// <param name="req">The req.</param>
/// <param name="res">The res.</param>
/// <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["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)
{
_logger.LogError(exception, "Error processing request for {RawUrl}", req.RawUrl);
if (!string.IsNullOrEmpty(exception.Message))
{
var error = exception.Message.Replace(Environment.NewLine, " ", StringComparison.Ordinal);
error = RemoveControlCharacters(error);
res.Headers.Add("X-Application-Error-Code", error);
}
}
if (dto is IHasHeaders hasHeaders)
{
if (!hasHeaders.Headers.ContainsKey(HeaderNames.Server))
{
hasHeaders.Headers[HeaderNames.Server] = "Microsoft-NetCore/2.0, UPnP/1.0 DLNADOC/1.50";
}
// Content length has to be explicitly set on on HttpListenerResponse or it won't be happy
if (hasHeaders.Headers.TryGetValue(HeaderNames.ContentLength, out string contentLength)
&& !string.IsNullOrEmpty(contentLength))
{
var length = long.Parse(contentLength, CultureInfo.InvariantCulture);
if (length > 0)
{
res.ContentLength = length;
}
}
}
}
/// <summary>
/// Removes the control characters.
/// </summary>
/// <param name="inString">The in string.</param>
/// <returns>System.String.</returns>
public static string RemoveControlCharacters(string inString)
{
if (inString == null)
{
return null;
}
else if (inString.Length == 0)
{
return inString;
}
var newString = new StringBuilder(inString.Length);
foreach (var ch in inString)
{
if (!char.IsControl(ch))
{
newString.Append(ch);
}
}
return newString.ToString();
}
}
}

View File

@@ -1,17 +1,7 @@
#pragma warning disable CS1591
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.Net;
using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
namespace Emby.Server.Implementations.HttpServer.Security
@@ -19,32 +9,11 @@ namespace Emby.Server.Implementations.HttpServer.Security
public class AuthService : IAuthService
{
private readonly IAuthorizationContext _authorizationContext;
private readonly ISessionManager _sessionManager;
private readonly IServerConfigurationManager _config;
private readonly INetworkManager _networkManager;
public AuthService(
IAuthorizationContext authorizationContext,
IServerConfigurationManager config,
ISessionManager sessionManager,
INetworkManager networkManager)
IAuthorizationContext authorizationContext)
{
_authorizationContext = authorizationContext;
_config = config;
_sessionManager = sessionManager;
_networkManager = networkManager;
}
public void Authenticate(IRequest request, IAuthenticationAttributes authAttributes)
{
ValidateUser(request, authAttributes);
}
public User Authenticate(HttpRequest request, IAuthenticationAttributes authAttributes)
{
var req = new WebSocketSharpRequest(request, null, request.Path);
var user = ValidateUser(req, authAttributes);
return user;
}
public AuthorizationInfo Authenticate(HttpRequest request)
@@ -62,185 +31,5 @@ namespace Emby.Server.Implementations.HttpServer.Security
return auth;
}
private User ValidateUser(IRequest request, IAuthenticationAttributes authAttributes)
{
// This code is executed before the service
var auth = _authorizationContext.GetAuthorizationInfo(request);
if (!IsExemptFromAuthenticationToken(authAttributes, request))
{
ValidateSecurityToken(request, auth.Token);
}
if (authAttributes.AllowLocalOnly && !request.IsLocal)
{
throw new SecurityException("Operation not found.");
}
var user = auth.User;
if (user == null && auth.UserId != Guid.Empty)
{
throw new AuthenticationException("User with Id " + auth.UserId + " not found");
}
if (user != null)
{
ValidateUserAccess(user, request, authAttributes);
}
var info = GetTokenInfo(request);
if (!IsExemptFromRoles(auth, authAttributes, request, info))
{
var roles = authAttributes.GetRoles();
ValidateRoles(roles, user);
}
if (!string.IsNullOrEmpty(auth.DeviceId) &&
!string.IsNullOrEmpty(auth.Client) &&
!string.IsNullOrEmpty(auth.Device))
{
_sessionManager.LogSessionActivity(
auth.Client,
auth.Version,
auth.DeviceId,
auth.Device,
request.RemoteIp,
user);
}
return user;
}
private void ValidateUserAccess(
User user,
IRequest request,
IAuthenticationAttributes authAttributes)
{
if (user.HasPermission(PermissionKind.IsDisabled))
{
throw new SecurityException("User account has been disabled.");
}
if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !_networkManager.IsInLocalNetwork(request.RemoteIp))
{
throw new SecurityException("User account has been disabled.");
}
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.");
}
}
private bool IsExemptFromAuthenticationToken(IAuthenticationAttributes authAttribtues, IRequest request)
{
if (!_config.Configuration.IsStartupWizardCompleted && authAttribtues.AllowBeforeStartupWizard)
{
return true;
}
if (authAttribtues.AllowLocal && request.IsLocal)
{
return true;
}
if (authAttribtues.AllowLocalOnly && request.IsLocal)
{
return true;
}
if (authAttribtues.IgnoreLegacyAuth)
{
return true;
}
return false;
}
private bool IsExemptFromRoles(AuthorizationInfo auth, IAuthenticationAttributes authAttribtues, IRequest request, AuthenticationInfo tokenInfo)
{
if (!_config.Configuration.IsStartupWizardCompleted && authAttribtues.AllowBeforeStartupWizard)
{
return true;
}
if (authAttribtues.AllowLocal && request.IsLocal)
{
return true;
}
if (authAttribtues.AllowLocalOnly && request.IsLocal)
{
return true;
}
if (string.IsNullOrEmpty(auth.Token))
{
return true;
}
if (tokenInfo != null && tokenInfo.UserId.Equals(Guid.Empty))
{
return true;
}
return false;
}
private static void ValidateRoles(string[] roles, User user)
{
if (roles.Contains("admin", StringComparer.OrdinalIgnoreCase))
{
if (user == null || !user.HasPermission(PermissionKind.IsAdministrator))
{
throw new SecurityException("User does not have admin access.");
}
}
if (roles.Contains("delete", StringComparer.OrdinalIgnoreCase))
{
if (user == null || !user.HasPermission(PermissionKind.EnableContentDeletion))
{
throw new SecurityException("User does not have delete access.");
}
}
if (roles.Contains("download", StringComparer.OrdinalIgnoreCase))
{
if (user == null || !user.HasPermission(PermissionKind.EnableContentDownloading))
{
throw new SecurityException("User does not have download access.");
}
}
}
private static AuthenticationInfo GetTokenInfo(IRequest request)
{
request.Items.TryGetValue("OriginalAuthenticationInfo", out var info);
return info as AuthenticationInfo;
}
private void ValidateSecurityToken(IRequest request, string token)
{
if (string.IsNullOrEmpty(token))
{
throw new AuthenticationException("Access token is required.");
}
var info = GetTokenInfo(request);
if (info == null)
{
throw new AuthenticationException("Access token is invalid or expired.");
}
}
}
}

View File

@@ -7,7 +7,6 @@ using System.Net;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
@@ -24,14 +23,9 @@ namespace Emby.Server.Implementations.HttpServer.Security
_userManager = userManager;
}
public AuthorizationInfo GetAuthorizationInfo(object requestContext)
public AuthorizationInfo GetAuthorizationInfo(HttpContext requestContext)
{
return GetAuthorizationInfo((IRequest)requestContext);
}
public AuthorizationInfo GetAuthorizationInfo(IRequest requestContext)
{
if (requestContext.Items.TryGetValue("AuthorizationInfo", out var cached))
if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached))
{
return (AuthorizationInfo)cached;
}
@@ -52,18 +46,18 @@ namespace Emby.Server.Implementations.HttpServer.Security
/// </summary>
/// <param name="httpReq">The HTTP req.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
private AuthorizationInfo GetAuthorization(IRequest httpReq)
private AuthorizationInfo GetAuthorization(HttpContext httpReq)
{
var auth = GetAuthorizationDictionary(httpReq);
var (authInfo, originalAuthInfo) =
GetAuthorizationInfoFromDictionary(auth, httpReq.Headers, httpReq.QueryString);
GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query);
if (originalAuthInfo != null)
{
httpReq.Items["OriginalAuthenticationInfo"] = originalAuthInfo;
httpReq.Request.HttpContext.Items["OriginalAuthenticationInfo"] = originalAuthInfo;
}
httpReq.Items["AuthorizationInfo"] = authInfo;
httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
return authInfo;
}
@@ -203,13 +197,13 @@ namespace Emby.Server.Implementations.HttpServer.Security
/// </summary>
/// <param name="httpReq">The HTTP req.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
private Dictionary<string, string> GetAuthorizationDictionary(IRequest httpReq)
private Dictionary<string, string> GetAuthorizationDictionary(HttpContext httpReq)
{
var auth = httpReq.Headers["X-Emby-Authorization"];
var auth = httpReq.Request.Headers["X-Emby-Authorization"];
if (string.IsNullOrEmpty(auth))
{
auth = httpReq.Headers[HeaderNames.Authorization];
auth = httpReq.Request.Headers[HeaderNames.Authorization];
}
return GetAuthorization(auth);

View File

@@ -2,11 +2,11 @@
using System;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
namespace Emby.Server.Implementations.HttpServer.Security
{
@@ -23,26 +23,20 @@ namespace Emby.Server.Implementations.HttpServer.Security
_sessionManager = sessionManager;
}
public SessionInfo GetSession(IRequest requestContext)
public SessionInfo GetSession(HttpContext requestContext)
{
var authorization = _authContext.GetAuthorizationInfo(requestContext);
var user = authorization.User;
return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.RemoteIp, user);
}
private AuthenticationInfo GetTokenInfo(IRequest request)
{
request.Items.TryGetValue("OriginalAuthenticationInfo", out var info);
return info as AuthenticationInfo;
return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.Request.RemoteIp(), user);
}
public SessionInfo GetSession(object requestContext)
{
return GetSession((IRequest)requestContext);
return GetSession((HttpContext)requestContext);
}
public User GetUser(IRequest requestContext)
public User GetUser(HttpContext requestContext)
{
var session = GetSession(requestContext);
@@ -51,7 +45,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
public User GetUser(object requestContext)
{
return GetUser((IRequest)requestContext);
return GetUser((HttpContext)requestContext);
}
}
}

View File

@@ -1,120 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.Services;
using Microsoft.Net.Http.Headers;
namespace Emby.Server.Implementations.HttpServer
{
/// <summary>
/// Class StreamWriter.
/// </summary>
public class StreamWriter : IAsyncStreamWriter, IHasHeaders
{
/// <summary>
/// The options.
/// </summary>
private readonly IDictionary<string, string> _options = new Dictionary<string, string>();
/// <summary>
/// Initializes a new instance of the <see cref="StreamWriter" /> class.
/// </summary>
/// <param name="source">The source.</param>
/// <param name="contentType">Type of the content.</param>
public StreamWriter(Stream source, string contentType)
{
if (string.IsNullOrEmpty(contentType))
{
throw new ArgumentNullException(nameof(contentType));
}
SourceStream = source;
Headers["Content-Type"] = contentType;
if (source.CanSeek)
{
Headers[HeaderNames.ContentLength] = source.Length.ToString(CultureInfo.InvariantCulture);
}
Headers[HeaderNames.ContentType] = contentType;
}
/// <summary>
/// Initializes a new instance of the <see cref="StreamWriter"/> class.
/// </summary>
/// <param name="source">The source.</param>
/// <param name="contentType">Type of the content.</param>
/// <param name="contentLength">The content length.</param>
public StreamWriter(byte[] source, string contentType, int contentLength)
{
if (string.IsNullOrEmpty(contentType))
{
throw new ArgumentNullException(nameof(contentType));
}
SourceBytes = source;
Headers[HeaderNames.ContentLength] = contentLength.ToString(CultureInfo.InvariantCulture);
Headers[HeaderNames.ContentType] = contentType;
}
/// <summary>
/// Gets or sets the source stream.
/// </summary>
/// <value>The source stream.</value>
private Stream SourceStream { get; set; }
private byte[] SourceBytes { get; set; }
/// <summary>
/// Gets the options.
/// </summary>
/// <value>The options.</value>
public IDictionary<string, string> Headers => _options;
/// <summary>
/// Fires when complete.
/// </summary>
public Action OnComplete { get; set; }
/// <summary>
/// Fires when an error occours.
/// </summary>
public Action OnError { get; set; }
/// <inheritdoc />
public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken)
{
try
{
var bytes = SourceBytes;
if (bytes != null)
{
await responseStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken).ConfigureAwait(false);
}
else
{
using (var src = SourceStream)
{
await src.CopyToAsync(responseStream, cancellationToken).ConfigureAwait(false);
}
}
}
catch
{
OnError?.Invoke();
throw;
}
finally
{
OnComplete?.Invoke();
}
}
}
}

View 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());
}
}
}

View File

@@ -17,5 +17,8 @@
"Genres": "Géneros",
"Folders": "Carpetas",
"Favorites": "Favoritos",
"FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido de {0}"
"FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido de {0}",
"HeaderFavoriteSongs": "Canciones Favoritas",
"HeaderFavoriteEpisodes": "Episodios Favoritos",
"HeaderFavoriteArtists": "Artistas Favoritos"
}

View File

@@ -45,7 +45,7 @@
"NameSeasonNumber": "Sesong {0}",
"NameSeasonUnknown": "Sesong ukjent",
"NewVersionIsAvailable": "En ny versjon av Jellyfin Server er tilgjengelig for nedlasting.",
"NotificationOptionApplicationUpdateAvailable": "Programvareoppdatering er tilgjengelig",
"NotificationOptionApplicationUpdateAvailable": "En programvareoppdatering er tilgjengelig",
"NotificationOptionApplicationUpdateInstalled": "Applikasjonsoppdatering installert",
"NotificationOptionAudioPlayback": "Lydavspilling startet",
"NotificationOptionAudioPlaybackStopped": "Lydavspilling stoppet",

View File

@@ -1,76 +1,117 @@
{
"ProviderValue": "ผู้ให้บริการ: {0}",
"PluginUpdatedWithName": "{0} ได้รับการ update แล้ว",
"PluginUninstalledWithName": "ถอนการติดตั้ง {0}",
"PluginInstalledWithName": "{0} ได้รับการติดตั้ง",
"Plugin": "Plugin",
"Playlists": "รายการ",
"PluginUpdatedWithName": "อัปเดต {0} แล้ว",
"PluginUninstalledWithName": "ถอนการติดตั้ง {0} แล้ว",
"PluginInstalledWithName": "ติดตั้ง {0} แล้ว",
"Plugin": "ปลั๊กอิน",
"Playlists": "เพลย์ลิสต์",
"Photos": "รูปภาพ",
"NotificationOptionVideoPlaybackStopped": "หยุดการเล่น Video",
"NotificationOptionVideoPlayback": "เริ่มแสดง Video",
"NotificationOptionUserLockedOut": "ผู้ใช้ Locked Out",
"NotificationOptionTaskFailed": "ตารางการทำงานล้มเหลว",
"NotificationOptionServerRestartRequired": "ควร Restart Server",
"NotificationOptionPluginUpdateInstalled": "Update Plugin แล้ว",
"NotificationOptionPluginUninstalled": "ถอด Plugin",
"NotificationOptionPluginInstalled": "ติดตั้ง Plugin แล้ว",
"NotificationOptionPluginError": "Plugin ล้มเหลว",
"NotificationOptionNewLibraryContent": "เพิ่มข้อมูลใหม่แล้ว",
"NotificationOptionInstallationFailed": "ติดตั้งล้มเหลว",
"NotificationOptionCameraImageUploaded": "รูปภาพถูก upload",
"NotificationOptionAudioPlaybackStopped": "หยุดการเล่นเสียง",
"NotificationOptionVideoPlaybackStopped": "หยุดเล่นวิดีโอ",
"NotificationOptionVideoPlayback": "เริ่มเล่นวิดีโอ",
"NotificationOptionUserLockedOut": "ผู้ใช้ถูกล็อก",
"NotificationOptionTaskFailed": "งานตามกำหนดการล้มเหลว",
"NotificationOptionServerRestartRequired": "จำเป็นต้องรีสตาร์ทเซิร์ฟเวอร์",
"NotificationOptionPluginUpdateInstalled": "ติดตั้งการอัปเดตปลั๊กอินแล้ว",
"NotificationOptionPluginUninstalled": "ถอนการติดตั้งปลั๊กอินแล้ว",
"NotificationOptionPluginInstalled": "ติดตั้งปลั๊กอินแล้ว",
"NotificationOptionPluginError": "ปลั๊กอินล้มเหลว",
"NotificationOptionNewLibraryContent": "เพิ่มเนื้อหาใหม่แล้ว",
"NotificationOptionInstallationFailed": "การติดตั้งล้มเหลว",
"NotificationOptionCameraImageUploaded": "อัปโหลดภาพถ่ายแล้ว",
"NotificationOptionAudioPlaybackStopped": "หยุดเล่นเสียง",
"NotificationOptionAudioPlayback": "เริ่มเล่นเสียง",
"NotificationOptionApplicationUpdateInstalled": "Update ระบบแล้ว",
"NotificationOptionApplicationUpdateAvailable": "ระบบ update สามารถใช้ได้แล้ว",
"NewVersionIsAvailable": "ตรวจพบ Jellyfin เวอร์ชั่นใหม่",
"NameSeasonUnknown": "ไม่ทราบปี",
"NameSeasonNumber": "ปี {0}",
"NameInstallFailed": "{0} ติดตั้งไม่สำเร็จ",
"MusicVideos": "MV",
"Music": "เพลง",
"Movies": "ภาพยนต์",
"MixedContent": "รายการแบบผสม",
"MessageServerConfigurationUpdated": "การตั้งค่า update แล้ว",
"MessageNamedServerConfigurationUpdatedWithValue": "รายการตั้งค่า {0} ได้รับการ update แล้ว",
"MessageApplicationUpdatedTo": "Jellyfin Server จะ update ไปที่ {0}",
"MessageApplicationUpdated": "Jellyfin Server update แล้ว",
"NotificationOptionApplicationUpdateInstalled": "ติดตั้งการอัปเดตแอพพลิเคชันแล้ว",
"NotificationOptionApplicationUpdateAvailable": "มีการอัปเดตแอพพลิเคชัน",
"NewVersionIsAvailable": "เวอร์ชันใหม่ของเซิร์ฟเวอร์ Jellyfin พร้อมให้ดาวน์โหลดแล้ว",
"NameSeasonUnknown": "ไม่ทราบซีซัน",
"NameSeasonNumber": "ซีซัน {0}",
"NameInstallFailed": "การติดตั้ง {0} ล้มเหลว",
"MusicVideos": "มิวสิควิดีโอ",
"Music": "ดนตรี",
"Movies": "ภาพยนต์",
"MixedContent": "เนื้อหาผสม",
"MessageServerConfigurationUpdated": "อัปเดตการกำหนดค่าเซิร์ฟเวอร์แล้ว",
"MessageNamedServerConfigurationUpdatedWithValue": "อัปเดตการกำหนดค่าเซิร์ฟเวอร์ในส่วน {0} แล้ว",
"MessageApplicationUpdatedTo": "เซิร์ฟเวอร์ Jellyfin ได้รับการอัปเดตเป็น {0}",
"MessageApplicationUpdated": "อัพเดตเซิร์ฟเวอร์ Jellyfin แล้ว",
"Latest": "ล่าสุด",
"LabelRunningTimeValue": "เวลาที่เล่น : {0}",
"LabelIpAddressValue": "IP address: {0}",
"ItemRemovedWithName": "{0} ถูกลบจากรายการ",
"ItemAddedWithName": "{0} ถูกเพิ่มในรายการ",
"Inherit": "การสืบทอด",
"HomeVideos": "วีดีโอส่วนตัว",
"HeaderRecordingGroups": "ค่ายบันทึก",
"LabelRunningTimeValue": "ผ่านไปแล้ว: {0}",
"LabelIpAddressValue": "ที่อยู่ IP: {0}",
"ItemRemovedWithName": "{0} ถูกลบออกจากไลบรารี",
"ItemAddedWithName": "{0} ถูกเพิ่มลงในไลบรารีแล้ว",
"Inherit": "สืบทอด",
"HomeVideos": "โฮมวิดีโอ",
"HeaderRecordingGroups": "กลุ่มการบันทึก",
"HeaderNextUp": "ถัดไป",
"HeaderLiveTV": "รายการสด",
"HeaderFavoriteSongs": "เพลงโปรด",
"HeaderFavoriteShows": "รายการโชว์โปรด",
"HeaderFavoriteEpisodes": "ฉากโปรด",
"HeaderFavoriteArtists": "นักแสดงโปรด",
"HeaderFavoriteAlbums": "อัมบั้มโปรด",
"HeaderContinueWatching": "ชมต่อจากเดิม",
"HeaderCameraUploads": "Upload รูปภาพ",
"HeaderAlbumArtists": "อัลบั้มนักแสดง",
"HeaderLiveTV": "ทีวีสด",
"HeaderFavoriteSongs": "เพลงที่ชื่นชอบ",
"HeaderFavoriteShows": "รายการที่ชื่นชอบ",
"HeaderFavoriteEpisodes": "ตอนที่ชื่นชอบ",
"HeaderFavoriteArtists": "ศิลปินที่ชื่นชอบ",
"HeaderFavoriteAlbums": "อัมบั้มที่ชื่นชอบ",
"HeaderContinueWatching": "ดูต่อ",
"HeaderCameraUploads": "อัปโหลดรูปถ่าย",
"HeaderAlbumArtists": "อัลบั้มศิลปิน",
"Genres": "ประเภท",
"Folders": "โฟลเดอร์",
"Favorites": "รายการโปรด",
"FailedLoginAttemptWithUserName": "การเชื่อมต่อล้มเหลวจาก {0}",
"DeviceOnlineWithName": "{0} เชื่อมต่อสำเร็จ",
"DeviceOfflineWithName": "{0} ตัดการเชื่อมต่อ",
"Collections": "ชุด",
"ChapterNameValue": "บทที่ {0}",
"Channels": "ชาแนล",
"CameraImageUploadedFrom": "รูปภาพถูก upload จาก {0}",
"FailedLoginAttemptWithUserName": "ความพยายามในการเข้าสู่ระบบล้มเหลวจาก {0}",
"DeviceOnlineWithName": "{0} เชื่อมต่อสำเร็จแล้ว",
"DeviceOfflineWithName": "{0} ยกเลิกการเชื่อมต่อแล้ว",
"Collections": "คอลเลกชัน",
"ChapterNameValue": "บท {0}",
"Channels": "ช่อง",
"CameraImageUploadedFrom": "ภาพถ่ายใหม่ได้ถูกอัปโหลดมาจาก {0}",
"Books": "หนังสือ",
"AuthenticationSucceededWithUserName": "{0} ยืนยันตัวสำเร็จ",
"Artists": "นักแสดง",
"Application": "แอพลิเคชัน",
"AppDeviceValues": "App: {0}, อุปกรณ์: {1}",
"AuthenticationSucceededWithUserName": "{0} ยืนยันตัวสำเร็จแล้ว",
"Artists": "ศิลปิน",
"Application": "แอพลิเคชัน",
"AppDeviceValues": "แอพ: {0}, อุปกรณ์: {1}",
"Albums": "อัลบั้ม",
"ScheduledTaskStartedWithName": "{0} เริ่มต้น",
"ScheduledTaskFailedWithName": "{0} ล้มเหลว",
"Songs": "เพลง",
"Shows": "แสดง",
"ServerNameNeedsToBeRestarted": "{0} ต้องการรีสตาร์ท"
"Shows": "รายการ",
"ServerNameNeedsToBeRestarted": "{0} ต้องการการรีสตาร์ท",
"TaskDownloadMissingSubtitlesDescription": "ค้นหาคำบรรยายที่หายไปในอินเทอร์เน็ตตามค่ากำหนดในข้อมูลเมตา",
"TaskDownloadMissingSubtitles": "ดาวน์โหลดคำบรรยายที่ขาดหายไป",
"TaskRefreshChannelsDescription": "รีเฟรชข้อมูลช่องอินเทอร์เน็ต",
"TaskRefreshChannels": "รีเฟรชช่อง",
"TaskCleanTranscodeDescription": "ลบไฟล์ทรานส์โค้ดที่มีอายุมากกว่าหนึ่งวัน",
"TaskCleanTranscode": "ล้างไดเรกทอรีทรานส์โค้ด",
"TaskUpdatePluginsDescription": "ดาวน์โหลดและติดตั้งโปรแกรมปรับปรุงให้กับปลั๊กอินที่กำหนดค่าให้อัปเดตโดยอัตโนมัติ",
"TaskUpdatePlugins": "อัปเดตปลั๊กอิน",
"TaskRefreshPeopleDescription": "อัปเดตข้อมูลเมตานักแสดงและผู้กำกับในไลบรารีสื่อ",
"TaskRefreshPeople": "รีเฟรชบุคคล",
"TaskCleanLogsDescription": "ลบไฟล์บันทึกที่เก่ากว่า {0} วัน",
"TaskCleanLogs": "ล้างไดเรกทอรีบันทึก",
"TaskRefreshLibraryDescription": "สแกนไลบรารีสื่อของคุณเพื่อหาไฟล์ใหม่และรีเฟรชข้อมูลเมตา",
"TaskRefreshLibrary": "สแกนไลบรารีสื่อ",
"TaskRefreshChapterImagesDescription": "สร้างภาพขนาดย่อสำหรับวิดีโอที่มีบท",
"TaskRefreshChapterImages": "แตกรูปภาพบท",
"TaskCleanCacheDescription": "ลบไฟล์แคชที่ระบบไม่ต้องการ",
"TaskCleanCache": "ล้างไดเรกทอรีแคช",
"TasksChannelsCategory": "ช่องอินเทอร์เน็ต",
"TasksApplicationCategory": "แอพพลิเคชัน",
"TasksLibraryCategory": "ไลบรารี",
"TasksMaintenanceCategory": "ปิดซ่อมบำรุง",
"VersionNumber": "เวอร์ชัน {0}",
"ValueSpecialEpisodeName": "พิเศษ - {0}",
"ValueHasBeenAddedToLibrary": "เพิ่ม {0} ลงในไลบรารีสื่อของคุณแล้ว",
"UserStoppedPlayingItemWithValues": "{0} เล่นเสร็จแล้ว {1} บน {2}",
"UserStartedPlayingItemWithValues": "{0} กำลังเล่น {1} บน {2}",
"UserPolicyUpdatedWithName": "มีการอัปเดตนโยบายผู้ใช้ของ {0}",
"UserPasswordChangedWithName": "มีการเปลี่ยนรหัสผ่านของผู้ใช้ {0}",
"UserOnlineFromDevice": "{0} ออนไลน์จาก {1}",
"UserOfflineFromDevice": "{0} ได้ยกเลิกการเชื่อมต่อจาก {1}",
"UserLockedOutWithName": "ผู้ใช้ {0} ถูกล็อก",
"UserDownloadingItemWithValues": "{0} กำลังดาวน์โหลด {1}",
"UserDeletedWithName": "ลบผู้ใช้ {0} แล้ว",
"UserCreatedWithName": "สร้างผู้ใช้ {0} แล้ว",
"User": "ผู้ใช้งาน",
"TvShows": "รายการทีวี",
"System": "ระบบ",
"Sync": "ซิงค์",
"SubtitleDownloadFailureFromForItem": "ไม่สามารถดาวน์โหลดคำบรรยายจาก {0} สำหรับ {1} ได้",
"StartupEmbyServerIsLoading": "กำลังโหลดเซิร์ฟเวอร์ Jellyfin โปรดลองอีกครั้งในอีกสักครู่"
}

View File

@@ -0,0 +1,285 @@
using System;
using System.Collections.Concurrent;
using System.Globalization;
using System.Linq;
using System.Security.Cryptography;
using MediaBrowser.Common;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.QuickConnect;
using MediaBrowser.Controller.Security;
using MediaBrowser.Model.QuickConnect;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.QuickConnect
{
/// <summary>
/// Quick connect implementation.
/// </summary>
public class QuickConnectManager : IQuickConnect, IDisposable
{
private readonly RNGCryptoServiceProvider _rng = new RNGCryptoServiceProvider();
private readonly ConcurrentDictionary<string, QuickConnectResult> _currentRequests = new ConcurrentDictionary<string, QuickConnectResult>();
private readonly IServerConfigurationManager _config;
private readonly ILogger<QuickConnectManager> _logger;
private readonly IAuthenticationRepository _authenticationRepository;
private readonly IAuthorizationContext _authContext;
private readonly IServerApplicationHost _appHost;
/// <summary>
/// Initializes a new instance of the <see cref="QuickConnectManager"/> class.
/// Should only be called at server startup when a singleton is created.
/// </summary>
/// <param name="config">Configuration.</param>
/// <param name="logger">Logger.</param>
/// <param name="appHost">Application host.</param>
/// <param name="authContext">Authentication context.</param>
/// <param name="authenticationRepository">Authentication repository.</param>
public QuickConnectManager(
IServerConfigurationManager config,
ILogger<QuickConnectManager> logger,
IServerApplicationHost appHost,
IAuthorizationContext authContext,
IAuthenticationRepository authenticationRepository)
{
_config = config;
_logger = logger;
_appHost = appHost;
_authContext = authContext;
_authenticationRepository = authenticationRepository;
ReloadConfiguration();
}
/// <inheritdoc/>
public int CodeLength { get; set; } = 6;
/// <inheritdoc/>
public string TokenName { get; set; } = "QuickConnect";
/// <inheritdoc/>
public QuickConnectState State { get; private set; } = QuickConnectState.Unavailable;
/// <inheritdoc/>
public int Timeout { get; set; } = 5;
private DateTime DateActivated { get; set; }
/// <inheritdoc/>
public void AssertActive()
{
if (State != QuickConnectState.Active)
{
throw new ArgumentException("Quick connect is not active on this server");
}
}
/// <inheritdoc/>
public void Activate()
{
DateActivated = DateTime.UtcNow;
SetState(QuickConnectState.Active);
}
/// <inheritdoc/>
public void SetState(QuickConnectState newState)
{
_logger.LogDebug("Changed quick connect state from {State} to {newState}", State, newState);
ExpireRequests(true);
State = newState;
_config.Configuration.QuickConnectAvailable = newState == QuickConnectState.Available || newState == QuickConnectState.Active;
_config.SaveConfiguration();
_logger.LogDebug("Configuration saved");
}
/// <inheritdoc/>
public QuickConnectResult TryConnect()
{
ExpireRequests();
if (State != QuickConnectState.Active)
{
_logger.LogDebug("Refusing quick connect initiation request, current state is {State}", State);
throw new AuthenticationException("Quick connect is not active on this server");
}
var code = GenerateCode();
var result = new QuickConnectResult()
{
Secret = GenerateSecureRandom(),
DateAdded = DateTime.UtcNow,
Code = code
};
_currentRequests[code] = result;
return result;
}
/// <inheritdoc/>
public QuickConnectResult CheckRequestStatus(string secret)
{
ExpireRequests();
AssertActive();
string code = _currentRequests.Where(x => x.Value.Secret == secret).Select(x => x.Value.Code).DefaultIfEmpty(string.Empty).First();
if (!_currentRequests.TryGetValue(code, out QuickConnectResult result))
{
throw new ResourceNotFoundException("Unable to find request with provided secret");
}
return result;
}
/// <inheritdoc/>
public string GenerateCode()
{
Span<byte> raw = stackalloc byte[4];
int min = (int)Math.Pow(10, CodeLength - 1);
int max = (int)Math.Pow(10, CodeLength);
uint scale = uint.MaxValue;
while (scale == uint.MaxValue)
{
_rng.GetBytes(raw);
scale = BitConverter.ToUInt32(raw);
}
int code = (int)(min + ((max - min) * (scale / (double)uint.MaxValue)));
return code.ToString(CultureInfo.InvariantCulture);
}
/// <inheritdoc/>
public bool AuthorizeRequest(Guid userId, string code)
{
ExpireRequests();
AssertActive();
if (!_currentRequests.TryGetValue(code, out QuickConnectResult result))
{
throw new ResourceNotFoundException("Unable to find request");
}
if (result.Authenticated)
{
throw new InvalidOperationException("Request is already authorized");
}
result.Authentication = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
// Change the time on the request so it expires one minute into the future. It can't expire immediately as otherwise some clients wouldn't ever see that they have been authenticated.
var added = result.DateAdded ?? DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(Timeout));
result.DateAdded = added.Subtract(TimeSpan.FromMinutes(Timeout - 1));
_authenticationRepository.Create(new AuthenticationInfo
{
AppName = TokenName,
AccessToken = result.Authentication,
DateCreated = DateTime.UtcNow,
DeviceId = _appHost.SystemId,
DeviceName = _appHost.FriendlyName,
AppVersion = _appHost.ApplicationVersionString,
UserId = userId
});
_logger.LogDebug("Authorizing device with code {Code} to login as user {userId}", code, userId);
return true;
}
/// <inheritdoc/>
public int DeleteAllDevices(Guid user)
{
var raw = _authenticationRepository.Get(new AuthenticationInfoQuery()
{
DeviceId = _appHost.SystemId,
UserId = user
});
var tokens = raw.Items.Where(x => x.AppName.StartsWith(TokenName, StringComparison.Ordinal));
var removed = 0;
foreach (var token in tokens)
{
_authenticationRepository.Delete(token);
_logger.LogDebug("Deleted token {AccessToken}", token.AccessToken);
removed++;
}
return removed;
}
/// <summary>
/// Dispose.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Dispose.
/// </summary>
/// <param name="disposing">Dispose unmanaged resources.</param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_rng?.Dispose();
}
}
private string GenerateSecureRandom(int length = 32)
{
Span<byte> bytes = stackalloc byte[length];
_rng.GetBytes(bytes);
return Hex.Encode(bytes);
}
/// <inheritdoc/>
public void ExpireRequests(bool expireAll = false)
{
// Check if quick connect should be deactivated
if (State == QuickConnectState.Active && DateTime.UtcNow > DateActivated.AddMinutes(Timeout) && !expireAll)
{
_logger.LogDebug("Quick connect time expired, deactivating");
SetState(QuickConnectState.Available);
expireAll = true;
}
// Expire stale connection requests
var code = string.Empty;
var values = _currentRequests.Values.ToList();
for (int i = 0; i < values.Count; i++)
{
var added = values[i].DateAdded ?? DateTime.UnixEpoch;
if (DateTime.UtcNow > added.AddMinutes(Timeout) || expireAll)
{
code = values[i].Code;
_logger.LogDebug("Removing expired request {code}", code);
if (!_currentRequests.TryRemove(code, out _))
{
_logger.LogWarning("Request {code} already expired", code);
}
}
}
}
private void ReloadConfiguration()
{
State = _config.Configuration.QuickConnectAvailable ? QuickConnectState.Available : QuickConnectState.Unavailable;
}
}
}

View File

@@ -1,64 +0,0 @@
#pragma warning disable CS1591
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.Services;
namespace Emby.Server.Implementations.Services
{
public class HttpResult
: IHttpResult, IAsyncStreamWriter
{
public HttpResult(object response, string contentType, HttpStatusCode statusCode)
{
this.Headers = new Dictionary<string, string>();
this.Response = response;
this.ContentType = contentType;
this.StatusCode = statusCode;
}
public object Response { get; set; }
public string ContentType { get; set; }
public IDictionary<string, string> Headers { get; private set; }
public int Status { get; set; }
public HttpStatusCode StatusCode
{
get => (HttpStatusCode)Status;
set => Status = (int)value;
}
public IRequest RequestContext { get; set; }
public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken)
{
var response = RequestContext?.Response;
if (this.Response is byte[] bytesResponse)
{
var contentLength = bytesResponse.Length;
if (response != null)
{
response.ContentLength = contentLength;
}
if (contentLength > 0)
{
await responseStream.WriteAsync(bytesResponse, 0, contentLength, cancellationToken).ConfigureAwait(false);
}
return;
}
await ResponseHelper.WriteObject(this.RequestContext, this.Response, response).ConfigureAwait(false);
}
}
}

View File

@@ -1,51 +0,0 @@
#pragma warning disable CS1591
using System;
using System.IO;
using System.Threading.Tasks;
using Emby.Server.Implementations.HttpServer;
namespace Emby.Server.Implementations.Services
{
public class RequestHelper
{
public static Func<Type, Stream, Task<object>> GetRequestReader(HttpListenerHost host, string contentType)
{
switch (GetContentTypeWithoutEncoding(contentType))
{
case "application/xml":
case "text/xml":
case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml
return host.DeserializeXml;
case "application/json":
case "text/json":
return host.DeserializeJson;
}
return null;
}
public static Action<object, Stream> GetResponseWriter(HttpListenerHost host, string contentType)
{
switch (GetContentTypeWithoutEncoding(contentType))
{
case "application/xml":
case "text/xml":
case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml
return host.SerializeToXml;
case "application/json":
case "text/json":
return host.SerializeToJson;
}
return null;
}
private static string GetContentTypeWithoutEncoding(string contentType)
{
return contentType?.Split(';')[0].ToLowerInvariant().Trim();
}
}
}

View File

@@ -1,141 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Globalization;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.HttpServer;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
namespace Emby.Server.Implementations.Services
{
public static class ResponseHelper
{
public static Task WriteToResponse(HttpResponse response, IRequest request, object result, CancellationToken cancellationToken)
{
if (result == null)
{
if (response.StatusCode == (int)HttpStatusCode.OK)
{
response.StatusCode = (int)HttpStatusCode.NoContent;
}
response.ContentLength = 0;
return Task.CompletedTask;
}
var httpResult = result as IHttpResult;
if (httpResult != null)
{
httpResult.RequestContext = request;
request.ResponseContentType = httpResult.ContentType ?? request.ResponseContentType;
}
var defaultContentType = request.ResponseContentType;
if (httpResult != null)
{
if (httpResult.RequestContext == null)
{
httpResult.RequestContext = request;
}
response.StatusCode = httpResult.Status;
}
if (result is IHasHeaders responseOptions)
{
foreach (var responseHeaders in responseOptions.Headers)
{
if (string.Equals(responseHeaders.Key, "Content-Length", StringComparison.OrdinalIgnoreCase))
{
response.ContentLength = long.Parse(responseHeaders.Value, CultureInfo.InvariantCulture);
continue;
}
response.Headers.Add(responseHeaders.Key, responseHeaders.Value);
}
}
// ContentType='text/html' is the default for a HttpResponse
// Do not override if another has been set
if (response.ContentType == null || response.ContentType == "text/html")
{
response.ContentType = defaultContentType;
}
if (response.ContentType == "application/json")
{
response.ContentType += "; charset=utf-8";
}
switch (result)
{
case IAsyncStreamWriter asyncStreamWriter:
return asyncStreamWriter.WriteToAsync(response.Body, cancellationToken);
case IStreamWriter streamWriter:
streamWriter.WriteTo(response.Body);
return Task.CompletedTask;
case FileWriter fileWriter:
return fileWriter.WriteToAsync(response, cancellationToken);
case Stream stream:
return CopyStream(stream, response.Body);
case byte[] bytes:
response.ContentType = "application/octet-stream";
response.ContentLength = bytes.Length;
if (bytes.Length > 0)
{
return response.Body.WriteAsync(bytes, 0, bytes.Length, cancellationToken);
}
return Task.CompletedTask;
case string responseText:
var responseTextAsBytes = Encoding.UTF8.GetBytes(responseText);
response.ContentLength = responseTextAsBytes.Length;
if (responseTextAsBytes.Length > 0)
{
return response.Body.WriteAsync(responseTextAsBytes, 0, responseTextAsBytes.Length, cancellationToken);
}
return Task.CompletedTask;
}
return WriteObject(request, result, response);
}
private static async Task CopyStream(Stream src, Stream dest)
{
using (src)
{
await src.CopyToAsync(dest).ConfigureAwait(false);
}
}
public static async Task WriteObject(IRequest request, object result, HttpResponse response)
{
var contentType = request.ResponseContentType;
var serializer = RequestHelper.GetResponseWriter(HttpListenerHost.Instance, contentType);
using (var ms = new MemoryStream())
{
serializer(result, ms);
ms.Position = 0;
var contentLength = ms.Length;
response.ContentLength = contentLength;
if (contentLength > 0)
{
await ms.CopyToAsync(response.Body).ConfigureAwait(false);
}
}
}
}
}

View File

@@ -1,202 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading.Tasks;
using Emby.Server.Implementations.HttpServer;
using MediaBrowser.Model.Services;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Services
{
public delegate object ActionInvokerFn(object intance, object request);
public delegate void VoidActionInvokerFn(object intance, object request);
public class ServiceController
{
private readonly ILogger<ServiceController> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="ServiceController"/> class.
/// </summary>
/// <param name="logger">The <see cref="ServiceController"/> logger.</param>
public ServiceController(ILogger<ServiceController> logger)
{
_logger = logger;
}
public void Init(HttpListenerHost appHost, IEnumerable<Type> serviceTypes)
{
foreach (var serviceType in serviceTypes)
{
RegisterService(appHost, serviceType);
}
}
public void RegisterService(HttpListenerHost appHost, Type serviceType)
{
// Make sure the provided type implements IService
if (!typeof(IService).IsAssignableFrom(serviceType))
{
_logger.LogWarning("Tried to register a service that does not implement IService: {ServiceType}", serviceType);
return;
}
var processedReqs = new HashSet<Type>();
var actions = ServiceExecGeneral.Reset(serviceType);
foreach (var mi in serviceType.GetActions())
{
var requestType = mi.GetParameters()[0].ParameterType;
if (processedReqs.Contains(requestType))
{
continue;
}
processedReqs.Add(requestType);
ServiceExecGeneral.CreateServiceRunnersFor(requestType, actions);
// var returnMarker = GetTypeWithGenericTypeDefinitionOf(requestType, typeof(IReturn<>));
// var responseType = returnMarker != null ?
// GetGenericArguments(returnMarker)[0]
// : mi.ReturnType != typeof(object) && mi.ReturnType != typeof(void) ?
// mi.ReturnType
// : Type.GetType(requestType.FullName + "Response");
RegisterRestPaths(appHost, requestType, serviceType);
appHost.AddServiceInfo(serviceType, requestType);
}
}
public readonly RestPath.RestPathMap RestPathMap = new RestPath.RestPathMap();
public void RegisterRestPaths(HttpListenerHost appHost, Type requestType, Type serviceType)
{
var attrs = appHost.GetRouteAttributes(requestType);
foreach (var attr in attrs)
{
var restPath = new RestPath(appHost.CreateInstance, appHost.GetParseFn, requestType, serviceType, attr.Path, attr.Verbs, attr.IsHidden, attr.Summary, attr.Description);
RegisterRestPath(restPath);
}
}
private static readonly char[] InvalidRouteChars = new[] { '?', '&' };
public void RegisterRestPath(RestPath restPath)
{
if (restPath.Path[0] != '/')
{
throw new ArgumentException(
string.Format(
CultureInfo.InvariantCulture,
"Route '{0}' on '{1}' must start with a '/'",
restPath.Path,
restPath.RequestType.GetMethodName()));
}
if (restPath.Path.IndexOfAny(InvalidRouteChars) != -1)
{
throw new ArgumentException(
string.Format(
CultureInfo.InvariantCulture,
"Route '{0}' on '{1}' contains invalid chars. ",
restPath.Path,
restPath.RequestType.GetMethodName()));
}
if (RestPathMap.TryGetValue(restPath.FirstMatchHashKey, out List<RestPath> pathsAtFirstMatch))
{
pathsAtFirstMatch.Add(restPath);
}
else
{
RestPathMap[restPath.FirstMatchHashKey] = new List<RestPath>() { restPath };
}
}
public RestPath GetRestPathForRequest(string httpMethod, string pathInfo)
{
var matchUsingPathParts = RestPath.GetPathPartsForMatching(pathInfo);
List<RestPath> firstMatches;
var yieldedHashMatches = RestPath.GetFirstMatchHashKeys(matchUsingPathParts);
foreach (var potentialHashMatch in yieldedHashMatches)
{
if (!this.RestPathMap.TryGetValue(potentialHashMatch, out firstMatches))
{
continue;
}
var bestScore = -1;
RestPath bestMatch = null;
foreach (var restPath in firstMatches)
{
var score = restPath.MatchScore(httpMethod, matchUsingPathParts);
if (score > bestScore)
{
bestScore = score;
bestMatch = restPath;
}
}
if (bestScore > 0 && bestMatch != null)
{
return bestMatch;
}
}
var yieldedWildcardMatches = RestPath.GetFirstMatchWildCardHashKeys(matchUsingPathParts);
foreach (var potentialHashMatch in yieldedWildcardMatches)
{
if (!this.RestPathMap.TryGetValue(potentialHashMatch, out firstMatches))
{
continue;
}
var bestScore = -1;
RestPath bestMatch = null;
foreach (var restPath in firstMatches)
{
var score = restPath.MatchScore(httpMethod, matchUsingPathParts);
if (score > bestScore)
{
bestScore = score;
bestMatch = restPath;
}
}
if (bestScore > 0 && bestMatch != null)
{
return bestMatch;
}
}
return null;
}
public Task<object> Execute(HttpListenerHost httpHost, object requestDto, IRequest req)
{
var requestType = requestDto.GetType();
req.OperationName = requestType.Name;
var serviceType = httpHost.GetServiceTypeByRequest(requestType);
var service = httpHost.CreateInstance(serviceType);
if (service is IRequiresRequest serviceRequiresContext)
{
serviceRequiresContext.Request = req;
}
// Executes the service and returns the result
return ServiceExecGeneral.Execute(serviceType, req, service, requestDto, requestType.GetMethodName());
}
}
}

View File

@@ -1,230 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading.Tasks;
using MediaBrowser.Model.Services;
namespace Emby.Server.Implementations.Services
{
public static class ServiceExecExtensions
{
public static string[] AllVerbs = new[] {
"OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "CONNECT", // RFC 2616
"PROPFIND", "PROPPATCH", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", // RFC 2518
"VERSION-CONTROL", "REPORT", "CHECKOUT", "CHECKIN", "UNCHECKOUT",
"MKWORKSPACE", "UPDATE", "LABEL", "MERGE", "BASELINE-CONTROL", "MKACTIVITY", // RFC 3253
"ORDERPATCH", // RFC 3648
"ACL", // RFC 3744
"PATCH", // https://datatracker.ietf.org/doc/draft-dusseault-http-patch/
"SEARCH", // https://datatracker.ietf.org/doc/draft-reschke-webdav-search/
"BCOPY", "BDELETE", "BMOVE", "BPROPFIND", "BPROPPATCH", "NOTIFY",
"POLL", "SUBSCRIBE", "UNSUBSCRIBE"
};
public static List<MethodInfo> GetActions(this Type serviceType)
{
var list = new List<MethodInfo>();
foreach (var mi in serviceType.GetRuntimeMethods())
{
if (!mi.IsPublic)
{
continue;
}
if (mi.IsStatic)
{
continue;
}
if (mi.GetParameters().Length != 1)
{
continue;
}
var actionName = mi.Name;
if (!AllVerbs.Contains(actionName, StringComparer.OrdinalIgnoreCase))
{
continue;
}
list.Add(mi);
}
return list;
}
}
internal static class ServiceExecGeneral
{
private static Dictionary<string, ServiceMethod> execMap = new Dictionary<string, ServiceMethod>();
public static void CreateServiceRunnersFor(Type requestType, List<ServiceMethod> actions)
{
foreach (var actionCtx in actions)
{
if (execMap.ContainsKey(actionCtx.Id))
{
continue;
}
execMap[actionCtx.Id] = actionCtx;
}
}
public static Task<object> Execute(Type serviceType, IRequest request, object instance, object requestDto, string requestName)
{
var actionName = request.Verb ?? "POST";
if (execMap.TryGetValue(ServiceMethod.Key(serviceType, actionName, requestName), out ServiceMethod actionContext))
{
if (actionContext.RequestFilters != null)
{
foreach (var requestFilter in actionContext.RequestFilters)
{
requestFilter.RequestFilter(request, request.Response, requestDto);
if (request.Response.HasStarted)
{
Task.FromResult<object>(null);
}
}
}
var response = actionContext.ServiceAction(instance, requestDto);
if (response is Task taskResponse)
{
return GetTaskResult(taskResponse);
}
return Task.FromResult(response);
}
var expectedMethodName = actionName.Substring(0, 1) + actionName.Substring(1).ToLowerInvariant();
throw new NotImplementedException(
string.Format(
CultureInfo.InvariantCulture,
"Could not find method named {1}({0}) or Any({0}) on Service {2}",
requestDto.GetType().GetMethodName(),
expectedMethodName,
serviceType.GetMethodName()));
}
private static async Task<object> GetTaskResult(Task task)
{
try
{
if (task is Task<object> taskObject)
{
return await taskObject.ConfigureAwait(false);
}
await task.ConfigureAwait(false);
var type = task.GetType().GetTypeInfo();
if (!type.IsGenericType)
{
return null;
}
var resultProperty = type.GetDeclaredProperty("Result");
if (resultProperty == null)
{
return null;
}
var result = resultProperty.GetValue(task);
// hack alert
if (result.GetType().Name.IndexOf("voidtaskresult", StringComparison.OrdinalIgnoreCase) != -1)
{
return null;
}
return result;
}
catch (TypeAccessException)
{
return null; // return null for void Task's
}
}
public static List<ServiceMethod> Reset(Type serviceType)
{
var actions = new List<ServiceMethod>();
foreach (var mi in serviceType.GetActions())
{
var actionName = mi.Name;
var args = mi.GetParameters();
var requestType = args[0].ParameterType;
var actionCtx = new ServiceMethod
{
Id = ServiceMethod.Key(serviceType, actionName, requestType.GetMethodName())
};
actionCtx.ServiceAction = CreateExecFn(serviceType, requestType, mi);
var reqFilters = new List<IHasRequestFilter>();
foreach (var attr in mi.GetCustomAttributes(true))
{
if (attr is IHasRequestFilter hasReqFilter)
{
reqFilters.Add(hasReqFilter);
}
}
if (reqFilters.Count > 0)
{
actionCtx.RequestFilters = reqFilters.OrderBy(i => i.Priority).ToArray();
}
actions.Add(actionCtx);
}
return actions;
}
private static ActionInvokerFn CreateExecFn(Type serviceType, Type requestType, MethodInfo mi)
{
var serviceParam = Expression.Parameter(typeof(object), "serviceObj");
var serviceStrong = Expression.Convert(serviceParam, serviceType);
var requestDtoParam = Expression.Parameter(typeof(object), "requestDto");
var requestDtoStrong = Expression.Convert(requestDtoParam, requestType);
Expression callExecute = Expression.Call(
serviceStrong, mi, requestDtoStrong);
if (mi.ReturnType != typeof(void))
{
var executeFunc = Expression.Lambda<ActionInvokerFn>(
callExecute,
serviceParam,
requestDtoParam).Compile();
return executeFunc;
}
else
{
var executeFunc = Expression.Lambda<VoidActionInvokerFn>(
callExecute,
serviceParam,
requestDtoParam).Compile();
return (service, request) =>
{
executeFunc(service, request);
return null;
};
}
}
}
}

View File

@@ -1,212 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Net.Mime;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.HttpServer;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Services
{
public class ServiceHandler
{
private RestPath _restPath;
private string _responseContentType;
internal ServiceHandler(RestPath restPath, string responseContentType)
{
_restPath = restPath;
_responseContentType = responseContentType;
}
protected static Task<object> CreateContentTypeRequest(HttpListenerHost host, IRequest httpReq, Type requestType, string contentType)
{
if (!string.IsNullOrEmpty(contentType) && httpReq.ContentLength > 0)
{
var deserializer = RequestHelper.GetRequestReader(host, contentType);
if (deserializer != null)
{
return deserializer.Invoke(requestType, httpReq.InputStream);
}
}
return Task.FromResult(host.CreateInstance(requestType));
}
public static string GetSanitizedPathInfo(string pathInfo, out string contentType)
{
contentType = null;
var pos = pathInfo.LastIndexOf('.');
if (pos != -1)
{
var format = pathInfo.AsSpan().Slice(pos + 1);
contentType = GetFormatContentType(format);
if (contentType != null)
{
pathInfo = pathInfo.Substring(0, pos);
}
}
return pathInfo;
}
private static string GetFormatContentType(ReadOnlySpan<char> format)
{
if (format.Equals("json", StringComparison.Ordinal))
{
return MediaTypeNames.Application.Json;
}
else if (format.Equals("xml", StringComparison.Ordinal))
{
return MediaTypeNames.Application.Xml;
}
return null;
}
public async Task ProcessRequestAsync(HttpListenerHost httpHost, IRequest httpReq, HttpResponse httpRes, CancellationToken cancellationToken)
{
httpReq.Items["__route"] = _restPath;
if (_responseContentType != null)
{
httpReq.ResponseContentType = _responseContentType;
}
var request = await CreateRequest(httpHost, httpReq, _restPath).ConfigureAwait(false);
httpHost.ApplyRequestFilters(httpReq, httpRes, request);
httpRes.HttpContext.SetServiceStackRequest(httpReq);
var response = await httpHost.ServiceController.Execute(httpHost, request, httpReq).ConfigureAwait(false);
// Apply response filters
foreach (var responseFilter in httpHost.ResponseFilters)
{
responseFilter(httpReq, httpRes, response);
}
await ResponseHelper.WriteToResponse(httpRes, httpReq, response, cancellationToken).ConfigureAwait(false);
}
public static async Task<object> CreateRequest(HttpListenerHost host, IRequest httpReq, RestPath restPath)
{
var requestType = restPath.RequestType;
if (RequireqRequestStream(requestType))
{
// Used by IRequiresRequestStream
var requestParams = GetRequestParams(httpReq.Response.HttpContext.Request);
var request = ServiceHandler.CreateRequest(httpReq, restPath, requestParams, host.CreateInstance(requestType));
var rawReq = (IRequiresRequestStream)request;
rawReq.RequestStream = httpReq.InputStream;
return rawReq;
}
else
{
var requestParams = GetFlattenedRequestParams(httpReq.Response.HttpContext.Request);
var requestDto = await CreateContentTypeRequest(host, httpReq, restPath.RequestType, httpReq.ContentType).ConfigureAwait(false);
return CreateRequest(httpReq, restPath, requestParams, requestDto);
}
}
public static bool RequireqRequestStream(Type requestType)
{
var requiresRequestStreamTypeInfo = typeof(IRequiresRequestStream).GetTypeInfo();
return requiresRequestStreamTypeInfo.IsAssignableFrom(requestType.GetTypeInfo());
}
public static object CreateRequest(IRequest httpReq, RestPath restPath, Dictionary<string, string> requestParams, object requestDto)
{
var pathInfo = !restPath.IsWildCardPath
? GetSanitizedPathInfo(httpReq.PathInfo, out _)
: httpReq.PathInfo;
return restPath.CreateRequest(pathInfo, requestParams, requestDto);
}
/// <summary>
/// Duplicate Params are given a unique key by appending a #1 suffix
/// </summary>
private static Dictionary<string, string> GetRequestParams(HttpRequest request)
{
var map = new Dictionary<string, string>();
foreach (var pair in request.Query)
{
var values = pair.Value;
if (values.Count == 1)
{
map[pair.Key] = values[0];
}
else
{
for (var i = 0; i < values.Count; i++)
{
map[pair.Key + (i == 0 ? string.Empty : "#" + i)] = values[i];
}
}
}
if ((IsMethod(request.Method, "POST") || IsMethod(request.Method, "PUT"))
&& request.HasFormContentType)
{
foreach (var pair in request.Form)
{
var values = pair.Value;
if (values.Count == 1)
{
map[pair.Key] = values[0];
}
else
{
for (var i = 0; i < values.Count; i++)
{
map[pair.Key + (i == 0 ? string.Empty : "#" + i)] = values[i];
}
}
}
}
return map;
}
private static bool IsMethod(string method, string expected)
=> string.Equals(method, expected, StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Duplicate params have their values joined together in a comma-delimited string.
/// </summary>
private static Dictionary<string, string> GetFlattenedRequestParams(HttpRequest request)
{
var map = new Dictionary<string, string>();
foreach (var pair in request.Query)
{
map[pair.Key] = pair.Value;
}
if ((IsMethod(request.Method, "POST") || IsMethod(request.Method, "PUT"))
&& request.HasFormContentType)
{
foreach (var pair in request.Form)
{
map[pair.Key] = pair.Value;
}
}
return map;
}
}
}

View File

@@ -1,20 +0,0 @@
#pragma warning disable CS1591
using System;
namespace Emby.Server.Implementations.Services
{
public class ServiceMethod
{
public string Id { get; set; }
public ActionInvokerFn ServiceAction { get; set; }
public MediaBrowser.Model.Services.IHasRequestFilter[] RequestFilters { get; set; }
public static string Key(Type serviceType, string method, string requestDtoName)
{
return serviceType.FullName + " " + method.ToUpperInvariant() + " " + requestDtoName;
}
}
}

View File

@@ -1,550 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json.Serialization;
namespace Emby.Server.Implementations.Services
{
public class RestPath
{
private const string WildCard = "*";
private const char WildCardChar = '*';
private const string PathSeperator = "/";
private const char PathSeperatorChar = '/';
private const char ComponentSeperator = '.';
private const string VariablePrefix = "{";
private readonly bool[] componentsWithSeparators;
private readonly string restPath;
public bool IsWildCardPath { get; private set; }
private readonly string[] literalsToMatch;
private readonly string[] variablesNames;
private readonly bool[] isWildcard;
private readonly int wildcardCount = 0;
internal static string[] IgnoreAttributesNamed = new[]
{
nameof(JsonIgnoreAttribute)
};
private static Type _excludeType = typeof(Stream);
public int VariableArgsCount { get; set; }
/// <summary>
/// The number of segments separated by '/' determinable by path.Split('/').Length
/// e.g. /path/to/here.ext == 3
/// </summary>
public int PathComponentsCount { get; set; }
/// <summary>
/// Gets or sets the total number of segments after subparts have been exploded ('.')
/// e.g. /path/to/here.ext == 4.
/// </summary>
public int TotalComponentsCount { get; set; }
public string[] Verbs { get; private set; }
public Type RequestType { get; private set; }
public Type ServiceType { get; private set; }
public string Path => this.restPath;
public string Summary { get; private set; }
public string Description { get; private set; }
public bool IsHidden { get; private set; }
public static string[] GetPathPartsForMatching(string pathInfo)
{
return pathInfo.ToLowerInvariant().Split(new[] { PathSeperatorChar }, StringSplitOptions.RemoveEmptyEntries);
}
public static List<string> GetFirstMatchHashKeys(string[] pathPartsForMatching)
{
var hashPrefix = pathPartsForMatching.Length + PathSeperator;
return GetPotentialMatchesWithPrefix(hashPrefix, pathPartsForMatching);
}
public static List<string> GetFirstMatchWildCardHashKeys(string[] pathPartsForMatching)
{
const string HashPrefix = WildCard + PathSeperator;
return GetPotentialMatchesWithPrefix(HashPrefix, pathPartsForMatching);
}
private static List<string> GetPotentialMatchesWithPrefix(string hashPrefix, string[] pathPartsForMatching)
{
var list = new List<string>();
foreach (var part in pathPartsForMatching)
{
list.Add(hashPrefix + part);
if (part.IndexOf(ComponentSeperator, StringComparison.Ordinal) == -1)
{
continue;
}
var subParts = part.Split(ComponentSeperator);
foreach (var subPart in subParts)
{
list.Add(hashPrefix + subPart);
}
}
return list;
}
public RestPath(Func<Type, object> createInstanceFn, Func<Type, Func<string, object>> getParseFn, Type requestType, Type serviceType, string path, string verbs, bool isHidden = false, string summary = null, string description = null)
{
this.RequestType = requestType;
this.ServiceType = serviceType;
this.Summary = summary;
this.IsHidden = isHidden;
this.Description = description;
this.restPath = path;
this.Verbs = string.IsNullOrWhiteSpace(verbs) ? ServiceExecExtensions.AllVerbs : verbs.ToUpperInvariant().Split(new[] { ' ', ',' }, StringSplitOptions.RemoveEmptyEntries);
var componentsList = new List<string>();
// We only split on '.' if the restPath has them. Allows for /{action}.{type}
var hasSeparators = new List<bool>();
foreach (var component in this.restPath.Split(PathSeperatorChar))
{
if (string.IsNullOrEmpty(component))
{
continue;
}
if (component.IndexOf(VariablePrefix, StringComparison.OrdinalIgnoreCase) != -1
&& component.IndexOf(ComponentSeperator, StringComparison.Ordinal) != -1)
{
hasSeparators.Add(true);
componentsList.AddRange(component.Split(ComponentSeperator));
}
else
{
hasSeparators.Add(false);
componentsList.Add(component);
}
}
var components = componentsList.ToArray();
this.TotalComponentsCount = components.Length;
this.literalsToMatch = new string[this.TotalComponentsCount];
this.variablesNames = new string[this.TotalComponentsCount];
this.isWildcard = new bool[this.TotalComponentsCount];
this.componentsWithSeparators = hasSeparators.ToArray();
this.PathComponentsCount = this.componentsWithSeparators.Length;
string firstLiteralMatch = null;
for (var i = 0; i < components.Length; i++)
{
var component = components[i];
if (component.StartsWith(VariablePrefix, StringComparison.Ordinal))
{
var variableName = component.Substring(1, component.Length - 2);
if (variableName[variableName.Length - 1] == WildCardChar)
{
this.isWildcard[i] = true;
variableName = variableName.Substring(0, variableName.Length - 1);
}
this.variablesNames[i] = variableName;
this.VariableArgsCount++;
}
else
{
this.literalsToMatch[i] = component.ToLowerInvariant();
if (firstLiteralMatch == null)
{
firstLiteralMatch = this.literalsToMatch[i];
}
}
}
for (var i = 0; i < components.Length - 1; i++)
{
if (!this.isWildcard[i])
{
continue;
}
if (this.literalsToMatch[i + 1] == null)
{
throw new ArgumentException(
"A wildcard path component must be at the end of the path or followed by a literal path component.");
}
}
this.wildcardCount = this.isWildcard.Length;
this.IsWildCardPath = this.wildcardCount > 0;
this.FirstMatchHashKey = !this.IsWildCardPath
? this.PathComponentsCount + PathSeperator + firstLiteralMatch
: WildCardChar + PathSeperator + firstLiteralMatch;
this.typeDeserializer = new StringMapTypeDeserializer(createInstanceFn, getParseFn, this.RequestType);
_propertyNamesMap = new HashSet<string>(
GetSerializableProperties(RequestType).Select(x => x.Name),
StringComparer.OrdinalIgnoreCase);
}
internal static IEnumerable<PropertyInfo> GetSerializableProperties(Type type)
{
foreach (var prop in GetPublicProperties(type))
{
if (prop.GetMethod == null
|| _excludeType == prop.PropertyType)
{
continue;
}
var ignored = false;
foreach (var attr in prop.GetCustomAttributes(true))
{
if (IgnoreAttributesNamed.Contains(attr.GetType().Name))
{
ignored = true;
break;
}
}
if (!ignored)
{
yield return prop;
}
}
}
private static IEnumerable<PropertyInfo> GetPublicProperties(Type type)
{
if (type.IsInterface)
{
var propertyInfos = new List<PropertyInfo>();
var considered = new List<Type>()
{
type
};
var queue = new Queue<Type>();
queue.Enqueue(type);
while (queue.Count > 0)
{
var subType = queue.Dequeue();
foreach (var subInterface in subType.GetTypeInfo().ImplementedInterfaces)
{
if (considered.Contains(subInterface))
{
continue;
}
considered.Add(subInterface);
queue.Enqueue(subInterface);
}
var newPropertyInfos = GetTypesPublicProperties(subType)
.Where(x => !propertyInfos.Contains(x));
propertyInfos.InsertRange(0, newPropertyInfos);
}
return propertyInfos;
}
return GetTypesPublicProperties(type)
.Where(x => x.GetIndexParameters().Length == 0);
}
private static IEnumerable<PropertyInfo> GetTypesPublicProperties(Type subType)
{
foreach (var pi in subType.GetRuntimeProperties())
{
var mi = pi.GetMethod ?? pi.SetMethod;
if (mi != null && mi.IsStatic)
{
continue;
}
yield return pi;
}
}
/// <summary>
/// Provide for quick lookups based on hashes that can be determined from a request url.
/// </summary>
public string FirstMatchHashKey { get; private set; }
private readonly StringMapTypeDeserializer typeDeserializer;
private readonly HashSet<string> _propertyNamesMap;
public int MatchScore(string httpMethod, string[] withPathInfoParts)
{
var isMatch = IsMatch(httpMethod, withPathInfoParts, out var wildcardMatchCount);
if (!isMatch)
{
return -1;
}
// Routes with least wildcard matches get the highest score
var score = Math.Max(100 - wildcardMatchCount, 1) * 1000
// Routes with less variable (and more literal) matches
+ Math.Max(10 - VariableArgsCount, 1) * 100;
// Exact verb match is better than ANY
if (Verbs.Length == 1 && string.Equals(httpMethod, Verbs[0], StringComparison.OrdinalIgnoreCase))
{
score += 10;
}
else
{
score += 1;
}
return score;
}
/// <summary>
/// For performance withPathInfoParts should already be a lower case string
/// to minimize redundant matching operations.
/// </summary>
public bool IsMatch(string httpMethod, string[] withPathInfoParts, out int wildcardMatchCount)
{
wildcardMatchCount = 0;
if (withPathInfoParts.Length != this.PathComponentsCount && !this.IsWildCardPath)
{
return false;
}
if (!Verbs.Contains(httpMethod, StringComparer.OrdinalIgnoreCase))
{
return false;
}
if (!ExplodeComponents(ref withPathInfoParts))
{
return false;
}
if (this.TotalComponentsCount != withPathInfoParts.Length && !this.IsWildCardPath)
{
return false;
}
int pathIx = 0;
for (var i = 0; i < this.TotalComponentsCount; i++)
{
if (this.isWildcard[i])
{
if (i < this.TotalComponentsCount - 1)
{
// Continue to consume up until a match with the next literal
while (pathIx < withPathInfoParts.Length
&& !string.Equals(withPathInfoParts[pathIx], this.literalsToMatch[i + 1], StringComparison.InvariantCultureIgnoreCase))
{
pathIx++;
wildcardMatchCount++;
}
// Ensure there are still enough parts left to match the remainder
if ((withPathInfoParts.Length - pathIx) < (this.TotalComponentsCount - i - 1))
{
return false;
}
}
else
{
// A wildcard at the end matches the remainder of path
wildcardMatchCount += withPathInfoParts.Length - pathIx;
pathIx = withPathInfoParts.Length;
}
}
else
{
var literalToMatch = this.literalsToMatch[i];
if (literalToMatch == null)
{
// Matching an ordinary (non-wildcard) variable consumes a single part
pathIx++;
continue;
}
if (withPathInfoParts.Length <= pathIx
|| !string.Equals(withPathInfoParts[pathIx], literalToMatch, StringComparison.InvariantCultureIgnoreCase))
{
return false;
}
pathIx++;
}
}
return pathIx == withPathInfoParts.Length;
}
private bool ExplodeComponents(ref string[] withPathInfoParts)
{
var totalComponents = new List<string>();
for (var i = 0; i < withPathInfoParts.Length; i++)
{
var component = withPathInfoParts[i];
if (string.IsNullOrEmpty(component))
{
continue;
}
if (this.PathComponentsCount != this.TotalComponentsCount
&& this.componentsWithSeparators[i])
{
var subComponents = component.Split(ComponentSeperator);
if (subComponents.Length < 2)
{
return false;
}
totalComponents.AddRange(subComponents);
}
else
{
totalComponents.Add(component);
}
}
withPathInfoParts = totalComponents.ToArray();
return true;
}
public object CreateRequest(string pathInfo, Dictionary<string, string> queryStringAndFormData, object fromInstance)
{
var requestComponents = pathInfo.Split(new[] { PathSeperatorChar }, StringSplitOptions.RemoveEmptyEntries);
ExplodeComponents(ref requestComponents);
if (requestComponents.Length != this.TotalComponentsCount)
{
var isValidWildCardPath = this.IsWildCardPath
&& requestComponents.Length >= this.TotalComponentsCount - this.wildcardCount;
if (!isValidWildCardPath)
{
throw new ArgumentException(
string.Format(
CultureInfo.InvariantCulture,
"Path Mismatch: Request Path '{0}' has invalid number of components compared to: '{1}'",
pathInfo,
this.restPath));
}
}
var requestKeyValuesMap = new Dictionary<string, string>();
var pathIx = 0;
for (var i = 0; i < this.TotalComponentsCount; i++)
{
var variableName = this.variablesNames[i];
if (variableName == null)
{
pathIx++;
continue;
}
if (!this._propertyNamesMap.Contains(variableName))
{
if (string.Equals("ignore", variableName, StringComparison.OrdinalIgnoreCase))
{
pathIx++;
continue;
}
throw new ArgumentException("Could not find property "
+ variableName + " on " + RequestType.GetMethodName());
}
var value = requestComponents.Length > pathIx ? requestComponents[pathIx] : null; // wildcard has arg mismatch
if (value != null && this.isWildcard[i])
{
if (i == this.TotalComponentsCount - 1)
{
// Wildcard at end of path definition consumes all the rest
var sb = new StringBuilder();
sb.Append(value);
for (var j = pathIx + 1; j < requestComponents.Length; j++)
{
sb.Append(PathSeperatorChar)
.Append(requestComponents[j]);
}
value = sb.ToString();
}
else
{
// Wildcard in middle of path definition consumes up until it
// hits a match for the next element in the definition (which must be a literal)
// It may consume 0 or more path parts
var stopLiteral = i == this.TotalComponentsCount - 1 ? null : this.literalsToMatch[i + 1];
if (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
{
var sb = new StringBuilder(value);
pathIx++;
while (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
{
sb.Append(PathSeperatorChar)
.Append(requestComponents[pathIx++]);
}
value = sb.ToString();
}
else
{
value = null;
}
}
}
else
{
// Variable consumes single path item
pathIx++;
}
requestKeyValuesMap[variableName] = value;
}
if (queryStringAndFormData != null)
{
// Query String and form data can override variable path matches
// path variables < query string < form data
foreach (var name in queryStringAndFormData)
{
requestKeyValuesMap[name.Key] = name.Value;
}
}
return this.typeDeserializer.PopulateFromMap(fromInstance, requestKeyValuesMap);
}
public class RestPathMap : SortedDictionary<string, List<RestPath>>
{
public RestPathMap() : base(StringComparer.OrdinalIgnoreCase)
{
}
}
}
}

View File

@@ -1,118 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Reflection;
using MediaBrowser.Common.Extensions;
namespace Emby.Server.Implementations.Services
{
/// <summary>
/// Serializer cache of delegates required to create a type from a string map (e.g. for REST urls)
/// </summary>
public class StringMapTypeDeserializer
{
internal class PropertySerializerEntry
{
public PropertySerializerEntry(Action<object, object> propertySetFn, Func<string, object> propertyParseStringFn, Type propertyType)
{
PropertySetFn = propertySetFn;
PropertyParseStringFn = propertyParseStringFn;
PropertyType = propertyType;
}
public Action<object, object> PropertySetFn { get; private set; }
public Func<string, object> PropertyParseStringFn { get; private set; }
public Type PropertyType { get; private set; }
}
private readonly Type type;
private readonly Dictionary<string, PropertySerializerEntry> propertySetterMap
= new Dictionary<string, PropertySerializerEntry>(StringComparer.OrdinalIgnoreCase);
public Func<string, object> GetParseFn(Type propertyType)
{
if (propertyType == typeof(string))
{
return s => s;
}
return _GetParseFn(propertyType);
}
private readonly Func<Type, object> _CreateInstanceFn;
private readonly Func<Type, Func<string, object>> _GetParseFn;
public StringMapTypeDeserializer(Func<Type, object> createInstanceFn, Func<Type, Func<string, object>> getParseFn, Type type)
{
_CreateInstanceFn = createInstanceFn;
_GetParseFn = getParseFn;
this.type = type;
foreach (var propertyInfo in RestPath.GetSerializableProperties(type))
{
var propertySetFn = TypeAccessor.GetSetPropertyMethod(propertyInfo);
var propertyType = propertyInfo.PropertyType;
var propertyParseStringFn = GetParseFn(propertyType);
var propertySerializer = new PropertySerializerEntry(propertySetFn, propertyParseStringFn, propertyType);
propertySetterMap[propertyInfo.Name] = propertySerializer;
}
}
public object PopulateFromMap(object instance, IDictionary<string, string> keyValuePairs)
{
PropertySerializerEntry propertySerializerEntry = null;
if (instance == null)
{
instance = _CreateInstanceFn(type);
}
foreach (var pair in keyValuePairs)
{
string propertyName = pair.Key;
string propertyTextValue = pair.Value;
if (propertyTextValue == null
|| !propertySetterMap.TryGetValue(propertyName, out propertySerializerEntry)
|| propertySerializerEntry.PropertySetFn == null)
{
continue;
}
if (propertySerializerEntry.PropertyType == typeof(bool))
{
// InputExtensions.cs#530 MVC Checkbox helper emits extra hidden input field, generating 2 values, first is the real value
propertyTextValue = StringExtensions.LeftPart(propertyTextValue, ',').ToString();
}
var value = propertySerializerEntry.PropertyParseStringFn(propertyTextValue);
if (value == null)
{
continue;
}
propertySerializerEntry.PropertySetFn(instance, value);
}
return instance;
}
}
internal static class TypeAccessor
{
public static Action<object, object> GetSetPropertyMethod(PropertyInfo propertyInfo)
{
if (!propertyInfo.CanWrite || propertyInfo.GetIndexParameters().Length > 0)
{
return null;
}
var setMethodInfo = propertyInfo.SetMethod;
return (instance, value) => setMethodInfo.Invoke(instance, new[] { value });
}
}
}

View File

@@ -1,27 +0,0 @@
#pragma warning disable CS1591
using System;
using MediaBrowser.Common.Extensions;
namespace Emby.Server.Implementations.Services
{
/// <summary>
/// Donated by Ivan Korneliuk from his post:
/// http://korneliuk.blogspot.com/2012/08/servicestack-reusing-dtos.html
///
/// Modified to only allow using routes matching the supplied HTTP Verb.
/// </summary>
public static class UrlExtensions
{
public static string GetMethodName(this Type type)
{
var typeName = type.FullName != null // can be null, e.g. generic types
? StringExtensions.LeftPart(type.FullName, "[[", StringComparison.Ordinal).ToString() // Generic Fullname
.Replace(type.Namespace + ".", string.Empty, StringComparison.Ordinal) // Trim Namespaces
.Replace("+", ".", StringComparison.Ordinal) // Convert nested into normal type
: type.Name;
return type.IsGenericParameter ? "'" + typeName : typeName;
}
}
}

View File

@@ -1429,6 +1429,24 @@ namespace Emby.Server.Implementations.Session
return AuthenticateNewSessionInternal(request, false);
}
public Task<AuthenticationResult> AuthenticateQuickConnect(AuthenticationRequest request, string token)
{
var result = _authRepo.Get(new AuthenticationInfoQuery()
{
AccessToken = token,
DeviceId = _appHost.SystemId,
Limit = 1
});
if (result.TotalRecordCount == 0)
{
throw new SecurityException("Unknown quick connect token");
}
request.UserId = result.Items[0].UserId;
return AuthenticateNewSessionInternal(request, false);
}
private async Task<AuthenticationResult> AuthenticateNewSessionInternal(AuthenticationRequest request, bool enforcePassword)
{
CheckDisposed();

View File

@@ -44,7 +44,7 @@ namespace Emby.Server.Implementations.Session
private readonly ILogger<SessionWebSocketListener> _logger;
private readonly ILoggerFactory _loggerFactory;
private readonly IHttpServer _httpServer;
private readonly IWebSocketManager _webSocketManager;
/// <summary>
/// The KeepAlive cancellation token.
@@ -72,19 +72,19 @@ namespace Emby.Server.Implementations.Session
/// <param name="logger">The logger.</param>
/// <param name="sessionManager">The session manager.</param>
/// <param name="loggerFactory">The logger factory.</param>
/// <param name="httpServer">The HTTP server.</param>
/// <param name="webSocketManager">The HTTP server.</param>
public SessionWebSocketListener(
ILogger<SessionWebSocketListener> logger,
ISessionManager sessionManager,
ILoggerFactory loggerFactory,
IHttpServer httpServer)
IWebSocketManager webSocketManager)
{
_logger = logger;
_sessionManager = sessionManager;
_loggerFactory = loggerFactory;
_httpServer = httpServer;
_webSocketManager = webSocketManager;
httpServer.WebSocketConnected += OnServerManagerWebSocketConnected;
webSocketManager.WebSocketConnected += OnServerManagerWebSocketConnected;
}
private async void OnServerManagerWebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e)
@@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.Session
/// <inheritdoc />
public void Dispose()
{
_httpServer.WebSocketConnected -= OnServerManagerWebSocketConnected;
_webSocketManager.WebSocketConnected -= OnServerManagerWebSocketConnected;
StopKeepAlive();
}

View File

@@ -1,248 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Mime;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using IHttpRequest = MediaBrowser.Model.Services.IHttpRequest;
namespace Emby.Server.Implementations.SocketSharp
{
public class WebSocketSharpRequest : IHttpRequest
{
private const string FormUrlEncoded = "application/x-www-form-urlencoded";
private const string MultiPartFormData = "multipart/form-data";
private const string Soap11 = "text/xml; charset=utf-8";
private string _remoteIp;
private Dictionary<string, object> _items;
private string _responseContentType;
public WebSocketSharpRequest(HttpRequest httpRequest, HttpResponse httpResponse, string operationName)
{
this.OperationName = operationName;
this.Request = httpRequest;
this.Response = httpResponse;
}
public string Accept => StringValues.IsNullOrEmpty(Request.Headers[HeaderNames.Accept]) ? null : Request.Headers[HeaderNames.Accept].ToString();
public string Authorization => StringValues.IsNullOrEmpty(Request.Headers[HeaderNames.Authorization]) ? null : Request.Headers[HeaderNames.Authorization].ToString();
public HttpRequest Request { get; }
public HttpResponse Response { get; }
public string OperationName { get; set; }
public string RawUrl => Request.GetEncodedPathAndQuery();
public string AbsoluteUri => Request.GetDisplayUrl().TrimEnd('/');
public string RemoteIp
{
get
{
if (_remoteIp != null)
{
return _remoteIp;
}
IPAddress ip;
// "Real" remote ip might be in X-Forwarded-For of X-Real-Ip
// (if the server is behind a reverse proxy for example)
if (!IPAddress.TryParse(GetHeader(CustomHeaderNames.XForwardedFor), out ip))
{
if (!IPAddress.TryParse(GetHeader(CustomHeaderNames.XRealIP), out ip))
{
ip = Request.HttpContext.Connection.RemoteIpAddress;
// Default to the loopback address if no RemoteIpAddress is specified (i.e. during integration tests)
ip ??= IPAddress.Loopback;
}
}
return _remoteIp = NormalizeIp(ip).ToString();
}
}
public string[] AcceptTypes => Request.Headers.GetCommaSeparatedValues(HeaderNames.Accept);
public Dictionary<string, object> Items => _items ?? (_items = new Dictionary<string, object>());
public string ResponseContentType
{
get =>
_responseContentType
?? (_responseContentType = GetResponseContentType(Request));
set => _responseContentType = value;
}
public string PathInfo => Request.Path.Value;
public string UserAgent => Request.Headers[HeaderNames.UserAgent];
public IHeaderDictionary Headers => Request.Headers;
public IQueryCollection QueryString => Request.Query;
public bool IsLocal =>
(Request.HttpContext.Connection.LocalIpAddress == null
&& Request.HttpContext.Connection.RemoteIpAddress == null)
|| Request.HttpContext.Connection.LocalIpAddress.Equals(Request.HttpContext.Connection.RemoteIpAddress);
public string HttpMethod => Request.Method;
public string Verb => HttpMethod;
public string ContentType => Request.ContentType;
public Uri UrlReferrer => Request.GetTypedHeaders().Referer;
public Stream InputStream => Request.Body;
public long ContentLength => Request.ContentLength ?? 0;
private string GetHeader(string name) => Request.Headers[name].ToString();
private static IPAddress NormalizeIp(IPAddress ip)
{
if (ip.IsIPv4MappedToIPv6)
{
return ip.MapToIPv4();
}
return ip;
}
public static string GetResponseContentType(HttpRequest httpReq)
{
var specifiedContentType = GetQueryStringContentType(httpReq);
if (!string.IsNullOrEmpty(specifiedContentType))
{
return specifiedContentType;
}
const string ServerDefaultContentType = MediaTypeNames.Application.Json;
var acceptContentTypes = httpReq.Headers.GetCommaSeparatedValues(HeaderNames.Accept);
string defaultContentType = null;
if (HasAnyOfContentTypes(httpReq, FormUrlEncoded, MultiPartFormData))
{
defaultContentType = ServerDefaultContentType;
}
var acceptsAnything = false;
var hasDefaultContentType = defaultContentType != null;
if (acceptContentTypes != null)
{
foreach (ReadOnlySpan<char> acceptsType in acceptContentTypes)
{
ReadOnlySpan<char> contentType = acceptsType;
var index = contentType.IndexOf(';');
if (index != -1)
{
contentType = contentType.Slice(0, index);
}
contentType = contentType.Trim();
acceptsAnything = contentType.Equals("*/*", StringComparison.OrdinalIgnoreCase);
if (acceptsAnything)
{
break;
}
}
if (acceptsAnything)
{
if (hasDefaultContentType)
{
return defaultContentType;
}
else
{
return ServerDefaultContentType;
}
}
}
if (acceptContentTypes == null && httpReq.ContentType == Soap11)
{
return Soap11;
}
// We could also send a '406 Not Acceptable', but this is allowed also
return ServerDefaultContentType;
}
public static bool HasAnyOfContentTypes(HttpRequest request, params string[] contentTypes)
{
if (contentTypes == null || request.ContentType == null)
{
return false;
}
foreach (var contentType in contentTypes)
{
if (IsContentType(request, contentType))
{
return true;
}
}
return false;
}
public static bool IsContentType(HttpRequest request, string contentType)
{
return request.ContentType.StartsWith(contentType, StringComparison.OrdinalIgnoreCase);
}
private static string GetQueryStringContentType(HttpRequest httpReq)
{
ReadOnlySpan<char> format = httpReq.Query["format"].ToString();
if (format == ReadOnlySpan<char>.Empty)
{
const int FormatMaxLength = 4;
ReadOnlySpan<char> pi = httpReq.Path.ToString();
if (pi == null || pi.Length <= FormatMaxLength)
{
return null;
}
if (pi[0] == '/')
{
pi = pi.Slice(1);
}
format = pi.LeftPart('/');
if (format.Length > FormatMaxLength)
{
return null;
}
}
format = format.LeftPart('.');
if (format.Contains("json", StringComparison.OrdinalIgnoreCase))
{
return "application/json";
}
else if (format.Contains("xml", StringComparison.OrdinalIgnoreCase))
{
return "application/xml";
}
return null;
}
}
}