diff options
| author | Luke <luke.pulverenti@gmail.com> | 2016-12-18 00:44:33 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2016-12-18 00:44:33 -0500 |
| commit | e7cebb91a73354dc3e0d0b6340c9fbd6511f4406 (patch) | |
| tree | 6f1c368c766c17b7514fe749c0e92e69cd89194a /Emby.Server.Implementations/HttpServer | |
| parent | 025905a3e4d50b9a2e07fbf4ff0a203af6604ced (diff) | |
| parent | aaa027f3229073e9a40756c3157d41af2a442922 (diff) | |
Merge pull request #2350 from MediaBrowser/beta
Beta
Diffstat (limited to 'Emby.Server.Implementations/HttpServer')
19 files changed, 5660 insertions, 0 deletions
diff --git a/Emby.Server.Implementations/HttpServer/GetSwaggerResource.cs b/Emby.Server.Implementations/HttpServer/GetSwaggerResource.cs new file mode 100644 index 000000000..819ede1ab --- /dev/null +++ b/Emby.Server.Implementations/HttpServer/GetSwaggerResource.cs @@ -0,0 +1,17 @@ +using MediaBrowser.Model.Services; + +namespace Emby.Server.Implementations.HttpServer +{ + /// <summary> + /// Class GetDashboardResource + /// </summary> + [Route("/swagger-ui/{ResourceName*}", "GET")] + public class GetSwaggerResource + { + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <value>The name.</value> + public string ResourceName { get; set; } + } +}
\ No newline at end of file diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs new file mode 100644 index 000000000..0e1f5a551 --- /dev/null +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -0,0 +1,729 @@ +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Logging; +using ServiceStack; +using ServiceStack.Host; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Emby.Server.Implementations.HttpServer; +using Emby.Server.Implementations.HttpServer.SocketSharp; +using MediaBrowser.Common.Net; +using MediaBrowser.Common.Security; +using MediaBrowser.Controller; +using MediaBrowser.Model.Cryptography; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Services; +using MediaBrowser.Model.System; +using MediaBrowser.Model.Text; +using SocketHttpListener.Net; +using SocketHttpListener.Primitives; + +namespace Emby.Server.Implementations.HttpServer +{ + public class HttpListenerHost : ServiceStackHost, IHttpServer + { + private string DefaultRedirectPath { get; set; } + + private readonly ILogger _logger; + public IEnumerable<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; + + 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 IJsonSerializer _jsonSerializer; + private readonly IXmlSerializer _xmlSerializer; + private readonly ICertificate _certificate; + private readonly IEnvironmentInfo _environment; + private readonly IStreamFactory _streamFactory; + private readonly Func<Type, Func<string, object>> _funcParseFn; + private readonly bool _enableDualModeSockets; + + 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, ICertificate certificate, IStreamFactory streamFactory, Func<Type, Func<string, object>> funcParseFn, bool enableDualModeSockets) + : base(serviceName) + { + _appHost = applicationHost; + DefaultRedirectPath = defaultRedirectPath; + _networkManager = networkManager; + _memoryStreamProvider = memoryStreamProvider; + _textEncoding = textEncoding; + _socketFactory = socketFactory; + _cryptoProvider = cryptoProvider; + _jsonSerializer = jsonSerializer; + _xmlSerializer = xmlSerializer; + _environment = environment; + _certificate = certificate; + _streamFactory = streamFactory; + _funcParseFn = funcParseFn; + _enableDualModeSockets = enableDualModeSockets; + _config = config; + + _logger = logger; + } + + public string GlobalResponse { get; set; } + + readonly Dictionary<Type, int> _mapExceptionToStatusCode = new Dictionary<Type, int> + { + {typeof (InvalidOperationException), 500}, + {typeof (NotImplementedException), 500}, + {typeof (ResourceNotFoundException), 404}, + {typeof (FileNotFoundException), 404}, + //{typeof (DirectoryNotFoundException), 404}, + {typeof (SecurityException), 401}, + {typeof (PaymentRequiredException), 402}, + {typeof (UnauthorizedAccessException), 500}, + {typeof (PlatformNotSupportedException), 500}, + {typeof (NotSupportedException), 500} + }; + + public override void Configure() + { + var requestFilters = _appHost.GetExports<IRequestFilter>().ToList(); + foreach (var filter in requestFilters) + { + GlobalRequestFilters.Add(filter.Filter); + } + + GlobalResponseFilters.Add(new ResponseFilter(_logger).FilterResponse); + } + + protected override ILogger Logger + { + get + { + return _logger; + } + } + + public override T Resolve<T>() + { + return _appHost.Resolve<T>(); + } + + public override T TryResolve<T>() + { + return _appHost.TryResolve<T>(); + } + + public override object CreateInstance(Type type) + { + return _appHost.CreateInstance(type); + } + + protected override ServiceController CreateServiceController() + { + var types = _restServices.Select(r => r.GetType()).ToArray(); + + return new ServiceController(() => types); + } + + public override ServiceStackHost Start(string listeningAtUrlBase) + { + StartListener(); + return this; + } + + /// <summary> + /// Starts the Web Service + /// </summary> + private void StartListener() + { + WebSocketSharpRequest.HandlerFactoryPath = GetHandlerPathIfAny(UrlPrefixes.First()); + + _listener = GetListener(); + + _listener.WebSocketConnected = OnWebSocketConnected; + _listener.WebSocketConnecting = OnWebSocketConnecting; + _listener.ErrorHandler = ErrorHandler; + _listener.RequestHandler = RequestHandler; + + _listener.Start(UrlPrefixes); + } + + public 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('/'); + } + + private IHttpListener GetListener() + { + return new WebSocketSharpListener(_logger, + _certificate, + _memoryStreamProvider, + _textEncoding, + _networkManager, + _socketFactory, + _cryptoProvider, + _streamFactory, + _enableDualModeSockets, + GetRequest); + } + + private IHttpRequest GetRequest(HttpListenerContext httpContext) + { + var operationName = httpContext.Request.GetOperationName(); + + var req = new WebSocketSharpRequest(httpContext, operationName, _logger, _memoryStreamProvider); + + return req; + } + + private void OnWebSocketConnecting(WebSocketConnectingEventArgs args) + { + if (_disposed) + { + return; + } + + if (WebSocketConnecting != null) + { + WebSocketConnecting(this, args); + } + } + + private void OnWebSocketConnected(WebSocketConnectEventArgs args) + { + if (_disposed) + { + return; + } + + if (WebSocketConnected != null) + { + WebSocketConnected(this, args); + } + } + + private void ErrorHandler(Exception ex, IRequest httpReq) + { + try + { + _logger.ErrorException("Error processing request", ex); + + var httpRes = httpReq.Response; + + if (httpRes.IsClosed) + { + return; + } + + int statusCode; + if (!_mapExceptionToStatusCode.TryGetValue(ex.GetType(), out statusCode)) + { + statusCode = 500; + } + httpRes.StatusCode = statusCode; + + httpRes.ContentType = "text/html"; + Write(httpRes, ex.Message); + } + catch + { + //_logger.ErrorException("Error this.ProcessRequest(context)(Exception while writing error to the response)", errorEx); + } + } + + /// <summary> + /// Shut down the Web Service + /// </summary> + public void Stop() + { + if (_listener != null) + { + _listener.Stop(); + } + } + + private readonly Dictionary<string, int> _skipLogExtensions = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase) + { + {".js", 0}, + {".css", 0}, + {".woff", 0}, + {".woff2", 0}, + {".ttf", 0}, + {".html", 0} + }; + + private bool EnableLogging(string url, string localPath) + { + var extension = GetExtension(url); + + if (string.IsNullOrWhiteSpace(extension) || !_skipLogExtensions.ContainsKey(extension)) + { + if (string.IsNullOrWhiteSpace(localPath) || localPath.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) == -1) + { + return true; + } + } + + return false; + } + + private string GetExtension(string url) + { + var parts = url.Split(new[] { '?' }, 2); + + return Path.GetExtension(parts[0]); + } + + public static string RemoveQueryStringByKey(string url, string key) + { + var uri = new Uri(url); + + // this gets all the query string key value pairs as a collection + var newQueryString = MyHttpUtility.ParseQueryString(uri.Query); + + var originalCount = newQueryString.Count; + + if (originalCount == 0) + { + return url; + } + + // this removes the key if exists + newQueryString.Remove(key); + + if (originalCount == newQueryString.Count) + { + return url; + } + + // this gets the page path from root without QueryString + string pagePathWithoutQueryString = url.Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries)[0]; + + return newQueryString.Count > 0 + ? String.Format("{0}?{1}", pagePathWithoutQueryString, newQueryString) + : pagePathWithoutQueryString; + } + + private string GetUrlToLog(string url) + { + url = RemoveQueryStringByKey(url, "api_key"); + + return url; + } + + private string NormalizeConfiguredLocalAddress(string address) + { + var index = address.Trim('/').IndexOf('/'); + + if (index != -1) + { + address = address.Substring(index + 1); + } + + return address.Trim('/'); + } + + private bool ValidateHost(Uri url) + { + var hosts = _config + .Configuration + .LocalNetworkAddresses + .Select(NormalizeConfiguredLocalAddress) + .ToList(); + + if (hosts.Count == 0) + { + return true; + } + + var host = url.Host ?? string.Empty; + + _logger.Debug("Validating host {0}", host); + + if (_networkManager.IsInPrivateAddressSpace(host)) + { + hosts.Add("localhost"); + hosts.Add("127.0.0.1"); + + return hosts.Any(i => host.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1); + } + + return true; + } + + /// <summary> + /// Overridable method that can be used to implement a custom hnandler + /// </summary> + /// <param name="httpReq">The HTTP req.</param> + /// <param name="url">The URL.</param> + /// <returns>Task.</returns> + protected async Task RequestHandler(IHttpRequest httpReq, Uri url) + { + var date = DateTime.Now; + var httpRes = httpReq.Response; + bool enableLog = false; + string urlToLog = null; + string remoteIp = null; + + try + { + if (_disposed) + { + httpRes.StatusCode = 503; + httpRes.ContentType = "text/plain"; + Write(httpRes, "Server shutting down"); + return; + } + + if (!ValidateHost(url)) + { + httpRes.StatusCode = 400; + httpRes.ContentType = "text/plain"; + Write(httpRes, "Invalid host"); + return; + } + + if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase)) + { + httpRes.StatusCode = 200; + httpRes.AddHeader("Access-Control-Allow-Origin", "*"); + 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); + return; + } + + var operationName = httpReq.OperationName; + var localPath = url.LocalPath; + + var urlString = url.OriginalString; + enableLog = EnableLogging(urlString, localPath); + urlToLog = urlString; + + if (enableLog) + { + urlToLog = GetUrlToLog(urlString); + remoteIp = httpReq.RemoteIp; + + LoggerUtils.LogRequest(_logger, urlToLog, httpReq.HttpMethod, httpReq.UserAgent); + } + + if (string.Equals(localPath, "/emby/", StringComparison.OrdinalIgnoreCase) || + string.Equals(localPath, "/mediabrowser/", StringComparison.OrdinalIgnoreCase)) + { + RedirectToUrl(httpRes, DefaultRedirectPath); + return; + } + if (string.Equals(localPath, "/emby", StringComparison.OrdinalIgnoreCase) || + string.Equals(localPath, "/mediabrowser", StringComparison.OrdinalIgnoreCase)) + { + RedirectToUrl(httpRes, "emby/" + DefaultRedirectPath); + return; + } + + if (string.Equals(localPath, "/mediabrowser/", StringComparison.OrdinalIgnoreCase) || + string.Equals(localPath, "/mediabrowser", StringComparison.OrdinalIgnoreCase) || + localPath.IndexOf("mediabrowser/web", StringComparison.OrdinalIgnoreCase) != -1) + { + httpRes.StatusCode = 200; + httpRes.ContentType = "text/html"; + var newUrl = urlString.Replace("mediabrowser", "emby", StringComparison.OrdinalIgnoreCase) + .Replace("/dashboard/", "/web/", StringComparison.OrdinalIgnoreCase); + + if (!string.Equals(newUrl, urlString, StringComparison.OrdinalIgnoreCase)) + { + Write(httpRes, + "<!doctype html><html><head><title>Emby</title></head><body>Please update your Emby bookmark to <a href=\"" + + newUrl + "\">" + newUrl + "</a></body></html>"); + return; + } + } + + if (localPath.IndexOf("dashboard/", StringComparison.OrdinalIgnoreCase) != -1 && + localPath.IndexOf("web/dashboard", StringComparison.OrdinalIgnoreCase) == -1) + { + httpRes.StatusCode = 200; + httpRes.ContentType = "text/html"; + var newUrl = urlString.Replace("mediabrowser", "emby", StringComparison.OrdinalIgnoreCase) + .Replace("/dashboard/", "/web/", StringComparison.OrdinalIgnoreCase); + + if (!string.Equals(newUrl, urlString, StringComparison.OrdinalIgnoreCase)) + { + Write(httpRes, + "<!doctype html><html><head><title>Emby</title></head><body>Please update your Emby bookmark to <a href=\"" + + newUrl + "\">" + newUrl + "</a></body></html>"); + return; + } + } + + if (string.Equals(localPath, "/web", StringComparison.OrdinalIgnoreCase)) + { + RedirectToUrl(httpRes, DefaultRedirectPath); + return; + } + if (string.Equals(localPath, "/web/", StringComparison.OrdinalIgnoreCase)) + { + RedirectToUrl(httpRes, "../" + DefaultRedirectPath); + return; + } + if (string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase)) + { + RedirectToUrl(httpRes, DefaultRedirectPath); + return; + } + if (string.IsNullOrEmpty(localPath)) + { + RedirectToUrl(httpRes, "/" + DefaultRedirectPath); + return; + } + + if (string.Equals(localPath, "/emby/pin", StringComparison.OrdinalIgnoreCase)) + { + RedirectToUrl(httpRes, "web/pin.html"); + return; + } + + if (!string.IsNullOrWhiteSpace(GlobalResponse)) + { + httpRes.StatusCode = 503; + httpRes.ContentType = "text/html"; + Write(httpRes, GlobalResponse); + return; + } + + var handler = HttpHandlerFactory.GetHandler(httpReq, _logger); + + if (handler != null) + { + await handler.ProcessRequestAsync(httpReq, httpRes, operationName).ConfigureAwait(false); + } + else + { + ErrorHandler(new FileNotFoundException(), httpReq); + } + } + catch (Exception ex) + { + ErrorHandler(ex, httpReq); + } + finally + { + httpRes.Close(); + + if (enableLog) + { + var statusCode = httpRes.StatusCode; + + var duration = DateTime.Now - date; + + LoggerUtils.LogResponse(_logger, statusCode, urlToLog, remoteIp, duration); + } + } + } + + private void 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); + } + + public static void RedirectToUrl(IResponse httpRes, string url) + { + httpRes.StatusCode = 302; + httpRes.AddHeader("Location", url); + } + + + /// <summary> + /// Adds the rest handlers. + /// </summary> + /// <param name="services">The services.</param> + public void Init(IEnumerable<IService> services) + { + _restServices.AddRange(services); + + ServiceController = CreateServiceController(); + + _logger.Info("Calling ServiceStack AppHost.Init"); + + base.Init(); + } + + public override RouteAttribute[] GetRouteAttributes(Type requestType) + { + var routes = base.GetRouteAttributes(requestType).ToList(); + var clone = routes.ToList(); + + foreach (var route in clone) + { + routes.Add(new RouteAttribute(NormalizeEmbyRoutePath(route.Path), route.Verbs) + { + Notes = route.Notes, + Priority = route.Priority, + Summary = route.Summary + }); + + routes.Add(new RouteAttribute(NormalizeRoutePath(route.Path), route.Verbs) + { + Notes = route.Notes, + Priority = route.Priority, + Summary = route.Summary + }); + + routes.Add(new RouteAttribute(DoubleNormalizeEmbyRoutePath(route.Path), route.Verbs) + { + Notes = route.Notes, + Priority = route.Priority, + Summary = route.Summary + }); + } + + return routes.ToArray(); + } + + public override object GetTaskResult(Task task, string requestName) + { + try + { + var taskObject = task as Task<object>; + if (taskObject != null) + { + return taskObject.Result; + } + + task.Wait(); + + var type = task.GetType().GetTypeInfo(); + if (!type.IsGenericType) + { + return null; + } + + Logger.Warn("Getting task result from " + requestName + " using reflection. For better performance have your api return Task<object>"); + return type.GetDeclaredProperty("Result").GetValue(task); + } + catch (TypeAccessException) + { + return null; //return null for void Task's + } + } + + public override Func<string, object> GetParseFn(Type propertyType) + { + return _funcParseFn(propertyType); + } + + public override void SerializeToJson(object o, Stream stream) + { + _jsonSerializer.SerializeToStream(o, stream); + } + + public override void SerializeToXml(object o, Stream stream) + { + _xmlSerializer.SerializeToStream(o, stream); + } + + public override object DeserializeXml(Type type, Stream stream) + { + return _xmlSerializer.DeserializeFromStream(type, stream); + } + + public override object DeserializeJson(Type type, Stream stream) + { + return _jsonSerializer.DeserializeFromStream(stream, type); + } + + private string NormalizeEmbyRoutePath(string path) + { + if (path.StartsWith("/", StringComparison.OrdinalIgnoreCase)) + { + return "/emby" + path; + } + + return "emby/" + path; + } + + private string DoubleNormalizeEmbyRoutePath(string path) + { + if (path.StartsWith("/", StringComparison.OrdinalIgnoreCase)) + { + return "/emby/emby" + path; + } + + return "emby/emby/" + path; + } + + private string NormalizeRoutePath(string path) + { + if (path.StartsWith("/", StringComparison.OrdinalIgnoreCase)) + { + return "/mediabrowser" + path; + } + + return "mediabrowser/" + path; + } + + private bool _disposed; + private readonly object _disposeLock = new object(); + protected virtual void Dispose(bool disposing) + { + if (_disposed) return; + base.Dispose(); + + lock (_disposeLock) + { + if (_disposed) return; + + if (disposing) + { + Stop(); + } + + //release unmanaged resources here... + _disposed = true; + } + } + + public override void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public void StartServer(IEnumerable<string> urlPrefixes) + { + UrlPrefixes = urlPrefixes.ToList(); + Start(UrlPrefixes.First()); + } + } +}
\ No newline at end of file diff --git a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs new file mode 100644 index 000000000..995dc7b7b --- /dev/null +++ b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs @@ -0,0 +1,830 @@ +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.IO.Compression; +using System.Net; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using Emby.Server.Implementations.HttpServer; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Services; +using ServiceStack; +using ServiceStack.Host; +using IRequest = MediaBrowser.Model.Services.IRequest; +using MimeTypes = MediaBrowser.Model.Net.MimeTypes; +using StreamWriter = Emby.Server.Implementations.HttpServer.StreamWriter; + +namespace Emby.Server.Implementations.HttpServer +{ + /// <summary> + /// Class HttpResultFactory + /// </summary> + public class HttpResultFactory : IHttpResultFactory + { + /// <summary> + /// The _logger + /// </summary> + private readonly ILogger _logger; + private readonly IFileSystem _fileSystem; + private readonly IJsonSerializer _jsonSerializer; + private readonly IMemoryStreamFactory _memoryStreamFactory; + + /// <summary> + /// Initializes a new instance of the <see cref="HttpResultFactory" /> class. + /// </summary> + public HttpResultFactory(ILogManager logManager, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IMemoryStreamFactory memoryStreamFactory) + { + _fileSystem = fileSystem; + _jsonSerializer = jsonSerializer; + _memoryStreamFactory = memoryStreamFactory; + _logger = logManager.GetLogger("HttpResultFactory"); + } + + /// <summary> + /// Gets the result. + /// </summary> + /// <param name="content">The content.</param> + /// <param name="contentType">Type of the content.</param> + /// <param name="responseHeaders">The response headers.</param> + /// <returns>System.Object.</returns> + public object GetResult(object content, string contentType, IDictionary<string, string> responseHeaders = null) + { + return GetHttpResult(content, contentType, true, responseHeaders); + } + + /// <summary> + /// Gets the HTTP result. + /// </summary> + private IHasHeaders GetHttpResult(object content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null) + { + IHasHeaders result; + + var stream = content as Stream; + + if (stream != null) + { + result = new StreamWriter(stream, contentType, _logger); + } + + else + { + var bytes = content as byte[]; + + if (bytes != null) + { + result = new StreamWriter(bytes, contentType, _logger); + } + else + { + var text = content as string; + + if (text != null) + { + result = new StreamWriter(Encoding.UTF8.GetBytes(text), contentType, _logger); + } + else + { + result = new HttpResult(content, contentType, HttpStatusCode.OK); + } + } + } + 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 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) + 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"); + } + + var optimizedResult = ToOptimizedResult(requestContext, result); + + if (responseHeaders == null) + { + responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + } + + if (addCachePrevention) + { + responseHeaders["Expires"] = "-1"; + } + + // Apply headers + var hasHeaders = optimizedResult as IHasHeaders; + + if (hasHeaders != null) + { + AddResponseHeaders(hasHeaders, responseHeaders); + } + + return optimizedResult; + } + + public static string GetCompressionType(IRequest request) + { + var acceptEncoding = request.Headers["Accept-Encoding"]; + + if (!string.IsNullOrWhiteSpace(acceptEncoding)) + { + if (acceptEncoding.Contains("deflate")) + return "deflate"; + + if (acceptEncoding.Contains("gzip")) + return "gzip"; + } + + return null; + } + + /// <summary> + /// Returns the optimized result for the IRequestContext. + /// Does not use or store results in any cache. + /// </summary> + /// <param name="request"></param> + /// <param name="dto"></param> + /// <returns></returns> + public object ToOptimizedResult<T>(IRequest request, T dto) + { + var compressionType = GetCompressionType(request); + if (compressionType == null) + { + var contentType = request.ResponseContentType; + + switch (GetRealContentType(contentType)) + { + case "application/xml": + case "text/xml": + case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml + return SerializeToXmlString(dto); + + case "application/json": + case "text/json": + return _jsonSerializer.SerializeToString(dto); + } + } + + // Do not use the memoryStreamFactory here, they don't place nice with compression + using (var ms = new MemoryStream()) + { + using (var compressionStream = GetCompressionStream(ms, compressionType)) + { + ContentTypes.Instance.SerializeToStream(request, dto, compressionStream); + compressionStream.Dispose(); + + var compressedBytes = ms.ToArray(); + + var httpResult = new StreamWriter(compressedBytes, request.ResponseContentType, _logger); + + //httpResult.Headers["Content-Length"] = compressedBytes.Length.ToString(UsCulture); + httpResult.Headers["Content-Encoding"] = compressionType; + + return httpResult; + } + } + } + + private static Stream GetCompressionStream(Stream outputStream, string compressionType) + { + if (compressionType == "deflate") + return new DeflateStream(outputStream, CompressionMode.Compress, true); + if (compressionType == "gzip") + return new GZipStream(outputStream, CompressionMode.Compress, true); + + throw new NotSupportedException(compressionType); + } + + 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()) + { + var xwSettings = new XmlWriterSettings(); + xwSettings.Encoding = new UTF8Encoding(false); + xwSettings.OmitXmlDeclaration = false; + + using (var xw = XmlWriter.Create(ms, xwSettings)) + { + var serializer = new DataContractSerializer(from.GetType()); + serializer.WriteObject(xw, from); + xw.Flush(); + ms.Seek(0, SeekOrigin.Begin); + var reader = new StreamReader(ms); + return reader.ReadToEnd(); + } + } + } + + /// <summary> + /// Gets the optimized result using cache. + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="requestContext">The request context.</param> + /// <param name="cacheKey">The cache key.</param> + /// <param name="lastDateModified">The last date modified.</param> + /// <param name="cacheDuration">Duration of the cache.</param> + /// <param name="factoryFn">The factory fn.</param> + /// <param name="responseHeaders">The response headers.</param> + /// <returns>System.Object.</returns> + /// <exception cref="System.ArgumentNullException">cacheKey + /// or + /// factoryFn</exception> + public object GetOptimizedResultUsingCache<T>(IRequest requestContext, Guid cacheKey, DateTime? lastDateModified, TimeSpan? cacheDuration, Func<T> factoryFn, IDictionary<string, string> responseHeaders = null) + where T : class + { + if (cacheKey == Guid.Empty) + { + throw new ArgumentNullException("cacheKey"); + } + if (factoryFn == null) + { + throw new ArgumentNullException("factoryFn"); + } + + var key = cacheKey.ToString("N"); + + if (responseHeaders == null) + { + responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + } + + // See if the result is already cached in the browser + var result = GetCachedResult(requestContext, responseHeaders, cacheKey, key, lastDateModified, cacheDuration, null); + + if (result != null) + { + return result; + } + + return GetOptimizedResultInternal(requestContext, factoryFn(), false, responseHeaders); + } + + /// <summary> + /// To the cached result. + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="requestContext">The request context.</param> + /// <param name="cacheKey">The cache key.</param> + /// <param name="lastDateModified">The last date modified.</param> + /// <param name="cacheDuration">Duration of the cache.</param> + /// <param name="factoryFn">The factory fn.</param> + /// <param name="contentType">Type of the content.</param> + /// <param name="responseHeaders">The response headers.</param> + /// <returns>System.Object.</returns> + /// <exception cref="System.ArgumentNullException">cacheKey</exception> + public object GetCachedResult<T>(IRequest requestContext, Guid cacheKey, DateTime? lastDateModified, TimeSpan? cacheDuration, Func<T> factoryFn, string contentType, IDictionary<string, string> responseHeaders = null) + where T : class + { + if (cacheKey == Guid.Empty) + { + throw new ArgumentNullException("cacheKey"); + } + if (factoryFn == null) + { + throw new ArgumentNullException("factoryFn"); + } + + var key = cacheKey.ToString("N"); + + if (responseHeaders == null) + { + responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + } + + // See if the result is already cached in the browser + var result = GetCachedResult(requestContext, responseHeaders, cacheKey, key, lastDateModified, cacheDuration, contentType); + + if (result != null) + { + return result; + } + + result = factoryFn(); + + // Apply caching headers + var hasHeaders = result as IHasHeaders; + + if (hasHeaders != null) + { + AddResponseHeaders(hasHeaders, responseHeaders); + return hasHeaders; + } + + return GetHttpResult(result, contentType, false, responseHeaders); + } + + /// <summary> + /// Pres the process optimized result. + /// </summary> + /// <param name="requestContext">The request context.</param> + /// <param name="responseHeaders">The responseHeaders.</param> + /// <param name="cacheKey">The cache key.</param> + /// <param name="cacheKeyString">The cache key string.</param> + /// <param name="lastDateModified">The last date modified.</param> + /// <param name="cacheDuration">Duration of the cache.</param> + /// <param name="contentType">Type of the content.</param> + /// <returns>System.Object.</returns> + private object GetCachedResult(IRequest requestContext, IDictionary<string, string> responseHeaders, Guid cacheKey, string cacheKeyString, DateTime? lastDateModified, TimeSpan? cacheDuration, string contentType) + { + responseHeaders["ETag"] = string.Format("\"{0}\"", cacheKeyString); + + if (IsNotModified(requestContext, cacheKey, lastDateModified, cacheDuration)) + { + AddAgeHeader(responseHeaders, lastDateModified); + AddExpiresHeader(responseHeaders, cacheKeyString, cacheDuration); + + var result = new HttpResult(new byte[] { }, contentType ?? "text/html", HttpStatusCode.NotModified); + + AddResponseHeaders(result, responseHeaders); + + return result; + } + + AddCachingHeaders(responseHeaders, cacheKeyString, lastDateModified, cacheDuration); + + return null; + } + + public Task<object> GetStaticFileResult(IRequest requestContext, + string path, + FileShareMode fileShare = FileShareMode.Read) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException("path"); + } + + return GetStaticFileResult(requestContext, new StaticFileResultOptions + { + Path = path, + FileShare = fileShare + }); + } + + public Task<object> GetStaticFileResult(IRequest requestContext, + StaticFileResultOptions options) + { + var path = options.Path; + var fileShare = options.FileShare; + + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException("path"); + } + + if (fileShare != FileShareMode.Read && fileShare != FileShareMode.ReadWrite) + { + throw new ArgumentException("FileShare must be either Read or ReadWrite"); + } + + if (string.IsNullOrWhiteSpace(options.ContentType)) + { + options.ContentType = MimeTypes.GetMimeType(path); + } + + if (!options.DateLastModified.HasValue) + { + options.DateLastModified = _fileSystem.GetLastWriteTimeUtc(path); + } + + var cacheKey = path + options.DateLastModified.Value.Ticks; + + options.CacheKey = cacheKey.GetMD5(); + options.ContentFactory = () => Task.FromResult(GetFileStream(path, fileShare)); + + return GetStaticResult(requestContext, options); + } + + /// <summary> + /// Gets the file stream. + /// </summary> + /// <param name="path">The path.</param> + /// <param name="fileShare">The file share.</param> + /// <returns>Stream.</returns> + private Stream GetFileStream(string path, FileShareMode fileShare) + { + return _fileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, fileShare); + } + + public Task<object> GetStaticResult(IRequest requestContext, + Guid cacheKey, + DateTime? lastDateModified, + TimeSpan? cacheDuration, + string contentType, + Func<Task<Stream>> factoryFn, + IDictionary<string, string> responseHeaders = null, + bool isHeadRequest = false) + { + return GetStaticResult(requestContext, new StaticResultOptions + { + CacheDuration = cacheDuration, + CacheKey = cacheKey, + ContentFactory = factoryFn, + ContentType = contentType, + DateLastModified = lastDateModified, + IsHeadRequest = isHeadRequest, + ResponseHeaders = responseHeaders + }); + } + + public async Task<object> GetStaticResult(IRequest requestContext, StaticResultOptions options) + { + var cacheKey = options.CacheKey; + options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + var contentType = options.ContentType; + + if (cacheKey == Guid.Empty) + { + throw new ArgumentNullException("cacheKey"); + } + if (options.ContentFactory == null) + { + throw new ArgumentNullException("factoryFn"); + } + + 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); + + if (result != null) + { + return result; + } + + var compress = ShouldCompressResponse(requestContext, contentType); + var hasHeaders = await GetStaticResult(requestContext, options, compress).ConfigureAwait(false); + AddResponseHeaders(hasHeaders, options.ResponseHeaders); + + return hasHeaders; + } + + /// <summary> + /// Shoulds the compress response. + /// </summary> + /// <param name="requestContext">The request context.</param> + /// <param name="contentType">Type of the content.</param> + /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> + private bool ShouldCompressResponse(IRequest requestContext, string contentType) + { + // It will take some work to support compression with byte range requests + if (!string.IsNullOrEmpty(requestContext.Headers.Get("Range"))) + { + return false; + } + + // Don't compress media + if (contentType.StartsWith("audio/", StringComparison.OrdinalIgnoreCase) || contentType.StartsWith("video/", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + // Don't compress images + if (contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (contentType.StartsWith("font/", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + if (contentType.StartsWith("application/", StringComparison.OrdinalIgnoreCase)) + { + if (string.Equals(contentType, "application/x-javascript", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + if (string.Equals(contentType, "application/xml", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + return false; + } + + return true; + } + + /// <summary> + /// The us culture + /// </summary> + private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); + + private async Task<IHasHeaders> GetStaticResult(IRequest requestContext, StaticResultOptions options, bool compress) + { + var isHeadRequest = options.IsHeadRequest; + var factoryFn = options.ContentFactory; + var contentType = options.ContentType; + var responseHeaders = options.ResponseHeaders; + + var requestedCompressionType = GetCompressionType(requestContext); + + if (!compress || string.IsNullOrEmpty(requestedCompressionType)) + { + var rangeHeader = requestContext.Headers.Get("Range"); + + var stream = await factoryFn().ConfigureAwait(false); + + if (!string.IsNullOrEmpty(rangeHeader)) + { + return new RangeRequestWriter(rangeHeader, stream, contentType, isHeadRequest, _logger) + { + OnComplete = options.OnComplete + }; + } + + responseHeaders["Content-Length"] = stream.Length.ToString(UsCulture); + + if (isHeadRequest) + { + stream.Dispose(); + + return GetHttpResult(new byte[] { }, contentType, true); + } + + return new StreamWriter(stream, contentType, _logger) + { + OnComplete = options.OnComplete, + OnError = options.OnError + }; + } + + string content; + + using (var stream = await factoryFn().ConfigureAwait(false)) + { + using (var reader = new StreamReader(stream)) + { + content = await reader.ReadToEndAsync().ConfigureAwait(false); + } + } + + var contents = Compress(content, requestedCompressionType); + + responseHeaders["Content-Length"] = contents.Length.ToString(UsCulture); + responseHeaders["Content-Encoding"] = requestedCompressionType; + + if (isHeadRequest) + { + return GetHttpResult(new byte[] { }, contentType, true); + } + + return GetHttpResult(contents, contentType, true, responseHeaders); + } + + private byte[] Compress(string text, string compressionType) + { + if (compressionType == "deflate") + return Deflate(text); + + if (compressionType == "gzip") + return GZip(text); + + throw new NotSupportedException(compressionType); + } + + private byte[] Deflate(string text) + { + return Deflate(Encoding.UTF8.GetBytes(text)); + } + + private byte[] Deflate(byte[] bytes) + { + // In .NET FX incompat-ville, you can't access compressed bytes without closing DeflateStream + // Which means we must use MemoryStream since you have to use ToArray() on a closed Stream + using (var ms = new MemoryStream()) + using (var zipStream = new DeflateStream(ms, CompressionMode.Compress)) + { + zipStream.Write(bytes, 0, bytes.Length); + zipStream.Dispose(); + + return ms.ToArray(); + } + } + + private byte[] GZip(string text) + { + return GZip(Encoding.UTF8.GetBytes(text)); + } + + private byte[] GZip(byte[] buffer) + { + using (var ms = new MemoryStream()) + using (var zipStream = new GZipStream(ms, CompressionMode.Compress)) + { + zipStream.Write(buffer, 0, buffer.Length); + zipStream.Dispose(); + + return ms.ToArray(); + } + } + + /// <summary> + /// Adds the caching responseHeaders. + /// </summary> + /// <param name="responseHeaders">The responseHeaders.</param> + /// <param name="cacheKey">The cache key.</param> + /// <param name="lastDateModified">The last date modified.</param> + /// <param name="cacheDuration">Duration of the cache.</param> + private void AddCachingHeaders(IDictionary<string, string> responseHeaders, string cacheKey, DateTime? lastDateModified, TimeSpan? cacheDuration) + { + // Don't specify both last modified and Etag, unless caching unconditionally. They are redundant + // https://developers.google.com/speed/docs/best-practices/caching#LeverageBrowserCaching + if (lastDateModified.HasValue && (string.IsNullOrEmpty(cacheKey) || cacheDuration.HasValue)) + { + AddAgeHeader(responseHeaders, lastDateModified); + responseHeaders["Last-Modified"] = lastDateModified.Value.ToString("r"); + } + + if (cacheDuration.HasValue) + { + responseHeaders["Cache-Control"] = "public, max-age=" + Convert.ToInt32(cacheDuration.Value.TotalSeconds); + } + else if (!string.IsNullOrEmpty(cacheKey)) + { + responseHeaders["Cache-Control"] = "public"; + } + else + { + responseHeaders["Cache-Control"] = "no-cache, no-store, must-revalidate"; + responseHeaders["pragma"] = "no-cache, no-store, must-revalidate"; + } + + AddExpiresHeader(responseHeaders, cacheKey, cacheDuration); + } + + /// <summary> + /// Adds the expires header. + /// </summary> + /// <param name="responseHeaders">The responseHeaders.</param> + /// <param name="cacheKey">The cache key.</param> + /// <param name="cacheDuration">Duration of the cache.</param> + private void AddExpiresHeader(IDictionary<string, string> responseHeaders, string cacheKey, TimeSpan? cacheDuration) + { + if (cacheDuration.HasValue) + { + responseHeaders["Expires"] = DateTime.UtcNow.Add(cacheDuration.Value).ToString("r"); + } + else if (string.IsNullOrEmpty(cacheKey)) + { + responseHeaders["Expires"] = "-1"; + } + } + + /// <summary> + /// Adds the age header. + /// </summary> + /// <param name="responseHeaders">The responseHeaders.</param> + /// <param name="lastDateModified">The last date modified.</param> + private void AddAgeHeader(IDictionary<string, string> responseHeaders, DateTime? lastDateModified) + { + if (lastDateModified.HasValue) + { + responseHeaders["Age"] = Convert.ToInt64((DateTime.UtcNow - lastDateModified.Value).TotalSeconds).ToString(CultureInfo.InvariantCulture); + } + } + /// <summary> + /// Determines whether [is not modified] [the specified cache key]. + /// </summary> + /// <param name="requestContext">The request context.</param> + /// <param name="cacheKey">The cache key.</param> + /// <param name="lastDateModified">The last date modified.</param> + /// <param name="cacheDuration">Duration of the cache.</param> + /// <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) + { + //var isNotModified = true; + + var ifModifiedSinceHeader = requestContext.Headers.Get("If-Modified-Since"); + + if (!string.IsNullOrEmpty(ifModifiedSinceHeader)) + { + DateTime ifModifiedSince; + + if (DateTime.TryParse(ifModifiedSinceHeader, out ifModifiedSince)) + { + if (IsNotModified(ifModifiedSince.ToUniversalTime(), cacheDuration, lastDateModified)) + { + return true; + } + } + } + + var ifNoneMatchHeader = requestContext.Headers.Get("If-None-Match"); + + // Validate If-None-Match + if ((cacheKey.HasValue || !string.IsNullOrEmpty(ifNoneMatchHeader))) + { + Guid ifNoneMatch; + + ifNoneMatchHeader = (ifNoneMatchHeader ?? string.Empty).Trim('\"'); + + if (Guid.TryParse(ifNoneMatchHeader, out ifNoneMatch)) + { + if (cacheKey.HasValue && cacheKey.Value == ifNoneMatch) + { + return true; + } + } + } + + return false; + } + + /// <summary> + /// Determines whether [is not modified] [the specified if modified since]. + /// </summary> + /// <param name="ifModifiedSince">If modified since.</param> + /// <param name="cacheDuration">Duration of the cache.</param> + /// <param name="dateModified">The date modified.</param> + /// <returns><c>true</c> if [is not modified] [the specified if modified since]; otherwise, <c>false</c>.</returns> + private bool IsNotModified(DateTime ifModifiedSince, TimeSpan? cacheDuration, DateTime? dateModified) + { + if (dateModified.HasValue) + { + var lastModified = NormalizeDateForComparison(dateModified.Value); + ifModifiedSince = NormalizeDateForComparison(ifModifiedSince); + + return lastModified <= ifModifiedSince; + } + + if (cacheDuration.HasValue) + { + var cacheExpirationDate = ifModifiedSince.Add(cacheDuration.Value); + + if (DateTime.UtcNow < cacheExpirationDate) + { + return true; + } + } + + return false; + } + + + /// <summary> + /// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that + /// </summary> + /// <param name="date">The date.</param> + /// <returns>DateTime.</returns> + private DateTime NormalizeDateForComparison(DateTime date) + { + return new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second, date.Kind); + } + + /// <summary> + /// Adds the response headers. + /// </summary> + /// <param name="hasHeaders">The has options.</param> + /// <param name="responseHeaders">The response headers.</param> + private void AddResponseHeaders(IHasHeaders hasHeaders, IEnumerable<KeyValuePair<string, string>> responseHeaders) + { + foreach (var item in responseHeaders) + { + hasHeaders.Headers[item.Key] = item.Value; + } + } + } +}
\ No newline at end of file diff --git a/Emby.Server.Implementations/HttpServer/IHttpListener.cs b/Emby.Server.Implementations/HttpServer/IHttpListener.cs new file mode 100644 index 000000000..9f96a8e49 --- /dev/null +++ b/Emby.Server.Implementations/HttpServer/IHttpListener.cs @@ -0,0 +1,46 @@ +using MediaBrowser.Controller.Net; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MediaBrowser.Model.Services; + +namespace Emby.Server.Implementations.HttpServer +{ + public interface IHttpListener : IDisposable + { + /// <summary> + /// Gets or sets the error handler. + /// </summary> + /// <value>The error handler.</value> + Action<Exception, IRequest> ErrorHandler { get; set; } + + /// <summary> + /// Gets or sets the request handler. + /// </summary> + /// <value>The request handler.</value> + Func<IHttpRequest, Uri, Task> RequestHandler { get; set; } + + /// <summary> + /// Gets or sets the web socket handler. + /// </summary> + /// <value>The web socket handler.</value> + Action<WebSocketConnectEventArgs> WebSocketConnected { get; set; } + + /// <summary> + /// Gets or sets the web socket connecting. + /// </summary> + /// <value>The web socket connecting.</value> + Action<WebSocketConnectingEventArgs> WebSocketConnecting { get; set; } + + /// <summary> + /// Starts this instance. + /// </summary> + /// <param name="urlPrefixes">The URL prefixes.</param> + void Start(IEnumerable<string> urlPrefixes); + + /// <summary> + /// Stops this instance. + /// </summary> + void Stop(); + } +} diff --git a/Emby.Server.Implementations/HttpServer/LoggerUtils.cs b/Emby.Server.Implementations/HttpServer/LoggerUtils.cs new file mode 100644 index 000000000..8fc92a09a --- /dev/null +++ b/Emby.Server.Implementations/HttpServer/LoggerUtils.cs @@ -0,0 +1,43 @@ +using MediaBrowser.Model.Logging; +using System; +using System.Globalization; +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) + { + logger.Info("{0} {1}. UserAgent: {2}", "HTTP " + method, url, userAgent ?? string.Empty); + } + + /// <summary> + /// Logs the response. + /// </summary> + /// <param name="logger">The logger.</param> + /// <param name="statusCode">The status code.</param> + /// <param name="url">The URL.</param> + /// <param name="endPoint">The end point.</param> + /// <param name="duration">The duration.</param> + public static void LogResponse(ILogger logger, int statusCode, string url, string endPoint, TimeSpan duration) + { + var durationMs = duration.TotalMilliseconds; + var logSuffix = durationMs >= 1000 && durationMs < 60000 ? "ms (slow)" : "ms"; + + logger.Info("HTTP Response {0} to {1}. Time: {2}{3}. {4}", statusCode, endPoint, Convert.ToInt32(durationMs).ToString(CultureInfo.InvariantCulture), logSuffix, url); + } + } +} diff --git a/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs b/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs new file mode 100644 index 000000000..e88994bec --- /dev/null +++ b/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs @@ -0,0 +1,224 @@ +using MediaBrowser.Model.Logging; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.Services; + +namespace Emby.Server.Implementations.HttpServer +{ + public class RangeRequestWriter : IAsyncStreamWriter, IHttpResult + { + /// <summary> + /// Gets or sets the source stream. + /// </summary> + /// <value>The source stream.</value> + private Stream SourceStream { get; set; } + private string RangeHeader { get; set; } + private bool IsHeadRequest { get; set; } + + private long RangeStart { get; set; } + private long RangeEnd { get; set; } + private long RangeLength { get; set; } + private long TotalContentLength { get; set; } + + public Action OnComplete { get; set; } + private readonly ILogger _logger; + + private const int BufferSize = 81920; + + /// <summary> + /// The _options + /// </summary> + private readonly Dictionary<string, string> _options = new Dictionary<string, string>(); + + /// <summary> + /// The us culture + /// </summary> + private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); + + public List<Cookie> Cookies { get; private set; } + + /// <summary> + /// Additional HTTP Headers + /// </summary> + /// <value>The headers.</value> + public IDictionary<string, string> Headers + { + get { return _options; } + } + + /// <summary> + /// Initializes a new instance of the <see cref="StreamWriter" /> class. + /// </summary> + /// <param name="rangeHeader">The range header.</param> + /// <param name="source">The source.</param> + /// <param name="contentType">Type of the content.</param> + /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param> + public RangeRequestWriter(string rangeHeader, Stream source, string contentType, bool isHeadRequest, ILogger logger) + { + if (string.IsNullOrEmpty(contentType)) + { + throw new ArgumentNullException("contentType"); + } + + RangeHeader = rangeHeader; + SourceStream = source; + IsHeadRequest = isHeadRequest; + this._logger = logger; + + ContentType = contentType; + Headers["Content-Type"] = contentType; + Headers["Accept-Ranges"] = "bytes"; + StatusCode = HttpStatusCode.PartialContent; + + Cookies = new List<Cookie>(); + SetRangeValues(); + } + + /// <summary> + /// Sets the range values. + /// </summary> + private void SetRangeValues() + { + var requestedRange = RequestedRanges[0]; + + TotalContentLength = SourceStream.Length; + + // If the requested range is "0-", we can optimize by just doing a stream copy + if (!requestedRange.Value.HasValue) + { + RangeEnd = TotalContentLength - 1; + } + else + { + RangeEnd = requestedRange.Value.Value; + } + + RangeStart = requestedRange.Key; + RangeLength = 1 + RangeEnd - RangeStart; + + // Content-Length is the length of what we're serving, not the original content + Headers["Content-Length"] = RangeLength.ToString(UsCulture); + Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", RangeStart, RangeEnd, TotalContentLength); + + if (RangeStart > 0) + { + SourceStream.Position = RangeStart; + } + } + + /// <summary> + /// The _requested ranges + /// </summary> + private List<KeyValuePair<long, long?>> _requestedRanges; + /// <summary> + /// Gets the requested ranges. + /// </summary> + /// <value>The requested ranges.</value> + protected List<KeyValuePair<long, long?>> RequestedRanges + { + get + { + if (_requestedRanges == null) + { + _requestedRanges = new List<KeyValuePair<long, long?>>(); + + // Example: bytes=0-,32-63 + var ranges = RangeHeader.Split('=')[1].Split(','); + + foreach (var range in ranges) + { + var vals = range.Split('-'); + + long start = 0; + long? end = null; + + if (!string.IsNullOrEmpty(vals[0])) + { + start = long.Parse(vals[0], UsCulture); + } + if (!string.IsNullOrEmpty(vals[1])) + { + end = long.Parse(vals[1], UsCulture); + } + + _requestedRanges.Add(new KeyValuePair<long, long?>(start, end)); + } + } + + return _requestedRanges; + } + } + + public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken) + { + try + { + // Headers only + if (IsHeadRequest) + { + return; + } + + using (var source = SourceStream) + { + // If the requested range is "0-", we can optimize by just doing a stream copy + if (RangeEnd >= TotalContentLength - 1) + { + await source.CopyToAsync(responseStream, BufferSize).ConfigureAwait(false); + } + else + { + await CopyToInternalAsync(source, responseStream, RangeLength).ConfigureAwait(false); + } + } + } + finally + { + if (OnComplete != null) + { + OnComplete(); + } + } + } + + private async Task CopyToInternalAsync(Stream source, Stream destination, long copyLength) + { + var array = new byte[BufferSize]; + int count; + while ((count = await source.ReadAsync(array, 0, array.Length).ConfigureAwait(false)) != 0) + { + var bytesToCopy = Math.Min(count, copyLength); + + await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToCopy)).ConfigureAwait(false); + + copyLength -= bytesToCopy; + + if (copyLength <= 0) + { + break; + } + } + } + + public string ContentType { get; set; } + + public IRequest RequestContext { get; set; } + + public object Response { get; set; } + + public int Status { get; set; } + + public HttpStatusCode StatusCode + { + get { return (HttpStatusCode)Status; } + set { Status = (int)value; } + } + + public string StatusDescription { get; set; } + } +}
\ No newline at end of file diff --git a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs new file mode 100644 index 000000000..6d9d7d921 --- /dev/null +++ b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs @@ -0,0 +1,129 @@ +using MediaBrowser.Model.Logging; +using System; +using System.Globalization; +using System.Text; +using Emby.Server.Implementations.HttpServer.SocketSharp; +using MediaBrowser.Model.Services; + +namespace Emby.Server.Implementations.HttpServer +{ + public class ResponseFilter + { + private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); + private readonly ILogger _logger; + + public ResponseFilter(ILogger logger) + { + _logger = logger; + } + + /// <summary> + /// Filters the response. + /// </summary> + /// <param name="req">The req.</param> + /// <param name="res">The res.</param> + /// <param name="dto">The dto.</param> + public void FilterResponse(IRequest req, IResponse res, object dto) + { + // Try to prevent compatibility view + res.AddHeader("X-UA-Compatible", "IE=Edge"); + res.AddHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization"); + res.AddHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS"); + res.AddHeader("Access-Control-Allow-Origin", "*"); + + var exception = dto as Exception; + + if (exception != null) + { + _logger.ErrorException("Error processing request for {0}", exception, req.RawUrl); + + if (!string.IsNullOrEmpty(exception.Message)) + { + var error = exception.Message.Replace(Environment.NewLine, " "); + error = RemoveControlCharacters(error); + + res.AddHeader("X-Application-Error-Code", error); + } + } + + var vary = "Accept-Encoding"; + + var hasHeaders = dto as IHasHeaders; + var sharpResponse = res as WebSocketSharpResponse; + + if (hasHeaders != null) + { + if (!hasHeaders.Headers.ContainsKey("Server")) + { + hasHeaders.Headers["Server"] = "Mono-HTTPAPI/1.1, UPnP/1.0 DLNADOC/1.50"; + //hasHeaders.Headers["Server"] = "Mono-HTTPAPI/1.1"; + } + + // Content length has to be explicitly set on on HttpListenerResponse or it won't be happy + string contentLength; + + if (hasHeaders.Headers.TryGetValue("Content-Length", out contentLength) && !string.IsNullOrEmpty(contentLength)) + { + var length = long.Parse(contentLength, UsCulture); + + if (length > 0) + { + res.SetContentLength(length); + + //var listenerResponse = res.OriginalResponse as HttpListenerResponse; + + //if (listenerResponse != null) + //{ + // // Disable chunked encoding. Technically this is only needed when using Content-Range, but + // // anytime we know the content length there's no need for it + // listenerResponse.SendChunked = false; + // return; + //} + + if (sharpResponse != null) + { + sharpResponse.SendChunked = false; + } + } + } + + string hasHeadersVary; + if (hasHeaders.Headers.TryGetValue("Vary", out hasHeadersVary)) + { + vary = hasHeadersVary; + } + + hasHeaders.Headers["Vary"] = vary; + } + + //res.KeepAlive = false; + + // Per Google PageSpeed + // This instructs the proxies to cache two versions of the resource: one compressed, and one uncompressed. + // The correct version of the resource is delivered based on the client request header. + // This is a good choice for applications that are singly homed and depend on public proxies for user locality. + res.AddHeader("Vary", vary); + } + + /// <summary> + /// Removes the control characters. + /// </summary> + /// <param name="inString">The in string.</param> + /// <returns>System.String.</returns> + public static string RemoveControlCharacters(string inString) + { + if (inString == null) return null; + + var newString = new StringBuilder(); + + foreach (var ch in inString) + { + if (!char.IsControl(ch)) + { + newString.Append(ch); + } + } + return newString.ToString(); + } + } +} diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs new file mode 100644 index 000000000..4d00c9b19 --- /dev/null +++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs @@ -0,0 +1,246 @@ +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Connect; +using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Security; +using MediaBrowser.Controller.Session; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Emby.Server.Implementations.HttpServer.Security +{ + public class AuthService : IAuthService + { + private readonly IServerConfigurationManager _config; + + public AuthService(IUserManager userManager, IAuthorizationContext authorizationContext, IServerConfigurationManager config, IConnectManager connectManager, ISessionManager sessionManager, IDeviceManager deviceManager) + { + AuthorizationContext = authorizationContext; + _config = config; + DeviceManager = deviceManager; + SessionManager = sessionManager; + ConnectManager = connectManager; + UserManager = userManager; + } + + 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; } + + /// <summary> + /// Redirect the client to a specific URL if authentication failed. + /// If this property is null, simply `401 Unauthorized` is returned. + /// </summary> + public string HtmlRedirect { get; set; } + + public void Authenticate(IServiceRequest request, + IAuthenticationAttributes authAttribtues) + { + ValidateUser(request, authAttribtues); + } + + private void ValidateUser(IServiceRequest request, + IAuthenticationAttributes authAttribtues) + { + // This code is executed before the service + var auth = AuthorizationContext.GetAuthorizationInfo(request); + + if (!IsExemptFromAuthenticationToken(auth, authAttribtues)) + { + var valid = IsValidConnectKey(auth.Token); + + if (!valid) + { + ValidateSecurityToken(request, auth.Token); + } + } + + var user = string.IsNullOrWhiteSpace(auth.UserId) + ? null + : UserManager.GetUserById(auth.UserId); + + if (user == null & !string.IsNullOrWhiteSpace(auth.UserId)) + { + throw new SecurityException("User with Id " + auth.UserId + " not found"); + } + + if (user != null) + { + ValidateUserAccess(user, request, authAttribtues, auth); + } + + var info = GetTokenInfo(request); + + if (!IsExemptFromRoles(auth, authAttribtues, info)) + { + var roles = authAttribtues.GetRoles().ToList(); + + ValidateRoles(roles, user); + } + + if (!string.IsNullOrWhiteSpace(auth.DeviceId) && + !string.IsNullOrWhiteSpace(auth.Client) && + !string.IsNullOrWhiteSpace(auth.Device)) + { + SessionManager.LogSessionActivity(auth.Client, + auth.Version, + auth.DeviceId, + auth.Device, + request.RemoteIp, + user); + } + } + + private void ValidateUserAccess(User user, IServiceRequest request, + IAuthenticationAttributes authAttribtues, + AuthorizationInfo auth) + { + if (user.Policy.IsDisabled) + { + throw new SecurityException("User account has been disabled.") + { + SecurityExceptionType = SecurityExceptionType.Unauthenticated + }; + } + + if (!user.Policy.IsAdministrator && + !authAttribtues.EscapeParentalControl && + !user.IsParentalScheduleAllowed()) + { + request.AddResponseHeader("X-Application-Error-Code", "ParentalControl"); + + throw new SecurityException("This user account is not allowed access at this time.") + { + 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) + { + if (!_config.Configuration.IsStartupWizardCompleted && authAttribtues.AllowBeforeStartupWizard) + { + return true; + } + + return false; + } + + private bool IsExemptFromRoles(AuthorizationInfo auth, IAuthenticationAttributes authAttribtues, AuthenticationInfo tokenInfo) + { + if (!_config.Configuration.IsStartupWizardCompleted && authAttribtues.AllowBeforeStartupWizard) + { + return true; + } + + if (string.IsNullOrWhiteSpace(auth.Token)) + { + return true; + } + + if (tokenInfo != null && string.IsNullOrWhiteSpace(tokenInfo.UserId)) + { + return true; + } + + return false; + } + + private void ValidateRoles(List<string> roles, User user) + { + if (roles.Contains("admin", StringComparer.OrdinalIgnoreCase)) + { + if (user == null || !user.Policy.IsAdministrator) + { + throw new SecurityException("User does not have admin access.") + { + SecurityExceptionType = SecurityExceptionType.Unauthenticated + }; + } + } + if (roles.Contains("delete", StringComparer.OrdinalIgnoreCase)) + { + if (user == null || !user.Policy.EnableContentDeletion) + { + throw new SecurityException("User does not have delete access.") + { + SecurityExceptionType = SecurityExceptionType.Unauthenticated + }; + } + } + if (roles.Contains("download", StringComparer.OrdinalIgnoreCase)) + { + if (user == null || !user.Policy.EnableContentDownloading) + { + throw new SecurityException("User does not have download access.") + { + SecurityExceptionType = SecurityExceptionType.Unauthenticated + }; + } + } + } + + private AuthenticationInfo GetTokenInfo(IServiceRequest request) + { + object info; + request.Items.TryGetValue("OriginalAuthenticationInfo", out info); + return info as AuthenticationInfo; + } + + private bool IsValidConnectKey(string token) + { + if (string.IsNullOrEmpty(token)) + { + return false; + } + + return ConnectManager.IsAuthorizationTokenValid(token); + } + + private void ValidateSecurityToken(IServiceRequest request, string token) + { + if (string.IsNullOrWhiteSpace(token)) + { + throw new SecurityException("Access token is required."); + } + + var info = GetTokenInfo(request); + + if (info == null) + { + throw new SecurityException("Access token is invalid or expired."); + } + + if (!info.IsActive) + { + throw new SecurityException("Access token has expired."); + } + + //if (!string.IsNullOrWhiteSpace(info.UserId)) + //{ + // var user = _userManager.GetUserById(info.UserId); + + // if (user == null || user.Configuration.IsDisabled) + // { + // throw new SecurityException("User account has been disabled."); + // } + //} + } + } +} diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs new file mode 100644 index 000000000..ec3dfeb60 --- /dev/null +++ b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs @@ -0,0 +1,195 @@ +using MediaBrowser.Controller.Connect; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Security; +using System; +using System.Collections.Generic; +using System.Linq; +using MediaBrowser.Model.Services; + +namespace Emby.Server.Implementations.HttpServer.Security +{ + public class AuthorizationContext : IAuthorizationContext + { + private readonly IAuthenticationRepository _authRepo; + private readonly IConnectManager _connectManager; + + public AuthorizationContext(IAuthenticationRepository authRepo, IConnectManager connectManager) + { + _authRepo = authRepo; + _connectManager = connectManager; + } + + public AuthorizationInfo GetAuthorizationInfo(object requestContext) + { + var req = new ServiceRequest((IRequest)requestContext); + return GetAuthorizationInfo(req); + } + + public AuthorizationInfo GetAuthorizationInfo(IServiceRequest requestContext) + { + object cached; + if (requestContext.Items.TryGetValue("AuthorizationInfo", out cached)) + { + return (AuthorizationInfo)cached; + } + + return GetAuthorization(requestContext); + } + + /// <summary> + /// Gets the authorization. + /// </summary> + /// <param name="httpReq">The HTTP req.</param> + /// <returns>Dictionary{System.StringSystem.String}.</returns> + private AuthorizationInfo GetAuthorization(IServiceRequest httpReq) + { + var auth = GetAuthorizationDictionary(httpReq); + + string deviceId = null; + string device = null; + string client = null; + string version = null; + + if (auth != null) + { + auth.TryGetValue("DeviceId", out deviceId); + auth.TryGetValue("Device", out device); + auth.TryGetValue("Client", out client); + auth.TryGetValue("Version", out version); + } + + var token = httpReq.Headers["X-Emby-Token"]; + + if (string.IsNullOrWhiteSpace(token)) + { + token = httpReq.Headers["X-MediaBrowser-Token"]; + } + if (string.IsNullOrWhiteSpace(token)) + { + token = httpReq.QueryString["api_key"]; + } + + var info = new AuthorizationInfo + { + Client = client, + Device = device, + DeviceId = deviceId, + Version = version, + Token = token + }; + + if (!string.IsNullOrWhiteSpace(token)) + { + var result = _authRepo.Get(new AuthenticationInfoQuery + { + AccessToken = token + }); + + var tokenInfo = result.Items.FirstOrDefault(); + + if (tokenInfo != null) + { + info.UserId = tokenInfo.UserId; + + // TODO: Remove these checks for IsNullOrWhiteSpace + if (string.IsNullOrWhiteSpace(info.Client)) + { + info.Client = tokenInfo.AppName; + } + if (string.IsNullOrWhiteSpace(info.Device)) + { + info.Device = tokenInfo.DeviceName; + } + if (string.IsNullOrWhiteSpace(info.DeviceId)) + { + info.DeviceId = tokenInfo.DeviceId; + } + if (string.IsNullOrWhiteSpace(info.Version)) + { + info.Version = tokenInfo.AppVersion; + } + } + else + { + var user = _connectManager.GetUserFromExchangeToken(token); + if (user != null) + { + info.UserId = user.Id.ToString("N"); + } + } + httpReq.Items["OriginalAuthenticationInfo"] = tokenInfo; + } + + httpReq.Items["AuthorizationInfo"] = info; + + return info; + } + + /// <summary> + /// Gets the auth. + /// </summary> + /// <param name="httpReq">The HTTP req.</param> + /// <returns>Dictionary{System.StringSystem.String}.</returns> + private Dictionary<string, string> GetAuthorizationDictionary(IServiceRequest httpReq) + { + var auth = httpReq.Headers["X-Emby-Authorization"]; + + if (string.IsNullOrWhiteSpace(auth)) + { + auth = httpReq.Headers["Authorization"]; + } + + return GetAuthorization(auth); + } + + /// <summary> + /// Gets the authorization. + /// </summary> + /// <param name="authorizationHeader">The authorization header.</param> + /// <returns>Dictionary{System.StringSystem.String}.</returns> + private Dictionary<string, string> GetAuthorization(string authorizationHeader) + { + if (authorizationHeader == null) return null; + + var parts = authorizationHeader.Split(new[] { ' ' }, 2); + + // There should be at least to parts + if (parts.Length != 2) return null; + + // It has to be a digest request + if (!string.Equals(parts[0], "MediaBrowser", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + // Remove uptil the first space + authorizationHeader = parts[1]; + parts = authorizationHeader.Split(','); + + var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + + foreach (var item in parts) + { + var param = item.Trim().Split(new[] { '=' }, 2); + + if (param.Length == 2) + { + var value = NormalizeValue (param[1].Trim(new[] { '"' })); + result.Add(param[0], value); + } + } + + return result; + } + + private string NormalizeValue(string value) + { + if (string.IsNullOrWhiteSpace (value)) + { + return value; + } + + return System.Net.WebUtility.HtmlEncode(value); + } + } +} diff --git a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs new file mode 100644 index 000000000..33dd4e2d7 --- /dev/null +++ b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs @@ -0,0 +1,67 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Security; +using MediaBrowser.Controller.Session; +using System.Threading.Tasks; +using MediaBrowser.Model.Services; + +namespace Emby.Server.Implementations.HttpServer.Security +{ + public class SessionContext : ISessionContext + { + private readonly IUserManager _userManager; + private readonly ISessionManager _sessionManager; + private readonly IAuthorizationContext _authContext; + + public SessionContext(IUserManager userManager, IAuthorizationContext authContext, ISessionManager sessionManager) + { + _userManager = userManager; + _authContext = authContext; + _sessionManager = sessionManager; + } + + public Task<SessionInfo> GetSession(IServiceRequest requestContext) + { + var authorization = _authContext.GetAuthorizationInfo(requestContext); + + //if (!string.IsNullOrWhiteSpace(authorization.Token)) + //{ + // var auth = GetTokenInfo(requestContext); + // if (auth != null) + // { + // return _sessionManager.GetSessionByAuthenticationToken(auth, authorization.DeviceId, requestContext.RemoteIp, authorization.Version); + // } + //} + + var user = string.IsNullOrWhiteSpace(authorization.UserId) ? null : _userManager.GetUserById(authorization.UserId); + return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.RemoteIp, user); + } + + private AuthenticationInfo GetTokenInfo(IServiceRequest request) + { + object info; + request.Items.TryGetValue("OriginalAuthenticationInfo", out info); + return info as AuthenticationInfo; + } + + public Task<SessionInfo> GetSession(object requestContext) + { + var req = new ServiceRequest((IRequest)requestContext); + return GetSession(req); + } + + public async Task<User> GetUser(IServiceRequest requestContext) + { + var session = await GetSession(requestContext).ConfigureAwait(false); + + return session == null || !session.UserId.HasValue ? null : _userManager.GetUserById(session.UserId.Value); + } + + public Task<User> GetUser(object requestContext) + { + var req = new ServiceRequest((IRequest)requestContext); + return GetUser(req); + } + } +} diff --git a/Emby.Server.Implementations/HttpServer/SocketSharp/Extensions.cs b/Emby.Server.Implementations/HttpServer/SocketSharp/Extensions.cs new file mode 100644 index 000000000..07a338f19 --- /dev/null +++ b/Emby.Server.Implementations/HttpServer/SocketSharp/Extensions.cs @@ -0,0 +1,12 @@ +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 new file mode 100644 index 000000000..4fbe0ed94 --- /dev/null +++ b/Emby.Server.Implementations/HttpServer/SocketSharp/HttpUtility.cs @@ -0,0 +1,922 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using MediaBrowser.Model.Services; + +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 = 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 new file mode 100644 index 000000000..ec14c32c8 --- /dev/null +++ b/Emby.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs @@ -0,0 +1,846 @@ +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 new file mode 100644 index 000000000..9823a2ff5 --- /dev/null +++ b/Emby.Server.Implementations/HttpServer/SocketSharp/SharpWebSocket.cs @@ -0,0 +1,158 @@ +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); + } + + /// <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 new file mode 100644 index 000000000..4606d0e31 --- /dev/null +++ b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs @@ -0,0 +1,207 @@ +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Logging; +using SocketHttpListener.Net; +using System; +using System.Collections.Generic; +using System.Linq; +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.Text; +using SocketHttpListener.Primitives; + +namespace Emby.Server.Implementations.HttpServer.SocketSharp +{ + public class WebSocketSharpListener : IHttpListener + { + private HttpListener _listener; + + private readonly ILogger _logger; + private readonly ICertificate _certificate; + private readonly IMemoryStreamFactory _memoryStreamProvider; + private readonly ITextEncoding _textEncoding; + private readonly INetworkManager _networkManager; + private readonly ISocketFactory _socketFactory; + private readonly ICryptoProvider _cryptoProvider; + private readonly IStreamFactory _streamFactory; + private readonly Func<HttpListenerContext, IHttpRequest> _httpRequestFactory; + private readonly bool _enableDualMode; + + public WebSocketSharpListener(ILogger logger, ICertificate certificate, IMemoryStreamFactory memoryStreamProvider, ITextEncoding textEncoding, INetworkManager networkManager, ISocketFactory socketFactory, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, bool enableDualMode, Func<HttpListenerContext, IHttpRequest> httpRequestFactory) + { + _logger = logger; + _certificate = certificate; + _memoryStreamProvider = memoryStreamProvider; + _textEncoding = textEncoding; + _networkManager = networkManager; + _socketFactory = socketFactory; + _cryptoProvider = cryptoProvider; + _streamFactory = streamFactory; + _enableDualMode = enableDualMode; + _httpRequestFactory = httpRequestFactory; + } + + public Action<Exception, IRequest> ErrorHandler { get; set; } + public Func<IHttpRequest, Uri, 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, _streamFactory, _socketFactory, _networkManager, _textEncoding, _memoryStreamProvider); + + _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) + { + //Task.Factory.StartNew(() => InitTask(context), TaskCreationOptions.DenyChildAttach | TaskCreationOptions.PreferFairness); + Task.Run(() => InitTask(context)); + } + + private Task InitTask(HttpListenerContext context) + { + 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); + return Task.FromResult(true); + } + + return RequestHandler(httpReq, request.Url); + } + + 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) + { + return _httpRequestFactory(httpContext); + } + + public void Stop() + { + if (_listener != null) + { + foreach (var prefix in _listener.Prefixes.ToList()) + { + _listener.Prefixes.Remove(prefix); + } + + _listener.Close(); + } + } + + 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 new file mode 100644 index 000000000..b3fcde745 --- /dev/null +++ b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs @@ -0,0 +1,620 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +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); + } + + 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.IpAddress.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; + HasExplicitResponseContentType = true; + } + } + + public const string FormUrlEncoded = "application/x-www-form-urlencoded"; + public const string MultiPartFormData = "multipart/form-data"; + private 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 bool HasExplicitResponseContentType { get; private set; } + + 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 = 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 = (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 Param(string name) + { + return Headers[name] + ?? QueryString[name] + ?? FormData[name]; + } + + 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 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 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 new file mode 100644 index 000000000..36f795411 --- /dev/null +++ b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs @@ -0,0 +1,193 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; +using MediaBrowser.Model.Logging; +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 bool UseBufferedStream { get; 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 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 + if (outputStream is ResponseStream) + { + //if (!string.IsNullOrWhiteSpace(GetHeader("Content-Encoding"))) + { + 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() + { + } + } +} diff --git a/Emby.Server.Implementations/HttpServer/StreamWriter.cs b/Emby.Server.Implementations/HttpServer/StreamWriter.cs new file mode 100644 index 000000000..33378949c --- /dev/null +++ b/Emby.Server.Implementations/HttpServer/StreamWriter.cs @@ -0,0 +1,129 @@ +using MediaBrowser.Model.Logging; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.IO; +using MediaBrowser.Model.Services; + +namespace Emby.Server.Implementations.HttpServer +{ + /// <summary> + /// Class StreamWriter + /// </summary> + public class StreamWriter : IAsyncStreamWriter, IHasHeaders + { + private ILogger Logger { get; set; } + + private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); + + /// <summary> + /// Gets or sets the source stream. + /// </summary> + /// <value>The source stream.</value> + private Stream SourceStream { get; set; } + + private byte[] SourceBytes { get; set; } + + /// <summary> + /// The _options + /// </summary> + private readonly IDictionary<string, string> _options = new Dictionary<string, string>(); + /// <summary> + /// Gets the options. + /// </summary> + /// <value>The options.</value> + public IDictionary<string, string> Headers + { + get { return _options; } + } + + public Action OnComplete { get; set; } + public Action OnError { get; set; } + + /// <summary> + /// Initializes a new instance of the <see cref="StreamWriter" /> class. + /// </summary> + /// <param name="source">The source.</param> + /// <param name="contentType">Type of the content.</param> + /// <param name="logger">The logger.</param> + public StreamWriter(Stream source, string contentType, ILogger logger) + { + if (string.IsNullOrEmpty(contentType)) + { + throw new ArgumentNullException("contentType"); + } + + SourceStream = source; + Logger = logger; + + Headers["Content-Type"] = contentType; + + if (source.CanSeek) + { + Headers["Content-Length"] = source.Length.ToString(UsCulture); + } + } + + /// <summary> + /// Initializes a new instance of the <see cref="StreamWriter"/> class. + /// </summary> + /// <param name="source">The source.</param> + /// <param name="contentType">Type of the content.</param> + /// <param name="logger">The logger.</param> + public StreamWriter(byte[] source, string contentType, ILogger logger) + { + if (string.IsNullOrEmpty(contentType)) + { + throw new ArgumentNullException("contentType"); + } + + SourceBytes = source; + Logger = logger; + + Headers["Content-Type"] = contentType; + + Headers["Content-Length"] = source.Length.ToString(UsCulture); + } + + public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken) + { + try + { + var bytes = SourceBytes; + + if (bytes != null) + { + await responseStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false); + } + else + { + using (var src = SourceStream) + { + await src.CopyToAsync(responseStream).ConfigureAwait(false); + } + } + } + catch (Exception ex) + { + Logger.ErrorException("Error streaming data", ex); + + if (OnError != null) + { + OnError(); + } + + throw; + } + finally + { + if (OnComplete != null) + { + OnComplete(); + } + } + } + } +} diff --git a/Emby.Server.Implementations/HttpServer/SwaggerService.cs b/Emby.Server.Implementations/HttpServer/SwaggerService.cs new file mode 100644 index 000000000..d41946645 --- /dev/null +++ b/Emby.Server.Implementations/HttpServer/SwaggerService.cs @@ -0,0 +1,47 @@ +using MediaBrowser.Controller; +using MediaBrowser.Controller.Net; +using System.IO; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Services; + +namespace Emby.Server.Implementations.HttpServer +{ + public class SwaggerService : IService, IRequiresRequest + { + private readonly IServerApplicationPaths _appPaths; + private readonly IFileSystem _fileSystem; + + public SwaggerService(IServerApplicationPaths appPaths, IFileSystem fileSystem, IHttpResultFactory resultFactory) + { + _appPaths = appPaths; + _fileSystem = fileSystem; + _resultFactory = resultFactory; + } + + /// <summary> + /// Gets the specified request. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>System.Object.</returns> + public object Get(GetSwaggerResource request) + { + var swaggerDirectory = Path.Combine(_appPaths.ApplicationResourcesPath, "swagger-ui"); + + var requestedFile = Path.Combine(swaggerDirectory, request.ResourceName.Replace('/', _fileSystem.DirectorySeparatorChar)); + + return _resultFactory.GetStaticFileResult(Request, requestedFile).Result; + } + + /// <summary> + /// Gets or sets the result factory. + /// </summary> + /// <value>The result factory.</value> + private readonly IHttpResultFactory _resultFactory; + + /// <summary> + /// Gets or sets the request context. + /// </summary> + /// <value>The request context.</value> + public IRequest Request { get; set; } + } +} |
