mirror of
https://github.com/jellyfin/jellyfin.git
synced 2024-11-15 18:08:53 -07:00
Merge pull request #4039 from cvium/remove_shit_and_shit_adjacent_shit
Remove ServiceStack and related stuff
This commit is contained in:
commit
52b34eb407
@ -41,7 +41,6 @@ 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;
|
||||
@ -90,7 +89,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;
|
||||
@ -544,8 +542,6 @@ namespace Emby.Server.Implementations
|
||||
|
||||
ServiceCollection.AddSingleton<IZipClient, ZipClient>();
|
||||
|
||||
ServiceCollection.AddSingleton<IHttpResultFactory, HttpResultFactory>();
|
||||
|
||||
ServiceCollection.AddSingleton<IServerApplicationHost>(this);
|
||||
ServiceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
|
||||
|
||||
@ -581,7 +577,6 @@ namespace Emby.Server.Implementations
|
||||
|
||||
ServiceCollection.AddSingleton<ISearchEngine, SearchEngine>();
|
||||
|
||||
ServiceCollection.AddSingleton<ServiceController>();
|
||||
ServiceCollection.AddSingleton<IHttpServer, HttpListenerHost>();
|
||||
|
||||
ServiceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
|
||||
@ -757,7 +752,6 @@ namespace Emby.Server.Implementations
|
||||
CollectionFolder.XmlSerializer = _xmlSerializer;
|
||||
CollectionFolder.JsonSerializer = Resolve<IJsonSerializer>();
|
||||
CollectionFolder.ApplicationHost = this;
|
||||
AuthenticatedAttribute.AuthService = Resolve<IAuthService>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -777,7 +771,7 @@ namespace Emby.Server.Implementations
|
||||
.Where(i => i != null)
|
||||
.ToArray();
|
||||
|
||||
_httpServer.Init(GetExportTypes<IService>(), GetExports<IWebSocketListener>(), GetUrlPrefixes());
|
||||
_httpServer.Init(GetExports<IWebSocketListener>(), GetUrlPrefixes());
|
||||
|
||||
Resolve<ILibraryManager>().AddParts(
|
||||
GetExports<IResolverIgnoreRule>(),
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -7,11 +7,8 @@ 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;
|
||||
@ -20,8 +17,6 @@ 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;
|
||||
@ -29,7 +24,6 @@ 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
|
||||
{
|
||||
@ -46,13 +40,9 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
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>();
|
||||
@ -64,10 +54,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
IServerConfigurationManager config,
|
||||
IConfiguration configuration,
|
||||
INetworkManager networkManager,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IXmlSerializer xmlSerializer,
|
||||
ILocalizationManager localizationManager,
|
||||
ServiceController serviceController,
|
||||
IHostEnvironment hostEnvironment,
|
||||
ILoggerFactory loggerFactory)
|
||||
{
|
||||
@ -77,102 +64,21 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
_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)
|
||||
@ -210,7 +116,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ErrorHandler(Exception ex, IRequest httpReq, int statusCode, string urlToLog, bool ignoreStackTrace)
|
||||
private async Task ErrorHandler(Exception ex, HttpContext httpContext, int statusCode, string urlToLog, bool ignoreStackTrace)
|
||||
{
|
||||
if (ignoreStackTrace)
|
||||
{
|
||||
@ -221,7 +127,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
_logger.LogError(ex, "Error processing request. URL: {Url}", urlToLog);
|
||||
}
|
||||
|
||||
var httpRes = httpReq.Response;
|
||||
var httpRes = httpContext.Response;
|
||||
|
||||
if (httpRes.HasStarted)
|
||||
{
|
||||
@ -395,24 +301,22 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
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);
|
||||
return RequestHandler(context, 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)
|
||||
private async Task RequestHandler(HttpContext httpContext, CancellationToken cancellationToken)
|
||||
{
|
||||
var stopWatch = new Stopwatch();
|
||||
stopWatch.Start();
|
||||
var httpRes = httpReq.Response;
|
||||
var httpRes = httpContext.Response;
|
||||
var host = httpContext.Request.Host.ToString();
|
||||
var localPath = httpContext.Request.Path.ToString();
|
||||
var urlString = httpContext.Request.GetDisplayUrl();
|
||||
string urlToLog = GetUrlToLog(urlString);
|
||||
string remoteIp = httpReq.RemoteIp;
|
||||
string remoteIp = httpContext.Request.RemoteIp();
|
||||
|
||||
try
|
||||
{
|
||||
@ -432,7 +336,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ValidateRequest(remoteIp, httpReq.IsLocal))
|
||||
if (!ValidateRequest(remoteIp, httpContext.Request.IsLocal()))
|
||||
{
|
||||
httpRes.StatusCode = 403;
|
||||
httpRes.ContentType = "text/plain";
|
||||
@ -440,16 +344,16 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ValidateSsl(httpReq.RemoteIp, urlString))
|
||||
if (!ValidateSsl(httpContext.Request.RemoteIp(), urlString))
|
||||
{
|
||||
RedirectToSecureUrl(httpReq, httpRes, urlString);
|
||||
RedirectToSecureUrl(httpRes, urlString);
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(httpContext.Request.Method, "OPTIONS", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
httpRes.StatusCode = 200;
|
||||
foreach (var (key, value) in GetDefaultCorsHeaders(httpReq))
|
||||
foreach (var (key, value) in GetDefaultCorsHeaders(httpContext))
|
||||
{
|
||||
httpRes.Headers.Add(key, value);
|
||||
}
|
||||
@ -483,15 +387,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
}
|
||||
}
|
||||
|
||||
var handler = GetServiceHandler(httpReq);
|
||||
if (handler != null)
|
||||
{
|
||||
await handler.ProcessRequestAsync(this, httpReq, httpRes, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
catch (Exception requestEx)
|
||||
{
|
||||
@ -500,7 +396,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
var requestInnerEx = GetActualException(requestEx);
|
||||
var statusCode = GetStatusCode(requestInnerEx);
|
||||
|
||||
foreach (var (key, value) in GetDefaultCorsHeaders(httpReq))
|
||||
foreach (var (key, value) in GetDefaultCorsHeaders(httpContext))
|
||||
{
|
||||
if (!httpRes.Headers.ContainsKey(key))
|
||||
{
|
||||
@ -525,7 +421,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
throw;
|
||||
}
|
||||
|
||||
await ErrorHandler(requestInnerEx, httpReq, statusCode, urlToLog, ignoreStackTrace).ConfigureAwait(false);
|
||||
await ErrorHandler(requestInnerEx, httpContext, statusCode, urlToLog, ignoreStackTrace).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception handlerException)
|
||||
{
|
||||
@ -591,17 +487,13 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the default CORS headers.
|
||||
/// </summary>
|
||||
/// <param name="req"></param>
|
||||
/// <returns></returns>
|
||||
public IDictionary<string, string> GetDefaultCorsHeaders(IRequest req)
|
||||
/// <inheritdoc />
|
||||
public IDictionary<string, string> GetDefaultCorsHeaders(HttpContext httpContext)
|
||||
{
|
||||
var origin = req.Headers["Origin"];
|
||||
var origin = httpContext.Request.Headers["Origin"];
|
||||
if (origin == StringValues.Empty)
|
||||
{
|
||||
origin = req.Headers["Host"];
|
||||
origin = httpContext.Request.Headers["Host"];
|
||||
if (origin == StringValues.Empty)
|
||||
{
|
||||
origin = "*";
|
||||
@ -616,23 +508,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
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)
|
||||
private void RedirectToSecureUrl(HttpResponse httpRes, string url)
|
||||
{
|
||||
if (Uri.TryCreate(url, UriKind.Absolute, out Uri uri))
|
||||
{
|
||||
@ -650,95 +526,12 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// <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)
|
||||
public void Init(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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using MediaBrowser.Model.Services;
|
||||
using System.Net;
|
||||
using MediaBrowser.Common.Net;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace MediaBrowser.Common.Extensions
|
||||
@ -8,26 +9,55 @@ namespace MediaBrowser.Common.Extensions
|
||||
/// </summary>
|
||||
public static class HttpContextExtensions
|
||||
{
|
||||
private const string ServiceStackRequest = "ServiceStackRequest";
|
||||
|
||||
/// <summary>
|
||||
/// Set the ServiceStack request.
|
||||
/// Checks the origin of the HTTP request.
|
||||
/// </summary>
|
||||
/// <param name="httpContext">The HttpContext instance.</param>
|
||||
/// <param name="request">The service stack request instance.</param>
|
||||
public static void SetServiceStackRequest(this HttpContext httpContext, IRequest request)
|
||||
/// <param name="request">The incoming HTTP request.</param>
|
||||
/// <returns><c>true</c> if the request is coming from LAN, <c>false</c> otherwise.</returns>
|
||||
public static bool IsLocal(this HttpRequest request)
|
||||
{
|
||||
httpContext.Items[ServiceStackRequest] = request;
|
||||
return (request.HttpContext.Connection.LocalIpAddress == null
|
||||
&& request.HttpContext.Connection.RemoteIpAddress == null)
|
||||
|| request.HttpContext.Connection.LocalIpAddress.Equals(request.HttpContext.Connection.RemoteIpAddress);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the ServiceStack request.
|
||||
/// Extracts the remote IP address of the caller of the HTTP request.
|
||||
/// </summary>
|
||||
/// <param name="httpContext">The HttpContext instance.</param>
|
||||
/// <returns>The service stack request instance.</returns>
|
||||
public static IRequest GetServiceStackRequest(this HttpContext httpContext)
|
||||
/// <param name="request">The HTTP request.</param>
|
||||
/// <returns>The remote caller IP address.</returns>
|
||||
public static string RemoteIp(this HttpRequest request)
|
||||
{
|
||||
return (IRequest)httpContext.Items[ServiceStackRequest];
|
||||
var cachedRemoteIp = request.HttpContext.Items["RemoteIp"].ToString();
|
||||
if (!string.IsNullOrEmpty(cachedRemoteIp))
|
||||
{
|
||||
return cachedRemoteIp;
|
||||
}
|
||||
|
||||
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(request.Headers[CustomHeaderNames.XForwardedFor].ToString(), out ip))
|
||||
{
|
||||
if (!IPAddress.TryParse(request.Headers[CustomHeaderNames.XRealIP].ToString(), 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;
|
||||
}
|
||||
}
|
||||
|
||||
if (ip.IsIPv4MappedToIPv6)
|
||||
{
|
||||
ip = ip.MapToIPv4();
|
||||
}
|
||||
|
||||
var normalizedIp = ip.ToString();
|
||||
|
||||
request.HttpContext.Items["RemoteIp"] = normalizedIp;
|
||||
return normalizedIp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Services;
|
||||
|
||||
namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
@ -63,26 +62,20 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
/// <value>The id.</value>
|
||||
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
[ApiMember(Name = "MediaSourceId", Description = "The media version id, if playing an alternate version", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public string MediaSourceId { get; set; }
|
||||
|
||||
[ApiMember(Name = "DeviceId", Description = "The device id of the client requesting. Used to stop encoding processes when needed.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||
public string DeviceId { get; set; }
|
||||
|
||||
[ApiMember(Name = "Container", Description = "Container", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||
public string Container { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the audio codec.
|
||||
/// </summary>
|
||||
/// <value>The audio codec.</value>
|
||||
[ApiMember(Name = "AudioCodec", Description = "Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||
public string AudioCodec { get; set; }
|
||||
|
||||
[ApiMember(Name = "EnableAutoStreamCopy", Description = "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
|
||||
public bool EnableAutoStreamCopy { get; set; }
|
||||
|
||||
public bool AllowVideoStreamCopy { get; set; }
|
||||
@ -95,7 +88,6 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
/// Gets or sets the audio sample rate.
|
||||
/// </summary>
|
||||
/// <value>The audio sample rate.</value>
|
||||
[ApiMember(Name = "AudioSampleRate", Description = "Optional. Specify a specific audio sample rate, e.g. 44100", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
||||
public int? AudioSampleRate { get; set; }
|
||||
|
||||
public int? MaxAudioBitDepth { get; set; }
|
||||
@ -104,105 +96,86 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
/// Gets or sets the audio bit rate.
|
||||
/// </summary>
|
||||
/// <value>The audio bit rate.</value>
|
||||
[ApiMember(Name = "AudioBitRate", Description = "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
||||
public int? AudioBitRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the audio channels.
|
||||
/// </summary>
|
||||
/// <value>The audio channels.</value>
|
||||
[ApiMember(Name = "AudioChannels", Description = "Optional. Specify a specific number of audio channels to encode to, e.g. 2", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
||||
public int? AudioChannels { get; set; }
|
||||
|
||||
[ApiMember(Name = "MaxAudioChannels", Description = "Optional. Specify a maximum number of audio channels to encode to, e.g. 2", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
||||
public int? MaxAudioChannels { get; set; }
|
||||
|
||||
[ApiMember(Name = "Static", Description = "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
|
||||
public bool Static { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the profile.
|
||||
/// </summary>
|
||||
/// <value>The profile.</value>
|
||||
[ApiMember(Name = "Profile", Description = "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||
public string Profile { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the level.
|
||||
/// </summary>
|
||||
/// <value>The level.</value>
|
||||
[ApiMember(Name = "Level", Description = "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||
public string Level { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the framerate.
|
||||
/// </summary>
|
||||
/// <value>The framerate.</value>
|
||||
[ApiMember(Name = "Framerate", Description = "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", IsRequired = false, DataType = "double", ParameterType = "query", Verb = "GET")]
|
||||
public float? Framerate { get; set; }
|
||||
|
||||
[ApiMember(Name = "MaxFramerate", Description = "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", IsRequired = false, DataType = "double", ParameterType = "query", Verb = "GET")]
|
||||
public float? MaxFramerate { get; set; }
|
||||
|
||||
[ApiMember(Name = "CopyTimestamps", Description = "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
|
||||
public bool CopyTimestamps { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the start time ticks.
|
||||
/// </summary>
|
||||
/// <value>The start time ticks.</value>
|
||||
[ApiMember(Name = "StartTimeTicks", Description = "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
||||
public long? StartTimeTicks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the width.
|
||||
/// </summary>
|
||||
/// <value>The width.</value>
|
||||
[ApiMember(Name = "Width", Description = "Optional. The fixed horizontal resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
||||
public int? Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the height.
|
||||
/// </summary>
|
||||
/// <value>The height.</value>
|
||||
[ApiMember(Name = "Height", Description = "Optional. The fixed vertical resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
||||
public int? Height { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the width of the max.
|
||||
/// </summary>
|
||||
/// <value>The width of the max.</value>
|
||||
[ApiMember(Name = "MaxWidth", Description = "Optional. The maximum horizontal resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
||||
public int? MaxWidth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the height of the max.
|
||||
/// </summary>
|
||||
/// <value>The height of the max.</value>
|
||||
[ApiMember(Name = "MaxHeight", Description = "Optional. The maximum vertical resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
||||
public int? MaxHeight { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the video bit rate.
|
||||
/// </summary>
|
||||
/// <value>The video bit rate.</value>
|
||||
[ApiMember(Name = "VideoBitRate", Description = "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
||||
public int? VideoBitRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the index of the subtitle stream.
|
||||
/// </summary>
|
||||
/// <value>The index of the subtitle stream.</value>
|
||||
[ApiMember(Name = "SubtitleStreamIndex", Description = "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
||||
public int? SubtitleStreamIndex { get; set; }
|
||||
|
||||
[ApiMember(Name = "SubtitleMethod", Description = "Optional. Specify the subtitle delivery method.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||
public SubtitleDeliveryMethod SubtitleMethod { get; set; }
|
||||
|
||||
[ApiMember(Name = "MaxRefFrames", Description = "Optional.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
||||
public int? MaxRefFrames { get; set; }
|
||||
|
||||
[ApiMember(Name = "MaxVideoBitDepth", Description = "Optional.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
||||
public int? MaxVideoBitDepth { get; set; }
|
||||
|
||||
public bool RequireAvc { get; set; }
|
||||
@ -223,7 +196,6 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
/// Gets or sets the video codec.
|
||||
/// </summary>
|
||||
/// <value>The video codec.</value>
|
||||
[ApiMember(Name = "VideoCodec", Description = "Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||
public string VideoCodec { get; set; }
|
||||
|
||||
public string SubtitleCodec { get; set; }
|
||||
@ -234,14 +206,12 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
/// Gets or sets the index of the audio stream.
|
||||
/// </summary>
|
||||
/// <value>The index of the audio stream.</value>
|
||||
[ApiMember(Name = "AudioStreamIndex", Description = "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
||||
public int? AudioStreamIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the index of the video stream.
|
||||
/// </summary>
|
||||
/// <value>The index of the video stream.</value>
|
||||
[ApiMember(Name = "VideoStreamIndex", Description = "Optional. The index of the video stream to use. If omitted the first video stream will be used.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
||||
public int? VideoStreamIndex { get; set; }
|
||||
|
||||
public EncodingContext Context { get; set; }
|
||||
|
@ -1,76 +0,0 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using MediaBrowser.Model.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace MediaBrowser.Controller.Net
|
||||
{
|
||||
public class AuthenticatedAttribute : Attribute, IHasRequestFilter, IAuthenticationAttributes
|
||||
{
|
||||
public static IAuthService AuthService { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the roles.
|
||||
/// </summary>
|
||||
/// <value>The roles.</value>
|
||||
public string Roles { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether [escape parental control].
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [escape parental control]; otherwise, <c>false</c>.</value>
|
||||
public bool EscapeParentalControl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether [allow before startup wizard].
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [allow before startup wizard]; otherwise, <c>false</c>.</value>
|
||||
public bool AllowBeforeStartupWizard { get; set; }
|
||||
|
||||
public bool AllowLocal { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The request filter is executed before the service.
|
||||
/// </summary>
|
||||
/// <param name="request">The http request wrapper.</param>
|
||||
/// <param name="response">The http response wrapper.</param>
|
||||
/// <param name="requestDto">The request DTO.</param>
|
||||
public void RequestFilter(IRequest request, HttpResponse response, object requestDto)
|
||||
{
|
||||
AuthService.Authenticate(request, this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Order in which Request Filters are executed.
|
||||
/// <0 Executed before global request filters
|
||||
/// >0 Executed after global request filters
|
||||
/// </summary>
|
||||
/// <value>The priority.</value>
|
||||
public int Priority => 0;
|
||||
|
||||
public string[] GetRoles()
|
||||
{
|
||||
return (Roles ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
public bool IgnoreLegacyAuth { get; set; }
|
||||
|
||||
public bool AllowLocalOnly { get; set; }
|
||||
}
|
||||
|
||||
public interface IAuthenticationAttributes
|
||||
{
|
||||
bool EscapeParentalControl { get; }
|
||||
|
||||
bool AllowBeforeStartupWizard { get; }
|
||||
|
||||
bool AllowLocal { get; }
|
||||
|
||||
bool AllowLocalOnly { get; }
|
||||
|
||||
string[] GetRoles();
|
||||
|
||||
bool IgnoreLegacyAuth { get; }
|
||||
}
|
||||
}
|
@ -1,7 +1,5 @@
|
||||
#nullable enable
|
||||
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Model.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace MediaBrowser.Controller.Net
|
||||
@ -11,21 +9,6 @@ namespace MediaBrowser.Controller.Net
|
||||
/// </summary>
|
||||
public interface IAuthService
|
||||
{
|
||||
/// <summary>
|
||||
/// Authenticate and authorize request.
|
||||
/// </summary>
|
||||
/// <param name="request">Request.</param>
|
||||
/// <param name="authAttribtutes">Authorization attributes.</param>
|
||||
void Authenticate(IRequest request, IAuthenticationAttributes authAttribtutes);
|
||||
|
||||
/// <summary>
|
||||
/// Authenticate and authorize request.
|
||||
/// </summary>
|
||||
/// <param name="request">Request.</param>
|
||||
/// <param name="authAttribtutes">Authorization attributes.</param>
|
||||
/// <returns>Authenticated user.</returns>
|
||||
User? Authenticate(HttpRequest request, IAuthenticationAttributes authAttribtutes);
|
||||
|
||||
/// <summary>
|
||||
/// Authenticate request.
|
||||
/// </summary>
|
||||
|
@ -1,4 +1,3 @@
|
||||
using MediaBrowser.Model.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace MediaBrowser.Controller.Net
|
||||
@ -13,14 +12,7 @@ namespace MediaBrowser.Controller.Net
|
||||
/// </summary>
|
||||
/// <param name="requestContext">The request context.</param>
|
||||
/// <returns>AuthorizationInfo.</returns>
|
||||
AuthorizationInfo GetAuthorizationInfo(object requestContext);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authorization information.
|
||||
/// </summary>
|
||||
/// <param name="requestContext">The request context.</param>
|
||||
/// <returns>AuthorizationInfo.</returns>
|
||||
AuthorizationInfo GetAuthorizationInfo(IRequest requestContext);
|
||||
AuthorizationInfo GetAuthorizationInfo(HttpContext requestContext);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authorization information.
|
||||
|
@ -1,17 +0,0 @@
|
||||
using MediaBrowser.Model.Services;
|
||||
|
||||
namespace MediaBrowser.Controller.Net
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface IHasResultFactory
|
||||
/// Services that require a ResultFactory should implement this
|
||||
/// </summary>
|
||||
public interface IHasResultFactory : IRequiresRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the result factory.
|
||||
/// </summary>
|
||||
/// <value>The result factory.</value>
|
||||
IHttpResultFactory ResultFactory { get; set; }
|
||||
}
|
||||
}
|
@ -1,82 +0,0 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.Services;
|
||||
|
||||
namespace MediaBrowser.Controller.Net
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface IHttpResultFactory.
|
||||
/// </summary>
|
||||
public interface IHttpResultFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the result.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
object GetResult(string content, string contentType, IDictionary<string, string> responseHeaders = null);
|
||||
|
||||
object GetResult(IRequest requestContext, byte[] content, string contentType, IDictionary<string, string> responseHeaders = null);
|
||||
|
||||
object GetResult(IRequest requestContext, Stream content, string contentType, IDictionary<string, string> responseHeaders = null);
|
||||
|
||||
object GetResult(IRequest requestContext, string content, string contentType, IDictionary<string, string> responseHeaders = null);
|
||||
|
||||
object GetRedirectResult(string url);
|
||||
|
||||
object GetResult<T>(IRequest requestContext, T result, IDictionary<string, string> responseHeaders = null)
|
||||
where T : class;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the static result.
|
||||
/// </summary>
|
||||
/// <param name="requestContext">The request context.</param>
|
||||
/// <param name="cacheKey">The cache key.</param>
|
||||
/// <param name="lastDateModified">The last date modified.</param>
|
||||
/// <param name="cacheDuration">Duration of the cache.</param>
|
||||
/// <param name="contentType">Type of the content.</param>
|
||||
/// <param name="factoryFn">The factory fn.</param>
|
||||
/// <param name="responseHeaders">The response headers.</param>
|
||||
/// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
|
||||
/// <returns>System.Object.</returns>
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the static result.
|
||||
/// </summary>
|
||||
/// <param name="requestContext">The request context.</param>
|
||||
/// <param name="options">The options.</param>
|
||||
/// <returns>System.Object.</returns>
|
||||
Task<object> GetStaticResult(IRequest requestContext, StaticResultOptions options);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the static file result.
|
||||
/// </summary>
|
||||
/// <param name="requestContext">The request context.</param>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="fileShare">The file share.</param>
|
||||
/// <returns>System.Object.</returns>
|
||||
Task<object> GetStaticFileResult(IRequest requestContext, string path, FileShare fileShare = FileShare.Read);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the static file result.
|
||||
/// </summary>
|
||||
/// <param name="requestContext">The request context.</param>
|
||||
/// <param name="options">The options.</param>
|
||||
/// <returns>System.Object.</returns>
|
||||
Task<object> GetStaticFileResult(IRequest requestContext,
|
||||
StaticFileResultOptions options);
|
||||
}
|
||||
}
|
@ -2,7 +2,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Events;
|
||||
using MediaBrowser.Model.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace MediaBrowser.Controller.Net
|
||||
@ -26,7 +25,7 @@ namespace MediaBrowser.Controller.Net
|
||||
/// <summary>
|
||||
/// Inits this instance.
|
||||
/// </summary>
|
||||
void Init(IEnumerable<Type> serviceTypes, IEnumerable<IWebSocketListener> listener, IEnumerable<string> urlPrefixes);
|
||||
void Init(IEnumerable<IWebSocketListener> listener, IEnumerable<string> urlPrefixes);
|
||||
|
||||
/// <summary>
|
||||
/// If set, all requests will respond with this message.
|
||||
@ -43,8 +42,8 @@ namespace MediaBrowser.Controller.Net
|
||||
/// <summary>
|
||||
/// Get the default CORS headers.
|
||||
/// </summary>
|
||||
/// <param name="req"></param>
|
||||
/// <returns></returns>
|
||||
IDictionary<string, string> GetDefaultCorsHeaders(IRequest req);
|
||||
/// <param name="httpContext">The HTTP context of the current request.</param>
|
||||
/// <returns>The default CORS headers for the context.</returns>
|
||||
IDictionary<string, string> GetDefaultCorsHeaders(HttpContext httpContext);
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace MediaBrowser.Controller.Net
|
||||
{
|
||||
@ -12,8 +12,8 @@ namespace MediaBrowser.Controller.Net
|
||||
|
||||
User GetUser(object requestContext);
|
||||
|
||||
SessionInfo GetSession(IRequest requestContext);
|
||||
SessionInfo GetSession(HttpContext requestContext);
|
||||
|
||||
User GetUser(IRequest requestContext);
|
||||
User GetUser(HttpContext requestContext);
|
||||
}
|
||||
}
|
||||
|
@ -1,44 +0,0 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Net
|
||||
{
|
||||
public class StaticResultOptions
|
||||
{
|
||||
public string ContentType { get; set; }
|
||||
|
||||
public TimeSpan? CacheDuration { get; set; }
|
||||
|
||||
public DateTime? DateLastModified { get; set; }
|
||||
|
||||
public Func<Task<Stream>> ContentFactory { get; set; }
|
||||
|
||||
public bool IsHeadRequest { get; set; }
|
||||
|
||||
public IDictionary<string, string> ResponseHeaders { get; set; }
|
||||
|
||||
public Action OnComplete { get; set; }
|
||||
|
||||
public Action OnError { get; set; }
|
||||
|
||||
public string Path { get; set; }
|
||||
|
||||
public long? ContentLength { get; set; }
|
||||
|
||||
public FileShare FileShare { get; set; }
|
||||
|
||||
public StaticResultOptions()
|
||||
{
|
||||
ResponseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
FileShare = FileShare.Read;
|
||||
}
|
||||
}
|
||||
|
||||
public class StaticFileResultOptions : StaticResultOptions
|
||||
{
|
||||
}
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
#nullable disable
|
||||
using System;
|
||||
|
||||
namespace MediaBrowser.Model.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifies a single API endpoint.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
|
||||
public class ApiMemberAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets verb to which applies attribute. By default applies to all verbs.
|
||||
/// </summary>
|
||||
public string Verb { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets parameter type: It can be only one of the following: path, query, body, form, or header.
|
||||
/// </summary>
|
||||
public string ParameterType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets unique name for the parameter. Each name must be unique, even if they are associated with different paramType values.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Other notes on the name field:
|
||||
/// If paramType is body, the name is used only for UI and codegeneration.
|
||||
/// If paramType is path, the name field must correspond to the associated path segment from the path field in the api object.
|
||||
/// If paramType is query, the name field corresponds to the query param name.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the human-readable description for the parameter.
|
||||
/// </summary>
|
||||
public string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// For path, query, and header paramTypes, this field must be a primitive. For body, this can be a complex or container datatype.
|
||||
/// </summary>
|
||||
public string DataType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// For path, this is always true. Otherwise, this field tells the client whether or not the field must be supplied.
|
||||
/// </summary>
|
||||
public bool IsRequired { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// For query params, this specifies that a comma-separated list of values can be passed to the API. For path and body types, this field cannot be true.
|
||||
/// </summary>
|
||||
public bool AllowMultiple { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets route to which applies attribute, matches using StartsWith. By default applies to all routes.
|
||||
/// </summary>
|
||||
public string Route { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to exclude this property from being included in the ModelSchema.
|
||||
/// </summary>
|
||||
public bool ExcludeInSchema { get; set; }
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Model.Services
|
||||
{
|
||||
public interface IAsyncStreamWriter
|
||||
{
|
||||
Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Model.Services
|
||||
{
|
||||
public interface IHasHeaders
|
||||
{
|
||||
IDictionary<string, string> Headers { get; }
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace MediaBrowser.Model.Services
|
||||
{
|
||||
public interface IHasRequestFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the order in which Request Filters are executed.
|
||||
/// <0 Executed before global request filters.
|
||||
/// >0 Executed after global request filters.
|
||||
/// </summary>
|
||||
int Priority { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The request filter is executed before the service.
|
||||
/// </summary>
|
||||
/// <param name="req">The http request wrapper.</param>
|
||||
/// <param name="res">The http response wrapper.</param>
|
||||
/// <param name="requestDto">The request DTO.</param>
|
||||
void RequestFilter(IRequest req, HttpResponse res, object requestDto);
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
namespace MediaBrowser.Model.Services
|
||||
{
|
||||
public interface IHttpRequest : IRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the HTTP Verb.
|
||||
/// </summary>
|
||||
string HttpMethod { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value of the Accept HTTP Request Header.
|
||||
/// </summary>
|
||||
string Accept { get; }
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
#nullable disable
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Net;
|
||||
|
||||
namespace MediaBrowser.Model.Services
|
||||
{
|
||||
public interface IHttpResult : IHasHeaders
|
||||
{
|
||||
/// <summary>
|
||||
/// The HTTP Response Status.
|
||||
/// </summary>
|
||||
int Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The HTTP Response Status Code.
|
||||
/// </summary>
|
||||
HttpStatusCode StatusCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The HTTP Response ContentType.
|
||||
/// </summary>
|
||||
string ContentType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Response DTO.
|
||||
/// </summary>
|
||||
object Response { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Holds the request call context.
|
||||
/// </summary>
|
||||
IRequest RequestContext { get; set; }
|
||||
}
|
||||
}
|
@ -1,93 +0,0 @@
|
||||
#nullable disable
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace MediaBrowser.Model.Services
|
||||
{
|
||||
public interface IRequest
|
||||
{
|
||||
HttpResponse Response { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the service being called (e.g. Request DTO Name)
|
||||
/// </summary>
|
||||
string OperationName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Verb / HttpMethod or Action for this request
|
||||
/// </summary>
|
||||
string Verb { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The request ContentType.
|
||||
/// </summary>
|
||||
string ContentType { get; }
|
||||
|
||||
bool IsLocal { get; }
|
||||
|
||||
string UserAgent { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The expected Response ContentType for this request.
|
||||
/// </summary>
|
||||
string ResponseContentType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Attach any data to this request that all filters and services can access.
|
||||
/// </summary>
|
||||
Dictionary<string, object> Items { get; }
|
||||
|
||||
IHeaderDictionary Headers { get; }
|
||||
|
||||
IQueryCollection QueryString { get; }
|
||||
|
||||
string RawUrl { get; }
|
||||
|
||||
string AbsoluteUri { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The Remote Ip as reported by X-Forwarded-For, X-Real-IP or Request.UserHostAddress
|
||||
/// </summary>
|
||||
string RemoteIp { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The value of the Authorization Header used to send the Api Key, null if not available.
|
||||
/// </summary>
|
||||
string Authorization { get; }
|
||||
|
||||
string[] AcceptTypes { get; }
|
||||
|
||||
string PathInfo { get; }
|
||||
|
||||
Stream InputStream { get; }
|
||||
|
||||
long ContentLength { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The value of the Referrer, null if not available.
|
||||
/// </summary>
|
||||
Uri UrlReferrer { get; }
|
||||
}
|
||||
|
||||
public interface IHttpFile
|
||||
{
|
||||
string Name { get; }
|
||||
|
||||
string FileName { get; }
|
||||
|
||||
long ContentLength { get; }
|
||||
|
||||
string ContentType { get; }
|
||||
|
||||
Stream InputStream { get; }
|
||||
}
|
||||
|
||||
public interface IRequiresRequest
|
||||
{
|
||||
IRequest Request { get; set; }
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.IO;
|
||||
|
||||
namespace MediaBrowser.Model.Services
|
||||
{
|
||||
public interface IRequiresRequestStream
|
||||
{
|
||||
/// <summary>
|
||||
/// The raw Http Request Input Stream.
|
||||
/// </summary>
|
||||
Stream RequestStream { get; set; }
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
namespace MediaBrowser.Model.Services
|
||||
{
|
||||
// marker interface
|
||||
public interface IService
|
||||
{
|
||||
}
|
||||
|
||||
public interface IReturn { }
|
||||
|
||||
public interface IReturn<T> : IReturn { }
|
||||
|
||||
public interface IReturnVoid : IReturn { }
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.IO;
|
||||
|
||||
namespace MediaBrowser.Model.Services
|
||||
{
|
||||
public interface IStreamWriter
|
||||
{
|
||||
void WriteTo(Stream responseStream);
|
||||
}
|
||||
}
|
@ -1,147 +0,0 @@
|
||||
#nullable disable
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Model.Dto;
|
||||
|
||||
namespace MediaBrowser.Model.Services
|
||||
{
|
||||
// Remove this garbage class, it's just a bastard copy of NameValueCollection
|
||||
public class QueryParamCollection : List<NameValuePair>
|
||||
{
|
||||
public QueryParamCollection()
|
||||
{
|
||||
}
|
||||
|
||||
private static StringComparison GetStringComparison()
|
||||
{
|
||||
return StringComparison.OrdinalIgnoreCase;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new query parameter.
|
||||
/// </summary>
|
||||
public void Add(string key, string value)
|
||||
{
|
||||
Add(new NameValuePair(key, value));
|
||||
}
|
||||
|
||||
private void Set(string key, string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
var parameters = GetItems(key);
|
||||
|
||||
foreach (var p in parameters)
|
||||
{
|
||||
Remove(p);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var pair in this)
|
||||
{
|
||||
var stringComparison = GetStringComparison();
|
||||
|
||||
if (string.Equals(key, pair.Name, stringComparison))
|
||||
{
|
||||
pair.Value = value;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Add(key, value);
|
||||
}
|
||||
|
||||
private string Get(string name)
|
||||
{
|
||||
var stringComparison = GetStringComparison();
|
||||
|
||||
foreach (var pair in this)
|
||||
{
|
||||
if (string.Equals(pair.Name, name, stringComparison))
|
||||
{
|
||||
return pair.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<NameValuePair> GetItems(string name)
|
||||
{
|
||||
var stringComparison = GetStringComparison();
|
||||
|
||||
var list = new List<NameValuePair>();
|
||||
|
||||
foreach (var pair in this)
|
||||
{
|
||||
if (string.Equals(pair.Name, name, stringComparison))
|
||||
{
|
||||
list.Add(pair);
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public virtual List<string> GetValues(string name)
|
||||
{
|
||||
var stringComparison = GetStringComparison();
|
||||
|
||||
var list = new List<string>();
|
||||
|
||||
foreach (var pair in this)
|
||||
{
|
||||
if (string.Equals(pair.Name, name, stringComparison))
|
||||
{
|
||||
list.Add(pair.Value);
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public IEnumerable<string> Keys
|
||||
{
|
||||
get
|
||||
{
|
||||
var keys = new string[this.Count];
|
||||
|
||||
for (var i = 0; i < keys.Length; i++)
|
||||
{
|
||||
keys[i] = this[i].Name;
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a query parameter value by name. A query may contain multiple values of the same name
|
||||
/// (i.e. "x=1&x=2"), in which case the value is an array, which works for both getting and setting.
|
||||
/// </summary>
|
||||
/// <param name="name">The query parameter name.</param>
|
||||
/// <returns>The query parameter value or array of values.</returns>
|
||||
public string this[string name]
|
||||
{
|
||||
get => Get(name);
|
||||
set => Set(name, value);
|
||||
}
|
||||
|
||||
private string GetQueryStringValue(NameValuePair pair)
|
||||
{
|
||||
return pair.Name + "=" + pair.Value;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var vals = this.Select(GetQueryStringValue).ToArray();
|
||||
|
||||
return string.Join("&", vals);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,163 +0,0 @@
|
||||
#nullable disable
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
namespace MediaBrowser.Model.Services
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
|
||||
public class RouteAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// <para>Initializes an instance of the <see cref="RouteAttribute"/> class.</para>
|
||||
/// </summary>
|
||||
/// <param name="path">
|
||||
/// <para>The path template to map to the request. See
|
||||
/// <see cref="Path">RouteAttribute.Path</see>
|
||||
/// for details on the correct format.</para>
|
||||
/// </param>
|
||||
public RouteAttribute(string path)
|
||||
: this(path, null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>Initializes an instance of the <see cref="RouteAttribute"/> class.</para>
|
||||
/// </summary>
|
||||
/// <param name="path">
|
||||
/// <para>The path template to map to the request. See
|
||||
/// <see cref="Path">RouteAttribute.Path</see>
|
||||
/// for details on the correct format.</para>
|
||||
/// </param>
|
||||
/// <param name="verbs">A comma-delimited list of HTTP verbs supported by the
|
||||
/// service. If unspecified, all verbs are assumed to be supported.</param>
|
||||
public RouteAttribute(string path, string verbs)
|
||||
{
|
||||
Path = path;
|
||||
Verbs = verbs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the path template to be mapped to the request.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A <see cref="String"/> value providing the path mapped to
|
||||
/// the request. Never <see langword="null"/>.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// <para>Some examples of valid paths are:</para>
|
||||
///
|
||||
/// <list>
|
||||
/// <item>"/Inventory"</item>
|
||||
/// <item>"/Inventory/{Category}/{ItemId}"</item>
|
||||
/// <item>"/Inventory/{ItemPath*}"</item>
|
||||
/// </list>
|
||||
///
|
||||
/// <para>Variables are specified within "{}"
|
||||
/// brackets. Each variable in the path is mapped to the same-named property
|
||||
/// on the request DTO. At runtime, ServiceStack will parse the
|
||||
/// request URL, extract the variable values, instantiate the request DTO,
|
||||
/// and assign the variable values into the corresponding request properties,
|
||||
/// prior to passing the request DTO to the service object for processing.</para>
|
||||
///
|
||||
/// <para>It is not necessary to specify all request properties as
|
||||
/// variables in the path. For unspecified properties, callers may provide
|
||||
/// values in the query string. For example: the URL
|
||||
/// "http://services/Inventory?Category=Books&ItemId=12345" causes the same
|
||||
/// request DTO to be processed as "http://services/Inventory/Books/12345",
|
||||
/// provided that the paths "/Inventory" (which supports the first URL) and
|
||||
/// "/Inventory/{Category}/{ItemId}" (which supports the second URL)
|
||||
/// are both mapped to the request DTO.</para>
|
||||
///
|
||||
/// <para>Please note that while it is possible to specify property values
|
||||
/// in the query string, it is generally considered to be less RESTful and
|
||||
/// less desirable than to specify them as variables in the path. Using the
|
||||
/// query string to specify property values may also interfere with HTTP
|
||||
/// caching.</para>
|
||||
///
|
||||
/// <para>The final variable in the path may contain a "*" suffix
|
||||
/// to grab all remaining segments in the path portion of the request URL and assign
|
||||
/// them to a single property on the request DTO.
|
||||
/// For example, if the path "/Inventory/{ItemPath*}" is mapped to the request DTO,
|
||||
/// then the request URL "http://services/Inventory/Books/12345" will result
|
||||
/// in a request DTO whose ItemPath property contains "Books/12345".
|
||||
/// You may only specify one such variable in the path, and it must be positioned at
|
||||
/// the end of the path.</para>
|
||||
/// </remarks>
|
||||
public string Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets short summary of what the route does.
|
||||
/// </summary>
|
||||
public string Summary { get; set; }
|
||||
|
||||
public string Description { get; set; }
|
||||
|
||||
public bool IsHidden { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets longer text to explain the behaviour of the route.
|
||||
/// </summary>
|
||||
public string Notes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a comma-delimited list of HTTP verbs supported by the service, such as
|
||||
/// "GET,PUT,POST,DELETE".
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A <see cref="String"/> providing a comma-delimited list of HTTP verbs supported
|
||||
/// by the service, <see langword="null"/> or empty if all verbs are supported.
|
||||
/// </value>
|
||||
public string Verbs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Used to rank the precedences of route definitions in reverse routing.
|
||||
/// i.e. Priorities below 0 are auto-generated have less precedence.
|
||||
/// </summary>
|
||||
public int Priority { get; set; }
|
||||
|
||||
protected bool Equals(RouteAttribute other)
|
||||
{
|
||||
return base.Equals(other)
|
||||
&& string.Equals(Path, other.Path)
|
||||
&& string.Equals(Summary, other.Summary)
|
||||
&& string.Equals(Notes, other.Notes)
|
||||
&& string.Equals(Verbs, other.Verbs)
|
||||
&& Priority == other.Priority;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (ReferenceEquals(null, obj))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(this, obj))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (obj.GetType() != this.GetType())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Equals((RouteAttribute)obj);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
var hashCode = base.GetHashCode();
|
||||
hashCode = (hashCode * 397) ^ (Path != null ? Path.GetHashCode() : 0);
|
||||
hashCode = (hashCode * 397) ^ (Summary != null ? Summary.GetHashCode() : 0);
|
||||
hashCode = (hashCode * 397) ^ (Notes != null ? Notes.GetHashCode() : 0);
|
||||
hashCode = (hashCode * 397) ^ (Verbs != null ? Verbs.GetHashCode() : 0);
|
||||
hashCode = (hashCode * 397) ^ Priority;
|
||||
return hashCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -2,7 +2,6 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using MediaBrowser.Model.Services;
|
||||
|
||||
namespace MediaBrowser.Model.Session
|
||||
{
|
||||
|
@ -1,18 +0,0 @@
|
||||
using Emby.Server.Implementations.HttpServer;
|
||||
using Xunit;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.Tests.HttpServer
|
||||
{
|
||||
public class ResponseFilterTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(null, null)]
|
||||
[InlineData("", "")]
|
||||
[InlineData("This is a clean string.", "This is a clean string.")]
|
||||
[InlineData("This isn't \n\ra clean string.", "This isn't a clean string.")]
|
||||
public void RemoveControlCharacters_ValidArgs_Correct(string? input, string? result)
|
||||
{
|
||||
Assert.Equal(result, ResponseFilter.RemoveControlCharacters(input));
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user