diff options
Diffstat (limited to 'Emby.Server.Implementations/HttpServer')
19 files changed, 910 insertions, 3358 deletions
diff --git a/Emby.Server.Implementations/HttpServer/FileWriter.cs b/Emby.Server.Implementations/HttpServer/FileWriter.cs index aa679e1b9..353ba5282 100644 --- a/Emby.Server.Implementations/HttpServer/FileWriter.cs +++ b/Emby.Server.Implementations/HttpServer/FileWriter.cs @@ -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 { diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index 937eb8029..0093258e3 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -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 (!ValidateSsl(httpReq.RemoteIp, urlString)) + if (!ValidateRequest(remoteIp, httpReq.IsLocal)) { - var httpsUrl = urlString - .Replace("http://", "https://", StringComparison.OrdinalIgnoreCase) - .Replace(":" + _config.Configuration.PublicPort.ToString(CultureInfo.InvariantCulture), ":" + _config.Configuration.PublicHttpsPort.ToString(CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase); + httpRes.StatusCode = 403; + httpRes.ContentType = "text/plain"; + await Write(httpRes, "Forbidden").ConfigureAwait(false); + return; + } - RedirectToUrl(httpRes, httpsUrl); + if (!ValidateSsl(httpReq.RemoteIp, urlString)) + { + 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; diff --git a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs index 86deccee1..df493b4c3 100644 --- a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs +++ b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs @@ -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(requestContext, content, contentType, true, responseHeaders); + } + + public object GetResult(string content, string contentType, IDictionary<string, string> responseHeaders = null) { - return GetHttpResult(content, contentType, true, responseHeaders); + 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) { - IHasHeaders result; + var result = new StreamWriter(content, contentType, _logger); - var stream = content as Stream; + if (responseHeaders == null) + { + responseHeaders = new Dictionary<string, string>(); + } - if (stream != null) + string expires; + if (addCachePrevention && !responseHeaders.TryGetValue("Expires", out expires)) { - result = new StreamWriter(stream, contentType, _logger); + responseHeaders["Expires"] = "-1"; } - else + 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 compressionType = requestContext == null ? null : GetCompressionType(requestContext, content, contentType); + + var isHeadRequest = string.Equals(requestContext.Verb, "head", StringComparison.OrdinalIgnoreCase); + + if (string.IsNullOrEmpty(compressionType)) { - var bytes = content as byte[]; + var contentLength = content.Length; - if (bytes != null) + if (isHeadRequest) { - result = new StreamWriter(bytes, contentType, _logger); + content = Array.Empty<byte>(); } - 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 = new StreamWriter(content, contentType, contentLength, _logger); + } + else + { + 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,20 +199,9 @@ 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) { throw new ArgumentNullException("result"); @@ -147,24 +212,49 @@ namespace Emby.Server.Implementations.HttpServer responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); } - if (addCachePrevention) + responseHeaders["Expires"] = "-1"; + + return ToOptimizedResultInternal(requestContext, result, responseHeaders); + } + + private string GetCompressionType(IRequest request, byte[] content, string responseContentType) + { + if (responseContentType == null) { - responseHeaders["Expires"] = "-1"; + return null; } - return ToOptimizedResultInternal(requestContext, result, responseHeaders); + // 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); } - public static string GetCompressionType(IRequest 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,153 +282,137 @@ 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: - { - var ms = new MemoryStream(); - var writerFn = RequestHelper.GetResponseWriter(HttpListenerHost.Instance, contentType); - - writerFn(dto, ms); + break; + } - ms.Position = 0; + var isHeadRequest = string.Equals(request.Verb, "head", StringComparison.OrdinalIgnoreCase); - if (string.Equals(request.Verb, "head", StringComparison.OrdinalIgnoreCase)) - { - return GetHttpResult(new byte[] { }, contentType, true, responseHeaders); - } + var ms = new MemoryStream(); + var writerFn = RequestHelper.GetResponseWriter(HttpListenerHost.Instance, contentType); - return GetHttpResult(ms, contentType, true, responseHeaders); - } - } - } + writerFn(dto, ms); - public static string GetRealContentType(string contentType) - { - return contentType == null - ? null - : contentType.Split(';')[0].ToLower().Trim(); - } + ms.Position = 0; - private string SerializeToXmlString(object from) - { - using (var ms = new MemoryStream()) + if (isHeadRequest) { - var xwSettings = new XmlWriterSettings(); - xwSettings.Encoding = new UTF8Encoding(false); - xwSettings.OmitXmlDeclaration = false; - - using (var xw = XmlWriter.Create(ms, xwSettings)) + using (ms) { - var serializer = new DataContractSerializer(from.GetType()); - serializer.WriteObject(xw, from); - xw.Flush(); - ms.Seek(0, SeekOrigin.Begin); - var reader = new StreamReader(ms); - return reader.ReadToEnd(); + return GetHttpResult(request, Array.Empty<byte>(), contentType, true, responseHeaders); } } + + return GetHttpResult(request, ms, contentType, true, responseHeaders); } - /// <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 + private IHasHeaders GetCompressedResult(byte[] content, + string requestedCompressionType, + IDictionary<string, string> responseHeaders, + bool isHeadRequest, + string contentType) { - 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); + content = Compress(content, requestedCompressionType); + responseHeaders["Content-Encoding"] = requestedCompressionType; - if (result != null) - { - return result; - } + responseHeaders["Vary"] = "Accept-Encoding"; - return GetOptimizedResultInternal(requestContext, factoryFn(), false, responseHeaders); - } + var contentLength = content.Length; - /// <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) + if (isHeadRequest) { - throw new ArgumentNullException("cacheKey"); + var result = new StreamWriter(Array.Empty<byte>(), contentType, contentLength, _logger); + AddResponseHeaders(result, responseHeaders); + return result; } - if (factoryFn == null) + else { - throw new ArgumentNullException("factoryFn"); + var result = new StreamWriter(content, contentType, contentLength, _logger); + AddResponseHeaders(result, responseHeaders); + return result; } + } - var key = cacheKey.ToString("N"); + private byte[] Compress(byte[] bytes, string compressionType) + { + if (string.Equals(compressionType, "br", StringComparison.OrdinalIgnoreCase)) + return CompressBrotli(bytes); - if (responseHeaders == null) - { - responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - } + if (string.Equals(compressionType, "deflate", StringComparison.OrdinalIgnoreCase)) + return Deflate(bytes); + + if (string.Equals(compressionType, "gzip", StringComparison.OrdinalIgnoreCase)) + return GZip(bytes); - // See if the result is already cached in the browser - var result = GetCachedResult(requestContext, responseHeaders, cacheKey, key, lastDateModified, cacheDuration, contentType); + throw new NotSupportedException(compressionType); + } + + private byte[] CompressBrotli(byte[] bytes) + { + return _brotliCompressor.Compress(bytes); + } - if (result != null) + 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)) { - return result; + zipStream.Write(bytes, 0, bytes.Length); + zipStream.Dispose(); + + return ms.ToArray(); } + } - result = factoryFn(); + 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(); - // Apply caching headers - var hasHeaders = result as IHasHeaders; + return ms.ToArray(); + } + } - if (hasHeaders != null) + public static string GetRealContentType(string contentType) + { + return contentType == null + ? null + : contentType.Split(';')[0].ToLower().Trim(); + } + + private string SerializeToXmlString(object from) + { + using (var ms = new MemoryStream()) { - AddResponseHeaders(hasHeaders, responseHeaders); - return hasHeaders; - } + var xwSettings = new XmlWriterSettings(); + xwSettings.Encoding = new UTF8Encoding(false); + xwSettings.OmitXmlDeclaration = false; - return GetHttpResult(result, contentType, false, responseHeaders); + using (var xw = XmlWriter.Create(ms, xwSettings)) + { + var serializer = new DataContractSerializer(from.GetType()); + serializer.WriteObject(xw, from); + xw.Flush(); + ms.Seek(0, SeekOrigin.Begin); + var reader = new StreamReader(ms); + return reader.ReadToEnd(); + } + } } /// <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 totalContentLength = options.ContentLength; + if (!totalContentLength.HasValue) { - var stream = await factoryFn().ConfigureAwait(false); + try + { + totalContentLength = stream.Length; + } + catch (NotSupportedException) + { - var hasHeaders = new RangeRequestWriter(rangeHeader, stream, contentType, isHeadRequest, _logger) + } + } + + 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); + } }
\ No newline at end of file diff --git a/Emby.Server.Implementations/HttpServer/IHttpListener.cs b/Emby.Server.Implementations/HttpServer/IHttpListener.cs index 9feb2311d..e21607ebd 100644 --- a/Emby.Server.Implementations/HttpServer/IHttpListener.cs +++ b/Emby.Server.Implementations/HttpServer/IHttpListener.cs @@ -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. diff --git a/Emby.Server.Implementations/HttpServer/LoggerUtils.cs b/Emby.Server.Implementations/HttpServer/LoggerUtils.cs index 46bb4c7f9..3aa48efd4 100644 --- a/Emby.Server.Implementations/HttpServer/LoggerUtils.cs +++ b/Emby.Server.Implementations/HttpServer/LoggerUtils.cs @@ -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) diff --git a/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs b/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs index 7c967949b..4177c7e78 100644 --- a/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs +++ b/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs @@ -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; } diff --git a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs index ac36f8f51..385d19b6b 100644 --- a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs +++ b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs @@ -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; } } } diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs index fadab4482..8f6d042dd 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs @@ -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 (string.IsNullOrEmpty(auth.Token)) { return true; } - if (tokenInfo != null && string.IsNullOrWhiteSpace(tokenInfo.UserId)) + 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); diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs index a41c51d1a..ee8f6c60b 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs @@ -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; } diff --git a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs index 9826a0d56..a919ce008 100644 --- a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs +++ b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs @@ -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); } diff --git a/Emby.Server.Implementations/HttpServer/SocketSharp/Extensions.cs b/Emby.Server.Implementations/HttpServer/SocketSharp/Extensions.cs deleted file mode 100644 index 07a338f19..000000000 --- a/Emby.Server.Implementations/HttpServer/SocketSharp/Extensions.cs +++ /dev/null @@ -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]; - } - } -} diff --git a/Emby.Server.Implementations/HttpServer/SocketSharp/HttpUtility.cs b/Emby.Server.Implementations/HttpServer/SocketSharp/HttpUtility.cs deleted file mode 100644 index 4e8dd7362..000000000 --- a/Emby.Server.Implementations/HttpServer/SocketSharp/HttpUtility.cs +++ /dev/null @@ -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 - } -} diff --git a/Emby.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs b/Emby.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs deleted file mode 100644 index ec14c32c8..000000000 --- a/Emby.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs +++ /dev/null @@ -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); - } - } - - } -} diff --git a/Emby.Server.Implementations/HttpServer/SocketSharp/SharpWebSocket.cs b/Emby.Server.Implementations/HttpServer/SocketSharp/SharpWebSocket.cs deleted file mode 100644 index cc7a4557e..000000000 --- a/Emby.Server.Implementations/HttpServer/SocketSharp/SharpWebSocket.cs +++ /dev/null @@ -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; } - } -} diff --git a/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs deleted file mode 100644 index 8fb0d4f3e..000000000 --- a/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs +++ /dev/null @@ -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; - } - } - } - -}
\ No newline at end of file diff --git a/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs deleted file mode 100644 index 522377f0c..000000000 --- a/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs +++ /dev/null @@ -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; } - } -} diff --git a/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs deleted file mode 100644 index 5b51c0cf1..000000000 --- a/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs +++ /dev/null @@ -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); - } - } -} diff --git a/Emby.Server.Implementations/HttpServer/StreamWriter.cs b/Emby.Server.Implementations/HttpServer/StreamWriter.cs index 5d42f42fa..3f394d3ac 100644 --- a/Emby.Server.Implementations/HttpServer/StreamWriter.cs +++ b/Emby.Server.Implementations/HttpServer/StreamWriter.cs @@ -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(); diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs new file mode 100644 index 000000000..d449e4424 --- /dev/null +++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs @@ -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(); + } + } + } +} |
