From 4b514766b2c119fffde8b4e3290ead8861dacab1 Mon Sep 17 00:00:00 2001 From: Bartosz Sendek <sendek.bartosz@gmail.com> Date: Thu, 28 Apr 2022 01:59:14 +0200 Subject: [PATCH] Add/Modify Url Encoding/Decoding for IIS UrlRewrite (#40483) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Change encoding function to excape data string for performance. Add decoding function and plug it into input parser (#5985) * Add test InputParsers ParseString for server variable * Add encoded query values tests Co-authored-by: Bartosz Sendek <bsendek@sii.pl> Co-authored-by: Sébastien Ros <sebastienros@gmail.com> --- .../Rewrite/src/IISUrlRewrite/InputParser.cs | 9 +++++- .../src/PatternSegments/UrlDecodeSegment.cs | 29 +++++++++++++++++ .../src/PatternSegments/UrlEncodeSegment.cs | 3 +- .../test/IISUrlRewrite/InputParserTests.cs | 31 +++++++++++++++++++ .../PatternSegments/UrlDecodeSegmentTests.cs | 28 +++++++++++++++++ 5 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 src/Middleware/Rewrite/src/PatternSegments/UrlDecodeSegment.cs create mode 100644 src/Middleware/Rewrite/test/PatternSegments/UrlDecodeSegmentTests.cs diff --git a/src/Middleware/Rewrite/src/IISUrlRewrite/InputParser.cs b/src/Middleware/Rewrite/src/IISUrlRewrite/InputParser.cs index 1679fa839e3..ea37eb19f6d 100644 --- a/src/Middleware/Rewrite/src/IISUrlRewrite/InputParser.cs +++ b/src/Middleware/Rewrite/src/IISUrlRewrite/InputParser.cs @@ -124,7 +124,14 @@ internal class InputParser } case "UrlDecode": { - throw new NotImplementedException("UrlDecode is not implemented because of no great library available"); + var pattern = ParseString(context, uriMatchPart); + results.Add(new UrlDecodeSegment(pattern)); + + if (context.Current != CloseBrace) + { + throw new FormatException(Resources.FormatError_InputParserMissingCloseBrace(context.Index)); + } + return; } case "UrlEncode": { diff --git a/src/Middleware/Rewrite/src/PatternSegments/UrlDecodeSegment.cs b/src/Middleware/Rewrite/src/PatternSegments/UrlDecodeSegment.cs new file mode 100644 index 00000000000..c0afc1b6e00 --- /dev/null +++ b/src/Middleware/Rewrite/src/PatternSegments/UrlDecodeSegment.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; + +namespace Microsoft.AspNetCore.Rewrite.PatternSegments; + +internal class UrlDecodeSegment: PatternSegment +{ + private readonly Pattern _pattern; + + public UrlDecodeSegment(Pattern pattern) + { + _pattern = pattern; + } + + public override string? Evaluate(RewriteContext context, BackReferenceCollection? ruleBackReferences, BackReferenceCollection? conditionBackReferences) + { + var oldBuilder = context.Builder; + // PERF + // Because we need to be able to evaluate multiple nested patterns, + // we provided a new string builder and evaluate the new pattern, + // and restore it after evaluation. + context.Builder = new StringBuilder(64); + var pattern = _pattern.Evaluate(context, ruleBackReferences, conditionBackReferences); + context.Builder = oldBuilder; + return Uri.UnescapeDataString(pattern); + } +} diff --git a/src/Middleware/Rewrite/src/PatternSegments/UrlEncodeSegment.cs b/src/Middleware/Rewrite/src/PatternSegments/UrlEncodeSegment.cs index 291d3d401f2..bffea5cafc6 100644 --- a/src/Middleware/Rewrite/src/PatternSegments/UrlEncodeSegment.cs +++ b/src/Middleware/Rewrite/src/PatternSegments/UrlEncodeSegment.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Text; -using System.Text.Encodings.Web; namespace Microsoft.AspNetCore.Rewrite.PatternSegments; @@ -25,6 +24,6 @@ internal class UrlEncodeSegment : PatternSegment context.Builder = new StringBuilder(64); var pattern = _pattern.Evaluate(context, ruleBackReferences, conditionBackReferences); context.Builder = oldBuilder; - return UrlEncoder.Default.Encode(pattern); + return Uri.EscapeDataString(pattern); } } diff --git a/src/Middleware/Rewrite/test/IISUrlRewrite/InputParserTests.cs b/src/Middleware/Rewrite/test/IISUrlRewrite/InputParserTests.cs index 8885424fef7..e73666dc1c3 100644 --- a/src/Middleware/Rewrite/test/IISUrlRewrite/InputParserTests.cs +++ b/src/Middleware/Rewrite/test/IISUrlRewrite/InputParserTests.cs @@ -3,8 +3,10 @@ using System.Text.RegularExpressions; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Rewrite.IISUrlRewrite; using Microsoft.AspNetCore.Rewrite.PatternSegments; +using Microsoft.AspNetCore.Rewrite.Tests.IISUrlRewrite; using Microsoft.Extensions.Logging.Abstractions; namespace Microsoft.AspNetCore.Rewrite.Tests.UrlRewrite; @@ -63,12 +65,41 @@ public class InputParserTests [Theory] [InlineData("hey/{UrlEncode:<hey>}", "hey/%3Chey%3E")] + [InlineData("hey/?returnUrl={UrlEncode:http://domain.com?query=résumé}", "hey/?returnUrl=http%3A%2F%2Fdomain.com%3Fquery%3Dr%C3%A9sum%C3%A9")] public void EvaluatUriEncodeRule(string testString, string expected) { var middle = new InputParser().ParseInputString(testString, UriMatchPart.Path); var result = middle.Evaluate(CreateTestRewriteContext(), CreateTestRuleBackReferences(), CreateTestCondBackReferences()); Assert.Equal(expected, result); } + [Theory] + [InlineData("hey/{UrlDecode:%3Chey%3E}","hey/<hey>")] + [InlineData("{UrlDecode:http%3A%2F%2Fdomain.com%3Fquery%3Dr%C3%A9sum%C3%A9}", "http://domain.com?query=résumé")] + public void EvaluateUriDecodeRule(string testString, string expected) + { + var middle = new InputParser().ParseInputString(testString, UriMatchPart.Path); + var result = middle.Evaluate(CreateTestRewriteContext(), CreateTestRuleBackReferences(), CreateTestCondBackReferences()); + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("hey/{HTTP_URL}","hey/TEST_VARIABLE")] + public void ParseString_WithContextContainingServerVariableString_ShouldReturnResultContainingValueOfVariable(string testString, string expected) + { + var variablesDict = new Dictionary<string, string>() + { + { "HTTP_URL", "TEST_VARIABLE"} + }; + var features = new FeatureCollection(1); + features.Set<IServerVariablesFeature>(new TestServerVariablesFeature(variablesDict)); + + var rewriteContext= new RewriteContext { HttpContext = new DefaultHttpContext(features), StaticFileProvider = null, Logger = NullLogger.Instance }; + + var middle = new InputParser().ParseInputString(testString, UriMatchPart.Path); + var result = middle.Evaluate(rewriteContext, CreateTestRuleBackReferences(), CreateTestCondBackReferences()); + + Assert.Equal(expected, result); + } [Theory] [InlineData("{")] diff --git a/src/Middleware/Rewrite/test/PatternSegments/UrlDecodeSegmentTests.cs b/src/Middleware/Rewrite/test/PatternSegments/UrlDecodeSegmentTests.cs new file mode 100644 index 00000000000..2eb1d2b5c64 --- /dev/null +++ b/src/Middleware/Rewrite/test/PatternSegments/UrlDecodeSegmentTests.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Rewrite.PatternSegments; + +namespace Microsoft.AspNetCore.Rewrite.Tests.PatternSegments; + +public class UrlDecodeSegmentTests +{ + [Theory] + [InlineData("%20", " ")] + [InlineData("x%26y", "x&y")] + [InlineData("hey", "hey")] + public void FromLower_AssertLowerCaseWorksAppropriately(string input, string expected) + { + // Arrange + var pattern = new Pattern(new List<PatternSegment>()); + pattern.PatternSegments.Add(new LiteralSegment(input)); + var segement = new UrlDecodeSegment(pattern); + var context = new RewriteContext(); + + // Act + var results = segement.Evaluate(context, null, null); + + // Assert + Assert.Equal(expected, results); + } +} -- GitLab