aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations/SocketSharp
diff options
context:
space:
mode:
Diffstat (limited to 'Emby.Server.Implementations/SocketSharp')
-rw-r--r--Emby.Server.Implementations/SocketSharp/HttpFile.cs18
-rw-r--r--Emby.Server.Implementations/SocketSharp/HttpPostedFile.cs204
-rw-r--r--Emby.Server.Implementations/SocketSharp/RequestMono.cs659
-rw-r--r--Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs105
-rw-r--r--Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs136
-rw-r--r--Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs518
-rw-r--r--Emby.Server.Implementations/SocketSharp/WebSocketSharpResponse.cs98
7 files changed, 1738 insertions, 0 deletions
diff --git a/Emby.Server.Implementations/SocketSharp/HttpFile.cs b/Emby.Server.Implementations/SocketSharp/HttpFile.cs
new file mode 100644
index 000000000..120ac50d9
--- /dev/null
+++ b/Emby.Server.Implementations/SocketSharp/HttpFile.cs
@@ -0,0 +1,18 @@
+using System.IO;
+using MediaBrowser.Model.Services;
+
+namespace Emby.Server.Implementations.SocketSharp
+{
+ public class HttpFile : IHttpFile
+ {
+ public string Name { get; set; }
+
+ public string FileName { get; set; }
+
+ public long ContentLength { get; set; }
+
+ public string ContentType { get; set; }
+
+ public Stream InputStream { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/SocketSharp/HttpPostedFile.cs b/Emby.Server.Implementations/SocketSharp/HttpPostedFile.cs
new file mode 100644
index 000000000..f38ed848e
--- /dev/null
+++ b/Emby.Server.Implementations/SocketSharp/HttpPostedFile.cs
@@ -0,0 +1,204 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Net;
+using System.Text;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Services;
+
+public sealed class HttpPostedFile : IDisposable
+{
+ private string _name;
+ private string _contentType;
+ private Stream _stream;
+ private bool _disposed = false;
+
+ internal HttpPostedFile(string name, string content_type, Stream base_stream, long offset, long length)
+ {
+ _name = name;
+ _contentType = content_type;
+ _stream = new ReadSubStream(base_stream, offset, length);
+ }
+
+ public string ContentType => _contentType;
+
+ public int ContentLength => (int)_stream.Length;
+
+ public string FileName => _name;
+
+ public Stream InputStream => _stream;
+
+ /// <summary>
+ /// Releases the unmanaged resources and disposes of the managed resources used.
+ /// </summary>
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _stream.Dispose();
+ _stream = null;
+
+ _name = null;
+ _contentType = null;
+
+ _disposed = true;
+ }
+
+ private class ReadSubStream : Stream
+ {
+ private Stream _stream;
+ private long _offset;
+ private long _end;
+ private long _position;
+
+ public ReadSubStream(Stream s, long offset, long length)
+ {
+ _stream = s;
+ _offset = offset;
+ _end = offset + length;
+ _position = offset;
+ }
+
+ public override void Flush()
+ {
+ }
+
+ public override int Read(byte[] buffer, int dest_offset, int count)
+ {
+ if (buffer == null)
+ {
+ throw new ArgumentNullException(nameof(buffer));
+ }
+
+ if (dest_offset < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(dest_offset), "< 0");
+ }
+
+ if (count < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(count), "< 0");
+ }
+
+ int len = buffer.Length;
+ if (dest_offset > len)
+ {
+ throw new ArgumentException("destination offset is beyond array size", nameof(dest_offset));
+ }
+
+ // reordered to avoid possible integer overflow
+ if (dest_offset > len - count)
+ {
+ throw new ArgumentException("Reading would overrun buffer", nameof(count));
+ }
+
+ if (count > _end - _position)
+ {
+ count = (int)(_end - _position);
+ }
+
+ if (count <= 0)
+ {
+ return 0;
+ }
+
+ _stream.Position = _position;
+ int result = _stream.Read(buffer, dest_offset, count);
+ if (result > 0)
+ {
+ _position += result;
+ }
+ else
+ {
+ _position = _end;
+ }
+
+ return result;
+ }
+
+ public override int ReadByte()
+ {
+ if (_position >= _end)
+ {
+ return -1;
+ }
+
+ _stream.Position = _position;
+ int result = _stream.ReadByte();
+ if (result < 0)
+ {
+ _position = _end;
+ }
+ else
+ {
+ _position++;
+ }
+
+ return result;
+ }
+
+ public override long Seek(long d, SeekOrigin origin)
+ {
+ long real;
+ switch (origin)
+ {
+ case SeekOrigin.Begin:
+ real = _offset + d;
+ break;
+ case SeekOrigin.End:
+ real = _end + d;
+ break;
+ case SeekOrigin.Current:
+ real = _position + d;
+ break;
+ default:
+ throw new ArgumentException("Unknown SeekOrigin value", nameof(origin));
+ }
+
+ long virt = real - _offset;
+ if (virt < 0 || virt > Length)
+ {
+ throw new ArgumentException("Invalid position", nameof(d));
+ }
+
+ _position = _stream.Seek(real, SeekOrigin.Begin);
+ return _position;
+ }
+
+ public override void SetLength(long value)
+ {
+ throw new NotSupportedException();
+ }
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ throw new NotSupportedException();
+ }
+
+ public override bool CanRead => true;
+
+ public override bool CanSeek => true;
+
+ public override bool CanWrite => false;
+
+ public override long Length => _end - _offset;
+
+ public override long Position
+ {
+ get => _position - _offset;
+ set
+ {
+ if (value > Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value));
+ }
+
+ _position = Seek(value, SeekOrigin.Begin);
+ }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/SocketSharp/RequestMono.cs b/Emby.Server.Implementations/SocketSharp/RequestMono.cs
new file mode 100644
index 000000000..373f6d758
--- /dev/null
+++ b/Emby.Server.Implementations/SocketSharp/RequestMono.cs
@@ -0,0 +1,659 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Net;
+using System.Text;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Services;
+using Microsoft.Extensions.Primitives;
+using Microsoft.Net.Http.Headers;
+
+namespace Emby.Server.Implementations.SocketSharp
+{
+ public partial class WebSocketSharpRequest : IHttpRequest
+ {
+ internal static string GetParameter(ReadOnlySpan<char> header, string attr)
+ {
+ int ap = header.IndexOf(attr.AsSpan(), StringComparison.Ordinal);
+ if (ap == -1)
+ {
+ return null;
+ }
+
+ ap += attr.Length;
+ if (ap >= header.Length)
+ {
+ return null;
+ }
+
+ char ending = header[ap];
+ if (ending != '"')
+ {
+ ending = ' ';
+ }
+
+ var slice = header.Slice(ap + 1);
+ int end = slice.IndexOf(ending);
+ if (end == -1)
+ {
+ return ending == '"' ? null : header.Slice(ap).ToString();
+ }
+
+ return slice.Slice(0, end - ap - 1).ToString();
+ }
+
+ private async Task LoadMultiPart(WebROCollection form)
+ {
+ string boundary = GetParameter(ContentType.AsSpan(), "; boundary=");
+ if (boundary == null)
+ {
+ return;
+ }
+
+ using (var requestStream = InputStream)
+ {
+ // DB: 30/01/11 - Hack to get around non-seekable stream and received HTTP request
+ // Not ending with \r\n?
+ var ms = new MemoryStream(32 * 1024);
+ await requestStream.CopyToAsync(ms).ConfigureAwait(false);
+
+ var input = ms;
+ ms.WriteByte((byte)'\r');
+ ms.WriteByte((byte)'\n');
+
+ input.Position = 0;
+
+ // Uncomment to debug
+ // var content = new StreamReader(ms).ReadToEnd();
+ // Console.WriteLine(boundary + "::" + content);
+ // input.Position = 0;
+
+ var multi_part = new HttpMultipart(input, boundary, ContentEncoding);
+
+ HttpMultipart.Element e;
+ while ((e = multi_part.ReadNextElement()) != null)
+ {
+ if (e.Filename == null)
+ {
+ byte[] copy = new byte[e.Length];
+
+ input.Position = e.Start;
+ await input.ReadAsync(copy, 0, (int)e.Length).ConfigureAwait(false);
+
+ form.Add(e.Name, (e.Encoding ?? ContentEncoding).GetString(copy, 0, copy.Length));
+ }
+ else
+ {
+ // We use a substream, as in 2.x we will support large uploads streamed to disk,
+ var sub = new HttpPostedFile(e.Filename, e.ContentType, input, e.Start, e.Length);
+ files[e.Name] = sub;
+ }
+ }
+ }
+ }
+
+ public async Task<QueryParamCollection> GetFormData()
+ {
+ var form = new WebROCollection();
+ files = new Dictionary<string, HttpPostedFile>();
+
+ if (IsContentType("multipart/form-data"))
+ {
+ await LoadMultiPart(form).ConfigureAwait(false);
+ }
+ else if (IsContentType("application/x-www-form-urlencoded"))
+ {
+ await LoadWwwForm(form).ConfigureAwait(false);
+ }
+
+ if (validate_form && !checked_form)
+ {
+ checked_form = true;
+ ValidateNameValueCollection("Form", form);
+ }
+
+ return form;
+ }
+
+ public string Accept => StringValues.IsNullOrEmpty(request.Headers[HeaderNames.Accept]) ? null : request.Headers[HeaderNames.Accept].ToString();
+
+ public string Authorization => StringValues.IsNullOrEmpty(request.Headers[HeaderNames.Authorization]) ? null : request.Headers[HeaderNames.Authorization].ToString();
+
+ protected bool validate_form { get; set; }
+ protected bool checked_form { get; set; }
+
+ private static void ThrowValidationException(string name, string key, string value)
+ {
+ string v = "\"" + value + "\"";
+ if (v.Length > 20)
+ {
+ v = v.Substring(0, 16) + "...\"";
+ }
+
+ string msg = string.Format(
+ CultureInfo.InvariantCulture,
+ "A potentially dangerous Request.{0} value was detected from the client ({1}={2}).",
+ name,
+ key,
+ v);
+
+ throw new Exception(msg);
+ }
+
+ private static void ValidateNameValueCollection(string name, QueryParamCollection coll)
+ {
+ if (coll == null)
+ {
+ return;
+ }
+
+ foreach (var pair in coll)
+ {
+ var key = pair.Name;
+ var val = pair.Value;
+ if (val != null && val.Length > 0 && IsInvalidString(val))
+ {
+ ThrowValidationException(name, key, val);
+ }
+ }
+ }
+
+ internal static bool IsInvalidString(string val)
+ => IsInvalidString(val, out var validationFailureIndex);
+
+ internal static bool IsInvalidString(string val, out int validationFailureIndex)
+ {
+ validationFailureIndex = 0;
+
+ int len = val.Length;
+ if (len < 2)
+ {
+ return false;
+ }
+
+ char current = val[0];
+ for (int idx = 1; idx < len; idx++)
+ {
+ char next = val[idx];
+
+ // See http://secunia.com/advisories/14325
+ if (current == '<' || current == '\xff1c')
+ {
+ if (next == '!' || next < ' '
+ || (next >= 'a' && next <= 'z')
+ || (next >= 'A' && next <= 'Z'))
+ {
+ validationFailureIndex = idx - 1;
+ return true;
+ }
+ }
+ else if (current == '&' && next == '#')
+ {
+ validationFailureIndex = idx - 1;
+ return true;
+ }
+
+ current = next;
+ }
+
+ return false;
+ }
+
+ private bool IsContentType(string ct)
+ {
+ if (ContentType == null)
+ {
+ return false;
+ }
+
+ return ContentType.StartsWith(ct, StringComparison.OrdinalIgnoreCase);
+ }
+
+ private async Task LoadWwwForm(WebROCollection form)
+ {
+ using (var input = InputStream)
+ {
+ using (var ms = new MemoryStream())
+ {
+ await input.CopyToAsync(ms).ConfigureAwait(false);
+ ms.Position = 0;
+
+ using (var s = new StreamReader(ms, ContentEncoding))
+ {
+ var key = new StringBuilder();
+ var value = new StringBuilder();
+ int c;
+
+ while ((c = s.Read()) != -1)
+ {
+ if (c == '=')
+ {
+ value.Length = 0;
+ while ((c = s.Read()) != -1)
+ {
+ if (c == '&')
+ {
+ AddRawKeyValue(form, key, value);
+ break;
+ }
+ else
+ {
+ value.Append((char)c);
+ }
+ }
+
+ if (c == -1)
+ {
+ AddRawKeyValue(form, key, value);
+ return;
+ }
+ }
+ else if (c == '&')
+ {
+ AddRawKeyValue(form, key, value);
+ }
+ else
+ {
+ key.Append((char)c);
+ }
+ }
+
+ if (c == -1)
+ {
+ AddRawKeyValue(form, key, value);
+ }
+ }
+ }
+ }
+ }
+
+ private static void AddRawKeyValue(WebROCollection form, StringBuilder key, StringBuilder value)
+ {
+ form.Add(WebUtility.UrlDecode(key.ToString()), WebUtility.UrlDecode(value.ToString()));
+
+ key.Length = 0;
+ value.Length = 0;
+ }
+
+ private Dictionary<string, HttpPostedFile> files;
+
+ private class WebROCollection : QueryParamCollection
+ {
+ public override string ToString()
+ {
+ var result = new StringBuilder();
+ foreach (var pair in this)
+ {
+ if (result.Length > 0)
+ {
+ result.Append('&');
+ }
+
+ var key = pair.Name;
+ if (key != null && key.Length > 0)
+ {
+ result.Append(key);
+ result.Append('=');
+ }
+
+ result.Append(pair.Value);
+ }
+
+ return result.ToString();
+ }
+ }
+ private class HttpMultipart
+ {
+
+ public class Element
+ {
+ public string ContentType { get; set; }
+
+ public string Name { get; set; }
+
+ public string Filename { get; set; }
+
+ public Encoding Encoding { get; set; }
+
+ public long Start { get; set; }
+
+ public long Length { get; set; }
+
+ public override string ToString()
+ {
+ return "ContentType " + ContentType + ", Name " + Name + ", Filename " + Filename + ", Start " +
+ Start.ToString(CultureInfo.CurrentCulture) + ", Length " + Length.ToString(CultureInfo.CurrentCulture);
+ }
+ }
+
+ private const byte LF = (byte)'\n';
+
+ private const byte CR = (byte)'\r';
+
+ private Stream data;
+
+ private string boundary;
+
+ private byte[] boundaryBytes;
+
+ private byte[] buffer;
+
+ private bool atEof;
+
+ private Encoding encoding;
+
+ private StringBuilder sb;
+
+ // See RFC 2046
+ // In the case of multipart entities, in which one or more different
+ // sets of data are combined in a single body, a "multipart" media type
+ // field must appear in the entity's header. The body must then contain
+ // one or more body parts, each preceded by a boundary delimiter line,
+ // and the last one followed by a closing boundary delimiter line.
+ // After its boundary delimiter line, each body part then consists of a
+ // header area, a blank line, and a body area. Thus a body part is
+ // similar to an RFC 822 message in syntax, but different in meaning.
+
+ public HttpMultipart(Stream data, string b, Encoding encoding)
+ {
+ this.data = data;
+ boundary = b;
+ boundaryBytes = encoding.GetBytes(b);
+ buffer = new byte[boundaryBytes.Length + 2]; // CRLF or '--'
+ this.encoding = encoding;
+ sb = new StringBuilder();
+ }
+
+ public Element ReadNextElement()
+ {
+ if (atEof || ReadBoundary())
+ {
+ return null;
+ }
+
+ var elem = new Element();
+ ReadOnlySpan<char> header;
+ while ((header = ReadHeaders().AsSpan()) != null)
+ {
+ if (header.StartsWith("Content-Disposition:".AsSpan(), StringComparison.OrdinalIgnoreCase))
+ {
+ elem.Name = GetContentDispositionAttribute(header, "name");
+ elem.Filename = StripPath(GetContentDispositionAttributeWithEncoding(header, "filename"));
+ }
+ else if (header.StartsWith("Content-Type:".AsSpan(), StringComparison.OrdinalIgnoreCase))
+ {
+ elem.ContentType = header.Slice("Content-Type:".Length).Trim().ToString();
+ elem.Encoding = GetEncoding(elem.ContentType);
+ }
+ }
+
+ long start = data.Position;
+ elem.Start = start;
+ long pos = MoveToNextBoundary();
+ if (pos == -1)
+ {
+ return null;
+ }
+
+ elem.Length = pos - start;
+ return elem;
+ }
+
+ private string ReadLine()
+ {
+ // CRLF or LF are ok as line endings.
+ bool got_cr = false;
+ int b = 0;
+ sb.Length = 0;
+ while (true)
+ {
+ b = data.ReadByte();
+ if (b == -1)
+ {
+ return null;
+ }
+
+ if (b == LF)
+ {
+ break;
+ }
+
+ got_cr = b == CR;
+ sb.Append((char)b);
+ }
+
+ if (got_cr)
+ {
+ sb.Length--;
+ }
+
+ return sb.ToString();
+ }
+
+ private static string GetContentDispositionAttribute(ReadOnlySpan<char> l, string name)
+ {
+ int idx = l.IndexOf((name + "=\"").AsSpan(), StringComparison.Ordinal);
+ if (idx < 0)
+ {
+ return null;
+ }
+
+ int begin = idx + name.Length + "=\"".Length;
+ int end = l.Slice(begin).IndexOf('"');
+ if (end < 0)
+ {
+ return null;
+ }
+
+ if (begin == end)
+ {
+ return string.Empty;
+ }
+
+ return l.Slice(begin, end - begin).ToString();
+ }
+
+ private string GetContentDispositionAttributeWithEncoding(ReadOnlySpan<char> l, string name)
+ {
+ int idx = l.IndexOf((name + "=\"").AsSpan(), StringComparison.Ordinal);
+ if (idx < 0)
+ {
+ return null;
+ }
+
+ int begin = idx + name.Length + "=\"".Length;
+ int end = l.Slice(begin).IndexOf('"');
+ if (end < 0)
+ {
+ return null;
+ }
+
+ if (begin == end)
+ {
+ return string.Empty;
+ }
+
+ ReadOnlySpan<char> temp = l.Slice(begin, end - begin);
+ byte[] source = new byte[temp.Length];
+ for (int i = temp.Length - 1; i >= 0; i--)
+ {
+ source[i] = (byte)temp[i];
+ }
+
+ return encoding.GetString(source, 0, source.Length);
+ }
+
+ private bool ReadBoundary()
+ {
+ try
+ {
+ string line;
+ do
+ {
+ line = ReadLine();
+ }
+ while (line.Length == 0);
+
+ if (line[0] != '-' || line[1] != '-')
+ {
+ return false;
+ }
+
+ if (!line.EndsWith(boundary, StringComparison.Ordinal))
+ {
+ return true;
+ }
+ }
+ catch
+ {
+
+ }
+
+ return false;
+ }
+
+ private string ReadHeaders()
+ {
+ string s = ReadLine();
+ if (s.Length == 0)
+ {
+ return null;
+ }
+
+ return s;
+ }
+
+ private static bool CompareBytes(byte[] orig, byte[] other)
+ {
+ for (int i = orig.Length - 1; i >= 0; i--)
+ {
+ if (orig[i] != other[i])
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private long MoveToNextBoundary()
+ {
+ long retval = 0;
+ bool got_cr = false;
+
+ int state = 0;
+ int c = data.ReadByte();
+ while (true)
+ {
+ if (c == -1)
+ {
+ return -1;
+ }
+
+ if (state == 0 && c == LF)
+ {
+ retval = data.Position - 1;
+ if (got_cr)
+ {
+ retval--;
+ }
+
+ state = 1;
+ c = data.ReadByte();
+ }
+ else if (state == 0)
+ {
+ got_cr = c == CR;
+ c = data.ReadByte();
+ }
+ else if (state == 1 && c == '-')
+ {
+ c = data.ReadByte();
+ if (c == -1)
+ {
+ return -1;
+ }
+
+ if (c != '-')
+ {
+ state = 0;
+ got_cr = false;
+ continue; // no ReadByte() here
+ }
+
+ int nread = data.Read(buffer, 0, buffer.Length);
+ int bl = buffer.Length;
+ if (nread != bl)
+ {
+ return -1;
+ }
+
+ if (!CompareBytes(boundaryBytes, buffer))
+ {
+ state = 0;
+ data.Position = retval + 2;
+ if (got_cr)
+ {
+ data.Position++;
+ got_cr = false;
+ }
+
+ c = data.ReadByte();
+ continue;
+ }
+
+ if (buffer[bl - 2] == '-' && buffer[bl - 1] == '-')
+ {
+ atEof = true;
+ }
+ else if (buffer[bl - 2] != CR || buffer[bl - 1] != LF)
+ {
+ state = 0;
+ data.Position = retval + 2;
+ if (got_cr)
+ {
+ data.Position++;
+ got_cr = false;
+ }
+
+ c = data.ReadByte();
+ continue;
+ }
+
+ data.Position = retval + 2;
+ if (got_cr)
+ {
+ data.Position++;
+ }
+
+ break;
+ }
+ else
+ {
+ // state == 1
+ state = 0; // no ReadByte() here
+ }
+ }
+
+ return retval;
+ }
+
+ private static string StripPath(string path)
+ {
+ if (path == null || path.Length == 0)
+ {
+ return path;
+ }
+
+ if (path.IndexOf(":\\", StringComparison.Ordinal) != 1
+ && !path.StartsWith("\\\\", StringComparison.Ordinal))
+ {
+ return path;
+ }
+
+ return path.Substring(path.LastIndexOf('\\') + 1);
+ }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs b/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs
new file mode 100644
index 000000000..62b16ed8c
--- /dev/null
+++ b/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs
@@ -0,0 +1,105 @@
+using System;
+using System.Net.WebSockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Emby.Server.Implementations.Net;
+using Microsoft.Extensions.Logging;
+
+namespace Emby.Server.Implementations.SocketSharp
+{
+ public class SharpWebSocket : IWebSocket
+ {
+ /// <summary>
+ /// The logger
+ /// </summary>
+ private readonly ILogger _logger;
+
+ public event EventHandler<EventArgs> Closed;
+
+ /// <summary>
+ /// Gets or sets the web socket.
+ /// </summary>
+ /// <value>The web socket.</value>
+ private readonly WebSocket _webSocket;
+
+ private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
+ private bool _disposed;
+
+ public SharpWebSocket(WebSocket socket, ILogger logger)
+ {
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ _webSocket = socket ?? throw new ArgumentNullException(nameof(socket));
+ }
+
+ /// <summary>
+ /// Gets or sets the state.
+ /// </summary>
+ /// <value>The state.</value>
+ public WebSocketState State => _webSocket.State;
+
+ /// <summary>
+ /// Sends the async.
+ /// </summary>
+ /// <param name="bytes">The bytes.</param>
+ /// <param name="endOfMessage">if set to <c>true</c> [end of message].</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public Task SendAsync(byte[] bytes, bool endOfMessage, CancellationToken cancellationToken)
+ {
+ return _webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Binary, endOfMessage, cancellationToken);
+ }
+
+ /// <summary>
+ /// Sends the asynchronous.
+ /// </summary>
+ /// <param name="text">The text.</param>
+ /// <param name="endOfMessage">if set to <c>true</c> [end of message].</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken)
+ {
+ return _webSocket.SendAsync(new ArraySegment<byte>(Encoding.UTF8.GetBytes(text)), WebSocketMessageType.Text, endOfMessage, cancellationToken);
+ }
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources.
+ /// </summary>
+ /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected virtual void Dispose(bool dispose)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ if (dispose)
+ {
+ _cancellationTokenSource.Cancel();
+ if (_webSocket.State == WebSocketState.Open)
+ {
+ _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closed by client",
+ CancellationToken.None);
+ }
+ Closed?.Invoke(this, EventArgs.Empty);
+ }
+
+ _disposed = true;
+ }
+
+ /// <summary>
+ /// Gets or sets the receive action.
+ /// </summary>
+ /// <value>The receive action.</value>
+ public Action<byte[]> OnReceiveBytes { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs
new file mode 100644
index 000000000..dd313b336
--- /dev/null
+++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs
@@ -0,0 +1,136 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Net.WebSockets;
+using System.Threading;
+using System.Threading.Tasks;
+using Emby.Server.Implementations.HttpServer;
+using Emby.Server.Implementations.Net;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.Services;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Extensions;
+using Microsoft.Extensions.Logging;
+using Microsoft.Net.Http.Headers;
+
+namespace Emby.Server.Implementations.SocketSharp
+{
+ public class WebSocketSharpListener : IHttpListener
+ {
+ private readonly ILogger _logger;
+
+ private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
+ private CancellationToken _disposeCancellationToken;
+
+ public WebSocketSharpListener(
+ ILogger logger)
+ {
+ _logger = logger;
+
+ _disposeCancellationToken = _disposeCancellationTokenSource.Token;
+ }
+
+ public Func<Exception, IRequest, bool, bool, Task> ErrorHandler { get; set; }
+ public Func<IHttpRequest, string, string, string, CancellationToken, Task> RequestHandler { get; set; }
+
+ public Action<WebSocketConnectEventArgs> WebSocketConnected { get; set; }
+
+ private static void LogRequest(ILogger logger, HttpRequest request)
+ {
+ var url = request.GetDisplayUrl();
+
+ logger.LogInformation("WS {Url}. UserAgent: {UserAgent}", url, request.Headers[HeaderNames.UserAgent].ToString());
+ }
+
+ public async Task ProcessWebSocketRequest(HttpContext ctx)
+ {
+ try
+ {
+ LogRequest(_logger, ctx.Request);
+ var endpoint = ctx.Connection.RemoteIpAddress.ToString();
+ var url = ctx.Request.GetDisplayUrl();
+
+ var webSocketContext = await ctx.WebSockets.AcceptWebSocketAsync(null).ConfigureAwait(false);
+ var socket = new SharpWebSocket(webSocketContext, _logger);
+
+ WebSocketConnected(new WebSocketConnectEventArgs
+ {
+ Url = url,
+ QueryString = ctx.Request.Query,
+ WebSocket = socket,
+ Endpoint = endpoint
+ });
+
+ WebSocketReceiveResult result;
+ var message = new List<byte>();
+
+ do
+ {
+ var buffer = WebSocket.CreateServerBuffer(4096);
+ result = await webSocketContext.ReceiveAsync(buffer, _disposeCancellationToken);
+ message.AddRange(buffer.Array.Take(result.Count));
+
+ if (result.EndOfMessage)
+ {
+ socket.OnReceiveBytes(message.ToArray());
+ message.Clear();
+ }
+ } while (socket.State == WebSocketState.Open && result.MessageType != WebSocketMessageType.Close);
+
+
+ if (webSocketContext.State == WebSocketState.Open)
+ {
+ await webSocketContext.CloseAsync(result.CloseStatus ?? WebSocketCloseStatus.NormalClosure,
+ result.CloseStatusDescription, _disposeCancellationToken);
+ }
+
+ socket.Dispose();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "AcceptWebSocketAsync error");
+ if (!ctx.Response.HasStarted)
+ {
+ ctx.Response.StatusCode = 500;
+ }
+ }
+ }
+
+ public Task Stop()
+ {
+ _disposeCancellationTokenSource.Cancel();
+ return Task.CompletedTask;
+ }
+
+ /// <summary>
+ /// Releases the unmanaged resources and disposes of the managed resources used.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ private bool _disposed;
+
+ /// <summary>
+ /// Releases the unmanaged resources and disposes of the managed resources used.
+ /// </summary>
+ /// <param name="disposing">Whether or not the managed resources should be disposed</param>
+ protected virtual void Dispose(bool disposing)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ if (disposing)
+ {
+ Stop().GetAwaiter().GetResult();
+ }
+
+ _disposed = true;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs
new file mode 100644
index 000000000..e0a0ee286
--- /dev/null
+++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs
@@ -0,0 +1,518 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Net;
+using System.Text;
+using MediaBrowser.Model.Services;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Extensions;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Primitives;
+using Microsoft.Net.Http.Headers;
+using IHttpFile = MediaBrowser.Model.Services.IHttpFile;
+using IHttpRequest = MediaBrowser.Model.Services.IHttpRequest;
+using IResponse = MediaBrowser.Model.Services.IResponse;
+
+namespace Emby.Server.Implementations.SocketSharp
+{
+ public partial class WebSocketSharpRequest : IHttpRequest
+ {
+ private readonly HttpRequest request;
+
+ public WebSocketSharpRequest(HttpRequest httpContext, HttpResponse response, string operationName, ILogger logger)
+ {
+ this.OperationName = operationName;
+ this.request = httpContext;
+ this.Response = new WebSocketSharpResponse(logger, response);
+
+ // HandlerFactoryPath = GetHandlerPathIfAny(UrlPrefixes[0]);
+ }
+
+ public HttpRequest HttpRequest => request;
+
+ public IResponse Response { get; }
+
+ public string OperationName { get; set; }
+
+ public object Dto { get; set; }
+
+ public string RawUrl => request.GetEncodedPathAndQuery();
+
+ public string AbsoluteUri => request.GetDisplayUrl().TrimEnd('/');
+
+ public string XForwardedFor
+ => StringValues.IsNullOrEmpty(request.Headers["X-Forwarded-For"]) ? null : request.Headers["X-Forwarded-For"].ToString();
+
+ public int? XForwardedPort
+ => StringValues.IsNullOrEmpty(request.Headers["X-Forwarded-Port"]) ? (int?)null : int.Parse(request.Headers["X-Forwarded-Port"], CultureInfo.InvariantCulture);
+
+ public string XForwardedProtocol => StringValues.IsNullOrEmpty(request.Headers["X-Forwarded-Proto"]) ? null : request.Headers["X-Forwarded-Proto"].ToString();
+
+ public string XRealIp => StringValues.IsNullOrEmpty(request.Headers["X-Real-IP"]) ? null : request.Headers["X-Real-IP"].ToString();
+
+ private string remoteIp;
+ public string RemoteIp
+ {
+ get
+ {
+ if (remoteIp != null)
+ {
+ return remoteIp;
+ }
+
+ var temp = CheckBadChars(XForwardedFor.AsSpan());
+ if (temp.Length != 0)
+ {
+ return remoteIp = temp.ToString();
+ }
+
+ temp = CheckBadChars(XRealIp.AsSpan());
+ if (temp.Length != 0)
+ {
+ return remoteIp = NormalizeIp(temp).ToString();
+ }
+
+ return remoteIp = NormalizeIp(request.HttpContext.Connection.RemoteIpAddress.ToString().AsSpan()).ToString();
+ }
+ }
+
+ private static readonly char[] HttpTrimCharacters = new char[] { (char)0x09, (char)0xA, (char)0xB, (char)0xC, (char)0xD, (char)0x20 };
+
+ // CheckBadChars - throws on invalid chars to be not found in header name/value
+ internal static ReadOnlySpan<char> CheckBadChars(ReadOnlySpan<char> name)
+ {
+ if (name.Length == 0)
+ {
+ return name;
+ }
+
+ // VALUE check
+ // Trim spaces from both ends
+ name = name.Trim(HttpTrimCharacters);
+
+ // First, check for correctly formed multi-line value
+ // Second, check for absence of CTL characters
+ int crlf = 0;
+ for (int i = 0; i < name.Length; ++i)
+ {
+ char c = (char)(0x000000ff & (uint)name[i]);
+ switch (crlf)
+ {
+ case 0:
+ {
+ if (c == '\r')
+ {
+ crlf = 1;
+ }
+ else if (c == '\n')
+ {
+ // Technically this is bad HTTP. But it would be a breaking change to throw here.
+ // Is there an exploit?
+ crlf = 2;
+ }
+ else if (c == 127 || (c < ' ' && c != '\t'))
+ {
+ throw new ArgumentException("net_WebHeaderInvalidControlChars", nameof(name));
+ }
+
+ break;
+ }
+
+ case 1:
+ {
+ if (c == '\n')
+ {
+ crlf = 2;
+ break;
+ }
+
+ throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name));
+ }
+
+ case 2:
+ {
+ if (c == ' ' || c == '\t')
+ {
+ crlf = 0;
+ break;
+ }
+
+ throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name));
+ }
+ }
+ }
+
+ if (crlf != 0)
+ {
+ throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name));
+ }
+
+ return name;
+ }
+
+ private ReadOnlySpan<char> NormalizeIp(ReadOnlySpan<char> ip)
+ {
+ if (ip.Length != 0 && !ip.IsWhiteSpace())
+ {
+ // Handle ipv4 mapped to ipv6
+ const string srch = "::ffff:";
+ var index = ip.IndexOf(srch.AsSpan(), StringComparison.OrdinalIgnoreCase);
+ if (index == 0)
+ {
+ ip = ip.Slice(srch.Length);
+ }
+ }
+
+ return ip;
+ }
+
+ public string[] AcceptTypes => request.Headers.GetCommaSeparatedValues(HeaderNames.Accept);
+
+ private Dictionary<string, object> items;
+ public Dictionary<string, object> Items => items ?? (items = new Dictionary<string, object>());
+
+ private string responseContentType;
+ public string ResponseContentType
+ {
+ get =>
+ responseContentType
+ ?? (responseContentType = GetResponseContentType(HttpRequest));
+ set => this.responseContentType = value;
+ }
+
+ public const string FormUrlEncoded = "application/x-www-form-urlencoded";
+ public const string MultiPartFormData = "multipart/form-data";
+ public static string GetResponseContentType(HttpRequest httpReq)
+ {
+ var specifiedContentType = GetQueryStringContentType(httpReq);
+ if (!string.IsNullOrEmpty(specifiedContentType))
+ {
+ return specifiedContentType;
+ }
+
+ const string serverDefaultContentType = "application/json";
+
+ var acceptContentTypes = httpReq.Headers.GetCommaSeparatedValues(HeaderNames.Accept);
+ string defaultContentType = null;
+ if (HasAnyOfContentTypes(httpReq, FormUrlEncoded, MultiPartFormData))
+ {
+ defaultContentType = serverDefaultContentType;
+ }
+
+ var acceptsAnything = false;
+ var hasDefaultContentType = defaultContentType != null;
+ if (acceptContentTypes != null)
+ {
+ foreach (var acceptsType in acceptContentTypes)
+ {
+ // TODO: @bond move to Span when Span.Split lands
+ // https://github.com/dotnet/corefx/issues/26528
+ var contentType = acceptsType?.Split(';')[0].Trim();
+ acceptsAnything = contentType.Equals("*/*", StringComparison.OrdinalIgnoreCase);
+
+ if (acceptsAnything)
+ {
+ break;
+ }
+ }
+
+ if (acceptsAnything)
+ {
+ if (hasDefaultContentType)
+ {
+ return defaultContentType;
+ }
+ else
+ {
+ return serverDefaultContentType;
+ }
+ }
+ }
+
+ if (acceptContentTypes == null && httpReq.ContentType == Soap11)
+ {
+ return Soap11;
+ }
+
+ // We could also send a '406 Not Acceptable', but this is allowed also
+ return serverDefaultContentType;
+ }
+
+ public const string Soap11 = "text/xml; charset=utf-8";
+
+ public static bool HasAnyOfContentTypes(HttpRequest request, params string[] contentTypes)
+ {
+ if (contentTypes == null || request.ContentType == null)
+ {
+ return false;
+ }
+
+ foreach (var contentType in contentTypes)
+ {
+ if (IsContentType(request, contentType))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public static bool IsContentType(HttpRequest request, string contentType)
+ {
+ return request.ContentType.StartsWith(contentType, StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static string GetQueryStringContentType(HttpRequest httpReq)
+ {
+ ReadOnlySpan<char> format = httpReq.Query["format"].ToString().AsSpan();
+ if (format == null)
+ {
+ const int formatMaxLength = 4;
+ ReadOnlySpan<char> pi = httpReq.Path.ToString().AsSpan();
+ if (pi == null || pi.Length <= formatMaxLength)
+ {
+ return null;
+ }
+
+ if (pi[0] == '/')
+ {
+ pi = pi.Slice(1);
+ }
+
+ format = LeftPart(pi, '/');
+ if (format.Length > formatMaxLength)
+ {
+ return null;
+ }
+ }
+
+ format = LeftPart(format, '.');
+ if (format.Contains("json".AsSpan(), StringComparison.OrdinalIgnoreCase))
+ {
+ return "application/json";
+ }
+ else if (format.Contains("xml".AsSpan(), StringComparison.OrdinalIgnoreCase))
+ {
+ return "application/xml";
+ }
+
+ return null;
+ }
+
+ public static ReadOnlySpan<char> LeftPart(ReadOnlySpan<char> strVal, char needle)
+ {
+ if (strVal == null)
+ {
+ return null;
+ }
+
+ var pos = strVal.IndexOf(needle);
+ return pos == -1 ? strVal : strVal.Slice(0, pos);
+ }
+
+ public static string HandlerFactoryPath;
+
+ private string pathInfo;
+ public string PathInfo
+ {
+ get
+ {
+ if (this.pathInfo == null)
+ {
+ var mode = HandlerFactoryPath;
+
+ var pos = RawUrl.IndexOf("?", StringComparison.Ordinal);
+ if (pos != -1)
+ {
+ var path = RawUrl.Substring(0, pos);
+ this.pathInfo = GetPathInfo(
+ path,
+ mode,
+ mode ?? string.Empty);
+ }
+ else
+ {
+ this.pathInfo = RawUrl;
+ }
+
+ this.pathInfo = WebUtility.UrlDecode(pathInfo);
+ this.pathInfo = NormalizePathInfo(pathInfo, mode).ToString();
+ }
+
+ return this.pathInfo;
+ }
+ }
+
+ private static string GetPathInfo(string fullPath, string mode, string appPath)
+ {
+ var pathInfo = ResolvePathInfoFromMappedPath(fullPath, mode);
+ if (!string.IsNullOrEmpty(pathInfo))
+ {
+ return pathInfo;
+ }
+
+ // Wildcard mode relies on this to work out the handlerPath
+ pathInfo = ResolvePathInfoFromMappedPath(fullPath, appPath);
+ if (!string.IsNullOrEmpty(pathInfo))
+ {
+ return pathInfo;
+ }
+
+ return fullPath;
+ }
+
+ private static string ResolvePathInfoFromMappedPath(string fullPath, string mappedPathRoot)
+ {
+ if (mappedPathRoot == null)
+ {
+ return null;
+ }
+
+ var sbPathInfo = new StringBuilder();
+ var fullPathParts = fullPath.Split('/');
+ var mappedPathRootParts = mappedPathRoot.Split('/');
+ var fullPathIndexOffset = mappedPathRootParts.Length - 1;
+ var pathRootFound = false;
+
+ for (var fullPathIndex = 0; fullPathIndex < fullPathParts.Length; fullPathIndex++)
+ {
+ if (pathRootFound)
+ {
+ sbPathInfo.Append("/" + fullPathParts[fullPathIndex]);
+ }
+ else if (fullPathIndex - fullPathIndexOffset >= 0)
+ {
+ pathRootFound = true;
+ for (var mappedPathRootIndex = 0; mappedPathRootIndex < mappedPathRootParts.Length; mappedPathRootIndex++)
+ {
+ if (!string.Equals(fullPathParts[fullPathIndex - fullPathIndexOffset + mappedPathRootIndex], mappedPathRootParts[mappedPathRootIndex], StringComparison.OrdinalIgnoreCase))
+ {
+ pathRootFound = false;
+ break;
+ }
+ }
+ }
+ }
+
+ if (!pathRootFound)
+ {
+ return null;
+ }
+
+ return sbPathInfo.Length > 1 ? sbPathInfo.ToString().TrimEnd('/') : "/";
+ }
+
+ public string UserAgent => request.Headers[HeaderNames.UserAgent];
+
+ public IHeaderDictionary Headers => request.Headers;
+
+ public IQueryCollection QueryString => request.Query;
+
+ public bool IsLocal => string.Equals(request.HttpContext.Connection.LocalIpAddress.ToString(), request.HttpContext.Connection.RemoteIpAddress.ToString());
+
+ private string httpMethod;
+ public string HttpMethod =>
+ httpMethod
+ ?? (httpMethod = request.Method);
+
+ public string Verb => HttpMethod;
+
+ public string ContentType => request.ContentType;
+
+ private Encoding ContentEncoding
+ {
+ get
+ {
+ // TODO is this necessary?
+ if (UserAgent != null && CultureInfo.InvariantCulture.CompareInfo.IsPrefix(UserAgent, "UP"))
+ {
+ string postDataCharset = Headers["x-up-devcap-post-charset"];
+ if (!string.IsNullOrEmpty(postDataCharset))
+ {
+ try
+ {
+ return Encoding.GetEncoding(postDataCharset);
+ }
+ catch (ArgumentException)
+ {
+ }
+ }
+ }
+
+ return request.GetTypedHeaders().ContentType.Encoding ?? Encoding.UTF8;
+ }
+ }
+
+ public Uri UrlReferrer => request.GetTypedHeaders().Referer;
+
+ public static Encoding GetEncoding(string contentTypeHeader)
+ {
+ var param = GetParameter(contentTypeHeader.AsSpan(), "charset=");
+ if (param == null)
+ {
+ return null;
+ }
+
+ try
+ {
+ return Encoding.GetEncoding(param);
+ }
+ catch (ArgumentException)
+ {
+ return null;
+ }
+ }
+
+ public Stream InputStream => request.Body;
+
+ public long ContentLength => request.ContentLength ?? 0;
+
+ private IHttpFile[] httpFiles;
+ public IHttpFile[] Files
+ {
+ get
+ {
+ if (httpFiles == null)
+ {
+ if (files == null)
+ {
+ return httpFiles = Array.Empty<IHttpFile>();
+ }
+
+ httpFiles = new IHttpFile[files.Count];
+ var i = 0;
+ foreach (var pair in files)
+ {
+ var reqFile = pair.Value;
+ httpFiles[i] = new HttpFile
+ {
+ ContentType = reqFile.ContentType,
+ ContentLength = reqFile.ContentLength,
+ FileName = reqFile.FileName,
+ InputStream = reqFile.InputStream,
+ };
+ i++;
+ }
+ }
+
+ return httpFiles;
+ }
+ }
+
+ public static ReadOnlySpan<char> NormalizePathInfo(string pathInfo, string handlerPath)
+ {
+ if (handlerPath != null)
+ {
+ var trimmed = pathInfo.AsSpan().TrimStart('/');
+ if (trimmed.StartsWith(handlerPath.AsSpan(), StringComparison.OrdinalIgnoreCase))
+ {
+ return trimmed.Slice(handlerPath.Length).ToString().AsSpan();
+ }
+ }
+
+ return pathInfo.AsSpan();
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpResponse.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpResponse.cs
new file mode 100644
index 000000000..0f67eaa62
--- /dev/null
+++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpResponse.cs
@@ -0,0 +1,98 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Services;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+using IRequest = MediaBrowser.Model.Services.IRequest;
+
+namespace Emby.Server.Implementations.SocketSharp
+{
+ public class WebSocketSharpResponse : IResponse
+ {
+ private readonly ILogger _logger;
+
+ public WebSocketSharpResponse(ILogger logger, HttpResponse response)
+ {
+ _logger = logger;
+ OriginalResponse = response;
+ }
+
+ public HttpResponse OriginalResponse { get; }
+
+ public int StatusCode
+ {
+ get => OriginalResponse.StatusCode;
+ set => OriginalResponse.StatusCode = value;
+ }
+
+ public string StatusDescription { get; set; }
+
+ public string ContentType
+ {
+ get => OriginalResponse.ContentType;
+ set => OriginalResponse.ContentType = value;
+ }
+
+ public void AddHeader(string name, string value)
+ {
+ if (string.Equals(name, "Content-Type", StringComparison.OrdinalIgnoreCase))
+ {
+ ContentType = value;
+ return;
+ }
+
+ OriginalResponse.Headers.Add(name, value);
+ }
+
+ public void Redirect(string url)
+ {
+ OriginalResponse.Redirect(url);
+ }
+
+ public Stream OutputStream => OriginalResponse.Body;
+
+ public bool SendChunked { get; set; }
+
+ const int StreamCopyToBufferSize = 81920;
+ public async Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, IFileSystem fileSystem, IStreamHelper streamHelper, CancellationToken cancellationToken)
+ {
+ var allowAsync = !RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
+
+ //if (count <= 0)
+ //{
+ // allowAsync = true;
+ //}
+
+ var fileOpenOptions = 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;
+ }
+
+ if (count > 0)
+ {
+ await streamHelper.CopyToAsync(fs, OutputStream, count, cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ await fs.CopyToAsync(OutputStream, StreamCopyToBufferSize, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ }
+ }
+}