From f07af448fa11330db93dd7ddcabac37ef9e014c7 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Wed, 24 May 2017 15:12:55 -0400 Subject: update main projects --- SocketHttpListener/Net/HttpListenerRequest.cs | 654 ++++++++++++++++++++++++++ 1 file changed, 654 insertions(+) create mode 100644 SocketHttpListener/Net/HttpListenerRequest.cs (limited to 'SocketHttpListener/Net/HttpListenerRequest.cs') diff --git a/SocketHttpListener/Net/HttpListenerRequest.cs b/SocketHttpListener/Net/HttpListenerRequest.cs new file mode 100644 index 000000000..cfbd49203 --- /dev/null +++ b/SocketHttpListener/Net/HttpListenerRequest.cs @@ -0,0 +1,654 @@ +using System; +using System.Collections.Specialized; +using System.Globalization; +using System.IO; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Services; +using MediaBrowser.Model.Text; +using SocketHttpListener.Primitives; + +namespace SocketHttpListener.Net +{ + public sealed class HttpListenerRequest + { + string[] accept_types; + Encoding content_encoding; + long content_length; + bool cl_set; + CookieCollection cookies; + WebHeaderCollection headers; + string method; + Stream input_stream; + Version version; + QueryParamCollection query_string; // check if null is ok, check if read-only, check case-sensitiveness + string raw_url; + Uri url; + Uri referrer; + string[] user_languages; + HttpListenerContext context; + bool is_chunked; + bool ka_set; + bool keep_alive; + + private readonly ITextEncoding _textEncoding; + + internal HttpListenerRequest(HttpListenerContext context, ITextEncoding textEncoding) + { + this.context = context; + _textEncoding = textEncoding; + headers = new WebHeaderCollection(); + version = HttpVersion.Version10; + } + + static char[] separators = new char[] { ' ' }; + + internal void SetRequestLine(string req) + { + string[] parts = req.Split(separators, 3); + if (parts.Length != 3) + { + context.ErrorMessage = "Invalid request line (parts)."; + return; + } + + method = parts[0]; + foreach (char c in method) + { + int ic = (int)c; + + if ((ic >= 'A' && ic <= 'Z') || + (ic > 32 && c < 127 && c != '(' && c != ')' && c != '<' && + c != '<' && c != '>' && c != '@' && c != ',' && c != ';' && + c != ':' && c != '\\' && c != '"' && c != '/' && c != '[' && + c != ']' && c != '?' && c != '=' && c != '{' && c != '}')) + continue; + + context.ErrorMessage = "(Invalid verb)"; + return; + } + + raw_url = parts[1]; + if (parts[2].Length != 8 || !parts[2].StartsWith("HTTP/")) + { + context.ErrorMessage = "Invalid request line (version)."; + return; + } + + try + { + version = new Version(parts[2].Substring(5)); + if (version.Major < 1) + throw new Exception(); + } + catch + { + context.ErrorMessage = "Invalid request line (version)."; + return; + } + } + + void CreateQueryString(string query) + { + if (query == null || query.Length == 0) + { + query_string = new QueryParamCollection(); + return; + } + + query_string = new QueryParamCollection(); + if (query[0] == '?') + query = query.Substring(1); + string[] components = query.Split('&'); + foreach (string kv in components) + { + int pos = kv.IndexOf('='); + if (pos == -1) + { + query_string.Add(null, WebUtility.UrlDecode(kv)); + } + else + { + string key = WebUtility.UrlDecode(kv.Substring(0, pos)); + string val = WebUtility.UrlDecode(kv.Substring(pos + 1)); + + query_string.Add(key, val); + } + } + } + + internal void FinishInitialization() + { + string host = UserHostName; + if (version > HttpVersion.Version10 && (host == null || host.Length == 0)) + { + context.ErrorMessage = "Invalid host name"; + return; + } + + string path; + Uri raw_uri = null; + if (MaybeUri(raw_url.ToLowerInvariant()) && Uri.TryCreate(raw_url, UriKind.Absolute, out raw_uri)) + path = raw_uri.PathAndQuery; + else + path = raw_url; + + if ((host == null || host.Length == 0)) + host = UserHostAddress; + + if (raw_uri != null) + host = raw_uri.Host; + + int colon = host.LastIndexOf(':'); + if (colon >= 0) + host = host.Substring(0, colon); + + string base_uri = String.Format("{0}://{1}:{2}", + (IsSecureConnection) ? (IsWebSocketRequest ? "wss" : "https") : (IsWebSocketRequest ? "ws" : "http"), + host, LocalEndPoint.Port); + + if (!Uri.TryCreate(base_uri + path, UriKind.Absolute, out url)) + { + context.ErrorMessage = WebUtility.HtmlEncode("Invalid url: " + base_uri + path); + return; return; + } + + CreateQueryString(url.Query); + + if (version >= HttpVersion.Version11) + { + string t_encoding = Headers["Transfer-Encoding"]; + is_chunked = (t_encoding != null && String.Compare(t_encoding, "chunked", StringComparison.OrdinalIgnoreCase) == 0); + // 'identity' is not valid! + if (t_encoding != null && !is_chunked) + { + context.Connection.SendError(null, 501); + return; + } + } + + if (!is_chunked && !cl_set) + { + if (String.Compare(method, "POST", StringComparison.OrdinalIgnoreCase) == 0 || + String.Compare(method, "PUT", StringComparison.OrdinalIgnoreCase) == 0) + { + context.Connection.SendError(null, 411); + return; + } + } + + if (String.Compare(Headers["Expect"], "100-continue", StringComparison.OrdinalIgnoreCase) == 0) + { + var output = (ResponseStream)context.Connection.GetResponseStream(true); + + var _100continue = _textEncoding.GetASCIIEncoding().GetBytes("HTTP/1.1 100 Continue\r\n\r\n"); + + output.InternalWrite(_100continue, 0, _100continue.Length); + } + } + + static bool MaybeUri(string s) + { + int p = s.IndexOf(':'); + if (p == -1) + return false; + + if (p >= 10) + return false; + + return IsPredefinedScheme(s.Substring(0, p)); + } + + // + // Using a simple block of if's is twice as slow as the compiler generated + // switch statement. But using this tuned code is faster than the + // compiler generated code, with a million loops on x86-64: + // + // With "http": .10 vs .51 (first check) + // with "https": .16 vs .51 (second check) + // with "foo": .22 vs .31 (never found) + // with "mailto": .12 vs .51 (last check) + // + // + static bool IsPredefinedScheme(string scheme) + { + if (scheme == null || scheme.Length < 3) + return false; + + char c = scheme[0]; + if (c == 'h') + return (scheme == "http" || scheme == "https"); + if (c == 'f') + return (scheme == "file" || scheme == "ftp"); + + if (c == 'n') + { + c = scheme[1]; + if (c == 'e') + return (scheme == "news" || scheme == "net.pipe" || scheme == "net.tcp"); + if (scheme == "nntp") + return true; + return false; + } + if ((c == 'g' && scheme == "gopher") || (c == 'm' && scheme == "mailto")) + return true; + + return false; + } + + internal static string Unquote(String str) + { + int start = str.IndexOf('\"'); + int end = str.LastIndexOf('\"'); + if (start >= 0 && end >= 0) + str = str.Substring(start + 1, end - 1); + return str.Trim(); + } + + internal void AddHeader(string header) + { + int colon = header.IndexOf(':'); + if (colon == -1 || colon == 0) + { + context.ErrorMessage = "Bad Request"; + context.ErrorStatus = 400; + return; + } + + string name = header.Substring(0, colon).Trim(); + string val = header.Substring(colon + 1).Trim(); + string lower = name.ToLowerInvariant(); + headers.SetInternal(name, val); + switch (lower) + { + case "accept-language": + user_languages = val.Split(','); // yes, only split with a ',' + break; + case "accept": + accept_types = val.Split(','); // yes, only split with a ',' + break; + case "content-length": + try + { + //TODO: max. content_length? + content_length = Int64.Parse(val.Trim()); + if (content_length < 0) + context.ErrorMessage = "Invalid Content-Length."; + cl_set = true; + } + catch + { + context.ErrorMessage = "Invalid Content-Length."; + } + + break; + case "content-type": + { + var contents = val.Split(';'); + foreach (var content in contents) + { + var tmp = content.Trim(); + if (tmp.StartsWith("charset")) + { + var charset = tmp.GetValue("="); + if (charset != null && charset.Length > 0) + { + try + { + + // Support upnp/dlna devices - CONTENT-TYPE: text/xml ; charset="utf-8"\r\n + charset = charset.Trim('"'); + var index = charset.IndexOf('"'); + if (index != -1) charset = charset.Substring(0, index); + + content_encoding = Encoding.GetEncoding(charset); + } + catch + { + context.ErrorMessage = "Invalid Content-Type header: " + charset; + } + } + + break; + } + } + } + break; + case "referer": + try + { + referrer = new Uri(val); + } + catch + { + referrer = new Uri("http://someone.is.screwing.with.the.headers.com/"); + } + break; + case "cookie": + if (cookies == null) + cookies = new CookieCollection(); + + string[] cookieStrings = val.Split(new char[] { ',', ';' }); + Cookie current = null; + int version = 0; + foreach (string cookieString in cookieStrings) + { + string str = cookieString.Trim(); + if (str.Length == 0) + continue; + if (str.StartsWith("$Version")) + { + version = Int32.Parse(Unquote(str.Substring(str.IndexOf('=') + 1))); + } + else if (str.StartsWith("$Path")) + { + if (current != null) + current.Path = str.Substring(str.IndexOf('=') + 1).Trim(); + } + else if (str.StartsWith("$Domain")) + { + if (current != null) + current.Domain = str.Substring(str.IndexOf('=') + 1).Trim(); + } + else if (str.StartsWith("$Port")) + { + if (current != null) + current.Port = str.Substring(str.IndexOf('=') + 1).Trim(); + } + else + { + if (current != null) + { + cookies.Add(current); + } + current = new Cookie(); + int idx = str.IndexOf('='); + if (idx > 0) + { + current.Name = str.Substring(0, idx).Trim(); + current.Value = str.Substring(idx + 1).Trim(); + } + else + { + current.Name = str.Trim(); + current.Value = String.Empty; + } + current.Version = version; + } + } + if (current != null) + { + cookies.Add(current); + } + break; + } + } + + // returns true is the stream could be reused. + internal bool FlushInput() + { + if (!HasEntityBody) + return true; + + int length = 2048; + if (content_length > 0) + length = (int)Math.Min(content_length, (long)length); + + byte[] bytes = new byte[length]; + while (true) + { + // TODO: test if MS has a timeout when doing this + try + { + var task = InputStream.ReadAsync(bytes, 0, length); + var result = Task.WaitAll(new [] { task }, 1000); + if (!result) + { + return false; + } + if (task.Result <= 0) + { + return true; + } + } + catch (ObjectDisposedException e) + { + input_stream = null; + return true; + } + catch + { + return false; + } + } + } + + public string[] AcceptTypes + { + get { return accept_types; } + } + + public int ClientCertificateError + { + get + { + HttpConnection cnc = context.Connection; + //if (cnc.ClientCertificate == null) + // throw new InvalidOperationException("No client certificate"); + //int[] errors = cnc.ClientCertificateErrors; + //if (errors != null && errors.Length > 0) + // return errors[0]; + return 0; + } + } + + public Encoding ContentEncoding + { + get + { + if (content_encoding == null) + content_encoding = _textEncoding.GetDefaultEncoding(); + return content_encoding; + } + } + + public long ContentLength64 + { + get { return is_chunked ? -1 : content_length; } + } + + public string ContentType + { + get { return headers["content-type"]; } + } + + public CookieCollection Cookies + { + get + { + // TODO: check if the collection is read-only + if (cookies == null) + cookies = new CookieCollection(); + return cookies; + } + } + + public bool HasEntityBody + { + get { return (content_length > 0 || is_chunked); } + } + + public QueryParamCollection Headers + { + get { return headers; } + } + + public string HttpMethod + { + get { return method; } + } + + public Stream InputStream + { + get + { + if (input_stream == null) + { + if (is_chunked || content_length > 0) + input_stream = context.Connection.GetRequestStream(is_chunked, content_length); + else + input_stream = Stream.Null; + } + + return input_stream; + } + } + + public bool IsAuthenticated + { + get { return false; } + } + + public bool IsLocal + { + get { return RemoteEndPoint.IpAddress.Equals(IpAddressInfo.Loopback) || RemoteEndPoint.IpAddress.Equals(IpAddressInfo.IPv6Loopback) || LocalEndPoint.IpAddress.Equals(RemoteEndPoint.IpAddress); } + } + + public bool IsSecureConnection + { + get { return context.Connection.IsSecure; } + } + + public bool KeepAlive + { + get + { + if (ka_set) + return keep_alive; + + ka_set = true; + // 1. Connection header + // 2. Protocol (1.1 == keep-alive by default) + // 3. Keep-Alive header + string cnc = headers["Connection"]; + if (!String.IsNullOrEmpty(cnc)) + { + keep_alive = (0 == String.Compare(cnc, "keep-alive", StringComparison.OrdinalIgnoreCase)); + } + else if (version == HttpVersion.Version11) + { + keep_alive = true; + } + else + { + cnc = headers["keep-alive"]; + if (!String.IsNullOrEmpty(cnc)) + keep_alive = (0 != String.Compare(cnc, "closed", StringComparison.OrdinalIgnoreCase)); + } + return keep_alive; + } + } + + public IpEndPointInfo LocalEndPoint + { + get { return context.Connection.LocalEndPoint; } + } + + public Version ProtocolVersion + { + get { return version; } + } + + public QueryParamCollection QueryString + { + get { return query_string; } + } + + public string RawUrl + { + get { return raw_url; } + } + + public IpEndPointInfo RemoteEndPoint + { + get { return context.Connection.RemoteEndPoint; } + } + + public Guid RequestTraceIdentifier + { + get { return Guid.Empty; } + } + + public Uri Url + { + get { return url; } + } + + public Uri UrlReferrer + { + get { return referrer; } + } + + public string UserAgent + { + get { return headers["user-agent"]; } + } + + public string UserHostAddress + { + get { return LocalEndPoint.ToString(); } + } + + public string UserHostName + { + get { return headers["host"]; } + } + + public string[] UserLanguages + { + get { return user_languages; } + } + + public string ServiceName + { + get + { + return null; + } + } + + private bool _websocketRequestWasSet; + private bool _websocketRequest; + + /// + /// Gets a value indicating whether the request is a WebSocket connection request. + /// + /// + /// true if the request is a WebSocket connection request; otherwise, false. + /// + public bool IsWebSocketRequest + { + get + { + if (!_websocketRequestWasSet) + { + _websocketRequest = method == "GET" && + version > HttpVersion.Version10 && + headers.Contains("Upgrade", "websocket") && + headers.Contains("Connection", "Upgrade"); + + _websocketRequestWasSet = true; + } + + return _websocketRequest; + } + } + + public Task GetClientCertificateAsync() + { + return Task.FromResult(null); + } + } +} -- cgit v1.2.3 From 71f7fc4e116f32a20c63ddbb17844a81283a6ba4 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Wed, 24 May 2017 15:39:59 -0400 Subject: 3.2.17.12 --- .../SocketSharp/WebSocketSharpResponse.cs | 10 +- SharedVersion.cs | 2 +- SocketHttpListener/Net/HttpConnection.cs | 4 +- SocketHttpListener/Net/HttpListenerRequest.cs | 2 +- SocketHttpListener/Net/HttpListenerResponse.cs | 2 +- .../Net/HttpResponseStream.Managed.cs | 459 +++++++++++++++++++++ SocketHttpListener/Net/HttpResponseStream.cs | 139 +++++++ SocketHttpListener/Net/ResponseStream.cs | 400 ------------------ SocketHttpListener/SocketHttpListener.csproj | 3 +- 9 files changed, 608 insertions(+), 413 deletions(-) create mode 100644 SocketHttpListener/Net/HttpResponseStream.Managed.cs create mode 100644 SocketHttpListener/Net/HttpResponseStream.cs delete mode 100644 SocketHttpListener/Net/ResponseStream.cs (limited to 'SocketHttpListener/Net/HttpListenerRequest.cs') diff --git a/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs index 9e58ee57c..d6762d94b 100644 --- a/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs +++ b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs @@ -114,15 +114,9 @@ namespace Emby.Server.Implementations.HttpServer.SocketSharp var outputStream = response.OutputStream; // This is needed with compression - if (outputStream is ResponseStream) - { - //if (!string.IsNullOrWhiteSpace(GetHeader("Content-Encoding"))) - { - outputStream.Flush(); - } + outputStream.Flush(); + outputStream.Dispose(); - outputStream.Dispose(); - } response.Close(); } catch (Exception ex) diff --git a/SharedVersion.cs b/SharedVersion.cs index 1564e4a41..e2671c988 100644 --- a/SharedVersion.cs +++ b/SharedVersion.cs @@ -1,3 +1,3 @@ using System.Reflection; -[assembly: AssemblyVersion("3.2.17.11")] +[assembly: AssemblyVersion("3.2.17.12")] diff --git a/SocketHttpListener/Net/HttpConnection.cs b/SocketHttpListener/Net/HttpConnection.cs index 848b80f99..627b671bf 100644 --- a/SocketHttpListener/Net/HttpConnection.cs +++ b/SocketHttpListener/Net/HttpConnection.cs @@ -218,7 +218,9 @@ namespace SocketHttpListener.Net var supportsDirectSocketAccess = !context.Response.SendChunked && !isExpect100Continue && !secure; - o_stream = new ResponseStream(stream, context.Response, _memoryStreamFactory, _textEncoding, _fileSystem, sock, supportsDirectSocketAccess, _logger, _environment); + //o_stream = new ResponseStream(stream, context.Response, _memoryStreamFactory, _textEncoding, _fileSystem, sock, supportsDirectSocketAccess, _logger, _environment); + + o_stream = new HttpResponseStream(stream, context.Response, false, _memoryStreamFactory, sock, supportsDirectSocketAccess, _environment, _fileSystem); } return o_stream; } diff --git a/SocketHttpListener/Net/HttpListenerRequest.cs b/SocketHttpListener/Net/HttpListenerRequest.cs index cfbd49203..6a99eb078 100644 --- a/SocketHttpListener/Net/HttpListenerRequest.cs +++ b/SocketHttpListener/Net/HttpListenerRequest.cs @@ -181,7 +181,7 @@ namespace SocketHttpListener.Net if (String.Compare(Headers["Expect"], "100-continue", StringComparison.OrdinalIgnoreCase) == 0) { - var output = (ResponseStream)context.Connection.GetResponseStream(true); + var output = (HttpResponseStream)context.Connection.GetResponseStream(true); var _100continue = _textEncoding.GetASCIIEncoding().GetBytes("HTTP/1.1 100 Continue\r\n\r\n"); diff --git a/SocketHttpListener/Net/HttpListenerResponse.cs b/SocketHttpListener/Net/HttpListenerResponse.cs index 3cb6a0d75..185454ef6 100644 --- a/SocketHttpListener/Net/HttpListenerResponse.cs +++ b/SocketHttpListener/Net/HttpListenerResponse.cs @@ -519,7 +519,7 @@ namespace SocketHttpListener.Net public Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken) { - return ((ResponseStream)OutputStream).TransmitFile(path, offset, count, fileShareMode, cancellationToken); + return ((HttpResponseStream)OutputStream).TransmitFile(path, offset, count, fileShareMode, cancellationToken); } } } \ No newline at end of file diff --git a/SocketHttpListener/Net/HttpResponseStream.Managed.cs b/SocketHttpListener/Net/HttpResponseStream.Managed.cs new file mode 100644 index 000000000..0a9efccb2 --- /dev/null +++ b/SocketHttpListener/Net/HttpResponseStream.Managed.cs @@ -0,0 +1,459 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Runtime.ExceptionServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.System; + +namespace SocketHttpListener.Net +{ + // Licensed to the .NET Foundation under one or more agreements. + // See the LICENSE file in the project root for more information. + // + // System.Net.ResponseStream + // + // Author: + // Gonzalo Paniagua Javier (gonzalo@novell.com) + // + // Copyright (c) 2005 Novell, Inc. (http://www.novell.com) + // + // Permission is hereby granted, free of charge, to any person obtaining + // a copy of this software and associated documentation files (the + // "Software"), to deal in the Software without restriction, including + // without limitation the rights to use, copy, modify, merge, publish, + // distribute, sublicense, and/or sell copies of the Software, and to + // permit persons to whom the Software is furnished to do so, subject to + // the following conditions: + // + // The above copyright notice and this permission notice shall be + // included in all copies or substantial portions of the Software. + // + // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + // + + internal partial class HttpResponseStream : Stream + { + private HttpListenerResponse _response; + private bool _ignore_errors; + private bool _trailer_sent; + private Stream _stream; + private readonly IMemoryStreamFactory _memoryStreamFactory; + private readonly IAcceptSocket _socket; + private readonly bool _supportsDirectSocketAccess; + private readonly IEnvironmentInfo _environment; + private readonly IFileSystem _fileSystem; + internal HttpResponseStream(Stream stream, HttpListenerResponse response, bool ignore_errors, IMemoryStreamFactory memoryStreamFactory, IAcceptSocket socket, bool supportsDirectSocketAccess, IEnvironmentInfo environment, IFileSystem fileSystem) + { + _response = response; + _ignore_errors = ignore_errors; + _memoryStreamFactory = memoryStreamFactory; + _socket = socket; + _supportsDirectSocketAccess = supportsDirectSocketAccess; + _environment = environment; + _fileSystem = fileSystem; + _stream = stream; + } + + private void DisposeCore() + { + byte[] bytes = null; + MemoryStream ms = GetHeaders(true); + bool chunked = _response.SendChunked; + if (_stream.CanWrite) + { + try + { + if (ms != null) + { + long start = ms.Position; + if (chunked && !_trailer_sent) + { + bytes = GetChunkSizeBytes(0, true); + ms.Position = ms.Length; + ms.Write(bytes, 0, bytes.Length); + } + InternalWrite(ms.GetBuffer(), (int)start, (int)(ms.Length - start)); + _trailer_sent = true; + } + else if (chunked && !_trailer_sent) + { + bytes = GetChunkSizeBytes(0, true); + InternalWrite(bytes, 0, bytes.Length); + _trailer_sent = true; + } + } + catch (HttpListenerException) + { + // Ignore error due to connection reset by peer + } + } + _response.Close(); + } + + internal async Task WriteWebSocketHandshakeHeadersAsync() + { + if (_closed) + throw new ObjectDisposedException(GetType().ToString()); + + if (_stream.CanWrite) + { + MemoryStream ms = GetHeaders(closing: false, isWebSocketHandshake: true); + bool chunked = _response.SendChunked; + + long start = ms.Position; + if (chunked) + { + byte[] bytes = GetChunkSizeBytes(0, true); + ms.Position = ms.Length; + ms.Write(bytes, 0, bytes.Length); + } + + await InternalWriteAsync(ms.GetBuffer(), (int)start, (int)(ms.Length - start)).ConfigureAwait(false); + await _stream.FlushAsync().ConfigureAwait(false); + } + } + + private MemoryStream GetHeaders(bool closing, bool isWebSocketHandshake = false) + { + // SendHeaders works on shared headers + lock (_response.headers_lock) + { + if (_response.HeadersSent) + return null; + var ms = _memoryStreamFactory.CreateNew(); + _response.SendHeaders(closing, ms); + return ms; + } + + //lock (_response._headersLock) + //{ + // if (_response.SentHeaders) + // { + // return null; + // } + + // MemoryStream ms = new MemoryStream(); + // _response.SendHeaders(closing, ms, isWebSocketHandshake); + // return ms; + //} + } + + private static byte[] s_crlf = new byte[] { 13, 10 }; + private static byte[] GetChunkSizeBytes(int size, bool final) + { + string str = String.Format("{0:x}\r\n{1}", size, final ? "\r\n" : ""); + return Encoding.ASCII.GetBytes(str); + } + + internal void InternalWrite(byte[] buffer, int offset, int count) + { + if (_ignore_errors) + { + try + { + _stream.Write(buffer, offset, count); + } + catch { } + } + else + { + try + { + _stream.Write(buffer, offset, count); + } + catch (IOException ex) + { + throw new HttpListenerException(ex.HResult, ex.Message); + } + } + } + + internal Task InternalWriteAsync(byte[] buffer, int offset, int count) => + _ignore_errors ? InternalWriteIgnoreErrorsAsync(buffer, offset, count) : _stream.WriteAsync(buffer, offset, count); + + private async Task InternalWriteIgnoreErrorsAsync(byte[] buffer, int offset, int count) + { + try { await _stream.WriteAsync(buffer, offset, count).ConfigureAwait(false); } + catch { } + } + + private void WriteCore(byte[] buffer, int offset, int size) + { + if (size == 0) + return; + + byte[] bytes = null; + MemoryStream ms = GetHeaders(false); + bool chunked = _response.SendChunked; + if (ms != null) + { + long start = ms.Position; // After the possible preamble for the encoding + ms.Position = ms.Length; + if (chunked) + { + bytes = GetChunkSizeBytes(size, false); + ms.Write(bytes, 0, bytes.Length); + } + + int new_count = Math.Min(size, 16384 - (int)ms.Position + (int)start); + ms.Write(buffer, offset, new_count); + size -= new_count; + offset += new_count; + InternalWrite(ms.GetBuffer(), (int)start, (int)(ms.Length - start)); + ms.SetLength(0); + ms.Capacity = 0; // 'dispose' the buffer in ms. + } + else if (chunked) + { + bytes = GetChunkSizeBytes(size, false); + InternalWrite(bytes, 0, bytes.Length); + } + + if (size > 0) + InternalWrite(buffer, offset, size); + if (chunked) + InternalWrite(s_crlf, 0, 2); + } + + private IAsyncResult BeginWriteCore(byte[] buffer, int offset, int size, AsyncCallback cback, object state) + { + if (_closed) + { + HttpStreamAsyncResult ares = new HttpStreamAsyncResult(this); + ares._callback = cback; + ares._state = state; + ares.Complete(); + return ares; + } + + byte[] bytes = null; + MemoryStream ms = GetHeaders(false); + bool chunked = _response.SendChunked; + if (ms != null) + { + long start = ms.Position; + ms.Position = ms.Length; + if (chunked) + { + bytes = GetChunkSizeBytes(size, false); + ms.Write(bytes, 0, bytes.Length); + } + ms.Write(buffer, offset, size); + buffer = ms.GetBuffer(); + offset = (int)start; + size = (int)(ms.Position - start); + } + else if (chunked) + { + bytes = GetChunkSizeBytes(size, false); + InternalWrite(bytes, 0, bytes.Length); + } + + try + { + return _stream.BeginWrite(buffer, offset, size, cback, state); + } + catch (IOException ex) + { + if (_ignore_errors) + { + HttpStreamAsyncResult ares = new HttpStreamAsyncResult(this); + ares._callback = cback; + ares._state = state; + ares.Complete(); + return ares; + } + else + { + throw new HttpListenerException(ex.HResult, ex.Message); + } + } + } + + private void EndWriteCore(IAsyncResult asyncResult) + { + if (_closed) + return; + + if (_ignore_errors) + { + try + { + _stream.EndWrite(asyncResult); + if (_response.SendChunked) + _stream.Write(s_crlf, 0, 2); + } + catch { } + } + else + { + try + { + _stream.EndWrite(asyncResult); + if (_response.SendChunked) + _stream.Write(s_crlf, 0, 2); + } + catch (IOException ex) + { + // NetworkStream wraps exceptions in IOExceptions; if the underlying socket operation + // failed because of invalid arguments or usage, propagate that error. Otherwise + // wrap the whole thing in an HttpListenerException. This is all to match Windows behavior. + if (ex.InnerException is ArgumentException || ex.InnerException is InvalidOperationException) + { + throw ex.InnerException; + } + + throw new HttpListenerException(ex.HResult, ex.Message); + } + } + } + + private bool EnableSendFileWithSocket + { + get { return false; } + } + + public Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken) + { + if (_supportsDirectSocketAccess && offset == 0 && count == 0 && !_response.SendChunked && _response.ContentLength64 > 8192) + { + if (EnableSendFileWithSocket) + { + return TransmitFileOverSocket(path, offset, count, fileShareMode, cancellationToken); + } + } + return TransmitFileManaged(path, offset, count, fileShareMode, cancellationToken); + } + + private readonly byte[] _emptyBuffer = new byte[] { }; + private Task TransmitFileOverSocket(string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken) + { + var ms = GetHeaders(false); + + byte[] preBuffer; + if (ms != null) + { + using (var msCopy = new MemoryStream()) + { + ms.CopyTo(msCopy); + preBuffer = msCopy.ToArray(); + } + } + else + { + return TransmitFileManaged(path, offset, count, fileShareMode, cancellationToken); + } + + //_logger.Info("Socket sending file {0} {1}", path, response.ContentLength64); + return _socket.SendFile(path, preBuffer, _emptyBuffer, cancellationToken); + } + + const int StreamCopyToBufferSize = 81920; + private async Task TransmitFileManaged(string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken) + { + var allowAsync = _environment.OperatingSystem != MediaBrowser.Model.System.OperatingSystem.Windows; + + var fileOpenOptions = offset > 0 + ? FileOpenOptions.RandomAccess + : FileOpenOptions.SequentialScan; + + if (allowAsync) + { + fileOpenOptions |= FileOpenOptions.Asynchronous; + } + + // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039 + + using (var fs = _fileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, fileShareMode, fileOpenOptions)) + { + if (offset > 0) + { + fs.Position = offset; + } + + var targetStream = this; + + if (count > 0) + { + if (allowAsync) + { + await CopyToInternalAsync(fs, targetStream, count, cancellationToken).ConfigureAwait(false); + } + else + { + await CopyToInternalAsyncWithSyncRead(fs, targetStream, count, cancellationToken).ConfigureAwait(false); + } + } + else + { + if (allowAsync) + { + await fs.CopyToAsync(targetStream, StreamCopyToBufferSize, cancellationToken).ConfigureAwait(false); + } + else + { + fs.CopyTo(targetStream, StreamCopyToBufferSize); + } + } + } + } + + private static async Task CopyToInternalAsyncWithSyncRead(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken) + { + var array = new byte[StreamCopyToBufferSize]; + int bytesRead; + + while ((bytesRead = source.Read(array, 0, array.Length)) != 0) + { + var bytesToWrite = Math.Min(bytesRead, copyLength); + + if (bytesToWrite > 0) + { + await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false); + } + + copyLength -= bytesToWrite; + + if (copyLength <= 0) + { + break; + } + } + } + + private static async Task CopyToInternalAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken) + { + var array = new byte[StreamCopyToBufferSize]; + int bytesRead; + + while ((bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false)) != 0) + { + var bytesToWrite = Math.Min(bytesRead, copyLength); + + if (bytesToWrite > 0) + { + await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false); + } + + copyLength -= bytesToWrite; + + if (copyLength <= 0) + { + break; + } + } + } + } +} diff --git a/SocketHttpListener/Net/HttpResponseStream.cs b/SocketHttpListener/Net/HttpResponseStream.cs new file mode 100644 index 000000000..f7140be66 --- /dev/null +++ b/SocketHttpListener/Net/HttpResponseStream.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace SocketHttpListener.Net +{ + internal sealed partial class HttpResponseStream : Stream + { + private bool _closed; + internal bool Closed => _closed; + + public override bool CanRead => false; + public override bool CanSeek => false; + public override bool CanWrite => true; + + public override void Flush() { } + public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public override long Length + { + get + { + throw new NotImplementedException(); + } + } + + public override long Position + { + get + { + throw new NotImplementedException(); + } + + set + { + throw new NotImplementedException(); + } + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotImplementedException(); + } + + public override void SetLength(long value) + { + throw new NotImplementedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } + + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + return base.BeginRead(buffer, offset, count, callback, state); + } + + public override int EndRead(IAsyncResult asyncResult) + { + return base.EndRead(asyncResult); + } + + public override void Write(byte[] buffer, int offset, int size) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0 || offset > buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (size < 0 || size > buffer.Length - offset) + { + throw new ArgumentOutOfRangeException(nameof(size)); + } + if (_closed) + { + return; + } + + WriteCore(buffer, offset, size); + } + + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int size, AsyncCallback callback, object state) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0 || offset > buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (size < 0 || size > buffer.Length - offset) + { + throw new ArgumentOutOfRangeException(nameof(size)); + } + + return BeginWriteCore(buffer, offset, size, callback, state); + } + + public override void EndWrite(IAsyncResult asyncResult) + { + if (asyncResult == null) + { + throw new ArgumentNullException(nameof(asyncResult)); + } + + EndWriteCore(asyncResult); + } + + protected override void Dispose(bool disposing) + { + try + { + if (disposing) + { + if (_closed) + { + return; + } + _closed = true; + DisposeCore(); + } + } + finally + { + base.Dispose(disposing); + } + } + } +} diff --git a/SocketHttpListener/Net/ResponseStream.cs b/SocketHttpListener/Net/ResponseStream.cs deleted file mode 100644 index 5949e3817..000000000 --- a/SocketHttpListener/Net/ResponseStream.cs +++ /dev/null @@ -1,400 +0,0 @@ -using System; -using System.IO; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Net; -using MediaBrowser.Model.System; -using MediaBrowser.Model.Text; -using SocketHttpListener.Primitives; - -namespace SocketHttpListener.Net -{ - // FIXME: Does this buffer the response until Close? - // Update: we send a single packet for the first non-chunked Write - // What happens when we set content-length to X and write X-1 bytes then close? - // what if we don't set content-length at all? - public class ResponseStream : Stream - { - HttpListenerResponse response; - bool disposed; - bool trailer_sent; - Stream stream; - private readonly IMemoryStreamFactory _memoryStreamFactory; - private readonly ITextEncoding _textEncoding; - private readonly IFileSystem _fileSystem; - private readonly IAcceptSocket _socket; - private readonly bool _supportsDirectSocketAccess; - private readonly ILogger _logger; - private readonly IEnvironmentInfo _environment; - - internal ResponseStream(Stream stream, HttpListenerResponse response, IMemoryStreamFactory memoryStreamFactory, ITextEncoding textEncoding, IFileSystem fileSystem, IAcceptSocket socket, bool supportsDirectSocketAccess, ILogger logger, IEnvironmentInfo environment) - { - this.response = response; - _memoryStreamFactory = memoryStreamFactory; - _textEncoding = textEncoding; - _fileSystem = fileSystem; - _socket = socket; - _supportsDirectSocketAccess = supportsDirectSocketAccess; - _logger = logger; - _environment = environment; - this.stream = stream; - } - - public override bool CanRead - { - get { return false; } - } - - public override bool CanSeek - { - get { return false; } - } - - public override bool CanWrite - { - get { return true; } - } - - public override long Length - { - get { throw new NotSupportedException(); } - } - - public override long Position - { - get { throw new NotSupportedException(); } - set { throw new NotSupportedException(); } - } - - - protected override void Dispose(bool disposing) - { - if (disposed == false) - { - disposed = true; - using (var ms = GetHeaders(response, _memoryStreamFactory, false)) - { - if (stream.CanWrite) - { - try - { - bool chunked = response.SendChunked; - - if (ms != null) - { - var start = ms.Position; - if (chunked && !trailer_sent) - { - trailer_sent = true; - var bytes = GetChunkSizeBytes(0, true); - ms.Position = ms.Length; - ms.Write(bytes, 0, bytes.Length); - ms.Position = start; - } - - ms.CopyTo(stream); - } - else if (chunked && !trailer_sent) - { - trailer_sent = true; - - var bytes = GetChunkSizeBytes(0, true); - stream.Write(bytes, 0, bytes.Length); - } - } - catch (IOException ex) - { - // Ignore error due to connection reset by peer - } - } - response.Close(); - } - } - - base.Dispose(disposing); - } - - internal static MemoryStream GetHeaders(HttpListenerResponse response, IMemoryStreamFactory memoryStreamFactory, bool closing) - { - // SendHeaders works on shared headers - lock (response.headers_lock) - { - if (response.HeadersSent) - return null; - var ms = memoryStreamFactory.CreateNew(); - response.SendHeaders(closing, ms); - return ms; - } - } - - public override void Flush() - { - } - - static byte[] crlf = new byte[] { 13, 10 }; - byte[] GetChunkSizeBytes(int size, bool final) - { - string str = String.Format("{0:x}\r\n{1}", size, final ? "\r\n" : ""); - return _textEncoding.GetASCIIEncoding().GetBytes(str); - } - - internal void InternalWrite(byte[] buffer, int offset, int count) - { - stream.Write(buffer, offset, count); - } - - const int MsCopyBufferSize = 81920; - const int StreamCopyToBufferSize = 81920; - public override void Write(byte[] buffer, int offset, int count) - { - if (disposed) - throw new ObjectDisposedException(GetType().ToString()); - - if (count == 0) - { - return; - } - - using (var ms = GetHeaders(response, _memoryStreamFactory, false)) - { - bool chunked = response.SendChunked; - if (ms != null) - { - long start = ms.Position; // After the possible preamble for the encoding - ms.Position = ms.Length; - if (chunked) - { - var bytes = GetChunkSizeBytes(count, false); - ms.Write(bytes, 0, bytes.Length); - } - - ms.Write(buffer, offset, count); - - if (chunked) - { - ms.Write(crlf, 0, 2); - } - - ms.Position = start; - ms.CopyTo(stream, MsCopyBufferSize); - - return; - } - - if (chunked) - { - var bytes = GetChunkSizeBytes(count, false); - stream.Write(bytes, 0, bytes.Length); - } - - stream.Write(buffer, offset, count); - - if (chunked) - stream.Write(crlf, 0, 2); - } - } - - public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - if (disposed) - throw new ObjectDisposedException(GetType().ToString()); - - if (count == 0) - { - return; - } - - using (var ms = GetHeaders(response, _memoryStreamFactory, false)) - { - bool chunked = response.SendChunked; - if (ms != null) - { - long start = ms.Position; // After the possible preamble for the encoding - ms.Position = ms.Length; - if (chunked) - { - var bytes = GetChunkSizeBytes(count, false); - ms.Write(bytes, 0, bytes.Length); - } - - ms.Write(buffer, offset, count); - - if (chunked) - { - ms.Write(crlf, 0, 2); - } - - ms.Position = start; - await ms.CopyToAsync(stream, MsCopyBufferSize, cancellationToken).ConfigureAwait(false); - - return; - } - - if (chunked) - { - var bytes = GetChunkSizeBytes(count, false); - stream.Write(bytes, 0, bytes.Length); - } - - await stream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); - - if (chunked) - stream.Write(crlf, 0, 2); - } - } - - public override int Read([In, Out] byte[] buffer, int offset, int count) - { - throw new NotSupportedException(); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotSupportedException(); - } - - public override void SetLength(long value) - { - throw new NotSupportedException(); - } - - private bool EnableSendFileWithSocket - { - get { return false; } - } - - public Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken) - { - if (_supportsDirectSocketAccess && offset == 0 && count == 0 && !response.SendChunked && response.ContentLength64 > 8192) - { - if (EnableSendFileWithSocket) - { - return TransmitFileOverSocket(path, offset, count, fileShareMode, cancellationToken); - } - } - return TransmitFileManaged(path, offset, count, fileShareMode, cancellationToken); - } - - private readonly byte[] _emptyBuffer = new byte[] { }; - private Task TransmitFileOverSocket(string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken) - { - var ms = GetHeaders(response, _memoryStreamFactory, false); - - byte[] preBuffer; - if (ms != null) - { - using (var msCopy = new MemoryStream()) - { - ms.CopyTo(msCopy); - preBuffer = msCopy.ToArray(); - } - } - else - { - return TransmitFileManaged(path, offset, count, fileShareMode, cancellationToken); - } - - _logger.Info("Socket sending file {0} {1}", path, response.ContentLength64); - return _socket.SendFile(path, preBuffer, _emptyBuffer, cancellationToken); - } - - private async Task TransmitFileManaged(string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken) - { - var allowAsync = _environment.OperatingSystem != MediaBrowser.Model.System.OperatingSystem.Windows; - - var fileOpenOptions = offset > 0 - ? FileOpenOptions.RandomAccess - : FileOpenOptions.SequentialScan; - - if (allowAsync) - { - fileOpenOptions |= FileOpenOptions.Asynchronous; - } - - // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039 - - using (var fs = _fileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, fileShareMode, fileOpenOptions)) - { - if (offset > 0) - { - fs.Position = offset; - } - - var targetStream = this; - - if (count > 0) - { - if (allowAsync) - { - await CopyToInternalAsync(fs, targetStream, count, cancellationToken).ConfigureAwait(false); - } - else - { - await CopyToInternalAsyncWithSyncRead(fs, targetStream, count, cancellationToken).ConfigureAwait(false); - } - } - else - { - if (allowAsync) - { - await fs.CopyToAsync(targetStream, StreamCopyToBufferSize, cancellationToken).ConfigureAwait(false); - } - else - { - fs.CopyTo(targetStream, StreamCopyToBufferSize); - } - } - } - } - - private static async Task CopyToInternalAsyncWithSyncRead(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken) - { - var array = new byte[StreamCopyToBufferSize]; - int bytesRead; - - while ((bytesRead = source.Read(array, 0, array.Length)) != 0) - { - var bytesToWrite = Math.Min(bytesRead, copyLength); - - if (bytesToWrite > 0) - { - await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false); - } - - copyLength -= bytesToWrite; - - if (copyLength <= 0) - { - break; - } - } - } - - private static async Task CopyToInternalAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken) - { - var array = new byte[StreamCopyToBufferSize]; - int bytesRead; - - while ((bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false)) != 0) - { - var bytesToWrite = Math.Min(bytesRead, copyLength); - - if (bytesToWrite > 0) - { - await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false); - } - - copyLength -= bytesToWrite; - - if (copyLength <= 0) - { - break; - } - } - } - } -} diff --git a/SocketHttpListener/SocketHttpListener.csproj b/SocketHttpListener/SocketHttpListener.csproj index dd2d2cf0f..3be51071c 100644 --- a/SocketHttpListener/SocketHttpListener.csproj +++ b/SocketHttpListener/SocketHttpListener.csproj @@ -70,11 +70,12 @@ + + - -- cgit v1.2.3