diff --git a/MediaBrowser.Server.Implementations/Archiving/ZipClient.cs b/Emby.Common.Implementations/Archiving/ZipClient.cs similarity index 85% rename from MediaBrowser.Server.Implementations/Archiving/ZipClient.cs rename to Emby.Common.Implementations/Archiving/ZipClient.cs index 29b922436c..791c6678cd 100644 --- a/MediaBrowser.Server.Implementations/Archiving/ZipClient.cs +++ b/Emby.Common.Implementations/Archiving/ZipClient.cs @@ -1,13 +1,13 @@ using System.IO; using MediaBrowser.Model.IO; -using SharpCompress.Archive.Rar; -using SharpCompress.Archive.SevenZip; -using SharpCompress.Archive.Tar; +using SharpCompress.Archives.Rar; +using SharpCompress.Archives.SevenZip; +using SharpCompress.Archives.Tar; using SharpCompress.Common; -using SharpCompress.Reader; -using SharpCompress.Reader.Zip; +using SharpCompress.Readers; +using SharpCompress.Readers.Zip; -namespace MediaBrowser.Server.Implementations.Archiving +namespace Emby.Common.Implementations.Archiving { /// /// Class DotNetZipClient @@ -45,11 +45,12 @@ namespace MediaBrowser.Server.Implementations.Archiving { using (var reader = ReaderFactory.Open(source)) { - var options = ExtractOptions.ExtractFullPath; + var options = new ExtractionOptions(); + options.ExtractFullPath = true; if (overwriteExistingFiles) { - options = options | ExtractOptions.Overwrite; + options.Overwrite = true; } reader.WriteAllToDirectory(targetPath, options); @@ -60,11 +61,12 @@ namespace MediaBrowser.Server.Implementations.Archiving { using (var reader = ZipReader.Open(source)) { - var options = ExtractOptions.ExtractFullPath; + var options = new ExtractionOptions(); + options.ExtractFullPath = true; if (overwriteExistingFiles) { - options = options | ExtractOptions.Overwrite; + options.Overwrite = true; } reader.WriteAllToDirectory(targetPath, options); @@ -97,11 +99,12 @@ namespace MediaBrowser.Server.Implementations.Archiving { using (var reader = archive.ExtractAllEntries()) { - var options = ExtractOptions.ExtractFullPath; + var options = new ExtractionOptions(); + options.ExtractFullPath = true; if (overwriteExistingFiles) { - options = options | ExtractOptions.Overwrite; + options.Overwrite = true; } reader.WriteAllToDirectory(targetPath, options); @@ -136,11 +139,12 @@ namespace MediaBrowser.Server.Implementations.Archiving { using (var reader = archive.ExtractAllEntries()) { - var options = ExtractOptions.ExtractFullPath; + var options = new ExtractionOptions(); + options.ExtractFullPath = true; if (overwriteExistingFiles) { - options = options | ExtractOptions.Overwrite; + options.Overwrite = true; } reader.WriteAllToDirectory(targetPath, options); @@ -174,11 +178,12 @@ namespace MediaBrowser.Server.Implementations.Archiving { using (var reader = archive.ExtractAllEntries()) { - var options = ExtractOptions.ExtractFullPath; + var options = new ExtractionOptions(); + options.ExtractFullPath = true; if (overwriteExistingFiles) { - options = options | ExtractOptions.Overwrite; + options.Overwrite = true; } reader.WriteAllToDirectory(targetPath, options); diff --git a/Emby.Common.Implementations/BaseApplicationHost.cs b/Emby.Common.Implementations/BaseApplicationHost.cs index 0cf11e8255..f0309511e0 100644 --- a/Emby.Common.Implementations/BaseApplicationHost.cs +++ b/Emby.Common.Implementations/BaseApplicationHost.cs @@ -152,8 +152,6 @@ namespace Emby.Common.Implementations protected IIsoManager IsoManager { get; private set; } - protected ISystemEvents SystemEvents { get; private set; } - protected IProcessFactory ProcessFactory { get; private set; } protected ITimerFactory TimerFactory { get; private set; } protected ISocketFactory SocketFactory { get; private set; } @@ -172,7 +170,7 @@ namespace Emby.Common.Implementations protected ICryptoProvider CryptographyProvider = new CryptographyProvider(); - protected IEnvironmentInfo EnvironmentInfo = new Emby.Common.Implementations.EnvironmentInfo.EnvironmentInfo(); + protected IEnvironmentInfo EnvironmentInfo { get; private set; } private DeviceId _deviceId; public string SystemId @@ -193,20 +191,30 @@ namespace Emby.Common.Implementations get { return EnvironmentInfo.OperatingSystemName; } } - public IMemoryStreamFactory MemoryStreamProvider { get; set; } - /// /// The container /// protected readonly SimpleInjector.Container Container = new SimpleInjector.Container(); + protected ISystemEvents SystemEvents { get; private set; } + protected IMemoryStreamFactory MemoryStreamFactory { get; private set; } + /// /// Initializes a new instance of the class. /// protected BaseApplicationHost(TApplicationPathsType applicationPaths, ILogManager logManager, - IFileSystem fileSystem) + IFileSystem fileSystem, + IEnvironmentInfo environmentInfo, + ISystemEvents systemEvents, + IMemoryStreamFactory memoryStreamFactory, + INetworkManager networkManager) { + NetworkManager = networkManager; + EnvironmentInfo = environmentInfo; + SystemEvents = systemEvents; + MemoryStreamFactory = memoryStreamFactory; + // hack alert, until common can target .net core BaseExtensions.CryptographyProvider = CryptographyProvider; @@ -233,9 +241,6 @@ namespace Emby.Common.Implementations JsonSerializer = CreateJsonSerializer(); - MemoryStreamProvider = CreateMemoryStreamProvider(); - SystemEvents = CreateSystemEvents(); - OnLoggerLoaded(true); LogManager.LoggerLoaded += (s, e) => OnLoggerLoaded(false); @@ -267,9 +272,6 @@ namespace Emby.Common.Implementations progress.Report(100); } - protected abstract IMemoryStreamFactory CreateMemoryStreamProvider(); - protected abstract ISystemEvents CreateSystemEvents(); - protected virtual void OnLoggerLoaded(bool isFirstLoad) { Logger.Info("Application version: {0}", ApplicationVersion); @@ -521,7 +523,7 @@ return null; RegisterSingleInstance(JsonSerializer); RegisterSingleInstance(XmlSerializer); - RegisterSingleInstance(MemoryStreamProvider); + RegisterSingleInstance(MemoryStreamFactory); RegisterSingleInstance(SystemEvents); RegisterSingleInstance(LogManager); @@ -532,10 +534,9 @@ return null; RegisterSingleInstance(FileSystemManager); - HttpClient = new HttpClientManager.HttpClientManager(ApplicationPaths, LogManager.GetLogger("HttpClient"), FileSystemManager, MemoryStreamProvider); + HttpClient = new HttpClientManager.HttpClientManager(ApplicationPaths, LogManager.GetLogger("HttpClient"), FileSystemManager, MemoryStreamFactory); RegisterSingleInstance(HttpClient); - NetworkManager = CreateNetworkManager(LogManager.GetLogger("NetworkManager")); RegisterSingleInstance(NetworkManager); IsoManager = new IsoManager(); @@ -588,8 +589,6 @@ return null; } } - protected abstract INetworkManager CreateNetworkManager(ILogger logger); - /// /// Creates an instance of type and resolves all constructor dependancies /// diff --git a/Emby.Common.Implementations/EnvironmentInfo/EnvironmentInfo.cs b/Emby.Common.Implementations/EnvironmentInfo/EnvironmentInfo.cs index 8cea617eae..6a1b3ef74c 100644 --- a/Emby.Common.Implementations/EnvironmentInfo/EnvironmentInfo.cs +++ b/Emby.Common.Implementations/EnvironmentInfo/EnvironmentInfo.cs @@ -9,6 +9,8 @@ namespace Emby.Common.Implementations.EnvironmentInfo { public class EnvironmentInfo : IEnvironmentInfo { + public MediaBrowser.Model.System.Architecture? CustomArchitecture { get; set; } + public MediaBrowser.Model.System.OperatingSystem OperatingSystem { get @@ -66,5 +68,32 @@ namespace Emby.Common.Implementations.EnvironmentInfo return "1.0"; } } + + public MediaBrowser.Model.System.Architecture SystemArchitecture + { + get + { + if (CustomArchitecture.HasValue) + { + return CustomArchitecture.Value; + } +#if NET46 + return Environment.Is64BitOperatingSystem ? MediaBrowser.Model.System.Architecture.X64 : MediaBrowser.Model.System.Architecture.X86; +#elif NETSTANDARD1_6 + switch(System.Runtime.InteropServices.RuntimeInformation.OSArchitecture) + { + case System.Runtime.InteropServices.Architecture.Arm: + return MediaBrowser.Model.System.Architecture.Arm; + case System.Runtime.InteropServices.Architecture.Arm64: + return MediaBrowser.Model.System.Architecture.Arm64; + case System.Runtime.InteropServices.Architecture.X64: + return MediaBrowser.Model.System.Architecture.X64; + case System.Runtime.InteropServices.Architecture.X86: + return MediaBrowser.Model.System.Architecture.X86; + } +#endif + return MediaBrowser.Model.System.Architecture.X64; + } + } } } diff --git a/Emby.Common.Implementations/IO/ManagedFileSystem.cs b/Emby.Common.Implementations/IO/ManagedFileSystem.cs index 37b4575987..81ca8dcff2 100644 --- a/Emby.Common.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Common.Implementations/IO/ManagedFileSystem.cs @@ -761,5 +761,10 @@ namespace Emby.Common.Implementations.IO var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; return Directory.EnumerateFileSystemEntries(path, "*", searchOption); } + + public virtual void SetExecutable(string path) + { + + } } } diff --git a/Emby.Common.Implementations/Net/NetSocket.cs b/Emby.Common.Implementations/Net/NetSocket.cs index 72faa41a9a..faa1a81e2d 100644 --- a/Emby.Common.Implementations/Net/NetSocket.cs +++ b/Emby.Common.Implementations/Net/NetSocket.cs @@ -23,7 +23,7 @@ namespace Emby.Common.Implementations.Net { get { - return BaseNetworkManager.ToIpEndPointInfo((IPEndPoint)Socket.LocalEndPoint); + return NetworkManager.ToIpEndPointInfo((IPEndPoint)Socket.LocalEndPoint); } } @@ -31,7 +31,7 @@ namespace Emby.Common.Implementations.Net { get { - return BaseNetworkManager.ToIpEndPointInfo((IPEndPoint)Socket.RemoteEndPoint); + return NetworkManager.ToIpEndPointInfo((IPEndPoint)Socket.RemoteEndPoint); } } @@ -64,7 +64,7 @@ namespace Emby.Common.Implementations.Net public void Bind(IpEndPointInfo endpoint) { - var nativeEndpoint = BaseNetworkManager.ToIPEndPoint(endpoint); + var nativeEndpoint = NetworkManager.ToIPEndPoint(endpoint); Socket.Bind(nativeEndpoint); } diff --git a/Emby.Common.Implementations/Net/UdpSocket.cs b/Emby.Common.Implementations/Net/UdpSocket.cs index 244b37bb41..eca82034b0 100644 --- a/Emby.Common.Implementations/Net/UdpSocket.cs +++ b/Emby.Common.Implementations/Net/UdpSocket.cs @@ -175,7 +175,7 @@ namespace Emby.Common.Implementations.Net return null; } - return BaseNetworkManager.ToIpEndPointInfo(endpoint); + return NetworkManager.ToIpEndPointInfo(endpoint); } private void ProcessResponse(IAsyncResult asyncResult) diff --git a/Emby.Common.Implementations/Networking/BaseNetworkManager.cs b/Emby.Common.Implementations/Networking/NetworkManager.cs similarity index 95% rename from Emby.Common.Implementations/Networking/BaseNetworkManager.cs rename to Emby.Common.Implementations/Networking/NetworkManager.cs index f1ac8413be..e336973377 100644 --- a/Emby.Common.Implementations/Networking/BaseNetworkManager.cs +++ b/Emby.Common.Implementations/Networking/NetworkManager.cs @@ -9,15 +9,17 @@ using System.Net.Sockets; using System.Threading.Tasks; using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Net; +using MediaBrowser.Model.IO; +using MediaBrowser.Common.Net; namespace Emby.Common.Implementations.Networking { - public abstract class BaseNetworkManager + public class NetworkManager : INetworkManager { protected ILogger Logger { get; private set; } private DateTime _lastRefresh; - protected BaseNetworkManager(ILogger logger) + public NetworkManager(ILogger logger) { Logger = logger; } @@ -481,5 +483,24 @@ namespace Emby.Common.Implementations.Networking var addresses = await Dns.GetHostAddressesAsync(host).ConfigureAwait(false); return addresses.Select(ToIpAddressInfo).ToArray(); } + + /// + /// Gets the network shares. + /// + /// The path. + /// IEnumerable{NetworkShare}. + public IEnumerable GetNetworkShares(string path) + { + return new List(); + } + + /// + /// Gets available devices within the domain + /// + /// PC's in the Domain + public IEnumerable GetNetworkDevices() + { + return new List(); + } } } diff --git a/MediaBrowser.Server.Implementations/Serialization/JsonSerializer.cs b/Emby.Common.Implementations/Serialization/JsonSerializer.cs similarity index 99% rename from MediaBrowser.Server.Implementations/Serialization/JsonSerializer.cs rename to Emby.Common.Implementations/Serialization/JsonSerializer.cs index b9a03242c0..c9db336890 100644 --- a/MediaBrowser.Server.Implementations/Serialization/JsonSerializer.cs +++ b/Emby.Common.Implementations/Serialization/JsonSerializer.cs @@ -4,7 +4,7 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Serialization; -namespace MediaBrowser.Server.Implementations.Serialization +namespace Emby.Common.Implementations.Serialization { /// /// Provides a wrapper around third party json serialization. diff --git a/Emby.Common.Implementations/project.json b/Emby.Common.Implementations/project.json index 2b2357e383..b0a35bdf38 100644 --- a/Emby.Common.Implementations/project.json +++ b/Emby.Common.Implementations/project.json @@ -2,7 +2,7 @@ "version": "1.0.0-*", "dependencies": { - + }, "frameworks": { @@ -19,46 +19,51 @@ "System.Text.Encoding": "4.0.0.0", "System.Threading": "4.0.0.0", "System.Threading.Tasks": "4.0.0.0", - "System.Xml.ReaderWriter": "4.0.0" + "System.Xml.ReaderWriter": "4.0.0" }, "dependencies": { "SimpleInjector": "3.2.4", + "ServiceStack.Text": "4.5.4", "NLog": "4.4.0-betaV15", + "sharpcompress": "0.14.0", "MediaBrowser.Model": { "target": "project" }, "MediaBrowser.Common": { "target": "project" - } - } + } + } }, "netstandard1.6": { "imports": "dnxcore50", "dependencies": { "NETStandard.Library": "1.6.0", - "System.IO.FileSystem.DriveInfo": "4.0.0", - "System.Diagnostics.Process": "4.1.0", - "System.Threading.Timer": "4.0.1", - "System.Net.Requests": "4.0.11", - "System.Xml.ReaderWriter": "4.0.11", - "System.Xml.XmlSerializer": "4.0.11", - "System.Net.Http": "4.1.0", - "System.Net.Primitives": "4.0.11", - "System.Net.Sockets": "4.1.0", - "System.Net.NetworkInformation": "4.1.0", - "System.Net.NameResolution": "4.0.0", - "System.Runtime.InteropServices.RuntimeInformation": "4.0.0", - "System.Reflection": "4.1.0", - "System.Reflection.Primitives": "4.0.1", - "System.Runtime.Loader": "4.0.0", - "SimpleInjector": "3.2.4", + "System.IO.FileSystem.DriveInfo": "4.0.0", + "System.Diagnostics.Process": "4.1.0", + "System.Threading.Timer": "4.0.1", + "System.Net.Requests": "4.0.11", + "System.Xml.ReaderWriter": "4.0.11", + "System.Xml.XmlSerializer": "4.0.11", + "System.Net.Http": "4.1.0", + "System.Net.Primitives": "4.0.11", + "System.Net.Sockets": "4.1.0", + "System.Net.NetworkInformation": "4.1.0", + "System.Net.NameResolution": "4.0.0", + "System.Runtime.InteropServices.RuntimeInformation": "4.0.0", + "System.Reflection": "4.1.0", + "System.Reflection.Primitives": "4.0.1", + "System.Runtime.Loader": "4.0.0", + "SimpleInjector": "3.2.4", + "ServiceStack.Text.Core": "1.0.27", "NLog": "4.4.0-betaV15", + "sharpcompress": "0.14.0", "MediaBrowser.Model": { "target": "project" }, "MediaBrowser.Common": { "target": "project" - } } + } + } } } } diff --git a/Emby.Server.Implementations/project.json b/Emby.Dlna/project.json similarity index 98% rename from Emby.Server.Implementations/project.json rename to Emby.Dlna/project.json index 13bdedb418..fbbe9eaf32 100644 --- a/Emby.Server.Implementations/project.json +++ b/Emby.Dlna/project.json @@ -1,4 +1,4 @@ -{ +{ "frameworks":{ "netstandard1.6":{ "dependencies":{ diff --git a/Emby.Drawing.ImageMagick/Emby.Drawing.ImageMagick.csproj b/Emby.Drawing.ImageMagick/Emby.Drawing.ImageMagick.csproj new file mode 100644 index 0000000000..98e99c92b1 --- /dev/null +++ b/Emby.Drawing.ImageMagick/Emby.Drawing.ImageMagick.csproj @@ -0,0 +1,83 @@ + + + + + Debug + AnyCPU + {6CFEE013-6E7C-432B-AC37-CABF0880C69A} + Library + Properties + Emby.Drawing.ImageMagick + Emby.Drawing.ImageMagick + v4.5.2 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\ImageMagickSharp.1.0.0.18\lib\net45\ImageMagickSharp.dll + True + + + + + + + + + + + + + + + + + + + + + + + + + + + + {9142eefa-7570-41e1-bfcc-468bb571af2f} + MediaBrowser.Common + + + {17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2} + MediaBrowser.Controller + + + {7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b} + MediaBrowser.Model + + + + + \ No newline at end of file diff --git a/Emby.Drawing.ImageMagick/ImageHelpers.cs b/Emby.Drawing.ImageMagick/ImageHelpers.cs new file mode 100644 index 0000000000..c623c21aa2 --- /dev/null +++ b/Emby.Drawing.ImageMagick/ImageHelpers.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Emby.Drawing.ImageMagick +{ + internal static class ImageHelpers + { + internal static List ProjectPaths(List paths, int count) + { + if (count <= 0) + { + throw new ArgumentOutOfRangeException("count"); + } + if (paths.Count == 0) + { + throw new ArgumentOutOfRangeException("paths"); + } + + var list = new List(); + + AddToList(list, paths, count); + + return list.Take(count).ToList(); + } + + private static void AddToList(List list, List paths, int count) + { + while (list.Count < count) + { + foreach (var path in paths) + { + list.Add(path); + + if (list.Count >= count) + { + return; + } + } + } + } + } +} diff --git a/Emby.Drawing/ImageMagick/ImageMagickEncoder.cs b/Emby.Drawing.ImageMagick/ImageMagickEncoder.cs similarity index 95% rename from Emby.Drawing/ImageMagick/ImageMagickEncoder.cs rename to Emby.Drawing.ImageMagick/ImageMagickEncoder.cs index 3410ef003f..242898e332 100644 --- a/Emby.Drawing/ImageMagick/ImageMagickEncoder.cs +++ b/Emby.Drawing.ImageMagick/ImageMagickEncoder.cs @@ -8,9 +8,6 @@ using MediaBrowser.Model.Logging; using System; using System.IO; using System.Linq; -using MediaBrowser.Common.IO; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.IO; using MediaBrowser.Model.IO; namespace Emby.Drawing.ImageMagick @@ -19,17 +16,15 @@ namespace Emby.Drawing.ImageMagick { private readonly ILogger _logger; private readonly IApplicationPaths _appPaths; - private readonly IHttpClient _httpClient; + private readonly Func _httpClientFactory; private readonly IFileSystem _fileSystem; - private readonly IServerConfigurationManager _config; - public ImageMagickEncoder(ILogger logger, IApplicationPaths appPaths, IHttpClient httpClient, IFileSystem fileSystem, IServerConfigurationManager config) + public ImageMagickEncoder(ILogger logger, IApplicationPaths appPaths, Func httpClientFactory, IFileSystem fileSystem) { _logger = logger; _appPaths = appPaths; - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _fileSystem = fileSystem; - _config = config; LogVersion(); } @@ -260,7 +255,7 @@ namespace Emby.Drawing.ImageMagick { var currentImageSize = new ImageSize(imageWidth, imageHeight); - var task = new PlayedIndicatorDrawer(_appPaths, _httpClient, _fileSystem).DrawPlayedIndicator(wand, currentImageSize); + var task = new PlayedIndicatorDrawer(_appPaths, _httpClientFactory(), _fileSystem).DrawPlayedIndicator(wand, currentImageSize); Task.WaitAll(task); } else if (options.UnplayedCount.HasValue) diff --git a/Emby.Drawing/ImageMagick/PercentPlayedDrawer.cs b/Emby.Drawing.ImageMagick/PercentPlayedDrawer.cs similarity index 100% rename from Emby.Drawing/ImageMagick/PercentPlayedDrawer.cs rename to Emby.Drawing.ImageMagick/PercentPlayedDrawer.cs diff --git a/Emby.Drawing/ImageMagick/PlayedIndicatorDrawer.cs b/Emby.Drawing.ImageMagick/PlayedIndicatorDrawer.cs similarity index 100% rename from Emby.Drawing/ImageMagick/PlayedIndicatorDrawer.cs rename to Emby.Drawing.ImageMagick/PlayedIndicatorDrawer.cs diff --git a/Emby.Drawing.ImageMagick/Properties/AssemblyInfo.cs b/Emby.Drawing.ImageMagick/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..1089607d67 --- /dev/null +++ b/Emby.Drawing.ImageMagick/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +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("Emby.Drawing.ImageMagick")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Emby.Drawing.ImageMagick")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("6cfee013-6e7c-432b-ac37-cabf0880c69a")] + +// 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/Emby.Drawing/ImageMagick/StripCollageBuilder.cs b/Emby.Drawing.ImageMagick/StripCollageBuilder.cs similarity index 100% rename from Emby.Drawing/ImageMagick/StripCollageBuilder.cs rename to Emby.Drawing.ImageMagick/StripCollageBuilder.cs diff --git a/Emby.Drawing/ImageMagick/UnplayedCountIndicator.cs b/Emby.Drawing.ImageMagick/UnplayedCountIndicator.cs similarity index 100% rename from Emby.Drawing/ImageMagick/UnplayedCountIndicator.cs rename to Emby.Drawing.ImageMagick/UnplayedCountIndicator.cs diff --git a/Emby.Drawing/packages.config b/Emby.Drawing.ImageMagick/packages.config similarity index 81% rename from Emby.Drawing/packages.config rename to Emby.Drawing.ImageMagick/packages.config index be2fb41879..619310d28e 100644 --- a/Emby.Drawing/packages.config +++ b/Emby.Drawing.ImageMagick/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file diff --git a/Emby.Drawing/GDI/DynamicImageHelpers.cs b/Emby.Drawing.Net/DynamicImageHelpers.cs similarity index 99% rename from Emby.Drawing/GDI/DynamicImageHelpers.cs rename to Emby.Drawing.Net/DynamicImageHelpers.cs index 9cbd6cbe3a..1910f7840d 100644 --- a/Emby.Drawing/GDI/DynamicImageHelpers.cs +++ b/Emby.Drawing.Net/DynamicImageHelpers.cs @@ -7,7 +7,7 @@ using MediaBrowser.Common.IO; using MediaBrowser.Controller.IO; using MediaBrowser.Model.IO; -namespace Emby.Drawing.GDI +namespace Emby.Drawing.Net { public static class DynamicImageHelpers { diff --git a/Emby.Drawing.Net/Emby.Drawing.Net.csproj b/Emby.Drawing.Net/Emby.Drawing.Net.csproj new file mode 100644 index 0000000000..16e72a085b --- /dev/null +++ b/Emby.Drawing.Net/Emby.Drawing.Net.csproj @@ -0,0 +1,78 @@ + + + + + Debug + AnyCPU + {C97A239E-A96C-4D64-A844-CCF8CC30AECB} + Library + Properties + Emby.Drawing.Net + Emby.Drawing.Net + v4.5.2 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + {9142eefa-7570-41e1-bfcc-468bb571af2f} + MediaBrowser.Common + + + {17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2} + MediaBrowser.Controller + + + {7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b} + MediaBrowser.Model + + + + + + + + \ No newline at end of file diff --git a/Emby.Drawing/GDI/GDIImageEncoder.cs b/Emby.Drawing.Net/GDIImageEncoder.cs similarity index 99% rename from Emby.Drawing/GDI/GDIImageEncoder.cs rename to Emby.Drawing.Net/GDIImageEncoder.cs index 2fd2cac7e8..831a579792 100644 --- a/Emby.Drawing/GDI/GDIImageEncoder.cs +++ b/Emby.Drawing.Net/GDIImageEncoder.cs @@ -12,7 +12,7 @@ using MediaBrowser.Controller.IO; using MediaBrowser.Model.IO; using ImageFormat = MediaBrowser.Model.Drawing.ImageFormat; -namespace Emby.Drawing.GDI +namespace Emby.Drawing.Net { public class GDIImageEncoder : IImageEncoder { diff --git a/Emby.Drawing/GDI/ImageExtensions.cs b/Emby.Drawing.Net/ImageExtensions.cs similarity index 99% rename from Emby.Drawing/GDI/ImageExtensions.cs rename to Emby.Drawing.Net/ImageExtensions.cs index 6af5a8688f..dec2613d0f 100644 --- a/Emby.Drawing/GDI/ImageExtensions.cs +++ b/Emby.Drawing.Net/ImageExtensions.cs @@ -4,7 +4,7 @@ using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.IO; -namespace Emby.Drawing.GDI +namespace Emby.Drawing.Net { public static class ImageExtensions { diff --git a/Emby.Drawing/ImageHelpers.cs b/Emby.Drawing.Net/ImageHelpers.cs similarity index 97% rename from Emby.Drawing/ImageHelpers.cs rename to Emby.Drawing.Net/ImageHelpers.cs index 90bde8b3bd..1afc47cd03 100644 --- a/Emby.Drawing/ImageHelpers.cs +++ b/Emby.Drawing.Net/ImageHelpers.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; -namespace Emby.Drawing +namespace Emby.Drawing.Net { internal static class ImageHelpers { diff --git a/Emby.Drawing/GDI/PercentPlayedDrawer.cs b/Emby.Drawing.Net/PercentPlayedDrawer.cs similarity index 97% rename from Emby.Drawing/GDI/PercentPlayedDrawer.cs rename to Emby.Drawing.Net/PercentPlayedDrawer.cs index c7afda60e4..fac15ba47a 100644 --- a/Emby.Drawing/GDI/PercentPlayedDrawer.cs +++ b/Emby.Drawing.Net/PercentPlayedDrawer.cs @@ -1,7 +1,7 @@ using System; using System.Drawing; -namespace Emby.Drawing.GDI +namespace Emby.Drawing.Net { public class PercentPlayedDrawer { diff --git a/Emby.Drawing/GDI/PlayedIndicatorDrawer.cs b/Emby.Drawing.Net/PlayedIndicatorDrawer.cs similarity index 97% rename from Emby.Drawing/GDI/PlayedIndicatorDrawer.cs rename to Emby.Drawing.Net/PlayedIndicatorDrawer.cs index 4428e4cbac..53683e6f45 100644 --- a/Emby.Drawing/GDI/PlayedIndicatorDrawer.cs +++ b/Emby.Drawing.Net/PlayedIndicatorDrawer.cs @@ -1,6 +1,6 @@ using System.Drawing; -namespace Emby.Drawing.GDI +namespace Emby.Drawing.Net { public class PlayedIndicatorDrawer { diff --git a/Emby.Drawing.Net/Properties/AssemblyInfo.cs b/Emby.Drawing.Net/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..321c3a297c --- /dev/null +++ b/Emby.Drawing.Net/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +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("Emby.Drawing.Net")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Emby.Drawing.Net")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("c97a239e-a96c-4d64-a844-ccf8cc30aecb")] + +// 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/Emby.Drawing/GDI/UnplayedCountIndicator.cs b/Emby.Drawing.Net/UnplayedCountIndicator.cs similarity index 98% rename from Emby.Drawing/GDI/UnplayedCountIndicator.cs rename to Emby.Drawing.Net/UnplayedCountIndicator.cs index 6420abb27f..a38abeb324 100644 --- a/Emby.Drawing/GDI/UnplayedCountIndicator.cs +++ b/Emby.Drawing.Net/UnplayedCountIndicator.cs @@ -1,6 +1,6 @@ using System.Drawing; -namespace Emby.Drawing.GDI +namespace Emby.Drawing.Net { public class UnplayedCountIndicator { diff --git a/Emby.Drawing/GDI/empty.png b/Emby.Drawing.Net/empty.png similarity index 100% rename from Emby.Drawing/GDI/empty.png rename to Emby.Drawing.Net/empty.png diff --git a/Emby.Drawing/Common/ImageHeader.cs b/Emby.Drawing/Common/ImageHeader.cs index 45a8f0d474..c385779a1e 100644 --- a/Emby.Drawing/Common/ImageHeader.cs +++ b/Emby.Drawing/Common/ImageHeader.cs @@ -48,7 +48,7 @@ namespace Emby.Drawing.Common /// The image was of an unrecognised format. public static ImageSize GetDimensions(string path, ILogger logger, IFileSystem fileSystem) { - using (var fs = File.OpenRead(path)) + using (var fs = fileSystem.OpenRead(path)) { using (var binaryReader = new BinaryReader(fs)) { diff --git a/Emby.Drawing/Emby.Drawing.csproj b/Emby.Drawing/Emby.Drawing.csproj index a883d06495..90418f6317 100644 --- a/Emby.Drawing/Emby.Drawing.csproj +++ b/Emby.Drawing/Emby.Drawing.csproj @@ -9,10 +9,11 @@ Properties Emby.Drawing Emby.Drawing - v4.6 + {786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Profile7 + v4.5 512 ..\ - true @@ -32,18 +33,6 @@ 4 - - False - ..\packages\ImageMagickSharp.1.0.0.18\lib\net45\ImageMagickSharp.dll - - - - - - - - - ..\ThirdParty\taglib\TagLib.Portable.dll @@ -53,25 +42,9 @@ Properties\SharedVersion.cs - - - - - - - - - - - - - - - - @@ -87,13 +60,8 @@ MediaBrowser.Model - - - - - - - + + + + + {9142eefa-7570-41e1-bfcc-468bb571af2f} + MediaBrowser.Common + + + {7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b} + MediaBrowser.Model + + + \ No newline at end of file diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.nuget.targets b/ServiceStack/ServiceStack.nuget.targets similarity index 100% rename from Emby.Server.Implementations/Emby.Server.Implementations.nuget.targets rename to ServiceStack/ServiceStack.nuget.targets diff --git a/ServiceStack/ServiceStackHost.Runtime.cs b/ServiceStack/ServiceStackHost.Runtime.cs new file mode 100644 index 0000000000..1a1656a0ec --- /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 0000000000..8a1db25e48 --- /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 0000000000..762e8aafff --- /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 0000000000..7b5a50ef17 --- /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 0000000000..6b8deb9c96 --- /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 0000000000..fbbe9eaf32 --- /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 0000000000..f5db52fd72 --- /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 0000000000..b1bb4b1960 --- /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 0000000000..62a268bce1 --- /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 0000000000..36a48d94cb --- /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 0000000000..bf1d6fc95f --- /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 0000000000..303263d0b9 --- /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 0000000000..f91401b995 --- /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 0000000000..5172ba4975 --- /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 0000000000..5aca28c7c3 --- /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 0000000000..adc2f098e9 --- /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 0000000000..9dbadb9ab2 --- /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 0000000000..c6e7e538ec --- /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 0000000000..3f3b4a667a --- /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 0000000000..6dfd8d8a1d --- /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 0000000000..470507d6b7 --- /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 0000000000..b50660ad0a --- /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 0000000000..797684b3e2 --- /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 0000000000..d31da41329 --- /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 0000000000..83660100ae --- /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 0000000000..faa26693d7 --- /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 0000000000..84c6a8c193 --- /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 0000000000..0b05539eea --- /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 0000000000..63d5e510d2 --- /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 0000000000..0bc827b5a4 --- /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 0000000000..93da82ba08 --- /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 0000000000..518c45acba --- /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 0000000000..c0839b46d5 --- /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 0000000000..2c314da506 --- /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 0000000000..58030500d1 --- /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 0000000000..6ecbf97427 --- /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 0000000000..d20f99b9b8 --- /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 0000000000..426e15ecde --- /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 0000000000..3ffa6e639a --- /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 0000000000..62b7d8585c --- /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 0000000000..a6318da2b8 --- /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 0000000000..7b383fd230 --- /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 0000000000..1289da13d7 --- /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 0000000000..f189b95b47 --- /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 0000000000..b10145687b --- /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 0000000000..8704264603 --- /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 0000000000..668059b8a3 --- /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 0000000000..f7b3a643cc --- /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 0000000000..e69ce0e64f --- /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 0000000000..889880387e --- /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 0000000000..260721317c --- /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 0000000000..44fa4a5dc3 --- /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 0000000000..73b3a49ddc --- /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 0000000000..2aae715b5a --- /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 0000000000..fbbe9eaf32 --- /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 diff --git a/src/Emby.Server/Emby.Server.xproj b/src/Emby.Server/Emby.Server.xproj index 81fffc6391..6a23809ced 100644 --- a/src/Emby.Server/Emby.Server.xproj +++ b/src/Emby.Server/Emby.Server.xproj @@ -18,14 +18,24 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/Emby.Server/project.json b/src/Emby.Server/project.json index 66ebeec3c9..2693435a45 100644 --- a/src/Emby.Server/project.json +++ b/src/Emby.Server/project.json @@ -6,13 +6,12 @@ "dependencies": { "Emby.Common.Implementations": "1.0.0-*", - "Emby.Dlna": "1.0.0-*", + "Emby.Server.Core": "1.0.0-*", "Microsoft.NETCore.App": { "type": "platform", "version": "1.0.1" }, - "Mono.Nat": "1.0.0-*", - "RSSDP": "1.0.0-*" + "Mono.Nat": "1.0.0-*" }, "frameworks": { @@ -25,6 +24,21 @@ "DvdLib": { "target": "project" }, + "Emby.Dlna": { + "target": "project" + }, + "Emby.Drawing": { + "target": "project" + }, + "Emby.Photos": { + "target": "project" + }, + "Emby.Server.Implementations": { + "target": "project" + }, + "MediaBrowser.Api": { + "target": "project" + }, "MediaBrowser.Common": { "target": "project" }, @@ -34,12 +48,18 @@ "MediaBrowser.LocalMetadata": { "target": "project" }, + "MediaBrowser.MediaEncoding": { + "target": "project" + }, "MediaBrowser.Model": { "target": "project" }, "MediaBrowser.Providers": { "target": "project" }, + "MediaBrowser.Server.Implementations": { + "target": "project" + }, "MediaBrowser.WebDashboard": { "target": "project" }, @@ -48,6 +68,15 @@ }, "OpenSubtitlesHandler": { "target": "project" + }, + "RSSDP": { + "target": "project" + }, + "ServiceStack": { + "target": "project" + }, + "SocketHttpListener.Portable": { + "target": "project" } } }