From a9c2656404727d1be6a7ceb3a836f2d3a48a3ef2 Mon Sep 17 00:00:00 2001 From: Justin Kotalik Date: Fri, 15 Jul 2016 15:42:42 -0700 Subject: [PATCH] Url Rewrite WIP --- BasicMiddleware.sln | 24 +- .../RewriteSample/Properties/AssemblyInfo.cs | 19 ++ samples/RewriteSample/Rewrite.txt | 12 + samples/RewriteSample/RewriteSample.xproj | 21 ++ samples/RewriteSample/Startup.cs | 29 ++ samples/RewriteSample/project.json | 32 ++ .../Microsoft.AspNetCore.Rewrite.xproj | 21 ++ .../ModRewrite/Condition.cs | 18 ++ .../ModRewrite/ConditionBuilder.cs | 94 ++++++ .../ModRewrite/ConditionExpression.cs | 29 ++ .../ModRewrite/ConditionFlagType.cs | 12 + .../ModRewrite/ConditionFlags.cs | 101 +++++++ .../ModRewrite/ConditionPatternParser.cs | 230 +++++++++++++++ .../ModRewrite/ConditionTestStringParser.cs | 205 +++++++++++++ .../ModRewrite/ConditionType.cs | 13 + .../ModRewrite/ExpressionCreator.cs | 128 ++++++++ .../ModRewrite/FileParser.cs | 103 +++++++ .../ModRewrite/FlagParser.cs | 103 +++++++ .../ModRewrite/ModRewriteExtensions.cs | 73 +++++ .../ModRewrite/ModRewriteRule.cs | 209 +++++++++++++ .../ModRewrite/OperationType.cs | 23 ++ .../ModRewrite/ParsedConditionExpression.cs | 22 ++ .../ModRewrite/Pattern.cs | 69 +++++ .../ModRewrite/PatternSegment.cs | 26 ++ .../ModRewrite/RuleBuilder.cs | 122 ++++++++ .../ModRewrite/RuleFlagType.cs | 35 +++ .../ModRewrite/RuleFlags.cs | 133 +++++++++ .../ModRewrite/RuleRegexParser.cs | 26 ++ .../ModRewrite/SegmentType.cs | 13 + .../ModRewrite/ServerVariables.cs | 180 ++++++++++++ .../ModRewrite/Tokenizer.cs | 84 ++++++ .../Operands/IntegerOperand.cs | 57 ++++ .../Operands/IntegerOperation.cs | 15 + .../Operands/Operand.cs | 13 + .../Operands/PropertyOperand.cs | 44 +++ .../Operands/PropertyOperation.cs | 17 ++ .../Operands/RegexOperand.cs | 24 ++ .../Operands/StringOperand.cs | 40 +++ .../Operands/StringOperation.cs | 14 + .../ParserContext.cs | 70 +++++ .../Properties/AssemblyInfo.cs | 19 ++ .../RuleAbstraction/FunctionalRule.cs | 14 + .../RuleAbstraction/PathRule.cs | 48 +++ .../RuleAbstraction/Rule.cs | 11 + .../RuleAbstraction/RuleExpression.cs | 13 + .../RuleAbstraction/RuleResult.cs | 10 + .../RuleAbstraction/RuleTermination.cs | 14 + .../RuleAbstraction/SchemeRule.cs | 54 ++++ .../RuleAbstraction/Transformation.cs | 12 + .../UrlRewriteContext.cs | 17 ++ .../UrlRewriteExtensions.cs | 35 +++ .../UrlRewriteMiddleware.cs | 76 +++++ .../UrlRewriteOptions.cs | 21 ++ .../UrlRewriteOptionsAddRulesExtensions.cs | 108 +++++++ src/Microsoft.AspNetCore.Rewrite/project.json | 37 +++ .../IPNetworkTest.cs | 1 - .../ConditionActionTest.cs | 99 +++++++ .../FlagParserTest.cs | 58 ++++ .../Microsoft.AspNetCore.Rewrite.Tests.xproj | 22 ++ .../ModRewriteConditionBuilderTest.cs | 50 ++++ .../ModRewriteCreatorTest.cs | 9 + .../ModRewriteFlagTest.cs | 88 ++++++ .../ModRewriteMiddlewareTest.cs | 278 ++++++++++++++++++ .../ModRewriteRuleBuilderTest.cs | 14 + .../Properties/AssemblyInfo.cs | 19 ++ .../Rewrite2MiddlewareTests.cs | 92 ++++++ .../RewriteTokenizerTest.cs | 39 +++ .../RuleAbstraction/RuleRegexParserTest.cs | 33 +++ .../project.json | 25 ++ 69 files changed, 3816 insertions(+), 3 deletions(-) create mode 100644 samples/RewriteSample/Properties/AssemblyInfo.cs create mode 100644 samples/RewriteSample/Rewrite.txt create mode 100644 samples/RewriteSample/RewriteSample.xproj create mode 100644 samples/RewriteSample/Startup.cs create mode 100644 samples/RewriteSample/project.json create mode 100644 src/Microsoft.AspNetCore.Rewrite/Microsoft.AspNetCore.Rewrite.xproj create mode 100644 src/Microsoft.AspNetCore.Rewrite/ModRewrite/Condition.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/ModRewrite/ConditionBuilder.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/ModRewrite/ConditionExpression.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/ModRewrite/ConditionFlagType.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/ModRewrite/ConditionFlags.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/ModRewrite/ConditionPatternParser.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/ModRewrite/ConditionTestStringParser.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/ModRewrite/ConditionType.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/ModRewrite/ExpressionCreator.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/ModRewrite/FileParser.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/ModRewrite/FlagParser.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/ModRewrite/ModRewriteExtensions.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/ModRewrite/ModRewriteRule.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/ModRewrite/OperationType.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/ModRewrite/ParsedConditionExpression.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/ModRewrite/Pattern.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/ModRewrite/PatternSegment.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/ModRewrite/RuleBuilder.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/ModRewrite/RuleFlagType.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/ModRewrite/RuleFlags.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/ModRewrite/RuleRegexParser.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/ModRewrite/SegmentType.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/ModRewrite/ServerVariables.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/ModRewrite/Tokenizer.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/Operands/IntegerOperand.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/Operands/IntegerOperation.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/Operands/Operand.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/Operands/PropertyOperand.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/Operands/PropertyOperation.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/Operands/RegexOperand.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/Operands/StringOperand.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/Operands/StringOperation.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/ParserContext.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/Properties/AssemblyInfo.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/RuleAbstraction/FunctionalRule.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/RuleAbstraction/PathRule.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/RuleAbstraction/Rule.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/RuleAbstraction/RuleExpression.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/RuleAbstraction/RuleResult.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/RuleAbstraction/RuleTermination.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/RuleAbstraction/SchemeRule.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/RuleAbstraction/Transformation.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/UrlRewriteContext.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/UrlRewriteExtensions.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/UrlRewriteMiddleware.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/UrlRewriteOptions.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/UrlRewriteOptionsAddRulesExtensions.cs create mode 100644 src/Microsoft.AspNetCore.Rewrite/project.json create mode 100644 test/Microsoft.AspNetCore.Rewrite.Tests/ConditionActionTest.cs create mode 100644 test/Microsoft.AspNetCore.Rewrite.Tests/FlagParserTest.cs create mode 100644 test/Microsoft.AspNetCore.Rewrite.Tests/Microsoft.AspNetCore.Rewrite.Tests.xproj create mode 100644 test/Microsoft.AspNetCore.Rewrite.Tests/ModRewriteConditionBuilderTest.cs create mode 100644 test/Microsoft.AspNetCore.Rewrite.Tests/ModRewriteCreatorTest.cs create mode 100644 test/Microsoft.AspNetCore.Rewrite.Tests/ModRewriteFlagTest.cs create mode 100644 test/Microsoft.AspNetCore.Rewrite.Tests/ModRewriteMiddlewareTest.cs create mode 100644 test/Microsoft.AspNetCore.Rewrite.Tests/ModRewriteRuleBuilderTest.cs create mode 100644 test/Microsoft.AspNetCore.Rewrite.Tests/Properties/AssemblyInfo.cs create mode 100644 test/Microsoft.AspNetCore.Rewrite.Tests/Rewrite2MiddlewareTests.cs create mode 100644 test/Microsoft.AspNetCore.Rewrite.Tests/RewriteTokenizerTest.cs create mode 100644 test/Microsoft.AspNetCore.Rewrite.Tests/RuleAbstraction/RuleRegexParserTest.cs create mode 100644 test/Microsoft.AspNetCore.Rewrite.Tests/project.json 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