Url Rewrite WIP

This commit is contained in:
Justin Kotalik 2016-07-15 15:42:42 -07:00
parent d3816fa458
commit a9c2656404
69 changed files with 3816 additions and 3 deletions

View File

@ -1,7 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.24720.0
VisualStudioVersion = 14.0.25420.1
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
@ -26,6 +25,12 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ResponseBufferingSample", "
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "HttpOverridesSample", "samples\HttpOverridesSample\HttpOverridesSample.xproj", "{7F95478D-E1D4-4A64-BA42-B041591A96EB}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Rewrite", "src\Microsoft.AspNetCore.Rewrite\Microsoft.AspNetCore.Rewrite.xproj", "{0E7CA1A7-1DC3-4CE6-B9C7-1688FE1410F1}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "RewriteSample", "samples\RewriteSample\RewriteSample.xproj", "{9E049645-13BC-4598-89E1-5B43D36E5D14}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Rewrite.Tests", "test\Microsoft.AspNetCore.Rewrite.Tests\Microsoft.AspNetCore.Rewrite.Tests.xproj", "{31794F9E-A1AA-4535-B03C-A3233737CD1A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -56,6 +61,18 @@ Global
{7F95478D-E1D4-4A64-BA42-B041591A96EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7F95478D-E1D4-4A64-BA42-B041591A96EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7F95478D-E1D4-4A64-BA42-B041591A96EB}.Release|Any CPU.Build.0 = Release|Any CPU
{0E7CA1A7-1DC3-4CE6-B9C7-1688FE1410F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0E7CA1A7-1DC3-4CE6-B9C7-1688FE1410F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0E7CA1A7-1DC3-4CE6-B9C7-1688FE1410F1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0E7CA1A7-1DC3-4CE6-B9C7-1688FE1410F1}.Release|Any CPU.Build.0 = Release|Any CPU
{9E049645-13BC-4598-89E1-5B43D36E5D14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9E049645-13BC-4598-89E1-5B43D36E5D14}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9E049645-13BC-4598-89E1-5B43D36E5D14}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9E049645-13BC-4598-89E1-5B43D36E5D14}.Release|Any CPU.Build.0 = Release|Any CPU
{31794F9E-A1AA-4535-B03C-A3233737CD1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{31794F9E-A1AA-4535-B03C-A3233737CD1A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{31794F9E-A1AA-4535-B03C-A3233737CD1A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{31794F9E-A1AA-4535-B03C-A3233737CD1A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -67,5 +84,8 @@ Global
{F5F1D123-9C81-4A9E-8644-AA46B8E578FB} = {8437B0F3-3894-4828-A945-A9187F37631D}
{E5C55B80-7827-40EB-B661-32B0E0E431CA} = {9587FE9F-5A17-42C4-8021-E87F59CECB98}
{7F95478D-E1D4-4A64-BA42-B041591A96EB} = {9587FE9F-5A17-42C4-8021-E87F59CECB98}
{0E7CA1A7-1DC3-4CE6-B9C7-1688FE1410F1} = {A5076D28-FA7E-4606-9410-FEDD0D603527}
{9E049645-13BC-4598-89E1-5B43D36E5D14} = {9587FE9F-5A17-42C4-8021-E87F59CECB98}
{31794F9E-A1AA-4535-B03C-A3233737CD1A} = {8437B0F3-3894-4828-A945-A9187F37631D}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,19 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("RewriteSample")]
[assembly: AssemblyTrademark("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("9e049645-13bc-4598-89e1-5b43d36e5d14")]

View File

@ -0,0 +1,12 @@
# 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]
# Rewrite path with additional sub directory
RewriteRule ^(.*)$ /foo$1
# Forbid a certain url from being accessed
RewriteRule /bar - [F]
RewriteRule /bar/ - [F]

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>9e049645-13bc-4598-89e1-5b43d36e5d14</ProjectGuid>
<RootNamespace>RewriteSample</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@ -0,0 +1,29 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.AspNetCore.Rewrite;
using Microsoft.Extensions.DependencyInjection;
namespace RewriteSample
{
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.UseRewriter(new UrlRewriteOptions()
.ImportFromModRewrite("Rewrite.txt"));
app.Run(context => context.Response.WriteAsync(context.Request.Path));
}
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseKestrel()
.UseStartup<Startup>()
.Build();
host.Run();
}
}
}

View File

@ -0,0 +1,32 @@
{
"version": "1.1.0-*",
"dependencies": {
"Microsoft.AspNetCore.Rewrite": "1.1.0-*",
"Microsoft.AspNetCore.Server.Kestrel": "1.1.0-*",
"Microsoft.AspNetCore.StaticFiles": "1.1.0-*"
},
"buildOptions": {
"emitEntryPoint": true,
"preserveCompilationContext": true
},
"frameworks": {
"net451": {},
"netcoreapp1.0": {
"dependencies": {
"Microsoft.NETCore.App": {
"version": "1.0.0-*",
"type": "platform"
}
}
}
},
"publish": {
"exclude": [
"node_modules",
"bower_components",
"**.xproj",
"**.user",
"**.vspscc"
]
}
}

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>0e7ca1a7-1dc3-4ce6-b9c7-1688fe1410f1</ProjectGuid>
<RootNamespace>Microsoft.AspNetCore.Rewrite</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@ -0,0 +1,18 @@
// 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.ModRewrite
{
public class Condition
{
public Pattern TestStringSegments { get; }
public ConditionExpression ConditionExpression { get; }
public ConditionFlags Flags { get; }
public Condition(Pattern testStringSegments, ConditionExpression conditionRegex, ConditionFlags flags)
{
TestStringSegments = testStringSegments;
ConditionExpression = conditionRegex;
Flags = flags;
}
}
}

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;
namespace Microsoft.AspNetCore.Rewrite.ModRewrite
{
public class ConditionBuilder
{
private Pattern _testString;
private ParsedModRewriteExpression _pce;
private ConditionFlags _flags;
public ConditionBuilder(string conditionString)
{
var tokens = Tokenizer.Tokenize(conditionString);
if (tokens.Count == 3)
{
CreateCondition(tokens[1], tokens[2], flagsString: null);
}
else if (tokens.Count == 4)
{
CreateCondition(tokens[1], tokens[2], tokens[3]);
}
else
{
throw new FormatException("Invalid number of tokens.");
}
}
public ConditionBuilder(string testString, string condition)
{
CreateCondition(testString, condition, flagsString: null);
}
public ConditionBuilder(string testString, string condition, string flags)
{
CreateCondition(testString, condition, flags);
}
public Condition Build()
{
var expression = ExpressionCreator.CreateConditionExpression(_pce, _flags);
return new Condition(_testString, expression, _flags);
}
private void CreateCondition(string testString, string condition, string flagsString)
{
_testString = ConditionTestStringParser.ParseConditionTestString(testString);
_pce = ConditionPatternParser.ParseActionCondition(condition);
_flags = FlagParser.ParseConditionFlags(flagsString);
}
public void SetFlag(string flag)
{
SetFlag(flag, value: null);
}
public void SetFlag(ConditionFlagType flag)
{
SetFlag(flag, value: null);
}
public void SetFlag(string flag, string value)
{
if (_flags == null)
{
_flags = new ConditionFlags();
}
_flags.SetFlag(flag, value);
}
public void SetFlag(ConditionFlagType flag, string value)
{
if (_flags == null)
{
_flags = new ConditionFlags();
}
_flags.SetFlag(flag, value);
}
public void SetFlags(string flags)
{
if (_flags == null)
{
_flags = FlagParser.ParseConditionFlags(flags);
}
else
{
FlagParser.ParseConditionFlags(flags, _flags);
}
}
}
}

View File

@ -0,0 +1,29 @@
// 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.Rewrite.Operands;
namespace Microsoft.AspNetCore.Rewrite.ModRewrite
{
/// <summary>
/// Represents the ConditionPattern for a mod_rewrite rule.
/// </summary>
public class ConditionExpression
{
public Operand Operand { get; set; }
public bool Invert { get; set; }
/// <summary>
/// Checks if a condition matches the context.
/// </summary>
/// <param name="context">The UrlRewriteContext.</param>
/// <param name="previous">The previous condition results (for backreferences).</param>
/// <param name="testString">The testString created from the <see cref="Pattern"/>.</param>
/// <returns>If the testString satisfies the condition</returns>
public bool? CheckConditionExpression(UrlRewriteContext context, Match previous, string testString)
{
return Operand.CheckOperation(previous, testString, context.FileProvider) ^ Invert;
}
}
}

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.ModRewrite
{
public enum ConditionFlagType
{
NoCase,
Or,
NoVary
}
}

View File

@ -0,0 +1,101 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Rewrite.ModRewrite
{
// TODO Refactor Condition Flags and Rule Flags under base flag class
public class ConditionFlags
{
private IDictionary<string, ConditionFlagType> _conditionFlagLookup = new Dictionary<string, ConditionFlagType>(StringComparer.OrdinalIgnoreCase) {
{ "nc", ConditionFlagType.NoCase},
{ "nocase", ConditionFlagType.NoCase },
{ "or", ConditionFlagType.Or},
{ "ornext", ConditionFlagType.Or },
{ "nv", ConditionFlagType.NoVary},
{ "novary", ConditionFlagType.NoVary}
};
public IDictionary<ConditionFlagType, string> FlagDictionary { get; }
public ConditionFlags(IDictionary<ConditionFlagType, string> flags)
{
FlagDictionary = flags;
}
public ConditionFlags()
{
FlagDictionary = new Dictionary<ConditionFlagType, string>();
}
public void SetFlag(string flag)
{
SetFlag(flag, null);
}
public void SetFlag(string flag, string value)
{
ConditionFlagType res;
if (!_conditionFlagLookup.TryGetValue(flag, out res))
{
throw new ArgumentException("Invalid flag");
}
SetFlag(res, value);
}
public void SetFlag(ConditionFlagType flag, string value)
{
if (value == null)
{
value = string.Empty;
}
FlagDictionary[flag] = value;
}
public string GetFlag(ConditionFlagType flag)
{
CleanupResources();
string res;
if (!FlagDictionary.TryGetValue(flag, out res))
{
return null;
}
return res;
}
public string this[ConditionFlagType flag]
{
get
{
string res;
if (!FlagDictionary.TryGetValue(flag, out res))
{
return null;
}
return res;
}
set
{
FlagDictionary[flag] = value ?? string.Empty;
}
}
public bool HasFlag(ConditionFlagType flag)
{
CleanupResources();
string res;
return FlagDictionary.TryGetValue(flag, out res);
}
// If this method is called, all flags have been processed,
// therefore to clean up memory, delete dictionary.
private void CleanupResources()
{
if (_conditionFlagLookup != null)
{
_conditionFlagLookup = null;
}
}
}
}

View File

@ -0,0 +1,230 @@
// 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;
namespace Microsoft.AspNetCore.Rewrite.ModRewrite
{
/// <summary>
/// Parses the "CondPattern" portion of the RewriteCond.
/// RewriteCond TestString CondPattern
/// </summary>
public static class ConditionPatternParser
{
private const char Not = '!';
private const char Dash = '-';
private const char Less = '<';
private const char Greater = '>';
private const char EqualSign = '=';
/// <summary>
/// Given a CondPattern, create a ParsedConditionExpression, containing the type of operation
/// and value.
/// ParsedConditionExpression is an intermediary object, which will be made into a ConditionExpression
/// once the flags are parsed.
/// </summary>
/// <param name="condition">The CondPattern portion of a mod_rewrite RewriteCond.</param>
/// <returns>A new parsed condition.</returns>
public static ParsedModRewriteExpression ParseActionCondition(string condition)
{
if (condition == null)
{
condition = string.Empty;
}
var context = new ParserContext(condition);
var results = new ParsedModRewriteExpression();
if (!context.Next())
{
throw new FormatException(context.Error());
}
// If we hit a !, make sure the condition is inverted when resolving the string
if (context.Current == Not)
{
results.Invert = true;
if (!context.Next())
{
throw new FormatException(context.Error());
}
}
// Control Block for strings. Set the operation and type fields based on the sign
switch (context.Current)
{
case Greater:
if (!context.Next())
{
// Dangling ">"
throw new FormatException(context.Error());
}
if (context.Current == EqualSign)
{
if (!context.Next())
{
// Dangling ">="
throw new FormatException(context.Error());
}
results.Operation = OperationType.GreaterEqual;
results.Type = ConditionType.StringComp;
}
else
{
results.Operation = OperationType.Greater;
results.Type = ConditionType.StringComp;
}
break;
case Less:
if (!context.Next())
{
// Dangling "<"
throw new FormatException(context.Error());
}
if (context.Current == EqualSign)
{
if (!context.Next())
{
// Dangling "<="
throw new FormatException(context.Error());
}
results.Operation = OperationType.LessEqual;
results.Type = ConditionType.StringComp;
}
else
{
results.Operation = OperationType.Less;
results.Type = ConditionType.StringComp;
}
break;
case EqualSign:
if (!context.Next())
{
// Dangling "="
throw new FormatException(context.Error());
}
results.Operation = OperationType.Equal;
results.Type = ConditionType.StringComp;
break;
case Dash:
results = ParseProperty(context, results.Invert);
if (results.Type == ConditionType.PropertyTest)
{
return results;
}
context.Next();
break;
default:
results.Type = ConditionType.Regex;
break;
}
// Capture the rest of the string guarantee validity.
results.Operand = (condition.Substring(context.GetIndex()));
if (IsValidActionCondition(results))
{
return results;
}
else
{
throw new FormatException(context.Error());
}
}
/// <summary>
/// Given that the current index is a property (ex checks for directory or regular files), create a
/// new ParsedConditionExpression with the appropriate property operation.
/// </summary>
/// <param name="context"></param>
/// <param name="invert"></param>
/// <returns></returns>
public static ParsedModRewriteExpression ParseProperty(ParserContext context, bool invert)
{
if (!context.Next())
{
throw new FormatException(context.Error());
}
switch (context.Current)
{
case 'd':
return new ParsedModRewriteExpression(invert, ConditionType.PropertyTest, OperationType.Directory, null);
case 'f':
return new ParsedModRewriteExpression(invert, ConditionType.PropertyTest, OperationType.RegularFile, null);
case 'F':
return new ParsedModRewriteExpression(invert, ConditionType.PropertyTest, OperationType.ExistingFile, null);
case 'h':
case 'L':
return new ParsedModRewriteExpression(invert, ConditionType.PropertyTest, OperationType.SymbolicLink, null);
case 's':
return new ParsedModRewriteExpression(invert, ConditionType.PropertyTest, OperationType.Size, null);
case 'U':
return new ParsedModRewriteExpression(invert, ConditionType.PropertyTest, OperationType.ExistingUrl, null);
case 'x':
return new ParsedModRewriteExpression(invert, ConditionType.PropertyTest, OperationType.Executable, null);
case 'e':
if (!context.Next() || context.Current != 'q')
{
// Illegal statement.
throw new FormatException(context.Error());
}
return new ParsedModRewriteExpression(invert, ConditionType.IntComp, OperationType.Equal, null);
case 'g':
if (!context.Next())
{
throw new FormatException(context.Error());
}
if (context.Current == 't')
{
return new ParsedModRewriteExpression(invert, ConditionType.IntComp, OperationType.Greater, null);
}
else if (context.Current == 'e')
{
return new ParsedModRewriteExpression(invert, ConditionType.IntComp, OperationType.GreaterEqual, null);
}
else
{
throw new FormatException(context.Error());
}
case 'l':
if (!context.Next())
{
return new ParsedModRewriteExpression(invert, ConditionType.PropertyTest, OperationType.SymbolicLink, null);
}
if (context.Current == 't')
{
return new ParsedModRewriteExpression(invert, ConditionType.IntComp, OperationType.Less, null);
}
else if (context.Current == 'e')
{
return new ParsedModRewriteExpression(invert, ConditionType.IntComp, OperationType.LessEqual, null);
}
else
{
throw new FormatException(context.Error());
}
case 'n':
if (!context.Next() || context.Current != 'e')
{
throw new FormatException(context.Error());
}
return new ParsedModRewriteExpression(invert, ConditionType.IntComp, OperationType.NotEqual, null);
default:
throw new FormatException(context.Error());
}
}
private static bool IsValidActionCondition(ParsedModRewriteExpression results)
{
if (results.Type == ConditionType.IntComp)
{
// If the type is an integer, verify operand is actually an int
int res;
if (!int.TryParse(results.Operand, out res))
{
return false;
}
}
return true;
}
}
}

View File

@ -0,0 +1,205 @@
// 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 Microsoft.AspNetCore.Rewrite.Internal;
namespace Microsoft.AspNetCore.Rewrite.ModRewrite
{
/// <summary>
/// Parses the TestString segment of the mod_rewrite condition.
/// </summary>
public class ConditionTestStringParser
{
private const char Percent = '%';
private const char Dollar = '$';
private const char Space = ' ';
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 pattern. Can contain server variables, back references, etc.
/// </summary>
/// <param name="testString">The test string portion of the RewriteCond
/// Examples:
/// %{REMOTE_ADDR}
/// /var/www/%{REQUEST_URI}
/// %1
/// $1</param>
/// <returns>A new <see cref="Pattern"/>, containing a list of <see cref="PatternSegment"/></returns>
public static Pattern ParseConditionTestString(string testString)
{
if (testString == null)
{
testString = string.Empty;
}
var context = new ParserContext(testString);
var results = new List<PatternSegment>();
while (context.Next())
{
if (context.Current == Percent)
{
// This is a server parameter, parse for a condition variable
if (!context.Next())
{
throw new FormatException(context.Error());
}
if (!ParseConditionParameter(context, results))
{
throw new FormatException(context.Error());
}
}
else if (context.Current == Dollar)
{
// This is a parameter from the rule, verify that it is a number from 0 to 9 directly after it
// and create a new Pattern Segment.
if (!context.Next())
{
throw new FormatException(context.Error());
}
context.Mark();
if (context.Current >= '0' && context.Current <= '9')
{
context.Next();
var ruleVariable = context.Capture();
context.Back();
results.Add(new PatternSegment(ruleVariable, SegmentType.RuleParameter));
}
else
{
throw new FormatException(context.Error());
}
}
else
{
// Parse for literals, which will return on either the end of the test string
// or when it hits a special character
if (!ParseLiteral(context, results))
{
throw new FormatException(context.Error());
}
}
}
return new Pattern(results);
}
/// <summary>
/// Obtains the condition parameter, which could either be a condition variable or a
/// server variable. Assumes the current character is immediately after the '%'.
/// context, on return will be on the last character of variable captured, such that after
/// Next() is called, it will be on the character immediately after the condition parameter.
/// </summary>
/// <param name="context">The ParserContext</param>
/// <param name="results">The List of results which the new condition parameter will be added.</param>
/// <returns>true </returns>
private static bool ParseConditionParameter(ParserContext context, List<PatternSegment> results)
{
// Parse { }
if (context.Current == OpenBrace)
{
// Start of a server variable
if (!context.Next())
{
// Dangling {
return false;
}
context.Mark();
while (context.Current != CloseBrace)
{
if (!context.Next())
{
// No closing } for the server variable
return false;
}
else if (context.Current == Colon)
{
// Have a segmented look up Ex: HTTP:xxxx
// TODO
}
}
// Need to verify server variable captured exists
var rawServerVariable = context.Capture();
if (IsValidServerVariable(rawServerVariable))
{
results.Add(new PatternSegment(rawServerVariable, SegmentType.ServerParameter));
}
else
{
// invalid.
return false;
}
}
else if (context.Current >= '0' && context.Current <= '9')
{
// means we have a segmented lookup
// store information in the testString result to know what to look up.
context.Mark();
context.Next();
var rawConditionParameter = context.Capture();
// Once we leave this method, the while loop will call next again. Because
// capture is exclusive, we need to go one past the end index, capture, and then go back.
context.Back();
results.Add(new PatternSegment(rawConditionParameter, SegmentType.ConditionParameter));
}
else
{
// illegal escape of a character
return false;
}
return true;
}
/// <summary>
/// Parse a string literal in the test string. Continues capturing until the start of a new variable type.
/// </summary>
/// <param name="context"></param>
/// <param name="results"></param>
/// <returns></returns>
private static bool ParseLiteral(ParserContext context, List<PatternSegment> results)
{
context.Mark();
string literal;
while (true)
{
if (context.Current == Percent || context.Current == Dollar)
{
literal = context.Capture();
context.Back();
break;
}
if (!context.Next())
{
literal = context.Capture();
break;
}
}
if (IsValidLiteral(context, literal))
{
// add results
results.Add(new PatternSegment(literal, SegmentType.Literal));
return true;
}
else
{
return false;
}
}
private static bool IsValidLiteral(ParserContext context, string literal)
{
// TODO Once escape characters are discussed, figure this out.
return true;
}
private static bool IsValidServerVariable(string variable)
{
// TODO Once escape characters are discussed, figure this out.
return ServerVariables.ValidServerVariables.Contains(variable);
}
}
}

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.ModRewrite
{
public enum ConditionType
{
Regex,
PropertyTest,
StringComp,
IntComp
}
}

View File

@ -0,0 +1,128 @@
// 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.Rewrite.Operands;
using Microsoft.AspNetCore.Rewrite.RuleAbstraction;
namespace Microsoft.AspNetCore.Rewrite.ModRewrite
{
/// <summary>
/// Converts a parsed expression into a mod_rewrite condition.
/// </summary>
public class ExpressionCreator
{
private static readonly TimeSpan RegexTimeout = TimeSpan.FromMilliseconds(1);
public static ConditionExpression CreateConditionExpression(ParsedModRewriteExpression pce, ConditionFlags flags)
{
var condExp = new ConditionExpression();
condExp.Invert = pce.Invert;
if (pce.Type == ConditionType.Regex)
{
// TODO make nullable?
if (flags != null && flags.HasFlag(ConditionFlagType.NoCase))
{
condExp.Operand = new RegexOperand(new Regex(pce.Operand, RegexOptions.IgnoreCase | RegexOptions.Compiled, RegexTimeout));
}
else
{
condExp.Operand = new RegexOperand(new Regex(pce.Operand, RegexOptions.Compiled, RegexTimeout));
}
}
else if (pce.Type == ConditionType.IntComp)
{
switch (pce.Operation)
{
case OperationType.Equal:
condExp.Operand = new IntegerOperand(pce.Operand, IntegerOperationType.Equal);
break;
case OperationType.Greater:
condExp.Operand = new IntegerOperand(pce.Operand, IntegerOperationType.Greater);
break;
case OperationType.GreaterEqual:
condExp.Operand = new IntegerOperand(pce.Operand, IntegerOperationType.GreaterEqual);
break;
case OperationType.Less:
condExp.Operand = new IntegerOperand(pce.Operand, IntegerOperationType.Less);
break;
case OperationType.LessEqual:
condExp.Operand = new IntegerOperand(pce.Operand, IntegerOperationType.LessEqual);
break;
case OperationType.NotEqual:
condExp.Operand = new IntegerOperand(pce.Operand, IntegerOperationType.NotEqual);
break;
default:
throw new ArgumentException("No defined operation for integer comparison.");
}
}
else if (pce.Type == ConditionType.StringComp)
{
switch (pce.Operation)
{
case OperationType.Equal:
condExp.Operand = new StringOperand(pce.Operand, StringOperationType.Equal);
break;
case OperationType.Greater:
condExp.Operand = new StringOperand(pce.Operand, StringOperationType.Greater);
break;
case OperationType.GreaterEqual:
condExp.Operand = new StringOperand(pce.Operand, StringOperationType.GreaterEqual);
break;
case OperationType.Less:
condExp.Operand = new StringOperand(pce.Operand, StringOperationType.Less);
break;
case OperationType.LessEqual:
condExp.Operand = new StringOperand(pce.Operand, StringOperationType.LessEqual);
break;
default:
throw new ArgumentException("No defined operation for string comparison.");
}
}
else
{
switch (pce.Operation)
{
case OperationType.Directory:
condExp.Operand = new PropertyOperand(PropertyOperationType.Directory);
break;
case OperationType.RegularFile:
condExp.Operand = new PropertyOperand(PropertyOperationType.RegularFile);
break;
case OperationType.ExistingFile:
condExp.Operand = new PropertyOperand(PropertyOperationType.ExistingFile);
break;
case OperationType.SymbolicLink:
condExp.Operand = new PropertyOperand(PropertyOperationType.SymbolicLink);
break;
case OperationType.Size:
condExp.Operand = new PropertyOperand(PropertyOperationType.Size);
break;
case OperationType.ExistingUrl:
condExp.Operand = new PropertyOperand(PropertyOperationType.ExistingUrl);
break;
case OperationType.Executable:
condExp.Operand = new PropertyOperand(PropertyOperationType.Executable);
break;
default:
throw new ArgumentException("No defined operation for property comparison.");
}
}
return condExp;
}
public static RuleExpression CreateRuleExpression(ParsedModRewriteExpression pce, RuleFlags flags)
{
var ruleExp = new RuleExpression();
ruleExp.Invert = pce.Invert;
if (flags.HasFlag(RuleFlagType.NoCase))
{
ruleExp.Operand = new RegexOperand(new Regex(pce.Operand, RegexOptions.IgnoreCase | RegexOptions.Compiled, RegexTimeout));
}
else
{
ruleExp.Operand = new RegexOperand(new Regex(pce.Operand, RegexOptions.Compiled, RegexTimeout));
}
return ruleExp;
}
}
}

View File

@ -0,0 +1,103 @@
// 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 Microsoft.AspNetCore.Rewrite.RuleAbstraction;
namespace Microsoft.AspNetCore.Rewrite.ModRewrite
{
/// <summary>
///
/// </summary>
public static class FileParser
{
public static List<Rule> Parse(TextReader input)
{
string line = null;
var rules = new List<Rule>();
var conditions = new List<Condition>();
// TODO consider passing Itokenizer and Ifileparser and provide implementations
while ((line = input.ReadLine()) != null)
{
if (string.IsNullOrEmpty(line))
{
continue;
}
if (line.StartsWith("#"))
{
continue;
}
var tokens = Tokenizer.Tokenize(line);
if (tokens.Count > 4)
{
// This means the line didn't have an appropriate format, throw format exception
throw new FormatException();
}
// TODO make a new class called rule parser that does and either return an exception or return the rule.
switch (tokens[0])
{
case "RewriteBase":
throw new NotSupportedException();
//if (tokens.Count == 2)
//{
// ModRewriteBase.Base = tokens[1];
//}
//else
//{
// throw new FormatException("");
//}
//break;
case "RewriteCond":
{
ConditionBuilder builder = null;
if (tokens.Count == 3)
{
builder = new ConditionBuilder(tokens[1], tokens[2]);
}
else if (tokens.Count == 4)
{
builder = new ConditionBuilder(tokens[1], tokens[2], tokens[3]);
}
else
{
throw new FormatException();
}
conditions.Add(builder.Build());
break;
}
case "RewriteRule":
{
RuleBuilder builder = null;
if (tokens.Count == 3)
{
builder = new RuleBuilder(tokens[1], tokens[2]);
}
else if (tokens.Count == 4)
{
builder = new RuleBuilder(tokens[1], tokens[2], tokens[3]);
}
else
{
throw new FormatException();
}
builder.AddConditions(conditions);
rules.Add(builder.Build());
conditions = new List<Condition>();
break;
}
case "RewriteMap":
throw new NotImplementedException("RewriteMaps to be added soon.");
case "RewriteEngine":
// Explicitly do nothing here, no notion of turning on regex engine.
break;
default:
throw new FormatException(tokens[0]);
}
}
return rules;
}
}
}

View File

@ -0,0 +1,103 @@
// 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;
namespace Microsoft.AspNetCore.Rewrite.ModRewrite
{
/// <summary>
/// Parses the flags
/// </summary>
public class FlagParser
{
// TODO Refactor Rule and Condition Flags under IFlags
public static RuleFlags ParseRuleFlags(string flagString)
{
var flags = new RuleFlags();
ParseRuleFlags(flagString, flags);
return flags;
}
public static void ParseRuleFlags(string flagString, RuleFlags flags)
{
if (string.IsNullOrEmpty(flagString))
{
return;
}
// Check that flags are contained within []
if (!flagString.StartsWith("[") || !flagString.EndsWith("]"))
{
throw new FormatException();
}
// Illegal syntax to have any spaces.
var tokens = flagString.Substring(1, flagString.Length - 2).Split(',');
// Go through tokens and verify they have meaning.
// Flags can be KVPs, delimited by '='.
foreach (string token in tokens)
{
if (string.IsNullOrEmpty(token))
{
continue;
}
string[] kvp = token.Split('=');
if (kvp.Length == 1)
{
flags.SetFlag(kvp[0], null);
}
else if (kvp.Length == 2)
{
flags.SetFlag(kvp[0], kvp[1]);
}
else
{
throw new FormatException();
}
}
}
public static ConditionFlags ParseConditionFlags(string flagString)
{
var flags = new ConditionFlags();
ParseConditionFlags(flagString, flags);
return flags;
}
public static void ParseConditionFlags(string flagString, ConditionFlags flags)
{
if (string.IsNullOrEmpty(flagString))
{
return;
}
// Check that flags are contained within []
if (!flagString.StartsWith("[") || !flagString.EndsWith("]"))
{
throw new FormatException();
}
// Lexing esque step to split all flags.
// Illegal syntax to have any spaces.
var tokens = flagString.Substring(1, flagString.Length - 2).Split(',');
// Go through tokens and verify they have meaning.
// Flags can be KVPs, delimited by '='.
foreach (string token in tokens)
{
if (string.IsNullOrEmpty(token))
{
continue;
}
string[] kvp = token.Split('=');
if (kvp.Length == 1)
{
flags.SetFlag(kvp[0], null);
}
else if (kvp.Length == 2)
{
flags.SetFlag(kvp[0], kvp[1]);
}
else
{
throw new FormatException();
}
}
}
}
}

View File

@ -0,0 +1,73 @@
// 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.Rewrite.ModRewrite;
namespace Microsoft.AspNetCore.Rewrite
{
public static class ModRewriteExtensions
{
/// <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="filePath">The path to the file containing mod_rewrite rules.</param>
public static UrlRewriteOptions ImportFromModRewrite(this UrlRewriteOptions options, string filePath)
{
// TODO use IHostingEnvironment as param.
if (string.IsNullOrEmpty(filePath))
{
throw new ArgumentException(nameof(filePath));
}
// TODO IHosting to fix!
using (var stream = File.OpenRead(filePath))
{
options.Rules.AddRange(FileParser.Parse(new StreamReader(stream)));
};
return options;
}
/// <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="reader">Text reader containing a stream of mod_rewrite rules.</param>
public static UrlRewriteOptions ImportFromModRewrite(this UrlRewriteOptions options, TextReader reader)
{
options.Rules.AddRange(FileParser.Parse(reader));
return options;
}
/// <summary>
/// Adds a mod_rewrite rule to the current rules.
/// Additional properties (conditions, flags) for the rule can be added through the action.
/// </summary>
/// <param name="options">The UrlRewrite options.</param>
/// <param name="rule">The literal string of a mod_rewrite rule:
/// "RewriteRule Pattern Substitution [Flags]"</param>
/// <param name="action">Action to perform on the <see cref="RuleBuilder"/> </param>
public static UrlRewriteOptions AddModRewriteRule(this UrlRewriteOptions options, string rule, Action<RuleBuilder> action)
{
var builder = new RuleBuilder(rule);
action(builder);
options.Rules.Add(builder.Build());
return options;
}
/// <summary>
/// Adds a mod_rewrite rule to the current rules.
/// </summary>
/// <param name="options">The UrlRewrite options.</param>
/// <param name="rule">The literal string of a mod_rewrite rule:
/// "RewriteRule Pattern Substitution [Flags]"</param>
public static UrlRewriteOptions AddModRewriteRule(this UrlRewriteOptions options, string rule)
{
var builder = new RuleBuilder(rule);
options.Rules.Add(builder.Build());
return options;
}
}
}

View File

@ -0,0 +1,209 @@
// 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 Microsoft.Net.Http.Headers;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Rewrite.RuleAbstraction;
namespace Microsoft.AspNetCore.Rewrite.ModRewrite
{
public class ModRewriteRule : Rule
{
public List<Condition> Conditions { get; set; } = new List<Condition>();
public string Description { get; set; } = string.Empty;
public RuleExpression InitialRule { get; set; }
public Pattern Transform { get; set; }
public RuleFlags Flags { get; set; } = new RuleFlags();
public ModRewriteRule() { }
public ModRewriteRule(List<Condition> conditions, RuleExpression initialRule, Pattern transforms, RuleFlags flags, string description = "")
{
Conditions = conditions;
InitialRule = initialRule;
Transform = transforms;
Flags = flags;
Description = description;
}
public override RuleResult ApplyRule(UrlRewriteContext context)
{
// 1. Figure out which section of the string to match for the initial rule.
var results = InitialRule.Operand.RegexOperation.Match(context.HttpContext.Request.Path.ToString());
string flagRes = null;
if (CheckMatchResult(results.Success))
{
return new RuleResult { Result = RuleTerminiation.Continue };
}
if (Flags.HasFlag(RuleFlagType.EscapeBackreference))
{
// TODO Escape Backreferences here.
}
// 2. Go through all conditions and compare them to the created string
var previous = Match.Empty;
if (!CheckCondition(context, results, previous))
{
return new RuleResult { Result = RuleTerminiation.Continue };
}
// TODO add chained flag
// at this point, our rule passed, we can now apply the on match function
var result = Transform.GetPattern(context.HttpContext, results, previous);
if (Flags.HasFlag(RuleFlagType.QSDiscard))
{
context.HttpContext.Request.QueryString = new QueryString();
}
if ((flagRes = Flags.GetValue(RuleFlagType.Cookie)) != null)
{
// TODO CreateCookies(context);
// context.HttpContext.Response.Cookies.Append()
// Make sure this in on compile.
}
if ((flagRes = Flags.GetValue(RuleFlagType.Env)) != null)
{
// TODO CreateEnv(context)
// context.HttpContext...
}
if ((flagRes = Flags.GetValue(RuleFlagType.Next)) != null)
{
// TODO Next flag
}
if (Flags.HasFlag(RuleFlagType.Forbidden))
{
context.HttpContext.Response.StatusCode = StatusCodes.Status403Forbidden;
return new RuleResult { Result = RuleTerminiation.ResponseComplete };
}
else if (Flags.HasFlag(RuleFlagType.Gone))
{
context.HttpContext.Response.StatusCode = StatusCodes.Status410Gone;
return new RuleResult { Result = RuleTerminiation.ResponseComplete };
}
else if (result == "-")
{
// TODO set url to result.
}
else if (Flags.HasFlag(RuleFlagType.QSAppend))
{
context.HttpContext.Request.QueryString = context.HttpContext.Request.QueryString.Add(new QueryString(result));
}
if ((flagRes = Flags.GetValue(RuleFlagType.Redirect)) != null)
{
int parsedInt;
if (!int.TryParse(flagRes, out parsedInt))
{
// TODO PERF parse the status code when the flag is parsed rather than per request
throw new FormatException("Trying to parse non-int in integer comparison.");
}
context.HttpContext.Response.StatusCode = parsedInt;
if (Flags.HasFlag(RuleFlagType.FullUrl))
{
// TODO review escaping
context.HttpContext.Response.Headers[HeaderNames.Location] = result;
}
else
{
// TODO str cat is bad, polish, review escaping
if (result.StartsWith("/"))
{
context.HttpContext.Response.Headers[HeaderNames.Location] = result + context.HttpContext.Request.QueryString;
}
else
{
context.HttpContext.Response.Headers[HeaderNames.Location] = "/" + result + context.HttpContext.Request.QueryString;
}
}
return new RuleResult { Result = RuleTerminiation.ResponseComplete };
}
else
{
if (Flags.HasFlag(RuleFlagType.FullUrl))
{
ModifyHttpContextFromUri(context.HttpContext, result);
}
else
{
if (result.StartsWith("/"))
{
context.HttpContext.Request.Path = new PathString(result);
}
else
{
context.HttpContext.Request.Path = new PathString("/" + result);
}
}
if (Flags.HasFlag(RuleFlagType.Last) || Flags.HasFlag(RuleFlagType.End))
{
return new RuleResult { Result = RuleTerminiation.StopRules };
}
else
{
return new RuleResult { Result = RuleTerminiation.Continue };
}
}
}
private bool CheckMatchResult(bool? result)
{
if (result == null)
{
return false;
}
return !(result.Value ^ InitialRule.Invert);
}
private bool CheckCondition(UrlRewriteContext context, Match results, Match previous)
{
if (Conditions == null)
{
return true;
}
// TODO Visitor pattern here?
foreach (var condition in Conditions)
{
var concatTestString = condition.TestStringSegments.GetPattern(context.HttpContext, results, previous);
var match = condition.ConditionExpression.CheckConditionExpression(context, previous, concatTestString);
if (match == null)
{
return false;
}
if (!match.Value && !(condition.Flags.HasFlag(ConditionFlagType.Or)))
{
return false;
}
}
return true;
}
private void ModifyHttpContextFromUri(HttpContext context, string uriString)
{
var uri = new Uri(uriString);
// TODO this is ugly, fix in later push.
// TODO super bad for perf, cache/locally store these and update httpcontext after all rules are applied.
var pathBase = PathString.FromUriComponent(uri);
if (!pathBase.Value.StartsWith(context.Request.PathBase))
{
// cannot distinguish between path base and path.
throw new NotSupportedException("Modified path base from mod_rewrite rule");
}
context.Request.Host = HostString.FromUriComponent(uri);
context.Request.Path = PathString.FromUriComponent(uri);
context.Request.QueryString = QueryString.FromUriComponent(uri);
context.Request.Scheme = uri.Scheme;
}
}
}

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.
namespace Microsoft.AspNetCore.Rewrite.ModRewrite
{
public enum OperationType
{
None,
Equal,
Greater,
GreaterEqual,
Less,
LessEqual,
NotEqual,
Directory,
RegularFile,
ExistingFile,
SymbolicLink,
Size,
ExistingUrl,
Executable
}
}

View File

@ -0,0 +1,22 @@
// 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.ModRewrite
{
public class ParsedModRewriteExpression
{
public bool Invert { get; set; }
public ConditionType Type { get; set; }
public OperationType Operation { get; set; }
public string Operand { get; set; }
public ParsedModRewriteExpression(bool invert, ConditionType type, OperationType operation, string operand)
{
Invert = invert;
Type = type;
Operation = operation;
Operand = operand;
}
public ParsedModRewriteExpression() { }
}
}

View File

@ -0,0 +1,69 @@
// 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;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Rewrite.ModRewrite;
namespace Microsoft.AspNetCore.Rewrite
{
/// <summary>
/// Contains a sequence of pattern segments, which on obtaining the context, will create the appropriate
/// test string and condition for rules and conditions.
/// </summary>
public class Pattern
{
private IReadOnlyList<PatternSegment> Segments { get; }
/// <summary>
/// Creates a new Pattern
/// </summary>
/// <param name="segments">List of pattern segments which will be applied.</param>
public Pattern(IReadOnlyList<PatternSegment> segments)
{
Segments = segments;
}
/// <summary>
/// Creates the appropriate test string from the Httpcontext and Segments.
/// </summary>
/// <param name="context"></param>
/// <param name="ruleMatch"></param>
/// <param name="prevCondition"></param>
/// <returns></returns>
public string GetPattern(HttpContext context, Match ruleMatch, Match prevCondition)
{
var res = new StringBuilder();
foreach (var segment in Segments)
{
// TODO handle case when segment.Variable is 0 in rule and condition
switch (segment.Type)
{
case SegmentType.Literal:
res.Append(segment.Variable);
break;
case SegmentType.ServerParameter:
res.Append(ServerVariables.Resolve(segment.Variable, context));
break;
case SegmentType.RuleParameter:
var ruleParam = ruleMatch.Groups[segment.Variable];
if (ruleParam != null)
{
res.Append(ruleParam);
}
break;
case SegmentType.ConditionParameter:
var condParam = prevCondition.Groups[segment.Variable];
if (condParam != null)
{
res.Append(condParam);
}
break;
}
}
return res.ToString();
}
}
}

View File

@ -0,0 +1,26 @@
// 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
{
/// <summary>
/// A Pattern segment contains a portion of the test string/ substitution segment with a type associated.
/// This type can either be: Regex, Rule Variable, Condition Variable, or a Server Variable.
/// </summary>
public class PatternSegment
{
public string Variable { get; } // TODO make this a range s.t. we don't copy the string.
public SegmentType Type { get; }
/// <summary>
/// Create a Pattern segment.
/// </summary>
/// <param name="variable"></param>
/// <param name="type"></param>
public PatternSegment(string variable, SegmentType type)
{
Variable = variable;
Type = type;
}
}
}

View File

@ -0,0 +1,122 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Rewrite.ModRewrite
{
public class RuleBuilder
{
private ParsedModRewriteExpression _pce;
private List<Condition> _conditions;
private RuleFlags _flags;
private Pattern _patterns;
public ModRewriteRule Build()
{
var ruleExpression = ExpressionCreator.CreateRuleExpression(_pce, _flags);
return new ModRewriteRule(_conditions, ruleExpression, _patterns, _flags);
}
public RuleBuilder(string initialRule, string transformation) :
this(initialRule, transformation, flags: null)
{
}
public RuleBuilder(string rule)
{
var tokens = Tokenizer.Tokenize(rule);
if (tokens.Count == 3)
{
CreateRule(tokens[1], tokens[2], flags: null);
}
else if (tokens.Count == 4)
{
CreateRule(tokens[1], tokens[2], tokens[3]);
}
else
{
throw new ArgumentException();
}
}
public RuleBuilder(string initialRule, string transformation, string flags)
{
CreateRule(initialRule, transformation, flags);
}
public void CreateRule(string initialRule, string transformation, string flags)
{
_pce = RuleRegexParser.ParseRuleRegex(initialRule);
_patterns = ConditionTestStringParser.ParseConditionTestString(transformation);
_flags = FlagParser.ParseRuleFlags(flags);
}
public void AddCondition(string condition)
{
if (_conditions == null)
{
_conditions = new List<Condition>();
}
var condBuilder = new ConditionBuilder(condition);
_conditions.Add(condBuilder.Build());
}
public void AddCondition(Condition condition)
{
if (_conditions == null)
{
_conditions = new List<Condition>();
}
_conditions.Add(condition);
}
public void AddConditions(List<Condition> conditions)
{
if (_conditions == null)
{
_conditions = new List<Condition>();
}
_conditions.AddRange(conditions);
}
public void SetFlag(string flag)
{
SetFlag(flag, value: null);
}
public void SetFlag(RuleFlagType flag)
{
SetFlag(flag, value: null);
}
public void SetFlag(string flag, string value)
{
if (_flags == null)
{
_flags = new RuleFlags();
}
_flags.SetFlag(flag, value);
}
public void SetFlag(RuleFlagType flag, string value)
{
if (_flags == null)
{
_flags = new RuleFlags();
}
_flags.SetFlag(flag, value);
}
public void SetFlags(string flags)
{
if (_flags == null)
{
_flags = FlagParser.ParseRuleFlags(flags);
}
else
{
FlagParser.ParseRuleFlags(flags, _flags);
}
}
}
}

View File

@ -0,0 +1,35 @@
// 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.ModRewrite
{
public enum RuleFlagType
{
EscapeBackreference,
Chain,
Cookie,
DiscardPath,
Env,
End,
Forbidden,
Gone,
Handler,
Last,
Next,
NoCase,
NoEscape,
NoSubReq,
NoVary,
Or,
Proxy,
PassThrough,
QSAppend,
QSDiscard,
QSLast,
Redirect,
Skip,
Type,
// Non-modrewrite rule
FullUrl
}
}

View File

@ -0,0 +1,133 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Rewrite.ModRewrite
{
public class RuleFlags
{
private IDictionary<string, RuleFlagType> _ruleFlagLookup = new Dictionary<string, RuleFlagType>(StringComparer.OrdinalIgnoreCase) {
{ "b", RuleFlagType.EscapeBackreference},
{ "c", RuleFlagType.Chain },
{ "chain", RuleFlagType.Chain},
{ "co", RuleFlagType.Cookie },
{ "cookie", RuleFlagType.Cookie },
{ "dpi", RuleFlagType.DiscardPath },
{ "discardpath", RuleFlagType.DiscardPath },
{ "e", RuleFlagType.Env},
{ "env", RuleFlagType.Env},
{ "end", RuleFlagType.End },
{ "f", RuleFlagType.Forbidden },
{ "forbidden", RuleFlagType.Forbidden },
{ "g", RuleFlagType.Gone },
{ "gone", RuleFlagType.Gone },
{ "h", RuleFlagType.Handler },
{ "handler", RuleFlagType.Handler },
{ "l", RuleFlagType.Last },
{ "last", RuleFlagType.Last },
{ "n", RuleFlagType.Next },
{ "next", RuleFlagType.Next },
{ "nc", RuleFlagType.NoCase },
{ "nocase", RuleFlagType.NoCase },
{ "ne", RuleFlagType.NoEscape },
{ "noescape", RuleFlagType.NoEscape },
{ "ns", RuleFlagType.NoSubReq },
{ "nosubreq", RuleFlagType.NoSubReq },
{ "p", RuleFlagType.Proxy },
{ "proxy", RuleFlagType.Proxy },
{ "pt", RuleFlagType.PassThrough },
{ "passthrough", RuleFlagType.PassThrough },
{ "qsa", RuleFlagType.QSAppend },
{ "qsappend", RuleFlagType.QSAppend },
{ "qsd", RuleFlagType.QSDiscard },
{ "qsdiscard", RuleFlagType.QSDiscard },
{ "qsl", RuleFlagType.QSLast },
{ "qslast", RuleFlagType.QSLast },
{ "r", RuleFlagType.Redirect },
{ "redirect", RuleFlagType.Redirect },
{ "s", RuleFlagType.Skip },
{ "skip", RuleFlagType.Skip },
{ "t", RuleFlagType.Type },
{ "type", RuleFlagType.Type },
// TODO make this a load bool instead of a flag for the file and rules.
{ "u", RuleFlagType.FullUrl },
{ "url", RuleFlagType.FullUrl }
};
public IDictionary<RuleFlagType, string> FlagDictionary { get; }
public RuleFlags(IDictionary<RuleFlagType, string> flags)
{
// TODO use ref to check dictionary equality
FlagDictionary = flags;
}
public RuleFlags()
{
FlagDictionary = new Dictionary<RuleFlagType, string>();
}
public void SetFlag(string flag, string value)
{
RuleFlagType res;
if (!_ruleFlagLookup.TryGetValue(flag, out res))
{
throw new FormatException("Invalid flag");
}
SetFlag(res, value);
}
public void SetFlag(RuleFlagType flag, string value)
{
if (value == null)
{
value = string.Empty;
}
FlagDictionary[flag] = value;
}
public string GetValue(RuleFlagType flag)
{
CleanupResources();
string res;
if (!FlagDictionary.TryGetValue(flag, out res))
{
return null;
}
return res;
}
public string this[RuleFlagType flag]
{
get
{
string res;
if (!FlagDictionary.TryGetValue(flag, out res))
{
return null;
}
return res;
}
set
{
FlagDictionary[flag] = value ?? string.Empty;
}
}
public bool HasFlag(RuleFlagType flag)
{
CleanupResources();
string res;
return FlagDictionary.TryGetValue(flag, out res);
}
private void CleanupResources()
{
if (_ruleFlagLookup != null)
{
_ruleFlagLookup = null;
}
}
}
}

View File

@ -0,0 +1,26 @@
// 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;
namespace Microsoft.AspNetCore.Rewrite.ModRewrite
{
public static class RuleRegexParser
{
public static ParsedModRewriteExpression ParseRuleRegex(string regex)
{
if (regex == null || regex == String.Empty)
{
throw new FormatException();
}
if (regex.StartsWith("!"))
{
return new ParsedModRewriteExpression { Invert = true, Operand = regex.Substring(1) };
}
else
{
return new ParsedModRewriteExpression { Invert = false, Operand = regex};
}
}
}
}

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
{
public enum SegmentType
{
Literal,
ServerParameter,
ConditionParameter,
RuleParameter
}
}

View File

@ -0,0 +1,180 @@
// 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.Globalization;
using System.Net.Sockets;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Rewrite.ModRewrite
{
/// <summary>
/// mod_rewrite lookups for specific string constants.
/// </summary>
public static class ServerVariables
{
public static HashSet<string> ValidServerVariables = new HashSet<string>()
{
"HTTP_ACCEPT",
"HTTP_COOKIE",
"HTTP_FORWARDED",
"HTTP_HOST",
"HTTP_PROXY_CONNECTION",
"HTTP_REFERER",
"HTTP_USER_AGENT",
"AUTH_TYPE",
"CONN_REMOTE_ADDR",
"CONTEXT_PREFIX",
"CONTEXT_DOCUMENT_ROOT",
"IPV6",
"PATH_INFO",
"QUERY_STRING",
"REMOTE_ADDR",
"REMOTE_HOST",
"REMOTE_IDENT",
"REMOTE_PORT",
"REMOTE_USER",
"REQUEST_METHOD",
"SCRIPT_FILENAME",
"DOCUMENT_ROOT",
"SCRIPT_GROUP",
"SCRIPT_USER",
"SERVER_ADDR",
"SERVER_ADMIN",
"SERVER_NAME",
"SERVER_PORT",
"SERVER_PROTOCOL",
"SERVER_SOFTWARE",
"TIME_YEAR",
"TIME_MON",
"TIME_DAY",
"TIME_HOUR",
"TIME_MIN",
"TIME_SEC",
"TIME_WDAY",
"TIME",
"API_VERSION",
"HTTPS",
"IS_SUBREQ",
"REQUEST_FILENAME",
"REQUEST_SCHEME",
"REQUEST_URI",
"THE_REQUEST"
};
/// <summary>
/// Translates mod_rewrite server variables strings to an enum of different server variables.
/// </summary>
/// <param name="variable">The server variable string.</param>
/// <param name="context">The HttpContext context.</param>
/// <returns>The appropriate enum if the server variable exists, else ServerVariable.None</returns>
public static string Resolve(string variable, HttpContext context)
{
// TODO talk about perf here
switch (variable)
{
case "HTTP_ACCEPT":
return context.Request.Headers[HeaderNames.Accept];
case "HTTP_COOKIE":
return context.Request.Headers[HeaderNames.Cookie];
case "HTTP_FORWARDED":
return context.Request.Headers["Forwarded"];
case "HTTP_HOST":
return context.Request.Headers[HeaderNames.Host];
case "HTTP_PROXY_CONNECTION":
return context.Request.Headers[HeaderNames.ProxyAuthenticate];
case "HTTP_REFERER":
return context.Request.Headers[HeaderNames.Referer];
case "HTTP_USER_AGENT":
return context.Request.Headers[HeaderNames.UserAgent];
case "AUTH_TYPE":
throw new NotImplementedException();
case "CONN_REMOTE_ADDR":
return context.Connection.RemoteIpAddress?.ToString();
case "CONTEXT_PREFIX":
throw new NotImplementedException();
case "CONTEXT_DOCUMENT_ROOT":
throw new NotImplementedException();
case "IPV6":
return context.Connection.LocalIpAddress.AddressFamily == AddressFamily.InterNetworkV6 ? "on" : "off";
case "PATH_INFO":
throw new NotImplementedException();
case "QUERY_STRING":
return context.Request.QueryString.Value;
case "REMOTE_ADDR":
return context.Connection.RemoteIpAddress?.ToString();
case "REMOTE_HOST":
throw new NotImplementedException();
case "REMOTE_IDENT":
throw new NotImplementedException();
case "REMOTE_PORT":
return context.Connection.RemotePort.ToString(CultureInfo.InvariantCulture);
case "REMOTE_USER":
throw new NotImplementedException();
case "REQUEST_METHOD":
return context.Request.Method;
case "SCRIPT_FILENAME":
throw new NotImplementedException();
case "DOCUMENT_ROOT":
throw new NotImplementedException();
case "SCRIPT_GROUP":
throw new NotImplementedException();
case "SCRIPT_USER":
throw new NotImplementedException();
case "SERVER_ADDR":
return context.Connection.LocalIpAddress?.ToString();
case "SERVER_ADMIN":
throw new NotImplementedException();
case "SERVER_NAME":
throw new NotImplementedException();
case "SERVER_PORT":
return context.Connection.LocalPort.ToString(CultureInfo.InvariantCulture);
case "SERVER_PROTOCOL":
return context.Features.Get<IHttpRequestFeature>()?.Protocol;
case "SERVER_SOFTWARE":
throw new NotImplementedException();
case "TIME_YEAR":
return DateTimeOffset.UtcNow.Year.ToString(CultureInfo.InvariantCulture);
case "TIME_MON":
return DateTimeOffset.UtcNow.Month.ToString(CultureInfo.InvariantCulture);
case "TIME_DAY":
return DateTimeOffset.UtcNow.Day.ToString(CultureInfo.InvariantCulture);
case "TIME_HOUR":
return DateTimeOffset.UtcNow.Hour.ToString(CultureInfo.InvariantCulture);
case "TIME_MIN":
return DateTimeOffset.UtcNow.Minute.ToString(CultureInfo.InvariantCulture);
case "TIME_SEC":
return DateTimeOffset.UtcNow.Second.ToString(CultureInfo.InvariantCulture);
case "TIME_WDAY":
return ((int) DateTimeOffset.UtcNow.DayOfWeek).ToString(CultureInfo.InvariantCulture);
case "TIME":
return DateTimeOffset.UtcNow.ToString(CultureInfo.InvariantCulture);
case "API_VERSION":
throw new NotImplementedException();
case "HTTPS":
return context.Request.IsHttps ? "on" : "off";
case "HTTP2":
return context.Request.Scheme == "http2" ? "on" : "off";
case "IS_SUBREQ":
// TODO maybe can do this? context.Request.HttpContext ?
throw new NotImplementedException();
case "REQUEST_FILENAME":
return context.Request.Path.Value.Substring(1);
case "REQUEST_SCHEME":
return context.Request.Scheme;
case "REQUEST_URI":
// TODO This isn't an ideal solution. What this assumes is that all conditions don't have a leading slash before it.
return context.Request.Path.Value.Substring(1);
case "THE_REQUEST":
// TODO
throw new NotImplementedException();
default:
return null;
}
}
}
}

View File

@ -0,0 +1,84 @@
// 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 Microsoft.AspNetCore.Rewrite.Internal;
namespace Microsoft.AspNetCore.Rewrite.ModRewrite
{
/// <summary>
/// Tokenizes a mod_rewrite rule, delimited by spaces.
/// </summary>
public static class Tokenizer
{
private const char Space = ' ';
private const char Escape = '\\';
private const char Tab = '\t';
/// <summary>
/// Splits a string on whitespace, ignoring spaces, creating into a list of strings.
/// </summary>
/// <param name="rule">The rule to tokenize.</param>
/// <returns>A list of tokens.</returns>
public static List<string> Tokenize(string rule)
{
// TODO make list of strings a reference to the original rule? (run into problems with escaped spaces).
// TODO handle "s and probably replace \ character with no slash.
if (string.IsNullOrEmpty(rule))
{
return null;
}
var context = new ParserContext(rule);
if (!context.Next())
{
return null;
}
var tokens = new List<string>();
context.Mark();
while (true)
{
if (!context.Next())
{
// End of string. Capture.
break;
}
else if (context.Current == Escape)
{
// Need to progress such that the next character is not evaluated.
if (!context.Next())
{
// Means that a character was not escaped appropriately Ex: "foo\"
throw new ArgumentException();
}
}
else if (context.Current == Space || context.Current == Tab)
{
// time to capture!
// TODO: This kinda sucks, set state and skip
var token = context.Capture();
if (!string.IsNullOrEmpty(token))
{
tokens.Add(token);
while (context.Current == Space || context.Current == Tab)
{
if (!context.Next())
{
// At end of string, we can return at this point.
return tokens;
}
}
context.Mark();
}
}
}
var done = context.Capture();
if (!string.IsNullOrEmpty(done))
{
tokens.Add(done);
}
return tokens;
}
}
}

View File

@ -0,0 +1,57 @@
// 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.Extensions.FileProviders;
namespace Microsoft.AspNetCore.Rewrite.Operands
{
public class IntegerOperand : Operand
{
public int Value { get; }
public IntegerOperationType Operation { get; }
public IntegerOperand(int value, IntegerOperationType operation)
{
Value = value;
Operation = operation;
}
public IntegerOperand(string value, IntegerOperationType operation)
{
int compValue;
if (!int.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out compValue))
{
throw new FormatException("Syntax error for integers in comparison.");
}
Operation = operation;
}
public override bool? CheckOperation(Match previous, string testString, IFileProvider fileProvider)
{
int compValue;
if (!int.TryParse(testString, NumberStyles.None, CultureInfo.InvariantCulture, out compValue))
{
return false;
}
switch (Operation)
{
case IntegerOperationType.Equal:
return compValue == Value;
case IntegerOperationType.Greater:
return compValue > Value;
case IntegerOperationType.GreaterEqual:
return compValue >= Value;
case IntegerOperationType.Less:
return compValue < Value;
case IntegerOperationType.LessEqual:
return compValue <= Value;
case IntegerOperationType.NotEqual:
return compValue != Value;
default:
return null;
}
}
}
}

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.
namespace Microsoft.AspNetCore.Rewrite.Operands
{
public enum IntegerOperationType
{
Equal,
Greater,
GreaterEqual,
Less,
LessEqual,
NotEqual
}
}

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.
using System.Text.RegularExpressions;
using Microsoft.Extensions.FileProviders;
namespace Microsoft.AspNetCore.Rewrite.Operands
{
public abstract class Operand
{
public abstract bool? CheckOperation(Match previous, string concatTestString, IFileProvider fileProvider);
}
}

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.Text.RegularExpressions;
using Microsoft.Extensions.FileProviders;
namespace Microsoft.AspNetCore.Rewrite.Operands
{
public class PropertyOperand : Operand
{
public PropertyOperationType Operation { get; }
public PropertyOperand(PropertyOperationType operation)
{
Operation = operation;
}
public override bool? CheckOperation(Match previous, string testString, IFileProvider fileProvider)
{
switch(Operation)
{
case PropertyOperationType.Directory:
return fileProvider.GetFileInfo(testString).IsDirectory;
case PropertyOperationType.RegularFile:
return fileProvider.GetFileInfo(testString).Exists;
case PropertyOperationType.Size:
var fileInfo = fileProvider.GetFileInfo(testString);
return fileInfo.Exists && fileInfo.Length > 0;
case PropertyOperationType.ExistingUrl:
throw new NotSupportedException("No support for internal sub requests.");
case PropertyOperationType.ExistingFile:
throw new NotSupportedException("No support for internal sub requests.");
case PropertyOperationType.SymbolicLink:
throw new NotSupportedException("No support for checking symbolic links.");
case PropertyOperationType.Executable:
throw new NotSupportedException("No support for checking executable permissions.");
default:
return false;
}
}
}
}

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.
namespace Microsoft.AspNetCore.Rewrite.Operands
{
public enum PropertyOperationType
{
None,
Directory,
RegularFile,
ExistingFile,
SymbolicLink,
Size,
ExistingUrl,
Executable
}
}

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.Extensions.FileProviders;
namespace Microsoft.AspNetCore.Rewrite.Operands
{
public class RegexOperand : Operand
{
public Regex RegexOperation { get; }
public RegexOperand(Regex regex)
{
RegexOperation = regex;
}
public override bool? CheckOperation(Match previous, string concatTestString, IFileProvider fileProvider)
{
previous = RegexOperation.Match(concatTestString);
return previous.Success;
}
}
}

View File

@ -0,0 +1,40 @@
// 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.Extensions.FileProviders;
namespace Microsoft.AspNetCore.Rewrite.Operands
{
public class StringOperand : Operand
{
public string Value { get; set; }
public StringOperationType Operation { get; set; }
public StringOperand(string value, StringOperationType operation)
{
Value = value;
Operation = operation;
}
public override bool? CheckOperation(Match previous, string concatTestString, IFileProvider fileProvider)
{
switch (Operation)
{
case StringOperationType.Equal:
return concatTestString.CompareTo(Value) == 0;
case StringOperationType.Greater:
return concatTestString.CompareTo(Value) > 0;
case StringOperationType.GreaterEqual:
return concatTestString.CompareTo(Value) >= 0;
case StringOperationType.Less:
return concatTestString.CompareTo(Value) < 0;
case StringOperationType.LessEqual:
return concatTestString.CompareTo(Value) <= 0;
default:
return null;
}
}
}
}

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.Operands
{
public enum StringOperationType
{
Equal,
Greater,
GreaterEqual,
Less,
LessEqual
}
}

View File

@ -0,0 +1,70 @@
// 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
{
/// <summary>
/// Represents a string iterator, with captures.
/// </summary>
public class ParserContext
{
private readonly string _template;
private int _index;
private int? _mark;
public ParserContext(string condition)
{
_template = condition;
_index = -1;
}
public char Current
{
get { return (_index < _template.Length && _index >= 0) ? _template[_index] : (char)0; }
}
public bool Back()
{
return --_index >= 0;
}
public bool Next()
{
return ++_index < _template.Length;
}
public bool HasNext()
{
return (_index + 1) < _template.Length;
}
public void Mark()
{
_mark = _index;
}
public int GetIndex()
{
return _index;
}
public string Capture()
{
// TODO make this return a range rather than a string.
if (_mark.HasValue)
{
var value = _template.Substring(_mark.Value, _index - _mark.Value);
_mark = null;
return value;
}
else
{
return null;
}
}
public string Error()
{
return string.Format("Syntax Error at index: ", _index, " with character: ", Current);
}
}
}

View File

@ -0,0 +1,19 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Microsoft.AspNetCore.Rewrite")]
[assembly: AssemblyTrademark("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("0e7ca1a7-1dc3-4ce6-b9c7-1688fe1410f1")]

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;
namespace Microsoft.AspNetCore.Rewrite.RuleAbstraction
{
public class FunctionalRule : Rule
{
public Func<UrlRewriteContext, RuleResult> OnApplyRule { get; set; }
public Transformation OnCompletion { get; set; } = Transformation.Rewrite;
public override RuleResult ApplyRule(UrlRewriteContext context) => OnApplyRule(context);
}
}

View File

@ -0,0 +1,48 @@
// 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.RuleAbstraction
{
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(UrlRewriteContext 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 new RuleResult { Result = RuleTerminiation.ResponseComplete };
}
else
{
context.HttpContext.Request.Path = path;
}
if (OnCompletion == Transformation.TerminatingRewrite)
{
return new RuleResult { Result = RuleTerminiation.StopRules };
}
else
{
return new RuleResult { Result = RuleTerminiation.Continue };
}
}
return new RuleResult { Result = RuleTerminiation.Continue };
}
}
}

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.RuleAbstraction
{
public abstract class Rule
{
public abstract RuleResult ApplyRule(UrlRewriteContext context);
}
}

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.
using Microsoft.AspNetCore.Rewrite.Operands;
namespace Microsoft.AspNetCore.Rewrite.RuleAbstraction
{
public class RuleExpression
{
public RegexOperand Operand { get; set; }
public bool Invert { get; set; }
}
}

View File

@ -0,0 +1,10 @@
// 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.RuleAbstraction
{
public class RuleResult
{
public RuleTerminiation Result { get; set; }
}
}

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Rewrite.RuleAbstraction
{
public enum RuleTerminiation
{
Continue,
ResponseComplete,
StopRules
}
}

View File

@ -0,0 +1,54 @@
// 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;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Rewrite.RuleAbstraction
{
public class SchemeRule : Rule
{
public int? SSLPort { get; set; }
public Transformation OnCompletion { get; set; } = Transformation.Rewrite;
public override RuleResult ApplyRule(UrlRewriteContext 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);
}
if ((OnCompletion != Transformation.Redirect))
{
context.HttpContext.Request.Scheme = "https";
context.HttpContext.Request.Host = host;
if (OnCompletion == Transformation.TerminatingRewrite)
{
return new RuleResult { Result = RuleTerminiation.StopRules };
}
else
{
return new RuleResult { Result = RuleTerminiation.Continue };
}
}
var req = context.HttpContext.Request;
var newUrl = new StringBuilder().Append("https://").Append(host).Append(req.PathBase).Append(req.Path).Append(req.QueryString);
context.HttpContext.Response.Redirect(newUrl.ToString());
return new RuleResult { Result = RuleTerminiation.ResponseComplete };
}
return new RuleResult { Result = RuleTerminiation.Continue }; ;
}
}
}

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.RuleAbstraction
{
public enum Transformation
{
Rewrite,
Redirect,
TerminatingRewrite
}
}

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 Microsoft.AspNetCore.Http;
using Microsoft.Extensions.FileProviders;
namespace Microsoft.AspNetCore.Rewrite
{
/// <summary>
/// The UrlRewrite Context contains the HttpContext of the request and the file provider to check conditions.
/// </summary>
public class UrlRewriteContext
{
public HttpContext HttpContext { get; set; }
public IFileProvider FileProvider { get; set; }
}
}

View File

@ -0,0 +1,35 @@
// 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;
namespace Microsoft.AspNetCore.Builder
{
/// <summary>
/// Extension methods for the <see cref="UrlRewriteMiddleware"/>
/// </summary>
public static class UrlRewriteExtensions
{
/// <summary>
/// Checks if a given Url matches rules and conditions, and modifies the HttpContext on match.
/// </summary>
/// <param name="app"></param>
/// <param name="options">Options for urlrewrite.</param>
/// <returns></returns>
public static IApplicationBuilder UseRewriter(this IApplicationBuilder app, UrlRewriteOptions options)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
// put middleware in pipeline
return app.UseMiddleware<UrlRewriteMiddleware>(options);
}
}
}

View File

@ -0,0 +1,76 @@
// 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.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Rewrite.RuleAbstraction;
using Microsoft.Extensions.FileProviders;
namespace Microsoft.AspNetCore.Rewrite
{
/// <summary>
/// Represents a middleware that rewrites urls imported from mod_rewrite, UrlRewrite, and code.
/// </summary>
public class UrlRewriteMiddleware
{
private readonly RequestDelegate _next;
private readonly UrlRewriteOptions _options;
private readonly IFileProvider _fileProvider;
/// <summary>
/// Creates a new instance of <see cref="UrlRewriteMiddleware"/>
/// </summary>
/// <param name="next">The delegate representing the next middleware in the request pipeline.</param>
/// <param name="hostingEnv">The Hosting Environment.</param>
/// <param name="options">The middleware options, containing the rules to apply.</param>
public UrlRewriteMiddleware(RequestDelegate next, IHostingEnvironment hostingEnv, UrlRewriteOptions options)
{
if (next == null)
{
throw new ArgumentNullException(nameof(next));
}
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
_next = next;
_options = options;
_fileProvider = _options.FileProvider ?? hostingEnv.WebRootFileProvider;
}
/// <summary>
/// Executes the middleware.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> for the current request.</param>
/// <returns>A task that represents the execution of this middleware.</returns>
public Task Invoke(HttpContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var urlContext = new UrlRewriteContext { HttpContext = context, FileProvider = _fileProvider };
foreach (var rule in _options.Rules)
{
// Apply the rule
var result = rule.ApplyRule(urlContext);
switch (result.Result)
{
case RuleTerminiation.Continue:
// Explicitly show that we continue executing rules
break;
case RuleTerminiation.ResponseComplete:
// TODO cache task for perf
return Task.FromResult(0);
case RuleTerminiation.StopRules:
return _next(context);
}
}
return _next(context);
}
}
}

View File

@ -0,0 +1,21 @@
// 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 Microsoft.AspNetCore.Rewrite.RuleAbstraction;
using Microsoft.Extensions.FileProviders;
namespace Microsoft.AspNetCore.Rewrite
{
/// <summary>
/// Options for the <see cref="UrlRewriteMiddleware"/>
/// </summary>
public class UrlRewriteOptions
{
/// <summary>
/// The ordered list of rules to apply to the context.
/// </summary>
public List<Rule> Rules { get; set; } = new List<Rule>();
public IFileProvider FileProvider { get; set; }
}
}

View File

@ -0,0 +1,108 @@
// 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.Text.RegularExpressions;
using Microsoft.AspNetCore.Rewrite.ModRewrite;
using Microsoft.AspNetCore.Rewrite.RuleAbstraction;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.FileProviders;
namespace Microsoft.AspNetCore.Rewrite
{
/// <summary>
/// The builder to a list of rules for <see cref="UrlRewriteOptions"/> and <see cref="UrlRewriteMiddleware"/>
/// </summary>
public static class UrlRewriteOptionsAddRulesExtensions
{
/// <summary>
/// Adds a rule to the current rules.
/// </summary>
/// <param name="options">The UrlRewrite options.</param>
/// <param name="rule">A rule to be added to the current rules.</param>
public static UrlRewriteOptions AddRule(this UrlRewriteOptions options, Rule rule)
{
options.Rules.Add(rule);
return options;
}
/// <summary>
/// Adds a list of rules to the current rules.
/// </summary>
/// <param name="options">The UrlRewrite options.</param>
/// <param name="rules">A list of rules.</param>
public static UrlRewriteOptions AddRules(this UrlRewriteOptions options, List<Rule> rules)
{
options.Rules.AddRange(rules);
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 UrlRewriteOptions RewritePath(this UrlRewriteOptions options, string regex, string newPath, bool stopRewriteOnSuccess = false)
{
options.Rules.Add(new PathRule { MatchPattern = new Regex(regex, RegexOptions.Compiled, TimeSpan.FromMilliseconds(1)), OnMatch = newPath, OnCompletion = stopRewriteOnSuccess ? Transformation.TerminatingRewrite : Transformation.Rewrite });
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 UrlRewriteOptions RewriteScheme(this UrlRewriteOptions options, bool stopRewriteOnSuccess = false)
{
options.Rules.Add(new SchemeRule {OnCompletion = stopRewriteOnSuccess ? Transformation.TerminatingRewrite : Transformation.Rewrite });
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 UrlRewriteOptions RedirectPath(this UrlRewriteOptions options, string regex, string newPath, bool stopRewriteOnSuccess = false)
{
options.Rules.Add(new PathRule { MatchPattern = new Regex(regex, RegexOptions.Compiled, TimeSpan.FromMilliseconds(1)), OnMatch = newPath, OnCompletion = Transformation.Redirect });
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 UrlRewriteOptions RedirectScheme(this UrlRewriteOptions options, int? sslPort)
{
options.Rules.Add(new SchemeRule { SSLPort = sslPort, OnCompletion = Transformation.Redirect });
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 UrlRewriteOptions CustomRule(this UrlRewriteOptions options, Func<UrlRewriteContext, RuleResult> onApplyRule, Transformation transform, string description = null)
{
options.Rules.Add(new FunctionalRule { OnApplyRule = onApplyRule, OnCompletion = transform});
return options;
}
}
}

View File

@ -0,0 +1,37 @@
{
"version": "1.1.0-*",
"buildOptions": {
"warningsAsErrors": true,
"keyFile": "../../tools/Key.snk",
"nowarn": [
"CS1591"
],
"xmlDoc": true
},
"description": "ASP.NET Core basic middleware for:\r\nRewrite Url module.",
"packOptions": {
"repository": {
"type": "git",
"url": "git://github.com/aspnet/basicmiddleware"
},
"tags": [
"aspnetcore",
"proxy",
"headers",
"xforwarded"
]
},
"dependencies": {
"Microsoft.AspNetCore.Hosting.Abstractions": "1.1.0-*",
"Microsoft.AspNetCore.Http.Extensions": "1.1.0-*",
"Microsoft.Extensions.Configuration.Abstractions": "1.1.0-*",
"Microsoft.Extensions.FileProviders.Physical": "1.1.0-*",
"Microsoft.Extensions.Logging.Abstractions": "1.1.0-*",
"Microsoft.Extensions.Options": "1.1.0-*",
"System.Text.RegularExpressions": "4.1.0-*"
},
"frameworks": {
"net451": {},
"netstandard1.3": {}
}
}

View File

@ -1,6 +1,5 @@
// 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.Net;
using Xunit;

View File

@ -0,0 +1,99 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Rewrite.ModRewrite;
using Xunit;
namespace Microsoft.AspNetCore.Rewrite
{
public class ConditionActionTest
{
[Theory]
[InlineData(">hey", OperationType.Greater, "hey", ConditionType.StringComp)]
[InlineData("<hey", OperationType.Less, "hey", ConditionType.StringComp)]
[InlineData(">=hey", OperationType.GreaterEqual, "hey", ConditionType.StringComp)]
[InlineData("<=hey", OperationType.LessEqual, "hey", ConditionType.StringComp)]
[InlineData("=hey", OperationType.Equal, "hey", ConditionType.StringComp)]
public void ConditionParser_CheckStringComp(string condition, OperationType operation, string variable, ConditionType conditionType)
{
var results = ConditionPatternParser.ParseActionCondition(condition);
var expected = new ParsedModRewriteExpression { Operation = operation, Type = conditionType, Operand = variable, Invert = false };
Assert.True(CompareConditions(results, expected));
}
[Fact]
public void ConditionParser_CheckRegexEqual()
{
var condition = @"(.*)";
var results = ConditionPatternParser.ParseActionCondition(condition);
var expected = new ParsedModRewriteExpression { Type = ConditionType.Regex, Operand = "(.*)", Invert = false };
Assert.True(CompareConditions(results, expected));
}
[Theory]
[InlineData("-d", OperationType.Directory, ConditionType.PropertyTest)]
[InlineData("-f", OperationType.RegularFile, ConditionType.PropertyTest)]
[InlineData("-F", OperationType.ExistingFile, ConditionType.PropertyTest)]
[InlineData("-h", OperationType.SymbolicLink, ConditionType.PropertyTest)]
[InlineData("-L", OperationType.SymbolicLink, ConditionType.PropertyTest)]
[InlineData("-l", OperationType.SymbolicLink, ConditionType.PropertyTest)]
[InlineData("-s", OperationType.Size, ConditionType.PropertyTest)]
[InlineData("-U", OperationType.ExistingUrl, ConditionType.PropertyTest)]
[InlineData("-x", OperationType.Executable, ConditionType.PropertyTest)]
public void ConditionParser_CheckFileOperations(string condition, OperationType operation, ConditionType cond)
{
var results = ConditionPatternParser.ParseActionCondition(condition);
var expected = new ParsedModRewriteExpression { Type = cond, Operation = operation , Invert = false };
Assert.True(CompareConditions(results, expected));
}
[Theory]
[InlineData("!-d", OperationType.Directory, ConditionType.PropertyTest)]
[InlineData("!-f", OperationType.RegularFile, ConditionType.PropertyTest)]
[InlineData("!-F", OperationType.ExistingFile, ConditionType.PropertyTest)]
[InlineData("!-h", OperationType.SymbolicLink, ConditionType.PropertyTest)]
[InlineData("!-L", OperationType.SymbolicLink, ConditionType.PropertyTest)]
[InlineData("!-l", OperationType.SymbolicLink, ConditionType.PropertyTest)]
[InlineData("!-s", OperationType.Size, ConditionType.PropertyTest)]
[InlineData("!-U", OperationType.ExistingUrl, ConditionType.PropertyTest)]
[InlineData("!-x", OperationType.Executable, ConditionType.PropertyTest)]
public void ConditionParser_CheckFileOperationsInverted(string condition, OperationType operation, ConditionType cond)
{
var results = ConditionPatternParser.ParseActionCondition(condition);
var expected = new ParsedModRewriteExpression { Type = cond, Operation = operation, Invert = true };
Assert.True(CompareConditions(results, expected));
}
[Theory]
[InlineData("-gt1", OperationType.Greater, "1", ConditionType.IntComp)]
[InlineData("-lt1", OperationType.Less, "1", ConditionType.IntComp)]
[InlineData("-ge1", OperationType.GreaterEqual, "1", ConditionType.IntComp)]
[InlineData("-le1", OperationType.LessEqual, "1", ConditionType.IntComp)]
[InlineData("-eq1", OperationType.Equal, "1", ConditionType.IntComp)]
[InlineData("-ne1", OperationType.NotEqual, "1", ConditionType.IntComp)]
public void ConditionParser_CheckIntComp(string condition, OperationType operation, string variable, ConditionType cond)
{
var results = ConditionPatternParser.ParseActionCondition(condition);
var expected = new ParsedModRewriteExpression { Type = cond, Operation = operation, Invert = false, Operand = variable };
Assert.True(CompareConditions(results, expected));
}
// TODO negative tests
private bool CompareConditions(ParsedModRewriteExpression i1, ParsedModRewriteExpression i2)
{
if (i1.Operation != i2.Operation ||
i1.Type != i2.Type ||
i1.Operand != i2.Operand ||
i1.Invert != i2.Invert)
{
return false;
}
return true;
}
}
}

View File

@ -0,0 +1,58 @@
// 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.Linq;
using Microsoft.AspNetCore.Rewrite.ModRewrite;
using Xunit;
namespace Microsoft.AspNetCore.Rewrite
{
public class FlagParserTest
{
[Fact]
public void FlagParser_CheckSingleTerm()
{
var results = FlagParser.ParseRuleFlags("[NC]");
var dict = new Dictionary<RuleFlagType, string>();
dict.Add(RuleFlagType.NoCase, string.Empty);
var expected = new RuleFlags(dict);
Assert.True(DictionaryContentsEqual(results.FlagDictionary, expected.FlagDictionary));
}
[Fact]
public void FlagParser_CheckManyTerms()
{
var results = FlagParser.ParseRuleFlags("[NC,F,L]");
var dict = new Dictionary<RuleFlagType, string>();
dict.Add(RuleFlagType.NoCase, string.Empty);
dict.Add(RuleFlagType.Forbidden, string.Empty);
dict.Add(RuleFlagType.Last, string.Empty);
var expected = new RuleFlags(dict);
Assert.True(DictionaryContentsEqual(results.FlagDictionary, expected.FlagDictionary));
}
[Fact]
public void FlagParser_CheckManyTermsWithEquals()
{
var results = FlagParser.ParseRuleFlags("[NC,F,R=301]");
var dict = new Dictionary<RuleFlagType, string>();
dict.Add(RuleFlagType.NoCase, string.Empty);
dict.Add(RuleFlagType.Forbidden, string.Empty);
dict.Add(RuleFlagType.Redirect, "301");
var expected = new RuleFlags(dict);
Assert.True(DictionaryContentsEqual(results.FlagDictionary, expected.FlagDictionary));
}
public bool DictionaryContentsEqual<TKey, TValue>(IDictionary<TKey, TValue> dictionary, IDictionary<TKey, TValue> other)
{
return (other ?? new Dictionary<TKey, TValue>())
.OrderBy(kvp => kvp.Key)
.SequenceEqual((dictionary ?? new Dictionary<TKey, TValue>())
.OrderBy(kvp => kvp.Key));
}
}
}

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>31794f9e-a1aa-4535-b03c-a3233737cd1a</ProjectGuid>
<RootNamespace>Microsoft.AspNetCore.Rewrite.Tests</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<ItemGroup>
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
</ItemGroup>
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@ -0,0 +1,50 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Rewrite.ModRewrite;
using Microsoft.AspNetCore.Rewrite.Operands;
using Xunit;
namespace RewriteTest
{
// This file tests an input of a list of tokens and verifies that the appropriate condition is obtained
public class ModRewriteConditionBuilderTest
{
[Fact]
public void ConditionBuilder_PassInNoFlagsFlagsEmpty()
{
var conditionString = "RewriteCond /$1 /hello";
var builder = new ConditionBuilder(conditionString);
var results = builder.Build();
//var expected = new Condition(
// new Pattern(
// new List<PatternSegment>() {
// new PatternSegment("/", SegmentType.Literal),
// new PatternSegment("1", SegmentType.RuleParameter)
// }),
// new ConditionExpression { Operand = new RegexOperand {Regex = new Regex("/hello") } },
// new ConditionFlags());
var expected = (new ConditionBuilder("/$1", "/hello")).Build();
Assert.True(results.Flags.FlagDictionary.Count == 0);
Assert.True(results.Flags.FlagDictionary.Count == expected.Flags.FlagDictionary.Count);
Assert.True((results.ConditionExpression.Operand is RegexOperand)
&& (expected.ConditionExpression.Operand is RegexOperand));
}
[Fact]
public void ConditionBuilder_PassInFlagsFlagsExist()
{
var conditionString = "RewriteCond /$1 /hello [NC]";
var builder = new ConditionBuilder(conditionString);
var results = builder.Build();
var expected = (new ConditionBuilder("/$1", "/hello", "[NC]")).Build();
Assert.True(results.Flags.FlagDictionary.Count == 1);
Assert.True(results.Flags.FlagDictionary.Count == expected.Flags.FlagDictionary.Count);
Assert.True((results.ConditionExpression.Operand is RegexOperand)
&& (expected.ConditionExpression.Operand is RegexOperand));
}
}
}

View File

@ -0,0 +1,9 @@
// 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
{
public class ModRewriteCreatorTest
{
}
}

View File

@ -0,0 +1,88 @@
// 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;
using Microsoft.AspNetCore.Rewrite.ModRewrite;
using Microsoft.AspNetCore.Rewrite.Operands;
using Microsoft.AspNetCore.Rewrite.RuleAbstraction;
using Xunit;
namespace Microsoft.AspNetCore.Rewrite
{
public class ModRewriteFlagTest
{
// Flag tests
[Fact]
public void ModRewriteRule_Check403OnForbiddenFlag()
{
var context = new UrlRewriteContext { HttpContext = CreateRequest("/", "/hey/hello") };
var rule = new ModRewriteRule
{
InitialRule = new RuleExpression { Operand = new RegexOperand(new Regex("/hey/(.*)")) , Invert = false },
Transform = ConditionTestStringParser.ParseConditionTestString("/$1"),
Flags = FlagParser.ParseRuleFlags("[F]")
};
var res = rule.ApplyRule(context);
Assert.True(res.Result == RuleTerminiation.ResponseComplete);
Assert.True(context.HttpContext.Response.StatusCode == 403);
}
[Fact]
public void ModRewriteRule_Check410OnGoneFlag()
{
var context = new UrlRewriteContext { HttpContext = CreateRequest("/", "/hey/hello") };
var rule = new ModRewriteRule
{
InitialRule = new RuleExpression { Operand = new RegexOperand(new Regex("/hey/(.*)")), Invert = false },
Transform = ConditionTestStringParser.ParseConditionTestString("/$1"),
Flags = FlagParser.ParseRuleFlags("[G]")
};
var res = rule.ApplyRule(context);
Assert.True(res.Result == RuleTerminiation.ResponseComplete);
Assert.True(context.HttpContext.Response.StatusCode == 410);
}
[Fact]
public void ModRewriteRule_CheckLastFlag()
{
var context = new UrlRewriteContext { HttpContext = CreateRequest("/", "/hey/hello") };
var rule = new ModRewriteRule
{
InitialRule = new RuleExpression { Operand = new RegexOperand(new Regex("/hey/(.*)")), Invert = false },
Transform = ConditionTestStringParser.ParseConditionTestString("/$1"),
Flags = FlagParser.ParseRuleFlags("[L]")
};
var res = rule.ApplyRule(context);
Assert.True(res.Result == RuleTerminiation.StopRules);
Assert.True(context.HttpContext.Request.Path.Equals(new PathString("/hello")));
}
[Fact]
public void ModRewriteRule_CheckRedirectFlag()
{
// TODO fix this test.
var context = new UrlRewriteContext { HttpContext = CreateRequest("/", "/hey/hello") };
var rule = new ModRewriteRule
{
InitialRule = new RuleExpression { Operand = new RegexOperand(new Regex("/hey/(.*)")), Invert = false },
Transform = ConditionTestStringParser.ParseConditionTestString("/$1"),
Flags = FlagParser.ParseRuleFlags("[G]")
};
var res = rule.ApplyRule(context);
Assert.True(res.Result == RuleTerminiation.ResponseComplete);
Assert.True(context.HttpContext.Response.StatusCode == 410);
}
private HttpContext CreateRequest(string basePath, string requestPath, string requestQuery = "", string hostName = "")
{
HttpContext context = new DefaultHttpContext();
context.Request.PathBase = new PathString(basePath);
context.Request.Path = new PathString(requestPath);
context.Request.QueryString = new QueryString(requestQuery);
context.Request.Host = new HostString(hostName);
return context;
}
}
}

View File

@ -0,0 +1,278 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO;
using System.Net;
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
{
public class ModRewriteMiddlewareTest
{
[Fact]
public async Task Invoke_RewritePathWhenMatching()
{
var options = new UrlRewriteOptions().AddModRewriteRule("RewriteRule /hey/(.*) /$1 ");
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseRewriter(options);
app.Run(context => context.Response.WriteAsync(context.Request.Path));
});
var server = new TestServer(builder);
var response = await server.CreateClient().GetStringAsync("/hey/hello");
Assert.Equal(response, "/hello");
}
[Fact]
public async Task Invoke_RewritePathTerminatesOnFirstSuccessOfRule()
{
var options = new UrlRewriteOptions().AddModRewriteRule("RewriteRule /hey/(.*) /$1 [L]")
.AddModRewriteRule("RewriteRule /hello /what");
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseRewriter(options);
app.Run(context => context.Response.WriteAsync(context.Request.Path));
});
var server = new TestServer(builder);
var response = await server.CreateClient().GetStringAsync("/hey/hello");
Assert.Equal(response, "/hello");
}
[Fact]
public async Task Invoke_RewritePathDoesNotTerminateOnFirstSuccessOfRule()
{
var options = new UrlRewriteOptions().AddModRewriteRule("RewriteRule /hey/(.*) /$1")
.AddModRewriteRule("RewriteRule /hello /what");
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseRewriter(options);
app.Run(context => context.Response.WriteAsync(context.Request.Path));
});
var server = new TestServer(builder);
var response = await server.CreateClient().GetStringAsync("/hey/hello");
Assert.Equal(response, "/what");
}
[Theory]
[InlineData("", true)]
public void Invoke_StringComparisonTests(string input, bool expected)
{
}
[Fact]
public async Task Invoke_ShouldIgnoreComments()
{
var options = new UrlRewriteOptions().ImportFromModRewrite(new StringReader("#RewriteRule ^/hey/(.*) /$1 "));
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseRewriter(options);
app.Run(context => context.Response.WriteAsync(context.Request.Path));
});
var server = new TestServer(builder);
var response = await server.CreateClient().GetStringAsync("/hey/hello");
Assert.Equal(response, "/hey/hello");
}
// TODO Add tests to check '//' being handled appropriately.
[Fact]
public async Task Invoke_ShouldRewriteHomepage()
{
var options = new UrlRewriteOptions().ImportFromModRewrite(new StringReader(@"RewriteRule ^/$ /homepage.html"));
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseRewriter(options);
app.Run(context => context.Response.WriteAsync(context.Request.Path));
});
var server = new TestServer(builder);
var response = await server.CreateClient().GetStringAsync("http://www.foo.org/");
Assert.Equal(response, "/homepage.html");
}
[Fact]
public async Task Invoke_ShouldIgnorePorts()
{
var options = new UrlRewriteOptions().ImportFromModRewrite(new StringReader(@"RewriteRule ^/$ /homepage.html"));
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseRewriter(options);
app.Run(context => context.Response.WriteAsync(context.Request.Path));
});
var server = new TestServer(builder);
var response = await server.CreateClient().GetStringAsync("http://www.foo.org:42/");
Assert.Equal(response, "/homepage.html");
}
[Fact]
public async Task Invoke_HandleNegatedRewriteRules()
{
var options = new UrlRewriteOptions().ImportFromModRewrite(new StringReader(@"RewriteRule !^/$ /homepage.html"));
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseRewriter(options);
app.Run(context => context.Response.WriteAsync(context.Request.Path));
});
var server = new TestServer(builder);
var response = await server.CreateClient().GetStringAsync("http://www.foo.org/");
Assert.Equal(response, "/");
}
[Fact]
public async Task Invoke_BackReferencesShouldBeApplied()
{
var options = new UrlRewriteOptions().ImportFromModRewrite(new StringReader(@"RewriteRule (.*)\.aspx $1.php"));
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseRewriter(options);
app.Run(context => context.Response.WriteAsync(context.Request.Path));
});
var server = new TestServer(builder);
var response = await server.CreateClient().GetStringAsync("http://www.foo.org/homepage.aspx");
Assert.Equal(response, "/homepage.php");
}
[Theory]
[InlineData("http://www.foo.org/homepage.aspx", @"RewriteRule (.*)\.aspx $1.php", "/homepage.php")]
[InlineData("http://www.foo.org/homepage.ASPX", @"RewriteRule (.*)\.aspx $1.php", "/homepage.ASPX")]
[InlineData("http://www.foo.org/homepage.aspx", @"RewriteRule (.*)\.aspx $1.php [NC]", "/homepage.php")]
[InlineData("http://www.foo.org/homepage.ASPX", @"RewriteRule (.*)\.aspx $1.php [NC]", "/homepage.php")]
[InlineData("http://www.foo.org/homepage.aspx", @"RewriteRule (.*)\.aspx $1.php [nocase]", "/homepage.php")]
[InlineData("http://www.foo.org/homepage.ASPX", @"RewriteRule (.*)\.aspx $1.php [nocase]", "/homepage.php")]
public async Task Invoke_ShouldHandleFlagNoCase(string url, string rule, string expected)
{
var options = new UrlRewriteOptions().ImportFromModRewrite(new StringReader(rule));
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseRewriter(options);
app.Run(context => context.Response.WriteAsync(context.Request.Path));
});
var server = new TestServer(builder);
var response = await server.CreateClient().GetStringAsync(url);
Assert.Equal(response, expected);
}
[Fact(Skip="Need to handle escape characters")]
public async Task Invoke_HandleMultipleBackReferences()
{
var options = new UrlRewriteOptions()
.ImportFromModRewrite(new StringReader(@"RewriteRule ^/blog/([0-9]+)-([a-z]+) /blog/index.php?archive=$1-$2"));
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseRewriter(options);
app.Run(context => context.Response.WriteAsync(context.Request.Path));
});
var server = new TestServer(builder);
var response = await server.CreateClient().GetStringAsync("http://www.foo.org/blog/2016-jun");
Assert.Equal(response, @"/blog/index.php?archive=2016-jun");
}
[Fact]
public async Task Invoke_CheckFullUrlWithUFlagOnlyPath()
{
var options = new UrlRewriteOptions()
.ImportFromModRewrite(new StringReader(@"RewriteRule (.+) http://www.example.com$1/ [U]"));
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseRewriter(options);
app.Run(context => context.Response.WriteAsync(context.Request.Path));
});
var server = new TestServer(builder);
var response = await server.CreateClient().GetStringAsync("http://www.foo.org/blog/2016-jun");
Assert.Equal(response, @"/blog/2016-jun/");
}
[Fact]
public async Task Invoke_CheckFullUrlWithUFlag()
{
var options = new UrlRewriteOptions()
.ImportFromModRewrite(new StringReader(@"RewriteRule (.+) http://www.example.com$1/ [U]"));
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseRewriter(options);
app.Run(context => context.Response.WriteAsync(context.Request.Scheme + "://" + context.Request.Host.Host + context.Request.Path + context.Request.QueryString));
});
var server = new TestServer(builder);
var response = await server.CreateClient().GetStringAsync("http://www.foo.org/blog/2016-jun");
Assert.Equal(response, @"http://www.example.com/blog/2016-jun/");
}
[Fact]
public async Task Invoke_CheckModFileConditions()
{
var options = new UrlRewriteOptions()
.ImportFromModRewrite(new StringReader(@"RewriteRule (.+) http://www.example.com$1/ [U]"));
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseRewriter(options);
app.Run(context => context.Response.WriteAsync(context.Request.Scheme + "://" + context.Request.Host.Host + context.Request.Path + context.Request.QueryString));
});
var server = new TestServer(builder);
var response = await server.CreateClient().GetStringAsync("http://www.foo.org/blog/2016-jun");
Assert.Equal(response, @"http://www.example.com/blog/2016-jun/");
}
[Theory]
[InlineData("http://www.example.com/foo/")]
public async Task Invoke_EnsureHttps(string input)
{
var options = new UrlRewriteOptions()
.ImportFromModRewrite(new StringReader("RewriteCond %{REQUEST_URI} ^foo/ \nRewriteCond %{HTTPS} !on \nRewriteRule ^(.*)$ https://www.example.com$1 [R=301,L,U]"));
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseRewriter(options);
app.Run(context => context.Response.WriteAsync(context.Request.Scheme + "://" + context.Request.Host.Host + context.Request.Path + context.Request.QueryString));
});
var server = new TestServer(builder);
var response = await server.CreateClient().GetAsync(input);
Assert.Equal(response.StatusCode, (HttpStatusCode)301);
Assert.Equal(response.Headers.Location.AbsoluteUri, @"https://www.example.com/foo/");
}
}
}

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;
using Microsoft.AspNetCore.Http;
using Xunit;
namespace Microsoft.AspNetCore.Rewrite
{
public class ModRewriteRuleBuilderTest
{
}
}

View File

@ -0,0 +1,19 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Microsoft.AspNetCore.Rewrite.Tests")]
[assembly: AssemblyTrademark("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("31794f9e-a1aa-4535-b03c-a3233737cd1a")]

View File

@ -0,0 +1,92 @@
// 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.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Builder.Internal;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.TestHost;
using Xunit;
namespace Microsoft.AspNetCore.Rewrite
{
public class RewriteMiddlewareTests
{
[Theory(Skip = "Adapting to TestServer")]
[InlineData("/foo", "", "/foo", "/yes")]
[InlineData("/foo", "", "/foo/", "/yes")]
[InlineData("/foo", "/Bar", "/foo", "/yes")]
[InlineData("/foo", "/Bar", "/foo/cho", "/yes")]
[InlineData("/foo", "/Bar", "/foo/cho/", "/yes")]
[InlineData("/foo/cho", "/Bar", "/foo/cho", "/yes")]
[InlineData("/foo/cho", "/Bar", "/foo/cho/do", "/yes")]
public void PathMatchFunc_RewriteDone(string matchPath, string basePath, string requestPath, string rewrite)
{
var context = CreateRequest(basePath, requestPath);
var options = new UrlRewriteOptions().RewritePath(matchPath, rewrite, false);
var builder = new ApplicationBuilder(serviceProvider: null)
.UseRewriter(options);
var app = builder.Build();
app.Invoke(context).Wait();
Assert.Equal(rewrite, context.Request.Path);
}
[Theory(Skip = "Adapting to TestServer")]
[InlineData(@"/(?<name>\w+)?/(?<id>\w+)?", @"", "/hey/hello", "/${id}/${name}", "/hello/hey")]
[InlineData(@"/(?<name>\w+)?/(?<id>\w+)?/(?<temp>\w+)?", @"", "/hey/hello/what", "/${temp}/${id}/${name}", "/what/hello/hey")]
public void PathMatchFunc_RegexRewriteDone(string matchPath, string basePath, string requestPath, string rewrite, string expected)
{
var context = CreateRequest(basePath, requestPath);
var options = new UrlRewriteOptions().RewritePath(matchPath, rewrite, false);
var builder = new ApplicationBuilder(serviceProvider: null)
.UseRewriter(options);
var app = builder.Build();
app.Invoke(context).Wait();
Assert.Equal(expected, context.Request.Path);
}
[Fact(Skip = "Adapting to TestServer")]
public void PathMatchFunc_RedirectScheme()
{
HttpContext context = CreateRequest("/", "/");
context.Request.Scheme = "http";
var options = new UrlRewriteOptions().RedirectScheme(30);
var builder = new ApplicationBuilder(serviceProvider: null)
.UseRewriter(options);
var app = builder.Build();
app.Invoke(context).Wait();
Assert.True(context.Response.Headers["location"].First().StartsWith("https"));
}
[Theory(Skip = "Adapting to TestServer")]
public async Task PathMatchFunc_RewriteScheme()
{
var options = new UrlRewriteOptions().RewriteScheme();
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseRewriter(options);
app.Run(context => context.Response.WriteAsync(context.Request.Path));
});
var server = new TestServer(builder);
var response = await server.CreateClient().GetAsync("http://foo.com/bar");
//Assert.True(response.RequestMessage.);
}
private HttpContext CreateRequest(string basePath, string requestPath)
{
HttpContext context = new DefaultHttpContext();
context.Request.PathBase = new PathString(basePath);
context.Request.Path = new PathString(requestPath);
context.Request.Host = new HostString("example.com");
return context;
}
}
}

View File

@ -0,0 +1,39 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Rewrite.ModRewrite;
using Xunit;
namespace Microsoft.AspNetCore.Rewrite
{
public class RewriteTokenizerTest
{
[Fact]
public void Tokenize_RewriteCondtion()
{
var testString = "RewriteCond %{HTTPS} !-f";
var tokens = Tokenizer.Tokenize(testString);
var expected = new List<string>();
expected.Add("RewriteCond");
expected.Add("%{HTTPS}");
expected.Add("!-f");
Assert.Equal(tokens, expected);
}
[Fact]
public void Tokenize_CheckEscapedSpaceIgnored()
{
// TODO need consultation on escape characters.
var testString = @"RewriteCond %{HTTPS}\ what !-f";
var tokens = Tokenizer.Tokenize(testString);
var expected = new List<string>();
expected.Add("RewriteCond");
expected.Add(@"%{HTTPS}\ what"); // TODO maybe just have the space here? talking point
expected.Add("!-f");
Assert.Equal(tokens,expected);
}
}
}

View File

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Rewrite.ModRewrite;
using Microsoft.AspNetCore.Rewrite.Operands;
using Microsoft.AspNetCore.Rewrite.RuleAbstraction;
using Xunit;
namespace Microsoft.AspNetCore.Rewrite.Tests.RuleAbstraction
{
public class RuleRegexParserTest
{
[Fact]
public void RuleRegexParser_ShouldThrowOnNull()
{
Assert.Throws<FormatException>(() => RuleRegexParser.ParseRuleRegex(null));
}
[Fact]
public void RuleRegexParser_ShouldThrowOnEmpty()
{
Assert.Throws<FormatException>(() => RuleRegexParser.ParseRuleRegex(string.Empty));
}
[Fact]
public void RuleRegexParser_RegularRegexExpression()
{
var results = RuleRegexParser.ParseRuleRegex("(.*)");
Assert.False(results.Invert);
Assert.Equal(results.Operand, "(.*)");
}
}
}

View File

@ -0,0 +1,25 @@
{
"version": "1.1.0-*",
"buildOptions": {
"warningsAsErrors": true
},
"dependencies": {
"dotnet-test-xunit": "2.2.0-*",
"Microsoft.AspNetCore.Rewrite": "1.1.0-*",
"Microsoft.AspNetCore.TestHost": "1.1.0-*",
"Microsoft.Extensions.Logging.Testing": "1.1.0-*",
"xunit": "2.2.0-*"
},
"frameworks": {
"netcoreapp1.0": {
"dependencies": {
"Microsoft.NETCore.App": {
"version": "1.0.0-*",
"type": "platform"
}
}
},
"net451": {}
},
"testRunner": "xunit"
}