From 01fc207b62151a858c0d7edb802a24690505bb95 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Thu, 10 Nov 2016 20:37:20 -0500 Subject: update portable components --- .../HttpServer/HttpResultFactory.cs | 847 +++++++++++++++++++++ 1 file changed, 847 insertions(+) create mode 100644 Emby.Server.Implementations/HttpServer/HttpResultFactory.cs (limited to 'Emby.Server.Implementations/HttpServer') 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 +{ + /// + /// Class HttpResultFactory + /// + public class HttpResultFactory : IHttpResultFactory + { + /// + /// The _logger + /// + private readonly ILogger _logger; + private readonly IFileSystem _fileSystem; + private readonly IJsonSerializer _jsonSerializer; + private readonly IXmlSerializer _xmlSerializer; + + /// + /// Initializes a new instance of the class. + /// + /// The log manager. + /// The file system. + /// The json serializer. + public HttpResultFactory(ILogManager logManager, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IXmlSerializer xmlSerializer) + { + _fileSystem = fileSystem; + _jsonSerializer = jsonSerializer; + _xmlSerializer = xmlSerializer; + _logger = logManager.GetLogger("HttpResultFactory"); + } + + /// + /// Gets the result. + /// + /// The content. + /// Type of the content. + /// The response headers. + /// System.Object. + public object GetResult(object content, string contentType, IDictionary responseHeaders = null) + { + return GetHttpResult(content, contentType, responseHeaders); + } + + /// + /// Gets the HTTP result. + /// + /// The content. + /// Type of the content. + /// The response headers. + /// IHasHeaders. + private IHasHeaders GetHttpResult(object content, string contentType, IDictionary 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(); + } + + responseHeaders["Expires"] = "-1"; + AddResponseHeaders(result, responseHeaders); + + return result; + } + + /// + /// Gets the optimized result. + /// + /// + /// The request context. + /// The result. + /// The response headers. + /// System.Object. + /// result + public object GetOptimizedResult(IRequest requestContext, T result, IDictionary responseHeaders = null) + where T : class + { + return GetOptimizedResultInternal(requestContext, result, true, responseHeaders); + } + + private object GetOptimizedResultInternal(IRequest requestContext, T result, bool addCachePrevention, IDictionary responseHeaders = null) + where T : class + { + if (result == null) + { + throw new ArgumentNullException("result"); + } + + var optimizedResult = ToOptimizedResult(requestContext, result); + + if (responseHeaders == null) + { + responseHeaders = new Dictionary(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; + } + + /// + /// Returns the optimized result for the IRequestContext. + /// Does not use or store results in any cache. + /// + /// + /// + /// + public object ToOptimizedResult(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); + } + + /// + /// Gets the optimized result using cache. + /// + /// + /// The request context. + /// The cache key. + /// The last date modified. + /// Duration of the cache. + /// The factory fn. + /// The response headers. + /// System.Object. + /// cacheKey + /// or + /// factoryFn + public object GetOptimizedResultUsingCache(IRequest requestContext, Guid cacheKey, DateTime? lastDateModified, TimeSpan? cacheDuration, Func factoryFn, IDictionary 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(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); + } + + /// + /// To the cached result. + /// + /// + /// The request context. + /// The cache key. + /// The last date modified. + /// Duration of the cache. + /// The factory fn. + /// Type of the content. + /// The response headers. + /// System.Object. + /// cacheKey + public object GetCachedResult(IRequest requestContext, Guid cacheKey, DateTime? lastDateModified, TimeSpan? cacheDuration, Func factoryFn, string contentType, IDictionary 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(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; + } + + /// + /// Pres the process optimized result. + /// + /// The request context. + /// The responseHeaders. + /// The cache key. + /// The cache key string. + /// The last date modified. + /// Duration of the cache. + /// Type of the content. + /// System.Object. + private object GetCachedResult(IRequest requestContext, IDictionary 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 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 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); + } + + /// + /// Gets the file stream. + /// + /// The path. + /// The file share. + /// Stream. + private Stream GetFileStream(string path, FileShareMode fileShare) + { + return _fileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, fileShare); + } + + public Task GetStaticResult(IRequest requestContext, + Guid cacheKey, + DateTime? lastDateModified, + TimeSpan? cacheDuration, + string contentType, + Func> factoryFn, + IDictionary 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 GetStaticResult(IRequest requestContext, StaticResultOptions options) + { + var cacheKey = options.CacheKey; + options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary(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; + } + + /// + /// Shoulds the compress response. + /// + /// The request context. + /// Type of the content. + /// true if XXXX, false otherwise + 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; + } + + /// + /// The us culture + /// + private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); + + private async Task 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(); + } + } + + /// + /// Adds the caching responseHeaders. + /// + /// The responseHeaders. + /// The cache key. + /// The last date modified. + /// Duration of the cache. + private void AddCachingHeaders(IDictionary 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); + } + + /// + /// Adds the expires header. + /// + /// The responseHeaders. + /// The cache key. + /// Duration of the cache. + private void AddExpiresHeader(IDictionary 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"; + } + } + + /// + /// Adds the age header. + /// + /// The responseHeaders. + /// The last date modified. + private void AddAgeHeader(IDictionary responseHeaders, DateTime? lastDateModified) + { + if (lastDateModified.HasValue) + { + responseHeaders["Age"] = Convert.ToInt64((DateTime.UtcNow - lastDateModified.Value).TotalSeconds).ToString(CultureInfo.InvariantCulture); + } + } + /// + /// Determines whether [is not modified] [the specified cache key]. + /// + /// The request context. + /// The cache key. + /// The last date modified. + /// Duration of the cache. + /// true if [is not modified] [the specified cache key]; otherwise, false. + 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; + } + + /// + /// Determines whether [is not modified] [the specified if modified since]. + /// + /// If modified since. + /// Duration of the cache. + /// The date modified. + /// true if [is not modified] [the specified if modified since]; otherwise, false. + 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; + } + + + /// + /// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that + /// + /// The date. + /// DateTime. + private DateTime NormalizeDateForComparison(DateTime date) + { + return new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second, date.Kind); + } + + /// + /// Adds the response headers. + /// + /// The has options. + /// The response headers. + private void AddResponseHeaders(IHasHeaders hasHeaders, IEnumerable> responseHeaders) + { + foreach (var item in responseHeaders) + { + hasHeaders.Headers[item.Key] = item.Value; + } + } + } +} \ No newline at end of file -- cgit v1.2.3 From 836e1fdc30f149d3548dc0b529d31ba96de28734 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Thu, 10 Nov 2016 20:58:20 -0500 Subject: make additional classes portable --- .../Emby.Server.Implementations.csproj | 2 + .../HttpServer/SocketSharp/RequestMono.cs | 846 +++++++++++++++++++ .../SocketSharp/WebSocketSharpRequest.cs | 620 ++++++++++++++ .../HttpServer/HttpListenerHost.cs | 6 +- .../HttpServer/SocketSharp/RequestMono.cs | 917 --------------------- .../SocketSharp/WebSocketSharpRequest.cs | 609 -------------- .../MediaBrowser.Server.Implementations.csproj | 2 - 7 files changed, 1469 insertions(+), 1533 deletions(-) create mode 100644 Emby.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs create mode 100644 Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs delete mode 100644 MediaBrowser.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs delete mode 100644 MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs (limited to 'Emby.Server.Implementations/HttpServer') diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 3f55bd7fb..07f3bd020 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -79,8 +79,10 @@ + + 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(); + + 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 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/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 items; + public Dictionary Items + { + get { return items ?? (items = new Dictionary()); } + } + + 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 cookies; + public IDictionary Cookies + { + get + { + if (cookies == null) + { + cookies = new Dictionary(); + 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/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs b/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs index 4218370ac..f0a9c5ca3 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -2,10 +2,8 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Logging; -using MediaBrowser.Server.Implementations.HttpServer.SocketSharp; using ServiceStack; using ServiceStack.Host; -using ServiceStack.Web; using System; using System.Collections.Generic; using System.IO; @@ -158,14 +156,12 @@ namespace MediaBrowser.Server.Implementations.HttpServer return this; } - public static string HandlerFactoryPath; - /// /// Starts the Web Service /// private void StartListener() { - HandlerFactoryPath = GetHandlerPathIfAny(UrlPrefixes.First()); + WebSocketSharpRequest.HandlerFactoryPath = GetHandlerPathIfAny(UrlPrefixes.First()); _listener = GetListener(); diff --git a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs deleted file mode 100644 index 543eb4afe..000000000 --- a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs +++ /dev/null @@ -1,917 +0,0 @@ -using System; -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 MediaBrowser.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)); - } - 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.AddFile(e.Name, sub); - } - } - } - } - - public QueryParamCollection Form - { - get - { - if (form == null) - { - form = new WebROCollection(); - files = new HttpFileCollection(); - - 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.Compare(ContentType, ct, true, Helpers.InvariantCulture) == 0; - } - - 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; - - HttpFileCollection files; - - public sealed class HttpFileCollection : NameObjectCollectionBase - { - internal HttpFileCollection() - { - } - - internal void AddFile(string name, HttpPostedFile file) - { - BaseAdd(name, file); - } - - public void CopyTo(Array dest, int index) - { - /* XXX this is kind of gross and inefficient - * since it makes a copy of the superclass's - * list */ - object[] values = BaseGetAllValues(); - values.CopyTo(dest, index); - } - - public string GetKey(int index) - { - return BaseGetKey(index); - } - - public HttpPostedFile Get(int index) - { - return (HttpPostedFile)BaseGet(index); - } - - public HttpPostedFile Get(string key) - { - return (HttpPostedFile)BaseGet(key); - } - - public HttpPostedFile this[string key] - { - get - { - return Get(key); - } - } - - public HttpPostedFile this[int index] - { - get - { - return Get(index); - } - } - - public string[] AllKeys - { - get - { - return BaseGetAllKeys(); - } - } - } - 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 - { - StrUtils() { } - - public static bool StartsWith(string str1, string str2) - { - return StartsWith(str1, str2, false); - } - - public static bool StartsWith(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; - - return 0 == String.Compare(str1, 0, str2, 0, l2, ignore_case, Helpers.InvariantCulture); - } - - public static bool EndsWith(string str1, string str2) - { - return EndsWith(str1, str2, false); - } - - 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; - - return 0 == String.Compare(str1, l1 - l2, str2, 0, l2, ignore_case, Helpers.InvariantCulture); - } - } - - 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); - } - - 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/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs deleted file mode 100644 index 628e5cc7e..000000000 --- a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs +++ /dev/null @@ -1,609 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using Emby.Server.Implementations.HttpServer; -using Emby.Server.Implementations.HttpServer.SocketSharp; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Services; -using ServiceStack; -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 MediaBrowser.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 items; - public Dictionary Items - { - get { return items ?? (items = new Dictionary()); } - } - - 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 = pi.LeftPart('/'); - if (format.Length > formatMaxLength) return null; - } - - format = format.LeftPart('.').ToLower(); - if (format.Contains("json")) return "application/json"; - if (format.Contains("xml")) return Xml; - - return null; - } - - public bool HasExplicitResponseContentType { get; private set; } - - private string pathInfo; - public string PathInfo - { - get - { - if (this.pathInfo == null) - { - var mode = HttpListenerHost.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 = this.pathInfo.UrlDecode(); - 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 cookies; - public IDictionary Cookies - { - get - { - if (cookies == null) - { - cookies = new Dictionary(); - for (var i = 0; i < this.request.Cookies.Count; i++) - { - var httpCookie = this.request.Cookies[i]; - 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]; - for (var i = 0; i < files.Count; i++) - { - var reqFile = files[i]; - - httpFiles[i] = new HttpFile - { - ContentType = reqFile.ContentType, - ContentLength = reqFile.ContentLength, - FileName = reqFile.FileName, - InputStream = reqFile.InputStream, - }; - } - } - return httpFiles; - } - } - - static Stream GetSubStream(Stream stream, IMemoryStreamFactory streamProvider) - { - if (stream is MemoryStream) - { - var other = (MemoryStream)stream; - try - { - return new MemoryStream(other.GetBuffer(), 0, (int)other.Length, false, true); - } - catch (UnauthorizedAccessException) - { - return new MemoryStream(other.ToArray(), 0, (int)other.Length, false, true); - } - } - - return stream; - } - - public static string GetHandlerPathIfAny(string listenerUrl) - { - if (listenerUrl == null) return null; - var pos = listenerUrl.IndexOf("://", StringComparison.InvariantCultureIgnoreCase); - 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.InvariantCultureIgnoreCase)) - { - 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/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index a253753c5..6fb4f9e36 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -111,8 +111,6 @@ - - -- cgit v1.2.3 From 8492225deef59b4548976e162f6fa147abf923be Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Thu, 10 Nov 2016 22:29:51 -0500 Subject: update portable projects --- .../Emby.Server.Implementations.csproj | 2 + .../HttpServer/HttpListenerHost.cs | 710 +++++++++++++++++++ .../Playlists/ManualPlaylistsFolder.cs | 74 ++ .../ServerManager/ServerManager.cs | 8 +- MediaBrowser.Controller/Net/IHttpServer.cs | 10 +- MediaBrowser.Controller/Net/IServerManager.cs | 4 +- .../Configuration/ServerConfigurationManager.cs | 259 ------- .../Devices/CameraUploadsFolder.cs | 1 + .../HttpServer/HttpListenerHost.cs | 764 --------------------- .../HttpServer/ServerFactory.cs | 39 -- .../MediaBrowser.Server.Implementations.csproj | 44 +- .../Persistence/SqliteItemRepository.cs | 2 +- .../Playlists/ManualPlaylistsFolder.cs | 76 -- .../ServerApplicationPaths.cs | 233 ------- .../packages.config | 5 - .../ApplicationHost.cs | 43 +- .../Configuration/ServerConfigurationManager.cs | 257 +++++++ .../HttpServerFactory.cs | 107 +++ .../MediaBrowser.Server.Startup.Common.csproj | 15 +- .../ServerApplicationPaths.cs | 233 +++++++ MediaBrowser.Server.Startup.Common/packages.config | 1 + 21 files changed, 1442 insertions(+), 1445 deletions(-) create mode 100644 Emby.Server.Implementations/HttpServer/HttpListenerHost.cs create mode 100644 Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs delete mode 100644 MediaBrowser.Server.Implementations/Configuration/ServerConfigurationManager.cs delete mode 100644 MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs delete mode 100644 MediaBrowser.Server.Implementations/HttpServer/ServerFactory.cs delete mode 100644 MediaBrowser.Server.Implementations/Playlists/ManualPlaylistsFolder.cs delete mode 100644 MediaBrowser.Server.Implementations/ServerApplicationPaths.cs create mode 100644 MediaBrowser.Server.Startup.Common/Configuration/ServerConfigurationManager.cs create mode 100644 MediaBrowser.Server.Startup.Common/HttpServerFactory.cs create mode 100644 MediaBrowser.Server.Startup.Common/ServerApplicationPaths.cs (limited to 'Emby.Server.Implementations/HttpServer') diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 07f3bd020..ccdecbf7b 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -69,6 +69,7 @@ + @@ -172,6 +173,7 @@ + diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs new file mode 100644 index 000000000..d7897f165 --- /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 UrlPrefixes { get; private set; } + + private readonly List _restServices = new List(); + + private IHttpListener _listener; + + public event EventHandler WebSocketConnected; + public event EventHandler 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> _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> funcParseFn) + : base(serviceName, new Assembly[] { }) + { + _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 + { + {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().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() + { + return _appHost.Resolve(); + } + + public override T TryResolve() + { + return _appHost.TryResolve(); + } + + public override object CreateInstance(Type type) + { + return _appHost.CreateInstance(type); + } + + protected override ServiceController CreateServiceController(params Assembly[] assembliesWithServices) + { + var types = _restServices.Select(r => r.GetType()).ToArray(); + + return new ServiceController(this, () => types); + } + + public override ServiceStackHost Start(string listeningAtUrlBase) + { + StartListener(); + return this; + } + + /// + /// Starts the Web Service + /// + 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); + } + } + + /// + /// Shut down the Web Service + /// + public void Stop() + { + if (_listener != null) + { + _listener.Stop(); + } + } + + private readonly Dictionary _skipLogExtensions = new Dictionary(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; + } + + /// + /// Overridable method that can be used to implement a custom hnandler + /// + /// The HTTP req. + /// The URL. + /// Task. + 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( + "EmbyPlease update your Emby bookmark to " + newUrl + ""); + 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( + "EmbyPlease update your Emby bookmark to " + newUrl + ""); + 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); + } + + + /// + /// Adds the rest handlers. + /// + /// The services. + public void Init(IEnumerable 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; + 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"); + return type.GetDeclaredProperty("Result").GetValue(task); + } + catch (TypeAccessException) + { + return null; //return null for void Task's + } + } + + public override Func 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 urlPrefixes) + { + UrlPrefixes = urlPrefixes.ToList(); + Start(UrlPrefixes.First()); + } + } +} \ No newline at end of file diff --git a/Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs b/Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs new file mode 100644 index 000000000..8e65ccaf7 --- /dev/null +++ b/Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Playlists; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Querying; + +namespace Emby.Server.Implementations.Playlists +{ + public class PlaylistsFolder : BasePluginFolder + { + public PlaylistsFolder() + { + Name = "Playlists"; + } + + public override bool IsVisible(User user) + { + return base.IsVisible(user) && GetChildren(user, true).Any(); + } + + protected override IEnumerable GetEligibleChildrenForRecursiveChildren(User user) + { + return base.GetEligibleChildrenForRecursiveChildren(user).OfType(); + } + + public override bool IsHidden + { + get + { + return true; + } + } + + public override string CollectionType + { + get { return MediaBrowser.Model.Entities.CollectionType.Playlists; } + } + + protected override Task> GetItemsInternal(InternalItemsQuery query) + { + query.Recursive = false; + return base.GetItemsInternal(query); + } + } + + public class PlaylistsDynamicFolder : IVirtualFolderCreator + { + private readonly IApplicationPaths _appPaths; + private readonly IFileSystem _fileSystem; + + public PlaylistsDynamicFolder(IApplicationPaths appPaths, IFileSystem fileSystem) + { + _appPaths = appPaths; + _fileSystem = fileSystem; + } + + public BasePluginFolder GetFolder() + { + var path = Path.Combine(_appPaths.DataPath, "playlists"); + + _fileSystem.CreateDirectory(path); + + return new PlaylistsFolder + { + Path = path + }; + } + } +} + diff --git a/Emby.Server.Implementations/ServerManager/ServerManager.cs b/Emby.Server.Implementations/ServerManager/ServerManager.cs index f660d01ec..f7e4c0ce2 100644 --- a/Emby.Server.Implementations/ServerManager/ServerManager.cs +++ b/Emby.Server.Implementations/ServerManager/ServerManager.cs @@ -112,22 +112,22 @@ namespace Emby.Server.Implementations.ServerManager /// /// Starts this instance. /// - public void Start(IEnumerable urlPrefixes, string certificatePath) + public void Start(IEnumerable urlPrefixes) { - ReloadHttpServer(urlPrefixes, certificatePath); + ReloadHttpServer(urlPrefixes); } /// /// Restarts the Http Server, or starts it if not currently running /// - private void ReloadHttpServer(IEnumerable urlPrefixes, string certificatePath) + private void ReloadHttpServer(IEnumerable urlPrefixes) { _logger.Info("Loading Http Server"); try { HttpServer = _applicationHost.Resolve(); - HttpServer.StartServer(urlPrefixes, certificatePath); + HttpServer.StartServer(urlPrefixes); } catch (Exception ex) { diff --git a/MediaBrowser.Controller/Net/IHttpServer.cs b/MediaBrowser.Controller/Net/IHttpServer.cs index 8c503c199..f319244da 100644 --- a/MediaBrowser.Controller/Net/IHttpServer.cs +++ b/MediaBrowser.Controller/Net/IHttpServer.cs @@ -15,19 +15,11 @@ namespace MediaBrowser.Controller.Net /// The URL prefix. IEnumerable UrlPrefixes { get; } - /// - /// Gets the certificate path. - /// - /// The certificate path. - string CertificatePath { get; } - /// /// Starts the specified server name. /// /// The URL prefixes. - /// If an https prefix is specified, - /// the ssl certificate localtion on the file system. - void StartServer(IEnumerable urlPrefixes, string certificatePath); + void StartServer(IEnumerable urlPrefixes); /// /// Stops this instance. diff --git a/MediaBrowser.Controller/Net/IServerManager.cs b/MediaBrowser.Controller/Net/IServerManager.cs index 5191a62e3..202df2982 100644 --- a/MediaBrowser.Controller/Net/IServerManager.cs +++ b/MediaBrowser.Controller/Net/IServerManager.cs @@ -15,9 +15,7 @@ namespace MediaBrowser.Controller.Net /// Starts this instance. /// /// The URL prefixes. - /// If an https prefix is specified, - /// the ssl certificate localtion on the file system. - void Start(IEnumerable urlPrefixes, string certificatePath); + void Start(IEnumerable urlPrefixes); /// /// Sends a message to all clients currently connected via a web socket diff --git a/MediaBrowser.Server.Implementations/Configuration/ServerConfigurationManager.cs b/MediaBrowser.Server.Implementations/Configuration/ServerConfigurationManager.cs deleted file mode 100644 index 4b6195192..000000000 --- a/MediaBrowser.Server.Implementations/Configuration/ServerConfigurationManager.cs +++ /dev/null @@ -1,259 +0,0 @@ -using System.Collections.Generic; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Events; -using Emby.Common.Implementations.Configuration; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Controller.Entities.Movies; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Events; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Serialization; -using System; -using System.IO; -using System.Linq; -using MediaBrowser.Common.IO; -using MediaBrowser.Controller.IO; -using MediaBrowser.Model.IO; - -namespace MediaBrowser.Server.Implementations.Configuration -{ - /// - /// Class ServerConfigurationManager - /// - public class ServerConfigurationManager : BaseConfigurationManager, IServerConfigurationManager - { - - /// - /// Initializes a new instance of the class. - /// - /// The application paths. - /// The log manager. - /// The XML serializer. - /// The file system. - public ServerConfigurationManager(IApplicationPaths applicationPaths, ILogManager logManager, IXmlSerializer xmlSerializer, IFileSystem fileSystem) - : base(applicationPaths, logManager, xmlSerializer, fileSystem) - { - UpdateMetadataPath(); - } - - public event EventHandler> ConfigurationUpdating; - - /// - /// Gets the type of the configuration. - /// - /// The type of the configuration. - protected override Type ConfigurationType - { - get { return typeof(ServerConfiguration); } - } - - /// - /// Gets the application paths. - /// - /// The application paths. - public IServerApplicationPaths ApplicationPaths - { - get { return (IServerApplicationPaths)CommonApplicationPaths; } - } - - /// - /// Gets the configuration. - /// - /// The configuration. - public ServerConfiguration Configuration - { - get { return (ServerConfiguration)CommonConfiguration; } - } - - /// - /// Called when [configuration updated]. - /// - protected override void OnConfigurationUpdated() - { - UpdateMetadataPath(); - - base.OnConfigurationUpdated(); - } - - public override void AddParts(IEnumerable factories) - { - base.AddParts(factories); - - UpdateTranscodingTempPath(); - } - - /// - /// Updates the metadata path. - /// - private void UpdateMetadataPath() - { - string metadataPath; - - if (string.IsNullOrWhiteSpace(Configuration.MetadataPath)) - { - metadataPath = GetInternalMetadataPath(); - } - else - { - metadataPath = Path.Combine(Configuration.MetadataPath, "metadata"); - } - - ((ServerApplicationPaths)ApplicationPaths).InternalMetadataPath = metadataPath; - - ((ServerApplicationPaths)ApplicationPaths).ItemsByNamePath = ((ServerApplicationPaths)ApplicationPaths).InternalMetadataPath; - } - - private string GetInternalMetadataPath() - { - return Path.Combine(ApplicationPaths.ProgramDataPath, "metadata"); - } - - /// - /// Updates the transcoding temporary path. - /// - private void UpdateTranscodingTempPath() - { - var encodingConfig = this.GetConfiguration("encoding"); - - ((ServerApplicationPaths)ApplicationPaths).TranscodingTempPath = string.IsNullOrEmpty(encodingConfig.TranscodingTempPath) ? - null : - Path.Combine(encodingConfig.TranscodingTempPath, "transcoding-temp"); - } - - protected override void OnNamedConfigurationUpdated(string key, object configuration) - { - base.OnNamedConfigurationUpdated(key, configuration); - - if (string.Equals(key, "encoding", StringComparison.OrdinalIgnoreCase)) - { - UpdateTranscodingTempPath(); - } - } - - /// - /// Replaces the configuration. - /// - /// The new configuration. - /// - public override void ReplaceConfiguration(BaseApplicationConfiguration newConfiguration) - { - var newConfig = (ServerConfiguration)newConfiguration; - - ValidatePathSubstitutions(newConfig); - ValidateMetadataPath(newConfig); - ValidateSslCertificate(newConfig); - - EventHelper.FireEventIfNotNull(ConfigurationUpdating, this, new GenericEventArgs { Argument = newConfig }, Logger); - - base.ReplaceConfiguration(newConfiguration); - } - - - /// - /// Validates the SSL certificate. - /// - /// The new configuration. - /// - private void ValidateSslCertificate(BaseApplicationConfiguration newConfig) - { - var serverConfig = (ServerConfiguration)newConfig; - - var newPath = serverConfig.CertificatePath; - - if (!string.IsNullOrWhiteSpace(newPath) - && !string.Equals(Configuration.CertificatePath ?? string.Empty, newPath)) - { - // Validate - if (!FileSystem.FileExists(newPath)) - { - throw new FileNotFoundException(string.Format("Certificate file '{0}' does not exist.", newPath)); - } - } - } - - private void ValidatePathSubstitutions(ServerConfiguration newConfig) - { - foreach (var map in newConfig.PathSubstitutions) - { - if (string.IsNullOrWhiteSpace(map.From) || string.IsNullOrWhiteSpace(map.To)) - { - throw new ArgumentException("Invalid path substitution"); - } - } - } - - /// - /// Validates the metadata path. - /// - /// The new configuration. - /// - private void ValidateMetadataPath(ServerConfiguration newConfig) - { - var newPath = newConfig.MetadataPath; - - if (!string.IsNullOrWhiteSpace(newPath) - && !string.Equals(Configuration.MetadataPath ?? string.Empty, newPath)) - { - // Validate - if (!FileSystem.DirectoryExists(newPath)) - { - throw new DirectoryNotFoundException(string.Format("{0} does not exist.", newPath)); - } - - EnsureWriteAccess(newPath); - } - } - - public void DisableMetadataService(string service) - { - DisableMetadataService(typeof(Movie), Configuration, service); - DisableMetadataService(typeof(Episode), Configuration, service); - DisableMetadataService(typeof(Series), Configuration, service); - DisableMetadataService(typeof(Season), Configuration, service); - DisableMetadataService(typeof(MusicArtist), Configuration, service); - DisableMetadataService(typeof(MusicAlbum), Configuration, service); - DisableMetadataService(typeof(MusicVideo), Configuration, service); - DisableMetadataService(typeof(Video), Configuration, service); - } - - private void DisableMetadataService(Type type, ServerConfiguration config, string service) - { - var options = GetMetadataOptions(type, config); - - if (!options.DisabledMetadataSavers.Contains(service, StringComparer.OrdinalIgnoreCase)) - { - var list = options.DisabledMetadataSavers.ToList(); - - list.Add(service); - - options.DisabledMetadataSavers = list.ToArray(); - } - } - - private MetadataOptions GetMetadataOptions(Type type, ServerConfiguration config) - { - var options = config.MetadataOptions - .FirstOrDefault(i => string.Equals(i.ItemType, type.Name, StringComparison.OrdinalIgnoreCase)); - - if (options == null) - { - var list = config.MetadataOptions.ToList(); - - options = new MetadataOptions - { - ItemType = type.Name - }; - - list.Add(options); - - config.MetadataOptions = list.ToArray(); - } - - return options; - } - } -} diff --git a/MediaBrowser.Server.Implementations/Devices/CameraUploadsFolder.cs b/MediaBrowser.Server.Implementations/Devices/CameraUploadsFolder.cs index 19a0593e3..882701885 100644 --- a/MediaBrowser.Server.Implementations/Devices/CameraUploadsFolder.cs +++ b/MediaBrowser.Server.Implementations/Devices/CameraUploadsFolder.cs @@ -10,6 +10,7 @@ using MediaBrowser.Common.IO; using MediaBrowser.Controller.IO; using MediaBrowser.Model.IO; using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Serialization; namespace MediaBrowser.Server.Implementations.Devices { diff --git a/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs b/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs deleted file mode 100644 index f0a9c5ca3..000000000 --- a/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs +++ /dev/null @@ -1,764 +0,0 @@ -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.Net.Security; -using System.Net.Sockets; -using System.Reflection; -using System.Security.Cryptography.X509Certificates; -using System.Threading.Tasks; -using Emby.Common.Implementations.Net; -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.Text; -using ServiceStack.Text.Jsv; -using SocketHttpListener.Net; -using SocketHttpListener.Primitives; - -namespace MediaBrowser.Server.Implementations.HttpServer -{ - public class HttpListenerHost : ServiceStackHost, IHttpServer - { - private string DefaultRedirectPath { get; set; } - - private readonly ILogger _logger; - public IEnumerable UrlPrefixes { get; private set; } - - private readonly List _restServices = new List(); - - private IHttpListener _listener; - - public event EventHandler WebSocketConnected; - public event EventHandler WebSocketConnecting; - - public string CertificatePath { get; private set; } - - 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; - - public HttpListenerHost(IServerApplicationHost applicationHost, - ILogManager logManager, - IServerConfigurationManager config, - string serviceName, - string defaultRedirectPath, INetworkManager networkManager, IMemoryStreamFactory memoryStreamProvider, ITextEncoding textEncoding, ISocketFactory socketFactory, ICryptoProvider cryptoProvider, IJsonSerializer jsonSerializer, IXmlSerializer xmlSerializer) - : base(serviceName, new Assembly[] { }) - { - _appHost = applicationHost; - DefaultRedirectPath = defaultRedirectPath; - _networkManager = networkManager; - _memoryStreamProvider = memoryStreamProvider; - _textEncoding = textEncoding; - _socketFactory = socketFactory; - _cryptoProvider = cryptoProvider; - _jsonSerializer = jsonSerializer; - _xmlSerializer = xmlSerializer; - _config = config; - - _logger = logManager.GetLogger("HttpServer"); - } - - public string GlobalResponse { get; set; } - - public override void Configure() - { - var mapExceptionToStatusCode = new Dictionary - { - {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 (ApplicationException), 500}, - {typeof (PlatformNotSupportedException), 500}, - {typeof (NotSupportedException), 500} - }; - - var requestFilters = _appHost.GetExports().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() - { - return _appHost.Resolve(); - } - - public override Type[] GetGenericArguments(Type type) - { - return type.GetGenericArguments(); - } - - public override bool IsAssignableFrom(Type type1, Type type2) - { - return type1.IsAssignableFrom(type2); - } - - public override T TryResolve() - { - return _appHost.TryResolve(); - } - - public override object CreateInstance(Type type) - { - return _appHost.CreateInstance(type); - } - - protected override ServiceController CreateServiceController(params Assembly[] assembliesWithServices) - { - var types = _restServices.Select(r => r.GetType()).ToArray(); - - return new ServiceController(this, () => types); - } - - public override ServiceStackHost Start(string listeningAtUrlBase) - { - StartListener(); - return this; - } - - /// - /// Starts the Web Service - /// - 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 cert = !string.IsNullOrWhiteSpace(CertificatePath) && File.Exists(CertificatePath) - ? GetCert(CertificatePath) : - null; - - var enableDualMode = Environment.OSVersion.Platform == PlatformID.Win32NT; - - return new WebSocketSharpListener(_logger, cert, _memoryStreamProvider, _textEncoding, _networkManager, _socketFactory, _cryptoProvider, new StreamFactory(), enableDualMode, GetRequest); - } - - public ICertificate GetCert(string certificateLocation) - { - try - { - X509Certificate2 localCert = new X509Certificate2(certificateLocation); - //localCert.PrivateKey = PrivateKey.CreateFromFile(pvk_file).RSA; - if (localCert.PrivateKey == null) - { - //throw new FileNotFoundException("Secure requested, no private key included", certificateLocation); - return null; - } - - return new Certificate(localCert); - } - catch (Exception ex) - { - Logger.ErrorException("Error loading cert from {0}", ex, certificateLocation); - return null; - } - } - - 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); - } - } - - /// - /// Shut down the Web Service - /// - public void Stop() - { - if (_listener != null) - { - _listener.Stop(); - } - } - - private readonly Dictionary _skipLogExtensions = new Dictionary(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); - - if (newQueryString.Count == 0) - { - return url; - } - - // this removes the key if exists - newQueryString.Remove(key); - - // this gets the page path from root without QueryString - string pagePathWithoutQueryString = uri.GetLeftPart(UriPartial.Path); - - 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; - } - - /// - /// Overridable method that can be used to implement a custom hnandler - /// - /// The HTTP req. - /// The URL. - /// Task. - 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( - "EmbyPlease update your Emby bookmark to " + newUrl + ""); - 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( - "EmbyPlease update your Emby bookmark to " + newUrl + ""); - 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); - } - - - /// - /// Adds the rest handlers. - /// - /// The services. - public void Init(IEnumerable services) - { - _restServices.AddRange(services); - - ServiceController = CreateServiceController(); - - _logger.Info("Calling ServiceStack AppHost.Init"); - - base.Init(); - } - - public override Model.Services.RouteAttribute[] GetRouteAttributes(Type requestType) - { - var routes = base.GetRouteAttributes(requestType).ToList(); - var clone = routes.ToList(); - - foreach (var route in clone) - { - routes.Add(new Model.Services.RouteAttribute(NormalizeEmbyRoutePath(route.Path), route.Verbs) - { - Notes = route.Notes, - Priority = route.Priority, - Summary = route.Summary - }); - - routes.Add(new Model.Services.RouteAttribute(NormalizeRoutePath(route.Path), route.Verbs) - { - Notes = route.Notes, - Priority = route.Priority, - Summary = route.Summary - }); - - routes.Add(new Model.Services.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; - if (taskObject != null) - { - return taskObject.Result; - } - - task.Wait(); - - var type = task.GetType(); - if (!type.IsGenericType) - { - return null; - } - - Logger.Warn("Getting task result from " + requestName + " using reflection. For better performance have your api return Task"); - return type.GetProperty("Result").GetValue(task); - } - catch (TypeAccessException) - { - return null; //return null for void Task's - } - } - - public override Func GetParseFn(Type propertyType) - { - var fn = JsvReader.GetParseFn(propertyType); - - return s => fn(s); - } - - 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 urlPrefixes, string certificatePath) - { - CertificatePath = certificatePath; - UrlPrefixes = urlPrefixes.ToList(); - Start(UrlPrefixes.First()); - } - } - - public class StreamFactory : IStreamFactory - { - public Stream CreateNetworkStream(ISocket socket, bool ownsSocket) - { - var netSocket = (NetSocket)socket; - - return new NetworkStream(netSocket.Socket, ownsSocket); - } - - public Task AuthenticateSslStreamAsServer(Stream stream, ICertificate certificate) - { - var sslStream = (SslStream)stream; - var cert = (Certificate)certificate; - - return sslStream.AuthenticateAsServerAsync(cert.X509Certificate); - } - - public Stream CreateSslStream(Stream innerStream, bool leaveInnerStreamOpen) - { - return new SslStream(innerStream, leaveInnerStreamOpen); - } - } - - public class Certificate : ICertificate - { - public Certificate(X509Certificate x509Certificate) - { - X509Certificate = x509Certificate; - } - - public X509Certificate X509Certificate { get; private set; } - } -} \ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/HttpServer/ServerFactory.cs b/MediaBrowser.Server.Implementations/HttpServer/ServerFactory.cs deleted file mode 100644 index abcf84abd..000000000 --- a/MediaBrowser.Server.Implementations/HttpServer/ServerFactory.cs +++ /dev/null @@ -1,39 +0,0 @@ -using MediaBrowser.Common.Net; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Cryptography; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Net; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Text; - -namespace MediaBrowser.Server.Implementations.HttpServer -{ - /// - /// Class ServerFactory - /// - public static class ServerFactory - { - /// - /// Creates the server. - /// - /// IHttpServer. - public static IHttpServer CreateServer(IServerApplicationHost applicationHost, - ILogManager logManager, - IServerConfigurationManager config, - INetworkManager networkmanager, - IMemoryStreamFactory streamProvider, - string serverName, - string defaultRedirectpath, - ITextEncoding textEncoding, - ISocketFactory socketFactory, - ICryptoProvider cryptoProvider, - IJsonSerializer json, - IXmlSerializer xml) - { - return new HttpListenerHost(applicationHost, logManager, config, serverName, defaultRedirectpath, networkmanager, streamProvider, textEncoding, socketFactory, cryptoProvider, json, xml); - } - } -} diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index 6fb4f9e36..8d5697c49 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -42,21 +42,10 @@ 4 - - ..\ThirdParty\emby\Emby.Common.Implementations.dll - - - ..\packages\Emby.XmlTv.1.0.0.63\lib\portable-net45+win8\Emby.XmlTv.dll - True - ..\packages\ini-parser.2.3.0\lib\net20\INIFileParser.dll True - - ..\packages\MediaBrowser.Naming.1.0.0.59\lib\portable-net45+win8\MediaBrowser.Naming.dll - True - ..\packages\Microsoft.IO.RecyclableMemoryStream.1.1.0.0\lib\net45\Microsoft.IO.RecyclableMemoryStream.dll True @@ -65,38 +54,18 @@ False ..\ThirdParty\emby\Mono.Nat.dll - - ..\packages\Patterns.Logging.1.0.0.6\lib\portable-net45+win8\Patterns.Logging.dll - True - - - ..\packages\ServiceStack.Text.4.5.4\lib\net45\ServiceStack.Text.dll - True - False ..\ThirdParty\SharpCompress\SharpCompress.dll - - ..\ThirdParty\emby\SocketHttpListener.Portable.dll - - - - - ..\ThirdParty\ServiceStack\ServiceStack.dll - - - ..\packages\UniversalDetector.1.0.1\lib\portable-net45+sl4+wp71+win8+wpa81\UniversalDetector.dll - True - @@ -105,12 +74,9 @@ - - - @@ -143,10 +109,8 @@ - - @@ -170,10 +134,6 @@ {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B} MediaBrowser.Model - - {442b5058-dcaf-4263-bb6a-f21e31120a1b} - MediaBrowser.Providers - @@ -348,9 +308,7 @@ - - - + + + + {9142eefa-7570-41e1-bfcc-468bb571af2f} + MediaBrowser.Common + + + {7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b} + MediaBrowser.Model + + + \ No newline at end of file diff --git a/ServiceStack/ServiceStack.nuget.targets b/ServiceStack/ServiceStack.nuget.targets new file mode 100644 index 000000000..e69ce0e64 --- /dev/null +++ b/ServiceStack/ServiceStack.nuget.targets @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/ServiceStack/ServiceStack.xproj b/ServiceStack/ServiceStack.xproj new file mode 100644 index 000000000..ba8f8b8f2 --- /dev/null +++ b/ServiceStack/ServiceStack.xproj @@ -0,0 +1,19 @@ + + + + 14.0.25420 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + b2d733ab-620e-4c53-88a4-4b6638ab6a7a + ServiceStack + .\obj + .\bin\ + + + + 2.0 + + + \ No newline at end of file diff --git a/ServiceStack/ServiceStackHost.Runtime.cs b/ServiceStack/ServiceStackHost.Runtime.cs new file mode 100644 index 000000000..1a1656a0e --- /dev/null +++ b/ServiceStack/ServiceStackHost.Runtime.cs @@ -0,0 +1,74 @@ +// Copyright (c) Service Stack LLC. All Rights Reserved. +// License: https://raw.github.com/ServiceStack/ServiceStack/master/license.txt + + +using MediaBrowser.Model.Services; +using ServiceStack.Support.WebHost; + +namespace ServiceStack +{ + public abstract partial class ServiceStackHost + { + /// + /// Applies the request filters. Returns whether or not the request has been handled + /// and no more processing should be done. + /// + /// + public virtual bool ApplyRequestFilters(IRequest req, IResponse res, object requestDto) + { + if (res.IsClosed) return res.IsClosed; + + //Exec all RequestFilter attributes with Priority < 0 + var attributes = FilterAttributeCache.GetRequestFilterAttributes(requestDto.GetType()); + var i = 0; + for (; i < attributes.Length && attributes[i].Priority < 0; i++) + { + var attribute = attributes[i]; + attribute.RequestFilter(req, res, requestDto); + if (res.IsClosed) return res.IsClosed; + } + + if (res.IsClosed) return res.IsClosed; + + //Exec global filters + foreach (var requestFilter in GlobalRequestFilters) + { + requestFilter(req, res, requestDto); + if (res.IsClosed) return res.IsClosed; + } + + //Exec remaining RequestFilter attributes with Priority >= 0 + for (; i < attributes.Length && attributes[i].Priority >= 0; i++) + { + var attribute = attributes[i]; + attribute.RequestFilter(req, res, requestDto); + if (res.IsClosed) return res.IsClosed; + } + + return res.IsClosed; + } + + /// + /// Applies the response filters. Returns whether or not the request has been handled + /// and no more processing should be done. + /// + /// + public virtual bool ApplyResponseFilters(IRequest req, IResponse res, object response) + { + if (response != null) + { + if (res.IsClosed) return res.IsClosed; + } + + //Exec global filters + foreach (var responseFilter in GlobalResponseFilters) + { + responseFilter(req, res, response); + if (res.IsClosed) return res.IsClosed; + } + + return res.IsClosed; + } + } + +} \ No newline at end of file diff --git a/ServiceStack/ServiceStackHost.cs b/ServiceStack/ServiceStackHost.cs new file mode 100644 index 000000000..8a1db25e4 --- /dev/null +++ b/ServiceStack/ServiceStackHost.cs @@ -0,0 +1,104 @@ +// Copyright (c) Service Stack LLC. All Rights Reserved. +// License: https://raw.github.com/ServiceStack/ServiceStack/master/license.txt + + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Services; +using ServiceStack.Host; + +namespace ServiceStack +{ + public abstract partial class ServiceStackHost : IDisposable + { + public static ServiceStackHost Instance { get; protected set; } + + protected ServiceStackHost(string serviceName) + { + ServiceName = serviceName; + ServiceController = CreateServiceController(); + + RestPaths = new List(); + Metadata = new ServiceMetadata(); + GlobalRequestFilters = new List>(); + GlobalResponseFilters = new List>(); + } + + public abstract void Configure(); + + public abstract object CreateInstance(Type type); + + protected abstract ServiceController CreateServiceController(); + + public virtual ServiceStackHost Init() + { + Instance = this; + + ServiceController.Init(); + Configure(); + + ServiceController.AfterInit(); + + return this; + } + + public virtual ServiceStackHost Start(string urlBase) + { + throw new NotImplementedException("Start(listeningAtUrlBase) is not supported by this AppHost"); + } + + public string ServiceName { get; set; } + + public ServiceMetadata Metadata { get; set; } + + public ServiceController ServiceController { get; set; } + + public List RestPaths = new List(); + + public List> GlobalRequestFilters { get; set; } + + public List> GlobalResponseFilters { get; set; } + + public abstract T TryResolve(); + public abstract T Resolve(); + + public virtual MediaBrowser.Model.Services.RouteAttribute[] GetRouteAttributes(Type requestType) + { + return requestType.AllAttributes(); + } + + public abstract object GetTaskResult(Task task, string requestName); + + public abstract Func GetParseFn(Type propertyType); + + public abstract void SerializeToJson(object o, Stream stream); + public abstract void SerializeToXml(object o, Stream stream); + public abstract object DeserializeXml(Type type, Stream stream); + public abstract object DeserializeJson(Type type, Stream stream); + + public virtual void Dispose() + { + //JsConfig.Reset(); //Clears Runtime Attributes + + Instance = null; + } + + protected abstract ILogger Logger + { + get; + } + + public void OnLogError(Type type, string message) + { + Logger.Error(message); + } + + public void OnLogError(Type type, string message, Exception ex) + { + Logger.ErrorException(message, ex); + } + } +} diff --git a/ServiceStack/StringMapTypeDeserializer.cs b/ServiceStack/StringMapTypeDeserializer.cs new file mode 100644 index 000000000..762e8aaff --- /dev/null +++ b/ServiceStack/StringMapTypeDeserializer.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using System.Linq; +using System.Reflection; + +namespace ServiceStack.Serialization +{ + /// + /// Serializer cache of delegates required to create a type from a string map (e.g. for REST urls) + /// + public class StringMapTypeDeserializer + { + internal class PropertySerializerEntry + { + public PropertySerializerEntry(Action propertySetFn, Func propertyParseStringFn) + { + PropertySetFn = propertySetFn; + PropertyParseStringFn = propertyParseStringFn; + } + + public Action PropertySetFn; + public Func PropertyParseStringFn; + public Type PropertyType; + } + + private readonly Type type; + private readonly Dictionary propertySetterMap + = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public Func GetParseFn(Type propertyType) + { + //Don't JSV-decode string values for string properties + if (propertyType == typeof(string)) + return s => s; + + return ServiceStackHost.Instance.GetParseFn(propertyType); + } + + public StringMapTypeDeserializer(Type type) + { + this.type = type; + + foreach (var propertyInfo in type.GetSerializableProperties()) + { + var propertySetFn = TypeAccessor.GetSetPropertyMethod(type, propertyInfo); + var propertyType = propertyInfo.PropertyType; + var propertyParseStringFn = GetParseFn(propertyType); + var propertySerializer = new PropertySerializerEntry(propertySetFn, propertyParseStringFn) { PropertyType = propertyType }; + + var attr = propertyInfo.AllAttributes().FirstOrDefault(); + if (attr != null && attr.Name != null) + { + propertySetterMap[attr.Name] = propertySerializer; + } + propertySetterMap[propertyInfo.Name] = propertySerializer; + } + } + + public object PopulateFromMap(object instance, IDictionary keyValuePairs) + { + string propertyName = null; + string propertyTextValue = null; + PropertySerializerEntry propertySerializerEntry = null; + + if (instance == null) + instance = ServiceStackHost.Instance.CreateInstance(type); + + foreach (var pair in keyValuePairs.Where(x => !string.IsNullOrEmpty(x.Value))) + { + propertyName = pair.Key; + propertyTextValue = pair.Value; + + if (!propertySetterMap.TryGetValue(propertyName, out propertySerializerEntry)) + { + if (propertyName == "v") + { + continue; + } + + continue; + } + + if (propertySerializerEntry.PropertySetFn == null) + { + continue; + } + + if (propertySerializerEntry.PropertyType == typeof(bool)) + { + //InputExtensions.cs#530 MVC Checkbox helper emits extra hidden input field, generating 2 values, first is the real value + propertyTextValue = LeftPart(propertyTextValue, ','); + } + + var value = propertySerializerEntry.PropertyParseStringFn(propertyTextValue); + if (value == null) + { + continue; + } + propertySerializerEntry.PropertySetFn(instance, value); + } + + return instance; + } + + 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); + } + } + + internal class TypeAccessor + { + public static Action GetSetPropertyMethod(Type type, PropertyInfo propertyInfo) + { + if (!propertyInfo.CanWrite || propertyInfo.GetIndexParameters().Any()) return null; + + var setMethodInfo = propertyInfo.SetMethod; + return (instance, value) => setMethodInfo.Invoke(instance, new[] { value }); + } + } +} diff --git a/ServiceStack/UrlExtensions.cs b/ServiceStack/UrlExtensions.cs new file mode 100644 index 000000000..7b5a50ef1 --- /dev/null +++ b/ServiceStack/UrlExtensions.cs @@ -0,0 +1,33 @@ +using System; + +namespace ServiceStack +{ + /// + /// Donated by Ivan Korneliuk from his post: + /// http://korneliuk.blogspot.com/2012/08/servicestack-reusing-dtos.html + /// + /// Modified to only allow using routes matching the supplied HTTP Verb + /// + public static class UrlExtensions + { + public static string GetOperationName(this Type type) + { + var typeName = type.FullName != null //can be null, e.g. generic types + ? LeftPart(type.FullName, "[[") //Generic Fullname + .Replace(type.Namespace + ".", "") //Trim Namespaces + .Replace("+", ".") //Convert nested into normal type + : type.Name; + + return type.IsGenericParameter ? "'" + typeName : typeName; + } + + public static string LeftPart(string strVal, string needle) + { + if (strVal == null) return null; + var pos = strVal.IndexOf(needle, StringComparison.OrdinalIgnoreCase); + return pos == -1 + ? strVal + : strVal.Substring(0, pos); + } + } +} \ No newline at end of file diff --git a/ServiceStack/packages.config b/ServiceStack/packages.config new file mode 100644 index 000000000..6b8deb9c9 --- /dev/null +++ b/ServiceStack/packages.config @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ServiceStack/project.json b/ServiceStack/project.json new file mode 100644 index 000000000..fbbe9eaf3 --- /dev/null +++ b/ServiceStack/project.json @@ -0,0 +1,17 @@ +{ + "frameworks":{ + "netstandard1.6":{ + "dependencies":{ + "NETStandard.Library":"1.6.0", + } + }, + ".NETPortable,Version=v4.5,Profile=Profile7":{ + "buildOptions": { + "define": [ ] + }, + "frameworkAssemblies":{ + + } + } + } +} \ No newline at end of file diff --git a/SocketHttpListener.Portable/ByteOrder.cs b/SocketHttpListener.Portable/ByteOrder.cs new file mode 100644 index 000000000..f5db52fd7 --- /dev/null +++ b/SocketHttpListener.Portable/ByteOrder.cs @@ -0,0 +1,17 @@ +namespace SocketHttpListener +{ + /// + /// Contains the values that indicate whether the byte order is a Little-endian or Big-endian. + /// + public enum ByteOrder : byte + { + /// + /// Indicates a Little-endian. + /// + Little, + /// + /// Indicates a Big-endian. + /// + Big + } +} diff --git a/SocketHttpListener.Portable/CloseEventArgs.cs b/SocketHttpListener.Portable/CloseEventArgs.cs new file mode 100644 index 000000000..b1bb4b196 --- /dev/null +++ b/SocketHttpListener.Portable/CloseEventArgs.cs @@ -0,0 +1,90 @@ +using System; +using System.Text; + +namespace SocketHttpListener +{ + /// + /// Contains the event data associated with a event. + /// + /// + /// A event occurs when the WebSocket connection has been closed. + /// If you would like to get the reason for the close, you should access the or + /// property. + /// + public class CloseEventArgs : EventArgs + { + #region Private Fields + + private bool _clean; + private ushort _code; + private string _reason; + + #endregion + + #region Internal Constructors + + internal CloseEventArgs (PayloadData payload) + { + var data = payload.ApplicationData; + var len = data.Length; + _code = len > 1 + ? data.SubArray (0, 2).ToUInt16 (ByteOrder.Big) + : (ushort) CloseStatusCode.NoStatusCode; + + _reason = len > 2 + ? GetUtf8String(data.SubArray (2, len - 2)) + : String.Empty; + } + + private string GetUtf8String(byte[] bytes) + { + return Encoding.UTF8.GetString(bytes, 0, bytes.Length); + } + + #endregion + + #region Public Properties + + /// + /// Gets the status code for the close. + /// + /// + /// A that represents the status code for the close if any. + /// + public ushort Code { + get { + return _code; + } + } + + /// + /// Gets the reason for the close. + /// + /// + /// A that represents the reason for the close if any. + /// + public string Reason { + get { + return _reason; + } + } + + /// + /// Gets a value indicating whether the WebSocket connection has been closed cleanly. + /// + /// + /// true if the WebSocket connection has been closed cleanly; otherwise, false. + /// + public bool WasClean { + get { + return _clean; + } + + internal set { + _clean = value; + } + } + + #endregion + } +} diff --git a/SocketHttpListener.Portable/CloseStatusCode.cs b/SocketHttpListener.Portable/CloseStatusCode.cs new file mode 100644 index 000000000..62a268bce --- /dev/null +++ b/SocketHttpListener.Portable/CloseStatusCode.cs @@ -0,0 +1,94 @@ +namespace SocketHttpListener +{ + /// + /// Contains the values of the status code for the WebSocket connection close. + /// + /// + /// + /// The values of the status code are defined in + /// Section 7.4 + /// of RFC 6455. + /// + /// + /// "Reserved value" must not be set as a status code in a close control frame + /// by an endpoint. It's designated for use in applications expecting a status + /// code to indicate that the connection was closed due to the system grounds. + /// + /// + public enum CloseStatusCode : ushort + { + /// + /// Equivalent to close status 1000. + /// Indicates a normal close. + /// + Normal = 1000, + /// + /// Equivalent to close status 1001. + /// Indicates that an endpoint is going away. + /// + Away = 1001, + /// + /// Equivalent to close status 1002. + /// Indicates that an endpoint is terminating the connection due to a protocol error. + /// + ProtocolError = 1002, + /// + /// Equivalent to close status 1003. + /// Indicates that an endpoint is terminating the connection because it has received + /// an unacceptable type message. + /// + IncorrectData = 1003, + /// + /// Equivalent to close status 1004. + /// Still undefined. Reserved value. + /// + Undefined = 1004, + /// + /// Equivalent to close status 1005. + /// Indicates that no status code was actually present. Reserved value. + /// + NoStatusCode = 1005, + /// + /// Equivalent to close status 1006. + /// Indicates that the connection was closed abnormally. Reserved value. + /// + Abnormal = 1006, + /// + /// Equivalent to close status 1007. + /// Indicates that an endpoint is terminating the connection because it has received + /// a message that contains a data that isn't consistent with the type of the message. + /// + InconsistentData = 1007, + /// + /// Equivalent to close status 1008. + /// Indicates that an endpoint is terminating the connection because it has received + /// a message that violates its policy. + /// + PolicyViolation = 1008, + /// + /// Equivalent to close status 1009. + /// Indicates that an endpoint is terminating the connection because it has received + /// a message that is too big to process. + /// + TooBig = 1009, + /// + /// Equivalent to close status 1010. + /// Indicates that the client is terminating the connection because it has expected + /// the server to negotiate one or more extension, but the server didn't return them + /// in the handshake response. + /// + IgnoreExtension = 1010, + /// + /// Equivalent to close status 1011. + /// Indicates that the server is terminating the connection because it has encountered + /// an unexpected condition that prevented it from fulfilling the request. + /// + ServerError = 1011, + /// + /// Equivalent to close status 1015. + /// Indicates that the connection was closed due to a failure to perform a TLS handshake. + /// Reserved value. + /// + TlsHandshakeFailure = 1015 + } +} diff --git a/SocketHttpListener.Portable/CompressionMethod.cs b/SocketHttpListener.Portable/CompressionMethod.cs new file mode 100644 index 000000000..36a48d94c --- /dev/null +++ b/SocketHttpListener.Portable/CompressionMethod.cs @@ -0,0 +1,23 @@ +namespace SocketHttpListener +{ + /// + /// Contains the values of the compression method used to compress the message on the WebSocket + /// connection. + /// + /// + /// The values of the compression method are defined in + /// Compression + /// Extensions for WebSocket. + /// + public enum CompressionMethod : byte + { + /// + /// Indicates non compression. + /// + None, + /// + /// Indicates using DEFLATE. + /// + Deflate + } +} diff --git a/SocketHttpListener.Portable/ErrorEventArgs.cs b/SocketHttpListener.Portable/ErrorEventArgs.cs new file mode 100644 index 000000000..bf1d6fc95 --- /dev/null +++ b/SocketHttpListener.Portable/ErrorEventArgs.cs @@ -0,0 +1,46 @@ +using System; + +namespace SocketHttpListener +{ + /// + /// Contains the event data associated with a event. + /// + /// + /// A event occurs when the gets an error. + /// If you would like to get the error message, you should access the + /// property. + /// + public class ErrorEventArgs : EventArgs + { + #region Private Fields + + private string _message; + + #endregion + + #region Internal Constructors + + internal ErrorEventArgs (string message) + { + _message = message; + } + + #endregion + + #region Public Properties + + /// + /// Gets the error message. + /// + /// + /// A that represents the error message. + /// + public string Message { + get { + return _message; + } + } + + #endregion + } +} diff --git a/SocketHttpListener.Portable/Ext.cs b/SocketHttpListener.Portable/Ext.cs new file mode 100644 index 000000000..303263d0b --- /dev/null +++ b/SocketHttpListener.Portable/Ext.cs @@ -0,0 +1,1089 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.IO; +using System.IO.Compression; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using MediaBrowser.Model.Services; +using SocketHttpListener.Net; +using HttpListenerResponse = SocketHttpListener.Net.HttpListenerResponse; +using HttpStatusCode = SocketHttpListener.Net.HttpStatusCode; + +namespace SocketHttpListener +{ + /// + /// Provides a set of static methods for the websocket-sharp. + /// + public static class Ext + { + #region Private Const Fields + + private const string _tspecials = "()<>@,;:\\\"/[]?={} \t"; + + #endregion + + #region Private Methods + + private static MemoryStream compress(this Stream stream) + { + var output = new MemoryStream(); + if (stream.Length == 0) + return output; + + stream.Position = 0; + using (var ds = new DeflateStream(output, CompressionMode.Compress, true)) + { + stream.CopyTo(ds); + //ds.Close(); // "BFINAL" set to 1. + output.Position = 0; + + return output; + } + } + + private static byte[] decompress(this byte[] value) + { + if (value.Length == 0) + return value; + + using (var input = new MemoryStream(value)) + { + return input.decompressToArray(); + } + } + + private static MemoryStream decompress(this Stream stream) + { + var output = new MemoryStream(); + if (stream.Length == 0) + return output; + + stream.Position = 0; + using (var ds = new DeflateStream(stream, CompressionMode.Decompress, true)) + { + ds.CopyTo(output, true); + return output; + } + } + + private static byte[] decompressToArray(this Stream stream) + { + using (var decomp = stream.decompress()) + { + return decomp.ToArray(); + } + } + + private static byte[] readBytes(this Stream stream, byte[] buffer, int offset, int length) + { + var len = stream.Read(buffer, offset, length); + if (len < 1) + return buffer.SubArray(0, offset); + + var tmp = 0; + while (len < length) + { + tmp = stream.Read(buffer, offset + len, length - len); + if (tmp < 1) + break; + + len += tmp; + } + + return len < length + ? buffer.SubArray(0, offset + len) + : buffer; + } + + private static bool readBytes( + this Stream stream, byte[] buffer, int offset, int length, Stream dest) + { + var bytes = stream.readBytes(buffer, offset, length); + var len = bytes.Length; + dest.Write(bytes, 0, len); + + return len == offset + length; + } + + #endregion + + #region Internal Methods + + internal static byte[] Append(this ushort code, string reason) + { + using (var buffer = new MemoryStream()) + { + var tmp = code.ToByteArrayInternally(ByteOrder.Big); + buffer.Write(tmp, 0, 2); + if (reason != null && reason.Length > 0) + { + tmp = Encoding.UTF8.GetBytes(reason); + buffer.Write(tmp, 0, tmp.Length); + } + + return buffer.ToArray(); + } + } + + internal static string CheckIfClosable(this WebSocketState state) + { + return state == WebSocketState.Closing + ? "While closing the WebSocket connection." + : state == WebSocketState.Closed + ? "The WebSocket connection has already been closed." + : null; + } + + internal static string CheckIfOpen(this WebSocketState state) + { + return state == WebSocketState.Connecting + ? "A WebSocket connection isn't established." + : state == WebSocketState.Closing + ? "While closing the WebSocket connection." + : state == WebSocketState.Closed + ? "The WebSocket connection has already been closed." + : null; + } + + internal static string CheckIfValidControlData(this byte[] data, string paramName) + { + return data.Length > 125 + ? String.Format("'{0}' length must be less.", paramName) + : null; + } + + internal static string CheckIfValidSendData(this byte[] data) + { + return data == null + ? "'data' must not be null." + : null; + } + + internal static string CheckIfValidSendData(this string data) + { + return data == null + ? "'data' must not be null." + : null; + } + + internal static void Close(this HttpListenerResponse response, HttpStatusCode code) + { + response.StatusCode = (int)code; + response.OutputStream.Dispose(); + } + + internal static Stream Compress(this Stream stream, CompressionMethod method) + { + return method == CompressionMethod.Deflate + ? stream.compress() + : stream; + } + + internal static bool Contains(this IEnumerable source, Func condition) + { + foreach (T elm in source) + if (condition(elm)) + return true; + + return false; + } + + internal static void CopyTo(this Stream src, Stream dest, bool setDefaultPosition) + { + var readLen = 0; + var bufferLen = 256; + var buffer = new byte[bufferLen]; + while ((readLen = src.Read(buffer, 0, bufferLen)) > 0) + { + dest.Write(buffer, 0, readLen); + } + + if (setDefaultPosition) + dest.Position = 0; + } + + internal static byte[] Decompress(this byte[] value, CompressionMethod method) + { + return method == CompressionMethod.Deflate + ? value.decompress() + : value; + } + + internal static byte[] DecompressToArray(this Stream stream, CompressionMethod method) + { + return method == CompressionMethod.Deflate + ? stream.decompressToArray() + : stream.ToByteArray(); + } + + /// + /// Determines whether the specified equals the specified , + /// and invokes the specified Action<int> delegate at the same time. + /// + /// + /// true if equals ; + /// otherwise, false. + /// + /// + /// An to compare. + /// + /// + /// A to compare. + /// + /// + /// An Action<int> delegate that references the method(s) called at + /// the same time as comparing. An parameter to pass to + /// the method(s) is . + /// + /// + /// isn't between 0 and 255. + /// + internal static bool EqualsWith(this int value, char c, Action action) + { + if (value < 0 || value > 255) + throw new ArgumentOutOfRangeException("value"); + + action(value); + return value == c - 0; + } + + internal static string GetMessage(this CloseStatusCode code) + { + return code == CloseStatusCode.ProtocolError + ? "A WebSocket protocol error has occurred." + : code == CloseStatusCode.IncorrectData + ? "An incorrect data has been received." + : code == CloseStatusCode.Abnormal + ? "An exception has occurred." + : code == CloseStatusCode.InconsistentData + ? "An inconsistent data has been received." + : code == CloseStatusCode.PolicyViolation + ? "A policy violation has occurred." + : code == CloseStatusCode.TooBig + ? "A too big data has been received." + : code == CloseStatusCode.IgnoreExtension + ? "WebSocket client did not receive expected extension(s)." + : code == CloseStatusCode.ServerError + ? "WebSocket server got an internal error." + : code == CloseStatusCode.TlsHandshakeFailure + ? "An error has occurred while handshaking." + : String.Empty; + } + + internal static string GetNameInternal(this string nameAndValue, string separator) + { + var i = nameAndValue.IndexOf(separator); + return i > 0 + ? nameAndValue.Substring(0, i).Trim() + : null; + } + + internal static string GetValueInternal(this string nameAndValue, string separator) + { + var i = nameAndValue.IndexOf(separator); + return i >= 0 && i < nameAndValue.Length - 1 + ? nameAndValue.Substring(i + 1).Trim() + : null; + } + + internal static bool IsCompressionExtension(this string value, CompressionMethod method) + { + return value.StartsWith(method.ToExtensionString()); + } + + internal static bool IsPortNumber(this int value) + { + return value > 0 && value < 65536; + } + + internal static bool IsReserved(this ushort code) + { + return code == (ushort)CloseStatusCode.Undefined || + code == (ushort)CloseStatusCode.NoStatusCode || + code == (ushort)CloseStatusCode.Abnormal || + code == (ushort)CloseStatusCode.TlsHandshakeFailure; + } + + internal static bool IsReserved(this CloseStatusCode code) + { + return code == CloseStatusCode.Undefined || + code == CloseStatusCode.NoStatusCode || + code == CloseStatusCode.Abnormal || + code == CloseStatusCode.TlsHandshakeFailure; + } + + internal static bool IsText(this string value) + { + var len = value.Length; + for (var i = 0; i < len; i++) + { + char c = value[i]; + if (c < 0x20 && !"\r\n\t".Contains(c)) + return false; + + if (c == 0x7f) + return false; + + if (c == '\n' && ++i < len) + { + c = value[i]; + if (!" \t".Contains(c)) + return false; + } + } + + return true; + } + + internal static bool IsToken(this string value) + { + foreach (char c in value) + if (c < 0x20 || c >= 0x7f || _tspecials.Contains(c)) + return false; + + return true; + } + + internal static string Quote(this string value) + { + return value.IsToken() + ? value + : String.Format("\"{0}\"", value.Replace("\"", "\\\"")); + } + + internal static byte[] ReadBytes(this Stream stream, int length) + { + return stream.readBytes(new byte[length], 0, length); + } + + internal static byte[] ReadBytes(this Stream stream, long length, int bufferLength) + { + using (var result = new MemoryStream()) + { + var count = length / bufferLength; + var rem = (int)(length % bufferLength); + + var buffer = new byte[bufferLength]; + var end = false; + for (long i = 0; i < count; i++) + { + if (!stream.readBytes(buffer, 0, bufferLength, result)) + { + end = true; + break; + } + } + + if (!end && rem > 0) + stream.readBytes(new byte[rem], 0, rem, result); + + return result.ToArray(); + } + } + + internal static async Task ReadBytesAsync(this Stream stream, int length) + { + var buffer = new byte[length]; + + var len = await stream.ReadAsync(buffer, 0, length).ConfigureAwait(false); + var bytes = len < 1 + ? new byte[0] + : len < length + ? stream.readBytes(buffer, len, length - len) + : buffer; + + return bytes; + } + + internal static string RemovePrefix(this string value, params string[] prefixes) + { + var i = 0; + foreach (var prefix in prefixes) + { + if (value.StartsWith(prefix)) + { + i = prefix.Length; + break; + } + } + + return i > 0 + ? value.Substring(i) + : value; + } + + internal static T[] Reverse(this T[] array) + { + var len = array.Length; + T[] reverse = new T[len]; + + var end = len - 1; + for (var i = 0; i <= end; i++) + reverse[i] = array[end - i]; + + return reverse; + } + + internal static IEnumerable SplitHeaderValue( + this string value, params char[] separator) + { + var len = value.Length; + var separators = new string(separator); + + var buffer = new StringBuilder(32); + var quoted = false; + var escaped = false; + + char c; + for (var i = 0; i < len; i++) + { + c = value[i]; + if (c == '"') + { + if (escaped) + escaped = !escaped; + else + quoted = !quoted; + } + else if (c == '\\') + { + if (i < len - 1 && value[i + 1] == '"') + escaped = true; + } + else if (separators.Contains(c)) + { + if (!quoted) + { + yield return buffer.ToString(); + buffer.Length = 0; + + continue; + } + } + else { + } + + buffer.Append(c); + } + + if (buffer.Length > 0) + yield return buffer.ToString(); + } + + internal static byte[] ToByteArray(this Stream stream) + { + using (var output = new MemoryStream()) + { + stream.Position = 0; + stream.CopyTo(output); + + return output.ToArray(); + } + } + + internal static byte[] ToByteArrayInternally(this ushort value, ByteOrder order) + { + var bytes = BitConverter.GetBytes(value); + if (!order.IsHostOrder()) + Array.Reverse(bytes); + + return bytes; + } + + internal static byte[] ToByteArrayInternally(this ulong value, ByteOrder order) + { + var bytes = BitConverter.GetBytes(value); + if (!order.IsHostOrder()) + Array.Reverse(bytes); + + return bytes; + } + + internal static string ToExtensionString( + this CompressionMethod method, params string[] parameters) + { + if (method == CompressionMethod.None) + return String.Empty; + + var m = String.Format("permessage-{0}", method.ToString().ToLower()); + if (parameters == null || parameters.Length == 0) + return m; + + return String.Format("{0}; {1}", m, parameters.ToString("; ")); + } + + internal static List ToList(this IEnumerable source) + { + return new List(source); + } + + internal static ushort ToUInt16(this byte[] src, ByteOrder srcOrder) + { + return BitConverter.ToUInt16(src.ToHostOrder(srcOrder), 0); + } + + internal static ulong ToUInt64(this byte[] src, ByteOrder srcOrder) + { + return BitConverter.ToUInt64(src.ToHostOrder(srcOrder), 0); + } + + internal static string TrimEndSlash(this string value) + { + value = value.TrimEnd('/'); + return value.Length > 0 + ? value + : "/"; + } + + internal static string Unquote(this string value) + { + var start = value.IndexOf('\"'); + var end = value.LastIndexOf('\"'); + if (start < end) + value = value.Substring(start + 1, end - start - 1).Replace("\\\"", "\""); + + return value.Trim(); + } + + internal static void WriteBytes(this Stream stream, byte[] value) + { + using (var src = new MemoryStream(value)) + { + src.CopyTo(stream); + } + } + + #endregion + + #region Public Methods + + /// + /// Determines whether the specified contains any of characters + /// in the specified array of . + /// + /// + /// true if contains any of ; + /// otherwise, false. + /// + /// + /// A to test. + /// + /// + /// An array of that contains characters to find. + /// + public static bool Contains(this string value, params char[] chars) + { + return chars == null || chars.Length == 0 + ? true + : value == null || value.Length == 0 + ? false + : value.IndexOfAny(chars) != -1; + } + + /// + /// Determines whether the specified contains the entry + /// with the specified . + /// + /// + /// true if contains the entry + /// with ; otherwise, false. + /// + /// + /// A to test. + /// + /// + /// A that represents the key of the entry to find. + /// + public static bool Contains(this QueryParamCollection collection, string name) + { + return collection == null || collection.Count == 0 + ? false + : collection[name] != null; + } + + /// + /// Determines whether the specified contains the entry + /// with the specified both and . + /// + /// + /// true if contains the entry + /// with both and ; + /// otherwise, false. + /// + /// + /// A to test. + /// + /// + /// A that represents the key of the entry to find. + /// + /// + /// A that represents the value of the entry to find. + /// + public static bool Contains(this QueryParamCollection collection, string name, string value) + { + if (collection == null || collection.Count == 0) + return false; + + var values = collection[name]; + if (values == null) + return false; + + foreach (var v in values.Split(',')) + if (v.Trim().Equals(value, StringComparison.OrdinalIgnoreCase)) + return true; + + return false; + } + + /// + /// Emits the specified delegate if it isn't . + /// + /// + /// A to emit. + /// + /// + /// An from which emits this . + /// + /// + /// A that contains no event data. + /// + public static void Emit(this EventHandler eventHandler, object sender, EventArgs e) + { + if (eventHandler != null) + eventHandler(sender, e); + } + + /// + /// Emits the specified EventHandler<TEventArgs> delegate + /// if it isn't . + /// + /// + /// An EventHandler<TEventArgs> to emit. + /// + /// + /// An from which emits this . + /// + /// + /// A TEventArgs that represents the event data. + /// + /// + /// The type of the event data generated by the event. + /// + public static void Emit( + this EventHandler eventHandler, object sender, TEventArgs e) + where TEventArgs : EventArgs + { + if (eventHandler != null) + eventHandler(sender, e); + } + + /// + /// Gets the collection of the HTTP cookies from the specified HTTP . + /// + /// + /// A that receives a collection of the HTTP cookies. + /// + /// + /// A that contains a collection of the HTTP headers. + /// + /// + /// true if is a collection of the response headers; + /// otherwise, false. + /// + public static CookieCollection GetCookies(this QueryParamCollection headers, bool response) + { + var name = response ? "Set-Cookie" : "Cookie"; + return headers == null || !headers.Contains(name) + ? new CookieCollection() + : CookieHelper.Parse(headers[name], response); + } + + /// + /// Gets the description of the specified HTTP status . + /// + /// + /// A that represents the description of the HTTP status code. + /// + /// + /// One of enum values, indicates the HTTP status codes. + /// + public static string GetDescription(this HttpStatusCode code) + { + return ((int)code).GetStatusDescription(); + } + + /// + /// Gets the name from the specified that contains a pair of name and + /// value separated by a separator string. + /// + /// + /// A that represents the name if any; otherwise, null. + /// + /// + /// A that contains a pair of name and value separated by a separator + /// string. + /// + /// + /// A that represents a separator string. + /// + public static string GetName(this string nameAndValue, string separator) + { + return (nameAndValue != null && nameAndValue.Length > 0) && + (separator != null && separator.Length > 0) + ? nameAndValue.GetNameInternal(separator) + : null; + } + + /// + /// Gets the name and value from the specified that contains a pair of + /// name and value separated by a separator string. + /// + /// + /// A KeyValuePair<string, string> that represents the name and value if any. + /// + /// + /// A that contains a pair of name and value separated by a separator + /// string. + /// + /// + /// A that represents a separator string. + /// + public static KeyValuePair GetNameAndValue( + this string nameAndValue, string separator) + { + var name = nameAndValue.GetName(separator); + var value = nameAndValue.GetValue(separator); + return name != null + ? new KeyValuePair(name, value) + : new KeyValuePair(null, null); + } + + /// + /// Gets the description of the specified HTTP status . + /// + /// + /// A that represents the description of the HTTP status code. + /// + /// + /// An that represents the HTTP status code. + /// + public static string GetStatusDescription(this int code) + { + switch (code) + { + case 100: return "Continue"; + case 101: return "Switching Protocols"; + case 102: return "Processing"; + case 200: return "OK"; + case 201: return "Created"; + case 202: return "Accepted"; + case 203: return "Non-Authoritative Information"; + case 204: return "No Content"; + case 205: return "Reset Content"; + case 206: return "Partial Content"; + case 207: return "Multi-Status"; + case 300: return "Multiple Choices"; + case 301: return "Moved Permanently"; + case 302: return "Found"; + case 303: return "See Other"; + case 304: return "Not Modified"; + case 305: return "Use Proxy"; + case 307: return "Temporary Redirect"; + case 400: return "Bad Request"; + case 401: return "Unauthorized"; + case 402: return "Payment Required"; + case 403: return "Forbidden"; + case 404: return "Not Found"; + case 405: return "Method Not Allowed"; + case 406: return "Not Acceptable"; + case 407: return "Proxy Authentication Required"; + case 408: return "Request Timeout"; + case 409: return "Conflict"; + case 410: return "Gone"; + case 411: return "Length Required"; + case 412: return "Precondition Failed"; + case 413: return "Request Entity Too Large"; + case 414: return "Request-Uri Too Long"; + case 415: return "Unsupported Media Type"; + case 416: return "Requested Range Not Satisfiable"; + case 417: return "Expectation Failed"; + case 422: return "Unprocessable Entity"; + case 423: return "Locked"; + case 424: return "Failed Dependency"; + case 500: return "Internal Server Error"; + case 501: return "Not Implemented"; + case 502: return "Bad Gateway"; + case 503: return "Service Unavailable"; + case 504: return "Gateway Timeout"; + case 505: return "Http Version Not Supported"; + case 507: return "Insufficient Storage"; + } + + return String.Empty; + } + + /// + /// Gets the value from the specified that contains a pair of name and + /// value separated by a separator string. + /// + /// + /// A that represents the value if any; otherwise, null. + /// + /// + /// A that contains a pair of name and value separated by a separator + /// string. + /// + /// + /// A that represents a separator string. + /// + public static string GetValue(this string nameAndValue, string separator) + { + return (nameAndValue != null && nameAndValue.Length > 0) && + (separator != null && separator.Length > 0) + ? nameAndValue.GetValueInternal(separator) + : null; + } + + /// + /// Determines whether the specified is host + /// (this computer architecture) byte order. + /// + /// + /// true if is host byte order; + /// otherwise, false. + /// + /// + /// One of the enum values, to test. + /// + public static bool IsHostOrder(this ByteOrder order) + { + // true : !(true ^ true) or !(false ^ false) + // false: !(true ^ false) or !(false ^ true) + return !(BitConverter.IsLittleEndian ^ (order == ByteOrder.Little)); + } + + /// + /// Determines whether the specified is a predefined scheme. + /// + /// + /// true if is a predefined scheme; otherwise, false. + /// + /// + /// A to test. + /// + public static bool IsPredefinedScheme(this string value) + { + if (value == null || value.Length < 2) + return false; + + var c = value[0]; + if (c == 'h') + return value == "http" || value == "https"; + + if (c == 'w') + return value == "ws" || value == "wss"; + + if (c == 'f') + return value == "file" || value == "ftp"; + + if (c == 'n') + { + c = value[1]; + return c == 'e' + ? value == "news" || value == "net.pipe" || value == "net.tcp" + : value == "nntp"; + } + + return (c == 'g' && value == "gopher") || (c == 'm' && value == "mailto"); + } + + /// + /// Determines whether the specified is a URI string. + /// + /// + /// true if may be a URI string; otherwise, false. + /// + /// + /// A to test. + /// + public static bool MaybeUri(this string value) + { + if (value == null || value.Length == 0) + return false; + + var i = value.IndexOf(':'); + if (i == -1) + return false; + + if (i >= 10) + return false; + + return value.Substring(0, i).IsPredefinedScheme(); + } + + /// + /// Retrieves a sub-array from the specified . + /// A sub-array starts at the specified element position. + /// + /// + /// An array of T that receives a sub-array, or an empty array of T if any problems + /// with the parameters. + /// + /// + /// An array of T that contains the data to retrieve a sub-array. + /// + /// + /// An that contains the zero-based starting position of a sub-array + /// in . + /// + /// + /// An that contains the number of elements to retrieve a sub-array. + /// + /// + /// The type of elements in the . + /// + public static T[] SubArray(this T[] array, int startIndex, int length) + { + if (array == null || array.Length == 0) + return new T[0]; + + if (startIndex < 0 || length <= 0) + return new T[0]; + + if (startIndex + length > array.Length) + return new T[0]; + + if (startIndex == 0 && array.Length == length) + return array; + + T[] subArray = new T[length]; + Array.Copy(array, startIndex, subArray, 0, length); + + return subArray; + } + + /// + /// Converts the order of the specified array of to the host byte order. + /// + /// + /// An array of converted from . + /// + /// + /// An array of to convert. + /// + /// + /// One of the enum values, indicates the byte order of + /// . + /// + /// + /// is . + /// + public static byte[] ToHostOrder(this byte[] src, ByteOrder srcOrder) + { + if (src == null) + throw new ArgumentNullException("src"); + + return src.Length > 1 && !srcOrder.IsHostOrder() + ? src.Reverse() + : src; + } + + /// + /// Converts the specified to a that + /// concatenates the each element of across the specified + /// . + /// + /// + /// A converted from , + /// or if is empty. + /// + /// + /// An array of T to convert. + /// + /// + /// A that represents the separator string. + /// + /// + /// The type of elements in . + /// + /// + /// is . + /// + public static string ToString(this T[] array, string separator) + { + if (array == null) + throw new ArgumentNullException("array"); + + var len = array.Length; + if (len == 0) + return String.Empty; + + if (separator == null) + separator = String.Empty; + + var buff = new StringBuilder(64); + (len - 1).Times(i => buff.AppendFormat("{0}{1}", array[i].ToString(), separator)); + + buff.Append(array[len - 1].ToString()); + return buff.ToString(); + } + + /// + /// Executes the specified Action<int> delegate times. + /// + /// + /// An is the number of times to execute. + /// + /// + /// An Action<int> delegate that references the method(s) to execute. + /// An parameter to pass to the method(s) is the zero-based count of + /// iteration. + /// + public static void Times(this int n, Action action) + { + if (n > 0 && action != null) + for (int i = 0; i < n; i++) + action(i); + } + + /// + /// Converts the specified to a . + /// + /// + /// A converted from , or + /// if isn't successfully converted. + /// + /// + /// A to convert. + /// + public static Uri ToUri(this string uriString) + { + Uri res; + return Uri.TryCreate( + uriString, uriString.MaybeUri() ? UriKind.Absolute : UriKind.Relative, out res) + ? res + : null; + } + + /// + /// URL-decodes the specified . + /// + /// + /// A that receives the decoded string, or the + /// if it's or empty. + /// + /// + /// A to decode. + /// + public static string UrlDecode(this string value) + { + return value == null || value.Length == 0 + ? value + : WebUtility.UrlDecode(value); + } + + #endregion + } +} \ No newline at end of file diff --git a/SocketHttpListener.Portable/Fin.cs b/SocketHttpListener.Portable/Fin.cs new file mode 100644 index 000000000..f91401b99 --- /dev/null +++ b/SocketHttpListener.Portable/Fin.cs @@ -0,0 +1,8 @@ +namespace SocketHttpListener +{ + internal enum Fin : byte + { + More = 0x0, + Final = 0x1 + } +} diff --git a/SocketHttpListener.Portable/HttpBase.cs b/SocketHttpListener.Portable/HttpBase.cs new file mode 100644 index 000000000..5172ba497 --- /dev/null +++ b/SocketHttpListener.Portable/HttpBase.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.IO; +using System.Text; +using System.Threading; +using MediaBrowser.Model.Services; + +namespace SocketHttpListener +{ + internal abstract class HttpBase + { + #region Private Fields + + private QueryParamCollection _headers; + private Version _version; + + #endregion + + #region Internal Fields + + internal byte[] EntityBodyData; + + #endregion + + #region Protected Fields + + protected const string CrLf = "\r\n"; + + #endregion + + #region Protected Constructors + + protected HttpBase(Version version, QueryParamCollection headers) + { + _version = version; + _headers = headers; + } + + #endregion + + #region Public Properties + + public string EntityBody + { + get + { + var data = EntityBodyData; + + return data != null && data.Length > 0 + ? getEncoding(_headers["Content-Type"]).GetString(data, 0, data.Length) + : String.Empty; + } + } + + public QueryParamCollection Headers + { + get + { + return _headers; + } + } + + public Version ProtocolVersion + { + get + { + return _version; + } + } + + #endregion + + #region Private Methods + + private static Encoding getEncoding(string contentType) + { + if (contentType == null || contentType.Length == 0) + return Encoding.UTF8; + + var i = contentType.IndexOf("charset=", StringComparison.Ordinal); + if (i == -1) + return Encoding.UTF8; + + var charset = contentType.Substring(i + 8); + i = charset.IndexOf(';'); + if (i != -1) + charset = charset.Substring(0, i).TrimEnd(); + + return Encoding.GetEncoding(charset.Trim('"')); + } + + #endregion + + #region Public Methods + + public byte[] ToByteArray() + { + return Encoding.UTF8.GetBytes(ToString()); + } + + #endregion + } +} \ No newline at end of file diff --git a/SocketHttpListener.Portable/HttpResponse.cs b/SocketHttpListener.Portable/HttpResponse.cs new file mode 100644 index 000000000..5aca28c7c --- /dev/null +++ b/SocketHttpListener.Portable/HttpResponse.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Specialized; +using System.IO; +using System.Net; +using System.Text; +using HttpStatusCode = SocketHttpListener.Net.HttpStatusCode; +using HttpVersion = SocketHttpListener.Net.HttpVersion; +using System.Linq; +using MediaBrowser.Model.Services; + +namespace SocketHttpListener +{ + internal class HttpResponse : HttpBase + { + #region Private Fields + + private string _code; + private string _reason; + + #endregion + + #region Private Constructors + + private HttpResponse(string code, string reason, Version version, QueryParamCollection headers) + : base(version, headers) + { + _code = code; + _reason = reason; + } + + #endregion + + #region Internal Constructors + + internal HttpResponse(HttpStatusCode code) + : this(code, code.GetDescription()) + { + } + + internal HttpResponse(HttpStatusCode code, string reason) + : this(((int)code).ToString(), reason, HttpVersion.Version11, new QueryParamCollection()) + { + Headers["Server"] = "websocket-sharp/1.0"; + } + + #endregion + + #region Public Properties + + public CookieCollection Cookies + { + get + { + return Headers.GetCookies(true); + } + } + + public bool IsProxyAuthenticationRequired + { + get + { + return _code == "407"; + } + } + + public bool IsUnauthorized + { + get + { + return _code == "401"; + } + } + + public bool IsWebSocketResponse + { + get + { + var headers = Headers; + return ProtocolVersion > HttpVersion.Version10 && + _code == "101" && + headers.Contains("Upgrade", "websocket") && + headers.Contains("Connection", "Upgrade"); + } + } + + public string Reason + { + get + { + return _reason; + } + } + + public string StatusCode + { + get + { + return _code; + } + } + + #endregion + + #region Internal Methods + + internal static HttpResponse CreateCloseResponse(HttpStatusCode code) + { + var res = new HttpResponse(code); + res.Headers["Connection"] = "close"; + + return res; + } + + internal static HttpResponse CreateWebSocketResponse() + { + var res = new HttpResponse(HttpStatusCode.SwitchingProtocols); + + var headers = res.Headers; + headers["Upgrade"] = "websocket"; + headers["Connection"] = "Upgrade"; + + return res; + } + + #endregion + + #region Public Methods + + public void SetCookies(CookieCollection cookies) + { + if (cookies == null || cookies.Count == 0) + return; + + var headers = Headers; + var sorted = cookies.OfType().OrderBy(i => i.Name).ToList(); + + foreach (var cookie in sorted) + headers.Add("Set-Cookie", cookie.ToString()); + } + + public override string ToString() + { + var output = new StringBuilder(64); + output.AppendFormat("HTTP/{0} {1} {2}{3}", ProtocolVersion, _code, _reason, CrLf); + + var headers = Headers; + foreach (var key in headers.Keys) + output.AppendFormat("{0}: {1}{2}", key, headers[key], CrLf); + + output.Append(CrLf); + + var entity = EntityBody; + if (entity.Length > 0) + output.Append(entity); + + return output.ToString(); + } + + #endregion + } +} \ No newline at end of file diff --git a/SocketHttpListener.Portable/Mask.cs b/SocketHttpListener.Portable/Mask.cs new file mode 100644 index 000000000..adc2f098e --- /dev/null +++ b/SocketHttpListener.Portable/Mask.cs @@ -0,0 +1,8 @@ +namespace SocketHttpListener +{ + internal enum Mask : byte + { + Unmask = 0x0, + Mask = 0x1 + } +} diff --git a/SocketHttpListener.Portable/MessageEventArgs.cs b/SocketHttpListener.Portable/MessageEventArgs.cs new file mode 100644 index 000000000..9dbadb9ab --- /dev/null +++ b/SocketHttpListener.Portable/MessageEventArgs.cs @@ -0,0 +1,96 @@ +using System; +using System.Text; + +namespace SocketHttpListener +{ + /// + /// Contains the event data associated with a event. + /// + /// + /// A event occurs when the receives + /// a text or binary data frame. + /// If you want to get the received data, you access the or + /// property. + /// + public class MessageEventArgs : EventArgs + { + #region Private Fields + + private string _data; + private Opcode _opcode; + private byte[] _rawData; + + #endregion + + #region Internal Constructors + + internal MessageEventArgs (Opcode opcode, byte[] data) + { + _opcode = opcode; + _rawData = data; + _data = convertToString (opcode, data); + } + + internal MessageEventArgs (Opcode opcode, PayloadData payload) + { + _opcode = opcode; + _rawData = payload.ApplicationData; + _data = convertToString (opcode, _rawData); + } + + #endregion + + #region Public Properties + + /// + /// Gets the received data as a . + /// + /// + /// A that contains the received data. + /// + public string Data { + get { + return _data; + } + } + + /// + /// Gets the received data as an array of . + /// + /// + /// An array of that contains the received data. + /// + public byte [] RawData { + get { + return _rawData; + } + } + + /// + /// Gets the type of the received data. + /// + /// + /// One of the values, indicates the type of the received data. + /// + public Opcode Type { + get { + return _opcode; + } + } + + #endregion + + #region Private Methods + + private static string convertToString (Opcode opcode, byte [] data) + { + return data.Length == 0 + ? String.Empty + : opcode == Opcode.Text + ? Encoding.UTF8.GetString (data, 0, data.Length) + : opcode.ToString (); + } + + #endregion + } +} diff --git a/SocketHttpListener.Portable/Net/AuthenticationSchemeSelector.cs b/SocketHttpListener.Portable/Net/AuthenticationSchemeSelector.cs new file mode 100644 index 000000000..c6e7e538e --- /dev/null +++ b/SocketHttpListener.Portable/Net/AuthenticationSchemeSelector.cs @@ -0,0 +1,6 @@ +using System.Net; + +namespace SocketHttpListener.Net +{ + public delegate AuthenticationSchemes AuthenticationSchemeSelector(HttpListenerRequest httpRequest); +} diff --git a/SocketHttpListener.Portable/Net/ChunkStream.cs b/SocketHttpListener.Portable/Net/ChunkStream.cs new file mode 100644 index 000000000..3f3b4a667 --- /dev/null +++ b/SocketHttpListener.Portable/Net/ChunkStream.cs @@ -0,0 +1,371 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Net; +using System.Text; + +namespace SocketHttpListener.Net +{ + class ChunkStream + { + enum State + { + None, + PartialSize, + Body, + BodyFinished, + Trailer + } + + class Chunk + { + public byte[] Bytes; + public int Offset; + + public Chunk(byte[] chunk) + { + this.Bytes = chunk; + } + + public int Read(byte[] buffer, int offset, int size) + { + int nread = (size > Bytes.Length - Offset) ? Bytes.Length - Offset : size; + Buffer.BlockCopy(Bytes, Offset, buffer, offset, nread); + Offset += nread; + return nread; + } + } + + internal WebHeaderCollection headers; + int chunkSize; + int chunkRead; + int totalWritten; + State state; + //byte [] waitBuffer; + StringBuilder saved; + bool sawCR; + bool gotit; + int trailerState; + List chunks; + + public ChunkStream(WebHeaderCollection headers) + { + this.headers = headers; + saved = new StringBuilder(); + chunks = new List(); + chunkSize = -1; + totalWritten = 0; + } + + public void ResetBuffer() + { + chunkSize = -1; + chunkRead = 0; + totalWritten = 0; + chunks.Clear(); + } + + public void WriteAndReadBack(byte[] buffer, int offset, int size, ref int read) + { + if (offset + read > 0) + Write(buffer, offset, offset + read); + read = Read(buffer, offset, size); + } + + public int Read(byte[] buffer, int offset, int size) + { + return ReadFromChunks(buffer, offset, size); + } + + int ReadFromChunks(byte[] buffer, int offset, int size) + { + int count = chunks.Count; + int nread = 0; + + var chunksForRemoving = new List(count); + for (int i = 0; i < count; i++) + { + Chunk chunk = (Chunk)chunks[i]; + + if (chunk.Offset == chunk.Bytes.Length) + { + chunksForRemoving.Add(chunk); + continue; + } + + nread += chunk.Read(buffer, offset + nread, size - nread); + if (nread == size) + break; + } + + foreach (var chunk in chunksForRemoving) + chunks.Remove(chunk); + + return nread; + } + + public void Write(byte[] buffer, int offset, int size) + { + if (offset < size) + InternalWrite(buffer, ref offset, size); + } + + void InternalWrite(byte[] buffer, ref int offset, int size) + { + if (state == State.None || state == State.PartialSize) + { + state = GetChunkSize(buffer, ref offset, size); + if (state == State.PartialSize) + return; + + saved.Length = 0; + sawCR = false; + gotit = false; + } + + if (state == State.Body && offset < size) + { + state = ReadBody(buffer, ref offset, size); + if (state == State.Body) + return; + } + + if (state == State.BodyFinished && offset < size) + { + state = ReadCRLF(buffer, ref offset, size); + if (state == State.BodyFinished) + return; + + sawCR = false; + } + + if (state == State.Trailer && offset < size) + { + state = ReadTrailer(buffer, ref offset, size); + if (state == State.Trailer) + return; + + saved.Length = 0; + sawCR = false; + gotit = false; + } + + if (offset < size) + InternalWrite(buffer, ref offset, size); + } + + public bool WantMore + { + get { return (chunkRead != chunkSize || chunkSize != 0 || state != State.None); } + } + + public bool DataAvailable + { + get + { + int count = chunks.Count; + for (int i = 0; i < count; i++) + { + Chunk ch = (Chunk)chunks[i]; + if (ch == null || ch.Bytes == null) + continue; + if (ch.Bytes.Length > 0 && ch.Offset < ch.Bytes.Length) + return (state != State.Body); + } + return false; + } + } + + public int TotalDataSize + { + get { return totalWritten; } + } + + public int ChunkLeft + { + get { return chunkSize - chunkRead; } + } + + State ReadBody(byte[] buffer, ref int offset, int size) + { + if (chunkSize == 0) + return State.BodyFinished; + + int diff = size - offset; + if (diff + chunkRead > chunkSize) + diff = chunkSize - chunkRead; + + byte[] chunk = new byte[diff]; + Buffer.BlockCopy(buffer, offset, chunk, 0, diff); + chunks.Add(new Chunk(chunk)); + offset += diff; + chunkRead += diff; + totalWritten += diff; + return (chunkRead == chunkSize) ? State.BodyFinished : State.Body; + + } + + State GetChunkSize(byte[] buffer, ref int offset, int size) + { + chunkRead = 0; + chunkSize = 0; + char c = '\0'; + while (offset < size) + { + c = (char)buffer[offset++]; + if (c == '\r') + { + if (sawCR) + ThrowProtocolViolation("2 CR found"); + + sawCR = true; + continue; + } + + if (sawCR && c == '\n') + break; + + if (c == ' ') + gotit = true; + + if (!gotit) + saved.Append(c); + + if (saved.Length > 20) + ThrowProtocolViolation("chunk size too long."); + } + + if (!sawCR || c != '\n') + { + if (offset < size) + ThrowProtocolViolation("Missing \\n"); + + try + { + if (saved.Length > 0) + { + chunkSize = Int32.Parse(RemoveChunkExtension(saved.ToString()), NumberStyles.HexNumber); + } + } + catch (Exception) + { + ThrowProtocolViolation("Cannot parse chunk size."); + } + + return State.PartialSize; + } + + chunkRead = 0; + try + { + chunkSize = Int32.Parse(RemoveChunkExtension(saved.ToString()), NumberStyles.HexNumber); + } + catch (Exception) + { + ThrowProtocolViolation("Cannot parse chunk size."); + } + + if (chunkSize == 0) + { + trailerState = 2; + return State.Trailer; + } + + return State.Body; + } + + static string RemoveChunkExtension(string input) + { + int idx = input.IndexOf(';'); + if (idx == -1) + return input; + return input.Substring(0, idx); + } + + State ReadCRLF(byte[] buffer, ref int offset, int size) + { + if (!sawCR) + { + if ((char)buffer[offset++] != '\r') + ThrowProtocolViolation("Expecting \\r"); + + sawCR = true; + if (offset == size) + return State.BodyFinished; + } + + if (sawCR && (char)buffer[offset++] != '\n') + ThrowProtocolViolation("Expecting \\n"); + + return State.None; + } + + State ReadTrailer(byte[] buffer, ref int offset, int size) + { + char c = '\0'; + + // short path + if (trailerState == 2 && (char)buffer[offset] == '\r' && saved.Length == 0) + { + offset++; + if (offset < size && (char)buffer[offset] == '\n') + { + offset++; + return State.None; + } + offset--; + } + + int st = trailerState; + string stString = "\r\n\r"; + while (offset < size && st < 4) + { + c = (char)buffer[offset++]; + if ((st == 0 || st == 2) && c == '\r') + { + st++; + continue; + } + + if ((st == 1 || st == 3) && c == '\n') + { + st++; + continue; + } + + if (st > 0) + { + saved.Append(stString.Substring(0, saved.Length == 0 ? st - 2 : st)); + st = 0; + if (saved.Length > 4196) + ThrowProtocolViolation("Error reading trailer (too long)."); + } + } + + if (st < 4) + { + trailerState = st; + if (offset < size) + ThrowProtocolViolation("Error reading trailer."); + + return State.Trailer; + } + + StringReader reader = new StringReader(saved.ToString()); + string line; + while ((line = reader.ReadLine()) != null && line != "") + headers.Add(line); + + return State.None; + } + + static void ThrowProtocolViolation(string message) + { + WebException we = new WebException(message, null, WebExceptionStatus.UnknownError, null); + //WebException we = new WebException(message, null, WebExceptionStatus.ServerProtocolViolation, null); + throw we; + } + } +} diff --git a/SocketHttpListener.Portable/Net/ChunkedInputStream.cs b/SocketHttpListener.Portable/Net/ChunkedInputStream.cs new file mode 100644 index 000000000..6dfd8d8a1 --- /dev/null +++ b/SocketHttpListener.Portable/Net/ChunkedInputStream.cs @@ -0,0 +1,160 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using SocketHttpListener.Primitives; + +namespace SocketHttpListener.Net +{ + class ChunkedInputStream : RequestStream + { + bool disposed; + ChunkStream decoder; + HttpListenerContext context; + bool no_more_data; + + //class ReadBufferState + //{ + // public byte[] Buffer; + // public int Offset; + // public int Count; + // public int InitialCount; + // public HttpStreamAsyncResult Ares; + // public ReadBufferState(byte[] buffer, int offset, int count, + // HttpStreamAsyncResult ares) + // { + // Buffer = buffer; + // Offset = offset; + // Count = count; + // InitialCount = count; + // Ares = ares; + // } + //} + + public ChunkedInputStream(HttpListenerContext context, Stream stream, + byte[] buffer, int offset, int length) + : base(stream, buffer, offset, length) + { + this.context = context; + WebHeaderCollection coll = (WebHeaderCollection)context.Request.Headers; + decoder = new ChunkStream(coll); + } + + //public ChunkStream Decoder + //{ + // get { return decoder; } + // set { decoder = value; } + //} + + //public override int Read([In, Out] byte[] buffer, int offset, int count) + //{ + // IAsyncResult ares = BeginRead(buffer, offset, count, null, null); + // return EndRead(ares); + //} + + //public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, + // AsyncCallback cback, object state) + //{ + // if (disposed) + // throw new ObjectDisposedException(GetType().ToString()); + + // if (buffer == null) + // throw new ArgumentNullException("buffer"); + + // int len = buffer.Length; + // if (offset < 0 || offset > len) + // throw new ArgumentOutOfRangeException("offset exceeds the size of buffer"); + + // if (count < 0 || offset > len - count) + // throw new ArgumentOutOfRangeException("offset+size exceeds the size of buffer"); + + // HttpStreamAsyncResult ares = new HttpStreamAsyncResult(); + // ares.Callback = cback; + // ares.State = state; + // if (no_more_data) + // { + // ares.Complete(); + // return ares; + // } + // int nread = decoder.Read(buffer, offset, count); + // offset += nread; + // count -= nread; + // if (count == 0) + // { + // // got all we wanted, no need to bother the decoder yet + // ares.Count = nread; + // ares.Complete(); + // return ares; + // } + // if (!decoder.WantMore) + // { + // no_more_data = nread == 0; + // ares.Count = nread; + // ares.Complete(); + // return ares; + // } + // ares.Buffer = new byte[8192]; + // ares.Offset = 0; + // ares.Count = 8192; + // ReadBufferState rb = new ReadBufferState(buffer, offset, count, ares); + // rb.InitialCount += nread; + // base.BeginRead(ares.Buffer, ares.Offset, ares.Count, OnRead, rb); + // return ares; + //} + + //void OnRead(IAsyncResult base_ares) + //{ + // ReadBufferState rb = (ReadBufferState)base_ares.AsyncState; + // HttpStreamAsyncResult ares = rb.Ares; + // try + // { + // int nread = base.EndRead(base_ares); + // decoder.Write(ares.Buffer, ares.Offset, nread); + // nread = decoder.Read(rb.Buffer, rb.Offset, rb.Count); + // rb.Offset += nread; + // rb.Count -= nread; + // if (rb.Count == 0 || !decoder.WantMore || nread == 0) + // { + // no_more_data = !decoder.WantMore && nread == 0; + // ares.Count = rb.InitialCount - rb.Count; + // ares.Complete(); + // return; + // } + // ares.Offset = 0; + // ares.Count = Math.Min(8192, decoder.ChunkLeft + 6); + // base.BeginRead(ares.Buffer, ares.Offset, ares.Count, OnRead, rb); + // } + // catch (Exception e) + // { + // context.Connection.SendError(e.Message, 400); + // ares.Complete(e); + // } + //} + + //public override int EndRead(IAsyncResult ares) + //{ + // if (disposed) + // throw new ObjectDisposedException(GetType().ToString()); + + // HttpStreamAsyncResult my_ares = ares as HttpStreamAsyncResult; + // if (ares == null) + // throw new ArgumentException("Invalid IAsyncResult", "ares"); + + // if (!ares.IsCompleted) + // ares.AsyncWaitHandle.WaitOne(); + + // if (my_ares.Error != null) + // throw new HttpListenerException(400, "I/O operation aborted: " + my_ares.Error.Message); + + // return my_ares.Count; + //} + + //protected override void Dispose(bool disposing) + //{ + // if (!disposed) + // { + // disposed = true; + // base.Dispose(disposing); + // } + //} + } +} diff --git a/SocketHttpListener.Portable/Net/CookieHelper.cs b/SocketHttpListener.Portable/Net/CookieHelper.cs new file mode 100644 index 000000000..470507d6b --- /dev/null +++ b/SocketHttpListener.Portable/Net/CookieHelper.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +namespace SocketHttpListener.Net +{ + public static class CookieHelper + { + internal static CookieCollection Parse(string value, bool response) + { + return response + ? parseResponse(value) + : null; + } + + private static string[] splitCookieHeaderValue(string value) + { + return new List(value.SplitHeaderValue(',', ';')).ToArray(); + } + + private static CookieCollection parseResponse(string value) + { + var cookies = new CookieCollection(); + + Cookie cookie = null; + var pairs = splitCookieHeaderValue(value); + for (int i = 0; i < pairs.Length; i++) + { + var pair = pairs[i].Trim(); + if (pair.Length == 0) + continue; + + if (pair.StartsWith("version", StringComparison.OrdinalIgnoreCase)) + { + if (cookie != null) + cookie.Version = Int32.Parse(pair.GetValueInternal("=").Trim('"')); + } + else if (pair.StartsWith("expires", StringComparison.OrdinalIgnoreCase)) + { + var buffer = new StringBuilder(pair.GetValueInternal("="), 32); + if (i < pairs.Length - 1) + buffer.AppendFormat(", {0}", pairs[++i].Trim()); + + DateTime expires; + if (!DateTime.TryParseExact( + buffer.ToString(), + new[] { "ddd, dd'-'MMM'-'yyyy HH':'mm':'ss 'GMT'", "r" }, + new CultureInfo("en-US"), + DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, + out expires)) + expires = DateTime.Now; + + if (cookie != null && cookie.Expires == DateTime.MinValue) + cookie.Expires = expires.ToLocalTime(); + } + else if (pair.StartsWith("max-age", StringComparison.OrdinalIgnoreCase)) + { + var max = Int32.Parse(pair.GetValueInternal("=").Trim('"')); + var expires = DateTime.Now.AddSeconds((double)max); + if (cookie != null) + cookie.Expires = expires; + } + else if (pair.StartsWith("path", StringComparison.OrdinalIgnoreCase)) + { + if (cookie != null) + cookie.Path = pair.GetValueInternal("="); + } + else if (pair.StartsWith("domain", StringComparison.OrdinalIgnoreCase)) + { + if (cookie != null) + cookie.Domain = pair.GetValueInternal("="); + } + else if (pair.StartsWith("port", StringComparison.OrdinalIgnoreCase)) + { + var port = pair.Equals("port", StringComparison.OrdinalIgnoreCase) + ? "\"\"" + : pair.GetValueInternal("="); + + if (cookie != null) + cookie.Port = port; + } + else if (pair.StartsWith("comment", StringComparison.OrdinalIgnoreCase)) + { + if (cookie != null) + cookie.Comment = pair.GetValueInternal("=").UrlDecode(); + } + else if (pair.StartsWith("commenturl", StringComparison.OrdinalIgnoreCase)) + { + if (cookie != null) + cookie.CommentUri = pair.GetValueInternal("=").Trim('"').ToUri(); + } + else if (pair.StartsWith("discard", StringComparison.OrdinalIgnoreCase)) + { + if (cookie != null) + cookie.Discard = true; + } + else if (pair.StartsWith("secure", StringComparison.OrdinalIgnoreCase)) + { + if (cookie != null) + cookie.Secure = true; + } + else if (pair.StartsWith("httponly", StringComparison.OrdinalIgnoreCase)) + { + if (cookie != null) + cookie.HttpOnly = true; + } + else + { + if (cookie != null) + cookies.Add(cookie); + + string name; + string val = String.Empty; + + var pos = pair.IndexOf('='); + if (pos == -1) + { + name = pair; + } + else if (pos == pair.Length - 1) + { + name = pair.Substring(0, pos).TrimEnd(' '); + } + else + { + name = pair.Substring(0, pos).TrimEnd(' '); + val = pair.Substring(pos + 1).TrimStart(' '); + } + + cookie = new Cookie(name, val); + } + } + + if (cookie != null) + cookies.Add(cookie); + + return cookies; + } + } +} diff --git a/SocketHttpListener.Portable/Net/EndPointListener.cs b/SocketHttpListener.Portable/Net/EndPointListener.cs new file mode 100644 index 000000000..b50660ad0 --- /dev/null +++ b/SocketHttpListener.Portable/Net/EndPointListener.cs @@ -0,0 +1,368 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Threading; +using MediaBrowser.Model.Cryptography; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Text; +using SocketHttpListener.Primitives; + +namespace SocketHttpListener.Net +{ + sealed class EndPointListener + { + HttpListener listener; + IpEndPointInfo endpoint; + ISocket sock; + Dictionary prefixes; // Dictionary + List unhandled; // List unhandled; host = '*' + List all; // List all; host = '+' + ICertificate cert; + bool secure; + Dictionary unregistered; + private readonly ILogger _logger; + private bool _closed; + private readonly bool _enableDualMode; + private readonly ICryptoProvider _cryptoProvider; + private readonly IStreamFactory _streamFactory; + private readonly ISocketFactory _socketFactory; + private readonly ITextEncoding _textEncoding; + private readonly IMemoryStreamFactory _memoryStreamFactory; + + public EndPointListener(HttpListener listener, IpAddressInfo addr, int port, bool secure, ICertificate cert, ILogger logger, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, ISocketFactory socketFactory, IMemoryStreamFactory memoryStreamFactory, ITextEncoding textEncoding) + { + this.listener = listener; + _logger = logger; + _cryptoProvider = cryptoProvider; + _streamFactory = streamFactory; + _socketFactory = socketFactory; + _memoryStreamFactory = memoryStreamFactory; + _textEncoding = textEncoding; + + this.secure = secure; + this.cert = cert; + + _enableDualMode = addr.Equals(IpAddressInfo.IPv6Any); + endpoint = new IpEndPointInfo(addr, port); + + prefixes = new Dictionary(); + unregistered = new Dictionary(); + + CreateSocket(); + } + + internal HttpListener Listener + { + get + { + return listener; + } + } + + private void CreateSocket() + { + sock = _socketFactory.CreateSocket(endpoint.IpAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp, _enableDualMode); + + sock.Bind(endpoint); + + // This is the number TcpListener uses. + sock.Listen(2147483647); + + sock.StartAccept(ProcessAccept, () => _closed); + _closed = false; + } + + private async void ProcessAccept(ISocket accepted) + { + try + { + var listener = this; + + if (listener.secure && listener.cert == null) + { + accepted.Close(); + return; + } + + HttpConnection conn = await HttpConnection.Create(_logger, accepted, listener, listener.secure, listener.cert, _cryptoProvider, _streamFactory, _memoryStreamFactory, _textEncoding).ConfigureAwait(false); + + //_logger.Debug("Adding unregistered connection to {0}. Id: {1}", accepted.RemoteEndPoint, connectionId); + lock (listener.unregistered) + { + listener.unregistered[conn] = conn; + } + conn.BeginReadRequest(); + } + catch (Exception ex) + { + _logger.ErrorException("Error in ProcessAccept", ex); + } + } + + internal void RemoveConnection(HttpConnection conn) + { + lock (unregistered) + { + unregistered.Remove(conn); + } + } + + public bool BindContext(HttpListenerContext context) + { + HttpListenerRequest req = context.Request; + ListenerPrefix prefix; + HttpListener listener = SearchListener(req.Url, out prefix); + if (listener == null) + return false; + + context.Listener = listener; + context.Connection.Prefix = prefix; + return true; + } + + public void UnbindContext(HttpListenerContext context) + { + if (context == null || context.Request == null) + return; + + context.Listener.UnregisterContext(context); + } + + HttpListener SearchListener(Uri uri, out ListenerPrefix prefix) + { + prefix = null; + if (uri == null) + return null; + + string host = uri.Host; + int port = uri.Port; + string path = WebUtility.UrlDecode(uri.AbsolutePath); + string path_slash = path[path.Length - 1] == '/' ? path : path + "/"; + + HttpListener best_match = null; + int best_length = -1; + + if (host != null && host != "") + { + var p_ro = prefixes; + foreach (ListenerPrefix p in p_ro.Keys) + { + string ppath = p.Path; + if (ppath.Length < best_length) + continue; + + if (p.Host != host || p.Port != port) + continue; + + if (path.StartsWith(ppath) || path_slash.StartsWith(ppath)) + { + best_length = ppath.Length; + best_match = (HttpListener)p_ro[p]; + prefix = p; + } + } + if (best_length != -1) + return best_match; + } + + List list = unhandled; + best_match = MatchFromList(host, path, list, out prefix); + if (path != path_slash && best_match == null) + best_match = MatchFromList(host, path_slash, list, out prefix); + if (best_match != null) + return best_match; + + list = all; + best_match = MatchFromList(host, path, list, out prefix); + if (path != path_slash && best_match == null) + best_match = MatchFromList(host, path_slash, list, out prefix); + if (best_match != null) + return best_match; + + return null; + } + + HttpListener MatchFromList(string host, string path, List list, out ListenerPrefix prefix) + { + prefix = null; + if (list == null) + return null; + + HttpListener best_match = null; + int best_length = -1; + + foreach (ListenerPrefix p in list) + { + string ppath = p.Path; + if (ppath.Length < best_length) + continue; + + if (path.StartsWith(ppath)) + { + best_length = ppath.Length; + best_match = p.Listener; + prefix = p; + } + } + + return best_match; + } + + void AddSpecial(List coll, ListenerPrefix prefix) + { + if (coll == null) + return; + + foreach (ListenerPrefix p in coll) + { + if (p.Path == prefix.Path) //TODO: code + throw new HttpListenerException(400, "Prefix already in use."); + } + coll.Add(prefix); + } + + bool RemoveSpecial(List coll, ListenerPrefix prefix) + { + if (coll == null) + return false; + + int c = coll.Count; + for (int i = 0; i < c; i++) + { + ListenerPrefix p = (ListenerPrefix)coll[i]; + if (p.Path == prefix.Path) + { + coll.RemoveAt(i); + return true; + } + } + return false; + } + + void CheckIfRemove() + { + if (prefixes.Count > 0) + return; + + List list = unhandled; + if (list != null && list.Count > 0) + return; + + list = all; + if (list != null && list.Count > 0) + return; + + EndPointManager.RemoveEndPoint(this, endpoint); + } + + public void Close() + { + _closed = true; + sock.Close(); + lock (unregistered) + { + // + // Clone the list because RemoveConnection can be called from Close + // + var connections = new List(unregistered.Keys); + + foreach (HttpConnection c in connections) + c.Close(true); + unregistered.Clear(); + } + } + + public void AddPrefix(ListenerPrefix prefix, HttpListener listener) + { + List current; + List future; + if (prefix.Host == "*") + { + do + { + current = unhandled; + future = (current != null) ? current.ToList() : new List(); + prefix.Listener = listener; + AddSpecial(future, prefix); + } while (Interlocked.CompareExchange(ref unhandled, future, current) != current); + return; + } + + if (prefix.Host == "+") + { + do + { + current = all; + future = (current != null) ? current.ToList() : new List(); + prefix.Listener = listener; + AddSpecial(future, prefix); + } while (Interlocked.CompareExchange(ref all, future, current) != current); + return; + } + + Dictionary prefs; + Dictionary p2; + do + { + prefs = prefixes; + if (prefs.ContainsKey(prefix)) + { + HttpListener other = (HttpListener)prefs[prefix]; + if (other != listener) // TODO: code. + throw new HttpListenerException(400, "There's another listener for " + prefix); + return; + } + p2 = new Dictionary(prefs); + p2[prefix] = listener; + } while (Interlocked.CompareExchange(ref prefixes, p2, prefs) != prefs); + } + + public void RemovePrefix(ListenerPrefix prefix, HttpListener listener) + { + List current; + List future; + if (prefix.Host == "*") + { + do + { + current = unhandled; + future = (current != null) ? current.ToList() : new List(); + if (!RemoveSpecial(future, prefix)) + break; // Prefix not found + } while (Interlocked.CompareExchange(ref unhandled, future, current) != current); + CheckIfRemove(); + return; + } + + if (prefix.Host == "+") + { + do + { + current = all; + future = (current != null) ? current.ToList() : new List(); + if (!RemoveSpecial(future, prefix)) + break; // Prefix not found + } while (Interlocked.CompareExchange(ref all, future, current) != current); + CheckIfRemove(); + return; + } + + Dictionary prefs; + Dictionary p2; + do + { + prefs = prefixes; + if (!prefs.ContainsKey(prefix)) + break; + + p2 = new Dictionary(prefs); + p2.Remove(prefix); + } while (Interlocked.CompareExchange(ref prefixes, p2, prefs) != prefs); + CheckIfRemove(); + } + } +} diff --git a/SocketHttpListener.Portable/Net/EndPointManager.cs b/SocketHttpListener.Portable/Net/EndPointManager.cs new file mode 100644 index 000000000..797684b3e --- /dev/null +++ b/SocketHttpListener.Portable/Net/EndPointManager.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Threading.Tasks; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; +using SocketHttpListener.Primitives; + +namespace SocketHttpListener.Net +{ + sealed class EndPointManager + { + // Dictionary> + static Dictionary> ip_to_endpoints = new Dictionary>(); + + private EndPointManager() + { + } + + public static void AddListener(ILogger logger, HttpListener listener) + { + List added = new List(); + try + { + lock (ip_to_endpoints) + { + foreach (string prefix in listener.Prefixes) + { + AddPrefixInternal(logger, prefix, listener); + added.Add(prefix); + } + } + } + catch + { + foreach (string prefix in added) + { + RemovePrefix(logger, prefix, listener); + } + throw; + } + } + + public static void AddPrefix(ILogger logger, string prefix, HttpListener listener) + { + lock (ip_to_endpoints) + { + AddPrefixInternal(logger, prefix, listener); + } + } + + static void AddPrefixInternal(ILogger logger, string p, HttpListener listener) + { + ListenerPrefix lp = new ListenerPrefix(p); + if (lp.Path.IndexOf('%') != -1) + throw new HttpListenerException(400, "Invalid path."); + + if (lp.Path.IndexOf("//", StringComparison.Ordinal) != -1) // TODO: Code? + throw new HttpListenerException(400, "Invalid path."); + + // listens on all the interfaces if host name cannot be parsed by IPAddress. + EndPointListener epl = GetEPListener(logger, lp.Host, lp.Port, listener, lp.Secure).Result; + epl.AddPrefix(lp, listener); + } + + private static IpAddressInfo GetIpAnyAddress(HttpListener listener) + { + return listener.EnableDualMode ? IpAddressInfo.IPv6Any : IpAddressInfo.Any; + } + + static async Task GetEPListener(ILogger logger, string host, int port, HttpListener listener, bool secure) + { + var networkManager = listener.NetworkManager; + + IpAddressInfo addr; + if (host == "*" || host == "+") + addr = GetIpAnyAddress(listener); + else if (networkManager.TryParseIpAddress(host, out addr) == false) + { + try + { + addr = (await networkManager.GetHostAddressesAsync(host).ConfigureAwait(false)).FirstOrDefault() ?? + GetIpAnyAddress(listener); + } + catch + { + addr = GetIpAnyAddress(listener); + } + } + + Dictionary p = null; // Dictionary + if (!ip_to_endpoints.TryGetValue(addr.Address, out p)) + { + p = new Dictionary(); + ip_to_endpoints[addr.Address] = p; + } + + EndPointListener epl = null; + if (p.ContainsKey(port)) + { + epl = (EndPointListener)p[port]; + } + else + { + epl = new EndPointListener(listener, addr, port, secure, listener.Certificate, logger, listener.CryptoProvider, listener.StreamFactory, listener.SocketFactory, listener.MemoryStreamFactory, listener.TextEncoding); + p[port] = epl; + } + + return epl; + } + + public static void RemoveEndPoint(EndPointListener epl, IpEndPointInfo ep) + { + lock (ip_to_endpoints) + { + // Dictionary p + Dictionary p; + if (ip_to_endpoints.TryGetValue(ep.IpAddress.Address, out p)) + { + p.Remove(ep.Port); + if (p.Count == 0) + { + ip_to_endpoints.Remove(ep.IpAddress.Address); + } + } + epl.Close(); + } + } + + public static void RemoveListener(ILogger logger, HttpListener listener) + { + lock (ip_to_endpoints) + { + foreach (string prefix in listener.Prefixes) + { + RemovePrefixInternal(logger, prefix, listener); + } + } + } + + public static void RemovePrefix(ILogger logger, string prefix, HttpListener listener) + { + lock (ip_to_endpoints) + { + RemovePrefixInternal(logger, prefix, listener); + } + } + + static void RemovePrefixInternal(ILogger logger, string prefix, HttpListener listener) + { + ListenerPrefix lp = new ListenerPrefix(prefix); + if (lp.Path.IndexOf('%') != -1) + return; + + if (lp.Path.IndexOf("//", StringComparison.Ordinal) != -1) + return; + + EndPointListener epl = GetEPListener(logger, lp.Host, lp.Port, listener, lp.Secure).Result; + epl.RemovePrefix(lp, listener); + } + } +} diff --git a/SocketHttpListener.Portable/Net/HttpConnection.cs b/SocketHttpListener.Portable/Net/HttpConnection.cs new file mode 100644 index 000000000..d31da4132 --- /dev/null +++ b/SocketHttpListener.Portable/Net/HttpConnection.cs @@ -0,0 +1,550 @@ +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.Cryptography; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Text; +using SocketHttpListener.Primitives; + +namespace SocketHttpListener.Net +{ + sealed class HttpConnection + { + const int BufferSize = 8192; + ISocket sock; + Stream stream; + EndPointListener epl; + MemoryStream ms; + byte[] buffer; + HttpListenerContext context; + StringBuilder current_line; + ListenerPrefix prefix; + RequestStream i_stream; + ResponseStream o_stream; + bool chunked; + int reuses; + bool context_bound; + bool secure; + int s_timeout = 300000; // 90k ms for first request, 15k ms from then on + IpEndPointInfo local_ep; + HttpListener last_listener; + int[] client_cert_errors; + ICertificate cert; + Stream ssl_stream; + + private ILogger _logger; + private readonly ICryptoProvider _cryptoProvider; + private readonly IMemoryStreamFactory _memoryStreamFactory; + private readonly ITextEncoding _textEncoding; + private readonly IStreamFactory _streamFactory; + + private HttpConnection(ILogger logger, ISocket sock, EndPointListener epl, bool secure, ICertificate cert, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, IMemoryStreamFactory memoryStreamFactory, ITextEncoding textEncoding) + { + _logger = logger; + this.sock = sock; + this.epl = epl; + this.secure = secure; + this.cert = cert; + _cryptoProvider = cryptoProvider; + _memoryStreamFactory = memoryStreamFactory; + _textEncoding = textEncoding; + _streamFactory = streamFactory; + } + + private async Task InitStream() + { + if (secure == false) + { + stream = _streamFactory.CreateNetworkStream(sock, false); + } + else + { + //ssl_stream = epl.Listener.CreateSslStream(new NetworkStream(sock, false), false, (t, c, ch, e) => + //{ + // if (c == null) + // return true; + // var c2 = c as X509Certificate2; + // if (c2 == null) + // c2 = new X509Certificate2(c.GetRawCertData()); + // client_cert = c2; + // client_cert_errors = new int[] { (int)e }; + // return true; + //}); + //stream = ssl_stream.AuthenticatedStream; + + ssl_stream = _streamFactory.CreateSslStream(_streamFactory.CreateNetworkStream(sock, false), false); + await _streamFactory.AuthenticateSslStreamAsServer(ssl_stream, cert).ConfigureAwait(false); + stream = ssl_stream; + } + Init(); + } + + public static async Task Create(ILogger logger, ISocket sock, EndPointListener epl, bool secure, ICertificate cert, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, IMemoryStreamFactory memoryStreamFactory, ITextEncoding textEncoding) + { + var connection = new HttpConnection(logger, sock, epl, secure, cert, cryptoProvider, streamFactory, memoryStreamFactory, textEncoding); + + await connection.InitStream().ConfigureAwait(false); + + return connection; + } + + public Stream Stream + { + get + { + return stream; + } + } + + internal int[] ClientCertificateErrors + { + get { return client_cert_errors; } + } + + void Init() + { + if (ssl_stream != null) + { + //ssl_stream.AuthenticateAsServer(client_cert, true, (SslProtocols)ServicePointManager.SecurityProtocol, false); + //_streamFactory.AuthenticateSslStreamAsServer(ssl_stream, cert); + } + + context_bound = false; + i_stream = null; + o_stream = null; + prefix = null; + chunked = false; + ms = _memoryStreamFactory.CreateNew(); + position = 0; + input_state = InputState.RequestLine; + line_state = LineState.None; + context = new HttpListenerContext(this, _logger, _cryptoProvider, _memoryStreamFactory, _textEncoding); + } + + public bool IsClosed + { + get { return (sock == null); } + } + + public int Reuses + { + get { return reuses; } + } + + public IpEndPointInfo LocalEndPoint + { + get + { + if (local_ep != null) + return local_ep; + + local_ep = (IpEndPointInfo)sock.LocalEndPoint; + return local_ep; + } + } + + public IpEndPointInfo RemoteEndPoint + { + get { return (IpEndPointInfo)sock.RemoteEndPoint; } + } + + public bool IsSecure + { + get { return secure; } + } + + public ListenerPrefix Prefix + { + get { return prefix; } + set { prefix = value; } + } + + public async Task BeginReadRequest() + { + if (buffer == null) + buffer = new byte[BufferSize]; + + try + { + //if (reuses == 1) + // s_timeout = 15000; + var nRead = await stream.ReadAsync(buffer, 0, BufferSize).ConfigureAwait(false); + + OnReadInternal(nRead); + } + catch (Exception ex) + { + OnReadInternalException(ms, ex); + } + } + + public RequestStream GetRequestStream(bool chunked, long contentlength) + { + if (i_stream == null) + { + byte[] buffer; + _memoryStreamFactory.TryGetBuffer(ms, out buffer); + + int length = (int)ms.Length; + ms = null; + if (chunked) + { + this.chunked = true; + //context.Response.SendChunked = true; + i_stream = new ChunkedInputStream(context, stream, buffer, position, length - position); + } + else + { + i_stream = new RequestStream(stream, buffer, position, length - position, contentlength); + } + } + return i_stream; + } + + public ResponseStream GetResponseStream() + { + // TODO: can we get this stream before reading the input? + if (o_stream == null) + { + HttpListener listener = context.Listener; + + if (listener == null) + return new ResponseStream(stream, context.Response, true, _memoryStreamFactory, _textEncoding); + + o_stream = new ResponseStream(stream, context.Response, listener.IgnoreWriteExceptions, _memoryStreamFactory, _textEncoding); + } + return o_stream; + } + + void OnReadInternal(int nread) + { + ms.Write(buffer, 0, nread); + if (ms.Length > 32768) + { + SendError("Bad request", 400); + Close(true); + return; + } + + if (nread == 0) + { + //if (ms.Length > 0) + // SendError (); // Why bother? + CloseSocket(); + Unbind(); + return; + } + + if (ProcessInput(ms)) + { + if (!context.HaveError) + context.Request.FinishInitialization(); + + if (context.HaveError) + { + SendError(); + Close(true); + return; + } + + if (!epl.BindContext(context)) + { + SendError("Invalid host", 400); + Close(true); + return; + } + HttpListener listener = context.Listener; + if (last_listener != listener) + { + RemoveConnection(); + listener.AddConnection(this); + last_listener = listener; + } + + context_bound = true; + listener.RegisterContext(context); + return; + } + + BeginReadRequest(); + } + + private void OnReadInternalException(MemoryStream ms, Exception ex) + { + //_logger.ErrorException("Error in HttpConnection.OnReadInternal", ex); + + if (ms != null && ms.Length > 0) + SendError(); + if (sock != null) + { + CloseSocket(); + Unbind(); + } + } + + void RemoveConnection() + { + if (last_listener == null) + epl.RemoveConnection(this); + else + last_listener.RemoveConnection(this); + } + + enum InputState + { + RequestLine, + Headers + } + + enum LineState + { + None, + CR, + LF + } + + InputState input_state = InputState.RequestLine; + LineState line_state = LineState.None; + int position; + + // true -> done processing + // false -> need more input + bool ProcessInput(MemoryStream ms) + { + byte[] buffer; + _memoryStreamFactory.TryGetBuffer(ms, out buffer); + + int len = (int)ms.Length; + int used = 0; + string line; + + while (true) + { + if (context.HaveError) + return true; + + if (position >= len) + break; + + try + { + line = ReadLine(buffer, position, len - position, ref used); + position += used; + } + catch + { + context.ErrorMessage = "Bad request"; + context.ErrorStatus = 400; + return true; + } + + if (line == null) + break; + + if (line == "") + { + if (input_state == InputState.RequestLine) + continue; + current_line = null; + ms = null; + return true; + } + + if (input_state == InputState.RequestLine) + { + context.Request.SetRequestLine(line); + input_state = InputState.Headers; + } + else + { + try + { + context.Request.AddHeader(line); + } + catch (Exception e) + { + context.ErrorMessage = e.Message; + context.ErrorStatus = 400; + return true; + } + } + } + + if (used == len) + { + ms.SetLength(0); + position = 0; + } + return false; + } + + string ReadLine(byte[] buffer, int offset, int len, ref int used) + { + if (current_line == null) + current_line = new StringBuilder(128); + int last = offset + len; + used = 0; + + for (int i = offset; i < last && line_state != LineState.LF; i++) + { + used++; + byte b = buffer[i]; + if (b == 13) + { + line_state = LineState.CR; + } + else if (b == 10) + { + line_state = LineState.LF; + } + else + { + current_line.Append((char)b); + } + } + + string result = null; + if (line_state == LineState.LF) + { + line_state = LineState.None; + result = current_line.ToString(); + current_line.Length = 0; + } + + return result; + } + + public void SendError(string msg, int status) + { + try + { + HttpListenerResponse response = context.Response; + response.StatusCode = status; + response.ContentType = "text/html"; + string description = HttpListenerResponse.GetStatusDescription(status); + string str; + if (msg != null) + str = String.Format("

{0} ({1})

", description, msg); + else + str = String.Format("

{0}

", description); + + byte[] error = context.Response.ContentEncoding.GetBytes(str); + response.Close(error, false); + } + catch + { + // response was already closed + } + } + + public void SendError() + { + SendError(context.ErrorMessage, context.ErrorStatus); + } + + void Unbind() + { + if (context_bound) + { + epl.UnbindContext(context); + context_bound = false; + } + } + + public void Close() + { + Close(false); + } + + private void CloseSocket() + { + if (sock == null) + return; + + try + { + sock.Close(); + } + catch + { + } + finally + { + sock = null; + } + RemoveConnection(); + } + + internal void Close(bool force_close) + { + if (sock != null) + { + if (!context.Request.IsWebSocketRequest || force_close) + { + Stream st = GetResponseStream(); + if (st != null) + st.Dispose(); + + o_stream = null; + } + } + + if (sock != null) + { + force_close |= !context.Request.KeepAlive; + if (!force_close) + force_close = (context.Response.Headers["connection"] == "close"); + /* + if (!force_close) { +// bool conn_close = (status_code == 400 || status_code == 408 || status_code == 411 || +// status_code == 413 || status_code == 414 || status_code == 500 || +// status_code == 503); + force_close |= (context.Request.ProtocolVersion <= HttpVersion.Version10); + } + */ + + if (!force_close && context.Request.FlushInput()) + { + if (chunked && context.Response.ForceCloseChunked == false) + { + // Don't close. Keep working. + reuses++; + Unbind(); + Init(); + BeginReadRequest(); + return; + } + + reuses++; + Unbind(); + Init(); + BeginReadRequest(); + return; + } + + ISocket s = sock; + sock = null; + try + { + if (s != null) + s.Shutdown(true); + } + catch + { + } + finally + { + if (s != null) + s.Close(); + } + Unbind(); + RemoveConnection(); + return; + } + } + } +} \ No newline at end of file diff --git a/SocketHttpListener.Portable/Net/HttpListener.cs b/SocketHttpListener.Portable/Net/HttpListener.cs new file mode 100644 index 000000000..83660100a --- /dev/null +++ b/SocketHttpListener.Portable/Net/HttpListener.cs @@ -0,0 +1,299 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Net; +using MediaBrowser.Common.Net; +using MediaBrowser.Model.Cryptography; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Text; +using SocketHttpListener.Primitives; + +namespace SocketHttpListener.Net +{ + public sealed class HttpListener : IDisposable + { + internal ICryptoProvider CryptoProvider { get; private set; } + internal IStreamFactory StreamFactory { get; private set; } + internal ISocketFactory SocketFactory { get; private set; } + internal ITextEncoding TextEncoding { get; private set; } + internal IMemoryStreamFactory MemoryStreamFactory { get; private set; } + internal INetworkManager NetworkManager { get; private set; } + + public bool EnableDualMode { get; set; } + + AuthenticationSchemes auth_schemes; + HttpListenerPrefixCollection prefixes; + AuthenticationSchemeSelector auth_selector; + string realm; + bool ignore_write_exceptions; + bool unsafe_ntlm_auth; + bool listening; + bool disposed; + + Dictionary registry; // Dictionary + Dictionary connections; + private ILogger _logger; + private ICertificate _certificate; + + public Action OnContext { get; set; } + + public HttpListener(ILogger logger, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, ISocketFactory socketFactory, INetworkManager networkManager, ITextEncoding textEncoding, IMemoryStreamFactory memoryStreamFactory) + { + _logger = logger; + CryptoProvider = cryptoProvider; + StreamFactory = streamFactory; + SocketFactory = socketFactory; + NetworkManager = networkManager; + TextEncoding = textEncoding; + MemoryStreamFactory = memoryStreamFactory; + prefixes = new HttpListenerPrefixCollection(logger, this); + registry = new Dictionary(); + connections = new Dictionary(); + auth_schemes = AuthenticationSchemes.Anonymous; + } + + public HttpListener(ICertificate certificate, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, ISocketFactory socketFactory, INetworkManager networkManager, ITextEncoding textEncoding, IMemoryStreamFactory memoryStreamFactory) + :this(new NullLogger(), certificate, cryptoProvider, streamFactory, socketFactory, networkManager, textEncoding, memoryStreamFactory) + { + } + + public HttpListener(ILogger logger, ICertificate certificate, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, ISocketFactory socketFactory, INetworkManager networkManager, ITextEncoding textEncoding, IMemoryStreamFactory memoryStreamFactory) + : this(logger, cryptoProvider, streamFactory, socketFactory, networkManager, textEncoding, memoryStreamFactory) + { + _certificate = certificate; + } + + public void LoadCert(ICertificate cert) + { + _certificate = cert; + } + + // TODO: Digest, NTLM and Negotiate require ControlPrincipal + public AuthenticationSchemes AuthenticationSchemes + { + get { return auth_schemes; } + set + { + CheckDisposed(); + auth_schemes = value; + } + } + + public AuthenticationSchemeSelector AuthenticationSchemeSelectorDelegate + { + get { return auth_selector; } + set + { + CheckDisposed(); + auth_selector = value; + } + } + + public bool IgnoreWriteExceptions + { + get { return ignore_write_exceptions; } + set + { + CheckDisposed(); + ignore_write_exceptions = value; + } + } + + public bool IsListening + { + get { return listening; } + } + + public static bool IsSupported + { + get { return true; } + } + + public HttpListenerPrefixCollection Prefixes + { + get + { + CheckDisposed(); + return prefixes; + } + } + + // TODO: use this + public string Realm + { + get { return realm; } + set + { + CheckDisposed(); + realm = value; + } + } + + public bool UnsafeConnectionNtlmAuthentication + { + get { return unsafe_ntlm_auth; } + set + { + CheckDisposed(); + unsafe_ntlm_auth = value; + } + } + + //internal IMonoSslStream CreateSslStream(Stream innerStream, bool ownsStream, MSI.MonoRemoteCertificateValidationCallback callback) + //{ + // lock (registry) + // { + // if (tlsProvider == null) + // tlsProvider = MonoTlsProviderFactory.GetProviderInternal(); + // if (tlsSettings == null) + // tlsSettings = MSI.MonoTlsSettings.CopyDefaultSettings(); + // if (tlsSettings.RemoteCertificateValidationCallback == null) + // tlsSettings.RemoteCertificateValidationCallback = callback; + // return tlsProvider.CreateSslStream(innerStream, ownsStream, tlsSettings); + // } + //} + + internal ICertificate Certificate + { + get { return _certificate; } + } + + public void Abort() + { + if (disposed) + return; + + if (!listening) + { + return; + } + + Close(true); + } + + public void Close() + { + if (disposed) + return; + + if (!listening) + { + disposed = true; + return; + } + + Close(true); + disposed = true; + } + + void Close(bool force) + { + CheckDisposed(); + EndPointManager.RemoveListener(_logger, this); + Cleanup(force); + } + + void Cleanup(bool close_existing) + { + lock (registry) + { + if (close_existing) + { + // Need to copy this since closing will call UnregisterContext + ICollection keys = registry.Keys; + var all = new HttpListenerContext[keys.Count]; + keys.CopyTo(all, 0); + registry.Clear(); + for (int i = all.Length - 1; i >= 0; i--) + all[i].Connection.Close(true); + } + + lock (connections) + { + ICollection keys = connections.Keys; + var conns = new HttpConnection[keys.Count]; + keys.CopyTo(conns, 0); + connections.Clear(); + for (int i = conns.Length - 1; i >= 0; i--) + conns[i].Close(true); + } + } + } + + internal AuthenticationSchemes SelectAuthenticationScheme(HttpListenerContext context) + { + if (AuthenticationSchemeSelectorDelegate != null) + return AuthenticationSchemeSelectorDelegate(context.Request); + else + return auth_schemes; + } + + public void Start() + { + CheckDisposed(); + if (listening) + return; + + EndPointManager.AddListener(_logger, this); + listening = true; + } + + public void Stop() + { + CheckDisposed(); + listening = false; + Close(false); + } + + void IDisposable.Dispose() + { + if (disposed) + return; + + Close(true); //TODO: Should we force here or not? + disposed = true; + } + + internal void CheckDisposed() + { + if (disposed) + throw new ObjectDisposedException(GetType().ToString()); + } + + internal void RegisterContext(HttpListenerContext context) + { + if (OnContext != null && IsListening) + { + OnContext(context); + } + + lock (registry) + registry[context] = context; + } + + internal void UnregisterContext(HttpListenerContext context) + { + lock (registry) + registry.Remove(context); + } + + internal void AddConnection(HttpConnection cnc) + { + lock (connections) + { + connections[cnc] = cnc; + } + } + + internal void RemoveConnection(HttpConnection cnc) + { + lock (connections) + { + connections.Remove(cnc); + } + } + } +} diff --git a/SocketHttpListener.Portable/Net/HttpListenerBasicIdentity.cs b/SocketHttpListener.Portable/Net/HttpListenerBasicIdentity.cs new file mode 100644 index 000000000..faa26693d --- /dev/null +++ b/SocketHttpListener.Portable/Net/HttpListenerBasicIdentity.cs @@ -0,0 +1,70 @@ +using System.Security.Principal; + +namespace SocketHttpListener.Net +{ + public class HttpListenerBasicIdentity : GenericIdentity + { + string password; + + public HttpListenerBasicIdentity(string username, string password) + : base(username, "Basic") + { + this.password = password; + } + + public virtual string Password + { + get { return password; } + } + } + + public class GenericIdentity : IIdentity + { + private string m_name; + private string m_type; + + public GenericIdentity(string name) + { + if (name == null) + throw new System.ArgumentNullException("name"); + + m_name = name; + m_type = ""; + } + + public GenericIdentity(string name, string type) + { + if (name == null) + throw new System.ArgumentNullException("name"); + if (type == null) + throw new System.ArgumentNullException("type"); + + m_name = name; + m_type = type; + } + + public virtual string Name + { + get + { + return m_name; + } + } + + public virtual string AuthenticationType + { + get + { + return m_type; + } + } + + public virtual bool IsAuthenticated + { + get + { + return !m_name.Equals(""); + } + } + } +} diff --git a/SocketHttpListener.Portable/Net/HttpListenerContext.cs b/SocketHttpListener.Portable/Net/HttpListenerContext.cs new file mode 100644 index 000000000..84c6a8c19 --- /dev/null +++ b/SocketHttpListener.Portable/Net/HttpListenerContext.cs @@ -0,0 +1,201 @@ +using System; +using System.Net; +using System.Security.Principal; +using MediaBrowser.Model.Cryptography; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Text; +using SocketHttpListener.Net.WebSockets; +using SocketHttpListener.Primitives; + +namespace SocketHttpListener.Net +{ + public sealed class HttpListenerContext + { + HttpListenerRequest request; + HttpListenerResponse response; + IPrincipal user; + HttpConnection cnc; + string error; + int err_status = 400; + internal HttpListener Listener; + private readonly ILogger _logger; + private readonly ICryptoProvider _cryptoProvider; + private readonly IMemoryStreamFactory _memoryStreamFactory; + private readonly ITextEncoding _textEncoding; + + internal HttpListenerContext(HttpConnection cnc, ILogger logger, ICryptoProvider cryptoProvider, IMemoryStreamFactory memoryStreamFactory, ITextEncoding textEncoding) + { + this.cnc = cnc; + _logger = logger; + _cryptoProvider = cryptoProvider; + _memoryStreamFactory = memoryStreamFactory; + _textEncoding = textEncoding; + request = new HttpListenerRequest(this, _textEncoding); + response = new HttpListenerResponse(this, _logger, _textEncoding); + } + + internal int ErrorStatus + { + get { return err_status; } + set { err_status = value; } + } + + internal string ErrorMessage + { + get { return error; } + set { error = value; } + } + + internal bool HaveError + { + get { return (error != null); } + } + + internal HttpConnection Connection + { + get { return cnc; } + } + + public HttpListenerRequest Request + { + get { return request; } + } + + public HttpListenerResponse Response + { + get { return response; } + } + + public IPrincipal User + { + get { return user; } + } + + internal void ParseAuthentication(AuthenticationSchemes expectedSchemes) + { + if (expectedSchemes == AuthenticationSchemes.Anonymous) + return; + + // TODO: Handle NTLM/Digest modes + string header = request.Headers["Authorization"]; + if (header == null || header.Length < 2) + return; + + string[] authenticationData = header.Split(new char[] { ' ' }, 2); + if (string.Equals(authenticationData[0], "basic", StringComparison.OrdinalIgnoreCase)) + { + user = ParseBasicAuthentication(authenticationData[1]); + } + // TODO: throw if malformed -> 400 bad request + } + + internal IPrincipal ParseBasicAuthentication(string authData) + { + try + { + // Basic AUTH Data is a formatted Base64 String + //string domain = null; + string user = null; + string password = null; + int pos = -1; + var authDataBytes = Convert.FromBase64String(authData); + string authString = _textEncoding.GetDefaultEncoding().GetString(authDataBytes, 0, authDataBytes.Length); + + // The format is DOMAIN\username:password + // Domain is optional + + pos = authString.IndexOf(':'); + + // parse the password off the end + password = authString.Substring(pos + 1); + + // discard the password + authString = authString.Substring(0, pos); + + // check if there is a domain + pos = authString.IndexOf('\\'); + + if (pos > 0) + { + //domain = authString.Substring (0, pos); + user = authString.Substring(pos); + } + else + { + user = authString; + } + + HttpListenerBasicIdentity identity = new HttpListenerBasicIdentity(user, password); + // TODO: What are the roles MS sets + return new GenericPrincipal(identity, new string[0]); + } + catch (Exception) + { + // Invalid auth data is swallowed silently + return null; + } + } + + public HttpListenerWebSocketContext AcceptWebSocket(string protocol) + { + if (protocol != null) + { + if (protocol.Length == 0) + throw new ArgumentException("An empty string.", "protocol"); + + if (!protocol.IsToken()) + throw new ArgumentException("Contains an invalid character.", "protocol"); + } + + return new HttpListenerWebSocketContext(this, protocol, _cryptoProvider, _memoryStreamFactory); + } + } + + public class GenericPrincipal : IPrincipal + { + private IIdentity m_identity; + private string[] m_roles; + + public GenericPrincipal(IIdentity identity, string[] roles) + { + if (identity == null) + throw new ArgumentNullException("identity"); + + m_identity = identity; + if (roles != null) + { + m_roles = new string[roles.Length]; + for (int i = 0; i < roles.Length; ++i) + { + m_roles[i] = roles[i]; + } + } + else + { + m_roles = null; + } + } + + public virtual IIdentity Identity + { + get + { + return m_identity; + } + } + + public virtual bool IsInRole(string role) + { + if (role == null || m_roles == null) + return false; + + for (int i = 0; i < m_roles.Length; ++i) + { + if (m_roles[i] != null && String.Compare(m_roles[i], role, StringComparison.OrdinalIgnoreCase) == 0) + return true; + } + return false; + } + } +} diff --git a/SocketHttpListener.Portable/Net/HttpListenerPrefixCollection.cs b/SocketHttpListener.Portable/Net/HttpListenerPrefixCollection.cs new file mode 100644 index 000000000..0b05539ee --- /dev/null +++ b/SocketHttpListener.Portable/Net/HttpListenerPrefixCollection.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using MediaBrowser.Model.Logging; + +namespace SocketHttpListener.Net +{ + public class HttpListenerPrefixCollection : ICollection, IEnumerable, IEnumerable + { + List prefixes = new List(); + HttpListener listener; + + private ILogger _logger; + + internal HttpListenerPrefixCollection(ILogger logger, HttpListener listener) + { + _logger = logger; + this.listener = listener; + } + + public int Count + { + get { return prefixes.Count; } + } + + public bool IsReadOnly + { + get { return false; } + } + + public bool IsSynchronized + { + get { return false; } + } + + public void Add(string uriPrefix) + { + listener.CheckDisposed(); + ListenerPrefix.CheckUri(uriPrefix); + if (prefixes.Contains(uriPrefix)) + return; + + prefixes.Add(uriPrefix); + if (listener.IsListening) + EndPointManager.AddPrefix(_logger, uriPrefix, listener); + } + + public void Clear() + { + listener.CheckDisposed(); + prefixes.Clear(); + if (listener.IsListening) + EndPointManager.RemoveListener(_logger, listener); + } + + public bool Contains(string uriPrefix) + { + listener.CheckDisposed(); + return prefixes.Contains(uriPrefix); + } + + public void CopyTo(string[] array, int offset) + { + listener.CheckDisposed(); + prefixes.CopyTo(array, offset); + } + + public void CopyTo(Array array, int offset) + { + listener.CheckDisposed(); + ((ICollection)prefixes).CopyTo(array, offset); + } + + public IEnumerator GetEnumerator() + { + return prefixes.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return prefixes.GetEnumerator(); + } + + public bool Remove(string uriPrefix) + { + listener.CheckDisposed(); + if (uriPrefix == null) + throw new ArgumentNullException("uriPrefix"); + + bool result = prefixes.Remove(uriPrefix); + if (result && listener.IsListening) + EndPointManager.RemovePrefix(_logger, uriPrefix, listener); + + return result; + } + } +} diff --git a/SocketHttpListener.Portable/Net/HttpListenerRequest.cs b/SocketHttpListener.Portable/Net/HttpListenerRequest.cs new file mode 100644 index 000000000..63d5e510d --- /dev/null +++ b/SocketHttpListener.Portable/Net/HttpListenerRequest.cs @@ -0,0 +1,654 @@ +using System; +using System.Collections.Specialized; +using System.Globalization; +using System.IO; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Services; +using MediaBrowser.Model.Text; +using SocketHttpListener.Primitives; + +namespace SocketHttpListener.Net +{ + public sealed class HttpListenerRequest + { + string[] accept_types; + Encoding content_encoding; + long content_length; + bool cl_set; + CookieCollection cookies; + WebHeaderCollection headers; + string method; + Stream input_stream; + Version version; + QueryParamCollection query_string; // check if null is ok, check if read-only, check case-sensitiveness + string raw_url; + Uri url; + Uri referrer; + string[] user_languages; + HttpListenerContext context; + bool is_chunked; + bool ka_set; + bool keep_alive; + + private readonly ITextEncoding _textEncoding; + + internal HttpListenerRequest(HttpListenerContext context, ITextEncoding textEncoding) + { + this.context = context; + _textEncoding = textEncoding; + headers = new WebHeaderCollection(); + version = HttpVersion.Version10; + } + + static char[] separators = new char[] { ' ' }; + + internal void SetRequestLine(string req) + { + string[] parts = req.Split(separators, 3); + if (parts.Length != 3) + { + context.ErrorMessage = "Invalid request line (parts)."; + return; + } + + method = parts[0]; + foreach (char c in method) + { + int ic = (int)c; + + if ((ic >= 'A' && ic <= 'Z') || + (ic > 32 && c < 127 && c != '(' && c != ')' && c != '<' && + c != '<' && c != '>' && c != '@' && c != ',' && c != ';' && + c != ':' && c != '\\' && c != '"' && c != '/' && c != '[' && + c != ']' && c != '?' && c != '=' && c != '{' && c != '}')) + continue; + + context.ErrorMessage = "(Invalid verb)"; + return; + } + + raw_url = parts[1]; + if (parts[2].Length != 8 || !parts[2].StartsWith("HTTP/")) + { + context.ErrorMessage = "Invalid request line (version)."; + return; + } + + try + { + version = new Version(parts[2].Substring(5)); + if (version.Major < 1) + throw new Exception(); + } + catch + { + context.ErrorMessage = "Invalid request line (version)."; + return; + } + } + + void CreateQueryString(string query) + { + if (query == null || query.Length == 0) + { + query_string = new QueryParamCollection(); + return; + } + + query_string = new QueryParamCollection(); + if (query[0] == '?') + query = query.Substring(1); + string[] components = query.Split('&'); + foreach (string kv in components) + { + int pos = kv.IndexOf('='); + if (pos == -1) + { + query_string.Add(null, WebUtility.UrlDecode(kv)); + } + else + { + string key = WebUtility.UrlDecode(kv.Substring(0, pos)); + string val = WebUtility.UrlDecode(kv.Substring(pos + 1)); + + query_string.Add(key, val); + } + } + } + + internal void FinishInitialization() + { + string host = UserHostName; + if (version > HttpVersion.Version10 && (host == null || host.Length == 0)) + { + context.ErrorMessage = "Invalid host name"; + return; + } + + string path; + Uri raw_uri = null; + if (MaybeUri(raw_url.ToLowerInvariant()) && Uri.TryCreate(raw_url, UriKind.Absolute, out raw_uri)) + path = raw_uri.PathAndQuery; + else + path = raw_url; + + if ((host == null || host.Length == 0)) + host = UserHostAddress; + + if (raw_uri != null) + host = raw_uri.Host; + + int colon = host.LastIndexOf(':'); + if (colon >= 0) + host = host.Substring(0, colon); + + string base_uri = String.Format("{0}://{1}:{2}", + (IsSecureConnection) ? (IsWebSocketRequest ? "wss" : "https") : (IsWebSocketRequest ? "ws" : "http"), + host, LocalEndPoint.Port); + + if (!Uri.TryCreate(base_uri + path, UriKind.Absolute, out url)) + { + context.ErrorMessage = WebUtility.HtmlEncode("Invalid url: " + base_uri + path); + return; return; + } + + CreateQueryString(url.Query); + + if (version >= HttpVersion.Version11) + { + string t_encoding = Headers["Transfer-Encoding"]; + is_chunked = (t_encoding != null && String.Compare(t_encoding, "chunked", StringComparison.OrdinalIgnoreCase) == 0); + // 'identity' is not valid! + if (t_encoding != null && !is_chunked) + { + context.Connection.SendError(null, 501); + return; + } + } + + if (!is_chunked && !cl_set) + { + if (String.Compare(method, "POST", StringComparison.OrdinalIgnoreCase) == 0 || + String.Compare(method, "PUT", StringComparison.OrdinalIgnoreCase) == 0) + { + context.Connection.SendError(null, 411); + return; + } + } + + if (String.Compare(Headers["Expect"], "100-continue", StringComparison.OrdinalIgnoreCase) == 0) + { + ResponseStream output = context.Connection.GetResponseStream(); + + var _100continue = _textEncoding.GetASCIIEncoding().GetBytes("HTTP/1.1 100 Continue\r\n\r\n"); + + output.InternalWrite(_100continue, 0, _100continue.Length); + } + } + + static bool MaybeUri(string s) + { + int p = s.IndexOf(':'); + if (p == -1) + return false; + + if (p >= 10) + return false; + + return IsPredefinedScheme(s.Substring(0, p)); + } + + // + // Using a simple block of if's is twice as slow as the compiler generated + // switch statement. But using this tuned code is faster than the + // compiler generated code, with a million loops on x86-64: + // + // With "http": .10 vs .51 (first check) + // with "https": .16 vs .51 (second check) + // with "foo": .22 vs .31 (never found) + // with "mailto": .12 vs .51 (last check) + // + // + static bool IsPredefinedScheme(string scheme) + { + if (scheme == null || scheme.Length < 3) + return false; + + char c = scheme[0]; + if (c == 'h') + return (scheme == "http" || scheme == "https"); + if (c == 'f') + return (scheme == "file" || scheme == "ftp"); + + if (c == 'n') + { + c = scheme[1]; + if (c == 'e') + return (scheme == "news" || scheme == "net.pipe" || scheme == "net.tcp"); + if (scheme == "nntp") + return true; + return false; + } + if ((c == 'g' && scheme == "gopher") || (c == 'm' && scheme == "mailto")) + return true; + + return false; + } + + internal static string Unquote(String str) + { + int start = str.IndexOf('\"'); + int end = str.LastIndexOf('\"'); + if (start >= 0 && end >= 0) + str = str.Substring(start + 1, end - 1); + return str.Trim(); + } + + internal void AddHeader(string header) + { + int colon = header.IndexOf(':'); + if (colon == -1 || colon == 0) + { + context.ErrorMessage = "Bad Request"; + context.ErrorStatus = 400; + return; + } + + string name = header.Substring(0, colon).Trim(); + string val = header.Substring(colon + 1).Trim(); + string lower = name.ToLowerInvariant(); + headers.SetInternal(name, val); + switch (lower) + { + case "accept-language": + user_languages = val.Split(','); // yes, only split with a ',' + break; + case "accept": + accept_types = val.Split(','); // yes, only split with a ',' + break; + case "content-length": + try + { + //TODO: max. content_length? + content_length = Int64.Parse(val.Trim()); + if (content_length < 0) + context.ErrorMessage = "Invalid Content-Length."; + cl_set = true; + } + catch + { + context.ErrorMessage = "Invalid Content-Length."; + } + + break; + case "content-type": + { + var contents = val.Split(';'); + foreach (var content in contents) + { + var tmp = content.Trim(); + if (tmp.StartsWith("charset")) + { + var charset = tmp.GetValue("="); + if (charset != null && charset.Length > 0) + { + try + { + + // Support upnp/dlna devices - CONTENT-TYPE: text/xml ; charset="utf-8"\r\n + charset = charset.Trim('"'); + var index = charset.IndexOf('"'); + if (index != -1) charset = charset.Substring(0, index); + + content_encoding = Encoding.GetEncoding(charset); + } + catch + { + context.ErrorMessage = "Invalid Content-Type header: " + charset; + } + } + + break; + } + } + } + break; + case "referer": + try + { + referrer = new Uri(val); + } + catch + { + referrer = new Uri("http://someone.is.screwing.with.the.headers.com/"); + } + break; + case "cookie": + if (cookies == null) + cookies = new CookieCollection(); + + string[] cookieStrings = val.Split(new char[] { ',', ';' }); + Cookie current = null; + int version = 0; + foreach (string cookieString in cookieStrings) + { + string str = cookieString.Trim(); + if (str.Length == 0) + continue; + if (str.StartsWith("$Version")) + { + version = Int32.Parse(Unquote(str.Substring(str.IndexOf('=') + 1))); + } + else if (str.StartsWith("$Path")) + { + if (current != null) + current.Path = str.Substring(str.IndexOf('=') + 1).Trim(); + } + else if (str.StartsWith("$Domain")) + { + if (current != null) + current.Domain = str.Substring(str.IndexOf('=') + 1).Trim(); + } + else if (str.StartsWith("$Port")) + { + if (current != null) + current.Port = str.Substring(str.IndexOf('=') + 1).Trim(); + } + else + { + if (current != null) + { + cookies.Add(current); + } + current = new Cookie(); + int idx = str.IndexOf('='); + if (idx > 0) + { + current.Name = str.Substring(0, idx).Trim(); + current.Value = str.Substring(idx + 1).Trim(); + } + else + { + current.Name = str.Trim(); + current.Value = String.Empty; + } + current.Version = version; + } + } + if (current != null) + { + cookies.Add(current); + } + break; + } + } + + // returns true is the stream could be reused. + internal bool FlushInput() + { + if (!HasEntityBody) + return true; + + int length = 2048; + if (content_length > 0) + length = (int)Math.Min(content_length, (long)length); + + byte[] bytes = new byte[length]; + while (true) + { + // TODO: test if MS has a timeout when doing this + try + { + var task = InputStream.ReadAsync(bytes, 0, length); + var result = Task.WaitAll(new [] { task }, 1000); + if (!result) + { + return false; + } + if (task.Result <= 0) + { + return true; + } + } + catch (ObjectDisposedException e) + { + input_stream = null; + return true; + } + catch + { + return false; + } + } + } + + public string[] AcceptTypes + { + get { return accept_types; } + } + + public int ClientCertificateError + { + get + { + HttpConnection cnc = context.Connection; + //if (cnc.ClientCertificate == null) + // throw new InvalidOperationException("No client certificate"); + //int[] errors = cnc.ClientCertificateErrors; + //if (errors != null && errors.Length > 0) + // return errors[0]; + return 0; + } + } + + public Encoding ContentEncoding + { + get + { + if (content_encoding == null) + content_encoding = _textEncoding.GetDefaultEncoding(); + return content_encoding; + } + } + + public long ContentLength64 + { + get { return content_length; } + } + + public string ContentType + { + get { return headers["content-type"]; } + } + + public CookieCollection Cookies + { + get + { + // TODO: check if the collection is read-only + if (cookies == null) + cookies = new CookieCollection(); + return cookies; + } + } + + public bool HasEntityBody + { + get { return (content_length > 0 || is_chunked); } + } + + public QueryParamCollection Headers + { + get { return headers; } + } + + public string HttpMethod + { + get { return method; } + } + + public Stream InputStream + { + get + { + if (input_stream == null) + { + if (is_chunked || content_length > 0) + input_stream = context.Connection.GetRequestStream(is_chunked, content_length); + else + input_stream = Stream.Null; + } + + return input_stream; + } + } + + public bool IsAuthenticated + { + get { return false; } + } + + public bool IsLocal + { + get { return RemoteEndPoint.IpAddress.Equals(IpAddressInfo.Loopback) || RemoteEndPoint.IpAddress.Equals(IpAddressInfo.IPv6Loopback) || LocalEndPoint.IpAddress.Equals(RemoteEndPoint.IpAddress); } + } + + public bool IsSecureConnection + { + get { return context.Connection.IsSecure; } + } + + public bool KeepAlive + { + get + { + if (ka_set) + return keep_alive; + + ka_set = true; + // 1. Connection header + // 2. Protocol (1.1 == keep-alive by default) + // 3. Keep-Alive header + string cnc = headers["Connection"]; + if (!String.IsNullOrEmpty(cnc)) + { + keep_alive = (0 == String.Compare(cnc, "keep-alive", StringComparison.OrdinalIgnoreCase)); + } + else if (version == HttpVersion.Version11) + { + keep_alive = true; + } + else + { + cnc = headers["keep-alive"]; + if (!String.IsNullOrEmpty(cnc)) + keep_alive = (0 != String.Compare(cnc, "closed", StringComparison.OrdinalIgnoreCase)); + } + return keep_alive; + } + } + + public IpEndPointInfo LocalEndPoint + { + get { return context.Connection.LocalEndPoint; } + } + + public Version ProtocolVersion + { + get { return version; } + } + + public QueryParamCollection QueryString + { + get { return query_string; } + } + + public string RawUrl + { + get { return raw_url; } + } + + public IpEndPointInfo RemoteEndPoint + { + get { return context.Connection.RemoteEndPoint; } + } + + public Guid RequestTraceIdentifier + { + get { return Guid.Empty; } + } + + public Uri Url + { + get { return url; } + } + + public Uri UrlReferrer + { + get { return referrer; } + } + + public string UserAgent + { + get { return headers["user-agent"]; } + } + + public string UserHostAddress + { + get { return LocalEndPoint.ToString(); } + } + + public string UserHostName + { + get { return headers["host"]; } + } + + public string[] UserLanguages + { + get { return user_languages; } + } + + public string ServiceName + { + get + { + return null; + } + } + + private bool _websocketRequestWasSet; + private bool _websocketRequest; + + /// + /// Gets a value indicating whether the request is a WebSocket connection request. + /// + /// + /// true if the request is a WebSocket connection request; otherwise, false. + /// + public bool IsWebSocketRequest + { + get + { + if (!_websocketRequestWasSet) + { + _websocketRequest = method == "GET" && + version > HttpVersion.Version10 && + headers.Contains("Upgrade", "websocket") && + headers.Contains("Connection", "Upgrade"); + + _websocketRequestWasSet = true; + } + + return _websocketRequest; + } + } + + public Task GetClientCertificateAsync() + { + return Task.FromResult(null); + } + } +} diff --git a/SocketHttpListener.Portable/Net/HttpListenerResponse.cs b/SocketHttpListener.Portable/Net/HttpListenerResponse.cs new file mode 100644 index 000000000..0bc827b5a --- /dev/null +++ b/SocketHttpListener.Portable/Net/HttpListenerResponse.cs @@ -0,0 +1,517 @@ +using System; +using System.Globalization; +using System.IO; +using System.Net; +using System.Text; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Text; +using SocketHttpListener.Primitives; + +namespace SocketHttpListener.Net +{ + public sealed class HttpListenerResponse : IDisposable + { + bool disposed; + Encoding content_encoding; + long content_length; + bool cl_set; + string content_type; + CookieCollection cookies; + WebHeaderCollection headers = new WebHeaderCollection(); + bool keep_alive = true; + ResponseStream output_stream; + Version version = HttpVersion.Version11; + string location; + int status_code = 200; + string status_description = "OK"; + bool chunked; + HttpListenerContext context; + + internal bool HeadersSent; + internal object headers_lock = new object(); + + bool force_close_chunked; + + private readonly ILogger _logger; + private readonly ITextEncoding _textEncoding; + + internal HttpListenerResponse(HttpListenerContext context, ILogger logger, ITextEncoding textEncoding) + { + this.context = context; + _logger = logger; + _textEncoding = textEncoding; + } + + internal bool CloseConnection + { + get + { + return headers["Connection"] == "close"; + } + } + + internal bool ForceCloseChunked + { + get { return force_close_chunked; } + } + + public Encoding ContentEncoding + { + get + { + if (content_encoding == null) + content_encoding = _textEncoding.GetDefaultEncoding(); + return content_encoding; + } + set + { + if (disposed) + throw new ObjectDisposedException(GetType().ToString()); + + content_encoding = value; + } + } + + public long ContentLength64 + { + get { return content_length; } + set + { + if (disposed) + throw new ObjectDisposedException(GetType().ToString()); + + if (HeadersSent) + throw new InvalidOperationException("Cannot be changed after headers are sent."); + + if (value < 0) + throw new ArgumentOutOfRangeException("Must be >= 0", "value"); + + cl_set = true; + content_length = value; + } + } + + public string ContentType + { + get { return content_type; } + set + { + // TODO: is null ok? + if (disposed) + throw new ObjectDisposedException(GetType().ToString()); + + content_type = value; + } + } + + // RFC 2109, 2965 + the netscape specification at http://wp.netscape.com/newsref/std/cookie_spec.html + public CookieCollection Cookies + { + get + { + if (cookies == null) + cookies = new CookieCollection(); + return cookies; + } + set { cookies = value; } // null allowed? + } + + public WebHeaderCollection Headers + { + get { return headers; } + set + { + /** + * "If you attempt to set a Content-Length, Keep-Alive, Transfer-Encoding, or + * WWW-Authenticate header using the Headers property, an exception will be + * thrown. Use the KeepAlive or ContentLength64 properties to set these headers. + * You cannot set the Transfer-Encoding or WWW-Authenticate headers manually." + */ + // TODO: check if this is marked readonly after headers are sent. + headers = value; + } + } + + public bool KeepAlive + { + get { return keep_alive; } + set + { + if (disposed) + throw new ObjectDisposedException(GetType().ToString()); + + keep_alive = value; + } + } + + public Stream OutputStream + { + get + { + if (output_stream == null) + output_stream = context.Connection.GetResponseStream(); + return output_stream; + } + } + + public Version ProtocolVersion + { + get { return version; } + set + { + if (disposed) + throw new ObjectDisposedException(GetType().ToString()); + + if (value == null) + throw new ArgumentNullException("value"); + + if (value.Major != 1 || (value.Minor != 0 && value.Minor != 1)) + throw new ArgumentException("Must be 1.0 or 1.1", "value"); + + if (disposed) + throw new ObjectDisposedException(GetType().ToString()); + + version = value; + } + } + + public string RedirectLocation + { + get { return location; } + set + { + if (disposed) + throw new ObjectDisposedException(GetType().ToString()); + + location = value; + } + } + + public bool SendChunked + { + get { return chunked; } + set + { + if (disposed) + throw new ObjectDisposedException(GetType().ToString()); + + chunked = value; + } + } + + public int StatusCode + { + get { return status_code; } + set + { + if (disposed) + throw new ObjectDisposedException(GetType().ToString()); + + if (value < 100 || value > 999) + throw new ProtocolViolationException("StatusCode must be between 100 and 999."); + status_code = value; + status_description = GetStatusDescription(value); + } + } + + internal static string GetStatusDescription(int code) + { + switch (code) + { + case 100: return "Continue"; + case 101: return "Switching Protocols"; + case 102: return "Processing"; + case 200: return "OK"; + case 201: return "Created"; + case 202: return "Accepted"; + case 203: return "Non-Authoritative Information"; + case 204: return "No Content"; + case 205: return "Reset Content"; + case 206: return "Partial Content"; + case 207: return "Multi-Status"; + case 300: return "Multiple Choices"; + case 301: return "Moved Permanently"; + case 302: return "Found"; + case 303: return "See Other"; + case 304: return "Not Modified"; + case 305: return "Use Proxy"; + case 307: return "Temporary Redirect"; + case 400: return "Bad Request"; + case 401: return "Unauthorized"; + case 402: return "Payment Required"; + case 403: return "Forbidden"; + case 404: return "Not Found"; + case 405: return "Method Not Allowed"; + case 406: return "Not Acceptable"; + case 407: return "Proxy Authentication Required"; + case 408: return "Request Timeout"; + case 409: return "Conflict"; + case 410: return "Gone"; + case 411: return "Length Required"; + case 412: return "Precondition Failed"; + case 413: return "Request Entity Too Large"; + case 414: return "Request-Uri Too Long"; + case 415: return "Unsupported Media Type"; + case 416: return "Requested Range Not Satisfiable"; + case 417: return "Expectation Failed"; + case 422: return "Unprocessable Entity"; + case 423: return "Locked"; + case 424: return "Failed Dependency"; + case 500: return "Internal Server Error"; + case 501: return "Not Implemented"; + case 502: return "Bad Gateway"; + case 503: return "Service Unavailable"; + case 504: return "Gateway Timeout"; + case 505: return "Http Version Not Supported"; + case 507: return "Insufficient Storage"; + } + return ""; + } + + public string StatusDescription + { + get { return status_description; } + set + { + status_description = value; + } + } + + void IDisposable.Dispose() + { + Close(true); //TODO: Abort or Close? + } + + public void Abort() + { + if (disposed) + return; + + Close(true); + } + + public void AddHeader(string name, string value) + { + if (name == null) + throw new ArgumentNullException("name"); + + if (name == "") + throw new ArgumentException("'name' cannot be empty", "name"); + + //TODO: check for forbidden headers and invalid characters + if (value.Length > 65535) + throw new ArgumentOutOfRangeException("value"); + + headers.Set(name, value); + } + + public void AppendCookie(Cookie cookie) + { + if (cookie == null) + throw new ArgumentNullException("cookie"); + + Cookies.Add(cookie); + } + + public void AppendHeader(string name, string value) + { + if (name == null) + throw new ArgumentNullException("name"); + + if (name == "") + throw new ArgumentException("'name' cannot be empty", "name"); + + if (value.Length > 65535) + throw new ArgumentOutOfRangeException("value"); + + headers.Add(name, value); + } + + void Close(bool force) + { + if (force) + { + _logger.Debug("HttpListenerResponse force closing HttpConnection"); + } + disposed = true; + context.Connection.Close(force); + } + + public void Close() + { + if (disposed) + return; + + Close(false); + } + + public void Close(byte[] responseEntity, bool willBlock) + { + if (disposed) + return; + + if (responseEntity == null) + throw new ArgumentNullException("responseEntity"); + + //TODO: if willBlock -> BeginWrite + Close ? + ContentLength64 = responseEntity.Length; + OutputStream.Write(responseEntity, 0, (int)content_length); + Close(false); + } + + public void Redirect(string url) + { + StatusCode = 302; // Found + location = url; + } + + bool FindCookie(Cookie cookie) + { + string name = cookie.Name; + string domain = cookie.Domain; + string path = cookie.Path; + foreach (Cookie c in cookies) + { + if (name != c.Name) + continue; + if (domain != c.Domain) + continue; + if (path == c.Path) + return true; + } + + return false; + } + + internal void SendHeaders(bool closing, MemoryStream ms) + { + Encoding encoding = content_encoding; + if (encoding == null) + encoding = _textEncoding.GetDefaultEncoding(); + + if (content_type != null) + { + if (content_encoding != null && content_type.IndexOf("charset=", StringComparison.Ordinal) == -1) + { + string enc_name = content_encoding.WebName; + headers.SetInternal("Content-Type", content_type + "; charset=" + enc_name); + } + else + { + headers.SetInternal("Content-Type", content_type); + } + } + + if (headers["Server"] == null) + headers.SetInternal("Server", "Mono-HTTPAPI/1.0"); + + CultureInfo inv = CultureInfo.InvariantCulture; + if (headers["Date"] == null) + headers.SetInternal("Date", DateTime.UtcNow.ToString("r", inv)); + + if (!chunked) + { + if (!cl_set && closing) + { + cl_set = true; + content_length = 0; + } + + if (cl_set) + headers.SetInternal("Content-Length", content_length.ToString(inv)); + } + + Version v = context.Request.ProtocolVersion; + if (!cl_set && !chunked && v >= HttpVersion.Version11) + chunked = true; + + /* Apache forces closing the connection for these status codes: + * HttpStatusCode.BadRequest 400 + * HttpStatusCode.RequestTimeout 408 + * HttpStatusCode.LengthRequired 411 + * HttpStatusCode.RequestEntityTooLarge 413 + * HttpStatusCode.RequestUriTooLong 414 + * HttpStatusCode.InternalServerError 500 + * HttpStatusCode.ServiceUnavailable 503 + */ + bool conn_close = (status_code == 400 || status_code == 408 || status_code == 411 || + status_code == 413 || status_code == 414 || status_code == 500 || + status_code == 503); + + if (conn_close == false) + conn_close = !context.Request.KeepAlive; + + // They sent both KeepAlive: true and Connection: close!? + if (!keep_alive || conn_close) + { + headers.SetInternal("Connection", "close"); + conn_close = true; + } + + if (chunked) + headers.SetInternal("Transfer-Encoding", "chunked"); + + //int reuses = context.Connection.Reuses; + //if (reuses >= 100) + //{ + // _logger.Debug("HttpListenerResponse - keep alive has exceeded 100 uses and will be closed."); + + // force_close_chunked = true; + // if (!conn_close) + // { + // headers.SetInternal("Connection", "close"); + // conn_close = true; + // } + //} + + if (!conn_close) + { + if (context.Request.ProtocolVersion <= HttpVersion.Version10) + headers.SetInternal("Connection", "keep-alive"); + } + + if (location != null) + headers.SetInternal("Location", location); + + if (cookies != null) + { + foreach (Cookie cookie in cookies) + headers.SetInternal("Set-Cookie", cookie.ToString()); + } + + using (StreamWriter writer = new StreamWriter(ms, encoding, 256, true)) + { + writer.Write("HTTP/{0} {1} {2}\r\n", version, status_code, status_description); + string headers_str = headers.ToStringMultiValue(); + writer.Write(headers_str); + writer.Flush(); + } + + int preamble = encoding.GetPreamble().Length; + if (output_stream == null) + output_stream = context.Connection.GetResponseStream(); + + /* Assumes that the ms was at position 0 */ + ms.Position = preamble; + HeadersSent = true; + } + + public void SetCookie(Cookie cookie) + { + if (cookie == null) + throw new ArgumentNullException("cookie"); + + if (cookies != null) + { + if (FindCookie(cookie)) + throw new ArgumentException("The cookie already exists."); + } + else + { + cookies = new CookieCollection(); + } + + cookies.Add(cookie); + } + } +} \ No newline at end of file diff --git a/SocketHttpListener.Portable/Net/HttpStatusCode.cs b/SocketHttpListener.Portable/Net/HttpStatusCode.cs new file mode 100644 index 000000000..93da82ba0 --- /dev/null +++ b/SocketHttpListener.Portable/Net/HttpStatusCode.cs @@ -0,0 +1,321 @@ +namespace SocketHttpListener.Net +{ + /// + /// Contains the values of the HTTP status codes. + /// + /// + /// The HttpStatusCode enumeration contains the values of the HTTP status codes defined in + /// RFC 2616 for HTTP 1.1. + /// + public enum HttpStatusCode + { + /// + /// Equivalent to status code 100. + /// Indicates that the client should continue with its request. + /// + Continue = 100, + /// + /// Equivalent to status code 101. + /// Indicates that the server is switching the HTTP version or protocol on the connection. + /// + SwitchingProtocols = 101, + /// + /// Equivalent to status code 200. + /// Indicates that the client's request has succeeded. + /// + OK = 200, + /// + /// Equivalent to status code 201. + /// Indicates that the client's request has been fulfilled and resulted in a new resource being + /// created. + /// + Created = 201, + /// + /// Equivalent to status code 202. + /// Indicates that the client's request has been accepted for processing, but the processing + /// hasn't been completed. + /// + Accepted = 202, + /// + /// Equivalent to status code 203. + /// Indicates that the returned metainformation is from a local or a third-party copy instead of + /// the origin server. + /// + NonAuthoritativeInformation = 203, + /// + /// Equivalent to status code 204. + /// Indicates that the server has fulfilled the client's request but doesn't need to return + /// an entity-body. + /// + NoContent = 204, + /// + /// Equivalent to status code 205. + /// Indicates that the server has fulfilled the client's request, and the user agent should + /// reset the document view which caused the request to be sent. + /// + ResetContent = 205, + /// + /// Equivalent to status code 206. + /// Indicates that the server has fulfilled the partial GET request for the resource. + /// + PartialContent = 206, + /// + /// + /// Equivalent to status code 300. + /// Indicates that the requested resource corresponds to any of multiple representations. + /// + /// + /// MultipleChoices is a synonym for Ambiguous. + /// + /// + MultipleChoices = 300, + /// + /// + /// Equivalent to status code 300. + /// Indicates that the requested resource corresponds to any of multiple representations. + /// + /// + /// Ambiguous is a synonym for MultipleChoices. + /// + /// + Ambiguous = 300, + /// + /// + /// Equivalent to status code 301. + /// Indicates that the requested resource has been assigned a new permanent URI and + /// any future references to this resource should use one of the returned URIs. + /// + /// + /// MovedPermanently is a synonym for Moved. + /// + /// + MovedPermanently = 301, + /// + /// + /// Equivalent to status code 301. + /// Indicates that the requested resource has been assigned a new permanent URI and + /// any future references to this resource should use one of the returned URIs. + /// + /// + /// Moved is a synonym for MovedPermanently. + /// + /// + Moved = 301, + /// + /// + /// Equivalent to status code 302. + /// Indicates that the requested resource is located temporarily under a different URI. + /// + /// + /// Found is a synonym for Redirect. + /// + /// + Found = 302, + /// + /// + /// Equivalent to status code 302. + /// Indicates that the requested resource is located temporarily under a different URI. + /// + /// + /// Redirect is a synonym for Found. + /// + /// + Redirect = 302, + /// + /// + /// Equivalent to status code 303. + /// Indicates that the response to the request can be found under a different URI and + /// should be retrieved using a GET method on that resource. + /// + /// + /// SeeOther is a synonym for RedirectMethod. + /// + /// + SeeOther = 303, + /// + /// + /// Equivalent to status code 303. + /// Indicates that the response to the request can be found under a different URI and + /// should be retrieved using a GET method on that resource. + /// + /// + /// RedirectMethod is a synonym for SeeOther. + /// + /// + RedirectMethod = 303, + /// + /// Equivalent to status code 304. + /// Indicates that the client has performed a conditional GET request and access is allowed, + /// but the document hasn't been modified. + /// + NotModified = 304, + /// + /// Equivalent to status code 305. + /// Indicates that the requested resource must be accessed through the proxy given by + /// the Location field. + /// + UseProxy = 305, + /// + /// Equivalent to status code 306. + /// This status code was used in a previous version of the specification, is no longer used, + /// and is reserved for future use. + /// + Unused = 306, + /// + /// + /// Equivalent to status code 307. + /// Indicates that the requested resource is located temporarily under a different URI. + /// + /// + /// TemporaryRedirect is a synonym for RedirectKeepVerb. + /// + /// + TemporaryRedirect = 307, + /// + /// + /// Equivalent to status code 307. + /// Indicates that the requested resource is located temporarily under a different URI. + /// + /// + /// RedirectKeepVerb is a synonym for TemporaryRedirect. + /// + /// + RedirectKeepVerb = 307, + /// + /// Equivalent to status code 400. + /// Indicates that the client's request couldn't be understood by the server due to + /// malformed syntax. + /// + BadRequest = 400, + /// + /// Equivalent to status code 401. + /// Indicates that the client's request requires user authentication. + /// + Unauthorized = 401, + /// + /// Equivalent to status code 402. + /// This status code is reserved for future use. + /// + PaymentRequired = 402, + /// + /// Equivalent to status code 403. + /// Indicates that the server understood the client's request but is refusing to fulfill it. + /// + Forbidden = 403, + /// + /// Equivalent to status code 404. + /// Indicates that the server hasn't found anything matching the request URI. + /// + NotFound = 404, + /// + /// Equivalent to status code 405. + /// Indicates that the method specified in the request line isn't allowed for the resource + /// identified by the request URI. + /// + MethodNotAllowed = 405, + /// + /// Equivalent to status code 406. + /// Indicates that the server doesn't have the appropriate resource to respond to the Accept + /// headers in the client's request. + /// + NotAcceptable = 406, + /// + /// Equivalent to status code 407. + /// Indicates that the client must first authenticate itself with the proxy. + /// + ProxyAuthenticationRequired = 407, + /// + /// Equivalent to status code 408. + /// Indicates that the client didn't produce a request within the time that the server was + /// prepared to wait. + /// + RequestTimeout = 408, + /// + /// Equivalent to status code 409. + /// Indicates that the client's request couldn't be completed due to a conflict on the server. + /// + Conflict = 409, + /// + /// Equivalent to status code 410. + /// Indicates that the requested resource is no longer available at the server and + /// no forwarding address is known. + /// + Gone = 410, + /// + /// Equivalent to status code 411. + /// Indicates that the server refuses to accept the client's request without a defined + /// Content-Length. + /// + LengthRequired = 411, + /// + /// Equivalent to status code 412. + /// Indicates that the precondition given in one or more of the request headers evaluated to + /// false when it was tested on the server. + /// + PreconditionFailed = 412, + /// + /// Equivalent to status code 413. + /// Indicates that the entity of the client's request is larger than the server is willing or + /// able to process. + /// + RequestEntityTooLarge = 413, + /// + /// Equivalent to status code 414. + /// Indicates that the request URI is longer than the server is willing to interpret. + /// + RequestUriTooLong = 414, + /// + /// Equivalent to status code 415. + /// Indicates that the entity of the client's request is in a format not supported by + /// the requested resource for the requested method. + /// + UnsupportedMediaType = 415, + /// + /// Equivalent to status code 416. + /// Indicates that none of the range specifier values in a Range request header overlap + /// the current extent of the selected resource. + /// + RequestedRangeNotSatisfiable = 416, + /// + /// Equivalent to status code 417. + /// Indicates that the expectation given in an Expect request header couldn't be met by + /// the server. + /// + ExpectationFailed = 417, + /// + /// Equivalent to status code 500. + /// Indicates that the server encountered an unexpected condition which prevented it from + /// fulfilling the client's request. + /// + InternalServerError = 500, + /// + /// Equivalent to status code 501. + /// Indicates that the server doesn't support the functionality required to fulfill the client's + /// request. + /// + NotImplemented = 501, + /// + /// Equivalent to status code 502. + /// Indicates that a gateway or proxy server received an invalid response from the upstream + /// server. + /// + BadGateway = 502, + /// + /// Equivalent to status code 503. + /// Indicates that the server is currently unable to handle the client's request due to + /// a temporary overloading or maintenance of the server. + /// + ServiceUnavailable = 503, + /// + /// Equivalent to status code 504. + /// Indicates that a gateway or proxy server didn't receive a timely response from the upstream + /// server or some other auxiliary server. + /// + GatewayTimeout = 504, + /// + /// Equivalent to status code 505. + /// Indicates that the server doesn't support the HTTP version used in the client's request. + /// + HttpVersionNotSupported = 505, + } +} diff --git a/SocketHttpListener.Portable/Net/HttpStreamAsyncResult.cs b/SocketHttpListener.Portable/Net/HttpStreamAsyncResult.cs new file mode 100644 index 000000000..518c45acb --- /dev/null +++ b/SocketHttpListener.Portable/Net/HttpStreamAsyncResult.cs @@ -0,0 +1,77 @@ +using System; +using System.Threading; + +namespace SocketHttpListener.Net +{ + class HttpStreamAsyncResult : IAsyncResult + { + object locker = new object(); + ManualResetEvent handle; + bool completed; + + internal byte[] Buffer; + internal int Offset; + internal int Count; + internal AsyncCallback Callback; + internal object State; + internal int SynchRead; + internal Exception Error; + + public void Complete(Exception e) + { + Error = e; + Complete(); + } + + public void Complete() + { + lock (locker) + { + if (completed) + return; + + completed = true; + if (handle != null) + handle.Set(); + + if (Callback != null) + Callback.BeginInvoke(this, null, null); + } + } + + public object AsyncState + { + get { return State; } + } + + public WaitHandle AsyncWaitHandle + { + get + { + lock (locker) + { + if (handle == null) + handle = new ManualResetEvent(completed); + } + + return handle; + } + } + + public bool CompletedSynchronously + { + get { return (SynchRead == Count); } + } + + public bool IsCompleted + { + get + { + lock (locker) + { + return completed; + } + } + } + } +} diff --git a/SocketHttpListener.Portable/Net/HttpVersion.cs b/SocketHttpListener.Portable/Net/HttpVersion.cs new file mode 100644 index 000000000..c0839b46d --- /dev/null +++ b/SocketHttpListener.Portable/Net/HttpVersion.cs @@ -0,0 +1,16 @@ +using System; + +namespace SocketHttpListener.Net +{ + // + // + public class HttpVersion + { + + public static readonly Version Version10 = new Version(1, 0); + public static readonly Version Version11 = new Version(1, 1); + + // pretty useless.. + public HttpVersion() { } + } +} diff --git a/SocketHttpListener.Portable/Net/ListenerPrefix.cs b/SocketHttpListener.Portable/Net/ListenerPrefix.cs new file mode 100644 index 000000000..2c314da50 --- /dev/null +++ b/SocketHttpListener.Portable/Net/ListenerPrefix.cs @@ -0,0 +1,148 @@ +using System; +using System.Net; +using MediaBrowser.Model.Net; + +namespace SocketHttpListener.Net +{ + sealed class ListenerPrefix + { + string original; + string host; + ushort port; + string path; + bool secure; + IpAddressInfo[] addresses; + public HttpListener Listener; + + public ListenerPrefix(string prefix) + { + this.original = prefix; + Parse(prefix); + } + + public override string ToString() + { + return original; + } + + public IpAddressInfo[] Addresses + { + get { return addresses; } + set { addresses = value; } + } + public bool Secure + { + get { return secure; } + } + + public string Host + { + get { return host; } + } + + public int Port + { + get { return (int)port; } + } + + public string Path + { + get { return path; } + } + + // Equals and GetHashCode are required to detect duplicates in HttpListenerPrefixCollection. + public override bool Equals(object o) + { + ListenerPrefix other = o as ListenerPrefix; + if (other == null) + return false; + + return (original == other.original); + } + + public override int GetHashCode() + { + return original.GetHashCode(); + } + + void Parse(string uri) + { + ushort default_port = 80; + if (uri.StartsWith("https://")) + { + default_port = 443; + secure = true; + } + + int length = uri.Length; + int start_host = uri.IndexOf(':') + 3; + if (start_host >= length) + throw new ArgumentException("No host specified."); + + int colon = uri.IndexOf(':', start_host, length - start_host); + int root; + if (colon > 0) + { + host = uri.Substring(start_host, colon - start_host); + root = uri.IndexOf('/', colon, length - colon); + port = (ushort)Int32.Parse(uri.Substring(colon + 1, root - colon - 1)); + path = uri.Substring(root); + } + else + { + root = uri.IndexOf('/', start_host, length - start_host); + host = uri.Substring(start_host, root - start_host); + port = default_port; + path = uri.Substring(root); + } + if (path.Length != 1) + path = path.Substring(0, path.Length - 1); + } + + public static void CheckUri(string uri) + { + if (uri == null) + throw new ArgumentNullException("uriPrefix"); + + if (!uri.StartsWith("http://") && !uri.StartsWith("https://")) + throw new ArgumentException("Only 'http' and 'https' schemes are supported."); + + int length = uri.Length; + int start_host = uri.IndexOf(':') + 3; + if (start_host >= length) + throw new ArgumentException("No host specified."); + + int colon = uri.IndexOf(':', start_host, length - start_host); + if (start_host == colon) + throw new ArgumentException("No host specified."); + + int root; + if (colon > 0) + { + root = uri.IndexOf('/', colon, length - colon); + if (root == -1) + throw new ArgumentException("No path specified."); + + try + { + int p = Int32.Parse(uri.Substring(colon + 1, root - colon - 1)); + if (p <= 0 || p >= 65536) + throw new Exception(); + } + catch + { + throw new ArgumentException("Invalid port."); + } + } + else + { + root = uri.IndexOf('/', start_host, length - start_host); + if (root == -1) + throw new ArgumentException("No path specified."); + } + + if (uri[uri.Length - 1] != '/') + throw new ArgumentException("The prefix must end with '/'"); + } + } +} diff --git a/SocketHttpListener.Portable/Net/RequestStream.cs b/SocketHttpListener.Portable/Net/RequestStream.cs new file mode 100644 index 000000000..58030500d --- /dev/null +++ b/SocketHttpListener.Portable/Net/RequestStream.cs @@ -0,0 +1,231 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace SocketHttpListener.Net +{ + class RequestStream : Stream + { + byte[] buffer; + int offset; + int length; + long remaining_body; + bool disposed; + Stream stream; + + internal RequestStream(Stream stream, byte[] buffer, int offset, int length) + : this(stream, buffer, offset, length, -1) + { + } + + internal RequestStream(Stream stream, byte[] buffer, int offset, int length, long contentlength) + { + this.stream = stream; + this.buffer = buffer; + this.offset = offset; + this.length = length; + this.remaining_body = contentlength; + } + + public override bool CanRead + { + get { return true; } + } + + public override bool CanSeek + { + get { return false; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override long Length + { + get { throw new NotSupportedException(); } + } + + public override long Position + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + + protected override void Dispose(bool disposing) + { + disposed = true; + } + + public override void Flush() + { + } + + + // Returns 0 if we can keep reading from the base stream, + // > 0 if we read something from the buffer. + // -1 if we had a content length set and we finished reading that many bytes. + int FillFromBuffer(byte[] buffer, int off, int count) + { + if (buffer == null) + throw new ArgumentNullException("buffer"); + if (off < 0) + throw new ArgumentOutOfRangeException("offset", "< 0"); + if (count < 0) + throw new ArgumentOutOfRangeException("count", "< 0"); + int len = buffer.Length; + if (off > len) + throw new ArgumentException("destination offset is beyond array size"); + if (off > len - count) + throw new ArgumentException("Reading would overrun buffer"); + + if (this.remaining_body == 0) + return -1; + + if (this.length == 0) + return 0; + + int size = Math.Min(this.length, count); + if (this.remaining_body > 0) + size = (int)Math.Min(size, this.remaining_body); + + if (this.offset > this.buffer.Length - size) + { + size = Math.Min(size, this.buffer.Length - this.offset); + } + if (size == 0) + return 0; + + Buffer.BlockCopy(this.buffer, this.offset, buffer, off, size); + this.offset += size; + this.length -= size; + if (this.remaining_body > 0) + remaining_body -= size; + return size; + } + + public override int Read([In, Out] byte[] buffer, int offset, int count) + { + if (disposed) + throw new ObjectDisposedException(typeof(RequestStream).ToString()); + + // Call FillFromBuffer to check for buffer boundaries even when remaining_body is 0 + int nread = FillFromBuffer(buffer, offset, count); + if (nread == -1) + { // No more bytes available (Content-Length) + return 0; + } + else if (nread > 0) + { + return nread; + } + + nread = stream.Read(buffer, offset, count); + if (nread > 0 && remaining_body > 0) + remaining_body -= nread; + return nread; + } + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (disposed) + throw new ObjectDisposedException(typeof(RequestStream).ToString()); + + int nread = FillFromBuffer(buffer, offset, count); + if (nread > 0 || nread == -1) + { + return Math.Max(0, nread); + } + + // Avoid reading past the end of the request to allow + // for HTTP pipelining + if (remaining_body >= 0 && count > remaining_body) + count = (int)Math.Min(Int32.MaxValue, remaining_body); + + nread = await stream.ReadAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); + if (remaining_body > 0 && nread > 0) + remaining_body -= nread; + return nread; + } + + //public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, + // AsyncCallback cback, object state) + //{ + // if (disposed) + // throw new ObjectDisposedException(typeof(RequestStream).ToString()); + + // int nread = FillFromBuffer(buffer, offset, count); + // if (nread > 0 || nread == -1) + // { + // HttpStreamAsyncResult ares = new HttpStreamAsyncResult(); + // ares.Buffer = buffer; + // ares.Offset = offset; + // ares.Count = count; + // ares.Callback = cback; + // ares.State = state; + // ares.SynchRead = Math.Max(0, nread); + // ares.Complete(); + // return ares; + // } + + // // Avoid reading past the end of the request to allow + // // for HTTP pipelining + // if (remaining_body >= 0 && count > remaining_body) + // count = (int)Math.Min(Int32.MaxValue, remaining_body); + // return stream.BeginRead(buffer, offset, count, cback, state); + //} + + //public override int EndRead(IAsyncResult ares) + //{ + // if (disposed) + // throw new ObjectDisposedException(typeof(RequestStream).ToString()); + + // if (ares == null) + // throw new ArgumentNullException("async_result"); + + // if (ares is HttpStreamAsyncResult) + // { + // HttpStreamAsyncResult r = (HttpStreamAsyncResult)ares; + // if (!ares.IsCompleted) + // ares.AsyncWaitHandle.WaitOne(); + // return r.SynchRead; + // } + + // // Close on exception? + // int nread = stream.EndRead(ares); + // if (remaining_body > 0 && nread > 0) + // remaining_body -= nread; + // return nread; + //} + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + //public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, + // AsyncCallback cback, object state) + //{ + // throw new NotSupportedException(); + //} + + //public override void EndWrite(IAsyncResult async_result) + //{ + // throw new NotSupportedException(); + //} + } +} diff --git a/SocketHttpListener.Portable/Net/ResponseStream.cs b/SocketHttpListener.Portable/Net/ResponseStream.cs new file mode 100644 index 000000000..6ecbf9742 --- /dev/null +++ b/SocketHttpListener.Portable/Net/ResponseStream.cs @@ -0,0 +1,316 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Text; +using SocketHttpListener.Primitives; + +namespace SocketHttpListener.Net +{ + // FIXME: Does this buffer the response until Close? + // Update: we send a single packet for the first non-chunked Write + // What happens when we set content-length to X and write X-1 bytes then close? + // what if we don't set content-length at all? + class ResponseStream : Stream + { + HttpListenerResponse response; + bool ignore_errors; + bool disposed; + bool trailer_sent; + Stream stream; + private readonly IMemoryStreamFactory _memoryStreamFactory; + private readonly ITextEncoding _textEncoding; + + internal ResponseStream(Stream stream, HttpListenerResponse response, bool ignore_errors, IMemoryStreamFactory memoryStreamFactory, ITextEncoding textEncoding) + { + this.response = response; + this.ignore_errors = ignore_errors; + _memoryStreamFactory = memoryStreamFactory; + _textEncoding = textEncoding; + this.stream = stream; + } + + public override bool CanRead + { + get { return false; } + } + + public override bool CanSeek + { + get { return false; } + } + + public override bool CanWrite + { + get { return true; } + } + + public override long Length + { + get { throw new NotSupportedException(); } + } + + public override long Position + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + + protected override void Dispose(bool disposing) + { + if (disposed == false) + { + disposed = true; + byte[] bytes = null; + MemoryStream ms = GetHeaders(true); + bool chunked = response.SendChunked; + if (stream.CanWrite) + { + try + { + if (ms != null) + { + long start = ms.Position; + if (chunked && !trailer_sent) + { + bytes = GetChunkSizeBytes(0, true); + ms.Position = ms.Length; + ms.Write(bytes, 0, bytes.Length); + } + byte[] msBuffer; + _memoryStreamFactory.TryGetBuffer(ms, out msBuffer); + InternalWrite(msBuffer, (int)start, (int)(ms.Length - start)); + trailer_sent = true; + } + else if (chunked && !trailer_sent) + { + bytes = GetChunkSizeBytes(0, true); + InternalWrite(bytes, 0, bytes.Length); + trailer_sent = true; + } + } + catch (IOException ex) + { + // Ignore error due to connection reset by peer + } + } + response.Close(); + } + + base.Dispose(disposing); + } + + MemoryStream GetHeaders(bool closing) + { + // SendHeaders works on shared headers + lock (response.headers_lock) + { + if (response.HeadersSent) + return null; + MemoryStream ms = _memoryStreamFactory.CreateNew(); + response.SendHeaders(closing, ms); + return ms; + } + } + + public override void Flush() + { + } + + static byte[] crlf = new byte[] { 13, 10 }; + byte[] GetChunkSizeBytes(int size, bool final) + { + string str = String.Format("{0:x}\r\n{1}", size, final ? "\r\n" : ""); + return _textEncoding.GetASCIIEncoding().GetBytes(str); + } + + internal void InternalWrite(byte[] buffer, int offset, int count) + { + if (ignore_errors) + { + try + { + stream.Write(buffer, offset, count); + } + catch { } + } + else + { + stream.Write(buffer, offset, count); + } + } + + public override void Write(byte[] buffer, int offset, int count) + { + if (disposed) + throw new ObjectDisposedException(GetType().ToString()); + + byte[] bytes = null; + MemoryStream ms = GetHeaders(false); + bool chunked = response.SendChunked; + if (ms != null) + { + long start = ms.Position; // After the possible preamble for the encoding + ms.Position = ms.Length; + if (chunked) + { + bytes = GetChunkSizeBytes(count, false); + ms.Write(bytes, 0, bytes.Length); + } + + int new_count = Math.Min(count, 16384 - (int)ms.Position + (int)start); + ms.Write(buffer, offset, new_count); + count -= new_count; + offset += new_count; + byte[] msBuffer; + _memoryStreamFactory.TryGetBuffer(ms, out msBuffer); + InternalWrite(msBuffer, (int)start, (int)(ms.Length - start)); + ms.SetLength(0); + ms.Capacity = 0; // 'dispose' the buffer in ms. + } + else if (chunked) + { + bytes = GetChunkSizeBytes(count, false); + InternalWrite(bytes, 0, bytes.Length); + } + + if (count > 0) + InternalWrite(buffer, offset, count); + if (chunked) + InternalWrite(crlf, 0, 2); + } + + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (disposed) + throw new ObjectDisposedException(GetType().ToString()); + + byte[] bytes = null; + MemoryStream ms = GetHeaders(false); + bool chunked = response.SendChunked; + if (ms != null) + { + long start = ms.Position; + ms.Position = ms.Length; + if (chunked) + { + bytes = GetChunkSizeBytes(count, false); + ms.Write(bytes, 0, bytes.Length); + } + ms.Write(buffer, offset, count); + byte[] msBuffer; + _memoryStreamFactory.TryGetBuffer(ms, out msBuffer); + buffer = msBuffer; + offset = (int)start; + count = (int)(ms.Position - start); + } + else if (chunked) + { + bytes = GetChunkSizeBytes(count, false); + InternalWrite(bytes, 0, bytes.Length); + } + + try + { + if (count > 0) + { + await stream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); + } + + if (response.SendChunked) + stream.Write(crlf, 0, 2); + } + catch + { + if (!ignore_errors) + { + throw; + } + } + } + + //public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, + // AsyncCallback cback, object state) + //{ + // if (disposed) + // throw new ObjectDisposedException(GetType().ToString()); + + // byte[] bytes = null; + // MemoryStream ms = GetHeaders(false); + // bool chunked = response.SendChunked; + // if (ms != null) + // { + // long start = ms.Position; + // ms.Position = ms.Length; + // if (chunked) + // { + // bytes = GetChunkSizeBytes(count, false); + // ms.Write(bytes, 0, bytes.Length); + // } + // ms.Write(buffer, offset, count); + // buffer = ms.ToArray(); + // offset = (int)start; + // count = (int)(ms.Position - start); + // } + // else if (chunked) + // { + // bytes = GetChunkSizeBytes(count, false); + // InternalWrite(bytes, 0, bytes.Length); + // } + + // return stream.BeginWrite(buffer, offset, count, cback, state); + //} + + //public override void EndWrite(IAsyncResult ares) + //{ + // if (disposed) + // throw new ObjectDisposedException(GetType().ToString()); + + // if (ignore_errors) + // { + // try + // { + // stream.EndWrite(ares); + // if (response.SendChunked) + // stream.Write(crlf, 0, 2); + // } + // catch { } + // } + // else { + // stream.EndWrite(ares); + // if (response.SendChunked) + // stream.Write(crlf, 0, 2); + // } + //} + + public override int Read([In, Out] byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + //public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, + // AsyncCallback cback, object state) + //{ + // throw new NotSupportedException(); + //} + + //public override int EndRead(IAsyncResult ares) + //{ + // throw new NotSupportedException(); + //} + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + } +} diff --git a/SocketHttpListener.Portable/Net/WebHeaderCollection.cs b/SocketHttpListener.Portable/Net/WebHeaderCollection.cs new file mode 100644 index 000000000..d20f99b9b --- /dev/null +++ b/SocketHttpListener.Portable/Net/WebHeaderCollection.cs @@ -0,0 +1,391 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Net; +using System.Runtime.InteropServices; +using System.Runtime.Serialization; +using System.Text; +using MediaBrowser.Model.Services; + +namespace SocketHttpListener.Net +{ + [ComVisible(true)] + public class WebHeaderCollection : QueryParamCollection + { + [Flags] + internal enum HeaderInfo + { + Request = 1, + Response = 1 << 1, + MultiValue = 1 << 10 + } + + static readonly bool[] allowed_chars = { + false, false, false, false, false, false, false, false, false, false, false, false, false, false, + false, false, false, false, false, false, false, false, false, false, false, false, false, false, + false, false, false, false, false, true, false, true, true, true, true, false, false, false, true, + true, false, true, true, false, true, true, true, true, true, true, true, true, true, true, false, + false, false, false, false, false, false, true, true, true, true, true, true, true, true, true, + true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, + false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, + true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, + false, true, false + }; + + static readonly Dictionary headers; + HeaderInfo? headerRestriction; + HeaderInfo? headerConsistency; + + static WebHeaderCollection() + { + headers = new Dictionary(StringComparer.OrdinalIgnoreCase) { + { "Allow", HeaderInfo.MultiValue }, + { "Accept", HeaderInfo.Request | HeaderInfo.MultiValue }, + { "Accept-Charset", HeaderInfo.MultiValue }, + { "Accept-Encoding", HeaderInfo.MultiValue }, + { "Accept-Language", HeaderInfo.MultiValue }, + { "Accept-Ranges", HeaderInfo.MultiValue }, + { "Age", HeaderInfo.Response }, + { "Authorization", HeaderInfo.MultiValue }, + { "Cache-Control", HeaderInfo.MultiValue }, + { "Cookie", HeaderInfo.MultiValue }, + { "Connection", HeaderInfo.Request | HeaderInfo.MultiValue }, + { "Content-Encoding", HeaderInfo.MultiValue }, + { "Content-Length", HeaderInfo.Request | HeaderInfo.Response }, + { "Content-Type", HeaderInfo.Request }, + { "Content-Language", HeaderInfo.MultiValue }, + { "Date", HeaderInfo.Request }, + { "Expect", HeaderInfo.Request | HeaderInfo.MultiValue}, + { "Host", HeaderInfo.Request }, + { "If-Match", HeaderInfo.MultiValue }, + { "If-Modified-Since", HeaderInfo.Request }, + { "If-None-Match", HeaderInfo.MultiValue }, + { "Keep-Alive", HeaderInfo.Response }, + { "Pragma", HeaderInfo.MultiValue }, + { "Proxy-Authenticate", HeaderInfo.MultiValue }, + { "Proxy-Authorization", HeaderInfo.MultiValue }, + { "Proxy-Connection", HeaderInfo.Request | HeaderInfo.MultiValue }, + { "Range", HeaderInfo.Request | HeaderInfo.MultiValue }, + { "Referer", HeaderInfo.Request }, + { "Set-Cookie", HeaderInfo.MultiValue }, + { "Set-Cookie2", HeaderInfo.MultiValue }, + { "Server", HeaderInfo.Response }, + { "TE", HeaderInfo.MultiValue }, + { "Trailer", HeaderInfo.MultiValue }, + { "Transfer-Encoding", HeaderInfo.Request | HeaderInfo.Response | HeaderInfo.MultiValue }, + { "Translate", HeaderInfo.Request | HeaderInfo.Response }, + { "Upgrade", HeaderInfo.MultiValue }, + { "User-Agent", HeaderInfo.Request }, + { "Vary", HeaderInfo.MultiValue }, + { "Via", HeaderInfo.MultiValue }, + { "Warning", HeaderInfo.MultiValue }, + { "WWW-Authenticate", HeaderInfo.Response | HeaderInfo. MultiValue }, + { "SecWebSocketAccept", HeaderInfo.Response }, + { "SecWebSocketExtensions", HeaderInfo.Request | HeaderInfo.Response | HeaderInfo. MultiValue }, + { "SecWebSocketKey", HeaderInfo.Request }, + { "Sec-WebSocket-Protocol", HeaderInfo.Request | HeaderInfo.Response | HeaderInfo. MultiValue }, + { "SecWebSocketVersion", HeaderInfo.Response | HeaderInfo. MultiValue } + }; + } + + // Methods + + public void Add(string header) + { + if (header == null) + throw new ArgumentNullException("header"); + int pos = header.IndexOf(':'); + if (pos == -1) + throw new ArgumentException("no colon found", "header"); + + this.Add(header.Substring(0, pos), header.Substring(pos + 1)); + } + + public override void Add(string name, string value) + { + if (name == null) + throw new ArgumentNullException("name"); + + ThrowIfRestricted(name); + this.AddWithoutValidate(name, value); + } + + protected void AddWithoutValidate(string headerName, string headerValue) + { + if (!IsHeaderName(headerName)) + throw new ArgumentException("invalid header name: " + headerName, "headerName"); + if (headerValue == null) + headerValue = String.Empty; + else + headerValue = headerValue.Trim(); + if (!IsHeaderValue(headerValue)) + throw new ArgumentException("invalid header value: " + headerValue, "headerValue"); + + AddValue(headerName, headerValue); + } + + internal void AddValue(string headerName, string headerValue) + { + base.Add(headerName, headerValue); + } + + internal string[] GetValues_internal(string header, bool split) + { + if (header == null) + throw new ArgumentNullException("header"); + + string[] values = base.GetValues(header); + if (values == null || values.Length == 0) + return null; + + if (split && IsMultiValue(header)) + { + List separated = null; + foreach (var value in values) + { + if (value.IndexOf(',') < 0) + { + if (separated != null) + separated.Add(value); + + continue; + } + + if (separated == null) + { + separated = new List(values.Length + 1); + foreach (var v in values) + { + if (v == value) + break; + + separated.Add(v); + } + } + + var slices = value.Split(','); + var slices_length = slices.Length; + if (value[value.Length - 1] == ',') + --slices_length; + + for (int i = 0; i < slices_length; ++i) + { + separated.Add(slices[i].Trim()); + } + } + + if (separated != null) + return separated.ToArray(); + } + + return values; + } + + public override string[] GetValues(string header) + { + return GetValues_internal(header, true); + } + + public override string[] GetValues(int index) + { + string[] values = base.GetValues(index); + + if (values == null || values.Length == 0) + { + return null; + } + + return values; + } + + public static bool IsRestricted(string headerName) + { + return IsRestricted(headerName, false); + } + + public static bool IsRestricted(string headerName, bool response) + { + if (headerName == null) + throw new ArgumentNullException("headerName"); + + if (headerName.Length == 0) + throw new ArgumentException("empty string", "headerName"); + + if (!IsHeaderName(headerName)) + throw new ArgumentException("Invalid character in header"); + + HeaderInfo info; + if (!headers.TryGetValue(headerName, out info)) + return false; + + var flag = response ? HeaderInfo.Response : HeaderInfo.Request; + return (info & flag) != 0; + } + + public override void Set(string name, string value) + { + if (name == null) + throw new ArgumentNullException("name"); + if (!IsHeaderName(name)) + throw new ArgumentException("invalid header name"); + if (value == null) + value = String.Empty; + else + value = value.Trim(); + if (!IsHeaderValue(value)) + throw new ArgumentException("invalid header value"); + + ThrowIfRestricted(name); + base.Set(name, value); + } + + internal string ToStringMultiValue() + { + StringBuilder sb = new StringBuilder(); + + int count = base.Count; + for (int i = 0; i < count; i++) + { + string key = GetKey(i); + if (IsMultiValue(key)) + { + foreach (string v in GetValues(i)) + { + sb.Append(key) + .Append(": ") + .Append(v) + .Append("\r\n"); + } + } + else + { + sb.Append(key) + .Append(": ") + .Append(Get(i)) + .Append("\r\n"); + } + } + return sb.Append("\r\n").ToString(); + } + + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + + int count = base.Count; + for (int i = 0; i < count; i++) + sb.Append(GetKey(i)) + .Append(": ") + .Append(Get(i)) + .Append("\r\n"); + + return sb.Append("\r\n").ToString(); + } + + + // Internal Methods + + // With this we don't check for invalid characters in header. See bug #55994. + internal void SetInternal(string header) + { + int pos = header.IndexOf(':'); + if (pos == -1) + throw new ArgumentException("no colon found", "header"); + + SetInternal(header.Substring(0, pos), header.Substring(pos + 1)); + } + + internal void SetInternal(string name, string value) + { + if (value == null) + value = String.Empty; + else + value = value.Trim(); + if (!IsHeaderValue(value)) + throw new ArgumentException("invalid header value"); + + if (IsMultiValue(name)) + { + base.Add(name, value); + } + else + { + base.Remove(name); + base.Set(name, value); + } + } + + // Private Methods + + public override int Remove(string name) + { + ThrowIfRestricted(name); + return base.Remove(name); + } + + protected void ThrowIfRestricted(string headerName) + { + if (!headerRestriction.HasValue) + return; + + HeaderInfo info; + if (!headers.TryGetValue(headerName, out info)) + return; + + if ((info & headerRestriction.Value) != 0) + throw new ArgumentException("This header must be modified with the appropriate property."); + } + + internal static bool IsMultiValue(string headerName) + { + if (headerName == null) + return false; + + HeaderInfo info; + return headers.TryGetValue(headerName, out info) && (info & HeaderInfo.MultiValue) != 0; + } + + internal static bool IsHeaderValue(string value) + { + // TEXT any 8 bit value except CTL's (0-31 and 127) + // but including \r\n space and \t + // after a newline at least one space or \t must follow + // certain header fields allow comments () + + int len = value.Length; + for (int i = 0; i < len; i++) + { + char c = value[i]; + if (c == 127) + return false; + if (c < 0x20 && (c != '\r' && c != '\n' && c != '\t')) + return false; + if (c == '\n' && ++i < len) + { + c = value[i]; + if (c != ' ' && c != '\t') + return false; + } + } + + return true; + } + + internal static bool IsHeaderName(string name) + { + if (name == null || name.Length == 0) + return false; + + int len = name.Length; + for (int i = 0; i < len; i++) + { + char c = name[i]; + if (c > 126 || !allowed_chars[c]) + return false; + } + + return true; + } + } +} diff --git a/SocketHttpListener.Portable/Net/WebSockets/HttpListenerWebSocketContext.cs b/SocketHttpListener.Portable/Net/WebSockets/HttpListenerWebSocketContext.cs new file mode 100644 index 000000000..426e15ecd --- /dev/null +++ b/SocketHttpListener.Portable/Net/WebSockets/HttpListenerWebSocketContext.cs @@ -0,0 +1,347 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.IO; +using System.Net; +using System.Security.Principal; +using MediaBrowser.Model.Cryptography; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Services; +using SocketHttpListener.Primitives; + +namespace SocketHttpListener.Net.WebSockets +{ + /// + /// Provides the properties used to access the information in a WebSocket connection request + /// received by the . + /// + /// + /// + public class HttpListenerWebSocketContext : WebSocketContext + { + #region Private Fields + + private HttpListenerContext _context; + private WebSocket _websocket; + + #endregion + + #region Internal Constructors + + internal HttpListenerWebSocketContext( + HttpListenerContext context, string protocol, ICryptoProvider cryptoProvider, IMemoryStreamFactory memoryStreamFactory) + { + _context = context; + _websocket = new WebSocket(this, protocol, cryptoProvider, memoryStreamFactory); + } + + #endregion + + #region Internal Properties + + internal Stream Stream + { + get + { + return _context.Connection.Stream; + } + } + + #endregion + + #region Public Properties + + /// + /// Gets the HTTP cookies included in the request. + /// + /// + /// A that contains the cookies. + /// + public override CookieCollection CookieCollection + { + get + { + return _context.Request.Cookies; + } + } + + /// + /// Gets the HTTP headers included in the request. + /// + /// + /// A that contains the headers. + /// + public override QueryParamCollection Headers + { + get + { + return _context.Request.Headers; + } + } + + /// + /// Gets the value of the Host header included in the request. + /// + /// + /// A that represents the value of the Host header. + /// + public override string Host + { + get + { + return _context.Request.Headers["Host"]; + } + } + + /// + /// Gets a value indicating whether the client is authenticated. + /// + /// + /// true if the client is authenticated; otherwise, false. + /// + public override bool IsAuthenticated + { + get + { + return _context.Request.IsAuthenticated; + } + } + + /// + /// Gets a value indicating whether the client connected from the local computer. + /// + /// + /// true if the client connected from the local computer; otherwise, false. + /// + public override bool IsLocal + { + get + { + return _context.Request.IsLocal; + } + } + + /// + /// Gets a value indicating whether the WebSocket connection is secured. + /// + /// + /// true if the connection is secured; otherwise, false. + /// + public override bool IsSecureConnection + { + get + { + return _context.Connection.IsSecure; + } + } + + /// + /// Gets a value indicating whether the request is a WebSocket connection request. + /// + /// + /// true if the request is a WebSocket connection request; otherwise, false. + /// + public override bool IsWebSocketRequest + { + get + { + return _context.Request.IsWebSocketRequest; + } + } + + /// + /// Gets the value of the Origin header included in the request. + /// + /// + /// A that represents the value of the Origin header. + /// + public override string Origin + { + get + { + return _context.Request.Headers["Origin"]; + } + } + + /// + /// Gets the query string included in the request. + /// + /// + /// A that contains the query string parameters. + /// + public override QueryParamCollection QueryString + { + get + { + return _context.Request.QueryString; + } + } + + /// + /// Gets the URI requested by the client. + /// + /// + /// A that represents the requested URI. + /// + public override Uri RequestUri + { + get + { + return _context.Request.Url; + } + } + + /// + /// Gets the value of the Sec-WebSocket-Key header included in the request. + /// + /// + /// This property provides a part of the information used by the server to prove that it + /// received a valid WebSocket connection request. + /// + /// + /// A that represents the value of the Sec-WebSocket-Key header. + /// + public override string SecWebSocketKey + { + get + { + return _context.Request.Headers["Sec-WebSocket-Key"]; + } + } + + /// + /// Gets the values of the Sec-WebSocket-Protocol header included in the request. + /// + /// + /// This property represents the subprotocols requested by the client. + /// + /// + /// An instance that provides + /// an enumerator which supports the iteration over the values of the Sec-WebSocket-Protocol + /// header. + /// + public override IEnumerable SecWebSocketProtocols + { + get + { + var protocols = _context.Request.Headers["Sec-WebSocket-Protocol"]; + if (protocols != null) + foreach (var protocol in protocols.Split(',')) + yield return protocol.Trim(); + } + } + + /// + /// Gets the value of the Sec-WebSocket-Version header included in the request. + /// + /// + /// This property represents the WebSocket protocol version. + /// + /// + /// A that represents the value of the Sec-WebSocket-Version header. + /// + public override string SecWebSocketVersion + { + get + { + return _context.Request.Headers["Sec-WebSocket-Version"]; + } + } + + /// + /// Gets the server endpoint as an IP address and a port number. + /// + /// + /// + public override IpEndPointInfo ServerEndPoint + { + get + { + return _context.Connection.LocalEndPoint; + } + } + + /// + /// Gets the client information (identity, authentication, and security roles). + /// + /// + /// A that represents the client information. + /// + public override IPrincipal User + { + get + { + return _context.User; + } + } + + /// + /// Gets the client endpoint as an IP address and a port number. + /// + /// + /// + public override IpEndPointInfo UserEndPoint + { + get + { + return _context.Connection.RemoteEndPoint; + } + } + + /// + /// Gets the instance used for two-way communication + /// between client and server. + /// + /// + /// A . + /// + public override WebSocket WebSocket + { + get + { + return _websocket; + } + } + + #endregion + + #region Internal Methods + + internal void Close() + { + try + { + _context.Connection.Close(true); + } + catch + { + // catch errors sending the closing handshake + } + } + + internal void Close(HttpStatusCode code) + { + _context.Response.Close(code); + } + + #endregion + + #region Public Methods + + /// + /// Returns a that represents the current + /// . + /// + /// + /// A that represents the current + /// . + /// + public override string ToString() + { + return _context.Request.ToString(); + } + + #endregion + } +} diff --git a/SocketHttpListener.Portable/Net/WebSockets/WebSocketContext.cs b/SocketHttpListener.Portable/Net/WebSockets/WebSocketContext.cs new file mode 100644 index 000000000..3ffa6e639 --- /dev/null +++ b/SocketHttpListener.Portable/Net/WebSockets/WebSocketContext.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Net; +using System.Security.Principal; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Services; + +namespace SocketHttpListener.Net.WebSockets +{ + /// + /// Exposes the properties used to access the information in a WebSocket connection request. + /// + /// + /// The WebSocketContext class is an abstract class. + /// + public abstract class WebSocketContext + { + #region Protected Constructors + + /// + /// Initializes a new instance of the class. + /// + protected WebSocketContext() + { + } + + #endregion + + #region Public Properties + + /// + /// Gets the HTTP cookies included in the request. + /// + /// + /// A that contains the cookies. + /// + public abstract CookieCollection CookieCollection { get; } + + /// + /// Gets the HTTP headers included in the request. + /// + /// + /// A that contains the headers. + /// + public abstract QueryParamCollection Headers { get; } + + /// + /// Gets the value of the Host header included in the request. + /// + /// + /// A that represents the value of the Host header. + /// + public abstract string Host { get; } + + /// + /// Gets a value indicating whether the client is authenticated. + /// + /// + /// true if the client is authenticated; otherwise, false. + /// + public abstract bool IsAuthenticated { get; } + + /// + /// Gets a value indicating whether the client connected from the local computer. + /// + /// + /// true if the client connected from the local computer; otherwise, false. + /// + public abstract bool IsLocal { get; } + + /// + /// Gets a value indicating whether the WebSocket connection is secured. + /// + /// + /// true if the connection is secured; otherwise, false. + /// + public abstract bool IsSecureConnection { get; } + + /// + /// Gets a value indicating whether the request is a WebSocket connection request. + /// + /// + /// true if the request is a WebSocket connection request; otherwise, false. + /// + public abstract bool IsWebSocketRequest { get; } + + /// + /// Gets the value of the Origin header included in the request. + /// + /// + /// A that represents the value of the Origin header. + /// + public abstract string Origin { get; } + + /// + /// Gets the query string included in the request. + /// + /// + /// A that contains the query string parameters. + /// + public abstract QueryParamCollection QueryString { get; } + + /// + /// Gets the URI requested by the client. + /// + /// + /// A that represents the requested URI. + /// + public abstract Uri RequestUri { get; } + + /// + /// Gets the value of the Sec-WebSocket-Key header included in the request. + /// + /// + /// This property provides a part of the information used by the server to prove that it + /// received a valid WebSocket connection request. + /// + /// + /// A that represents the value of the Sec-WebSocket-Key header. + /// + public abstract string SecWebSocketKey { get; } + + /// + /// Gets the values of the Sec-WebSocket-Protocol header included in the request. + /// + /// + /// This property represents the subprotocols requested by the client. + /// + /// + /// An instance that provides + /// an enumerator which supports the iteration over the values of the Sec-WebSocket-Protocol + /// header. + /// + public abstract IEnumerable SecWebSocketProtocols { get; } + + /// + /// Gets the value of the Sec-WebSocket-Version header included in the request. + /// + /// + /// This property represents the WebSocket protocol version. + /// + /// + /// A that represents the value of the Sec-WebSocket-Version header. + /// + public abstract string SecWebSocketVersion { get; } + + /// + /// Gets the server endpoint as an IP address and a port number. + /// + /// + /// A that represents the server endpoint. + /// + public abstract IpEndPointInfo ServerEndPoint { get; } + + /// + /// Gets the client information (identity, authentication, and security roles). + /// + /// + /// A that represents the client information. + /// + public abstract IPrincipal User { get; } + + /// + /// Gets the client endpoint as an IP address and a port number. + /// + /// + /// A that represents the client endpoint. + /// + public abstract IpEndPointInfo UserEndPoint { get; } + + /// + /// Gets the instance used for two-way communication + /// between client and server. + /// + /// + /// A . + /// + public abstract WebSocket WebSocket { get; } + + #endregion + } +} diff --git a/SocketHttpListener.Portable/Opcode.cs b/SocketHttpListener.Portable/Opcode.cs new file mode 100644 index 000000000..62b7d8585 --- /dev/null +++ b/SocketHttpListener.Portable/Opcode.cs @@ -0,0 +1,43 @@ +namespace SocketHttpListener +{ + /// + /// Contains the values of the opcode that indicates the type of a WebSocket frame. + /// + /// + /// The values of the opcode are defined in + /// Section 5.2 of RFC 6455. + /// + public enum Opcode : byte + { + /// + /// Equivalent to numeric value 0. + /// Indicates a continuation frame. + /// + Cont = 0x0, + /// + /// Equivalent to numeric value 1. + /// Indicates a text frame. + /// + Text = 0x1, + /// + /// Equivalent to numeric value 2. + /// Indicates a binary frame. + /// + Binary = 0x2, + /// + /// Equivalent to numeric value 8. + /// Indicates a connection close frame. + /// + Close = 0x8, + /// + /// Equivalent to numeric value 9. + /// Indicates a ping frame. + /// + Ping = 0x9, + /// + /// Equivalent to numeric value 10. + /// Indicates a pong frame. + /// + Pong = 0xa + } +} diff --git a/SocketHttpListener.Portable/PayloadData.cs b/SocketHttpListener.Portable/PayloadData.cs new file mode 100644 index 000000000..a6318da2b --- /dev/null +++ b/SocketHttpListener.Portable/PayloadData.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; + +namespace SocketHttpListener +{ + internal class PayloadData : IEnumerable + { + #region Private Fields + + private byte [] _applicationData; + private byte [] _extensionData; + private bool _masked; + + #endregion + + #region Public Const Fields + + public const ulong MaxLength = long.MaxValue; + + #endregion + + #region Public Constructors + + public PayloadData () + : this (new byte [0], new byte [0], false) + { + } + + public PayloadData (byte [] applicationData) + : this (new byte [0], applicationData, false) + { + } + + public PayloadData (string applicationData) + : this (new byte [0], Encoding.UTF8.GetBytes (applicationData), false) + { + } + + public PayloadData (byte [] applicationData, bool masked) + : this (new byte [0], applicationData, masked) + { + } + + public PayloadData (byte [] extensionData, byte [] applicationData, bool masked) + { + _extensionData = extensionData; + _applicationData = applicationData; + _masked = masked; + } + + #endregion + + #region Internal Properties + + internal bool ContainsReservedCloseStatusCode { + get { + return _applicationData.Length > 1 && + _applicationData.SubArray (0, 2).ToUInt16 (ByteOrder.Big).IsReserved (); + } + } + + #endregion + + #region Public Properties + + public byte [] ApplicationData { + get { + return _applicationData; + } + } + + public byte [] ExtensionData { + get { + return _extensionData; + } + } + + public bool IsMasked { + get { + return _masked; + } + } + + public ulong Length { + get { + return (ulong) (_extensionData.Length + _applicationData.Length); + } + } + + #endregion + + #region Private Methods + + private static void mask (byte [] src, byte [] key) + { + for (long i = 0; i < src.Length; i++) + src [i] = (byte) (src [i] ^ key [i % 4]); + } + + #endregion + + #region Public Methods + + public IEnumerator GetEnumerator () + { + foreach (byte b in _extensionData) + yield return b; + + foreach (byte b in _applicationData) + yield return b; + } + + public void Mask (byte [] maskingKey) + { + if (_extensionData.Length > 0) + mask (_extensionData, maskingKey); + + if (_applicationData.Length > 0) + mask (_applicationData, maskingKey); + + _masked = !_masked; + } + + public byte [] ToByteArray () + { + return _extensionData.Length > 0 + ? new List (this).ToArray () + : _applicationData; + } + + public override string ToString () + { + return BitConverter.ToString (ToByteArray ()); + } + + #endregion + + #region Explicitly Implemented Interface Members + + IEnumerator IEnumerable.GetEnumerator () + { + return GetEnumerator (); + } + + #endregion + } +} diff --git a/SocketHttpListener.Portable/Primitives/HttpListenerException.cs b/SocketHttpListener.Portable/Primitives/HttpListenerException.cs new file mode 100644 index 000000000..7b383fd23 --- /dev/null +++ b/SocketHttpListener.Portable/Primitives/HttpListenerException.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SocketHttpListener.Primitives +{ + public class HttpListenerException : Exception + { + public HttpListenerException(int statusCode, string message) + : base(message) + { + + } + } +} diff --git a/SocketHttpListener.Portable/Primitives/ICertificate.cs b/SocketHttpListener.Portable/Primitives/ICertificate.cs new file mode 100644 index 000000000..1289da13d --- /dev/null +++ b/SocketHttpListener.Portable/Primitives/ICertificate.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SocketHttpListener.Primitives +{ + public interface ICertificate + { + } +} diff --git a/SocketHttpListener.Portable/Primitives/IStreamFactory.cs b/SocketHttpListener.Portable/Primitives/IStreamFactory.cs new file mode 100644 index 000000000..f189b95b4 --- /dev/null +++ b/SocketHttpListener.Portable/Primitives/IStreamFactory.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using MediaBrowser.Model.Net; + +namespace SocketHttpListener.Primitives +{ + public interface IStreamFactory + { + Stream CreateNetworkStream(ISocket socket, bool ownsSocket); + Stream CreateSslStream(Stream innerStream, bool leaveInnerStreamOpen); + + Task AuthenticateSslStreamAsServer(Stream stream, ICertificate certificate); + } +} diff --git a/SocketHttpListener.Portable/Primitives/ITextEncoding.cs b/SocketHttpListener.Portable/Primitives/ITextEncoding.cs new file mode 100644 index 000000000..b10145687 --- /dev/null +++ b/SocketHttpListener.Portable/Primitives/ITextEncoding.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using MediaBrowser.Model.Text; + +namespace SocketHttpListener.Primitives +{ + public static class TextEncodingExtensions + { + public static Encoding GetDefaultEncoding(this ITextEncoding encoding) + { + return Encoding.UTF8; + } + } +} diff --git a/SocketHttpListener.Portable/Properties/AssemblyInfo.cs b/SocketHttpListener.Portable/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..870426460 --- /dev/null +++ b/SocketHttpListener.Portable/Properties/AssemblyInfo.cs @@ -0,0 +1,30 @@ +using System.Resources; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("SocketHttpListener.Portable")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("SocketHttpListener.Portable")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: NeutralResourcesLanguage("en")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/SocketHttpListener.Portable/Rsv.cs b/SocketHttpListener.Portable/Rsv.cs new file mode 100644 index 000000000..668059b8a --- /dev/null +++ b/SocketHttpListener.Portable/Rsv.cs @@ -0,0 +1,8 @@ +namespace SocketHttpListener +{ + internal enum Rsv : byte + { + Off = 0x0, + On = 0x1 + } +} diff --git a/SocketHttpListener.Portable/SocketHttpListener.Portable.csproj b/SocketHttpListener.Portable/SocketHttpListener.Portable.csproj new file mode 100644 index 000000000..f7b3a643c --- /dev/null +++ b/SocketHttpListener.Portable/SocketHttpListener.Portable.csproj @@ -0,0 +1,109 @@ + + + + + 11.0 + Debug + AnyCPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E} + Library + Properties + SocketHttpListener.Portable + SocketHttpListener.Portable + en-US + 512 + {786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Profile7 + v4.5 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {9142eefa-7570-41e1-bfcc-468bb571af2f} + MediaBrowser.Common + + + {7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b} + MediaBrowser.Model + + + + + if $(ConfigurationName) == Release ( +xcopy "$(TargetPath)" "$(SolutionDir)\Nuget\dlls\" /y /d /r /i +) + + + \ No newline at end of file diff --git a/SocketHttpListener.Portable/SocketHttpListener.Portable.nuget.targets b/SocketHttpListener.Portable/SocketHttpListener.Portable.nuget.targets new file mode 100644 index 000000000..e69ce0e64 --- /dev/null +++ b/SocketHttpListener.Portable/SocketHttpListener.Portable.nuget.targets @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/SocketHttpListener.Portable/WebSocket.cs b/SocketHttpListener.Portable/WebSocket.cs new file mode 100644 index 000000000..889880387 --- /dev/null +++ b/SocketHttpListener.Portable/WebSocket.cs @@ -0,0 +1,898 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; +using System.Threading; +using MediaBrowser.Model.Cryptography; +using MediaBrowser.Model.IO; +using SocketHttpListener.Net.WebSockets; +using SocketHttpListener.Primitives; +using HttpStatusCode = SocketHttpListener.Net.HttpStatusCode; + +namespace SocketHttpListener +{ + /// + /// Implements the WebSocket interface. + /// + /// + /// The WebSocket class provides a set of methods and properties for two-way communication using + /// the WebSocket protocol (RFC 6455). + /// + public class WebSocket : IDisposable + { + #region Private Fields + + private string _base64Key; + private Action _closeContext; + private CompressionMethod _compression; + private WebSocketContext _context; + private CookieCollection _cookies; + private string _extensions; + private AutoResetEvent _exitReceiving; + private object _forConn; + private object _forEvent; + private object _forMessageEventQueue; + private object _forSend; + private const string _guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + private Func + _handshakeRequestChecker; + private Queue _messageEventQueue; + private uint _nonceCount; + private string _origin; + private bool _preAuth; + private string _protocol; + private string[] _protocols; + private Uri _proxyUri; + private volatile WebSocketState _readyState; + private AutoResetEvent _receivePong; + private bool _secure; + private Stream _stream; + private Uri _uri; + private const string _version = "13"; + private readonly IMemoryStreamFactory _memoryStreamFactory; + + private readonly ICryptoProvider _cryptoProvider; + + #endregion + + #region Internal Fields + + internal const int FragmentLength = 1016; // Max value is int.MaxValue - 14. + + #endregion + + #region Internal Constructors + + // As server + internal WebSocket(HttpListenerWebSocketContext context, string protocol, ICryptoProvider cryptoProvider, IMemoryStreamFactory memoryStreamFactory) + { + _context = context; + _protocol = protocol; + _cryptoProvider = cryptoProvider; + _memoryStreamFactory = memoryStreamFactory; + + _closeContext = context.Close; + _secure = context.IsSecureConnection; + _stream = context.Stream; + + init(); + } + + #endregion + + // As server + internal Func CustomHandshakeRequestChecker + { + get + { + return _handshakeRequestChecker ?? (context => null); + } + + set + { + _handshakeRequestChecker = value; + } + } + + internal bool IsConnected + { + get + { + return _readyState == WebSocketState.Open || _readyState == WebSocketState.Closing; + } + } + + /// + /// Gets the state of the WebSocket connection. + /// + /// + /// One of the enum values, indicates the state of the WebSocket + /// connection. The default value is . + /// + public WebSocketState ReadyState + { + get + { + return _readyState; + } + } + + #region Public Events + + /// + /// Occurs when the WebSocket connection has been closed. + /// + public event EventHandler OnClose; + + /// + /// Occurs when the gets an error. + /// + public event EventHandler OnError; + + /// + /// Occurs when the receives a message. + /// + public event EventHandler OnMessage; + + /// + /// Occurs when the WebSocket connection has been established. + /// + public event EventHandler OnOpen; + + #endregion + + #region Private Methods + + // As server + private bool acceptHandshake() + { + var msg = checkIfValidHandshakeRequest(_context); + if (msg != null) + { + error("An error has occurred while connecting: " + msg); + Close(HttpStatusCode.BadRequest); + + return false; + } + + if (_protocol != null && + !_context.SecWebSocketProtocols.Contains(protocol => protocol == _protocol)) + _protocol = null; + + ////var extensions = _context.Headers["Sec-WebSocket-Extensions"]; + ////if (extensions != null && extensions.Length > 0) + //// processSecWebSocketExtensionsHeader(extensions); + + return sendHttpResponse(createHandshakeResponse()); + } + + // As server + private string checkIfValidHandshakeRequest(WebSocketContext context) + { + var headers = context.Headers; + return context.RequestUri == null + ? "Invalid request url." + : !context.IsWebSocketRequest + ? "Not WebSocket connection request." + : !validateSecWebSocketKeyHeader(headers["Sec-WebSocket-Key"]) + ? "Invalid Sec-WebSocket-Key header." + : !validateSecWebSocketVersionClientHeader(headers["Sec-WebSocket-Version"]) + ? "Invalid Sec-WebSocket-Version header." + : CustomHandshakeRequestChecker(context); + } + + private void close(CloseStatusCode code, string reason, bool wait) + { + close(new PayloadData(((ushort)code).Append(reason)), !code.IsReserved(), wait); + } + + private void close(PayloadData payload, bool send, bool wait) + { + lock (_forConn) + { + if (_readyState == WebSocketState.Closing || _readyState == WebSocketState.Closed) + { + return; + } + + _readyState = WebSocketState.Closing; + } + + var e = new CloseEventArgs(payload); + e.WasClean = + closeHandshake( + send ? WebSocketFrame.CreateCloseFrame(Mask.Unmask, payload).ToByteArray() : null, + wait ? 1000 : 0, + closeServerResources); + + _readyState = WebSocketState.Closed; + try + { + OnClose.Emit(this, e); + } + catch (Exception ex) + { + error("An exception has occurred while OnClose.", ex); + } + } + + private bool closeHandshake(byte[] frameAsBytes, int millisecondsTimeout, Action release) + { + var sent = frameAsBytes != null && writeBytes(frameAsBytes); + var received = + millisecondsTimeout == 0 || + (sent && _exitReceiving != null && _exitReceiving.WaitOne(millisecondsTimeout)); + + release(); + if (_receivePong != null) + { + _receivePong.Dispose(); + _receivePong = null; + } + + if (_exitReceiving != null) + { + _exitReceiving.Dispose(); + _exitReceiving = null; + } + + var result = sent && received; + + return result; + } + + // As server + private void closeServerResources() + { + if (_closeContext == null) + return; + + _closeContext(); + _closeContext = null; + _stream = null; + _context = null; + } + + private bool concatenateFragmentsInto(Stream dest) + { + while (true) + { + var frame = WebSocketFrame.Read(_stream, true); + if (frame.IsFinal) + { + /* FINAL */ + + // CONT + if (frame.IsContinuation) + { + dest.WriteBytes(frame.PayloadData.ApplicationData); + break; + } + + // PING + if (frame.IsPing) + { + processPingFrame(frame); + continue; + } + + // PONG + if (frame.IsPong) + { + processPongFrame(frame); + continue; + } + + // CLOSE + if (frame.IsClose) + return processCloseFrame(frame); + } + else + { + /* MORE */ + + // CONT + if (frame.IsContinuation) + { + dest.WriteBytes(frame.PayloadData.ApplicationData); + continue; + } + } + + // ? + return processUnsupportedFrame( + frame, + CloseStatusCode.IncorrectData, + "An incorrect data has been received while receiving fragmented data."); + } + + return true; + } + + // As server + private HttpResponse createHandshakeCloseResponse(HttpStatusCode code) + { + var res = HttpResponse.CreateCloseResponse(code); + res.Headers["Sec-WebSocket-Version"] = _version; + + return res; + } + + // As server + private HttpResponse createHandshakeResponse() + { + var res = HttpResponse.CreateWebSocketResponse(); + + var headers = res.Headers; + headers["Sec-WebSocket-Accept"] = CreateResponseKey(_base64Key); + + if (_protocol != null) + headers["Sec-WebSocket-Protocol"] = _protocol; + + if (_extensions != null) + headers["Sec-WebSocket-Extensions"] = _extensions; + + if (_cookies.Count > 0) + res.SetCookies(_cookies); + + return res; + } + + private MessageEventArgs dequeueFromMessageEventQueue() + { + lock (_forMessageEventQueue) + return _messageEventQueue.Count > 0 + ? _messageEventQueue.Dequeue() + : null; + } + + private void enqueueToMessageEventQueue(MessageEventArgs e) + { + lock (_forMessageEventQueue) + _messageEventQueue.Enqueue(e); + } + + private void error(string message, Exception exception) + { + try + { + if (exception != null) + { + message += ". Exception.Message: " + exception.Message; + } + OnError.Emit(this, new ErrorEventArgs(message)); + } + catch (Exception ex) + { + } + } + + private void error(string message) + { + try + { + OnError.Emit(this, new ErrorEventArgs(message)); + } + catch (Exception ex) + { + } + } + + private void init() + { + _compression = CompressionMethod.None; + _cookies = new CookieCollection(); + _forConn = new object(); + _forEvent = new object(); + _forSend = new object(); + _messageEventQueue = new Queue(); + _forMessageEventQueue = ((ICollection)_messageEventQueue).SyncRoot; + _readyState = WebSocketState.Connecting; + } + + private void open() + { + try + { + startReceiving(); + + lock (_forEvent) + { + try + { + OnOpen.Emit(this, EventArgs.Empty); + } + catch (Exception ex) + { + processException(ex, "An exception has occurred while OnOpen."); + } + } + } + catch (Exception ex) + { + processException(ex, "An exception has occurred while opening."); + } + } + + private bool processCloseFrame(WebSocketFrame frame) + { + var payload = frame.PayloadData; + close(payload, !payload.ContainsReservedCloseStatusCode, false); + + return false; + } + + private bool processDataFrame(WebSocketFrame frame) + { + var e = frame.IsCompressed + ? new MessageEventArgs( + frame.Opcode, frame.PayloadData.ApplicationData.Decompress(_compression)) + : new MessageEventArgs(frame.Opcode, frame.PayloadData); + + enqueueToMessageEventQueue(e); + return true; + } + + private void processException(Exception exception, string message) + { + var code = CloseStatusCode.Abnormal; + var reason = message; + if (exception is WebSocketException) + { + var wsex = (WebSocketException)exception; + code = wsex.Code; + reason = wsex.Message; + } + + error(message ?? code.GetMessage(), exception); + if (_readyState == WebSocketState.Connecting) + Close(HttpStatusCode.BadRequest); + else + close(code, reason ?? code.GetMessage(), false); + } + + private bool processFragmentedFrame(WebSocketFrame frame) + { + return frame.IsContinuation // Not first fragment + ? true + : processFragments(frame); + } + + private bool processFragments(WebSocketFrame first) + { + using (var buff = _memoryStreamFactory.CreateNew()) + { + buff.WriteBytes(first.PayloadData.ApplicationData); + if (!concatenateFragmentsInto(buff)) + return false; + + byte[] data; + if (_compression != CompressionMethod.None) + { + data = buff.DecompressToArray(_compression); + } + else + { + data = buff.ToArray(); + } + + enqueueToMessageEventQueue(new MessageEventArgs(first.Opcode, data)); + return true; + } + } + + private bool processPingFrame(WebSocketFrame frame) + { + var mask = Mask.Unmask; + + return true; + } + + private bool processPongFrame(WebSocketFrame frame) + { + _receivePong.Set(); + + return true; + } + + private bool processUnsupportedFrame(WebSocketFrame frame, CloseStatusCode code, string reason) + { + processException(new WebSocketException(code, reason), null); + + return false; + } + + private bool processWebSocketFrame(WebSocketFrame frame) + { + return frame.IsCompressed && _compression == CompressionMethod.None + ? processUnsupportedFrame( + frame, + CloseStatusCode.IncorrectData, + "A compressed data has been received without available decompression method.") + : frame.IsFragmented + ? processFragmentedFrame(frame) + : frame.IsData + ? processDataFrame(frame) + : frame.IsPing + ? processPingFrame(frame) + : frame.IsPong + ? processPongFrame(frame) + : frame.IsClose + ? processCloseFrame(frame) + : processUnsupportedFrame(frame, CloseStatusCode.PolicyViolation, null); + } + + private bool send(Opcode opcode, Stream stream) + { + lock (_forSend) + { + var src = stream; + var compressed = false; + var sent = false; + try + { + if (_compression != CompressionMethod.None) + { + stream = stream.Compress(_compression); + compressed = true; + } + + sent = send(opcode, Mask.Unmask, stream, compressed); + if (!sent) + error("Sending a data has been interrupted."); + } + catch (Exception ex) + { + error("An exception has occurred while sending a data.", ex); + } + finally + { + if (compressed) + stream.Dispose(); + + src.Dispose(); + } + + return sent; + } + } + + private bool send(Opcode opcode, Mask mask, Stream stream, bool compressed) + { + var len = stream.Length; + + /* Not fragmented */ + + if (len == 0) + return send(Fin.Final, opcode, mask, new byte[0], compressed); + + var quo = len / FragmentLength; + var rem = (int)(len % FragmentLength); + + byte[] buff = null; + if (quo == 0) + { + buff = new byte[rem]; + return stream.Read(buff, 0, rem) == rem && + send(Fin.Final, opcode, mask, buff, compressed); + } + + buff = new byte[FragmentLength]; + if (quo == 1 && rem == 0) + return stream.Read(buff, 0, FragmentLength) == FragmentLength && + send(Fin.Final, opcode, mask, buff, compressed); + + /* Send fragmented */ + + // Begin + if (stream.Read(buff, 0, FragmentLength) != FragmentLength || + !send(Fin.More, opcode, mask, buff, compressed)) + return false; + + var n = rem == 0 ? quo - 2 : quo - 1; + for (long i = 0; i < n; i++) + if (stream.Read(buff, 0, FragmentLength) != FragmentLength || + !send(Fin.More, Opcode.Cont, mask, buff, compressed)) + return false; + + // End + if (rem == 0) + rem = FragmentLength; + else + buff = new byte[rem]; + + return stream.Read(buff, 0, rem) == rem && + send(Fin.Final, Opcode.Cont, mask, buff, compressed); + } + + private bool send(Fin fin, Opcode opcode, Mask mask, byte[] data, bool compressed) + { + lock (_forConn) + { + if (_readyState != WebSocketState.Open) + { + return false; + } + + return writeBytes( + WebSocketFrame.CreateWebSocketFrame(fin, opcode, mask, data, compressed).ToByteArray()); + } + } + + private void sendAsync(Opcode opcode, Stream stream, Action completed) + { + Func sender = send; + sender.BeginInvoke( + opcode, + stream, + ar => + { + try + { + var sent = sender.EndInvoke(ar); + if (completed != null) + completed(sent); + } + catch (Exception ex) + { + error("An exception has occurred while callback.", ex); + } + }, + null); + } + + // As server + private bool sendHttpResponse(HttpResponse response) + { + return writeBytes(response.ToByteArray()); + } + + private void startReceiving() + { + if (_messageEventQueue.Count > 0) + _messageEventQueue.Clear(); + + _exitReceiving = new AutoResetEvent(false); + _receivePong = new AutoResetEvent(false); + + Action receive = null; + receive = () => WebSocketFrame.ReadAsync( + _stream, + true, + frame => + { + if (processWebSocketFrame(frame) && _readyState != WebSocketState.Closed) + { + receive(); + + if (!frame.IsData) + return; + + lock (_forEvent) + { + try + { + var e = dequeueFromMessageEventQueue(); + if (e != null && _readyState == WebSocketState.Open) + OnMessage.Emit(this, e); + } + catch (Exception ex) + { + processException(ex, "An exception has occurred while OnMessage."); + } + } + } + else if (_exitReceiving != null) + { + _exitReceiving.Set(); + } + }, + ex => processException(ex, "An exception has occurred while receiving a message.")); + + receive(); + } + + // As server + private bool validateSecWebSocketKeyHeader(string value) + { + if (value == null || value.Length == 0) + return false; + + _base64Key = value; + return true; + } + + // As server + private bool validateSecWebSocketVersionClientHeader(string value) + { + return true; + //return value != null && value == _version; + } + + private bool writeBytes(byte[] data) + { + try + { + _stream.Write(data, 0, data.Length); + return true; + } + catch (Exception ex) + { + return false; + } + } + + #endregion + + #region Internal Methods + + // As server + internal void Close(HttpResponse response) + { + _readyState = WebSocketState.Closing; + + sendHttpResponse(response); + closeServerResources(); + + _readyState = WebSocketState.Closed; + } + + // As server + internal void Close(HttpStatusCode code) + { + Close(createHandshakeCloseResponse(code)); + } + + // As server + public void ConnectAsServer() + { + try + { + if (acceptHandshake()) + { + _readyState = WebSocketState.Open; + open(); + } + } + catch (Exception ex) + { + processException(ex, "An exception has occurred while connecting."); + } + } + + private string CreateResponseKey(string base64Key) + { + var buff = new StringBuilder(base64Key, 64); + buff.Append(_guid); + var src = _cryptoProvider.ComputeSHA1(Encoding.UTF8.GetBytes(buff.ToString())); + + return Convert.ToBase64String(src); + } + + #endregion + + #region Public Methods + + /// + /// Closes the WebSocket connection, and releases all associated resources. + /// + public void Close() + { + var msg = _readyState.CheckIfClosable(); + if (msg != null) + { + error(msg); + + return; + } + + var send = _readyState == WebSocketState.Open; + close(new PayloadData(), send, send); + } + + /// + /// Closes the WebSocket connection with the specified + /// and , and releases all associated resources. + /// + /// + /// This method emits a event if the size + /// of is greater than 123 bytes. + /// + /// + /// One of the enum values, represents the status code + /// indicating the reason for the close. + /// + /// + /// A that represents the reason for the close. + /// + public void Close(CloseStatusCode code, string reason) + { + byte[] data = null; + var msg = _readyState.CheckIfClosable() ?? + (data = ((ushort)code).Append(reason)).CheckIfValidControlData("reason"); + + if (msg != null) + { + error(msg); + + return; + } + + var send = _readyState == WebSocketState.Open && !code.IsReserved(); + close(new PayloadData(data), send, send); + } + + /// + /// Sends a binary asynchronously using the WebSocket connection. + /// + /// + /// This method doesn't wait for the send to be complete. + /// + /// + /// An array of that represents the binary data to send. + /// + /// + /// An Action<bool> delegate that references the method(s) called when the send is + /// complete. A passed to this delegate is true if the send is + /// complete successfully; otherwise, false. + /// + public void SendAsync(byte[] data, Action completed) + { + var msg = _readyState.CheckIfOpen() ?? data.CheckIfValidSendData(); + if (msg != null) + { + error(msg); + + return; + } + + sendAsync(Opcode.Binary, _memoryStreamFactory.CreateNew(data), completed); + } + + /// + /// Sends a text asynchronously using the WebSocket connection. + /// + /// + /// This method doesn't wait for the send to be complete. + /// + /// + /// A that represents the text data to send. + /// + /// + /// An Action<bool> delegate that references the method(s) called when the send is + /// complete. A passed to this delegate is true if the send is + /// complete successfully; otherwise, false. + /// + public void SendAsync(string data, Action completed) + { + var msg = _readyState.CheckIfOpen() ?? data.CheckIfValidSendData(); + if (msg != null) + { + error(msg); + + return; + } + + sendAsync(Opcode.Text, _memoryStreamFactory.CreateNew(Encoding.UTF8.GetBytes(data)), completed); + } + + #endregion + + #region Explicit Interface Implementation + + /// + /// Closes the WebSocket connection, and releases all associated resources. + /// + /// + /// This method closes the WebSocket connection with . + /// + void IDisposable.Dispose() + { + Close(CloseStatusCode.Away, null); + } + + #endregion + } +} \ No newline at end of file diff --git a/SocketHttpListener.Portable/WebSocketException.cs b/SocketHttpListener.Portable/WebSocketException.cs new file mode 100644 index 000000000..260721317 --- /dev/null +++ b/SocketHttpListener.Portable/WebSocketException.cs @@ -0,0 +1,60 @@ +using System; + +namespace SocketHttpListener +{ + /// + /// The exception that is thrown when a gets a fatal error. + /// + public class WebSocketException : Exception + { + #region Internal Constructors + + internal WebSocketException () + : this (CloseStatusCode.Abnormal, null, null) + { + } + + internal WebSocketException (string message) + : this (CloseStatusCode.Abnormal, message, null) + { + } + + internal WebSocketException (CloseStatusCode code) + : this (code, null, null) + { + } + + internal WebSocketException (string message, Exception innerException) + : this (CloseStatusCode.Abnormal, message, innerException) + { + } + + internal WebSocketException (CloseStatusCode code, string message) + : this (code, message, null) + { + } + + internal WebSocketException (CloseStatusCode code, string message, Exception innerException) + : base (message ?? code.GetMessage (), innerException) + { + Code = code; + } + + #endregion + + #region Public Properties + + /// + /// Gets the status code indicating the cause for the exception. + /// + /// + /// One of the enum values, represents the status code indicating + /// the cause for the exception. + /// + public CloseStatusCode Code { + get; private set; + } + + #endregion + } +} diff --git a/SocketHttpListener.Portable/WebSocketFrame.cs b/SocketHttpListener.Portable/WebSocketFrame.cs new file mode 100644 index 000000000..44fa4a5dc --- /dev/null +++ b/SocketHttpListener.Portable/WebSocketFrame.cs @@ -0,0 +1,578 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace SocketHttpListener +{ + internal class WebSocketFrame : IEnumerable + { + #region Private Fields + + private byte[] _extPayloadLength; + private Fin _fin; + private Mask _mask; + private byte[] _maskingKey; + private Opcode _opcode; + private PayloadData _payloadData; + private byte _payloadLength; + private Rsv _rsv1; + private Rsv _rsv2; + private Rsv _rsv3; + + #endregion + + #region Internal Fields + + internal static readonly byte[] EmptyUnmaskPingData; + + #endregion + + #region Static Constructor + + static WebSocketFrame() + { + EmptyUnmaskPingData = CreatePingFrame(Mask.Unmask).ToByteArray(); + } + + #endregion + + #region Private Constructors + + private WebSocketFrame() + { + } + + #endregion + + #region Internal Constructors + + internal WebSocketFrame(Opcode opcode, PayloadData payload) + : this(Fin.Final, opcode, Mask.Mask, payload, false) + { + } + + internal WebSocketFrame(Opcode opcode, Mask mask, PayloadData payload) + : this(Fin.Final, opcode, mask, payload, false) + { + } + + internal WebSocketFrame(Fin fin, Opcode opcode, Mask mask, PayloadData payload) + : this(fin, opcode, mask, payload, false) + { + } + + internal WebSocketFrame( + Fin fin, Opcode opcode, Mask mask, PayloadData payload, bool compressed) + { + _fin = fin; + _rsv1 = isData(opcode) && compressed ? Rsv.On : Rsv.Off; + _rsv2 = Rsv.Off; + _rsv3 = Rsv.Off; + _opcode = opcode; + _mask = mask; + + var len = payload.Length; + if (len < 126) + { + _payloadLength = (byte)len; + _extPayloadLength = new byte[0]; + } + else if (len < 0x010000) + { + _payloadLength = (byte)126; + _extPayloadLength = ((ushort)len).ToByteArrayInternally(ByteOrder.Big); + } + else + { + _payloadLength = (byte)127; + _extPayloadLength = len.ToByteArrayInternally(ByteOrder.Big); + } + + if (mask == Mask.Mask) + { + _maskingKey = createMaskingKey(); + payload.Mask(_maskingKey); + } + else + { + _maskingKey = new byte[0]; + } + + _payloadData = payload; + } + + #endregion + + #region Public Properties + + public byte[] ExtendedPayloadLength + { + get + { + return _extPayloadLength; + } + } + + public Fin Fin + { + get + { + return _fin; + } + } + + public bool IsBinary + { + get + { + return _opcode == Opcode.Binary; + } + } + + public bool IsClose + { + get + { + return _opcode == Opcode.Close; + } + } + + public bool IsCompressed + { + get + { + return _rsv1 == Rsv.On; + } + } + + public bool IsContinuation + { + get + { + return _opcode == Opcode.Cont; + } + } + + public bool IsControl + { + get + { + return _opcode == Opcode.Close || _opcode == Opcode.Ping || _opcode == Opcode.Pong; + } + } + + public bool IsData + { + get + { + return _opcode == Opcode.Binary || _opcode == Opcode.Text; + } + } + + public bool IsFinal + { + get + { + return _fin == Fin.Final; + } + } + + public bool IsFragmented + { + get + { + return _fin == Fin.More || _opcode == Opcode.Cont; + } + } + + public bool IsMasked + { + get + { + return _mask == Mask.Mask; + } + } + + public bool IsPerMessageCompressed + { + get + { + return (_opcode == Opcode.Binary || _opcode == Opcode.Text) && _rsv1 == Rsv.On; + } + } + + public bool IsPing + { + get + { + return _opcode == Opcode.Ping; + } + } + + public bool IsPong + { + get + { + return _opcode == Opcode.Pong; + } + } + + public bool IsText + { + get + { + return _opcode == Opcode.Text; + } + } + + public ulong Length + { + get + { + return 2 + (ulong)(_extPayloadLength.Length + _maskingKey.Length) + _payloadData.Length; + } + } + + public Mask Mask + { + get + { + return _mask; + } + } + + public byte[] MaskingKey + { + get + { + return _maskingKey; + } + } + + public Opcode Opcode + { + get + { + return _opcode; + } + } + + public PayloadData PayloadData + { + get + { + return _payloadData; + } + } + + public byte PayloadLength + { + get + { + return _payloadLength; + } + } + + public Rsv Rsv1 + { + get + { + return _rsv1; + } + } + + public Rsv Rsv2 + { + get + { + return _rsv2; + } + } + + public Rsv Rsv3 + { + get + { + return _rsv3; + } + } + + #endregion + + #region Private Methods + + private byte[] createMaskingKey() + { + var key = new byte[4]; + var rand = new Random(); + rand.NextBytes(key); + + return key; + } + + private static bool isControl(Opcode opcode) + { + return opcode == Opcode.Close || opcode == Opcode.Ping || opcode == Opcode.Pong; + } + + private static bool isData(Opcode opcode) + { + return opcode == Opcode.Text || opcode == Opcode.Binary; + } + + private static WebSocketFrame read(byte[] header, Stream stream, bool unmask) + { + /* Header */ + + // FIN + var fin = (header[0] & 0x80) == 0x80 ? Fin.Final : Fin.More; + // RSV1 + var rsv1 = (header[0] & 0x40) == 0x40 ? Rsv.On : Rsv.Off; + // RSV2 + var rsv2 = (header[0] & 0x20) == 0x20 ? Rsv.On : Rsv.Off; + // RSV3 + var rsv3 = (header[0] & 0x10) == 0x10 ? Rsv.On : Rsv.Off; + // Opcode + var opcode = (Opcode)(header[0] & 0x0f); + // MASK + var mask = (header[1] & 0x80) == 0x80 ? Mask.Mask : Mask.Unmask; + // Payload Length + var payloadLen = (byte)(header[1] & 0x7f); + + // Check if correct frame. + var incorrect = isControl(opcode) && fin == Fin.More + ? "A control frame is fragmented." + : !isData(opcode) && rsv1 == Rsv.On + ? "A non data frame is compressed." + : null; + + if (incorrect != null) + throw new WebSocketException(CloseStatusCode.IncorrectData, incorrect); + + // Check if consistent frame. + if (isControl(opcode) && payloadLen > 125) + throw new WebSocketException( + CloseStatusCode.InconsistentData, + "The length of payload data of a control frame is greater than 125 bytes."); + + var frame = new WebSocketFrame(); + frame._fin = fin; + frame._rsv1 = rsv1; + frame._rsv2 = rsv2; + frame._rsv3 = rsv3; + frame._opcode = opcode; + frame._mask = mask; + frame._payloadLength = payloadLen; + + /* Extended Payload Length */ + + var size = payloadLen < 126 + ? 0 + : payloadLen == 126 + ? 2 + : 8; + + var extPayloadLen = size > 0 ? stream.ReadBytes(size) : new byte[0]; + if (size > 0 && extPayloadLen.Length != size) + throw new WebSocketException( + "The 'Extended Payload Length' of a frame cannot be read from the data source."); + + frame._extPayloadLength = extPayloadLen; + + /* Masking Key */ + + var masked = mask == Mask.Mask; + var maskingKey = masked ? stream.ReadBytes(4) : new byte[0]; + if (masked && maskingKey.Length != 4) + throw new WebSocketException( + "The 'Masking Key' of a frame cannot be read from the data source."); + + frame._maskingKey = maskingKey; + + /* Payload Data */ + + ulong len = payloadLen < 126 + ? payloadLen + : payloadLen == 126 + ? extPayloadLen.ToUInt16(ByteOrder.Big) + : extPayloadLen.ToUInt64(ByteOrder.Big); + + byte[] data = null; + if (len > 0) + { + // Check if allowable payload data length. + if (payloadLen > 126 && len > PayloadData.MaxLength) + throw new WebSocketException( + CloseStatusCode.TooBig, + "The length of 'Payload Data' of a frame is greater than the allowable length."); + + data = payloadLen > 126 + ? stream.ReadBytes((long)len, 1024) + : stream.ReadBytes((int)len); + + //if (data.LongLength != (long)len) + // throw new WebSocketException( + // "The 'Payload Data' of a frame cannot be read from the data source."); + } + else + { + data = new byte[0]; + } + + var payload = new PayloadData(data, masked); + if (masked && unmask) + { + payload.Mask(maskingKey); + frame._mask = Mask.Unmask; + frame._maskingKey = new byte[0]; + } + + frame._payloadData = payload; + return frame; + } + + #endregion + + #region Internal Methods + + internal static WebSocketFrame CreateCloseFrame(Mask mask, byte[] data) + { + return new WebSocketFrame(Opcode.Close, mask, new PayloadData(data)); + } + + internal static WebSocketFrame CreateCloseFrame(Mask mask, PayloadData payload) + { + return new WebSocketFrame(Opcode.Close, mask, payload); + } + + internal static WebSocketFrame CreateCloseFrame(Mask mask, CloseStatusCode code, string reason) + { + return new WebSocketFrame( + Opcode.Close, mask, new PayloadData(((ushort)code).Append(reason))); + } + + internal static WebSocketFrame CreatePingFrame(Mask mask) + { + return new WebSocketFrame(Opcode.Ping, mask, new PayloadData()); + } + + internal static WebSocketFrame CreatePingFrame(Mask mask, byte[] data) + { + return new WebSocketFrame(Opcode.Ping, mask, new PayloadData(data)); + } + + internal static WebSocketFrame CreatePongFrame(Mask mask, PayloadData payload) + { + return new WebSocketFrame(Opcode.Pong, mask, payload); + } + + internal static WebSocketFrame CreateWebSocketFrame( + Fin fin, Opcode opcode, Mask mask, byte[] data, bool compressed) + { + return new WebSocketFrame(fin, opcode, mask, new PayloadData(data), compressed); + } + + internal static WebSocketFrame Read(Stream stream) + { + return Read(stream, true); + } + + internal static WebSocketFrame Read(Stream stream, bool unmask) + { + var header = stream.ReadBytes(2); + if (header.Length != 2) + throw new WebSocketException( + "The header part of a frame cannot be read from the data source."); + + return read(header, stream, unmask); + } + + internal static async void ReadAsync( + Stream stream, bool unmask, Action completed, Action error) + { + try + { + var header = await stream.ReadBytesAsync(2).ConfigureAwait(false); + if (header.Length != 2) + throw new WebSocketException( + "The header part of a frame cannot be read from the data source."); + + var frame = read(header, stream, unmask); + if (completed != null) + completed(frame); + } + catch (Exception ex) + { + if (error != null) + { + error(ex); + } + } + } + + #endregion + + #region Public Methods + + public IEnumerator GetEnumerator() + { + foreach (var b in ToByteArray()) + yield return b; + } + + public void Print(bool dumped) + { + //Console.WriteLine(dumped ? dump(this) : print(this)); + } + + public byte[] ToByteArray() + { + using (var buff = new MemoryStream()) + { + var header = (int)_fin; + header = (header << 1) + (int)_rsv1; + header = (header << 1) + (int)_rsv2; + header = (header << 1) + (int)_rsv3; + header = (header << 4) + (int)_opcode; + header = (header << 1) + (int)_mask; + header = (header << 7) + (int)_payloadLength; + buff.Write(((ushort)header).ToByteArrayInternally(ByteOrder.Big), 0, 2); + + if (_payloadLength > 125) + buff.Write(_extPayloadLength, 0, _extPayloadLength.Length); + + if (_mask == Mask.Mask) + buff.Write(_maskingKey, 0, _maskingKey.Length); + + if (_payloadLength > 0) + { + var payload = _payloadData.ToByteArray(); + if (_payloadLength < 127) + buff.Write(payload, 0, payload.Length); + else + buff.WriteBytes(payload); + } + + return buff.ToArray(); + } + } + + public override string ToString() + { + return BitConverter.ToString(ToByteArray()); + } + + #endregion + + #region Explicitly Implemented Interface Members + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #endregion + } +} \ No newline at end of file diff --git a/SocketHttpListener.Portable/WebSocketState.cs b/SocketHttpListener.Portable/WebSocketState.cs new file mode 100644 index 000000000..73b3a49dd --- /dev/null +++ b/SocketHttpListener.Portable/WebSocketState.cs @@ -0,0 +1,35 @@ +namespace SocketHttpListener +{ + /// + /// Contains the values of the state of the WebSocket connection. + /// + /// + /// The values of the state are defined in + /// The WebSocket + /// API. + /// + public enum WebSocketState : ushort + { + /// + /// Equivalent to numeric value 0. + /// Indicates that the connection has not yet been established. + /// + Connecting = 0, + /// + /// Equivalent to numeric value 1. + /// Indicates that the connection is established and the communication is possible. + /// + Open = 1, + /// + /// Equivalent to numeric value 2. + /// Indicates that the connection is going through the closing handshake or + /// the WebSocket.Close method has been invoked. + /// + Closing = 2, + /// + /// Equivalent to numeric value 3. + /// Indicates that the connection has been closed or couldn't be opened. + /// + Closed = 3 + } +} diff --git a/SocketHttpListener.Portable/packages.config b/SocketHttpListener.Portable/packages.config new file mode 100644 index 000000000..2aae715b5 --- /dev/null +++ b/SocketHttpListener.Portable/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/SocketHttpListener.Portable/project.json b/SocketHttpListener.Portable/project.json new file mode 100644 index 000000000..fbbe9eaf3 --- /dev/null +++ b/SocketHttpListener.Portable/project.json @@ -0,0 +1,17 @@ +{ + "frameworks":{ + "netstandard1.6":{ + "dependencies":{ + "NETStandard.Library":"1.6.0", + } + }, + ".NETPortable,Version=v4.5,Profile=Profile7":{ + "buildOptions": { + "define": [ ] + }, + "frameworkAssemblies":{ + + } + } + } +} \ No newline at end of file -- cgit v1.2.3