diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 45a819f260..5e8611d531 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -42,6 +42,7 @@ using Emby.Server.Implementations.ScheduledTasks;
using Emby.Server.Implementations.Security;
using Emby.Server.Implementations.Serialization;
using Emby.Server.Implementations.Session;
+using Emby.Server.Implementations.SocketSharp;
using Emby.Server.Implementations.TV;
using Emby.Server.Implementations.Updates;
using Emby.Server.Implementations.Xml;
@@ -105,10 +106,15 @@ using MediaBrowser.Providers.Subtitles;
using MediaBrowser.Providers.TV.TheTVDB;
using MediaBrowser.WebDashboard.Api;
using MediaBrowser.XbmcMetadata.Providers;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
using ServiceStack;
+using HttpResponse = MediaBrowser.Model.Net.HttpResponse;
using X509Certificate = System.Security.Cryptography.X509Certificates.X509Certificate;
namespace Emby.Server.Implementations
@@ -123,7 +129,7 @@ namespace Emby.Server.Implementations
///
/// true if this instance can self restart; otherwise, false.
public abstract bool CanSelfRestart { get; }
-
+ public IWebHost Host { get; set; }
public virtual bool CanLaunchWebBrowser
{
get
@@ -612,6 +618,115 @@ namespace Emby.Server.Implementations
await RegisterResources(serviceCollection);
FindParts();
+
+ Host = new WebHostBuilder()
+ .UseKestrel()
+ .UseContentRoot("/Users/clausvium/RiderProjects/jellyfin/Jellyfin.Server/bin/Debug/netcoreapp2.1/jellyfin-web/src")
+ .UseStartup()
+// .ConfigureServices(async services =>
+// {
+// services.AddSingleton(startUp);
+// RegisterResources(services);
+// FindParts();
+// try
+// {
+// ImageProcessor.ImageEncoder =
+// new NullImageEncoder(); //SkiaEncoder(_loggerFactory, appPaths, fileSystem, localizationManager);
+// }
+// catch (Exception ex)
+// {
+// Logger.LogInformation(ex, "Skia not available. Will fallback to NullIMageEncoder. {0}");
+// ImageProcessor.ImageEncoder = new NullImageEncoder();
+// }
+// await RunStartupTasks().ConfigureAwait(false);
+// })
+ .UseUrls("http://localhost:8096")
+ .ConfigureServices(s => s.AddRouting())
+ .Configure( app =>
+ {
+ app.UseWebSockets(new WebSocketOptions {
+ KeepAliveInterval = TimeSpan.FromMilliseconds(1000000000),
+ ReceiveBufferSize = 0x10000
+ });
+
+ app.UseRouter(r =>
+ {
+ // TODO all the verbs, but really MVC...
+ r.MapGet("/{*localpath}", ExecuteHandler);
+ r.MapPut("/{*localpath}", ExecuteHandler);
+ r.MapPost("/{*localpath}", ExecuteHandler);
+ r.MapDelete("/{*localpath}", ExecuteHandler);
+ r.MapVerb("HEAD", "/{*localpath}", ExecuteHandler);
+ });
+ })
+ .Build();
+ }
+
+ public async Task ExecuteHandler(HttpRequest request, Microsoft.AspNetCore.Http.HttpResponse response, RouteData data)
+ {
+ var ctx = request.HttpContext;
+ if (ctx.WebSockets.IsWebSocketRequest)
+ {
+ try
+ {
+ var endpoint = ctx.Request.Path.ToString();
+ var url = ctx.Request.Path.ToString();
+
+ var queryString = new QueryParamCollection(request.Query);
+
+ var connectingArgs = new WebSocketConnectingEventArgs
+ {
+ Url = url,
+ QueryString = queryString,
+ Endpoint = endpoint
+ };
+
+ if (connectingArgs.AllowConnection)
+ {
+ Logger.LogDebug("Web socket connection allowed");
+
+ var webSocketContext = ctx.WebSockets.AcceptWebSocketAsync(null).Result;
+
+ //SharpWebSocket socket = new SharpWebSocket(webSocketContext, Logger);
+ //socket.ConnectAsServerAsync().ConfigureAwait(false);
+
+// var connection = new WebSocketConnection(webSocketContext, e.Endpoint, _jsonSerializer, _logger)
+// {
+// OnReceive = ProcessWebSocketMessageReceived,
+// Url = e.Url,
+// QueryString = e.QueryString ?? new QueryParamCollection()
+// };
+//
+// connection.Closed += Connection_Closed;
+//
+// lock (_webSocketConnections)
+// {
+// _webSocketConnections.Add(connection);
+// }
+//
+// WebSocketConnected(new WebSocketConnectEventArgs
+// {
+// Url = url,
+// QueryString = queryString,
+// WebSocket = socket,
+// Endpoint = endpoint
+// });
+ await webSocketContext.ReceiveAsync(new ArraySegment(), CancellationToken.None).ConfigureAwait(false);
+ }
+ else
+ {
+ Logger.LogWarning("Web socket connection not allowed");
+ ctx.Response.StatusCode = 401;
+ }
+ }
+ catch (Exception ex)
+ {
+ ctx.Response.StatusCode = 500;
+ }
+ }
+
+ var req = new WebSocketSharpRequest(request, response, request.Path, Logger);
+ await ((HttpListenerHost)HttpServer).RequestHandler(req,request.Path.ToString(), request.Host.ToString(), data.Values["localpath"].ToString(), CancellationToken.None).ConfigureAwait(false);
}
protected virtual IHttpClient CreateHttpClient()
@@ -1067,7 +1182,7 @@ namespace Emby.Server.Implementations
HttpServer.Init(GetExports(false), GetExports());
- StartServer();
+ //StartServer();
LibraryManager.AddParts(GetExports(),
GetExports(),
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index bbf165d627..acbc60c39e 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -22,6 +22,16 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
index fb42460f1b..1bd084259d 100644
--- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
+++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
@@ -422,7 +422,7 @@ namespace Emby.Server.Implementations.HttpServer
///
/// Overridable method that can be used to implement a custom hnandler
///
- protected async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken)
+ public async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken)
{
var stopWatch = new Stopwatch();
stopWatch.Start();
diff --git a/Emby.Server.Implementations/SocketSharp/HttpFile.cs b/Emby.Server.Implementations/SocketSharp/HttpFile.cs
new file mode 100644
index 0000000000..120ac50d9c
--- /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 0000000000..f38ed848ee
--- /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;
+
+ ///
+ /// Releases the unmanaged resources and disposes of the managed resources used.
+ ///
+ 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 0000000000..a8142aef68
--- /dev/null
+++ b/Emby.Server.Implementations/SocketSharp/RequestMono.cs
@@ -0,0 +1,681 @@
+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;
+
+namespace Emby.Server.Implementations.SocketSharp
+{
+ public partial class WebSocketSharpRequest : IHttpRequest
+ {
+ internal static string GetParameter(string header, string attr)
+ {
+ int ap = header.IndexOf(attr, StringComparison.Ordinal);
+ if (ap == -1)
+ {
+ return null;
+ }
+
+ ap += attr.Length;
+ if (ap >= header.Length)
+ {
+ return null;
+ }
+
+ char ending = header[ap];
+ if (ending != '"')
+ {
+ ending = ' ';
+ }
+
+ int end = header.IndexOf(ending, ap + 1);
+ if (end == -1)
+ {
+ return ending == '"' ? null : header.Substring(ap);
+ }
+
+ return header.Substring(ap + 1, end - ap - 1);
+ }
+
+ private async Task LoadMultiPart(WebROCollection form)
+ {
+ string boundary = GetParameter(ContentType, "; 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;
+ input.Read(copy, 0, (int)e.Length);
+
+ 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 GetFormData()
+ {
+ var form = new WebROCollection();
+ files = new Dictionary();
+
+ if (IsContentType("multipart/form-data", true))
+ {
+ await LoadMultiPart(form).ConfigureAwait(false);
+ }
+ else if (IsContentType("application/x-www-form-urlencoded", true))
+ {
+ await LoadWwwForm(form).ConfigureAwait(false);
+ }
+
+#if NET_4_0
+ if (validateRequestNewMode && !checked_form) {
+ // Setting this before calling the validator prevents
+ // possible endless recursion
+ checked_form = true;
+ ValidateNameValueCollection("Form", query_string_nvc, RequestValidationSource.Form);
+ } else
+#endif
+ if (validate_form && !checked_form)
+ {
+ checked_form = true;
+ ValidateNameValueCollection("Form", form);
+ }
+
+ return form;
+ }
+
+ public string Accept => StringValues.IsNullOrEmpty(request.Headers["Accept"]) ? null : request.Headers["Accept"].ToString();
+
+ public string Authorization => StringValues.IsNullOrEmpty(request.Headers["Authorization"]) ? null : request.Headers["Authorization"].ToString();
+
+ protected bool validate_cookies { get; set; }
+ protected bool validate_query_string { get; set; }
+ protected bool validate_form { get; set; }
+ protected bool checked_cookies { get; set; }
+ protected bool checked_query_string { 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;
+ }
+
+ public void ValidateInput()
+ {
+ validate_cookies = true;
+ validate_query_string = true;
+ validate_form = true;
+ }
+
+ private bool IsContentType(string ct, bool starts_with)
+ {
+ if (ct == null || ContentType == null)
+ {
+ return false;
+ }
+
+ if (starts_with)
+ {
+ return ContentType.StartsWith(ct, StringComparison.OrdinalIgnoreCase);
+ }
+
+ return string.Equals(ContentType, 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 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();
+ string header;
+ while ((header = ReadHeaders()) != null)
+ {
+ if (header.StartsWith("Content-Disposition:", StringComparison.OrdinalIgnoreCase))
+ {
+ elem.Name = GetContentDispositionAttribute(header, "name");
+ elem.Filename = StripPath(GetContentDispositionAttributeWithEncoding(header, "filename"));
+ }
+ else if (header.StartsWith("Content-Type:", StringComparison.OrdinalIgnoreCase))
+ {
+ elem.ContentType = header.Substring("Content-Type:".Length).Trim();
+ 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(string l, string name)
+ {
+ int idx = l.IndexOf(name + "=\"", StringComparison.Ordinal);
+ if (idx < 0)
+ {
+ return null;
+ }
+
+ int begin = idx + name.Length + "=\"".Length;
+ int end = l.IndexOf('"', begin);
+ if (end < 0)
+ {
+ return null;
+ }
+
+ if (begin == end)
+ {
+ return string.Empty;
+ }
+
+ return l.Substring(begin, end - begin);
+ }
+
+ private string GetContentDispositionAttributeWithEncoding(string l, string name)
+ {
+ int idx = l.IndexOf(name + "=\"", StringComparison.Ordinal);
+ if (idx < 0)
+ {
+ return null;
+ }
+
+ int begin = idx + name.Length + "=\"".Length;
+ int end = l.IndexOf('"', begin);
+ if (end < 0)
+ {
+ return null;
+ }
+
+ if (begin == end)
+ {
+ return string.Empty;
+ }
+
+ string temp = l.Substring(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 0000000000..80ef451dc7
--- /dev/null
+++ b/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs
@@ -0,0 +1,149 @@
+using System;
+using System.Net.WebSockets;
+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
+ {
+ ///
+ /// The logger
+ ///
+ private readonly ILogger _logger;
+
+ public event EventHandler Closed;
+
+ ///
+ /// Gets or sets the web socket.
+ ///
+ /// The web socket.
+ private SocketHttpListener.WebSocket WebSocket { get; set; }
+
+ private TaskCompletionSource _taskCompletionSource = new TaskCompletionSource();
+ private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
+ private bool _disposed = false;
+
+ public SharpWebSocket(SocketHttpListener.WebSocket socket, ILogger logger)
+ {
+ if (socket == null)
+ {
+ throw new ArgumentNullException(nameof(socket));
+ }
+
+ if (logger == null)
+ {
+ throw new ArgumentNullException(nameof(logger));
+ }
+
+ _logger = logger;
+ WebSocket = socket;
+
+ socket.OnMessage += OnSocketMessage;
+ socket.OnClose += OnSocketClose;
+ socket.OnError += OnSocketError;
+ }
+
+ public Task ConnectAsServerAsync()
+ => WebSocket.ConnectAsServer();
+
+ public Task StartReceive()
+ {
+ return _taskCompletionSource.Task;
+ }
+
+ private void OnSocketError(object sender, SocketHttpListener.ErrorEventArgs e)
+ {
+ _logger.LogError("Error in SharpWebSocket: {Message}", e.Message ?? string.Empty);
+
+ // Closed?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void OnSocketClose(object sender, SocketHttpListener.CloseEventArgs e)
+ {
+ _taskCompletionSource.TrySetResult(true);
+
+ Closed?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void OnSocketMessage(object sender, SocketHttpListener.MessageEventArgs e)
+ {
+ if (OnReceiveBytes != null)
+ {
+ OnReceiveBytes(e.RawData);
+ }
+ }
+
+ ///
+ /// Gets or sets the state.
+ ///
+ /// The state.
+ public WebSocketState State => WebSocket.ReadyState;
+
+ ///
+ /// Sends the async.
+ ///
+ /// The bytes.
+ /// if set to true [end of message].
+ /// The cancellation token.
+ /// Task.
+ public Task SendAsync(byte[] bytes, bool endOfMessage, CancellationToken cancellationToken)
+ {
+ return WebSocket.SendAsync(bytes);
+ }
+
+ ///
+ /// Sends the asynchronous.
+ ///
+ /// The text.
+ /// if set to true [end of message].
+ /// The cancellation token.
+ /// Task.
+ public Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken)
+ {
+ return WebSocket.SendAsync(text);
+ }
+
+ ///
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Releases unmanaged and - optionally - managed resources.
+ ///
+ /// true to release both managed and unmanaged resources; false to release only unmanaged resources.
+ protected virtual void Dispose(bool dispose)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ if (dispose)
+ {
+ WebSocket.OnMessage -= OnSocketMessage;
+ WebSocket.OnClose -= OnSocketClose;
+ WebSocket.OnError -= OnSocketError;
+
+ _cancellationTokenSource.Cancel();
+
+ WebSocket.CloseAsync().GetAwaiter().GetResult();
+ }
+
+ _disposed = true;
+ }
+
+ ///
+ /// Gets or sets the receive action.
+ ///
+ /// The receive action.
+ public Action OnReceiveBytes { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs
new file mode 100644
index 0000000000..ab7ddeca20
--- /dev/null
+++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs
@@ -0,0 +1,261 @@
+using System;
+using System.Collections.Generic;
+ using System.Net;
+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.Extensions.Logging;
+
+ namespace Emby.Server.Implementations.SocketSharp
+{
+ public class WebSocketSharpListener : IHttpListener
+ {
+ private HttpListener _listener;
+
+ private readonly ILogger _logger;
+
+ private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
+ private CancellationToken _disposeCancellationToken;
+
+ public WebSocketSharpListener(
+ ILogger logger)
+ {
+ _logger = logger;
+
+ _disposeCancellationToken = _disposeCancellationTokenSource.Token;
+ }
+
+ public Func ErrorHandler { get; set; }
+ public Func RequestHandler { get; set; }
+
+ public Action WebSocketConnecting { get; set; }
+
+ public Action WebSocketConnected { get; set; }
+
+// public void Start(IEnumerable urlPrefixes)
+// {
+// // TODO
+// //if (_listener == null)
+// //{
+// // _listener = new HttpListener(_logger, _cryptoProvider, _socketFactory, _streamHelper, _fileSystem, _environment);
+// //}
+//
+// //_listener.EnableDualMode = _enableDualMode;
+//
+// //if (_certificate != null)
+// //{
+// // _listener.LoadCert(_certificate);
+// //}
+//
+// //_logger.LogInformation("Adding HttpListener prefixes {Prefixes}", urlPrefixes);
+// //_listener.Prefixes.AddRange(urlPrefixes);
+//
+// //_listener.OnContext = async c => await InitTask(c, _disposeCancellationToken).ConfigureAwait(false);
+//
+// //_listener.Start();
+//
+// if (_listener == null)
+// {
+// _listener = new HttpListener();
+// }
+//
+// _logger.LogInformation("Adding HttpListener prefixes {Prefixes}", urlPrefixes);
+//
+// //foreach (var urlPrefix in urlPrefixes)
+// //{
+// // _listener.Prefixes.Add(urlPrefix);
+// //}
+// _listener.Prefixes.Add("http://localhost:8096/");
+//
+// _listener.Start();
+//
+// // TODO how to do this in netcore?
+// _listener.BeginGetContext(async c => await InitTask(c, _disposeCancellationToken).ConfigureAwait(false),
+// null);
+// }
+
+ private static void LogRequest(ILogger logger, HttpListenerRequest request)
+ {
+ var url = request.Url.ToString();
+
+ logger.LogInformation(
+ "{0} {1}. UserAgent: {2}",
+ request.IsWebSocketRequest ? "WS" : "HTTP " + request.HttpMethod,
+ url,
+ request.UserAgent ?? string.Empty);
+ }
+//
+// private Task InitTask(IAsyncResult asyncResult, CancellationToken cancellationToken)
+// {
+// var context = _listener.EndGetContext(asyncResult);
+// _listener.BeginGetContext(async c => await InitTask(c, _disposeCancellationToken).ConfigureAwait(false), null);
+// IHttpRequest httpReq = null;
+// var request = context.Request;
+//
+// try
+// {
+// if (request.IsWebSocketRequest)
+// {
+// LogRequest(_logger, request);
+//
+// return ProcessWebSocketRequest(context);
+// }
+//
+// httpReq = GetRequest(context);
+// }
+// catch (Exception ex)
+// {
+// _logger.LogError(ex, "Error processing request");
+//
+// httpReq = httpReq ?? GetRequest(context);
+// return ErrorHandler(ex, httpReq, true, true);
+// }
+//
+// var uri = request.Url;
+//
+// return RequestHandler(httpReq, uri.OriginalString, uri.Host, uri.LocalPath, cancellationToken);
+// }
+
+ private async Task ProcessWebSocketRequest(HttpListenerContext ctx)
+ {
+ try
+ {
+ var endpoint = ctx.Request.RemoteEndPoint.ToString();
+ var url = ctx.Request.RawUrl;
+
+ var queryString = new QueryParamCollection(ctx.Request.QueryString);
+
+ var connectingArgs = new WebSocketConnectingEventArgs
+ {
+ Url = url,
+ QueryString = queryString,
+ Endpoint = endpoint
+ };
+
+ WebSocketConnecting?.Invoke(connectingArgs);
+
+ if (connectingArgs.AllowConnection)
+ {
+ _logger.LogDebug("Web socket connection allowed");
+
+ var webSocketContext = await ctx.AcceptWebSocketAsync(null).ConfigureAwait(false);
+
+ if (WebSocketConnected != null)
+ {
+ SharpWebSocket socket = null; //new SharpWebSocket(webSocketContext.WebSocket, _logger);
+ await socket.ConnectAsServerAsync().ConfigureAwait(false);
+
+ WebSocketConnected(new WebSocketConnectEventArgs
+ {
+ Url = url,
+ QueryString = queryString,
+ WebSocket = socket,
+ Endpoint = endpoint
+ });
+
+ await ReceiveWebSocketAsync(ctx, socket).ConfigureAwait(false);
+ }
+ }
+ else
+ {
+ _logger.LogWarning("Web socket connection not allowed");
+ ctx.Response.StatusCode = 401;
+ ctx.Response.Close();
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "AcceptWebSocketAsync error");
+ ctx.Response.StatusCode = 500;
+ ctx.Response.Close();
+ }
+ }
+
+ private async Task ReceiveWebSocketAsync(HttpListenerContext ctx, SharpWebSocket socket)
+ {
+ try
+ {
+ await socket.StartReceive().ConfigureAwait(false);
+ }
+ finally
+ {
+ TryClose(ctx, 200);
+ }
+ }
+
+ private void TryClose(HttpListenerContext ctx, int statusCode)
+ {
+ try
+ {
+ ctx.Response.StatusCode = statusCode;
+ ctx.Response.Close();
+ }
+ catch (ObjectDisposedException)
+ {
+ // TODO: Investigate and properly fix.
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error closing web socket response");
+ }
+ }
+
+ private IHttpRequest GetRequest(HttpRequest httpContext)
+ {
+ var urlSegments = httpContext.Path;
+
+ var operationName = urlSegments;
+
+ var req = new WebSocketSharpRequest(httpContext, httpContext.HttpContext.Response, operationName, _logger);
+
+ return req;
+ }
+
+ public void Start(IEnumerable urlPrefixes)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task Stop()
+ {
+ _disposeCancellationTokenSource.Cancel();
+ _listener?.Close();
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// Releases the unmanaged resources and disposes of the managed resources used.
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ private bool _disposed;
+
+ ///
+ /// Releases the unmanaged resources and disposes of the managed resources used.
+ ///
+ /// Whether or not the managed resources should be disposed
+ 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 0000000000..facc544461
--- /dev/null
+++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs
@@ -0,0 +1,539 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Net;
+using System.Text;
+using Emby.Server.Implementations.HttpServer;
+using MediaBrowser.Model.Services;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Primitives;
+using Microsoft.Net.Http.Headers;
+using SocketHttpListener.Net;
+using IHttpFile = MediaBrowser.Model.Services.IHttpFile;
+using IHttpRequest = MediaBrowser.Model.Services.IHttpRequest;
+using IHttpResponse = MediaBrowser.Model.Services.IHttpResponse;
+using IResponse = MediaBrowser.Model.Services.IResponse;
+
+namespace Emby.Server.Implementations.SocketSharp
+{
+ public partial class WebSocketSharpRequest : IHttpRequest
+ {
+ private readonly HttpRequest request;
+ private readonly IHttpResponse response;
+
+ public WebSocketSharpRequest(HttpRequest httpContext, HttpResponse response, string operationName, ILogger logger)
+ {
+ this.OperationName = operationName;
+ this.request = httpContext;
+ this.response = new WebSocketSharpResponse(logger, response, this);
+
+ // HandlerFactoryPath = GetHandlerPathIfAny(UrlPrefixes[0]);
+ }
+
+ public HttpRequest HttpRequest => request;
+
+ public object OriginalRequest => request;
+
+ public IResponse Response => response;
+
+ public IHttpResponse HttpResponse => response;
+
+ public string OperationName { get; set; }
+
+ public object Dto { get; set; }
+
+ public string RawUrl => request.Path.ToUriComponent();
+
+ public string AbsoluteUri => request.Path.ToUriComponent().TrimEnd('/');
+
+ public string UserHostAddress => "";
+
+ 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 =>
+ remoteIp ??
+ (remoteIp = CheckBadChars(XForwardedFor) ??
+ NormalizeIp(CheckBadChars(XRealIp) ??
+ (string.IsNullOrEmpty(request.Host.Host) ? null : NormalizeIp(request.Host.Host))));
+
+ 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 string CheckBadChars(string name)
+ {
+ if (name == null || 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");
+ }
+
+ break;
+ }
+
+ case 1:
+ {
+ if (c == '\n')
+ {
+ crlf = 2;
+ break;
+ }
+
+ throw new ArgumentException("net_WebHeaderInvalidCRLFChars");
+ }
+
+ case 2:
+ {
+ if (c == ' ' || c == '\t')
+ {
+ crlf = 0;
+ break;
+ }
+
+ throw new ArgumentException("net_WebHeaderInvalidCRLFChars");
+ }
+ }
+ }
+
+ if (crlf != 0)
+ {
+ throw new ArgumentException("net_WebHeaderInvalidCRLFChars");
+ }
+
+ return name;
+ }
+
+ internal static bool ContainsNonAsciiChars(string token)
+ {
+ for (int i = 0; i < token.Length; ++i)
+ {
+ if ((token[i] < 0x20) || (token[i] > 0x7e))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private string NormalizeIp(string ip)
+ {
+ if (!string.IsNullOrWhiteSpace(ip))
+ {
+ // Handle ipv4 mapped to ipv6
+ const string srch = "::ffff:";
+ var index = ip.IndexOf(srch, StringComparison.OrdinalIgnoreCase);
+ if (index == 0)
+ {
+ ip = ip.Substring(srch.Length);
+ }
+ }
+
+ return ip;
+ }
+
+ public bool IsSecureConnection => request.IsHttps || XForwardedProtocol == "https";
+
+ public string[] AcceptTypes => request.Headers.GetCommaSeparatedValues(HeaderNames.Accept);
+
+ private Dictionary items;
+ public Dictionary Items => items ?? (items = new Dictionary());
+
+ 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); // TODO;
+ 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)
+ {
+ string format = httpReq.Query["format"];
+ if (format == null)
+ {
+ const int formatMaxLength = 4;
+ string pi = httpReq.Path.ToString();
+ if (pi == null || pi.Length <= formatMaxLength)
+ {
+ return null;
+ }
+
+ if (pi[0] == '/')
+ {
+ pi = pi.Substring(1);
+ }
+
+ format = LeftPart(pi, '/');
+ if (format.Length > formatMaxLength)
+ {
+ return null;
+ }
+ }
+
+ format = LeftPart(format, '.');
+ if (format.ToLower().Contains("json"))
+ {
+ return "application/json";
+ }
+ else if (format.ToLower().Contains("xml"))
+ {
+ return "application/xml";
+ }
+
+ return null;
+ }
+
+ public static string LeftPart(string strVal, char needle)
+ {
+ if (strVal == null)
+ {
+ return null;
+ }
+
+ var pos = strVal.IndexOf(needle.ToString(), StringComparison.Ordinal);
+ return pos == -1 ? strVal : strVal.Substring(0, pos);
+ }
+
+ public static ReadOnlySpan LeftPart(ReadOnlySpan strVal, char needle)
+ {
+ if (strVal == null)
+ {
+ return null;
+ }
+
+ var pos = strVal.IndexOf(needle.ToString());
+ 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 = request.Path.ToString().IndexOf("?", StringComparison.Ordinal);
+ if (pos != -1)
+ {
+ var path = request.Path.ToString().Substring(0, pos);
+ this.pathInfo = GetPathInfo(
+ path,
+ mode,
+ mode ?? string.Empty);
+ }
+ else
+ {
+ this.pathInfo = request.Path.ToString();
+ }
+
+ this.pathInfo = System.Net.WebUtility.UrlDecode(pathInfo);
+ this.pathInfo = NormalizePathInfo(pathInfo, mode);
+ }
+
+ 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;
+ }
+
+ var path = sbPathInfo.ToString();
+ return path.Length > 1 ? path.TrimEnd('/') : "/";
+ }
+
+ private Dictionary cookies;
+ public IDictionary Cookies
+ {
+ get
+ {
+ if (cookies == null)
+ {
+ cookies = new Dictionary();
+ foreach (var cookie in this.request.Cookies)
+ {
+ var httpCookie = cookie;
+ cookies[httpCookie.Key] = new Cookie(httpCookie.Key, httpCookie.Value, "", "");
+ }
+ }
+
+ return cookies;
+ }
+ }
+
+ public string UserAgent => request.Headers[HeaderNames.UserAgent];
+
+ public QueryParamCollection Headers => new QueryParamCollection(request.Headers);
+
+ private QueryParamCollection queryString;
+ public QueryParamCollection QueryString => queryString ?? (queryString = new QueryParamCollection(request.Query));
+
+ public bool IsLocal => true; // TODO
+
+ private string httpMethod;
+ public string HttpMethod =>
+ httpMethod
+ ?? (httpMethod = request.Method);
+
+ public string Verb => HttpMethod;
+
+ public string ContentType => request.ContentType;
+
+ private Encoding contentEncoding;
+ public Encoding ContentEncoding
+ {
+ get => contentEncoding ?? Encoding.GetEncoding(request.Headers[HeaderNames.ContentEncoding].ToString());
+ set => contentEncoding = value;
+ }
+
+ public Uri UrlReferrer => request.GetTypedHeaders().Referer;
+
+ public static Encoding GetEncoding(string contentTypeHeader)
+ {
+ var param = GetParameter(contentTypeHeader, "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();
+ }
+
+ 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 string NormalizePathInfo(string pathInfo, string handlerPath)
+ {
+ if (handlerPath != null)
+ {
+ var trimmed = pathInfo.TrimStart('/');
+ if (trimmed.StartsWith(handlerPath, StringComparison.OrdinalIgnoreCase))
+ {
+ return trimmed.Substring(handlerPath.Length);
+ }
+ }
+
+ return pathInfo;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpResponse.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpResponse.cs
new file mode 100644
index 0000000000..c68b959846
--- /dev/null
+++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpResponse.cs
@@ -0,0 +1,206 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Net.Sockets;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Emby.Server.Implementations;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Services;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+using IHttpResponse = MediaBrowser.Model.Services.IHttpResponse;
+using IRequest = MediaBrowser.Model.Services.IRequest;
+
+namespace Emby.Server.Implementations.SocketSharp
+{
+ public class WebSocketSharpResponse : IHttpResponse
+ {
+ private readonly ILogger _logger;
+
+ private readonly HttpResponse _response;
+
+ public WebSocketSharpResponse(ILogger logger, HttpResponse response, IRequest request)
+ {
+ _logger = logger;
+ this._response = response;
+ Items = new Dictionary();
+ Request = request;
+ }
+
+ public IRequest Request { get; private set; }
+
+ public Dictionary Items { get; private set; }
+
+ public object OriginalResponse => _response;
+
+ public int StatusCode
+ {
+ get => this._response.StatusCode;
+ set => this._response.StatusCode = value;
+ }
+
+ public string StatusDescription { get; set; }
+
+ public string ContentType
+ {
+ get => _response.ContentType;
+ set => _response.ContentType = value;
+ }
+
+ public QueryParamCollection Headers => new QueryParamCollection(_response.Headers);
+
+ private static string AsHeaderValue(Cookie cookie)
+ {
+ DateTime defaultExpires = DateTime.MinValue;
+
+ var path = cookie.Expires == defaultExpires
+ ? "/"
+ : cookie.Path ?? "/";
+
+ var sb = new StringBuilder();
+
+ sb.Append($"{cookie.Name}={cookie.Value};path={path}");
+
+ if (cookie.Expires != defaultExpires)
+ {
+ sb.Append($";expires={cookie.Expires:R}");
+ }
+
+ if (!string.IsNullOrEmpty(cookie.Domain))
+ {
+ sb.Append($";domain={cookie.Domain}");
+ }
+
+ if (cookie.Secure)
+ {
+ sb.Append(";Secure");
+ }
+
+ if (cookie.HttpOnly)
+ {
+ sb.Append(";HttpOnly");
+ }
+
+ return sb.ToString();
+ }
+
+ public void AddHeader(string name, string value)
+ {
+ if (string.Equals(name, "Content-Type", StringComparison.OrdinalIgnoreCase))
+ {
+ ContentType = value;
+ return;
+ }
+
+ _response.Headers.Add(name, value);
+ }
+
+ public string GetHeader(string name)
+ {
+ return _response.Headers[name];
+ }
+
+ public void Redirect(string url)
+ {
+ _response.Redirect(url);
+ }
+
+ public Stream OutputStream => _response.Body;
+
+ public void Close()
+ {
+ if (!this.IsClosed)
+ {
+ this.IsClosed = true;
+
+ try
+ {
+ var response = this._response;
+
+ var outputStream = response.Body;
+
+ // This is needed with compression
+ outputStream.Flush();
+ outputStream.Dispose();
+ }
+ catch (SocketException)
+ {
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error in HttpListenerResponseWrapper");
+ }
+ }
+ }
+
+ public bool IsClosed
+ {
+ get;
+ private set;
+ }
+
+ public void SetContentLength(long contentLength)
+ {
+ // you can happily set the Content-Length header in Asp.Net
+ // but HttpListener will complain if you do - you have to set ContentLength64 on the response.
+ // workaround: HttpListener throws "The parameter is incorrect" exceptions when we try to set the Content-Length header
+ //_response.ContentLength64 = contentLength;
+ }
+
+ public void SetCookie(Cookie cookie)
+ {
+ var cookieStr = AsHeaderValue(cookie);
+ _response.Headers.Add("Set-Cookie", cookieStr);
+ }
+
+ public bool SendChunked { get; set; }
+
+ public bool KeepAlive { get; set; }
+
+ public void ClearCookies()
+ {
+ }
+ const int StreamCopyToBufferSize = 81920;
+ public async Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, IFileSystem fileSystem, IStreamHelper streamHelper, CancellationToken cancellationToken)
+ {
+ // TODO
+ // return _response.TransmitFile(path, offset, count, fileShareMode, 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);
+ }
+ }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Startup.cs b/Emby.Server.Implementations/Startup.cs
new file mode 100644
index 0000000000..164c7eeaa7
--- /dev/null
+++ b/Emby.Server.Implementations/Startup.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Linq;
+using MediaBrowser.Api;
+using MediaBrowser.Controller;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Emby.Server.Implementations
+{
+ public class Startup
+ {
+ public IConfiguration Configuration { get; }
+
+ public Startup(IConfiguration configuration) => Configuration = configuration;
+
+ // Use this method to add services to the container.
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddRouting();
+ }
+
+ // Use this method to configure the HTTP request pipeline.
+ public void Configure(IApplicationBuilder app)
+ {
+
+ }
+ }
+
+}
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index 41ee73a565..bfc50468f7 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -20,6 +20,7 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
+using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -143,7 +144,7 @@ namespace Jellyfin.Server
appHost.ImageProcessor.ImageEncoder = GetImageEncoder(fileSystem, appPaths, appHost.LocalizationManager);
await appHost.RunStartupTasks().ConfigureAwait(false);
-
+ appHost.Host.Run();
// TODO: read input for a stop command
try
diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj
index f17fd7159d..2f1ea13d9b 100644
--- a/MediaBrowser.Model/MediaBrowser.Model.csproj
+++ b/MediaBrowser.Model/MediaBrowser.Model.csproj
@@ -1,4 +1,4 @@
-
+
Jellyfin Contributors
@@ -13,6 +13,8 @@
+
+
diff --git a/MediaBrowser.Model/Services/QueryParamCollection.cs b/MediaBrowser.Model/Services/QueryParamCollection.cs
index 0e0ebf848b..09806ed9c2 100644
--- a/MediaBrowser.Model/Services/QueryParamCollection.cs
+++ b/MediaBrowser.Model/Services/QueryParamCollection.cs
@@ -4,6 +4,7 @@ using System.Collections.Specialized;
using System.Linq;
using System.Net;
using MediaBrowser.Model.Dto;
+using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Model.Services
{
@@ -23,6 +24,14 @@ namespace MediaBrowser.Model.Services
}
}
+ public QueryParamCollection(Microsoft.AspNetCore.Http.IHeaderDictionary headers)
+ {
+ foreach (var pair in headers)
+ {
+ Add(pair.Key, pair.Value);
+ }
+ }
+
// TODO remove this shit
public QueryParamCollection(WebHeaderCollection webHeaderCollection)
{
@@ -47,6 +56,14 @@ namespace MediaBrowser.Model.Services
}
}
+ public QueryParamCollection(IQueryCollection queryCollection)
+ {
+ foreach (var pair in queryCollection)
+ {
+ Add(pair.Key, pair.Value);
+ }
+ }
+
private static StringComparison GetStringComparison()
{
return StringComparison.OrdinalIgnoreCase;