Update structure of code rules, refactor to IISRewrite

This commit is contained in:
Justin Kotalik 2016-08-08 14:26:57 -07:00 committed by Justin
parent 52c7a94db3
commit 903949cc74
21 changed files with 538 additions and 438 deletions

View File

@ -5,6 +5,9 @@ MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.HttpOverrides", "src\Microsoft.AspNetCore.HttpOverrides\Microsoft.AspNetCore.HttpOverrides.xproj", "{517308C3-B477-4B01-B461-CAB9C10B6928}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A5076D28-FA7E-4606-9410-FEDD0D603527}"
ProjectSection(SolutionItems) = preProject
global.json = global.json
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{8437B0F3-3894-4828-A945-A9187F37631D}"
EndProject

View File

@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Rewrite;
using Microsoft.AspNetCore.Rewrite.Internal;
namespace RewriteSample
{
@ -13,9 +14,29 @@ namespace RewriteSample
{
public void Configure(IApplicationBuilder app, IHostingEnvironment hostingEnv)
{
// Four main use cases for Rewrite Options.
// 1. Importing from a UrlRewrite file, which are IIS Rewrite rules.
// This file is in xml format, starting with the <rewrite> tag.
// 2. Importing from a mod_rewrite file, which are mod_rewrite rules.
// This file is in standard mod_rewrite format which only contains rewrite information.
// 3. Inline rules in code, where you can specify rules such as rewrites and redirects
// based on certain conditions. Ex: RedirectToHttps will check if the request is https,
// else it will redirect the request with https.
// 4. Functional rules. If a user has a very specific function they would like to implement
// (ex StringReplace) that are easy to implement in code, they can do so by calling
// AddFunctionalRule(Func);
// TODO make this startup do something useful.
app.UseRewriter(new RewriteOptions()
.ImportFromUrlRewrite(hostingEnv, "UrlRewrite.xml")
.ImportFromModRewrite(hostingEnv, "Rewrite.txt"));
.ImportFromModRewrite(hostingEnv, "Rewrite.txt")
.RedirectToHttps(StatusCodes.Status307TemporaryRedirect)
.RewriteRule("/foo/(.*)/bar", "{R:1}/bar")
.AddRule(ctx =>
{
ctx.HttpContext.Request.Path = "/index";
return RuleResult.Continue;
}));
app.Run(context => context.Response.WriteAsync(context.Request.Path));
}

View File

@ -3,8 +3,9 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Rewrite.Internal;
using Microsoft.AspNetCore.Rewrite.Internal.CodeRules;
using Microsoft.AspNetCore.Rewrite.Internal.UrlRewrite;
namespace Microsoft.AspNetCore.Rewrite
{
@ -35,69 +36,69 @@ namespace Microsoft.AspNetCore.Rewrite
return options;
}
/// <summary>
/// Creates a rewrite path rule.
/// </summary>
/// <param name="options">The Url rewrite options.</param>
/// <param name="regex">The string regex pattern to compare against the http context.</param>
/// <param name="newPath">The string to replace the path with (with capture parameters).</param>
/// <param name="stopRewriteOnSuccess">Whether or not to stop rewriting on success of rule.</param>
/// <returns></returns>
public static RewriteOptions RewritePath(this RewriteOptions options, string regex, string newPath, bool stopRewriteOnSuccess = false)
public static RewriteOptions RewriteRule(this RewriteOptions options, string regex, string onMatch)
{
options.Rules.Add(new PathRule { MatchPattern = new Regex(regex, RegexOptions.Compiled, TimeSpan.FromMilliseconds(1)), OnMatch = newPath, OnCompletion = stopRewriteOnSuccess ? Transformation.TerminatingRewrite : Transformation.Rewrite });
return RewriteRule(options, regex, onMatch, stopProcessing: false);
}
public static RewriteOptions RewriteRule(this RewriteOptions options, string regex, string onMatch, bool stopProcessing)
{
var builder = new UrlRewriteRuleBuilder();
var pattern = InputParser.ParseInputString(onMatch);
builder.AddUrlMatch(regex);
builder.AddUrlAction(pattern, actionType: ActionType.Rewrite, stopProcessing: stopProcessing);
options.Rules.Add(builder.Build());
return options;
}
/// <summary>
/// Rewrite http to https.
/// </summary>
/// <param name="options">The Url rewrite options.</param>
/// <param name="stopRewriteOnSuccess">Whether or not to stop rewriting on success of rule.</param>
/// <returns></returns>
public static RewriteOptions RewriteScheme(this RewriteOptions options, bool stopRewriteOnSuccess = false)
public static RewriteOptions RedirectRule(this RewriteOptions options, string regex, string onMatch, int statusCode)
{
options.Rules.Add(new SchemeRule {OnCompletion = stopRewriteOnSuccess ? Transformation.TerminatingRewrite : Transformation.Rewrite });
return RedirectRule(options, regex, onMatch, statusCode, stopProcessing: false);
}
public static RewriteOptions RedirectRule(this RewriteOptions options, string regex, string onMatch, int statusCode, bool stopProcessing)
{
var builder = new UrlRewriteRuleBuilder();
var pattern = InputParser.ParseInputString(onMatch);
builder.AddUrlMatch(regex);
builder.AddUrlAction(pattern, actionType: ActionType.Redirect, stopProcessing: stopProcessing);
options.Rules.Add(builder.Build());
return options;
}
/// <summary>
/// Redirect a path to another path.
/// </summary>
/// <param name="options">The Url rewrite options.</param>
/// <param name="regex">The string regex pattern to compare against the http context.</param>
/// <param name="newPath">The string to replace the path with (with capture parameters).</param>
/// <param name="stopRewriteOnSuccess">Whether or not to stop rewriting on success of rule.</param>
/// <returns></returns>
public static RewriteOptions RedirectPath(this RewriteOptions options, string regex, string newPath, bool stopRewriteOnSuccess = false)
public static RewriteOptions RedirectToHttps(this RewriteOptions options, int statusCode)
{
options.Rules.Add(new PathRule { MatchPattern = new Regex(regex, RegexOptions.Compiled, TimeSpan.FromMilliseconds(1)), OnMatch = newPath, OnCompletion = Transformation.Redirect });
return RedirectToHttps(options, statusCode, null);
}
// TODO Don't do this, it doesn't work in all cases. Will refactor tonight/ tomorrow.
public static RewriteOptions RedirectToHttps(this RewriteOptions options, int statusCode, int? sslPort)
{
options.Rules.Add(new RedirectToHttpsRule { StatusCode = statusCode, SSLPort = sslPort });
return options;
}
/// <summary>
/// Redirect http to https.
/// </summary>
/// <param name="options">The Url rewrite options.</param>
/// <param name="sslPort">The port to redirect the scheme to.</param>
/// <returns></returns>
public static RewriteOptions RedirectScheme(this RewriteOptions options, int? sslPort)
public static RewriteOptions RewriteToHttps(this RewriteOptions options)
{
return RewriteToHttps(options, sslPort: null, stopProcessing: false);
}
public static RewriteOptions RewriteToHttps(this RewriteOptions options, int? sslPort)
{
options.Rules.Add(new SchemeRule { SSLPort = sslPort, OnCompletion = Transformation.Redirect });
return RewriteToHttps(options, sslPort, stopProcessing: false);
}
public static RewriteOptions RewriteToHttps(this RewriteOptions options, int? sslPort, bool stopProcessing)
{
options.Rules.Add(new RewriteToHttpsRule {SSLPort = sslPort, stopProcessing = stopProcessing });
return options;
}
/// <summary>
/// User generated rule to do a specific match on a path and what to do on success of the match.
/// </summary>
/// <param name="options"></param>
/// <param name="onApplyRule"></param>
/// <param name="transform"></param>
/// <param name="description"></param>
/// <returns></returns>
public static RewriteOptions CustomRule(this RewriteOptions options, Func<RewriteContext, RuleResult> onApplyRule, Transformation transform, string description = null)
public static RewriteOptions AddRule(this RewriteOptions options, Func<RewriteContext, RuleResult> rule)
{
options.Rules.Add(new FunctionalRule { OnApplyRule = onApplyRule, OnCompletion = transform});
options.Rules.Add(new FunctionalRule { OnApplyRule = rule});
return options;
}
}

View File

@ -3,12 +3,11 @@
using System;
namespace Microsoft.AspNetCore.Rewrite.Internal
namespace Microsoft.AspNetCore.Rewrite.Internal.CodeRules
{
public class FunctionalRule : Rule
{
public Func<RewriteContext, RuleResult> OnApplyRule { get; set; }
public Transformation OnCompletion { get; set; } = Transformation.Rewrite;
public override RuleResult ApplyRule(RewriteContext context) => OnApplyRule(context);
}
}

View File

@ -4,12 +4,12 @@
using System.Text;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Rewrite.Internal
namespace Microsoft.AspNetCore.Rewrite.Internal.CodeRules
{
public class SchemeRule : Rule
public class RedirectToHttpsRule : Rule
{
public int? SSLPort { get; set; }
public Transformation OnCompletion { get; set; } = Transformation.Rewrite;
public int StatusCode { get; set; }
public override RuleResult ApplyRule(RewriteContext context)
{
@ -28,20 +28,6 @@ namespace Microsoft.AspNetCore.Rewrite.Internal
host = new HostString(host.Host);
}
if ((OnCompletion != Transformation.Redirect))
{
context.HttpContext.Request.Scheme = "https";
context.HttpContext.Request.Host = host;
if (OnCompletion == Transformation.TerminatingRewrite)
{
return RuleResult.StopRules;
}
else
{
return RuleResult.Continue;
}
}
var req = context.HttpContext.Request;
var newUrl = new StringBuilder().Append("https://").Append(host).Append(req.PathBase).Append(req.Path).Append(req.QueryString);
@ -51,4 +37,4 @@ namespace Microsoft.AspNetCore.Rewrite.Internal
return RuleResult.Continue;
}
}
}
}

View File

@ -0,0 +1,42 @@

using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Rewrite.Internal.CodeRules
{
public class RewriteToHttpsRule : Rule
{
public bool stopProcessing { get; set; }
public int? SSLPort { get; set; }
public override RuleResult ApplyRule(RewriteContext context)
{
// TODO this only does http to https, add more features in the future.
if (!context.HttpContext.Request.IsHttps)
{
var host = context.HttpContext.Request.Host;
if (SSLPort.HasValue && SSLPort.Value > 0)
{
// a specific SSL port is specified
host = new HostString(host.Host, SSLPort.Value);
}
else
{
// clear the port
host = new HostString(host.Host);
}
context.HttpContext.Request.Scheme = "https";
context.HttpContext.Request.Host = host;
if (stopProcessing)
{
return RuleResult.StopRules;
}
else
{
return RuleResult.Continue;
}
}
return RuleResult.Continue;
}
}
}

View File

@ -3,7 +3,7 @@
using Microsoft.AspNetCore.Rewrite.Internal.ModRewrite.Operands;
namespace Microsoft.AspNetCore.Rewrite.Internal
namespace Microsoft.AspNetCore.Rewrite.Internal.ModRewrite
{
public class RuleExpression
{

View File

@ -1,48 +0,0 @@
// 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.Internal
{
public class PathRule : Rule
{
public Regex MatchPattern { get; set; }
public string OnMatch { get; set; }
public Transformation OnCompletion { get; set; } = Transformation.Rewrite;
public override RuleResult ApplyRule(RewriteContext context)
{
var matches = MatchPattern.Match(context.HttpContext.Request.Path);
if (matches.Success)
{
// New method here to translate the outgoing format string to the correct value.
var path = matches.Result(OnMatch);
if (OnCompletion == Transformation.Redirect)
{
var req = context.HttpContext.Request;
var newUrl = string.Concat(
req.Scheme,
"://",
req.PathBase,
path,
req.QueryString);
context.HttpContext.Response.Redirect(newUrl);
return RuleResult.ResponseComplete;
}
else
{
context.HttpContext.Request.Path = path;
}
if (OnCompletion == Transformation.TerminatingRewrite)
{
return RuleResult.StopRules;
}
else
{
return RuleResult.Continue;
}
}
return RuleResult.Continue;
}
}
}

View File

@ -1,12 +0,0 @@
// 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.Internal
{
public enum Transformation
{
Rewrite,
Redirect,
TerminatingRewrite
}
}

View File

@ -1,12 +0,0 @@
// 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.Internal.UrlRewrite
{
public class ParsedCondition
{
public bool Negate { get; set; }
public bool IgnoreCase { get; set; } = true;
public MatchType MatchType { get; set; } = MatchType.Pattern;
}
}

View File

@ -1,14 +0,0 @@
// 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.Internal.UrlRewrite
{
public class ParsedUrlAction
{
public ActionType Type { get; set; }
public Pattern Url { get; set; }
public bool AppendQueryString { get; set; } = true;
public bool LogRewrittenUrl { get; set; } // Ignoring this flag.
public RedirectType RedirectType { get; set; } = RedirectType.Permanent;
}
}

View File

@ -1,11 +0,0 @@
// 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.Internal.UrlRewrite
{
public class ParsedUrlMatch
{
public bool IgnoreCase { get; set; }
public bool Negate { get; set; }
}
}

View File

@ -26,7 +26,7 @@ namespace Microsoft.AspNetCore.Rewrite.Internal.UrlRewrite
public const string Input = "input";
public const string Pattern = "pattern";
public const string Type = "type";
public const string AppendQuery = "appendQueryString";
public const string AppendQueryString = "appendQueryString";
public const string LogRewrittenUrl = "logRewrittenUrl";
public const string RedirectType = "redirectType";
}

View File

@ -8,7 +8,6 @@ namespace Microsoft.AspNetCore.Rewrite.Internal.UrlRewrite
public abstract class UrlAction
{
public Pattern Url { get; set; }
public abstract RuleResult ApplyAction(HttpContext context, MatchResults ruleMatch, MatchResults condMatch);
}
}

View File

@ -5,11 +5,8 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Linq;
using Microsoft.AspNetCore.Rewrite.Internal.UrlRewrite.UrlActions;
using Microsoft.AspNetCore.Rewrite.Internal.UrlRewrite.UrlMatches;
namespace Microsoft.AspNetCore.Rewrite.Internal.UrlRewrite
{
@ -27,14 +24,14 @@ namespace Microsoft.AspNetCore.Rewrite.Internal.UrlRewrite
var result = new List<UrlRewriteRule>();
// TODO Global rules are currently not treated differently than normal rules, fix.
// See: https://github.com/aspnet/BasicMiddleware/issues/59
ParseRules(xmlRoot.Descendants(RewriteTags.GlobalRules).FirstOrDefault(), result, isGlobalRule: true);
ParseRules(xmlRoot.Descendants(RewriteTags.Rules).FirstOrDefault(), result, isGlobalRule: false);
ParseRules(xmlRoot.Descendants(RewriteTags.GlobalRules).FirstOrDefault(), result);
ParseRules(xmlRoot.Descendants(RewriteTags.Rules).FirstOrDefault(), result);
return result;
}
return null;
}
private static void ParseRules(XElement rules, List<UrlRewriteRule> result, bool isGlobalRule)
private static void ParseRules(XElement rules, List<UrlRewriteRule> result)
{
if (rules == null)
{
@ -43,297 +40,176 @@ namespace Microsoft.AspNetCore.Rewrite.Internal.UrlRewrite
foreach (var rule in rules.Elements(RewriteTags.Rule))
{
var res = new UrlRewriteRule();
SetRuleAttributes(rule, res);
var action = rule.Element(RewriteTags.Action);
if (action == null)
var builder = new UrlRewriteRuleBuilder();
ParseRuleAttributes(rule, builder);
if (builder.Enabled)
{
ThrowUrlFormatException(rule, "Rule does not have an associated action attribute");
}
CreateUrlAction(action, res, isGlobalRule);
if (res.Enabled)
{
result.Add(res);
result.Add(builder.Build());
}
}
}
private static void SetRuleAttributes(XElement rule, UrlRewriteRule res)
private static void ParseRuleAttributes(XElement rule, UrlRewriteRuleBuilder builder)
{
res.Name = rule.Attribute(RewriteTags.Name)?.Value;
builder.Name = rule.Attribute(RewriteTags.Name)?.Value;
bool enabled;
if (bool.TryParse(rule.Attribute(RewriteTags.Enabled)?.Value, out enabled))
if (!bool.TryParse(rule.Attribute(RewriteTags.Enabled)?.Value, out enabled))
{
res.Enabled = enabled;
builder.Enabled = true;
}
PatternSyntax patternSyntax;
if (Enum.TryParse(rule.Attribute(RewriteTags.PatternSyntax)?.Value, out patternSyntax))
if (!Enum.TryParse(rule.Attribute(RewriteTags.PatternSyntax)?.Value, out patternSyntax))
{
res.PatternSyntax = patternSyntax;
patternSyntax = PatternSyntax.ECMAScript;
}
bool stopProcessing;
if (bool.TryParse(rule.Attribute(RewriteTags.StopProcessing)?.Value, out stopProcessing))
if (!bool.TryParse(rule.Attribute(RewriteTags.StopProcessing)?.Value, out stopProcessing))
{
res.StopProcessing = stopProcessing;
stopProcessing = false;
}
var match = rule.Element(RewriteTags.Match);
if (match == null)
{
ThrowUrlFormatException(rule, "Cannot have rule without match");
}
CreateMatch(match, res);
CreateConditions(rule.Element(RewriteTags.Conditions), res);
var action = rule.Element(RewriteTags.Action);
if (action == null)
{
ThrowUrlFormatException(rule, "Rule does not have an associated action attribute");
}
ParseMatch(match, builder, patternSyntax);
ParseConditions(rule.Element(RewriteTags.Conditions), builder, patternSyntax);
ParseUrlAction(action, builder, stopProcessing);
}
private static void CreateMatch(XElement match, UrlRewriteRule res)
private static void ParseMatch(XElement match, UrlRewriteRuleBuilder builder, PatternSyntax patternSyntax)
{
var matchRes = new ParsedUrlMatch();
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 (parsedInputString == null)
{
ThrowUrlFormatException(match, "Match must have Url Attribute");
}
switch (res.PatternSyntax)
bool ignoreCase;
if (!bool.TryParse(match.Attribute(RewriteTags.IgnoreCase)?.Value, out ignoreCase))
{
case PatternSyntax.ECMAScript:
{
if (matchRes.IgnoreCase)
{
var regex = new Regex(parsedInputString, RegexOptions.Compiled | RegexOptions.IgnoreCase, RegexTimeout);
res.InitialMatch = new RegexMatch(regex, matchRes.Negate);
}
else
{
var regex = new Regex(parsedInputString, RegexOptions.Compiled, RegexTimeout);
res.InitialMatch = new RegexMatch(regex, matchRes.Negate);
}
}
break;
case PatternSyntax.WildCard:
throw new NotImplementedException("Wildcard syntax is not supported.");
case PatternSyntax.ExactMatch:
res.InitialMatch = new ExactMatch(matchRes.IgnoreCase, parsedInputString, matchRes.Negate);
break;
ignoreCase = true; // default
}
bool negate;
if (!bool.TryParse(match.Attribute(RewriteTags.Negate)?.Value, out negate))
{
negate = false;
}
builder.AddUrlMatch(parsedInputString, ignoreCase, negate, patternSyntax);
}
private static void CreateConditions(XElement conditions, UrlRewriteRule res)
private static void ParseConditions(XElement conditions, UrlRewriteRuleBuilder builder, PatternSyntax patternSyntax)
{
// This is to avoid nullptr exception on referencing conditions.
res.Conditions = new Conditions();
if (conditions == null)
{
return;
}
LogicalGrouping grouping;
if (Enum.TryParse(conditions.Attribute(RewriteTags.MatchType)?.Value, out grouping))
if (!Enum.TryParse(conditions.Attribute(RewriteTags.MatchType)?.Value, out grouping))
{
res.Conditions.MatchType = grouping;
grouping = LogicalGrouping.MatchAll;
}
bool parBool;
if (bool.TryParse(conditions.Attribute(RewriteTags.TrackingAllCaptures)?.Value, out parBool))
bool trackingAllCaptures;
if (!bool.TryParse(conditions.Attribute(RewriteTags.TrackingAllCaptures)?.Value, out trackingAllCaptures))
{
res.Conditions.TrackingAllCaptures = parBool;
trackingAllCaptures = false;
}
builder.AddUrlConditions(grouping, trackingAllCaptures);
foreach (var cond in conditions.Elements(RewriteTags.Add))
{
CreateCondition(cond, res);
ParseCondition(cond, builder, patternSyntax);
}
}
private static void CreateCondition(XElement condition, UrlRewriteRule res)
private static void ParseCondition(XElement condition, UrlRewriteRuleBuilder builder, PatternSyntax patternSyntax)
{
var parsedCondRes = new ParsedCondition();
bool parBool;
if (bool.TryParse(condition.Attribute(RewriteTags.IgnoreCase)?.Value, out parBool))
bool ignoreCase;
if (!bool.TryParse(condition.Attribute(RewriteTags.IgnoreCase)?.Value, out ignoreCase))
{
parsedCondRes.IgnoreCase = parBool;
ignoreCase = true;
}
if (bool.TryParse(condition.Attribute(RewriteTags.Negate)?.Value, out parBool))
bool negate;
if (!bool.TryParse(condition.Attribute(RewriteTags.Negate)?.Value, out negate))
{
parsedCondRes.Negate = parBool;
ignoreCase = false;
}
MatchType matchType;
if (Enum.TryParse(condition.Attribute(RewriteTags.MatchType)?.Value, out matchType))
if (!Enum.TryParse(condition.Attribute(RewriteTags.MatchType)?.Value, out matchType))
{
parsedCondRes.MatchType = matchType;
matchType = MatchType.Pattern;
}
var parsedString = condition.Attribute(RewriteTags.Input)?.Value;
if (parsedString == null)
var parsedInputString = condition.Attribute(RewriteTags.Input)?.Value;
if (parsedInputString == null)
{
ThrowUrlFormatException(condition, "Conditions must have an input attribute");
}
var parsedPatternString = condition.Attribute(RewriteTags.Pattern)?.Value;
Pattern input = null;
try
{
input = InputParser.ParseInputString(parsedString);
input = InputParser.ParseInputString(parsedInputString);
builder.AddUrlCondition(input, parsedPatternString, patternSyntax, matchType, ignoreCase, negate);
}
catch (FormatException formatException)
{
ThrowUrlFormatException(condition, formatException.Message, formatException);
}
switch (res.PatternSyntax)
{
case PatternSyntax.ECMAScript:
{
switch (parsedCondRes.MatchType)
{
case MatchType.Pattern:
{
parsedString = condition.Attribute(RewriteTags.Pattern)?.Value;
if (parsedString == null)
{
ThrowUrlFormatException(condition, "Match does not have an associated pattern attribute in condition");
}
Regex regex = null;
if (parsedCondRes.IgnoreCase)
{
regex = new Regex(parsedString, RegexOptions.Compiled | RegexOptions.IgnoreCase, RegexTimeout);
}
else
{
regex = new Regex(parsedString, RegexOptions.Compiled, RegexTimeout);
}
res.Conditions.ConditionList.Add(new Condition { Input = input, Match = new RegexMatch(regex, parsedCondRes.Negate) });
}
break;
case MatchType.IsDirectory:
{
res.Conditions.ConditionList.Add(new Condition { Input = input, Match = new IsDirectoryMatch(parsedCondRes.Negate) });
}
break;
case MatchType.IsFile:
{
res.Conditions.ConditionList.Add(new Condition { Input = input, Match = new IsFileMatch(parsedCondRes.Negate) });
}
break;
default:
// TODO don't this this can ever be thrown
ThrowUrlFormatException(condition, "Unrecognized matchType");
break;
}
}
break;
case PatternSyntax.WildCard:
throw new NotImplementedException("Wildcard syntax is not supported");
case PatternSyntax.ExactMatch:
parsedString = condition.Attribute(RewriteTags.Pattern)?.Value;
if (parsedString == null)
{
ThrowUrlFormatException(condition, "Pattern match does not have an associated pattern attribute in condition");
}
res.Conditions.ConditionList.Add(new Condition { Input = input, Match = new ExactMatch(parsedCondRes.IgnoreCase, parsedString, parsedCondRes.Negate) });
break;
default:
ThrowUrlFormatException(condition, "Unrecognized pattern syntax");
break;
}
}
private static void CreateUrlAction(XElement urlAction, UrlRewriteRule res, bool globalRule)
private static void ParseUrlAction(XElement urlAction, UrlRewriteRuleBuilder builder, bool stopProcessing)
{
var actionRes = new ParsedUrlAction();
ActionType actionType;
if (Enum.TryParse(urlAction.Attribute(RewriteTags.Type)?.Value, out actionType))
if (!Enum.TryParse(urlAction.Attribute(RewriteTags.Type)?.Value, out actionType))
{
actionRes.Type = actionType;
actionType = ActionType.None;
}
bool parseBool;
if (bool.TryParse(urlAction.Attribute(RewriteTags.AppendQuery)?.Value, out parseBool))
bool appendQuery;
if (!bool.TryParse(urlAction.Attribute(RewriteTags.AppendQueryString)?.Value, out appendQuery))
{
actionRes.AppendQueryString = parseBool;
}
if (bool.TryParse(urlAction.Attribute(RewriteTags.LogRewrittenUrl)?.Value, out parseBool))
{
actionRes.LogRewrittenUrl = parseBool;
appendQuery = true;
}
RedirectType redirectType;
if (Enum.TryParse(urlAction.Attribute(RewriteTags.RedirectType)?.Value, out redirectType))
if (!Enum.TryParse(urlAction.Attribute(RewriteTags.RedirectType)?.Value, out redirectType))
{
actionRes.RedirectType = redirectType;
redirectType = RedirectType.Permanent;
}
try
{
actionRes.Url = InputParser.ParseInputString(urlAction.Attribute(RewriteTags.Url)?.Value);
var input = InputParser.ParseInputString(urlAction.Attribute(RewriteTags.Url)?.Value);
builder.AddUrlAction(input, actionType, appendQuery, stopProcessing, (int)redirectType);
}
catch (FormatException formatException)
{
ThrowUrlFormatException(urlAction, formatException.Message, formatException);
}
CreateUrlActionFromParsedAction(urlAction, actionRes, globalRule, res);
}
private static void CreateUrlActionFromParsedAction(XElement urlAction, ParsedUrlAction actionRes, bool globalRule, UrlRewriteRule res)
{
switch (actionRes.Type)
{
case ActionType.None:
res.Action = new VoidAction();
break;
case ActionType.Rewrite:
if (actionRes.AppendQueryString)
{
res.Action = new RewriteAction(res.StopProcessing ? RuleTerminiation.StopRules : RuleTerminiation.Continue, actionRes.Url, clearQuery: false);
}
else
{
res.Action = new RewriteAction(res.StopProcessing ? RuleTerminiation.StopRules : RuleTerminiation.Continue, actionRes.Url, clearQuery: true);
}
break;
case ActionType.Redirect:
if (actionRes.AppendQueryString)
{
res.Action = new RedirectAction((int)actionRes.RedirectType, actionRes.Url);
}
else
{
res.Action = new RedirectClearQueryAction((int)actionRes.RedirectType, actionRes.Url);
}
break;
case ActionType.AbortRequest:
ThrowUrlFormatException(urlAction, "Abort Requests are not supported.");
break;
case ActionType.CustomResponse:
// TODO
ThrowUrlFormatException(urlAction, "Custom Responses are not supported");
break;
}
}
private static void ThrowUrlFormatException(XElement element, string message)
@ -342,6 +218,7 @@ namespace Microsoft.AspNetCore.Rewrite.Internal.UrlRewrite
var col = ((IXmlLineInfo)element).LinePosition;
throw new FormatException(Resources.FormatError_UrlRewriteParseError(message, line, col));
}
private static void ThrowUrlFormatException(XElement element, string message, Exception ex)
{
var line = ((IXmlLineInfo)element).LineNumber;

View File

@ -0,0 +1,168 @@
// 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.RegularExpressions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Rewrite.Internal.UrlRewrite.UrlActions;
using Microsoft.AspNetCore.Rewrite.Internal.UrlRewrite.UrlMatches;
namespace Microsoft.AspNetCore.Rewrite.Internal.UrlRewrite
{
public class UrlRewriteRuleBuilder
{
private readonly TimeSpan RegexTimeout = TimeSpan.FromMilliseconds(1);
public string Name { get; set; }
public bool Enabled { get; set; }
private UrlMatch _initialMatch;
private Conditions _conditions;
private UrlAction _action;
public UrlRewriteRule Build()
{
// TODO some of these are required fields, throw if null?
var rule = new UrlRewriteRule();
rule.Action = _action;
rule.Conditions = _conditions;
rule.InitialMatch = _initialMatch;
rule.Name = Name;
return rule;
}
public void AddUrlAction(Pattern url, ActionType actionType = ActionType.None, bool appendQueryString = true, bool stopProcessing = false, int statusCode = StatusCodes.Status301MovedPermanently)
{
switch (actionType)
{
case ActionType.None:
_action = new VoidAction();
break;
case ActionType.Rewrite:
if (appendQueryString)
{
_action = new RewriteAction(stopProcessing ? RuleTerminiation.StopRules : RuleTerminiation.Continue, url, clearQuery: false);
}
else
{
_action = new RewriteAction(stopProcessing ? RuleTerminiation.StopRules : RuleTerminiation.Continue, url, clearQuery: true);
}
break;
case ActionType.Redirect:
if (appendQueryString)
{
_action = new RedirectAction(statusCode, url);
}
else
{
_action = new RedirectClearQueryAction(statusCode, url);
}
break;
case ActionType.AbortRequest:
throw new FormatException("Abort Requests are not supported");
case ActionType.CustomResponse:
// TODO
throw new FormatException("Custom Responses are not supported");
}
}
public void AddUrlMatch(string input, bool ignoreCase = true, bool negate = false, PatternSyntax patternSyntax = PatternSyntax.ECMAScript)
{
switch (patternSyntax)
{
case PatternSyntax.ECMAScript:
{
if (ignoreCase)
{
var regex = new Regex(input, RegexOptions.Compiled | RegexOptions.IgnoreCase, RegexTimeout);
_initialMatch = new RegexMatch(regex, negate);
}
else
{
var regex = new Regex(input, RegexOptions.Compiled, RegexTimeout);
_initialMatch = new RegexMatch(regex, negate);
}
break;
}
case PatternSyntax.WildCard:
throw new NotImplementedException("Wildcard syntax is not supported");
case PatternSyntax.ExactMatch:
_initialMatch = new ExactMatch(ignoreCase, input, negate);
break;
}
}
// TODO make this take two overloads and handle regex vs non regex case.
public void AddUrlCondition(Pattern input, string pattern, PatternSyntax patternSyntax, MatchType matchType, bool ignoreCase, bool negate)
{
if (_conditions == null)
{
AddUrlConditions(LogicalGrouping.MatchAll, trackingAllCaptures: false);
}
switch (patternSyntax)
{
case PatternSyntax.ECMAScript:
{
switch (matchType)
{
case MatchType.Pattern:
{
if (pattern == null)
{
throw new FormatException("Match does not have an associated pattern attribute in condition");
}
Regex regex = null;
if (ignoreCase)
{
regex = new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase, RegexTimeout);
}
else
{
regex = new Regex(pattern, RegexOptions.Compiled, RegexTimeout);
}
_conditions.ConditionList.Add(new Condition { Input = input, Match = new RegexMatch(regex, negate) });
break;
}
case MatchType.IsDirectory:
{
_conditions.ConditionList.Add(new Condition { Input = input, Match = new IsDirectoryMatch(negate) });
break;
}
case MatchType.IsFile:
{
_conditions.ConditionList.Add(new Condition { Input = input, Match = new IsFileMatch(negate) });
break;
}
default:
// TODO new exception handling
throw new FormatException("Unrecognized matchType");
}
break;
}
case PatternSyntax.WildCard:
throw new NotImplementedException("Wildcard syntax is not supported");
case PatternSyntax.ExactMatch:
if (pattern == null)
{
throw new FormatException("Match does not have an associated pattern attribute in condition");
}
_conditions.ConditionList.Add(new Condition { Input = input, Match = new ExactMatch(ignoreCase, pattern, negate) });
break;
default:
throw new FormatException("Unrecognized pattern syntax");
}
}
public void AddUrlConditions(LogicalGrouping logicalGrouping, bool trackingAllCaptures)
{
var conditions = new Conditions();
conditions.ConditionList = new List<Condition>();
conditions.MatchType = logicalGrouping;
conditions.TrackingAllCaptures = trackingAllCaptures;
_conditions = conditions;
}
}
}

View File

@ -7,8 +7,6 @@ namespace Microsoft.AspNetCore.Rewrite.Internal.UrlRewrite
{
public string Name { get; set; }
public bool Enabled { get; set; } = true;
public PatternSyntax PatternSyntax { get; set; }
public bool StopProcessing { get; set; }
public UrlMatch InitialMatch { get; set; }
public Conditions Conditions { get; set; }
public UrlAction Action { get; set; }
@ -19,8 +17,10 @@ namespace Microsoft.AspNetCore.Rewrite.Internal.UrlRewrite
{
return RuleResult.Continue;
}
// Due to the path string always having a leading slash,
// remove it from the path before regex comparison
// TODO may need to check if there is a leading slash and remove conditionally
var initMatchRes = InitialMatch.Evaluate(context.HttpContext.Request.Path.ToString().Substring(1), context);
if (!initMatchRes.Success)
@ -28,10 +28,14 @@ namespace Microsoft.AspNetCore.Rewrite.Internal.UrlRewrite
return RuleResult.Continue;
}
var condMatchRes = Conditions.Evaluate(context, initMatchRes);
if (!condMatchRes.Success)
MatchResults condMatchRes = null;
if (Conditions != null)
{
return RuleResult.Continue;
condMatchRes = Conditions.Evaluate(context, initMatchRes);
if (!condMatchRes.Success)
{
return RuleResult.Continue;
}
}
// at this point we know the rule passed, evaluate the replacement.

View File

@ -0,0 +1,90 @@
// 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.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.TestHost;
using Xunit;
namespace Microsoft.AspNetCore.Rewrite.Tests.CodeRules
{
public class MiddlewareTests
{
[Fact]
public async Task CheckRewritePath()
{
var options = new RewriteOptions().RewriteRule("(.*)", "http://example.com/{R:1}");
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseRewriter(options);
app.UseRewriter(options);
app.Run(context => context.Response.WriteAsync(
context.Request.Scheme +
"://" +
context.Request.Host +
context.Request.Path +
context.Request.QueryString));
});
var server = new TestServer(builder);
var response = await server.CreateClient().GetStringAsync("foo");
Assert.Equal(response, "http://example.com/foo");
}
[Fact]
public async Task CheckRedirectPath()
{
var options = new RewriteOptions().RedirectRule("(.*)","http://example.com/{R:1}", statusCode: 301);
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseRewriter(options);
});
var server = new TestServer(builder);
var response = await server.CreateClient().GetAsync("foo");
Assert.Equal(response.Headers.Location.OriginalString, "http://example.com/foo");
}
[Fact]
public async Task CheckRewriteToHttps()
{
var options = new RewriteOptions().RewriteToHttps();
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseRewriter(options);
app.UseRewriter(options);
app.Run(context => context.Response.WriteAsync(
context.Request.Scheme));
});
var server = new TestServer(builder);
var response = await server.CreateClient().GetStringAsync(new Uri("http://example.com"));
Assert.Equal(response, "https");
}
[Fact]
public async Task CheckRedirectToHttps()
{
var options = new RewriteOptions().RedirectToHttps(statusCode: 301);
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseRewriter(options);
});
var server = new TestServer(builder);
var response = await server.CreateClient().GetAsync(new Uri("http://example.com"));
Assert.Equal(response.Headers.Location.OriginalString, "https://example.com/");
}
}
}

View File

@ -150,8 +150,6 @@ namespace Microsoft.AspNetCore.Rewrite.Tests.UrlRewrite
Action = new RewriteAction(RuleTerminiation.Continue, InputParser.ParseInputString(Url), clearQuery: false),
Name = name,
Enabled = enabled,
StopProcessing = stopProcessing,
PatternSyntax = patternSyntax,
InitialMatch = new RegexMatch(new Regex("^OFF$"), false)
{
},
@ -164,29 +162,37 @@ namespace Microsoft.AspNetCore.Rewrite.Tests.UrlRewrite
};
}
private void AssertUrlRewriteRuleEquality(List<UrlRewriteRule> expected, List<UrlRewriteRule> actual)
private void AssertUrlRewriteRuleEquality(List<UrlRewriteRule> actual, List<UrlRewriteRule> expected)
{
Assert.Equal(expected.Count, actual.Count);
for (var i = 0; i < expected.Count; i++)
Assert.Equal(actual.Count, expected.Count);
for (var i = 0; i < actual.Count; i++)
{
var r1 = expected[i];
var r2 = actual[i];
var r1 = actual[i];
var r2 = expected[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);
// 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++)
if (r1.Conditions == null)
{
var c1 = r1.Conditions.ConditionList[j];
var c2 = r2.Conditions.ConditionList[j];
Assert.Equal(c1.Input.PatternSegments.Count, c2.Input.PatternSegments.Count);
Assert.Equal(r2.Conditions.ConditionList.Count, 0);
}
else if (r2.Conditions == null)
{
Assert.Equal(r1.Conditions.ConditionList.Count, 0);
}
else
{
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.Input.PatternSegments.Count, c2.Input.PatternSegments.Count);
}
}
}
}

View File

@ -38,7 +38,7 @@ namespace Microsoft.AspNetCore.Rewrite.Tests.UrlRewrite
</rule>
</rules>
</rewrite>",
"Could not parse the UrlRewrite file. Message: 'Abort Requests are not supported.'. Line number '5': '14'.")]
"Could not parse the UrlRewrite file. Message: 'Abort Requests are not supported'. Line number '5': '14'.")]
[InlineData(
@"<rewrite>
<rules>
@ -54,6 +54,7 @@ namespace Microsoft.AspNetCore.Rewrite.Tests.UrlRewrite
<rules>
<rule name=""Rewrite to article.aspx"">
<match />
<action type=""Rewrite"" url=""foo"" />
</rule>
</rules>
</rewrite>",

View File

@ -19,13 +19,13 @@ namespace Microsoft.AspNetCore.Rewrite.Tests.UrlRewrite
public async Task Invoke_RedirectPathToPathAndQuery()
{
var options = new RewriteOptions().ImportFromUrlRewrite(new StringReader(@"<rewrite>
<rules>
<rule name=""Rewrite to article.aspx"">
<match url = ""^article/([0-9]+)/([_0-9a-z-]+)"" />
<action type=""Redirect"" url =""article.aspx?id={R:1}&amp;title={R:2}"" />
</rule>
</rules>
</rewrite>"));
<rules>
<rule name=""Rewrite to article.aspx"">
<match url = ""^article/([0-9]+)/([_0-9a-z-]+)"" />
<action type=""Redirect"" url =""article.aspx?id={R:1}&amp;title={R:2}"" />
</rule>
</rules>
</rewrite>"));
var builder = new WebHostBuilder()
.Configure(app =>
{
@ -43,13 +43,13 @@ namespace Microsoft.AspNetCore.Rewrite.Tests.UrlRewrite
public async Task Invoke_RewritePathToPathAndQuery()
{
var options = new RewriteOptions().ImportFromUrlRewrite(new StringReader(@"<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>"));
<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 builder = new WebHostBuilder()
.Configure(app =>
{
@ -121,13 +121,13 @@ namespace Microsoft.AspNetCore.Rewrite.Tests.UrlRewrite
var options = new RewriteOptions().ImportFromUrlRewrite(new StringReader(@"<rewrite>
<rules>
<rule name=""Remove trailing slash"" stopProcessing=""true"">
<match url=""(.*)/$"" />
<conditions>
<add input=""{REQUEST_FILENAME}"" matchType=""IsFile"" negate=""true"" />
<add input=""{REQUEST_FILENAME}"" matchType=""IsDirectory"" negate=""true"" />
</conditions>
<action type=""Redirect"" redirectType=""Permanent"" url=""{R:1}"" />
</rule>
<match url=""(.*)/$"" />
<conditions>
<add input=""{REQUEST_FILENAME}"" matchType=""IsFile"" negate=""true"" />
<add input=""{REQUEST_FILENAME}"" matchType=""IsDirectory"" negate=""true"" />
</conditions>
<action type=""Redirect"" redirectType=""Permanent"" url=""{R:1}"" />
</rule>
</rules>
</rewrite>"));
var builder = new WebHostBuilder()
@ -148,13 +148,13 @@ namespace Microsoft.AspNetCore.Rewrite.Tests.UrlRewrite
var options = new RewriteOptions().ImportFromUrlRewrite(new StringReader(@"<rewrite>
<rules>
<rule name=""Add trailing slash"" stopProcessing=""true"">
<match url=""(.*[^/])$"" />
<conditions>
<add input=""{REQUEST_FILENAME}"" matchType=""IsFile"" negate=""true"" />
<add input=""{REQUEST_FILENAME}"" matchType=""IsDirectory"" negate=""true"" />
</conditions>
<action type=""Redirect"" redirectType=""Permanent"" url=""{R:1}/"" />
</rule>
<match url=""(.*[^/])$"" />
<conditions>
<add input=""{REQUEST_FILENAME}"" matchType=""IsFile"" negate=""true"" />
<add input=""{REQUEST_FILENAME}"" matchType=""IsDirectory"" negate=""true"" />
</conditions>
<action type=""Redirect"" redirectType=""Permanent"" url=""{R:1}/"" />
</rule>
</rules>
</rewrite>"));
var builder = new WebHostBuilder()
@ -174,13 +174,13 @@ namespace Microsoft.AspNetCore.Rewrite.Tests.UrlRewrite
{
var options = new RewriteOptions().ImportFromUrlRewrite(new StringReader(@"<rewrite>
<rules>
<rule name=""Redirect to HTTPS"" stopProcessing=""true"">
<match url=""(.*)"" />
<conditions>
<add input=""{HTTPS}"" pattern=""^OFF$"" />
</conditions>
<action type=""Redirect"" url=""https://{HTTP_HOST}/{R:1}"" redirectType=""Permanent"" />
</rule>
<rule name=""Redirect to HTTPS"" stopProcessing=""true"">
<match url=""(.*)"" />
<conditions>
<add input=""{HTTPS}"" pattern=""^OFF$"" />
</conditions>
<action type=""Redirect"" url=""https://{HTTP_HOST}/{R:1}"" redirectType=""Permanent"" />
</rule>
</rules>
</rewrite>"));
var builder = new WebHostBuilder()
@ -200,13 +200,13 @@ namespace Microsoft.AspNetCore.Rewrite.Tests.UrlRewrite
{
var options = new RewriteOptions().ImportFromUrlRewrite(new StringReader(@"<rewrite>
<rules>
<rule name=""Rewrite to HTTPS"" stopProcessing=""true"">
<match url=""(.*)"" />
<conditions>
<add input=""{HTTPS}"" pattern=""^OFF$"" />
</conditions>
<action type=""Rewrite"" url=""https://{HTTP_HOST}/{R:1}"" />
</rule>
<rule name=""Rewrite to HTTPS"" stopProcessing=""true"">
<match url=""(.*)"" />
<conditions>
<add input=""{HTTPS}"" pattern=""^OFF$"" />
</conditions>
<action type=""Rewrite"" url=""https://{HTTP_HOST}/{R:1}"" />
</rule>
</rules>
</rewrite>"));
var builder = new WebHostBuilder()
@ -214,10 +214,10 @@ namespace Microsoft.AspNetCore.Rewrite.Tests.UrlRewrite
{
app.UseRewriter(options);
app.Run(context => context.Response.WriteAsync(
context.Request.Scheme +
context.Request.Scheme +
"://" +
context.Request.Host +
context.Request.Path +
context.Request.Path +
context.Request.QueryString));
});
var server = new TestServer(builder);
@ -232,10 +232,10 @@ namespace Microsoft.AspNetCore.Rewrite.Tests.UrlRewrite
{
var options = new RewriteOptions().ImportFromUrlRewrite(new StringReader(@"<rewrite>
<rules>
<rule name=""Proxy"">
<match url=""(.*)"" />
<action type=""Rewrite"" url=""http://internalserver/{R:1}"" />
</rule>
<rule name=""Proxy"">
<match url=""(.*)"" />
<action type=""Rewrite"" url=""http://internalserver/{R:1}"" />
</rule>
</rules>
</rewrite>"));
var builder = new WebHostBuilder()