Add IIS rewrite map support (#168)

This commit is contained in:
David Peden 2017-02-07 14:37:23 -06:00 committed by Mikael Mengistu
parent 4fa6ed3792
commit 3faa3de14d
11 changed files with 323 additions and 13 deletions

View File

@ -66,4 +66,4 @@ namespace Microsoft.AspNetCore.Rewrite
return options;
}
}
}
}

View File

@ -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<string, string> _map = new Dictionary<string, string>();
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;
}
}
}
}

View File

@ -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<IISRewriteMap>
{
private readonly Dictionary<string, IISRewriteMap> _rewriteMaps = new Dictionary<string, IISRewriteMap>();
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<IISRewriteMap> GetEnumerator()
{
return _rewriteMaps.Values.GetEnumerator();
}
}
}

View File

@ -12,6 +12,16 @@ namespace Microsoft.AspNetCore.Rewrite.Internal.IISUrlRewrite
private const char Colon = ':';
private const char OpenBrace = '{';
private const char CloseBrace = '}';
private readonly IISRewriteMapCollection _rewriteMaps;
public InputParser()
{
}
public InputParser(IISRewriteMapCollection rewriteMaps)
{
_rewriteMaps = rewriteMaps;
}
/// <summary>
/// Creates a pattern, which is a template to create a new test string to
@ -31,7 +41,7 @@ namespace Microsoft.AspNetCore.Rewrite.Internal.IISUrlRewrite
return ParseString(context, global);
}
private static Pattern ParseString(ParserContext context, bool global)
private Pattern ParseString(ParserContext context, bool global)
{
var results = new List<PatternSegment>();
while (context.Next())
@ -60,7 +70,7 @@ namespace Microsoft.AspNetCore.Rewrite.Internal.IISUrlRewrite
return new Pattern(results);
}
private static void ParseParameter(ParserContext context, IList<PatternSegment> results, bool global)
private void ParseParameter(ParserContext context, IList<PatternSegment> results, bool global)
{
context.Mark();
// Four main cases:
@ -128,6 +138,13 @@ namespace Microsoft.AspNetCore.Rewrite.Internal.IISUrlRewrite
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));
}
}

View File

@ -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;
}
}
}

View File

@ -13,6 +13,7 @@ namespace Microsoft.AspNetCore.Rewrite.Internal.IISUrlRewrite
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 @@ namespace Microsoft.AspNetCore.Rewrite.Internal.IISUrlRewrite
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";
}
}

View File

@ -12,21 +12,28 @@ namespace Microsoft.AspNetCore.Rewrite.Internal.IISUrlRewrite
{
public class UrlRewriteFileParser
{
private readonly InputParser _inputParser = new InputParser();
private InputParser _inputParser;
/// <summary>
/// Parse an IIS rewrite section into a list of <see cref="IISUrlRewriteRule"/>s.
/// </summary>
/// <param name="reader">The reader containing the rewrite XML</param>
public IList<IISUrlRewriteRule> 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<IISUrlRewriteRule>();
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<IISUrlRewriteRule>();
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<IISUrlRewriteRule> result, bool global)

View File

@ -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];
}
}
}

View File

@ -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 @@ namespace Microsoft.AspNetCore.Rewrite.Tests.UrlRewrite
Assert.Throws<FormatException>(() => new InputParser().ParseInputString(testString, global: false));
}
[Fact]
public void Should_throw_FormatException_if_no_rewrite_maps_are_defined()
{
Assert.Throws<FormatException>(() => 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<FormatException>(() => 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 @@ namespace Microsoft.AspNetCore.Rewrite.Tests.UrlRewrite
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 };
}
}
}

View File

@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Builder;
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 @@ namespace Microsoft.AspNetCore.Rewrite.Tests.UrlRewrite
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(@"
<rewrite>
<rules>
<rule name=""Proxy"">
<match url=""([^/]*)(/?.*)"" />
<conditions>
<add input=""{environmentMap:{R:1}}"" pattern=""(.+)"" />
</conditions>
<action type=""Rewrite"" url=""http://{C:1}{R:2}"" appendQueryString=""true"" />
</rule>
</rules>
<rewriteMaps>
<rewriteMap name=""environmentMap"">
<add key=""dev"" value=""1.1.1.1"" />
</rewriteMap>
</rewriteMaps>
</rewrite>"));
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);
}
}
}

View File

@ -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 = $@"<rewrite>
<rewriteMaps>
<rewriteMap name=""{expectedMapName}"">
<add key=""{expectedKey}"" value=""{expectedValue}"" />
</rewriteMap>
</rewriteMaps>
</rewrite>";
// 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]);
}
}
}