mirror of
https://github.com/Readarr/Readarr.git
synced 2026-03-30 18:24:45 -04:00
New: Lidarr to Readarr
This commit is contained in:
12
src/Readarr.Http/Extensions/AccessControlHeaders.cs
Normal file
12
src/Readarr.Http/Extensions/AccessControlHeaders.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Readarr.Http.Extensions
|
||||
{
|
||||
public static class AccessControlHeaders
|
||||
{
|
||||
public const string RequestMethod = "Access-Control-Request-Method";
|
||||
public const string RequestHeaders = "Access-Control-Request-Headers";
|
||||
|
||||
public const string AllowOrigin = "Access-Control-Allow-Origin";
|
||||
public const string AllowMethods = "Access-Control-Allow-Methods";
|
||||
public const string AllowHeaders = "Access-Control-Allow-Headers";
|
||||
}
|
||||
}
|
||||
23
src/Readarr.Http/Extensions/NancyJsonSerializer.cs
Normal file
23
src/Readarr.Http/Extensions/NancyJsonSerializer.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Nancy;
|
||||
using Nancy.Responses.Negotiation;
|
||||
using NzbDrone.Common.Serializer;
|
||||
|
||||
namespace Readarr.Http.Extensions
|
||||
{
|
||||
public class NancyJsonSerializer : ISerializer
|
||||
{
|
||||
public bool CanSerialize(MediaRange contentType)
|
||||
{
|
||||
return contentType == "application/json";
|
||||
}
|
||||
|
||||
public void Serialize<TModel>(MediaRange contentType, TModel model, Stream outputStream)
|
||||
{
|
||||
Json.Serialize(model, outputStream);
|
||||
}
|
||||
|
||||
public IEnumerable<string> Extensions { get; private set; }
|
||||
}
|
||||
}
|
||||
41
src/Readarr.Http/Extensions/Pipelines/CacheHeaderPipeline.cs
Normal file
41
src/Readarr.Http/Extensions/Pipelines/CacheHeaderPipeline.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using Nancy;
|
||||
using Nancy.Bootstrapper;
|
||||
using Readarr.Http.Frontend;
|
||||
|
||||
namespace Readarr.Http.Extensions.Pipelines
|
||||
{
|
||||
public class CacheHeaderPipeline : IRegisterNancyPipeline
|
||||
{
|
||||
private readonly ICacheableSpecification _cacheableSpecification;
|
||||
|
||||
public CacheHeaderPipeline(ICacheableSpecification cacheableSpecification)
|
||||
{
|
||||
_cacheableSpecification = cacheableSpecification;
|
||||
}
|
||||
|
||||
public int Order => 0;
|
||||
|
||||
public void Register(IPipelines pipelines)
|
||||
{
|
||||
pipelines.AfterRequest.AddItemToStartOfPipeline(Handle);
|
||||
}
|
||||
|
||||
private void Handle(NancyContext context)
|
||||
{
|
||||
if (context.Request.Method == "OPTIONS")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_cacheableSpecification.IsCacheable(context))
|
||||
{
|
||||
context.Response.Headers.EnableCache();
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Response.Headers.DisableCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
80
src/Readarr.Http/Extensions/Pipelines/CorsPipeline.cs
Normal file
80
src/Readarr.Http/Extensions/Pipelines/CorsPipeline.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using System.Linq;
|
||||
using Nancy;
|
||||
using Nancy.Bootstrapper;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
||||
namespace Readarr.Http.Extensions.Pipelines
|
||||
{
|
||||
public class CorsPipeline : IRegisterNancyPipeline
|
||||
{
|
||||
public int Order => 0;
|
||||
|
||||
public void Register(IPipelines pipelines)
|
||||
{
|
||||
pipelines.BeforeRequest.AddItemToEndOfPipeline(HandleRequest);
|
||||
pipelines.AfterRequest.AddItemToEndOfPipeline(HandleResponse);
|
||||
}
|
||||
|
||||
private Response HandleRequest(NancyContext context)
|
||||
{
|
||||
if (context == null || context.Request.Method != "OPTIONS")
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var response = new Response()
|
||||
.WithStatusCode(HttpStatusCode.OK)
|
||||
.WithContentType("");
|
||||
ApplyResponseHeaders(response, context.Request);
|
||||
return response;
|
||||
}
|
||||
|
||||
private void HandleResponse(NancyContext context)
|
||||
{
|
||||
if (context == null || context.Response.Headers.ContainsKey(AccessControlHeaders.AllowOrigin))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyResponseHeaders(context.Response, context.Request);
|
||||
}
|
||||
|
||||
private static void ApplyResponseHeaders(Response response, Request request)
|
||||
{
|
||||
if (request.IsApiRequest())
|
||||
{
|
||||
// Allow Cross-Origin access to the API since it's protected with the apikey, and nothing else.
|
||||
ApplyCorsResponseHeaders(response, request, "*", "GET, OPTIONS, PATCH, POST, PUT, DELETE");
|
||||
}
|
||||
else if (request.IsSharedContentRequest())
|
||||
{
|
||||
// Allow Cross-Origin access to specific shared content such as mediacovers and images.
|
||||
ApplyCorsResponseHeaders(response, request, "*", "GET, OPTIONS");
|
||||
}
|
||||
|
||||
// Disallow Cross-Origin access for any other route.
|
||||
}
|
||||
|
||||
private static void ApplyCorsResponseHeaders(Response response, Request request, string allowOrigin, string allowedMethods)
|
||||
{
|
||||
response.Headers.Add(AccessControlHeaders.AllowOrigin, allowOrigin);
|
||||
|
||||
if (request.Method == "OPTIONS")
|
||||
{
|
||||
if (response.Headers.ContainsKey("Allow"))
|
||||
{
|
||||
allowedMethods = response.Headers["Allow"];
|
||||
}
|
||||
|
||||
response.Headers.Add(AccessControlHeaders.AllowMethods, allowedMethods);
|
||||
|
||||
if (request.Headers[AccessControlHeaders.RequestHeaders].Any())
|
||||
{
|
||||
var requestedHeaders = request.Headers[AccessControlHeaders.RequestHeaders].Join(", ");
|
||||
|
||||
response.Headers.Add(AccessControlHeaders.AllowHeaders, requestedHeaders);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
104
src/Readarr.Http/Extensions/Pipelines/GZipPipeline.cs
Normal file
104
src/Readarr.Http/Extensions/Pipelines/GZipPipeline.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using Nancy;
|
||||
using Nancy.Bootstrapper;
|
||||
using NLog;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
|
||||
namespace Readarr.Http.Extensions.Pipelines
|
||||
{
|
||||
public class GzipCompressionPipeline : IRegisterNancyPipeline
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
|
||||
public int Order => 0;
|
||||
|
||||
private readonly Action<Action<Stream>, Stream> _writeGZipStream;
|
||||
|
||||
public GzipCompressionPipeline(Logger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
|
||||
// On Mono GZipStream/DeflateStream leaks memory if an exception is thrown, use an intermediate buffer in that case.
|
||||
_writeGZipStream = PlatformInfo.IsMono ? WriteGZipStreamMono : (Action<Action<Stream>, Stream>)WriteGZipStream;
|
||||
}
|
||||
|
||||
public void Register(IPipelines pipelines)
|
||||
{
|
||||
pipelines.AfterRequest.AddItemToEndOfPipeline(CompressResponse);
|
||||
}
|
||||
|
||||
private void CompressResponse(NancyContext context)
|
||||
{
|
||||
var request = context.Request;
|
||||
var response = context.Response;
|
||||
|
||||
try
|
||||
{
|
||||
if (
|
||||
response.Contents != Response.NoBody
|
||||
&& !response.ContentType.Contains("image")
|
||||
&& !response.ContentType.Contains("font")
|
||||
&& request.Headers.AcceptEncoding.Any(x => x.Contains("gzip"))
|
||||
&& !AlreadyGzipEncoded(response)
|
||||
&& !ContentLengthIsTooSmall(response))
|
||||
{
|
||||
var contents = response.Contents;
|
||||
|
||||
response.Headers["Content-Encoding"] = "gzip";
|
||||
response.Contents = responseStream => _writeGZipStream(contents, responseStream);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Unable to gzip response");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteGZipStreamMono(Action<Stream> innerContent, Stream targetStream)
|
||||
{
|
||||
using (var membuffer = new MemoryStream())
|
||||
{
|
||||
WriteGZipStream(innerContent, membuffer);
|
||||
membuffer.Position = 0;
|
||||
membuffer.CopyTo(targetStream);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteGZipStream(Action<Stream> innerContent, Stream targetStream)
|
||||
{
|
||||
using (var gzip = new GZipStream(targetStream, CompressionMode.Compress, true))
|
||||
using (var buffered = new BufferedStream(gzip, 8192))
|
||||
{
|
||||
innerContent.Invoke(buffered);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ContentLengthIsTooSmall(Response response)
|
||||
{
|
||||
var contentLength = response.Headers.TryGetValue("Content-Length", out var value) ? value : null;
|
||||
|
||||
if (contentLength != null && long.Parse(contentLength) < 1024)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool AlreadyGzipEncoded(Response response)
|
||||
{
|
||||
var contentEncoding = response.Headers.TryGetValue("Content-Encoding", out var value) ? value : null;
|
||||
|
||||
if (contentEncoding == "gzip")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using Nancy.Bootstrapper;
|
||||
|
||||
namespace Readarr.Http.Extensions.Pipelines
|
||||
{
|
||||
public interface IRegisterNancyPipeline
|
||||
{
|
||||
int Order { get; }
|
||||
|
||||
void Register(IPipelines pipelines);
|
||||
}
|
||||
}
|
||||
36
src/Readarr.Http/Extensions/Pipelines/IfModifiedPipeline.cs
Normal file
36
src/Readarr.Http/Extensions/Pipelines/IfModifiedPipeline.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using Nancy;
|
||||
using Nancy.Bootstrapper;
|
||||
using Readarr.Http.Frontend;
|
||||
|
||||
namespace Readarr.Http.Extensions.Pipelines
|
||||
{
|
||||
public class IfModifiedPipeline : IRegisterNancyPipeline
|
||||
{
|
||||
private readonly ICacheableSpecification _cacheableSpecification;
|
||||
|
||||
public IfModifiedPipeline(ICacheableSpecification cacheableSpecification)
|
||||
{
|
||||
_cacheableSpecification = cacheableSpecification;
|
||||
}
|
||||
|
||||
public int Order => 0;
|
||||
|
||||
public void Register(IPipelines pipelines)
|
||||
{
|
||||
pipelines.BeforeRequest.AddItemToStartOfPipeline(Handle);
|
||||
}
|
||||
|
||||
private Response Handle(NancyContext context)
|
||||
{
|
||||
if (_cacheableSpecification.IsCacheable(context) && context.Request.Headers.IfModifiedSince.HasValue)
|
||||
{
|
||||
var response = new Response { ContentType = MimeTypes.GetMimeType(context.Request.Path), StatusCode = HttpStatusCode.NotModified };
|
||||
response.Headers.EnableCache();
|
||||
return response;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using Nancy;
|
||||
using Nancy.Bootstrapper;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
|
||||
namespace Readarr.Http.Extensions.Pipelines
|
||||
{
|
||||
public class ReadarrVersionPipeline : IRegisterNancyPipeline
|
||||
{
|
||||
public int Order => 0;
|
||||
|
||||
public void Register(IPipelines pipelines)
|
||||
{
|
||||
pipelines.AfterRequest.AddItemToStartOfPipeline(Handle);
|
||||
}
|
||||
|
||||
private void Handle(NancyContext context)
|
||||
{
|
||||
if (!context.Response.Headers.ContainsKey("X-ApplicationVersion"))
|
||||
{
|
||||
context.Response.Headers.Add("X-ApplicationVersion", BuildInfo.Version.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Nancy;
|
||||
using Nancy.Bootstrapper;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using Readarr.Http.ErrorManagement;
|
||||
using Readarr.Http.Extensions;
|
||||
using Readarr.Http.Extensions.Pipelines;
|
||||
|
||||
namespace NzbDrone.Api.Extensions.Pipelines
|
||||
{
|
||||
public class RequestLoggingPipeline : IRegisterNancyPipeline
|
||||
{
|
||||
private static readonly Logger _loggerHttp = LogManager.GetLogger("Http");
|
||||
private static readonly Logger _loggerApi = LogManager.GetLogger("Api");
|
||||
|
||||
private static int _requestSequenceID;
|
||||
|
||||
private readonly ReadarrErrorPipeline _errorPipeline;
|
||||
|
||||
public RequestLoggingPipeline(ReadarrErrorPipeline errorPipeline)
|
||||
{
|
||||
_errorPipeline = errorPipeline;
|
||||
}
|
||||
|
||||
public int Order => 100;
|
||||
|
||||
public void Register(IPipelines pipelines)
|
||||
{
|
||||
pipelines.BeforeRequest.AddItemToStartOfPipeline(LogStart);
|
||||
pipelines.AfterRequest.AddItemToEndOfPipeline(LogEnd);
|
||||
pipelines.OnError.AddItemToEndOfPipeline(LogError);
|
||||
}
|
||||
|
||||
private Response LogStart(NancyContext context)
|
||||
{
|
||||
var id = Interlocked.Increment(ref _requestSequenceID);
|
||||
|
||||
context.Items["ApiRequestSequenceID"] = id;
|
||||
context.Items["ApiRequestStartTime"] = DateTime.UtcNow;
|
||||
|
||||
var reqPath = GetRequestPathAndQuery(context.Request);
|
||||
|
||||
_loggerHttp.Trace("Req: {0} [{1}] {2}", id, context.Request.Method, reqPath);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void LogEnd(NancyContext context)
|
||||
{
|
||||
var id = (int)context.Items["ApiRequestSequenceID"];
|
||||
var startTime = (DateTime)context.Items["ApiRequestStartTime"];
|
||||
|
||||
var endTime = DateTime.UtcNow;
|
||||
var duration = endTime - startTime;
|
||||
|
||||
var reqPath = GetRequestPathAndQuery(context.Request);
|
||||
|
||||
_loggerHttp.Trace("Res: {0} [{1}] {2}: {3}.{4} ({5} ms)", id, context.Request.Method, reqPath, (int)context.Response.StatusCode, context.Response.StatusCode, (int)duration.TotalMilliseconds);
|
||||
|
||||
if (context.Request.IsApiRequest())
|
||||
{
|
||||
_loggerApi.Debug("[{0}] {1}: {2}.{3} ({4} ms)", context.Request.Method, reqPath, (int)context.Response.StatusCode, context.Response.StatusCode, (int)duration.TotalMilliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
private Response LogError(NancyContext context, Exception exception)
|
||||
{
|
||||
var response = _errorPipeline.HandleException(context, exception);
|
||||
|
||||
context.Response = response;
|
||||
|
||||
LogEnd(context);
|
||||
|
||||
context.Response = null;
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private static string GetRequestPathAndQuery(Request request)
|
||||
{
|
||||
if (request.Url.Query.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
return string.Concat(request.Url.Path, request.Url.Query);
|
||||
}
|
||||
else
|
||||
{
|
||||
return request.Url.Path;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
46
src/Readarr.Http/Extensions/Pipelines/UrlBasePipeline.cs
Normal file
46
src/Readarr.Http/Extensions/Pipelines/UrlBasePipeline.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using Nancy;
|
||||
using Nancy.Bootstrapper;
|
||||
using Nancy.Responses;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Configuration;
|
||||
|
||||
namespace Readarr.Http.Extensions.Pipelines
|
||||
{
|
||||
public class UrlBasePipeline : IRegisterNancyPipeline
|
||||
{
|
||||
private readonly string _urlBase;
|
||||
|
||||
public UrlBasePipeline(IConfigFileProvider configFileProvider)
|
||||
{
|
||||
_urlBase = configFileProvider.UrlBase;
|
||||
}
|
||||
|
||||
public int Order => 99;
|
||||
|
||||
public void Register(IPipelines pipelines)
|
||||
{
|
||||
if (_urlBase.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
pipelines.BeforeRequest.AddItemToStartOfPipeline(Handle);
|
||||
}
|
||||
}
|
||||
|
||||
private Response Handle(NancyContext context)
|
||||
{
|
||||
var basePath = context.Request.Url.BasePath;
|
||||
|
||||
if (basePath.IsNullOrWhiteSpace())
|
||||
{
|
||||
return new RedirectResponse($"{_urlBase}{context.Request.Path}{context.Request.Url.Query}");
|
||||
}
|
||||
|
||||
if (_urlBase != basePath)
|
||||
{
|
||||
return new NotFoundResponse();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
63
src/Readarr.Http/Extensions/ReqResExtensions.cs
Normal file
63
src/Readarr.Http/Extensions/ReqResExtensions.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Nancy;
|
||||
using Nancy.Responses;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Serializer;
|
||||
|
||||
namespace Readarr.Http.Extensions
|
||||
{
|
||||
public static class ReqResExtensions
|
||||
{
|
||||
private static readonly NancyJsonSerializer NancySerializer = new NancyJsonSerializer();
|
||||
|
||||
public static readonly string LastModified = BuildInfo.BuildDateTime.ToString("r");
|
||||
|
||||
public static T FromJson<T>(this Stream body)
|
||||
where T : class, new()
|
||||
{
|
||||
return FromJson<T>(body, typeof(T));
|
||||
}
|
||||
|
||||
public static T FromJson<T>(this Stream body, Type type)
|
||||
{
|
||||
return (T)FromJson(body, type);
|
||||
}
|
||||
|
||||
public static object FromJson(this Stream body, Type type)
|
||||
{
|
||||
var reader = new StreamReader(body, true);
|
||||
body.Position = 0;
|
||||
var value = reader.ReadToEnd();
|
||||
return Json.Deserialize(value, type);
|
||||
}
|
||||
|
||||
public static JsonResponse<TModel> AsResponse<TModel>(this TModel model, NancyContext context, HttpStatusCode statusCode = HttpStatusCode.OK)
|
||||
{
|
||||
var response = new JsonResponse<TModel>(model, NancySerializer, context.Environment) { StatusCode = statusCode };
|
||||
response.Headers.DisableCache();
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public static IDictionary<string, string> DisableCache(this IDictionary<string, string> headers)
|
||||
{
|
||||
headers["Cache-Control"] = "no-cache, no-store, must-revalidate, max-age=0";
|
||||
headers["Pragma"] = "no-cache";
|
||||
headers["Expires"] = "0";
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
public static IDictionary<string, string> EnableCache(this IDictionary<string, string> headers)
|
||||
{
|
||||
headers["Cache-Control"] = "max-age=31536000 , public";
|
||||
headers["Expires"] = "Sat, 29 Jun 2020 00:00:00 GMT";
|
||||
headers["Last-Modified"] = LastModified;
|
||||
headers["Age"] = "193266";
|
||||
|
||||
return headers;
|
||||
}
|
||||
}
|
||||
}
|
||||
58
src/Readarr.Http/Extensions/RequestExtensions.cs
Normal file
58
src/Readarr.Http/Extensions/RequestExtensions.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using Nancy;
|
||||
|
||||
namespace Readarr.Http.Extensions
|
||||
{
|
||||
public static class RequestExtensions
|
||||
{
|
||||
public static bool IsApiRequest(this Request request)
|
||||
{
|
||||
return request.Path.StartsWith("/api/", StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
public static bool IsFeedRequest(this Request request)
|
||||
{
|
||||
return request.Path.StartsWith("/feed/", StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
public static bool IsSignalRRequest(this Request request)
|
||||
{
|
||||
return request.Path.StartsWith("/signalr/", StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
public static bool IsLocalRequest(this Request request)
|
||||
{
|
||||
return request.UserHostAddress.Equals("localhost") ||
|
||||
request.UserHostAddress.Equals("127.0.0.1") ||
|
||||
request.UserHostAddress.Equals("::1");
|
||||
}
|
||||
|
||||
public static bool IsLoginRequest(this Request request)
|
||||
{
|
||||
return request.Path.Equals("/login", StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
public static bool IsContentRequest(this Request request)
|
||||
{
|
||||
return request.Path.StartsWith("/Content/", StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
public static bool GetBooleanQueryParameter(this Request request, string parameter, bool defaultValue = false)
|
||||
{
|
||||
var parameterValue = request.Query[parameter];
|
||||
|
||||
if (parameterValue.HasValue)
|
||||
{
|
||||
return bool.Parse(parameterValue.Value);
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public static bool IsSharedContentRequest(this Request request)
|
||||
{
|
||||
return request.Path.StartsWith("/MediaCover/", StringComparison.InvariantCultureIgnoreCase) ||
|
||||
request.Path.StartsWith("/Content/Images/", StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user