Add IIS rewrite map support (#168)
This commit is contained in:
parent
4fa6ed3792
commit
3faa3de14d
|
|
@ -66,4 +66,4 @@ namespace Microsoft.AspNetCore.Rewrite
|
|||
return options;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue