diff options
| author | Luke <luke.pulverenti@gmail.com> | 2017-02-12 20:17:43 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2017-02-12 20:17:43 -0500 |
| commit | 273aa822cfc37558883dbdd17647829abcf34758 (patch) | |
| tree | db858e3dfacfe4bd3aac6c701806a15203b8d63e /Emby.Server.Implementations/Services | |
| parent | 29c24420978ef324d66a381b71e0f3e3b2f294cb (diff) | |
| parent | 511a8702c29445288251fcf841c394e837db19cc (diff) | |
Merge pull request #2466 from MediaBrowser/dev
Dev
Diffstat (limited to 'Emby.Server.Implementations/Services')
| -rw-r--r-- | Emby.Server.Implementations/Services/HttpResult.cs | 59 | ||||
| -rw-r--r-- | Emby.Server.Implementations/Services/RequestHelper.cs | 51 | ||||
| -rw-r--r-- | Emby.Server.Implementations/Services/ResponseHelper.cs | 178 | ||||
| -rw-r--r-- | Emby.Server.Implementations/Services/ServiceController.cs | 189 | ||||
| -rw-r--r-- | Emby.Server.Implementations/Services/ServiceExec.cs | 166 | ||||
| -rw-r--r-- | Emby.Server.Implementations/Services/ServiceHandler.cs | 297 | ||||
| -rw-r--r-- | Emby.Server.Implementations/Services/ServiceMethod.cs | 24 |
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 |
