IIS UrlRewrite parsing.

This commit is contained in:
Justin Kotalik 2016-07-27 14:47:32 -07:00
parent 4687aad61e
commit a62e327c23
41 changed files with 1363 additions and 11 deletions

View File

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

View File

@ -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.

View File

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

View File

@ -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)

View File

@ -0,0 +1,19 @@
<rewrite>
<rules>
<rule name="Fail bad requests">
<match url=".*"/>
<conditions>
<add input="{HTTP_HOST}" pattern="localhost" negate="true" />
</conditions>
<action type="AbortRequest" />
</rule>
<rule name="Redirect from blog">
<match url="^blog/([_0-9a-z-]+)/([0-9]+)" />
<action type="Redirect" url="article/{R:2}/{R:1}" redirectType="Found" />
</rule>
<rule name="Rewrite to article.aspx">
<match url="^article/([0-9]+)/([_0-9a-z-]+)" />
<action type="Rewrite" url="article.aspx?id={R:1}&amp;title={R:2}" />
</rule>
</rules>
</rewrite>

View File

@ -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.
/// </summary>
/// <param name="options">The UrlRewrite options.</param>
/// <param name="hostingEnv"></param>
/// <param name="filePath">The path to the file containing mod_rewrite rules.</param>
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)));
};

View File

@ -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
{
/// <summary>
/// Contains a sequence of pattern segments, which on obtaining the context, will create the appropriate

View File

@ -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
{
/// <summary>
/// A Pattern segment contains a portion of the test string/ substitution segment with a type associated.

View File

@ -25,6 +25,7 @@ namespace Microsoft.AspNetCore.Rewrite.Operands
{
throw new FormatException("Syntax error for integers in comparison.");
}
Value = compValue;
Operation = operation;
}

View File

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

View File

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

View File

@ -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<Condition> ConditionList { get; set; } = new List<Condition>();
public LogicalGrouping MatchType { get; set; } // default is MatchAll
public bool TrackingAllCaptures { get; set; }
}
}

View File

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

View File

@ -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
{
/// <summary>
/// </summary>
public class InputParser
{
private const char Colon = ':';
private const char OpenBrace = '{';
private const char CloseBrace = '}';
/// <summary>
/// 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.
/// </summary>
/// <param name="testString"></param>
/// <returns>A new <see cref="Pattern"/>, containing a list of <see cref="PatternSegment"/></returns>
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<PatternSegment>();
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<PatternSegment> 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<PatternSegment> 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;
}
}
}

View File

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

View File

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

View File

@ -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<PatternSegment> PatternSegments { get; }
public Pattern(List<PatternSegment> 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();
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.");
}
}
}
}

View File

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

View File

@ -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
{
/// <summary>
/// Imports rules from a mod_rewrite file and adds the rules to current rules.
/// </summary>
/// <param name="options">The UrlRewrite options.</param>
/// <param name="hostingEnv"></param>
/// <param name="filePath">The path to the file containing urlrewrite rules.</param>
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;
}
}
}

View File

@ -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<UrlRewriteRule> Parse(TextReader reader)
{
var temp = XDocument.Load(reader);
var xmlRoot = temp.Descendants(RewriteTags.Rewrite);
var rules = new List<UrlRewriteRule>();
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<XElement> globalRules, List<UrlRewriteRule> result)
{
foreach (var rule in globalRules.Elements(RewriteTags.Rule) ?? Enumerable.Empty<XElement>())
{
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<XElement> rules, List<UrlRewriteRule> result)
{
// TODO Better null check?
foreach (var rule in rules.Elements(RewriteTags.Rule) ?? Enumerable.Empty<XElement>())
{
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;
}
}
}

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 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();
}
}
}

View File

@ -31,6 +31,7 @@
"frameworks": {
"net451": {
"frameworkAssemblies": {
"System.Xml": "",
"System.Xml.Linq": ""
}
},

View File

@ -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 = @"<rewrite>
<rules>
<rule name=""Rewrite to article.aspx"">
<match url = ""^article/([0-9]+)/([_0-9a-z-]+)"" />
<action type=""Rewrite"" url =""article.aspx?id={R:1}&amp;title={R:2}"" />
</rule>
</rules>
</rewrite>";
var expected = new List<UrlRewriteRule>();
expected.Add(CreateTestRule(new List<Condition>(),
Url: "^article/([0-9]+)/([_0-9a-z-]+)",
name: "Rewrite to article.aspx",
actionType: ActionType.Rewrite,
pattern: "article.aspx?id={R:1}&amp;title={R:2}"));
// act
var res = UrlRewriteFileParser.Parse(new StringReader(xml));
// assert
AssertUrlRewriteRuleEquality(res, expected);
}
[Fact]
public void RuleParse_ParseSingleRuleWithSingleCondition()
{
// arrange
var xml = @"<rewrite>
<rules>
<rule name=""Rewrite to article.aspx"">
<match url = ""^article/([0-9]+)/([_0-9a-z-]+)"" />
<conditions>
<add input=""{HTTPS}"" pattern=""^OFF$"" />
</conditions>
<action type=""Rewrite"" url =""article.aspx?id={R:1}&amp;title={R:2}"" />
</rule>
</rules>
</rewrite>";
var condList = new List<Condition>();
condList.Add(new Condition
{
Input = InputParser.ParseInputString("{HTTPS}"),
MatchPattern = new Regex("^OFF$")
});
var expected = new List<UrlRewriteRule>();
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}&amp;title={R:2}"));
// act
var res = UrlRewriteFileParser.Parse(new StringReader(xml));
// assert
AssertUrlRewriteRuleEquality(expected, res);
}
[Fact]
public void RuleParse_ParseMultipleRules()
{
// arrange
var xml = @"<rewrite>
<rules>
<rule name=""Rewrite to article.aspx"">
<match url = ""^article/([0-9]+)/([_0-9a-z-]+)"" />
<conditions>
<add input=""{HTTPS}"" pattern=""^OFF$"" />
</conditions>
<action type=""Rewrite"" url =""article.aspx?id={R:1}&amp;title={R:2}"" />
</rule>
<rule name=""Rewrite to article.aspx"">
<match url = ""^article/([0-9]+)/([_0-9a-z-]+)"" />
<conditions>
<add input=""{HTTPS}"" pattern=""^OFF$"" />
</conditions>
<action type=""Redirect"" url =""article.aspx?id={R:1}&amp;title={R:2}"" />
</rule>
</rules>
</rewrite>";
var condList = new List<Condition>();
condList.Add(new Condition
{
Input = InputParser.ParseInputString("{HTTPS}"),
MatchPattern = new Regex("^OFF$")
});
var expected = new List<UrlRewriteRule>();
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}&amp;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}&amp;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<Condition> 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<UrlRewriteRule> expected, List<UrlRewriteRule> 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);
}
}
}
}
}

View File

@ -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>}", "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<FormatException>(() => 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;
}
}
}