浏览代码

update series resolver

Luke Pulverenti 8 年之前
父节点
当前提交
ccb5b14d77

+ 0 - 2
Emby.Server.Core/ApplicationHost.cs

@@ -511,8 +511,6 @@ namespace Emby.Server.Core
         {
         {
             var migrations = new List<IVersionMigration>
             var migrations = new List<IVersionMigration>
             {
             {
-                new LibraryScanMigration(ServerConfigurationManager, TaskManager),
-                new GuideMigration(ServerConfigurationManager, TaskManager)
             };
             };
 
 
             foreach (var task in migrations)
             foreach (var task in migrations)

+ 0 - 2
Emby.Server.Implementations/Emby.Server.Implementations.csproj

@@ -182,8 +182,6 @@
     <Compile Include="Logging\UnhandledExceptionWriter.cs" />
     <Compile Include="Logging\UnhandledExceptionWriter.cs" />
     <Compile Include="MediaEncoder\EncodingManager.cs" />
     <Compile Include="MediaEncoder\EncodingManager.cs" />
     <Compile Include="Migrations\IVersionMigration.cs" />
     <Compile Include="Migrations\IVersionMigration.cs" />
-    <Compile Include="Migrations\LibraryScanMigration.cs" />
-    <Compile Include="Migrations\GuideMigration.cs" />
     <Compile Include="News\NewsEntryPoint.cs" />
     <Compile Include="News\NewsEntryPoint.cs" />
     <Compile Include="News\NewsService.cs" />
     <Compile Include="News\NewsService.cs" />
     <Compile Include="Notifications\CoreNotificationTypes.cs" />
     <Compile Include="Notifications\CoreNotificationTypes.cs" />

+ 1 - 1
Emby.Server.Implementations/HttpServer/ResponseFilter.cs

@@ -53,7 +53,7 @@ namespace Emby.Server.Implementations.HttpServer
             {
             {
                 if (!hasHeaders.Headers.ContainsKey("Server"))
                 if (!hasHeaders.Headers.ContainsKey("Server"))
                 {
                 {
-                    hasHeaders.Headers["Server"] = "Mono-HTTPAPI/1.1, UPnP/1.0 DLNADOC/1.50";
+                    hasHeaders.Headers["Server"] = "Microsoft-NetCore/2.0, UPnP/1.0 DLNADOC/1.50";
                     //hasHeaders.Headers["Server"] = "Mono-HTTPAPI/1.1";
                     //hasHeaders.Headers["Server"] = "Mono-HTTPAPI/1.1";
                 }
                 }
 
 

+ 45 - 16
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -2505,9 +2505,32 @@ namespace Emby.Server.Implementations.Library
 
 
         public NamingOptions GetNamingOptions()
         public NamingOptions GetNamingOptions()
         {
         {
+            return GetNamingOptions(true);
+        }
+
+        public NamingOptions GetNamingOptions(bool allowOptimisticEpisodeDetection)
+        {
+            if (!allowOptimisticEpisodeDetection)
+            {
+                if (_namingOptionsWithoutOptimisticEpisodeDetection == null)
+                {
+                    var namingOptions = new ExtendedNamingOptions();
+
+                    InitNamingOptions(namingOptions);
+                    namingOptions.EpisodeExpressions = namingOptions.EpisodeExpressions
+                        .Where(i => i.IsNamed && !i.IsOptimistic)
+                        .ToList();
+
+                    _namingOptionsWithoutOptimisticEpisodeDetection = namingOptions;
+                }
+
+                return _namingOptionsWithoutOptimisticEpisodeDetection;
+            }
+
             return GetNamingOptions(new LibraryOptions());
             return GetNamingOptions(new LibraryOptions());
         }
         }
 
 
+        private NamingOptions _namingOptionsWithoutOptimisticEpisodeDetection;
         private NamingOptions _namingOptions;
         private NamingOptions _namingOptions;
         private string[] _videoFileExtensions;
         private string[] _videoFileExtensions;
         public NamingOptions GetNamingOptions(LibraryOptions libraryOptions)
         public NamingOptions GetNamingOptions(LibraryOptions libraryOptions)
@@ -2516,23 +2539,8 @@ namespace Emby.Server.Implementations.Library
             {
             {
                 var options = new ExtendedNamingOptions();
                 var options = new ExtendedNamingOptions();
 
 
-                // These cause apps to have problems
-                options.AudioFileExtensions.Remove(".m3u");
-                options.AudioFileExtensions.Remove(".wpl");
-
-                //if (!libraryOptions.EnableArchiveMediaFiles)
-                {
-                    options.AudioFileExtensions.Remove(".rar");
-                    options.AudioFileExtensions.Remove(".zip");
-                }
+                InitNamingOptions(options);
 
 
-                //if (!libraryOptions.EnableArchiveMediaFiles)
-                {
-                    options.VideoFileExtensions.Remove(".rar");
-                    options.VideoFileExtensions.Remove(".zip");
-                }
-
-                options.VideoFileExtensions.Add(".tp");
                 _namingOptions = options;
                 _namingOptions = options;
                 _videoFileExtensions = _namingOptions.VideoFileExtensions.ToArray();
                 _videoFileExtensions = _namingOptions.VideoFileExtensions.ToArray();
             }
             }
@@ -2540,6 +2548,27 @@ namespace Emby.Server.Implementations.Library
             return _namingOptions;
             return _namingOptions;
         }
         }
 
 
+        private void InitNamingOptions(NamingOptions options)
+        {
+            // These cause apps to have problems
+            options.AudioFileExtensions.Remove(".m3u");
+            options.AudioFileExtensions.Remove(".wpl");
+
+            //if (!libraryOptions.EnableArchiveMediaFiles)
+            {
+                options.AudioFileExtensions.Remove(".rar");
+                options.AudioFileExtensions.Remove(".zip");
+            }
+
+            //if (!libraryOptions.EnableArchiveMediaFiles)
+            {
+                options.VideoFileExtensions.Remove(".rar");
+                options.VideoFileExtensions.Remove(".zip");
+            }
+
+            options.VideoFileExtensions.Add(".tp");
+        }
+
         public ItemLookupInfo ParseName(string name)
         public ItemLookupInfo ParseName(string name)
         {
         {
             var resolver = new VideoResolver(GetNamingOptions(), new NullLogger());
             var resolver = new VideoResolver(GetNamingOptions(), new NullLogger());

+ 8 - 0
Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs

@@ -3,6 +3,7 @@ using MediaBrowser.Controller.Resolvers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using System;
 using System;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
+using System.IO;
 
 
 namespace Emby.Server.Implementations.Library.Resolvers.Audio
 namespace Emby.Server.Implementations.Library.Resolvers.Audio
 {
 {
@@ -42,6 +43,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
 
 
                 if (_libraryManager.IsAudioFile(args.Path, libraryOptions))
                 if (_libraryManager.IsAudioFile(args.Path, libraryOptions))
                 {
                 {
+                    if (string.Equals(Path.GetExtension(args.Path), ".cue", StringComparison.OrdinalIgnoreCase))
+                    {
+                        // if audio file exists of same name, return null
+
+                        return null;
+                    }
+
                     var collectionType = args.GetCollectionType();
                     var collectionType = args.GetCollectionType();
 
 
                     var isMixed = string.IsNullOrWhiteSpace(collectionType);
                     var isMixed = string.IsNullOrWhiteSpace(collectionType);

+ 2 - 9
Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs

@@ -160,15 +160,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
                             return true;
                             return true;
                         }
                         }
 
 
-                        var namingOptions = ((LibraryManager)libraryManager).GetNamingOptions();
-
-                        // In mixed folders we need to be conservative and avoid expressions that may result in false positives (e.g. movies with numbers in the title)
-                        if (!isTvContentType)
-                        {
-                            namingOptions.EpisodeExpressions = namingOptions.EpisodeExpressions
-                                .Where(i => i.IsNamed && !i.IsOptimistic)
-                                .ToList();
-                        }
+                        var allowOptimisticEpisodeDetection = isTvContentType;
+                        var namingOptions = ((LibraryManager)libraryManager).GetNamingOptions(allowOptimisticEpisodeDetection);
 
 
                         var episodeResolver = new MediaBrowser.Naming.TV.EpisodeResolver(namingOptions, new NullLogger());
                         var episodeResolver = new MediaBrowser.Naming.TV.EpisodeResolver(namingOptions, new NullLogger());
                         var episodeInfo = episodeResolver.Resolve(fullName, false, false);
                         var episodeInfo = episodeResolver.Resolve(fullName, false, false);

+ 0 - 49
Emby.Server.Implementations/Migrations/GuideMigration.cs

@@ -1,49 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Common.Updates;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Model.Logging;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Tasks;
-using MediaBrowser.Model.Updates;
-using System.Linq;
-
-namespace Emby.Server.Implementations.Migrations
-{
-    public class GuideMigration : IVersionMigration
-    {
-        private readonly IServerConfigurationManager _config;
-        private readonly ITaskManager _taskManager;
-
-        public GuideMigration(IServerConfigurationManager config, ITaskManager taskManager)
-        {
-            _config = config;
-            _taskManager = taskManager;
-        }
-
-        public Task Run()
-        {
-            var name = "GuideRefresh3";
-
-            if (!_config.Configuration.Migrations.Contains(name, StringComparer.OrdinalIgnoreCase))
-            {
-                Task.Run(() =>
-                {
-                    _taskManager.QueueScheduledTask(_taskManager.ScheduledTasks.Select(i => i.ScheduledTask)
-                            .First(i => string.Equals(i.Key, "RefreshGuide", StringComparison.OrdinalIgnoreCase)));
-                });
-
-                var list = _config.Configuration.Migrations.ToList();
-                list.Add(name);
-                _config.Configuration.Migrations = list.ToArray();
-                _config.SaveConfiguration();
-            }
-
-            return Task.FromResult(true);
-        }
-    }
-}

+ 0 - 49
Emby.Server.Implementations/Migrations/LibraryScanMigration.cs

@@ -1,49 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Common.Updates;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Model.Logging;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Tasks;
-using MediaBrowser.Model.Updates;
-using System.Linq;
-
-namespace Emby.Server.Implementations.Migrations
-{
-    public class LibraryScanMigration : IVersionMigration
-    {
-        private readonly IServerConfigurationManager _config;
-        private readonly ITaskManager _taskManager;
-
-        public LibraryScanMigration(IServerConfigurationManager config, ITaskManager taskManager)
-        {
-            _config = config;
-            _taskManager = taskManager;
-        }
-
-        public Task Run()
-        {
-            var name = "LibraryScan6";
-
-            if (!_config.Configuration.Migrations.Contains(name, StringComparer.OrdinalIgnoreCase))
-            {
-                Task.Run(() =>
-                {
-                    _taskManager.QueueScheduledTask(_taskManager.ScheduledTasks.Select(i => i.ScheduledTask)
-                            .First(i => string.Equals(i.Key, "RefreshLibrary", StringComparison.OrdinalIgnoreCase)));
-                });
-
-                var list = _config.Configuration.Migrations.ToList();
-                list.Add(name);
-                _config.Configuration.Migrations = list.ToArray();
-                _config.SaveConfiguration();
-            }
-
-            return Task.FromResult(true);
-        }
-    }
-}

+ 0 - 2
MediaBrowser.Model/Configuration/ServerConfiguration.cs

@@ -187,7 +187,6 @@ namespace MediaBrowser.Model.Configuration
         public bool DisplayCollectionsView { get; set; }
         public bool DisplayCollectionsView { get; set; }
         public string[] LocalNetworkAddresses { get; set; }
         public string[] LocalNetworkAddresses { get; set; }
         public string[] CodecsUsed { get; set; }
         public string[] CodecsUsed { get; set; }
-        public string[] Migrations { get; set; }
         public bool EnableChannelView { get; set; }
         public bool EnableChannelView { get; set; }
         public bool EnableExternalContentInSuggestions { get; set; }
         public bool EnableExternalContentInSuggestions { get; set; }
 
 
@@ -203,7 +202,6 @@ namespace MediaBrowser.Model.Configuration
         {
         {
             LocalNetworkAddresses = new string[] { };
             LocalNetworkAddresses = new string[] { };
             CodecsUsed = new string[] { };
             CodecsUsed = new string[] { };
-            Migrations = new string[] { };
             ImageExtractionTimeoutMs = 0;
             ImageExtractionTimeoutMs = 0;
             EnableLocalizedGuids = true;
             EnableLocalizedGuids = true;
             PathSubstitutions = new PathSubstitution[] { };
             PathSubstitutions = new PathSubstitution[] { };

+ 16 - 0
MediaBrowser.Model/Net/MimeTypes.cs

@@ -248,6 +248,22 @@ namespace MediaBrowser.Model.Net
             {
             {
                 return "audio/ac3";
                 return "audio/ac3";
             }
             }
+            if (StringHelper.EqualsIgnoreCase(ext, ".dsf"))
+            {
+                return "audio/dsf";
+            }
+            if (StringHelper.EqualsIgnoreCase(ext, ".m4b"))
+            {
+                return "audio/m4b";
+            }
+            if (StringHelper.EqualsIgnoreCase(ext, ".xsp"))
+            {
+                return "audio/xsp";
+            }
+            if (StringHelper.EqualsIgnoreCase(ext, ".dsp"))
+            {
+                return "audio/dsp";
+            }
 
 
             // Playlists
             // Playlists
             if (StringHelper.EqualsIgnoreCase(ext, ".m3u8"))
             if (StringHelper.EqualsIgnoreCase(ext, ".m3u8"))

+ 1 - 1
SharedVersion.cs

@@ -1,3 +1,3 @@
 using System.Reflection;
 using System.Reflection;
 
 
-[assembly: AssemblyVersion("3.2.20.3")]
+[assembly: AssemblyVersion("3.2.20.4")]

+ 17 - 0
SocketHttpListener/Net/BoundaryType.cs

@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SocketHttpListener.Net
+{
+    internal enum BoundaryType
+    {
+        ContentLength = 0, // Content-Length: XXX
+        Chunked = 1, // Transfer-Encoding: chunked
+        Multipart = 3,
+        None = 4,
+        Invalid = 5,
+    }
+}

+ 14 - 0
SocketHttpListener/Net/EntitySendFormat.cs

@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SocketHttpListener.Net
+{
+    internal enum EntitySendFormat
+    {
+        ContentLength = 0, // Content-Length: XXX
+        Chunked = 1, // Transfer-Encoding: chunked
+    }
+}

+ 4 - 4
SocketHttpListener/Net/HttpConnection.cs

@@ -25,7 +25,7 @@ namespace SocketHttpListener.Net
         StringBuilder _currentLine;
         StringBuilder _currentLine;
         ListenerPrefix _prefix;
         ListenerPrefix _prefix;
         HttpRequestStream _requestStream;
         HttpRequestStream _requestStream;
-        Stream _responseStream;
+        HttpResponseStream _responseStream;
         bool _chunked;
         bool _chunked;
         int _reuses;
         int _reuses;
         bool _contextBound;
         bool _contextBound;
@@ -202,7 +202,7 @@ namespace SocketHttpListener.Net
             return _requestStream;
             return _requestStream;
         }
         }
 
 
-        public Stream GetResponseStream(bool isExpect100Continue = false)
+        public HttpResponseStream GetResponseStream(bool isExpect100Continue = false)
         {
         {
             // TODO: can we get this _stream before reading the input?
             // TODO: can we get this _stream before reading the input?
             if (_responseStream == null)
             if (_responseStream == null)
@@ -423,14 +423,14 @@ namespace SocketHttpListener.Net
                 HttpListenerResponse response = _context.Response;
                 HttpListenerResponse response = _context.Response;
                 response.StatusCode = status;
                 response.StatusCode = status;
                 response.ContentType = "text/html";
                 response.ContentType = "text/html";
-                string description = HttpListenerResponse.GetStatusDescription(status);
+                string description = HttpStatusDescription.Get(status);
                 string str;
                 string str;
                 if (msg != null)
                 if (msg != null)
                     str = string.Format("<h1>{0} ({1})</h1>", description, msg);
                     str = string.Format("<h1>{0} ({1})</h1>", description, msg);
                 else
                 else
                     str = string.Format("<h1>{0}</h1>", description);
                     str = string.Format("<h1>{0}</h1>", description);
 
 
-                byte[] error = Encoding.Default.GetBytes(str);
+                byte[] error = _textEncoding.GetDefaultEncoding().GetBytes(str);
                 response.Close(error, false);
                 response.Close(error, false);
             }
             }
             catch
             catch

+ 1 - 1
SocketHttpListener/Net/HttpListenerContext.cs

@@ -29,7 +29,7 @@ namespace SocketHttpListener.Net
             _memoryStreamFactory = memoryStreamFactory;
             _memoryStreamFactory = memoryStreamFactory;
             _textEncoding = textEncoding;
             _textEncoding = textEncoding;
             request = new HttpListenerRequest(this, _textEncoding);
             request = new HttpListenerRequest(this, _textEncoding);
-            response = new HttpListenerResponse(this, logger, _textEncoding, fileSystem);
+            response = new HttpListenerResponse(this, _textEncoding);
         }
         }
 
 
         internal int ErrorStatus
         internal int ErrorStatus

+ 329 - 0
SocketHttpListener/Net/HttpListenerResponse.Managed.cs

@@ -0,0 +1,329 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Text;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Text;
+using SocketHttpListener.Primitives;
+using System.Threading;
+using MediaBrowser.Model.IO;
+
+namespace SocketHttpListener.Net
+{
+    public sealed partial class HttpListenerResponse : IDisposable
+    {
+        private long _contentLength;
+        private Version _version = HttpVersion.Version11;
+        private int _statusCode = 200;
+        internal object _headersLock = new object();
+        private bool _forceCloseChunked;
+        private ITextEncoding _textEncoding;
+
+        internal HttpListenerResponse(HttpListenerContext context, ITextEncoding textEncoding)
+        {
+            _httpContext = context;
+            _textEncoding = textEncoding;
+        }
+
+        internal bool ForceCloseChunked => _forceCloseChunked;
+
+        private void EnsureResponseStream()
+        {
+            if (_responseStream == null)
+            {
+                _responseStream = _httpContext.Connection.GetResponseStream();
+            }
+        }
+
+        public Version ProtocolVersion
+        {
+            get => _version;
+            set
+            {
+                CheckDisposed();
+                if (value == null)
+                {
+                    throw new ArgumentNullException(nameof(value));
+                }
+                if (value.Major != 1 || (value.Minor != 0 && value.Minor != 1))
+                {
+                    throw new ArgumentException("Wrong version");
+                }
+
+                _version = new Version(value.Major, value.Minor); // match Windows behavior, trimming to just Major.Minor
+            }
+        }
+
+        public int StatusCode
+        {
+            get => _statusCode;
+            set
+            {
+                CheckDisposed();
+
+                if (value < 100 || value > 999)
+                    throw new ProtocolViolationException("Invalid status");
+
+                _statusCode = value;
+            }
+        }
+
+        private void Dispose() => Close(true);
+
+        public void Close()
+        {
+            if (Disposed)
+                return;
+
+            Close(false);
+        }
+
+        public void Abort()
+        {
+            if (Disposed)
+                return;
+
+            Close(true);
+        }
+
+        private void Close(bool force)
+        {
+            Disposed = true;
+            _httpContext.Connection.Close(force);
+        }
+
+        public void Close(byte[] responseEntity, bool willBlock)
+        {
+            CheckDisposed();
+
+            if (responseEntity == null)
+            {
+                throw new ArgumentNullException(nameof(responseEntity));
+            }
+
+            if (!SentHeaders && _boundaryType != BoundaryType.Chunked)
+            {
+                ContentLength64 = responseEntity.Length;
+            }
+
+            if (willBlock)
+            {
+                try
+                {
+                    OutputStream.Write(responseEntity, 0, responseEntity.Length);
+                }
+                finally
+                {
+                    Close(false);
+                }
+            }
+            else
+            {
+                OutputStream.BeginWrite(responseEntity, 0, responseEntity.Length, iar =>
+                {
+                    var thisRef = (HttpListenerResponse)iar.AsyncState;
+                    try
+                    {
+                        thisRef.OutputStream.EndWrite(iar);
+                    }
+                    finally
+                    {
+                        thisRef.Close(false);
+                    }
+                }, this);
+            }
+        }
+
+        public void CopyFrom(HttpListenerResponse templateResponse)
+        {
+            _webHeaders.Clear();
+            //_webHeaders.Add(templateResponse._webHeaders);
+            _contentLength = templateResponse._contentLength;
+            _statusCode = templateResponse._statusCode;
+            _statusDescription = templateResponse._statusDescription;
+            _keepAlive = templateResponse._keepAlive;
+            _version = templateResponse._version;
+        }
+
+        internal void SendHeaders(bool closing, MemoryStream ms, bool isWebSocketHandshake = false)
+        {
+            if (!isWebSocketHandshake)
+            {
+                if (_webHeaders["Server"] == null)
+                {
+                    _webHeaders.Set("Server", "Microsoft-NetCore/2.0");
+                }
+
+                if (_webHeaders["Date"] == null)
+                {
+                    _webHeaders.Set("Date", DateTime.UtcNow.ToString("r", CultureInfo.InvariantCulture));
+                }
+
+                if (_boundaryType == BoundaryType.None)
+                {
+                    if (HttpListenerRequest.ProtocolVersion <= HttpVersion.Version10)
+                    {
+                        _keepAlive = false;
+                    }
+                    else
+                    {
+                        _boundaryType = BoundaryType.Chunked;
+                    }
+
+                    if (CanSendResponseBody(_httpContext.Response.StatusCode))
+                    {
+                        _contentLength = -1;
+                    }
+                    else
+                    {
+                        _boundaryType = BoundaryType.ContentLength;
+                        _contentLength = 0;
+                    }
+                }
+
+                if (_boundaryType != BoundaryType.Chunked)
+                {
+                    if (_boundaryType != BoundaryType.ContentLength && closing)
+                    {
+                        _contentLength = CanSendResponseBody(_httpContext.Response.StatusCode) ? -1 : 0;
+                    }
+
+                    if (_boundaryType == BoundaryType.ContentLength)
+                    {
+                        _webHeaders.Set("Content-Length", _contentLength.ToString("D", CultureInfo.InvariantCulture));
+                    }
+                }
+
+                /* 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 = (_statusCode == (int)HttpStatusCode.BadRequest || _statusCode == (int)HttpStatusCode.RequestTimeout
+                        || _statusCode == (int)HttpStatusCode.LengthRequired || _statusCode == (int)HttpStatusCode.RequestEntityTooLarge
+                        || _statusCode == (int)HttpStatusCode.RequestUriTooLong || _statusCode == (int)HttpStatusCode.InternalServerError
+                        || _statusCode == (int)HttpStatusCode.ServiceUnavailable);
+
+                if (!conn_close)
+                {
+                    conn_close = !_httpContext.Request.KeepAlive;
+                }
+
+                // They sent both KeepAlive: true and Connection: close
+                if (!_keepAlive || conn_close)
+                {
+                    _webHeaders.Set("Connection", "Close");
+                    conn_close = true;
+                }
+
+                if (SendChunked)
+                {
+                    _webHeaders.Set("Transfer-Encoding", "Chunked");
+                }
+
+                int reuses = _httpContext.Connection.Reuses;
+                if (reuses >= 100)
+                {
+                    _forceCloseChunked = true;
+                    if (!conn_close)
+                    {
+                        _webHeaders.Set("Connection", "Close");
+                        conn_close = true;
+                    }
+                }
+
+                if (HttpListenerRequest.ProtocolVersion <= HttpVersion.Version10)
+                {
+                    if (_keepAlive)
+                    {
+                        Headers["Keep-Alive"] = "true";
+                    }
+
+                    if (!conn_close)
+                    {
+                        _webHeaders.Set("Connection", "Keep-Alive");
+                    }
+                }
+
+                ComputeCookies();
+            }
+
+            Encoding encoding = _textEncoding.GetDefaultEncoding();
+            StreamWriter writer = new StreamWriter(ms, encoding, 256);
+            writer.Write("HTTP/1.1 {0} ", _statusCode); // "1.1" matches Windows implementation, which ignores the response version
+            writer.Flush();
+            byte[] statusDescriptionBytes = WebHeaderEncoding.GetBytes(StatusDescription);
+            ms.Write(statusDescriptionBytes, 0, statusDescriptionBytes.Length);
+            writer.Write("\r\n");
+
+            writer.Write(FormatHeaders(_webHeaders));
+            writer.Flush();
+            int preamble = encoding.GetPreamble().Length;
+            EnsureResponseStream();
+
+            /* Assumes that the ms was at position 0 */
+            ms.Position = preamble;
+            SentHeaders = !isWebSocketHandshake;
+        }
+
+        private static bool HeaderCanHaveEmptyValue(string name) =>
+            !string.Equals(name, "Location", StringComparison.OrdinalIgnoreCase);
+
+        private static string FormatHeaders(WebHeaderCollection headers)
+        {
+            var sb = new StringBuilder();
+
+            for (int i = 0; i < headers.Count; i++)
+            {
+                string key = headers.GetKey(i);
+                string[] values = headers.GetValues(i);
+
+                int startingLength = sb.Length;
+
+                sb.Append(key).Append(": ");
+                bool anyValues = false;
+                for (int j = 0; j < values.Length; j++)
+                {
+                    string value = values[j];
+                    if (!string.IsNullOrWhiteSpace(value))
+                    {
+                        if (anyValues)
+                        {
+                            sb.Append(", ");
+                        }
+                        sb.Append(value);
+                        anyValues = true;
+                    }
+                }
+
+                if (anyValues || HeaderCanHaveEmptyValue(key))
+                {
+                    // Complete the header
+                    sb.Append("\r\n");
+                }
+                else
+                {
+                    // Empty header; remove it.
+                    sb.Length = startingLength;
+                }
+            }
+
+            return sb.Append("\r\n").ToString();
+        }
+
+        private bool Disposed { get; set; }
+        internal bool SentHeaders { get; set; }
+
+        public Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken)
+        {
+            return ((HttpResponseStream)OutputStream).TransmitFile(path, offset, count, fileShareMode, cancellationToken);
+        }
+    }
+}

+ 191 - 461
SocketHttpListener/Net/HttpListenerResponse.cs

@@ -1,572 +1,302 @@
-using System;
-using System.Globalization;
+using System;
+using System.Collections.Generic;
 using System.IO;
 using System.IO;
+using System.Linq;
 using System.Net;
 using System.Net;
 using System.Text;
 using System.Text;
-using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Logging;
-using MediaBrowser.Model.Text;
-using SocketHttpListener.Primitives;
+using System.Globalization;
+using System.Runtime.InteropServices;
+using System.ComponentModel;
+using System.Diagnostics;
+using Microsoft.Win32.SafeHandles;
 
 
 namespace SocketHttpListener.Net
 namespace SocketHttpListener.Net
 {
 {
-    public sealed class HttpListenerResponse : IDisposable
+    public sealed unsafe partial 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;
-        Stream 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();
-
-        private readonly ILogger _logger;
-        private readonly ITextEncoding _textEncoding;
-        private readonly IFileSystem _fileSystem;
-
-        internal HttpListenerResponse(HttpListenerContext context, ILogger logger, ITextEncoding textEncoding, IFileSystem fileSystem)
-        {
-            this.context = context;
-            _logger = logger;
-            _textEncoding = textEncoding;
-            _fileSystem = fileSystem;
-        }
-
-        internal bool CloseConnection
-        {
-            get
-            {
-                return headers["Connection"] == "close";
-            }
-        }
-
-        public bool ForceCloseChunked
-        {
-            get { return false; }
-        }
+        private BoundaryType _boundaryType = BoundaryType.None;
+        private CookieCollection _cookies;
+        private HttpListenerContext _httpContext;
+        private bool _keepAlive = true;
+        private HttpResponseStream _responseStream;
+        private string _statusDescription;
+        private WebHeaderCollection _webHeaders = new WebHeaderCollection();
 
 
-        public Encoding ContentEncoding
+        public WebHeaderCollection Headers
         {
         {
-            get
-            {
-                if (content_encoding == null)
-                    content_encoding = _textEncoding.GetDefaultEncoding();
-                return content_encoding;
-            }
-            set
-            {
-                if (disposed)
-                    throw new ObjectDisposedException(GetType().ToString());
-
-                content_encoding = value;
-            }
+            get => _webHeaders;
         }
         }
 
 
-        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 Encoding ContentEncoding { get; set; }
 
 
         public string ContentType
         public string ContentType
         {
         {
-            get { return content_type; }
+            get => Headers["Content-Type"];
             set
             set
             {
             {
-                // TODO: is null ok?
-                if (disposed)
-                    throw new ObjectDisposedException(GetType().ToString());
-
-                content_type = value;
+                CheckDisposed();
+                if (string.IsNullOrEmpty(value))
+                {
+                    Headers.Remove("Content-Type");
+                }
+                else
+                {
+                    Headers.Set("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?
-        }
+        private HttpListenerContext HttpListenerContext => _httpContext;
 
 
-        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;
-            }
-        }
+        private HttpListenerRequest HttpListenerRequest => HttpListenerContext.Request;
 
 
-        public bool KeepAlive
+        internal EntitySendFormat EntitySendFormat
         {
         {
-            get { return keep_alive; }
+            get => (EntitySendFormat)_boundaryType;
             set
             set
             {
             {
-                if (disposed)
-                    throw new ObjectDisposedException(GetType().ToString());
-
-                keep_alive = value;
+                CheckDisposed();
+                CheckSentHeaders();
+                if (value == EntitySendFormat.Chunked && HttpListenerRequest.ProtocolVersion.Minor == 0)
+                {
+                    throw new ProtocolViolationException("net_nochunkuploadonhttp10");
+                }
+                _boundaryType = (BoundaryType)value;
+                if (value != EntitySendFormat.ContentLength)
+                {
+                    _contentLength = -1;
+                }
             }
             }
         }
         }
 
 
-        public Stream OutputStream
+        public bool SendChunked
         {
         {
-            get
-            {
-                if (output_stream == null)
-                    output_stream = context.Connection.GetResponseStream();
-                return output_stream;
-            }
+            get => EntitySendFormat == EntitySendFormat.Chunked;
+            set => EntitySendFormat = value ? EntitySendFormat.Chunked : EntitySendFormat.ContentLength;
         }
         }
 
 
-        public Version ProtocolVersion
+        // We MUST NOT send message-body when we send responses with these Status codes
+        private static readonly int[] s_noResponseBody = { 100, 101, 204, 205, 304 };
+
+        private static bool CanSendResponseBody(int responseCode)
         {
         {
-            get { return version; }
-            set
+            for (int i = 0; i < s_noResponseBody.Length; i++)
             {
             {
-                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;
+                if (responseCode == s_noResponseBody[i])
+                {
+                    return false;
+                }
             }
             }
+            return true;
         }
         }
 
 
-        public string RedirectLocation
+        public long ContentLength64
         {
         {
-            get { return location; }
+            get => _contentLength;
             set
             set
             {
             {
-                if (disposed)
-                    throw new ObjectDisposedException(GetType().ToString());
-
-                location = value;
+                CheckDisposed();
+                CheckSentHeaders();
+                if (value >= 0)
+                {
+                    _contentLength = value;
+                    _boundaryType = BoundaryType.ContentLength;
+                }
+                else
+                {
+                    throw new ArgumentOutOfRangeException("net_clsmall");
+                }
             }
             }
         }
         }
 
 
-        public bool SendChunked
+        public CookieCollection Cookies
         {
         {
-            get { return chunked; }
-            set
-            {
-                if (disposed)
-                    throw new ObjectDisposedException(GetType().ToString());
-
-                chunked = value;
-            }
+            get => _cookies ?? (_cookies = new CookieCollection());
+            set => _cookies = value;
         }
         }
 
 
-        public int StatusCode
+        public bool KeepAlive
         {
         {
-            get { return status_code; }
+            get => _keepAlive;
             set
             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);
+                CheckDisposed();
+                _keepAlive = value;
             }
             }
         }
         }
 
 
-        internal static string GetStatusDescription(int code)
+        public Stream OutputStream
         {
         {
-            switch (code)
+            get
             {
             {
-                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";
+                CheckDisposed();
+                EnsureResponseStream();
+                return _responseStream;
             }
             }
-            return "";
         }
         }
 
 
-        public string StatusDescription
+        public string RedirectLocation
         {
         {
-            get { return status_description; }
+            get => Headers["Location"];
             set
             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);
-        }
-
-        private void Close(bool force)
-        {
-            if (force)
-            {
-                _logger.Debug("HttpListenerResponse force closing HttpConnection");
+                // note that this doesn't set the status code to a redirect one
+                CheckDisposed();
+                if (string.IsNullOrEmpty(value))
+                {
+                    Headers.Remove("Location");
+                }
+                else
+                {
+                    Headers.Set("Location", value);
+                }
             }
             }
-            disposed = true;
-            context.Connection.Close(force);
         }
         }
 
 
-        public void Close(byte[] responseEntity, bool willBlock)
+        public string StatusDescription
         {
         {
-            //CheckDisposed();
-
-            if (responseEntity == null)
-            {
-                throw new ArgumentNullException(nameof(responseEntity));
-            }
-
-            //if (_boundaryType != BoundaryType.Chunked)
-            {
-                ContentLength64 = responseEntity.Length;
-            }
-
-            if (willBlock)
+            get
             {
             {
-                try
+                if (_statusDescription == null)
                 {
                 {
-                    OutputStream.Write(responseEntity, 0, responseEntity.Length);
+                    // if the user hasn't set this, generated on the fly, if possible.
+                    // We know this one is safe, no need to verify it as in the setter.
+                    _statusDescription = HttpStatusDescription.Get(StatusCode);
                 }
                 }
-                finally
+                if (_statusDescription == null)
                 {
                 {
-                    Close(false);
+                    _statusDescription = string.Empty;
                 }
                 }
+                return _statusDescription;
             }
             }
-            else
+            set
             {
             {
-                OutputStream.BeginWrite(responseEntity, 0, responseEntity.Length, iar =>
+                CheckDisposed();
+                if (value == null)
                 {
                 {
-                    var thisRef = (HttpListenerResponse)iar.AsyncState;
-                    try
-                    {
-                        thisRef.OutputStream.EndWrite(iar);
-                    }
-                    finally
+                    throw new ArgumentNullException(nameof(value));
+                }
+
+                // Need to verify the status description doesn't contain any control characters except HT.  We mask off the high
+                // byte since that's how it's encoded.
+                for (int i = 0; i < value.Length; i++)
+                {
+                    char c = (char)(0x000000ff & (uint)value[i]);
+                    if ((c <= 31 && c != (byte)'\t') || c == 127)
                     {
                     {
-                        thisRef.Close(false);
+                        throw new ArgumentException("net_WebHeaderInvalidControlChars");
                     }
                     }
-                }, this);
+                }
+
+                _statusDescription = value;
             }
             }
         }
         }
 
 
-        public void Close()
+        public void AddHeader(string name, string value)
         {
         {
-            if (disposed)
-                return;
-
-            Close(false);
+            Headers.Set(name, value);
         }
         }
 
 
-        public void Redirect(string url)
+        public void AppendHeader(string name, string value)
         {
         {
-            StatusCode = 302; // Found
-            location = url;
+            Headers.Add(name, value);
         }
         }
 
 
-        bool FindCookie(Cookie cookie)
+        public void AppendCookie(Cookie cookie)
         {
         {
-            string name = cookie.Name;
-            string domain = cookie.Domain;
-            string path = cookie.Path;
-            foreach (Cookie c in cookies)
+            if (cookie == null)
             {
             {
-                if (name != c.Name)
-                    continue;
-                if (domain != c.Domain)
-                    continue;
-                if (path == c.Path)
-                    return true;
+                throw new ArgumentNullException(nameof(cookie));
             }
             }
-
-            return false;
+            Cookies.Add(cookie);
         }
         }
 
 
-        public void DetermineIfChunked()
+        private void ComputeCookies()
         {
         {
-            if (chunked)
+            if (_cookies != null)
             {
             {
-                return;
-            }
-
-            Version v = context.Request.ProtocolVersion;
-            if (!cl_set && !chunked && v >= HttpVersion.Version11)
-                chunked = true;
-            if (!chunked && string.Equals(headers["Transfer-Encoding"], "chunked"))
-            {
-                chunked = true;
+                // now go through the collection, and concatenate all the cookies in per-variant strings
+                //string setCookie2 = null, setCookie = null;
+                //for (int index = 0; index < _cookies.Count; index++)
+                //{
+                //    Cookie cookie = _cookies[index];
+                //    string cookieString = cookie.ToServerString();
+                //    if (cookieString == null || cookieString.Length == 0)
+                //    {
+                //        continue;
+                //    }
+
+                //    if (cookie.IsRfc2965Variant())
+                //    {
+                //        setCookie2 = setCookie2 == null ? cookieString : setCookie2 + ", " + cookieString;
+                //    }
+                //    else
+                //    {
+                //        setCookie = setCookie == null ? cookieString : setCookie + ", " + cookieString;
+                //    }
+                //}
+
+                //if (!string.IsNullOrEmpty(setCookie))
+                //{
+                //    Headers.Set(HttpKnownHeaderNames.SetCookie, setCookie);
+                //    if (string.IsNullOrEmpty(setCookie2))
+                //    {
+                //        Headers.Remove(HttpKnownHeaderNames.SetCookie2);
+                //    }
+                //}
+
+                //if (!string.IsNullOrEmpty(setCookie2))
+                //{
+                //    Headers.Set(HttpKnownHeaderNames.SetCookie2, setCookie2);
+                //    if (string.IsNullOrEmpty(setCookie))
+                //    {
+                //        Headers.Remove(HttpKnownHeaderNames.SetCookie);
+                //    }
+                //}
             }
             }
         }
         }
 
 
-        internal void SendHeaders(bool closing, MemoryStream ms)
+        public void Redirect(string url)
         {
         {
-            Encoding encoding = content_encoding;
-            if (encoding == null)
-                encoding = _textEncoding.GetDefaultEncoding();
-
-            if (content_type != null)
-            {
-                if (content_encoding != null && content_type.IndexOf("charset=", StringComparison.OrdinalIgnoreCase) == -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));
-            }
+            Headers["Location"] = url;
+            StatusCode = (int)HttpStatusCode.Redirect;
+            StatusDescription = "Found";
+        }
 
 
-            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)
+        public void SetCookie(Cookie cookie)
+        {
+            if (cookie == null)
             {
             {
-                headers.SetInternal("Connection", "close");
-                conn_close = true;
+                throw new ArgumentNullException(nameof(cookie));
             }
             }
 
 
-            if (chunked)
-                headers.SetInternal("Transfer-Encoding", "chunked");
+            //Cookie newCookie = cookie.Clone();
+            //int added = Cookies.InternalAdd(newCookie, true);
 
 
-            //int reuses = context.Connection.Reuses;
-            //if (reuses >= 100)
+            //if (added != 1)
             //{
             //{
-            //    _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;
-            //    }
+            //    // The Cookie already existed and couldn't be replaced.
+            //    throw new ArgumentException("Cookie exists");
             //}
             //}
+        }
 
 
-            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());
-            }
-
-            headers.SetInternal("Status", status_code.ToString(CultureInfo.InvariantCulture));
+        void IDisposable.Dispose() => Dispose();
 
 
-            using (StreamWriter writer = new StreamWriter(ms, encoding, 256, true))
+        private void CheckDisposed()
+        {
+            if (Disposed)
             {
             {
-                writer.Write("HTTP/{0} {1} {2}\r\n", version, status_code, status_description);
-                string headers_str = headers.ToStringMultiValue();
-                writer.Write(headers_str);
-                writer.Flush();
+                throw new ObjectDisposedException(GetType().FullName);
             }
             }
-
-            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)
+        private void CheckSentHeaders()
         {
         {
-            if (cookie == null)
-                throw new ArgumentNullException("cookie");
-
-            if (cookies != null)
+            if (SentHeaders)
             {
             {
-                if (FindCookie(cookie))
-                    throw new ArgumentException("The cookie already exists.");
+                throw new InvalidOperationException();
             }
             }
-            else
-            {
-                cookies = new CookieCollection();
-            }
-
-            cookies.Add(cookie);
-        }
-
-        public Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken)
-        {
-            return ((HttpResponseStream)OutputStream).TransmitFile(path, offset, count, fileShareMode, cancellationToken);
         }
         }
     }
     }
-}
+}

+ 25 - 7
SocketHttpListener/Net/HttpRequestStream.Managed.cs

@@ -104,9 +104,24 @@ namespace SocketHttpListener.Net
                 return nread;
                 return nread;
             }
             }
 
 
+            if (_remainingBody > 0)
+            {
+                size = (int)Math.Min(_remainingBody, (long)size);
+            }
+
             nread = _stream.Read(buffer, offset, size);
             nread = _stream.Read(buffer, offset, size);
-            if (nread > 0 && _remainingBody > 0)
+
+            if (_remainingBody > 0)
+            {
+                if (nread == 0)
+                {
+                    throw new Exception("Bad request");
+                }
+
+                //Debug.Assert(nread <= _remainingBody);
                 _remainingBody -= nread;
                 _remainingBody -= nread;
+            }
+
             return nread;
             return nread;
         }
         }
 
 
@@ -139,7 +154,7 @@ namespace SocketHttpListener.Net
             // for HTTP pipelining
             // for HTTP pipelining
             if (_remainingBody >= 0 && size > _remainingBody)
             if (_remainingBody >= 0 && size > _remainingBody)
             {
             {
-                size = (int)Math.Min(int.MaxValue, _remainingBody);
+                size = (int)Math.Min(_remainingBody, (long)size);
             }
             }
 
 
             return _stream.BeginRead(buffer, offset, size, cback, state);
             return _stream.BeginRead(buffer, offset, size, cback, state);
@@ -150,9 +165,7 @@ namespace SocketHttpListener.Net
             if (asyncResult == null)
             if (asyncResult == null)
                 throw new ArgumentNullException(nameof(asyncResult));
                 throw new ArgumentNullException(nameof(asyncResult));
 
 
-            var r = asyncResult as HttpStreamAsyncResult;
-
-            if (r != null)
+            if (asyncResult is HttpStreamAsyncResult r)
             {
             {
                 if (!ReferenceEquals(this, r._parent))
                 if (!ReferenceEquals(this, r._parent))
                 {
                 {
@@ -160,7 +173,7 @@ namespace SocketHttpListener.Net
                 }
                 }
                 if (r._endCalled)
                 if (r._endCalled)
                 {
                 {
-                    throw new InvalidOperationException("Invalid end call");
+                    throw new InvalidOperationException("invalid end call");
                 }
                 }
                 r._endCalled = true;
                 r._endCalled = true;
 
 
@@ -185,8 +198,13 @@ namespace SocketHttpListener.Net
                 throw e.InnerException;
                 throw e.InnerException;
             }
             }
 
 
-            if (_remainingBody > 0 && nread > 0)
+            if (_remainingBody > 0)
             {
             {
+                if (nread == 0)
+                {
+                    throw new Exception("Bad request");
+                }
+
                 _remainingBody -= nread;
                 _remainingBody -= nread;
             }
             }
 
 

+ 17 - 16
SocketHttpListener/Net/HttpResponseStream.Managed.cs

@@ -132,27 +132,28 @@ namespace SocketHttpListener.Net
 
 
         private MemoryStream GetHeaders(bool closing, bool isWebSocketHandshake = false)
         private MemoryStream GetHeaders(bool closing, bool isWebSocketHandshake = false)
         {
         {
+            //// SendHeaders works on shared headers
+            //lock (_response.headers_lock)
+            //{
+            //    if (_response.HeadersSent)
+            //        return null;
+            //    var ms = _memoryStreamFactory.CreateNew();
+            //    _response.SendHeaders(closing, ms);
+            //    return ms;
+            //}
+
             // SendHeaders works on shared headers
             // SendHeaders works on shared headers
-            lock (_response.headers_lock)
+            lock (_response._headersLock)
             {
             {
-                if (_response.HeadersSent)
+                if (_response.SentHeaders)
+                {
                     return null;
                     return null;
-                var ms = _memoryStreamFactory.CreateNew();
-                _response.SendHeaders(closing, ms);
+                }
+
+                MemoryStream ms = new MemoryStream();
+                _response.SendHeaders(closing, ms, isWebSocketHandshake);
                 return ms;
                 return ms;
             }
             }
-
-            //lock (_response._headersLock)
-            //{
-            //    if (_response.SentHeaders)
-            //    {
-            //        return null;
-            //    }
-
-            //    MemoryStream ms = new MemoryStream();
-            //    _response.SendHeaders(closing, ms, isWebSocketHandshake);
-            //    return ms;
-            //}
         }
         }
 
 
         private static byte[] s_crlf = new byte[] { 13, 10 };
         private static byte[] s_crlf = new byte[] { 13, 10 };

+ 75 - 0
SocketHttpListener/Net/HttpStatusDescription.cs

@@ -0,0 +1,75 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SocketHttpListener.Net
+{
+    internal static class HttpStatusDescription
+    {
+        internal static string Get(HttpStatusCode code)
+        {
+            return Get((int)code);
+        }
+
+        internal static string Get(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 426: return "Upgrade Required"; // RFC 2817
+
+                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 null;
+        }
+    }
+}

+ 131 - 0
SocketHttpListener/Net/WebHeaderEncoding.cs

@@ -0,0 +1,131 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SocketHttpListener.Net
+{
+    // we use this static class as a helper class to encode/decode HTTP headers.
+    // what we need is a 1-1 correspondence between a char in the range U+0000-U+00FF
+    // and a byte in the range 0x00-0xFF (which is the range that can hit the network).
+    // The Latin-1 encoding (ISO-88591-1) (GetEncoding(28591)) works for byte[] to string, but is a little slow.
+    // It doesn't work for string -> byte[] because of best-fit-mapping problems.
+    internal static class WebHeaderEncoding
+    {
+        // We don't want '?' replacement characters, just fail.
+        private static readonly Encoding s_utf8Decoder = Encoding.GetEncoding("utf-8", EncoderFallback.ExceptionFallback, DecoderFallback.ExceptionFallback);
+
+        internal static unsafe string GetString(byte[] bytes, int byteIndex, int byteCount)
+        {
+            fixed (byte* pBytes = bytes)
+                return GetString(pBytes + byteIndex, byteCount);
+        }
+
+        internal static unsafe string GetString(byte* pBytes, int byteCount)
+        {
+            if (byteCount < 1)
+                return "";
+
+            string s = new string('\0', byteCount);
+
+            fixed (char* pStr = s)
+            {
+                char* pString = pStr;
+                while (byteCount >= 8)
+                {
+                    pString[0] = (char)pBytes[0];
+                    pString[1] = (char)pBytes[1];
+                    pString[2] = (char)pBytes[2];
+                    pString[3] = (char)pBytes[3];
+                    pString[4] = (char)pBytes[4];
+                    pString[5] = (char)pBytes[5];
+                    pString[6] = (char)pBytes[6];
+                    pString[7] = (char)pBytes[7];
+                    pString += 8;
+                    pBytes += 8;
+                    byteCount -= 8;
+                }
+                for (int i = 0; i < byteCount; i++)
+                {
+                    pString[i] = (char)pBytes[i];
+                }
+            }
+
+            return s;
+        }
+
+        internal static int GetByteCount(string myString)
+        {
+            return myString.Length;
+        }
+        internal static unsafe void GetBytes(string myString, int charIndex, int charCount, byte[] bytes, int byteIndex)
+        {
+            if (myString.Length == 0)
+            {
+                return;
+            }
+            fixed (byte* bufferPointer = bytes)
+            {
+                byte* newBufferPointer = bufferPointer + byteIndex;
+                int finalIndex = charIndex + charCount;
+                while (charIndex < finalIndex)
+                {
+                    *newBufferPointer++ = (byte)myString[charIndex++];
+                }
+            }
+        }
+        internal static unsafe byte[] GetBytes(string myString)
+        {
+            byte[] bytes = new byte[myString.Length];
+            if (myString.Length != 0)
+            {
+                GetBytes(myString, 0, myString.Length, bytes, 0);
+            }
+            return bytes;
+        }
+
+        // The normal client header parser just casts bytes to chars (see GetString).
+        // Check if those bytes were actually utf-8 instead of ASCII.
+        // If not, just return the input value.
+        internal static string DecodeUtf8FromString(string input)
+        {
+            if (string.IsNullOrWhiteSpace(input))
+            {
+                return input;
+            }
+
+            bool possibleUtf8 = false;
+            for (int i = 0; i < input.Length; i++)
+            {
+                if (input[i] > (char)255)
+                {
+                    return input; // This couldn't have come from the wire, someone assigned it directly.
+                }
+                else if (input[i] > (char)127)
+                {
+                    possibleUtf8 = true;
+                    break;
+                }
+            }
+            if (possibleUtf8)
+            {
+                byte[] rawBytes = new byte[input.Length];
+                for (int i = 0; i < input.Length; i++)
+                {
+                    if (input[i] > (char)255)
+                    {
+                        return input; // This couldn't have come from the wire, someone assigned it directly.
+                    }
+                    rawBytes[i] = (byte)input[i];
+                }
+                try
+                {
+                    return s_utf8Decoder.GetString(rawBytes);
+                }
+                catch (ArgumentException) { } // Not actually Utf-8
+            }
+            return input;
+        }
+    }
+}

+ 6 - 0
SocketHttpListener/SocketHttpListener.csproj

@@ -21,6 +21,7 @@
     <DefineConstants>DEBUG;TRACE</DefineConstants>
     <DefineConstants>DEBUG;TRACE</DefineConstants>
     <ErrorReport>prompt</ErrorReport>
     <ErrorReport>prompt</ErrorReport>
     <WarningLevel>4</WarningLevel>
     <WarningLevel>4</WarningLevel>
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
     <DebugType>pdbonly</DebugType>
     <DebugType>pdbonly</DebugType>
@@ -56,27 +57,32 @@
     <Compile Include="Mask.cs" />
     <Compile Include="Mask.cs" />
     <Compile Include="MessageEventArgs.cs" />
     <Compile Include="MessageEventArgs.cs" />
     <Compile Include="Net\AuthenticationSchemeSelector.cs" />
     <Compile Include="Net\AuthenticationSchemeSelector.cs" />
+    <Compile Include="Net\BoundaryType.cs" />
     <Compile Include="Net\ChunkedInputStream.cs" />
     <Compile Include="Net\ChunkedInputStream.cs" />
     <Compile Include="Net\ChunkStream.cs" />
     <Compile Include="Net\ChunkStream.cs" />
     <Compile Include="Net\CookieHelper.cs" />
     <Compile Include="Net\CookieHelper.cs" />
     <Compile Include="Net\EndPointListener.cs" />
     <Compile Include="Net\EndPointListener.cs" />
     <Compile Include="Net\EndPointManager.cs" />
     <Compile Include="Net\EndPointManager.cs" />
+    <Compile Include="Net\EntitySendFormat.cs" />
     <Compile Include="Net\HttpConnection.cs" />
     <Compile Include="Net\HttpConnection.cs" />
     <Compile Include="Net\HttpListener.cs" />
     <Compile Include="Net\HttpListener.cs" />
     <Compile Include="Net\HttpListenerBasicIdentity.cs" />
     <Compile Include="Net\HttpListenerBasicIdentity.cs" />
     <Compile Include="Net\HttpListenerContext.cs" />
     <Compile Include="Net\HttpListenerContext.cs" />
     <Compile Include="Net\HttpListenerPrefixCollection.cs" />
     <Compile Include="Net\HttpListenerPrefixCollection.cs" />
     <Compile Include="Net\HttpListenerRequest.cs" />
     <Compile Include="Net\HttpListenerRequest.cs" />
+    <Compile Include="Net\HttpListenerResponse.Managed.cs" />
     <Compile Include="Net\HttpListenerResponse.cs" />
     <Compile Include="Net\HttpListenerResponse.cs" />
     <Compile Include="Net\HttpRequestStream.cs" />
     <Compile Include="Net\HttpRequestStream.cs" />
     <Compile Include="Net\HttpRequestStream.Managed.cs" />
     <Compile Include="Net\HttpRequestStream.Managed.cs" />
     <Compile Include="Net\HttpResponseStream.cs" />
     <Compile Include="Net\HttpResponseStream.cs" />
     <Compile Include="Net\HttpResponseStream.Managed.cs" />
     <Compile Include="Net\HttpResponseStream.Managed.cs" />
     <Compile Include="Net\HttpStatusCode.cs" />
     <Compile Include="Net\HttpStatusCode.cs" />
+    <Compile Include="Net\HttpStatusDescription.cs" />
     <Compile Include="Net\HttpStreamAsyncResult.cs" />
     <Compile Include="Net\HttpStreamAsyncResult.cs" />
     <Compile Include="Net\HttpVersion.cs" />
     <Compile Include="Net\HttpVersion.cs" />
     <Compile Include="Net\ListenerPrefix.cs" />
     <Compile Include="Net\ListenerPrefix.cs" />
     <Compile Include="Net\WebHeaderCollection.cs" />
     <Compile Include="Net\WebHeaderCollection.cs" />
+    <Compile Include="Net\WebHeaderEncoding.cs" />
     <Compile Include="Net\WebSockets\HttpListenerWebSocketContext.cs" />
     <Compile Include="Net\WebSockets\HttpListenerWebSocketContext.cs" />
     <Compile Include="Net\WebSockets\WebSocketContext.cs" />
     <Compile Include="Net\WebSockets\WebSocketContext.cs" />
     <Compile Include="Opcode.cs" />
     <Compile Include="Opcode.cs" />