#104 Improved encoding in PathString.ToUriComponent
This commit is contained in:
Родитель
ed78150d95
Коммит
7db01eb2db
|
@ -0,0 +1,47 @@
|
|||
// 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.Owin.Infrastructure
|
||||
{
|
||||
internal static class PathStringHelper
|
||||
{
|
||||
private static bool[] ValidPathChars = {
|
||||
false, false, false, false, false, false, false, false, // 0x00 - 0x07
|
||||
false, false, false, false, false, false, false, false, // 0x08 - 0x0F
|
||||
false, false, false, false, false, false, false, false, // 0x10 - 0x17
|
||||
false, false, false, false, false, false, false, false, // 0x18 - 0x1F
|
||||
false, true, false, false, true, false, true, true, // 0x20 - 0x27
|
||||
true, true, true, true, true, true, true, true, // 0x28 - 0x2F
|
||||
true, true, true, true, true, true, true, true, // 0x30 - 0x37
|
||||
true, true, true, true, false, true, false, false, // 0x38 - 0x3F
|
||||
true, true, true, true, true, true, true, true, // 0x40 - 0x47
|
||||
true, true, true, true, true, true, true, true, // 0x48 - 0x4F
|
||||
true, true, true, true, true, true, true, true, // 0x50 - 0x57
|
||||
true, true, true, false, false, false, false, true, // 0x58 - 0x5F
|
||||
false, true, true, true, true, true, true, true, // 0x60 - 0x67
|
||||
true, true, true, true, true, true, true, true, // 0x68 - 0x6F
|
||||
true, true, true, true, true, true, true, true, // 0x70 - 0x77
|
||||
true, true, true, false, false, false, true, false, // 0x78 - 0x7F
|
||||
};
|
||||
|
||||
public static bool IsValidPathChar(char c)
|
||||
{
|
||||
return c < ValidPathChars.Length && ValidPathChars[c];
|
||||
}
|
||||
|
||||
public static bool IsPercentEncodedChar(string str, int index)
|
||||
{
|
||||
return index < str.Length - 2
|
||||
&& str[index] == '%'
|
||||
&& IsHexadecimalChar(str[index + 1])
|
||||
&& IsHexadecimalChar(str[index + 2]);
|
||||
}
|
||||
|
||||
public static bool IsHexadecimalChar(char c)
|
||||
{
|
||||
return ('0' <= c && c <= '9')
|
||||
|| ('A' <= c && c <= 'F')
|
||||
|| ('a' <= c && c <= 'f');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -61,6 +61,7 @@
|
|||
<Compile Include="Infrastructure\CookieManager.cs" />
|
||||
<Compile Include="Infrastructure\ChunkingCookieManager.cs" />
|
||||
<Compile Include="Infrastructure\ICookieManager.cs" />
|
||||
<Compile Include="Infrastructure\PathStringHelper.cs" />
|
||||
<Compile Include="IOwinContext.cs" />
|
||||
<Compile Include="IOwinRequest.cs" />
|
||||
<Compile Include="IOwinResponse.cs" />
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.Owin.Infrastructure;
|
||||
|
||||
namespace Microsoft.Owin
|
||||
{
|
||||
|
@ -12,8 +14,6 @@ namespace Microsoft.Owin
|
|||
/// </summary>
|
||||
public struct PathString : IEquatable<PathString>
|
||||
{
|
||||
private static Func<string, string> EscapeDataString = Uri.EscapeDataString;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the empty path. This field is read-only.
|
||||
/// </summary>
|
||||
|
@ -67,36 +67,96 @@ namespace Microsoft.Owin
|
|||
[SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings", Justification = "Purpose of the method is to return a string")]
|
||||
public string ToUriComponent()
|
||||
{
|
||||
if (HasValue)
|
||||
if (!HasValue)
|
||||
{
|
||||
if (RequiresEscaping(_value))
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
StringBuilder buffer = null;
|
||||
|
||||
var start = 0;
|
||||
var count = 0;
|
||||
var requiresEscaping = false;
|
||||
var i = 0;
|
||||
|
||||
while (i < _value.Length)
|
||||
{
|
||||
var isPercentEncodedChar = PathStringHelper.IsPercentEncodedChar(_value, i);
|
||||
if (PathStringHelper.IsValidPathChar(_value[i]) || isPercentEncodedChar)
|
||||
{
|
||||
// TODO: Measure the cost of this escaping and consider optimizing.
|
||||
return String.Join("/", _value.Split('/').Select(EscapeDataString));
|
||||
if (requiresEscaping)
|
||||
{
|
||||
// the current segment requires escape
|
||||
if (buffer == null)
|
||||
{
|
||||
buffer = new StringBuilder(_value.Length * 3);
|
||||
}
|
||||
|
||||
buffer.Append(Uri.EscapeDataString(_value.Substring(start, count)));
|
||||
|
||||
requiresEscaping = false;
|
||||
start = i;
|
||||
count = 0;
|
||||
}
|
||||
|
||||
if (isPercentEncodedChar)
|
||||
{
|
||||
count += 3;
|
||||
i += 3;
|
||||
}
|
||||
else
|
||||
{
|
||||
count++;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!requiresEscaping)
|
||||
{
|
||||
// the current segument doesn't require escape
|
||||
if (buffer == null)
|
||||
{
|
||||
buffer = new StringBuilder(_value.Length * 3);
|
||||
}
|
||||
|
||||
buffer.Append(_value, start, count);
|
||||
|
||||
requiresEscaping = true;
|
||||
start = i;
|
||||
count = 0;
|
||||
}
|
||||
|
||||
count++;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (count == _value.Length && !requiresEscaping)
|
||||
{
|
||||
return _value;
|
||||
}
|
||||
return String.Empty;
|
||||
}
|
||||
|
||||
// Very conservative, these characters do not need to be escaped in a path.
|
||||
private static bool RequiresEscaping(string value)
|
||||
{
|
||||
for (int i = 0; i < value.Length; i++)
|
||||
else
|
||||
{
|
||||
char c = value[i];
|
||||
// Check conservatively for safe characters. See http://www.ietf.org/rfc/rfc3986.txt
|
||||
bool safeChar =
|
||||
(('a' <= c && c <= 'z')
|
||||
|| ('A' <= c && c <= 'Z')
|
||||
|| ('0' <= c && c <= '9')
|
||||
|| c == '/' || c == '-' || c == '_');
|
||||
if (!safeChar)
|
||||
if (count > 0)
|
||||
{
|
||||
return true;
|
||||
if (buffer == null)
|
||||
{
|
||||
buffer = new StringBuilder(_value.Length * 3);
|
||||
}
|
||||
|
||||
if (requiresEscaping)
|
||||
{
|
||||
buffer.Append(Uri.EscapeDataString(_value.Substring(start, count)));
|
||||
}
|
||||
else
|
||||
{
|
||||
buffer.Append(_value, start, count);
|
||||
}
|
||||
}
|
||||
|
||||
return buffer.ToString();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -32,6 +32,8 @@ namespace Microsoft.Owin.Tests
|
|||
[InlineData("/path/two", "/path/two")]
|
||||
[InlineData("/path?two", "/path%3Ftwo")]
|
||||
[InlineData("/path#two", "/path%23two")]
|
||||
// pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
|
||||
[InlineData("/abcd1234%-._~!$&'()*+,;=:@?#[]", "/abcd1234%25-._~!$&'()*+,;=:@%3F%23%5B%5D")]
|
||||
public void ToUriComponentWillEscapeAsAppropriate(string value, string uriComponent)
|
||||
{
|
||||
var path = new PathString(value);
|
||||
|
@ -168,11 +170,11 @@ namespace Microsoft.Owin.Tests
|
|||
singleEscapedPath.Value.ShouldBe("/one%2Ftwo");
|
||||
|
||||
var doubleEscapedString = singleEscapedPath.ToUriComponent();
|
||||
doubleEscapedString.ShouldBe("/one%252Ftwo");
|
||||
doubleEscapedString.ShouldBe("/one%2Ftwo");
|
||||
|
||||
var recreatedPath = PathString.FromUriComponent(doubleEscapedString);
|
||||
recreatedPath.Value.ShouldBe("/one%2Ftwo");
|
||||
recreatedPath.ToUriComponent().ShouldBe("/one%252Ftwo");
|
||||
recreatedPath.Value.ShouldBe("/one/two");
|
||||
recreatedPath.ToUriComponent().ShouldBe("/one/two");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
|
@ -20,10 +20,10 @@ namespace Microsoft.Owin.Tests
|
|||
[InlineData("", "/", "a#b", "http://host:1/?a%23b")]
|
||||
// System.Uri would trim trailing spaces, escape them if you want them.
|
||||
[InlineData("", "/ ", " ", "http://host:1/%20")]
|
||||
[InlineData("/a%.+#?", "/z", "a#b", "http://host:1/a%25.%2B%23%3F/z?a%23b")]
|
||||
[InlineData("/a%.+#?", "/z", "a#b", "http://host:1/a%25.+%23%3F/z?a%23b")]
|
||||
// Note: Http.Sys will not accept any characters in the path that it cannot un-escape,
|
||||
// so this double escaping is not a problem in production.
|
||||
[InlineData("", "/%20", "%20", "http://host:1/%2520?%20")]
|
||||
[InlineData("", "/%20", "%20", "http://host:1/%20?%20")]
|
||||
public void UriReconstruction(string pathBase, string path, string query, string expected)
|
||||
{
|
||||
IOwinRequest request = CreateRequest(pathBase, path, query);
|
||||
|
|
Загрузка…
Ссылка в новой задаче