diff --git a/src/Microsoft.AspNetCore.Rewrite/Internal/ApacheModRewrite/CookieActionFactory.cs b/src/Microsoft.AspNetCore.Rewrite/Internal/ApacheModRewrite/CookieActionFactory.cs new file mode 100644 index 0000000000..27cb805064 --- /dev/null +++ b/src/Microsoft.AspNetCore.Rewrite/Internal/ApacheModRewrite/CookieActionFactory.cs @@ -0,0 +1,120 @@ +// 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 Microsoft.AspNetCore.Rewrite.Internal.UrlActions; + +namespace Microsoft.AspNetCore.Rewrite.Internal.ApacheModRewrite +{ + public class CookieActionFactory + { + /// + /// Creates a for details. + /// + /// The flag + /// The action + public ChangeCookieAction Create(string flagValue) + { + if (string.IsNullOrEmpty(flagValue)) + { + throw new ArgumentException(nameof(flagValue)); + } + + var i = 0; + var separator = ':'; + if (flagValue[0] == ';') + { + separator = ';'; + i++; + } + + ChangeCookieAction action = null; + var currentField = Fields.Name; + var start = i; + for (; i < flagValue.Length; i++) + { + if (flagValue[i] == separator) + { + var length = i - start; + SetActionOption(flagValue.Substring(start, length).Trim(), currentField, ref action); + + currentField++; + start = i + 1; + } + } + + if (i != start) + { + SetActionOption(flagValue.Substring(start).Trim(new[] { ' ', separator }), currentField, ref action); + } + + if (currentField < Fields.Domain) + { + throw new FormatException(Resources.FormatError_InvalidChangeCookieFlag(flagValue)); + } + + return action; + } + + private static void SetActionOption(string value, Fields tokenType, ref ChangeCookieAction action) + { + switch (tokenType) + { + case Fields.Name: + action = new ChangeCookieAction(value); + break; + case Fields.Value: + action.Value = value; + break; + case Fields.Domain: + // despite what spec says, an empty domain field is allowed in mod_rewrite + // by specifying NAME:VALUE:; + action.Domain = string.IsNullOrEmpty(value) || value == ";" + ? null + : value; + break; + case Fields.Lifetime: + if (string.IsNullOrEmpty(value)) + { + break; + } + + uint minutes; + if (!uint.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out minutes)) + { + throw new FormatException(Resources.FormatError_CouldNotParseInteger(value)); + } + + action.Lifetime = TimeSpan.FromMinutes(minutes); + break; + case Fields.Path: + action.Path = value; + break; + case Fields.Secure: + action.Secure = "secure".Equals(value, StringComparison.OrdinalIgnoreCase) + || "true".Equals(value, StringComparison.OrdinalIgnoreCase) + || value == "1"; + break; + case Fields.HttpOnly: + action.HttpOnly = "httponly".Equals(value, StringComparison.OrdinalIgnoreCase) + || "true".Equals(value, StringComparison.OrdinalIgnoreCase) + || value == "1"; + break; + } + } + + // order matters + // see https://httpd.apache.org/docs/current/rewrite/flags.html#flag_co + private enum Fields + { + Name, + Value, + Domain, + Lifetime, + Path, + Secure, + HttpOnly + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Rewrite/Internal/ApacheModRewrite/FlagParser.cs b/src/Microsoft.AspNetCore.Rewrite/Internal/ApacheModRewrite/FlagParser.cs index b3e957a5c7..3d736d7b56 100644 --- a/src/Microsoft.AspNetCore.Rewrite/Internal/ApacheModRewrite/FlagParser.cs +++ b/src/Microsoft.AspNetCore.Rewrite/Internal/ApacheModRewrite/FlagParser.cs @@ -59,7 +59,7 @@ namespace Microsoft.AspNetCore.Rewrite.Internal.ApacheModRewrite { if (string.IsNullOrEmpty(flagString)) { - throw new ArgumentNullException(nameof(flagString)); + throw new ArgumentException(nameof(flagString)); } // Check that flags are contained within [] diff --git a/src/Microsoft.AspNetCore.Rewrite/Internal/ApacheModRewrite/RuleBuilder.cs b/src/Microsoft.AspNetCore.Rewrite/Internal/ApacheModRewrite/RuleBuilder.cs index 8e93d1db16..7bb0d1cd3f 100644 --- a/src/Microsoft.AspNetCore.Rewrite/Internal/ApacheModRewrite/RuleBuilder.cs +++ b/src/Microsoft.AspNetCore.Rewrite/Internal/ApacheModRewrite/RuleBuilder.cs @@ -14,6 +14,7 @@ namespace Microsoft.AspNetCore.Rewrite.Internal.ApacheModRewrite private IList _conditions; private IList _actions = new List(); private UrlMatch _match; + private CookieActionFactory _cookieActionFactory = new CookieActionFactory(); private readonly TimeSpan RegexTimeout = TimeSpan.FromMilliseconds(1); @@ -172,8 +173,8 @@ namespace Microsoft.AspNetCore.Rewrite.Internal.ApacheModRewrite string flag; if (flags.GetValue(FlagType.Cookie, out flag)) { - // parse cookie - _actions.Add(new ChangeCookieAction(flag)); + var action = _cookieActionFactory.Create(flag); + _actions.Add(action); } if (flags.GetValue(FlagType.Env, out flag)) diff --git a/src/Microsoft.AspNetCore.Rewrite/Internal/RedirectRule.cs b/src/Microsoft.AspNetCore.Rewrite/Internal/RedirectRule.cs index 2c9f471040..1198f27a6a 100644 --- a/src/Microsoft.AspNetCore.Rewrite/Internal/RedirectRule.cs +++ b/src/Microsoft.AspNetCore.Rewrite/Internal/RedirectRule.cs @@ -18,12 +18,12 @@ namespace Microsoft.AspNetCore.Rewrite.Internal { if (string.IsNullOrEmpty(regex)) { - throw new ArgumentNullException(nameof(regex)); + throw new ArgumentException(nameof(regex)); } if (string.IsNullOrEmpty(replacement)) { - throw new ArgumentNullException(nameof(replacement)); + throw new ArgumentException(nameof(replacement)); } InitialMatch = new Regex(regex, RegexOptions.Compiled | RegexOptions.CultureInvariant, _regexTimeout); diff --git a/src/Microsoft.AspNetCore.Rewrite/Internal/RewriteRule.cs b/src/Microsoft.AspNetCore.Rewrite/Internal/RewriteRule.cs index 336c2ba87d..d6452e8bc8 100644 --- a/src/Microsoft.AspNetCore.Rewrite/Internal/RewriteRule.cs +++ b/src/Microsoft.AspNetCore.Rewrite/Internal/RewriteRule.cs @@ -18,12 +18,12 @@ namespace Microsoft.AspNetCore.Rewrite.Internal { if (string.IsNullOrEmpty(regex)) { - throw new ArgumentNullException(nameof(regex)); + throw new ArgumentException(nameof(regex)); } if (string.IsNullOrEmpty(replacement)) { - throw new ArgumentNullException(nameof(replacement)); + throw new ArgumentException(nameof(replacement)); } InitialMatch = new Regex(regex, RegexOptions.Compiled | RegexOptions.CultureInvariant, _regexTimeout); diff --git a/src/Microsoft.AspNetCore.Rewrite/Internal/UrlActions/ChangeCookieAction.cs b/src/Microsoft.AspNetCore.Rewrite/Internal/UrlActions/ChangeCookieAction.cs index 3bdd95e990..640a022672 100644 --- a/src/Microsoft.AspNetCore.Rewrite/Internal/UrlActions/ChangeCookieAction.cs +++ b/src/Microsoft.AspNetCore.Rewrite/Internal/UrlActions/ChangeCookieAction.cs @@ -2,21 +2,74 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Rewrite.Internal.UrlActions { public class ChangeCookieAction : UrlAction { - public ChangeCookieAction(string cookie) + private readonly Func _timeSource; + private CookieOptions _cachedOptions; + + public ChangeCookieAction(string name) + : this(name, () => DateTimeOffset.UtcNow) { - // TODO - throw new NotImplementedException("Changing the cookie is not implemented"); } + // for testing + internal ChangeCookieAction(string name, Func timeSource) + { + _timeSource = timeSource; + + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException(nameof(name)); + } + + Name = name; + } + + public string Name { get; } + public string Value { get; set; } + public string Domain { get; set; } + public TimeSpan Lifetime { get; set; } + public string Path { get; set; } + public bool Secure { get; set; } + public bool HttpOnly { get; set; } + public override void ApplyAction(RewriteContext context, MatchResults ruleMatch, MatchResults condMatch) { - // modify the cookies - throw new NotImplementedException("Changing the cookie is not implemented"); + var options = GetOrCreateOptions(); + context.HttpContext.Response.Cookies.Append(Name, Value ?? string.Empty, options); + } + + private CookieOptions GetOrCreateOptions() + { + if (Lifetime > TimeSpan.Zero) + { + var now = _timeSource(); + return new CookieOptions() + { + Domain = Domain, + HttpOnly = HttpOnly, + Secure = Secure, + Path = Path, + Expires = now.Add(Lifetime) + }; + } + + if (_cachedOptions == null) + { + _cachedOptions = new CookieOptions() + { + Domain = Domain, + HttpOnly = HttpOnly, + Secure = Secure, + Path = Path + }; + } + + return _cachedOptions; } } } diff --git a/src/Microsoft.AspNetCore.Rewrite/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Rewrite/Properties/Resources.Designer.cs index 3c2e0d6b64..bda94e5268 100644 --- a/src/Microsoft.AspNetCore.Rewrite/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Rewrite/Properties/Resources.Designer.cs @@ -11,19 +11,35 @@ namespace Microsoft.AspNetCore.Rewrite = new ResourceManager("Microsoft.AspNetCore.Rewrite.Resources", typeof(Resources).GetTypeInfo().Assembly); /// - /// Could not parse the UrlRewrite file. Message: '{0}'. Line number '{1}': '{2}'. + /// Error adding a mod_rewrite rule. The change environment flag is not supported. /// - internal static string Error_UrlRewriteParseError + internal static string Error_ChangeEnvironmentNotSupported { - get { return GetString("Error_UrlRewriteParseError"); } + get { return GetString("Error_ChangeEnvironmentNotSupported"); } } /// - /// Could not parse the UrlRewrite file. Message: '{0}'. Line number '{1}': '{2}'. + /// Error adding a mod_rewrite rule. The change environment flag is not supported. /// - internal static string FormatError_UrlRewriteParseError(object p0, object p1, object p2) + internal static string FormatError_ChangeEnvironmentNotSupported() { - return string.Format(CultureInfo.CurrentCulture, GetString("Error_UrlRewriteParseError"), p0, p1, p2); + return GetString("Error_ChangeEnvironmentNotSupported"); + } + + /// + /// Could not parse integer from value '{0}'. + /// + internal static string Error_CouldNotParseInteger + { + get { return GetString("Error_CouldNotParseInteger"); } + } + + /// + /// Could not parse integer from value '{0}'. + /// + internal static string FormatError_CouldNotParseInteger(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("Error_CouldNotParseInteger"), p0); } /// @@ -106,6 +122,38 @@ namespace Microsoft.AspNetCore.Rewrite return string.Format(CultureInfo.CurrentCulture, GetString("Error_InputParserUnrecognizedParameter"), p0, p1); } + /// + /// Syntax error for integers in comparison. + /// + internal static string Error_IntegerMatch_FormatExceptionMessage + { + get { return GetString("Error_IntegerMatch_FormatExceptionMessage"); } + } + + /// + /// Syntax error for integers in comparison. + /// + internal static string FormatError_IntegerMatch_FormatExceptionMessage() + { + return GetString("Error_IntegerMatch_FormatExceptionMessage"); + } + + /// + /// Error parsing the mod_rewrite rule. The cookie flag (CO) has an incorrect format '{0}'. + /// + internal static string Error_InvalidChangeCookieFlag + { + get { return GetString("Error_InvalidChangeCookieFlag"); } + } + + /// + /// Error parsing the mod_rewrite rule. The cookie flag (CO) has an incorrect format '{0}'. + /// + internal static string FormatError_InvalidChangeCookieFlag(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("Error_InvalidChangeCookieFlag"), p0); + } + /// /// Could not parse the mod_rewrite file. Message: '{0}'. Line number '{1}'. /// @@ -139,35 +187,19 @@ namespace Microsoft.AspNetCore.Rewrite } /// - /// Syntax error for integers in comparison. + /// Could not parse the UrlRewrite file. Message: '{0}'. Line number '{1}': '{2}'. /// - internal static string Error_IntegerMatch_FormatExceptionMessage + internal static string Error_UrlRewriteParseError { - get { return GetString("Error_IntegerMatch_FormatExceptionMessage"); } + get { return GetString("Error_UrlRewriteParseError"); } } /// - /// Syntax error for integers in comparison. + /// Could not parse the UrlRewrite file. Message: '{0}'. Line number '{1}': '{2}'. /// - internal static string FormatError_IntegerMatch_FormatExceptionMessage() + internal static string FormatError_UrlRewriteParseError(object p0, object p1, object p2) { - return GetString("Error_IntegerMatch_FormatExceptionMessage"); - } - - /// - /// Error adding a mod_rewrite rule. The change environment flag is not supported. - /// - internal static string Error_ChangeEnvironmentNotSupported - { - get { return GetString("Error_ChangeEnvironmentNotSupported"); } - } - - /// - /// Error adding a mod_rewrite rule. The change environment flag is not supported. - /// - internal static string FormatError_ChangeEnvironmentNotSupported() - { - return GetString("Error_ChangeEnvironmentNotSupported"); + return string.Format(CultureInfo.CurrentCulture, GetString("Error_UrlRewriteParseError"), p0, p1, p2); } private static string GetString(string name, params string[] formatterNames) diff --git a/src/Microsoft.AspNetCore.Rewrite/Resources.resx b/src/Microsoft.AspNetCore.Rewrite/Resources.resx index 7b477cc447..2c68d33ec3 100644 --- a/src/Microsoft.AspNetCore.Rewrite/Resources.resx +++ b/src/Microsoft.AspNetCore.Rewrite/Resources.resx @@ -117,8 +117,11 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Could not parse the UrlRewrite file. Message: '{0}'. Line number '{1}': '{2}'. + + Error adding a mod_rewrite rule. The change environment flag is not supported. + + + Could not parse integer from value '{0}'. Index out of range for backreference: '{0}' at string index: '{1}' @@ -135,16 +138,19 @@ Unrecognized parameter type: '{0}', terminated at string index: '{1}' + + Syntax error for integers in comparison. + + + Error parsing the mod_rewrite rule. The cookie flag (CO) has an incorrect format '{0}'. + Could not parse the mod_rewrite file. Message: '{0}'. Line number '{1}'. Could not parse the mod_rewrite file. Line number '{0}'. - - Syntax error for integers in comparison. - - - Error adding a mod_rewrite rule. The change environment flag is not supported. + + Could not parse the UrlRewrite file. Message: '{0}'. Line number '{1}': '{2}'. \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Rewrite.Tests/ApacheModRewrite/CookieActionFactoryTest.cs b/test/Microsoft.AspNetCore.Rewrite.Tests/ApacheModRewrite/CookieActionFactoryTest.cs new file mode 100644 index 0000000000..d992eb7937 --- /dev/null +++ b/test/Microsoft.AspNetCore.Rewrite.Tests/ApacheModRewrite/CookieActionFactoryTest.cs @@ -0,0 +1,93 @@ +// 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.Internal.ApacheModRewrite; +using Xunit; + +namespace Microsoft.AspNetCore.Rewrite.Test +{ + public class CookieActionFactoryTest + { + [Fact] + public void Creates_OneCookie() + { + var cookie = new CookieActionFactory().Create("NAME:VALUE:DOMAIN:1440:path:secure:httponly"); + + Assert.Equal("NAME", cookie.Name); + Assert.Equal("VALUE", cookie.Value); + Assert.Equal("DOMAIN", cookie.Domain); + Assert.Equal(TimeSpan.FromMinutes(1440), cookie.Lifetime); + Assert.Equal("path", cookie.Path); + Assert.True(cookie.Secure); + Assert.True(cookie.HttpOnly); + } + + [Fact] + public void Creates_OneCookie_AltSeparator() + { + var action = new CookieActionFactory().Create(";NAME;VALUE:WithColon;DOMAIN;1440;path;secure;httponly"); + + Assert.Equal("NAME", action.Name); + Assert.Equal("VALUE:WithColon", action.Value); + Assert.Equal("DOMAIN", action.Domain); + Assert.Equal(TimeSpan.FromMinutes(1440), action.Lifetime); + Assert.Equal("path", action.Path); + Assert.True(action.Secure); + Assert.True(action.HttpOnly); + } + + [Fact] + public void Creates_HttpOnly() + { + var action = new CookieActionFactory().Create(";NAME;VALUE;DOMAIN;;;;httponly"); + + Assert.Equal("NAME", action.Name); + Assert.Equal("VALUE", action.Value); + Assert.Equal("DOMAIN", action.Domain); + Assert.Equal(0, action.Lifetime.TotalSeconds); + Assert.Equal(string.Empty, action.Path); + Assert.False(action.Secure); + Assert.True(action.HttpOnly); + } + + [Theory] + [InlineData("NAME::", "", null)] + [InlineData("NAME::domain", "", "domain")] + [InlineData("NAME:VALUE:;", "VALUE", null)] // special case with dangling ';' + [InlineData("NAME:value:", "value", null)] + [InlineData(" NAME : v : ", "v", null)] // trims values + public void TrimsValues(string flagValue, string value, string domain) + { + var factory = new CookieActionFactory(); + var action = factory.Create(flagValue); + Assert.Equal("NAME", action.Name); + Assert.NotNull(action.Value); + Assert.Equal(value, action.Value); + Assert.Equal(domain, action.Domain); + } + + [Theory] + [InlineData("NAME")] // missing value and domain + [InlineData("NAME: ")] // missing domain + [InlineData("NAME:VALUE")] // missing domain + [InlineData(";NAME;VAL:UE")] // missing domain + public void ThrowsForInvalidFormat(string flagValue) + { + var factory = new CookieActionFactory(); + var ex = Assert.Throws(() => factory.Create(flagValue)); + Assert.Equal(Resources.FormatError_InvalidChangeCookieFlag(flagValue), ex.Message); + } + + [Theory] + [InlineData("bad_number")] + [InlineData("-1")] + [InlineData("0.9")] + public void ThrowsForInvalidIntFormat(string badInt) + { + var factory = new CookieActionFactory(); + var ex = Assert.Throws(() => factory.Create("NAME:VALUE:DOMAIN:" + badInt)); + Assert.Equal(Resources.FormatError_CouldNotParseInteger(badInt), ex.Message); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Rewrite.Tests/ApacheModRewrite/FlagParserTest.cs b/test/Microsoft.AspNetCore.Rewrite.Tests/ApacheModRewrite/FlagParserTest.cs index 8f8d0cf7a8..bfb4c6420f 100644 --- a/test/Microsoft.AspNetCore.Rewrite.Tests/ApacheModRewrite/FlagParserTest.cs +++ b/test/Microsoft.AspNetCore.Rewrite.Tests/ApacheModRewrite/FlagParserTest.cs @@ -62,8 +62,20 @@ namespace Microsoft.AspNetCore.Rewrite.Tests.ModRewrite [Fact] public void FlagParser_AssertArgumentExceptionWhenFlagsAreNullOrEmpty() { - Assert.Throws(() => new FlagParser().Parse(null)); - Assert.Throws(() => new FlagParser().Parse(string.Empty)); + Assert.Throws(() => new FlagParser().Parse(null)); + Assert.Throws(() => new FlagParser().Parse(string.Empty)); + } + + [Theory] + [InlineData("[CO=VAR:VAL]", "VAR:VAL")] + [InlineData("[CO=!VAR]", "!VAR")] + [InlineData("[CO=;NAME:VALUE;ABC:123]", ";NAME:VALUE;ABC:123")] + public void Flag_ParserHandlesComplexFlags(string flagString, string expected) + { + var results = new FlagParser().Parse(flagString); + string value; + Assert.True(results.GetValue(FlagType.Cookie, out value)); + Assert.Equal(expected, value); } public bool DictionaryContentsEqual(IDictionary dictionary, IDictionary other) diff --git a/test/Microsoft.AspNetCore.Rewrite.Tests/UrlActions/ChangeCookieActionTests.cs b/test/Microsoft.AspNetCore.Rewrite.Tests/UrlActions/ChangeCookieActionTests.cs new file mode 100644 index 0000000000..20096ef162 --- /dev/null +++ b/test/Microsoft.AspNetCore.Rewrite.Tests/UrlActions/ChangeCookieActionTests.cs @@ -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 Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Rewrite.Internal.UrlActions; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Microsoft.AspNetCore.Rewrite.Tests.UrlActions +{ + public class ChangeCookieActionTests + { + [Fact] + public void SetsCookie() + { + var now = DateTimeOffset.UtcNow; + var context = new RewriteContext { HttpContext = new DefaultHttpContext() }; + var action = new ChangeCookieAction("Cookie", () => now) + { + Value = "Chocolate Chip", + Domain = "contoso.com", + Lifetime = TimeSpan.FromMinutes(1440), + Path = "/recipes", + Secure = true, + HttpOnly = true + }; + + action.ApplyAction(context, null, null); + + var cookieHeaders = context.HttpContext.Response.Headers[HeaderNames.SetCookie]; + var header = Assert.Single(cookieHeaders); + Assert.Equal($"Cookie=Chocolate%20Chip; expires={HeaderUtilities.FormatDate(now.AddMinutes(1440))}; domain=contoso.com; path=/recipes; secure; httponly", header); + } + + [Fact] + public void ZeroLifetime() + { + var context = new RewriteContext { HttpContext = new DefaultHttpContext() }; + var action = new ChangeCookieAction("Cookie") + { + Value = "Chocolate Chip", + }; + + action.ApplyAction(context, null, null); + + var cookieHeaders = context.HttpContext.Response.Headers[HeaderNames.SetCookie]; + var header = Assert.Single(cookieHeaders); + Assert.Equal($"Cookie=Chocolate%20Chip", header); + } + + + [Fact] + public void UnsetCookie() + { + var context = new RewriteContext { HttpContext = new DefaultHttpContext() }; + var action = new ChangeCookieAction("Cookie"); + + action.ApplyAction(context, null, null); + + var cookieHeaders = context.HttpContext.Response.Headers[HeaderNames.SetCookie]; + var header = Assert.Single(cookieHeaders); + Assert.Equal($"Cookie=", header); + } + } +} diff --git a/test/Microsoft.AspNetCore.Rewrite.Tests/UrlActions/ForbiddenActionTests.cs b/test/Microsoft.AspNetCore.Rewrite.Tests/UrlActions/ForbiddenActionTests.cs index b15354b672..a9eac5a807 100644 --- a/test/Microsoft.AspNetCore.Rewrite.Tests/UrlActions/ForbiddenActionTests.cs +++ b/test/Microsoft.AspNetCore.Rewrite.Tests/UrlActions/ForbiddenActionTests.cs @@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Rewrite.Tests.UrlActions public void Forbidden_Verify403IsInStatusCode() { - var context = new RewriteContext {HttpContext = new DefaultHttpContext()}; + var context = new RewriteContext { HttpContext = new DefaultHttpContext() }; var action = new ForbiddenAction(); action.ApplyAction(context, null, null);