Don't decode forward slashes in request path (#146).
This commit is contained in:
parent
905b5bcfc2
commit
d9e06f8e6e
|
|
@ -1,3 +1,3 @@
|
||||||
{
|
{
|
||||||
"projects": ["src"]
|
"projects": ["src", "test"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -109,21 +109,21 @@ namespace Microsoft.Net.Http.Server
|
||||||
}
|
}
|
||||||
|
|
||||||
UrlPrefix prefix = httpContext.Server.UrlPrefixes.GetPrefix((int)_contextId);
|
UrlPrefix prefix = httpContext.Server.UrlPrefixes.GetPrefix((int)_contextId);
|
||||||
string orriginalPath = RequestPath;
|
string originalPath = RequestPath;
|
||||||
|
|
||||||
// These paths are both unescaped already.
|
// These paths are both unescaped already.
|
||||||
if (orriginalPath.Length == prefix.Path.Length - 1)
|
if (originalPath.Length == prefix.Path.Length - 1)
|
||||||
{
|
{
|
||||||
// They matched exactly except for the trailing slash.
|
// They matched exactly except for the trailing slash.
|
||||||
_pathBase = orriginalPath;
|
_pathBase = originalPath;
|
||||||
_path = string.Empty;
|
_path = string.Empty;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// url: /base/path, prefix: /base/, base: /base, path: /path
|
// url: /base/path, prefix: /base/, base: /base, path: /path
|
||||||
// url: /, prefix: /, base: , path: /
|
// url: /, prefix: /, base: , path: /
|
||||||
_pathBase = orriginalPath.Substring(0, prefix.Path.Length - 1);
|
_pathBase = originalPath.Substring(0, prefix.Path.Length - 1);
|
||||||
_path = orriginalPath.Substring(prefix.Path.Length - 1);
|
_path = originalPath.Substring(prefix.Path.Length - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
int major = memoryBlob.RequestBlob->Version.MajorVersion;
|
int major = memoryBlob.RequestBlob->Version.MajorVersion;
|
||||||
|
|
@ -386,21 +386,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
{
|
{
|
||||||
get { return IsSecureConnection ? Constants.HttpsScheme : Constants.HttpScheme; }
|
get { return IsSecureConnection ? Constants.HttpsScheme : Constants.HttpScheme; }
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
internal Uri RequestUri
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (_requestUri == null)
|
|
||||||
{
|
|
||||||
_requestUri = RequestUriBuilder.GetRequestUri(
|
|
||||||
_rawUrl, RequestScheme, _cookedUrlHost, _cookedUrlPath, _cookedUrlQuery);
|
|
||||||
}
|
|
||||||
|
|
||||||
return _requestUri;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
internal string RequestPath
|
internal string RequestPath
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|
|
||||||
|
|
@ -32,64 +32,29 @@ namespace Microsoft.Net.Http.Server
|
||||||
// We don't use the cooked URL because http.sys unescapes all percent-encoded values. However,
|
// We don't use the cooked URL because http.sys unescapes all percent-encoded values. However,
|
||||||
// we also can't just use the raw Uri, since http.sys supports not only Utf-8, but also ANSI/DBCS and
|
// we also can't just use the raw Uri, since http.sys supports not only Utf-8, but also ANSI/DBCS and
|
||||||
// Unicode code points. System.Uri only supports Utf-8.
|
// Unicode code points. System.Uri only supports Utf-8.
|
||||||
// The purpose of this class is to convert all ANSI, DBCS, and Unicode code points into percent encoded
|
// The purpose of this class is to decode all UTF-8 percent encoded characters, with the
|
||||||
// Utf-8 characters.
|
// exception of %2F ('/'), which is left encoded.
|
||||||
internal sealed class RequestUriBuilder
|
internal sealed class RequestUriBuilder
|
||||||
{
|
{
|
||||||
private static readonly bool UseCookedRequestUrl;
|
|
||||||
private static readonly Encoding Utf8Encoding;
|
private static readonly Encoding Utf8Encoding;
|
||||||
private static readonly Encoding AnsiEncoding;
|
|
||||||
|
|
||||||
private readonly string _rawUri;
|
private readonly string _rawUri;
|
||||||
private readonly string _cookedUriScheme;
|
|
||||||
private readonly string _cookedUriHost;
|
|
||||||
private readonly string _cookedUriPath;
|
private readonly string _cookedUriPath;
|
||||||
private readonly string _cookedUriQuery;
|
|
||||||
|
|
||||||
// This field is used to build the final request Uri string from the Uri parts passed to the ctor.
|
// This field is used to build the final request Uri string from the Uri parts passed to the ctor.
|
||||||
private StringBuilder _requestUriString;
|
private StringBuilder _requestUriString;
|
||||||
|
|
||||||
// The raw path is parsed by looping through all characters from left to right. 'rawOctets'
|
// The raw path is parsed by looping through all characters from left to right. 'rawOctets'
|
||||||
// is used to store consecutive percent encoded octets as actual byte values: e.g. for path /pa%C3%84th%2F/
|
// is used to store consecutive percent encoded octets as actual byte values: e.g. for path /pa%C3%84th%20/
|
||||||
// rawOctets will be set to { 0xC3, 0x84 } when we reach character 't' and it will be { 0x2F } when
|
// rawOctets will be set to { 0xC3, 0x84 } when we reach character 't' and it will be { 0x20 } when
|
||||||
// we reach the final '/'. I.e. after a sequence of percent encoded octets ends, we use rawOctets as
|
// we reach the final '/'. I.e. after a sequence of percent encoded octets ends, we use rawOctets as
|
||||||
// input to the encoding and percent encode the resulting string into UTF-8 octets.
|
// input to the encoding and decode them into a string.
|
||||||
//
|
|
||||||
// When parsing ANSI (Latin 1) encoded path '/pa%C4th/', %C4 will be added to rawOctets and when
|
|
||||||
// we reach 't', the content of rawOctets { 0xC4 } will be fed into the ANSI encoding. The resulting
|
|
||||||
// string '�' will be percent encoded into UTF-8 octets and appended to requestUriString. The final
|
|
||||||
// path will be '/pa%C3%84th/', where '%C3%84' is the UTF-8 percent encoded character '�'.
|
|
||||||
private List<byte> _rawOctets;
|
private List<byte> _rawOctets;
|
||||||
private string _rawPath;
|
private string _rawPath;
|
||||||
|
|
||||||
// Holds the final request Uri.
|
|
||||||
private Uri _requestUri;
|
|
||||||
|
|
||||||
static RequestUriBuilder()
|
static RequestUriBuilder()
|
||||||
{
|
{
|
||||||
// TODO: False triggers more detailed/correct parsing, but it's rather slow.
|
|
||||||
UseCookedRequestUrl = true; // SettingsSectionInternal.Section.HttpListenerUnescapeRequestUrl;
|
|
||||||
Utf8Encoding = new UTF8Encoding(false, true);
|
Utf8Encoding = new UTF8Encoding(false, true);
|
||||||
#if DOTNET5_4
|
|
||||||
AnsiEncoding = Utf8Encoding;
|
|
||||||
#else
|
|
||||||
AnsiEncoding = Encoding.GetEncoding(0, new EncoderExceptionFallback(), new DecoderExceptionFallback());
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
private RequestUriBuilder(string rawUri, string cookedUriScheme, string cookedUriHost,
|
|
||||||
string cookedUriPath, string cookedUriQuery)
|
|
||||||
{
|
|
||||||
Debug.Assert(!string.IsNullOrEmpty(rawUri), "Empty raw URL.");
|
|
||||||
Debug.Assert(!string.IsNullOrEmpty(cookedUriScheme), "Empty cooked URL scheme.");
|
|
||||||
Debug.Assert(!string.IsNullOrEmpty(cookedUriHost), "Empty cooked URL host.");
|
|
||||||
Debug.Assert(!string.IsNullOrEmpty(cookedUriPath), "Empty cooked URL path.");
|
|
||||||
|
|
||||||
this._rawUri = rawUri;
|
|
||||||
this._cookedUriScheme = cookedUriScheme;
|
|
||||||
this._cookedUriHost = cookedUriHost;
|
|
||||||
this._cookedUriPath = AddSlashToAsteriskOnlyPath(cookedUriPath);
|
|
||||||
this._cookedUriQuery = cookedUriQuery ?? string.Empty;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private RequestUriBuilder(string rawUri, string cookedUriPath)
|
private RequestUriBuilder(string rawUri, string cookedUriPath)
|
||||||
|
|
@ -98,10 +63,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
Debug.Assert(!string.IsNullOrEmpty(cookedUriPath), "Empty cooked URL path.");
|
Debug.Assert(!string.IsNullOrEmpty(cookedUriPath), "Empty cooked URL path.");
|
||||||
|
|
||||||
this._rawUri = rawUri;
|
this._rawUri = rawUri;
|
||||||
this._cookedUriScheme = string.Empty;
|
|
||||||
this._cookedUriHost = string.Empty;
|
|
||||||
this._cookedUriPath = AddSlashToAsteriskOnlyPath(cookedUriPath);
|
this._cookedUriPath = AddSlashToAsteriskOnlyPath(cookedUriPath);
|
||||||
this._cookedUriQuery = string.Empty;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum ParsingResult
|
private enum ParsingResult
|
||||||
|
|
@ -111,48 +73,6 @@ namespace Microsoft.Net.Http.Server
|
||||||
EncodingError
|
EncodingError
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum EncodingType
|
|
||||||
{
|
|
||||||
Primary,
|
|
||||||
Secondary
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Uri GetRequestUri(string rawUri, string cookedUriScheme, string cookedUriHost,
|
|
||||||
string cookedUriPath, string cookedUriQuery)
|
|
||||||
{
|
|
||||||
RequestUriBuilder builder = new RequestUriBuilder(rawUri,
|
|
||||||
cookedUriScheme, cookedUriHost, cookedUriPath, cookedUriQuery);
|
|
||||||
|
|
||||||
return builder.Build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Uri Build()
|
|
||||||
{
|
|
||||||
// if the user enabled the "use raw Uri" setting in <httpListener> section, we'll use the raw
|
|
||||||
// path rather than the cooked path.
|
|
||||||
if (UseCookedRequestUrl)
|
|
||||||
{
|
|
||||||
// corresponds to pre-4.0 behavior: use the cooked URI.
|
|
||||||
BuildRequestUriUsingCookedPath();
|
|
||||||
|
|
||||||
if (_requestUri == null)
|
|
||||||
{
|
|
||||||
BuildRequestUriUsingRawPath();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
BuildRequestUriUsingRawPath();
|
|
||||||
|
|
||||||
if (_requestUri == null)
|
|
||||||
{
|
|
||||||
BuildRequestUriUsingCookedPath();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return _requestUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process only the path.
|
// Process only the path.
|
||||||
internal static string GetRequestPath(string rawUri, string cookedUriPath)
|
internal static string GetRequestPath(string rawUri, string cookedUriPath)
|
||||||
{
|
{
|
||||||
|
|
@ -163,11 +83,6 @@ namespace Microsoft.Net.Http.Server
|
||||||
|
|
||||||
private string GetPath()
|
private string GetPath()
|
||||||
{
|
{
|
||||||
if (UseCookedRequestUrl)
|
|
||||||
{
|
|
||||||
return _cookedUriPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize 'rawPath' only if really needed; i.e. if we build the request Uri from the raw Uri.
|
// Initialize 'rawPath' only if really needed; i.e. if we build the request Uri from the raw Uri.
|
||||||
_rawPath = GetPath(_rawUri);
|
_rawPath = GetPath(_rawUri);
|
||||||
|
|
||||||
|
|
@ -181,18 +96,10 @@ namespace Microsoft.Net.Http.Server
|
||||||
return _rawPath;
|
return _rawPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to check the raw path using first the primary encoding (according to http.sys settings);
|
|
||||||
// if it fails try the secondary encoding.
|
|
||||||
_rawOctets = new List<byte>();
|
_rawOctets = new List<byte>();
|
||||||
_requestUriString = new StringBuilder();
|
_requestUriString = new StringBuilder();
|
||||||
ParsingResult result = ParseRawPath(GetEncoding(EncodingType.Primary));
|
ParsingResult result = ParseRawPath(Utf8Encoding);
|
||||||
if (result == ParsingResult.EncodingError)
|
|
||||||
{
|
|
||||||
_rawOctets = new List<byte>();
|
|
||||||
_requestUriString = new StringBuilder();
|
|
||||||
result = ParseRawPath(GetEncoding(EncodingType.Secondary));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result == ParsingResult.Success)
|
if (result == ParsingResult.Success)
|
||||||
{
|
{
|
||||||
return _requestUriString.ToString();
|
return _requestUriString.ToString();
|
||||||
|
|
@ -202,115 +109,6 @@ namespace Microsoft.Net.Http.Server
|
||||||
return _cookedUriPath;
|
return _cookedUriPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BuildRequestUriUsingCookedPath()
|
|
||||||
{
|
|
||||||
bool isValid = Uri.TryCreate(_cookedUriScheme + Constants.SchemeDelimiter + _cookedUriHost + _cookedUriPath +
|
|
||||||
_cookedUriQuery, UriKind.Absolute, out _requestUri);
|
|
||||||
|
|
||||||
// Creating a Uri from the cooked Uri should really always work: If not, we log at least.
|
|
||||||
if (!isValid)
|
|
||||||
{
|
|
||||||
LogWarning("BuildRequestUriUsingCookedPath", "Unable to create URI: " + _cookedUriScheme + Constants.SchemeDelimiter +
|
|
||||||
_cookedUriHost + _cookedUriPath + _cookedUriQuery);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void BuildRequestUriUsingRawPath()
|
|
||||||
{
|
|
||||||
bool isValid = false;
|
|
||||||
|
|
||||||
// Initialize 'rawPath' only if really needed; i.e. if we build the request Uri from the raw Uri.
|
|
||||||
_rawPath = GetPath(_rawUri);
|
|
||||||
|
|
||||||
// If HTTP.sys only parses Utf-8, we can safely use the raw path: it must be a valid Utf-8 string.
|
|
||||||
if (!HttpSysSettings.EnableNonUtf8 || string.IsNullOrEmpty(_rawPath))
|
|
||||||
{
|
|
||||||
string path = _rawPath;
|
|
||||||
if (string.IsNullOrEmpty(path))
|
|
||||||
{
|
|
||||||
path = "/";
|
|
||||||
Debug.Assert(string.IsNullOrEmpty(_cookedUriQuery),
|
|
||||||
"Query is only allowed if there is a non-empty path. At least '/' path required.");
|
|
||||||
}
|
|
||||||
|
|
||||||
isValid = Uri.TryCreate(_cookedUriScheme + Constants.SchemeDelimiter + _cookedUriHost + path + _cookedUriQuery,
|
|
||||||
UriKind.Absolute, out _requestUri);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Try to check the raw path using first the primary encoding (according to http.sys settings);
|
|
||||||
// if it fails try the secondary encoding.
|
|
||||||
ParsingResult result = BuildRequestUriUsingRawPath(GetEncoding(EncodingType.Primary));
|
|
||||||
if (result == ParsingResult.EncodingError)
|
|
||||||
{
|
|
||||||
Encoding secondaryEncoding = GetEncoding(EncodingType.Secondary);
|
|
||||||
result = BuildRequestUriUsingRawPath(secondaryEncoding);
|
|
||||||
}
|
|
||||||
isValid = (result == ParsingResult.Success) ? true : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log that we weren't able to create a Uri from the raw string.
|
|
||||||
if (!isValid)
|
|
||||||
{
|
|
||||||
LogWarning("BuildRequestUriUsingRawPath", "Unable to create Uri: " + _cookedUriScheme + Constants.SchemeDelimiter
|
|
||||||
+ _cookedUriHost + _rawPath + _cookedUriQuery);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Encoding GetEncoding(EncodingType type)
|
|
||||||
{
|
|
||||||
Debug.Assert(HttpSysSettings.EnableNonUtf8,
|
|
||||||
"If 'EnableNonUtf8' is false we shouldn't require an encoding. It's always Utf-8.");
|
|
||||||
/* This is mucking up the profiler for some reason.
|
|
||||||
Debug.Assert((type == EncodingType.Primary) || (type == EncodingType.Secondary),
|
|
||||||
"Unknown 'EncodingType' value: " + type.ToString());
|
|
||||||
*/
|
|
||||||
if (((type == EncodingType.Primary) && (!HttpSysSettings.FavorUtf8)) ||
|
|
||||||
((type == EncodingType.Secondary) && (HttpSysSettings.FavorUtf8)))
|
|
||||||
{
|
|
||||||
return AnsiEncoding;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return Utf8Encoding;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ParsingResult BuildRequestUriUsingRawPath(Encoding encoding)
|
|
||||||
{
|
|
||||||
Debug.Assert(encoding != null, "'encoding' must be assigned.");
|
|
||||||
Debug.Assert(!string.IsNullOrEmpty(_rawPath), "'rawPath' must have at least one character.");
|
|
||||||
|
|
||||||
_rawOctets = new List<byte>();
|
|
||||||
_requestUriString = new StringBuilder();
|
|
||||||
_requestUriString.Append(_cookedUriScheme);
|
|
||||||
_requestUriString.Append(Constants.SchemeDelimiter);
|
|
||||||
_requestUriString.Append(_cookedUriHost);
|
|
||||||
|
|
||||||
ParsingResult result = ParseRawPath(encoding);
|
|
||||||
if (result == ParsingResult.Success)
|
|
||||||
{
|
|
||||||
_requestUriString.Append(_cookedUriQuery);
|
|
||||||
|
|
||||||
Debug.Assert(_rawOctets.Count == 0,
|
|
||||||
"Still raw octets left. They must be added to the result path.");
|
|
||||||
|
|
||||||
if (!Uri.TryCreate(_requestUriString.ToString(), UriKind.Absolute, out _requestUri))
|
|
||||||
{
|
|
||||||
// If we can't create a Uri from the string, this is an invalid string and it doesn't make
|
|
||||||
// sense to try another encoding.
|
|
||||||
result = ParsingResult.InvalidString;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result != ParsingResult.Success)
|
|
||||||
{
|
|
||||||
LogWarning("BuildRequestUriUsingRawPath", "Can't convert the raw path: " + _rawPath + " Encoding: " + encoding.WebName);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ParsingResult ParseRawPath(Encoding encoding)
|
private ParsingResult ParseRawPath(Encoding encoding)
|
||||||
{
|
{
|
||||||
Debug.Assert(encoding != null, "'encoding' must be assigned.");
|
Debug.Assert(encoding != null, "'encoding' must be assigned.");
|
||||||
|
|
@ -323,45 +121,31 @@ namespace Microsoft.Net.Http.Server
|
||||||
if (current == '%')
|
if (current == '%')
|
||||||
{
|
{
|
||||||
// Assert is enough, since http.sys accepted the request string already. This should never happen.
|
// Assert is enough, since http.sys accepted the request string already. This should never happen.
|
||||||
Debug.Assert(index + 2 < _rawPath.Length, "Expected >=2 characters after '%' (e.g. %2F)");
|
Debug.Assert(index + 2 < _rawPath.Length, "Expected at least 2 characters after '%' (e.g. %20)");
|
||||||
|
|
||||||
index++;
|
// We have a percent encoded octet: %XX
|
||||||
current = _rawPath[index];
|
var octetString = _rawPath.Substring(index + 1, 2);
|
||||||
if (current == 'u' || current == 'U')
|
|
||||||
{
|
|
||||||
// We found "%u" which means, we have a Unicode code point of the form "%uXXXX".
|
|
||||||
Debug.Assert(index + 4 < _rawPath.Length, "Expected >=4 characters after '%u' (e.g. %u0062)");
|
|
||||||
|
|
||||||
// Decode the content of rawOctets into percent encoded UTF-8 characters and append them
|
// Leave %2F as is, otherwise add to raw octets list for unescaping
|
||||||
// to requestUriString.
|
if (octetString == "2F" || octetString == "2f")
|
||||||
if (!EmptyDecodeAndAppendRawOctetsList(encoding))
|
|
||||||
{
|
|
||||||
return ParsingResult.EncodingError;
|
|
||||||
}
|
|
||||||
if (!AppendUnicodeCodePointValuePercentEncoded(_rawPath.Substring(index + 1, 4)))
|
|
||||||
{
|
|
||||||
return ParsingResult.InvalidString;
|
|
||||||
}
|
|
||||||
index += 5;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
// We found '%', but not followed by 'u', i.e. we have a percent encoded octed: %XX
|
_requestUriString.Append('%');
|
||||||
if (!AddPercentEncodedOctetToRawOctetsList(encoding, _rawPath.Substring(index, 2)))
|
_requestUriString.Append(octetString);
|
||||||
{
|
|
||||||
return ParsingResult.InvalidString;
|
|
||||||
}
|
|
||||||
index += 2;
|
|
||||||
}
|
}
|
||||||
|
else if (!AddPercentEncodedOctetToRawOctetsList(encoding, octetString))
|
||||||
|
{
|
||||||
|
return ParsingResult.InvalidString;
|
||||||
|
}
|
||||||
|
|
||||||
|
index += 3;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// We found a non-'%' character: decode the content of rawOctets into percent encoded
|
if (!EmptyDecodeAndAppendDecodedOctetsList(encoding))
|
||||||
// UTF-8 characters and append it to the result.
|
|
||||||
if (!EmptyDecodeAndAppendRawOctetsList(encoding))
|
|
||||||
{
|
{
|
||||||
return ParsingResult.EncodingError;
|
return ParsingResult.EncodingError;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append the current character to the result.
|
// Append the current character to the result.
|
||||||
_requestUriString.Append(current);
|
_requestUriString.Append(current);
|
||||||
index++;
|
index++;
|
||||||
|
|
@ -370,7 +154,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
|
|
||||||
// if the raw path ends with a sequence of percent encoded octets, make sure those get added to the
|
// if the raw path ends with a sequence of percent encoded octets, make sure those get added to the
|
||||||
// result (requestUriString).
|
// result (requestUriString).
|
||||||
if (!EmptyDecodeAndAppendRawOctetsList(encoding))
|
if (!EmptyDecodeAndAppendDecodedOctetsList(encoding))
|
||||||
{
|
{
|
||||||
return ParsingResult.EncodingError;
|
return ParsingResult.EncodingError;
|
||||||
}
|
}
|
||||||
|
|
@ -378,44 +162,12 @@ namespace Microsoft.Net.Http.Server
|
||||||
return ParsingResult.Success;
|
return ParsingResult.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool AppendUnicodeCodePointValuePercentEncoded(string codePoint)
|
|
||||||
{
|
|
||||||
// http.sys only supports %uXXXX (4 hex-digits), even though unicode code points could have up to
|
|
||||||
// 6 hex digits. Therefore we parse always 4 characters after %u and convert them to an int.
|
|
||||||
int codePointValue;
|
|
||||||
if (!int.TryParse(codePoint, NumberStyles.HexNumber, null, out codePointValue))
|
|
||||||
{
|
|
||||||
LogWarning("AppendUnicodeCodePointValuePercentEncoded", "Can't convert code point: " + codePoint);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
string unicodeString = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
unicodeString = char.ConvertFromUtf32(codePointValue);
|
|
||||||
AppendOctetsPercentEncoded(_requestUriString, Utf8Encoding.GetBytes(unicodeString));
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (ArgumentOutOfRangeException)
|
|
||||||
{
|
|
||||||
LogWarning("AppendUnicodeCodePointValuePercentEncoded", "Can't convert code point: " + codePoint);
|
|
||||||
}
|
|
||||||
catch (EncoderFallbackException e)
|
|
||||||
{
|
|
||||||
// If utf8Encoding.GetBytes() fails
|
|
||||||
LogWarning("AppendUnicodeCodePointValuePercentEncoded", "Can't convert code point: " + unicodeString, e.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool AddPercentEncodedOctetToRawOctetsList(Encoding encoding, string escapedCharacter)
|
private bool AddPercentEncodedOctetToRawOctetsList(Encoding encoding, string escapedCharacter)
|
||||||
{
|
{
|
||||||
byte encodedValue;
|
byte encodedValue;
|
||||||
if (!byte.TryParse(escapedCharacter, NumberStyles.HexNumber, null, out encodedValue))
|
if (!byte.TryParse(escapedCharacter, NumberStyles.HexNumber, null, out encodedValue))
|
||||||
{
|
{
|
||||||
LogWarning("AddPercentEncodedOctetToRawOctetsList", "Can't convert code point: " + escapedCharacter);
|
LogWarning(nameof(AddPercentEncodedOctetToRawOctetsList), "Can't convert code point: " + escapedCharacter);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -424,7 +176,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool EmptyDecodeAndAppendRawOctetsList(Encoding encoding)
|
private bool EmptyDecodeAndAppendDecodedOctetsList(Encoding encoding)
|
||||||
{
|
{
|
||||||
if (_rawOctets.Count == 0)
|
if (_rawOctets.Count == 0)
|
||||||
{
|
{
|
||||||
|
|
@ -436,44 +188,22 @@ namespace Microsoft.Net.Http.Server
|
||||||
{
|
{
|
||||||
// If the encoding can get a string out of the byte array, this is a valid string in the
|
// If the encoding can get a string out of the byte array, this is a valid string in the
|
||||||
// 'encoding' encoding.
|
// 'encoding' encoding.
|
||||||
byte[] bytes = _rawOctets.ToArray();
|
var bytes = _rawOctets.ToArray();
|
||||||
decodedString = encoding.GetString(bytes, 0, bytes.Length);
|
decodedString = encoding.GetString(bytes, 0, bytes.Length);
|
||||||
|
|
||||||
if (encoding == Utf8Encoding)
|
_requestUriString.Append(decodedString);
|
||||||
{
|
|
||||||
AppendOctetsPercentEncoded(_requestUriString, bytes);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
AppendOctetsPercentEncoded(_requestUriString, Utf8Encoding.GetBytes(decodedString));
|
|
||||||
}
|
|
||||||
|
|
||||||
_rawOctets.Clear();
|
_rawOctets.Clear();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (DecoderFallbackException e)
|
catch (DecoderFallbackException e)
|
||||||
{
|
{
|
||||||
LogWarning("EmptyDecodeAndAppendRawOctetsList", "Can't convert bytes: " + GetOctetsAsString(_rawOctets), e.Message);
|
LogWarning(nameof(EmptyDecodeAndAppendDecodedOctetsList), "Can't convert bytes: " + GetOctetsAsString(_rawOctets), e.Message);
|
||||||
}
|
|
||||||
catch (EncoderFallbackException e)
|
|
||||||
{
|
|
||||||
// If utf8Encoding.GetBytes() fails
|
|
||||||
LogWarning("EmptyDecodeAndAppendRawOctetsList", "Can't convert bytes: " + decodedString, e.Message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AppendOctetsPercentEncoded(StringBuilder target, IEnumerable<byte> octets)
|
|
||||||
{
|
|
||||||
foreach (byte octet in octets)
|
|
||||||
{
|
|
||||||
target.Append('%');
|
|
||||||
target.Append(octet.ToString("X2", CultureInfo.InvariantCulture));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetOctetsAsString(IEnumerable<byte> octets)
|
private static string GetOctetsAsString(IEnumerable<byte> octets)
|
||||||
{
|
{
|
||||||
StringBuilder octetString = new StringBuilder();
|
StringBuilder octetString = new StringBuilder();
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Net.Sockets;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNet.Hosting.Server;
|
using Microsoft.AspNet.Hosting.Server;
|
||||||
|
|
@ -87,6 +88,7 @@ namespace Microsoft.AspNet.Server.WebListener
|
||||||
[InlineData("/basepath/", "/basepath/subpath", "/basepath", "/subpath")]
|
[InlineData("/basepath/", "/basepath/subpath", "/basepath", "/subpath")]
|
||||||
[InlineData("/base path/", "/base%20path/sub path", "/base path", "/sub path")]
|
[InlineData("/base path/", "/base%20path/sub path", "/base path", "/sub path")]
|
||||||
[InlineData("/base葉path/", "/base%E8%91%89path/sub%E8%91%89path", "/base葉path", "/sub葉path")]
|
[InlineData("/base葉path/", "/base%E8%91%89path/sub%E8%91%89path", "/base葉path", "/sub葉path")]
|
||||||
|
[InlineData("/basepath/", "/basepath/sub%2Fpath", "/basepath", "/sub%2Fpath")]
|
||||||
public async Task Request_PathSplitting(string pathBase, string requestPath, string expectedPathBase, string expectedPath)
|
public async Task Request_PathSplitting(string pathBase, string requestPath, string expectedPathBase, string expectedPath)
|
||||||
{
|
{
|
||||||
string root;
|
string root;
|
||||||
|
|
@ -121,6 +123,23 @@ namespace Microsoft.AspNet.Server.WebListener
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Request_DoubleEscapingAllowed()
|
||||||
|
{
|
||||||
|
string root;
|
||||||
|
using (var server = Utilities.CreateHttpServerReturnRoot("/", out root, httpContext =>
|
||||||
|
{
|
||||||
|
var requestInfo = httpContext.Features.Get<IHttpRequestFeature>();
|
||||||
|
Assert.Equal("/%2F", requestInfo.Path);
|
||||||
|
return Task.FromResult(0);
|
||||||
|
}))
|
||||||
|
{
|
||||||
|
var response = await SendSocketRequestAsync(root, "/%252F");
|
||||||
|
var responseStatusCode = response.Substring(9); // Skip "HTTP/1.1 "
|
||||||
|
Assert.Equal("200", responseStatusCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
// The test server defines these prefixes: "/", "/11", "/2/3", "/2", "/11/2"
|
// The test server defines these prefixes: "/", "/11", "/2/3", "/2", "/11/2"
|
||||||
[InlineData("/", "", "/")]
|
[InlineData("/", "", "/")]
|
||||||
|
|
@ -189,5 +208,27 @@ namespace Microsoft.AspNet.Server.WebListener
|
||||||
return await client.GetStringAsync(uri);
|
return await client.GetStringAsync(uri);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<string> SendSocketRequestAsync(string address, string path)
|
||||||
|
{
|
||||||
|
var uri = new Uri(address);
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
builder.AppendLine("GET " + path + " HTTP/1.1");
|
||||||
|
builder.AppendLine("Connection: close");
|
||||||
|
builder.Append("HOST: ");
|
||||||
|
builder.AppendLine(uri.Authority);
|
||||||
|
builder.AppendLine();
|
||||||
|
|
||||||
|
byte[] request = Encoding.ASCII.GetBytes(builder.ToString());
|
||||||
|
|
||||||
|
using (var socket = new Socket(SocketType.Stream, ProtocolType.Tcp))
|
||||||
|
{
|
||||||
|
socket.Connect(uri.Host, uri.Port);
|
||||||
|
socket.Send(request);
|
||||||
|
var response = new byte[12];
|
||||||
|
await Task.Run(() => socket.Receive(response));
|
||||||
|
return Encoding.ASCII.GetString(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Net.Sockets;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
@ -54,8 +55,9 @@ namespace Microsoft.Net.Http.Server
|
||||||
[InlineData("/basepath/", "/basepath", "/basepath", "")]
|
[InlineData("/basepath/", "/basepath", "/basepath", "")]
|
||||||
[InlineData("/basepath/", "/basepath/", "/basepath", "/")]
|
[InlineData("/basepath/", "/basepath/", "/basepath", "/")]
|
||||||
[InlineData("/basepath/", "/basepath/subpath", "/basepath", "/subpath")]
|
[InlineData("/basepath/", "/basepath/subpath", "/basepath", "/subpath")]
|
||||||
[InlineData("/base path/", "/base%20path/sub path", "/base path", "/sub path")]
|
[InlineData("/base path/", "/base%20path/sub%20path", "/base path", "/sub path")]
|
||||||
[InlineData("/base葉path/", "/base%E8%91%89path/sub%E8%91%89path", "/base葉path", "/sub葉path")]
|
[InlineData("/base葉path/", "/base%E8%91%89path/sub%E8%91%89path", "/base葉path", "/sub葉path")]
|
||||||
|
[InlineData("/basepath/", "/basepath/sub%2Fpath", "/basepath", "/sub%2Fpath")]
|
||||||
public async Task Request_PathSplitting(string pathBase, string requestPath, string expectedPathBase, string expectedPath)
|
public async Task Request_PathSplitting(string pathBase, string requestPath, string expectedPathBase, string expectedPath)
|
||||||
{
|
{
|
||||||
string root;
|
string root;
|
||||||
|
|
@ -80,6 +82,36 @@ namespace Microsoft.Net.Http.Server
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("/path%")]
|
||||||
|
[InlineData("/path%XY")]
|
||||||
|
[InlineData("/path%F")]
|
||||||
|
[InlineData("/path with spaces")]
|
||||||
|
public async Task Request_MalformedPathReturns400StatusCode(string requestPath)
|
||||||
|
{
|
||||||
|
string root;
|
||||||
|
using (var server = Utilities.CreateHttpServerReturnRoot("/", out root))
|
||||||
|
{
|
||||||
|
var responseTask = SendSocketRequestAsync(root, requestPath);
|
||||||
|
var contextTask = server.GetContextAsync();
|
||||||
|
var response = await responseTask;
|
||||||
|
var responseStatusCode = response.Substring(9); // Skip "HTTP/1.1 "
|
||||||
|
Assert.Equal("400", responseStatusCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Request_DoubleEscapingAllowed()
|
||||||
|
{
|
||||||
|
string root;
|
||||||
|
using (var server = Utilities.CreateHttpServerReturnRoot("/", out root))
|
||||||
|
{
|
||||||
|
var responseTask = SendSocketRequestAsync(root, "/%252F");
|
||||||
|
var context = await server.GetContextAsync();
|
||||||
|
Assert.Equal("/%2F", context.Request.Path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
// The test server defines these prefixes: "/", "/11", "/2/3", "/2", "/11/2"
|
// The test server defines these prefixes: "/", "/11", "/2/3", "/2", "/11/2"
|
||||||
[InlineData("/", "", "/")]
|
[InlineData("/", "", "/")]
|
||||||
|
|
@ -131,5 +163,27 @@ namespace Microsoft.Net.Http.Server
|
||||||
return await client.GetStringAsync(uri);
|
return await client.GetStringAsync(uri);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<string> SendSocketRequestAsync(string address, string path)
|
||||||
|
{
|
||||||
|
var uri = new Uri(address);
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
builder.AppendLine("GET " + path + " HTTP/1.1");
|
||||||
|
builder.AppendLine("Connection: close");
|
||||||
|
builder.Append("HOST: ");
|
||||||
|
builder.AppendLine(uri.Authority);
|
||||||
|
builder.AppendLine();
|
||||||
|
|
||||||
|
byte[] request = Encoding.ASCII.GetBytes(builder.ToString());
|
||||||
|
|
||||||
|
using (var socket = new Socket(SocketType.Stream, ProtocolType.Tcp))
|
||||||
|
{
|
||||||
|
socket.Connect(uri.Host, uri.Port);
|
||||||
|
socket.Send(request);
|
||||||
|
var response = new byte[12];
|
||||||
|
await Task.Run(() => socket.Receive(response));
|
||||||
|
return Encoding.ASCII.GetString(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue