Modifies .Rewrite and .Redirect to use normal regex syntax for backreferences

This commit is contained in:
Justin Kotalik 2016-08-29 13:55:44 -07:00
parent 28133b6808
commit 4a06b37280
12 changed files with 305 additions and 239 deletions

View File

@ -27,7 +27,7 @@ namespace RewriteSample
// TODO make this startup do something useful.
app.UseRewriter(new RewriteOptions()
.Rewrite(@"foo/(\d+)", "foo?id={R:1}")
.Rewrite(@"foo/(\d+)", "foo?id=$1")
.ImportFromUrlRewrite(hostingEnv, "UrlRewrite.xml")
.ImportFromModRewrite(hostingEnv, "Rewrite.txt"));

View File

@ -0,0 +1,66 @@
// 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;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Rewrite.Internal.CodeRules
{
public class RedirectRule : Rule
{
private readonly TimeSpan _regexTimeout = TimeSpan.FromSeconds(1);
public Regex InitialMatch { get; }
public string Replacement { get; }
public int StatusCode { get; }
public RedirectRule(string regex, string replacement, int statusCode)
{
InitialMatch = new Regex(regex, RegexOptions.Compiled | RegexOptions.CultureInvariant, _regexTimeout);
Replacement = replacement;
StatusCode = statusCode;
}
public override void ApplyRule(RewriteContext context)
{
var path = context.HttpContext.Request.Path;
Match initMatchResults;
if (path == PathString.Empty)
{
initMatchResults = InitialMatch.Match(path.ToString());
}
else
{
initMatchResults = InitialMatch.Match(path.ToString().Substring(1));
}
if (initMatchResults.Success)
{
var newPath = initMatchResults.Result(Replacement);
var response = context.HttpContext.Response;
response.StatusCode = StatusCode;
if (newPath.IndexOf("://", StringComparison.Ordinal) == -1 && !newPath.StartsWith("/"))
{
newPath = '/' + newPath;
}
var split = newPath.IndexOf('?');
if (split >= 0)
{
var query = context.HttpContext.Request.QueryString.Add(
QueryString.FromUriComponent(
newPath.Substring(split)));
// not using the HttpContext.Response.redirect here because status codes may be 301, 302, 307, 308
response.Headers[HeaderNames.Location] = newPath.Substring(0, split) + query;
}
else
{
response.Headers[HeaderNames.Location] = newPath;
}
context.Result = RuleTermination.ResponseComplete;
}
}
}
}

View File

@ -0,0 +1,94 @@
// 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;
using Microsoft.AspNetCore.Http.Extensions;
namespace Microsoft.AspNetCore.Rewrite.Internal.CodeRules
{
public class RewriteRule : Rule
{
private readonly string ForwardSlash = "/";
private readonly TimeSpan _regexTimeout = TimeSpan.FromSeconds(1);
public Regex InitialMatch { get; }
public string Replacement { get; }
public bool StopProcessing { get; }
public RewriteRule(string regex, string replacement, bool stopProcessing)
{
InitialMatch = new Regex(regex, RegexOptions.Compiled | RegexOptions.CultureInvariant, _regexTimeout);
Replacement = replacement;
StopProcessing = stopProcessing;
}
public override void ApplyRule(RewriteContext context)
{
var path = context.HttpContext.Request.Path;
Match initMatchResults;
if (path == PathString.Empty)
{
initMatchResults = InitialMatch.Match(path.ToString());
}
else
{
initMatchResults = InitialMatch.Match(path.ToString().Substring(1));
}
if (initMatchResults.Success)
{
var result = initMatchResults.Result(Replacement);
var request = context.HttpContext.Request;
if (result.IndexOf("://", StringComparison.Ordinal) >= 0)
{
string scheme;
HostString host;
PathString pathString;
QueryString query;
FragmentString fragment;
UriHelper.FromAbsolute(result, out scheme, out host, out pathString, out query, out fragment);
request.Scheme = scheme;
request.Host = host;
request.Path = pathString;
request.QueryString = query.Add(request.QueryString);
}
else
{
var split = result.IndexOf('?');
if (split >= 0)
{
var newPath = result.Substring(0, split);
if (newPath.StartsWith(ForwardSlash))
{
request.Path = PathString.FromUriComponent(newPath);
}
else
{
request.Path = PathString.FromUriComponent(ForwardSlash + newPath);
}
request.QueryString = request.QueryString.Add(
QueryString.FromUriComponent(
result.Substring(split)));
}
else
{
if (result.StartsWith(ForwardSlash))
{
request.Path = PathString.FromUriComponent(result);
}
else
{
request.Path = PathString.FromUriComponent(ForwardSlash + result);
}
}
}
if (StopProcessing)
{
context.Result = RuleTermination.StopRules;
}
}
}
}
}

View File

@ -1,70 +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;
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Rewrite.Internal.ModRewrite
{
public class ModRewriteRedirectAction : UrlAction
{
public int StatusCode { get; }
public bool QueryStringAppend { get; }
public bool QueryStringDelete { get; }
public bool EscapeBackReferences { get; }
public ModRewriteRedirectAction(
int statusCode,
Pattern pattern,
bool queryStringAppend,
bool queryStringDelete,
bool escapeBackReferences)
{
StatusCode = statusCode;
Url = pattern;
QueryStringAppend = queryStringAppend;
QueryStringDelete = queryStringDelete;
EscapeBackReferences = escapeBackReferences;
}
public override void ApplyAction(RewriteContext context, MatchResults ruleMatch, MatchResults condMatch)
{
var pattern = Url.Evaluate(context, ruleMatch, condMatch);
if (EscapeBackReferences)
{
// because escapebackreferences will be encapsulated by the pattern, just escape the pattern
pattern = Uri.EscapeDataString(pattern);
}
context.HttpContext.Response.StatusCode = StatusCode;
// url can either contain the full url or the path and query
// always add to location header.
// TODO check for false positives
var split = pattern.IndexOf('?');
if (split >= 0 && QueryStringAppend)
{
var query = context.HttpContext.Request.QueryString.Add(
QueryString.FromUriComponent(
pattern.Substring(split)));
// not using the response.redirect here because status codes may be 301, 302, 307, 308
context.HttpContext.Response.Headers[HeaderNames.Location] = pattern.Substring(0, split) + query;
}
else
{
// If the request url has a query string and the target does not, append the query string
// by default.
if (QueryStringDelete)
{
context.HttpContext.Response.Headers[HeaderNames.Location] = pattern;
}
else
{
context.HttpContext.Response.Headers[HeaderNames.Location] = pattern + context.HttpContext.Request.QueryString;
}
}
context.Result = RuleTermination.ResponseComplete;
}
}
}

View File

@ -1,113 +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;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
namespace Microsoft.AspNetCore.Rewrite.Internal.ModRewrite
{
public class ModRewriteRewriteAction : UrlAction
{
private readonly string ForwardSlash = "/";
public RuleTermination Result { get; }
public bool QueryStringAppend { get; }
public bool QueryStringDelete { get; }
public bool EscapeBackReferences { get; }
public ModRewriteRewriteAction(
RuleTermination result,
Pattern pattern,
bool queryStringAppend,
bool queryStringDelete,
bool escapeBackReferences)
{
Result = result;
Url = pattern;
QueryStringAppend = queryStringAppend;
QueryStringDelete = queryStringDelete;
EscapeBackReferences = escapeBackReferences;
}
public override void ApplyAction(RewriteContext context, MatchResults ruleMatch, MatchResults condMatch)
{
var pattern = Url.Evaluate(context, ruleMatch, condMatch);
// TODO PERF, substrings, object creation, etc.
if (pattern.IndexOf("://", StringComparison.Ordinal) >= 0)
{
string scheme;
HostString host;
PathString path;
QueryString query;
FragmentString fragment;
UriHelper.FromAbsolute(pattern, out scheme, out host, out path, out query, out fragment);
if (query.HasValue)
{
if (QueryStringAppend)
{
context.HttpContext.Request.QueryString = context.HttpContext.Request.QueryString.Add(query);
}
else
{
context.HttpContext.Request.QueryString = query;
}
}
else if (QueryStringDelete)
{
context.HttpContext.Request.QueryString = QueryString.Empty;
}
context.HttpContext.Request.Scheme = scheme;
context.HttpContext.Request.Host = host;
context.HttpContext.Request.Path = path;
}
else
{
var split = pattern.IndexOf('?');
if (split >= 0)
{
var path = pattern.Substring(0, split);
if (path.StartsWith(ForwardSlash))
{
context.HttpContext.Request.Path = PathString.FromUriComponent(path);
}
else
{
context.HttpContext.Request.Path = PathString.FromUriComponent(ForwardSlash + path);
}
if (QueryStringAppend)
{
context.HttpContext.Request.QueryString = context.HttpContext.Request.QueryString.Add(
QueryString.FromUriComponent(
pattern.Substring(split)));
}
else
{
context.HttpContext.Request.QueryString = QueryString.FromUriComponent(
pattern.Substring(split));
}
}
else
{
if (pattern.StartsWith(ForwardSlash))
{
context.HttpContext.Request.Path = PathString.FromUriComponent(pattern);
}
else
{
context.HttpContext.Request.Path = PathString.FromUriComponent(ForwardSlash + pattern);
}
if (QueryStringDelete)
{
context.HttpContext.Request.QueryString = QueryString.Empty;
}
}
}
context.Result = Result;
}
}
}

View File

@ -210,13 +210,13 @@ namespace Microsoft.AspNetCore.Rewrite.Internal.ModRewrite
{
throw new FormatException(Resources.FormatError_InputParserInvalidInteger(statusCode, -1));
}
_action = new ModRewriteRedirectAction(res, pattern, queryStringAppend, queryStringDelete, escapeBackReference);
_action = new RedirectAction(res, pattern, queryStringAppend, queryStringDelete, escapeBackReference);
}
else
{
var last = flags.HasFlag(FlagType.End) || flags.HasFlag(FlagType.Last);
var termination = last ? RuleTermination.StopRules : RuleTermination.Continue;
_action = new ModRewriteRewriteAction(termination, pattern, queryStringAppend, queryStringDelete, escapeBackReference);
_action = new RewriteAction(termination, pattern, queryStringAppend, queryStringDelete, escapeBackReference);
}
}
}

View File

@ -2,7 +2,9 @@
// 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 Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Rewrite.Internal.PatternSegments;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Rewrite.Internal.UrlActions
@ -10,41 +12,80 @@ namespace Microsoft.AspNetCore.Rewrite.Internal.UrlActions
public class RedirectAction : UrlAction
{
public int StatusCode { get; }
public bool AppendQueryString { get; }
public bool QueryStringAppend { get; }
public bool QueryStringDelete { get; }
public bool EscapeBackReferences { get; }
public RedirectAction(int statusCode, Pattern pattern, bool appendQueryString)
public RedirectAction(
int statusCode,
Pattern pattern,
bool queryStringAppend,
bool queryStringDelete,
bool escapeBackReferences)
{
StatusCode = statusCode;
Url = pattern;
AppendQueryString = appendQueryString;
QueryStringAppend = queryStringAppend;
QueryStringDelete = queryStringDelete;
EscapeBackReferences = escapeBackReferences;
}
public RedirectAction(
int statusCode,
Pattern pattern,
bool queryStringAppend)
: this(
statusCode,
pattern,
queryStringAppend,
queryStringDelete: true,
escapeBackReferences: false)
{
}
public override void ApplyAction(RewriteContext context, MatchResults ruleMatch, MatchResults condMatch)
{
var pattern = Url.Evaluate(context, ruleMatch, condMatch);
context.HttpContext.Response.StatusCode = StatusCode;
var response = context.HttpContext.Response;
if (EscapeBackReferences)
{
// because escapebackreferences will be encapsulated by the pattern, just escape the pattern
pattern = Uri.EscapeDataString(pattern);
}
// TODO IIS guarantees that there will be a leading slash
if (pattern.IndexOf("://", StringComparison.Ordinal) == -1 && !pattern.StartsWith("/"))
{
pattern = '/' + pattern;
}
response.StatusCode = StatusCode;
// url can either contain the full url or the path and query
// always add to location header.
// TODO check for false positives
var split = pattern.IndexOf('?');
if (split >= 0 && AppendQueryString)
if (split >= 0 && QueryStringAppend)
{
var query = context.HttpContext.Request.QueryString.Add(
QueryString.FromUriComponent(
pattern.Substring(split)));
// not using the HttpContext.Response.redirect here because status codes may be 301, 302, 307, 308
context.HttpContext.Response.Headers[HeaderNames.Location] = pattern.Substring(0, split) + query;
// not using the response.redirect here because status codes may be 301, 302, 307, 308
response.Headers[HeaderNames.Location] = pattern.Substring(0, split) + query;
}
else
{
context.HttpContext.Response.Headers[HeaderNames.Location] = pattern;
// If the request url has a query string and the target does not, append the query string
// by default.
if (QueryStringDelete)
{
response.Headers[HeaderNames.Location] = pattern;
}
else
{
response.Headers[HeaderNames.Location] = pattern + context.HttpContext.Request.QueryString;
}
}
context.Result = RuleTermination.ResponseComplete;
}

View File

@ -11,24 +11,48 @@ namespace Microsoft.AspNetCore.Rewrite.Internal.UrlActions
{
private readonly string ForwardSlash = "/";
public RuleTermination Result { get; }
public bool ClearQuery { get; }
public bool QueryStringAppend { get; }
public bool QueryStringDelete { get; }
public bool EscapeBackReferences { get; }
public RewriteAction(RuleTermination result, Pattern pattern, bool clearQuery)
public RewriteAction(
RuleTermination result,
Pattern pattern,
bool queryStringAppend,
bool queryStringDelete,
bool escapeBackReferences)
{
Result = result;
Url = pattern;
ClearQuery = clearQuery;
QueryStringAppend = queryStringAppend;
QueryStringDelete = queryStringDelete;
EscapeBackReferences = escapeBackReferences;
}
public RewriteAction(
RuleTermination result,
Pattern pattern,
bool queryStringAppend):
this (result,
pattern,
queryStringAppend,
queryStringDelete: false,
escapeBackReferences: false)
{
}
public override void ApplyAction(RewriteContext context, MatchResults ruleMatch, MatchResults condMatch)
{
var pattern = Url.Evaluate(context, ruleMatch, condMatch);
if (ClearQuery)
var request = context.HttpContext.Request;
if (EscapeBackReferences)
{
context.HttpContext.Request.QueryString = QueryString.Empty;
// because escapebackreferences will be encapsulated by the pattern, just escape the pattern
pattern = Uri.EscapeDataString(pattern);
}
// TODO PERF, substrings, object creation, etc.
if (pattern.IndexOf("://", StringComparison.Ordinal) >= 0)
{
string scheme;
@ -38,10 +62,25 @@ namespace Microsoft.AspNetCore.Rewrite.Internal.UrlActions
FragmentString fragment;
UriHelper.FromAbsolute(pattern, out scheme, out host, out path, out query, out fragment);
context.HttpContext.Request.Scheme = scheme;
context.HttpContext.Request.Host = host;
context.HttpContext.Request.Path = path;
context.HttpContext.Request.QueryString = query.Add(context.HttpContext.Request.QueryString);
if (query.HasValue)
{
if (QueryStringAppend)
{
request.QueryString = request.QueryString.Add(query);
}
else
{
request.QueryString = query;
}
}
else if (QueryStringDelete)
{
request.QueryString = QueryString.Empty;
}
request.Scheme = scheme;
request.Host = host;
request.Path = path;
}
else
{
@ -51,25 +90,39 @@ namespace Microsoft.AspNetCore.Rewrite.Internal.UrlActions
var path = pattern.Substring(0, split);
if (path.StartsWith(ForwardSlash))
{
context.HttpContext.Request.Path = PathString.FromUriComponent(path);
request.Path = PathString.FromUriComponent(path);
}
else
{
context.HttpContext.Request.Path = PathString.FromUriComponent(ForwardSlash + path);
request.Path = PathString.FromUriComponent(ForwardSlash + path);
}
if (QueryStringAppend)
{
request.QueryString = request.QueryString.Add(
QueryString.FromUriComponent(
pattern.Substring(split)));
}
else
{
request.QueryString = QueryString.FromUriComponent(
pattern.Substring(split));
}
context.HttpContext.Request.QueryString = context.HttpContext.Request.QueryString.Add(
QueryString.FromUriComponent(
pattern.Substring(split)));
}
else
{
if (pattern.StartsWith(ForwardSlash))
{
context.HttpContext.Request.Path = PathString.FromUriComponent(pattern);
request.Path = PathString.FromUriComponent(pattern);
}
else
{
context.HttpContext.Request.Path = PathString.FromUriComponent(ForwardSlash + pattern);
request.Path = PathString.FromUriComponent(ForwardSlash + pattern);
}
if (QueryStringDelete)
{
request.QueryString = QueryString.Empty;
}
}
}

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Rewrite.Internal.ModRewrite;
using Microsoft.AspNetCore.Rewrite.Internal.UrlActions;
using Microsoft.AspNetCore.Rewrite.Internal.UrlMatches;
@ -46,7 +47,7 @@ namespace Microsoft.AspNetCore.Rewrite.Internal.UrlRewrite
break;
case ActionType.Rewrite:
_action = new RewriteAction(stopProcessing ? RuleTermination.StopRules : RuleTermination.Continue,
url, clearQuery: !appendQueryString);
url, appendQueryString);
break;
case ActionType.Redirect:
_action = new RedirectAction(statusCode, url, appendQueryString);

View File

@ -42,11 +42,11 @@ namespace Microsoft.AspNetCore.Rewrite
/// </summary>
/// <param name="options">The Rewrite options.</param>
/// <param name="regex">The regex string to compare with.</param>
/// <param name="urlPattern">If the regex matches, what to replace HttpContext with.</param>
/// <param name="replacement">If the regex matches, what to replace HttpContext with.</param>
/// <returns>The Rewrite options.</returns>
public static RewriteOptions Rewrite(this RewriteOptions options, string regex, string urlPattern)
public static RewriteOptions Rewrite(this RewriteOptions options, string regex, string replacement)
{
return Rewrite(options, regex, urlPattern, stopProcessing: false);
return Rewrite(options, regex, replacement, stopProcessing: false);
}
/// <summary>
@ -54,17 +54,12 @@ namespace Microsoft.AspNetCore.Rewrite
/// </summary>
/// <param name="options">The Rewrite options.</param>
/// <param name="regex">The regex string to compare with.</param>
/// <param name="urlPattern">If the regex matches, what to replace the uri with.</param>
/// <param name="replacement">If the regex matches, what to replace the uri with.</param>
/// <param name="stopProcessing">If the regex matches, conditionally stop processing other rules.</param>
/// <returns>The Rewrite options.</returns>
public static RewriteOptions Rewrite(this RewriteOptions options, string regex, string urlPattern, bool stopProcessing)
public static RewriteOptions Rewrite(this RewriteOptions options, string regex, string replacement, bool stopProcessing)
{
var builder = new UrlRewriteRuleBuilder();
var pattern = new InputParser().ParseInputString(urlPattern);
builder.AddUrlMatch(regex);
builder.AddUrlAction(pattern, actionType: ActionType.Rewrite, stopProcessing: stopProcessing);
options.Rules.Add(builder.Build());
options.Rules.Add(new RewriteRule(regex, replacement, stopProcessing));
return options;
}
@ -73,11 +68,11 @@ namespace Microsoft.AspNetCore.Rewrite
/// </summary>
/// <param name="options">The Rewrite options.</param>
/// <param name="regex">The regex string to compare with.</param>
/// <param name="urlPattern">If the regex matches, what to replace the uri with.</param>
/// <param name="replacement">If the regex matches, what to replace the uri with.</param>
/// <returns>The Rewrite options.</returns>
public static RewriteOptions Redirect(this RewriteOptions options, string regex, string urlPattern)
public static RewriteOptions Redirect(this RewriteOptions options, string regex, string replacement)
{
return Redirect(options, regex, urlPattern, statusCode: 302);
return Redirect(options, regex, replacement, statusCode: 302);
}
/// <summary>
@ -85,21 +80,19 @@ namespace Microsoft.AspNetCore.Rewrite
/// </summary>
/// <param name="options">The Rewrite options.</param>
/// <param name="regex">The regex string to compare with.</param>
/// <param name="urlPattern">If the regex matches, what to replace the uri with.</param>
/// <param name="replacement">If the regex matches, what to replace the uri with.</param>
/// <param name="statusCode">The status code to add to the response.</param>
/// <returns>The Rewrite options.</returns>
public static RewriteOptions Redirect(this RewriteOptions options, string regex, string urlPattern, int statusCode)
public static RewriteOptions Redirect(this RewriteOptions options, string regex, string replacement, int statusCode)
{
var builder = new UrlRewriteRuleBuilder();
var pattern = new InputParser().ParseInputString(urlPattern);
builder.AddUrlMatch(regex);
builder.AddUrlAction(pattern, actionType: ActionType.Redirect, stopProcessing: false);
options.Rules.Add(builder.Build());
options.Rules.Add(new RedirectRule(regex, replacement, statusCode));
return options;
}
// TODO 301 overload
public static RewriteOptions RedirectToHttpsPermanent(this RewriteOptions options)
{
return RedirectToHttps(options, statusCode: 301, sslPort: null);
}
/// <summary>
/// Redirect a request to https if the incoming request is http

View File

@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Rewrite.Tests.CodeRules
[Fact]
public async Task CheckRewritePath()
{
var options = new RewriteOptions().Rewrite("(.*)", "http://example.com/{R:1}");
var options = new RewriteOptions().Rewrite("(.*)", "http://example.com/$1");
var builder = new WebHostBuilder()
.Configure(app =>
{
@ -39,7 +39,7 @@ namespace Microsoft.AspNetCore.Rewrite.Tests.CodeRules
[Fact]
public async Task CheckRedirectPath()
{
var options = new RewriteOptions().Redirect("(.*)","http://example.com/{R:1}", statusCode: 301);
var options = new RewriteOptions().Redirect("(.*)","http://example.com/$1", statusCode: 301);
var builder = new WebHostBuilder()
.Configure(app =>
{

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Rewrite.Internal;
using Microsoft.AspNetCore.Rewrite.Internal.ModRewrite;
using Microsoft.AspNetCore.Rewrite.Internal.UrlActions;
using Microsoft.AspNetCore.Rewrite.Internal.UrlMatches;
using Microsoft.AspNetCore.Rewrite.Internal.UrlRewrite;
@ -146,7 +147,7 @@ namespace Microsoft.AspNetCore.Rewrite.Tests.UrlRewrite
)
{
return new UrlRewriteRule(name, new RegexMatch(new Regex("^OFF$"), false), conditions,
new RewriteAction(RuleTermination.Continue, new InputParser().ParseInputString(url), clearQuery: false));
new RewriteAction(RuleTermination.Continue, new InputParser().ParseInputString(url), queryStringAppend: false));
}
// TODO make rules comparable?