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