aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations
diff options
context:
space:
mode:
authorLuke <luke.pulverenti@gmail.com>2017-02-12 20:17:43 -0500
committerGitHub <noreply@github.com>2017-02-12 20:17:43 -0500
commit273aa822cfc37558883dbdd17647829abcf34758 (patch)
treedb858e3dfacfe4bd3aac6c701806a15203b8d63e /Emby.Server.Implementations
parent29c24420978ef324d66a381b71e0f3e3b2f294cb (diff)
parent511a8702c29445288251fcf841c394e837db19cc (diff)
Merge pull request #2466 from MediaBrowser/dev
Dev
Diffstat (limited to 'Emby.Server.Implementations')
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj7
-rw-r--r--Emby.Server.Implementations/HttpServer/HttpListenerHost.cs164
-rw-r--r--Emby.Server.Implementations/HttpServer/HttpResultFactory.cs8
-rw-r--r--Emby.Server.Implementations/Services/HttpResult.cs59
-rw-r--r--Emby.Server.Implementations/Services/RequestHelper.cs51
-rw-r--r--Emby.Server.Implementations/Services/ResponseHelper.cs178
-rw-r--r--Emby.Server.Implementations/Services/ServiceController.cs189
-rw-r--r--Emby.Server.Implementations/Services/ServiceExec.cs166
-rw-r--r--Emby.Server.Implementations/Services/ServiceHandler.cs297
-rw-r--r--Emby.Server.Implementations/Services/ServiceMethod.cs24
10 files changed, 1083 insertions, 60 deletions
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index b4bb92cfa..b1601df05 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -215,6 +215,13 @@
<Compile Include="Security\RegRecord.cs" />
<Compile Include="ServerManager\ServerManager.cs" />
<Compile Include="ServerManager\WebSocketConnection.cs" />
+ <Compile Include="Services\ServiceMethod.cs" />
+ <Compile Include="Services\ResponseHelper.cs" />
+ <Compile Include="Services\HttpResult.cs" />
+ <Compile Include="Services\RequestHelper.cs" />
+ <Compile Include="Services\ServiceHandler.cs" />
+ <Compile Include="Services\ServiceController.cs" />
+ <Compile Include="Services\ServiceExec.cs" />
<Compile Include="Session\HttpSessionController.cs" />
<Compile Include="Session\SessionManager.cs" />
<Compile Include="Session\SessionWebSocketListener.cs" />
diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
index 322cdf4f0..99ec146d7 100644
--- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
+++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
@@ -3,7 +3,6 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Logging;
using ServiceStack;
-using ServiceStack.Host;
using System;
using System.Collections.Generic;
using System.IO;
@@ -13,6 +12,7 @@ using System.Text;
using System.Threading.Tasks;
using Emby.Server.Implementations.HttpServer;
using Emby.Server.Implementations.HttpServer.SocketSharp;
+using Emby.Server.Implementations.Services;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Security;
using MediaBrowser.Controller;
@@ -61,12 +61,15 @@ namespace Emby.Server.Implementations.HttpServer
private readonly Func<Type, Func<string, object>> _funcParseFn;
private readonly bool _enableDualModeSockets;
+ public List<Action<IRequest, IResponse, object>> RequestFilters { get; set; }
+ private Dictionary<Type, Type> ServiceOperationsMap = new Dictionary<Type, Type>();
+
public HttpListenerHost(IServerApplicationHost applicationHost,
ILogger logger,
IServerConfigurationManager config,
string serviceName,
string defaultRedirectPath, INetworkManager networkManager, IMemoryStreamFactory memoryStreamProvider, ITextEncoding textEncoding, ISocketFactory socketFactory, ICryptoProvider cryptoProvider, IJsonSerializer jsonSerializer, IXmlSerializer xmlSerializer, IEnvironmentInfo environment, ICertificate certificate, IStreamFactory streamFactory, Func<Type, Func<string, object>> funcParseFn, bool enableDualModeSockets)
- : base(serviceName)
+ : base()
{
_appHost = applicationHost;
DefaultRedirectPath = defaultRedirectPath;
@@ -85,6 +88,8 @@ namespace Emby.Server.Implementations.HttpServer
_config = config;
_logger = logger;
+
+ RequestFilters = new List<Action<IRequest, IResponse, object>>();
}
public string GlobalResponse { get; set; }
@@ -99,18 +104,7 @@ namespace Emby.Server.Implementations.HttpServer
{typeof (ArgumentException), 400}
};
- public override void Configure()
- {
- var requestFilters = _appHost.GetExports<IRequestFilter>().ToList();
- foreach (var filter in requestFilters)
- {
- GlobalRequestFilters.Add(filter.Filter);
- }
-
- GlobalResponseFilters.Add(new ResponseFilter(_logger).FilterResponse);
- }
-
- protected override ILogger Logger
+ protected ILogger Logger
{
get
{
@@ -118,32 +112,73 @@ namespace Emby.Server.Implementations.HttpServer
}
}
- public override T Resolve<T>()
+ public override object CreateInstance(Type type)
{
- return _appHost.Resolve<T>();
+ return _appHost.CreateInstance(type);
}
- public override T TryResolve<T>()
+ private ServiceController CreateServiceController()
{
- return _appHost.TryResolve<T>();
+ var types = _restServices.Select(r => r.GetType()).ToArray();
+
+ return new ServiceController(() => types);
}
- public override object CreateInstance(Type type)
+ /// <summary>
+ /// Applies the request filters. Returns whether or not the request has been handled
+ /// and no more processing should be done.
+ /// </summary>
+ /// <returns></returns>
+ public void ApplyRequestFilters(IRequest req, IResponse res, object requestDto)
{
- return _appHost.CreateInstance(type);
+ //Exec all RequestFilter attributes with Priority < 0
+ var attributes = GetRequestFilterAttributes(requestDto.GetType());
+ var i = 0;
+ for (; i < attributes.Length && attributes[i].Priority < 0; i++)
+ {
+ var attribute = attributes[i];
+ attribute.RequestFilter(req, res, requestDto);
+ }
+
+ //Exec global filters
+ foreach (var requestFilter in RequestFilters)
+ {
+ requestFilter(req, res, requestDto);
+ }
+
+ //Exec remaining RequestFilter attributes with Priority >= 0
+ for (; i < attributes.Length && attributes[i].Priority >= 0; i++)
+ {
+ var attribute = attributes[i];
+ attribute.RequestFilter(req, res, requestDto);
+ }
}
- protected override ServiceController CreateServiceController()
+ public Type GetServiceTypeByRequest(Type requestType)
{
- var types = _restServices.Select(r => r.GetType()).ToArray();
+ Type serviceType;
+ ServiceOperationsMap.TryGetValue(requestType, out serviceType);
+ return serviceType;
+ }
- return new ServiceController(() => types);
+ public void AddServiceInfo(Type serviceType, Type requestType, Type responseType)
+ {
+ ServiceOperationsMap[requestType] = serviceType;
}
- public override ServiceStackHost Start(string listeningAtUrlBase)
+ private IHasRequestFilter[] GetRequestFilterAttributes(Type requestDtoType)
{
- StartListener();
- return this;
+ var attributes = requestDtoType.AllAttributes().OfType<IHasRequestFilter>().ToList();
+
+ var serviceType = GetServiceTypeByRequest(requestDtoType);
+ if (serviceType != null)
+ {
+ attributes.AddRange(serviceType.AllAttributes().OfType<IHasRequestFilter>());
+ }
+
+ attributes.Sort((x, y) => x.Priority - y.Priority);
+
+ return attributes.ToArray();
}
/// <summary>
@@ -531,11 +566,11 @@ namespace Emby.Server.Implementations.HttpServer
return;
}
- var handler = HttpHandlerFactory.GetHandler(httpReq, _logger);
+ var handler = GetServiceHandler(httpReq);
if (handler != null)
{
- await handler.ProcessRequestAsync(httpReq, httpRes, operationName).ConfigureAwait(false);
+ await handler.ProcessRequestAsync(this, httpReq, httpRes, Logger, operationName).ConfigureAwait(false);
}
else
{
@@ -565,6 +600,35 @@ namespace Emby.Server.Implementations.HttpServer
}
}
+ // Entry point for HttpListener
+ public ServiceHandler GetServiceHandler(IHttpRequest httpReq)
+ {
+ var pathInfo = httpReq.PathInfo;
+
+ var pathParts = pathInfo.TrimStart('/').Split('/');
+ if (pathParts.Length == 0)
+ {
+ _logger.Error("Path parts empty for PathInfo: {0}, Url: {1}", pathInfo, httpReq.RawUrl);
+ return null;
+ }
+
+ string contentType;
+ var restPath = ServiceHandler.FindMatchingRestPath(httpReq.HttpMethod, pathInfo, _logger, out contentType);
+
+ if (restPath != null)
+ {
+ return new ServiceHandler
+ {
+ RestPath = restPath,
+ ResponseContentType = contentType
+ };
+ }
+
+ _logger.Error("Could not find handler for {0}", pathInfo);
+ return null;
+ }
+
+
private void Write(IResponse response, string text)
{
var bOutput = Encoding.UTF8.GetBytes(text);
@@ -580,6 +644,7 @@ namespace Emby.Server.Implementations.HttpServer
httpRes.AddHeader("Location", url);
}
+ public ServiceController ServiceController { get; private set; }
/// <summary>
/// Adds the rest handlers.
@@ -593,12 +658,22 @@ namespace Emby.Server.Implementations.HttpServer
_logger.Info("Calling ServiceStack AppHost.Init");
- base.Init();
+ Instance = this;
+
+ ServiceController.Init(this);
+
+ var requestFilters = _appHost.GetExports<IRequestFilter>().ToList();
+ foreach (var filter in requestFilters)
+ {
+ RequestFilters.Add(filter.Filter);
+ }
+
+ GlobalResponseFilters.Add(new ResponseFilter(_logger).FilterResponse);
}
public override RouteAttribute[] GetRouteAttributes(Type requestType)
{
- var routes = base.GetRouteAttributes(requestType).ToList();
+ var routes = requestType.AllAttributes<RouteAttribute>();
var clone = routes.ToList();
foreach (var route in clone)
@@ -628,33 +703,6 @@ namespace Emby.Server.Implementations.HttpServer
return routes.ToArray();
}
- public override object GetTaskResult(Task task, string requestName)
- {
- try
- {
- var taskObject = task as Task<object>;
- if (taskObject != null)
- {
- return taskObject.Result;
- }
-
- task.Wait();
-
- var type = task.GetType().GetTypeInfo();
- if (!type.IsGenericType)
- {
- return null;
- }
-
- Logger.Warn("Getting task result from " + requestName + " using reflection. For better performance have your api return Task<object>");
- return type.GetDeclaredProperty("Result").GetValue(task);
- }
- catch (TypeAccessException)
- {
- return null; //return null for void Task's
- }
- }
-
public override Func<string, object> GetParseFn(Type propertyType)
{
return _funcParseFn(propertyType);
@@ -740,7 +788,7 @@ namespace Emby.Server.Implementations.HttpServer
public void StartServer(IEnumerable<string> urlPrefixes)
{
UrlPrefixes = urlPrefixes.ToList();
- Start(UrlPrefixes.First());
+ StartListener();
}
}
} \ No newline at end of file
diff --git a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs
index e78446bc8..3f756fc7a 100644
--- a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs
+++ b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs
@@ -13,10 +13,10 @@ using System.Text;
using System.Threading.Tasks;
using System.Xml;
using Emby.Server.Implementations.HttpServer;
+using Emby.Server.Implementations.Services;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Services;
using ServiceStack;
-using ServiceStack.Host;
using IRequest = MediaBrowser.Model.Services.IRequest;
using MimeTypes = MediaBrowser.Model.Net.MimeTypes;
using StreamWriter = Emby.Server.Implementations.HttpServer.StreamWriter;
@@ -203,7 +203,11 @@ namespace Emby.Server.Implementations.HttpServer
// Do not use the memoryStreamFactory here, they don't place nice with compression
using (var ms = new MemoryStream())
{
- ContentTypes.Instance.SerializeToStream(request, dto, ms);
+ var contentType = request.ResponseContentType;
+ var writerFn = RequestHelper.GetResponseWriter(contentType);
+
+ writerFn(dto, ms);
+
ms.Position = 0;
var responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
diff --git a/Emby.Server.Implementations/Services/HttpResult.cs b/Emby.Server.Implementations/Services/HttpResult.cs
new file mode 100644
index 000000000..585c3e4f8
--- /dev/null
+++ b/Emby.Server.Implementations/Services/HttpResult.cs
@@ -0,0 +1,59 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Services;
+using ServiceStack;
+
+namespace Emby.Server.Implementations.Services
+{
+ public class HttpResult
+ : IHttpResult, IAsyncStreamWriter
+ {
+ public object Response { get; set; }
+
+ public HttpResult(object response, string contentType, HttpStatusCode statusCode)
+ {
+ this.Headers = new Dictionary<string, string>();
+ this.Cookies = new List<Cookie>();
+
+ this.Response = response;
+ this.ContentType = contentType;
+ this.StatusCode = statusCode;
+ }
+
+ public string ContentType { get; set; }
+
+ public IDictionary<string, string> Headers { get; private set; }
+
+ public List<Cookie> Cookies { get; private set; }
+
+ public int Status { get; set; }
+
+ public HttpStatusCode StatusCode
+ {
+ get { return (HttpStatusCode)Status; }
+ set { Status = (int)value; }
+ }
+
+ public IRequest RequestContext { get; set; }
+
+ public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken)
+ {
+ var response = RequestContext != null ? RequestContext.Response : null;
+
+ var bytesResponse = this.Response as byte[];
+ if (bytesResponse != null)
+ {
+ if (response != null)
+ response.SetContentLength(bytesResponse.Length);
+
+ await responseStream.WriteAsync(bytesResponse, 0, bytesResponse.Length).ConfigureAwait(false);
+ return;
+ }
+
+ await ResponseHelper.WriteObject(this.RequestContext, this.Response, response).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Services/RequestHelper.cs b/Emby.Server.Implementations/Services/RequestHelper.cs
new file mode 100644
index 000000000..8cfc3d089
--- /dev/null
+++ b/Emby.Server.Implementations/Services/RequestHelper.cs
@@ -0,0 +1,51 @@
+using System;
+using System.IO;
+using ServiceStack;
+
+namespace Emby.Server.Implementations.Services
+{
+ public class RequestHelper
+ {
+ public static Func<Type, Stream, object> GetRequestReader(string contentType)
+ {
+ switch (GetContentTypeWithoutEncoding(contentType))
+ {
+ case "application/xml":
+ case "text/xml":
+ case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml
+ return ServiceStackHost.Instance.DeserializeXml;
+
+ case "application/json":
+ case "text/json":
+ return ServiceStackHost.Instance.DeserializeJson;
+ }
+
+ return null;
+ }
+
+ public static Action<object, Stream> GetResponseWriter(string contentType)
+ {
+ switch (GetContentTypeWithoutEncoding(contentType))
+ {
+ case "application/xml":
+ case "text/xml":
+ case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml
+ return (o, s) => ServiceStackHost.Instance.SerializeToXml(o, s);
+
+ case "application/json":
+ case "text/json":
+ return (o, s) => ServiceStackHost.Instance.SerializeToJson(o, s);
+ }
+
+ return null;
+ }
+
+ private static string GetContentTypeWithoutEncoding(string contentType)
+ {
+ return contentType == null
+ ? null
+ : contentType.Split(';')[0].ToLower().Trim();
+ }
+
+ }
+} \ No newline at end of file
diff --git a/Emby.Server.Implementations/Services/ResponseHelper.cs b/Emby.Server.Implementations/Services/ResponseHelper.cs
new file mode 100644
index 000000000..1af70ad7f
--- /dev/null
+++ b/Emby.Server.Implementations/Services/ResponseHelper.cs
@@ -0,0 +1,178 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Services;
+
+namespace Emby.Server.Implementations.Services
+{
+ public static class ResponseHelper
+ {
+ private static async Task<bool> WriteToOutputStream(IResponse response, object result)
+ {
+ var asyncStreamWriter = result as IAsyncStreamWriter;
+ if (asyncStreamWriter != null)
+ {
+ await asyncStreamWriter.WriteToAsync(response.OutputStream, CancellationToken.None).ConfigureAwait(false);
+ return true;
+ }
+
+ var streamWriter = result as IStreamWriter;
+ if (streamWriter != null)
+ {
+ streamWriter.WriteTo(response.OutputStream);
+ return true;
+ }
+
+ var stream = result as Stream;
+ if (stream != null)
+ {
+ using (stream)
+ {
+ await stream.CopyToAsync(response.OutputStream).ConfigureAwait(false);
+ return true;
+ }
+ }
+
+ var bytes = result as byte[];
+ if (bytes != null)
+ {
+ response.ContentType = "application/octet-stream";
+ response.SetContentLength(bytes.Length);
+
+ await response.OutputStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
+ return true;
+ }
+
+ return false;
+ }
+
+ public static Task WriteToResponse(IResponse httpRes, IRequest httpReq, object result)
+ {
+ if (result == null)
+ {
+ if (httpRes.StatusCode == (int)HttpStatusCode.OK)
+ {
+ httpRes.StatusCode = (int)HttpStatusCode.NoContent;
+ }
+
+ httpRes.SetContentLength(0);
+ return Task.FromResult(true);
+ }
+
+ var httpResult = result as IHttpResult;
+ if (httpResult != null)
+ {
+ httpResult.RequestContext = httpReq;
+ httpReq.ResponseContentType = httpResult.ContentType ?? httpReq.ResponseContentType;
+ return WriteToResponseInternal(httpRes, httpResult, httpReq);
+ }
+
+ return WriteToResponseInternal(httpRes, result, httpReq);
+ }
+
+ /// <summary>
+ /// Writes to response.
+ /// Response headers are customizable by implementing IHasHeaders an returning Dictionary of Http headers.
+ /// </summary>
+ /// <param name="response">The response.</param>
+ /// <param name="result">Whether or not it was implicity handled by ServiceStack's built-in handlers.</param>
+ /// <param name="request">The serialization context.</param>
+ /// <returns></returns>
+ private static async Task WriteToResponseInternal(IResponse response, object result, IRequest request)
+ {
+ var defaultContentType = request.ResponseContentType;
+
+ var httpResult = result as IHttpResult;
+ if (httpResult != null)
+ {
+ if (httpResult.RequestContext == null)
+ httpResult.RequestContext = request;
+
+ response.StatusCode = httpResult.Status;
+ response.StatusDescription = httpResult.StatusCode.ToString();
+ if (string.IsNullOrEmpty(httpResult.ContentType))
+ {
+ httpResult.ContentType = defaultContentType;
+ }
+ response.ContentType = httpResult.ContentType;
+
+ if (httpResult.Cookies != null)
+ {
+ var httpRes = response as IHttpResponse;
+ if (httpRes != null)
+ {
+ foreach (var cookie in httpResult.Cookies)
+ {
+ httpRes.SetCookie(cookie);
+ }
+ }
+ }
+ }
+
+ var responseOptions = result as IHasHeaders;
+ if (responseOptions != null)
+ {
+ foreach (var responseHeaders in responseOptions.Headers)
+ {
+ if (string.Equals(responseHeaders.Key, "Content-Length", StringComparison.OrdinalIgnoreCase))
+ {
+ response.SetContentLength(long.Parse(responseHeaders.Value));
+ continue;
+ }
+
+ response.AddHeader(responseHeaders.Key, responseHeaders.Value);
+ }
+ }
+
+ //ContentType='text/html' is the default for a HttpResponse
+ //Do not override if another has been set
+ if (response.ContentType == null || response.ContentType == "text/html")
+ {
+ response.ContentType = defaultContentType;
+ }
+
+ if (new HashSet<string> { "application/json", }.Contains(response.ContentType))
+ {
+ response.ContentType += "; charset=utf-8";
+ }
+
+ var writeToOutputStreamResult = await WriteToOutputStream(response, result).ConfigureAwait(false);
+ if (writeToOutputStreamResult)
+ {
+ return;
+ }
+
+ var responseText = result as string;
+ if (responseText != null)
+ {
+ var bytes = Encoding.UTF8.GetBytes(responseText);
+ response.SetContentLength(bytes.Length);
+ await response.OutputStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
+ return;
+ }
+
+ await WriteObject(request, result, response).ConfigureAwait(false);
+ }
+
+ public static async Task WriteObject(IRequest request, object result, IResponse response)
+ {
+ var contentType = request.ResponseContentType;
+ var serializer = RequestHelper.GetResponseWriter(contentType);
+
+ using (var ms = new MemoryStream())
+ {
+ serializer(result, ms);
+
+ ms.Position = 0;
+ response.SetContentLength(ms.Length);
+ await ms.CopyToAsync(response.OutputStream).ConfigureAwait(false);
+ }
+
+ //serializer(result, outputStream);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Services/ServiceController.cs b/Emby.Server.Implementations/Services/ServiceController.cs
new file mode 100644
index 000000000..714a16df5
--- /dev/null
+++ b/Emby.Server.Implementations/Services/ServiceController.cs
@@ -0,0 +1,189 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Threading.Tasks;
+using Emby.Server.Implementations.HttpServer;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Services;
+using ServiceStack;
+
+namespace Emby.Server.Implementations.Services
+{
+ public delegate Task<object> InstanceExecFn(IRequest requestContext, object intance, object request);
+ public delegate object ActionInvokerFn(object intance, object request);
+ public delegate void VoidActionInvokerFn(object intance, object request);
+
+ public class ServiceController
+ {
+ public static ServiceController Instance;
+ private readonly Func<IEnumerable<Type>> _resolveServicesFn;
+
+ public ServiceController(Func<IEnumerable<Type>> resolveServicesFn)
+ {
+ Instance = this;
+ _resolveServicesFn = resolveServicesFn;
+ }
+
+ public void Init(HttpListenerHost appHost)
+ {
+ foreach (var serviceType in _resolveServicesFn())
+ {
+ RegisterService(appHost, serviceType);
+ }
+ }
+
+ private Type[] GetGenericArguments(Type type)
+ {
+ return type.GetTypeInfo().IsGenericTypeDefinition
+ ? type.GetTypeInfo().GenericTypeParameters
+ : type.GetTypeInfo().GenericTypeArguments;
+ }
+
+ public void RegisterService(HttpListenerHost appHost, Type serviceType)
+ {
+ var processedReqs = new HashSet<Type>();
+
+ var actions = ServiceExecGeneral.Reset(serviceType);
+
+ foreach (var mi in serviceType.GetActions())
+ {
+ var requestType = mi.GetParameters()[0].ParameterType;
+ if (processedReqs.Contains(requestType)) continue;
+ processedReqs.Add(requestType);
+
+ ServiceExecGeneral.CreateServiceRunnersFor(requestType, actions);
+
+ var returnMarker = requestType.GetTypeWithGenericTypeDefinitionOf(typeof(IReturn<>));
+ var responseType = returnMarker != null ?
+ GetGenericArguments(returnMarker)[0]
+ : mi.ReturnType != typeof(object) && mi.ReturnType != typeof(void) ?
+ mi.ReturnType
+ : Type.GetType(requestType.FullName + "Response");
+
+ RegisterRestPaths(requestType);
+
+ appHost.AddServiceInfo(serviceType, requestType, responseType);
+ }
+ }
+
+ public readonly Dictionary<string, List<RestPath>> RestPathMap = new Dictionary<string, List<RestPath>>(StringComparer.OrdinalIgnoreCase);
+
+ public void RegisterRestPaths(Type requestType)
+ {
+ var appHost = ServiceStackHost.Instance;
+ var attrs = appHost.GetRouteAttributes(requestType);
+ foreach (MediaBrowser.Model.Services.RouteAttribute attr in attrs)
+ {
+ var restPath = new RestPath(requestType, attr.Path, attr.Verbs, attr.Summary, attr.Notes);
+
+ if (!restPath.IsValid)
+ throw new NotSupportedException(string.Format(
+ "RestPath '{0}' on Type '{1}' is not Valid", attr.Path, requestType.GetOperationName()));
+
+ RegisterRestPath(restPath);
+ }
+ }
+
+ private static readonly char[] InvalidRouteChars = new[] { '?', '&' };
+
+ public void RegisterRestPath(RestPath restPath)
+ {
+ if (!restPath.Path.StartsWith("/"))
+ throw new ArgumentException(string.Format("Route '{0}' on '{1}' must start with a '/'", restPath.Path, restPath.RequestType.GetOperationName()));
+ if (restPath.Path.IndexOfAny(InvalidRouteChars) != -1)
+ throw new ArgumentException(string.Format("Route '{0}' on '{1}' contains invalid chars. " +
+ "See https://github.com/ServiceStack/ServiceStack/wiki/Routing for info on valid routes.", restPath.Path, restPath.RequestType.GetOperationName()));
+
+ List<RestPath> pathsAtFirstMatch;
+ if (!RestPathMap.TryGetValue(restPath.FirstMatchHashKey, out pathsAtFirstMatch))
+ {
+ pathsAtFirstMatch = new List<RestPath>();
+ RestPathMap[restPath.FirstMatchHashKey] = pathsAtFirstMatch;
+ }
+ pathsAtFirstMatch.Add(restPath);
+ }
+
+ public RestPath GetRestPathForRequest(string httpMethod, string pathInfo, ILogger logger)
+ {
+ var matchUsingPathParts = RestPath.GetPathPartsForMatching(pathInfo);
+
+ List<RestPath> firstMatches;
+
+ var yieldedHashMatches = RestPath.GetFirstMatchHashKeys(matchUsingPathParts);
+ foreach (var potentialHashMatch in yieldedHashMatches)
+ {
+ if (!this.RestPathMap.TryGetValue(potentialHashMatch, out firstMatches))
+ {
+ continue;
+ }
+
+ var bestScore = -1;
+ foreach (var restPath in firstMatches)
+ {
+ var score = restPath.MatchScore(httpMethod, matchUsingPathParts, logger);
+ if (score > bestScore) bestScore = score;
+ }
+
+ if (bestScore > 0)
+ {
+ foreach (var restPath in firstMatches)
+ {
+ if (bestScore == restPath.MatchScore(httpMethod, matchUsingPathParts, logger))
+ return restPath;
+ }
+ }
+ }
+
+ var yieldedWildcardMatches = RestPath.GetFirstMatchWildCardHashKeys(matchUsingPathParts);
+ foreach (var potentialHashMatch in yieldedWildcardMatches)
+ {
+ if (!this.RestPathMap.TryGetValue(potentialHashMatch, out firstMatches)) continue;
+
+ var bestScore = -1;
+ foreach (var restPath in firstMatches)
+ {
+ var score = restPath.MatchScore(httpMethod, matchUsingPathParts, logger);
+ if (score > bestScore) bestScore = score;
+ }
+ if (bestScore > 0)
+ {
+ foreach (var restPath in firstMatches)
+ {
+ if (bestScore == restPath.MatchScore(httpMethod, matchUsingPathParts, logger))
+ return restPath;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public async Task<object> Execute(HttpListenerHost appHost, object requestDto, IRequest req)
+ {
+ req.Dto = requestDto;
+ var requestType = requestDto.GetType();
+ req.OperationName = requestType.Name;
+
+ var serviceType = appHost.GetServiceTypeByRequest(requestType);
+
+ var service = appHost.CreateInstance(serviceType);
+
+ //var service = typeFactory.CreateInstance(serviceType);
+
+ var serviceRequiresContext = service as IRequiresRequest;
+ if (serviceRequiresContext != null)
+ {
+ serviceRequiresContext.Request = req;
+ }
+
+ if (req.Dto == null) // Don't override existing batched DTO[]
+ req.Dto = requestDto;
+
+ //Executes the service and returns the result
+ var response = await ServiceExecGeneral.Execute(serviceType, req, service, requestDto, requestType.GetOperationName()).ConfigureAwait(false);
+
+ return response;
+ }
+ }
+
+} \ No newline at end of file
diff --git a/Emby.Server.Implementations/Services/ServiceExec.cs b/Emby.Server.Implementations/Services/ServiceExec.cs
new file mode 100644
index 000000000..59af3078f
--- /dev/null
+++ b/Emby.Server.Implementations/Services/ServiceExec.cs
@@ -0,0 +1,166 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Services;
+using ServiceStack;
+
+namespace Emby.Server.Implementations.Services
+{
+ public static class ServiceExecExtensions
+ {
+ public static HashSet<string> AllVerbs = new HashSet<string>(new[] {
+ "OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "CONNECT", // RFC 2616
+ "PROPFIND", "PROPPATCH", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", // RFC 2518
+ "VERSION-CONTROL", "REPORT", "CHECKOUT", "CHECKIN", "UNCHECKOUT",
+ "MKWORKSPACE", "UPDATE", "LABEL", "MERGE", "BASELINE-CONTROL", "MKACTIVITY", // RFC 3253
+ "ORDERPATCH", // RFC 3648
+ "ACL", // RFC 3744
+ "PATCH", // https://datatracker.ietf.org/doc/draft-dusseault-http-patch/
+ "SEARCH", // https://datatracker.ietf.org/doc/draft-reschke-webdav-search/
+ "BCOPY", "BDELETE", "BMOVE", "BPROPFIND", "BPROPPATCH", "NOTIFY",
+ "POLL", "SUBSCRIBE", "UNSUBSCRIBE"
+ });
+
+ public static IEnumerable<MethodInfo> GetActions(this Type serviceType)
+ {
+ foreach (var mi in serviceType.GetRuntimeMethods().Where(i => i.IsPublic && !i.IsStatic))
+ {
+ if (mi.GetParameters().Length != 1)
+ continue;
+
+ var actionName = mi.Name;
+ if (!AllVerbs.Contains(actionName, StringComparer.OrdinalIgnoreCase) && !string.Equals(actionName, ServiceMethod.AnyAction, StringComparison.OrdinalIgnoreCase))
+ continue;
+
+ yield return mi;
+ }
+ }
+ }
+
+ internal static class ServiceExecGeneral
+ {
+ public static Dictionary<string, ServiceMethod> execMap = new Dictionary<string, ServiceMethod>();
+
+ public static void CreateServiceRunnersFor(Type requestType, List<ServiceMethod> actions)
+ {
+ foreach (var actionCtx in actions)
+ {
+ if (execMap.ContainsKey(actionCtx.Id)) continue;
+
+ execMap[actionCtx.Id] = actionCtx;
+ }
+ }
+
+ public static async Task<object> Execute(Type serviceType, IRequest request, object instance, object requestDto, string requestName)
+ {
+ var actionName = request.Verb ?? "POST";
+
+ ServiceMethod actionContext;
+ if (ServiceExecGeneral.execMap.TryGetValue(ServiceMethod.Key(serviceType, actionName, requestName), out actionContext)
+ || ServiceExecGeneral.execMap.TryGetValue(ServiceMethod.AnyKey(serviceType, requestName), out actionContext))
+ {
+ if (actionContext.RequestFilters != null)
+ {
+ foreach (var requestFilter in actionContext.RequestFilters)
+ {
+ requestFilter.RequestFilter(request, request.Response, requestDto);
+ if (request.Response.IsClosed) return null;
+ }
+ }
+
+ var response = actionContext.ServiceAction(instance, requestDto);
+
+ var taskResponse = response as Task;
+ if (taskResponse != null)
+ {
+ await taskResponse.ConfigureAwait(false);
+ response = ServiceHandler.GetTaskResult(taskResponse);
+ }
+
+ return response;
+ }
+
+ var expectedMethodName = actionName.Substring(0, 1) + actionName.Substring(1).ToLower();
+ throw new NotImplementedException(string.Format("Could not find method named {1}({0}) or Any({0}) on Service {2}", requestDto.GetType().GetOperationName(), expectedMethodName, serviceType.GetOperationName()));
+ }
+
+ public static List<ServiceMethod> Reset(Type serviceType)
+ {
+ var actions = new List<ServiceMethod>();
+
+ foreach (var mi in serviceType.GetActions())
+ {
+ var actionName = mi.Name;
+ var args = mi.GetParameters();
+
+ var requestType = args[0].ParameterType;
+ var actionCtx = new ServiceMethod
+ {
+ Id = ServiceMethod.Key(serviceType, actionName, requestType.GetOperationName())
+ };
+
+ try
+ {
+ actionCtx.ServiceAction = CreateExecFn(serviceType, requestType, mi);
+ }
+ catch
+ {
+ //Potential problems with MONO, using reflection for fallback
+ actionCtx.ServiceAction = (service, request) =>
+ mi.Invoke(service, new[] { request });
+ }
+
+ var reqFilters = new List<IHasRequestFilter>();
+
+ foreach (var attr in mi.GetCustomAttributes(true))
+ {
+ var hasReqFilter = attr as IHasRequestFilter;
+
+ if (hasReqFilter != null)
+ reqFilters.Add(hasReqFilter);
+ }
+
+ if (reqFilters.Count > 0)
+ actionCtx.RequestFilters = reqFilters.OrderBy(i => i.Priority).ToArray();
+
+ actions.Add(actionCtx);
+ }
+
+ return actions;
+ }
+
+ private static ActionInvokerFn CreateExecFn(Type serviceType, Type requestType, MethodInfo mi)
+ {
+ var serviceParam = Expression.Parameter(typeof(object), "serviceObj");
+ var serviceStrong = Expression.Convert(serviceParam, serviceType);
+
+ var requestDtoParam = Expression.Parameter(typeof(object), "requestDto");
+ var requestDtoStrong = Expression.Convert(requestDtoParam, requestType);
+
+ Expression callExecute = Expression.Call(
+ serviceStrong, mi, requestDtoStrong);
+
+ if (mi.ReturnType != typeof(void))
+ {
+ var executeFunc = Expression.Lambda<ActionInvokerFn>
+ (callExecute, serviceParam, requestDtoParam).Compile();
+
+ return executeFunc;
+ }
+ else
+ {
+ var executeFunc = Expression.Lambda<VoidActionInvokerFn>
+ (callExecute, serviceParam, requestDtoParam).Compile();
+
+ return (service, request) =>
+ {
+ executeFunc(service, request);
+ return null;
+ };
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Emby.Server.Implementations/Services/ServiceHandler.cs b/Emby.Server.Implementations/Services/ServiceHandler.cs
new file mode 100644
index 000000000..003776f9c
--- /dev/null
+++ b/Emby.Server.Implementations/Services/ServiceHandler.cs
@@ -0,0 +1,297 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Threading.Tasks;
+using Emby.Server.Implementations.HttpServer;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Services;
+using ServiceStack;
+
+namespace Emby.Server.Implementations.Services
+{
+ public class ServiceHandler
+ {
+ public async Task<object> HandleResponseAsync(object response)
+ {
+ var taskResponse = response as Task;
+
+ if (taskResponse == null)
+ {
+ return response;
+ }
+
+ await taskResponse.ConfigureAwait(false);
+
+ var taskResult = GetTaskResult(taskResponse);
+
+ var subTask = taskResult as Task;
+ if (subTask != null)
+ {
+ taskResult = GetTaskResult(subTask);
+ }
+
+ return taskResult;
+ }
+
+ internal static object GetTaskResult(Task task)
+ {
+ try
+ {
+ var taskObject = task as Task<object>;
+ if (taskObject != null)
+ {
+ return taskObject.Result;
+ }
+
+ task.Wait();
+
+ var type = task.GetType().GetTypeInfo();
+ if (!type.IsGenericType)
+ {
+ return null;
+ }
+
+ return type.GetDeclaredProperty("Result").GetValue(task);
+ }
+ catch (TypeAccessException)
+ {
+ return null; //return null for void Task's
+ }
+ }
+
+ protected static object CreateContentTypeRequest(IRequest httpReq, Type requestType, string contentType)
+ {
+ if (!string.IsNullOrEmpty(contentType) && httpReq.ContentLength > 0)
+ {
+ var deserializer = RequestHelper.GetRequestReader(contentType);
+ if (deserializer != null)
+ {
+ return deserializer(requestType, httpReq.InputStream);
+ }
+ }
+ return ServiceStackHost.Instance.CreateInstance(requestType); //Return an empty DTO, even for empty request bodies
+ }
+
+ public static RestPath FindMatchingRestPath(string httpMethod, string pathInfo, ILogger logger, out string contentType)
+ {
+ pathInfo = GetSanitizedPathInfo(pathInfo, out contentType);
+
+ return ServiceController.Instance.GetRestPathForRequest(httpMethod, pathInfo, logger);
+ }
+
+ public static string GetSanitizedPathInfo(string pathInfo, out string contentType)
+ {
+ contentType = null;
+ var pos = pathInfo.LastIndexOf('.');
+ if (pos >= 0)
+ {
+ var format = pathInfo.Substring(pos + 1);
+ contentType = GetFormatContentType(format);
+ if (contentType != null)
+ {
+ pathInfo = pathInfo.Substring(0, pos);
+ }
+ }
+ return pathInfo;
+ }
+
+ private static string GetFormatContentType(string format)
+ {
+ //built-in formats
+ if (format == "json")
+ return "application/json";
+ if (format == "xml")
+ return "application/xml";
+
+ return null;
+ }
+
+ public RestPath GetRestPath(string httpMethod, string pathInfo)
+ {
+ if (this.RestPath == null)
+ {
+ string contentType;
+ this.RestPath = FindMatchingRestPath(httpMethod, pathInfo, new NullLogger(), out contentType);
+
+ if (contentType != null)
+ ResponseContentType = contentType;
+ }
+ return this.RestPath;
+ }
+
+ public RestPath RestPath { get; set; }
+
+ // Set from SSHHF.GetHandlerForPathInfo()
+ public string ResponseContentType { get; set; }
+
+ public async Task ProcessRequestAsync(HttpListenerHost appHost, IRequest httpReq, IResponse httpRes, ILogger logger, string operationName)
+ {
+ var restPath = GetRestPath(httpReq.Verb, httpReq.PathInfo);
+ if (restPath == null)
+ {
+ throw new NotSupportedException("No RestPath found for: " + httpReq.Verb + " " + httpReq.PathInfo);
+ }
+
+ SetRoute(httpReq, restPath);
+
+ if (ResponseContentType != null)
+ httpReq.ResponseContentType = ResponseContentType;
+
+ var request = httpReq.Dto = CreateRequest(httpReq, restPath, logger);
+
+ appHost.ApplyRequestFilters(httpReq, httpRes, request);
+
+ var rawResponse = await appHost.ServiceController.Execute(appHost, request, httpReq).ConfigureAwait(false);
+
+ var response = await HandleResponseAsync(rawResponse).ConfigureAwait(false);
+
+ // Apply response filters
+ foreach (var responseFilter in appHost.GlobalResponseFilters)
+ {
+ responseFilter(httpReq, httpRes, response);
+ }
+
+ await ResponseHelper.WriteToResponse(httpRes, httpReq, response).ConfigureAwait(false);
+ }
+
+ public static object CreateRequest(IRequest httpReq, RestPath restPath, ILogger logger)
+ {
+ var requestType = restPath.RequestType;
+
+ if (RequireqRequestStream(requestType))
+ {
+ // Used by IRequiresRequestStream
+ return CreateRequiresRequestStreamRequest(httpReq, requestType);
+ }
+
+ var requestParams = GetFlattenedRequestParams(httpReq);
+ return CreateRequest(httpReq, restPath, requestParams);
+ }
+
+ private static bool RequireqRequestStream(Type requestType)
+ {
+ var requiresRequestStreamTypeInfo = typeof(IRequiresRequestStream).GetTypeInfo();
+
+ return requiresRequestStreamTypeInfo.IsAssignableFrom(requestType.GetTypeInfo());
+ }
+
+ private static IRequiresRequestStream CreateRequiresRequestStreamRequest(IRequest req, Type requestType)
+ {
+ var restPath = GetRoute(req);
+ var request = ServiceHandler.CreateRequest(req, restPath, GetRequestParams(req), ServiceStackHost.Instance.CreateInstance(requestType));
+
+ var rawReq = (IRequiresRequestStream)request;
+ rawReq.RequestStream = req.InputStream;
+ return rawReq;
+ }
+
+ public static object CreateRequest(IRequest httpReq, RestPath restPath, Dictionary<string, string> requestParams)
+ {
+ var requestDto = CreateContentTypeRequest(httpReq, restPath.RequestType, httpReq.ContentType);
+
+ return CreateRequest(httpReq, restPath, requestParams, requestDto);
+ }
+
+ public static object CreateRequest(IRequest httpReq, RestPath restPath, Dictionary<string, string> requestParams, object requestDto)
+ {
+ string contentType;
+ var pathInfo = !restPath.IsWildCardPath
+ ? GetSanitizedPathInfo(httpReq.PathInfo, out contentType)
+ : httpReq.PathInfo;
+
+ return restPath.CreateRequest(pathInfo, requestParams, requestDto);
+ }
+
+ /// <summary>
+ /// Duplicate Params are given a unique key by appending a #1 suffix
+ /// </summary>
+ private static Dictionary<string, string> GetRequestParams(IRequest request)
+ {
+ var map = new Dictionary<string, string>();
+
+ foreach (var name in request.QueryString.Keys)
+ {
+ if (name == null) continue; //thank you ASP.NET
+
+ var values = request.QueryString.GetValues(name);
+ if (values.Length == 1)
+ {
+ map[name] = values[0];
+ }
+ else
+ {
+ for (var i = 0; i < values.Length; i++)
+ {
+ map[name + (i == 0 ? "" : "#" + i)] = values[i];
+ }
+ }
+ }
+
+ if ((IsMethod(request.Verb, "POST") || IsMethod(request.Verb, "PUT")) && request.FormData != null)
+ {
+ foreach (var name in request.FormData.Keys)
+ {
+ if (name == null) continue; //thank you ASP.NET
+
+ var values = request.FormData.GetValues(name);
+ if (values.Length == 1)
+ {
+ map[name] = values[0];
+ }
+ else
+ {
+ for (var i = 0; i < values.Length; i++)
+ {
+ map[name + (i == 0 ? "" : "#" + i)] = values[i];
+ }
+ }
+ }
+ }
+
+ return map;
+ }
+
+ private static bool IsMethod(string method, string expected)
+ {
+ return string.Equals(method, expected, StringComparison.OrdinalIgnoreCase);
+ }
+
+ /// <summary>
+ /// Duplicate params have their values joined together in a comma-delimited string
+ /// </summary>
+ private static Dictionary<string, string> GetFlattenedRequestParams(IRequest request)
+ {
+ var map = new Dictionary<string, string>();
+
+ foreach (var name in request.QueryString.Keys)
+ {
+ if (name == null) continue; //thank you ASP.NET
+ map[name] = request.QueryString[name];
+ }
+
+ if ((IsMethod(request.Verb, "POST") || IsMethod(request.Verb, "PUT")) && request.FormData != null)
+ {
+ foreach (var name in request.FormData.Keys)
+ {
+ if (name == null) continue; //thank you ASP.NET
+ map[name] = request.FormData[name];
+ }
+ }
+
+ return map;
+ }
+
+ private static void SetRoute(IRequest req, RestPath route)
+ {
+ req.Items["__route"] = route;
+ }
+
+ private static RestPath GetRoute(IRequest req)
+ {
+ object route;
+ req.Items.TryGetValue("__route", out route);
+ return route as RestPath;
+ }
+ }
+
+}
diff --git a/Emby.Server.Implementations/Services/ServiceMethod.cs b/Emby.Server.Implementations/Services/ServiceMethod.cs
new file mode 100644
index 000000000..bcbc6fb57
--- /dev/null
+++ b/Emby.Server.Implementations/Services/ServiceMethod.cs
@@ -0,0 +1,24 @@
+using System;
+
+namespace Emby.Server.Implementations.Services
+{
+ public class ServiceMethod
+ {
+ public const string AnyAction = "ANY";
+
+ public string Id { get; set; }
+
+ public ActionInvokerFn ServiceAction { get; set; }
+ public MediaBrowser.Model.Services.IHasRequestFilter[] RequestFilters { get; set; }
+
+ public static string Key(Type serviceType, string method, string requestDtoName)
+ {
+ return serviceType.FullName + " " + method.ToUpper() + " " + requestDtoName;
+ }
+
+ public static string AnyKey(Type serviceType, string requestDtoName)
+ {
+ return Key(serviceType, AnyAction, requestDtoName);
+ }
+ }
+} \ No newline at end of file