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