aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Server.Implementations/HttpServer
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.Server.Implementations/HttpServer')
-rw-r--r--MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs339
-rw-r--r--MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs138
-rw-r--r--MediaBrowser.Server.Implementations/HttpServer/IHttpListener.cs42
-rw-r--r--MediaBrowser.Server.Implementations/HttpServer/LoggerUtils.cs28
-rw-r--r--MediaBrowser.Server.Implementations/HttpServer/NativeWebSocket.cs21
-rw-r--r--MediaBrowser.Server.Implementations/HttpServer/NetListener/HttpListenerServer.cs299
-rw-r--r--MediaBrowser.Server.Implementations/HttpServer/RangeRequestWriter.cs17
-rw-r--r--MediaBrowser.Server.Implementations/HttpServer/ResponseFilter.cs25
-rw-r--r--MediaBrowser.Server.Implementations/HttpServer/Security/AuthService.cs164
-rw-r--r--MediaBrowser.Server.Implementations/HttpServer/Security/AuthorizationContext.cs104
-rw-r--r--MediaBrowser.Server.Implementations/HttpServer/Security/SessionAuthProvider.cs35
-rw-r--r--MediaBrowser.Server.Implementations/HttpServer/Security/SessionContext.cs36
-rw-r--r--MediaBrowser.Server.Implementations/HttpServer/SocketSharp/Extensions.cs28
-rw-r--r--MediaBrowser.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs918
-rw-r--r--MediaBrowser.Server.Implementations/HttpServer/SocketSharp/SharpWebSocket.cs165
-rw-r--r--MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs278
-rw-r--r--MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs402
-rw-r--r--MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs144
-rw-r--r--MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs13
-rw-r--r--MediaBrowser.Server.Implementations/HttpServer/ThrottledStream.cs389
20 files changed, 3235 insertions, 350 deletions
diff --git a/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs b/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs
index 0fc9265f6..1cec4461b 100644
--- a/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs
+++ b/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs
@@ -1,11 +1,11 @@
-using System.Net.Sockets;
-using System.Runtime.Serialization;
-using Funq;
+using Funq;
using MediaBrowser.Common;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Logging;
+using MediaBrowser.Server.Implementations.HttpServer.NetListener;
+using MediaBrowser.Server.Implementations.HttpServer.SocketSharp;
using ServiceStack;
using ServiceStack.Api.Swagger;
using ServiceStack.Host;
@@ -14,11 +14,9 @@ using ServiceStack.Host.HttpListener;
using ServiceStack.Logging;
using ServiceStack.Web;
using System;
-using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using System.Net;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
@@ -27,7 +25,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer
{
public class HttpListenerHost : ServiceStackHost, IHttpServer
{
- private string ServerName { get; set; }
private string HandlerPath { get; set; }
private string DefaultRedirectPath { get; set; }
@@ -36,14 +33,10 @@ namespace MediaBrowser.Server.Implementations.HttpServer
private readonly List<IRestfulService> _restServices = new List<IRestfulService>();
- private HttpListener Listener { get; set; }
- protected bool IsStarted = false;
-
- private readonly List<AutoResetEvent> _autoResetEvents = new List<AutoResetEvent>();
+ private IHttpListener _listener;
private readonly ContainerAdapter _containerAdapter;
- private readonly ConcurrentDictionary<string, string> _localEndPoints = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
public event EventHandler<WebSocketConnectEventArgs> WebSocketConnected;
/// <summary>
@@ -52,24 +45,18 @@ namespace MediaBrowser.Server.Implementations.HttpServer
/// <value>The local end points.</value>
public IEnumerable<string> LocalEndPoints
{
- get { return _localEndPoints.Keys.ToList(); }
+ get { return _listener == null ? new List<string>() : _listener.LocalEndPoints; }
}
public HttpListenerHost(IApplicationHost applicationHost, ILogManager logManager, string serviceName, string handlerPath, string defaultRedirectPath, params Assembly[] assembliesWithServices)
: base(serviceName, assembliesWithServices)
{
DefaultRedirectPath = defaultRedirectPath;
- ServerName = serviceName;
HandlerPath = handlerPath;
_logger = logManager.GetLogger("HttpServer");
_containerAdapter = new ContainerAdapter(applicationHost);
-
- for (var i = 0; i < 1; i++)
- {
- _autoResetEvents.Add(new AutoResetEvent(false));
- }
}
public override void Configure(Container container)
@@ -81,7 +68,8 @@ namespace MediaBrowser.Server.Implementations.HttpServer
{typeof (InvalidOperationException), 422},
{typeof (ResourceNotFoundException), 404},
{typeof (FileNotFoundException), 404},
- {typeof (DirectoryNotFoundException), 404}
+ {typeof (DirectoryNotFoundException), 404},
+ {typeof (Implementations.Security.AuthenticationException), 401}
};
HostConfig.Instance.DebugMode = true;
@@ -95,7 +83,21 @@ namespace MediaBrowser.Server.Implementations.HttpServer
container.Adapter = _containerAdapter;
Plugins.Add(new SwaggerFeature());
- Plugins.Add(new CorsFeature(allowedHeaders: "Content-Type, Authorization"));
+ Plugins.Add(new CorsFeature(allowedHeaders: "Content-Type, Authorization, Range, X-MediaBrowser-Token"));
+
+ //Plugins.Add(new AuthFeature(() => new AuthUserSession(), new IAuthProvider[] {
+ // new SessionAuthProvider(_containerAdapter.Resolve<ISessionContext>()),
+ //}));
+
+ PreRequestFilters.Add((httpReq, httpRes) =>
+ {
+ //Handles Request and closes Responses after emitting global HTTP Headers
+ if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase))
+ {
+ httpRes.EndRequest(); //add a 'using ServiceStack;'
+ }
+ });
+
HostContext.GlobalResponseFilters.Add(new ResponseFilter(_logger).FilterResponse);
}
@@ -154,213 +156,39 @@ namespace MediaBrowser.Server.Implementations.HttpServer
/// </summary>
private void StartListener()
{
- // *** Already running - just leave it in place
- if (IsStarted)
- return;
-
- if (Listener == null)
- Listener = new HttpListener();
-
HostContext.Config.HandlerFactoryPath = ListenerRequest.GetHandlerPathIfAny(UrlPrefixes.First());
- foreach (var prefix in UrlPrefixes)
- {
- _logger.Info("Adding HttpListener prefix " + prefix);
- Listener.Prefixes.Add(prefix);
- }
+ _listener = NativeWebSocket.IsSupported
+ ? _listener = new HttpListenerServer(_logger)
+ //? _listener = new WebSocketSharpListener(_logger)
+ : _listener = new WebSocketSharpListener(_logger);
- IsStarted = true;
- _logger.Info("Starting HttpListner");
- Listener.Start();
-
- for (var i = 0; i < _autoResetEvents.Count; i++)
- {
- var index = i;
- ThreadPool.QueueUserWorkItem(o => Listen(o, index));
- }
- }
+ _listener.WebSocketHandler = WebSocketHandler;
+ _listener.ErrorHandler = ErrorHandler;
+ _listener.RequestHandler = RequestHandler;
- private bool IsListening
- {
- get { return this.IsStarted && this.Listener != null && this.Listener.IsListening; }
+ _listener.Start(UrlPrefixes);
}
- // Loop here to begin processing of new requests.
- private void Listen(object state, int index)
+ private void WebSocketHandler(WebSocketConnectEventArgs args)
{
- while (IsListening)
+ if (WebSocketConnected != null)
{
- if (Listener == null) return;
-
- try
- {
- Listener.BeginGetContext(c => ListenerCallback(c, index), Listener);
-
- _autoResetEvents[index].WaitOne();
- }
- catch (Exception ex)
- {
- _logger.Error("Listen()", ex);
- return;
- }
- if (Listener == null) return;
+ WebSocketConnected(this, args);
}
}
- // Handle the processing of a request in here.
- private void ListenerCallback(IAsyncResult asyncResult, int index)
+ private void ErrorHandler(Exception ex, IRequest httpReq)
{
- var listener = asyncResult.AsyncState as HttpListener;
- HttpListenerContext context = null;
-
- if (listener == null) return;
-
try
{
- if (!IsListening)
- {
- _logger.Debug("Ignoring ListenerCallback() as HttpListener is no longer listening");
- return;
- }
- // The EndGetContext() method, as with all Begin/End asynchronous methods in the .NET Framework,
- // blocks until there is a request to be processed or some type of data is available.
- context = listener.EndGetContext(asyncResult);
- }
- catch (Exception ex)
- {
- // You will get an exception when httpListener.Stop() is called
- // because there will be a thread stopped waiting on the .EndGetContext()
- // method, and again, that is just the way most Begin/End asynchronous
- // methods of the .NET Framework work.
- var errMsg = ex + ": " + IsListening;
- _logger.Warn(errMsg);
- return;
- }
- finally
- {
- // Once we know we have a request (or exception), we signal the other thread
- // so that it calls the BeginGetContext() (or possibly exits if we're not
- // listening any more) method to start handling the next incoming request
- // while we continue to process this request on a different thread.
- _autoResetEvents[index].Set();
- }
-
- var date = DateTime.Now;
-
- Task.Factory.StartNew(async () =>
- {
- try
- {
- var request = context.Request;
-
- LogHttpRequest(request, index);
-
- if (request.IsWebSocketRequest)
- {
- await ProcessWebSocketRequest(context).ConfigureAwait(false);
- return;
- }
-
- var localPath = request.Url.LocalPath;
-
- if (string.Equals(localPath, "/mediabrowser/", StringComparison.OrdinalIgnoreCase))
- {
- context.Response.Redirect(DefaultRedirectPath);
- context.Response.Close();
- return;
- }
- if (string.Equals(localPath, "/mediabrowser", StringComparison.OrdinalIgnoreCase))
- {
- context.Response.Redirect("mediabrowser/" + DefaultRedirectPath);
- context.Response.Close();
- return;
- }
- if (string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase))
- {
- context.Response.Redirect("mediabrowser/" + DefaultRedirectPath);
- context.Response.Close();
- return;
- }
- if (string.IsNullOrEmpty(localPath))
- {
- context.Response.Redirect("/mediabrowser/" + DefaultRedirectPath);
- context.Response.Close();
- return;
- }
-
- var url = request.Url.ToString();
- var endPoint = request.RemoteEndPoint;
-
- await ProcessRequestAsync(context).ConfigureAwait(false);
-
- var duration = DateTime.Now - date;
-
- if (EnableHttpRequestLogging)
- {
- LoggerUtils.LogResponse(_logger, context.Response, url, endPoint, duration);
- }
- }
- catch (Exception ex)
- {
- _logger.ErrorException("ProcessRequest failure", ex);
-
- HandleError(ex, context, _logger);
- }
-
- });
- }
-
- /// <summary>
- /// Logs the HTTP request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <param name="index">The index.</param>
- private void LogHttpRequest(HttpListenerRequest request, int index)
- {
- var endpoint = request.LocalEndPoint;
-
- if (endpoint != null)
- {
- var address = endpoint.ToString();
-
- _localEndPoints.GetOrAdd(address, address);
- }
-
- if (EnableHttpRequestLogging)
- {
- LoggerUtils.LogRequest(_logger, request, index);
- }
- }
+ var httpRes = httpReq.Response;
- /// <summary>
- /// Processes the web socket request.
- /// </summary>
- /// <param name="ctx">The CTX.</param>
- /// <returns>Task.</returns>
- private async Task ProcessWebSocketRequest(HttpListenerContext ctx)
- {
-#if !__MonoCS__
- try
- {
- var webSocketContext = await ctx.AcceptWebSocketAsync(null).ConfigureAwait(false);
- if (WebSocketConnected != null)
+ if (httpRes.IsClosed)
{
- WebSocketConnected(this, new WebSocketConnectEventArgs { WebSocket = new NativeWebSocket(webSocketContext.WebSocket, _logger), Endpoint = ctx.Request.RemoteEndPoint.ToString() });
+ return;
}
- }
- catch (Exception ex)
- {
- _logger.ErrorException("AcceptWebSocketAsync error", ex);
- ctx.Response.StatusCode = 500;
- ctx.Response.Close();
- }
-#endif
- }
- public static void HandleError(Exception ex, HttpListenerContext context, ILogger logger)
- {
- try
- {
var errorResponse = new ErrorResponse
{
ResponseStatus = new ResponseStatus
@@ -371,9 +199,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer
}
};
- var operationName = context.Request.GetOperationName();
- var httpReq = GetRequest(context, operationName);
- var httpRes = httpReq.Response;
var contentType = httpReq.ResponseContentType;
var serializer = HostContext.ContentTypes.GetResponseSerializer(contentType);
@@ -402,48 +227,61 @@ namespace MediaBrowser.Server.Implementations.HttpServer
}
catch (Exception errorEx)
{
- logger.ErrorException("Error this.ProcessRequest(context)(Exception while writing error to the response)", errorEx);
+ _logger.ErrorException("Error this.ProcessRequest(context)(Exception while writing error to the response)", errorEx);
}
}
- private static ListenerRequest GetRequest(HttpListenerContext httpContext, string operationName)
- {
- var req = new ListenerRequest(httpContext, operationName, RequestAttributes.None);
- req.RequestAttributes = req.GetAttributes();
- return req;
- }
-
/// <summary>
/// Shut down the Web Service
/// </summary>
public void Stop()
{
- if (Listener != null)
+ if (_listener != null)
{
- foreach (var prefix in UrlPrefixes)
- {
- Listener.Prefixes.Remove(prefix);
- }
-
- Listener.Close();
+ _listener.Stop();
}
}
/// <summary>
/// Overridable method that can be used to implement a custom hnandler
/// </summary>
- /// <param name="context"></param>
- protected Task ProcessRequestAsync(HttpListenerContext context)
+ /// <param name="httpReq">The HTTP req.</param>
+ /// <param name="url">The URL.</param>
+ /// <returns>Task.</returns>
+ protected Task RequestHandler(IHttpRequest httpReq, Uri url)
{
- if (string.IsNullOrEmpty(context.Request.RawUrl))
- return ((object)null).AsTaskResult();
-
- var operationName = context.Request.GetOperationName();
+ var date = DateTime.Now;
- var httpReq = GetRequest(context, operationName);
var httpRes = httpReq.Response;
+
+ var operationName = httpReq.OperationName;
+ var localPath = url.LocalPath;
+
+ if (string.Equals(localPath, "/" + HandlerPath + "/", StringComparison.OrdinalIgnoreCase))
+ {
+ httpRes.RedirectToUrl(DefaultRedirectPath);
+ return Task.FromResult(true);
+ }
+ if (string.Equals(localPath, "/" + HandlerPath, StringComparison.OrdinalIgnoreCase))
+ {
+ httpRes.RedirectToUrl(HandlerPath + "/" + DefaultRedirectPath);
+ return Task.FromResult(true);
+ }
+ if (string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase))
+ {
+ httpRes.RedirectToUrl(HandlerPath + "/" + DefaultRedirectPath);
+ return Task.FromResult(true);
+ }
+ if (string.IsNullOrEmpty(localPath))
+ {
+ httpRes.RedirectToUrl("/" + HandlerPath + "/" + DefaultRedirectPath);
+ return Task.FromResult(true);
+ }
+
var handler = HttpHandlerFactory.GetHandler(httpReq);
+ var remoteIp = httpReq.RemoteIp;
+
var serviceStackHandler = handler as IServiceStackHandler;
if (serviceStackHandler != null)
{
@@ -454,8 +292,21 @@ namespace MediaBrowser.Server.Implementations.HttpServer
}
var task = serviceStackHandler.ProcessRequestAsync(httpReq, httpRes, operationName);
- task.ContinueWith(x => httpRes.Close());
+ task.ContinueWith(x => httpRes.Close(), TaskContinuationOptions.OnlyOnRanToCompletion | TaskContinuationOptions.AttachedToParent);
+ //Matches Exceptions handled in HttpListenerBase.InitTask()
+
+ var urlString = url.ToString();
+
+ task.ContinueWith(x =>
+ {
+ var statusCode = httpRes.StatusCode;
+
+ var duration = DateTime.Now - date;
+
+ LoggerUtils.LogResponse(_logger, statusCode, urlString, remoteIp, duration);
+
+ }, TaskContinuationOptions.None);
return task;
}
@@ -464,12 +315,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer
}
/// <summary>
- /// Gets or sets a value indicating whether [enable HTTP request logging].
- /// </summary>
- /// <value><c>true</c> if [enable HTTP request logging]; otherwise, <c>false</c>.</value>
- public bool EnableHttpRequestLogging { get; set; }
-
- /// <summary>
/// Adds the rest handlers.
/// </summary>
/// <param name="services">The services.</param>
@@ -484,6 +329,13 @@ namespace MediaBrowser.Server.Implementations.HttpServer
base.Init();
}
+ //public override RouteAttribute[] GetRouteAttributes(System.Type requestType)
+ //{
+ // var routes = base.GetRouteAttributes(requestType);
+ // routes.Each(x => x.Path = "/api" + x.Path);
+ // return routes;
+ //}
+
/// <summary>
/// Releases the specified instance.
/// </summary>
@@ -525,10 +377,5 @@ namespace MediaBrowser.Server.Implementations.HttpServer
UrlPrefixes = urlPrefixes.ToList();
Start(UrlPrefixes.First());
}
-
- public bool SupportsWebSockets
- {
- get { return NativeWebSocket.IsSupported; }
- }
}
} \ No newline at end of file
diff --git a/MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs b/MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs
index 925ef8050..9997cfbdb 100644
--- a/MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs
+++ b/MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs
@@ -289,30 +289,28 @@ namespace MediaBrowser.Server.Implementations.HttpServer
return null;
}
- /// <summary>
- /// Gets the static file result.
- /// </summary>
- /// <param name="requestContext">The request context.</param>
- /// <param name="path">The path.</param>
- /// <param name="fileShare">The file share.</param>
- /// <param name="responseHeaders">The response headers.</param>
- /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
- /// <returns>System.Object.</returns>
- /// <exception cref="System.ArgumentNullException">path</exception>
- public object GetStaticFileResult(IRequest requestContext, string path, FileShare fileShare = FileShare.Read, IDictionary<string, string> responseHeaders = null, bool isHeadRequest = false)
+ public object GetStaticFileResult(IRequest requestContext,
+ string path,
+ FileShare fileShare = FileShare.Read)
{
if (string.IsNullOrEmpty(path))
{
throw new ArgumentNullException("path");
}
- return GetStaticFileResult(requestContext, path, MimeTypes.GetMimeType(path), fileShare, responseHeaders, isHeadRequest);
+ return GetStaticFileResult(requestContext, new StaticFileResultOptions
+ {
+ Path = path,
+ FileShare = fileShare
+ });
}
- public object GetStaticFileResult(IRequest requestContext, string path, string contentType,
- FileShare fileShare = FileShare.Read, IDictionary<string, string> responseHeaders = null,
- bool isHeadRequest = false)
+ public object GetStaticFileResult(IRequest requestContext,
+ StaticFileResultOptions options)
{
+ var path = options.Path;
+ var fileShare = options.FileShare;
+
if (string.IsNullOrEmpty(path))
{
throw new ArgumentNullException("path");
@@ -323,11 +321,18 @@ namespace MediaBrowser.Server.Implementations.HttpServer
throw new ArgumentException("FileShare must be either Read or ReadWrite");
}
- var dateModified = _fileSystem.GetLastWriteTimeUtc(path);
+ if (string.IsNullOrWhiteSpace(options.ContentType))
+ {
+ options.ContentType = MimeTypes.GetMimeType(path);
+ }
+
+ options.DateLastModified = _fileSystem.GetLastWriteTimeUtc(path);
+ var cacheKey = path + options.DateLastModified.Value.Ticks;
- var cacheKey = path + dateModified.Ticks;
+ options.CacheKey = cacheKey.GetMD5();
+ options.ContentFactory = () => Task.FromResult(GetFileStream(path, fileShare));
- return GetStaticResult(requestContext, cacheKey.GetMD5(), dateModified, null, contentType, () => Task.FromResult(GetFileStream(path, fileShare)), responseHeaders, isHeadRequest);
+ return GetStaticResult(requestContext, options);
}
/// <summary>
@@ -341,41 +346,46 @@ namespace MediaBrowser.Server.Implementations.HttpServer
return _fileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, fileShare, true);
}
- /// <summary>
- /// Gets the static result.
- /// </summary>
- /// <param name="requestContext">The request context.</param>
- /// <param name="cacheKey">The cache key.</param>
- /// <param name="lastDateModified">The last date modified.</param>
- /// <param name="cacheDuration">Duration of the cache.</param>
- /// <param name="contentType">Type of the content.</param>
- /// <param name="factoryFn">The factory fn.</param>
- /// <param name="responseHeaders">The response headers.</param>
- /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
- /// <returns>System.Object.</returns>
- /// <exception cref="System.ArgumentNullException">cacheKey
- /// or
- /// factoryFn</exception>
- public object GetStaticResult(IRequest requestContext, Guid cacheKey, DateTime? lastDateModified, TimeSpan? cacheDuration, string contentType, Func<Task<Stream>> factoryFn, IDictionary<string, string> responseHeaders = null, bool isHeadRequest = false)
+ public object GetStaticResult(IRequest requestContext,
+ Guid cacheKey,
+ DateTime? lastDateModified,
+ TimeSpan? cacheDuration,
+ string contentType,
+ Func<Task<Stream>> factoryFn,
+ IDictionary<string, string> responseHeaders = null,
+ bool isHeadRequest = false)
{
+ return GetStaticResult(requestContext, new StaticResultOptions
+ {
+ CacheDuration = cacheDuration,
+ CacheKey = cacheKey,
+ ContentFactory = factoryFn,
+ ContentType = contentType,
+ DateLastModified = lastDateModified,
+ IsHeadRequest = isHeadRequest,
+ ResponseHeaders = responseHeaders
+ });
+ }
+
+ public object GetStaticResult(IRequest requestContext, StaticResultOptions options)
+ {
+ var cacheKey = options.CacheKey;
+ options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary<string, string>();
+ var contentType = options.ContentType;
+
if (cacheKey == Guid.Empty)
{
throw new ArgumentNullException("cacheKey");
}
- if (factoryFn == null)
+ if (options.ContentFactory == null)
{
throw new ArgumentNullException("factoryFn");
}
var key = cacheKey.ToString("N");
- if (responseHeaders == null)
- {
- responseHeaders = new Dictionary<string, string>();
- }
-
// See if the result is already cached in the browser
- var result = GetCachedResult(requestContext, responseHeaders, cacheKey, key, lastDateModified, cacheDuration, contentType);
+ var result = GetCachedResult(requestContext, options.ResponseHeaders, cacheKey, key, options.DateLastModified, options.CacheDuration, contentType);
if (result != null)
{
@@ -383,10 +393,8 @@ namespace MediaBrowser.Server.Implementations.HttpServer
}
var compress = ShouldCompressResponse(requestContext, contentType);
-
- var hasOptions = GetStaticResult(requestContext, responseHeaders, contentType, factoryFn, compress, isHeadRequest).Result;
-
- AddResponseHeaders(hasOptions, responseHeaders);
+ var hasOptions = GetStaticResult(requestContext, options, compress).Result;
+ AddResponseHeaders(hasOptions, options.ResponseHeaders);
return hasOptions;
}
@@ -442,18 +450,13 @@ namespace MediaBrowser.Server.Implementations.HttpServer
/// </summary>
private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
- /// <summary>
- /// Gets the static result.
- /// </summary>
- /// <param name="requestContext">The request context.</param>
- /// <param name="responseHeaders">The response headers.</param>
- /// <param name="contentType">Type of the content.</param>
- /// <param name="factoryFn">The factory fn.</param>
- /// <param name="compress">if set to <c>true</c> [compress].</param>
- /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
- /// <returns>Task{IHasOptions}.</returns>
- private async Task<IHasOptions> GetStaticResult(IRequest requestContext, IDictionary<string, string> responseHeaders, string contentType, Func<Task<Stream>> factoryFn, bool compress, bool isHeadRequest)
+ private async Task<IHasOptions> GetStaticResult(IRequest requestContext, StaticResultOptions options, bool compress)
{
+ var isHeadRequest = options.IsHeadRequest;
+ var factoryFn = options.ContentFactory;
+ var contentType = options.ContentType;
+ var responseHeaders = options.ResponseHeaders;
+
var requestedCompressionType = requestContext.GetCompressionType();
if (!compress || string.IsNullOrEmpty(requestedCompressionType))
@@ -464,7 +467,12 @@ namespace MediaBrowser.Server.Implementations.HttpServer
if (!string.IsNullOrEmpty(rangeHeader))
{
- return new RangeRequestWriter(rangeHeader, stream, contentType, isHeadRequest);
+ return new RangeRequestWriter(rangeHeader, stream, contentType, isHeadRequest)
+ {
+ Throttle = options.Throttle,
+ ThrottleLimit = options.ThrottleLimit,
+ MinThrottlePosition = options.MinThrottlePosition
+ };
}
responseHeaders["Content-Length"] = stream.Length.ToString(UsCulture);
@@ -476,7 +484,12 @@ namespace MediaBrowser.Server.Implementations.HttpServer
return GetHttpResult(new byte[] { }, contentType);
}
- return new StreamWriter(stream, contentType, _logger);
+ return new StreamWriter(stream, contentType, _logger)
+ {
+ Throttle = options.Throttle,
+ ThrottleLimit = options.ThrottleLimit,
+ MinThrottlePosition = options.MinThrottlePosition
+ };
}
string content;
@@ -705,14 +718,5 @@ namespace MediaBrowser.Server.Implementations.HttpServer
throw error;
}
-
- public object GetOptimizedSerializedResultUsingCache<T>(IRequest request, T result)
- where T : class
- {
- var json = _jsonSerializer.SerializeToString(result);
- var cacheKey = json.GetMD5();
-
- return GetOptimizedResultUsingCache(request, cacheKey, null, null, () => result);
- }
}
} \ No newline at end of file
diff --git a/MediaBrowser.Server.Implementations/HttpServer/IHttpListener.cs b/MediaBrowser.Server.Implementations/HttpServer/IHttpListener.cs
new file mode 100644
index 000000000..1d80a263c
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/HttpServer/IHttpListener.cs
@@ -0,0 +1,42 @@
+using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
+using ServiceStack.Web;
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Server.Implementations.HttpServer
+{
+ public interface IHttpListener : IDisposable
+ {
+ IEnumerable<string> LocalEndPoints { get; }
+
+ /// <summary>
+ /// Gets or sets the error handler.
+ /// </summary>
+ /// <value>The error handler.</value>
+ Action<Exception, IRequest> ErrorHandler { get; set; }
+
+ /// <summary>
+ /// Gets or sets the request handler.
+ /// </summary>
+ /// <value>The request handler.</value>
+ Func<IHttpRequest, Uri, Task> RequestHandler { get; set; }
+
+ /// <summary>
+ /// Gets or sets the web socket handler.
+ /// </summary>
+ /// <value>The web socket handler.</value>
+ Action<WebSocketConnectEventArgs> WebSocketHandler { get; set; }
+
+ /// <summary>
+ /// Starts this instance.
+ /// </summary>
+ /// <param name="urlPrefixes">The URL prefixes.</param>
+ void Start(IEnumerable<string> urlPrefixes);
+
+ /// <summary>
+ /// Stops this instance.
+ /// </summary>
+ void Stop();
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/HttpServer/LoggerUtils.cs b/MediaBrowser.Server.Implementations/HttpServer/LoggerUtils.cs
index 0f7e94ac5..955c4ed2d 100644
--- a/MediaBrowser.Server.Implementations/HttpServer/LoggerUtils.cs
+++ b/MediaBrowser.Server.Implementations/HttpServer/LoggerUtils.cs
@@ -1,6 +1,5 @@
using MediaBrowser.Model.Logging;
using System;
-using System.Linq;
using System.Net;
using System.Text;
@@ -9,43 +8,22 @@ namespace MediaBrowser.Server.Implementations.HttpServer
public static class LoggerUtils
{
/// <summary>
- /// Logs the request.
- /// </summary>
- /// <param name="logger">The logger.</param>
- /// <param name="request">The request.</param>
- /// <param name="workerIndex">Index of the worker.</param>
- public static void LogRequest(ILogger logger, HttpListenerRequest request, int workerIndex)
- {
- var log = new StringBuilder();
-
- //var headers = string.Join(",", request.Headers.AllKeys.Where(i => !string.Equals(i, "cookie", StringComparison.OrdinalIgnoreCase) && !string.Equals(i, "Referer", StringComparison.OrdinalIgnoreCase)).Select(k => k + "=" + request.Headers[k]));
-
- //log.AppendLine("Ip: " + request.RemoteEndPoint + ". Headers: " + headers);
-
- var type = request.IsWebSocketRequest ? "Web Socket" : "HTTP " + request.HttpMethod;
-
- logger.LogMultiline(type + " " + request.Url, LogSeverity.Debug, log);
- }
-
- /// <summary>
/// Logs the response.
/// </summary>
/// <param name="logger">The logger.</param>
- /// <param name="response">The response.</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, HttpListenerResponse response, string url, IPEndPoint endPoint, TimeSpan duration)
+ public static void LogResponse(ILogger logger, int statusCode, string url, string endPoint, TimeSpan duration)
{
- var statusCode = response.StatusCode;
-
var log = new StringBuilder();
log.AppendLine(string.Format("Url: {0}", url));
//log.AppendLine("Headers: " + string.Join(",", response.Headers.AllKeys.Select(k => k + "=" + response.Headers[k])));
- var responseTime = string.Format(". Response time: {0} ms", duration.TotalMilliseconds);
+ var responseTime = string.Format(". Response time: {0} ms.", duration.TotalMilliseconds);
var msg = "HTTP Response " + statusCode + " to " + endPoint + responseTime;
diff --git a/MediaBrowser.Server.Implementations/HttpServer/NativeWebSocket.cs b/MediaBrowser.Server.Implementations/HttpServer/NativeWebSocket.cs
index f89cdac47..c7669fecb 100644
--- a/MediaBrowser.Server.Implementations/HttpServer/NativeWebSocket.cs
+++ b/MediaBrowser.Server.Implementations/HttpServer/NativeWebSocket.cs
@@ -1,4 +1,5 @@
-using MediaBrowser.Common.Events;
+using System.Text;
+using MediaBrowser.Common.Events;
using MediaBrowser.Common.Net;
using MediaBrowser.Model.Logging;
using System;
@@ -36,7 +37,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer
/// <param name="socket">The socket.</param>
/// <param name="logger">The logger.</param>
/// <exception cref="System.ArgumentNullException">socket</exception>
- public NativeWebSocket(System.Net.WebSockets.WebSocket socket, ILogger logger)
+ public NativeWebSocket(WebSocket socket, ILogger logger)
{
if (socket == null)
{
@@ -155,6 +156,22 @@ namespace MediaBrowser.Server.Implementations.HttpServer
return WebSocket.SendAsync(new ArraySegment<byte>(bytes), nativeType, true, linkedTokenSource.Token);
}
+ public Task SendAsync(byte[] bytes, bool endOfMessage, CancellationToken cancellationToken)
+ {
+ var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationTokenSource.Token);
+
+ return WebSocket.SendAsync(new ArraySegment<byte>(bytes), System.Net.WebSockets.WebSocketMessageType.Binary, true, linkedTokenSource.Token);
+ }
+
+ public Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken)
+ {
+ var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationTokenSource.Token);
+
+ var bytes = Encoding.UTF8.GetBytes(text);
+
+ return WebSocket.SendAsync(new ArraySegment<byte>(bytes), System.Net.WebSockets.WebSocketMessageType.Text, true, linkedTokenSource.Token);
+ }
+
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
diff --git a/MediaBrowser.Server.Implementations/HttpServer/NetListener/HttpListenerServer.cs b/MediaBrowser.Server.Implementations/HttpServer/NetListener/HttpListenerServer.cs
new file mode 100644
index 000000000..bdc2750fb
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/HttpServer/NetListener/HttpListenerServer.cs
@@ -0,0 +1,299 @@
+using MediaBrowser.Common.Net;
+using MediaBrowser.Model.Logging;
+using ServiceStack;
+using ServiceStack.Host.HttpListener;
+using ServiceStack.Web;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Server.Implementations.HttpServer.NetListener
+{
+ public class HttpListenerServer : IHttpListener
+ {
+ private readonly ILogger _logger;
+ private HttpListener _listener;
+ private readonly AutoResetEvent _listenForNextRequest = new AutoResetEvent(false);
+
+ public System.Action<Exception, IRequest> ErrorHandler { get; set; }
+ public Action<WebSocketConnectEventArgs> WebSocketHandler { get; set; }
+ public System.Func<IHttpRequest, Uri, Task> RequestHandler { get; set; }
+
+ private readonly ConcurrentDictionary<string, string> _localEndPoints = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+
+ public HttpListenerServer(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ /// <summary>
+ /// Gets the local end points.
+ /// </summary>
+ /// <value>The local end points.</value>
+ public IEnumerable<string> LocalEndPoints
+ {
+ get { return _localEndPoints.Keys.ToList(); }
+ }
+
+ private List<string> UrlPrefixes { get; set; }
+
+ public void Start(IEnumerable<string> urlPrefixes)
+ {
+ UrlPrefixes = urlPrefixes.ToList();
+
+ if (_listener == null)
+ _listener = new System.Net.HttpListener();
+
+ //HostContext.Config.HandlerFactoryPath = ListenerRequest.GetHandlerPathIfAny(UrlPrefixes.First());
+
+ foreach (var prefix in UrlPrefixes)
+ {
+ _logger.Info("Adding HttpListener prefix " + prefix);
+ _listener.Prefixes.Add(prefix);
+ }
+
+ _listener.Start();
+
+ Task.Factory.StartNew(Listen, TaskCreationOptions.LongRunning);
+ }
+
+ private bool IsListening
+ {
+ get { return _listener != null && _listener.IsListening; }
+ }
+
+ // Loop here to begin processing of new requests.
+ private void Listen()
+ {
+ while (IsListening)
+ {
+ if (_listener == null) return;
+
+ try
+ {
+ _listener.BeginGetContext(ListenerCallback, _listener);
+ _listenForNextRequest.WaitOne();
+ }
+ catch (Exception ex)
+ {
+ _logger.Error("Listen()", ex);
+ return;
+ }
+ if (_listener == null) return;
+ }
+ }
+
+ // Handle the processing of a request in here.
+ private void ListenerCallback(IAsyncResult asyncResult)
+ {
+ var listener = asyncResult.AsyncState as HttpListener;
+ HttpListenerContext context;
+
+ if (listener == null) return;
+ var isListening = listener.IsListening;
+
+ try
+ {
+ if (!isListening)
+ {
+ _logger.Debug("Ignoring ListenerCallback() as HttpListener is no longer listening"); return;
+ }
+ // The EndGetContext() method, as with all Begin/End asynchronous methods in the .NET Framework,
+ // blocks until there is a request to be processed or some type of data is available.
+ context = listener.EndGetContext(asyncResult);
+ }
+ catch (Exception ex)
+ {
+ // You will get an exception when httpListener.Stop() is called
+ // because there will be a thread stopped waiting on the .EndGetContext()
+ // method, and again, that is just the way most Begin/End asynchronous
+ // methods of the .NET Framework work.
+ var errMsg = ex + ": " + IsListening;
+ _logger.Warn(errMsg);
+ return;
+ }
+ finally
+ {
+ // Once we know we have a request (or exception), we signal the other thread
+ // so that it calls the BeginGetContext() (or possibly exits if we're not
+ // listening any more) method to start handling the next incoming request
+ // while we continue to process this request on a different thread.
+ _listenForNextRequest.Set();
+ }
+
+ Task.Factory.StartNew(() => InitTask(context));
+ }
+
+ private void InitTask(HttpListenerContext context)
+ {
+ try
+ {
+ var task = this.ProcessRequestAsync(context);
+ task.ContinueWith(x => HandleError(x.Exception, context), TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.AttachedToParent);
+
+ if (task.Status == TaskStatus.Created)
+ {
+ task.RunSynchronously();
+ }
+ }
+ catch (Exception ex)
+ {
+ HandleError(ex, context);
+ }
+ }
+
+ private Task ProcessRequestAsync(HttpListenerContext context)
+ {
+ var request = context.Request;
+
+ LogHttpRequest(request);
+
+ if (request.IsWebSocketRequest)
+ {
+ return ProcessWebSocketRequest(context);
+ }
+
+ if (string.IsNullOrEmpty(context.Request.RawUrl))
+ return ((object)null).AsTaskResult();
+
+ var operationName = context.Request.GetOperationName();
+
+ var httpReq = GetRequest(context, operationName);
+
+ return RequestHandler(httpReq, request.Url);
+ }
+
+ /// <summary>
+ /// Processes the web socket request.
+ /// </summary>
+ /// <param name="ctx">The CTX.</param>
+ /// <returns>Task.</returns>
+ private async Task ProcessWebSocketRequest(HttpListenerContext ctx)
+ {
+#if !__MonoCS__
+ try
+ {
+ var webSocketContext = await ctx.AcceptWebSocketAsync(null).ConfigureAwait(false);
+
+ if (WebSocketHandler != null)
+ {
+ WebSocketHandler(new WebSocketConnectEventArgs
+ {
+ WebSocket = new NativeWebSocket(webSocketContext.WebSocket, _logger),
+ Endpoint = ctx.Request.RemoteEndPoint.ToString()
+ });
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("AcceptWebSocketAsync error", ex);
+ ctx.Response.StatusCode = 500;
+ ctx.Response.Close();
+ }
+#endif
+ }
+
+ private void HandleError(Exception ex, HttpListenerContext context)
+ {
+ var operationName = context.Request.GetOperationName();
+ var httpReq = GetRequest(context, operationName);
+
+ if (ErrorHandler != null)
+ {
+ ErrorHandler(ex, httpReq);
+ }
+ }
+
+ private static ListenerRequest GetRequest(HttpListenerContext httpContext, string operationName)
+ {
+ var req = new ListenerRequest(httpContext, operationName, RequestAttributes.None);
+ req.RequestAttributes = req.GetAttributes();
+
+ return req;
+ }
+
+ /// <summary>
+ /// Logs the HTTP request.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ private void LogHttpRequest(HttpListenerRequest request)
+ {
+ var endpoint = request.LocalEndPoint;
+
+ if (endpoint != null)
+ {
+ var address = endpoint.ToString();
+
+ _localEndPoints.GetOrAdd(address, address);
+ }
+
+ LogRequest(_logger, request);
+ }
+
+ /// <summary>
+ /// Logs the request.
+ /// </summary>
+ /// <param name="logger">The logger.</param>
+ /// <param name="request">The request.</param>
+ private static void LogRequest(ILogger logger, HttpListenerRequest request)
+ {
+ var log = new StringBuilder();
+
+ var logHeaders = true;
+
+ if (logHeaders)
+ {
+ var headers = string.Join(",", request.Headers.AllKeys.Where(i => !string.Equals(i, "cookie", StringComparison.OrdinalIgnoreCase) && !string.Equals(i, "Referer", StringComparison.OrdinalIgnoreCase)).Select(k => k + "=" + request.Headers[k]));
+
+ log.AppendLine("Ip: " + request.RemoteEndPoint + ". Headers: " + headers);
+ }
+
+ var type = request.IsWebSocketRequest ? "Web Socket" : "HTTP " + request.HttpMethod;
+
+ logger.LogMultiline(type + " " + request.Url, LogSeverity.Debug, log);
+ }
+
+ public void Stop()
+ {
+ if (_listener != null)
+ {
+ foreach (var prefix in UrlPrefixes)
+ {
+ _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;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/HttpServer/RangeRequestWriter.cs b/MediaBrowser.Server.Implementations/HttpServer/RangeRequestWriter.cs
index 1ff199eb4..657545069 100644
--- a/MediaBrowser.Server.Implementations/HttpServer/RangeRequestWriter.cs
+++ b/MediaBrowser.Server.Implementations/HttpServer/RangeRequestWriter.cs
@@ -1,10 +1,10 @@
-using System.Threading;
-using ServiceStack.Web;
+using ServiceStack.Web;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Net;
+using System.Threading;
using System.Threading.Tasks;
namespace MediaBrowser.Server.Implementations.HttpServer
@@ -24,6 +24,10 @@ namespace MediaBrowser.Server.Implementations.HttpServer
private long RangeLength { get; set; }
private long TotalContentLength { get; set; }
+ public bool Throttle { get; set; }
+ public long ThrottleLimit { get; set; }
+ public long MinThrottlePosition;
+
/// <summary>
/// The _options
/// </summary>
@@ -159,6 +163,13 @@ namespace MediaBrowser.Server.Implementations.HttpServer
/// <param name="responseStream">The response stream.</param>
public void WriteTo(Stream responseStream)
{
+ if (Throttle)
+ {
+ responseStream = new ThrottledStream(responseStream, ThrottleLimit)
+ {
+ MinThrottlePosition = MinThrottlePosition
+ };
+ }
var task = WriteToAsync(responseStream);
Task.WaitAll(task);
@@ -228,5 +239,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer
}
public string StatusDescription { get; set; }
+
+ public int PaddingLength { get; set; }
}
} \ No newline at end of file
diff --git a/MediaBrowser.Server.Implementations/HttpServer/ResponseFilter.cs b/MediaBrowser.Server.Implementations/HttpServer/ResponseFilter.cs
index ac1621709..e0a5764d5 100644
--- a/MediaBrowser.Server.Implementations/HttpServer/ResponseFilter.cs
+++ b/MediaBrowser.Server.Implementations/HttpServer/ResponseFilter.cs
@@ -1,4 +1,5 @@
using MediaBrowser.Model.Logging;
+using MediaBrowser.Server.Implementations.HttpServer.SocketSharp;
using ServiceStack;
using ServiceStack.Web;
using System;
@@ -66,13 +67,23 @@ namespace MediaBrowser.Server.Implementations.HttpServer
if (length > 0)
{
- var response = (HttpListenerResponse)res.OriginalResponse;
-
- response.ContentLength64 = length;
-
- // 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
- response.SendChunked = false;
+ 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;
+ }
+
+ var sharpResponse = res as WebSocketSharpResponse;
+ if (sharpResponse != null)
+ {
+ sharpResponse.SendChunked = false;
+ }
}
}
}
diff --git a/MediaBrowser.Server.Implementations/HttpServer/Security/AuthService.cs b/MediaBrowser.Server.Implementations/HttpServer/Security/AuthService.cs
new file mode 100644
index 000000000..19870c435
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/HttpServer/Security/AuthService.cs
@@ -0,0 +1,164 @@
+using System.Collections.Generic;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Session;
+using ServiceStack;
+using ServiceStack.Auth;
+using ServiceStack.Web;
+using System;
+using System.Collections.Specialized;
+using System.Linq;
+
+namespace MediaBrowser.Server.Implementations.HttpServer.Security
+{
+ public class AuthService : IAuthService
+ {
+ private readonly IServerConfigurationManager _config;
+
+ public AuthService(IUserManager userManager, ISessionManager sessionManager, IAuthorizationContext authorizationContext, IServerConfigurationManager config)
+ {
+ AuthorizationContext = authorizationContext;
+ _config = config;
+ SessionManager = sessionManager;
+ UserManager = userManager;
+ }
+
+ public IUserManager UserManager { get; private set; }
+ public ISessionManager SessionManager { get; private set; }
+ public IAuthorizationContext AuthorizationContext { get; private set; }
+
+ /// <summary>
+ /// Restrict authentication to a specific <see cref="IAuthProvider"/>.
+ /// For example, if this attribute should only permit access
+ /// if the user is authenticated with <see cref="BasicAuthProvider"/>,
+ /// you should set this property to <see cref="BasicAuthProvider.Name"/>.
+ /// </summary>
+ public string Provider { get; set; }
+
+ /// <summary>
+ /// Redirect the client to a specific URL if authentication failed.
+ /// If this property is null, simply `401 Unauthorized` is returned.
+ /// </summary>
+ public string HtmlRedirect { get; set; }
+
+ public void Authenticate(IRequest req, IResponse res, object requestDto)
+ {
+ if (HostContext.HasValidAuthSecret(req))
+ return;
+
+ //ExecuteBasic(req, res, requestDto); //first check if session is authenticated
+ //if (res.IsClosed) return; //AuthenticateAttribute already closed the request (ie auth failed)
+
+ ValidateUser(req);
+ }
+
+ // TODO: Remove this when all clients have supported the new sescurity
+ private readonly List<string> _updatedClients = new List<string>(){"Dashboard"};
+
+ private void ValidateUser(IRequest req)
+ {
+ //This code is executed before the service
+ var auth = AuthorizationContext.GetAuthorizationInfo(req);
+
+ if (!string.IsNullOrWhiteSpace(auth.Token)
+ || _config.Configuration.EnableTokenAuthentication
+ || _updatedClients.Contains(auth.Client ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ {
+ SessionManager.ValidateSecurityToken(auth.Token);
+ }
+
+ var user = string.IsNullOrWhiteSpace(auth.UserId)
+ ? null
+ : UserManager.GetUserById(new Guid(auth.UserId));
+
+ if (user == null & !string.IsNullOrWhiteSpace(auth.UserId))
+ {
+ // TODO: Re-enable
+ //throw new ArgumentException("User with Id " + auth.UserId + " not found");
+ }
+
+ if (user != null && user.Configuration.IsDisabled)
+ {
+ throw new AuthenticationException("User account has been disabled.");
+ }
+
+ if (!string.IsNullOrWhiteSpace(auth.DeviceId) &&
+ !string.IsNullOrWhiteSpace(auth.Client) &&
+ !string.IsNullOrWhiteSpace(auth.Device))
+ {
+ SessionManager.LogSessionActivity(auth.Client,
+ auth.Version,
+ auth.DeviceId,
+ auth.Device,
+ req.RemoteIp,
+ user);
+ }
+ }
+
+ private void ExecuteBasic(IRequest req, IResponse res, object requestDto)
+ {
+ if (AuthenticateService.AuthProviders == null)
+ throw new InvalidOperationException(
+ "The AuthService must be initialized by calling AuthService.Init to use an authenticate attribute");
+
+ var matchingOAuthConfigs = AuthenticateService.AuthProviders.Where(x =>
+ this.Provider.IsNullOrEmpty()
+ || x.Provider == this.Provider).ToList();
+
+ if (matchingOAuthConfigs.Count == 0)
+ {
+ res.WriteError(req, requestDto, "No OAuth Configs found matching {0} provider"
+ .Fmt(this.Provider ?? "any"));
+ res.EndRequest();
+ }
+
+ matchingOAuthConfigs.OfType<IAuthWithRequest>()
+ .Each(x => x.PreAuthenticate(req, res));
+
+ var session = req.GetSession();
+ if (session == null || !matchingOAuthConfigs.Any(x => session.IsAuthorized(x.Provider)))
+ {
+ if (this.DoHtmlRedirectIfConfigured(req, res, true)) return;
+
+ AuthProvider.HandleFailedAuth(matchingOAuthConfigs[0], session, req, res);
+ }
+ }
+
+ protected bool DoHtmlRedirectIfConfigured(IRequest req, IResponse res, bool includeRedirectParam = false)
+ {
+ var htmlRedirect = this.HtmlRedirect ?? AuthenticateService.HtmlRedirect;
+ if (htmlRedirect != null && req.ResponseContentType.MatchesContentType(MimeTypes.Html))
+ {
+ DoHtmlRedirect(htmlRedirect, req, res, includeRedirectParam);
+ return true;
+ }
+ return false;
+ }
+
+ public static void DoHtmlRedirect(string redirectUrl, IRequest req, IResponse res, bool includeRedirectParam)
+ {
+ var url = req.ResolveAbsoluteUrl(redirectUrl);
+ if (includeRedirectParam)
+ {
+ var absoluteRequestPath = req.ResolveAbsoluteUrl("~" + req.PathInfo + ToQueryString(req.QueryString));
+ url = url.AddQueryParam(HostContext.ResolveLocalizedString(LocalizedStrings.Redirect), absoluteRequestPath);
+ }
+
+ res.RedirectToUrl(url);
+ }
+
+ private static string ToQueryString(INameValueCollection queryStringCollection)
+ {
+ return ToQueryString((NameValueCollection)queryStringCollection.Original);
+ }
+
+ private static string ToQueryString(NameValueCollection queryStringCollection)
+ {
+ if (queryStringCollection == null || queryStringCollection.Count == 0)
+ return String.Empty;
+
+ return "?" + queryStringCollection.ToFormUrlEncoded();
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/MediaBrowser.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
new file mode 100644
index 000000000..94be37e95
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Controller.Net;
+using ServiceStack.Web;
+
+namespace MediaBrowser.Server.Implementations.HttpServer.Security
+{
+ public class AuthorizationContext : IAuthorizationContext
+ {
+ public AuthorizationInfo GetAuthorizationInfo(IRequest requestContext)
+ {
+ return GetAuthorization(requestContext);
+ }
+
+ /// <summary>
+ /// Gets the authorization.
+ /// </summary>
+ /// <param name="httpReq">The HTTP req.</param>
+ /// <returns>Dictionary{System.StringSystem.String}.</returns>
+ private static AuthorizationInfo GetAuthorization(IRequest httpReq)
+ {
+ var auth = GetAuthorizationDictionary(httpReq);
+
+ string userId = null;
+ string deviceId = null;
+ string device = null;
+ string client = null;
+ string version = null;
+
+ if (auth != null)
+ {
+ auth.TryGetValue("UserId", out userId);
+ auth.TryGetValue("DeviceId", out deviceId);
+ auth.TryGetValue("Device", out device);
+ auth.TryGetValue("Client", out client);
+ auth.TryGetValue("Version", out version);
+ }
+
+ var token = httpReq.Headers["X-MediaBrowser-Token"];
+
+ if (string.IsNullOrWhiteSpace(token))
+ {
+ token = httpReq.QueryString["api_key"];
+ }
+
+ return new AuthorizationInfo
+ {
+ Client = client,
+ Device = device,
+ DeviceId = deviceId,
+ UserId = userId,
+ Version = version,
+ Token = token
+ };
+ }
+
+ /// <summary>
+ /// Gets the auth.
+ /// </summary>
+ /// <param name="httpReq">The HTTP req.</param>
+ /// <returns>Dictionary{System.StringSystem.String}.</returns>
+ private static Dictionary<string, string> GetAuthorizationDictionary(IRequest httpReq)
+ {
+ var auth = httpReq.Headers["Authorization"];
+
+ return GetAuthorization(auth);
+ }
+
+ /// <summary>
+ /// Gets the authorization.
+ /// </summary>
+ /// <param name="authorizationHeader">The authorization header.</param>
+ /// <returns>Dictionary{System.StringSystem.String}.</returns>
+ private static Dictionary<string, string> GetAuthorization(string authorizationHeader)
+ {
+ if (authorizationHeader == null) return null;
+
+ var parts = authorizationHeader.Split(' ');
+
+ // There should be at least to parts
+ if (parts.Length < 2) return null;
+
+ // It has to be a digest request
+ if (!string.Equals(parts[0], "MediaBrowser", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+
+ // Remove uptil the first space
+ authorizationHeader = authorizationHeader.Substring(authorizationHeader.IndexOf(' '));
+ parts = authorizationHeader.Split(',');
+
+ var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+
+ foreach (var item in parts)
+ {
+ var param = item.Trim().Split(new[] { '=' }, 2);
+ result.Add(param[0], param[1].Trim(new[] { '"' }));
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/HttpServer/Security/SessionAuthProvider.cs b/MediaBrowser.Server.Implementations/HttpServer/Security/SessionAuthProvider.cs
new file mode 100644
index 000000000..7c3173101
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/HttpServer/Security/SessionAuthProvider.cs
@@ -0,0 +1,35 @@
+using MediaBrowser.Controller.Net;
+using ServiceStack;
+using ServiceStack.Auth;
+
+namespace MediaBrowser.Server.Implementations.HttpServer.Security
+{
+ public class SessionAuthProvider : CredentialsAuthProvider
+ {
+ private readonly ISessionContext _sessionContext;
+
+ public SessionAuthProvider(ISessionContext sessionContext)
+ {
+ _sessionContext = sessionContext;
+ }
+
+ public override bool TryAuthenticate(IServiceBase authService, string userName, string password)
+ {
+ return true;
+ }
+
+ public override bool IsAuthorized(IAuthSession session, IAuthTokens tokens, Authenticate request = null)
+ {
+ return true;
+ }
+
+ protected override void SaveUserAuth(IServiceBase authService, IAuthSession session, IAuthRepository authRepo, IAuthTokens tokens)
+ {
+ }
+
+ public override object Authenticate(IServiceBase authService, IAuthSession session, Authenticate request)
+ {
+ return base.Authenticate(authService, session, request);
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/HttpServer/Security/SessionContext.cs b/MediaBrowser.Server.Implementations/HttpServer/Security/SessionContext.cs
new file mode 100644
index 000000000..f67c643c8
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/HttpServer/Security/SessionContext.cs
@@ -0,0 +1,36 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Session;
+using ServiceStack.Web;
+
+namespace MediaBrowser.Server.Implementations.HttpServer.Security
+{
+ public class SessionContext : ISessionContext
+ {
+ private readonly IUserManager _userManager;
+ private readonly ISessionManager _sessionManager;
+ private readonly IAuthorizationContext _authContext;
+
+ public SessionContext(IUserManager userManager, IAuthorizationContext authContext, ISessionManager sessionManager)
+ {
+ _userManager = userManager;
+ _authContext = authContext;
+ _sessionManager = sessionManager;
+ }
+
+ public SessionInfo GetSession(IRequest requestContext)
+ {
+ var authorization = _authContext.GetAuthorizationInfo(requestContext);
+
+ return _sessionManager.GetSession(authorization.DeviceId, authorization.Client, authorization.Version);
+ }
+
+ public User GetUser(IRequest requestContext)
+ {
+ var session = GetSession(requestContext);
+
+ return session == null || !session.UserId.HasValue ? null : _userManager.GetUserById(session.UserId.Value);
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/Extensions.cs b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/Extensions.cs
new file mode 100644
index 000000000..63d57b6be
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/Extensions.cs
@@ -0,0 +1,28 @@
+using System;
+using MediaBrowser.Model.Logging;
+using WebSocketSharp.Net;
+
+namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp
+{
+ public static class Extensions
+ {
+ public static string GetOperationName(this HttpListenerRequest request)
+ {
+ return request.Url.Segments[request.Url.Segments.Length - 1];
+ }
+
+ public static void CloseOutputStream(this HttpListenerResponse response, ILogger logger)
+ {
+ try
+ {
+ response.OutputStream.Flush();
+ response.OutputStream.Close();
+ response.Close();
+ }
+ catch (Exception ex)
+ {
+ logger.ErrorException("Error in HttpListenerResponseWrapper: " + ex.Message, ex);
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs
new file mode 100644
index 000000000..226d97b3c
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs
@@ -0,0 +1,918 @@
+using System;
+using System.Collections.Specialized;
+using System.Globalization;
+using System.IO;
+using System.Text;
+using System.Web;
+using ServiceStack.Web;
+
+namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp
+{
+ public partial class WebSocketSharpRequest : IHttpRequest
+ {
+ static internal string GetParameter(string header, string attr)
+ {
+ int ap = header.IndexOf(attr);
+ 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);
+ }
+
+ void LoadMultiPart()
+ {
+ string boundary = GetParameter(ContentType, "; boundary=");
+ if (boundary == null)
+ return;
+
+ var input = GetSubStream(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);
+ input.CopyTo(ms);
+ 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));
+ }
+ else
+ {
+ //
+ // We use a substream, as in 2.x we will support large uploads streamed to disk,
+ //
+ HttpPostedFile sub = new HttpPostedFile(e.Filename, e.ContentType, input, e.Start, e.Length);
+ files.AddFile(e.Name, sub);
+ }
+ }
+ EndSubStream(input);
+ }
+
+ public NameValueCollection Form
+ {
+ get
+ {
+ if (form == null)
+ {
+ form = new WebROCollection();
+ files = new HttpFileCollection();
+
+ if (IsContentType("multipart/form-data", true))
+ LoadMultiPart();
+ else if (
+ IsContentType("application/x-www-form-urlencoded", true))
+ LoadWwwForm();
+
+ form.Protect();
+ }
+
+#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;
+ }
+ }
+
+
+ protected bool validate_cookies, validate_query_string, validate_form;
+ protected bool checked_cookies, checked_query_string, checked_form;
+
+ 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("A potentially dangerous Request.{0} value was " +
+ "detected from the client ({1}={2}).", name, key, v);
+
+ throw new HttpRequestValidationException(msg);
+ }
+
+ static void ValidateNameValueCollection(string name, NameValueCollection coll)
+ {
+ if (coll == null)
+ return;
+
+ foreach (string key in coll.Keys)
+ {
+ string val = coll[key];
+ if (val != null && val.Length > 0 && IsInvalidString(val))
+ ThrowValidationException(name, key, val);
+ }
+ }
+
+ internal static bool IsInvalidString(string val)
+ {
+ int validationFailureIndex;
+
+ return IsInvalidString(val, out 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;
+ }
+
+ bool IsContentType(string ct, bool starts_with)
+ {
+ if (ct == null || ContentType == null) return false;
+
+ if (starts_with)
+ return StrUtils.StartsWith(ContentType, ct, true);
+
+ return String.Compare(ContentType, ct, true, Helpers.InvariantCulture) == 0;
+ }
+
+
+
+
+
+ void LoadWwwForm()
+ {
+ using (Stream input = GetSubStream(InputStream))
+ {
+ using (StreamReader s = new StreamReader(input, ContentEncoding))
+ {
+ StringBuilder key = new StringBuilder();
+ StringBuilder value = new StringBuilder();
+ int c;
+
+ while ((c = s.Read()) != -1)
+ {
+ if (c == '=')
+ {
+ value.Length = 0;
+ while ((c = s.Read()) != -1)
+ {
+ if (c == '&')
+ {
+ AddRawKeyValue(key, value);
+ break;
+ }
+ else
+ value.Append((char)c);
+ }
+ if (c == -1)
+ {
+ AddRawKeyValue(key, value);
+ return;
+ }
+ }
+ else if (c == '&')
+ AddRawKeyValue(key, value);
+ else
+ key.Append((char)c);
+ }
+ if (c == -1)
+ AddRawKeyValue(key, value);
+
+ EndSubStream(input);
+ }
+ }
+ }
+
+ void AddRawKeyValue(StringBuilder key, StringBuilder value)
+ {
+ string decodedKey = HttpUtility.UrlDecode(key.ToString(), ContentEncoding);
+ form.Add(decodedKey,
+ HttpUtility.UrlDecode(value.ToString(), ContentEncoding));
+
+ key.Length = 0;
+ value.Length = 0;
+ }
+
+ WebROCollection form;
+
+ HttpFileCollection files;
+
+ public sealed class HttpFileCollection : NameObjectCollectionBase
+ {
+ internal HttpFileCollection()
+ {
+ }
+
+ internal void AddFile(string name, HttpPostedFile file)
+ {
+ BaseAdd(name, file);
+ }
+
+ public void CopyTo(Array dest, int index)
+ {
+ /* XXX this is kind of gross and inefficient
+ * since it makes a copy of the superclass's
+ * list */
+ object[] values = BaseGetAllValues();
+ values.CopyTo(dest, index);
+ }
+
+ public string GetKey(int index)
+ {
+ return BaseGetKey(index);
+ }
+
+ public HttpPostedFile Get(int index)
+ {
+ return (HttpPostedFile)BaseGet(index);
+ }
+
+ public HttpPostedFile Get(string key)
+ {
+ return (HttpPostedFile)BaseGet(key);
+ }
+
+ public HttpPostedFile this[string key]
+ {
+ get
+ {
+ return Get(key);
+ }
+ }
+
+ public HttpPostedFile this[int index]
+ {
+ get
+ {
+ return Get(index);
+ }
+ }
+
+ public string[] AllKeys
+ {
+ get
+ {
+ return BaseGetAllKeys();
+ }
+ }
+ }
+ class WebROCollection : NameValueCollection
+ {
+ bool got_id;
+ int id;
+
+ public bool GotID
+ {
+ get { return got_id; }
+ }
+
+ public int ID
+ {
+ get { return id; }
+ set
+ {
+ got_id = true;
+ id = value;
+ }
+ }
+ public void Protect()
+ {
+ IsReadOnly = true;
+ }
+
+ public void Unprotect()
+ {
+ IsReadOnly = false;
+ }
+
+ public override string ToString()
+ {
+ StringBuilder result = new StringBuilder();
+ foreach (string key in AllKeys)
+ {
+ if (result.Length > 0)
+ result.Append('&');
+
+ if (key != null && key.Length > 0)
+ {
+ result.Append(key);
+ result.Append('=');
+ }
+ result.Append(Get(key));
+ }
+
+ return result.ToString();
+ }
+ }
+
+ public sealed class HttpPostedFile
+ {
+ string name;
+ string content_type;
+ Stream stream;
+
+ class ReadSubStream : Stream
+ {
+ Stream s;
+ long offset;
+ long end;
+ long position;
+
+ public ReadSubStream(Stream s, long offset, long length)
+ {
+ this.s = s;
+ this.offset = offset;
+ this.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("buffer");
+
+ if (dest_offset < 0)
+ throw new ArgumentOutOfRangeException("dest_offset", "< 0");
+
+ if (count < 0)
+ throw new ArgumentOutOfRangeException("count", "< 0");
+
+ int len = buffer.Length;
+ if (dest_offset > len)
+ throw new ArgumentException("destination offset is beyond array size");
+ // reordered to avoid possible integer overflow
+ if (dest_offset > len - count)
+ throw new ArgumentException("Reading would overrun buffer");
+
+ if (count > end - position)
+ count = (int)(end - position);
+
+ if (count <= 0)
+ return 0;
+
+ s.Position = position;
+ int result = s.Read(buffer, dest_offset, count);
+ if (result > 0)
+ position += result;
+ else
+ position = end;
+
+ return result;
+ }
+
+ public override int ReadByte()
+ {
+ if (position >= end)
+ return -1;
+
+ s.Position = position;
+ int result = s.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();
+ }
+
+ long virt = real - offset;
+ if (virt < 0 || virt > Length)
+ throw new ArgumentException();
+
+ position = s.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
+ {
+ get { return true; }
+ }
+ public override bool CanSeek
+ {
+ get { return true; }
+ }
+ public override bool CanWrite
+ {
+ get { return false; }
+ }
+
+ public override long Length
+ {
+ get { return end - offset; }
+ }
+
+ public override long Position
+ {
+ get
+ {
+ return position - offset;
+ }
+ set
+ {
+ if (value > Length)
+ throw new ArgumentOutOfRangeException();
+
+ position = Seek(value, SeekOrigin.Begin);
+ }
+ }
+ }
+
+ internal HttpPostedFile(string name, string content_type, Stream base_stream, long offset, long length)
+ {
+ this.name = name;
+ this.content_type = content_type;
+ this.stream = new ReadSubStream(base_stream, offset, length);
+ }
+
+ public string ContentType
+ {
+ get
+ {
+ return (content_type);
+ }
+ }
+
+ public int ContentLength
+ {
+ get
+ {
+ return (int)stream.Length;
+ }
+ }
+
+ public string FileName
+ {
+ get
+ {
+ return (name);
+ }
+ }
+
+ public Stream InputStream
+ {
+ get
+ {
+ return (stream);
+ }
+ }
+
+ public void SaveAs(string filename)
+ {
+ byte[] buffer = new byte[16 * 1024];
+ long old_post = stream.Position;
+
+ try
+ {
+ File.Delete(filename);
+ using (FileStream fs = File.Create(filename))
+ {
+ stream.Position = 0;
+ int n;
+
+ while ((n = stream.Read(buffer, 0, 16 * 1024)) != 0)
+ {
+ fs.Write(buffer, 0, n);
+ }
+ }
+ }
+ finally
+ {
+ stream.Position = old_post;
+ }
+ }
+ }
+
+ class Helpers
+ {
+ public static readonly CultureInfo InvariantCulture = CultureInfo.InvariantCulture;
+ }
+
+ internal sealed class StrUtils
+ {
+ StrUtils() { }
+
+ public static bool StartsWith(string str1, string str2)
+ {
+ return StartsWith(str1, str2, false);
+ }
+
+ public static bool StartsWith(string str1, string str2, bool ignore_case)
+ {
+ int l2 = str2.Length;
+ if (l2 == 0)
+ return true;
+
+ int l1 = str1.Length;
+ if (l2 > l1)
+ return false;
+
+ return (0 == String.Compare(str1, 0, str2, 0, l2, ignore_case, Helpers.InvariantCulture));
+ }
+
+ public static bool EndsWith(string str1, string str2)
+ {
+ return EndsWith(str1, str2, false);
+ }
+
+ public static bool EndsWith(string str1, string str2, bool ignore_case)
+ {
+ int l2 = str2.Length;
+ if (l2 == 0)
+ return true;
+
+ int l1 = str1.Length;
+ if (l2 > l1)
+ return false;
+
+ return (0 == String.Compare(str1, l1 - l2, str2, 0, l2, ignore_case, Helpers.InvariantCulture));
+ }
+ }
+
+ class HttpMultipart
+ {
+
+ public class Element
+ {
+ public string ContentType;
+ public string Name;
+ public string Filename;
+ public Encoding Encoding;
+ public long Start;
+ public long Length;
+
+ public override string ToString()
+ {
+ return "ContentType " + ContentType + ", Name " + Name + ", Filename " + Filename + ", Start " +
+ Start.ToString() + ", Length " + Length.ToString();
+ }
+ }
+
+ Stream data;
+ string boundary;
+ byte[] boundary_bytes;
+ byte[] buffer;
+ bool at_eof;
+ Encoding encoding;
+ StringBuilder sb;
+
+ const byte HYPHEN = (byte)'-', LF = (byte)'\n', CR = (byte)'\r';
+
+ // 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;
+ //DB: 30/01/11: cannot set or read the Position in HttpListener in Win.NET
+ //var ms = new MemoryStream(32 * 1024);
+ //data.CopyTo(ms);
+ //this.data = ms;
+
+ boundary = b;
+ boundary_bytes = encoding.GetBytes(b);
+ buffer = new byte[boundary_bytes.Length + 2]; // CRLF or '--'
+ this.encoding = encoding;
+ sb = new StringBuilder();
+ }
+
+ 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();
+
+ }
+
+ static string GetContentDispositionAttribute(string l, string name)
+ {
+ int idx = l.IndexOf(name + "=\"");
+ 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 "";
+ return l.Substring(begin, end - begin);
+ }
+
+ string GetContentDispositionAttributeWithEncoding(string l, string name)
+ {
+ int idx = l.IndexOf(name + "=\"");
+ 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 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);
+ }
+
+ bool ReadBoundary()
+ {
+ try
+ {
+ string line = ReadLine();
+ while (line == "")
+ line = ReadLine();
+ if (line[0] != '-' || line[1] != '-')
+ return false;
+
+ if (!StrUtils.EndsWith(line, boundary, false))
+ return true;
+ }
+ catch
+ {
+ }
+
+ return false;
+ }
+
+ string ReadHeaders()
+ {
+ string s = ReadLine();
+ if (s == "")
+ return null;
+
+ return s;
+ }
+
+ bool CompareBytes(byte[] orig, byte[] other)
+ {
+ for (int i = orig.Length - 1; i >= 0; i--)
+ if (orig[i] != other[i])
+ return false;
+
+ return true;
+ }
+
+ 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(boundary_bytes, 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] == '-')
+ {
+ at_eof = 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;
+ }
+
+ public Element ReadNextElement()
+ {
+ if (at_eof || ReadBoundary())
+ return null;
+
+ Element elem = new Element();
+ string header;
+ while ((header = ReadHeaders()) != null)
+ {
+ if (StrUtils.StartsWith(header, "Content-Disposition:", true))
+ {
+ elem.Name = GetContentDispositionAttribute(header, "name");
+ elem.Filename = StripPath(GetContentDispositionAttributeWithEncoding(header, "filename"));
+ }
+ else if (StrUtils.StartsWith(header, "Content-Type:", true))
+ {
+ elem.ContentType = header.Substring("Content-Type:".Length).Trim();
+ elem.Encoding = GetEncoding(elem.ContentType);
+ }
+ }
+
+ long start = 0;
+ start = data.Position;
+ elem.Start = start;
+ long pos = MoveToNextBoundary();
+ if (pos == -1)
+ return null;
+
+ elem.Length = pos - start;
+ return elem;
+ }
+
+ static string StripPath(string path)
+ {
+ if (path == null || path.Length == 0)
+ return path;
+
+ if (path.IndexOf(":\\") != 1 && !path.StartsWith("\\\\"))
+ return path;
+ return path.Substring(path.LastIndexOf('\\') + 1);
+ }
+ }
+
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/SharpWebSocket.cs b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/SharpWebSocket.cs
new file mode 100644
index 000000000..7ff3a1247
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/SharpWebSocket.cs
@@ -0,0 +1,165 @@
+using System.Text;
+using MediaBrowser.Common.Events;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using WebSocketMessageType = MediaBrowser.Model.Net.WebSocketMessageType;
+using WebSocketState = MediaBrowser.Model.Net.WebSocketState;
+
+namespace MediaBrowser.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 WebSocketSharp.WebSocket WebSocket { get; set; }
+
+ private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NativeWebSocket" /> class.
+ /// </summary>
+ /// <param name="socket">The socket.</param>
+ /// <param name="logger">The logger.</param>
+ /// <exception cref="System.ArgumentNullException">socket</exception>
+ public SharpWebSocket(WebSocketSharp.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, WebSocketSharp.ErrorEventArgs e)
+ {
+ EventHelper.FireEventIfNotNull(Closed, this, EventArgs.Empty, _logger);
+ }
+
+ void socket_OnClose(object sender, WebSocketSharp.CloseEventArgs e)
+ {
+ EventHelper.FireEventIfNotNull(Closed, this, EventArgs.Empty, _logger);
+ }
+
+ void socket_OnMessage(object sender, WebSocketSharp.MessageEventArgs e)
+ {
+ if (OnReceive != 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; }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs
new file mode 100644
index 000000000..477aa3878
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs
@@ -0,0 +1,278 @@
+using MediaBrowser.Common.Net;
+using MediaBrowser.Model.Logging;
+using ServiceStack;
+using ServiceStack.Web;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using WebSocketSharp.Net;
+
+namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp
+{
+ public class WebSocketSharpListener : IHttpListener
+ {
+ private readonly ConcurrentDictionary<string, string> _localEndPoints = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ private WebSocketSharp.Net.HttpListener _listener;
+ private readonly AutoResetEvent _listenForNextRequest = new AutoResetEvent(false);
+
+ private readonly ILogger _logger;
+
+ public WebSocketSharpListener(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ public IEnumerable<string> LocalEndPoints
+ {
+ get { return _localEndPoints.Keys.ToList(); }
+ }
+
+ public Action<Exception, IRequest> ErrorHandler { get; set; }
+
+ public Func<IHttpRequest, Uri, Task> RequestHandler { get; set; }
+
+ public Action<WebSocketConnectEventArgs> WebSocketHandler { get; set; }
+
+ public void Start(IEnumerable<string> urlPrefixes)
+ {
+ if (_listener == null)
+ _listener = new WebSocketSharp.Net.HttpListener();
+
+ foreach (var prefix in urlPrefixes)
+ {
+ _logger.Info("Adding HttpListener prefix " + prefix);
+ _listener.Prefixes.Add(prefix);
+ }
+
+ _listener.Start();
+
+ Task.Factory.StartNew(Listen, TaskCreationOptions.LongRunning);
+ }
+
+ private bool IsListening
+ {
+ get { return _listener != null && _listener.IsListening; }
+ }
+
+ // Loop here to begin processing of new requests.
+ private void Listen()
+ {
+ while (IsListening)
+ {
+ if (_listener == null) return;
+
+ try
+ {
+ _listener.BeginGetContext(ListenerCallback, _listener);
+ _listenForNextRequest.WaitOne();
+ }
+ catch (Exception ex)
+ {
+ _logger.Error("Listen()", ex);
+ return;
+ }
+ if (_listener == null) return;
+ }
+ }
+
+ // Handle the processing of a request in here.
+ private void ListenerCallback(IAsyncResult asyncResult)
+ {
+ var listener = asyncResult.AsyncState as HttpListener;
+ HttpListenerContext context;
+
+ if (listener == null) return;
+ var isListening = listener.IsListening;
+
+ try
+ {
+ if (!isListening)
+ {
+ _logger.Debug("Ignoring ListenerCallback() as HttpListener is no longer listening"); return;
+ }
+ // The EndGetContext() method, as with all Begin/End asynchronous methods in the .NET Framework,
+ // blocks until there is a request to be processed or some type of data is available.
+ context = listener.EndGetContext(asyncResult);
+ }
+ catch (Exception ex)
+ {
+ // You will get an exception when httpListener.Stop() is called
+ // because there will be a thread stopped waiting on the .EndGetContext()
+ // method, and again, that is just the way most Begin/End asynchronous
+ // methods of the .NET Framework work.
+ var errMsg = ex + ": " + IsListening;
+ _logger.Warn(errMsg);
+ return;
+ }
+ finally
+ {
+ // Once we know we have a request (or exception), we signal the other thread
+ // so that it calls the BeginGetContext() (or possibly exits if we're not
+ // listening any more) method to start handling the next incoming request
+ // while we continue to process this request on a different thread.
+ _listenForNextRequest.Set();
+ }
+
+ Task.Factory.StartNew(() => InitTask(context));
+ }
+
+ private void InitTask(HttpListenerContext context)
+ {
+ try
+ {
+ var task = this.ProcessRequestAsync(context);
+ task.ContinueWith(x => HandleError(x.Exception, context), TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.AttachedToParent);
+
+ if (task.Status == TaskStatus.Created)
+ {
+ task.RunSynchronously();
+ }
+ }
+ catch (Exception ex)
+ {
+ HandleError(ex, context);
+ }
+ }
+
+ private Task ProcessRequestAsync(HttpListenerContext context)
+ {
+ var request = context.Request;
+
+ LogHttpRequest(request);
+
+ if (request.IsWebSocketRequest)
+ {
+ ProcessWebSocketRequest(context);
+ return Task.FromResult(true);
+ }
+
+ if (string.IsNullOrEmpty(context.Request.RawUrl))
+ return ((object)null).AsTaskResult();
+
+ var httpReq = GetRequest(context);
+
+ return RequestHandler(httpReq, request.Url);
+ }
+
+ /// <summary>
+ /// Logs the HTTP request.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ private void LogHttpRequest(HttpListenerRequest request)
+ {
+ var endpoint = request.LocalEndPoint;
+
+ if (endpoint != null)
+ {
+ var address = endpoint.ToString();
+
+ _localEndPoints.GetOrAdd(address, address);
+ }
+
+ LogRequest(_logger, request);
+ }
+
+ private void ProcessWebSocketRequest(HttpListenerContext ctx)
+ {
+ try
+ {
+ var webSocketContext = ctx.AcceptWebSocket(null);
+
+ if (WebSocketHandler != null)
+ {
+ WebSocketHandler(new WebSocketConnectEventArgs
+ {
+ WebSocket = new SharpWebSocket(webSocketContext.WebSocket, _logger),
+ Endpoint = ctx.Request.RemoteEndPoint.ToString()
+ });
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("AcceptWebSocketAsync error", ex);
+ ctx.Response.StatusCode = 500;
+ ctx.Response.Close();
+ }
+ }
+
+ private IHttpRequest GetRequest(HttpListenerContext httpContext)
+ {
+ var operationName = httpContext.Request.GetOperationName();
+
+ var req = new WebSocketSharpRequest(httpContext, operationName, RequestAttributes.None, _logger);
+ req.RequestAttributes = req.GetAttributes();
+
+ return req;
+ }
+
+ /// <summary>
+ /// Logs the request.
+ /// </summary>
+ /// <param name="logger">The logger.</param>
+ /// <param name="request">The request.</param>
+ private static void LogRequest(ILogger logger, HttpListenerRequest request)
+ {
+ var log = new StringBuilder();
+
+ //var headers = string.Join(",", request.Headers.AllKeys.Where(i => !string.Equals(i, "cookie", StringComparison.OrdinalIgnoreCase) && !string.Equals(i, "Referer", StringComparison.OrdinalIgnoreCase)).Select(k => k + "=" + request.Headers[k]));
+
+ //log.AppendLine("Ip: " + request.RemoteEndPoint + ". Headers: " + headers);
+
+ var type = request.IsWebSocketRequest ? "Web Socket" : "HTTP " + request.HttpMethod;
+
+ logger.LogMultiline(type + " " + request.Url, LogSeverity.Debug, log);
+ }
+
+ private void HandleError(Exception ex, HttpListenerContext context)
+ {
+ var httpReq = GetRequest(context);
+
+ if (ErrorHandler != null)
+ {
+ ErrorHandler(ex, httpReq);
+ }
+ }
+
+ 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;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs
new file mode 100644
index 000000000..7a5f6fbdc
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs
@@ -0,0 +1,402 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using Funq;
+using MediaBrowser.Model.Logging;
+using ServiceStack;
+using ServiceStack.Host;
+using ServiceStack.Web;
+using WebSocketSharp.Net;
+
+namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp
+{
+ public partial class WebSocketSharpRequest : IHttpRequest
+ {
+ public Container Container { get; set; }
+ private readonly HttpListenerRequest request;
+ private readonly IHttpResponse response;
+
+ public WebSocketSharpRequest(HttpListenerContext httpContext, string operationName, RequestAttributes requestAttributes, ILogger logger)
+ {
+ this.OperationName = operationName;
+ this.RequestAttributes = requestAttributes;
+ this.request = httpContext.Request;
+ this.response = new WebSocketSharpResponse(logger, httpContext.Response);
+
+ this.RequestPreferences = new RequestPreferences(this);
+ }
+
+ public HttpListenerRequest HttpRequest
+ {
+ get { return request; }
+ }
+
+ public object OriginalRequest
+ {
+ get { return request; }
+ }
+
+ public IResponse Response
+ {
+ get { return response; }
+ }
+
+ public IHttpResponse HttpResponse
+ {
+ get { return response; }
+ }
+
+ public RequestAttributes RequestAttributes { get; set; }
+
+ public IRequestPreferences RequestPreferences { get; private set; }
+
+ public T TryResolve<T>()
+ {
+ if (typeof(T) == typeof(IHttpRequest))
+ throw new Exception("You don't need to use IHttpRequest.TryResolve<IHttpRequest> to resolve itself");
+
+ if (typeof(T) == typeof(IHttpResponse))
+ throw new Exception("Resolve IHttpResponse with 'Response' property instead of IHttpRequest.TryResolve<IHttpResponse>");
+
+ return Container == null
+ ? HostContext.TryResolve<T>()
+ : Container.TryResolve<T>();
+ }
+
+ public string OperationName { get; set; }
+
+ public object Dto { get; set; }
+
+ public string GetRawBody()
+ {
+ if (bufferedStream != null)
+ {
+ return bufferedStream.ToArray().FromUtf8Bytes();
+ }
+
+ using (var reader = new StreamReader(InputStream))
+ {
+ return reader.ReadToEnd();
+ }
+ }
+
+ public string RawUrl
+ {
+ get { return request.RawUrl; }
+ }
+
+ public string AbsoluteUri
+ {
+ get { return request.Url.AbsoluteUri.TrimEnd('/'); }
+ }
+
+ public string UserHostAddress
+ {
+ get { return request.UserHostAddress; }
+ }
+
+ public string XForwardedFor
+ {
+ get
+ {
+ return String.IsNullOrEmpty(request.Headers[HttpHeaders.XForwardedFor]) ? null : request.Headers[HttpHeaders.XForwardedFor];
+ }
+ }
+
+ public int? XForwardedPort
+ {
+ get
+ {
+ return string.IsNullOrEmpty(request.Headers[HttpHeaders.XForwardedPort]) ? (int?)null : int.Parse(request.Headers[HttpHeaders.XForwardedPort]);
+ }
+ }
+
+ public string XForwardedProtocol
+ {
+ get
+ {
+ return string.IsNullOrEmpty(request.Headers[HttpHeaders.XForwardedProtocol]) ? null : request.Headers[HttpHeaders.XForwardedProtocol];
+ }
+ }
+
+ public string XRealIp
+ {
+ get
+ {
+ return String.IsNullOrEmpty(request.Headers[HttpHeaders.XRealIp]) ? null : request.Headers[HttpHeaders.XRealIp];
+ }
+ }
+
+ private string remoteIp;
+ public string RemoteIp
+ {
+ get
+ {
+ return remoteIp ??
+ (remoteIp = XForwardedFor ??
+ (XRealIp ??
+ ((request.RemoteEndPoint != null) ? request.RemoteEndPoint.Address.ToString() : null)));
+ }
+ }
+
+ public bool IsSecureConnection
+ {
+ get { return request.IsSecureConnection || XForwardedProtocol == "https"; }
+ }
+
+ public string[] AcceptTypes
+ {
+ get { return request.AcceptTypes; }
+ }
+
+ private Dictionary<string, object> items;
+ public Dictionary<string, object> Items
+ {
+ get { return items ?? (items = new Dictionary<string, object>()); }
+ }
+
+ private string responseContentType;
+ public string ResponseContentType
+ {
+ get
+ {
+ return responseContentType
+ ?? (responseContentType = this.GetResponseContentType());
+ }
+ set
+ {
+ this.responseContentType = value;
+ HasExplicitResponseContentType = true;
+ }
+ }
+
+ public bool HasExplicitResponseContentType { get; private set; }
+
+ private string pathInfo;
+ public string PathInfo
+ {
+ get
+ {
+ if (this.pathInfo == null)
+ {
+ var mode = HostContext.Config.HandlerFactoryPath;
+
+ var pos = request.RawUrl.IndexOf("?");
+ if (pos != -1)
+ {
+ var path = request.RawUrl.Substring(0, pos);
+ this.pathInfo = HttpRequestExtensions.GetPathInfo(
+ path,
+ mode,
+ mode ?? "");
+ }
+ else
+ {
+ this.pathInfo = request.RawUrl;
+ }
+
+ this.pathInfo = this.pathInfo.UrlDecode();
+ this.pathInfo = NormalizePathInfo(pathInfo, mode);
+ }
+ return this.pathInfo;
+ }
+ }
+
+ private Dictionary<string, System.Net.Cookie> cookies;
+ public IDictionary<string, System.Net.Cookie> Cookies
+ {
+ get
+ {
+ if (cookies == null)
+ {
+ cookies = new Dictionary<string, System.Net.Cookie>();
+ for (var i = 0; i < this.request.Cookies.Count; i++)
+ {
+ var httpCookie = this.request.Cookies[i];
+ cookies[httpCookie.Name] = new System.Net.Cookie(httpCookie.Name, httpCookie.Value, httpCookie.Path, httpCookie.Domain);
+ }
+ }
+
+ return cookies;
+ }
+ }
+
+ public string UserAgent
+ {
+ get { return request.UserAgent; }
+ }
+
+ private NameValueCollectionWrapper headers;
+ public INameValueCollection Headers
+ {
+ get { return headers ?? (headers = new NameValueCollectionWrapper(request.Headers)); }
+ }
+
+ private NameValueCollectionWrapper queryString;
+ public INameValueCollection QueryString
+ {
+ get { return queryString ?? (queryString = new NameValueCollectionWrapper(HttpUtility.ParseQueryString(request.Url.Query))); }
+ }
+
+ private NameValueCollectionWrapper formData;
+ public INameValueCollection FormData
+ {
+ get { return formData ?? (formData = new NameValueCollectionWrapper(this.Form)); }
+ }
+
+ public bool IsLocal
+ {
+ get { return request.IsLocal; }
+ }
+
+ private string httpMethod;
+ public string HttpMethod
+ {
+ get
+ {
+ return httpMethod
+ ?? (httpMethod = Param(HttpHeaders.XHttpMethodOverride)
+ ?? request.HttpMethod);
+ }
+ }
+
+ public string Verb
+ {
+ get { return HttpMethod; }
+ }
+
+ public string Param(string name)
+ {
+ return Headers[name]
+ ?? QueryString[name]
+ ?? FormData[name];
+ }
+
+ public string ContentType
+ {
+ get { return request.ContentType; }
+ }
+
+ public Encoding contentEncoding;
+ public Encoding ContentEncoding
+ {
+ get { return contentEncoding ?? request.ContentEncoding; }
+ set { contentEncoding = value; }
+ }
+
+ public Uri UrlReferrer
+ {
+ get { return request.UrlReferrer; }
+ }
+
+ 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 bool UseBufferedStream
+ {
+ get { return bufferedStream != null; }
+ set
+ {
+ bufferedStream = value
+ ? bufferedStream ?? new MemoryStream(request.InputStream.ReadFully())
+ : null;
+ }
+ }
+
+ private MemoryStream bufferedStream;
+ public Stream InputStream
+ {
+ get { return bufferedStream ?? request.InputStream; }
+ }
+
+ public long ContentLength
+ {
+ get { return request.ContentLength64; }
+ }
+
+ private IHttpFile[] httpFiles;
+ public IHttpFile[] Files
+ {
+ get
+ {
+ if (httpFiles == null)
+ {
+ if (files == null)
+ return httpFiles = new IHttpFile[0];
+
+ httpFiles = new IHttpFile[files.Count];
+ for (var i = 0; i < files.Count; i++)
+ {
+ var reqFile = files[i];
+
+ httpFiles[i] = new HttpFile
+ {
+ ContentType = reqFile.ContentType,
+ ContentLength = reqFile.ContentLength,
+ FileName = reqFile.FileName,
+ InputStream = reqFile.InputStream,
+ };
+ }
+ }
+ return httpFiles;
+ }
+ }
+
+ static Stream GetSubStream(Stream stream)
+ {
+ if (stream is MemoryStream)
+ {
+ var other = (MemoryStream)stream;
+ try
+ {
+ return new MemoryStream(other.GetBuffer(), 0, (int)other.Length, false, true);
+ }
+ catch (UnauthorizedAccessException)
+ {
+ return new MemoryStream(other.ToArray(), 0, (int)other.Length, false, true);
+ }
+ }
+
+ return stream;
+ }
+
+ static void EndSubStream(Stream stream)
+ {
+ }
+
+ public static string GetHandlerPathIfAny(string listenerUrl)
+ {
+ if (listenerUrl == null) return null;
+ var pos = listenerUrl.IndexOf("://", StringComparison.InvariantCultureIgnoreCase);
+ if (pos == -1) return null;
+ var startHostUrl = listenerUrl.Substring(pos + "://".Length);
+ var endPos = startHostUrl.IndexOf('/');
+ if (endPos == -1) return null;
+ var endHostUrl = startHostUrl.Substring(endPos + 1);
+ return String.IsNullOrEmpty(endHostUrl) ? null : endHostUrl.TrimEnd('/');
+ }
+
+ public static string NormalizePathInfo(string pathInfo, string handlerPath)
+ {
+ if (handlerPath != null && pathInfo.TrimStart('/').StartsWith(
+ handlerPath, StringComparison.InvariantCultureIgnoreCase))
+ {
+ return pathInfo.TrimStart('/').Substring(handlerPath.Length);
+ }
+
+ return pathInfo;
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs
new file mode 100644
index 000000000..2e3828512
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs
@@ -0,0 +1,144 @@
+using System;
+using System.IO;
+using System.Net;
+using MediaBrowser.Model.Logging;
+using ServiceStack;
+using ServiceStack.Host;
+using ServiceStack.Web;
+using HttpListenerResponse = WebSocketSharp.Net.HttpListenerResponse;
+
+namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp
+{
+ public class WebSocketSharpResponse : IHttpResponse
+ {
+ private readonly ILogger _logger;
+ private readonly HttpListenerResponse response;
+
+ public WebSocketSharpResponse(ILogger logger, HttpListenerResponse response)
+ {
+ _logger = logger;
+ this.response = response;
+ }
+
+ public bool UseBufferedStream { get; 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 void Redirect(string url)
+ {
+ response.Redirect(url);
+ }
+
+ public Stream OutputStream
+ {
+ get { return response.OutputStream; }
+ }
+
+ public object Dto { get; set; }
+
+ public void Write(string text)
+ {
+ try
+ {
+ var bOutput = System.Text.Encoding.UTF8.GetBytes(text);
+ response.ContentLength64 = bOutput.Length;
+
+ var outputStream = response.OutputStream;
+ outputStream.Write(bOutput, 0, bOutput.Length);
+ Close();
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Could not WriteTextToResponse: " + ex.Message, ex);
+ throw;
+ }
+ }
+
+ public void Close()
+ {
+ if (!this.IsClosed)
+ {
+ this.IsClosed = true;
+
+ try
+ {
+ this.response.CloseOutputStream(_logger);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error closing HttpListener output stream", 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 = cookie.AsHeaderValue();
+ response.Headers.Add(HttpHeaders.SetCookie, cookieStr);
+ }
+
+ public bool SendChunked
+ {
+ get { return response.SendChunked; }
+ set { response.SendChunked = value; }
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs b/MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs
index a8774f1b7..28fc094f7 100644
--- a/MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs
+++ b/MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs
@@ -36,6 +36,10 @@ namespace MediaBrowser.Server.Implementations.HttpServer
get { return _options; }
}
+ public bool Throttle { get; set; }
+ public long ThrottleLimit { get; set; }
+ public long MinThrottlePosition;
+
/// <summary>
/// Initializes a new instance of the <see cref="StreamWriter" /> class.
/// </summary>
@@ -77,6 +81,13 @@ namespace MediaBrowser.Server.Implementations.HttpServer
/// <param name="responseStream">The response stream.</param>
public void WriteTo(Stream responseStream)
{
+ if (Throttle)
+ {
+ responseStream = new ThrottledStream(responseStream, ThrottleLimit)
+ {
+ MinThrottlePosition = MinThrottlePosition
+ };
+ }
var task = WriteToAsync(responseStream);
Task.WaitAll(task);
@@ -98,7 +109,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer
}
catch (Exception ex)
{
- Logger.ErrorException("Error streaming media", ex);
+ Logger.ErrorException("Error streaming data", ex);
throw;
}
diff --git a/MediaBrowser.Server.Implementations/HttpServer/ThrottledStream.cs b/MediaBrowser.Server.Implementations/HttpServer/ThrottledStream.cs
new file mode 100644
index 000000000..067e53571
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/HttpServer/ThrottledStream.cs
@@ -0,0 +1,389 @@
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Server.Implementations.HttpServer
+{
+ /// <summary>
+ /// Class for streaming data with throttling support.
+ /// </summary>
+ public class ThrottledStream : Stream
+ {
+ /// <summary>
+ /// A constant used to specify an infinite number of bytes that can be transferred per second.
+ /// </summary>
+ public const long Infinite = 0;
+
+ #region Private members
+ /// <summary>
+ /// The base stream.
+ /// </summary>
+ private readonly Stream _baseStream;
+
+ /// <summary>
+ /// The maximum bytes per second that can be transferred through the base stream.
+ /// </summary>
+ private long _maximumBytesPerSecond;
+
+ /// <summary>
+ /// The number of bytes that has been transferred since the last throttle.
+ /// </summary>
+ private long _byteCount;
+
+ /// <summary>
+ /// The start time in milliseconds of the last throttle.
+ /// </summary>
+ private long _start;
+ #endregion
+
+ #region Properties
+ /// <summary>
+ /// Gets the current milliseconds.
+ /// </summary>
+ /// <value>The current milliseconds.</value>
+ protected long CurrentMilliseconds
+ {
+ get
+ {
+ return Environment.TickCount;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the maximum bytes per second that can be transferred through the base stream.
+ /// </summary>
+ /// <value>The maximum bytes per second.</value>
+ public long MaximumBytesPerSecond
+ {
+ get
+ {
+ return _maximumBytesPerSecond;
+ }
+ set
+ {
+ if (MaximumBytesPerSecond != value)
+ {
+ _maximumBytesPerSecond = value;
+ Reset();
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether the current stream supports reading.
+ /// </summary>
+ /// <returns>true if the stream supports reading; otherwise, false.</returns>
+ public override bool CanRead
+ {
+ get
+ {
+ return _baseStream.CanRead;
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether the current stream supports seeking.
+ /// </summary>
+ /// <value></value>
+ /// <returns>true if the stream supports seeking; otherwise, false.</returns>
+ public override bool CanSeek
+ {
+ get
+ {
+ return _baseStream.CanSeek;
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether the current stream supports writing.
+ /// </summary>
+ /// <value></value>
+ /// <returns>true if the stream supports writing; otherwise, false.</returns>
+ public override bool CanWrite
+ {
+ get
+ {
+ return _baseStream.CanWrite;
+ }
+ }
+
+ /// <summary>
+ /// Gets the length in bytes of the stream.
+ /// </summary>
+ /// <value></value>
+ /// <returns>A long value representing the length of the stream in bytes.</returns>
+ /// <exception cref="T:System.NotSupportedException">The base stream does not support seeking. </exception>
+ /// <exception cref="T:System.ObjectDisposedException">Methods were called after the stream was closed. </exception>
+ public override long Length
+ {
+ get
+ {
+ return _baseStream.Length;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the position within the current stream.
+ /// </summary>
+ /// <value></value>
+ /// <returns>The current position within the stream.</returns>
+ /// <exception cref="T:System.IO.IOException">An I/O error occurs. </exception>
+ /// <exception cref="T:System.NotSupportedException">The base stream does not support seeking. </exception>
+ /// <exception cref="T:System.ObjectDisposedException">Methods were called after the stream was closed. </exception>
+ public override long Position
+ {
+ get
+ {
+ return _baseStream.Position;
+ }
+ set
+ {
+ _baseStream.Position = value;
+ }
+ }
+ #endregion
+
+ public long MinThrottlePosition;
+
+ #region Ctor
+ /// <summary>
+ /// Initializes a new instance of the <see cref="T:ThrottledStream"/> class.
+ /// </summary>
+ /// <param name="baseStream">The base stream.</param>
+ /// <param name="maximumBytesPerSecond">The maximum bytes per second that can be transferred through the base stream.</param>
+ /// <exception cref="ArgumentNullException">Thrown when <see cref="baseStream"/> is a null reference.</exception>
+ /// <exception cref="ArgumentOutOfRangeException">Thrown when <see cref="maximumBytesPerSecond"/> is a negative value.</exception>
+ public ThrottledStream(Stream baseStream, long maximumBytesPerSecond)
+ {
+ if (baseStream == null)
+ {
+ throw new ArgumentNullException("baseStream");
+ }
+
+ if (maximumBytesPerSecond < 0)
+ {
+ throw new ArgumentOutOfRangeException("maximumBytesPerSecond",
+ maximumBytesPerSecond, "The maximum number of bytes per second can't be negative.");
+ }
+
+ _baseStream = baseStream;
+ _maximumBytesPerSecond = maximumBytesPerSecond;
+ _start = CurrentMilliseconds;
+ _byteCount = 0;
+ }
+ #endregion
+
+ #region Public methods
+ /// <summary>
+ /// Clears all buffers for this stream and causes any buffered data to be written to the underlying device.
+ /// </summary>
+ /// <exception cref="T:System.IO.IOException">An I/O error occurs.</exception>
+ public override void Flush()
+ {
+ _baseStream.Flush();
+ }
+
+ /// <summary>
+ /// Reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read.
+ /// </summary>
+ /// <param name="buffer">An array of bytes. When this method returns, the buffer contains the specified byte array with the values between offset and (offset + count - 1) replaced by the bytes read from the current source.</param>
+ /// <param name="offset">The zero-based byte offset in buffer at which to begin storing the data read from the current stream.</param>
+ /// <param name="count">The maximum number of bytes to be read from the current stream.</param>
+ /// <returns>
+ /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many bytes are not currently available, or zero (0) if the end of the stream has been reached.
+ /// </returns>
+ /// <exception cref="T:System.ArgumentException">The sum of offset and count is larger than the buffer length. </exception>
+ /// <exception cref="T:System.ObjectDisposedException">Methods were called after the stream was closed. </exception>
+ /// <exception cref="T:System.NotSupportedException">The base stream does not support reading. </exception>
+ /// <exception cref="T:System.ArgumentNullException">buffer is null. </exception>
+ /// <exception cref="T:System.IO.IOException">An I/O error occurs. </exception>
+ /// <exception cref="T:System.ArgumentOutOfRangeException">offset or count is negative. </exception>
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ Throttle(count);
+
+ return _baseStream.Read(buffer, offset, count);
+ }
+
+ /// <summary>
+ /// Sets the position within the current stream.
+ /// </summary>
+ /// <param name="offset">A byte offset relative to the origin parameter.</param>
+ /// <param name="origin">A value of type <see cref="T:System.IO.SeekOrigin"></see> indicating the reference point used to obtain the new position.</param>
+ /// <returns>
+ /// The new position within the current stream.
+ /// </returns>
+ /// <exception cref="T:System.IO.IOException">An I/O error occurs. </exception>
+ /// <exception cref="T:System.NotSupportedException">The base stream does not support seeking, such as if the stream is constructed from a pipe or console output. </exception>
+ /// <exception cref="T:System.ObjectDisposedException">Methods were called after the stream was closed. </exception>
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ return _baseStream.Seek(offset, origin);
+ }
+
+ /// <summary>
+ /// Sets the length of the current stream.
+ /// </summary>
+ /// <param name="value">The desired length of the current stream in bytes.</param>
+ /// <exception cref="T:System.NotSupportedException">The base stream does not support both writing and seeking, such as if the stream is constructed from a pipe or console output. </exception>
+ /// <exception cref="T:System.IO.IOException">An I/O error occurs. </exception>
+ /// <exception cref="T:System.ObjectDisposedException">Methods were called after the stream was closed. </exception>
+ public override void SetLength(long value)
+ {
+ _baseStream.SetLength(value);
+ }
+
+ private long _bytesWritten;
+
+ /// <summary>
+ /// Writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written.
+ /// </summary>
+ /// <param name="buffer">An array of bytes. This method copies count bytes from buffer to the current stream.</param>
+ /// <param name="offset">The zero-based byte offset in buffer at which to begin copying bytes to the current stream.</param>
+ /// <param name="count">The number of bytes to be written to the current stream.</param>
+ /// <exception cref="T:System.IO.IOException">An I/O error occurs. </exception>
+ /// <exception cref="T:System.NotSupportedException">The base stream does not support writing. </exception>
+ /// <exception cref="T:System.ObjectDisposedException">Methods were called after the stream was closed. </exception>
+ /// <exception cref="T:System.ArgumentNullException">buffer is null. </exception>
+ /// <exception cref="T:System.ArgumentException">The sum of offset and count is greater than the buffer length. </exception>
+ /// <exception cref="T:System.ArgumentOutOfRangeException">offset or count is negative. </exception>
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ Throttle(count);
+
+ _baseStream.Write(buffer, offset, count);
+
+ _bytesWritten += count;
+ }
+
+ public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ {
+ await ThrottleAsync(count, cancellationToken).ConfigureAwait(false);
+
+ await _baseStream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false);
+
+ _bytesWritten += count;
+ }
+
+ /// <summary>
+ /// Returns a <see cref="T:System.String"></see> that represents the current <see cref="T:System.Object"></see>.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="T:System.String"></see> that represents the current <see cref="T:System.Object"></see>.
+ /// </returns>
+ public override string ToString()
+ {
+ return _baseStream.ToString();
+ }
+ #endregion
+
+ #region Protected methods
+ /// <summary>
+ /// Throttles for the specified buffer size in bytes.
+ /// </summary>
+ /// <param name="bufferSizeInBytes">The buffer size in bytes.</param>
+ protected void Throttle(int bufferSizeInBytes)
+ {
+ if (_bytesWritten < MinThrottlePosition)
+ {
+ return;
+ }
+
+ // Make sure the buffer isn't empty.
+ if (_maximumBytesPerSecond <= 0 || bufferSizeInBytes <= 0)
+ {
+ return;
+ }
+
+ _byteCount += bufferSizeInBytes;
+ long elapsedMilliseconds = CurrentMilliseconds - _start;
+
+ if (elapsedMilliseconds > 0)
+ {
+ // Calculate the current bps.
+ long bps = _byteCount * 1000L / elapsedMilliseconds;
+
+ // If the bps are more then the maximum bps, try to throttle.
+ if (bps > _maximumBytesPerSecond)
+ {
+ // Calculate the time to sleep.
+ long wakeElapsed = _byteCount * 1000L / _maximumBytesPerSecond;
+ int toSleep = (int)(wakeElapsed - elapsedMilliseconds);
+
+ if (toSleep > 1)
+ {
+ try
+ {
+ // The time to sleep is more then a millisecond, so sleep.
+ Thread.Sleep(toSleep);
+ }
+ catch (ThreadAbortException)
+ {
+ // Eatup ThreadAbortException.
+ }
+
+ // A sleep has been done, reset.
+ Reset();
+ }
+ }
+ }
+ }
+
+ protected async Task ThrottleAsync(int bufferSizeInBytes, CancellationToken cancellationToken)
+ {
+ if (_bytesWritten < MinThrottlePosition)
+ {
+ return;
+ }
+
+ // Make sure the buffer isn't empty.
+ if (_maximumBytesPerSecond <= 0 || bufferSizeInBytes <= 0)
+ {
+ return;
+ }
+
+ _byteCount += bufferSizeInBytes;
+ long elapsedMilliseconds = CurrentMilliseconds - _start;
+
+ if (elapsedMilliseconds > 0)
+ {
+ // Calculate the current bps.
+ long bps = _byteCount * 1000L / elapsedMilliseconds;
+
+ // If the bps are more then the maximum bps, try to throttle.
+ if (bps > _maximumBytesPerSecond)
+ {
+ // Calculate the time to sleep.
+ long wakeElapsed = _byteCount * 1000L / _maximumBytesPerSecond;
+ int toSleep = (int)(wakeElapsed - elapsedMilliseconds);
+
+ if (toSleep > 1)
+ {
+ // The time to sleep is more then a millisecond, so sleep.
+ await Task.Delay(toSleep, cancellationToken).ConfigureAwait(false);
+
+ // A sleep has been done, reset.
+ Reset();
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Will reset the bytecount to 0 and reset the start time to the current time.
+ /// </summary>
+ protected void Reset()
+ {
+ long difference = CurrentMilliseconds - _start;
+
+ // Only reset counters when a known history is available of more then 1 second.
+ if (difference > 1000)
+ {
+ _byteCount = 0;
+ _start = CurrentMilliseconds;
+ }
+ }
+ #endregion
+ }
+} \ No newline at end of file