aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations/Services
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/Services
parent29c24420978ef324d66a381b71e0f3e3b2f294cb (diff)
parent511a8702c29445288251fcf841c394e837db19cc (diff)
Merge pull request #2466 from MediaBrowser/dev
Dev
Diffstat (limited to 'Emby.Server.Implementations/Services')
-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
7 files changed, 964 insertions, 0 deletions
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