From e2a0e887af17ff7c784149d4b36bf861705fdf31 Mon Sep 17 00:00:00 2001 From: John Luo Date: Mon, 15 Aug 2016 18:52:19 -0700 Subject: [PATCH] Add UsePathBase middleware --- .../Extensions/MapMiddleware.cs | 10 +- .../Extensions/UsePathBaseExtensions.cs | 38 ++++ .../Extensions/UsePathBaseMiddleware.cs | 77 ++++++++ .../PathString.cs | 48 ++++- .../MapPathMiddlewareTests.cs | 9 +- .../UsePathBaseExtensionsTests.cs | 168 ++++++++++++++++++ 6 files changed, 341 insertions(+), 9 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Http.Abstractions/Extensions/UsePathBaseExtensions.cs create mode 100644 src/Microsoft.AspNetCore.Http.Abstractions/Extensions/UsePathBaseMiddleware.cs create mode 100644 test/Microsoft.AspNetCore.Http.Abstractions.Tests/UsePathBaseExtensionsTests.cs diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/Extensions/MapMiddleware.cs b/src/Microsoft.AspNetCore.Http.Abstractions/Extensions/MapMiddleware.cs index 9d286d39d4..a4f67ce4a2 100644 --- a/src/Microsoft.AspNetCore.Http.Abstractions/Extensions/MapMiddleware.cs +++ b/src/Microsoft.AspNetCore.Http.Abstractions/Extensions/MapMiddleware.cs @@ -48,13 +48,15 @@ namespace Microsoft.AspNetCore.Builder.Extensions throw new ArgumentNullException(nameof(context)); } - PathString path = context.Request.Path; + PathString matchedPath; PathString remainingPath; - if (path.StartsWithSegments(_options.PathMatch, out remainingPath)) + + if (context.Request.Path.StartsWithSegments(_options.PathMatch, out matchedPath, out remainingPath)) { // Update the path - PathString pathBase = context.Request.PathBase; - context.Request.PathBase = pathBase + _options.PathMatch; + var path = context.Request.Path; + var pathBase = context.Request.PathBase; + context.Request.PathBase = pathBase.Add(matchedPath); context.Request.Path = remainingPath; try diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/Extensions/UsePathBaseExtensions.cs b/src/Microsoft.AspNetCore.Http.Abstractions/Extensions/UsePathBaseExtensions.cs new file mode 100644 index 0000000000..482f2f481f --- /dev/null +++ b/src/Microsoft.AspNetCore.Http.Abstractions/Extensions/UsePathBaseExtensions.cs @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Builder.Extensions; + +namespace Microsoft.AspNetCore.Builder +{ + /// + /// Extension methods for . + /// + public static class UsePathBaseExtensions + { + /// + /// Adds a middleware that extracts the specified path base from request path and postpend it to the request path base. + /// + /// The instance. + /// The path base to extract. + /// The instance. + public static IApplicationBuilder UsePathBase(this IApplicationBuilder app, PathString pathBase) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + // Strip trailing slashes + pathBase = pathBase.Value?.TrimEnd('/'); + if (!pathBase.HasValue) + { + return app; + } + + return app.UseMiddleware(pathBase); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/Extensions/UsePathBaseMiddleware.cs b/src/Microsoft.AspNetCore.Http.Abstractions/Extensions/UsePathBaseMiddleware.cs new file mode 100644 index 0000000000..6474aeda58 --- /dev/null +++ b/src/Microsoft.AspNetCore.Http.Abstractions/Extensions/UsePathBaseMiddleware.cs @@ -0,0 +1,77 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Builder.Extensions +{ + /// + /// Represents a middleware that extracts the specified path base from request path and postpend it to the request path base. + /// + public class UsePathBaseMiddleware + { + private readonly RequestDelegate _next; + private readonly PathString _pathBase; + + /// + /// Creates a new instace of . + /// + /// The delegate representing the next middleware in the request pipeline. + /// The path base to extract. + public UsePathBaseMiddleware(RequestDelegate next, PathString pathBase) + { + if (next == null) + { + throw new ArgumentNullException(nameof(next)); + } + + if (!pathBase.HasValue) + { + throw new ArgumentException($"{nameof(pathBase)} cannot be null or empty."); + } + + _next = next; + _pathBase = pathBase; + } + + /// + /// Executes the middleware. + /// + /// The for the current request. + /// A task that represents the execution of this middleware. + public async Task Invoke(HttpContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + PathString matchedPath; + PathString remainingPath; + + if (context.Request.Path.StartsWithSegments(_pathBase, out matchedPath, out remainingPath)) + { + var originalPath = context.Request.Path; + var originalPathBase = context.Request.PathBase; + context.Request.Path = remainingPath; + context.Request.PathBase = originalPathBase.Add(matchedPath); + + try + { + await _next(context); + } + finally + { + context.Request.Path = originalPath; + context.Request.PathBase = originalPathBase; + } + } + else + { + await _next(context); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/PathString.cs b/src/Microsoft.AspNetCore.Http.Abstractions/PathString.cs index 0b56e46292..a211d2737c 100644 --- a/src/Microsoft.AspNetCore.Http.Abstractions/PathString.cs +++ b/src/Microsoft.AspNetCore.Http.Abstractions/PathString.cs @@ -203,8 +203,8 @@ namespace Microsoft.AspNetCore.Http } /// - /// Determines whether the beginning of this PathString instance matches the specified when compared - /// using the specified comparison option and returns the remaining segments. + /// Determines whether the beginning of this instance matches the specified and returns + /// the remaining segments. /// /// The to compare. /// The remaining segments after the match. @@ -215,8 +215,8 @@ namespace Microsoft.AspNetCore.Http } /// - /// Determines whether the beginning of this instance matches the specified and returns - /// the remaining segments. + /// Determines whether the beginning of this instance matches the specified when compared + /// using the specified comparison option and returns the remaining segments. /// /// The to compare. /// One of the enumeration values that determines how this and value are compared. @@ -238,6 +238,46 @@ namespace Microsoft.AspNetCore.Http return false; } + /// + /// Determines whether the beginning of this instance matches the specified and returns + /// the matched and remaining segments. + /// + /// The to compare. + /// The matched segments with the original casing in the source value. + /// The remaining segments after the match. + /// true if value matches the beginning of this string; otherwise, false. + public bool StartsWithSegments(PathString other, out PathString matched, out PathString remaining) + { + return StartsWithSegments(other, StringComparison.OrdinalIgnoreCase, out matched, out remaining); + } + + /// + /// Determines whether the beginning of this instance matches the specified when compared + /// using the specified comparison option and returns the matched and remaining segments. + /// + /// The to compare. + /// One of the enumeration values that determines how this and value are compared. + /// The matched segments with the original casing in the source value. + /// The remaining segments after the match. + /// true if value matches the beginning of this string; otherwise, false. + public bool StartsWithSegments(PathString other, StringComparison comparisonType, out PathString matched, out PathString remaining) + { + var value1 = Value ?? string.Empty; + var value2 = other.Value ?? string.Empty; + if (value1.StartsWith(value2, comparisonType)) + { + if (value1.Length == value2.Length || value1[value2.Length] == '/') + { + matched = new PathString(value1.Substring(0, value2.Length)); + remaining = new PathString(value1.Substring(value2.Length)); + return true; + } + } + remaining = Empty; + matched = Empty; + return false; + } + /// /// Adds two PathString instances into a combined PathString value. /// diff --git a/test/Microsoft.AspNetCore.Http.Abstractions.Tests/MapPathMiddlewareTests.cs b/test/Microsoft.AspNetCore.Http.Abstractions.Tests/MapPathMiddlewareTests.cs index aaee3cc318..a30e99603c 100644 --- a/test/Microsoft.AspNetCore.Http.Abstractions.Tests/MapPathMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.Http.Abstractions.Tests/MapPathMiddlewareTests.cs @@ -75,6 +75,13 @@ namespace Microsoft.AspNetCore.Builder.Extensions [InlineData("/foo", "/Bar", "/foo/cho/")] [InlineData("/foo/cho", "/Bar", "/foo/cho")] [InlineData("/foo/cho", "/Bar", "/foo/cho/do")] + [InlineData("/foo", "", "/Foo")] + [InlineData("/foo", "", "/Foo/")] + [InlineData("/foo", "/Bar", "/Foo")] + [InlineData("/foo", "/Bar", "/Foo/Cho")] + [InlineData("/foo", "/Bar", "/Foo/Cho/")] + [InlineData("/foo/cho", "/Bar", "/Foo/Cho")] + [InlineData("/foo/cho", "/Bar", "/Foo/Cho/do")] public void PathMatchAction_BranchTaken(string matchPath, string basePath, string requestPath) { HttpContext context = CreateRequest(basePath, requestPath); @@ -84,7 +91,7 @@ namespace Microsoft.AspNetCore.Builder.Extensions app.Invoke(context).Wait(); Assert.Equal(200, context.Response.StatusCode); - Assert.Equal(basePath + matchPath, context.Items["test.PathBase"]); + Assert.Equal(basePath + requestPath.Substring(0, matchPath.Length), (string)context.Items["test.PathBase"]); Assert.Equal(requestPath.Substring(matchPath.Length), context.Items["test.Path"]); } diff --git a/test/Microsoft.AspNetCore.Http.Abstractions.Tests/UsePathBaseExtensionsTests.cs b/test/Microsoft.AspNetCore.Http.Abstractions.Tests/UsePathBaseExtensionsTests.cs new file mode 100644 index 0000000000..4b8e9d71cb --- /dev/null +++ b/test/Microsoft.AspNetCore.Http.Abstractions.Tests/UsePathBaseExtensionsTests.cs @@ -0,0 +1,168 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder.Internal; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Xunit; + +namespace Microsoft.AspNetCore.Builder.Extensions +{ + public class UsePathBaseExtensionsTests + { + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("/")] + public void EmptyOrNullPathBase_DoNotAddMiddleware(string pathBase) + { + // Arrange + var useCalled = false; + var builder = new ApplicationBuilderWrapper(CreateBuilder(), () => useCalled = true) + .UsePathBase(pathBase); + + // Act + builder.Build(); + + // Assert + Assert.False(useCalled); + } + + private class ApplicationBuilderWrapper : IApplicationBuilder + { + private readonly IApplicationBuilder _wrappedBuilder; + private readonly Action _useCallback; + + public ApplicationBuilderWrapper(IApplicationBuilder applicationBuilder, Action useCallback) + { + _wrappedBuilder = applicationBuilder; + _useCallback = useCallback; + } + + public IApplicationBuilder Use(Func middleware) + { + _useCallback(); + return _wrappedBuilder.Use(middleware); + } + + public IServiceProvider ApplicationServices + { + get { return _wrappedBuilder.ApplicationServices; } + set { _wrappedBuilder.ApplicationServices = value; } + } + + public IDictionary Properties => _wrappedBuilder.Properties; + public IFeatureCollection ServerFeatures => _wrappedBuilder.ServerFeatures; + public RequestDelegate Build() => _wrappedBuilder.Build(); + public IApplicationBuilder New() => _wrappedBuilder.New(); + + } + + [Theory] + [InlineData("/base", "", "/base", "/base", "")] + [InlineData("/base", "", "/base/", "/base", "/")] + [InlineData("/base", "", "/base/something", "/base", "/something")] + [InlineData("/base", "", "/base/something/", "/base", "/something/")] + [InlineData("/base/more", "", "/base/more", "/base/more", "")] + [InlineData("/base/more", "", "/base/more/something", "/base/more", "/something")] + [InlineData("/base/more", "", "/base/more/something/", "/base/more", "/something/")] + [InlineData("/base", "/oldbase", "/base", "/oldbase/base", "")] + [InlineData("/base", "/oldbase", "/base/", "/oldbase/base", "/")] + [InlineData("/base", "/oldbase", "/base/something", "/oldbase/base", "/something")] + [InlineData("/base", "/oldbase", "/base/something/", "/oldbase/base", "/something/")] + [InlineData("/base/more", "/oldbase", "/base/more", "/oldbase/base/more", "")] + [InlineData("/base/more", "/oldbase", "/base/more/something", "/oldbase/base/more", "/something")] + [InlineData("/base/more", "/oldbase", "/base/more/something/", "/oldbase/base/more", "/something/")] + public void RequestPathBaseContainingPathBase_IsSplit(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath) + { + TestPathBase(registeredPathBase, pathBase, requestPath, expectedPathBase, expectedPath); + } + + [Theory] + [InlineData("/base", "", "/something", "", "/something")] + [InlineData("/base", "", "/baseandsomething", "", "/baseandsomething")] + [InlineData("/base", "", "/ba", "", "/ba")] + [InlineData("/base", "", "/ba/se", "", "/ba/se")] + [InlineData("/base", "/oldbase", "/something", "/oldbase", "/something")] + [InlineData("/base", "/oldbase", "/baseandsomething", "/oldbase", "/baseandsomething")] + [InlineData("/base", "/oldbase", "/ba", "/oldbase", "/ba")] + [InlineData("/base", "/oldbase", "/ba/se", "/oldbase", "/ba/se")] + public void RequestPathBaseNotContainingPathBase_IsNotSplit(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath) + { + TestPathBase(registeredPathBase, pathBase, requestPath, expectedPathBase, expectedPath); + } + + [Theory] + [InlineData("", "", "/", "", "/")] + [InlineData("/", "", "/", "", "/")] + [InlineData("/base", "", "/base/", "/base", "/")] + [InlineData("/base/", "", "/base", "/base", "")] + [InlineData("/base/", "", "/base/", "/base", "/")] + [InlineData("", "/oldbase", "/", "/oldbase", "/")] + [InlineData("/", "/oldbase", "/", "/oldbase", "/")] + [InlineData("/base", "/oldbase", "/base/", "/oldbase/base", "/")] + [InlineData("/base/", "/oldbase", "/base", "/oldbase/base", "")] + [InlineData("/base/", "/oldbase", "/base/", "/oldbase/base", "/")] + public void PathBaseNeverEndsWithSlash(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath) + { + TestPathBase(registeredPathBase, pathBase, requestPath, expectedPathBase, expectedPath); + } + + [Theory] + [InlineData("/base", "", "/Base/Something", "/Base", "/Something")] + [InlineData("/base", "/OldBase", "/Base/Something", "/OldBase/Base", "/Something")] + public void PathBaseAndPathPreserveRequestCasing(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath) + { + TestPathBase(registeredPathBase, pathBase, requestPath, expectedPathBase, expectedPath); + } + + [Theory] + [InlineData("/b♫se", "", "/b♫se/something", "/b♫se", "/something")] + [InlineData("/b♫se", "", "/B♫se/something", "/B♫se", "/something")] + [InlineData("/b♫se", "", "/b♫se/Something", "/b♫se", "/Something")] + [InlineData("/b♫se", "/oldb♫se", "/b♫se/something", "/oldb♫se/b♫se", "/something")] + [InlineData("/b♫se", "/oldb♫se", "/b♫se/Something", "/oldb♫se/b♫se", "/Something")] + [InlineData("/b♫se", "/oldb♫se", "/B♫se/something", "/oldb♫se/B♫se", "/something")] + public void PathBaseCanHaveUnicodeCharacters(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath) + { + TestPathBase(registeredPathBase, pathBase, requestPath, expectedPathBase, expectedPath); + } + + private static void TestPathBase(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath) + { + HttpContext requestContext = CreateRequest(pathBase, requestPath); + var builder = CreateBuilder() + .UsePathBase(registeredPathBase); + builder.Run(context => + { + context.Items["test.Path"] = context.Request.Path; + context.Items["test.PathBase"] = context.Request.PathBase; + return Task.FromResult(0); + }); + builder.Build().Invoke(requestContext).Wait(); + + // Assert path and pathBase are split after middleware + Assert.Equal(expectedPath, ((PathString)requestContext.Items["test.Path"]).Value); + Assert.Equal(expectedPathBase, ((PathString)requestContext.Items["test.PathBase"]).Value); + // Assert path and pathBase are reset after request + Assert.Equal(pathBase, requestContext.Request.PathBase.Value); + Assert.Equal(requestPath, requestContext.Request.Path.Value); + } + + private static HttpContext CreateRequest(string pathBase, string requestPath) + { + HttpContext context = new DefaultHttpContext(); + context.Request.PathBase = new PathString(pathBase); + context.Request.Path = new PathString(requestPath); + return context; + } + + private static ApplicationBuilder CreateBuilder() + { + return new ApplicationBuilder(serviceProvider: null); + } + } +}