diff options
| author | Luke <luke.pulverenti@gmail.com> | 2016-11-11 15:37:53 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2016-11-11 15:37:53 -0500 |
| commit | b4c6cad2fa9256c4af83752a34679d2533c96b11 (patch) | |
| tree | 43e25b4ed64a535823e21128ee74f3e0a1245069 /Emby.Server.Implementations/HttpServer | |
| parent | 00316bc8ca9791f2ad68e84e29f2fbc6b1af2173 (diff) | |
| parent | 2b19df9544315603673ff034e8fd621cf0b85a4b (diff) | |
Merge pull request #2281 from MediaBrowser/dev
Dev
Diffstat (limited to 'Emby.Server.Implementations/HttpServer')
5 files changed, 3024 insertions, 2 deletions
diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs new file mode 100644 index 000000000..64f498b12 --- /dev/null +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -0,0 +1,710 @@ +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.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; + + 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) + : 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; + _config = config; + + _logger = logger; + } + + public string GlobalResponse { get; set; } + + public override void Configure() + { + var 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} + }; + + 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() + { + var enableDualMode = _environment.OperatingSystem == OperatingSystem.Windows; + + return new WebSocketSharpListener(_logger, + _certificate, + _memoryStreamProvider, + _textEncoding, + _networkManager, + _socketFactory, + _cryptoProvider, + _streamFactory, + enableDualMode, + 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; + } + + httpRes.StatusCode = 500; + + httpRes.ContentType = "text/html"; + httpRes.Write(ex.Message); + + httpRes.Close(); + } + 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; + return; + } + + if (!ValidateHost(url)) + { + httpRes.StatusCode = 400; + httpRes.ContentType = "text/plain"; + httpRes.Write("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/html"; + 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)) + { + httpRes.Write( + "<!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)) + { + httpRes.Write( + "<!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"; + httpRes.Write(GlobalResponse); + return; + } + + var handler = HttpHandlerFactory.GetHandler(httpReq); + + if (handler != null) + { + await handler.ProcessRequestAsync(httpReq, httpRes, operationName).ConfigureAwait(false); + } + } + 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); + } + } + } + + 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..bbd556661 --- /dev/null +++ b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs @@ -0,0 +1,847 @@ +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 IXmlSerializer _xmlSerializer; + + /// <summary> + /// Initializes a new instance of the <see cref="HttpResultFactory" /> class. + /// </summary> + /// <param name="logManager">The log manager.</param> + /// <param name="fileSystem">The file system.</param> + /// <param name="jsonSerializer">The json serializer.</param> + public HttpResultFactory(ILogManager logManager, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IXmlSerializer xmlSerializer) + { + _fileSystem = fileSystem; + _jsonSerializer = jsonSerializer; + _xmlSerializer = xmlSerializer; + _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, responseHeaders); + } + + /// <summary> + /// Gets the HTTP result. + /// </summary> + /// <param name="content">The content.</param> + /// <param name="contentType">Type of the content.</param> + /// <param name="responseHeaders">The response headers.</param> + /// <returns>IHasHeaders.</returns> + private IHasHeaders GetHttpResult(object content, string contentType, 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); + } + } + } + if (responseHeaders == null) + { + responseHeaders = new Dictionary<string, string>(); + } + + 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) + { + request.Response.Dto = 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); + } + } + + 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 HttpResult(compressedBytes, request.ResponseContentType) + { + Status = request.Response.StatusCode + }; + + httpResult.Headers["Content-Length"] = compressedBytes.Length.ToString(UsCulture); + httpResult.Headers["Content-Encoding"] = compressionType; + + return httpResult; + } + } + } + + public static string GetRealContentType(string contentType) + { + return contentType == null + ? null + : contentType.Split(';')[0].ToLower().Trim(); + } + + public static string SerializeToXmlString(object from) + { + using (var ms = new MemoryStream()) + { + var xwSettings = new XmlWriterSettings(); + xwSettings.Encoding = new UTF8Encoding(false); + xwSettings.OmitXmlDeclaration = false; + + using (var xw = XmlWriter.Create(ms, xwSettings)) + { + var serializer = new DataContractSerializer(from.GetType()); + serializer.WriteObject(xw, from); + xw.Flush(); + ms.Seek(0, SeekOrigin.Begin); + var reader = new StreamReader(ms); + return reader.ReadToEnd(); + } + } + } + + private static Stream GetCompressionStream(Stream outputStream, string compressionType) + { + if (compressionType == "deflate") + return new DeflateStream(outputStream, CompressionMode.Compress); + if (compressionType == "gzip") + return new GZipStream(outputStream, CompressionMode.Compress); + + throw new NotSupportedException(compressionType); + } + + /// <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; + } + + IHasHeaders httpResult; + + var stream = result as Stream; + + if (stream != null) + { + httpResult = new StreamWriter(stream, contentType, _logger); + } + else + { + // Otherwise wrap into an HttpResult + httpResult = new HttpResult(result, contentType ?? "text/html", HttpStatusCode.NotModified); + } + + AddResponseHeaders(httpResult, responseHeaders); + + return httpResult; + } + + /// <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); + } + + 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); + } + + return GetHttpResult(contents, contentType, responseHeaders); + } + + public static byte[] Compress(string text, string compressionType) + { + if (compressionType == "deflate") + return Deflate(text); + + if (compressionType == "gzip") + return GZip(text); + + throw new NotSupportedException(compressionType); + } + + public static byte[] Deflate(string text) + { + return Deflate(Encoding.UTF8.GetBytes(text)); + } + + public static byte[] Deflate(byte[] bytes) + { + // In .NET FX incompat-ville, you can't access compressed bytes without closing DeflateStream + // Which means we must use MemoryStream since you have to use ToArray() on a closed Stream + using (var ms = new MemoryStream()) + using (var zipStream = new DeflateStream(ms, CompressionMode.Compress)) + { + zipStream.Write(bytes, 0, bytes.Length); + zipStream.Dispose(); + + return ms.ToArray(); + } + } + + public static byte[] GZip(string text) + { + return GZip(Encoding.UTF8.GetBytes(text)); + } + + public static byte[] GZip(byte[] buffer) + { + using (var ms = new MemoryStream()) + using (var zipStream = new GZipStream(ms, CompressionMode.Compress)) + { + zipStream.Write(buffer, 0, buffer.Length); + zipStream.Dispose(); + + return ms.ToArray(); + } + } + + /// <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)) + { + isNotModified = IsNotModified(ifModifiedSince.ToUniversalTime(), cacheDuration, lastDateModified); + } + } + + var ifNoneMatchHeader = requestContext.Headers.Get("If-None-Match"); + + // Validate If-None-Match + if (isNotModified && (cacheKey.HasValue || !string.IsNullOrEmpty(ifNoneMatchHeader))) + { + Guid ifNoneMatch; + + if (Guid.TryParse(ifNoneMatchHeader ?? string.Empty, 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/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/WebSocketSharpListener.cs b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs index db7b9bc4b..94cc383a7 100644 --- a/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs +++ b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Emby.Server.Implementations.Logging; using MediaBrowser.Common.Net; using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.IO; @@ -55,7 +54,7 @@ namespace Emby.Server.Implementations.HttpServer.SocketSharp public void Start(IEnumerable<string> urlPrefixes) { if (_listener == null) - _listener = new HttpListener(new PatternsLogger(_logger), _cryptoProvider, _streamFactory, _socketFactory, _networkManager, _textEncoding, _memoryStreamProvider); + _listener = new HttpListener(_logger, _cryptoProvider, _streamFactory, _socketFactory, _networkManager, _textEncoding, _memoryStreamProvider); _listener.EnableDualMode = _enableDualMode; 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; } + } +} |
