diff --git a/src/Microsoft.AspNetCore.Rewrite/IISUrlRewriteOptionsExtensions.cs b/src/Microsoft.AspNetCore.Rewrite/IISUrlRewriteOptionsExtensions.cs index bed837ae..e31819b1 100644 --- a/src/Microsoft.AspNetCore.Rewrite/IISUrlRewriteOptionsExtensions.cs +++ b/src/Microsoft.AspNetCore.Rewrite/IISUrlRewriteOptionsExtensions.cs @@ -66,4 +66,4 @@ public static RewriteOptions AddIISUrlRewrite(this RewriteOptions options, TextR return options; } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Rewrite/Internal/IISUrlRewrite/IISRewriteMap.cs b/src/Microsoft.AspNetCore.Rewrite/Internal/IISUrlRewrite/IISRewriteMap.cs new file mode 100644 index 00000000..0cd52338 --- /dev/null +++ b/src/Microsoft.AspNetCore.Rewrite/Internal/IISUrlRewrite/IISRewriteMap.cs @@ -0,0 +1,45 @@ +// 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.Internal.IISUrlRewrite +{ + public class IISRewriteMap + { + private readonly Dictionary _map = new Dictionary(); + + public IISRewriteMap(string name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException(nameof(name)); + } + Name = name; + } + + public string Name { get; } + + public string this[string key] + { + get + { + string value; + return _map.TryGetValue(key, out value) ? value : null; + } + set + { + if (string.IsNullOrEmpty(key)) + { + throw new ArgumentException(nameof(key)); + } + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentException(nameof(value)); + } + _map[key] = value; + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Rewrite/Internal/IISUrlRewrite/IISRewriteMapCollection.cs b/src/Microsoft.AspNetCore.Rewrite/Internal/IISUrlRewrite/IISRewriteMapCollection.cs new file mode 100644 index 00000000..4f8a9006 --- /dev/null +++ b/src/Microsoft.AspNetCore.Rewrite/Internal/IISUrlRewrite/IISRewriteMapCollection.cs @@ -0,0 +1,42 @@ +// 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; +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Rewrite.Internal.IISUrlRewrite +{ + public class IISRewriteMapCollection : IEnumerable + { + private readonly Dictionary _rewriteMaps = new Dictionary(); + + public void Add(IISRewriteMap rewriteMap) + { + if (rewriteMap != null) + { + _rewriteMaps[rewriteMap.Name] = rewriteMap; + } + } + + public int Count => _rewriteMaps.Count; + + public IISRewriteMap this[string key] + { + get + { + IISRewriteMap value; + return _rewriteMaps.TryGetValue(key, out value) ? value : null; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _rewriteMaps.Values.GetEnumerator(); + } + + public IEnumerator GetEnumerator() + { + return _rewriteMaps.Values.GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Rewrite/Internal/IISUrlRewrite/InputParser.cs b/src/Microsoft.AspNetCore.Rewrite/Internal/IISUrlRewrite/InputParser.cs index c99790cb..d73e561d 100644 --- a/src/Microsoft.AspNetCore.Rewrite/Internal/IISUrlRewrite/InputParser.cs +++ b/src/Microsoft.AspNetCore.Rewrite/Internal/IISUrlRewrite/InputParser.cs @@ -12,6 +12,16 @@ public class InputParser private const char Colon = ':'; private const char OpenBrace = '{'; private const char CloseBrace = '}'; + private readonly IISRewriteMapCollection _rewriteMaps; + + public InputParser() + { + } + + public InputParser(IISRewriteMapCollection rewriteMaps) + { + _rewriteMaps = rewriteMaps; + } /// /// Creates a pattern, which is a template to create a new test string to @@ -31,7 +41,7 @@ public Pattern ParseInputString(string testString, bool global) return ParseString(context, global); } - private static Pattern ParseString(ParserContext context, bool global) + private Pattern ParseString(ParserContext context, bool global) { var results = new List(); while (context.Next()) @@ -60,7 +70,7 @@ private static Pattern ParseString(ParserContext context, bool global) return new Pattern(results); } - private static void ParseParameter(ParserContext context, IList results, bool global) + private void ParseParameter(ParserContext context, IList results, bool global) { context.Mark(); // Four main cases: @@ -128,6 +138,13 @@ private static void ParseParameter(ParserContext context, IList return; } default: + var rewriteMap = _rewriteMaps?[parameter]; + if (rewriteMap != null) + { + var pattern = ParseString(context, global); + results.Add(new RewriteMapSegment(rewriteMap, pattern)); + return; + } throw new FormatException(Resources.FormatError_InputParserUnrecognizedParameter(parameter, context.Index)); } } diff --git a/src/Microsoft.AspNetCore.Rewrite/Internal/IISUrlRewrite/RewriteMapParser.cs b/src/Microsoft.AspNetCore.Rewrite/Internal/IISUrlRewrite/RewriteMapParser.cs new file mode 100644 index 00000000..4e9b21d8 --- /dev/null +++ b/src/Microsoft.AspNetCore.Rewrite/Internal/IISUrlRewrite/RewriteMapParser.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; +using System.Linq; +using System.Xml.Linq; + +namespace Microsoft.AspNetCore.Rewrite.Internal.IISUrlRewrite +{ + public static class RewriteMapParser + { + public static IISRewriteMapCollection Parse(XElement xmlRoot) + { + if (xmlRoot == null) + { + throw new ArgumentNullException(nameof(xmlRoot)); + } + + var mapsElement = xmlRoot.Descendants(RewriteTags.RewriteMaps).SingleOrDefault(); + if (mapsElement == null) + { + return null; + } + + var rewriteMaps = new IISRewriteMapCollection(); + foreach (var mapElement in mapsElement.Elements(RewriteTags.RewriteMap)) + { + var map = new IISRewriteMap(mapElement.Attribute(RewriteTags.Name)?.Value); + foreach (var addElement in mapElement.Elements(RewriteTags.Add)) + { + map[addElement.Attribute(RewriteTags.Key).Value.ToLowerInvariant()] = addElement.Attribute(RewriteTags.Value).Value; + } + rewriteMaps.Add(map); + } + + return rewriteMaps; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Rewrite/Internal/IISUrlRewrite/RewriteTags.cs b/src/Microsoft.AspNetCore.Rewrite/Internal/IISUrlRewrite/RewriteTags.cs index 9b62b265..42a8e5aa 100644 --- a/src/Microsoft.AspNetCore.Rewrite/Internal/IISUrlRewrite/RewriteTags.cs +++ b/src/Microsoft.AspNetCore.Rewrite/Internal/IISUrlRewrite/RewriteTags.cs @@ -13,6 +13,7 @@ public static class RewriteTags public const string GlobalRules = "globalRules"; public const string IgnoreCase = "ignoreCase"; public const string Input = "input"; + public const string Key = "key"; public const string LogicalGrouping = "logicalGrouping"; public const string LogRewrittenUrl = "logRewrittenUrl"; public const string Match = "match"; @@ -22,13 +23,16 @@ public static class RewriteTags public const string Negate = "negate"; public const string Pattern = "pattern"; public const string PatternSyntax = "patternSyntax"; - public const string Rewrite = "rewrite"; public const string RedirectType = "redirectType"; + public const string Rewrite = "rewrite"; + public const string RewriteMap = "rewriteMap"; + public const string RewriteMaps = "rewriteMaps"; public const string Rule = "rule"; public const string Rules = "rules"; public const string StopProcessing = "stopProcessing"; public const string TrackAllCaptures = "trackAllCaptures"; public const string Type = "type"; public const string Url = "url"; + public const string Value = "value"; } } diff --git a/src/Microsoft.AspNetCore.Rewrite/Internal/IISUrlRewrite/UrlRewriteFileParser.cs b/src/Microsoft.AspNetCore.Rewrite/Internal/IISUrlRewrite/UrlRewriteFileParser.cs index e4d8296c..cb96c8b0 100644 --- a/src/Microsoft.AspNetCore.Rewrite/Internal/IISUrlRewrite/UrlRewriteFileParser.cs +++ b/src/Microsoft.AspNetCore.Rewrite/Internal/IISUrlRewrite/UrlRewriteFileParser.cs @@ -12,21 +12,28 @@ namespace Microsoft.AspNetCore.Rewrite.Internal.IISUrlRewrite { public class UrlRewriteFileParser { - private readonly InputParser _inputParser = new InputParser(); + private InputParser _inputParser; + /// + /// Parse an IIS rewrite section into a list of s. + /// + /// The reader containing the rewrite XML public IList Parse(TextReader reader) { var xmlDoc = XDocument.Load(reader, LoadOptions.SetLineInfo); var xmlRoot = xmlDoc.Descendants(RewriteTags.Rewrite).FirstOrDefault(); - if (xmlRoot != null) + if (xmlRoot == null) { - var result = new List(); - ParseRules(xmlRoot.Descendants(RewriteTags.GlobalRules).FirstOrDefault(), result, global: true); - ParseRules(xmlRoot.Descendants(RewriteTags.Rules).FirstOrDefault(), result, global: false); - return result; + return null; } - return null; + + _inputParser = new InputParser(RewriteMapParser.Parse(xmlRoot)); + + var result = new List(); + ParseRules(xmlRoot.Descendants(RewriteTags.GlobalRules).FirstOrDefault(), result, global: true); + ParseRules(xmlRoot.Descendants(RewriteTags.Rules).FirstOrDefault(), result, global: false); + return result; } private void ParseRules(XElement rules, IList result, bool global) diff --git a/src/Microsoft.AspNetCore.Rewrite/Internal/PatternSegments/RewriteMapSegment.cs b/src/Microsoft.AspNetCore.Rewrite/Internal/PatternSegments/RewriteMapSegment.cs new file mode 100644 index 00000000..3dd50c9f --- /dev/null +++ b/src/Microsoft.AspNetCore.Rewrite/Internal/PatternSegments/RewriteMapSegment.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 Microsoft.AspNetCore.Rewrite.Internal.IISUrlRewrite; + +namespace Microsoft.AspNetCore.Rewrite.Internal.PatternSegments +{ + public class RewriteMapSegment : PatternSegment + { + private readonly IISRewriteMap _rewriteMap; + private readonly Pattern _pattern; + + public RewriteMapSegment(IISRewriteMap rewriteMap, Pattern pattern) + { + _rewriteMap = rewriteMap; + _pattern = pattern; + } + + public override string Evaluate(RewriteContext context, BackReferenceCollection ruleBackReferences, BackReferenceCollection conditionBackReferences) + { + var key = _pattern.Evaluate(context, ruleBackReferences, conditionBackReferences).ToLowerInvariant(); + return _rewriteMap[key]; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Rewrite.Tests/IISUrlRewrite/InputParserTests.cs b/test/Microsoft.AspNetCore.Rewrite.Tests/IISUrlRewrite/InputParserTests.cs index ea3d4819..d636f152 100644 --- a/test/Microsoft.AspNetCore.Rewrite.Tests/IISUrlRewrite/InputParserTests.cs +++ b/test/Microsoft.AspNetCore.Rewrite.Tests/IISUrlRewrite/InputParserTests.cs @@ -2,10 +2,13 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Linq; using System.Text.RegularExpressions; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Rewrite.Internal; using Microsoft.AspNetCore.Rewrite.Internal.IISUrlRewrite; +using Microsoft.AspNetCore.Rewrite.Internal.PatternSegments; +using Microsoft.Extensions.Logging.Testing; using Xunit; namespace Microsoft.AspNetCore.Rewrite.Tests.UrlRewrite @@ -88,11 +91,48 @@ public void FormatExceptionsOnBadSyntax(string testString) Assert.Throws(() => new InputParser().ParseInputString(testString, global: false)); } - private RewriteContext CreateTestRewriteContext() + [Fact] + public void Should_throw_FormatException_if_no_rewrite_maps_are_defined() + { + Assert.Throws(() => new InputParser(null).ParseInputString("{apiMap:{R:1}}", global: false)); + } + + [Fact] + public void Should_throw_FormatException_if_rewrite_map_not_found() { + const string definedMapName = "testMap"; + const string undefinedMapName = "apiMap"; + var map = new IISRewriteMap(definedMapName); + var maps = new IISRewriteMapCollection { map }; + Assert.Throws(() => new InputParser(maps).ParseInputString($"{{{undefinedMapName}:{{R:1}}}}", global: false)); + } + + [Fact] + public void Should_parse_RewriteMapSegment_and_successfully_evaluate_result() + { + const string expectedMapName = "apiMap"; + const string expectedKey = "api.test.com"; + const string expectedValue = "test.com/api"; + var map = new IISRewriteMap(expectedMapName); + map[expectedKey] = expectedValue; + var maps = new IISRewriteMapCollection { map }; + + var inputString = $"{{{expectedMapName}:{{R:1}}}}"; + var pattern = new InputParser(maps).ParseInputString(inputString, global: false); + Assert.Equal(1, pattern.PatternSegments.Count); + var segment = pattern.PatternSegments.Single(); + var rewriteMapSegment = segment as RewriteMapSegment; + Assert.NotNull(rewriteMapSegment); + + var result = rewriteMapSegment.Evaluate(CreateTestRewriteContext(), CreateRewriteMapRuleMatch(expectedKey).BackReferences, CreateRewriteMapConditionMatch(inputString).BackReferences); + Assert.Equal(expectedValue, result); + } + + private RewriteContext CreateTestRewriteContext() + { var context = new DefaultHttpContext(); - return new RewriteContext { HttpContext = context, StaticFileProvider = null }; + return new RewriteContext { HttpContext = context, StaticFileProvider = null, Logger = new NullLogger() }; } private BackReferenceCollection CreateTestRuleBackReferences() @@ -106,5 +146,17 @@ private BackReferenceCollection CreateTestCondBackReferences() var match = Regex.Match("foo/bar/baz", "(.*)/(.*)/(.*)"); return new BackReferenceCollection(match.Groups); } + + private MatchResults CreateRewriteMapRuleMatch(string input) + { + var match = Regex.Match(input, "([^/]*)/?(.*)"); + return new MatchResults { BackReferences = new BackReferenceCollection(match.Groups), Success = match.Success }; + } + + private MatchResults CreateRewriteMapConditionMatch(string input) + { + var match = Regex.Match(input, "(.+)"); + return new MatchResults { BackReferences = new BackReferenceCollection(match.Groups), Success = match.Success }; + } } } diff --git a/test/Microsoft.AspNetCore.Rewrite.Tests/IISUrlRewrite/MiddleWareTests.cs b/test/Microsoft.AspNetCore.Rewrite.Tests/IISUrlRewrite/MiddleWareTests.cs index a720a58b..ed5d95ff 100644 --- a/test/Microsoft.AspNetCore.Rewrite.Tests/IISUrlRewrite/MiddleWareTests.cs +++ b/test/Microsoft.AspNetCore.Rewrite.Tests/IISUrlRewrite/MiddleWareTests.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Rewrite.Internal.IISUrlRewrite; using Microsoft.AspNetCore.TestHost; using Microsoft.Net.Http.Headers; using Xunit; @@ -480,5 +481,40 @@ public async Task Invoke_GlobalRuleConditionMatchesAgainstFullUri() Assert.Equal("http://www.test.com/foo/bar", response); } + + [Theory] + [InlineData("http://fetch.environment.local/dev/path", "http://1.1.1.1/path")] + [InlineData("http://fetch.environment.local/qa/path", "http://fetch.environment.local/qa/path")] + public async Task Invoke_ReverseProxyToAnotherSiteUsingXmlConfiguredRewriteMap(string requestUri, string expectedRewrittenUri) + { + var options = new RewriteOptions().AddIISUrlRewrite(new StringReader(@" + + + + + + + + + + + + + + + + ")); + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseRewriter(options); + app.Run(context => context.Response.WriteAsync(context.Request.GetEncodedUrl())); + }); + var server = new TestServer(builder); + + var response = await server.CreateClient().GetStringAsync(new Uri(requestUri)); + + Assert.Equal(expectedRewrittenUri, response); + } } } diff --git a/test/Microsoft.AspNetCore.Rewrite.Tests/IISUrlRewrite/RewriteMapParserTests.cs b/test/Microsoft.AspNetCore.Rewrite.Tests/IISUrlRewrite/RewriteMapParserTests.cs new file mode 100644 index 00000000..a917e675 --- /dev/null +++ b/test/Microsoft.AspNetCore.Rewrite.Tests/IISUrlRewrite/RewriteMapParserTests.cs @@ -0,0 +1,43 @@ +// 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.Linq; +using System.Xml.Linq; +using Microsoft.AspNetCore.Rewrite.Internal.IISUrlRewrite; +using Xunit; + +namespace Microsoft.AspNetCore.Rewrite.Tests.IISUrlRewrite +{ + public class RewriteMapParserTests + { + [Fact] + public void Should_parse_rewrite_map() + { + // arrange + const string expectedMapName = "apiMap"; + const string expectedKey = "api.test.com"; + const string expectedValue = "test.com/api"; + var xml = $@" + + + + + + "; + + // act + var xmlDoc = XDocument.Load(new StringReader(xml), LoadOptions.SetLineInfo); + var xmlRoot = xmlDoc.Descendants(RewriteTags.Rewrite).FirstOrDefault(); + var actualMaps = RewriteMapParser.Parse(xmlRoot); + + // assert + Assert.Equal(1, actualMaps.Count); + + var actualMap = actualMaps[expectedMapName]; + Assert.NotNull(actualMap); + Assert.Equal(expectedMapName, actualMap.Name); + Assert.Equal(expectedValue, actualMap[expectedKey]); + } + } +} \ No newline at end of file