Files
Umbraco-CMS/src/Umbraco.Web/RoutableDocumentFilter.cs
Bjarke Berg 85c01d9905 Merge remote-tracking branch 'origin/netcore/dev' into netcore/netcore
# Conflicts:
#	src/Umbraco.Configuration/Legacy/GlobalSettings.cs
#	src/Umbraco.Core/Configuration/IGlobalSettings.cs
#	src/Umbraco.Core/Models/ContentBaseExtensions.cs
#	src/Umbraco.Core/Routing/ContentFinderByRedirectUrl.cs
#	src/Umbraco.Core/Routing/DefaultUrlProvider.cs
#	src/Umbraco.Core/Runtime/MainDom.cs
#	src/Umbraco.Core/Services/IRuntimeState.cs
#	src/Umbraco.Infrastructure/Compose/NotificationsComponent.cs
#	src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs
#	src/Umbraco.Infrastructure/RuntimeState.cs
#	src/Umbraco.Tests/Routing/UrlsWithNestedDomains.cs
#	src/Umbraco.Tests/Runtimes/StandaloneTests.cs
#	src/Umbraco.Tests/TestHelpers/TestObjects.cs
#	src/Umbraco.Web.BackOffice/Controllers/LogViewerController.cs
#	src/Umbraco.Web.BackOffice/Controllers/UsersController.cs
#	src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs
#	src/Umbraco.Web.BackOffice/PropertyEditors/RteEmbedController.cs
#	src/Umbraco.Web.BackOffice/Trees/DictionaryTreeController.cs
#	src/Umbraco.Web.UI.NetCore/umbraco/UmbracoBackOffice/Default.cshtml
#	src/Umbraco.Web.UI.NetCore/umbraco/config/lang/da.xml
#	src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en.xml
#	src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en_us.xml
#	src/Umbraco.Web.UI/Umbraco/config/lang/cs.xml
#	src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Rte.cshtml
#	src/Umbraco.Web/Controllers/UmbLoginController.cs
#	src/Umbraco.Web/Install/Controllers/InstallController.cs
#	src/Umbraco.Web/PublishedElementExtensions.cs
#	src/Umbraco.Web/Runtime/WebInitialComposer.cs
#	src/Umbraco.Web/UmbracoHelper.cs
#	src/Umbraco.Web/UmbracoInjectedModule.cs
#	src/Umbraco.Web/UrlHelperExtensions.cs
#	src/Umbraco.Web/UrlHelperRenderExtensions.cs
#	src/Umbraco.Web/WebApi/UmbracoApiControllerBase.cs
2020-12-01 11:34:13 +01:00

211 lines
9.2 KiB
C#

using System;
using System.IO;
using System.Web;
using System.Web.Routing;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using System.Threading;
using System.Collections.Generic;
using System.Linq;
using System.Collections.Concurrent;
using Umbraco.Core.Collections;
using Umbraco.Core.Configuration.Models;
using Umbraco.Core.IO;
using Umbraco.Web.Composing;
namespace Umbraco.Web
{
/// <summary>
/// Utility class used to check if the current request is for a front-end request
/// </summary>
/// <remarks>
/// There are various checks to determine if this is a front-end request such as checking if the request is part of any reserved paths or existing MVC routes.
/// </remarks>
public sealed class RoutableDocumentFilter
{
public RoutableDocumentFilter(GlobalSettings globalSettings, IIOHelper ioHelper)
{
_globalSettings = globalSettings;
_ioHelper = ioHelper;
}
private static readonly ConcurrentDictionary<string, bool> RouteChecks = new ConcurrentDictionary<string, bool>();
private readonly GlobalSettings _globalSettings;
private readonly IIOHelper _ioHelper;
private object _locker = new object();
private bool _isInit = false;
private int? _routeCount;
private HashSet<string> _reservedList;
/// <summary>
/// Checks if the request is a document request (i.e. one that the module should handle)
/// </summary>
/// <param name="httpContext"></param>
/// <param name="uri"></param>
/// <returns></returns>
public bool IsDocumentRequest(HttpContextBase httpContext, Uri uri)
{
var maybeDoc = true;
var lpath = uri.AbsolutePath.ToLowerInvariant();
// handle directory-URLs used for asmx
// TODO: legacy - what's the point really?
var asmxPos = lpath.IndexOf(".asmx/", StringComparison.OrdinalIgnoreCase);
if (asmxPos >= 0)
{
// use uri.AbsolutePath, not path, 'cos path has been lowercased
httpContext.RewritePath(uri.AbsolutePath.Substring(0, asmxPos + 5), // filePath
uri.AbsolutePath.Substring(asmxPos + 5), // pathInfo
uri.Query.TrimStart('?'));
maybeDoc = false;
}
// a document request should be
// /foo/bar/nil
// /foo/bar/nil/
// /foo/bar/nil.aspx
// where /foo is not a reserved path
// if the path contains an extension that is not .aspx
// then it cannot be a document request
var extension = Path.GetExtension(lpath);
if (maybeDoc && extension.IsNullOrWhiteSpace() == false && extension != ".aspx")
maybeDoc = false;
// at that point, either we have no extension, or it is .aspx
// if the path is reserved then it cannot be a document request
if (maybeDoc && IsReservedPathOrUrl(lpath, httpContext, RouteTable.Routes))
maybeDoc = false;
//NOTE: No need to warn, plus if we do we should log the document, as this message doesn't really tell us anything :)
//if (!maybeDoc)
//{
// Logger.LogWarning<UmbracoModule>("Not a document");
//}
return maybeDoc;
}
/// <summary>
/// Determines whether the specified URL is reserved or is inside a reserved path.
/// </summary>
/// <param name="url">The URL to check.</param>
/// <returns>
/// <c>true</c> if the specified URL is reserved; otherwise, <c>false</c>.
/// </returns>
internal bool IsReservedPathOrUrl(string url)
{
LazyInitializer.EnsureInitialized(ref _reservedList, ref _isInit, ref _locker, () =>
{
// store references to strings to determine changes
var reservedPathsCache = _globalSettings.ReservedPaths;
var reservedUrlsCache = _globalSettings.ReservedUrls;
// add URLs and paths to a new list
var newReservedList = new HashSet<string>();
foreach (var reservedUrlTrimmed in reservedUrlsCache
.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.Trim().ToLowerInvariant())
.Where(x => x.IsNullOrWhiteSpace() == false)
.Select(reservedUrl => _ioHelper.ResolveUrl(reservedUrl).Trim().EnsureStartsWith("/"))
.Where(reservedUrlTrimmed => reservedUrlTrimmed.IsNullOrWhiteSpace() == false))
{
newReservedList.Add(reservedUrlTrimmed);
}
foreach (var reservedPathTrimmed in NormalizePaths(reservedPathsCache.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries)))
{
newReservedList.Add(reservedPathTrimmed);
}
foreach (var reservedPathTrimmed in NormalizePaths(ReservedPaths))
{
newReservedList.Add(reservedPathTrimmed);
}
// use the new list from now on
return newReservedList;
});
//The URL should be cleaned up before checking:
// * If it doesn't contain an '.' in the path then we assume it is a path based URL, if that is the case we should add an trailing '/' because all of our reservedPaths use a trailing '/'
// * We shouldn't be comparing the query at all
var pathPart = url.Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries)[0].ToLowerInvariant();
if (pathPart.Contains(".") == false)
{
pathPart = pathPart.EnsureEndsWith('/');
}
// return true if URL starts with an element of the reserved list
return _reservedList.Any(x => pathPart.InvariantStartsWith(x));
}
private IEnumerable<string> NormalizePaths(IEnumerable<string> paths)
{
return paths
.Select(x => x.Trim().ToLowerInvariant())
.Where(x => x.IsNullOrWhiteSpace() == false)
.Select(reservedPath => _ioHelper.ResolveUrl(reservedPath).Trim().EnsureStartsWith("/").EnsureEndsWith("/"))
.Where(reservedPathTrimmed => reservedPathTrimmed.IsNullOrWhiteSpace() == false);
}
/// <summary>
/// Determines whether the current request is reserved based on the route table and
/// whether the specified URL is reserved or is inside a reserved path.
/// </summary>
/// <param name="url"></param>
/// <param name="httpContext"></param>
/// <param name="routes">The route collection to lookup the request in</param>
/// <returns></returns>
internal bool IsReservedPathOrUrl(string url, HttpContextBase httpContext, RouteCollection routes)
{
if (httpContext == null) throw new ArgumentNullException(nameof(httpContext));
if (routes == null) throw new ArgumentNullException(nameof(routes));
//This is some rudimentary code to check if the route table has changed at runtime, we're basically just keeping a count
//of the routes. This isn't fail safe but there's no way to monitor changes to the route table. Else we need to create a hash
//of all routes and then recompare but that will be annoying to do on each request and then we might as well just do the whole MVC
//route on each request like we were doing before instead of caching the result of GetRouteData.
var changed = false;
using (routes.GetReadLock())
{
if (!_routeCount.HasValue || _routeCount.Value != routes.Count)
{
//the counts are not set or have changed, need to reset
changed = true;
}
}
if (changed)
{
using (routes.GetWriteLock())
{
_routeCount = routes.Count;
//try clearing each entry
foreach(var r in RouteChecks.Keys.ToList())
RouteChecks.TryRemove(r, out _);
}
}
var absPath = httpContext?.Request?.Url.AbsolutePath;
if (absPath.IsNullOrWhiteSpace())
return false;
//check if the current request matches a route, if so then it is reserved.
var hasRoute = RouteChecks.GetOrAdd(absPath, x => routes.GetRouteData(httpContext) != null);
if (hasRoute)
return true;
//continue with the standard ignore routine
return IsReservedPathOrUrl(url);
}
/// <summary>
/// This is used internally to track any registered callback paths for Identity providers. If the request path matches
/// any of the registered paths, then the module will let the request keep executing
/// </summary>
internal static readonly ConcurrentHashSet<string> ReservedPaths = new ConcurrentHashSet<string>();
}
}