Merge branch 'api-migration' into redoc

This commit is contained in:
Cody Robibero 2020-04-29 07:38:18 -06:00 committed by GitHub
commit d491b1b45f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 237 additions and 0 deletions

View File

@ -1,4 +1,5 @@
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using Jellyfin.Server.Middleware;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
namespace Jellyfin.Server.Extensions namespace Jellyfin.Server.Extensions

View File

@ -9,6 +9,7 @@ using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
using Jellyfin.Api.Auth.RequiresElevationPolicy; using Jellyfin.Api.Auth.RequiresElevationPolicy;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Controllers; using Jellyfin.Api.Controllers;
using Jellyfin.Server.Formatters;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -71,6 +72,8 @@ namespace Jellyfin.Server.Extensions
return serviceCollection.AddMvc(opts => return serviceCollection.AddMvc(opts =>
{ {
opts.UseGeneralRoutePrefix(baseUrl); 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 // Clear app parts to avoid other assemblies being picked up

View File

@ -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\""));
}
}
}

View File

@ -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\""));
}
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}

View File

@ -1,4 +1,5 @@
using Jellyfin.Server.Extensions; using Jellyfin.Server.Extensions;
using Jellyfin.Server.Middleware;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
@ -58,6 +59,8 @@ namespace Jellyfin.Server
app.UseDeveloperExceptionPage(); app.UseDeveloperExceptionPage();
} }
app.UseMiddleware<ExceptionMiddleware>();
app.UseWebSockets(); app.UseWebSockets();
app.UseResponseCompression(); app.UseResponseCompression();