diff --git a/Emby.Server.Core/ApplicationHost.cs b/Emby.Server.Core/ApplicationHost.cs index 5e9024e1d3..7019fe0ded 100644 --- a/Emby.Server.Core/ApplicationHost.cs +++ b/Emby.Server.Core/ApplicationHost.cs @@ -511,8 +511,6 @@ namespace Emby.Server.Core { var migrations = new List { - new LibraryScanMigration(ServerConfigurationManager, TaskManager), - new GuideMigration(ServerConfigurationManager, TaskManager) }; foreach (var task in migrations) diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 4482bb13b2..210448cd95 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -182,8 +182,6 @@ - - diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index 79209d438b..28c23b7665 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -598,9 +598,10 @@ namespace Emby.Server.Implementations.HttpServer { ErrorHandler(ex, httpReq, false); } + catch (Exception ex) { - ErrorHandler(ex, httpReq); + ErrorHandler(ex, httpReq, !string.Equals(ex.GetType().Name, "SocketException", StringComparison.OrdinalIgnoreCase)); } finally { diff --git a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs index 57eef5db03..ac36f8f516 100644 --- a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs +++ b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs @@ -53,7 +53,7 @@ namespace Emby.Server.Implementations.HttpServer { 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"; } diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index a08c744748..15efd3d39c 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2505,9 +2505,32 @@ namespace Emby.Server.Implementations.Library 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()); } + private NamingOptions _namingOptionsWithoutOptimisticEpisodeDetection; private NamingOptions _namingOptions; private string[] _videoFileExtensions; public NamingOptions GetNamingOptions(LibraryOptions libraryOptions) @@ -2516,23 +2539,8 @@ namespace Emby.Server.Implementations.Library { var options = new ExtendedNamingOptions(); - // These cause apps to have problems - options.AudioFileExtensions.Remove(".m3u"); - options.AudioFileExtensions.Remove(".wpl"); + InitNamingOptions(options); - //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"); _namingOptions = options; _videoFileExtensions = _namingOptions.VideoFileExtensions.ToArray(); } @@ -2540,6 +2548,27 @@ namespace Emby.Server.Implementations.Library 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) { var resolver = new VideoResolver(GetNamingOptions(), new NullLogger()); diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs index 2e3d81474a..e2f2946db1 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs @@ -3,6 +3,7 @@ using MediaBrowser.Controller.Resolvers; using MediaBrowser.Model.Entities; using System; using MediaBrowser.Controller.Entities; +using System.IO; 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 (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 isMixed = string.IsNullOrWhiteSpace(collectionType); diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs index 60260e98a9..e1c18c9131 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs @@ -160,15 +160,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV 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 episodeInfo = episodeResolver.Resolve(fullName, false, false); diff --git a/Emby.Server.Implementations/Migrations/GuideMigration.cs b/Emby.Server.Implementations/Migrations/GuideMigration.cs deleted file mode 100644 index 78fb6c222d..0000000000 --- a/Emby.Server.Implementations/Migrations/GuideMigration.cs +++ /dev/null @@ -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); - } - } -} diff --git a/Emby.Server.Implementations/Migrations/LibraryScanMigration.cs b/Emby.Server.Implementations/Migrations/LibraryScanMigration.cs deleted file mode 100644 index 9d7f67a4f7..0000000000 --- a/Emby.Server.Implementations/Migrations/LibraryScanMigration.cs +++ /dev/null @@ -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); - } - } -} diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 4529de59a4..a570f7b10c 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -187,7 +187,6 @@ namespace MediaBrowser.Model.Configuration public bool DisplayCollectionsView { get; set; } public string[] LocalNetworkAddresses { get; set; } public string[] CodecsUsed { get; set; } - public string[] Migrations { get; set; } public bool EnableChannelView { get; set; } public bool EnableExternalContentInSuggestions { get; set; } @@ -203,7 +202,6 @@ namespace MediaBrowser.Model.Configuration { LocalNetworkAddresses = new string[] { }; CodecsUsed = new string[] { }; - Migrations = new string[] { }; ImageExtractionTimeoutMs = 0; EnableLocalizedGuids = true; PathSubstitutions = new PathSubstitution[] { }; diff --git a/MediaBrowser.Model/Net/MimeTypes.cs b/MediaBrowser.Model/Net/MimeTypes.cs index 790f8a8f61..3c3c2bbc78 100644 --- a/MediaBrowser.Model/Net/MimeTypes.cs +++ b/MediaBrowser.Model/Net/MimeTypes.cs @@ -248,6 +248,22 @@ namespace MediaBrowser.Model.Net { 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 if (StringHelper.EqualsIgnoreCase(ext, ".m3u8")) diff --git a/SharedVersion.cs b/SharedVersion.cs index f362517731..adeff35086 100644 --- a/SharedVersion.cs +++ b/SharedVersion.cs @@ -1,3 +1,3 @@ using System.Reflection; -[assembly: AssemblyVersion("3.2.20.3")] +[assembly: AssemblyVersion("3.2.20.4")] diff --git a/SocketHttpListener/Net/BoundaryType.cs b/SocketHttpListener/Net/BoundaryType.cs new file mode 100644 index 0000000000..c3ac00c0fd --- /dev/null +++ b/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, + } +} diff --git a/SocketHttpListener/Net/EntitySendFormat.cs b/SocketHttpListener/Net/EntitySendFormat.cs new file mode 100644 index 0000000000..6e585bdc9b --- /dev/null +++ b/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 + } +} diff --git a/SocketHttpListener/Net/HttpConnection.cs b/SocketHttpListener/Net/HttpConnection.cs index 9c87ff0763..79491d6f9c 100644 --- a/SocketHttpListener/Net/HttpConnection.cs +++ b/SocketHttpListener/Net/HttpConnection.cs @@ -25,7 +25,7 @@ namespace SocketHttpListener.Net StringBuilder _currentLine; ListenerPrefix _prefix; HttpRequestStream _requestStream; - Stream _responseStream; + HttpResponseStream _responseStream; bool _chunked; int _reuses; bool _contextBound; @@ -202,7 +202,7 @@ namespace SocketHttpListener.Net return _requestStream; } - public Stream GetResponseStream(bool isExpect100Continue = false) + public HttpResponseStream GetResponseStream(bool isExpect100Continue = false) { // TODO: can we get this _stream before reading the input? if (_responseStream == null) @@ -423,14 +423,14 @@ namespace SocketHttpListener.Net HttpListenerResponse response = _context.Response; response.StatusCode = status; response.ContentType = "text/html"; - string description = HttpListenerResponse.GetStatusDescription(status); + string description = HttpStatusDescription.Get(status); string str; if (msg != null) str = string.Format("

{0} ({1})

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

{0}

", description); - byte[] error = Encoding.Default.GetBytes(str); + byte[] error = _textEncoding.GetDefaultEncoding().GetBytes(str); response.Close(error, false); } catch diff --git a/SocketHttpListener/Net/HttpListenerContext.cs b/SocketHttpListener/Net/HttpListenerContext.cs index 58d769f22a..1bf39589d9 100644 --- a/SocketHttpListener/Net/HttpListenerContext.cs +++ b/SocketHttpListener/Net/HttpListenerContext.cs @@ -29,7 +29,7 @@ namespace SocketHttpListener.Net _memoryStreamFactory = memoryStreamFactory; _textEncoding = textEncoding; request = new HttpListenerRequest(this, _textEncoding); - response = new HttpListenerResponse(this, logger, _textEncoding, fileSystem); + response = new HttpListenerResponse(this, _textEncoding); } internal int ErrorStatus diff --git a/SocketHttpListener/Net/HttpListenerResponse.Managed.cs b/SocketHttpListener/Net/HttpListenerResponse.Managed.cs new file mode 100644 index 0000000000..52576fdf2f --- /dev/null +++ b/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); + } + } +} diff --git a/SocketHttpListener/Net/HttpListenerResponse.cs b/SocketHttpListener/Net/HttpListenerResponse.cs index da7aff0818..240c67930c 100644 --- a/SocketHttpListener/Net/HttpListenerResponse.cs +++ b/SocketHttpListener/Net/HttpListenerResponse.cs @@ -1,149 +1,128 @@ -using System; -using System.Globalization; +using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Net; using System.Text; -using System.Threading; 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 { - 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; + 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(); - 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) + public WebHeaderCollection Headers { - this.context = context; - _logger = logger; - _textEncoding = textEncoding; - _fileSystem = fileSystem; + get => _webHeaders; } - internal bool CloseConnection - { - get - { - return headers["Connection"] == "close"; - } - } + public Encoding ContentEncoding { get; set; } - public bool ForceCloseChunked + public string ContentType { - get { return false; } - } - - public Encoding ContentEncoding - { - get - { - if (content_encoding == null) - content_encoding = _textEncoding.GetDefaultEncoding(); - return content_encoding; - } + get => Headers["Content-Type"]; set { - if (disposed) - throw new ObjectDisposedException(GetType().ToString()); - - content_encoding = value; + CheckDisposed(); + if (string.IsNullOrEmpty(value)) + { + Headers.Remove("Content-Type"); + } + else + { + Headers.Set("Content-Type", value); + } } } + private HttpListenerContext HttpListenerContext => _httpContext; + + private HttpListenerRequest HttpListenerRequest => HttpListenerContext.Request; + + internal EntitySendFormat EntitySendFormat + { + get => (EntitySendFormat)_boundaryType; + set + { + 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 bool SendChunked + { + get => EntitySendFormat == EntitySendFormat.Chunked; + set => EntitySendFormat = value ? EntitySendFormat.Chunked : EntitySendFormat.ContentLength; + } + + // 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) + { + for (int i = 0; i < s_noResponseBody.Length; i++) + { + if (responseCode == s_noResponseBody[i]) + { + return false; + } + } + return true; + } + public long ContentLength64 { - get { return content_length; } + get => _contentLength; 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; + CheckDisposed(); + CheckSentHeaders(); + if (value >= 0) + { + _contentLength = value; + _boundaryType = BoundaryType.ContentLength; + } + else + { + throw new ArgumentOutOfRangeException("net_clsmall"); + } } } - 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; - } + get => _cookies ?? (_cookies = new CookieCollection()); + set => _cookies = value; } public bool KeepAlive { - get { return keep_alive; } + get => _keepAlive; set { - if (disposed) - throw new ObjectDisposedException(GetType().ToString()); - - keep_alive = value; + CheckDisposed(); + _keepAlive = value; } } @@ -151,422 +130,173 @@ namespace SocketHttpListener.Net { 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; + CheckDisposed(); + EnsureResponseStream(); + return _responseStream; } } public string RedirectLocation { - get { return location; } + get => Headers["Location"]; set { - if (disposed) - throw new ObjectDisposedException(GetType().ToString()); - - location = value; + // 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); + } } } - 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; } + get + { + if (_statusDescription == null) + { + // 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); + } + if (_statusDescription == null) + { + _statusDescription = string.Empty; + } + return _statusDescription; + } set { - status_description = value; + CheckDisposed(); + if (value == null) + { + 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) + { + throw new ArgumentException("net_WebHeaderInvalidControlChars"); + } + } + + _statusDescription = 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"); + Headers.Set(name, value); + } - 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 AppendHeader(string name, string value) + { + Headers.Add(name, value); } public void AppendCookie(Cookie cookie) { if (cookie == null) - throw new ArgumentNullException("cookie"); - + { + throw new ArgumentNullException(nameof(cookie)); + } Cookies.Add(cookie); } - public void AppendHeader(string name, string value) + private void ComputeCookies() { - 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) + if (_cookies != null) { - _logger.Debug("HttpListenerResponse force closing HttpConnection"); + // 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); + // } + //} } - disposed = true; - context.Connection.Close(force); - } - - public void Close(byte[] responseEntity, bool willBlock) - { - //CheckDisposed(); - - if (responseEntity == null) - { - throw new ArgumentNullException(nameof(responseEntity)); - } - - //if (_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 Close() - { - if (disposed) - return; - - 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; - } - - public void DetermineIfChunked() - { - if (chunked) - { - 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; - } - } - - 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.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)); - } - - 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()); - } - - headers.SetInternal("Status", status_code.ToString(CultureInfo.InvariantCulture)); - - 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; + Headers["Location"] = url; + StatusCode = (int)HttpStatusCode.Redirect; + StatusDescription = "Found"; } 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(); + throw new ArgumentNullException(nameof(cookie)); } - cookies.Add(cookie); + //Cookie newCookie = cookie.Clone(); + //int added = Cookies.InternalAdd(newCookie, true); + + //if (added != 1) + //{ + // // The Cookie already existed and couldn't be replaced. + // throw new ArgumentException("Cookie exists"); + //} } - public Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken) + void IDisposable.Dispose() => Dispose(); + + private void CheckDisposed() { - return ((HttpResponseStream)OutputStream).TransmitFile(path, offset, count, fileShareMode, cancellationToken); + if (Disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + + private void CheckSentHeaders() + { + if (SentHeaders) + { + throw new InvalidOperationException(); + } } } -} \ No newline at end of file +} diff --git a/SocketHttpListener/Net/HttpRequestStream.Managed.cs b/SocketHttpListener/Net/HttpRequestStream.Managed.cs index cb02a4d5a2..2b5dfc8382 100644 --- a/SocketHttpListener/Net/HttpRequestStream.Managed.cs +++ b/SocketHttpListener/Net/HttpRequestStream.Managed.cs @@ -104,9 +104,24 @@ namespace SocketHttpListener.Net return nread; } + if (_remainingBody > 0) + { + size = (int)Math.Min(_remainingBody, (long)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; + } + return nread; } @@ -139,7 +154,7 @@ namespace SocketHttpListener.Net // for HTTP pipelining 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); @@ -150,9 +165,7 @@ namespace SocketHttpListener.Net if (asyncResult == null) throw new ArgumentNullException(nameof(asyncResult)); - var r = asyncResult as HttpStreamAsyncResult; - - if (r != null) + if (asyncResult is HttpStreamAsyncResult r) { if (!ReferenceEquals(this, r._parent)) { @@ -160,7 +173,7 @@ namespace SocketHttpListener.Net } if (r._endCalled) { - throw new InvalidOperationException("Invalid end call"); + throw new InvalidOperationException("invalid end call"); } r._endCalled = true; @@ -185,8 +198,13 @@ namespace SocketHttpListener.Net throw e.InnerException; } - if (_remainingBody > 0 && nread > 0) + if (_remainingBody > 0) { + if (nread == 0) + { + throw new Exception("Bad request"); + } + _remainingBody -= nread; } diff --git a/SocketHttpListener/Net/HttpResponseStream.Managed.cs b/SocketHttpListener/Net/HttpResponseStream.Managed.cs index 42db03e476..116c3280a7 100644 --- a/SocketHttpListener/Net/HttpResponseStream.Managed.cs +++ b/SocketHttpListener/Net/HttpResponseStream.Managed.cs @@ -132,27 +132,28 @@ namespace SocketHttpListener.Net 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; - } - - //lock (_response._headersLock) + //// SendHeaders works on shared headers + //lock (_response.headers_lock) //{ - // if (_response.SentHeaders) - // { + // if (_response.HeadersSent) // return null; - // } - - // MemoryStream ms = new MemoryStream(); - // _response.SendHeaders(closing, ms, isWebSocketHandshake); + // var ms = _memoryStreamFactory.CreateNew(); + // _response.SendHeaders(closing, ms); // return ms; //} + + // SendHeaders works on shared headers + 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 }; diff --git a/SocketHttpListener/Net/HttpStatusDescription.cs b/SocketHttpListener/Net/HttpStatusDescription.cs new file mode 100644 index 0000000000..8d490c5117 --- /dev/null +++ b/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; + } + } +} diff --git a/SocketHttpListener/Net/WebHeaderEncoding.cs b/SocketHttpListener/Net/WebHeaderEncoding.cs new file mode 100644 index 0000000000..64330c1b43 --- /dev/null +++ b/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; + } + } +} diff --git a/SocketHttpListener/SocketHttpListener.csproj b/SocketHttpListener/SocketHttpListener.csproj index fde6ed544e..9fb7c50619 100644 --- a/SocketHttpListener/SocketHttpListener.csproj +++ b/SocketHttpListener/SocketHttpListener.csproj @@ -21,6 +21,7 @@ DEBUG;TRACE prompt 4 + true pdbonly @@ -56,27 +57,32 @@ + + + + +