mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-03-26 03:57:12 +00:00
Update to 3.5.2 and .net core 2.1
This commit is contained in:
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.Services;
|
||||
using System.Linq;
|
||||
|
||||
namespace Emby.Server.Implementations.HttpServer
|
||||
{
|
||||
@@ -147,6 +148,13 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
}
|
||||
}
|
||||
|
||||
private string[] SkipLogExtensions = new string[]
|
||||
{
|
||||
".js",
|
||||
".html",
|
||||
".css"
|
||||
};
|
||||
|
||||
public async Task WriteToAsync(IResponse response, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
@@ -157,17 +165,24 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
return;
|
||||
}
|
||||
|
||||
var path = Path;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(RangeHeader) || (RangeStart <= 0 && RangeEnd >= TotalContentLength - 1))
|
||||
{
|
||||
Logger.Info("Transmit file {0}", Path);
|
||||
var extension = System.IO.Path.GetExtension(path);
|
||||
|
||||
if (extension == null || !SkipLogExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
Logger.Debug("Transmit file {0}", path);
|
||||
}
|
||||
|
||||
//var count = FileShare == FileShareMode.ReadWrite ? TotalContentLength : 0;
|
||||
|
||||
await response.TransmitFile(Path, 0, 0, FileShare, cancellationToken).ConfigureAwait(false);
|
||||
await response.TransmitFile(path, 0, 0, FileShare, cancellationToken).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
await response.TransmitFile(Path, RangeStart, RangeLength, FileShare, cancellationToken).ConfigureAwait(false);
|
||||
await response.TransmitFile(path, RangeStart, RangeLength, FileShare, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -12,7 +12,6 @@ using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Server.Implementations.HttpServer.SocketSharp;
|
||||
using Emby.Server.Implementations.Services;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Common.Security;
|
||||
@@ -25,6 +24,10 @@ using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Model.Services;
|
||||
using MediaBrowser.Model.System;
|
||||
using MediaBrowser.Model.Text;
|
||||
using System.Net.Sockets;
|
||||
using Emby.Server.Implementations.Net;
|
||||
using MediaBrowser.Common.Events;
|
||||
using MediaBrowser.Model.Events;
|
||||
|
||||
namespace Emby.Server.Implementations.HttpServer
|
||||
{
|
||||
@@ -35,64 +38,47 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
private readonly ILogger _logger;
|
||||
public string[] UrlPrefixes { get; private set; }
|
||||
|
||||
private readonly List<IService> _restServices = new List<IService>();
|
||||
|
||||
private IHttpListener _listener;
|
||||
|
||||
public event EventHandler<WebSocketConnectEventArgs> WebSocketConnected;
|
||||
public event EventHandler<WebSocketConnectingEventArgs> WebSocketConnecting;
|
||||
public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;
|
||||
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly INetworkManager _networkManager;
|
||||
private readonly IMemoryStreamFactory _memoryStreamProvider;
|
||||
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
|
||||
private readonly ITextEncoding _textEncoding;
|
||||
private readonly ISocketFactory _socketFactory;
|
||||
private readonly ICryptoProvider _cryptoProvider;
|
||||
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IXmlSerializer _xmlSerializer;
|
||||
private readonly X509Certificate _certificate;
|
||||
private readonly IEnvironmentInfo _environment;
|
||||
private readonly Func<Type, Func<string, object>> _funcParseFn;
|
||||
private readonly bool _enableDualModeSockets;
|
||||
|
||||
public Action<IRequest, IResponse, object>[] RequestFilters { get; set; }
|
||||
public Action<IRequest, IResponse, object>[] ResponseFilters { get; set; }
|
||||
|
||||
private readonly Dictionary<Type, Type> ServiceOperationsMap = new Dictionary<Type, Type>();
|
||||
public static HttpListenerHost Instance { get; protected set; }
|
||||
|
||||
private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
|
||||
private readonly List<IWebSocketConnection> _webSocketConnections = new List<IWebSocketConnection>();
|
||||
|
||||
public HttpListenerHost(IServerApplicationHost applicationHost,
|
||||
ILogger logger,
|
||||
IServerConfigurationManager config,
|
||||
string serviceName,
|
||||
string defaultRedirectPath, INetworkManager networkManager, IMemoryStreamFactory memoryStreamProvider, ITextEncoding textEncoding, ISocketFactory socketFactory, ICryptoProvider cryptoProvider, IJsonSerializer jsonSerializer, IXmlSerializer xmlSerializer, IEnvironmentInfo environment, X509Certificate certificate, Func<Type, Func<string, object>> funcParseFn, bool enableDualModeSockets, IFileSystem fileSystem)
|
||||
string defaultRedirectPath, INetworkManager networkManager, ITextEncoding textEncoding, IJsonSerializer jsonSerializer, IXmlSerializer xmlSerializer, Func<Type, Func<string, object>> funcParseFn)
|
||||
{
|
||||
Instance = this;
|
||||
|
||||
_appHost = applicationHost;
|
||||
DefaultRedirectPath = defaultRedirectPath;
|
||||
_networkManager = networkManager;
|
||||
_memoryStreamProvider = memoryStreamProvider;
|
||||
_textEncoding = textEncoding;
|
||||
_socketFactory = socketFactory;
|
||||
_cryptoProvider = cryptoProvider;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_xmlSerializer = xmlSerializer;
|
||||
_environment = environment;
|
||||
_certificate = certificate;
|
||||
_funcParseFn = funcParseFn;
|
||||
_enableDualModeSockets = enableDualModeSockets;
|
||||
_fileSystem = fileSystem;
|
||||
_config = config;
|
||||
|
||||
_logger = logger;
|
||||
_funcParseFn = funcParseFn;
|
||||
|
||||
RequestFilters = new Action<IRequest, IResponse, object>[] { };
|
||||
ResponseFilters = new Action<IRequest, IResponse, object>[] { };
|
||||
}
|
||||
|
||||
@@ -140,12 +126,6 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
attribute.RequestFilter(req, res, requestDto);
|
||||
}
|
||||
|
||||
//Exec global filters
|
||||
foreach (var requestFilter in RequestFilters)
|
||||
{
|
||||
requestFilter(req, res, requestDto);
|
||||
}
|
||||
|
||||
//Exec remaining RequestFilter attributes with Priority >= 0
|
||||
for (; i < count && attributes[i].Priority >= 0; i++)
|
||||
{
|
||||
@@ -181,45 +161,38 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
return attributes;
|
||||
}
|
||||
|
||||
private IHttpListener GetListener()
|
||||
{
|
||||
//return new KestrelHost.KestrelListener(_logger, _environment, _fileSystem);
|
||||
|
||||
return new WebSocketSharpListener(_logger,
|
||||
_certificate,
|
||||
_memoryStreamProvider,
|
||||
_textEncoding,
|
||||
_networkManager,
|
||||
_socketFactory,
|
||||
_cryptoProvider,
|
||||
_enableDualModeSockets,
|
||||
_fileSystem,
|
||||
_environment);
|
||||
}
|
||||
|
||||
private void OnWebSocketConnecting(WebSocketConnectingEventArgs args)
|
||||
private void OnWebSocketConnected(WebSocketConnectEventArgs e)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (WebSocketConnecting != null)
|
||||
var connection = new WebSocketConnection(e.WebSocket, e.Endpoint, _jsonSerializer, _logger, _textEncoding)
|
||||
{
|
||||
WebSocketConnecting(this, args);
|
||||
}
|
||||
}
|
||||
OnReceive = ProcessWebSocketMessageReceived,
|
||||
Url = e.Url,
|
||||
QueryString = e.QueryString ?? new QueryParamCollection()
|
||||
};
|
||||
|
||||
private void OnWebSocketConnected(WebSocketConnectEventArgs args)
|
||||
{
|
||||
if (_disposed)
|
||||
connection.Closed += Connection_Closed;
|
||||
|
||||
lock (_webSocketConnections)
|
||||
{
|
||||
return;
|
||||
_webSocketConnections.Add(connection);
|
||||
}
|
||||
|
||||
if (WebSocketConnected != null)
|
||||
{
|
||||
WebSocketConnected(this, args);
|
||||
EventHelper.FireEventIfNotNull(WebSocketConnected, this, new GenericEventArgs<IWebSocketConnection>(connection), _logger);
|
||||
}
|
||||
}
|
||||
|
||||
private void Connection_Closed(object sender, EventArgs e)
|
||||
{
|
||||
lock (_webSocketConnections)
|
||||
{
|
||||
_webSocketConnections.Remove((IWebSocketConnection)sender);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,16 +244,20 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
return statusCode;
|
||||
}
|
||||
|
||||
private void ErrorHandler(Exception ex, IRequest httpReq, bool logException = true)
|
||||
private async Task ErrorHandler(Exception ex, IRequest httpReq, bool logExceptionStackTrace, bool logExceptionMessage)
|
||||
{
|
||||
try
|
||||
{
|
||||
ex = GetActualException(ex);
|
||||
|
||||
if (logException)
|
||||
if (logExceptionStackTrace)
|
||||
{
|
||||
_logger.ErrorException("Error processing request", ex);
|
||||
}
|
||||
else if (logExceptionMessage)
|
||||
{
|
||||
_logger.Error(ex.Message);
|
||||
}
|
||||
|
||||
var httpRes = httpReq.Response;
|
||||
|
||||
@@ -293,7 +270,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
httpRes.StatusCode = statusCode;
|
||||
|
||||
httpRes.ContentType = "text/html";
|
||||
Write(httpRes, ex.Message);
|
||||
await Write(httpRes, NormalizeExceptionMessage(ex.Message)).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -301,11 +278,46 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
}
|
||||
}
|
||||
|
||||
private string NormalizeExceptionMessage(string msg)
|
||||
{
|
||||
if (msg == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// Strip any information we don't want to reveal
|
||||
|
||||
msg = msg.Replace(_config.ApplicationPaths.ProgramSystemPath, string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
msg = msg.Replace(_config.ApplicationPaths.ProgramDataPath, string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shut down the Web Service
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
List<IWebSocketConnection> connections;
|
||||
|
||||
lock (_webSocketConnections)
|
||||
{
|
||||
connections = _webSocketConnections.ToList();
|
||||
_webSocketConnections.Clear();
|
||||
}
|
||||
|
||||
foreach (var connection in connections)
|
||||
{
|
||||
try
|
||||
{
|
||||
connection.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (_listener != null)
|
||||
{
|
||||
_logger.Info("Stopping HttpListener...");
|
||||
@@ -329,9 +341,9 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
{
|
||||
var extension = GetExtension(url);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(extension) || !_skipLogExtensions.ContainsKey(extension))
|
||||
if (string.IsNullOrEmpty(extension) || !_skipLogExtensions.ContainsKey(extension))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(localPath) || localPath.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) == -1)
|
||||
if (string.IsNullOrEmpty(localPath) || localPath.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) == -1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -422,12 +434,53 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool ValidateRequest(string remoteIp, bool isLocal)
|
||||
{
|
||||
if (isLocal)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_config.Configuration.EnableRemoteAccess)
|
||||
{
|
||||
var addressFilter = _config.Configuration.RemoteIPFilter.Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
|
||||
|
||||
if (addressFilter.Length > 0 && !_networkManager.IsInLocalNetwork(remoteIp))
|
||||
{
|
||||
if (_config.Configuration.IsRemoteIPFilterBlacklist)
|
||||
{
|
||||
return !_networkManager.IsAddressInSubnets(remoteIp, addressFilter);
|
||||
}
|
||||
else
|
||||
{
|
||||
return _networkManager.IsAddressInSubnets(remoteIp, addressFilter);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_networkManager.IsInLocalNetwork(remoteIp))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool ValidateSsl(string remoteIp, string urlString)
|
||||
{
|
||||
if (_config.Configuration.RequireHttps && _appHost.EnableHttps)
|
||||
if (_config.Configuration.RequireHttps && _appHost.EnableHttps && !_config.Configuration.IsBehindProxy)
|
||||
{
|
||||
if (urlString.IndexOf("https://", StringComparison.OrdinalIgnoreCase) == -1)
|
||||
{
|
||||
// These are hacks, but if these ever occur on ipv6 in the local network they could be incorrectly redirected
|
||||
if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1 ||
|
||||
urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!_networkManager.IsInLocalNetwork(remoteIp))
|
||||
{
|
||||
return false;
|
||||
@@ -448,7 +501,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
bool enableLog = false;
|
||||
bool logHeaders = false;
|
||||
string urlToLog = null;
|
||||
string remoteIp = null;
|
||||
string remoteIp = httpReq.RemoteIp;
|
||||
|
||||
try
|
||||
{
|
||||
@@ -456,7 +509,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
{
|
||||
httpRes.StatusCode = 503;
|
||||
httpRes.ContentType = "text/plain";
|
||||
Write(httpRes, "Server shutting down");
|
||||
await Write(httpRes, "Server shutting down").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -464,17 +517,21 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
{
|
||||
httpRes.StatusCode = 400;
|
||||
httpRes.ContentType = "text/plain";
|
||||
Write(httpRes, "Invalid host");
|
||||
await Write(httpRes, "Invalid host").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ValidateRequest(remoteIp, httpReq.IsLocal))
|
||||
{
|
||||
httpRes.StatusCode = 403;
|
||||
httpRes.ContentType = "text/plain";
|
||||
await Write(httpRes, "Forbidden").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ValidateSsl(httpReq.RemoteIp, urlString))
|
||||
{
|
||||
var httpsUrl = urlString
|
||||
.Replace("http://", "https://", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace(":" + _config.Configuration.PublicPort.ToString(CultureInfo.InvariantCulture), ":" + _config.Configuration.PublicHttpsPort.ToString(CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
RedirectToUrl(httpRes, httpsUrl);
|
||||
RedirectToSecureUrl(httpReq, httpRes, urlString);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -485,7 +542,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
httpRes.AddHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
|
||||
httpRes.AddHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization");
|
||||
httpRes.ContentType = "text/plain";
|
||||
Write(httpRes, string.Empty);
|
||||
await Write(httpRes, string.Empty).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -498,7 +555,6 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
if (enableLog)
|
||||
{
|
||||
urlToLog = GetUrlToLog(urlString);
|
||||
remoteIp = httpReq.RemoteIp;
|
||||
|
||||
LoggerUtils.LogRequest(_logger, urlToLog, httpReq.HttpMethod, httpReq.UserAgent, logHeaders ? httpReq.Headers : null);
|
||||
}
|
||||
@@ -527,9 +583,9 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
|
||||
if (!string.Equals(newUrl, urlString, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Write(httpRes,
|
||||
await Write(httpRes,
|
||||
"<!doctype html><html><head><title>Emby</title></head><body>Please update your Emby bookmark to <a href=\"" +
|
||||
newUrl + "\">" + newUrl + "</a></body></html>");
|
||||
newUrl + "\">" + newUrl + "</a></body></html>").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -544,9 +600,9 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
|
||||
if (!string.Equals(newUrl, urlString, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Write(httpRes,
|
||||
await Write(httpRes,
|
||||
"<!doctype html><html><head><title>Emby</title></head><body>Please update your Emby bookmark to <a href=\"" +
|
||||
newUrl + "\">" + newUrl + "</a></body></html>");
|
||||
newUrl + "\">" + newUrl + "</a></body></html>").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -572,18 +628,29 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.Equals(localPath, "/emby/pin", StringComparison.OrdinalIgnoreCase))
|
||||
if (!string.Equals(httpReq.QueryString["r"], "0", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
RedirectToUrl(httpRes, "web/pin.html");
|
||||
return;
|
||||
if (localPath.EndsWith("web/dashboard.html", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
RedirectToUrl(httpRes, "index.html#!/dashboard.html");
|
||||
}
|
||||
|
||||
if (localPath.EndsWith("web/home.html", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
RedirectToUrl(httpRes, "index.html");
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(GlobalResponse))
|
||||
if (!string.IsNullOrEmpty(GlobalResponse))
|
||||
{
|
||||
httpRes.StatusCode = 503;
|
||||
httpRes.ContentType = "text/html";
|
||||
Write(httpRes, GlobalResponse);
|
||||
return;
|
||||
// We don't want the address pings in ApplicationHost to fail
|
||||
if (localPath.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) == -1)
|
||||
{
|
||||
httpRes.StatusCode = 503;
|
||||
httpRes.ContentType = "text/html";
|
||||
await Write(httpRes, GlobalResponse).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var handler = GetServiceHandler(httpReq);
|
||||
@@ -594,23 +661,34 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
}
|
||||
else
|
||||
{
|
||||
ErrorHandler(new FileNotFoundException(), httpReq, false);
|
||||
await ErrorHandler(new FileNotFoundException(), httpReq, false, false).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
ErrorHandler(ex, httpReq, false);
|
||||
await ErrorHandler(ex, httpReq, false, false).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
catch (IOException ex)
|
||||
{
|
||||
await ErrorHandler(ex, httpReq, false, false).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
catch (SocketException ex)
|
||||
{
|
||||
await ErrorHandler(ex, httpReq, false, false).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
catch (SecurityException ex)
|
||||
{
|
||||
await ErrorHandler(ex, httpReq, false, true).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
catch (Exception ex)
|
||||
{
|
||||
var logException = !string.Equals(ex.GetType().Name, "SocketException", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
#if DEBUG
|
||||
logException = true;
|
||||
#endif
|
||||
|
||||
ErrorHandler(ex, httpReq, logException);
|
||||
await ErrorHandler(ex, httpReq, logException, false).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -655,13 +733,36 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
return null;
|
||||
}
|
||||
|
||||
private void Write(IResponse response, string text)
|
||||
private Task Write(IResponse response, string text)
|
||||
{
|
||||
var bOutput = Encoding.UTF8.GetBytes(text);
|
||||
response.SetContentLength(bOutput.Length);
|
||||
|
||||
var outputStream = response.OutputStream;
|
||||
outputStream.Write(bOutput, 0, bOutput.Length);
|
||||
return response.OutputStream.WriteAsync(bOutput, 0, bOutput.Length);
|
||||
}
|
||||
|
||||
private void RedirectToSecureUrl(IHttpRequest httpReq, IResponse httpRes, string url)
|
||||
{
|
||||
int currentPort;
|
||||
Uri uri;
|
||||
if (Uri.TryCreate(url, UriKind.Absolute, out uri))
|
||||
{
|
||||
currentPort = uri.Port;
|
||||
var builder = new UriBuilder(uri);
|
||||
builder.Port = _config.Configuration.PublicHttpsPort;
|
||||
builder.Scheme = "https";
|
||||
url = builder.Uri.ToString();
|
||||
|
||||
RedirectToUrl(httpRes, url);
|
||||
}
|
||||
else
|
||||
{
|
||||
var httpsUrl = url
|
||||
.Replace("http://", "https://", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace(":" + _config.Configuration.PublicPort.ToString(CultureInfo.InvariantCulture), ":" + _config.Configuration.PublicHttpsPort.ToString(CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
RedirectToUrl(httpRes, url);
|
||||
}
|
||||
}
|
||||
|
||||
public static void RedirectToUrl(IResponse httpRes, string url)
|
||||
@@ -676,26 +777,18 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// Adds the rest handlers.
|
||||
/// </summary>
|
||||
/// <param name="services">The services.</param>
|
||||
public void Init(IEnumerable<IService> services)
|
||||
public void Init(IEnumerable<IService> services, IEnumerable<IWebSocketListener> listeners)
|
||||
{
|
||||
_restServices.AddRange(services);
|
||||
_webSocketListeners = listeners.ToArray();
|
||||
|
||||
ServiceController = new ServiceController();
|
||||
|
||||
_logger.Info("Calling ServiceStack AppHost.Init");
|
||||
|
||||
var types = _restServices.Select(r => r.GetType()).ToArray();
|
||||
var types = services.Select(r => r.GetType()).ToArray();
|
||||
|
||||
ServiceController.Init(this, types);
|
||||
|
||||
var list = new List<Action<IRequest, IResponse, object>>();
|
||||
foreach (var filter in _appHost.GetExports<IRequestFilter>())
|
||||
{
|
||||
list.Add(filter.Filter);
|
||||
}
|
||||
|
||||
RequestFilters = list.ToArray();
|
||||
|
||||
ResponseFilters = new Action<IRequest, IResponse, object>[]
|
||||
{
|
||||
new ResponseFilter(_logger).FilterResponse
|
||||
@@ -750,12 +843,12 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
_xmlSerializer.SerializeToStream(o, stream);
|
||||
}
|
||||
|
||||
public object DeserializeXml(Type type, Stream stream)
|
||||
public Task<object> DeserializeXml(Type type, Stream stream)
|
||||
{
|
||||
return _xmlSerializer.DeserializeFromStream(type, stream);
|
||||
return Task.FromResult(_xmlSerializer.DeserializeFromStream(type, stream));
|
||||
}
|
||||
|
||||
public object DeserializeJson(Type type, Stream stream)
|
||||
public Task<object> DeserializeJson(Type type, Stream stream)
|
||||
{
|
||||
//using (var reader = new StreamReader(stream))
|
||||
//{
|
||||
@@ -763,7 +856,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
// Logger.Info(json);
|
||||
// return _jsonSerializer.DeserializeFromString(json, type);
|
||||
//}
|
||||
return _jsonSerializer.DeserializeFromStream(stream, type);
|
||||
return _jsonSerializer.DeserializeFromStreamAsync(stream, type);
|
||||
}
|
||||
|
||||
private string NormalizeEmbyRoutePath(string path)
|
||||
@@ -815,20 +908,46 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes the web socket message received.
|
||||
/// </summary>
|
||||
/// <param name="result">The result.</param>
|
||||
private Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
//_logger.Debug("Websocket message received: {0}", result.MessageType);
|
||||
|
||||
var tasks = _webSocketListeners.Select(i => Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await i.ProcessMessage(result).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("{0} failed processing WebSocket message {1}", ex, i.GetType().Name, result.MessageType ?? string.Empty);
|
||||
}
|
||||
}));
|
||||
|
||||
return Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public void StartServer(string[] urlPrefixes)
|
||||
public void StartServer(string[] urlPrefixes, IHttpListener httpListener)
|
||||
{
|
||||
UrlPrefixes = urlPrefixes;
|
||||
|
||||
_listener = GetListener();
|
||||
_listener = httpListener;
|
||||
|
||||
_listener.WebSocketConnected = OnWebSocketConnected;
|
||||
_listener.WebSocketConnecting = OnWebSocketConnecting;
|
||||
_listener.ErrorHandler = ErrorHandler;
|
||||
_listener.RequestHandler = RequestHandler;
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ 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;
|
||||
@@ -30,16 +31,17 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
private readonly ILogger _logger;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IMemoryStreamFactory _memoryStreamFactory;
|
||||
|
||||
private IBrotliCompressor _brotliCompressor;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HttpResultFactory" /> class.
|
||||
/// </summary>
|
||||
public HttpResultFactory(ILogManager logManager, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IMemoryStreamFactory memoryStreamFactory)
|
||||
public HttpResultFactory(ILogManager logManager, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IBrotliCompressor brotliCompressor)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_memoryStreamFactory = memoryStreamFactory;
|
||||
_brotliCompressor = brotliCompressor;
|
||||
_logger = logManager.GetLogger("HttpResultFactory");
|
||||
}
|
||||
|
||||
@@ -50,9 +52,24 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// <param name="contentType">Type of the content.</param>
|
||||
/// <param name="responseHeaders">The response headers.</param>
|
||||
/// <returns>System.Object.</returns>
|
||||
public object GetResult(object content, string contentType, IDictionary<string, string> responseHeaders = null)
|
||||
public object GetResult(IRequest requestContext, byte[] content, string contentType, IDictionary<string, string> responseHeaders = null)
|
||||
{
|
||||
return GetHttpResult(content, contentType, true, responseHeaders);
|
||||
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)
|
||||
@@ -60,7 +77,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
var responseHeaders = new Dictionary<string, string>();
|
||||
responseHeaders["Location"] = url;
|
||||
|
||||
var result = new HttpResult(new byte[] { }, "text/plain", HttpStatusCode.Redirect);
|
||||
var result = new HttpResult(Array.Empty<byte>(), "text/plain", HttpStatusCode.Redirect);
|
||||
|
||||
AddResponseHeaders(result, responseHeaders);
|
||||
|
||||
@@ -70,39 +87,98 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// <summary>
|
||||
/// Gets the HTTP result.
|
||||
/// </summary>
|
||||
private IHasHeaders GetHttpResult(object content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null)
|
||||
private IHasHeaders GetHttpResult(IRequest requestContext, Stream content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null)
|
||||
{
|
||||
var result = new StreamWriter(content, contentType, _logger);
|
||||
|
||||
if (responseHeaders == null)
|
||||
{
|
||||
responseHeaders = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
string expires;
|
||||
if (addCachePrevention && !responseHeaders.TryGetValue("Expires", out expires))
|
||||
{
|
||||
responseHeaders["Expires"] = "-1";
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
IHasHeaders result;
|
||||
|
||||
var stream = content as Stream;
|
||||
var compressionType = requestContext == null ? null : GetCompressionType(requestContext, content, contentType);
|
||||
|
||||
if (stream != null)
|
||||
var isHeadRequest = string.Equals(requestContext.Verb, "head", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (string.IsNullOrEmpty(compressionType))
|
||||
{
|
||||
result = new StreamWriter(stream, contentType, _logger);
|
||||
}
|
||||
var contentLength = content.Length;
|
||||
|
||||
if (isHeadRequest)
|
||||
{
|
||||
content = Array.Empty<byte>();
|
||||
}
|
||||
|
||||
result = new StreamWriter(content, contentType, contentLength, _logger);
|
||||
}
|
||||
else
|
||||
{
|
||||
var bytes = content as byte[];
|
||||
|
||||
if (bytes != null)
|
||||
{
|
||||
result = new StreamWriter(bytes, contentType, _logger);
|
||||
}
|
||||
else
|
||||
{
|
||||
var text = content as string;
|
||||
|
||||
if (text != null)
|
||||
{
|
||||
result = new StreamWriter(Encoding.UTF8.GetBytes(text), contentType, _logger);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = new HttpResult(content, contentType, HttpStatusCode.OK);
|
||||
}
|
||||
}
|
||||
result = GetCompressedResult(content, compressionType, responseHeaders, isHeadRequest, contentType);
|
||||
}
|
||||
|
||||
if (responseHeaders == null)
|
||||
{
|
||||
responseHeaders = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
string expires;
|
||||
if (addCachePrevention && !responseHeaders.TryGetValue("Expires", out expires))
|
||||
{
|
||||
responseHeaders["Expires"] = "-1";
|
||||
}
|
||||
|
||||
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, _logger);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = GetCompressedResult(bytes, compressionType, responseHeaders, isHeadRequest, contentType);
|
||||
}
|
||||
|
||||
if (responseHeaders == null)
|
||||
{
|
||||
responseHeaders = new Dictionary<string, string>();
|
||||
@@ -123,19 +199,8 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// Gets the optimized result.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="requestContext">The request context.</param>
|
||||
/// <param name="result">The result.</param>
|
||||
/// <param name="responseHeaders">The response headers.</param>
|
||||
/// <returns>System.Object.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">result</exception>
|
||||
public object GetOptimizedResult<T>(IRequest requestContext, T result, IDictionary<string, string> responseHeaders = null)
|
||||
public object GetResult<T>(IRequest requestContext, T result, IDictionary<string, string> responseHeaders = null)
|
||||
where T : class
|
||||
{
|
||||
return GetOptimizedResultInternal<T>(requestContext, result, true, responseHeaders);
|
||||
}
|
||||
|
||||
private object GetOptimizedResultInternal<T>(IRequest requestContext, T result, bool addCachePrevention, IDictionary<string, string> responseHeaders = null)
|
||||
where T : class
|
||||
{
|
||||
if (result == null)
|
||||
{
|
||||
@@ -147,24 +212,49 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
if (addCachePrevention)
|
||||
{
|
||||
responseHeaders["Expires"] = "-1";
|
||||
}
|
||||
responseHeaders["Expires"] = "-1";
|
||||
|
||||
return ToOptimizedResultInternal(requestContext, result, responseHeaders);
|
||||
}
|
||||
|
||||
public static string GetCompressionType(IRequest request)
|
||||
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 string GetCompressionType(IRequest request)
|
||||
{
|
||||
var acceptEncoding = request.Headers["Accept-Encoding"];
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(acceptEncoding))
|
||||
if (acceptEncoding != null)
|
||||
{
|
||||
if (acceptEncoding.Contains("deflate"))
|
||||
//if (_brotliCompressor != null && acceptEncoding.IndexOf("br", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
// return "br";
|
||||
|
||||
if (acceptEncoding.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
return "deflate";
|
||||
|
||||
if (acceptEncoding.Contains("gzip"))
|
||||
if (acceptEncoding.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
return "gzip";
|
||||
}
|
||||
|
||||
@@ -180,7 +270,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// <returns></returns>
|
||||
public object ToOptimizedResult<T>(IRequest request, T dto)
|
||||
{
|
||||
return ToOptimizedResultInternal(request, dto, null);
|
||||
return ToOptimizedResultInternal(request, dto);
|
||||
}
|
||||
|
||||
private object ToOptimizedResultInternal<T>(IRequest request, T dto, IDictionary<string, string> responseHeaders = null)
|
||||
@@ -192,28 +282,110 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
case "application/xml":
|
||||
case "text/xml":
|
||||
case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml
|
||||
return GetHttpResult(SerializeToXmlString(dto), contentType, false, responseHeaders);
|
||||
return GetHttpResult(request, SerializeToXmlString(dto), contentType, false, responseHeaders);
|
||||
|
||||
case "application/json":
|
||||
case "text/json":
|
||||
return GetHttpResult(_jsonSerializer.SerializeToString(dto), contentType, false, responseHeaders);
|
||||
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)
|
||||
{
|
||||
var ms = new MemoryStream();
|
||||
var writerFn = RequestHelper.GetResponseWriter(HttpListenerHost.Instance, contentType);
|
||||
|
||||
writerFn(dto, ms);
|
||||
|
||||
ms.Position = 0;
|
||||
|
||||
if (string.Equals(request.Verb, "head", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return GetHttpResult(new byte[] { }, contentType, true, responseHeaders);
|
||||
}
|
||||
|
||||
return GetHttpResult(ms, contentType, true, responseHeaders);
|
||||
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["Content-Encoding"] = requestedCompressionType;
|
||||
|
||||
responseHeaders["Vary"] = "Accept-Encoding";
|
||||
|
||||
var contentLength = content.Length;
|
||||
|
||||
if (isHeadRequest)
|
||||
{
|
||||
var result = new StreamWriter(Array.Empty<byte>(), contentType, contentLength, _logger);
|
||||
AddResponseHeaders(result, responseHeaders);
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = new StreamWriter(content, contentType, contentLength, _logger);
|
||||
AddResponseHeaders(result, responseHeaders);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] Compress(byte[] bytes, string compressionType)
|
||||
{
|
||||
if (string.Equals(compressionType, "br", StringComparison.OrdinalIgnoreCase))
|
||||
return CompressBrotli(bytes);
|
||||
|
||||
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 byte[] CompressBrotli(byte[] bytes)
|
||||
{
|
||||
return _brotliCompressor.Compress(bytes);
|
||||
}
|
||||
|
||||
private 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 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();
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetRealContentType(string contentType)
|
||||
@@ -243,104 +415,6 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the optimized result using cache.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <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="factoryFn">The factory fn.</param>
|
||||
/// <param name="responseHeaders">The response headers.</param>
|
||||
/// <returns>System.Object.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">cacheKey
|
||||
/// or
|
||||
/// factoryFn</exception>
|
||||
public object GetOptimizedResultUsingCache<T>(IRequest requestContext, Guid cacheKey, DateTime? lastDateModified, TimeSpan? cacheDuration, Func<T> factoryFn, IDictionary<string, string> responseHeaders = null)
|
||||
where T : class
|
||||
{
|
||||
if (cacheKey == Guid.Empty)
|
||||
{
|
||||
throw new ArgumentNullException("cacheKey");
|
||||
}
|
||||
if (factoryFn == null)
|
||||
{
|
||||
throw new ArgumentNullException("factoryFn");
|
||||
}
|
||||
|
||||
var key = cacheKey.ToString("N");
|
||||
|
||||
if (responseHeaders == null)
|
||||
{
|
||||
responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
// See if the result is already cached in the browser
|
||||
var result = GetCachedResult(requestContext, responseHeaders, cacheKey, key, lastDateModified, cacheDuration, null);
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return GetOptimizedResultInternal(requestContext, factoryFn(), false, responseHeaders);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// To the cached result.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <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="factoryFn">The factory fn.</param>
|
||||
/// <param name="contentType">Type of the content.</param>
|
||||
/// <param name="responseHeaders">The response headers.</param>
|
||||
/// <returns>System.Object.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">cacheKey</exception>
|
||||
public object GetCachedResult<T>(IRequest requestContext, Guid cacheKey, DateTime? lastDateModified, TimeSpan? cacheDuration, Func<T> factoryFn, string contentType, IDictionary<string, string> responseHeaders = null)
|
||||
where T : class
|
||||
{
|
||||
if (cacheKey == Guid.Empty)
|
||||
{
|
||||
throw new ArgumentNullException("cacheKey");
|
||||
}
|
||||
if (factoryFn == null)
|
||||
{
|
||||
throw new ArgumentNullException("factoryFn");
|
||||
}
|
||||
|
||||
var key = cacheKey.ToString("N");
|
||||
|
||||
if (responseHeaders == null)
|
||||
{
|
||||
responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
// See if the result is already cached in the browser
|
||||
var result = GetCachedResult(requestContext, responseHeaders, cacheKey, key, lastDateModified, cacheDuration, contentType);
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
result = factoryFn();
|
||||
|
||||
// Apply caching headers
|
||||
var hasHeaders = result as IHasHeaders;
|
||||
|
||||
if (hasHeaders != null)
|
||||
{
|
||||
AddResponseHeaders(hasHeaders, responseHeaders);
|
||||
return hasHeaders;
|
||||
}
|
||||
|
||||
return GetHttpResult(result, contentType, false, responseHeaders);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pres the process optimized result.
|
||||
/// </summary>
|
||||
@@ -357,7 +431,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
AddAgeHeader(responseHeaders, lastDateModified);
|
||||
AddExpiresHeader(responseHeaders, cacheKeyString, cacheDuration);
|
||||
|
||||
var result = new HttpResult(new byte[] { }, contentType ?? "text/html", HttpStatusCode.NotModified);
|
||||
var result = new HttpResult(Array.Empty<byte>(), contentType ?? "text/html", HttpStatusCode.NotModified);
|
||||
|
||||
AddResponseHeaders(result, responseHeaders);
|
||||
|
||||
@@ -402,7 +476,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
throw new ArgumentException("FileShare must be either Read or ReadWrite");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(options.ContentType))
|
||||
if (string.IsNullOrEmpty(options.ContentType))
|
||||
{
|
||||
options.ContentType = MimeTypes.GetMimeType(path);
|
||||
}
|
||||
@@ -460,19 +534,17 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var contentType = options.ContentType;
|
||||
|
||||
if (cacheKey == Guid.Empty)
|
||||
if (!cacheKey.Equals(Guid.Empty))
|
||||
{
|
||||
throw new ArgumentNullException("cacheKey");
|
||||
}
|
||||
var key = cacheKey.ToString("N");
|
||||
|
||||
var key = cacheKey.ToString("N");
|
||||
// See if the result is already cached in the browser
|
||||
var result = GetCachedResult(requestContext, options.ResponseHeaders, cacheKey, key, options.DateLastModified, options.CacheDuration, contentType);
|
||||
|
||||
// See if the result is already cached in the browser
|
||||
var result = GetCachedResult(requestContext, options.ResponseHeaders, cacheKey, key, options.DateLastModified, options.CacheDuration, contentType);
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
return result;
|
||||
if (result != null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: We don't really need the option value
|
||||
@@ -484,7 +556,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
|
||||
var rangeHeader = requestContext.Headers.Get("Range");
|
||||
|
||||
if (!isHeadRequest && !string.IsNullOrWhiteSpace(options.Path))
|
||||
if (!isHeadRequest && !string.IsNullOrEmpty(options.Path))
|
||||
{
|
||||
var hasHeaders = new FileWriter(options.Path, contentType, rangeHeader, _logger, _fileSystem)
|
||||
{
|
||||
@@ -497,11 +569,24 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
return hasHeaders;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(rangeHeader))
|
||||
{
|
||||
var stream = await factoryFn().ConfigureAwait(false);
|
||||
var stream = await factoryFn().ConfigureAwait(false);
|
||||
|
||||
var hasHeaders = new RangeRequestWriter(rangeHeader, stream, contentType, isHeadRequest, _logger)
|
||||
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, _logger)
|
||||
{
|
||||
OnComplete = options.OnComplete
|
||||
};
|
||||
@@ -511,15 +596,17 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
}
|
||||
else
|
||||
{
|
||||
var stream = await factoryFn().ConfigureAwait(false);
|
||||
|
||||
responseHeaders["Content-Length"] = stream.Length.ToString(UsCulture);
|
||||
if (totalContentLength.HasValue)
|
||||
{
|
||||
responseHeaders["Content-Length"] = totalContentLength.Value.ToString(UsCulture);
|
||||
}
|
||||
|
||||
if (isHeadRequest)
|
||||
{
|
||||
stream.Dispose();
|
||||
|
||||
return GetHttpResult(new byte[] { }, contentType, true, responseHeaders);
|
||||
using (stream)
|
||||
{
|
||||
return GetHttpResult(requestContext, Array.Empty<byte>(), contentType, true, responseHeaders);
|
||||
}
|
||||
}
|
||||
|
||||
var hasHeaders = new StreamWriter(stream, contentType, _logger)
|
||||
@@ -603,7 +690,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// <param name="lastDateModified">The last date modified.</param>
|
||||
/// <param name="cacheDuration">Duration of the cache.</param>
|
||||
/// <returns><c>true</c> if [is not modified] [the specified cache key]; otherwise, <c>false</c>.</returns>
|
||||
private bool IsNotModified(IRequest requestContext, Guid? cacheKey, DateTime? lastDateModified, TimeSpan? cacheDuration)
|
||||
private bool IsNotModified(IRequest requestContext, Guid cacheKey, DateTime? lastDateModified, TimeSpan? cacheDuration)
|
||||
{
|
||||
//var isNotModified = true;
|
||||
|
||||
@@ -624,8 +711,10 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
|
||||
var ifNoneMatchHeader = requestContext.Headers.Get("If-None-Match");
|
||||
|
||||
var hasCacheKey = !cacheKey.Equals(Guid.Empty);
|
||||
|
||||
// Validate If-None-Match
|
||||
if ((cacheKey.HasValue || !string.IsNullOrEmpty(ifNoneMatchHeader)))
|
||||
if ((hasCacheKey || !string.IsNullOrEmpty(ifNoneMatchHeader)))
|
||||
{
|
||||
Guid ifNoneMatch;
|
||||
|
||||
@@ -633,7 +722,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
|
||||
if (Guid.TryParse(ifNoneMatchHeader, out ifNoneMatch))
|
||||
{
|
||||
if (cacheKey.HasValue && cacheKey.Value == ifNoneMatch)
|
||||
if (hasCacheKey && cacheKey.Equals(ifNoneMatch))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -697,4 +786,9 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface IBrotliCompressor
|
||||
{
|
||||
byte[] Compress(byte[] content);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.Services;
|
||||
using Emby.Server.Implementations.Net;
|
||||
|
||||
namespace Emby.Server.Implementations.HttpServer
|
||||
{
|
||||
@@ -13,7 +14,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// Gets or sets the error handler.
|
||||
/// </summary>
|
||||
/// <value>The error handler.</value>
|
||||
Action<Exception, IRequest, bool> ErrorHandler { get; set; }
|
||||
Func<Exception, IRequest, bool, bool, Task> ErrorHandler { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the request handler.
|
||||
|
||||
@@ -2,24 +2,11 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using MediaBrowser.Model.Services;
|
||||
using SocketHttpListener.Net;
|
||||
|
||||
namespace Emby.Server.Implementations.HttpServer
|
||||
{
|
||||
public static class LoggerUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// Logs the request.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="request">The request.</param>
|
||||
public static void LogRequest(ILogger logger, HttpListenerRequest request)
|
||||
{
|
||||
var url = request.Url.ToString();
|
||||
|
||||
logger.Info("{0} {1}. UserAgent: {2}", request.IsWebSocketRequest ? "WS" : "HTTP " + request.HttpMethod, url, request.UserAgent ?? string.Empty);
|
||||
}
|
||||
|
||||
public static void LogRequest(ILogger logger, string url, string method, string userAgent, QueryParamCollection headers)
|
||||
{
|
||||
if (headers == null)
|
||||
|
||||
@@ -58,7 +58,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// <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, Stream source, string contentType, bool isHeadRequest, ILogger logger)
|
||||
public RangeRequestWriter(string rangeHeader, long contentLength, Stream source, string contentType, bool isHeadRequest, ILogger logger)
|
||||
{
|
||||
if (string.IsNullOrEmpty(contentType))
|
||||
{
|
||||
@@ -76,17 +76,17 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
StatusCode = HttpStatusCode.PartialContent;
|
||||
|
||||
Cookies = new List<Cookie>();
|
||||
SetRangeValues();
|
||||
SetRangeValues(contentLength);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the range values.
|
||||
/// </summary>
|
||||
private void SetRangeValues()
|
||||
private void SetRangeValues(long contentLength)
|
||||
{
|
||||
var requestedRange = RequestedRanges[0];
|
||||
|
||||
TotalContentLength = SourceStream.Length;
|
||||
TotalContentLength = contentLength;
|
||||
|
||||
// If the requested range is "0-", we can optimize by just doing a stream copy
|
||||
if (!requestedRange.Value.HasValue)
|
||||
@@ -105,7 +105,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
Headers["Content-Length"] = RangeLength.ToString(UsCulture);
|
||||
Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", RangeStart, RangeEnd, TotalContentLength);
|
||||
|
||||
if (RangeStart > 0)
|
||||
if (RangeStart > 0 && SourceStream.CanSeek)
|
||||
{
|
||||
SourceStream.Position = RangeStart;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using Emby.Server.Implementations.HttpServer.SocketSharp;
|
||||
using MediaBrowser.Model.Services;
|
||||
|
||||
namespace Emby.Server.Implementations.HttpServer
|
||||
@@ -47,7 +46,6 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
}
|
||||
|
||||
var hasHeaders = dto as IHasHeaders;
|
||||
var sharpResponse = res as WebSocketSharpResponse;
|
||||
|
||||
if (hasHeaders != null)
|
||||
{
|
||||
@@ -67,7 +65,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
if (length > 0)
|
||||
{
|
||||
res.SetContentLength(length);
|
||||
|
||||
|
||||
//var listenerResponse = res.OriginalResponse as HttpListenerResponse;
|
||||
|
||||
//if (listenerResponse != null)
|
||||
@@ -78,10 +76,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
// return;
|
||||
//}
|
||||
|
||||
if (sharpResponse != null)
|
||||
{
|
||||
sharpResponse.SendChunked = false;
|
||||
}
|
||||
res.SendChunked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ using MediaBrowser.Controller.Session;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Model.Services;
|
||||
using MediaBrowser.Common.Net;
|
||||
|
||||
namespace Emby.Server.Implementations.HttpServer.Security
|
||||
{
|
||||
@@ -16,21 +17,21 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
{
|
||||
private readonly IServerConfigurationManager _config;
|
||||
|
||||
public AuthService(IUserManager userManager, IAuthorizationContext authorizationContext, IServerConfigurationManager config, IConnectManager connectManager, ISessionManager sessionManager, IDeviceManager deviceManager)
|
||||
public AuthService(IUserManager userManager, IAuthorizationContext authorizationContext, IServerConfigurationManager config, IConnectManager connectManager, ISessionManager sessionManager, INetworkManager networkManager)
|
||||
{
|
||||
AuthorizationContext = authorizationContext;
|
||||
_config = config;
|
||||
DeviceManager = deviceManager;
|
||||
SessionManager = sessionManager;
|
||||
ConnectManager = connectManager;
|
||||
UserManager = userManager;
|
||||
NetworkManager = networkManager;
|
||||
}
|
||||
|
||||
public IUserManager UserManager { get; private set; }
|
||||
public IAuthorizationContext AuthorizationContext { get; private set; }
|
||||
public IConnectManager ConnectManager { get; private set; }
|
||||
public ISessionManager SessionManager { get; private set; }
|
||||
public IDeviceManager DeviceManager { get; private set; }
|
||||
public INetworkManager NetworkManager { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Redirect the client to a specific URL if authentication failed.
|
||||
@@ -38,14 +39,12 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
/// </summary>
|
||||
public string HtmlRedirect { get; set; }
|
||||
|
||||
public void Authenticate(IRequest request,
|
||||
IAuthenticationAttributes authAttribtues)
|
||||
public void Authenticate(IRequest request, IAuthenticationAttributes authAttribtues)
|
||||
{
|
||||
ValidateUser(request, authAttribtues);
|
||||
}
|
||||
|
||||
private void ValidateUser(IRequest request,
|
||||
IAuthenticationAttributes authAttribtues)
|
||||
private void ValidateUser(IRequest request, IAuthenticationAttributes authAttribtues)
|
||||
{
|
||||
// This code is executed before the service
|
||||
var auth = AuthorizationContext.GetAuthorizationInfo(request);
|
||||
@@ -60,11 +59,14 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
}
|
||||
}
|
||||
|
||||
var user = string.IsNullOrWhiteSpace(auth.UserId)
|
||||
? null
|
||||
: UserManager.GetUserById(auth.UserId);
|
||||
if (authAttribtues.AllowLocalOnly && !request.IsLocal)
|
||||
{
|
||||
throw new SecurityException("Operation not found.");
|
||||
}
|
||||
|
||||
if (user == null & !string.IsNullOrWhiteSpace(auth.UserId))
|
||||
var user = auth.User;
|
||||
|
||||
if (user == null & !auth.UserId.Equals(Guid.Empty))
|
||||
{
|
||||
throw new SecurityException("User with Id " + auth.UserId + " not found");
|
||||
}
|
||||
@@ -83,9 +85,9 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
ValidateRoles(roles, user);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(auth.DeviceId) &&
|
||||
!string.IsNullOrWhiteSpace(auth.Client) &&
|
||||
!string.IsNullOrWhiteSpace(auth.Device))
|
||||
if (!string.IsNullOrEmpty(auth.DeviceId) &&
|
||||
!string.IsNullOrEmpty(auth.Client) &&
|
||||
!string.IsNullOrEmpty(auth.Device))
|
||||
{
|
||||
SessionManager.LogSessionActivity(auth.Client,
|
||||
auth.Version,
|
||||
@@ -108,6 +110,14 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
};
|
||||
}
|
||||
|
||||
if (!user.Policy.EnableRemoteAccess && !NetworkManager.IsInLocalNetwork(request.RemoteIp))
|
||||
{
|
||||
throw new SecurityException("User account has been disabled.")
|
||||
{
|
||||
SecurityExceptionType = SecurityExceptionType.Unauthenticated
|
||||
};
|
||||
}
|
||||
|
||||
if (!user.Policy.IsAdministrator &&
|
||||
!authAttribtues.EscapeParentalControl &&
|
||||
!user.IsParentalScheduleAllowed())
|
||||
@@ -119,17 +129,6 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
SecurityExceptionType = SecurityExceptionType.ParentalControl
|
||||
};
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(auth.DeviceId))
|
||||
{
|
||||
if (!DeviceManager.CanAccessDevice(user.Id.ToString("N"), auth.DeviceId))
|
||||
{
|
||||
throw new SecurityException("User is not allowed access from this device.")
|
||||
{
|
||||
SecurityExceptionType = SecurityExceptionType.ParentalControl
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsExemptFromAuthenticationToken(AuthorizationInfo auth, IAuthenticationAttributes authAttribtues, IRequest request)
|
||||
@@ -143,6 +142,10 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (authAttribtues.AllowLocalOnly && request.IsLocal)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -159,12 +162,17 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
return true;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(auth.Token))
|
||||
if (authAttribtues.AllowLocalOnly && request.IsLocal)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (tokenInfo != null && string.IsNullOrWhiteSpace(tokenInfo.UserId))
|
||||
if (string.IsNullOrEmpty(auth.Token))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (tokenInfo != null && tokenInfo.UserId.Equals(Guid.Empty))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -225,7 +233,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
|
||||
private void ValidateSecurityToken(IRequest request, string token)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
throw new SecurityException("Access token is required.");
|
||||
}
|
||||
@@ -237,12 +245,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
throw new SecurityException("Access token is invalid or expired.");
|
||||
}
|
||||
|
||||
if (!info.IsActive)
|
||||
{
|
||||
throw new SecurityException("Access token has expired.");
|
||||
}
|
||||
|
||||
//if (!string.IsNullOrWhiteSpace(info.UserId))
|
||||
//if (!string.IsNullOrEmpty(info.UserId))
|
||||
//{
|
||||
// var user = _userManager.GetUserById(info.UserId);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||
using MediaBrowser.Model.Services;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using MediaBrowser.Controller.Library;
|
||||
|
||||
namespace Emby.Server.Implementations.HttpServer.Security
|
||||
{
|
||||
@@ -13,11 +14,13 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
{
|
||||
private readonly IAuthenticationRepository _authRepo;
|
||||
private readonly IConnectManager _connectManager;
|
||||
private readonly IUserManager _userManager;
|
||||
|
||||
public AuthorizationContext(IAuthenticationRepository authRepo, IConnectManager connectManager)
|
||||
public AuthorizationContext(IAuthenticationRepository authRepo, IConnectManager connectManager, IUserManager userManager)
|
||||
{
|
||||
_authRepo = authRepo;
|
||||
_connectManager = connectManager;
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
public AuthorizationInfo GetAuthorizationInfo(object requestContext)
|
||||
@@ -60,16 +63,16 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
auth.TryGetValue("Token", out token);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
token = httpReq.Headers["X-Emby-Token"];
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
token = httpReq.Headers["X-MediaBrowser-Token"];
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
token = httpReq.QueryString["api_key"];
|
||||
}
|
||||
@@ -94,8 +97,6 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
|
||||
if (tokenInfo != null)
|
||||
{
|
||||
info.UserId = tokenInfo.UserId;
|
||||
|
||||
var updateToken = false;
|
||||
|
||||
// TODO: Remove these checks for IsNullOrWhiteSpace
|
||||
@@ -109,15 +110,21 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
info.DeviceId = tokenInfo.DeviceId;
|
||||
}
|
||||
|
||||
// Temporary. TODO - allow clients to specify that the token has been shared with a casting device
|
||||
var allowTokenInfoUpdate = info.Client == null || info.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(info.Device))
|
||||
{
|
||||
info.Device = tokenInfo.DeviceName;
|
||||
}
|
||||
|
||||
else if (!string.Equals(info.Device, tokenInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
updateToken = true;
|
||||
tokenInfo.DeviceName = info.Device;
|
||||
if (allowTokenInfoUpdate)
|
||||
{
|
||||
updateToken = true;
|
||||
tokenInfo.DeviceName = info.Device;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(info.Version))
|
||||
@@ -126,22 +133,38 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
}
|
||||
else if (!string.Equals(info.Version, tokenInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (allowTokenInfoUpdate)
|
||||
{
|
||||
updateToken = true;
|
||||
tokenInfo.AppVersion = info.Version;
|
||||
}
|
||||
}
|
||||
|
||||
if ((DateTime.UtcNow - tokenInfo.DateLastActivity).TotalMinutes > 3)
|
||||
{
|
||||
tokenInfo.DateLastActivity = DateTime.UtcNow;
|
||||
updateToken = true;
|
||||
tokenInfo.AppVersion = info.Version;
|
||||
}
|
||||
|
||||
if (!tokenInfo.UserId.Equals(Guid.Empty))
|
||||
{
|
||||
info.User = _userManager.GetUserById(tokenInfo.UserId);
|
||||
|
||||
if (info.User != null && !string.Equals(info.User.Name, tokenInfo.UserName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
tokenInfo.UserName = info.User.Name;
|
||||
updateToken = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (updateToken)
|
||||
{
|
||||
_authRepo.Update(tokenInfo, CancellationToken.None);
|
||||
_authRepo.Update(tokenInfo);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var user = _connectManager.GetUserFromExchangeToken(token);
|
||||
if (user != null)
|
||||
{
|
||||
info.UserId = user.Id.ToString("N");
|
||||
}
|
||||
info.User = _connectManager.GetUserFromExchangeToken(token);
|
||||
}
|
||||
httpReq.Items["OriginalAuthenticationInfo"] = tokenInfo;
|
||||
}
|
||||
@@ -160,7 +183,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
{
|
||||
var auth = httpReq.Headers["X-Emby-Authorization"];
|
||||
|
||||
if (string.IsNullOrWhiteSpace(auth))
|
||||
if (string.IsNullOrEmpty(auth))
|
||||
{
|
||||
auth = httpReq.Headers["Authorization"];
|
||||
}
|
||||
@@ -212,7 +235,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
|
||||
private string NormalizeValue(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using MediaBrowser.Controller.Security;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.Services;
|
||||
using System;
|
||||
|
||||
namespace Emby.Server.Implementations.HttpServer.Security
|
||||
{
|
||||
@@ -21,11 +22,11 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
_sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
public Task<SessionInfo> GetSession(IRequest requestContext)
|
||||
public SessionInfo GetSession(IRequest requestContext)
|
||||
{
|
||||
var authorization = _authContext.GetAuthorizationInfo(requestContext);
|
||||
|
||||
var user = string.IsNullOrWhiteSpace(authorization.UserId) ? null : _userManager.GetUserById(authorization.UserId);
|
||||
var user = authorization.User;
|
||||
return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.RemoteIp, user);
|
||||
}
|
||||
|
||||
@@ -36,19 +37,19 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
return info as AuthenticationInfo;
|
||||
}
|
||||
|
||||
public Task<SessionInfo> GetSession(object requestContext)
|
||||
public SessionInfo GetSession(object requestContext)
|
||||
{
|
||||
return GetSession((IRequest)requestContext);
|
||||
}
|
||||
|
||||
public async Task<User> GetUser(IRequest requestContext)
|
||||
public User GetUser(IRequest requestContext)
|
||||
{
|
||||
var session = await GetSession(requestContext).ConfigureAwait(false);
|
||||
var session = GetSession(requestContext);
|
||||
|
||||
return session == null || !session.UserId.HasValue ? null : _userManager.GetUserById(session.UserId.Value);
|
||||
return session == null || session.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(session.UserId);
|
||||
}
|
||||
|
||||
public Task<User> GetUser(object requestContext)
|
||||
public User GetUser(object requestContext)
|
||||
{
|
||||
return GetUser((IRequest)requestContext);
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
using SocketHttpListener.Net;
|
||||
|
||||
namespace Emby.Server.Implementations.HttpServer.SocketSharp
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static string GetOperationName(this HttpListenerRequest request)
|
||||
{
|
||||
return request.Url.Segments[request.Url.Segments.Length - 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,923 +0,0 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using MediaBrowser.Model.Services;
|
||||
using MediaBrowser.Model.Extensions;
|
||||
|
||||
namespace Emby.Server.Implementations.HttpServer.SocketSharp
|
||||
{
|
||||
public static class MyHttpUtility
|
||||
{
|
||||
// Must be sorted
|
||||
static readonly long[] entities = new long[] {
|
||||
(long)'A' << 56 | (long)'E' << 48 | (long)'l' << 40 | (long)'i' << 32 | (long)'g' << 24,
|
||||
(long)'A' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16,
|
||||
(long)'A' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24,
|
||||
(long)'A' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16,
|
||||
(long)'A' << 56 | (long)'l' << 48 | (long)'p' << 40 | (long)'h' << 32 | (long)'a' << 24,
|
||||
(long)'A' << 56 | (long)'r' << 48 | (long)'i' << 40 | (long)'n' << 32 | (long)'g' << 24,
|
||||
(long)'A' << 56 | (long)'t' << 48 | (long)'i' << 40 | (long)'l' << 32 | (long)'d' << 24 | (long)'e' << 16,
|
||||
(long)'A' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32,
|
||||
(long)'B' << 56 | (long)'e' << 48 | (long)'t' << 40 | (long)'a' << 32,
|
||||
(long)'C' << 56 | (long)'c' << 48 | (long)'e' << 40 | (long)'d' << 32 | (long)'i' << 24 | (long)'l' << 16,
|
||||
(long)'C' << 56 | (long)'h' << 48 | (long)'i' << 40,
|
||||
(long)'D' << 56 | (long)'a' << 48 | (long)'g' << 40 | (long)'g' << 32 | (long)'e' << 24 | (long)'r' << 16,
|
||||
(long)'D' << 56 | (long)'e' << 48 | (long)'l' << 40 | (long)'t' << 32 | (long)'a' << 24,
|
||||
(long)'E' << 56 | (long)'T' << 48 | (long)'H' << 40,
|
||||
(long)'E' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16,
|
||||
(long)'E' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24,
|
||||
(long)'E' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16,
|
||||
(long)'E' << 56 | (long)'p' << 48 | (long)'s' << 40 | (long)'i' << 32 | (long)'l' << 24 | (long)'o' << 16 | (long)'n' << 8,
|
||||
(long)'E' << 56 | (long)'t' << 48 | (long)'a' << 40,
|
||||
(long)'E' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32,
|
||||
(long)'G' << 56 | (long)'a' << 48 | (long)'m' << 40 | (long)'m' << 32 | (long)'a' << 24,
|
||||
(long)'I' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16,
|
||||
(long)'I' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24,
|
||||
(long)'I' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16,
|
||||
(long)'I' << 56 | (long)'o' << 48 | (long)'t' << 40 | (long)'a' << 32,
|
||||
(long)'I' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32,
|
||||
(long)'K' << 56 | (long)'a' << 48 | (long)'p' << 40 | (long)'p' << 32 | (long)'a' << 24,
|
||||
(long)'L' << 56 | (long)'a' << 48 | (long)'m' << 40 | (long)'b' << 32 | (long)'d' << 24 | (long)'a' << 16,
|
||||
(long)'M' << 56 | (long)'u' << 48,
|
||||
(long)'N' << 56 | (long)'t' << 48 | (long)'i' << 40 | (long)'l' << 32 | (long)'d' << 24 | (long)'e' << 16,
|
||||
(long)'N' << 56 | (long)'u' << 48,
|
||||
(long)'O' << 56 | (long)'E' << 48 | (long)'l' << 40 | (long)'i' << 32 | (long)'g' << 24,
|
||||
(long)'O' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16,
|
||||
(long)'O' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24,
|
||||
(long)'O' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16,
|
||||
(long)'O' << 56 | (long)'m' << 48 | (long)'e' << 40 | (long)'g' << 32 | (long)'a' << 24,
|
||||
(long)'O' << 56 | (long)'m' << 48 | (long)'i' << 40 | (long)'c' << 32 | (long)'r' << 24 | (long)'o' << 16 | (long)'n' << 8,
|
||||
(long)'O' << 56 | (long)'s' << 48 | (long)'l' << 40 | (long)'a' << 32 | (long)'s' << 24 | (long)'h' << 16,
|
||||
(long)'O' << 56 | (long)'t' << 48 | (long)'i' << 40 | (long)'l' << 32 | (long)'d' << 24 | (long)'e' << 16,
|
||||
(long)'O' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32,
|
||||
(long)'P' << 56 | (long)'h' << 48 | (long)'i' << 40,
|
||||
(long)'P' << 56 | (long)'i' << 48,
|
||||
(long)'P' << 56 | (long)'r' << 48 | (long)'i' << 40 | (long)'m' << 32 | (long)'e' << 24,
|
||||
(long)'P' << 56 | (long)'s' << 48 | (long)'i' << 40,
|
||||
(long)'R' << 56 | (long)'h' << 48 | (long)'o' << 40,
|
||||
(long)'S' << 56 | (long)'c' << 48 | (long)'a' << 40 | (long)'r' << 32 | (long)'o' << 24 | (long)'n' << 16,
|
||||
(long)'S' << 56 | (long)'i' << 48 | (long)'g' << 40 | (long)'m' << 32 | (long)'a' << 24,
|
||||
(long)'T' << 56 | (long)'H' << 48 | (long)'O' << 40 | (long)'R' << 32 | (long)'N' << 24,
|
||||
(long)'T' << 56 | (long)'a' << 48 | (long)'u' << 40,
|
||||
(long)'T' << 56 | (long)'h' << 48 | (long)'e' << 40 | (long)'t' << 32 | (long)'a' << 24,
|
||||
(long)'U' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16,
|
||||
(long)'U' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24,
|
||||
(long)'U' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16,
|
||||
(long)'U' << 56 | (long)'p' << 48 | (long)'s' << 40 | (long)'i' << 32 | (long)'l' << 24 | (long)'o' << 16 | (long)'n' << 8,
|
||||
(long)'U' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32,
|
||||
(long)'X' << 56 | (long)'i' << 48,
|
||||
(long)'Y' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16,
|
||||
(long)'Y' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32,
|
||||
(long)'Z' << 56 | (long)'e' << 48 | (long)'t' << 40 | (long)'a' << 32,
|
||||
(long)'a' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16,
|
||||
(long)'a' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24,
|
||||
(long)'a' << 56 | (long)'c' << 48 | (long)'u' << 40 | (long)'t' << 32 | (long)'e' << 24,
|
||||
(long)'a' << 56 | (long)'e' << 48 | (long)'l' << 40 | (long)'i' << 32 | (long)'g' << 24,
|
||||
(long)'a' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16,
|
||||
(long)'a' << 56 | (long)'l' << 48 | (long)'e' << 40 | (long)'f' << 32 | (long)'s' << 24 | (long)'y' << 16 | (long)'m' << 8,
|
||||
(long)'a' << 56 | (long)'l' << 48 | (long)'p' << 40 | (long)'h' << 32 | (long)'a' << 24,
|
||||
(long)'a' << 56 | (long)'m' << 48 | (long)'p' << 40,
|
||||
(long)'a' << 56 | (long)'n' << 48 | (long)'d' << 40,
|
||||
(long)'a' << 56 | (long)'n' << 48 | (long)'g' << 40,
|
||||
(long)'a' << 56 | (long)'p' << 48 | (long)'o' << 40 | (long)'s' << 32,
|
||||
(long)'a' << 56 | (long)'r' << 48 | (long)'i' << 40 | (long)'n' << 32 | (long)'g' << 24,
|
||||
(long)'a' << 56 | (long)'s' << 48 | (long)'y' << 40 | (long)'m' << 32 | (long)'p' << 24,
|
||||
(long)'a' << 56 | (long)'t' << 48 | (long)'i' << 40 | (long)'l' << 32 | (long)'d' << 24 | (long)'e' << 16,
|
||||
(long)'a' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32,
|
||||
(long)'b' << 56 | (long)'d' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24,
|
||||
(long)'b' << 56 | (long)'e' << 48 | (long)'t' << 40 | (long)'a' << 32,
|
||||
(long)'b' << 56 | (long)'r' << 48 | (long)'v' << 40 | (long)'b' << 32 | (long)'a' << 24 | (long)'r' << 16,
|
||||
(long)'b' << 56 | (long)'u' << 48 | (long)'l' << 40 | (long)'l' << 32,
|
||||
(long)'c' << 56 | (long)'a' << 48 | (long)'p' << 40,
|
||||
(long)'c' << 56 | (long)'c' << 48 | (long)'e' << 40 | (long)'d' << 32 | (long)'i' << 24 | (long)'l' << 16,
|
||||
(long)'c' << 56 | (long)'e' << 48 | (long)'d' << 40 | (long)'i' << 32 | (long)'l' << 24,
|
||||
(long)'c' << 56 | (long)'e' << 48 | (long)'n' << 40 | (long)'t' << 32,
|
||||
(long)'c' << 56 | (long)'h' << 48 | (long)'i' << 40,
|
||||
(long)'c' << 56 | (long)'i' << 48 | (long)'r' << 40 | (long)'c' << 32,
|
||||
(long)'c' << 56 | (long)'l' << 48 | (long)'u' << 40 | (long)'b' << 32 | (long)'s' << 24,
|
||||
(long)'c' << 56 | (long)'o' << 48 | (long)'n' << 40 | (long)'g' << 32,
|
||||
(long)'c' << 56 | (long)'o' << 48 | (long)'p' << 40 | (long)'y' << 32,
|
||||
(long)'c' << 56 | (long)'r' << 48 | (long)'a' << 40 | (long)'r' << 32 | (long)'r' << 24,
|
||||
(long)'c' << 56 | (long)'u' << 48 | (long)'p' << 40,
|
||||
(long)'c' << 56 | (long)'u' << 48 | (long)'r' << 40 | (long)'r' << 32 | (long)'e' << 24 | (long)'n' << 16,
|
||||
(long)'d' << 56 | (long)'A' << 48 | (long)'r' << 40 | (long)'r' << 32,
|
||||
(long)'d' << 56 | (long)'a' << 48 | (long)'g' << 40 | (long)'g' << 32 | (long)'e' << 24 | (long)'r' << 16,
|
||||
(long)'d' << 56 | (long)'a' << 48 | (long)'r' << 40 | (long)'r' << 32,
|
||||
(long)'d' << 56 | (long)'e' << 48 | (long)'g' << 40,
|
||||
(long)'d' << 56 | (long)'e' << 48 | (long)'l' << 40 | (long)'t' << 32 | (long)'a' << 24,
|
||||
(long)'d' << 56 | (long)'i' << 48 | (long)'a' << 40 | (long)'m' << 32 | (long)'s' << 24,
|
||||
(long)'d' << 56 | (long)'i' << 48 | (long)'v' << 40 | (long)'i' << 32 | (long)'d' << 24 | (long)'e' << 16,
|
||||
(long)'e' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16,
|
||||
(long)'e' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24,
|
||||
(long)'e' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16,
|
||||
(long)'e' << 56 | (long)'m' << 48 | (long)'p' << 40 | (long)'t' << 32 | (long)'y' << 24,
|
||||
(long)'e' << 56 | (long)'m' << 48 | (long)'s' << 40 | (long)'p' << 32,
|
||||
(long)'e' << 56 | (long)'n' << 48 | (long)'s' << 40 | (long)'p' << 32,
|
||||
(long)'e' << 56 | (long)'p' << 48 | (long)'s' << 40 | (long)'i' << 32 | (long)'l' << 24 | (long)'o' << 16 | (long)'n' << 8,
|
||||
(long)'e' << 56 | (long)'q' << 48 | (long)'u' << 40 | (long)'i' << 32 | (long)'v' << 24,
|
||||
(long)'e' << 56 | (long)'t' << 48 | (long)'a' << 40,
|
||||
(long)'e' << 56 | (long)'t' << 48 | (long)'h' << 40,
|
||||
(long)'e' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32,
|
||||
(long)'e' << 56 | (long)'u' << 48 | (long)'r' << 40 | (long)'o' << 32,
|
||||
(long)'e' << 56 | (long)'x' << 48 | (long)'i' << 40 | (long)'s' << 32 | (long)'t' << 24,
|
||||
(long)'f' << 56 | (long)'n' << 48 | (long)'o' << 40 | (long)'f' << 32,
|
||||
(long)'f' << 56 | (long)'o' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'l' << 24 | (long)'l' << 16,
|
||||
(long)'f' << 56 | (long)'r' << 48 | (long)'a' << 40 | (long)'c' << 32 | (long)'1' << 24 | (long)'2' << 16,
|
||||
(long)'f' << 56 | (long)'r' << 48 | (long)'a' << 40 | (long)'c' << 32 | (long)'1' << 24 | (long)'4' << 16,
|
||||
(long)'f' << 56 | (long)'r' << 48 | (long)'a' << 40 | (long)'c' << 32 | (long)'3' << 24 | (long)'4' << 16,
|
||||
(long)'f' << 56 | (long)'r' << 48 | (long)'a' << 40 | (long)'s' << 32 | (long)'l' << 24,
|
||||
(long)'g' << 56 | (long)'a' << 48 | (long)'m' << 40 | (long)'m' << 32 | (long)'a' << 24,
|
||||
(long)'g' << 56 | (long)'e' << 48,
|
||||
(long)'g' << 56 | (long)'t' << 48,
|
||||
(long)'h' << 56 | (long)'A' << 48 | (long)'r' << 40 | (long)'r' << 32,
|
||||
(long)'h' << 56 | (long)'a' << 48 | (long)'r' << 40 | (long)'r' << 32,
|
||||
(long)'h' << 56 | (long)'e' << 48 | (long)'a' << 40 | (long)'r' << 32 | (long)'t' << 24 | (long)'s' << 16,
|
||||
(long)'h' << 56 | (long)'e' << 48 | (long)'l' << 40 | (long)'l' << 32 | (long)'i' << 24 | (long)'p' << 16,
|
||||
(long)'i' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16,
|
||||
(long)'i' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24,
|
||||
(long)'i' << 56 | (long)'e' << 48 | (long)'x' << 40 | (long)'c' << 32 | (long)'l' << 24,
|
||||
(long)'i' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16,
|
||||
(long)'i' << 56 | (long)'m' << 48 | (long)'a' << 40 | (long)'g' << 32 | (long)'e' << 24,
|
||||
(long)'i' << 56 | (long)'n' << 48 | (long)'f' << 40 | (long)'i' << 32 | (long)'n' << 24,
|
||||
(long)'i' << 56 | (long)'n' << 48 | (long)'t' << 40,
|
||||
(long)'i' << 56 | (long)'o' << 48 | (long)'t' << 40 | (long)'a' << 32,
|
||||
(long)'i' << 56 | (long)'q' << 48 | (long)'u' << 40 | (long)'e' << 32 | (long)'s' << 24 | (long)'t' << 16,
|
||||
(long)'i' << 56 | (long)'s' << 48 | (long)'i' << 40 | (long)'n' << 32,
|
||||
(long)'i' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32,
|
||||
(long)'k' << 56 | (long)'a' << 48 | (long)'p' << 40 | (long)'p' << 32 | (long)'a' << 24,
|
||||
(long)'l' << 56 | (long)'A' << 48 | (long)'r' << 40 | (long)'r' << 32,
|
||||
(long)'l' << 56 | (long)'a' << 48 | (long)'m' << 40 | (long)'b' << 32 | (long)'d' << 24 | (long)'a' << 16,
|
||||
(long)'l' << 56 | (long)'a' << 48 | (long)'n' << 40 | (long)'g' << 32,
|
||||
(long)'l' << 56 | (long)'a' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24,
|
||||
(long)'l' << 56 | (long)'a' << 48 | (long)'r' << 40 | (long)'r' << 32,
|
||||
(long)'l' << 56 | (long)'c' << 48 | (long)'e' << 40 | (long)'i' << 32 | (long)'l' << 24,
|
||||
(long)'l' << 56 | (long)'d' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24,
|
||||
(long)'l' << 56 | (long)'e' << 48,
|
||||
(long)'l' << 56 | (long)'f' << 48 | (long)'l' << 40 | (long)'o' << 32 | (long)'o' << 24 | (long)'r' << 16,
|
||||
(long)'l' << 56 | (long)'o' << 48 | (long)'w' << 40 | (long)'a' << 32 | (long)'s' << 24 | (long)'t' << 16,
|
||||
(long)'l' << 56 | (long)'o' << 48 | (long)'z' << 40,
|
||||
(long)'l' << 56 | (long)'r' << 48 | (long)'m' << 40,
|
||||
(long)'l' << 56 | (long)'s' << 48 | (long)'a' << 40 | (long)'q' << 32 | (long)'u' << 24 | (long)'o' << 16,
|
||||
(long)'l' << 56 | (long)'s' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24,
|
||||
(long)'l' << 56 | (long)'t' << 48,
|
||||
(long)'m' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'r' << 32,
|
||||
(long)'m' << 56 | (long)'d' << 48 | (long)'a' << 40 | (long)'s' << 32 | (long)'h' << 24,
|
||||
(long)'m' << 56 | (long)'i' << 48 | (long)'c' << 40 | (long)'r' << 32 | (long)'o' << 24,
|
||||
(long)'m' << 56 | (long)'i' << 48 | (long)'d' << 40 | (long)'d' << 32 | (long)'o' << 24 | (long)'t' << 16,
|
||||
(long)'m' << 56 | (long)'i' << 48 | (long)'n' << 40 | (long)'u' << 32 | (long)'s' << 24,
|
||||
(long)'m' << 56 | (long)'u' << 48,
|
||||
(long)'n' << 56 | (long)'a' << 48 | (long)'b' << 40 | (long)'l' << 32 | (long)'a' << 24,
|
||||
(long)'n' << 56 | (long)'b' << 48 | (long)'s' << 40 | (long)'p' << 32,
|
||||
(long)'n' << 56 | (long)'d' << 48 | (long)'a' << 40 | (long)'s' << 32 | (long)'h' << 24,
|
||||
(long)'n' << 56 | (long)'e' << 48,
|
||||
(long)'n' << 56 | (long)'i' << 48,
|
||||
(long)'n' << 56 | (long)'o' << 48 | (long)'t' << 40,
|
||||
(long)'n' << 56 | (long)'o' << 48 | (long)'t' << 40 | (long)'i' << 32 | (long)'n' << 24,
|
||||
(long)'n' << 56 | (long)'s' << 48 | (long)'u' << 40 | (long)'b' << 32,
|
||||
(long)'n' << 56 | (long)'t' << 48 | (long)'i' << 40 | (long)'l' << 32 | (long)'d' << 24 | (long)'e' << 16,
|
||||
(long)'n' << 56 | (long)'u' << 48,
|
||||
(long)'o' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16,
|
||||
(long)'o' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24,
|
||||
(long)'o' << 56 | (long)'e' << 48 | (long)'l' << 40 | (long)'i' << 32 | (long)'g' << 24,
|
||||
(long)'o' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16,
|
||||
(long)'o' << 56 | (long)'l' << 48 | (long)'i' << 40 | (long)'n' << 32 | (long)'e' << 24,
|
||||
(long)'o' << 56 | (long)'m' << 48 | (long)'e' << 40 | (long)'g' << 32 | (long)'a' << 24,
|
||||
(long)'o' << 56 | (long)'m' << 48 | (long)'i' << 40 | (long)'c' << 32 | (long)'r' << 24 | (long)'o' << 16 | (long)'n' << 8,
|
||||
(long)'o' << 56 | (long)'p' << 48 | (long)'l' << 40 | (long)'u' << 32 | (long)'s' << 24,
|
||||
(long)'o' << 56 | (long)'r' << 48,
|
||||
(long)'o' << 56 | (long)'r' << 48 | (long)'d' << 40 | (long)'f' << 32,
|
||||
(long)'o' << 56 | (long)'r' << 48 | (long)'d' << 40 | (long)'m' << 32,
|
||||
(long)'o' << 56 | (long)'s' << 48 | (long)'l' << 40 | (long)'a' << 32 | (long)'s' << 24 | (long)'h' << 16,
|
||||
(long)'o' << 56 | (long)'t' << 48 | (long)'i' << 40 | (long)'l' << 32 | (long)'d' << 24 | (long)'e' << 16,
|
||||
(long)'o' << 56 | (long)'t' << 48 | (long)'i' << 40 | (long)'m' << 32 | (long)'e' << 24 | (long)'s' << 16,
|
||||
(long)'o' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32,
|
||||
(long)'p' << 56 | (long)'a' << 48 | (long)'r' << 40 | (long)'a' << 32,
|
||||
(long)'p' << 56 | (long)'a' << 48 | (long)'r' << 40 | (long)'t' << 32,
|
||||
(long)'p' << 56 | (long)'e' << 48 | (long)'r' << 40 | (long)'m' << 32 | (long)'i' << 24 | (long)'l' << 16,
|
||||
(long)'p' << 56 | (long)'e' << 48 | (long)'r' << 40 | (long)'p' << 32,
|
||||
(long)'p' << 56 | (long)'h' << 48 | (long)'i' << 40,
|
||||
(long)'p' << 56 | (long)'i' << 48,
|
||||
(long)'p' << 56 | (long)'i' << 48 | (long)'v' << 40,
|
||||
(long)'p' << 56 | (long)'l' << 48 | (long)'u' << 40 | (long)'s' << 32 | (long)'m' << 24 | (long)'n' << 16,
|
||||
(long)'p' << 56 | (long)'o' << 48 | (long)'u' << 40 | (long)'n' << 32 | (long)'d' << 24,
|
||||
(long)'p' << 56 | (long)'r' << 48 | (long)'i' << 40 | (long)'m' << 32 | (long)'e' << 24,
|
||||
(long)'p' << 56 | (long)'r' << 48 | (long)'o' << 40 | (long)'d' << 32,
|
||||
(long)'p' << 56 | (long)'r' << 48 | (long)'o' << 40 | (long)'p' << 32,
|
||||
(long)'p' << 56 | (long)'s' << 48 | (long)'i' << 40,
|
||||
(long)'q' << 56 | (long)'u' << 48 | (long)'o' << 40 | (long)'t' << 32,
|
||||
(long)'r' << 56 | (long)'A' << 48 | (long)'r' << 40 | (long)'r' << 32,
|
||||
(long)'r' << 56 | (long)'a' << 48 | (long)'d' << 40 | (long)'i' << 32 | (long)'c' << 24,
|
||||
(long)'r' << 56 | (long)'a' << 48 | (long)'n' << 40 | (long)'g' << 32,
|
||||
(long)'r' << 56 | (long)'a' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24,
|
||||
(long)'r' << 56 | (long)'a' << 48 | (long)'r' << 40 | (long)'r' << 32,
|
||||
(long)'r' << 56 | (long)'c' << 48 | (long)'e' << 40 | (long)'i' << 32 | (long)'l' << 24,
|
||||
(long)'r' << 56 | (long)'d' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24,
|
||||
(long)'r' << 56 | (long)'e' << 48 | (long)'a' << 40 | (long)'l' << 32,
|
||||
(long)'r' << 56 | (long)'e' << 48 | (long)'g' << 40,
|
||||
(long)'r' << 56 | (long)'f' << 48 | (long)'l' << 40 | (long)'o' << 32 | (long)'o' << 24 | (long)'r' << 16,
|
||||
(long)'r' << 56 | (long)'h' << 48 | (long)'o' << 40,
|
||||
(long)'r' << 56 | (long)'l' << 48 | (long)'m' << 40,
|
||||
(long)'r' << 56 | (long)'s' << 48 | (long)'a' << 40 | (long)'q' << 32 | (long)'u' << 24 | (long)'o' << 16,
|
||||
(long)'r' << 56 | (long)'s' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24,
|
||||
(long)'s' << 56 | (long)'b' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24,
|
||||
(long)'s' << 56 | (long)'c' << 48 | (long)'a' << 40 | (long)'r' << 32 | (long)'o' << 24 | (long)'n' << 16,
|
||||
(long)'s' << 56 | (long)'d' << 48 | (long)'o' << 40 | (long)'t' << 32,
|
||||
(long)'s' << 56 | (long)'e' << 48 | (long)'c' << 40 | (long)'t' << 32,
|
||||
(long)'s' << 56 | (long)'h' << 48 | (long)'y' << 40,
|
||||
(long)'s' << 56 | (long)'i' << 48 | (long)'g' << 40 | (long)'m' << 32 | (long)'a' << 24,
|
||||
(long)'s' << 56 | (long)'i' << 48 | (long)'g' << 40 | (long)'m' << 32 | (long)'a' << 24 | (long)'f' << 16,
|
||||
(long)'s' << 56 | (long)'i' << 48 | (long)'m' << 40,
|
||||
(long)'s' << 56 | (long)'p' << 48 | (long)'a' << 40 | (long)'d' << 32 | (long)'e' << 24 | (long)'s' << 16,
|
||||
(long)'s' << 56 | (long)'u' << 48 | (long)'b' << 40,
|
||||
(long)'s' << 56 | (long)'u' << 48 | (long)'b' << 40 | (long)'e' << 32,
|
||||
(long)'s' << 56 | (long)'u' << 48 | (long)'m' << 40,
|
||||
(long)'s' << 56 | (long)'u' << 48 | (long)'p' << 40,
|
||||
(long)'s' << 56 | (long)'u' << 48 | (long)'p' << 40 | (long)'1' << 32,
|
||||
(long)'s' << 56 | (long)'u' << 48 | (long)'p' << 40 | (long)'2' << 32,
|
||||
(long)'s' << 56 | (long)'u' << 48 | (long)'p' << 40 | (long)'3' << 32,
|
||||
(long)'s' << 56 | (long)'u' << 48 | (long)'p' << 40 | (long)'e' << 32,
|
||||
(long)'s' << 56 | (long)'z' << 48 | (long)'l' << 40 | (long)'i' << 32 | (long)'g' << 24,
|
||||
(long)'t' << 56 | (long)'a' << 48 | (long)'u' << 40,
|
||||
(long)'t' << 56 | (long)'h' << 48 | (long)'e' << 40 | (long)'r' << 32 | (long)'e' << 24 | (long)'4' << 16,
|
||||
(long)'t' << 56 | (long)'h' << 48 | (long)'e' << 40 | (long)'t' << 32 | (long)'a' << 24,
|
||||
(long)'t' << 56 | (long)'h' << 48 | (long)'e' << 40 | (long)'t' << 32 | (long)'a' << 24 | (long)'s' << 16 | (long)'y' << 8 | (long)'m' << 0,
|
||||
(long)'t' << 56 | (long)'h' << 48 | (long)'i' << 40 | (long)'n' << 32 | (long)'s' << 24 | (long)'p' << 16,
|
||||
(long)'t' << 56 | (long)'h' << 48 | (long)'o' << 40 | (long)'r' << 32 | (long)'n' << 24,
|
||||
(long)'t' << 56 | (long)'i' << 48 | (long)'l' << 40 | (long)'d' << 32 | (long)'e' << 24,
|
||||
(long)'t' << 56 | (long)'i' << 48 | (long)'m' << 40 | (long)'e' << 32 | (long)'s' << 24,
|
||||
(long)'t' << 56 | (long)'r' << 48 | (long)'a' << 40 | (long)'d' << 32 | (long)'e' << 24,
|
||||
(long)'u' << 56 | (long)'A' << 48 | (long)'r' << 40 | (long)'r' << 32,
|
||||
(long)'u' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16,
|
||||
(long)'u' << 56 | (long)'a' << 48 | (long)'r' << 40 | (long)'r' << 32,
|
||||
(long)'u' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24,
|
||||
(long)'u' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16,
|
||||
(long)'u' << 56 | (long)'m' << 48 | (long)'l' << 40,
|
||||
(long)'u' << 56 | (long)'p' << 48 | (long)'s' << 40 | (long)'i' << 32 | (long)'h' << 24,
|
||||
(long)'u' << 56 | (long)'p' << 48 | (long)'s' << 40 | (long)'i' << 32 | (long)'l' << 24 | (long)'o' << 16 | (long)'n' << 8,
|
||||
(long)'u' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32,
|
||||
(long)'w' << 56 | (long)'e' << 48 | (long)'i' << 40 | (long)'e' << 32 | (long)'r' << 24 | (long)'p' << 16,
|
||||
(long)'x' << 56 | (long)'i' << 48,
|
||||
(long)'y' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16,
|
||||
(long)'y' << 56 | (long)'e' << 48 | (long)'n' << 40,
|
||||
(long)'y' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32,
|
||||
(long)'z' << 56 | (long)'e' << 48 | (long)'t' << 40 | (long)'a' << 32,
|
||||
(long)'z' << 56 | (long)'w' << 48 | (long)'j' << 40,
|
||||
(long)'z' << 56 | (long)'w' << 48 | (long)'n' << 40 | (long)'j' << 32
|
||||
};
|
||||
|
||||
static readonly char[] entities_values = new char[] {
|
||||
'\u00C6',
|
||||
'\u00C1',
|
||||
'\u00C2',
|
||||
'\u00C0',
|
||||
'\u0391',
|
||||
'\u00C5',
|
||||
'\u00C3',
|
||||
'\u00C4',
|
||||
'\u0392',
|
||||
'\u00C7',
|
||||
'\u03A7',
|
||||
'\u2021',
|
||||
'\u0394',
|
||||
'\u00D0',
|
||||
'\u00C9',
|
||||
'\u00CA',
|
||||
'\u00C8',
|
||||
'\u0395',
|
||||
'\u0397',
|
||||
'\u00CB',
|
||||
'\u0393',
|
||||
'\u00CD',
|
||||
'\u00CE',
|
||||
'\u00CC',
|
||||
'\u0399',
|
||||
'\u00CF',
|
||||
'\u039A',
|
||||
'\u039B',
|
||||
'\u039C',
|
||||
'\u00D1',
|
||||
'\u039D',
|
||||
'\u0152',
|
||||
'\u00D3',
|
||||
'\u00D4',
|
||||
'\u00D2',
|
||||
'\u03A9',
|
||||
'\u039F',
|
||||
'\u00D8',
|
||||
'\u00D5',
|
||||
'\u00D6',
|
||||
'\u03A6',
|
||||
'\u03A0',
|
||||
'\u2033',
|
||||
'\u03A8',
|
||||
'\u03A1',
|
||||
'\u0160',
|
||||
'\u03A3',
|
||||
'\u00DE',
|
||||
'\u03A4',
|
||||
'\u0398',
|
||||
'\u00DA',
|
||||
'\u00DB',
|
||||
'\u00D9',
|
||||
'\u03A5',
|
||||
'\u00DC',
|
||||
'\u039E',
|
||||
'\u00DD',
|
||||
'\u0178',
|
||||
'\u0396',
|
||||
'\u00E1',
|
||||
'\u00E2',
|
||||
'\u00B4',
|
||||
'\u00E6',
|
||||
'\u00E0',
|
||||
'\u2135',
|
||||
'\u03B1',
|
||||
'\u0026',
|
||||
'\u2227',
|
||||
'\u2220',
|
||||
'\u0027',
|
||||
'\u00E5',
|
||||
'\u2248',
|
||||
'\u00E3',
|
||||
'\u00E4',
|
||||
'\u201E',
|
||||
'\u03B2',
|
||||
'\u00A6',
|
||||
'\u2022',
|
||||
'\u2229',
|
||||
'\u00E7',
|
||||
'\u00B8',
|
||||
'\u00A2',
|
||||
'\u03C7',
|
||||
'\u02C6',
|
||||
'\u2663',
|
||||
'\u2245',
|
||||
'\u00A9',
|
||||
'\u21B5',
|
||||
'\u222A',
|
||||
'\u00A4',
|
||||
'\u21D3',
|
||||
'\u2020',
|
||||
'\u2193',
|
||||
'\u00B0',
|
||||
'\u03B4',
|
||||
'\u2666',
|
||||
'\u00F7',
|
||||
'\u00E9',
|
||||
'\u00EA',
|
||||
'\u00E8',
|
||||
'\u2205',
|
||||
'\u2003',
|
||||
'\u2002',
|
||||
'\u03B5',
|
||||
'\u2261',
|
||||
'\u03B7',
|
||||
'\u00F0',
|
||||
'\u00EB',
|
||||
'\u20AC',
|
||||
'\u2203',
|
||||
'\u0192',
|
||||
'\u2200',
|
||||
'\u00BD',
|
||||
'\u00BC',
|
||||
'\u00BE',
|
||||
'\u2044',
|
||||
'\u03B3',
|
||||
'\u2265',
|
||||
'\u003E',
|
||||
'\u21D4',
|
||||
'\u2194',
|
||||
'\u2665',
|
||||
'\u2026',
|
||||
'\u00ED',
|
||||
'\u00EE',
|
||||
'\u00A1',
|
||||
'\u00EC',
|
||||
'\u2111',
|
||||
'\u221E',
|
||||
'\u222B',
|
||||
'\u03B9',
|
||||
'\u00BF',
|
||||
'\u2208',
|
||||
'\u00EF',
|
||||
'\u03BA',
|
||||
'\u21D0',
|
||||
'\u03BB',
|
||||
'\u2329',
|
||||
'\u00AB',
|
||||
'\u2190',
|
||||
'\u2308',
|
||||
'\u201C',
|
||||
'\u2264',
|
||||
'\u230A',
|
||||
'\u2217',
|
||||
'\u25CA',
|
||||
'\u200E',
|
||||
'\u2039',
|
||||
'\u2018',
|
||||
'\u003C',
|
||||
'\u00AF',
|
||||
'\u2014',
|
||||
'\u00B5',
|
||||
'\u00B7',
|
||||
'\u2212',
|
||||
'\u03BC',
|
||||
'\u2207',
|
||||
'\u00A0',
|
||||
'\u2013',
|
||||
'\u2260',
|
||||
'\u220B',
|
||||
'\u00AC',
|
||||
'\u2209',
|
||||
'\u2284',
|
||||
'\u00F1',
|
||||
'\u03BD',
|
||||
'\u00F3',
|
||||
'\u00F4',
|
||||
'\u0153',
|
||||
'\u00F2',
|
||||
'\u203E',
|
||||
'\u03C9',
|
||||
'\u03BF',
|
||||
'\u2295',
|
||||
'\u2228',
|
||||
'\u00AA',
|
||||
'\u00BA',
|
||||
'\u00F8',
|
||||
'\u00F5',
|
||||
'\u2297',
|
||||
'\u00F6',
|
||||
'\u00B6',
|
||||
'\u2202',
|
||||
'\u2030',
|
||||
'\u22A5',
|
||||
'\u03C6',
|
||||
'\u03C0',
|
||||
'\u03D6',
|
||||
'\u00B1',
|
||||
'\u00A3',
|
||||
'\u2032',
|
||||
'\u220F',
|
||||
'\u221D',
|
||||
'\u03C8',
|
||||
'\u0022',
|
||||
'\u21D2',
|
||||
'\u221A',
|
||||
'\u232A',
|
||||
'\u00BB',
|
||||
'\u2192',
|
||||
'\u2309',
|
||||
'\u201D',
|
||||
'\u211C',
|
||||
'\u00AE',
|
||||
'\u230B',
|
||||
'\u03C1',
|
||||
'\u200F',
|
||||
'\u203A',
|
||||
'\u2019',
|
||||
'\u201A',
|
||||
'\u0161',
|
||||
'\u22C5',
|
||||
'\u00A7',
|
||||
'\u00AD',
|
||||
'\u03C3',
|
||||
'\u03C2',
|
||||
'\u223C',
|
||||
'\u2660',
|
||||
'\u2282',
|
||||
'\u2286',
|
||||
'\u2211',
|
||||
'\u2283',
|
||||
'\u00B9',
|
||||
'\u00B2',
|
||||
'\u00B3',
|
||||
'\u2287',
|
||||
'\u00DF',
|
||||
'\u03C4',
|
||||
'\u2234',
|
||||
'\u03B8',
|
||||
'\u03D1',
|
||||
'\u2009',
|
||||
'\u00FE',
|
||||
'\u02DC',
|
||||
'\u00D7',
|
||||
'\u2122',
|
||||
'\u21D1',
|
||||
'\u00FA',
|
||||
'\u2191',
|
||||
'\u00FB',
|
||||
'\u00F9',
|
||||
'\u00A8',
|
||||
'\u03D2',
|
||||
'\u03C5',
|
||||
'\u00FC',
|
||||
'\u2118',
|
||||
'\u03BE',
|
||||
'\u00FD',
|
||||
'\u00A5',
|
||||
'\u00FF',
|
||||
'\u03B6',
|
||||
'\u200D',
|
||||
'\u200C'
|
||||
};
|
||||
|
||||
#region Methods
|
||||
|
||||
static void WriteCharBytes(IList buf, char ch, Encoding e)
|
||||
{
|
||||
if (ch > 255)
|
||||
{
|
||||
foreach (byte b in e.GetBytes(new char[] { ch }))
|
||||
buf.Add(b);
|
||||
}
|
||||
else
|
||||
buf.Add((byte)ch);
|
||||
}
|
||||
|
||||
public static string UrlDecode(string s, Encoding e)
|
||||
{
|
||||
if (null == s)
|
||||
return null;
|
||||
|
||||
if (s.IndexOf('%') == -1 && s.IndexOf('+') == -1)
|
||||
return s;
|
||||
|
||||
if (e == null)
|
||||
e = Encoding.UTF8;
|
||||
|
||||
long len = s.Length;
|
||||
var bytes = new List<byte>();
|
||||
int xchar;
|
||||
char ch;
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
ch = s[i];
|
||||
if (ch == '%' && i + 2 < len && s[i + 1] != '%')
|
||||
{
|
||||
if (s[i + 1] == 'u' && i + 5 < len)
|
||||
{
|
||||
// unicode hex sequence
|
||||
xchar = GetChar(s, i + 2, 4);
|
||||
if (xchar != -1)
|
||||
{
|
||||
WriteCharBytes(bytes, (char)xchar, e);
|
||||
i += 5;
|
||||
}
|
||||
else
|
||||
WriteCharBytes(bytes, '%', e);
|
||||
}
|
||||
else if ((xchar = GetChar(s, i + 1, 2)) != -1)
|
||||
{
|
||||
WriteCharBytes(bytes, (char)xchar, e);
|
||||
i += 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteCharBytes(bytes, '%', e);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch == '+')
|
||||
WriteCharBytes(bytes, ' ', e);
|
||||
else
|
||||
WriteCharBytes(bytes, ch, e);
|
||||
}
|
||||
|
||||
byte[] buf = bytes.ToArray(bytes.Count);
|
||||
bytes = null;
|
||||
return e.GetString(buf, 0, buf.Length);
|
||||
|
||||
}
|
||||
|
||||
static int GetInt(byte b)
|
||||
{
|
||||
char c = (char)b;
|
||||
if (c >= '0' && c <= '9')
|
||||
return c - '0';
|
||||
|
||||
if (c >= 'a' && c <= 'f')
|
||||
return c - 'a' + 10;
|
||||
|
||||
if (c >= 'A' && c <= 'F')
|
||||
return c - 'A' + 10;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int GetChar(string str, int offset, int length)
|
||||
{
|
||||
int val = 0;
|
||||
int end = length + offset;
|
||||
for (int i = offset; i < end; i++)
|
||||
{
|
||||
char c = str[i];
|
||||
if (c > 127)
|
||||
return -1;
|
||||
|
||||
int current = GetInt((byte)c);
|
||||
if (current == -1)
|
||||
return -1;
|
||||
val = (val << 4) + current;
|
||||
}
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
static bool TryConvertKeyToEntity(string key, out char value)
|
||||
{
|
||||
var token = CalculateKeyValue(key);
|
||||
if (token == 0)
|
||||
{
|
||||
value = '\0';
|
||||
return false;
|
||||
}
|
||||
|
||||
var idx = Array.BinarySearch(entities, token);
|
||||
if (idx < 0)
|
||||
{
|
||||
value = '\0';
|
||||
return false;
|
||||
}
|
||||
|
||||
value = entities_values[idx];
|
||||
return true;
|
||||
}
|
||||
|
||||
static long CalculateKeyValue(string s)
|
||||
{
|
||||
if (s.Length > 8)
|
||||
return 0;
|
||||
|
||||
long key = 0;
|
||||
for (int i = 0; i < s.Length; ++i)
|
||||
{
|
||||
long ch = s[i];
|
||||
if (ch > 'z' || ch < '0')
|
||||
return 0;
|
||||
|
||||
key |= ch << ((7 - i) * 8);
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decodes an HTML-encoded string and returns the decoded string.
|
||||
/// </summary>
|
||||
/// <param name="s">The HTML string to decode. </param>
|
||||
/// <returns>The decoded text.</returns>
|
||||
public static string HtmlDecode(string s)
|
||||
{
|
||||
if (s == null)
|
||||
throw new ArgumentNullException("s");
|
||||
|
||||
if (s.IndexOf('&') == -1)
|
||||
return s;
|
||||
|
||||
StringBuilder entity = new StringBuilder();
|
||||
StringBuilder output = new StringBuilder();
|
||||
int len = s.Length;
|
||||
// 0 -> nothing,
|
||||
// 1 -> right after '&'
|
||||
// 2 -> between '&' and ';' but no '#'
|
||||
// 3 -> '#' found after '&' and getting numbers
|
||||
int state = 0;
|
||||
int number = 0;
|
||||
int digit_start = 0;
|
||||
bool hex_number = false;
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
char c = s[i];
|
||||
if (state == 0)
|
||||
{
|
||||
if (c == '&')
|
||||
{
|
||||
entity.Append(c);
|
||||
state = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
output.Append(c);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '&')
|
||||
{
|
||||
state = 1;
|
||||
if (digit_start > 0)
|
||||
{
|
||||
entity.Append(s, digit_start, i - digit_start);
|
||||
digit_start = 0;
|
||||
}
|
||||
|
||||
output.Append(entity.ToString());
|
||||
entity.Length = 0;
|
||||
entity.Append('&');
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case 1:
|
||||
if (c == ';')
|
||||
{
|
||||
state = 0;
|
||||
output.Append(entity.ToString());
|
||||
output.Append(c);
|
||||
entity.Length = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
number = 0;
|
||||
hex_number = false;
|
||||
if (c != '#')
|
||||
{
|
||||
state = 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
state = 3;
|
||||
}
|
||||
entity.Append(c);
|
||||
|
||||
break;
|
||||
case 2:
|
||||
entity.Append(c);
|
||||
if (c == ';')
|
||||
{
|
||||
string key = entity.ToString();
|
||||
state = 0;
|
||||
entity.Length = 0;
|
||||
|
||||
if (key.Length > 1)
|
||||
{
|
||||
var skey = key.Substring(1, key.Length - 2);
|
||||
if (TryConvertKeyToEntity(skey, out c))
|
||||
{
|
||||
output.Append(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
output.Append(key);
|
||||
}
|
||||
|
||||
break;
|
||||
case 3:
|
||||
if (c == ';')
|
||||
{
|
||||
if (number < 0x10000)
|
||||
{
|
||||
output.Append((char)number);
|
||||
}
|
||||
else
|
||||
{
|
||||
output.Append((char)(0xd800 + ((number - 0x10000) >> 10)));
|
||||
output.Append((char)(0xdc00 + ((number - 0x10000) & 0x3ff)));
|
||||
}
|
||||
state = 0;
|
||||
entity.Length = 0;
|
||||
digit_start = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
if (c == 'x' || c == 'X' && !hex_number)
|
||||
{
|
||||
digit_start = i;
|
||||
hex_number = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (Char.IsDigit(c))
|
||||
{
|
||||
if (digit_start == 0)
|
||||
digit_start = i;
|
||||
|
||||
number = number * (hex_number ? 16 : 10) + ((int)c - '0');
|
||||
break;
|
||||
}
|
||||
|
||||
if (hex_number)
|
||||
{
|
||||
if (c >= 'a' && c <= 'f')
|
||||
{
|
||||
number = number * 16 + 10 + ((int)c - 'a');
|
||||
break;
|
||||
}
|
||||
if (c >= 'A' && c <= 'F')
|
||||
{
|
||||
number = number * 16 + 10 + ((int)c - 'A');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
state = 2;
|
||||
if (digit_start > 0)
|
||||
{
|
||||
entity.Append(s, digit_start, i - digit_start);
|
||||
digit_start = 0;
|
||||
}
|
||||
|
||||
entity.Append(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (entity.Length > 0)
|
||||
{
|
||||
output.Append(entity);
|
||||
}
|
||||
else if (digit_start > 0)
|
||||
{
|
||||
output.Append(s, digit_start, s.Length - digit_start);
|
||||
}
|
||||
return output.ToString();
|
||||
}
|
||||
|
||||
public static QueryParamCollection ParseQueryString(string query)
|
||||
{
|
||||
return ParseQueryString(query, Encoding.UTF8);
|
||||
}
|
||||
|
||||
public static QueryParamCollection ParseQueryString(string query, Encoding encoding)
|
||||
{
|
||||
if (query == null)
|
||||
throw new ArgumentNullException("query");
|
||||
if (encoding == null)
|
||||
throw new ArgumentNullException("encoding");
|
||||
if (query.Length == 0 || (query.Length == 1 && query[0] == '?'))
|
||||
return new QueryParamCollection();
|
||||
if (query[0] == '?')
|
||||
query = query.Substring(1);
|
||||
|
||||
QueryParamCollection result = new QueryParamCollection();
|
||||
ParseQueryString(query, encoding, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
internal static void ParseQueryString(string query, Encoding encoding, QueryParamCollection result)
|
||||
{
|
||||
if (query.Length == 0)
|
||||
return;
|
||||
|
||||
string decoded = HtmlDecode(query);
|
||||
int decodedLength = decoded.Length;
|
||||
int namePos = 0;
|
||||
bool first = true;
|
||||
while (namePos <= decodedLength)
|
||||
{
|
||||
int valuePos = -1, valueEnd = -1;
|
||||
for (int q = namePos; q < decodedLength; q++)
|
||||
{
|
||||
if (valuePos == -1 && decoded[q] == '=')
|
||||
{
|
||||
valuePos = q + 1;
|
||||
}
|
||||
else if (decoded[q] == '&')
|
||||
{
|
||||
valueEnd = q;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (first)
|
||||
{
|
||||
first = false;
|
||||
if (decoded[namePos] == '?')
|
||||
namePos++;
|
||||
}
|
||||
|
||||
string name, value;
|
||||
if (valuePos == -1)
|
||||
{
|
||||
name = null;
|
||||
valuePos = namePos;
|
||||
}
|
||||
else
|
||||
{
|
||||
name = UrlDecode(decoded.Substring(namePos, valuePos - namePos - 1), encoding);
|
||||
}
|
||||
if (valueEnd < 0)
|
||||
{
|
||||
namePos = -1;
|
||||
valueEnd = decoded.Length;
|
||||
}
|
||||
else
|
||||
{
|
||||
namePos = valueEnd + 1;
|
||||
}
|
||||
value = UrlDecode(decoded.Substring(valuePos, valueEnd - valuePos), encoding);
|
||||
|
||||
result.Add(name, value);
|
||||
if (namePos == -1)
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endregion // Methods
|
||||
}
|
||||
}
|
||||
@@ -1,846 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.Services;
|
||||
|
||||
namespace Emby.Server.Implementations.HttpServer.SocketSharp
|
||||
{
|
||||
public partial class WebSocketSharpRequest : IHttpRequest
|
||||
{
|
||||
static internal string GetParameter(string header, string attr)
|
||||
{
|
||||
int ap = header.IndexOf(attr);
|
||||
if (ap == -1)
|
||||
return null;
|
||||
|
||||
ap += attr.Length;
|
||||
if (ap >= header.Length)
|
||||
return null;
|
||||
|
||||
char ending = header[ap];
|
||||
if (ending != '"')
|
||||
ending = ' ';
|
||||
|
||||
int end = header.IndexOf(ending, ap + 1);
|
||||
if (end == -1)
|
||||
return ending == '"' ? null : header.Substring(ap);
|
||||
|
||||
return header.Substring(ap + 1, end - ap - 1);
|
||||
}
|
||||
|
||||
async Task LoadMultiPart()
|
||||
{
|
||||
string boundary = GetParameter(ContentType, "; boundary=");
|
||||
if (boundary == null)
|
||||
return;
|
||||
|
||||
using (var requestStream = GetSubStream(InputStream, _memoryStreamProvider))
|
||||
{
|
||||
//DB: 30/01/11 - Hack to get around non-seekable stream and received HTTP request
|
||||
//Not ending with \r\n?
|
||||
var ms = _memoryStreamProvider.CreateNew(32 * 1024);
|
||||
await requestStream.CopyToAsync(ms).ConfigureAwait(false);
|
||||
|
||||
var input = ms;
|
||||
ms.WriteByte((byte)'\r');
|
||||
ms.WriteByte((byte)'\n');
|
||||
|
||||
input.Position = 0;
|
||||
|
||||
//Uncomment to debug
|
||||
//var content = new StreamReader(ms).ReadToEnd();
|
||||
//Console.WriteLine(boundary + "::" + content);
|
||||
//input.Position = 0;
|
||||
|
||||
var multi_part = new HttpMultipart(input, boundary, ContentEncoding);
|
||||
|
||||
HttpMultipart.Element e;
|
||||
while ((e = multi_part.ReadNextElement()) != null)
|
||||
{
|
||||
if (e.Filename == null)
|
||||
{
|
||||
byte[] copy = new byte[e.Length];
|
||||
|
||||
input.Position = e.Start;
|
||||
input.Read(copy, 0, (int)e.Length);
|
||||
|
||||
form.Add(e.Name, (e.Encoding ?? ContentEncoding).GetString(copy, 0, copy.Length));
|
||||
}
|
||||
else
|
||||
{
|
||||
//
|
||||
// We use a substream, as in 2.x we will support large uploads streamed to disk,
|
||||
//
|
||||
HttpPostedFile sub = new HttpPostedFile(e.Filename, e.ContentType, input, e.Start, e.Length);
|
||||
files[e.Name] = sub;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public QueryParamCollection Form
|
||||
{
|
||||
get
|
||||
{
|
||||
if (form == null)
|
||||
{
|
||||
form = new WebROCollection();
|
||||
files = new Dictionary<string, HttpPostedFile>();
|
||||
|
||||
if (IsContentType("multipart/form-data", true))
|
||||
{
|
||||
var task = LoadMultiPart();
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
else if (IsContentType("application/x-www-form-urlencoded", true))
|
||||
{
|
||||
var task = LoadWwwForm();
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
|
||||
form.Protect();
|
||||
}
|
||||
|
||||
#if NET_4_0
|
||||
if (validateRequestNewMode && !checked_form) {
|
||||
// Setting this before calling the validator prevents
|
||||
// possible endless recursion
|
||||
checked_form = true;
|
||||
ValidateNameValueCollection ("Form", query_string_nvc, RequestValidationSource.Form);
|
||||
} else
|
||||
#endif
|
||||
if (validate_form && !checked_form)
|
||||
{
|
||||
checked_form = true;
|
||||
ValidateNameValueCollection("Form", form);
|
||||
}
|
||||
|
||||
return form;
|
||||
}
|
||||
}
|
||||
|
||||
public string Accept
|
||||
{
|
||||
get
|
||||
{
|
||||
return string.IsNullOrEmpty(request.Headers["Accept"]) ? null : request.Headers["Accept"];
|
||||
}
|
||||
}
|
||||
|
||||
public string Authorization
|
||||
{
|
||||
get
|
||||
{
|
||||
return string.IsNullOrEmpty(request.Headers["Authorization"]) ? null : request.Headers["Authorization"];
|
||||
}
|
||||
}
|
||||
|
||||
protected bool validate_cookies, validate_query_string, validate_form;
|
||||
protected bool checked_cookies, checked_query_string, checked_form;
|
||||
|
||||
static void ThrowValidationException(string name, string key, string value)
|
||||
{
|
||||
string v = "\"" + value + "\"";
|
||||
if (v.Length > 20)
|
||||
v = v.Substring(0, 16) + "...\"";
|
||||
|
||||
string msg = String.Format("A potentially dangerous Request.{0} value was " +
|
||||
"detected from the client ({1}={2}).", name, key, v);
|
||||
|
||||
throw new Exception(msg);
|
||||
}
|
||||
|
||||
static void ValidateNameValueCollection(string name, QueryParamCollection coll)
|
||||
{
|
||||
if (coll == null)
|
||||
return;
|
||||
|
||||
foreach (var pair in coll)
|
||||
{
|
||||
var key = pair.Name;
|
||||
var val = pair.Value;
|
||||
if (val != null && val.Length > 0 && IsInvalidString(val))
|
||||
ThrowValidationException(name, key, val);
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool IsInvalidString(string val)
|
||||
{
|
||||
int validationFailureIndex;
|
||||
|
||||
return IsInvalidString(val, out validationFailureIndex);
|
||||
}
|
||||
|
||||
internal static bool IsInvalidString(string val, out int validationFailureIndex)
|
||||
{
|
||||
validationFailureIndex = 0;
|
||||
|
||||
int len = val.Length;
|
||||
if (len < 2)
|
||||
return false;
|
||||
|
||||
char current = val[0];
|
||||
for (int idx = 1; idx < len; idx++)
|
||||
{
|
||||
char next = val[idx];
|
||||
// See http://secunia.com/advisories/14325
|
||||
if (current == '<' || current == '\xff1c')
|
||||
{
|
||||
if (next == '!' || next < ' '
|
||||
|| (next >= 'a' && next <= 'z')
|
||||
|| (next >= 'A' && next <= 'Z'))
|
||||
{
|
||||
validationFailureIndex = idx - 1;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (current == '&' && next == '#')
|
||||
{
|
||||
validationFailureIndex = idx - 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
current = next;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void ValidateInput()
|
||||
{
|
||||
validate_cookies = true;
|
||||
validate_query_string = true;
|
||||
validate_form = true;
|
||||
}
|
||||
|
||||
bool IsContentType(string ct, bool starts_with)
|
||||
{
|
||||
if (ct == null || ContentType == null) return false;
|
||||
|
||||
if (starts_with)
|
||||
return StrUtils.StartsWith(ContentType, ct, true);
|
||||
|
||||
return string.Equals(ContentType, ct, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
async Task LoadWwwForm()
|
||||
{
|
||||
using (Stream input = GetSubStream(InputStream, _memoryStreamProvider))
|
||||
{
|
||||
using (var ms = _memoryStreamProvider.CreateNew())
|
||||
{
|
||||
await input.CopyToAsync(ms).ConfigureAwait(false);
|
||||
ms.Position = 0;
|
||||
|
||||
using (StreamReader s = new StreamReader(ms, ContentEncoding))
|
||||
{
|
||||
StringBuilder key = new StringBuilder();
|
||||
StringBuilder value = new StringBuilder();
|
||||
int c;
|
||||
|
||||
while ((c = s.Read()) != -1)
|
||||
{
|
||||
if (c == '=')
|
||||
{
|
||||
value.Length = 0;
|
||||
while ((c = s.Read()) != -1)
|
||||
{
|
||||
if (c == '&')
|
||||
{
|
||||
AddRawKeyValue(key, value);
|
||||
break;
|
||||
}
|
||||
else
|
||||
value.Append((char)c);
|
||||
}
|
||||
if (c == -1)
|
||||
{
|
||||
AddRawKeyValue(key, value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (c == '&')
|
||||
AddRawKeyValue(key, value);
|
||||
else
|
||||
key.Append((char)c);
|
||||
}
|
||||
if (c == -1)
|
||||
AddRawKeyValue(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AddRawKeyValue(StringBuilder key, StringBuilder value)
|
||||
{
|
||||
string decodedKey = WebUtility.UrlDecode(key.ToString());
|
||||
form.Add(decodedKey,
|
||||
WebUtility.UrlDecode(value.ToString()));
|
||||
|
||||
key.Length = 0;
|
||||
value.Length = 0;
|
||||
}
|
||||
|
||||
WebROCollection form;
|
||||
|
||||
Dictionary<string, HttpPostedFile> files;
|
||||
|
||||
class WebROCollection : QueryParamCollection
|
||||
{
|
||||
bool got_id;
|
||||
int id;
|
||||
|
||||
public bool GotID
|
||||
{
|
||||
get { return got_id; }
|
||||
}
|
||||
|
||||
public int ID
|
||||
{
|
||||
get { return id; }
|
||||
set
|
||||
{
|
||||
got_id = true;
|
||||
id = value;
|
||||
}
|
||||
}
|
||||
public void Protect()
|
||||
{
|
||||
//IsReadOnly = true;
|
||||
}
|
||||
|
||||
public void Unprotect()
|
||||
{
|
||||
//IsReadOnly = false;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
StringBuilder result = new StringBuilder();
|
||||
foreach (var pair in this)
|
||||
{
|
||||
if (result.Length > 0)
|
||||
result.Append('&');
|
||||
|
||||
var key = pair.Name;
|
||||
if (key != null && key.Length > 0)
|
||||
{
|
||||
result.Append(key);
|
||||
result.Append('=');
|
||||
}
|
||||
result.Append(pair.Value);
|
||||
}
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class HttpPostedFile
|
||||
{
|
||||
string name;
|
||||
string content_type;
|
||||
Stream stream;
|
||||
|
||||
class ReadSubStream : Stream
|
||||
{
|
||||
Stream s;
|
||||
long offset;
|
||||
long end;
|
||||
long position;
|
||||
|
||||
public ReadSubStream(Stream s, long offset, long length)
|
||||
{
|
||||
this.s = s;
|
||||
this.offset = offset;
|
||||
this.end = offset + length;
|
||||
position = offset;
|
||||
}
|
||||
|
||||
public override void Flush()
|
||||
{
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int dest_offset, int count)
|
||||
{
|
||||
if (buffer == null)
|
||||
throw new ArgumentNullException("buffer");
|
||||
|
||||
if (dest_offset < 0)
|
||||
throw new ArgumentOutOfRangeException("dest_offset", "< 0");
|
||||
|
||||
if (count < 0)
|
||||
throw new ArgumentOutOfRangeException("count", "< 0");
|
||||
|
||||
int len = buffer.Length;
|
||||
if (dest_offset > len)
|
||||
throw new ArgumentException("destination offset is beyond array size");
|
||||
// reordered to avoid possible integer overflow
|
||||
if (dest_offset > len - count)
|
||||
throw new ArgumentException("Reading would overrun buffer");
|
||||
|
||||
if (count > end - position)
|
||||
count = (int)(end - position);
|
||||
|
||||
if (count <= 0)
|
||||
return 0;
|
||||
|
||||
s.Position = position;
|
||||
int result = s.Read(buffer, dest_offset, count);
|
||||
if (result > 0)
|
||||
position += result;
|
||||
else
|
||||
position = end;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public override int ReadByte()
|
||||
{
|
||||
if (position >= end)
|
||||
return -1;
|
||||
|
||||
s.Position = position;
|
||||
int result = s.ReadByte();
|
||||
if (result < 0)
|
||||
position = end;
|
||||
else
|
||||
position++;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public override long Seek(long d, SeekOrigin origin)
|
||||
{
|
||||
long real;
|
||||
switch (origin)
|
||||
{
|
||||
case SeekOrigin.Begin:
|
||||
real = offset + d;
|
||||
break;
|
||||
case SeekOrigin.End:
|
||||
real = end + d;
|
||||
break;
|
||||
case SeekOrigin.Current:
|
||||
real = position + d;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException();
|
||||
}
|
||||
|
||||
long virt = real - offset;
|
||||
if (virt < 0 || virt > Length)
|
||||
throw new ArgumentException();
|
||||
|
||||
position = s.Seek(real, SeekOrigin.Begin);
|
||||
return position;
|
||||
}
|
||||
|
||||
public override void SetLength(long value)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override bool CanRead
|
||||
{
|
||||
get { return true; }
|
||||
}
|
||||
public override bool CanSeek
|
||||
{
|
||||
get { return true; }
|
||||
}
|
||||
public override bool CanWrite
|
||||
{
|
||||
get { return false; }
|
||||
}
|
||||
|
||||
public override long Length
|
||||
{
|
||||
get { return end - offset; }
|
||||
}
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get
|
||||
{
|
||||
return position - offset;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (value > Length)
|
||||
throw new ArgumentOutOfRangeException();
|
||||
|
||||
position = Seek(value, SeekOrigin.Begin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal HttpPostedFile(string name, string content_type, Stream base_stream, long offset, long length)
|
||||
{
|
||||
this.name = name;
|
||||
this.content_type = content_type;
|
||||
this.stream = new ReadSubStream(base_stream, offset, length);
|
||||
}
|
||||
|
||||
public string ContentType
|
||||
{
|
||||
get
|
||||
{
|
||||
return content_type;
|
||||
}
|
||||
}
|
||||
|
||||
public int ContentLength
|
||||
{
|
||||
get
|
||||
{
|
||||
return (int)stream.Length;
|
||||
}
|
||||
}
|
||||
|
||||
public string FileName
|
||||
{
|
||||
get
|
||||
{
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
public Stream InputStream
|
||||
{
|
||||
get
|
||||
{
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Helpers
|
||||
{
|
||||
public static readonly CultureInfo InvariantCulture = CultureInfo.InvariantCulture;
|
||||
}
|
||||
|
||||
internal sealed class StrUtils
|
||||
{
|
||||
public static bool StartsWith(string str1, string str2, bool ignore_case)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(str1))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var comparison = ignore_case ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
|
||||
return str1.IndexOf(str2, comparison) == 0;
|
||||
}
|
||||
|
||||
public static bool EndsWith(string str1, string str2, bool ignore_case)
|
||||
{
|
||||
int l2 = str2.Length;
|
||||
if (l2 == 0)
|
||||
return true;
|
||||
|
||||
int l1 = str1.Length;
|
||||
if (l2 > l1)
|
||||
return false;
|
||||
|
||||
var comparison = ignore_case ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
|
||||
return str1.IndexOf(str2, comparison) == str1.Length - str2.Length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
class HttpMultipart
|
||||
{
|
||||
|
||||
public class Element
|
||||
{
|
||||
public string ContentType;
|
||||
public string Name;
|
||||
public string Filename;
|
||||
public Encoding Encoding;
|
||||
public long Start;
|
||||
public long Length;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return "ContentType " + ContentType + ", Name " + Name + ", Filename " + Filename + ", Start " +
|
||||
Start.ToString() + ", Length " + Length.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
Stream data;
|
||||
string boundary;
|
||||
byte[] boundary_bytes;
|
||||
byte[] buffer;
|
||||
bool at_eof;
|
||||
Encoding encoding;
|
||||
StringBuilder sb;
|
||||
|
||||
const byte HYPHEN = (byte)'-', LF = (byte)'\n', CR = (byte)'\r';
|
||||
|
||||
// See RFC 2046
|
||||
// In the case of multipart entities, in which one or more different
|
||||
// sets of data are combined in a single body, a "multipart" media type
|
||||
// field must appear in the entity's header. The body must then contain
|
||||
// one or more body parts, each preceded by a boundary delimiter line,
|
||||
// and the last one followed by a closing boundary delimiter line.
|
||||
// After its boundary delimiter line, each body part then consists of a
|
||||
// header area, a blank line, and a body area. Thus a body part is
|
||||
// similar to an RFC 822 message in syntax, but different in meaning.
|
||||
|
||||
public HttpMultipart(Stream data, string b, Encoding encoding)
|
||||
{
|
||||
this.data = data;
|
||||
//DB: 30/01/11: cannot set or read the Position in HttpListener in Win.NET
|
||||
//var ms = new MemoryStream(32 * 1024);
|
||||
//data.CopyTo(ms);
|
||||
//this.data = ms;
|
||||
|
||||
boundary = b;
|
||||
boundary_bytes = encoding.GetBytes(b);
|
||||
buffer = new byte[boundary_bytes.Length + 2]; // CRLF or '--'
|
||||
this.encoding = encoding;
|
||||
sb = new StringBuilder();
|
||||
}
|
||||
|
||||
string ReadLine()
|
||||
{
|
||||
// CRLF or LF are ok as line endings.
|
||||
bool got_cr = false;
|
||||
int b = 0;
|
||||
sb.Length = 0;
|
||||
while (true)
|
||||
{
|
||||
b = data.ReadByte();
|
||||
if (b == -1)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (b == LF)
|
||||
{
|
||||
break;
|
||||
}
|
||||
got_cr = b == CR;
|
||||
sb.Append((char)b);
|
||||
}
|
||||
|
||||
if (got_cr)
|
||||
sb.Length--;
|
||||
|
||||
return sb.ToString();
|
||||
|
||||
}
|
||||
|
||||
static string GetContentDispositionAttribute(string l, string name)
|
||||
{
|
||||
int idx = l.IndexOf(name + "=\"");
|
||||
if (idx < 0)
|
||||
return null;
|
||||
int begin = idx + name.Length + "=\"".Length;
|
||||
int end = l.IndexOf('"', begin);
|
||||
if (end < 0)
|
||||
return null;
|
||||
if (begin == end)
|
||||
return "";
|
||||
return l.Substring(begin, end - begin);
|
||||
}
|
||||
|
||||
string GetContentDispositionAttributeWithEncoding(string l, string name)
|
||||
{
|
||||
int idx = l.IndexOf(name + "=\"");
|
||||
if (idx < 0)
|
||||
return null;
|
||||
int begin = idx + name.Length + "=\"".Length;
|
||||
int end = l.IndexOf('"', begin);
|
||||
if (end < 0)
|
||||
return null;
|
||||
if (begin == end)
|
||||
return "";
|
||||
|
||||
string temp = l.Substring(begin, end - begin);
|
||||
byte[] source = new byte[temp.Length];
|
||||
for (int i = temp.Length - 1; i >= 0; i--)
|
||||
source[i] = (byte)temp[i];
|
||||
|
||||
return encoding.GetString(source, 0, source.Length);
|
||||
}
|
||||
|
||||
bool ReadBoundary()
|
||||
{
|
||||
try
|
||||
{
|
||||
string line = ReadLine();
|
||||
while (line == "")
|
||||
line = ReadLine();
|
||||
if (line[0] != '-' || line[1] != '-')
|
||||
return false;
|
||||
|
||||
if (!StrUtils.EndsWith(line, boundary, false))
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
string ReadHeaders()
|
||||
{
|
||||
string s = ReadLine();
|
||||
if (s == "")
|
||||
return null;
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
bool CompareBytes(byte[] orig, byte[] other)
|
||||
{
|
||||
for (int i = orig.Length - 1; i >= 0; i--)
|
||||
if (orig[i] != other[i])
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
long MoveToNextBoundary()
|
||||
{
|
||||
long retval = 0;
|
||||
bool got_cr = false;
|
||||
|
||||
int state = 0;
|
||||
int c = data.ReadByte();
|
||||
while (true)
|
||||
{
|
||||
if (c == -1)
|
||||
return -1;
|
||||
|
||||
if (state == 0 && c == LF)
|
||||
{
|
||||
retval = data.Position - 1;
|
||||
if (got_cr)
|
||||
retval--;
|
||||
state = 1;
|
||||
c = data.ReadByte();
|
||||
}
|
||||
else if (state == 0)
|
||||
{
|
||||
got_cr = c == CR;
|
||||
c = data.ReadByte();
|
||||
}
|
||||
else if (state == 1 && c == '-')
|
||||
{
|
||||
c = data.ReadByte();
|
||||
if (c == -1)
|
||||
return -1;
|
||||
|
||||
if (c != '-')
|
||||
{
|
||||
state = 0;
|
||||
got_cr = false;
|
||||
continue; // no ReadByte() here
|
||||
}
|
||||
|
||||
int nread = data.Read(buffer, 0, buffer.Length);
|
||||
int bl = buffer.Length;
|
||||
if (nread != bl)
|
||||
return -1;
|
||||
|
||||
if (!CompareBytes(boundary_bytes, buffer))
|
||||
{
|
||||
state = 0;
|
||||
data.Position = retval + 2;
|
||||
if (got_cr)
|
||||
{
|
||||
data.Position++;
|
||||
got_cr = false;
|
||||
}
|
||||
c = data.ReadByte();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (buffer[bl - 2] == '-' && buffer[bl - 1] == '-')
|
||||
{
|
||||
at_eof = true;
|
||||
}
|
||||
else if (buffer[bl - 2] != CR || buffer[bl - 1] != LF)
|
||||
{
|
||||
state = 0;
|
||||
data.Position = retval + 2;
|
||||
if (got_cr)
|
||||
{
|
||||
data.Position++;
|
||||
got_cr = false;
|
||||
}
|
||||
c = data.ReadByte();
|
||||
continue;
|
||||
}
|
||||
data.Position = retval + 2;
|
||||
if (got_cr)
|
||||
data.Position++;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// state == 1
|
||||
state = 0; // no ReadByte() here
|
||||
}
|
||||
}
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
public Element ReadNextElement()
|
||||
{
|
||||
if (at_eof || ReadBoundary())
|
||||
return null;
|
||||
|
||||
Element elem = new Element();
|
||||
string header;
|
||||
while ((header = ReadHeaders()) != null)
|
||||
{
|
||||
if (StrUtils.StartsWith(header, "Content-Disposition:", true))
|
||||
{
|
||||
elem.Name = GetContentDispositionAttribute(header, "name");
|
||||
elem.Filename = StripPath(GetContentDispositionAttributeWithEncoding(header, "filename"));
|
||||
}
|
||||
else if (StrUtils.StartsWith(header, "Content-Type:", true))
|
||||
{
|
||||
elem.ContentType = header.Substring("Content-Type:".Length).Trim();
|
||||
elem.Encoding = GetEncoding(elem.ContentType);
|
||||
}
|
||||
}
|
||||
|
||||
long start = 0;
|
||||
start = data.Position;
|
||||
elem.Start = start;
|
||||
long pos = MoveToNextBoundary();
|
||||
if (pos == -1)
|
||||
return null;
|
||||
|
||||
elem.Length = pos - start;
|
||||
return elem;
|
||||
}
|
||||
|
||||
static string StripPath(string path)
|
||||
{
|
||||
if (path == null || path.Length == 0)
|
||||
return path;
|
||||
|
||||
if (path.IndexOf(":\\") != 1 && !path.StartsWith("\\\\"))
|
||||
return path;
|
||||
return path.Substring(path.LastIndexOf('\\') + 1);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
using MediaBrowser.Common.Events;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using WebSocketState = MediaBrowser.Model.Net.WebSocketState;
|
||||
|
||||
namespace Emby.Server.Implementations.HttpServer.SocketSharp
|
||||
{
|
||||
public class SharpWebSocket : IWebSocket
|
||||
{
|
||||
/// <summary>
|
||||
/// The logger
|
||||
/// </summary>
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public event EventHandler<EventArgs> Closed;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the web socket.
|
||||
/// </summary>
|
||||
/// <value>The web socket.</value>
|
||||
private SocketHttpListener.WebSocket WebSocket { get; set; }
|
||||
|
||||
private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
public SharpWebSocket(SocketHttpListener.WebSocket socket, ILogger logger)
|
||||
{
|
||||
if (socket == null)
|
||||
{
|
||||
throw new ArgumentNullException("socket");
|
||||
}
|
||||
|
||||
if (logger == null)
|
||||
{
|
||||
throw new ArgumentNullException("logger");
|
||||
}
|
||||
|
||||
_logger = logger;
|
||||
WebSocket = socket;
|
||||
|
||||
socket.OnMessage += socket_OnMessage;
|
||||
socket.OnClose += socket_OnClose;
|
||||
socket.OnError += socket_OnError;
|
||||
|
||||
WebSocket.ConnectAsServer();
|
||||
}
|
||||
|
||||
void socket_OnError(object sender, SocketHttpListener.ErrorEventArgs e)
|
||||
{
|
||||
_logger.Error("Error in SharpWebSocket: {0}", e.Message ?? string.Empty);
|
||||
//EventHelper.FireEventIfNotNull(Closed, this, EventArgs.Empty, _logger);
|
||||
}
|
||||
|
||||
void socket_OnClose(object sender, SocketHttpListener.CloseEventArgs e)
|
||||
{
|
||||
EventHelper.FireEventIfNotNull(Closed, this, EventArgs.Empty, _logger);
|
||||
}
|
||||
|
||||
void socket_OnMessage(object sender, SocketHttpListener.MessageEventArgs e)
|
||||
{
|
||||
//if (!string.IsNullOrWhiteSpace(e.Data))
|
||||
//{
|
||||
// if (OnReceive != null)
|
||||
// {
|
||||
// OnReceive(e.Data);
|
||||
// }
|
||||
// return;
|
||||
//}
|
||||
if (OnReceiveBytes != null)
|
||||
{
|
||||
OnReceiveBytes(e.RawData);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the state.
|
||||
/// </summary>
|
||||
/// <value>The state.</value>
|
||||
public WebSocketState State
|
||||
{
|
||||
get
|
||||
{
|
||||
WebSocketState commonState;
|
||||
|
||||
if (!Enum.TryParse(WebSocket.ReadyState.ToString(), true, out commonState))
|
||||
{
|
||||
_logger.Warn("Unrecognized WebSocketState: {0}", WebSocket.ReadyState.ToString());
|
||||
}
|
||||
|
||||
return commonState;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the async.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The bytes.</param>
|
||||
/// <param name="endOfMessage">if set to <c>true</c> [end of message].</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task SendAsync(byte[] bytes, bool endOfMessage, CancellationToken cancellationToken)
|
||||
{
|
||||
return WebSocket.SendAsync(bytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the asynchronous.
|
||||
/// </summary>
|
||||
/// <param name="text">The text.</param>
|
||||
/// <param name="endOfMessage">if set to <c>true</c> [end of message].</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken)
|
||||
{
|
||||
return WebSocket.SendAsync(text);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool dispose)
|
||||
{
|
||||
if (dispose)
|
||||
{
|
||||
WebSocket.OnMessage -= socket_OnMessage;
|
||||
WebSocket.OnClose -= socket_OnClose;
|
||||
WebSocket.OnError -= socket_OnError;
|
||||
|
||||
_cancellationTokenSource.Cancel();
|
||||
|
||||
WebSocket.Close();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the receive action.
|
||||
/// </summary>
|
||||
/// <value>The receive action.</value>
|
||||
public Action<byte[]> OnReceiveBytes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the on receive.
|
||||
/// </summary>
|
||||
/// <value>The on receive.</value>
|
||||
public Action<string> OnReceive { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using SocketHttpListener.Net;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Model.Cryptography;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Model.Services;
|
||||
using MediaBrowser.Model.System;
|
||||
using MediaBrowser.Model.Text;
|
||||
using SocketHttpListener.Primitives;
|
||||
|
||||
namespace Emby.Server.Implementations.HttpServer.SocketSharp
|
||||
{
|
||||
public class WebSocketSharpListener : IHttpListener
|
||||
{
|
||||
private HttpListener _listener;
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private readonly X509Certificate _certificate;
|
||||
private readonly IMemoryStreamFactory _memoryStreamProvider;
|
||||
private readonly ITextEncoding _textEncoding;
|
||||
private readonly INetworkManager _networkManager;
|
||||
private readonly ISocketFactory _socketFactory;
|
||||
private readonly ICryptoProvider _cryptoProvider;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly bool _enableDualMode;
|
||||
private readonly IEnvironmentInfo _environment;
|
||||
|
||||
private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
|
||||
private CancellationToken _disposeCancellationToken;
|
||||
|
||||
public WebSocketSharpListener(ILogger logger, X509Certificate certificate, IMemoryStreamFactory memoryStreamProvider, ITextEncoding textEncoding, INetworkManager networkManager, ISocketFactory socketFactory, ICryptoProvider cryptoProvider, bool enableDualMode, IFileSystem fileSystem, IEnvironmentInfo environment)
|
||||
{
|
||||
_logger = logger;
|
||||
_certificate = certificate;
|
||||
_memoryStreamProvider = memoryStreamProvider;
|
||||
_textEncoding = textEncoding;
|
||||
_networkManager = networkManager;
|
||||
_socketFactory = socketFactory;
|
||||
_cryptoProvider = cryptoProvider;
|
||||
_enableDualMode = enableDualMode;
|
||||
_fileSystem = fileSystem;
|
||||
_environment = environment;
|
||||
|
||||
_disposeCancellationToken = _disposeCancellationTokenSource.Token;
|
||||
}
|
||||
|
||||
public Action<Exception, IRequest, bool> ErrorHandler { get; set; }
|
||||
public Func<IHttpRequest, string, string, string, CancellationToken, Task> RequestHandler { get; set; }
|
||||
|
||||
public Action<WebSocketConnectingEventArgs> WebSocketConnecting { get; set; }
|
||||
|
||||
public Action<WebSocketConnectEventArgs> WebSocketConnected { get; set; }
|
||||
|
||||
public void Start(IEnumerable<string> urlPrefixes)
|
||||
{
|
||||
if (_listener == null)
|
||||
_listener = new HttpListener(_logger, _cryptoProvider, _socketFactory, _networkManager, _textEncoding, _memoryStreamProvider, _fileSystem, _environment);
|
||||
|
||||
_listener.EnableDualMode = _enableDualMode;
|
||||
|
||||
if (_certificate != null)
|
||||
{
|
||||
_listener.LoadCert(_certificate);
|
||||
}
|
||||
|
||||
foreach (var prefix in urlPrefixes)
|
||||
{
|
||||
_logger.Info("Adding HttpListener prefix " + prefix);
|
||||
_listener.Prefixes.Add(prefix);
|
||||
}
|
||||
|
||||
_listener.OnContext = ProcessContext;
|
||||
|
||||
_listener.Start();
|
||||
}
|
||||
|
||||
private void ProcessContext(HttpListenerContext context)
|
||||
{
|
||||
//InitTask(context, _disposeCancellationToken);
|
||||
Task.Run(() => InitTask(context, _disposeCancellationToken));
|
||||
}
|
||||
|
||||
private Task InitTask(HttpListenerContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
IHttpRequest httpReq = null;
|
||||
var request = context.Request;
|
||||
|
||||
try
|
||||
{
|
||||
if (request.IsWebSocketRequest)
|
||||
{
|
||||
LoggerUtils.LogRequest(_logger, request);
|
||||
|
||||
ProcessWebSocketRequest(context);
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
httpReq = GetRequest(context);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error processing request", ex);
|
||||
|
||||
httpReq = httpReq ?? GetRequest(context);
|
||||
ErrorHandler(ex, httpReq, true);
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
var uri = request.Url;
|
||||
|
||||
return RequestHandler(httpReq, uri.OriginalString, uri.Host, uri.LocalPath, cancellationToken);
|
||||
}
|
||||
|
||||
private void ProcessWebSocketRequest(HttpListenerContext ctx)
|
||||
{
|
||||
try
|
||||
{
|
||||
var endpoint = ctx.Request.RemoteEndPoint.ToString();
|
||||
var url = ctx.Request.RawUrl;
|
||||
|
||||
var connectingArgs = new WebSocketConnectingEventArgs
|
||||
{
|
||||
Url = url,
|
||||
QueryString = ctx.Request.QueryString,
|
||||
Endpoint = endpoint
|
||||
};
|
||||
|
||||
if (WebSocketConnecting != null)
|
||||
{
|
||||
WebSocketConnecting(connectingArgs);
|
||||
}
|
||||
|
||||
if (connectingArgs.AllowConnection)
|
||||
{
|
||||
_logger.Debug("Web socket connection allowed");
|
||||
|
||||
var webSocketContext = ctx.AcceptWebSocket(null);
|
||||
|
||||
if (WebSocketConnected != null)
|
||||
{
|
||||
WebSocketConnected(new WebSocketConnectEventArgs
|
||||
{
|
||||
Url = url,
|
||||
QueryString = ctx.Request.QueryString,
|
||||
WebSocket = new SharpWebSocket(webSocketContext.WebSocket, _logger),
|
||||
Endpoint = endpoint
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Warn("Web socket connection not allowed");
|
||||
ctx.Response.StatusCode = 401;
|
||||
ctx.Response.Close();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("AcceptWebSocketAsync error", ex);
|
||||
ctx.Response.StatusCode = 500;
|
||||
ctx.Response.Close();
|
||||
}
|
||||
}
|
||||
|
||||
private IHttpRequest GetRequest(HttpListenerContext httpContext)
|
||||
{
|
||||
var operationName = httpContext.Request.GetOperationName();
|
||||
|
||||
var req = new WebSocketSharpRequest(httpContext, operationName, _logger, _memoryStreamProvider);
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
public Task Stop()
|
||||
{
|
||||
_disposeCancellationTokenSource.Cancel();
|
||||
|
||||
if (_listener != null)
|
||||
{
|
||||
_listener.Close();
|
||||
}
|
||||
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
private bool _disposed;
|
||||
private readonly object _disposeLock = new object();
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
||||
lock (_disposeLock)
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
//release unmanaged resources here...
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,611 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Emby.Server.Implementations.HttpServer;
|
||||
using Emby.Server.Implementations.HttpServer.SocketSharp;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.Services;
|
||||
using SocketHttpListener.Net;
|
||||
using IHttpFile = MediaBrowser.Model.Services.IHttpFile;
|
||||
using IHttpRequest = MediaBrowser.Model.Services.IHttpRequest;
|
||||
using IHttpResponse = MediaBrowser.Model.Services.IHttpResponse;
|
||||
using IResponse = MediaBrowser.Model.Services.IResponse;
|
||||
|
||||
namespace Emby.Server.Implementations.HttpServer.SocketSharp
|
||||
{
|
||||
public partial class WebSocketSharpRequest : IHttpRequest
|
||||
{
|
||||
private readonly HttpListenerRequest request;
|
||||
private readonly IHttpResponse response;
|
||||
private readonly IMemoryStreamFactory _memoryStreamProvider;
|
||||
|
||||
public WebSocketSharpRequest(HttpListenerContext httpContext, string operationName, ILogger logger, IMemoryStreamFactory memoryStreamProvider)
|
||||
{
|
||||
this.OperationName = operationName;
|
||||
_memoryStreamProvider = memoryStreamProvider;
|
||||
this.request = httpContext.Request;
|
||||
this.response = new WebSocketSharpResponse(logger, httpContext.Response, this);
|
||||
|
||||
//HandlerFactoryPath = GetHandlerPathIfAny(UrlPrefixes[0]);
|
||||
}
|
||||
|
||||
private static string GetHandlerPathIfAny(string listenerUrl)
|
||||
{
|
||||
if (listenerUrl == null) return null;
|
||||
var pos = listenerUrl.IndexOf("://", StringComparison.OrdinalIgnoreCase);
|
||||
if (pos == -1) return null;
|
||||
var startHostUrl = listenerUrl.Substring(pos + "://".Length);
|
||||
var endPos = startHostUrl.IndexOf('/');
|
||||
if (endPos == -1) return null;
|
||||
var endHostUrl = startHostUrl.Substring(endPos + 1);
|
||||
return string.IsNullOrEmpty(endHostUrl) ? null : endHostUrl.TrimEnd('/');
|
||||
}
|
||||
|
||||
public HttpListenerRequest HttpRequest
|
||||
{
|
||||
get { return request; }
|
||||
}
|
||||
|
||||
public object OriginalRequest
|
||||
{
|
||||
get { return request; }
|
||||
}
|
||||
|
||||
public IResponse Response
|
||||
{
|
||||
get { return response; }
|
||||
}
|
||||
|
||||
public IHttpResponse HttpResponse
|
||||
{
|
||||
get { return response; }
|
||||
}
|
||||
|
||||
public string OperationName { get; set; }
|
||||
|
||||
public object Dto { get; set; }
|
||||
|
||||
public string RawUrl
|
||||
{
|
||||
get { return request.RawUrl; }
|
||||
}
|
||||
|
||||
public string AbsoluteUri
|
||||
{
|
||||
get { return request.Url.AbsoluteUri.TrimEnd('/'); }
|
||||
}
|
||||
|
||||
public string UserHostAddress
|
||||
{
|
||||
get { return request.UserHostAddress; }
|
||||
}
|
||||
|
||||
public string XForwardedFor
|
||||
{
|
||||
get
|
||||
{
|
||||
return String.IsNullOrEmpty(request.Headers["X-Forwarded-For"]) ? null : request.Headers["X-Forwarded-For"];
|
||||
}
|
||||
}
|
||||
|
||||
public int? XForwardedPort
|
||||
{
|
||||
get
|
||||
{
|
||||
return string.IsNullOrEmpty(request.Headers["X-Forwarded-Port"]) ? (int?)null : int.Parse(request.Headers["X-Forwarded-Port"]);
|
||||
}
|
||||
}
|
||||
|
||||
public string XForwardedProtocol
|
||||
{
|
||||
get
|
||||
{
|
||||
return string.IsNullOrEmpty(request.Headers["X-Forwarded-Proto"]) ? null : request.Headers["X-Forwarded-Proto"];
|
||||
}
|
||||
}
|
||||
|
||||
public string XRealIp
|
||||
{
|
||||
get
|
||||
{
|
||||
return String.IsNullOrEmpty(request.Headers["X-Real-IP"]) ? null : request.Headers["X-Real-IP"];
|
||||
}
|
||||
}
|
||||
|
||||
private string remoteIp;
|
||||
public string RemoteIp
|
||||
{
|
||||
get
|
||||
{
|
||||
return remoteIp ??
|
||||
(remoteIp = (CheckBadChars(XForwardedFor)) ??
|
||||
(NormalizeIp(CheckBadChars(XRealIp)) ??
|
||||
(request.RemoteEndPoint != null ? NormalizeIp(request.RemoteEndPoint.Address.ToString()) : null)));
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly char[] HttpTrimCharacters = new char[] { (char)0x09, (char)0xA, (char)0xB, (char)0xC, (char)0xD, (char)0x20 };
|
||||
|
||||
//
|
||||
// CheckBadChars - throws on invalid chars to be not found in header name/value
|
||||
//
|
||||
internal static string CheckBadChars(string name)
|
||||
{
|
||||
if (name == null || name.Length == 0)
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
// VALUE check
|
||||
//Trim spaces from both ends
|
||||
name = name.Trim(HttpTrimCharacters);
|
||||
|
||||
//First, check for correctly formed multi-line value
|
||||
//Second, check for absenece of CTL characters
|
||||
int crlf = 0;
|
||||
for (int i = 0; i < name.Length; ++i)
|
||||
{
|
||||
char c = (char)(0x000000ff & (uint)name[i]);
|
||||
switch (crlf)
|
||||
{
|
||||
case 0:
|
||||
if (c == '\r')
|
||||
{
|
||||
crlf = 1;
|
||||
}
|
||||
else if (c == '\n')
|
||||
{
|
||||
// Technically this is bad HTTP. But it would be a breaking change to throw here.
|
||||
// Is there an exploit?
|
||||
crlf = 2;
|
||||
}
|
||||
else if (c == 127 || (c < ' ' && c != '\t'))
|
||||
{
|
||||
throw new ArgumentException("net_WebHeaderInvalidControlChars");
|
||||
}
|
||||
break;
|
||||
|
||||
case 1:
|
||||
if (c == '\n')
|
||||
{
|
||||
crlf = 2;
|
||||
break;
|
||||
}
|
||||
throw new ArgumentException("net_WebHeaderInvalidCRLFChars");
|
||||
|
||||
case 2:
|
||||
if (c == ' ' || c == '\t')
|
||||
{
|
||||
crlf = 0;
|
||||
break;
|
||||
}
|
||||
throw new ArgumentException("net_WebHeaderInvalidCRLFChars");
|
||||
}
|
||||
}
|
||||
if (crlf != 0)
|
||||
{
|
||||
throw new ArgumentException("net_WebHeaderInvalidCRLFChars");
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
internal static bool ContainsNonAsciiChars(string token)
|
||||
{
|
||||
for (int i = 0; i < token.Length; ++i)
|
||||
{
|
||||
if ((token[i] < 0x20) || (token[i] > 0x7e))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private string NormalizeIp(string ip)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(ip))
|
||||
{
|
||||
// Handle ipv4 mapped to ipv6
|
||||
const string srch = "::ffff:";
|
||||
var index = ip.IndexOf(srch, StringComparison.OrdinalIgnoreCase);
|
||||
if (index == 0)
|
||||
{
|
||||
ip = ip.Substring(srch.Length);
|
||||
}
|
||||
}
|
||||
|
||||
return ip;
|
||||
}
|
||||
|
||||
public bool IsSecureConnection
|
||||
{
|
||||
get { return request.IsSecureConnection || XForwardedProtocol == "https"; }
|
||||
}
|
||||
|
||||
public string[] AcceptTypes
|
||||
{
|
||||
get { return request.AcceptTypes; }
|
||||
}
|
||||
|
||||
private Dictionary<string, object> items;
|
||||
public Dictionary<string, object> Items
|
||||
{
|
||||
get { return items ?? (items = new Dictionary<string, object>()); }
|
||||
}
|
||||
|
||||
private string responseContentType;
|
||||
public string ResponseContentType
|
||||
{
|
||||
get
|
||||
{
|
||||
return responseContentType
|
||||
?? (responseContentType = GetResponseContentType(this));
|
||||
}
|
||||
set
|
||||
{
|
||||
this.responseContentType = value;
|
||||
}
|
||||
}
|
||||
|
||||
public const string FormUrlEncoded = "application/x-www-form-urlencoded";
|
||||
public const string MultiPartFormData = "multipart/form-data";
|
||||
public static string GetResponseContentType(IRequest httpReq)
|
||||
{
|
||||
var specifiedContentType = GetQueryStringContentType(httpReq);
|
||||
if (!string.IsNullOrEmpty(specifiedContentType)) return specifiedContentType;
|
||||
|
||||
var serverDefaultContentType = "application/json";
|
||||
|
||||
var acceptContentTypes = httpReq.AcceptTypes;
|
||||
var defaultContentType = httpReq.ContentType;
|
||||
if (HasAnyOfContentTypes(httpReq, FormUrlEncoded, MultiPartFormData))
|
||||
{
|
||||
defaultContentType = serverDefaultContentType;
|
||||
}
|
||||
|
||||
var preferredContentTypes = new string[] {};
|
||||
|
||||
var acceptsAnything = false;
|
||||
var hasDefaultContentType = !string.IsNullOrEmpty(defaultContentType);
|
||||
if (acceptContentTypes != null)
|
||||
{
|
||||
var hasPreferredContentTypes = new bool[preferredContentTypes.Length];
|
||||
foreach (var acceptsType in acceptContentTypes)
|
||||
{
|
||||
var contentType = HttpResultFactory.GetRealContentType(acceptsType);
|
||||
acceptsAnything = acceptsAnything || contentType == "*/*";
|
||||
|
||||
for (var i = 0; i < preferredContentTypes.Length; i++)
|
||||
{
|
||||
if (hasPreferredContentTypes[i]) continue;
|
||||
var preferredContentType = preferredContentTypes[i];
|
||||
hasPreferredContentTypes[i] = contentType.StartsWith(preferredContentType);
|
||||
|
||||
//Prefer Request.ContentType if it is also a preferredContentType
|
||||
if (hasPreferredContentTypes[i] && preferredContentType == defaultContentType)
|
||||
return preferredContentType;
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < preferredContentTypes.Length; i++)
|
||||
{
|
||||
if (hasPreferredContentTypes[i]) return preferredContentTypes[i];
|
||||
}
|
||||
|
||||
if (acceptsAnything)
|
||||
{
|
||||
if (hasDefaultContentType)
|
||||
return defaultContentType;
|
||||
if (serverDefaultContentType != null)
|
||||
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 const string Soap11 = "text/xml; charset=utf-8";
|
||||
|
||||
public static bool HasAnyOfContentTypes(IRequest 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(IRequest request, string contentType)
|
||||
{
|
||||
return request.ContentType.StartsWith(contentType, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public const string Xml = "application/xml";
|
||||
private static string GetQueryStringContentType(IRequest httpReq)
|
||||
{
|
||||
var format = httpReq.QueryString["format"];
|
||||
if (format == null)
|
||||
{
|
||||
const int formatMaxLength = 4;
|
||||
var pi = httpReq.PathInfo;
|
||||
if (pi == null || pi.Length <= formatMaxLength) return null;
|
||||
if (pi[0] == '/') pi = pi.Substring(1);
|
||||
format = LeftPart(pi, '/');
|
||||
if (format.Length > formatMaxLength) return null;
|
||||
}
|
||||
|
||||
format = LeftPart(format, '.').ToLower();
|
||||
if (format.Contains("json")) return "application/json";
|
||||
if (format.Contains("xml")) return Xml;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string LeftPart(string strVal, char needle)
|
||||
{
|
||||
if (strVal == null) return null;
|
||||
var pos = strVal.IndexOf(needle);
|
||||
return pos == -1
|
||||
? strVal
|
||||
: strVal.Substring(0, pos);
|
||||
}
|
||||
|
||||
public static string HandlerFactoryPath;
|
||||
|
||||
private string pathInfo;
|
||||
public string PathInfo
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this.pathInfo == null)
|
||||
{
|
||||
var mode = HandlerFactoryPath;
|
||||
|
||||
var pos = request.RawUrl.IndexOf("?");
|
||||
if (pos != -1)
|
||||
{
|
||||
var path = request.RawUrl.Substring(0, pos);
|
||||
this.pathInfo = GetPathInfo(
|
||||
path,
|
||||
mode,
|
||||
mode ?? "");
|
||||
}
|
||||
else
|
||||
{
|
||||
this.pathInfo = request.RawUrl;
|
||||
}
|
||||
|
||||
this.pathInfo = System.Net.WebUtility.UrlDecode(pathInfo);
|
||||
this.pathInfo = NormalizePathInfo(pathInfo, mode);
|
||||
}
|
||||
return this.pathInfo;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetPathInfo(string fullPath, string mode, string appPath)
|
||||
{
|
||||
var pathInfo = ResolvePathInfoFromMappedPath(fullPath, mode);
|
||||
if (!string.IsNullOrEmpty(pathInfo)) return pathInfo;
|
||||
|
||||
//Wildcard mode relies on this to work out the handlerPath
|
||||
pathInfo = ResolvePathInfoFromMappedPath(fullPath, appPath);
|
||||
if (!string.IsNullOrEmpty(pathInfo)) return pathInfo;
|
||||
|
||||
return fullPath;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static string ResolvePathInfoFromMappedPath(string fullPath, string mappedPathRoot)
|
||||
{
|
||||
if (mappedPathRoot == null) return null;
|
||||
|
||||
var sbPathInfo = new StringBuilder();
|
||||
var fullPathParts = fullPath.Split('/');
|
||||
var mappedPathRootParts = mappedPathRoot.Split('/');
|
||||
var fullPathIndexOffset = mappedPathRootParts.Length - 1;
|
||||
var pathRootFound = false;
|
||||
|
||||
for (var fullPathIndex = 0; fullPathIndex < fullPathParts.Length; fullPathIndex++)
|
||||
{
|
||||
if (pathRootFound)
|
||||
{
|
||||
sbPathInfo.Append("/" + fullPathParts[fullPathIndex]);
|
||||
}
|
||||
else if (fullPathIndex - fullPathIndexOffset >= 0)
|
||||
{
|
||||
pathRootFound = true;
|
||||
for (var mappedPathRootIndex = 0; mappedPathRootIndex < mappedPathRootParts.Length; mappedPathRootIndex++)
|
||||
{
|
||||
if (!string.Equals(fullPathParts[fullPathIndex - fullPathIndexOffset + mappedPathRootIndex], mappedPathRootParts[mappedPathRootIndex], StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
pathRootFound = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!pathRootFound) return null;
|
||||
|
||||
var path = sbPathInfo.ToString();
|
||||
return path.Length > 1 ? path.TrimEnd('/') : "/";
|
||||
}
|
||||
|
||||
private Dictionary<string, System.Net.Cookie> cookies;
|
||||
public IDictionary<string, System.Net.Cookie> Cookies
|
||||
{
|
||||
get
|
||||
{
|
||||
if (cookies == null)
|
||||
{
|
||||
cookies = new Dictionary<string, System.Net.Cookie>();
|
||||
foreach (var cookie in this.request.Cookies)
|
||||
{
|
||||
var httpCookie = (System.Net.Cookie) cookie;
|
||||
cookies[httpCookie.Name] = new System.Net.Cookie(httpCookie.Name, httpCookie.Value, httpCookie.Path, httpCookie.Domain);
|
||||
}
|
||||
}
|
||||
|
||||
return cookies;
|
||||
}
|
||||
}
|
||||
|
||||
public string UserAgent
|
||||
{
|
||||
get { return request.UserAgent; }
|
||||
}
|
||||
|
||||
public QueryParamCollection Headers
|
||||
{
|
||||
get { return request.Headers; }
|
||||
}
|
||||
|
||||
private QueryParamCollection queryString;
|
||||
public QueryParamCollection QueryString
|
||||
{
|
||||
get { return queryString ?? (queryString = MyHttpUtility.ParseQueryString(request.Url.Query)); }
|
||||
}
|
||||
|
||||
private QueryParamCollection formData;
|
||||
public QueryParamCollection FormData
|
||||
{
|
||||
get { return formData ?? (formData = this.Form); }
|
||||
}
|
||||
|
||||
public bool IsLocal
|
||||
{
|
||||
get { return request.IsLocal; }
|
||||
}
|
||||
|
||||
private string httpMethod;
|
||||
public string HttpMethod
|
||||
{
|
||||
get
|
||||
{
|
||||
return httpMethod
|
||||
?? (httpMethod = request.HttpMethod);
|
||||
}
|
||||
}
|
||||
|
||||
public string Verb
|
||||
{
|
||||
get { return HttpMethod; }
|
||||
}
|
||||
|
||||
public string ContentType
|
||||
{
|
||||
get { return request.ContentType; }
|
||||
}
|
||||
|
||||
public Encoding contentEncoding;
|
||||
public Encoding ContentEncoding
|
||||
{
|
||||
get { return contentEncoding ?? request.ContentEncoding; }
|
||||
set { contentEncoding = value; }
|
||||
}
|
||||
|
||||
public Uri UrlReferrer
|
||||
{
|
||||
get { return request.UrlReferrer; }
|
||||
}
|
||||
|
||||
public static Encoding GetEncoding(string contentTypeHeader)
|
||||
{
|
||||
var param = GetParameter(contentTypeHeader, "charset=");
|
||||
if (param == null) return null;
|
||||
try
|
||||
{
|
||||
return Encoding.GetEncoding(param);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public Stream InputStream
|
||||
{
|
||||
get { return request.InputStream; }
|
||||
}
|
||||
|
||||
public long ContentLength
|
||||
{
|
||||
get { return request.ContentLength64; }
|
||||
}
|
||||
|
||||
private IHttpFile[] httpFiles;
|
||||
public IHttpFile[] Files
|
||||
{
|
||||
get
|
||||
{
|
||||
if (httpFiles == null)
|
||||
{
|
||||
if (files == null)
|
||||
return httpFiles = new IHttpFile[0];
|
||||
|
||||
httpFiles = new IHttpFile[files.Count];
|
||||
var i = 0;
|
||||
foreach (var pair in files)
|
||||
{
|
||||
var reqFile = pair.Value;
|
||||
httpFiles[i] = new HttpFile
|
||||
{
|
||||
ContentType = reqFile.ContentType,
|
||||
ContentLength = reqFile.ContentLength,
|
||||
FileName = reqFile.FileName,
|
||||
InputStream = reqFile.InputStream,
|
||||
};
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return httpFiles;
|
||||
}
|
||||
}
|
||||
|
||||
static Stream GetSubStream(Stream stream, IMemoryStreamFactory streamProvider)
|
||||
{
|
||||
if (stream is MemoryStream)
|
||||
{
|
||||
var other = (MemoryStream)stream;
|
||||
|
||||
byte[] buffer;
|
||||
if (streamProvider.TryGetBuffer(other, out buffer))
|
||||
{
|
||||
return streamProvider.CreateNew(buffer);
|
||||
}
|
||||
return streamProvider.CreateNew(other.ToArray());
|
||||
}
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
public static string NormalizePathInfo(string pathInfo, string handlerPath)
|
||||
{
|
||||
if (handlerPath != null && pathInfo.TrimStart('/').StartsWith(
|
||||
handlerPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return pathInfo.TrimStart('/').Substring(handlerPath.Length);
|
||||
}
|
||||
|
||||
return pathInfo;
|
||||
}
|
||||
}
|
||||
|
||||
public class HttpFile : IHttpFile
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string FileName { get; set; }
|
||||
public long ContentLength { get; set; }
|
||||
public string ContentType { get; set; }
|
||||
public Stream InputStream { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.Services;
|
||||
using SocketHttpListener.Net;
|
||||
using HttpListenerResponse = SocketHttpListener.Net.HttpListenerResponse;
|
||||
using IHttpResponse = MediaBrowser.Model.Services.IHttpResponse;
|
||||
using IRequest = MediaBrowser.Model.Services.IRequest;
|
||||
|
||||
namespace Emby.Server.Implementations.HttpServer.SocketSharp
|
||||
{
|
||||
public class WebSocketSharpResponse : IHttpResponse
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly HttpListenerResponse _response;
|
||||
|
||||
public WebSocketSharpResponse(ILogger logger, HttpListenerResponse response, IRequest request)
|
||||
{
|
||||
_logger = logger;
|
||||
this._response = response;
|
||||
Items = new Dictionary<string, object>();
|
||||
Request = request;
|
||||
}
|
||||
|
||||
public IRequest Request { get; private set; }
|
||||
public Dictionary<string, object> Items { get; private set; }
|
||||
public object OriginalResponse
|
||||
{
|
||||
get { return _response; }
|
||||
}
|
||||
|
||||
public int StatusCode
|
||||
{
|
||||
get { return this._response.StatusCode; }
|
||||
set { this._response.StatusCode = value; }
|
||||
}
|
||||
|
||||
public string StatusDescription
|
||||
{
|
||||
get { return this._response.StatusDescription; }
|
||||
set { this._response.StatusDescription = value; }
|
||||
}
|
||||
|
||||
public string ContentType
|
||||
{
|
||||
get { return _response.ContentType; }
|
||||
set { _response.ContentType = value; }
|
||||
}
|
||||
|
||||
//public ICookies Cookies { get; set; }
|
||||
|
||||
public void AddHeader(string name, string value)
|
||||
{
|
||||
if (string.Equals(name, "Content-Type", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ContentType = value;
|
||||
return;
|
||||
}
|
||||
|
||||
_response.AddHeader(name, value);
|
||||
}
|
||||
|
||||
public QueryParamCollection Headers
|
||||
{
|
||||
get
|
||||
{
|
||||
return _response.Headers;
|
||||
}
|
||||
}
|
||||
|
||||
public string GetHeader(string name)
|
||||
{
|
||||
return _response.Headers[name];
|
||||
}
|
||||
|
||||
public void Redirect(string url)
|
||||
{
|
||||
_response.Redirect(url);
|
||||
}
|
||||
|
||||
public Stream OutputStream
|
||||
{
|
||||
get { return _response.OutputStream; }
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
if (!this.IsClosed)
|
||||
{
|
||||
this.IsClosed = true;
|
||||
|
||||
try
|
||||
{
|
||||
CloseOutputStream(this._response);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error closing HttpListener output stream", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void CloseOutputStream(HttpListenerResponse response)
|
||||
{
|
||||
try
|
||||
{
|
||||
var outputStream = response.OutputStream;
|
||||
|
||||
// This is needed with compression
|
||||
outputStream.Flush();
|
||||
outputStream.Dispose();
|
||||
|
||||
response.Close();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error in HttpListenerResponseWrapper: " + ex.Message, ex);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsClosed
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public void SetContentLength(long contentLength)
|
||||
{
|
||||
//you can happily set the Content-Length header in Asp.Net
|
||||
//but HttpListener will complain if you do - you have to set ContentLength64 on the response.
|
||||
//workaround: HttpListener throws "The parameter is incorrect" exceptions when we try to set the Content-Length header
|
||||
_response.ContentLength64 = contentLength;
|
||||
}
|
||||
|
||||
public void SetCookie(Cookie cookie)
|
||||
{
|
||||
var cookieStr = AsHeaderValue(cookie);
|
||||
_response.Headers.Add("Set-Cookie", cookieStr);
|
||||
}
|
||||
|
||||
public static string AsHeaderValue(Cookie cookie)
|
||||
{
|
||||
var defaultExpires = DateTime.MinValue;
|
||||
|
||||
var path = cookie.Expires == defaultExpires
|
||||
? "/"
|
||||
: cookie.Path ?? "/";
|
||||
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.Append($"{cookie.Name}={cookie.Value};path={path}");
|
||||
|
||||
if (cookie.Expires != defaultExpires)
|
||||
{
|
||||
sb.Append($";expires={cookie.Expires:R}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(cookie.Domain))
|
||||
{
|
||||
sb.Append($";domain={cookie.Domain}");
|
||||
}
|
||||
//else if (restrictAllCookiesToDomain != null)
|
||||
//{
|
||||
// sb.Append($";domain={restrictAllCookiesToDomain}");
|
||||
//}
|
||||
|
||||
if (cookie.Secure)
|
||||
{
|
||||
sb.Append(";Secure");
|
||||
}
|
||||
if (cookie.HttpOnly)
|
||||
{
|
||||
sb.Append(";HttpOnly");
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
|
||||
public bool SendChunked
|
||||
{
|
||||
get { return _response.SendChunked; }
|
||||
set { _response.SendChunked = value; }
|
||||
}
|
||||
|
||||
public bool KeepAlive { get; set; }
|
||||
|
||||
public void ClearCookies()
|
||||
{
|
||||
}
|
||||
|
||||
public Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken)
|
||||
{
|
||||
return _response.TransmitFile(path, offset, count, fileShareMode, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="contentType">Type of the content.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public StreamWriter(byte[] source, string contentType, ILogger logger)
|
||||
public StreamWriter(byte[] source, string contentType, int contentLength, ILogger logger)
|
||||
{
|
||||
if (string.IsNullOrEmpty(contentType))
|
||||
{
|
||||
@@ -85,7 +85,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
|
||||
Headers["Content-Type"] = contentType;
|
||||
|
||||
Headers["Content-Length"] = source.Length.ToString(UsCulture);
|
||||
Headers["Content-Length"] = contentLength.ToString(UsCulture);
|
||||
}
|
||||
|
||||
public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken)
|
||||
@@ -106,10 +106,8 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
Logger.ErrorException("Error streaming data", ex);
|
||||
|
||||
if (OnError != null)
|
||||
{
|
||||
OnError();
|
||||
|
||||
290
Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
Normal file
290
Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
Normal file
@@ -0,0 +1,290 @@
|
||||
using System.Text;
|
||||
using MediaBrowser.Common.Events;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.Services;
|
||||
using MediaBrowser.Model.Text;
|
||||
using System.Net.WebSockets;
|
||||
using Emby.Server.Implementations.Net;
|
||||
|
||||
namespace Emby.Server.Implementations.HttpServer
|
||||
{
|
||||
/// <summary>
|
||||
/// Class WebSocketConnection
|
||||
/// </summary>
|
||||
public class WebSocketConnection : IWebSocketConnection
|
||||
{
|
||||
public event EventHandler<EventArgs> Closed;
|
||||
|
||||
/// <summary>
|
||||
/// The _socket
|
||||
/// </summary>
|
||||
private readonly IWebSocket _socket;
|
||||
|
||||
/// <summary>
|
||||
/// The _remote end point
|
||||
/// </summary>
|
||||
public string RemoteEndPoint { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The logger
|
||||
/// </summary>
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// The _json serializer
|
||||
/// </summary>
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the receive action.
|
||||
/// </summary>
|
||||
/// <value>The receive action.</value>
|
||||
public Func<WebSocketMessageInfo, Task> OnReceive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the last activity date.
|
||||
/// </summary>
|
||||
/// <value>The last activity date.</value>
|
||||
public DateTime LastActivityDate { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the id.
|
||||
/// </summary>
|
||||
/// <value>The id.</value>
|
||||
public Guid Id { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the URL.
|
||||
/// </summary>
|
||||
/// <value>The URL.</value>
|
||||
public string Url { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the query string.
|
||||
/// </summary>
|
||||
/// <value>The query string.</value>
|
||||
public QueryParamCollection QueryString { get; set; }
|
||||
private readonly ITextEncoding _textEncoding;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WebSocketConnection" /> class.
|
||||
/// </summary>
|
||||
/// <param name="socket">The socket.</param>
|
||||
/// <param name="remoteEndPoint">The remote end point.</param>
|
||||
/// <param name="jsonSerializer">The json serializer.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <exception cref="System.ArgumentNullException">socket</exception>
|
||||
public WebSocketConnection(IWebSocket socket, string remoteEndPoint, IJsonSerializer jsonSerializer, ILogger logger, ITextEncoding textEncoding)
|
||||
{
|
||||
if (socket == null)
|
||||
{
|
||||
throw new ArgumentNullException("socket");
|
||||
}
|
||||
if (string.IsNullOrEmpty(remoteEndPoint))
|
||||
{
|
||||
throw new ArgumentNullException("remoteEndPoint");
|
||||
}
|
||||
if (jsonSerializer == null)
|
||||
{
|
||||
throw new ArgumentNullException("jsonSerializer");
|
||||
}
|
||||
if (logger == null)
|
||||
{
|
||||
throw new ArgumentNullException("logger");
|
||||
}
|
||||
|
||||
Id = Guid.NewGuid();
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_socket = socket;
|
||||
_socket.OnReceiveBytes = OnReceiveInternal;
|
||||
|
||||
var memorySocket = socket as IMemoryWebSocket;
|
||||
if (memorySocket != null)
|
||||
{
|
||||
memorySocket.OnReceiveMemoryBytes = OnReceiveInternal;
|
||||
}
|
||||
|
||||
RemoteEndPoint = remoteEndPoint;
|
||||
_logger = logger;
|
||||
_textEncoding = textEncoding;
|
||||
|
||||
socket.Closed += socket_Closed;
|
||||
}
|
||||
|
||||
void socket_Closed(object sender, EventArgs e)
|
||||
{
|
||||
EventHelper.FireEventIfNotNull(Closed, this, EventArgs.Empty, _logger);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when [receive].
|
||||
/// </summary>
|
||||
/// <param name="bytes">The bytes.</param>
|
||||
private void OnReceiveInternal(byte[] bytes)
|
||||
{
|
||||
LastActivityDate = DateTime.UtcNow;
|
||||
|
||||
if (OnReceive == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var charset = _textEncoding.GetDetectedEncodingName(bytes, bytes.Length, null, false);
|
||||
|
||||
if (string.Equals(charset, "utf-8", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
OnReceiveInternal(Encoding.UTF8.GetString(bytes, 0, bytes.Length));
|
||||
}
|
||||
else
|
||||
{
|
||||
OnReceiveInternal(_textEncoding.GetASCIIEncoding().GetString(bytes, 0, bytes.Length));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when [receive].
|
||||
/// </summary>
|
||||
/// <param name="bytes">The bytes.</param>
|
||||
private void OnReceiveInternal(Memory<byte> memory, int length)
|
||||
{
|
||||
LastActivityDate = DateTime.UtcNow;
|
||||
|
||||
if (OnReceive == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var bytes = memory.Slice(0, length).ToArray();
|
||||
|
||||
var charset = _textEncoding.GetDetectedEncodingName(bytes, bytes.Length, null, false);
|
||||
|
||||
if (string.Equals(charset, "utf-8", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
OnReceiveInternal(Encoding.UTF8.GetString(bytes, 0, bytes.Length));
|
||||
}
|
||||
else
|
||||
{
|
||||
OnReceiveInternal(_textEncoding.GetASCIIEncoding().GetString(bytes, 0, bytes.Length));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnReceiveInternal(string message)
|
||||
{
|
||||
LastActivityDate = DateTime.UtcNow;
|
||||
|
||||
if (!message.StartsWith("{", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// This info is useful sometimes but also clogs up the log
|
||||
//_logger.Error("Received web socket message that is not a json structure: " + message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (OnReceive == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var stub = (WebSocketMessage<object>)_jsonSerializer.DeserializeFromString(message, typeof(WebSocketMessage<object>));
|
||||
|
||||
var info = new WebSocketMessageInfo
|
||||
{
|
||||
MessageType = stub.MessageType,
|
||||
Data = stub.Data == null ? null : stub.Data.ToString(),
|
||||
Connection = this
|
||||
};
|
||||
|
||||
OnReceive(info);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error processing web socket message", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message asynchronously.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">message</exception>
|
||||
public Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken)
|
||||
{
|
||||
if (message == null)
|
||||
{
|
||||
throw new ArgumentNullException("message");
|
||||
}
|
||||
|
||||
var json = _jsonSerializer.SerializeToString(message);
|
||||
|
||||
return SendAsync(json, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The buffer.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task SendAsync(byte[] buffer, CancellationToken cancellationToken)
|
||||
{
|
||||
if (buffer == null)
|
||||
{
|
||||
throw new ArgumentNullException("buffer");
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
return _socket.SendAsync(buffer, true, cancellationToken);
|
||||
}
|
||||
|
||||
public Task SendAsync(string text, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
throw new ArgumentNullException("text");
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
return _socket.SendAsync(text, true, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the state.
|
||||
/// </summary>
|
||||
/// <value>The state.</value>
|
||||
public WebSocketState State
|
||||
{
|
||||
get { return _socket.State; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool dispose)
|
||||
{
|
||||
if (dispose)
|
||||
{
|
||||
_socket.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user