diff --git a/BasicMiddleware.sln b/BasicMiddleware.sln
index 9ac27d55d2..a431cdab18 100644
--- a/BasicMiddleware.sln
+++ b/BasicMiddleware.sln
@@ -13,6 +13,7 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{99B72A07-32D6-434D-B44D-D064E3C13E08}"
ProjectSection(SolutionItems) = preProject
global.json = global.json
+ NuGetPackageVerifier.json = NuGetPackageVerifier.json
EndProjectSection
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Buffering", "src\Microsoft.AspNetCore.Buffering\Microsoft.AspNetCore.Buffering.xproj", "{2363D0DD-A3BF-437E-9B64-B33AE132D875}"
diff --git a/NuGetPackageVerifier.json b/NuGetPackageVerifier.json
index 0895735cea..5fc01f59bd 100644
--- a/NuGetPackageVerifier.json
+++ b/NuGetPackageVerifier.json
@@ -5,7 +5,8 @@
],
"packages": {
"Microsoft.AspNetCore.Buffering": { },
- "Microsoft.AspNetCore.HttpOverrides": { }
+ "Microsoft.AspNetCore.HttpOverrides": { },
+ "Microsoft.AspNetCore.Rewrite": { }
}
},
"Default": { // Rules to run for packages not listed in any other set.
diff --git a/samples/RewriteSample/Rewrite.txt b/samples/RewriteSample/Rewrite.txt
index fdc4175f86..ed2ae33ce1 100644
--- a/samples/RewriteSample/Rewrite.txt
+++ b/samples/RewriteSample/Rewrite.txt
@@ -1,5 +1,4 @@
# Ensure Https
-RewriteCond %{REQUEST_URI} ^foo/
RewriteCond %{HTTPS} off
# U is a new flag to represent full URL rewrites
RewriteRule ^(.*)$ https://www.example.com$1 [L,U]
diff --git a/samples/RewriteSample/Startup.cs b/samples/RewriteSample/Startup.cs
index 09ae531da5..f476c62dd5 100644
--- a/samples/RewriteSample/Startup.cs
+++ b/samples/RewriteSample/Startup.cs
@@ -7,11 +7,13 @@ namespace RewriteSample
{
public class Startup
{
- public void Configure(IApplicationBuilder app)
- {
+ public void Configure(IApplicationBuilder app, IHostingEnvironment hostingEnv)
+ {
app.UseRewriter(new UrlRewriteOptions()
- .ImportFromModRewrite("Rewrite.txt"));
+ .ImportFromUrlRewrite(hostingEnv, "UrlRewrite.xml")
+ .ImportFromModRewrite(hostingEnv, "Rewrite.txt"));
app.Run(context => context.Response.WriteAsync(context.Request.Path));
+
}
public static void Main(string[] args)
diff --git a/samples/RewriteSample/UrlRewrite.xml b/samples/RewriteSample/UrlRewrite.xml
new file mode 100644
index 0000000000..708ef0e8be
--- /dev/null
+++ b/samples/RewriteSample/UrlRewrite.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ModRewriteExtensions.cs b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ModRewriteExtensions.cs
index 445ecde591..a5c4986e53 100644
--- a/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ModRewriteExtensions.cs
+++ b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ModRewriteExtensions.cs
@@ -3,6 +3,7 @@
using System;
using System.IO;
+using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Rewrite.ModRewrite;
namespace Microsoft.AspNetCore.Rewrite
@@ -13,17 +14,27 @@ namespace Microsoft.AspNetCore.Rewrite
/// Imports rules from a mod_rewrite file and adds the rules to current rules.
///
/// The UrlRewrite options.
+ ///
/// The path to the file containing mod_rewrite rules.
- public static UrlRewriteOptions ImportFromModRewrite(this UrlRewriteOptions options, string filePath)
+ public static UrlRewriteOptions ImportFromModRewrite(this UrlRewriteOptions options, IHostingEnvironment hostingEnv, string filePath)
{
- // TODO use IHostingEnvironment as param.
+ if (options == null)
+ {
+ throw new ArgumentNullException("UrlRewriteOptions is null");
+ }
+
+ if (hostingEnv == null)
+ {
+ throw new ArgumentNullException("HostingEnvironment is null");
+ }
+
if (string.IsNullOrEmpty(filePath))
{
throw new ArgumentException(nameof(filePath));
}
- // TODO IHosting to fix!
- using (var stream = File.OpenRead(filePath))
+ var path = Path.Combine(hostingEnv.ContentRootPath, filePath);
+ using (var stream = File.OpenRead(path))
{
options.Rules.AddRange(FileParser.Parse(new StreamReader(stream)));
};
diff --git a/src/Microsoft.AspNetCore.Rewrite/ModRewrite/Pattern.cs b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/Pattern.cs
index b3070faf1d..8adcad99dd 100644
--- a/src/Microsoft.AspNetCore.Rewrite/ModRewrite/Pattern.cs
+++ b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/Pattern.cs
@@ -8,7 +8,7 @@ using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Rewrite.ModRewrite;
-namespace Microsoft.AspNetCore.Rewrite
+namespace Microsoft.AspNetCore.Rewrite.ModRewrite
{
///
/// Contains a sequence of pattern segments, which on obtaining the context, will create the appropriate
diff --git a/src/Microsoft.AspNetCore.Rewrite/ModRewrite/PatternSegment.cs b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/PatternSegment.cs
index 13a22cb50c..8cb0e961b7 100644
--- a/src/Microsoft.AspNetCore.Rewrite/ModRewrite/PatternSegment.cs
+++ b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/PatternSegment.cs
@@ -1,7 +1,7 @@
// 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.
-namespace Microsoft.AspNetCore.Rewrite
+namespace Microsoft.AspNetCore.Rewrite.ModRewrite
{
///
/// A Pattern segment contains a portion of the test string/ substitution segment with a type associated.
diff --git a/src/Microsoft.AspNetCore.Rewrite/Operands/IntegerOperand.cs b/src/Microsoft.AspNetCore.Rewrite/Operands/IntegerOperand.cs
index 1d19c4a620..e65d7bac93 100644
--- a/src/Microsoft.AspNetCore.Rewrite/Operands/IntegerOperand.cs
+++ b/src/Microsoft.AspNetCore.Rewrite/Operands/IntegerOperand.cs
@@ -25,6 +25,7 @@ namespace Microsoft.AspNetCore.Rewrite.Operands
{
throw new FormatException("Syntax error for integers in comparison.");
}
+ Value = compValue;
Operation = operation;
}
diff --git a/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/ActionType.cs b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/ActionType.cs
new file mode 100644
index 0000000000..a7eff7669d
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/ActionType.cs
@@ -0,0 +1,14 @@
+// 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.
+
+namespace Microsoft.AspNetCore.Rewrite.UrlRewrite
+{
+ public enum ActionType
+ {
+ None,
+ Rewrite,
+ Redirect,
+ CustomResponse,
+ AbortRequest
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/Condition.cs b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/Condition.cs
new file mode 100644
index 0000000000..17030d2d5f
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/Condition.cs
@@ -0,0 +1,16 @@
+// 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.Text.RegularExpressions;
+
+namespace Microsoft.AspNetCore.Rewrite.UrlRewrite
+{
+ public class Condition
+ {
+ public Pattern Input { get; set; }
+ public Regex MatchPattern { get; set; }
+ public bool Negate { get; set; }
+ public bool IgnoreCase { get; set; } = true;
+ public MatchType MatchType { get; set; } = MatchType.Pattern;
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/Conditions.cs b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/Conditions.cs
new file mode 100644
index 0000000000..0e3ec81df7
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/Conditions.cs
@@ -0,0 +1,14 @@
+// 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.Collections.Generic;
+
+namespace Microsoft.AspNetCore.Rewrite.UrlRewrite
+{
+ public class Conditions
+ {
+ public List ConditionList { get; set; } = new List();
+ public LogicalGrouping MatchType { get; set; } // default is MatchAll
+ public bool TrackingAllCaptures { get; set; }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/InitialMatch.cs b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/InitialMatch.cs
new file mode 100644
index 0000000000..bfbdcad006
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/InitialMatch.cs
@@ -0,0 +1,14 @@
+// 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.Text.RegularExpressions;
+
+namespace Microsoft.AspNetCore.Rewrite.UrlRewrite
+{
+ public class InitialMatch
+ {
+ public Regex Url { get; set; } // TODO must be a non-empty string, throw in check after parsing?
+ public bool IgnoreCase { get; set; } = true;
+ public bool Negate { get; set; }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/InputParser.cs b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/InputParser.cs
new file mode 100644
index 0000000000..eb8bbd0410
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/InputParser.cs
@@ -0,0 +1,197 @@
+// 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.Text.Encodings.Web;
+using Microsoft.AspNetCore.Rewrite.Internal;
+using Microsoft.AspNetCore.Rewrite.UrlRewrite.PatternSegments;
+
+namespace Microsoft.AspNetCore.Rewrite.UrlRewrite
+{
+ ///
+ ///
+ public class InputParser
+ {
+ private const char Colon = ':';
+ private const char OpenBrace = '{';
+ private const char CloseBrace = '}';
+
+ ///
+ /// Creates a pattern, which is a template to create a new test string to
+ /// compare to the condition. Can contain server variables, back references, etc.
+ ///
+ ///
+ /// A new , containing a list of
+ public static Pattern ParseInputString(string testString)
+ {
+ if (testString == null)
+ {
+ testString = string.Empty;
+ }
+
+ var context = new ParserContext(testString);
+ return ParseString(context);
+ }
+
+ private static Pattern ParseString(ParserContext context)
+ {
+ var results = new List();
+ while (context.Next())
+ {
+ if (context.Current == OpenBrace)
+ {
+ // This is a server parameter, parse for a condition variable
+ if (!context.Next())
+ {
+ throw new FormatException(context.Error());
+ }
+ ParseParameter(context, results);
+ }
+ else if (context.Current == CloseBrace)
+ {
+ // TODO we should be throwing a syntax error if we have uneven close braces
+ // Can fix by keeping track of the number of '{' and '}' with an int, where {
+ // increments and } decrements. Throw if < 0.
+ return new Pattern(results);
+ }
+ else
+ {
+ // Parse for literals, which will return on either the end of the test string
+ // or when it hits a special character
+ ParseLiteral(context, results);
+ }
+ }
+ return new Pattern(results);
+ }
+
+ private static void ParseParameter(ParserContext context, List results)
+ {
+ context.Mark();
+ // Four main cases:
+ // 1. {NAME} - Server Variable, create lambda to get the part of the context
+ // 2. {R:1} - Rule parameter
+ // 3. {C:1} - Condition Parameter
+ // 4. {function:xxx} - String function
+ // TODO consider perf here. This is on startup and will only happen one time
+ // (unless we support Reload)
+ string parameter;
+ while (context.Next())
+ {
+ if (context.Current == CloseBrace)
+ {
+ // This is just a server variable, so we do a lookup and verify the server variable exists.
+ parameter = context.Capture();
+ results.Add(ServerVariables.FindServerVariable(parameter));
+ return;
+ }
+ else if (context.Current == Colon)
+ {
+ parameter = context.Capture();
+
+ // Only 5 strings to expect here. Case sensitive.
+ switch (parameter)
+ {
+ case "ToLower":
+ {
+ var pattern = ParseString(context);
+ results.Add(new ToLowerSegment(pattern));
+
+ // at this point, we expect our context to be on the ending closing brace,
+ // because the ParseString() call will increment the context until it
+ // has processed the new string.
+ if (context.Current != CloseBrace)
+ {
+ throw new FormatException("Lacking close brace for parameter at index: " + context.GetIndex());
+ }
+ }
+ break;
+ case "UrlDecode":
+ {
+ throw new NotImplementedException("UrlDecode is not supported.");
+ }
+ case "UrlEncode":
+ {
+ var pattern = ParseString(context);
+ results.Add(new UrlEncodeSegment(pattern));
+
+ if (context.Current != CloseBrace)
+ {
+ throw new FormatException("Lacking close brace for parameter at index: " + context.GetIndex());
+ }
+ }
+ break;
+ case "R":
+ {
+ var index = GetBackReferenceIndex(context);
+ results.Add(new RuleMatchSegment(index));
+ return;
+ }
+ case "C":
+ {
+ var index = GetBackReferenceIndex(context);
+ results.Add(new ConditionMatchSegment(index));
+ return;
+ }
+ default:
+ throw new FormatException("Unrecognized parameter type: " + parameter + ", terminated at index: " + context.GetIndex());
+ }
+ }
+ }
+ }
+
+ private static int GetBackReferenceIndex(ParserContext context)
+ {
+ if (!context.Next())
+ {
+ throw new FormatException("No index avaible for backreference at index: " + context.GetIndex());
+ }
+
+ context.Mark();
+ while (context.Current != CloseBrace)
+ {
+ if (!context.Next())
+ {
+ throw new FormatException("Lacking close brace for parameter at index: " + context.GetIndex());
+ }
+ }
+
+ var res = context.Capture();
+ int index;
+ if (!int.TryParse(res, out index))
+ {
+ throw new FormatException("Syntax error, invalid integer in response parameter at index: " + context.GetIndex());
+ }
+
+ if (index > 9 || index < 0)
+ {
+ throw new FormatException("Invalid integer into backreference " + index + "at index: " + context.GetIndex());
+ }
+ return index;
+ }
+
+ private static bool ParseLiteral(ParserContext context, List results)
+ {
+ context.Mark();
+ string literal;
+ while (true)
+ {
+ if (context.Current == OpenBrace || context.Current == CloseBrace)
+ {
+ literal = context.Capture();
+ context.Back();
+ break;
+ }
+
+ if (!context.Next())
+ {
+ literal = context.Capture();
+ break;
+ }
+ }
+
+ results.Add(new LiteralSegment(literal));
+ return true;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/LogicalGrouping.cs b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/LogicalGrouping.cs
new file mode 100644
index 0000000000..5f2568c8e6
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/LogicalGrouping.cs
@@ -0,0 +1,11 @@
+// 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.
+
+namespace Microsoft.AspNetCore.Rewrite.UrlRewrite
+{
+ public enum LogicalGrouping
+ {
+ MatchAll,
+ MatchAny
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/MatchType.cs b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/MatchType.cs
new file mode 100644
index 0000000000..260ecba3d0
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/MatchType.cs
@@ -0,0 +1,12 @@
+// 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.
+
+namespace Microsoft.AspNetCore.Rewrite.UrlRewrite
+{
+ public enum MatchType
+ {
+ Pattern,
+ IsFile,
+ IsDirectory
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/Pattern.cs b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/Pattern.cs
new file mode 100644
index 0000000000..aa62e3c825
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/Pattern.cs
@@ -0,0 +1,30 @@
+// 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.Collections.Generic;
+using System.Text;
+using System.Text.RegularExpressions;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Rewrite.UrlRewrite
+{
+ public class Pattern
+ {
+ public IList PatternSegments { get; }
+
+ public Pattern(List patternSegments)
+ {
+ PatternSegments = patternSegments;
+ }
+
+ public string Evaluate(HttpContext context, Match ruleMatch, Match condMatch)
+ {
+ var strBuilder = new StringBuilder();
+ foreach (var pattern in PatternSegments)
+ {
+ strBuilder.Append(pattern.Evaluate(context, ruleMatch, condMatch));
+ }
+ return strBuilder.ToString();
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSegment.cs b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSegment.cs
new file mode 100644
index 0000000000..d80755d77a
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSegment.cs
@@ -0,0 +1,15 @@
+// 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.Text.RegularExpressions;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Rewrite.UrlRewrite
+{
+ public abstract class PatternSegment
+ {
+ // Match from prevRule, Match from prevCond
+ public abstract string Evaluate(HttpContext context, Match ruleMatch, Match condMatch);
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSegments/ConditionMatchSegment.cs b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSegments/ConditionMatchSegment.cs
new file mode 100644
index 0000000000..ec53c1549a
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSegments/ConditionMatchSegment.cs
@@ -0,0 +1,23 @@
+// 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.Text.RegularExpressions;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Rewrite.UrlRewrite.PatternSegments
+{
+ public class ConditionMatchSegment : PatternSegment
+ {
+ public int Index { get; set; }
+
+ public ConditionMatchSegment(int index)
+ {
+ Index = index;
+ }
+
+ public override string Evaluate(HttpContext context, Match ruleMatch, Match condMatch)
+ {
+ return condMatch?.Groups[Index]?.Value;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSegments/HeaderSegment.cs b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSegments/HeaderSegment.cs
new file mode 100644
index 0000000000..0353598bea
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSegments/HeaderSegment.cs
@@ -0,0 +1,23 @@
+// 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.Text.RegularExpressions;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Rewrite.UrlRewrite.PatternSegments
+{
+ public class HeaderSegment : PatternSegment
+ {
+ public string Header { get; set; }
+
+ public HeaderSegment(string header)
+ {
+ Header = header;
+ }
+
+ public override string Evaluate(HttpContext context, Match ruleMatch, Match condMatch)
+ {
+ return context.Request.Headers[Header];
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSegments/IsHttpsSegment.cs b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSegments/IsHttpsSegment.cs
new file mode 100644
index 0000000000..ff6be8e823
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSegments/IsHttpsSegment.cs
@@ -0,0 +1,16 @@
+// 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.Text.RegularExpressions;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Rewrite.UrlRewrite.PatternSegments
+{
+ public class IsHttpsSegment : PatternSegment
+ {
+ public override string Evaluate(HttpContext context, Match ruleMatch, Match condMatch)
+ {
+ return context.Request.IsHttps ? "ON" : "OFF";
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSegments/LiteralSegment.cs b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSegments/LiteralSegment.cs
new file mode 100644
index 0000000000..43b45fec9c
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSegments/LiteralSegment.cs
@@ -0,0 +1,23 @@
+// 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.Text.RegularExpressions;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Rewrite.UrlRewrite.PatternSegments
+{
+ public class LiteralSegment : PatternSegment
+ {
+ public string Literal { get; set; }
+
+ public LiteralSegment(string literal)
+ {
+ Literal = literal;
+ }
+
+ public override string Evaluate(HttpContext context, Match ruleMatch, Match condMatch)
+ {
+ return Literal;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSegments/LocalAddressSegment.cs b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSegments/LocalAddressSegment.cs
new file mode 100644
index 0000000000..a26e79238b
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSegments/LocalAddressSegment.cs
@@ -0,0 +1,16 @@
+// 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.Text.RegularExpressions;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Rewrite.UrlRewrite.PatternSegments
+{
+ public class LocalAddressSegment : PatternSegment
+ {
+ public override string Evaluate(HttpContext context, Match ruleMatch, Match condMatch)
+ {
+ return context.Connection.LocalIpAddress?.ToString();
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSegments/QueryStringSegment.cs b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSegments/QueryStringSegment.cs
new file mode 100644
index 0000000000..5b51d8f98c
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSegments/QueryStringSegment.cs
@@ -0,0 +1,16 @@
+// 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.Text.RegularExpressions;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Rewrite.UrlRewrite.PatternSegments
+{
+ public class QueryStringSegment : PatternSegment
+ {
+ public override string Evaluate(HttpContext context, Match ruleMatch, Match condMatch)
+ {
+ return context.Request.QueryString.ToString();
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSegments/RemoteAddressSegment.cs b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSegments/RemoteAddressSegment.cs
new file mode 100644
index 0000000000..20bf50c523
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSegments/RemoteAddressSegment.cs
@@ -0,0 +1,16 @@
+// 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.Text.RegularExpressions;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Rewrite.UrlRewrite.PatternSegments
+{
+ public class RemoteAddressSegment : PatternSegment
+ {
+ public override string Evaluate(HttpContext context, Match ruleMatch, Match condMatch)
+ {
+ return context.Connection.RemoteIpAddress?.ToString();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSegments/RemotePortSegment.cs b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSegments/RemotePortSegment.cs
new file mode 100644
index 0000000000..bb1c391b29
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSegments/RemotePortSegment.cs
@@ -0,0 +1,17 @@
+// 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.Globalization;
+using System.Text.RegularExpressions;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Rewrite.UrlRewrite.PatternSegments
+{
+ public class RemotePortSegment : PatternSegment
+ {
+ public override string Evaluate(HttpContext context, Match ruleMatch, Match condMatch)
+ {
+ return context.Connection.RemotePort.ToString(CultureInfo.InvariantCulture);
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSegments/RuleMatchSegment.cs b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSegments/RuleMatchSegment.cs
new file mode 100644
index 0000000000..2ebbb0f61c
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSegments/RuleMatchSegment.cs
@@ -0,0 +1,23 @@
+// 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.Text.RegularExpressions;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Rewrite.UrlRewrite.PatternSegments
+{
+ public class RuleMatchSegment : PatternSegment
+ {
+ public int Index { get; set; }
+
+ public RuleMatchSegment(int index)
+ {
+ Index = index;
+ }
+
+ public override string Evaluate(HttpContext context, Match ruleMatch, Match condMatch)
+ {
+ return ruleMatch?.Groups[Index]?.Value;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSegments/ToLowerSegment.cs b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSegments/ToLowerSegment.cs
new file mode 100644
index 0000000000..945bdd19f8
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSegments/ToLowerSegment.cs
@@ -0,0 +1,24 @@
+// 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.Text.RegularExpressions;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Rewrite.UrlRewrite.PatternSegments
+{
+ public class ToLowerSegment : PatternSegment
+ {
+ public Pattern Pattern { get; set; }
+
+ public ToLowerSegment(Pattern pattern)
+ {
+ Pattern = pattern;
+ }
+
+ public override string Evaluate(HttpContext context, Match ruleMatch, Match condMatch)
+ {
+ var pattern = Pattern.Evaluate(context, ruleMatch, condMatch);
+ return pattern.ToLowerInvariant();
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSegments/UrlEncodeSegment.cs b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSegments/UrlEncodeSegment.cs
new file mode 100644
index 0000000000..4f97346a8d
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSegments/UrlEncodeSegment.cs
@@ -0,0 +1,25 @@
+// 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.Text.Encodings.Web;
+using System.Text.RegularExpressions;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Rewrite.UrlRewrite.PatternSegments
+{
+ public class UrlEncodeSegment : PatternSegment
+ {
+ public Pattern Pattern { get; set; }
+
+ public UrlEncodeSegment(Pattern pattern)
+ {
+ Pattern = pattern;
+ }
+
+ public override string Evaluate(HttpContext context, Match ruleMatch, Match condMatch)
+ {
+ var pattern = Pattern.Evaluate(context, ruleMatch, condMatch);
+ return UrlEncoder.Default.Encode(pattern);
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSegments/UrlSegment.cs b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSegments/UrlSegment.cs
new file mode 100644
index 0000000000..9dc24ec930
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSegments/UrlSegment.cs
@@ -0,0 +1,16 @@
+// 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.Text.RegularExpressions;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Rewrite.UrlRewrite.PatternSegments
+{
+ public class UrlSegment : PatternSegment
+ {
+ public override string Evaluate(HttpContext context, Match ruleMatch, Match condMatch)
+ {
+ return context.Request.Path.ToString();
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSyntax.cs b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSyntax.cs
new file mode 100644
index 0000000000..efdf403427
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/PatternSyntax.cs
@@ -0,0 +1,12 @@
+// 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.
+
+namespace Microsoft.AspNetCore.Rewrite.UrlRewrite
+{
+ public enum PatternSyntax
+ {
+ ECMAScript,
+ WildCard,
+ ExactMatch
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/RedirectType.cs b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/RedirectType.cs
new file mode 100644
index 0000000000..5d842c57d4
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/RedirectType.cs
@@ -0,0 +1,13 @@
+// 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.
+
+namespace Microsoft.AspNetCore.Rewrite.UrlRewrite
+{
+ public enum RedirectType
+ {
+ Permanent = 301,
+ Found = 302,
+ SeeOther = 303,
+ Temporary = 307
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/RewriteTags.cs b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/RewriteTags.cs
new file mode 100644
index 0000000000..5e4d23242d
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/RewriteTags.cs
@@ -0,0 +1,34 @@
+// 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.
+
+namespace Microsoft.AspNetCore.Rewrite.UrlRewrite
+{
+ public static class RewriteTags
+ {
+ // TODO More strings to be added later once further implementations are added.
+ public const string Rewrite = "rewrite";
+ public const string GlobalRules = "globalRules";
+ public const string Rules = "rules";
+ public const string Rule = "rule";
+ public const string Action = "action";
+ public const string Name = "name";
+ public const string Enabled = "enabled";
+ public const string PatternSyntax = "patternSyntax";
+ public const string StopProcessing = "stopProcessing";
+ public const string Match = "match";
+ public const string Conditions = "conditions";
+ public const string IgnoreCase = "ignoreCase";
+ public const string Negate = "negate";
+ public const string Url = "url";
+ public const string MatchType = "matchType";
+ public const string Add = "add";
+ public const string TrackingAllCaptures = "trackingAllCaptures";
+ public const string MatchPattern = "matchPattern";
+ public const string Input = "input";
+ public const string Pattern = "pattern";
+ public const string Type = "type";
+ public const string AppendQuery = "appendQuery";
+ public const string LogRewrittenUrl = "logRewrittenUrl";
+ public const string RedirectType = "redirectType";
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/ServerVariables.cs b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/ServerVariables.cs
new file mode 100644
index 0000000000..7ffb9a5da4
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/ServerVariables.cs
@@ -0,0 +1,61 @@
+// 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.Globalization;
+using System.Text.RegularExpressions;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Rewrite.UrlRewrite.PatternSegments;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.AspNetCore.Rewrite.UrlRewrite
+{
+ public static class ServerVariables
+ {
+ public static PatternSegment FindServerVariable(string serverVariable)
+ {
+ switch(serverVariable)
+ {
+ // TODO Add all server variables here.
+ case "ALL_RAW":
+ throw new NotImplementedException();
+ case "APP_POOL_ID":
+ throw new NotImplementedException();
+ case "CONTENT_LENGTH":
+ return new HeaderSegment(HeaderNames.ContentLength);
+ case "CONTENT_TYPE":
+ return new HeaderSegment(HeaderNames.ContentType);
+ case "HTTP_ACCEPT":
+ return new HeaderSegment(HeaderNames.Accept);
+ case "HTTP_COOKIE":
+ return new HeaderSegment(HeaderNames.Cookie);
+ case "HTTP_HOST":
+ return new HeaderSegment(HeaderNames.Host);
+ case "HTTP_PROXY_CONNECTION":
+ return new HeaderSegment(HeaderNames.ProxyAuthenticate);
+ case "HTTP_REFERER":
+ return new HeaderSegment(HeaderNames.Referer);
+ case "HTTP_USER_AGENT":
+ return new HeaderSegment(HeaderNames.UserAgent);
+ case "HTTP_CONNECTION":
+ return new HeaderSegment(HeaderNames.Connection);
+ case "HTTP_URL":
+ return new UrlSegment();
+ case "HTTPS":
+ return new IsHttpsSegment();
+ case "LOCAL_ADDR":
+ return new LocalAddressSegment();
+ case "QUERY_STRING":
+ return new QueryStringSegment();
+ case "REMOTE_ADDR":
+ return new RemoteAddressSegment();
+ case "REMOTE_HOST":
+ throw new NotImplementedException();
+ case "REMOTE_PORT":
+ return new RemotePortSegment();
+ default:
+ throw new FormatException("Unrecognized server variable.");
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/UrlAction.cs b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/UrlAction.cs
new file mode 100644
index 0000000000..c0d56f603a
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/UrlAction.cs
@@ -0,0 +1,14 @@
+// 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.
+
+namespace Microsoft.AspNetCore.Rewrite.UrlRewrite
+{
+ public class UrlAction
+ {
+ public ActionType Type { get; set; }
+ public Pattern Url { get; set; }
+ public bool AppendQueryString { get; set; }
+ public bool LogRewrittenUrl { get; set; }
+ public RedirectType RedirectType { get; set; } = RedirectType.Permanent;
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/UrlRewriteExtensions.cs b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/UrlRewriteExtensions.cs
new file mode 100644
index 0000000000..b958d2ed60
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/UrlRewriteExtensions.cs
@@ -0,0 +1,44 @@
+// 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.IO;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Rewrite.UrlRewrite;
+
+namespace Microsoft.AspNetCore.Rewrite
+{
+ public static class UrlRewriteExtensions
+ {
+ ///
+ /// Imports rules from a mod_rewrite file and adds the rules to current rules.
+ ///
+ /// The UrlRewrite options.
+ ///
+ /// The path to the file containing urlrewrite rules.
+ public static UrlRewriteOptions ImportFromUrlRewrite(this UrlRewriteOptions options, IHostingEnvironment hostingEnv, string filePath)
+ {
+ if (options == null)
+ {
+ throw new ArgumentNullException("UrlRewriteOptions is null");
+ }
+
+ if (hostingEnv == null)
+ {
+ throw new ArgumentNullException("HostingEnvironment is null");
+ }
+
+ if (string.IsNullOrEmpty(filePath))
+ {
+ throw new ArgumentException(nameof(filePath));
+ }
+
+ var path = Path.Combine(hostingEnv.ContentRootPath, filePath);
+ using (var stream = File.OpenRead(path))
+ {
+ options.Rules.AddRange(UrlRewriteFileParser.Parse(new StreamReader(stream)));
+ };
+ return options;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/UrlRewriteFileParser.cs b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/UrlRewriteFileParser.cs
new file mode 100644
index 0000000000..454555c79e
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/UrlRewriteFileParser.cs
@@ -0,0 +1,228 @@
+// 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.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Xml.Linq;
+
+namespace Microsoft.AspNetCore.Rewrite.UrlRewrite
+{
+ // TODO rename
+ public static class UrlRewriteFileParser
+ {
+ private static readonly TimeSpan RegexTimeout = TimeSpan.FromMilliseconds(1);
+ public static List Parse(TextReader reader)
+ {
+ var temp = XDocument.Load(reader);
+ var xmlRoot = temp.Descendants(RewriteTags.Rewrite);
+ var rules = new List();
+
+ if (xmlRoot != null)
+ {
+ // there is a valid rewrite block, go through each rule and process
+ GetGlobalRules(xmlRoot.Descendants(RewriteTags.GlobalRules), rules);
+ GetRules(xmlRoot.Descendants(RewriteTags.Rules), rules);
+ }
+ return rules;
+ }
+
+ private static void GetGlobalRules(IEnumerable globalRules, List result)
+ {
+ foreach (var rule in globalRules.Elements(RewriteTags.Rule) ?? Enumerable.Empty())
+ {
+ var res = new UrlRewriteRule();
+ SetRuleAttributes(rule, res);
+ // TODO handle full url with global rules - may or may not support
+ res.Action = CreateUrlAction(rule.Element(RewriteTags.Action));
+ result.Add(res);
+ }
+ }
+
+ private static void GetRules(IEnumerable rules, List result)
+ {
+ // TODO Better null check?
+ foreach (var rule in rules.Elements(RewriteTags.Rule) ?? Enumerable.Empty())
+ {
+ var res = new UrlRewriteRule();
+ SetRuleAttributes(rule, res);
+ res.Action = CreateUrlAction(rule.Element(RewriteTags.Action));
+ result.Add(res);
+ }
+ }
+
+ private static void SetRuleAttributes(XElement rule, UrlRewriteRule res)
+ {
+ if (rule == null)
+ {
+ return;
+ }
+
+ res.Name = rule.Attribute(RewriteTags.Name)?.Value;
+
+ bool enabled;
+ if (bool.TryParse(rule.Attribute(RewriteTags.Enabled)?.Value, out enabled))
+ {
+ res.Enabled = enabled;
+ }
+
+ PatternSyntax patternSyntax;
+ if (Enum.TryParse(rule.Attribute(RewriteTags.PatternSyntax)?.Value, out patternSyntax))
+ {
+ res.PatternSyntax = patternSyntax;
+ }
+
+ bool stopProcessing;
+ if (bool.TryParse(rule.Attribute(RewriteTags.StopProcessing)?.Value, out stopProcessing))
+ {
+ res.StopProcessing = stopProcessing;
+ }
+
+ res.Match = CreateMatch(rule.Element(RewriteTags.Match));
+ res.Conditions = CreateConditions(rule.Element(RewriteTags.Conditions));
+ }
+
+ private static InitialMatch CreateMatch(XElement match)
+ {
+ if (match == null)
+ {
+ return null;
+ }
+
+ var matchRes = new InitialMatch();
+
+ bool parBool;
+ if (bool.TryParse(match.Attribute(RewriteTags.IgnoreCase)?.Value, out parBool))
+ {
+ matchRes.IgnoreCase = parBool;
+ }
+
+ if (bool.TryParse(match.Attribute(RewriteTags.Negate)?.Value, out parBool))
+ {
+ matchRes.Negate = parBool;
+ }
+
+ var parsedInputString = match.Attribute(RewriteTags.Url)?.Value;
+
+ if (matchRes.IgnoreCase)
+ {
+ matchRes.Url = new Regex(parsedInputString, RegexOptions.IgnoreCase | RegexOptions.Compiled, RegexTimeout);
+ }
+ else
+ {
+ matchRes.Url = new Regex(parsedInputString, RegexOptions.Compiled, RegexTimeout);
+ }
+ return matchRes;
+ }
+
+
+ private static Conditions CreateConditions(XElement conditions)
+ {
+ var condRes = new Conditions();
+ if (conditions == null)
+ {
+ return condRes; // TODO make sure no null exception on Conditions
+ }
+
+ LogicalGrouping grouping;
+ if (Enum.TryParse(conditions.Attribute(RewriteTags.MatchType)?.Value, out grouping))
+ {
+ condRes.MatchType = grouping;
+ }
+
+ bool parBool;
+ if (bool.TryParse(conditions.Attribute(RewriteTags.TrackingAllCaptures)?.Value, out parBool))
+ {
+ condRes.TrackingAllCaptures = parBool;
+ }
+
+ foreach (var cond in conditions.Elements(RewriteTags.Add))
+ {
+ condRes.ConditionList.Add(CreateCondition(cond));
+ }
+ return condRes;
+ }
+
+ private static Condition CreateCondition(XElement condition)
+ {
+ if (condition == null)
+ {
+ return null;
+ }
+
+ var condRes = new Condition();
+
+ bool parBool;
+ if (bool.TryParse(condition.Attribute(RewriteTags.IgnoreCase)?.Value, out parBool))
+ {
+ condRes.IgnoreCase = parBool;
+ }
+
+ if (bool.TryParse(condition.Attribute(RewriteTags.Negate)?.Value, out parBool))
+ {
+ condRes.Negate = parBool;
+ }
+
+ MatchType matchType;
+ if (Enum.TryParse(condition.Attribute(RewriteTags.MatchPattern)?.Value, out matchType))
+ {
+ condRes.MatchType = matchType;
+ }
+
+ var parsedInputString = condition.Attribute(RewriteTags.Input)?.Value;
+ if (parsedInputString != null)
+ {
+ condRes.Input = InputParser.ParseInputString(parsedInputString);
+ }
+
+ parsedInputString = condition.Attribute(RewriteTags.Pattern)?.Value;
+
+ if (condRes.IgnoreCase)
+ {
+ condRes.MatchPattern = new Regex(parsedInputString, RegexOptions.IgnoreCase | RegexOptions.Compiled, RegexTimeout);
+ }
+ else
+ {
+ condRes.MatchPattern = new Regex(parsedInputString, RegexOptions.Compiled, RegexTimeout);
+ }
+ return condRes;
+ }
+
+ private static UrlAction CreateUrlAction(XElement urlAction)
+ {
+ if (urlAction == null)
+ {
+ throw new FormatException("Action is a required element of a rule.");
+ }
+ var actionRes = new UrlAction();
+
+ ActionType actionType;
+ if (Enum.TryParse(urlAction.Attribute(RewriteTags.Type)?.Value, out actionType))
+ {
+ actionRes.Type = actionType;
+ }
+
+ bool parseBool;
+ if (bool.TryParse(urlAction.Attribute(RewriteTags.AppendQuery)?.Value, out parseBool))
+ {
+ actionRes.AppendQueryString = parseBool;
+ }
+
+ if (bool.TryParse(urlAction.Attribute(RewriteTags.LogRewrittenUrl)?.Value, out parseBool))
+ {
+ actionRes.LogRewrittenUrl = parseBool;
+ }
+
+ RedirectType redirectType;
+ if (Enum.TryParse(urlAction.Attribute(RewriteTags.RedirectType)?.Value, out redirectType))
+ {
+ actionRes.RedirectType = redirectType;
+ }
+
+ actionRes.Url = InputParser.ParseInputString(urlAction.Attribute(RewriteTags.Url)?.Value);
+ return actionRes;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/UrlRewriteRule.cs b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/UrlRewriteRule.cs
new file mode 100644
index 0000000000..db33219ed0
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/UrlRewrite/UrlRewriteRule.cs
@@ -0,0 +1,25 @@
+// 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.Rewrite.RuleAbstraction;
+
+namespace Microsoft.AspNetCore.Rewrite.UrlRewrite
+{
+ public class UrlRewriteRule : Rule
+ {
+ public string Name { get; set; }
+ public bool Enabled { get; set; } = true;
+ public PatternSyntax PatternSyntax { get; set; }
+ public bool StopProcessing { get; set; }
+ public InitialMatch Match { get; set; }
+ public Conditions Conditions { get; set; }
+ public UrlAction Action { get; set; }
+
+ public override RuleResult ApplyRule(UrlRewriteContext context)
+ {
+ // TODO
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/project.json b/src/Microsoft.AspNetCore.Rewrite/project.json
index e50df4e012..866c21b8a2 100644
--- a/src/Microsoft.AspNetCore.Rewrite/project.json
+++ b/src/Microsoft.AspNetCore.Rewrite/project.json
@@ -31,6 +31,7 @@
"frameworks": {
"net451": {
"frameworkAssemblies": {
+ "System.Xml": "",
"System.Xml.Linq": ""
}
},
diff --git a/test/Microsoft.AspNetCore.Rewrite.Tests/UrlRewrite/FileParserTests.cs b/test/Microsoft.AspNetCore.Rewrite.Tests/UrlRewrite/FileParserTests.cs
new file mode 100644
index 0000000000..d2f42c21a3
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Rewrite.Tests/UrlRewrite/FileParserTests.cs
@@ -0,0 +1,212 @@
+// 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.Collections.Generic;
+using System.IO;
+using System.Text.RegularExpressions;
+using Microsoft.AspNetCore.Rewrite.UrlRewrite;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Rewrite.Tests.UrlRewrite
+{
+ public class FileParserTests
+ {
+ [Fact]
+ public void RuleParse_ParseTypicalRule()
+ {
+ // arrange
+ var xml = @"
+
+
+
+
+
+
+ ";
+
+ var expected = new List();
+ expected.Add(CreateTestRule(new List(),
+ Url: "^article/([0-9]+)/([_0-9a-z-]+)",
+ name: "Rewrite to article.aspx",
+ actionType: ActionType.Rewrite,
+ pattern: "article.aspx?id={R:1}&title={R:2}"));
+
+ // act
+ var res = UrlRewriteFileParser.Parse(new StringReader(xml));
+
+ // assert
+ AssertUrlRewriteRuleEquality(res, expected);
+ }
+
+ [Fact]
+ public void RuleParse_ParseSingleRuleWithSingleCondition()
+ {
+ // arrange
+ var xml = @"
+
+
+
+
+
+
+
+
+
+ ";
+
+ var condList = new List();
+ condList.Add(new Condition
+ {
+ Input = InputParser.ParseInputString("{HTTPS}"),
+ MatchPattern = new Regex("^OFF$")
+ });
+
+ var expected = new List();
+ expected.Add(CreateTestRule(condList,
+ Url: "^article/([0-9]+)/([_0-9a-z-]+)",
+ name: "Rewrite to article.aspx",
+ actionType: ActionType.Rewrite,
+ pattern: "article.aspx?id={R:1}&title={R:2}"));
+
+ // act
+ var res = UrlRewriteFileParser.Parse(new StringReader(xml));
+
+ // assert
+ AssertUrlRewriteRuleEquality(expected, res);
+ }
+
+ [Fact]
+ public void RuleParse_ParseMultipleRules()
+ {
+ // arrange
+ var xml = @"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ";
+
+ var condList = new List();
+ condList.Add(new Condition
+ {
+ Input = InputParser.ParseInputString("{HTTPS}"),
+ MatchPattern = new Regex("^OFF$")
+ });
+
+ var expected = new List();
+ expected.Add(CreateTestRule(condList,
+ Url: "^article/([0-9]+)/([_0-9a-z-]+)",
+ name: "Rewrite to article.aspx",
+ actionType: ActionType.Rewrite,
+ pattern: "article.aspx?id={R:1}&title={R:2}"));
+ expected.Add(CreateTestRule(condList,
+ Url: "^article/([0-9]+)/([_0-9a-z-]+)",
+ name: "Rewrite to article.aspx",
+ actionType: ActionType.Redirect,
+ pattern: "article.aspx?id={R:1}&title={R:2}"));
+
+ // act
+ var res = UrlRewriteFileParser.Parse(new StringReader(xml));
+
+ // assert
+ AssertUrlRewriteRuleEquality(expected, res);
+ }
+
+ // Creates a rule with appropriate default values of the url rewrite rule.
+ private UrlRewriteRule CreateTestRule(List conditions,
+ LogicalGrouping condGrouping = LogicalGrouping.MatchAll,
+ bool condTracking = false,
+ string name = "",
+ bool enabled = true,
+ PatternSyntax patternSyntax = PatternSyntax.ECMAScript,
+ bool stopProcessing = false,
+ string Url = "",
+ bool ignoreCase = true,
+ bool negate = false,
+ ActionType actionType = ActionType.None,
+ string pattern = "",
+ bool appendQueryString = false,
+ bool rewrittenUrl = false,
+ RedirectType redirectType = RedirectType.Permanent
+ )
+ {
+ return new UrlRewriteRule
+ {
+ Action = new UrlAction
+ {
+ Url = InputParser.ParseInputString(pattern),
+ Type = actionType,
+ AppendQueryString = appendQueryString,
+ LogRewrittenUrl = rewrittenUrl,
+ RedirectType = redirectType
+ },
+ Name = name,
+ Enabled = enabled,
+ StopProcessing = stopProcessing,
+ PatternSyntax = patternSyntax,
+ Match = new InitialMatch
+ {
+ Url = new Regex(Url),
+ IgnoreCase = ignoreCase,
+ Negate = negate
+ },
+ Conditions = new Conditions
+ {
+ ConditionList = conditions,
+ MatchType = condGrouping,
+ TrackingAllCaptures = condTracking
+ }
+ };
+ }
+
+ private void AssertUrlRewriteRuleEquality(List expected, List actual)
+ {
+ Assert.Equal(expected.Count, actual.Count);
+ for (var i = 0; i < expected.Count; i++)
+ {
+ var r1 = expected[i];
+ var r2 = actual[i];
+
+ Assert.Equal(r1.Name, r2.Name);
+ Assert.Equal(r1.Enabled, r2.Enabled);
+ Assert.Equal(r1.StopProcessing, r2.StopProcessing);
+ Assert.Equal(r1.PatternSyntax, r2.PatternSyntax);
+
+ Assert.Equal(r1.Match.IgnoreCase, r2.Match.IgnoreCase);
+ Assert.Equal(r1.Match.Negate, r2.Match.Negate);
+
+ Assert.Equal(r1.Action.Type, r2.Action.Type);
+ Assert.Equal(r1.Action.AppendQueryString, r2.Action.AppendQueryString);
+ Assert.Equal(r1.Action.RedirectType, r2.Action.RedirectType);
+ Assert.Equal(r1.Action.LogRewrittenUrl, r2.Action.LogRewrittenUrl);
+
+ // TODO conditions, url pattern, initial match regex
+ Assert.Equal(r1.Conditions.MatchType, r2.Conditions.MatchType);
+ Assert.Equal(r1.Conditions.TrackingAllCaptures, r2.Conditions.TrackingAllCaptures);
+ Assert.Equal(r1.Conditions.ConditionList.Count, r2.Conditions.ConditionList.Count);
+
+ for (var j = 0; j < r1.Conditions.ConditionList.Count; j++)
+ {
+ var c1 = r1.Conditions.ConditionList[j];
+ var c2 = r2.Conditions.ConditionList[j];
+ Assert.Equal(c1.IgnoreCase, c2.IgnoreCase);
+ Assert.Equal(c1.Negate, c2.Negate);
+ Assert.Equal(c1.MatchType, c2.MatchType);
+ Assert.Equal(c1.Input.PatternSegments.Count, c2.Input.PatternSegments.Count);
+ }
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Rewrite.Tests/UrlRewrite/InputParserTests.cs b/test/Microsoft.AspNetCore.Rewrite.Tests/UrlRewrite/InputParserTests.cs
new file mode 100644
index 0000000000..037804f267
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Rewrite.Tests/UrlRewrite/InputParserTests.cs
@@ -0,0 +1,113 @@
+// 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.Linq;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Rewrite.UrlRewrite;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Rewrite.Tests.UrlRewrite
+{
+ public class InputParserTests
+ {
+ [Fact]
+ public void InputParser_ParseLiteralString()
+ {
+ var testString = "hello/hey/what";
+ var result = InputParser.ParseInputString(testString);
+ Assert.Equal(result.PatternSegments.Count, 1);
+ }
+
+ // Tests sizes of the pattern segments. These are all anonyomus lambdas, so cant check contents.
+ [Theory]
+ [InlineData("foo/bar/{R:1}/what", 3)]
+ [InlineData("foo/{R:1}", 2)]
+ [InlineData("foo/{R:1}/{C:2}", 4)]
+ [InlineData("foo/{R:1}{C:2}", 3)]
+ [InlineData("foo/", 1)]
+ public void InputParser_ParseStringWithBackReference(string testString, int expected)
+ {
+ var result = InputParser.ParseInputString(testString);
+ Assert.Equal(result.PatternSegments.Count, expected);
+ }
+
+ // Test actual evaluation of the lambdas, verifying the correct string comes from the evalation
+ [Theory]
+ [InlineData("hey/hello/what", "hey/hello/what")]
+ [InlineData("hey/{R:1}/what", "hey/foo/what")]
+ [InlineData("hey/{R:2}/what", "hey/bar/what")]
+ [InlineData("hey/{R:3}/what", "hey/baz/what")]
+ [InlineData("hey/{C:1}/what", "hey/foo/what")]
+ [InlineData("hey/{C:2}/what", "hey/bar/what")]
+ [InlineData("hey/{C:3}/what", "hey/baz/what")]
+ [InlineData("hey/{R:1}/{C:1}", "hey/foo/foo")]
+ public void EvaluateBackReferenceRule(string testString, string expected)
+ {
+ var middle = InputParser.ParseInputString(testString);
+ var result = middle.Evaluate(CreateTestHttpContext(), CreateTestRuleMatch(), CreateTestCondMatch());
+ Assert.Equal(result, expected);
+ }
+
+ [Theory]
+ [InlineData("hey/{ToLower:HEY}", "hey/hey")]
+ [InlineData("hey/{ToLower:{R:1}}", "hey/foo")]
+ [InlineData("hey/{ToLower:{C:1}}", "hey/foo")]
+ [InlineData("hey/{ToLower:{C:1}/what}", "hey/foo/what")]
+ [InlineData("hey/ToLower:/what", "hey/ToLower:/what")]
+ public void EvaluatToLowerRule(string testString, string expected)
+ {
+ var middle = InputParser.ParseInputString(testString);
+ var result = middle.Evaluate(CreateTestHttpContext(), CreateTestRuleMatch(), CreateTestCondMatch());
+ Assert.Equal(result, expected);
+ }
+
+ [Theory]
+ [InlineData("hey/{UrlEncode:}", "hey/%3Chey%3E")]
+ public void EvaluatUriEncodeRule(string testString, string expected)
+ {
+ var middle = InputParser.ParseInputString(testString);
+ var result = middle.Evaluate(CreateTestHttpContext(), CreateTestRuleMatch(), CreateTestCondMatch());
+ Assert.Equal(result, expected);
+ }
+
+ [Theory]
+ [InlineData("{")]
+ [InlineData("{:}")]
+ [InlineData("{R:")]
+ [InlineData("{R:1")]
+ [InlineData("{R:A}")]
+ [InlineData("{R:10}")]
+ [InlineData("{R:-1}")]
+ [InlineData("{foo:1")]
+ [InlineData("{UrlEncode:{R:}}")]
+ [InlineData("{UrlEncode:{R:1}")]
+ public void FormatExceptionsOnBadSyntax(string testString)
+ {
+ Assert.Throws(() => InputParser.ParseInputString(testString));
+ }
+
+ private HttpContext CreateTestHttpContext()
+ {
+
+ HttpContext context = new DefaultHttpContext();
+ // TODO add fields if necessary
+ return context;
+ }
+
+ private Match CreateTestRuleMatch()
+ {
+ var match = Regex.Match("foo/bar/baz", "(.*)/(.*)/(.*)");
+ return match;
+ }
+
+ private Match CreateTestCondMatch()
+ {
+ var match = Regex.Match("foo/bar/baz", "(.*)/(.*)/(.*)");
+ return match;
+ }
+ }
+}