update series resolver

pull/1154/head
Luke Pulverenti 7 years ago
parent b615a2aeb1
commit ccb5b14d77

@ -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)

@ -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" />

@ -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";
}

@ -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());

@ -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);

@ -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);

@ -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);
}
}
}

@ -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);
}
}
}

@ -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[] { };

@ -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"))

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

@ -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,
}
}

@ -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
}
}

@ -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

@ -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

@ -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);
}
}
}

@ -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);
}
}
}
}

@ -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;
}

@ -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 };

@ -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;
}
}
}

@ -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;
}
}
}

@ -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" />

Loading…
Cancel
Save