diff --git a/BasicMiddleware.sln b/BasicMiddleware.sln
index 5dcf6b1d8f..9ac27d55d2 100644
--- a/BasicMiddleware.sln
+++ b/BasicMiddleware.sln
@@ -1,7 +1,6 @@
-
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
-VisualStudioVersion = 14.0.24720.0
+VisualStudioVersion = 14.0.25420.1
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.HttpOverrides", "src\Microsoft.AspNetCore.HttpOverrides\Microsoft.AspNetCore.HttpOverrides.xproj", "{517308C3-B477-4B01-B461-CAB9C10B6928}"
EndProject
@@ -26,6 +25,12 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ResponseBufferingSample", "
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "HttpOverridesSample", "samples\HttpOverridesSample\HttpOverridesSample.xproj", "{7F95478D-E1D4-4A64-BA42-B041591A96EB}"
EndProject
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Rewrite", "src\Microsoft.AspNetCore.Rewrite\Microsoft.AspNetCore.Rewrite.xproj", "{0E7CA1A7-1DC3-4CE6-B9C7-1688FE1410F1}"
+EndProject
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "RewriteSample", "samples\RewriteSample\RewriteSample.xproj", "{9E049645-13BC-4598-89E1-5B43D36E5D14}"
+EndProject
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Rewrite.Tests", "test\Microsoft.AspNetCore.Rewrite.Tests\Microsoft.AspNetCore.Rewrite.Tests.xproj", "{31794F9E-A1AA-4535-B03C-A3233737CD1A}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -56,6 +61,18 @@ Global
{7F95478D-E1D4-4A64-BA42-B041591A96EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7F95478D-E1D4-4A64-BA42-B041591A96EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7F95478D-E1D4-4A64-BA42-B041591A96EB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0E7CA1A7-1DC3-4CE6-B9C7-1688FE1410F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0E7CA1A7-1DC3-4CE6-B9C7-1688FE1410F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0E7CA1A7-1DC3-4CE6-B9C7-1688FE1410F1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0E7CA1A7-1DC3-4CE6-B9C7-1688FE1410F1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9E049645-13BC-4598-89E1-5B43D36E5D14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9E049645-13BC-4598-89E1-5B43D36E5D14}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9E049645-13BC-4598-89E1-5B43D36E5D14}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9E049645-13BC-4598-89E1-5B43D36E5D14}.Release|Any CPU.Build.0 = Release|Any CPU
+ {31794F9E-A1AA-4535-B03C-A3233737CD1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {31794F9E-A1AA-4535-B03C-A3233737CD1A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {31794F9E-A1AA-4535-B03C-A3233737CD1A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {31794F9E-A1AA-4535-B03C-A3233737CD1A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -67,5 +84,8 @@ Global
{F5F1D123-9C81-4A9E-8644-AA46B8E578FB} = {8437B0F3-3894-4828-A945-A9187F37631D}
{E5C55B80-7827-40EB-B661-32B0E0E431CA} = {9587FE9F-5A17-42C4-8021-E87F59CECB98}
{7F95478D-E1D4-4A64-BA42-B041591A96EB} = {9587FE9F-5A17-42C4-8021-E87F59CECB98}
+ {0E7CA1A7-1DC3-4CE6-B9C7-1688FE1410F1} = {A5076D28-FA7E-4606-9410-FEDD0D603527}
+ {9E049645-13BC-4598-89E1-5B43D36E5D14} = {9587FE9F-5A17-42C4-8021-E87F59CECB98}
+ {31794F9E-A1AA-4535-B03C-A3233737CD1A} = {8437B0F3-3894-4828-A945-A9187F37631D}
EndGlobalSection
EndGlobal
diff --git a/samples/RewriteSample/Properties/AssemblyInfo.cs b/samples/RewriteSample/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..22fae21612
--- /dev/null
+++ b/samples/RewriteSample/Properties/AssemblyInfo.cs
@@ -0,0 +1,19 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("RewriteSample")]
+[assembly: AssemblyTrademark("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("9e049645-13bc-4598-89e1-5b43d36e5d14")]
diff --git a/samples/RewriteSample/Rewrite.txt b/samples/RewriteSample/Rewrite.txt
new file mode 100644
index 0000000000..fdc4175f86
--- /dev/null
+++ b/samples/RewriteSample/Rewrite.txt
@@ -0,0 +1,12 @@
+# 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]
+
+# Rewrite path with additional sub directory
+RewriteRule ^(.*)$ /foo$1
+
+# Forbid a certain url from being accessed
+RewriteRule /bar - [F]
+RewriteRule /bar/ - [F]
diff --git a/samples/RewriteSample/RewriteSample.xproj b/samples/RewriteSample/RewriteSample.xproj
new file mode 100644
index 0000000000..542c81359a
--- /dev/null
+++ b/samples/RewriteSample/RewriteSample.xproj
@@ -0,0 +1,21 @@
+
+
+
+ 14.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+
+
+ 9e049645-13bc-4598-89e1-5b43d36e5d14
+ RewriteSample
+ .\obj
+ .\bin\
+ v4.5.2
+
+
+
+ 2.0
+
+
+
diff --git a/samples/RewriteSample/Startup.cs b/samples/RewriteSample/Startup.cs
new file mode 100644
index 0000000000..3c30c0e6cf
--- /dev/null
+++ b/samples/RewriteSample/Startup.cs
@@ -0,0 +1,29 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Configuration;
+using Microsoft.AspNetCore.Rewrite;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace RewriteSample
+{
+ public class Startup
+ {
+ public void Configure(IApplicationBuilder app)
+ {
+ app.UseRewriter(new UrlRewriteOptions()
+ .ImportFromModRewrite("Rewrite.txt"));
+ app.Run(context => context.Response.WriteAsync(context.Request.Path));
+ }
+
+ public static void Main(string[] args)
+ {
+ var host = new WebHostBuilder()
+ .UseKestrel()
+ .UseStartup()
+ .Build();
+
+ host.Run();
+ }
+ }
+}
diff --git a/samples/RewriteSample/project.json b/samples/RewriteSample/project.json
new file mode 100644
index 0000000000..3bca09f40e
--- /dev/null
+++ b/samples/RewriteSample/project.json
@@ -0,0 +1,32 @@
+{
+ "version": "1.1.0-*",
+ "dependencies": {
+ "Microsoft.AspNetCore.Rewrite": "1.1.0-*",
+ "Microsoft.AspNetCore.Server.Kestrel": "1.1.0-*",
+ "Microsoft.AspNetCore.StaticFiles": "1.1.0-*"
+ },
+ "buildOptions": {
+ "emitEntryPoint": true,
+ "preserveCompilationContext": true
+ },
+ "frameworks": {
+ "net451": {},
+ "netcoreapp1.0": {
+ "dependencies": {
+ "Microsoft.NETCore.App": {
+ "version": "1.0.0-*",
+ "type": "platform"
+ }
+ }
+ }
+ },
+ "publish": {
+ "exclude": [
+ "node_modules",
+ "bower_components",
+ "**.xproj",
+ "**.user",
+ "**.vspscc"
+ ]
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/Microsoft.AspNetCore.Rewrite.xproj b/src/Microsoft.AspNetCore.Rewrite/Microsoft.AspNetCore.Rewrite.xproj
new file mode 100644
index 0000000000..fbaad5c7ae
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/Microsoft.AspNetCore.Rewrite.xproj
@@ -0,0 +1,21 @@
+
+
+
+ 14.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+
+
+ 0e7ca1a7-1dc3-4ce6-b9c7-1688fe1410f1
+ Microsoft.AspNetCore.Rewrite
+ .\obj
+ .\bin\
+ v4.5.2
+
+
+
+ 2.0
+
+
+
diff --git a/src/Microsoft.AspNetCore.Rewrite/ModRewrite/Condition.cs b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/Condition.cs
new file mode 100644
index 0000000000..f3940ecaea
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/Condition.cs
@@ -0,0 +1,18 @@
+// 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.ModRewrite
+{
+ public class Condition
+ {
+ public Pattern TestStringSegments { get; }
+ public ConditionExpression ConditionExpression { get; }
+ public ConditionFlags Flags { get; }
+ public Condition(Pattern testStringSegments, ConditionExpression conditionRegex, ConditionFlags flags)
+ {
+ TestStringSegments = testStringSegments;
+ ConditionExpression = conditionRegex;
+ Flags = flags;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ConditionBuilder.cs b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ConditionBuilder.cs
new file mode 100644
index 0000000000..109b3b59e1
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ConditionBuilder.cs
@@ -0,0 +1,94 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Rewrite.ModRewrite
+{
+ public class ConditionBuilder
+ {
+ private Pattern _testString;
+ private ParsedModRewriteExpression _pce;
+ private ConditionFlags _flags;
+
+ public ConditionBuilder(string conditionString)
+ {
+ var tokens = Tokenizer.Tokenize(conditionString);
+ if (tokens.Count == 3)
+ {
+ CreateCondition(tokens[1], tokens[2], flagsString: null);
+ }
+ else if (tokens.Count == 4)
+ {
+ CreateCondition(tokens[1], tokens[2], tokens[3]);
+ }
+ else
+ {
+ throw new FormatException("Invalid number of tokens.");
+ }
+ }
+
+ public ConditionBuilder(string testString, string condition)
+ {
+ CreateCondition(testString, condition, flagsString: null);
+ }
+
+ public ConditionBuilder(string testString, string condition, string flags)
+ {
+ CreateCondition(testString, condition, flags);
+ }
+
+ public Condition Build()
+ {
+ var expression = ExpressionCreator.CreateConditionExpression(_pce, _flags);
+ return new Condition(_testString, expression, _flags);
+ }
+
+ private void CreateCondition(string testString, string condition, string flagsString)
+ {
+ _testString = ConditionTestStringParser.ParseConditionTestString(testString);
+ _pce = ConditionPatternParser.ParseActionCondition(condition);
+ _flags = FlagParser.ParseConditionFlags(flagsString);
+ }
+
+ public void SetFlag(string flag)
+ {
+ SetFlag(flag, value: null);
+ }
+
+ public void SetFlag(ConditionFlagType flag)
+ {
+ SetFlag(flag, value: null);
+ }
+
+ public void SetFlag(string flag, string value)
+ {
+ if (_flags == null)
+ {
+ _flags = new ConditionFlags();
+ }
+ _flags.SetFlag(flag, value);
+ }
+
+ public void SetFlag(ConditionFlagType flag, string value)
+ {
+ if (_flags == null)
+ {
+ _flags = new ConditionFlags();
+ }
+ _flags.SetFlag(flag, value);
+ }
+
+ public void SetFlags(string flags)
+ {
+ if (_flags == null)
+ {
+ _flags = FlagParser.ParseConditionFlags(flags);
+ }
+ else
+ {
+ FlagParser.ParseConditionFlags(flags, _flags);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ConditionExpression.cs b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ConditionExpression.cs
new file mode 100644
index 0000000000..85222ba020
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ConditionExpression.cs
@@ -0,0 +1,29 @@
+// 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.Rewrite.Operands;
+
+namespace Microsoft.AspNetCore.Rewrite.ModRewrite
+{
+ ///
+ /// Represents the ConditionPattern for a mod_rewrite rule.
+ ///
+ public class ConditionExpression
+ {
+ public Operand Operand { get; set; }
+ public bool Invert { get; set; }
+
+ ///
+ /// Checks if a condition matches the context.
+ ///
+ /// The UrlRewriteContext.
+ /// The previous condition results (for backreferences).
+ /// The testString created from the .
+ /// If the testString satisfies the condition
+ public bool? CheckConditionExpression(UrlRewriteContext context, Match previous, string testString)
+ {
+ return Operand.CheckOperation(previous, testString, context.FileProvider) ^ Invert;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ConditionFlagType.cs b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ConditionFlagType.cs
new file mode 100644
index 0000000000..e1b650e8c0
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ConditionFlagType.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.ModRewrite
+{
+ public enum ConditionFlagType
+ {
+ NoCase,
+ Or,
+ NoVary
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ConditionFlags.cs b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ConditionFlags.cs
new file mode 100644
index 0000000000..143e008403
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ConditionFlags.cs
@@ -0,0 +1,101 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Rewrite.ModRewrite
+{
+ // TODO Refactor Condition Flags and Rule Flags under base flag class
+ public class ConditionFlags
+ {
+ private IDictionary _conditionFlagLookup = new Dictionary(StringComparer.OrdinalIgnoreCase) {
+ { "nc", ConditionFlagType.NoCase},
+ { "nocase", ConditionFlagType.NoCase },
+ { "or", ConditionFlagType.Or},
+ { "ornext", ConditionFlagType.Or },
+ { "nv", ConditionFlagType.NoVary},
+ { "novary", ConditionFlagType.NoVary}
+ };
+
+ public IDictionary FlagDictionary { get; }
+
+ public ConditionFlags(IDictionary flags)
+ {
+ FlagDictionary = flags;
+ }
+
+ public ConditionFlags()
+ {
+ FlagDictionary = new Dictionary();
+ }
+ public void SetFlag(string flag)
+ {
+ SetFlag(flag, null);
+ }
+
+ public void SetFlag(string flag, string value)
+ {
+ ConditionFlagType res;
+ if (!_conditionFlagLookup.TryGetValue(flag, out res))
+ {
+ throw new ArgumentException("Invalid flag");
+ }
+ SetFlag(res, value);
+ }
+
+ public void SetFlag(ConditionFlagType flag, string value)
+ {
+ if (value == null)
+ {
+ value = string.Empty;
+ }
+ FlagDictionary[flag] = value;
+ }
+
+ public string GetFlag(ConditionFlagType flag)
+ {
+ CleanupResources();
+ string res;
+ if (!FlagDictionary.TryGetValue(flag, out res))
+ {
+ return null;
+ }
+ return res;
+ }
+
+ public string this[ConditionFlagType flag]
+ {
+ get
+ {
+ string res;
+ if (!FlagDictionary.TryGetValue(flag, out res))
+ {
+ return null;
+ }
+ return res;
+ }
+ set
+ {
+ FlagDictionary[flag] = value ?? string.Empty;
+ }
+ }
+
+ public bool HasFlag(ConditionFlagType flag)
+ {
+ CleanupResources();
+ string res;
+ return FlagDictionary.TryGetValue(flag, out res);
+ }
+
+ // If this method is called, all flags have been processed,
+ // therefore to clean up memory, delete dictionary.
+ private void CleanupResources()
+ {
+ if (_conditionFlagLookup != null)
+ {
+ _conditionFlagLookup = null;
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ConditionPatternParser.cs b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ConditionPatternParser.cs
new file mode 100644
index 0000000000..24cd7e67c5
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ConditionPatternParser.cs
@@ -0,0 +1,230 @@
+// 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.Internal;
+
+namespace Microsoft.AspNetCore.Rewrite.ModRewrite
+{
+ ///
+ /// Parses the "CondPattern" portion of the RewriteCond.
+ /// RewriteCond TestString CondPattern
+ ///
+ public static class ConditionPatternParser
+ {
+ private const char Not = '!';
+ private const char Dash = '-';
+ private const char Less = '<';
+ private const char Greater = '>';
+ private const char EqualSign = '=';
+
+ ///
+ /// Given a CondPattern, create a ParsedConditionExpression, containing the type of operation
+ /// and value.
+ /// ParsedConditionExpression is an intermediary object, which will be made into a ConditionExpression
+ /// once the flags are parsed.
+ ///
+ /// The CondPattern portion of a mod_rewrite RewriteCond.
+ /// A new parsed condition.
+ public static ParsedModRewriteExpression ParseActionCondition(string condition)
+ {
+ if (condition == null)
+ {
+ condition = string.Empty;
+ }
+ var context = new ParserContext(condition);
+ var results = new ParsedModRewriteExpression();
+ if (!context.Next())
+ {
+ throw new FormatException(context.Error());
+ }
+
+ // If we hit a !, make sure the condition is inverted when resolving the string
+ if (context.Current == Not)
+ {
+ results.Invert = true;
+ if (!context.Next())
+ {
+ throw new FormatException(context.Error());
+ }
+ }
+
+ // Control Block for strings. Set the operation and type fields based on the sign
+ switch (context.Current)
+ {
+ case Greater:
+ if (!context.Next())
+ {
+ // Dangling ">"
+ throw new FormatException(context.Error());
+ }
+ if (context.Current == EqualSign)
+ {
+ if (!context.Next())
+ {
+ // Dangling ">="
+ throw new FormatException(context.Error());
+ }
+ results.Operation = OperationType.GreaterEqual;
+ results.Type = ConditionType.StringComp;
+ }
+ else
+ {
+ results.Operation = OperationType.Greater;
+ results.Type = ConditionType.StringComp;
+ }
+ break;
+ case Less:
+ if (!context.Next())
+ {
+ // Dangling "<"
+ throw new FormatException(context.Error());
+ }
+ if (context.Current == EqualSign)
+ {
+ if (!context.Next())
+ {
+ // Dangling "<="
+ throw new FormatException(context.Error());
+ }
+ results.Operation = OperationType.LessEqual;
+ results.Type = ConditionType.StringComp;
+ }
+ else
+ {
+ results.Operation = OperationType.Less;
+ results.Type = ConditionType.StringComp;
+ }
+ break;
+ case EqualSign:
+ if (!context.Next())
+ {
+ // Dangling "="
+ throw new FormatException(context.Error());
+ }
+ results.Operation = OperationType.Equal;
+ results.Type = ConditionType.StringComp;
+ break;
+ case Dash:
+ results = ParseProperty(context, results.Invert);
+ if (results.Type == ConditionType.PropertyTest)
+ {
+ return results;
+ }
+ context.Next();
+ break;
+ default:
+ results.Type = ConditionType.Regex;
+ break;
+ }
+
+ // Capture the rest of the string guarantee validity.
+ results.Operand = (condition.Substring(context.GetIndex()));
+ if (IsValidActionCondition(results))
+ {
+ return results;
+ }
+ else
+ {
+ throw new FormatException(context.Error());
+ }
+ }
+
+ ///
+ /// Given that the current index is a property (ex checks for directory or regular files), create a
+ /// new ParsedConditionExpression with the appropriate property operation.
+ ///
+ ///
+ ///
+ ///
+ public static ParsedModRewriteExpression ParseProperty(ParserContext context, bool invert)
+ {
+ if (!context.Next())
+ {
+ throw new FormatException(context.Error());
+ }
+ switch (context.Current)
+ {
+ case 'd':
+ return new ParsedModRewriteExpression(invert, ConditionType.PropertyTest, OperationType.Directory, null);
+ case 'f':
+ return new ParsedModRewriteExpression(invert, ConditionType.PropertyTest, OperationType.RegularFile, null);
+ case 'F':
+ return new ParsedModRewriteExpression(invert, ConditionType.PropertyTest, OperationType.ExistingFile, null);
+ case 'h':
+ case 'L':
+ return new ParsedModRewriteExpression(invert, ConditionType.PropertyTest, OperationType.SymbolicLink, null);
+ case 's':
+ return new ParsedModRewriteExpression(invert, ConditionType.PropertyTest, OperationType.Size, null);
+ case 'U':
+ return new ParsedModRewriteExpression(invert, ConditionType.PropertyTest, OperationType.ExistingUrl, null);
+ case 'x':
+ return new ParsedModRewriteExpression(invert, ConditionType.PropertyTest, OperationType.Executable, null);
+ case 'e':
+
+ if (!context.Next() || context.Current != 'q')
+ {
+ // Illegal statement.
+ throw new FormatException(context.Error());
+ }
+ return new ParsedModRewriteExpression(invert, ConditionType.IntComp, OperationType.Equal, null);
+ case 'g':
+ if (!context.Next())
+ {
+ throw new FormatException(context.Error());
+ }
+ if (context.Current == 't')
+ {
+ return new ParsedModRewriteExpression(invert, ConditionType.IntComp, OperationType.Greater, null);
+ }
+ else if (context.Current == 'e')
+ {
+ return new ParsedModRewriteExpression(invert, ConditionType.IntComp, OperationType.GreaterEqual, null);
+ }
+ else
+ {
+ throw new FormatException(context.Error());
+ }
+ case 'l':
+ if (!context.Next())
+ {
+ return new ParsedModRewriteExpression(invert, ConditionType.PropertyTest, OperationType.SymbolicLink, null);
+ }
+ if (context.Current == 't')
+ {
+ return new ParsedModRewriteExpression(invert, ConditionType.IntComp, OperationType.Less, null);
+ }
+ else if (context.Current == 'e')
+ {
+ return new ParsedModRewriteExpression(invert, ConditionType.IntComp, OperationType.LessEqual, null);
+ }
+ else
+ {
+ throw new FormatException(context.Error());
+ }
+ case 'n':
+ if (!context.Next() || context.Current != 'e')
+ {
+ throw new FormatException(context.Error());
+ }
+ return new ParsedModRewriteExpression(invert, ConditionType.IntComp, OperationType.NotEqual, null);
+ default:
+ throw new FormatException(context.Error());
+ }
+ }
+
+ private static bool IsValidActionCondition(ParsedModRewriteExpression results)
+ {
+ if (results.Type == ConditionType.IntComp)
+ {
+ // If the type is an integer, verify operand is actually an int
+ int res;
+ if (!int.TryParse(results.Operand, out res))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ConditionTestStringParser.cs b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ConditionTestStringParser.cs
new file mode 100644
index 0000000000..04b16b649e
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ConditionTestStringParser.cs
@@ -0,0 +1,205 @@
+// 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 Microsoft.AspNetCore.Rewrite.Internal;
+
+namespace Microsoft.AspNetCore.Rewrite.ModRewrite
+{
+ ///
+ /// Parses the TestString segment of the mod_rewrite condition.
+ ///
+ public class ConditionTestStringParser
+ {
+ private const char Percent = '%';
+ private const char Dollar = '$';
+ private const char Space = ' ';
+ 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 pattern. Can contain server variables, back references, etc.
+ ///
+ /// The test string portion of the RewriteCond
+ /// Examples:
+ /// %{REMOTE_ADDR}
+ /// /var/www/%{REQUEST_URI}
+ /// %1
+ /// $1
+ /// A new , containing a list of
+ public static Pattern ParseConditionTestString(string testString)
+ {
+ if (testString == null)
+ {
+ testString = string.Empty;
+ }
+ var context = new ParserContext(testString);
+ var results = new List();
+ while (context.Next())
+ {
+ if (context.Current == Percent)
+ {
+ // This is a server parameter, parse for a condition variable
+ if (!context.Next())
+ {
+ throw new FormatException(context.Error());
+ }
+ if (!ParseConditionParameter(context, results))
+ {
+ throw new FormatException(context.Error());
+ }
+ }
+ else if (context.Current == Dollar)
+ {
+ // This is a parameter from the rule, verify that it is a number from 0 to 9 directly after it
+ // and create a new Pattern Segment.
+ if (!context.Next())
+ {
+ throw new FormatException(context.Error());
+ }
+ context.Mark();
+ if (context.Current >= '0' && context.Current <= '9')
+ {
+ context.Next();
+ var ruleVariable = context.Capture();
+ context.Back();
+ results.Add(new PatternSegment(ruleVariable, SegmentType.RuleParameter));
+ }
+ else
+ {
+ throw new FormatException(context.Error());
+ }
+ }
+ else
+ {
+ // Parse for literals, which will return on either the end of the test string
+ // or when it hits a special character
+ if (!ParseLiteral(context, results))
+ {
+ throw new FormatException(context.Error());
+ }
+ }
+ }
+ return new Pattern(results);
+ }
+
+ ///
+ /// Obtains the condition parameter, which could either be a condition variable or a
+ /// server variable. Assumes the current character is immediately after the '%'.
+ /// context, on return will be on the last character of variable captured, such that after
+ /// Next() is called, it will be on the character immediately after the condition parameter.
+ ///
+ /// The ParserContext
+ /// The List of results which the new condition parameter will be added.
+ /// true
+ private static bool ParseConditionParameter(ParserContext context, List results)
+ {
+ // Parse { }
+ if (context.Current == OpenBrace)
+ {
+ // Start of a server variable
+ if (!context.Next())
+ {
+ // Dangling {
+ return false;
+ }
+ context.Mark();
+ while (context.Current != CloseBrace)
+ {
+ if (!context.Next())
+ {
+ // No closing } for the server variable
+ return false;
+ }
+ else if (context.Current == Colon)
+ {
+ // Have a segmented look up Ex: HTTP:xxxx
+ // TODO
+ }
+ }
+
+ // Need to verify server variable captured exists
+ var rawServerVariable = context.Capture();
+ if (IsValidServerVariable(rawServerVariable))
+ {
+ results.Add(new PatternSegment(rawServerVariable, SegmentType.ServerParameter));
+ }
+ else
+ {
+ // invalid.
+ return false;
+ }
+ }
+ else if (context.Current >= '0' && context.Current <= '9')
+ {
+ // means we have a segmented lookup
+ // store information in the testString result to know what to look up.
+ context.Mark();
+ context.Next();
+ var rawConditionParameter = context.Capture();
+ // Once we leave this method, the while loop will call next again. Because
+ // capture is exclusive, we need to go one past the end index, capture, and then go back.
+ context.Back();
+ results.Add(new PatternSegment(rawConditionParameter, SegmentType.ConditionParameter));
+ }
+ else
+ {
+ // illegal escape of a character
+ return false;
+ }
+ return true;
+ }
+
+ ///
+ /// Parse a string literal in the test string. Continues capturing until the start of a new variable type.
+ ///
+ ///
+ ///
+ ///
+ private static bool ParseLiteral(ParserContext context, List results)
+ {
+ context.Mark();
+ string literal;
+ while (true)
+ {
+ if (context.Current == Percent || context.Current == Dollar)
+ {
+ literal = context.Capture();
+ context.Back();
+ break;
+ }
+ if (!context.Next())
+ {
+ literal = context.Capture();
+ break;
+ }
+ }
+
+ if (IsValidLiteral(context, literal))
+ {
+ // add results
+ results.Add(new PatternSegment(literal, SegmentType.Literal));
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ private static bool IsValidLiteral(ParserContext context, string literal)
+ {
+ // TODO Once escape characters are discussed, figure this out.
+ return true;
+ }
+
+ private static bool IsValidServerVariable(string variable)
+ {
+ // TODO Once escape characters are discussed, figure this out.
+ return ServerVariables.ValidServerVariables.Contains(variable);
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ConditionType.cs b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ConditionType.cs
new file mode 100644
index 0000000000..e30325a8e5
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ConditionType.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.ModRewrite
+{
+ public enum ConditionType
+ {
+ Regex,
+ PropertyTest,
+ StringComp,
+ IntComp
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ExpressionCreator.cs b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ExpressionCreator.cs
new file mode 100644
index 0000000000..886ccea5a1
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ExpressionCreator.cs
@@ -0,0 +1,128 @@
+// 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.Rewrite.Operands;
+using Microsoft.AspNetCore.Rewrite.RuleAbstraction;
+
+namespace Microsoft.AspNetCore.Rewrite.ModRewrite
+{
+ ///
+ /// Converts a parsed expression into a mod_rewrite condition.
+ ///
+ public class ExpressionCreator
+ {
+ private static readonly TimeSpan RegexTimeout = TimeSpan.FromMilliseconds(1);
+ public static ConditionExpression CreateConditionExpression(ParsedModRewriteExpression pce, ConditionFlags flags)
+ {
+ var condExp = new ConditionExpression();
+ condExp.Invert = pce.Invert;
+ if (pce.Type == ConditionType.Regex)
+ {
+ // TODO make nullable?
+ if (flags != null && flags.HasFlag(ConditionFlagType.NoCase))
+ {
+ condExp.Operand = new RegexOperand(new Regex(pce.Operand, RegexOptions.IgnoreCase | RegexOptions.Compiled, RegexTimeout));
+ }
+ else
+ {
+ condExp.Operand = new RegexOperand(new Regex(pce.Operand, RegexOptions.Compiled, RegexTimeout));
+ }
+ }
+ else if (pce.Type == ConditionType.IntComp)
+ {
+ switch (pce.Operation)
+ {
+ case OperationType.Equal:
+ condExp.Operand = new IntegerOperand(pce.Operand, IntegerOperationType.Equal);
+ break;
+ case OperationType.Greater:
+ condExp.Operand = new IntegerOperand(pce.Operand, IntegerOperationType.Greater);
+ break;
+ case OperationType.GreaterEqual:
+ condExp.Operand = new IntegerOperand(pce.Operand, IntegerOperationType.GreaterEqual);
+ break;
+ case OperationType.Less:
+ condExp.Operand = new IntegerOperand(pce.Operand, IntegerOperationType.Less);
+ break;
+ case OperationType.LessEqual:
+ condExp.Operand = new IntegerOperand(pce.Operand, IntegerOperationType.LessEqual);
+ break;
+ case OperationType.NotEqual:
+ condExp.Operand = new IntegerOperand(pce.Operand, IntegerOperationType.NotEqual);
+ break;
+ default:
+ throw new ArgumentException("No defined operation for integer comparison.");
+ }
+ }
+ else if (pce.Type == ConditionType.StringComp)
+ {
+ switch (pce.Operation)
+ {
+ case OperationType.Equal:
+ condExp.Operand = new StringOperand(pce.Operand, StringOperationType.Equal);
+ break;
+ case OperationType.Greater:
+ condExp.Operand = new StringOperand(pce.Operand, StringOperationType.Greater);
+ break;
+ case OperationType.GreaterEqual:
+ condExp.Operand = new StringOperand(pce.Operand, StringOperationType.GreaterEqual);
+ break;
+ case OperationType.Less:
+ condExp.Operand = new StringOperand(pce.Operand, StringOperationType.Less);
+ break;
+ case OperationType.LessEqual:
+ condExp.Operand = new StringOperand(pce.Operand, StringOperationType.LessEqual);
+ break;
+ default:
+ throw new ArgumentException("No defined operation for string comparison.");
+ }
+ }
+ else
+ {
+ switch (pce.Operation)
+ {
+ case OperationType.Directory:
+ condExp.Operand = new PropertyOperand(PropertyOperationType.Directory);
+ break;
+ case OperationType.RegularFile:
+ condExp.Operand = new PropertyOperand(PropertyOperationType.RegularFile);
+ break;
+ case OperationType.ExistingFile:
+ condExp.Operand = new PropertyOperand(PropertyOperationType.ExistingFile);
+ break;
+ case OperationType.SymbolicLink:
+ condExp.Operand = new PropertyOperand(PropertyOperationType.SymbolicLink);
+ break;
+ case OperationType.Size:
+ condExp.Operand = new PropertyOperand(PropertyOperationType.Size);
+ break;
+ case OperationType.ExistingUrl:
+ condExp.Operand = new PropertyOperand(PropertyOperationType.ExistingUrl);
+ break;
+ case OperationType.Executable:
+ condExp.Operand = new PropertyOperand(PropertyOperationType.Executable);
+ break;
+ default:
+ throw new ArgumentException("No defined operation for property comparison.");
+ }
+ }
+ return condExp;
+ }
+ public static RuleExpression CreateRuleExpression(ParsedModRewriteExpression pce, RuleFlags flags)
+ {
+ var ruleExp = new RuleExpression();
+ ruleExp.Invert = pce.Invert;
+ if (flags.HasFlag(RuleFlagType.NoCase))
+ {
+ ruleExp.Operand = new RegexOperand(new Regex(pce.Operand, RegexOptions.IgnoreCase | RegexOptions.Compiled, RegexTimeout));
+ }
+ else
+ {
+ ruleExp.Operand = new RegexOperand(new Regex(pce.Operand, RegexOptions.Compiled, RegexTimeout));
+ }
+ return ruleExp;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/ModRewrite/FileParser.cs b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/FileParser.cs
new file mode 100644
index 0000000000..201fba8834
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/FileParser.cs
@@ -0,0 +1,103 @@
+// 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 Microsoft.AspNetCore.Rewrite.RuleAbstraction;
+
+namespace Microsoft.AspNetCore.Rewrite.ModRewrite
+{
+ ///
+ ///
+ ///
+ public static class FileParser
+ {
+ public static List Parse(TextReader input)
+ {
+ string line = null;
+ var rules = new List();
+ var conditions = new List();
+ // TODO consider passing Itokenizer and Ifileparser and provide implementations
+ while ((line = input.ReadLine()) != null)
+ {
+ if (string.IsNullOrEmpty(line))
+ {
+ continue;
+ }
+ if (line.StartsWith("#"))
+ {
+ continue;
+ }
+ var tokens = Tokenizer.Tokenize(line);
+ if (tokens.Count > 4)
+ {
+ // This means the line didn't have an appropriate format, throw format exception
+ throw new FormatException();
+ }
+ // TODO make a new class called rule parser that does and either return an exception or return the rule.
+ switch (tokens[0])
+ {
+ case "RewriteBase":
+ throw new NotSupportedException();
+ //if (tokens.Count == 2)
+ //{
+ // ModRewriteBase.Base = tokens[1];
+ //}
+ //else
+ //{
+ // throw new FormatException("");
+ //}
+ //break;
+ case "RewriteCond":
+ {
+ ConditionBuilder builder = null;
+ if (tokens.Count == 3)
+ {
+ builder = new ConditionBuilder(tokens[1], tokens[2]);
+ }
+ else if (tokens.Count == 4)
+ {
+ builder = new ConditionBuilder(tokens[1], tokens[2], tokens[3]);
+ }
+ else
+ {
+ throw new FormatException();
+ }
+ conditions.Add(builder.Build());
+ break;
+ }
+ case "RewriteRule":
+ {
+ RuleBuilder builder = null;
+ if (tokens.Count == 3)
+ {
+ builder = new RuleBuilder(tokens[1], tokens[2]);
+ }
+ else if (tokens.Count == 4)
+ {
+ builder = new RuleBuilder(tokens[1], tokens[2], tokens[3]);
+ }
+ else
+ {
+ throw new FormatException();
+ }
+ builder.AddConditions(conditions);
+ rules.Add(builder.Build());
+ conditions = new List();
+ break;
+ }
+ case "RewriteMap":
+ throw new NotImplementedException("RewriteMaps to be added soon.");
+ case "RewriteEngine":
+ // Explicitly do nothing here, no notion of turning on regex engine.
+ break;
+ default:
+ throw new FormatException(tokens[0]);
+ }
+ }
+ return rules;
+ }
+
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/ModRewrite/FlagParser.cs b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/FlagParser.cs
new file mode 100644
index 0000000000..b96de6e81a
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/FlagParser.cs
@@ -0,0 +1,103 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Rewrite.ModRewrite
+{
+ ///
+ /// Parses the flags
+ ///
+ public class FlagParser
+ {
+ // TODO Refactor Rule and Condition Flags under IFlags
+ public static RuleFlags ParseRuleFlags(string flagString)
+ {
+ var flags = new RuleFlags();
+ ParseRuleFlags(flagString, flags);
+ return flags;
+ }
+
+ public static void ParseRuleFlags(string flagString, RuleFlags flags)
+ {
+ if (string.IsNullOrEmpty(flagString))
+ {
+ return;
+ }
+ // Check that flags are contained within []
+ if (!flagString.StartsWith("[") || !flagString.EndsWith("]"))
+ {
+ throw new FormatException();
+ }
+ // Illegal syntax to have any spaces.
+ var tokens = flagString.Substring(1, flagString.Length - 2).Split(',');
+ // Go through tokens and verify they have meaning.
+ // Flags can be KVPs, delimited by '='.
+ foreach (string token in tokens)
+ {
+ if (string.IsNullOrEmpty(token))
+ {
+ continue;
+ }
+ string[] kvp = token.Split('=');
+ if (kvp.Length == 1)
+ {
+ flags.SetFlag(kvp[0], null);
+ }
+ else if (kvp.Length == 2)
+ {
+ flags.SetFlag(kvp[0], kvp[1]);
+ }
+ else
+ {
+ throw new FormatException();
+ }
+ }
+ }
+
+ public static ConditionFlags ParseConditionFlags(string flagString)
+ {
+ var flags = new ConditionFlags();
+ ParseConditionFlags(flagString, flags);
+ return flags;
+ }
+
+ public static void ParseConditionFlags(string flagString, ConditionFlags flags)
+ {
+ if (string.IsNullOrEmpty(flagString))
+ {
+ return;
+ }
+ // Check that flags are contained within []
+ if (!flagString.StartsWith("[") || !flagString.EndsWith("]"))
+ {
+ throw new FormatException();
+ }
+ // Lexing esque step to split all flags.
+ // Illegal syntax to have any spaces.
+ var tokens = flagString.Substring(1, flagString.Length - 2).Split(',');
+ // Go through tokens and verify they have meaning.
+ // Flags can be KVPs, delimited by '='.
+ foreach (string token in tokens)
+ {
+ if (string.IsNullOrEmpty(token))
+ {
+ continue;
+ }
+ string[] kvp = token.Split('=');
+ if (kvp.Length == 1)
+ {
+ flags.SetFlag(kvp[0], null);
+ }
+ else if (kvp.Length == 2)
+ {
+ flags.SetFlag(kvp[0], kvp[1]);
+ }
+ else
+ {
+ throw new FormatException();
+ }
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ModRewriteExtensions.cs b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ModRewriteExtensions.cs
new file mode 100644
index 0000000000..445ecde591
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ModRewriteExtensions.cs
@@ -0,0 +1,73 @@
+// 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.Rewrite.ModRewrite;
+
+namespace Microsoft.AspNetCore.Rewrite
+{
+ public static class ModRewriteExtensions
+ {
+ ///
+ /// 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)
+ {
+ // TODO use IHostingEnvironment as param.
+ if (string.IsNullOrEmpty(filePath))
+ {
+ throw new ArgumentException(nameof(filePath));
+ }
+ // TODO IHosting to fix!
+
+ using (var stream = File.OpenRead(filePath))
+ {
+ options.Rules.AddRange(FileParser.Parse(new StreamReader(stream)));
+ };
+ return options;
+ }
+
+ ///
+ /// Imports rules from a mod_rewrite file and adds the rules to current rules.
+ ///
+ /// The UrlRewrite options.
+ /// Text reader containing a stream of mod_rewrite rules.
+ public static UrlRewriteOptions ImportFromModRewrite(this UrlRewriteOptions options, TextReader reader)
+ {
+ options.Rules.AddRange(FileParser.Parse(reader));
+ return options;
+ }
+
+ ///
+ /// Adds a mod_rewrite rule to the current rules.
+ /// Additional properties (conditions, flags) for the rule can be added through the action.
+ ///
+ /// The UrlRewrite options.
+ /// The literal string of a mod_rewrite rule:
+ /// "RewriteRule Pattern Substitution [Flags]"
+ /// Action to perform on the
+ public static UrlRewriteOptions AddModRewriteRule(this UrlRewriteOptions options, string rule, Action action)
+ {
+ var builder = new RuleBuilder(rule);
+ action(builder);
+ options.Rules.Add(builder.Build());
+ return options;
+ }
+
+ ///
+ /// Adds a mod_rewrite rule to the current rules.
+ ///
+ /// The UrlRewrite options.
+ /// The literal string of a mod_rewrite rule:
+ /// "RewriteRule Pattern Substitution [Flags]"
+ public static UrlRewriteOptions AddModRewriteRule(this UrlRewriteOptions options, string rule)
+ {
+ var builder = new RuleBuilder(rule);
+ options.Rules.Add(builder.Build());
+ return options;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ModRewriteRule.cs b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ModRewriteRule.cs
new file mode 100644
index 0000000000..364fbf7275
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ModRewriteRule.cs
@@ -0,0 +1,209 @@
+// 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 Microsoft.Net.Http.Headers;
+using System.Text.RegularExpressions;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Rewrite.RuleAbstraction;
+
+namespace Microsoft.AspNetCore.Rewrite.ModRewrite
+{
+ public class ModRewriteRule : Rule
+ {
+ public List Conditions { get; set; } = new List();
+ public string Description { get; set; } = string.Empty;
+ public RuleExpression InitialRule { get; set; }
+ public Pattern Transform { get; set; }
+ public RuleFlags Flags { get; set; } = new RuleFlags();
+ public ModRewriteRule() { }
+
+ public ModRewriteRule(List conditions, RuleExpression initialRule, Pattern transforms, RuleFlags flags, string description = "")
+ {
+ Conditions = conditions;
+ InitialRule = initialRule;
+ Transform = transforms;
+ Flags = flags;
+ Description = description;
+ }
+
+ public override RuleResult ApplyRule(UrlRewriteContext context)
+ {
+ // 1. Figure out which section of the string to match for the initial rule.
+ var results = InitialRule.Operand.RegexOperation.Match(context.HttpContext.Request.Path.ToString());
+
+ string flagRes = null;
+ if (CheckMatchResult(results.Success))
+ {
+ return new RuleResult { Result = RuleTerminiation.Continue };
+ }
+
+ if (Flags.HasFlag(RuleFlagType.EscapeBackreference))
+ {
+ // TODO Escape Backreferences here.
+ }
+
+ // 2. Go through all conditions and compare them to the created string
+ var previous = Match.Empty;
+
+ if (!CheckCondition(context, results, previous))
+ {
+ return new RuleResult { Result = RuleTerminiation.Continue };
+ }
+ // TODO add chained flag
+
+ // at this point, our rule passed, we can now apply the on match function
+ var result = Transform.GetPattern(context.HttpContext, results, previous);
+
+ if (Flags.HasFlag(RuleFlagType.QSDiscard))
+ {
+ context.HttpContext.Request.QueryString = new QueryString();
+ }
+
+ if ((flagRes = Flags.GetValue(RuleFlagType.Cookie)) != null)
+ {
+ // TODO CreateCookies(context);
+ // context.HttpContext.Response.Cookies.Append()
+ // Make sure this in on compile.
+ }
+
+ if ((flagRes = Flags.GetValue(RuleFlagType.Env)) != null)
+ {
+ // TODO CreateEnv(context)
+ // context.HttpContext...
+ }
+
+ if ((flagRes = Flags.GetValue(RuleFlagType.Next)) != null)
+ {
+ // TODO Next flag
+ }
+
+ if (Flags.HasFlag(RuleFlagType.Forbidden))
+ {
+ context.HttpContext.Response.StatusCode = StatusCodes.Status403Forbidden;
+ return new RuleResult { Result = RuleTerminiation.ResponseComplete };
+ }
+ else if (Flags.HasFlag(RuleFlagType.Gone))
+ {
+ context.HttpContext.Response.StatusCode = StatusCodes.Status410Gone;
+ return new RuleResult { Result = RuleTerminiation.ResponseComplete };
+ }
+ else if (result == "-")
+ {
+ // TODO set url to result.
+ }
+ else if (Flags.HasFlag(RuleFlagType.QSAppend))
+ {
+ context.HttpContext.Request.QueryString = context.HttpContext.Request.QueryString.Add(new QueryString(result));
+ }
+
+ if ((flagRes = Flags.GetValue(RuleFlagType.Redirect)) != null)
+ {
+ int parsedInt;
+ if (!int.TryParse(flagRes, out parsedInt))
+ {
+ // TODO PERF parse the status code when the flag is parsed rather than per request
+ throw new FormatException("Trying to parse non-int in integer comparison.");
+ }
+ context.HttpContext.Response.StatusCode = parsedInt;
+ if (Flags.HasFlag(RuleFlagType.FullUrl))
+ {
+ // TODO review escaping
+ context.HttpContext.Response.Headers[HeaderNames.Location] = result;
+ }
+ else
+ {
+ // TODO str cat is bad, polish, review escaping
+ if (result.StartsWith("/"))
+ {
+ context.HttpContext.Response.Headers[HeaderNames.Location] = result + context.HttpContext.Request.QueryString;
+ }
+ else
+ {
+ context.HttpContext.Response.Headers[HeaderNames.Location] = "/" + result + context.HttpContext.Request.QueryString;
+ }
+ }
+ return new RuleResult { Result = RuleTerminiation.ResponseComplete };
+ }
+ else
+ {
+ if (Flags.HasFlag(RuleFlagType.FullUrl))
+ {
+ ModifyHttpContextFromUri(context.HttpContext, result);
+ }
+ else
+ {
+ if (result.StartsWith("/"))
+ {
+ context.HttpContext.Request.Path = new PathString(result);
+ }
+ else
+ {
+ context.HttpContext.Request.Path = new PathString("/" + result);
+ }
+ }
+ if (Flags.HasFlag(RuleFlagType.Last) || Flags.HasFlag(RuleFlagType.End))
+ {
+ return new RuleResult { Result = RuleTerminiation.StopRules };
+ }
+ else
+ {
+ return new RuleResult { Result = RuleTerminiation.Continue };
+ }
+ }
+ }
+
+ private bool CheckMatchResult(bool? result)
+ {
+ if (result == null)
+ {
+ return false;
+ }
+ return !(result.Value ^ InitialRule.Invert);
+ }
+
+ private bool CheckCondition(UrlRewriteContext context, Match results, Match previous)
+ {
+ if (Conditions == null)
+ {
+ return true;
+ }
+
+ // TODO Visitor pattern here?
+ foreach (var condition in Conditions)
+ {
+ var concatTestString = condition.TestStringSegments.GetPattern(context.HttpContext, results, previous);
+ var match = condition.ConditionExpression.CheckConditionExpression(context, previous, concatTestString);
+
+ if (match == null)
+ {
+ return false;
+ }
+
+ if (!match.Value && !(condition.Flags.HasFlag(ConditionFlagType.Or)))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private void ModifyHttpContextFromUri(HttpContext context, string uriString)
+ {
+ var uri = new Uri(uriString);
+ // TODO this is ugly, fix in later push.
+ // TODO super bad for perf, cache/locally store these and update httpcontext after all rules are applied.
+ var pathBase = PathString.FromUriComponent(uri);
+ if (!pathBase.Value.StartsWith(context.Request.PathBase))
+ {
+ // cannot distinguish between path base and path.
+ throw new NotSupportedException("Modified path base from mod_rewrite rule");
+ }
+ context.Request.Host = HostString.FromUriComponent(uri);
+ context.Request.Path = PathString.FromUriComponent(uri);
+ context.Request.QueryString = QueryString.FromUriComponent(uri);
+ context.Request.Scheme = uri.Scheme;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/ModRewrite/OperationType.cs b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/OperationType.cs
new file mode 100644
index 0000000000..8f2ec10e9b
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/OperationType.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.
+
+namespace Microsoft.AspNetCore.Rewrite.ModRewrite
+{
+ public enum OperationType
+ {
+ None,
+ Equal,
+ Greater,
+ GreaterEqual,
+ Less,
+ LessEqual,
+ NotEqual,
+ Directory,
+ RegularFile,
+ ExistingFile,
+ SymbolicLink,
+ Size,
+ ExistingUrl,
+ Executable
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ParsedConditionExpression.cs b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ParsedConditionExpression.cs
new file mode 100644
index 0000000000..b0402f9e99
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ParsedConditionExpression.cs
@@ -0,0 +1,22 @@
+// 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.ModRewrite
+{
+ public class ParsedModRewriteExpression
+ {
+ public bool Invert { get; set; }
+ public ConditionType Type { get; set; }
+ public OperationType Operation { get; set; }
+ public string Operand { get; set; }
+ public ParsedModRewriteExpression(bool invert, ConditionType type, OperationType operation, string operand)
+ {
+ Invert = invert;
+ Type = type;
+ Operation = operation;
+ Operand = operand;
+ }
+
+ public ParsedModRewriteExpression() { }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/ModRewrite/Pattern.cs b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/Pattern.cs
new file mode 100644
index 0000000000..b3070faf1d
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/Pattern.cs
@@ -0,0 +1,69 @@
+// 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;
+using System.Text.RegularExpressions;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Rewrite.ModRewrite;
+
+namespace Microsoft.AspNetCore.Rewrite
+{
+ ///
+ /// Contains a sequence of pattern segments, which on obtaining the context, will create the appropriate
+ /// test string and condition for rules and conditions.
+ ///
+ public class Pattern
+ {
+ private IReadOnlyList Segments { get; }
+ ///
+ /// Creates a new Pattern
+ ///
+ /// List of pattern segments which will be applied.
+ public Pattern(IReadOnlyList segments)
+ {
+ Segments = segments;
+ }
+
+ ///
+ /// Creates the appropriate test string from the Httpcontext and Segments.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public string GetPattern(HttpContext context, Match ruleMatch, Match prevCondition)
+ {
+ var res = new StringBuilder();
+ foreach (var segment in Segments)
+ {
+ // TODO handle case when segment.Variable is 0 in rule and condition
+ switch (segment.Type)
+ {
+ case SegmentType.Literal:
+ res.Append(segment.Variable);
+ break;
+ case SegmentType.ServerParameter:
+ res.Append(ServerVariables.Resolve(segment.Variable, context));
+ break;
+ case SegmentType.RuleParameter:
+ var ruleParam = ruleMatch.Groups[segment.Variable];
+ if (ruleParam != null)
+ {
+ res.Append(ruleParam);
+ }
+ break;
+ case SegmentType.ConditionParameter:
+ var condParam = prevCondition.Groups[segment.Variable];
+ if (condParam != null)
+ {
+ res.Append(condParam);
+ }
+ break;
+ }
+ }
+ return res.ToString();
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/ModRewrite/PatternSegment.cs b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/PatternSegment.cs
new file mode 100644
index 0000000000..13a22cb50c
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/PatternSegment.cs
@@ -0,0 +1,26 @@
+// 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
+{
+ ///
+ /// A Pattern segment contains a portion of the test string/ substitution segment with a type associated.
+ /// This type can either be: Regex, Rule Variable, Condition Variable, or a Server Variable.
+ ///
+ public class PatternSegment
+ {
+ public string Variable { get; } // TODO make this a range s.t. we don't copy the string.
+ public SegmentType Type { get; }
+
+ ///
+ /// Create a Pattern segment.
+ ///
+ ///
+ ///
+ public PatternSegment(string variable, SegmentType type)
+ {
+ Variable = variable;
+ Type = type;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/ModRewrite/RuleBuilder.cs b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/RuleBuilder.cs
new file mode 100644
index 0000000000..35530a80bd
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/RuleBuilder.cs
@@ -0,0 +1,122 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Rewrite.ModRewrite
+{
+ public class RuleBuilder
+ {
+ private ParsedModRewriteExpression _pce;
+ private List _conditions;
+ private RuleFlags _flags;
+ private Pattern _patterns;
+ public ModRewriteRule Build()
+ {
+ var ruleExpression = ExpressionCreator.CreateRuleExpression(_pce, _flags);
+ return new ModRewriteRule(_conditions, ruleExpression, _patterns, _flags);
+ }
+
+ public RuleBuilder(string initialRule, string transformation) :
+ this(initialRule, transformation, flags: null)
+ {
+ }
+ public RuleBuilder(string rule)
+ {
+ var tokens = Tokenizer.Tokenize(rule);
+ if (tokens.Count == 3)
+ {
+ CreateRule(tokens[1], tokens[2], flags: null);
+ }
+ else if (tokens.Count == 4)
+ {
+ CreateRule(tokens[1], tokens[2], tokens[3]);
+ }
+ else
+ {
+ throw new ArgumentException();
+ }
+ }
+
+ public RuleBuilder(string initialRule, string transformation, string flags)
+ {
+ CreateRule(initialRule, transformation, flags);
+ }
+
+ public void CreateRule(string initialRule, string transformation, string flags)
+ {
+ _pce = RuleRegexParser.ParseRuleRegex(initialRule);
+ _patterns = ConditionTestStringParser.ParseConditionTestString(transformation);
+ _flags = FlagParser.ParseRuleFlags(flags);
+ }
+
+ public void AddCondition(string condition)
+ {
+ if (_conditions == null)
+ {
+ _conditions = new List();
+ }
+ var condBuilder = new ConditionBuilder(condition);
+ _conditions.Add(condBuilder.Build());
+ }
+
+ public void AddCondition(Condition condition)
+ {
+ if (_conditions == null)
+ {
+ _conditions = new List();
+ }
+ _conditions.Add(condition);
+ }
+
+ public void AddConditions(List conditions)
+ {
+ if (_conditions == null)
+ {
+ _conditions = new List();
+ }
+ _conditions.AddRange(conditions);
+ }
+
+ public void SetFlag(string flag)
+ {
+ SetFlag(flag, value: null);
+ }
+
+ public void SetFlag(RuleFlagType flag)
+ {
+ SetFlag(flag, value: null);
+ }
+
+ public void SetFlag(string flag, string value)
+ {
+ if (_flags == null)
+ {
+ _flags = new RuleFlags();
+ }
+ _flags.SetFlag(flag, value);
+ }
+
+ public void SetFlag(RuleFlagType flag, string value)
+ {
+ if (_flags == null)
+ {
+ _flags = new RuleFlags();
+ }
+ _flags.SetFlag(flag, value);
+ }
+
+ public void SetFlags(string flags)
+ {
+ if (_flags == null)
+ {
+ _flags = FlagParser.ParseRuleFlags(flags);
+ }
+ else
+ {
+ FlagParser.ParseRuleFlags(flags, _flags);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/ModRewrite/RuleFlagType.cs b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/RuleFlagType.cs
new file mode 100644
index 0000000000..d67e698c39
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/RuleFlagType.cs
@@ -0,0 +1,35 @@
+// 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.ModRewrite
+{
+ public enum RuleFlagType
+ {
+ EscapeBackreference,
+ Chain,
+ Cookie,
+ DiscardPath,
+ Env,
+ End,
+ Forbidden,
+ Gone,
+ Handler,
+ Last,
+ Next,
+ NoCase,
+ NoEscape,
+ NoSubReq,
+ NoVary,
+ Or,
+ Proxy,
+ PassThrough,
+ QSAppend,
+ QSDiscard,
+ QSLast,
+ Redirect,
+ Skip,
+ Type,
+ // Non-modrewrite rule
+ FullUrl
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/ModRewrite/RuleFlags.cs b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/RuleFlags.cs
new file mode 100644
index 0000000000..58496996bd
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/RuleFlags.cs
@@ -0,0 +1,133 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Rewrite.ModRewrite
+{
+ public class RuleFlags
+ {
+ private IDictionary _ruleFlagLookup = new Dictionary(StringComparer.OrdinalIgnoreCase) {
+ { "b", RuleFlagType.EscapeBackreference},
+ { "c", RuleFlagType.Chain },
+ { "chain", RuleFlagType.Chain},
+ { "co", RuleFlagType.Cookie },
+ { "cookie", RuleFlagType.Cookie },
+ { "dpi", RuleFlagType.DiscardPath },
+ { "discardpath", RuleFlagType.DiscardPath },
+ { "e", RuleFlagType.Env},
+ { "env", RuleFlagType.Env},
+ { "end", RuleFlagType.End },
+ { "f", RuleFlagType.Forbidden },
+ { "forbidden", RuleFlagType.Forbidden },
+ { "g", RuleFlagType.Gone },
+ { "gone", RuleFlagType.Gone },
+ { "h", RuleFlagType.Handler },
+ { "handler", RuleFlagType.Handler },
+ { "l", RuleFlagType.Last },
+ { "last", RuleFlagType.Last },
+ { "n", RuleFlagType.Next },
+ { "next", RuleFlagType.Next },
+ { "nc", RuleFlagType.NoCase },
+ { "nocase", RuleFlagType.NoCase },
+ { "ne", RuleFlagType.NoEscape },
+ { "noescape", RuleFlagType.NoEscape },
+ { "ns", RuleFlagType.NoSubReq },
+ { "nosubreq", RuleFlagType.NoSubReq },
+ { "p", RuleFlagType.Proxy },
+ { "proxy", RuleFlagType.Proxy },
+ { "pt", RuleFlagType.PassThrough },
+ { "passthrough", RuleFlagType.PassThrough },
+ { "qsa", RuleFlagType.QSAppend },
+ { "qsappend", RuleFlagType.QSAppend },
+ { "qsd", RuleFlagType.QSDiscard },
+ { "qsdiscard", RuleFlagType.QSDiscard },
+ { "qsl", RuleFlagType.QSLast },
+ { "qslast", RuleFlagType.QSLast },
+ { "r", RuleFlagType.Redirect },
+ { "redirect", RuleFlagType.Redirect },
+ { "s", RuleFlagType.Skip },
+ { "skip", RuleFlagType.Skip },
+ { "t", RuleFlagType.Type },
+ { "type", RuleFlagType.Type },
+ // TODO make this a load bool instead of a flag for the file and rules.
+ { "u", RuleFlagType.FullUrl },
+ { "url", RuleFlagType.FullUrl }
+ };
+
+ public IDictionary FlagDictionary { get; }
+
+ public RuleFlags(IDictionary flags)
+ {
+ // TODO use ref to check dictionary equality
+ FlagDictionary = flags;
+ }
+
+ public RuleFlags()
+ {
+ FlagDictionary = new Dictionary();
+ }
+
+ public void SetFlag(string flag, string value)
+ {
+ RuleFlagType res;
+ if (!_ruleFlagLookup.TryGetValue(flag, out res))
+ {
+ throw new FormatException("Invalid flag");
+ }
+ SetFlag(res, value);
+ }
+ public void SetFlag(RuleFlagType flag, string value)
+ {
+ if (value == null)
+ {
+ value = string.Empty;
+ }
+ FlagDictionary[flag] = value;
+ }
+
+ public string GetValue(RuleFlagType flag)
+ {
+ CleanupResources();
+ string res;
+ if (!FlagDictionary.TryGetValue(flag, out res))
+ {
+ return null;
+ }
+ return res;
+ }
+
+ public string this[RuleFlagType flag]
+ {
+ get
+ {
+ string res;
+ if (!FlagDictionary.TryGetValue(flag, out res))
+ {
+ return null;
+ }
+ return res;
+ }
+ set
+ {
+ FlagDictionary[flag] = value ?? string.Empty;
+ }
+ }
+
+ public bool HasFlag(RuleFlagType flag)
+ {
+ CleanupResources();
+ string res;
+ return FlagDictionary.TryGetValue(flag, out res);
+ }
+
+ private void CleanupResources()
+ {
+ if (_ruleFlagLookup != null)
+ {
+ _ruleFlagLookup = null;
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/ModRewrite/RuleRegexParser.cs b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/RuleRegexParser.cs
new file mode 100644
index 0000000000..358eaebc15
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/RuleRegexParser.cs
@@ -0,0 +1,26 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Rewrite.ModRewrite
+{
+ public static class RuleRegexParser
+ {
+ public static ParsedModRewriteExpression ParseRuleRegex(string regex)
+ {
+ if (regex == null || regex == String.Empty)
+ {
+ throw new FormatException();
+ }
+ if (regex.StartsWith("!"))
+ {
+ return new ParsedModRewriteExpression { Invert = true, Operand = regex.Substring(1) };
+ }
+ else
+ {
+ return new ParsedModRewriteExpression { Invert = false, Operand = regex};
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/ModRewrite/SegmentType.cs b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/SegmentType.cs
new file mode 100644
index 0000000000..b8cbb3f3c3
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/SegmentType.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
+{
+ public enum SegmentType
+ {
+ Literal,
+ ServerParameter,
+ ConditionParameter,
+ RuleParameter
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ServerVariables.cs b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ServerVariables.cs
new file mode 100644
index 0000000000..421ace308f
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/ServerVariables.cs
@@ -0,0 +1,180 @@
+// 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.Globalization;
+using System.Net.Sockets;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.AspNetCore.Rewrite.ModRewrite
+{
+ ///
+ /// mod_rewrite lookups for specific string constants.
+ ///
+ public static class ServerVariables
+ {
+ public static HashSet ValidServerVariables = new HashSet()
+ {
+ "HTTP_ACCEPT",
+ "HTTP_COOKIE",
+ "HTTP_FORWARDED",
+ "HTTP_HOST",
+ "HTTP_PROXY_CONNECTION",
+ "HTTP_REFERER",
+ "HTTP_USER_AGENT",
+ "AUTH_TYPE",
+ "CONN_REMOTE_ADDR",
+ "CONTEXT_PREFIX",
+ "CONTEXT_DOCUMENT_ROOT",
+ "IPV6",
+ "PATH_INFO",
+ "QUERY_STRING",
+ "REMOTE_ADDR",
+ "REMOTE_HOST",
+ "REMOTE_IDENT",
+ "REMOTE_PORT",
+ "REMOTE_USER",
+ "REQUEST_METHOD",
+ "SCRIPT_FILENAME",
+ "DOCUMENT_ROOT",
+ "SCRIPT_GROUP",
+ "SCRIPT_USER",
+ "SERVER_ADDR",
+ "SERVER_ADMIN",
+ "SERVER_NAME",
+ "SERVER_PORT",
+ "SERVER_PROTOCOL",
+ "SERVER_SOFTWARE",
+ "TIME_YEAR",
+ "TIME_MON",
+ "TIME_DAY",
+ "TIME_HOUR",
+ "TIME_MIN",
+ "TIME_SEC",
+ "TIME_WDAY",
+ "TIME",
+ "API_VERSION",
+ "HTTPS",
+ "IS_SUBREQ",
+ "REQUEST_FILENAME",
+ "REQUEST_SCHEME",
+ "REQUEST_URI",
+ "THE_REQUEST"
+
+ };
+
+ ///
+ /// Translates mod_rewrite server variables strings to an enum of different server variables.
+ ///
+ /// The server variable string.
+ /// The HttpContext context.
+ /// The appropriate enum if the server variable exists, else ServerVariable.None
+ public static string Resolve(string variable, HttpContext context)
+ {
+ // TODO talk about perf here
+ switch (variable)
+ {
+ case "HTTP_ACCEPT":
+ return context.Request.Headers[HeaderNames.Accept];
+ case "HTTP_COOKIE":
+ return context.Request.Headers[HeaderNames.Cookie];
+ case "HTTP_FORWARDED":
+ return context.Request.Headers["Forwarded"];
+ case "HTTP_HOST":
+ return context.Request.Headers[HeaderNames.Host];
+ case "HTTP_PROXY_CONNECTION":
+ return context.Request.Headers[HeaderNames.ProxyAuthenticate];
+ case "HTTP_REFERER":
+ return context.Request.Headers[HeaderNames.Referer];
+ case "HTTP_USER_AGENT":
+ return context.Request.Headers[HeaderNames.UserAgent];
+ case "AUTH_TYPE":
+ throw new NotImplementedException();
+ case "CONN_REMOTE_ADDR":
+ return context.Connection.RemoteIpAddress?.ToString();
+ case "CONTEXT_PREFIX":
+ throw new NotImplementedException();
+ case "CONTEXT_DOCUMENT_ROOT":
+ throw new NotImplementedException();
+ case "IPV6":
+ return context.Connection.LocalIpAddress.AddressFamily == AddressFamily.InterNetworkV6 ? "on" : "off";
+ case "PATH_INFO":
+ throw new NotImplementedException();
+ case "QUERY_STRING":
+ return context.Request.QueryString.Value;
+ case "REMOTE_ADDR":
+ return context.Connection.RemoteIpAddress?.ToString();
+ case "REMOTE_HOST":
+ throw new NotImplementedException();
+ case "REMOTE_IDENT":
+ throw new NotImplementedException();
+ case "REMOTE_PORT":
+ return context.Connection.RemotePort.ToString(CultureInfo.InvariantCulture);
+ case "REMOTE_USER":
+ throw new NotImplementedException();
+ case "REQUEST_METHOD":
+ return context.Request.Method;
+ case "SCRIPT_FILENAME":
+ throw new NotImplementedException();
+ case "DOCUMENT_ROOT":
+ throw new NotImplementedException();
+ case "SCRIPT_GROUP":
+ throw new NotImplementedException();
+ case "SCRIPT_USER":
+ throw new NotImplementedException();
+ case "SERVER_ADDR":
+ return context.Connection.LocalIpAddress?.ToString();
+ case "SERVER_ADMIN":
+ throw new NotImplementedException();
+ case "SERVER_NAME":
+ throw new NotImplementedException();
+ case "SERVER_PORT":
+ return context.Connection.LocalPort.ToString(CultureInfo.InvariantCulture);
+ case "SERVER_PROTOCOL":
+ return context.Features.Get()?.Protocol;
+ case "SERVER_SOFTWARE":
+ throw new NotImplementedException();
+ case "TIME_YEAR":
+ return DateTimeOffset.UtcNow.Year.ToString(CultureInfo.InvariantCulture);
+ case "TIME_MON":
+ return DateTimeOffset.UtcNow.Month.ToString(CultureInfo.InvariantCulture);
+ case "TIME_DAY":
+ return DateTimeOffset.UtcNow.Day.ToString(CultureInfo.InvariantCulture);
+ case "TIME_HOUR":
+ return DateTimeOffset.UtcNow.Hour.ToString(CultureInfo.InvariantCulture);
+ case "TIME_MIN":
+ return DateTimeOffset.UtcNow.Minute.ToString(CultureInfo.InvariantCulture);
+ case "TIME_SEC":
+ return DateTimeOffset.UtcNow.Second.ToString(CultureInfo.InvariantCulture);
+ case "TIME_WDAY":
+ return ((int) DateTimeOffset.UtcNow.DayOfWeek).ToString(CultureInfo.InvariantCulture);
+ case "TIME":
+ return DateTimeOffset.UtcNow.ToString(CultureInfo.InvariantCulture);
+ case "API_VERSION":
+ throw new NotImplementedException();
+ case "HTTPS":
+ return context.Request.IsHttps ? "on" : "off";
+ case "HTTP2":
+ return context.Request.Scheme == "http2" ? "on" : "off";
+ case "IS_SUBREQ":
+ // TODO maybe can do this? context.Request.HttpContext ?
+ throw new NotImplementedException();
+ case "REQUEST_FILENAME":
+ return context.Request.Path.Value.Substring(1);
+ case "REQUEST_SCHEME":
+ return context.Request.Scheme;
+ case "REQUEST_URI":
+ // TODO This isn't an ideal solution. What this assumes is that all conditions don't have a leading slash before it.
+ return context.Request.Path.Value.Substring(1);
+ case "THE_REQUEST":
+ // TODO
+ throw new NotImplementedException();
+ default:
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/ModRewrite/Tokenizer.cs b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/Tokenizer.cs
new file mode 100644
index 0000000000..8d6f5c0b26
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/ModRewrite/Tokenizer.cs
@@ -0,0 +1,84 @@
+// 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 Microsoft.AspNetCore.Rewrite.Internal;
+
+namespace Microsoft.AspNetCore.Rewrite.ModRewrite
+{
+ ///
+ /// Tokenizes a mod_rewrite rule, delimited by spaces.
+ ///
+ public static class Tokenizer
+ {
+ private const char Space = ' ';
+ private const char Escape = '\\';
+ private const char Tab = '\t';
+
+ ///
+ /// Splits a string on whitespace, ignoring spaces, creating into a list of strings.
+ ///
+ /// The rule to tokenize.
+ /// A list of tokens.
+ public static List Tokenize(string rule)
+ {
+ // TODO make list of strings a reference to the original rule? (run into problems with escaped spaces).
+ // TODO handle "s and probably replace \ character with no slash.
+ if (string.IsNullOrEmpty(rule))
+ {
+ return null;
+ }
+ var context = new ParserContext(rule);
+ if (!context.Next())
+ {
+ return null;
+ }
+
+ var tokens = new List();
+ context.Mark();
+ while (true)
+ {
+ if (!context.Next())
+ {
+ // End of string. Capture.
+ break;
+ }
+ else if (context.Current == Escape)
+ {
+ // Need to progress such that the next character is not evaluated.
+ if (!context.Next())
+ {
+ // Means that a character was not escaped appropriately Ex: "foo\"
+ throw new ArgumentException();
+ }
+ }
+ else if (context.Current == Space || context.Current == Tab)
+ {
+ // time to capture!
+ // TODO: This kinda sucks, set state and skip
+ var token = context.Capture();
+ if (!string.IsNullOrEmpty(token))
+ {
+ tokens.Add(token);
+ while (context.Current == Space || context.Current == Tab)
+ {
+ if (!context.Next())
+ {
+ // At end of string, we can return at this point.
+ return tokens;
+ }
+ }
+ context.Mark();
+ }
+ }
+ }
+ var done = context.Capture();
+ if (!string.IsNullOrEmpty(done))
+ {
+ tokens.Add(done);
+ }
+ return tokens;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/Operands/IntegerOperand.cs b/src/Microsoft.AspNetCore.Rewrite/Operands/IntegerOperand.cs
new file mode 100644
index 0000000000..1d19c4a620
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/Operands/IntegerOperand.cs
@@ -0,0 +1,57 @@
+// 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.Extensions.FileProviders;
+
+namespace Microsoft.AspNetCore.Rewrite.Operands
+{
+ public class IntegerOperand : Operand
+ {
+ public int Value { get; }
+ public IntegerOperationType Operation { get; }
+ public IntegerOperand(int value, IntegerOperationType operation)
+ {
+ Value = value;
+ Operation = operation;
+ }
+
+ public IntegerOperand(string value, IntegerOperationType operation)
+ {
+ int compValue;
+ if (!int.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out compValue))
+ {
+ throw new FormatException("Syntax error for integers in comparison.");
+ }
+ Operation = operation;
+ }
+
+ public override bool? CheckOperation(Match previous, string testString, IFileProvider fileProvider)
+ {
+ int compValue;
+ if (!int.TryParse(testString, NumberStyles.None, CultureInfo.InvariantCulture, out compValue))
+ {
+ return false;
+ }
+ switch (Operation)
+ {
+ case IntegerOperationType.Equal:
+ return compValue == Value;
+ case IntegerOperationType.Greater:
+ return compValue > Value;
+ case IntegerOperationType.GreaterEqual:
+ return compValue >= Value;
+ case IntegerOperationType.Less:
+ return compValue < Value;
+ case IntegerOperationType.LessEqual:
+ return compValue <= Value;
+ case IntegerOperationType.NotEqual:
+ return compValue != Value;
+ default:
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/Operands/IntegerOperation.cs b/src/Microsoft.AspNetCore.Rewrite/Operands/IntegerOperation.cs
new file mode 100644
index 0000000000..3abd0b3e16
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/Operands/IntegerOperation.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.
+
+namespace Microsoft.AspNetCore.Rewrite.Operands
+{
+ public enum IntegerOperationType
+ {
+ Equal,
+ Greater,
+ GreaterEqual,
+ Less,
+ LessEqual,
+ NotEqual
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/Operands/Operand.cs b/src/Microsoft.AspNetCore.Rewrite/Operands/Operand.cs
new file mode 100644
index 0000000000..0562e14b7c
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/Operands/Operand.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.
+
+using System.Text.RegularExpressions;
+using Microsoft.Extensions.FileProviders;
+
+namespace Microsoft.AspNetCore.Rewrite.Operands
+{
+ public abstract class Operand
+ {
+ public abstract bool? CheckOperation(Match previous, string concatTestString, IFileProvider fileProvider);
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/Operands/PropertyOperand.cs b/src/Microsoft.AspNetCore.Rewrite/Operands/PropertyOperand.cs
new file mode 100644
index 0000000000..2637a00119
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/Operands/PropertyOperand.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.Text.RegularExpressions;
+using Microsoft.Extensions.FileProviders;
+
+namespace Microsoft.AspNetCore.Rewrite.Operands
+{
+ public class PropertyOperand : Operand
+ {
+ public PropertyOperationType Operation { get; }
+
+ public PropertyOperand(PropertyOperationType operation)
+ {
+ Operation = operation;
+ }
+ public override bool? CheckOperation(Match previous, string testString, IFileProvider fileProvider)
+ {
+ switch(Operation)
+ {
+ case PropertyOperationType.Directory:
+ return fileProvider.GetFileInfo(testString).IsDirectory;
+ case PropertyOperationType.RegularFile:
+ return fileProvider.GetFileInfo(testString).Exists;
+ case PropertyOperationType.Size:
+ var fileInfo = fileProvider.GetFileInfo(testString);
+ return fileInfo.Exists && fileInfo.Length > 0;
+ case PropertyOperationType.ExistingUrl:
+ throw new NotSupportedException("No support for internal sub requests.");
+ case PropertyOperationType.ExistingFile:
+ throw new NotSupportedException("No support for internal sub requests.");
+ case PropertyOperationType.SymbolicLink:
+ throw new NotSupportedException("No support for checking symbolic links.");
+ case PropertyOperationType.Executable:
+ throw new NotSupportedException("No support for checking executable permissions.");
+ default:
+ return false;
+ }
+ }
+ }
+
+
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/Operands/PropertyOperation.cs b/src/Microsoft.AspNetCore.Rewrite/Operands/PropertyOperation.cs
new file mode 100644
index 0000000000..4fc409e98f
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/Operands/PropertyOperation.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.
+
+namespace Microsoft.AspNetCore.Rewrite.Operands
+{
+ public enum PropertyOperationType
+ {
+ None,
+ Directory,
+ RegularFile,
+ ExistingFile,
+ SymbolicLink,
+ Size,
+ ExistingUrl,
+ Executable
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/Operands/RegexOperand.cs b/src/Microsoft.AspNetCore.Rewrite/Operands/RegexOperand.cs
new file mode 100644
index 0000000000..e9eac3b7bb
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/Operands/RegexOperand.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.Extensions.FileProviders;
+
+namespace Microsoft.AspNetCore.Rewrite.Operands
+{
+ public class RegexOperand : Operand
+ {
+ public Regex RegexOperation { get; }
+
+ public RegexOperand(Regex regex)
+ {
+ RegexOperation = regex;
+ }
+
+ public override bool? CheckOperation(Match previous, string concatTestString, IFileProvider fileProvider)
+ {
+ previous = RegexOperation.Match(concatTestString);
+ return previous.Success;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/Operands/StringOperand.cs b/src/Microsoft.AspNetCore.Rewrite/Operands/StringOperand.cs
new file mode 100644
index 0000000000..06360eb38a
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/Operands/StringOperand.cs
@@ -0,0 +1,40 @@
+// 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.Extensions.FileProviders;
+
+namespace Microsoft.AspNetCore.Rewrite.Operands
+{
+ public class StringOperand : Operand
+ {
+ public string Value { get; set; }
+ public StringOperationType Operation { get; set; }
+
+ public StringOperand(string value, StringOperationType operation)
+ {
+ Value = value;
+ Operation = operation;
+ }
+
+ public override bool? CheckOperation(Match previous, string concatTestString, IFileProvider fileProvider)
+ {
+ switch (Operation)
+ {
+ case StringOperationType.Equal:
+ return concatTestString.CompareTo(Value) == 0;
+ case StringOperationType.Greater:
+ return concatTestString.CompareTo(Value) > 0;
+ case StringOperationType.GreaterEqual:
+ return concatTestString.CompareTo(Value) >= 0;
+ case StringOperationType.Less:
+ return concatTestString.CompareTo(Value) < 0;
+ case StringOperationType.LessEqual:
+ return concatTestString.CompareTo(Value) <= 0;
+ default:
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/Operands/StringOperation.cs b/src/Microsoft.AspNetCore.Rewrite/Operands/StringOperation.cs
new file mode 100644
index 0000000000..51bc4ec48a
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/Operands/StringOperation.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.Operands
+{
+ public enum StringOperationType
+ {
+ Equal,
+ Greater,
+ GreaterEqual,
+ Less,
+ LessEqual
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/ParserContext.cs b/src/Microsoft.AspNetCore.Rewrite/ParserContext.cs
new file mode 100644
index 0000000000..34b78f5b67
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/ParserContext.cs
@@ -0,0 +1,70 @@
+// 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.Internal
+{
+ ///
+ /// Represents a string iterator, with captures.
+ ///
+ public class ParserContext
+ {
+ private readonly string _template;
+ private int _index;
+ private int? _mark;
+
+ public ParserContext(string condition)
+ {
+ _template = condition;
+ _index = -1;
+ }
+
+ public char Current
+ {
+ get { return (_index < _template.Length && _index >= 0) ? _template[_index] : (char)0; }
+ }
+
+ public bool Back()
+ {
+ return --_index >= 0;
+ }
+
+ public bool Next()
+ {
+ return ++_index < _template.Length;
+ }
+
+ public bool HasNext()
+ {
+ return (_index + 1) < _template.Length;
+ }
+
+ public void Mark()
+ {
+ _mark = _index;
+ }
+
+ public int GetIndex()
+ {
+ return _index;
+ }
+
+ public string Capture()
+ {
+ // TODO make this return a range rather than a string.
+ if (_mark.HasValue)
+ {
+ var value = _template.Substring(_mark.Value, _index - _mark.Value);
+ _mark = null;
+ return value;
+ }
+ else
+ {
+ return null;
+ }
+ }
+ public string Error()
+ {
+ return string.Format("Syntax Error at index: ", _index, " with character: ", Current);
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Rewrite/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..c2ce8b42a6
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/Properties/AssemblyInfo.cs
@@ -0,0 +1,19 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("Microsoft.AspNetCore.Rewrite")]
+[assembly: AssemblyTrademark("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("0e7ca1a7-1dc3-4ce6-b9c7-1688fe1410f1")]
diff --git a/src/Microsoft.AspNetCore.Rewrite/RuleAbstraction/FunctionalRule.cs b/src/Microsoft.AspNetCore.Rewrite/RuleAbstraction/FunctionalRule.cs
new file mode 100644
index 0000000000..073e31945d
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/RuleAbstraction/FunctionalRule.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;
+
+namespace Microsoft.AspNetCore.Rewrite.RuleAbstraction
+{
+ public class FunctionalRule : Rule
+ {
+ public Func OnApplyRule { get; set; }
+ public Transformation OnCompletion { get; set; } = Transformation.Rewrite;
+ public override RuleResult ApplyRule(UrlRewriteContext context) => OnApplyRule(context);
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Rewrite/RuleAbstraction/PathRule.cs b/src/Microsoft.AspNetCore.Rewrite/RuleAbstraction/PathRule.cs
new file mode 100644
index 0000000000..697810ef35
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/RuleAbstraction/PathRule.cs
@@ -0,0 +1,48 @@
+// 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.RuleAbstraction
+{
+ public class PathRule : Rule
+ {
+ public Regex MatchPattern { get; set; }
+ public string OnMatch { get; set; }
+ public Transformation OnCompletion { get; set; } = Transformation.Rewrite;
+ public override RuleResult ApplyRule(UrlRewriteContext context)
+ {
+ var matches = MatchPattern.Match(context.HttpContext.Request.Path);
+ if (matches.Success)
+ {
+ // New method here to translate the outgoing format string to the correct value.
+ var path = matches.Result(OnMatch);
+ if (OnCompletion == Transformation.Redirect)
+ {
+ var req = context.HttpContext.Request;
+ var newUrl = string.Concat(
+ req.Scheme,
+ "://",
+ req.PathBase,
+ path,
+ req.QueryString);
+ context.HttpContext.Response.Redirect(newUrl);
+ return new RuleResult { Result = RuleTerminiation.ResponseComplete };
+ }
+ else
+ {
+ context.HttpContext.Request.Path = path;
+ }
+ if (OnCompletion == Transformation.TerminatingRewrite)
+ {
+ return new RuleResult { Result = RuleTerminiation.StopRules };
+ }
+ else
+ {
+ return new RuleResult { Result = RuleTerminiation.Continue };
+ }
+ }
+ return new RuleResult { Result = RuleTerminiation.Continue };
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/RuleAbstraction/Rule.cs b/src/Microsoft.AspNetCore.Rewrite/RuleAbstraction/Rule.cs
new file mode 100644
index 0000000000..0b636120d2
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/RuleAbstraction/Rule.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.RuleAbstraction
+{
+ public abstract class Rule
+ {
+ public abstract RuleResult ApplyRule(UrlRewriteContext context);
+ }
+}
+
diff --git a/src/Microsoft.AspNetCore.Rewrite/RuleAbstraction/RuleExpression.cs b/src/Microsoft.AspNetCore.Rewrite/RuleAbstraction/RuleExpression.cs
new file mode 100644
index 0000000000..e14728e33c
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/RuleAbstraction/RuleExpression.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.
+
+using Microsoft.AspNetCore.Rewrite.Operands;
+
+namespace Microsoft.AspNetCore.Rewrite.RuleAbstraction
+{
+ public class RuleExpression
+ {
+ public RegexOperand Operand { get; set; }
+ public bool Invert { get; set; }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/RuleAbstraction/RuleResult.cs b/src/Microsoft.AspNetCore.Rewrite/RuleAbstraction/RuleResult.cs
new file mode 100644
index 0000000000..67aa51f583
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/RuleAbstraction/RuleResult.cs
@@ -0,0 +1,10 @@
+// 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.RuleAbstraction
+{
+ public class RuleResult
+ {
+ public RuleTerminiation Result { get; set; }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/RuleAbstraction/RuleTermination.cs b/src/Microsoft.AspNetCore.Rewrite/RuleAbstraction/RuleTermination.cs
new file mode 100644
index 0000000000..eae0a13ffa
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/RuleAbstraction/RuleTermination.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Rewrite.RuleAbstraction
+{
+ public enum RuleTerminiation
+ {
+ Continue,
+ ResponseComplete,
+ StopRules
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/RuleAbstraction/SchemeRule.cs b/src/Microsoft.AspNetCore.Rewrite/RuleAbstraction/SchemeRule.cs
new file mode 100644
index 0000000000..427a38bfc6
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/RuleAbstraction/SchemeRule.cs
@@ -0,0 +1,54 @@
+// 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;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Rewrite.RuleAbstraction
+{
+ public class SchemeRule : Rule
+ {
+ public int? SSLPort { get; set; }
+ public Transformation OnCompletion { get; set; } = Transformation.Rewrite;
+ public override RuleResult ApplyRule(UrlRewriteContext context)
+ {
+
+ // TODO this only does http to https, add more features in the future.
+ if (!context.HttpContext.Request.IsHttps)
+ {
+ var host = context.HttpContext.Request.Host;
+ if (SSLPort.HasValue && SSLPort.Value > 0)
+ {
+ // a specific SSL port is specified
+ host = new HostString(host.Host, SSLPort.Value);
+ }
+ else
+ {
+ // clear the port
+ host = new HostString(host.Host);
+ }
+
+ if ((OnCompletion != Transformation.Redirect))
+ {
+ context.HttpContext.Request.Scheme = "https";
+ context.HttpContext.Request.Host = host;
+ if (OnCompletion == Transformation.TerminatingRewrite)
+ {
+ return new RuleResult { Result = RuleTerminiation.StopRules };
+ }
+ else
+ {
+ return new RuleResult { Result = RuleTerminiation.Continue };
+ }
+ }
+
+ var req = context.HttpContext.Request;
+
+ var newUrl = new StringBuilder().Append("https://").Append(host).Append(req.PathBase).Append(req.Path).Append(req.QueryString);
+ context.HttpContext.Response.Redirect(newUrl.ToString());
+ return new RuleResult { Result = RuleTerminiation.ResponseComplete };
+ }
+ return new RuleResult { Result = RuleTerminiation.Continue }; ;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/RuleAbstraction/Transformation.cs b/src/Microsoft.AspNetCore.Rewrite/RuleAbstraction/Transformation.cs
new file mode 100644
index 0000000000..44008b89a4
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/RuleAbstraction/Transformation.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.RuleAbstraction
+{
+ public enum Transformation
+ {
+ Rewrite,
+ Redirect,
+ TerminatingRewrite
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/UrlRewriteContext.cs b/src/Microsoft.AspNetCore.Rewrite/UrlRewriteContext.cs
new file mode 100644
index 0000000000..ab97b358ef
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/UrlRewriteContext.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 Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.FileProviders;
+
+namespace Microsoft.AspNetCore.Rewrite
+{
+ ///
+ /// The UrlRewrite Context contains the HttpContext of the request and the file provider to check conditions.
+ ///
+ public class UrlRewriteContext
+ {
+ public HttpContext HttpContext { get; set; }
+ public IFileProvider FileProvider { get; set; }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/UrlRewriteExtensions.cs b/src/Microsoft.AspNetCore.Rewrite/UrlRewriteExtensions.cs
new file mode 100644
index 0000000000..2c0942f110
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/UrlRewriteExtensions.cs
@@ -0,0 +1,35 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ ///
+ /// Extension methods for the
+ ///
+ public static class UrlRewriteExtensions
+ {
+ ///
+ /// Checks if a given Url matches rules and conditions, and modifies the HttpContext on match.
+ ///
+ ///
+ /// Options for urlrewrite.
+ ///
+ public static IApplicationBuilder UseRewriter(this IApplicationBuilder app, UrlRewriteOptions options)
+ {
+ if (app == null)
+ {
+ throw new ArgumentNullException(nameof(app));
+ }
+
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+ // put middleware in pipeline
+ return app.UseMiddleware(options);
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/UrlRewriteMiddleware.cs b/src/Microsoft.AspNetCore.Rewrite/UrlRewriteMiddleware.cs
new file mode 100644
index 0000000000..deb987d647
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/UrlRewriteMiddleware.cs
@@ -0,0 +1,76 @@
+// 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.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Rewrite.RuleAbstraction;
+using Microsoft.Extensions.FileProviders;
+
+namespace Microsoft.AspNetCore.Rewrite
+{
+ ///
+ /// Represents a middleware that rewrites urls imported from mod_rewrite, UrlRewrite, and code.
+ ///
+ public class UrlRewriteMiddleware
+ {
+ private readonly RequestDelegate _next;
+ private readonly UrlRewriteOptions _options;
+ private readonly IFileProvider _fileProvider;
+
+ ///
+ /// Creates a new instance of
+ ///
+ /// The delegate representing the next middleware in the request pipeline.
+ /// The Hosting Environment.
+ /// The middleware options, containing the rules to apply.
+ public UrlRewriteMiddleware(RequestDelegate next, IHostingEnvironment hostingEnv, UrlRewriteOptions options)
+ {
+ if (next == null)
+ {
+ throw new ArgumentNullException(nameof(next));
+ }
+
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ _next = next;
+ _options = options;
+ _fileProvider = _options.FileProvider ?? hostingEnv.WebRootFileProvider;
+ }
+
+ ///
+ /// Executes the middleware.
+ ///
+ /// The for the current request.
+ /// A task that represents the execution of this middleware.
+ public Task Invoke(HttpContext context)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+ var urlContext = new UrlRewriteContext { HttpContext = context, FileProvider = _fileProvider };
+ foreach (var rule in _options.Rules)
+ {
+ // Apply the rule
+ var result = rule.ApplyRule(urlContext);
+ switch (result.Result)
+ {
+ case RuleTerminiation.Continue:
+ // Explicitly show that we continue executing rules
+ break;
+ case RuleTerminiation.ResponseComplete:
+ // TODO cache task for perf
+ return Task.FromResult(0);
+ case RuleTerminiation.StopRules:
+ return _next(context);
+ }
+ }
+ return _next(context);
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/UrlRewriteOptions.cs b/src/Microsoft.AspNetCore.Rewrite/UrlRewriteOptions.cs
new file mode 100644
index 0000000000..becea3063e
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/UrlRewriteOptions.cs
@@ -0,0 +1,21 @@
+// 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 Microsoft.AspNetCore.Rewrite.RuleAbstraction;
+using Microsoft.Extensions.FileProviders;
+
+namespace Microsoft.AspNetCore.Rewrite
+{
+ ///
+ /// Options for the
+ ///
+ public class UrlRewriteOptions
+ {
+ ///
+ /// The ordered list of rules to apply to the context.
+ ///
+ public List Rules { get; set; } = new List();
+ public IFileProvider FileProvider { get; set; }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/UrlRewriteOptionsAddRulesExtensions.cs b/src/Microsoft.AspNetCore.Rewrite/UrlRewriteOptionsAddRulesExtensions.cs
new file mode 100644
index 0000000000..3d6601baad
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/UrlRewriteOptionsAddRulesExtensions.cs
@@ -0,0 +1,108 @@
+// 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.Text.RegularExpressions;
+using Microsoft.AspNetCore.Rewrite.ModRewrite;
+using Microsoft.AspNetCore.Rewrite.RuleAbstraction;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.FileProviders;
+
+namespace Microsoft.AspNetCore.Rewrite
+{
+ ///
+ /// The builder to a list of rules for and
+ ///
+ public static class UrlRewriteOptionsAddRulesExtensions
+ {
+ ///
+ /// Adds a rule to the current rules.
+ ///
+ /// The UrlRewrite options.
+ /// A rule to be added to the current rules.
+ public static UrlRewriteOptions AddRule(this UrlRewriteOptions options, Rule rule)
+ {
+ options.Rules.Add(rule);
+ return options;
+ }
+
+ ///
+ /// Adds a list of rules to the current rules.
+ ///
+ /// The UrlRewrite options.
+ /// A list of rules.
+ public static UrlRewriteOptions AddRules(this UrlRewriteOptions options, List rules)
+ {
+ options.Rules.AddRange(rules);
+ return options;
+ }
+
+ ///
+ /// Creates a rewrite path rule.
+ ///
+ /// The Url rewrite options.
+ /// The string regex pattern to compare against the http context.
+ /// The string to replace the path with (with capture parameters).
+ /// Whether or not to stop rewriting on success of rule.
+ ///
+ public static UrlRewriteOptions RewritePath(this UrlRewriteOptions options, string regex, string newPath, bool stopRewriteOnSuccess = false)
+ {
+ options.Rules.Add(new PathRule { MatchPattern = new Regex(regex, RegexOptions.Compiled, TimeSpan.FromMilliseconds(1)), OnMatch = newPath, OnCompletion = stopRewriteOnSuccess ? Transformation.TerminatingRewrite : Transformation.Rewrite });
+ return options;
+ }
+
+ ///
+ /// Rewrite http to https.
+ ///
+ /// The Url rewrite options.
+ /// Whether or not to stop rewriting on success of rule.
+ ///
+ public static UrlRewriteOptions RewriteScheme(this UrlRewriteOptions options, bool stopRewriteOnSuccess = false)
+ {
+ options.Rules.Add(new SchemeRule {OnCompletion = stopRewriteOnSuccess ? Transformation.TerminatingRewrite : Transformation.Rewrite });
+ return options;
+ }
+
+ ///
+ /// Redirect a path to another path.
+ ///
+ /// The Url rewrite options.
+ /// The string regex pattern to compare against the http context.
+ /// The string to replace the path with (with capture parameters).
+ /// Whether or not to stop rewriting on success of rule.
+ ///
+ public static UrlRewriteOptions RedirectPath(this UrlRewriteOptions options, string regex, string newPath, bool stopRewriteOnSuccess = false)
+ {
+ options.Rules.Add(new PathRule { MatchPattern = new Regex(regex, RegexOptions.Compiled, TimeSpan.FromMilliseconds(1)), OnMatch = newPath, OnCompletion = Transformation.Redirect });
+ return options;
+ }
+
+ ///
+ /// Redirect http to https.
+ ///
+ /// The Url rewrite options.
+ /// The port to redirect the scheme to.
+ ///
+ public static UrlRewriteOptions RedirectScheme(this UrlRewriteOptions options, int? sslPort)
+ {
+ options.Rules.Add(new SchemeRule { SSLPort = sslPort, OnCompletion = Transformation.Redirect });
+ return options;
+ }
+
+ ///
+ /// User generated rule to do a specific match on a path and what to do on success of the match.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static UrlRewriteOptions CustomRule(this UrlRewriteOptions options, Func onApplyRule, Transformation transform, string description = null)
+ {
+ options.Rules.Add(new FunctionalRule { OnApplyRule = onApplyRule, OnCompletion = transform});
+ return options;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Rewrite/project.json b/src/Microsoft.AspNetCore.Rewrite/project.json
new file mode 100644
index 0000000000..e829903667
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Rewrite/project.json
@@ -0,0 +1,37 @@
+{
+ "version": "1.1.0-*",
+ "buildOptions": {
+ "warningsAsErrors": true,
+ "keyFile": "../../tools/Key.snk",
+ "nowarn": [
+ "CS1591"
+ ],
+ "xmlDoc": true
+ },
+ "description": "ASP.NET Core basic middleware for:\r\nRewrite Url module.",
+ "packOptions": {
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/aspnet/basicmiddleware"
+ },
+ "tags": [
+ "aspnetcore",
+ "proxy",
+ "headers",
+ "xforwarded"
+ ]
+ },
+ "dependencies": {
+ "Microsoft.AspNetCore.Hosting.Abstractions": "1.1.0-*",
+ "Microsoft.AspNetCore.Http.Extensions": "1.1.0-*",
+ "Microsoft.Extensions.Configuration.Abstractions": "1.1.0-*",
+ "Microsoft.Extensions.FileProviders.Physical": "1.1.0-*",
+ "Microsoft.Extensions.Logging.Abstractions": "1.1.0-*",
+ "Microsoft.Extensions.Options": "1.1.0-*",
+ "System.Text.RegularExpressions": "4.1.0-*"
+ },
+ "frameworks": {
+ "net451": {},
+ "netstandard1.3": {}
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.HttpOverrides.Tests/IPNetworkTest.cs b/test/Microsoft.AspNetCore.HttpOverrides.Tests/IPNetworkTest.cs
index b3700c583f..fa9a099057 100644
--- a/test/Microsoft.AspNetCore.HttpOverrides.Tests/IPNetworkTest.cs
+++ b/test/Microsoft.AspNetCore.HttpOverrides.Tests/IPNetworkTest.cs
@@ -1,6 +1,5 @@
// 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.Net;
using Xunit;
diff --git a/test/Microsoft.AspNetCore.Rewrite.Tests/ConditionActionTest.cs b/test/Microsoft.AspNetCore.Rewrite.Tests/ConditionActionTest.cs
new file mode 100644
index 0000000000..7f1a37f1b4
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Rewrite.Tests/ConditionActionTest.cs
@@ -0,0 +1,99 @@
+// 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 Microsoft.AspNetCore.Rewrite.ModRewrite;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Rewrite
+{
+ public class ConditionActionTest
+ {
+ [Theory]
+ [InlineData(">hey", OperationType.Greater, "hey", ConditionType.StringComp)]
+ [InlineData("=hey", OperationType.GreaterEqual, "hey", ConditionType.StringComp)]
+ [InlineData("<=hey", OperationType.LessEqual, "hey", ConditionType.StringComp)]
+ [InlineData("=hey", OperationType.Equal, "hey", ConditionType.StringComp)]
+ public void ConditionParser_CheckStringComp(string condition, OperationType operation, string variable, ConditionType conditionType)
+ {
+ var results = ConditionPatternParser.ParseActionCondition(condition);
+
+ var expected = new ParsedModRewriteExpression { Operation = operation, Type = conditionType, Operand = variable, Invert = false };
+ Assert.True(CompareConditions(results, expected));
+ }
+
+ [Fact]
+ public void ConditionParser_CheckRegexEqual()
+ {
+ var condition = @"(.*)";
+ var results = ConditionPatternParser.ParseActionCondition(condition);
+
+ var expected = new ParsedModRewriteExpression { Type = ConditionType.Regex, Operand = "(.*)", Invert = false };
+ Assert.True(CompareConditions(results, expected));
+ }
+
+ [Theory]
+ [InlineData("-d", OperationType.Directory, ConditionType.PropertyTest)]
+ [InlineData("-f", OperationType.RegularFile, ConditionType.PropertyTest)]
+ [InlineData("-F", OperationType.ExistingFile, ConditionType.PropertyTest)]
+ [InlineData("-h", OperationType.SymbolicLink, ConditionType.PropertyTest)]
+ [InlineData("-L", OperationType.SymbolicLink, ConditionType.PropertyTest)]
+ [InlineData("-l", OperationType.SymbolicLink, ConditionType.PropertyTest)]
+ [InlineData("-s", OperationType.Size, ConditionType.PropertyTest)]
+ [InlineData("-U", OperationType.ExistingUrl, ConditionType.PropertyTest)]
+ [InlineData("-x", OperationType.Executable, ConditionType.PropertyTest)]
+ public void ConditionParser_CheckFileOperations(string condition, OperationType operation, ConditionType cond)
+ {
+ var results = ConditionPatternParser.ParseActionCondition(condition);
+
+ var expected = new ParsedModRewriteExpression { Type = cond, Operation = operation , Invert = false };
+ Assert.True(CompareConditions(results, expected));
+ }
+
+ [Theory]
+ [InlineData("!-d", OperationType.Directory, ConditionType.PropertyTest)]
+ [InlineData("!-f", OperationType.RegularFile, ConditionType.PropertyTest)]
+ [InlineData("!-F", OperationType.ExistingFile, ConditionType.PropertyTest)]
+ [InlineData("!-h", OperationType.SymbolicLink, ConditionType.PropertyTest)]
+ [InlineData("!-L", OperationType.SymbolicLink, ConditionType.PropertyTest)]
+ [InlineData("!-l", OperationType.SymbolicLink, ConditionType.PropertyTest)]
+ [InlineData("!-s", OperationType.Size, ConditionType.PropertyTest)]
+ [InlineData("!-U", OperationType.ExistingUrl, ConditionType.PropertyTest)]
+ [InlineData("!-x", OperationType.Executable, ConditionType.PropertyTest)]
+ public void ConditionParser_CheckFileOperationsInverted(string condition, OperationType operation, ConditionType cond)
+ {
+ var results = ConditionPatternParser.ParseActionCondition(condition);
+
+ var expected = new ParsedModRewriteExpression { Type = cond, Operation = operation, Invert = true };
+ Assert.True(CompareConditions(results, expected));
+ }
+
+ [Theory]
+ [InlineData("-gt1", OperationType.Greater, "1", ConditionType.IntComp)]
+ [InlineData("-lt1", OperationType.Less, "1", ConditionType.IntComp)]
+ [InlineData("-ge1", OperationType.GreaterEqual, "1", ConditionType.IntComp)]
+ [InlineData("-le1", OperationType.LessEqual, "1", ConditionType.IntComp)]
+ [InlineData("-eq1", OperationType.Equal, "1", ConditionType.IntComp)]
+ [InlineData("-ne1", OperationType.NotEqual, "1", ConditionType.IntComp)]
+ public void ConditionParser_CheckIntComp(string condition, OperationType operation, string variable, ConditionType cond)
+ {
+ var results = ConditionPatternParser.ParseActionCondition(condition);
+
+ var expected = new ParsedModRewriteExpression { Type = cond, Operation = operation, Invert = false, Operand = variable };
+ Assert.True(CompareConditions(results, expected));
+ }
+
+ // TODO negative tests
+ private bool CompareConditions(ParsedModRewriteExpression i1, ParsedModRewriteExpression i2)
+ {
+ if (i1.Operation != i2.Operation ||
+ i1.Type != i2.Type ||
+ i1.Operand != i2.Operand ||
+ i1.Invert != i2.Invert)
+ {
+ return false;
+ }
+ return true;
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Rewrite.Tests/FlagParserTest.cs b/test/Microsoft.AspNetCore.Rewrite.Tests/FlagParserTest.cs
new file mode 100644
index 0000000000..dce8be83a0
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Rewrite.Tests/FlagParserTest.cs
@@ -0,0 +1,58 @@
+// 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.Linq;
+using Microsoft.AspNetCore.Rewrite.ModRewrite;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Rewrite
+{
+ public class FlagParserTest
+ {
+ [Fact]
+ public void FlagParser_CheckSingleTerm()
+ {
+ var results = FlagParser.ParseRuleFlags("[NC]");
+ var dict = new Dictionary();
+ dict.Add(RuleFlagType.NoCase, string.Empty);
+ var expected = new RuleFlags(dict);
+
+ Assert.True(DictionaryContentsEqual(results.FlagDictionary, expected.FlagDictionary));
+ }
+
+ [Fact]
+ public void FlagParser_CheckManyTerms()
+ {
+ var results = FlagParser.ParseRuleFlags("[NC,F,L]");
+ var dict = new Dictionary();
+ dict.Add(RuleFlagType.NoCase, string.Empty);
+ dict.Add(RuleFlagType.Forbidden, string.Empty);
+ dict.Add(RuleFlagType.Last, string.Empty);
+ var expected = new RuleFlags(dict);
+
+ Assert.True(DictionaryContentsEqual(results.FlagDictionary, expected.FlagDictionary));
+ }
+
+ [Fact]
+ public void FlagParser_CheckManyTermsWithEquals()
+ {
+ var results = FlagParser.ParseRuleFlags("[NC,F,R=301]");
+ var dict = new Dictionary();
+ dict.Add(RuleFlagType.NoCase, string.Empty);
+ dict.Add(RuleFlagType.Forbidden, string.Empty);
+ dict.Add(RuleFlagType.Redirect, "301");
+ var expected = new RuleFlags(dict);
+
+ Assert.True(DictionaryContentsEqual(results.FlagDictionary, expected.FlagDictionary));
+ }
+
+ public bool DictionaryContentsEqual(IDictionary dictionary, IDictionary other)
+ {
+ return (other ?? new Dictionary())
+ .OrderBy(kvp => kvp.Key)
+ .SequenceEqual((dictionary ?? new Dictionary())
+ .OrderBy(kvp => kvp.Key));
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Rewrite.Tests/Microsoft.AspNetCore.Rewrite.Tests.xproj b/test/Microsoft.AspNetCore.Rewrite.Tests/Microsoft.AspNetCore.Rewrite.Tests.xproj
new file mode 100644
index 0000000000..ca7df9cddb
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Rewrite.Tests/Microsoft.AspNetCore.Rewrite.Tests.xproj
@@ -0,0 +1,22 @@
+
+
+
+ 14.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+
+ 31794f9e-a1aa-4535-b03c-a3233737cd1a
+ Microsoft.AspNetCore.Rewrite.Tests
+ .\obj
+ .\bin\
+ v4.5.2
+
+
+ 2.0
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.Rewrite.Tests/ModRewriteConditionBuilderTest.cs b/test/Microsoft.AspNetCore.Rewrite.Tests/ModRewriteConditionBuilderTest.cs
new file mode 100644
index 0000000000..3436bcaef3
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Rewrite.Tests/ModRewriteConditionBuilderTest.cs
@@ -0,0 +1,50 @@
+// 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 Microsoft.AspNetCore.Rewrite.ModRewrite;
+using Microsoft.AspNetCore.Rewrite.Operands;
+using Xunit;
+
+namespace RewriteTest
+{
+ // This file tests an input of a list of tokens and verifies that the appropriate condition is obtained
+ public class ModRewriteConditionBuilderTest
+ {
+ [Fact]
+ public void ConditionBuilder_PassInNoFlagsFlagsEmpty()
+ {
+ var conditionString = "RewriteCond /$1 /hello";
+ var builder = new ConditionBuilder(conditionString);
+ var results = builder.Build();
+
+ //var expected = new Condition(
+ // new Pattern(
+ // new List() {
+ // new PatternSegment("/", SegmentType.Literal),
+ // new PatternSegment("1", SegmentType.RuleParameter)
+ // }),
+ // new ConditionExpression { Operand = new RegexOperand {Regex = new Regex("/hello") } },
+ // new ConditionFlags());
+ var expected = (new ConditionBuilder("/$1", "/hello")).Build();
+
+ Assert.True(results.Flags.FlagDictionary.Count == 0);
+ Assert.True(results.Flags.FlagDictionary.Count == expected.Flags.FlagDictionary.Count);
+ Assert.True((results.ConditionExpression.Operand is RegexOperand)
+ && (expected.ConditionExpression.Operand is RegexOperand));
+ }
+
+ [Fact]
+ public void ConditionBuilder_PassInFlagsFlagsExist()
+ {
+ var conditionString = "RewriteCond /$1 /hello [NC]";
+ var builder = new ConditionBuilder(conditionString);
+ var results = builder.Build();
+ var expected = (new ConditionBuilder("/$1", "/hello", "[NC]")).Build();
+
+ Assert.True(results.Flags.FlagDictionary.Count == 1);
+ Assert.True(results.Flags.FlagDictionary.Count == expected.Flags.FlagDictionary.Count);
+ Assert.True((results.ConditionExpression.Operand is RegexOperand)
+ && (expected.ConditionExpression.Operand is RegexOperand));
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Rewrite.Tests/ModRewriteCreatorTest.cs b/test/Microsoft.AspNetCore.Rewrite.Tests/ModRewriteCreatorTest.cs
new file mode 100644
index 0000000000..c9a9dd21a1
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Rewrite.Tests/ModRewriteCreatorTest.cs
@@ -0,0 +1,9 @@
+// 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
+{
+ public class ModRewriteCreatorTest
+ {
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Rewrite.Tests/ModRewriteFlagTest.cs b/test/Microsoft.AspNetCore.Rewrite.Tests/ModRewriteFlagTest.cs
new file mode 100644
index 0000000000..e9938eff0c
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Rewrite.Tests/ModRewriteFlagTest.cs
@@ -0,0 +1,88 @@
+// 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;
+using Microsoft.AspNetCore.Rewrite.ModRewrite;
+using Microsoft.AspNetCore.Rewrite.Operands;
+using Microsoft.AspNetCore.Rewrite.RuleAbstraction;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Rewrite
+{
+ public class ModRewriteFlagTest
+ {
+ // Flag tests
+ [Fact]
+ public void ModRewriteRule_Check403OnForbiddenFlag()
+ {
+ var context = new UrlRewriteContext { HttpContext = CreateRequest("/", "/hey/hello") };
+ var rule = new ModRewriteRule
+ {
+ InitialRule = new RuleExpression { Operand = new RegexOperand(new Regex("/hey/(.*)")) , Invert = false },
+ Transform = ConditionTestStringParser.ParseConditionTestString("/$1"),
+ Flags = FlagParser.ParseRuleFlags("[F]")
+ };
+ var res = rule.ApplyRule(context);
+ Assert.True(res.Result == RuleTerminiation.ResponseComplete);
+ Assert.True(context.HttpContext.Response.StatusCode == 403);
+ }
+
+ [Fact]
+ public void ModRewriteRule_Check410OnGoneFlag()
+ {
+ var context = new UrlRewriteContext { HttpContext = CreateRequest("/", "/hey/hello") };
+ var rule = new ModRewriteRule
+ {
+ InitialRule = new RuleExpression { Operand = new RegexOperand(new Regex("/hey/(.*)")), Invert = false },
+ Transform = ConditionTestStringParser.ParseConditionTestString("/$1"),
+ Flags = FlagParser.ParseRuleFlags("[G]")
+ };
+ var res = rule.ApplyRule(context);
+ Assert.True(res.Result == RuleTerminiation.ResponseComplete);
+ Assert.True(context.HttpContext.Response.StatusCode == 410);
+ }
+
+ [Fact]
+ public void ModRewriteRule_CheckLastFlag()
+ {
+ var context = new UrlRewriteContext { HttpContext = CreateRequest("/", "/hey/hello") };
+ var rule = new ModRewriteRule
+ {
+ InitialRule = new RuleExpression { Operand = new RegexOperand(new Regex("/hey/(.*)")), Invert = false },
+ Transform = ConditionTestStringParser.ParseConditionTestString("/$1"),
+ Flags = FlagParser.ParseRuleFlags("[L]")
+ };
+ var res = rule.ApplyRule(context);
+ Assert.True(res.Result == RuleTerminiation.StopRules);
+ Assert.True(context.HttpContext.Request.Path.Equals(new PathString("/hello")));
+ }
+
+
+ [Fact]
+ public void ModRewriteRule_CheckRedirectFlag()
+ {
+ // TODO fix this test.
+ var context = new UrlRewriteContext { HttpContext = CreateRequest("/", "/hey/hello") };
+ var rule = new ModRewriteRule
+ {
+ InitialRule = new RuleExpression { Operand = new RegexOperand(new Regex("/hey/(.*)")), Invert = false },
+ Transform = ConditionTestStringParser.ParseConditionTestString("/$1"),
+ Flags = FlagParser.ParseRuleFlags("[G]")
+ };
+ var res = rule.ApplyRule(context);
+ Assert.True(res.Result == RuleTerminiation.ResponseComplete);
+ Assert.True(context.HttpContext.Response.StatusCode == 410);
+ }
+
+ private HttpContext CreateRequest(string basePath, string requestPath, string requestQuery = "", string hostName = "")
+ {
+ HttpContext context = new DefaultHttpContext();
+ context.Request.PathBase = new PathString(basePath);
+ context.Request.Path = new PathString(requestPath);
+ context.Request.QueryString = new QueryString(requestQuery);
+ context.Request.Host = new HostString(hostName);
+ return context;
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Rewrite.Tests/ModRewriteMiddlewareTest.cs b/test/Microsoft.AspNetCore.Rewrite.Tests/ModRewriteMiddlewareTest.cs
new file mode 100644
index 0000000000..92c6dcb327
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Rewrite.Tests/ModRewriteMiddlewareTest.cs
@@ -0,0 +1,278 @@
+// 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.IO;
+using System.Net;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.TestHost;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Rewrite
+{
+ public class ModRewriteMiddlewareTest
+ {
+ [Fact]
+ public async Task Invoke_RewritePathWhenMatching()
+ {
+ var options = new UrlRewriteOptions().AddModRewriteRule("RewriteRule /hey/(.*) /$1 ");
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseRewriter(options);
+ app.Run(context => context.Response.WriteAsync(context.Request.Path));
+ });
+ var server = new TestServer(builder);
+
+ var response = await server.CreateClient().GetStringAsync("/hey/hello");
+
+ Assert.Equal(response, "/hello");
+ }
+
+ [Fact]
+ public async Task Invoke_RewritePathTerminatesOnFirstSuccessOfRule()
+ {
+ var options = new UrlRewriteOptions().AddModRewriteRule("RewriteRule /hey/(.*) /$1 [L]")
+ .AddModRewriteRule("RewriteRule /hello /what");
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseRewriter(options);
+ app.Run(context => context.Response.WriteAsync(context.Request.Path));
+ });
+ var server = new TestServer(builder);
+
+ var response = await server.CreateClient().GetStringAsync("/hey/hello");
+
+ Assert.Equal(response, "/hello");
+ }
+
+ [Fact]
+ public async Task Invoke_RewritePathDoesNotTerminateOnFirstSuccessOfRule()
+ {
+ var options = new UrlRewriteOptions().AddModRewriteRule("RewriteRule /hey/(.*) /$1")
+ .AddModRewriteRule("RewriteRule /hello /what");
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseRewriter(options);
+ app.Run(context => context.Response.WriteAsync(context.Request.Path));
+ });
+ var server = new TestServer(builder);
+
+ var response = await server.CreateClient().GetStringAsync("/hey/hello");
+
+ Assert.Equal(response, "/what");
+ }
+ [Theory]
+ [InlineData("", true)]
+ public void Invoke_StringComparisonTests(string input, bool expected)
+ {
+
+ }
+
+ [Fact]
+ public async Task Invoke_ShouldIgnoreComments()
+ {
+ var options = new UrlRewriteOptions().ImportFromModRewrite(new StringReader("#RewriteRule ^/hey/(.*) /$1 "));
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseRewriter(options);
+ app.Run(context => context.Response.WriteAsync(context.Request.Path));
+ });
+ var server = new TestServer(builder);
+
+ var response = await server.CreateClient().GetStringAsync("/hey/hello");
+
+ Assert.Equal(response, "/hey/hello");
+ }
+
+ // TODO Add tests to check '//' being handled appropriately.
+
+ [Fact]
+ public async Task Invoke_ShouldRewriteHomepage()
+ {
+ var options = new UrlRewriteOptions().ImportFromModRewrite(new StringReader(@"RewriteRule ^/$ /homepage.html"));
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseRewriter(options);
+ app.Run(context => context.Response.WriteAsync(context.Request.Path));
+ });
+ var server = new TestServer(builder);
+
+ var response = await server.CreateClient().GetStringAsync("http://www.foo.org/");
+
+ Assert.Equal(response, "/homepage.html");
+ }
+
+ [Fact]
+ public async Task Invoke_ShouldIgnorePorts()
+ {
+ var options = new UrlRewriteOptions().ImportFromModRewrite(new StringReader(@"RewriteRule ^/$ /homepage.html"));
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseRewriter(options);
+ app.Run(context => context.Response.WriteAsync(context.Request.Path));
+ });
+ var server = new TestServer(builder);
+
+ var response = await server.CreateClient().GetStringAsync("http://www.foo.org:42/");
+
+ Assert.Equal(response, "/homepage.html");
+ }
+
+ [Fact]
+ public async Task Invoke_HandleNegatedRewriteRules()
+ {
+ var options = new UrlRewriteOptions().ImportFromModRewrite(new StringReader(@"RewriteRule !^/$ /homepage.html"));
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseRewriter(options);
+ app.Run(context => context.Response.WriteAsync(context.Request.Path));
+ });
+ var server = new TestServer(builder);
+
+ var response = await server.CreateClient().GetStringAsync("http://www.foo.org/");
+
+ Assert.Equal(response, "/");
+ }
+
+ [Fact]
+ public async Task Invoke_BackReferencesShouldBeApplied()
+ {
+ var options = new UrlRewriteOptions().ImportFromModRewrite(new StringReader(@"RewriteRule (.*)\.aspx $1.php"));
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseRewriter(options);
+ app.Run(context => context.Response.WriteAsync(context.Request.Path));
+ });
+ var server = new TestServer(builder);
+
+ var response = await server.CreateClient().GetStringAsync("http://www.foo.org/homepage.aspx");
+
+ Assert.Equal(response, "/homepage.php");
+ }
+
+ [Theory]
+ [InlineData("http://www.foo.org/homepage.aspx", @"RewriteRule (.*)\.aspx $1.php", "/homepage.php")]
+ [InlineData("http://www.foo.org/homepage.ASPX", @"RewriteRule (.*)\.aspx $1.php", "/homepage.ASPX")]
+ [InlineData("http://www.foo.org/homepage.aspx", @"RewriteRule (.*)\.aspx $1.php [NC]", "/homepage.php")]
+ [InlineData("http://www.foo.org/homepage.ASPX", @"RewriteRule (.*)\.aspx $1.php [NC]", "/homepage.php")]
+ [InlineData("http://www.foo.org/homepage.aspx", @"RewriteRule (.*)\.aspx $1.php [nocase]", "/homepage.php")]
+ [InlineData("http://www.foo.org/homepage.ASPX", @"RewriteRule (.*)\.aspx $1.php [nocase]", "/homepage.php")]
+ public async Task Invoke_ShouldHandleFlagNoCase(string url, string rule, string expected)
+ {
+ var options = new UrlRewriteOptions().ImportFromModRewrite(new StringReader(rule));
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseRewriter(options);
+ app.Run(context => context.Response.WriteAsync(context.Request.Path));
+ });
+ var server = new TestServer(builder);
+
+ var response = await server.CreateClient().GetStringAsync(url);
+
+ Assert.Equal(response, expected);
+ }
+
+ [Fact(Skip="Need to handle escape characters")]
+ public async Task Invoke_HandleMultipleBackReferences()
+ {
+ var options = new UrlRewriteOptions()
+ .ImportFromModRewrite(new StringReader(@"RewriteRule ^/blog/([0-9]+)-([a-z]+) /blog/index.php?archive=$1-$2"));
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseRewriter(options);
+ app.Run(context => context.Response.WriteAsync(context.Request.Path));
+ });
+ var server = new TestServer(builder);
+
+ var response = await server.CreateClient().GetStringAsync("http://www.foo.org/blog/2016-jun");
+
+ Assert.Equal(response, @"/blog/index.php?archive=2016-jun");
+ }
+
+ [Fact]
+ public async Task Invoke_CheckFullUrlWithUFlagOnlyPath()
+ {
+ var options = new UrlRewriteOptions()
+ .ImportFromModRewrite(new StringReader(@"RewriteRule (.+) http://www.example.com$1/ [U]"));
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseRewriter(options);
+ app.Run(context => context.Response.WriteAsync(context.Request.Path));
+ });
+ var server = new TestServer(builder);
+
+ var response = await server.CreateClient().GetStringAsync("http://www.foo.org/blog/2016-jun");
+
+ Assert.Equal(response, @"/blog/2016-jun/");
+ }
+
+ [Fact]
+ public async Task Invoke_CheckFullUrlWithUFlag()
+ {
+ var options = new UrlRewriteOptions()
+ .ImportFromModRewrite(new StringReader(@"RewriteRule (.+) http://www.example.com$1/ [U]"));
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseRewriter(options);
+ app.Run(context => context.Response.WriteAsync(context.Request.Scheme + "://" + context.Request.Host.Host + context.Request.Path + context.Request.QueryString));
+ });
+ var server = new TestServer(builder);
+
+ var response = await server.CreateClient().GetStringAsync("http://www.foo.org/blog/2016-jun");
+
+ Assert.Equal(response, @"http://www.example.com/blog/2016-jun/");
+ }
+
+ [Fact]
+ public async Task Invoke_CheckModFileConditions()
+ {
+ var options = new UrlRewriteOptions()
+ .ImportFromModRewrite(new StringReader(@"RewriteRule (.+) http://www.example.com$1/ [U]"));
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseRewriter(options);
+ app.Run(context => context.Response.WriteAsync(context.Request.Scheme + "://" + context.Request.Host.Host + context.Request.Path + context.Request.QueryString));
+ });
+ var server = new TestServer(builder);
+
+ var response = await server.CreateClient().GetStringAsync("http://www.foo.org/blog/2016-jun");
+
+ Assert.Equal(response, @"http://www.example.com/blog/2016-jun/");
+ }
+
+ [Theory]
+ [InlineData("http://www.example.com/foo/")]
+ public async Task Invoke_EnsureHttps(string input)
+ {
+ var options = new UrlRewriteOptions()
+ .ImportFromModRewrite(new StringReader("RewriteCond %{REQUEST_URI} ^foo/ \nRewriteCond %{HTTPS} !on \nRewriteRule ^(.*)$ https://www.example.com$1 [R=301,L,U]"));
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseRewriter(options);
+ app.Run(context => context.Response.WriteAsync(context.Request.Scheme + "://" + context.Request.Host.Host + context.Request.Path + context.Request.QueryString));
+ });
+ var server = new TestServer(builder);
+
+ var response = await server.CreateClient().GetAsync(input);
+
+ Assert.Equal(response.StatusCode, (HttpStatusCode)301);
+ Assert.Equal(response.Headers.Location.AbsoluteUri, @"https://www.example.com/foo/");
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Rewrite.Tests/ModRewriteRuleBuilderTest.cs b/test/Microsoft.AspNetCore.Rewrite.Tests/ModRewriteRuleBuilderTest.cs
new file mode 100644
index 0000000000..a871f6daa2
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Rewrite.Tests/ModRewriteRuleBuilderTest.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;
+using Microsoft.AspNetCore.Http;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Rewrite
+{
+ public class ModRewriteRuleBuilderTest
+ {
+
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Rewrite.Tests/Properties/AssemblyInfo.cs b/test/Microsoft.AspNetCore.Rewrite.Tests/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..3fc4674978
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Rewrite.Tests/Properties/AssemblyInfo.cs
@@ -0,0 +1,19 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("Microsoft.AspNetCore.Rewrite.Tests")]
+[assembly: AssemblyTrademark("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("31794f9e-a1aa-4535-b03c-a3233737cd1a")]
diff --git a/test/Microsoft.AspNetCore.Rewrite.Tests/Rewrite2MiddlewareTests.cs b/test/Microsoft.AspNetCore.Rewrite.Tests/Rewrite2MiddlewareTests.cs
new file mode 100644
index 0000000000..7fb8806bdd
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Rewrite.Tests/Rewrite2MiddlewareTests.cs
@@ -0,0 +1,92 @@
+// 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.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Builder.Internal;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.TestHost;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Rewrite
+{
+ public class RewriteMiddlewareTests
+ {
+
+ [Theory(Skip = "Adapting to TestServer")]
+ [InlineData("/foo", "", "/foo", "/yes")]
+ [InlineData("/foo", "", "/foo/", "/yes")]
+ [InlineData("/foo", "/Bar", "/foo", "/yes")]
+ [InlineData("/foo", "/Bar", "/foo/cho", "/yes")]
+ [InlineData("/foo", "/Bar", "/foo/cho/", "/yes")]
+ [InlineData("/foo/cho", "/Bar", "/foo/cho", "/yes")]
+ [InlineData("/foo/cho", "/Bar", "/foo/cho/do", "/yes")]
+ public void PathMatchFunc_RewriteDone(string matchPath, string basePath, string requestPath, string rewrite)
+ {
+ var context = CreateRequest(basePath, requestPath);
+ var options = new UrlRewriteOptions().RewritePath(matchPath, rewrite, false);
+ var builder = new ApplicationBuilder(serviceProvider: null)
+ .UseRewriter(options);
+ var app = builder.Build();
+ app.Invoke(context).Wait();
+ Assert.Equal(rewrite, context.Request.Path);
+ }
+ [Theory(Skip = "Adapting to TestServer")]
+ [InlineData(@"/(?\w+)?/(?\w+)?", @"", "/hey/hello", "/${id}/${name}", "/hello/hey")]
+ [InlineData(@"/(?\w+)?/(?\w+)?/(?\w+)?", @"", "/hey/hello/what", "/${temp}/${id}/${name}", "/what/hello/hey")]
+ public void PathMatchFunc_RegexRewriteDone(string matchPath, string basePath, string requestPath, string rewrite, string expected)
+ {
+ var context = CreateRequest(basePath, requestPath);
+ var options = new UrlRewriteOptions().RewritePath(matchPath, rewrite, false);
+ var builder = new ApplicationBuilder(serviceProvider: null)
+ .UseRewriter(options);
+
+ var app = builder.Build();
+ app.Invoke(context).Wait();
+ Assert.Equal(expected, context.Request.Path);
+ }
+
+ [Fact(Skip = "Adapting to TestServer")]
+ public void PathMatchFunc_RedirectScheme()
+ {
+ HttpContext context = CreateRequest("/", "/");
+ context.Request.Scheme = "http";
+ var options = new UrlRewriteOptions().RedirectScheme(30);
+ var builder = new ApplicationBuilder(serviceProvider: null)
+ .UseRewriter(options);
+ var app = builder.Build();
+ app.Invoke(context).Wait();
+ Assert.True(context.Response.Headers["location"].First().StartsWith("https"));
+ }
+
+ [Theory(Skip = "Adapting to TestServer")]
+ public async Task PathMatchFunc_RewriteScheme()
+ {
+ var options = new UrlRewriteOptions().RewriteScheme();
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseRewriter(options);
+
+ app.Run(context => context.Response.WriteAsync(context.Request.Path));
+ });
+ var server = new TestServer(builder);
+
+ var response = await server.CreateClient().GetAsync("http://foo.com/bar");
+
+ //Assert.True(response.RequestMessage.);
+ }
+
+
+ private HttpContext CreateRequest(string basePath, string requestPath)
+ {
+ HttpContext context = new DefaultHttpContext();
+ context.Request.PathBase = new PathString(basePath);
+ context.Request.Path = new PathString(requestPath);
+ context.Request.Host = new HostString("example.com");
+ return context;
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Rewrite.Tests/RewriteTokenizerTest.cs b/test/Microsoft.AspNetCore.Rewrite.Tests/RewriteTokenizerTest.cs
new file mode 100644
index 0000000000..2961a3bb25
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Rewrite.Tests/RewriteTokenizerTest.cs
@@ -0,0 +1,39 @@
+// 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.Linq;
+using Microsoft.AspNetCore.Rewrite.ModRewrite;
+using Xunit;
+namespace Microsoft.AspNetCore.Rewrite
+{
+ public class RewriteTokenizerTest
+ {
+ [Fact]
+ public void Tokenize_RewriteCondtion()
+ {
+ var testString = "RewriteCond %{HTTPS} !-f";
+ var tokens = Tokenizer.Tokenize(testString);
+
+ var expected = new List();
+ expected.Add("RewriteCond");
+ expected.Add("%{HTTPS}");
+ expected.Add("!-f");
+ Assert.Equal(tokens, expected);
+ }
+
+ [Fact]
+ public void Tokenize_CheckEscapedSpaceIgnored()
+ {
+ // TODO need consultation on escape characters.
+ var testString = @"RewriteCond %{HTTPS}\ what !-f";
+ var tokens = Tokenizer.Tokenize(testString);
+
+ var expected = new List();
+ expected.Add("RewriteCond");
+ expected.Add(@"%{HTTPS}\ what"); // TODO maybe just have the space here? talking point
+ expected.Add("!-f");
+ Assert.Equal(tokens,expected);
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Rewrite.Tests/RuleAbstraction/RuleRegexParserTest.cs b/test/Microsoft.AspNetCore.Rewrite.Tests/RuleAbstraction/RuleRegexParserTest.cs
new file mode 100644
index 0000000000..bb47b73069
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Rewrite.Tests/RuleAbstraction/RuleRegexParserTest.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Rewrite.ModRewrite;
+using Microsoft.AspNetCore.Rewrite.Operands;
+using Microsoft.AspNetCore.Rewrite.RuleAbstraction;
+using Xunit;
+namespace Microsoft.AspNetCore.Rewrite.Tests.RuleAbstraction
+{
+ public class RuleRegexParserTest
+ {
+ [Fact]
+ public void RuleRegexParser_ShouldThrowOnNull()
+ {
+ Assert.Throws(() => RuleRegexParser.ParseRuleRegex(null));
+ }
+
+ [Fact]
+ public void RuleRegexParser_ShouldThrowOnEmpty()
+ {
+ Assert.Throws(() => RuleRegexParser.ParseRuleRegex(string.Empty));
+ }
+
+ [Fact]
+ public void RuleRegexParser_RegularRegexExpression()
+ {
+ var results = RuleRegexParser.ParseRuleRegex("(.*)");
+ Assert.False(results.Invert);
+ Assert.Equal(results.Operand, "(.*)");
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Rewrite.Tests/project.json b/test/Microsoft.AspNetCore.Rewrite.Tests/project.json
new file mode 100644
index 0000000000..904880d014
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Rewrite.Tests/project.json
@@ -0,0 +1,25 @@
+{
+ "version": "1.1.0-*",
+ "buildOptions": {
+ "warningsAsErrors": true
+ },
+ "dependencies": {
+ "dotnet-test-xunit": "2.2.0-*",
+ "Microsoft.AspNetCore.Rewrite": "1.1.0-*",
+ "Microsoft.AspNetCore.TestHost": "1.1.0-*",
+ "Microsoft.Extensions.Logging.Testing": "1.1.0-*",
+ "xunit": "2.2.0-*"
+ },
+ "frameworks": {
+ "netcoreapp1.0": {
+ "dependencies": {
+ "Microsoft.NETCore.App": {
+ "version": "1.0.0-*",
+ "type": "platform"
+ }
+ }
+ },
+ "net451": {}
+ },
+ "testRunner": "xunit"
+}
\ No newline at end of file