diff options
| author | Cody Robibero <cody@robibe.ro> | 2020-04-29 07:38:18 -0600 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-04-29 07:38:18 -0600 |
| commit | d491b1b45fc813cb3bd1fe87918f693897621555 (patch) | |
| tree | 4bb52a35e9cd7cd0326c6c89d80b5349b3dfee0a | |
| parent | 068368df6352cfad4e69df599c364b3f05b367ba (diff) | |
| parent | 2cb0f6f1263cabee288ab30109b55c57e24c7ed0 (diff) | |
Merge branch 'api-migration' into redoc
| -rw-r--r-- | Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs | 1 | ||||
| -rw-r--r-- | Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs | 3 | ||||
| -rw-r--r-- | Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs | 21 | ||||
| -rw-r--r-- | Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs | 23 | ||||
| -rw-r--r-- | Jellyfin.Server/Middleware/ExceptionMiddleware.cs | 145 | ||||
| -rw-r--r-- | Jellyfin.Server/Models/JsonOptions.cs | 41 | ||||
| -rw-r--r-- | Jellyfin.Server/Startup.cs | 3 |
7 files changed, 237 insertions, 0 deletions
diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs index d09424225..33fd77d9c 100644 --- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs @@ -1,4 +1,5 @@ using MediaBrowser.Controller.Configuration; +using Jellyfin.Server.Middleware; using Microsoft.AspNetCore.Builder; namespace Jellyfin.Server.Extensions diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 00a73ade6..a24785d57 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -9,6 +9,7 @@ using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy; using Jellyfin.Api.Auth.RequiresElevationPolicy; using Jellyfin.Api.Constants; using Jellyfin.Api.Controllers; +using Jellyfin.Server.Formatters; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.DependencyInjection; @@ -71,6 +72,8 @@ namespace Jellyfin.Server.Extensions return serviceCollection.AddMvc(opts => { opts.UseGeneralRoutePrefix(baseUrl); + opts.OutputFormatters.Insert(0, new CamelCaseJsonProfileFormatter()); + opts.OutputFormatters.Insert(0, new PascalCaseJsonProfileFormatter()); }) // Clear app parts to avoid other assemblies being picked up diff --git a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs new file mode 100644 index 000000000..e6ad6dfb1 --- /dev/null +++ b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs @@ -0,0 +1,21 @@ +using Jellyfin.Server.Models; +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.Net.Http.Headers; + +namespace Jellyfin.Server.Formatters +{ + /// <summary> + /// Camel Case Json Profile Formatter. + /// </summary> + public class CamelCaseJsonProfileFormatter : SystemTextJsonOutputFormatter + { + /// <summary> + /// Initializes a new instance of the <see cref="CamelCaseJsonProfileFormatter"/> class. + /// </summary> + public CamelCaseJsonProfileFormatter() : base(JsonOptions.CamelCase) + { + SupportedMediaTypes.Clear(); + SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json;profile=\"CamelCase\"")); + } + } +} diff --git a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs new file mode 100644 index 000000000..675f4c79e --- /dev/null +++ b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs @@ -0,0 +1,23 @@ +using Jellyfin.Server.Models; +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.Net.Http.Headers; + +namespace Jellyfin.Server.Formatters +{ + /// <summary> + /// Pascal Case Json Profile Formatter. + /// </summary> + public class PascalCaseJsonProfileFormatter : SystemTextJsonOutputFormatter + { + /// <summary> + /// Initializes a new instance of the <see cref="PascalCaseJsonProfileFormatter"/> class. + /// </summary> + public PascalCaseJsonProfileFormatter() : base(JsonOptions.PascalCase) + { + SupportedMediaTypes.Clear(); + // Add application/json for default formatter + SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json")); + SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json;profile=\"PascalCase\"")); + } + } +} diff --git a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs new file mode 100644 index 000000000..0d79bbfaf --- /dev/null +++ b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs @@ -0,0 +1,145 @@ +using System; +using System.IO; +using System.Net.Mime; +using System.Net.Sockets; +using System.Threading.Tasks; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Net; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Middleware +{ + /// <summary> + /// Exception Middleware. + /// </summary> + public class ExceptionMiddleware + { + private readonly RequestDelegate _next; + private readonly ILogger<ExceptionMiddleware> _logger; + private readonly IServerConfigurationManager _configuration; + + /// <summary> + /// Initializes a new instance of the <see cref="ExceptionMiddleware"/> class. + /// </summary> + /// <param name="next">Next request delegate.</param> + /// <param name="logger">Instance of the <see cref="ILogger{ExceptionMiddleware}"/> interface.</param> + /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> + public ExceptionMiddleware( + RequestDelegate next, + ILogger<ExceptionMiddleware> logger, + IServerConfigurationManager serverConfigurationManager) + { + _next = next; + _logger = logger; + _configuration = serverConfigurationManager; + } + + /// <summary> + /// Invoke request. + /// </summary> + /// <param name="context">Request context.</param> + /// <returns>Task.</returns> + public async Task Invoke(HttpContext context) + { + try + { + await _next(context).ConfigureAwait(false); + } + catch (Exception ex) + { + if (context.Response.HasStarted) + { + _logger.LogWarning("The response has already started, the exception middleware will not be executed."); + throw; + } + + ex = GetActualException(ex); + + bool ignoreStackTrace = + ex is SocketException + || ex is IOException + || ex is OperationCanceledException + || ex is SecurityException + || ex is AuthenticationException + || ex is FileNotFoundException; + + if (ignoreStackTrace) + { + _logger.LogError( + "Error processing request: {ExceptionMessage}. URL {Method} {Url}.", + ex.Message.TrimEnd('.'), + context.Request.Method, + context.Request.Path); + } + else + { + _logger.LogError( + ex, + "Error processing request. URL {Method} {Url}.", + context.Request.Method, + context.Request.Path); + } + + context.Response.StatusCode = GetStatusCode(ex); + context.Response.ContentType = MediaTypeNames.Text.Plain; + var errorContent = NormalizeExceptionMessage(ex.Message); + await context.Response.WriteAsync(errorContent).ConfigureAwait(false); + } + } + + private static Exception GetActualException(Exception ex) + { + if (ex is AggregateException agg) + { + var inner = agg.InnerException; + if (inner != null) + { + return GetActualException(inner); + } + + var inners = agg.InnerExceptions; + if (inners.Count > 0) + { + return GetActualException(inners[0]); + } + } + + return ex; + } + + private static int GetStatusCode(Exception ex) + { + switch (ex) + { + case ArgumentException _: return StatusCodes.Status400BadRequest; + case SecurityException _: return StatusCodes.Status401Unauthorized; + case DirectoryNotFoundException _: + case FileNotFoundException _: + case ResourceNotFoundException _: return StatusCodes.Status404NotFound; + case MethodNotAllowedException _: return StatusCodes.Status405MethodNotAllowed; + default: return StatusCodes.Status500InternalServerError; + } + } + + private string NormalizeExceptionMessage(string msg) + { + if (msg == null) + { + return string.Empty; + } + + // Strip any information we don't want to reveal + return msg.Replace( + _configuration.ApplicationPaths.ProgramSystemPath, + string.Empty, + StringComparison.OrdinalIgnoreCase) + .Replace( + _configuration.ApplicationPaths.ProgramDataPath, + string.Empty, + StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/Jellyfin.Server/Models/JsonOptions.cs b/Jellyfin.Server/Models/JsonOptions.cs new file mode 100644 index 000000000..2f0df3d2c --- /dev/null +++ b/Jellyfin.Server/Models/JsonOptions.cs @@ -0,0 +1,41 @@ +using System.Text.Json; + +namespace Jellyfin.Server.Models +{ + /// <summary> + /// Json Options. + /// </summary> + public static class JsonOptions + { + /// <summary> + /// Gets CamelCase json options. + /// </summary> + public static JsonSerializerOptions CamelCase + { + get + { + var options = DefaultJsonOptions; + options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + return options; + } + } + + /// <summary> + /// Gets PascalCase json options. + /// </summary> + public static JsonSerializerOptions PascalCase + { + get + { + var options = DefaultJsonOptions; + options.PropertyNamingPolicy = null; + return options; + } + } + + /// <summary> + /// Gets base Json Serializer Options. + /// </summary> + private static JsonSerializerOptions DefaultJsonOptions => new JsonSerializerOptions(); + } +} diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index ee08d2580..7c49afbfc 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -1,4 +1,5 @@ using Jellyfin.Server.Extensions; +using Jellyfin.Server.Middleware; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Builder; @@ -58,6 +59,8 @@ namespace Jellyfin.Server app.UseDeveloperExceptionPage(); } + app.UseMiddleware<ExceptionMiddleware>(); + app.UseWebSockets(); app.UseResponseCompression(); |
