mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-01-15 23:58:57 +00:00
update portable projects
This commit is contained in:
43
Emby.Server.Implementations/HttpServer/LoggerUtils.cs
Normal file
43
Emby.Server.Implementations/HttpServer/LoggerUtils.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using MediaBrowser.Model.Logging;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using SocketHttpListener.Net;
|
||||
|
||||
namespace Emby.Server.Implementations.HttpServer
|
||||
{
|
||||
public static class LoggerUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// Logs the request.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="request">The request.</param>
|
||||
public static void LogRequest(ILogger logger, HttpListenerRequest request)
|
||||
{
|
||||
var url = request.Url.ToString();
|
||||
|
||||
logger.Info("{0} {1}. UserAgent: {2}", request.IsWebSocketRequest ? "WS" : "HTTP " + request.HttpMethod, url, request.UserAgent ?? string.Empty);
|
||||
}
|
||||
|
||||
public static void LogRequest(ILogger logger, string url, string method, string userAgent)
|
||||
{
|
||||
logger.Info("{0} {1}. UserAgent: {2}", "HTTP " + method, url, userAgent ?? string.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs the response.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="statusCode">The status code.</param>
|
||||
/// <param name="url">The URL.</param>
|
||||
/// <param name="endPoint">The end point.</param>
|
||||
/// <param name="duration">The duration.</param>
|
||||
public static void LogResponse(ILogger logger, int statusCode, string url, string endPoint, TimeSpan duration)
|
||||
{
|
||||
var durationMs = duration.TotalMilliseconds;
|
||||
var logSuffix = durationMs >= 1000 && durationMs < 60000 ? "ms (slow)" : "ms";
|
||||
|
||||
logger.Info("HTTP Response {0} to {1}. Time: {2}{3}. {4}", statusCode, endPoint, Convert.ToInt32(durationMs).ToString(CultureInfo.InvariantCulture), logSuffix, url);
|
||||
}
|
||||
}
|
||||
}
|
||||
224
Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs
Normal file
224
Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs
Normal file
@@ -0,0 +1,224 @@
|
||||
using MediaBrowser.Model.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.Services;
|
||||
|
||||
namespace Emby.Server.Implementations.HttpServer
|
||||
{
|
||||
public class RangeRequestWriter : IAsyncStreamWriter, IHttpResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the source stream.
|
||||
/// </summary>
|
||||
/// <value>The source stream.</value>
|
||||
private Stream SourceStream { get; set; }
|
||||
private string RangeHeader { get; set; }
|
||||
private bool IsHeadRequest { get; set; }
|
||||
|
||||
private long RangeStart { get; set; }
|
||||
private long RangeEnd { get; set; }
|
||||
private long RangeLength { get; set; }
|
||||
private long TotalContentLength { get; set; }
|
||||
|
||||
public Action OnComplete { get; set; }
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private const int BufferSize = 81920;
|
||||
|
||||
/// <summary>
|
||||
/// The _options
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, string> _options = new Dictionary<string, string>();
|
||||
|
||||
/// <summary>
|
||||
/// The us culture
|
||||
/// </summary>
|
||||
private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
|
||||
|
||||
public List<Cookie> Cookies { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional HTTP Headers
|
||||
/// </summary>
|
||||
/// <value>The headers.</value>
|
||||
public IDictionary<string, string> Headers
|
||||
{
|
||||
get { return _options; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StreamWriter" /> class.
|
||||
/// </summary>
|
||||
/// <param name="rangeHeader">The range header.</param>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="contentType">Type of the content.</param>
|
||||
/// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
|
||||
public RangeRequestWriter(string rangeHeader, Stream source, string contentType, bool isHeadRequest, ILogger logger)
|
||||
{
|
||||
if (string.IsNullOrEmpty(contentType))
|
||||
{
|
||||
throw new ArgumentNullException("contentType");
|
||||
}
|
||||
|
||||
RangeHeader = rangeHeader;
|
||||
SourceStream = source;
|
||||
IsHeadRequest = isHeadRequest;
|
||||
this._logger = logger;
|
||||
|
||||
ContentType = contentType;
|
||||
Headers["Content-Type"] = contentType;
|
||||
Headers["Accept-Ranges"] = "bytes";
|
||||
StatusCode = HttpStatusCode.PartialContent;
|
||||
|
||||
Cookies = new List<Cookie>();
|
||||
SetRangeValues();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the range values.
|
||||
/// </summary>
|
||||
private void SetRangeValues()
|
||||
{
|
||||
var requestedRange = RequestedRanges[0];
|
||||
|
||||
TotalContentLength = SourceStream.Length;
|
||||
|
||||
// If the requested range is "0-", we can optimize by just doing a stream copy
|
||||
if (!requestedRange.Value.HasValue)
|
||||
{
|
||||
RangeEnd = TotalContentLength - 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
RangeEnd = requestedRange.Value.Value;
|
||||
}
|
||||
|
||||
RangeStart = requestedRange.Key;
|
||||
RangeLength = 1 + RangeEnd - RangeStart;
|
||||
|
||||
// Content-Length is the length of what we're serving, not the original content
|
||||
Headers["Content-Length"] = RangeLength.ToString(UsCulture);
|
||||
Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", RangeStart, RangeEnd, TotalContentLength);
|
||||
|
||||
if (RangeStart > 0)
|
||||
{
|
||||
SourceStream.Position = RangeStart;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The _requested ranges
|
||||
/// </summary>
|
||||
private List<KeyValuePair<long, long?>> _requestedRanges;
|
||||
/// <summary>
|
||||
/// Gets the requested ranges.
|
||||
/// </summary>
|
||||
/// <value>The requested ranges.</value>
|
||||
protected List<KeyValuePair<long, long?>> RequestedRanges
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_requestedRanges == null)
|
||||
{
|
||||
_requestedRanges = new List<KeyValuePair<long, long?>>();
|
||||
|
||||
// Example: bytes=0-,32-63
|
||||
var ranges = RangeHeader.Split('=')[1].Split(',');
|
||||
|
||||
foreach (var range in ranges)
|
||||
{
|
||||
var vals = range.Split('-');
|
||||
|
||||
long start = 0;
|
||||
long? end = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(vals[0]))
|
||||
{
|
||||
start = long.Parse(vals[0], UsCulture);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(vals[1]))
|
||||
{
|
||||
end = long.Parse(vals[1], UsCulture);
|
||||
}
|
||||
|
||||
_requestedRanges.Add(new KeyValuePair<long, long?>(start, end));
|
||||
}
|
||||
}
|
||||
|
||||
return _requestedRanges;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Headers only
|
||||
if (IsHeadRequest)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using (var source = SourceStream)
|
||||
{
|
||||
// If the requested range is "0-", we can optimize by just doing a stream copy
|
||||
if (RangeEnd >= TotalContentLength - 1)
|
||||
{
|
||||
await source.CopyToAsync(responseStream, BufferSize).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await CopyToInternalAsync(source, responseStream, RangeLength).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (OnComplete != null)
|
||||
{
|
||||
OnComplete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CopyToInternalAsync(Stream source, Stream destination, long copyLength)
|
||||
{
|
||||
var array = new byte[BufferSize];
|
||||
int count;
|
||||
while ((count = await source.ReadAsync(array, 0, array.Length).ConfigureAwait(false)) != 0)
|
||||
{
|
||||
var bytesToCopy = Math.Min(count, copyLength);
|
||||
|
||||
await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToCopy)).ConfigureAwait(false);
|
||||
|
||||
copyLength -= bytesToCopy;
|
||||
|
||||
if (copyLength <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string ContentType { get; set; }
|
||||
|
||||
public IRequest RequestContext { get; set; }
|
||||
|
||||
public object Response { get; set; }
|
||||
|
||||
public int Status { get; set; }
|
||||
|
||||
public HttpStatusCode StatusCode
|
||||
{
|
||||
get { return (HttpStatusCode)Status; }
|
||||
set { Status = (int)value; }
|
||||
}
|
||||
|
||||
public string StatusDescription { get; set; }
|
||||
}
|
||||
}
|
||||
129
Emby.Server.Implementations/HttpServer/ResponseFilter.cs
Normal file
129
Emby.Server.Implementations/HttpServer/ResponseFilter.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
using MediaBrowser.Model.Logging;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using Emby.Server.Implementations.HttpServer.SocketSharp;
|
||||
using MediaBrowser.Model.Services;
|
||||
|
||||
namespace Emby.Server.Implementations.HttpServer
|
||||
{
|
||||
public class ResponseFilter
|
||||
{
|
||||
private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public ResponseFilter(ILogger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters the response.
|
||||
/// </summary>
|
||||
/// <param name="req">The req.</param>
|
||||
/// <param name="res">The res.</param>
|
||||
/// <param name="dto">The dto.</param>
|
||||
public void FilterResponse(IRequest req, IResponse res, object dto)
|
||||
{
|
||||
// Try to prevent compatibility view
|
||||
res.AddHeader("X-UA-Compatible", "IE=Edge");
|
||||
res.AddHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization");
|
||||
res.AddHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
|
||||
res.AddHeader("Access-Control-Allow-Origin", "*");
|
||||
|
||||
var exception = dto as Exception;
|
||||
|
||||
if (exception != null)
|
||||
{
|
||||
_logger.ErrorException("Error processing request for {0}", exception, req.RawUrl);
|
||||
|
||||
if (!string.IsNullOrEmpty(exception.Message))
|
||||
{
|
||||
var error = exception.Message.Replace(Environment.NewLine, " ");
|
||||
error = RemoveControlCharacters(error);
|
||||
|
||||
res.AddHeader("X-Application-Error-Code", error);
|
||||
}
|
||||
}
|
||||
|
||||
var vary = "Accept-Encoding";
|
||||
|
||||
var hasHeaders = dto as IHasHeaders;
|
||||
var sharpResponse = res as WebSocketSharpResponse;
|
||||
|
||||
if (hasHeaders != null)
|
||||
{
|
||||
if (!hasHeaders.Headers.ContainsKey("Server"))
|
||||
{
|
||||
hasHeaders.Headers["Server"] = "Mono-HTTPAPI/1.1, UPnP/1.0 DLNADOC/1.50";
|
||||
//hasHeaders.Headers["Server"] = "Mono-HTTPAPI/1.1";
|
||||
}
|
||||
|
||||
// Content length has to be explicitly set on on HttpListenerResponse or it won't be happy
|
||||
string contentLength;
|
||||
|
||||
if (hasHeaders.Headers.TryGetValue("Content-Length", out contentLength) && !string.IsNullOrEmpty(contentLength))
|
||||
{
|
||||
var length = long.Parse(contentLength, UsCulture);
|
||||
|
||||
if (length > 0)
|
||||
{
|
||||
res.SetContentLength(length);
|
||||
|
||||
//var listenerResponse = res.OriginalResponse as HttpListenerResponse;
|
||||
|
||||
//if (listenerResponse != null)
|
||||
//{
|
||||
// // Disable chunked encoding. Technically this is only needed when using Content-Range, but
|
||||
// // anytime we know the content length there's no need for it
|
||||
// listenerResponse.SendChunked = false;
|
||||
// return;
|
||||
//}
|
||||
|
||||
if (sharpResponse != null)
|
||||
{
|
||||
sharpResponse.SendChunked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
string hasHeadersVary;
|
||||
if (hasHeaders.Headers.TryGetValue("Vary", out hasHeadersVary))
|
||||
{
|
||||
vary = hasHeadersVary;
|
||||
}
|
||||
|
||||
hasHeaders.Headers["Vary"] = vary;
|
||||
}
|
||||
|
||||
//res.KeepAlive = false;
|
||||
|
||||
// Per Google PageSpeed
|
||||
// This instructs the proxies to cache two versions of the resource: one compressed, and one uncompressed.
|
||||
// The correct version of the resource is delivered based on the client request header.
|
||||
// This is a good choice for applications that are singly homed and depend on public proxies for user locality.
|
||||
res.AddHeader("Vary", vary);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the control characters.
|
||||
/// </summary>
|
||||
/// <param name="inString">The in string.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
public static string RemoveControlCharacters(string inString)
|
||||
{
|
||||
if (inString == null) return null;
|
||||
|
||||
var newString = new StringBuilder();
|
||||
|
||||
foreach (var ch in inString)
|
||||
{
|
||||
if (!char.IsControl(ch))
|
||||
{
|
||||
newString.Append(ch);
|
||||
}
|
||||
}
|
||||
return newString.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using SocketHttpListener.Net;
|
||||
|
||||
namespace Emby.Server.Implementations.HttpServer.SocketSharp
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static string GetOperationName(this HttpListenerRequest request)
|
||||
{
|
||||
return request.Url.Segments[request.Url.Segments.Length - 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
using MediaBrowser.Common.Events;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using WebSocketState = MediaBrowser.Model.Net.WebSocketState;
|
||||
|
||||
namespace Emby.Server.Implementations.HttpServer.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 SocketHttpListener.WebSocket WebSocket { get; set; }
|
||||
|
||||
private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
public SharpWebSocket(SocketHttpListener.WebSocket socket, ILogger logger)
|
||||
{
|
||||
if (socket == null)
|
||||
{
|
||||
throw new ArgumentNullException("socket");
|
||||
}
|
||||
|
||||
if (logger == null)
|
||||
{
|
||||
throw new ArgumentNullException("logger");
|
||||
}
|
||||
|
||||
_logger = logger;
|
||||
WebSocket = socket;
|
||||
|
||||
socket.OnMessage += socket_OnMessage;
|
||||
socket.OnClose += socket_OnClose;
|
||||
socket.OnError += socket_OnError;
|
||||
|
||||
WebSocket.ConnectAsServer();
|
||||
}
|
||||
|
||||
void socket_OnError(object sender, SocketHttpListener.ErrorEventArgs e)
|
||||
{
|
||||
_logger.Error("Error in SharpWebSocket: {0}", e.Message ?? string.Empty);
|
||||
//EventHelper.FireEventIfNotNull(Closed, this, EventArgs.Empty, _logger);
|
||||
}
|
||||
|
||||
void socket_OnClose(object sender, SocketHttpListener.CloseEventArgs e)
|
||||
{
|
||||
EventHelper.FireEventIfNotNull(Closed, this, EventArgs.Empty, _logger);
|
||||
}
|
||||
|
||||
void socket_OnMessage(object sender, SocketHttpListener.MessageEventArgs e)
|
||||
{
|
||||
//if (!string.IsNullOrWhiteSpace(e.Data))
|
||||
//{
|
||||
// if (OnReceive != null)
|
||||
// {
|
||||
// OnReceive(e.Data);
|
||||
// }
|
||||
// return;
|
||||
//}
|
||||
if (OnReceiveBytes != null)
|
||||
{
|
||||
OnReceiveBytes(e.RawData);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the state.
|
||||
/// </summary>
|
||||
/// <value>The state.</value>
|
||||
public WebSocketState State
|
||||
{
|
||||
get
|
||||
{
|
||||
WebSocketState commonState;
|
||||
|
||||
if (!Enum.TryParse(WebSocket.ReadyState.ToString(), true, out commonState))
|
||||
{
|
||||
_logger.Warn("Unrecognized WebSocketState: {0}", WebSocket.ReadyState.ToString());
|
||||
}
|
||||
|
||||
return commonState;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
var completionSource = new TaskCompletionSource<bool>();
|
||||
|
||||
WebSocket.SendAsync(bytes, res => completionSource.TrySetResult(true));
|
||||
|
||||
return completionSource.Task;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
var completionSource = new TaskCompletionSource<bool>();
|
||||
|
||||
WebSocket.SendAsync(text, res => completionSource.TrySetResult(true));
|
||||
|
||||
return completionSource.Task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
/// <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 (dispose)
|
||||
{
|
||||
WebSocket.OnMessage -= socket_OnMessage;
|
||||
WebSocket.OnClose -= socket_OnClose;
|
||||
WebSocket.OnError -= socket_OnError;
|
||||
|
||||
_cancellationTokenSource.Cancel();
|
||||
|
||||
WebSocket.Close();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the receive action.
|
||||
/// </summary>
|
||||
/// <value>The receive action.</value>
|
||||
public Action<byte[]> OnReceiveBytes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the on receive.
|
||||
/// </summary>
|
||||
/// <value>The on receive.</value>
|
||||
public Action<string> OnReceive { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using SocketHttpListener.Net;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Server.Implementations.Logging;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Model.Cryptography;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Model.Services;
|
||||
using MediaBrowser.Model.Text;
|
||||
using SocketHttpListener.Primitives;
|
||||
|
||||
namespace Emby.Server.Implementations.HttpServer.SocketSharp
|
||||
{
|
||||
public class WebSocketSharpListener : IHttpListener
|
||||
{
|
||||
private HttpListener _listener;
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private readonly ICertificate _certificate;
|
||||
private readonly IMemoryStreamFactory _memoryStreamProvider;
|
||||
private readonly ITextEncoding _textEncoding;
|
||||
private readonly INetworkManager _networkManager;
|
||||
private readonly ISocketFactory _socketFactory;
|
||||
private readonly ICryptoProvider _cryptoProvider;
|
||||
private readonly IStreamFactory _streamFactory;
|
||||
private readonly Func<HttpListenerContext, IHttpRequest> _httpRequestFactory;
|
||||
|
||||
public WebSocketSharpListener(ILogger logger, ICertificate certificate, IMemoryStreamFactory memoryStreamProvider, ITextEncoding textEncoding, INetworkManager networkManager, ISocketFactory socketFactory, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, Func<HttpListenerContext, IHttpRequest> httpRequestFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_certificate = certificate;
|
||||
_memoryStreamProvider = memoryStreamProvider;
|
||||
_textEncoding = textEncoding;
|
||||
_networkManager = networkManager;
|
||||
_socketFactory = socketFactory;
|
||||
_cryptoProvider = cryptoProvider;
|
||||
_streamFactory = streamFactory;
|
||||
_httpRequestFactory = httpRequestFactory;
|
||||
}
|
||||
|
||||
public Action<Exception, IRequest> ErrorHandler { get; set; }
|
||||
public Func<IHttpRequest, Uri, Task> RequestHandler { get; set; }
|
||||
|
||||
public Action<WebSocketConnectingEventArgs> WebSocketConnecting { get; set; }
|
||||
|
||||
public Action<WebSocketConnectEventArgs> WebSocketConnected { get; set; }
|
||||
|
||||
public void Start(IEnumerable<string> urlPrefixes)
|
||||
{
|
||||
if (_listener == null)
|
||||
_listener = new HttpListener(new PatternsLogger(_logger), _cryptoProvider, _streamFactory, _socketFactory, _networkManager, _textEncoding, _memoryStreamProvider);
|
||||
|
||||
if (_certificate != null)
|
||||
{
|
||||
_listener.LoadCert(_certificate);
|
||||
}
|
||||
|
||||
foreach (var prefix in urlPrefixes)
|
||||
{
|
||||
_logger.Info("Adding HttpListener prefix " + prefix);
|
||||
_listener.Prefixes.Add(prefix);
|
||||
}
|
||||
|
||||
_listener.OnContext = ProcessContext;
|
||||
|
||||
_listener.Start();
|
||||
}
|
||||
|
||||
private void ProcessContext(HttpListenerContext context)
|
||||
{
|
||||
Task.Factory.StartNew(() => InitTask(context));
|
||||
}
|
||||
|
||||
private Task InitTask(HttpListenerContext context)
|
||||
{
|
||||
IHttpRequest httpReq = null;
|
||||
var request = context.Request;
|
||||
|
||||
try
|
||||
{
|
||||
if (request.IsWebSocketRequest)
|
||||
{
|
||||
LoggerUtils.LogRequest(_logger, request);
|
||||
|
||||
ProcessWebSocketRequest(context);
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
httpReq = GetRequest(context);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error processing request", ex);
|
||||
|
||||
httpReq = httpReq ?? GetRequest(context);
|
||||
ErrorHandler(ex, httpReq);
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
return RequestHandler(httpReq, request.Url);
|
||||
}
|
||||
|
||||
private void ProcessWebSocketRequest(HttpListenerContext ctx)
|
||||
{
|
||||
try
|
||||
{
|
||||
var endpoint = ctx.Request.RemoteEndPoint.ToString();
|
||||
var url = ctx.Request.RawUrl;
|
||||
|
||||
var connectingArgs = new WebSocketConnectingEventArgs
|
||||
{
|
||||
Url = url,
|
||||
QueryString = ctx.Request.QueryString,
|
||||
Endpoint = endpoint
|
||||
};
|
||||
|
||||
if (WebSocketConnecting != null)
|
||||
{
|
||||
WebSocketConnecting(connectingArgs);
|
||||
}
|
||||
|
||||
if (connectingArgs.AllowConnection)
|
||||
{
|
||||
_logger.Debug("Web socket connection allowed");
|
||||
|
||||
var webSocketContext = ctx.AcceptWebSocket(null);
|
||||
|
||||
if (WebSocketConnected != null)
|
||||
{
|
||||
WebSocketConnected(new WebSocketConnectEventArgs
|
||||
{
|
||||
Url = url,
|
||||
QueryString = ctx.Request.QueryString,
|
||||
WebSocket = new SharpWebSocket(webSocketContext.WebSocket, _logger),
|
||||
Endpoint = endpoint
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Warn("Web socket connection not allowed");
|
||||
ctx.Response.StatusCode = 401;
|
||||
ctx.Response.Close();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("AcceptWebSocketAsync error", ex);
|
||||
ctx.Response.StatusCode = 500;
|
||||
ctx.Response.Close();
|
||||
}
|
||||
}
|
||||
|
||||
private IHttpRequest GetRequest(HttpListenerContext httpContext)
|
||||
{
|
||||
return _httpRequestFactory(httpContext);
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
if (_listener != null)
|
||||
{
|
||||
foreach (var prefix in _listener.Prefixes.ToList())
|
||||
{
|
||||
_listener.Prefixes.Remove(prefix);
|
||||
}
|
||||
|
||||
_listener.Close();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
private bool _disposed;
|
||||
private readonly object _disposeLock = new object();
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
||||
lock (_disposeLock)
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
//release unmanaged resources here...
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using HttpListenerResponse = SocketHttpListener.Net.HttpListenerResponse;
|
||||
using IHttpResponse = MediaBrowser.Model.Services.IHttpResponse;
|
||||
using IRequest = MediaBrowser.Model.Services.IRequest;
|
||||
|
||||
namespace Emby.Server.Implementations.HttpServer.SocketSharp
|
||||
{
|
||||
public class WebSocketSharpResponse : IHttpResponse
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly HttpListenerResponse _response;
|
||||
|
||||
public WebSocketSharpResponse(ILogger logger, HttpListenerResponse response, IRequest request)
|
||||
{
|
||||
_logger = logger;
|
||||
this._response = response;
|
||||
Items = new Dictionary<string, object>();
|
||||
Request = request;
|
||||
}
|
||||
|
||||
public IRequest Request { get; private set; }
|
||||
public bool UseBufferedStream { get; set; }
|
||||
public Dictionary<string, object> Items { get; private set; }
|
||||
public object OriginalResponse
|
||||
{
|
||||
get { return _response; }
|
||||
}
|
||||
|
||||
public int StatusCode
|
||||
{
|
||||
get { return this._response.StatusCode; }
|
||||
set { this._response.StatusCode = value; }
|
||||
}
|
||||
|
||||
public string StatusDescription
|
||||
{
|
||||
get { return this._response.StatusDescription; }
|
||||
set { this._response.StatusDescription = value; }
|
||||
}
|
||||
|
||||
public string ContentType
|
||||
{
|
||||
get { return _response.ContentType; }
|
||||
set { _response.ContentType = value; }
|
||||
}
|
||||
|
||||
//public ICookies Cookies { get; set; }
|
||||
|
||||
public void AddHeader(string name, string value)
|
||||
{
|
||||
if (string.Equals(name, "Content-Type", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ContentType = value;
|
||||
return;
|
||||
}
|
||||
|
||||
_response.AddHeader(name, value);
|
||||
}
|
||||
|
||||
public string GetHeader(string name)
|
||||
{
|
||||
return _response.Headers[name];
|
||||
}
|
||||
|
||||
public void Redirect(string url)
|
||||
{
|
||||
_response.Redirect(url);
|
||||
}
|
||||
|
||||
public Stream OutputStream
|
||||
{
|
||||
get { return _response.OutputStream; }
|
||||
}
|
||||
|
||||
public object Dto { get; set; }
|
||||
|
||||
public void Write(string text)
|
||||
{
|
||||
var bOutput = System.Text.Encoding.UTF8.GetBytes(text);
|
||||
_response.ContentLength64 = bOutput.Length;
|
||||
|
||||
var outputStream = _response.OutputStream;
|
||||
outputStream.Write(bOutput, 0, bOutput.Length);
|
||||
Close();
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
if (!this.IsClosed)
|
||||
{
|
||||
this.IsClosed = true;
|
||||
|
||||
try
|
||||
{
|
||||
CloseOutputStream(this._response);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error closing HttpListener output stream", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void CloseOutputStream(HttpListenerResponse response)
|
||||
{
|
||||
try
|
||||
{
|
||||
response.OutputStream.Flush();
|
||||
response.OutputStream.Dispose();
|
||||
response.Close();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error in HttpListenerResponseWrapper: " + ex.Message, ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void End()
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
public void Flush()
|
||||
{
|
||||
_response.OutputStream.Flush();
|
||||
}
|
||||
|
||||
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 static string AsHeaderValue(Cookie cookie)
|
||||
{
|
||||
var 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}");
|
||||
}
|
||||
//else if (restrictAllCookiesToDomain != null)
|
||||
//{
|
||||
// sb.Append($";domain={restrictAllCookiesToDomain}");
|
||||
//}
|
||||
|
||||
if (cookie.Secure)
|
||||
{
|
||||
sb.Append(";Secure");
|
||||
}
|
||||
if (cookie.HttpOnly)
|
||||
{
|
||||
sb.Append(";HttpOnly");
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
|
||||
public bool SendChunked
|
||||
{
|
||||
get { return _response.SendChunked; }
|
||||
set { _response.SendChunked = value; }
|
||||
}
|
||||
|
||||
public bool KeepAlive { get; set; }
|
||||
|
||||
public void ClearCookies()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user