aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuke Pulverenti <luke.pulverenti@gmail.com>2017-06-15 13:22:05 -0400
committerLuke Pulverenti <luke.pulverenti@gmail.com>2017-06-15 13:22:05 -0400
commitccb5b14d772c2d1028fa9003ee64fde3cd4196b3 (patch)
tree294a56f3d34ed3e921257ca124f7d2fa28f79d2e
parentb615a2aeb16a527fe56a88a352574d76e77783e8 (diff)
update series resolver
-rw-r--r--Emby.Server.Core/ApplicationHost.cs2
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj2
-rw-r--r--Emby.Server.Implementations/HttpServer/ResponseFilter.cs2
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs61
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs8
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs11
-rw-r--r--Emby.Server.Implementations/Migrations/GuideMigration.cs49
-rw-r--r--Emby.Server.Implementations/Migrations/LibraryScanMigration.cs49
-rw-r--r--MediaBrowser.Model/Configuration/ServerConfiguration.cs2
-rw-r--r--MediaBrowser.Model/Net/MimeTypes.cs16
-rw-r--r--SharedVersion.cs2
-rw-r--r--SocketHttpListener/Net/BoundaryType.cs17
-rw-r--r--SocketHttpListener/Net/EntitySendFormat.cs14
-rw-r--r--SocketHttpListener/Net/HttpConnection.cs8
-rw-r--r--SocketHttpListener/Net/HttpListenerContext.cs2
-rw-r--r--SocketHttpListener/Net/HttpListenerResponse.Managed.cs329
-rw-r--r--SocketHttpListener/Net/HttpListenerResponse.cs652
-rw-r--r--SocketHttpListener/Net/HttpRequestStream.Managed.cs32
-rw-r--r--SocketHttpListener/Net/HttpResponseStream.Managed.cs33
-rw-r--r--SocketHttpListener/Net/HttpStatusDescription.cs75
-rw-r--r--SocketHttpListener/Net/WebHeaderEncoding.cs131
-rw-r--r--SocketHttpListener/SocketHttpListener.csproj6
22 files changed, 883 insertions, 620 deletions
diff --git a/Emby.Server.Core/ApplicationHost.cs b/Emby.Server.Core/ApplicationHost.cs
index 5e9024e1d..7019fe0de 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<IVersionMigration>
{
- 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 4482bb13b..210448cd9 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -182,8 +182,6 @@
<Compile Include="Logging\UnhandledExceptionWriter.cs" />
<Compile Include="MediaEncoder\EncodingManager.cs" />
<Compile Include="Migrations\IVersionMigration.cs" />
- <Compile Include="Migrations\LibraryScanMigration.cs" />
- <Compile Include="Migrations\GuideMigration.cs" />
<Compile Include="News\NewsEntryPoint.cs" />
<Compile Include="News\NewsService.cs" />
<Compile Include="Notifications\CoreNotificationTypes.cs" />
diff --git a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs
index 57eef5db0..ac36f8f51 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 a08c74474..15efd3d39 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");
-
- //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;
_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 2e3d81474..e2f2946db 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 60260e98a..e1c18c913 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 78fb6c222..000000000
--- 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 9d7f67a4f..000000000
--- 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 4529de59a..a570f7b10 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 790f8a8f6..3c3c2bbc7 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 f36251773..adeff3508 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 000000000..c3ac00c0f
--- /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 000000000..6e585bdc9
--- /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 9c87ff076..79491d6f9 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("<h1>{0} ({1})</h1>", description, msg);
else
str = string.Format("<h1>{0}</h1>", 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 58d769f22..1bf39589d 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 000000000..52576fdf2
--- /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 da7aff081..240c67930 100644
--- a/SocketHttpListener/Net/HttpListenerResponse.cs
+++ b/SocketHttpListener/Net/HttpListenerResponse.cs
@@ -1,572 +1,302 @@
-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;
-
- 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
{
- get { return content_type; }
+ get => Headers["Content-Type"];
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
{
- 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
{
- 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
{
- 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
{
- 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);
}
}
-} \ No newline at end of file
+}
diff --git a/SocketHttpListener/Net/HttpRequestStream.Managed.cs b/SocketHttpListener/Net/HttpRequestStream.Managed.cs
index cb02a4d5a..2b5dfc838 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 42db03e47..116c3280a 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;
+ //}
+
// SendHeaders works on shared headers
- lock (_response.headers_lock)
+ lock (_response._headersLock)
{
- if (_response.HeadersSent)
+ if (_response.SentHeaders)
+ {
return null;
- var ms = _memoryStreamFactory.CreateNew();
- _response.SendHeaders(closing, ms);
+ }
+
+ MemoryStream ms = new MemoryStream();
+ _response.SendHeaders(closing, ms, isWebSocketHandshake);
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 };
diff --git a/SocketHttpListener/Net/HttpStatusDescription.cs b/SocketHttpListener/Net/HttpStatusDescription.cs
new file mode 100644
index 000000000..8d490c511
--- /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 000000000..64330c1b4
--- /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 fde6ed544..9fb7c5061 100644
--- a/SocketHttpListener/SocketHttpListener.csproj
+++ b/SocketHttpListener/SocketHttpListener.csproj
@@ -21,6 +21,7 @@
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
@@ -56,27 +57,32 @@
<Compile Include="Mask.cs" />
<Compile Include="MessageEventArgs.cs" />
<Compile Include="Net\AuthenticationSchemeSelector.cs" />
+ <Compile Include="Net\BoundaryType.cs" />
<Compile Include="Net\ChunkedInputStream.cs" />
<Compile Include="Net\ChunkStream.cs" />
<Compile Include="Net\CookieHelper.cs" />
<Compile Include="Net\EndPointListener.cs" />
<Compile Include="Net\EndPointManager.cs" />
+ <Compile Include="Net\EntitySendFormat.cs" />
<Compile Include="Net\HttpConnection.cs" />
<Compile Include="Net\HttpListener.cs" />
<Compile Include="Net\HttpListenerBasicIdentity.cs" />
<Compile Include="Net\HttpListenerContext.cs" />
<Compile Include="Net\HttpListenerPrefixCollection.cs" />
<Compile Include="Net\HttpListenerRequest.cs" />
+ <Compile Include="Net\HttpListenerResponse.Managed.cs" />
<Compile Include="Net\HttpListenerResponse.cs" />
<Compile Include="Net\HttpRequestStream.cs" />
<Compile Include="Net\HttpRequestStream.Managed.cs" />
<Compile Include="Net\HttpResponseStream.cs" />
<Compile Include="Net\HttpResponseStream.Managed.cs" />
<Compile Include="Net\HttpStatusCode.cs" />
+ <Compile Include="Net\HttpStatusDescription.cs" />
<Compile Include="Net\HttpStreamAsyncResult.cs" />
<Compile Include="Net\HttpVersion.cs" />
<Compile Include="Net\ListenerPrefix.cs" />
<Compile Include="Net\WebHeaderCollection.cs" />
+ <Compile Include="Net\WebHeaderEncoding.cs" />
<Compile Include="Net\WebSockets\HttpListenerWebSocketContext.cs" />
<Compile Include="Net\WebSockets\WebSocketContext.cs" />
<Compile Include="Opcode.cs" />