e79aa3c0ed
Former-commit-id: a2155e9bd80020e49e72e86c44da02a8ac0e57a4
997 lines
37 KiB
C#
997 lines
37 KiB
C#
//------------------------------------------------------------------------------
|
|
// <copyright file="HttpEncoder.cs" company="Microsoft">
|
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
// </copyright>
|
|
//------------------------------------------------------------------------------
|
|
|
|
/*
|
|
* Base class providing extensibility hooks for custom encoding / decoding
|
|
*
|
|
* Copyright (c) 2009 Microsoft Corporation
|
|
*/
|
|
|
|
namespace System.Web.Util {
|
|
using System;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Net;
|
|
using System.Text;
|
|
using System.Web;
|
|
using System.Web.Configuration;
|
|
|
|
public class HttpEncoder {
|
|
|
|
private static HttpEncoder _customEncoder;
|
|
private readonly bool _isDefaultEncoder;
|
|
|
|
private static readonly Lazy<HttpEncoder> _customEncoderResolver =
|
|
new Lazy<HttpEncoder>(GetCustomEncoderFromConfig);
|
|
|
|
private static readonly HttpEncoder _defaultEncoder = new HttpEncoder();
|
|
|
|
private static readonly string[] _headerEncodingTable = new string[] {
|
|
"%00", "%01", "%02", "%03", "%04", "%05", "%06", "%07",
|
|
"%08", "%09", "%0a", "%0b", "%0c", "%0d", "%0e", "%0f",
|
|
"%10", "%11", "%12", "%13", "%14", "%15", "%16", "%17",
|
|
"%18", "%19", "%1a", "%1b", "%1c", "%1d", "%1e", "%1f"
|
|
};
|
|
|
|
public HttpEncoder() {
|
|
_isDefaultEncoder = (GetType() == typeof(HttpEncoder));
|
|
}
|
|
|
|
public static HttpEncoder Current {
|
|
get {
|
|
// always use the fallback encoder when rendering an error page so that we can at least display *something*
|
|
// to the user before closing the connection
|
|
|
|
HttpContext httpContext = HttpContext.Current;
|
|
if (httpContext != null && httpContext.DisableCustomHttpEncoder) {
|
|
return _defaultEncoder;
|
|
}
|
|
else {
|
|
if (_customEncoder == null) {
|
|
_customEncoder = _customEncoderResolver.Value;
|
|
}
|
|
return _customEncoder;
|
|
}
|
|
}
|
|
set {
|
|
if (value == null) {
|
|
throw new ArgumentNullException("value");
|
|
}
|
|
_customEncoder = value;
|
|
}
|
|
}
|
|
|
|
public static HttpEncoder Default {
|
|
get {
|
|
return _defaultEncoder;
|
|
}
|
|
}
|
|
|
|
internal virtual bool JavaScriptEncodeAmpersand {
|
|
get {
|
|
return !AppSettings.JavaScriptDoNotEncodeAmpersand;
|
|
}
|
|
}
|
|
|
|
private static void AppendCharAsUnicodeJavaScript(StringBuilder builder, char c) {
|
|
builder.Append("\\u");
|
|
builder.Append(((int)c).ToString("x4", CultureInfo.InvariantCulture));
|
|
}
|
|
|
|
private bool CharRequiresJavaScriptEncoding(char c) {
|
|
return c < 0x20 // control chars always have to be encoded
|
|
|| c == '\"' // chars which must be encoded per JSON spec
|
|
|| c == '\\'
|
|
|| c == '\'' // HTML-sensitive chars encoded for safety
|
|
|| c == '<'
|
|
|| c == '>'
|
|
|| (c == '&' && JavaScriptEncodeAmpersand) // Bug Dev11 #133237. Encode '&' to provide additional security for people who incorrectly call the encoding methods (unless turned off by backcompat switch)
|
|
|| c == '\u0085' // newline chars (see Unicode 6.2, Table 5-1 [http://www.unicode.org/versions/Unicode6.2.0/ch05.pdf]) have to be encoded (DevDiv #663531)
|
|
|| c == '\u2028'
|
|
|| c == '\u2029';
|
|
}
|
|
|
|
internal static string CollapsePercentUFromStringInternal(string s, Encoding e) {
|
|
int count = s.Length;
|
|
UrlDecoder helper = new UrlDecoder(count, e);
|
|
|
|
// go thorugh the string's chars collapsing just %uXXXX and
|
|
// appending each char as char
|
|
int loc = s.IndexOf("%u", StringComparison.Ordinal);
|
|
if (loc == -1) {
|
|
return s;
|
|
}
|
|
|
|
for (int pos = 0; pos < count; pos++) {
|
|
char ch = s[pos];
|
|
|
|
if (ch == '%' && pos < count - 5) {
|
|
if (s[pos + 1] == 'u') {
|
|
int h1 = HttpEncoderUtility.HexToInt(s[pos + 2]);
|
|
int h2 = HttpEncoderUtility.HexToInt(s[pos + 3]);
|
|
int h3 = HttpEncoderUtility.HexToInt(s[pos + 4]);
|
|
int h4 = HttpEncoderUtility.HexToInt(s[pos + 5]);
|
|
|
|
if (h1 >= 0 && h2 >= 0 && h3 >= 0 && h4 >= 0) { //valid 4 hex chars
|
|
ch = (char)((h1 << 12) | (h2 << 8) | (h3 << 4) | h4);
|
|
pos += 5;
|
|
|
|
// add as char
|
|
helper.AddChar(ch);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
if ((ch & 0xFF80) == 0)
|
|
helper.AddByte((byte)ch); // 7 bit have to go as bytes because of Unicode
|
|
else
|
|
helper.AddChar(ch);
|
|
}
|
|
return Utf16StringValidator.ValidateString(helper.GetString());
|
|
}
|
|
|
|
private static HttpEncoder GetCustomEncoderFromConfig() {
|
|
// App since this is static per AppDomain
|
|
RuntimeConfig config = RuntimeConfig.GetAppConfig();
|
|
HttpRuntimeSection runtimeSection = config.HttpRuntime;
|
|
string encoderTypeName = runtimeSection.EncoderType;
|
|
|
|
// validate the type
|
|
Type encoderType = ConfigUtil.GetType(encoderTypeName, "encoderType", runtimeSection);
|
|
ConfigUtil.CheckBaseType(typeof(HttpEncoder) /* expectedBaseType */, encoderType, "encoderType", runtimeSection);
|
|
|
|
// instantiate
|
|
HttpEncoder encoder = (HttpEncoder)HttpRuntime.CreatePublicInstance(encoderType);
|
|
return encoder;
|
|
}
|
|
|
|
// Encode the header if it contains a CRLF pair
|
|
// VSWhidbey 257154
|
|
private static string HeaderEncodeInternal(string value) {
|
|
string sanitizedHeader = value;
|
|
if (HeaderValueNeedsEncoding(value)) {
|
|
// DevDiv Bugs 146028
|
|
// Denial Of Service scenarios involving
|
|
// control characters are possible.
|
|
// We are encoding the following characters:
|
|
// - All CTL characters except HT (horizontal tab)
|
|
// - DEL character (\x7f)
|
|
StringBuilder sb = new StringBuilder();
|
|
foreach (char c in value) {
|
|
if (c < 32 && c != 9) {
|
|
sb.Append(_headerEncodingTable[c]);
|
|
}
|
|
else if (c == 127) {
|
|
sb.Append("%7f");
|
|
}
|
|
else {
|
|
sb.Append(c);
|
|
}
|
|
}
|
|
sanitizedHeader = sb.ToString();
|
|
}
|
|
|
|
return sanitizedHeader;
|
|
}
|
|
|
|
[SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters",
|
|
Justification = "Input parameter strings are immutable, so this is an appropriate way to return multiple strings.")]
|
|
protected internal virtual void HeaderNameValueEncode(string headerName, string headerValue, out string encodedHeaderName, out string encodedHeaderValue) {
|
|
encodedHeaderName = (String.IsNullOrEmpty(headerName)) ? headerName : HeaderEncodeInternal(headerName);
|
|
encodedHeaderValue = (String.IsNullOrEmpty(headerValue)) ? headerValue : HeaderEncodeInternal(headerValue);
|
|
}
|
|
|
|
// Returns true if the string contains a control character (other than horizontal tab) or the DEL character.
|
|
private static bool HeaderValueNeedsEncoding(string value) {
|
|
foreach (char c in value) {
|
|
if ((c < 32 && c != 9) || (c == 127)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
internal string HtmlAttributeEncode(string value) {
|
|
if (String.IsNullOrEmpty(value)) {
|
|
return value;
|
|
}
|
|
|
|
if(_isDefaultEncoder) {
|
|
// Don't create string writer if we don't have nothing to encode
|
|
int pos = IndexOfHtmlAttributeEncodingChars(value, 0);
|
|
if (pos == -1) {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
StringWriter writer = new StringWriter(CultureInfo.InvariantCulture);
|
|
HtmlAttributeEncode(value, writer);
|
|
return writer.ToString();
|
|
}
|
|
|
|
protected internal virtual void HtmlAttributeEncode(string value, TextWriter output) {
|
|
if (value == null) {
|
|
return;
|
|
}
|
|
if (output == null) {
|
|
throw new ArgumentNullException("output");
|
|
}
|
|
|
|
// we have a special faster path for HttpWriter
|
|
HttpWriter httpWriter = output as HttpWriter;
|
|
if (httpWriter != null) {
|
|
HtmlAttributeEncodeInternal(value, httpWriter);
|
|
}
|
|
else {
|
|
HtmlAttributeEncodeInternal(value, output);
|
|
}
|
|
}
|
|
|
|
private static void HtmlAttributeEncodeInternal(string value, HttpWriter writer) {
|
|
int pos = IndexOfHtmlAttributeEncodingChars(value, 0);
|
|
if (pos == -1) {
|
|
writer.Write(value);
|
|
return;
|
|
}
|
|
|
|
int cch = value.Length;
|
|
int startPos = 0;
|
|
for (; ; ) {
|
|
if (pos > startPos) {
|
|
writer.WriteString(value, startPos, pos - startPos);
|
|
}
|
|
|
|
char ch = value[pos];
|
|
switch (ch) {
|
|
case '"':
|
|
writer.Write(""");
|
|
break;
|
|
case '\'':
|
|
writer.Write("'");
|
|
break;
|
|
case '&':
|
|
writer.Write("&");
|
|
break;
|
|
case '<':
|
|
// Whidbey 32404: The character '<' is not valid in an XML attribute value.
|
|
// (See the W3C XML rec).
|
|
writer.Write("<");
|
|
break;
|
|
}
|
|
|
|
startPos = pos + 1;
|
|
if (startPos >= cch)
|
|
break;
|
|
|
|
pos = IndexOfHtmlAttributeEncodingChars(value, startPos);
|
|
if (pos == -1) {
|
|
writer.WriteString(value, startPos, cch - startPos);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private unsafe static void HtmlAttributeEncodeInternal(String s, TextWriter output) {
|
|
int index = IndexOfHtmlAttributeEncodingChars(s, 0);
|
|
if (index == -1) {
|
|
output.Write(s);
|
|
}
|
|
else {
|
|
int cch = s.Length - index;
|
|
fixed (char* str = s) {
|
|
char* pch = str;
|
|
while (index-- > 0) {
|
|
output.Write(*pch++);
|
|
}
|
|
|
|
while (cch-- > 0) {
|
|
char ch = *pch++;
|
|
if (ch <= '<') {
|
|
switch (ch) {
|
|
case '<':
|
|
output.Write("<");
|
|
break;
|
|
case '"':
|
|
output.Write(""");
|
|
break;
|
|
case '\'':
|
|
output.Write("'");
|
|
break;
|
|
case '&':
|
|
output.Write("&");
|
|
break;
|
|
default:
|
|
output.Write(ch);
|
|
break;
|
|
}
|
|
}
|
|
else {
|
|
output.Write(ch);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
internal string HtmlDecode(string value) {
|
|
if (String.IsNullOrEmpty(value))
|
|
{
|
|
return value;
|
|
}
|
|
|
|
if(_isDefaultEncoder) {
|
|
return WebUtility.HtmlDecode(value);
|
|
}
|
|
|
|
StringWriter writer = new StringWriter(CultureInfo.InvariantCulture);
|
|
HtmlDecode(value, writer);
|
|
return writer.ToString();
|
|
}
|
|
|
|
protected internal virtual void HtmlDecode(string value, TextWriter output) {
|
|
WebUtility.HtmlDecode(value, output);
|
|
}
|
|
|
|
internal string HtmlEncode(string value) {
|
|
if (String.IsNullOrEmpty(value))
|
|
{
|
|
return value;
|
|
}
|
|
|
|
if(_isDefaultEncoder) {
|
|
return WebUtility.HtmlEncode(value);
|
|
}
|
|
|
|
StringWriter writer = new StringWriter(CultureInfo.InvariantCulture);
|
|
HtmlEncode(value, writer);
|
|
return writer.ToString();
|
|
}
|
|
|
|
protected internal virtual void HtmlEncode(string value, TextWriter output) {
|
|
WebUtility.HtmlEncode(value, output);
|
|
}
|
|
|
|
private static unsafe int IndexOfHtmlAttributeEncodingChars(string s, int startPos) {
|
|
Debug.Assert(0 <= startPos && startPos <= s.Length, "0 <= startPos && startPos <= s.Length");
|
|
int cch = s.Length - startPos;
|
|
fixed (char* str = s) {
|
|
for (char* pch = &str[startPos]; cch > 0; pch++, cch--) {
|
|
char ch = *pch;
|
|
if (ch <= '<') {
|
|
switch (ch) {
|
|
case '<':
|
|
case '"':
|
|
case '\'':
|
|
case '&':
|
|
return s.Length - cch;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
internal static void InitializeOnFirstRequest() {
|
|
// Instantiate the encoder if it hasn't already been created. Note that this isn't storing the returned encoder
|
|
// anywhere; it's just priming the Lazy<T> so that future calls to the Value property getter will return quickly
|
|
// without going back to config.
|
|
|
|
HttpEncoder encoder = _customEncoderResolver.Value;
|
|
}
|
|
|
|
private static bool IsNonAsciiByte(byte b) {
|
|
return (b >= 0x7F || b < 0x20);
|
|
}
|
|
|
|
protected internal virtual string JavaScriptStringEncode(string value) {
|
|
if (String.IsNullOrEmpty(value)) {
|
|
return String.Empty;
|
|
}
|
|
|
|
StringBuilder b = null;
|
|
int startIndex = 0;
|
|
int count = 0;
|
|
for (int i = 0; i < value.Length; i++) {
|
|
char c = value[i];
|
|
|
|
// Append the unhandled characters (that do not require special treament)
|
|
// to the string builder when special characters are detected.
|
|
if (CharRequiresJavaScriptEncoding(c)) {
|
|
if (b == null) {
|
|
b = new StringBuilder(value.Length + 5);
|
|
}
|
|
|
|
if (count > 0) {
|
|
b.Append(value, startIndex, count);
|
|
}
|
|
|
|
startIndex = i + 1;
|
|
count = 0;
|
|
}
|
|
|
|
switch (c) {
|
|
case '\r':
|
|
b.Append("\\r");
|
|
break;
|
|
case '\t':
|
|
b.Append("\\t");
|
|
break;
|
|
case '\"':
|
|
b.Append("\\\"");
|
|
break;
|
|
case '\\':
|
|
b.Append("\\\\");
|
|
break;
|
|
case '\n':
|
|
b.Append("\\n");
|
|
break;
|
|
case '\b':
|
|
b.Append("\\b");
|
|
break;
|
|
case '\f':
|
|
b.Append("\\f");
|
|
break;
|
|
default:
|
|
if (CharRequiresJavaScriptEncoding(c)) {
|
|
AppendCharAsUnicodeJavaScript(b, c);
|
|
}
|
|
else {
|
|
count++;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (b == null) {
|
|
return value;
|
|
}
|
|
|
|
if (count > 0) {
|
|
b.Append(value, startIndex, count);
|
|
}
|
|
|
|
return b.ToString();
|
|
}
|
|
|
|
internal byte[] UrlDecode(byte[] bytes, int offset, int count) {
|
|
if (!ValidateUrlEncodingParameters(bytes, offset, count)) {
|
|
return null;
|
|
}
|
|
|
|
int decodedBytesCount = 0;
|
|
byte[] decodedBytes = new byte[count];
|
|
|
|
for (int i = 0; i < count; i++) {
|
|
int pos = offset + i;
|
|
byte b = bytes[pos];
|
|
|
|
if (b == '+') {
|
|
b = (byte)' ';
|
|
}
|
|
else if (b == '%' && i < count - 2) {
|
|
int h1 = HttpEncoderUtility.HexToInt((char)bytes[pos + 1]);
|
|
int h2 = HttpEncoderUtility.HexToInt((char)bytes[pos + 2]);
|
|
|
|
if (h1 >= 0 && h2 >= 0) { // valid 2 hex chars
|
|
b = (byte)((h1 << 4) | h2);
|
|
i += 2;
|
|
}
|
|
}
|
|
|
|
decodedBytes[decodedBytesCount++] = b;
|
|
}
|
|
|
|
if (decodedBytesCount < decodedBytes.Length) {
|
|
byte[] newDecodedBytes = new byte[decodedBytesCount];
|
|
Array.Copy(decodedBytes, newDecodedBytes, decodedBytesCount);
|
|
decodedBytes = newDecodedBytes;
|
|
}
|
|
|
|
return decodedBytes;
|
|
}
|
|
|
|
internal string UrlDecode(byte[] bytes, int offset, int count, Encoding encoding) {
|
|
if (!ValidateUrlEncodingParameters(bytes, offset, count)) {
|
|
return null;
|
|
}
|
|
|
|
UrlDecoder helper = new UrlDecoder(count, encoding);
|
|
|
|
// go through the bytes collapsing %XX and %uXXXX and appending
|
|
// each byte as byte, with exception of %uXXXX constructs that
|
|
// are appended as chars
|
|
|
|
for (int i = 0; i < count; i++) {
|
|
int pos = offset + i;
|
|
byte b = bytes[pos];
|
|
|
|
// The code assumes that + and % cannot be in multibyte sequence
|
|
|
|
if (b == '+') {
|
|
b = (byte)' ';
|
|
}
|
|
else if (b == '%' && i < count - 2) {
|
|
if (bytes[pos + 1] == 'u' && i < count - 5) {
|
|
int h1 = HttpEncoderUtility.HexToInt((char)bytes[pos + 2]);
|
|
int h2 = HttpEncoderUtility.HexToInt((char)bytes[pos + 3]);
|
|
int h3 = HttpEncoderUtility.HexToInt((char)bytes[pos + 4]);
|
|
int h4 = HttpEncoderUtility.HexToInt((char)bytes[pos + 5]);
|
|
|
|
if (h1 >= 0 && h2 >= 0 && h3 >= 0 && h4 >= 0) { // valid 4 hex chars
|
|
char ch = (char)((h1 << 12) | (h2 << 8) | (h3 << 4) | h4);
|
|
i += 5;
|
|
|
|
// don't add as byte
|
|
helper.AddChar(ch);
|
|
continue;
|
|
}
|
|
}
|
|
else {
|
|
int h1 = HttpEncoderUtility.HexToInt((char)bytes[pos + 1]);
|
|
int h2 = HttpEncoderUtility.HexToInt((char)bytes[pos + 2]);
|
|
|
|
if (h1 >= 0 && h2 >= 0) { // valid 2 hex chars
|
|
b = (byte)((h1 << 4) | h2);
|
|
i += 2;
|
|
}
|
|
}
|
|
}
|
|
|
|
helper.AddByte(b);
|
|
}
|
|
|
|
return Utf16StringValidator.ValidateString(helper.GetString());
|
|
}
|
|
|
|
internal string UrlDecode(string value, Encoding encoding) {
|
|
if (value == null) {
|
|
return null;
|
|
}
|
|
|
|
int count = value.Length;
|
|
UrlDecoder helper = new UrlDecoder(count, encoding);
|
|
|
|
// go through the string's chars collapsing %XX and %uXXXX and
|
|
// appending each char as char, with exception of %XX constructs
|
|
// that are appended as bytes
|
|
|
|
for (int pos = 0; pos < count; pos++) {
|
|
char ch = value[pos];
|
|
|
|
if (ch == '+') {
|
|
ch = ' ';
|
|
}
|
|
else if (ch == '%' && pos < count - 2) {
|
|
if (value[pos + 1] == 'u' && pos < count - 5) {
|
|
int h1 = HttpEncoderUtility.HexToInt(value[pos + 2]);
|
|
int h2 = HttpEncoderUtility.HexToInt(value[pos + 3]);
|
|
int h3 = HttpEncoderUtility.HexToInt(value[pos + 4]);
|
|
int h4 = HttpEncoderUtility.HexToInt(value[pos + 5]);
|
|
|
|
if (h1 >= 0 && h2 >= 0 && h3 >= 0 && h4 >= 0) { // valid 4 hex chars
|
|
ch = (char)((h1 << 12) | (h2 << 8) | (h3 << 4) | h4);
|
|
pos += 5;
|
|
|
|
// only add as char
|
|
helper.AddChar(ch);
|
|
continue;
|
|
}
|
|
}
|
|
else {
|
|
int h1 = HttpEncoderUtility.HexToInt(value[pos + 1]);
|
|
int h2 = HttpEncoderUtility.HexToInt(value[pos + 2]);
|
|
|
|
if (h1 >= 0 && h2 >= 0) { // valid 2 hex chars
|
|
byte b = (byte)((h1 << 4) | h2);
|
|
pos += 2;
|
|
|
|
// don't add as char
|
|
helper.AddByte(b);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ((ch & 0xFF80) == 0)
|
|
helper.AddByte((byte)ch); // 7 bit have to go as bytes because of Unicode
|
|
else
|
|
helper.AddChar(ch);
|
|
}
|
|
|
|
return Utf16StringValidator.ValidateString(helper.GetString());
|
|
}
|
|
|
|
internal byte[] UrlEncode(byte[] bytes, int offset, int count, bool alwaysCreateNewReturnValue) {
|
|
byte[] encoded = UrlEncode(bytes, offset, count);
|
|
|
|
return (alwaysCreateNewReturnValue && (encoded != null) && (encoded == bytes))
|
|
? (byte[])encoded.Clone()
|
|
: encoded;
|
|
}
|
|
|
|
protected internal virtual byte[] UrlEncode(byte[] bytes, int offset, int count) {
|
|
if (!ValidateUrlEncodingParameters(bytes, offset, count)) {
|
|
return null;
|
|
}
|
|
|
|
int cSpaces = 0;
|
|
int cUnsafe = 0;
|
|
|
|
// count them first
|
|
for (int i = 0; i < count; i++) {
|
|
char ch = (char)bytes[offset + i];
|
|
|
|
if (ch == ' ')
|
|
cSpaces++;
|
|
else if (!HttpEncoderUtility.IsUrlSafeChar(ch))
|
|
cUnsafe++;
|
|
}
|
|
|
|
// nothing to expand?
|
|
if (cSpaces == 0 && cUnsafe == 0) {
|
|
// DevDiv 912606: respect "offset" and "count"
|
|
if (0 == offset && bytes.Length == count) {
|
|
return bytes;
|
|
}
|
|
else {
|
|
var subarray = new byte[count];
|
|
Buffer.BlockCopy(bytes, offset, subarray, 0, count);
|
|
return subarray;
|
|
}
|
|
}
|
|
|
|
// expand not 'safe' characters into %XX, spaces to +s
|
|
byte[] expandedBytes = new byte[count + cUnsafe * 2];
|
|
int pos = 0;
|
|
|
|
for (int i = 0; i < count; i++) {
|
|
byte b = bytes[offset + i];
|
|
char ch = (char)b;
|
|
|
|
if (HttpEncoderUtility.IsUrlSafeChar(ch)) {
|
|
expandedBytes[pos++] = b;
|
|
}
|
|
else if (ch == ' ') {
|
|
expandedBytes[pos++] = (byte)'+';
|
|
}
|
|
else {
|
|
expandedBytes[pos++] = (byte)'%';
|
|
expandedBytes[pos++] = (byte)HttpEncoderUtility.IntToHex((b >> 4) & 0xf);
|
|
expandedBytes[pos++] = (byte)HttpEncoderUtility.IntToHex(b & 0x0f);
|
|
}
|
|
}
|
|
|
|
return expandedBytes;
|
|
}
|
|
|
|
// Helper to encode the non-ASCII url characters only
|
|
internal String UrlEncodeNonAscii(string str, Encoding e) {
|
|
if (String.IsNullOrEmpty(str))
|
|
return str;
|
|
if (e == null)
|
|
e = Encoding.UTF8;
|
|
byte[] bytes = e.GetBytes(str);
|
|
byte[] encodedBytes = UrlEncodeNonAscii(bytes, 0, bytes.Length, false /* alwaysCreateNewReturnValue */);
|
|
return Encoding.ASCII.GetString(encodedBytes);
|
|
}
|
|
|
|
internal byte[] UrlEncodeNonAscii(byte[] bytes, int offset, int count, bool alwaysCreateNewReturnValue) {
|
|
if (!ValidateUrlEncodingParameters(bytes, offset, count)) {
|
|
return null;
|
|
}
|
|
|
|
int cNonAscii = 0;
|
|
|
|
// count them first
|
|
for (int i = 0; i < count; i++) {
|
|
if (IsNonAsciiByte(bytes[offset + i]))
|
|
cNonAscii++;
|
|
}
|
|
|
|
// nothing to expand?
|
|
if (!alwaysCreateNewReturnValue && cNonAscii == 0)
|
|
return bytes;
|
|
|
|
// expand not 'safe' characters into %XX, spaces to +s
|
|
byte[] expandedBytes = new byte[count + cNonAscii * 2];
|
|
int pos = 0;
|
|
|
|
for (int i = 0; i < count; i++) {
|
|
byte b = bytes[offset + i];
|
|
|
|
if (IsNonAsciiByte(b)) {
|
|
expandedBytes[pos++] = (byte)'%';
|
|
expandedBytes[pos++] = (byte)HttpEncoderUtility.IntToHex((b >> 4) & 0xf);
|
|
expandedBytes[pos++] = (byte)HttpEncoderUtility.IntToHex(b & 0x0f);
|
|
}
|
|
else {
|
|
expandedBytes[pos++] = b;
|
|
}
|
|
}
|
|
|
|
return expandedBytes;
|
|
}
|
|
|
|
[Obsolete("This method produces non-standards-compliant output and has interoperability issues. The preferred alternative is UrlEncode(*).")]
|
|
internal string UrlEncodeUnicode(string value, bool ignoreAscii) {
|
|
if (value == null) {
|
|
return null;
|
|
}
|
|
|
|
int l = value.Length;
|
|
StringBuilder sb = new StringBuilder(l);
|
|
|
|
for (int i = 0; i < l; i++) {
|
|
char ch = value[i];
|
|
|
|
if ((ch & 0xff80) == 0) { // 7 bit?
|
|
if (ignoreAscii || HttpEncoderUtility.IsUrlSafeChar(ch)) {
|
|
sb.Append(ch);
|
|
}
|
|
else if (ch == ' ') {
|
|
sb.Append('+');
|
|
}
|
|
else {
|
|
sb.Append('%');
|
|
sb.Append(HttpEncoderUtility.IntToHex((ch >> 4) & 0xf));
|
|
sb.Append(HttpEncoderUtility.IntToHex((ch) & 0xf));
|
|
}
|
|
}
|
|
else { // arbitrary Unicode?
|
|
sb.Append("%u");
|
|
sb.Append(HttpEncoderUtility.IntToHex((ch >> 12) & 0xf));
|
|
sb.Append(HttpEncoderUtility.IntToHex((ch >> 8) & 0xf));
|
|
sb.Append(HttpEncoderUtility.IntToHex((ch >> 4) & 0xf));
|
|
sb.Append(HttpEncoderUtility.IntToHex((ch) & 0xf));
|
|
}
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
[SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings",
|
|
Justification = "Does not represent an entire URL, just a portion.")]
|
|
protected internal virtual string UrlPathEncode(string value) {
|
|
// DevDiv 995259: HttpUtility.UrlPathEncode should not encode IDN part of the url
|
|
if (BinaryCompatibility.Current.TargetsAtLeastFramework46) {
|
|
if (String.IsNullOrEmpty(value)) {
|
|
return value;
|
|
}
|
|
|
|
string schemeAndAuthority;
|
|
string path;
|
|
string queryAndFragment;
|
|
bool isValidUrl = UriUtil.TrySplitUriForPathEncode(value, out schemeAndAuthority, out path, out queryAndFragment, checkScheme: false);
|
|
|
|
if (!isValidUrl) {
|
|
// If the value is not a valid url, we treat it as a relative url.
|
|
// We don't need to extract query string from the url since UrlPathEncode()
|
|
// does not encode query string.
|
|
schemeAndAuthority = null;
|
|
path = value;
|
|
queryAndFragment = null;
|
|
}
|
|
|
|
return schemeAndAuthority + UrlPathEncodeImpl(path) + queryAndFragment;
|
|
}
|
|
else {
|
|
return UrlPathEncodeImpl(value);
|
|
}
|
|
}
|
|
|
|
// This is the original UrlPathEncode(string)
|
|
[SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings",
|
|
Justification = "Does not represent an entire URL, just a portion.")]
|
|
private string UrlPathEncodeImpl(string value) {
|
|
if (String.IsNullOrEmpty(value)) {
|
|
return value;
|
|
}
|
|
|
|
// recurse in case there is a query string
|
|
int i = value.IndexOf('?');
|
|
if (i >= 0)
|
|
return UrlPathEncodeImpl(value.Substring(0, i)) + value.Substring(i);
|
|
|
|
// encode DBCS characters and spaces only
|
|
return HttpEncoderUtility.UrlEncodeSpaces(UrlEncodeNonAscii(value, Encoding.UTF8));
|
|
}
|
|
|
|
internal byte[] UrlTokenDecode(string input) {
|
|
if (input == null)
|
|
throw new ArgumentNullException("input");
|
|
|
|
int len = input.Length;
|
|
if (len < 1)
|
|
return new byte[0];
|
|
|
|
///////////////////////////////////////////////////////////////////
|
|
// Step 1: Calculate the number of padding chars to append to this string.
|
|
// The number of padding chars to append is stored in the last char of the string.
|
|
int numPadChars = (int)input[len - 1] - (int)'0';
|
|
if (numPadChars < 0 || numPadChars > 10)
|
|
return null;
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////
|
|
// Step 2: Create array to store the chars (not including the last char)
|
|
// and the padding chars
|
|
char[] base64Chars = new char[len - 1 + numPadChars];
|
|
|
|
|
|
////////////////////////////////////////////////////////
|
|
// Step 3: Copy in the chars. Transform the "-" to "+", and "*" to "/"
|
|
for (int iter = 0; iter < len - 1; iter++) {
|
|
char c = input[iter];
|
|
|
|
switch (c) {
|
|
case '-':
|
|
base64Chars[iter] = '+';
|
|
break;
|
|
|
|
case '_':
|
|
base64Chars[iter] = '/';
|
|
break;
|
|
|
|
default:
|
|
base64Chars[iter] = c;
|
|
break;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////
|
|
// Step 4: Add padding chars
|
|
for (int iter = len - 1; iter < base64Chars.Length; iter++) {
|
|
base64Chars[iter] = '=';
|
|
}
|
|
|
|
// Do the actual conversion
|
|
return Convert.FromBase64CharArray(base64Chars, 0, base64Chars.Length);
|
|
}
|
|
|
|
internal string UrlTokenEncode(byte[] input) {
|
|
if (input == null)
|
|
throw new ArgumentNullException("input");
|
|
if (input.Length < 1)
|
|
return String.Empty;
|
|
|
|
string base64Str = null;
|
|
int endPos = 0;
|
|
char[] base64Chars = null;
|
|
|
|
////////////////////////////////////////////////////////
|
|
// Step 1: Do a Base64 encoding
|
|
base64Str = Convert.ToBase64String(input);
|
|
if (base64Str == null)
|
|
return null;
|
|
|
|
////////////////////////////////////////////////////////
|
|
// Step 2: Find how many padding chars are present in the end
|
|
for (endPos = base64Str.Length; endPos > 0; endPos--) {
|
|
if (base64Str[endPos - 1] != '=') // Found a non-padding char!
|
|
{
|
|
break; // Stop here
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////
|
|
// Step 3: Create char array to store all non-padding chars,
|
|
// plus a char to indicate how many padding chars are needed
|
|
base64Chars = new char[endPos + 1];
|
|
base64Chars[endPos] = (char)((int)'0' + base64Str.Length - endPos); // Store a char at the end, to indicate how many padding chars are needed
|
|
|
|
////////////////////////////////////////////////////////
|
|
// Step 3: Copy in the other chars. Transform the "+" to "-", and "/" to "_"
|
|
for (int iter = 0; iter < endPos; iter++) {
|
|
char c = base64Str[iter];
|
|
|
|
switch (c) {
|
|
case '+':
|
|
base64Chars[iter] = '-';
|
|
break;
|
|
|
|
case '/':
|
|
base64Chars[iter] = '_';
|
|
break;
|
|
|
|
case '=':
|
|
Debug.Assert(false);
|
|
base64Chars[iter] = c;
|
|
break;
|
|
|
|
default:
|
|
base64Chars[iter] = c;
|
|
break;
|
|
}
|
|
}
|
|
return new string(base64Chars);
|
|
}
|
|
|
|
internal static bool ValidateUrlEncodingParameters(byte[] bytes, int offset, int count) {
|
|
if (bytes == null && count == 0)
|
|
return false;
|
|
if (bytes == null) {
|
|
throw new ArgumentNullException("bytes");
|
|
}
|
|
if (offset < 0 || offset > bytes.Length) {
|
|
throw new ArgumentOutOfRangeException("offset");
|
|
}
|
|
if (count < 0 || offset + count > bytes.Length) {
|
|
throw new ArgumentOutOfRangeException("count");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Internal class to facilitate URL decoding -- keeps char buffer and byte buffer, allows appending of either chars or bytes
|
|
private class UrlDecoder {
|
|
private int _bufferSize;
|
|
|
|
// Accumulate characters in a special array
|
|
private int _numChars;
|
|
private char[] _charBuffer;
|
|
|
|
// Accumulate bytes for decoding into characters in a special array
|
|
private int _numBytes;
|
|
private byte[] _byteBuffer;
|
|
|
|
// Encoding to convert chars to bytes
|
|
private Encoding _encoding;
|
|
|
|
private void FlushBytes() {
|
|
if (_numBytes > 0) {
|
|
_numChars += _encoding.GetChars(_byteBuffer, 0, _numBytes, _charBuffer, _numChars);
|
|
_numBytes = 0;
|
|
}
|
|
}
|
|
|
|
internal UrlDecoder(int bufferSize, Encoding encoding) {
|
|
_bufferSize = bufferSize;
|
|
_encoding = encoding;
|
|
|
|
_charBuffer = new char[bufferSize];
|
|
// byte buffer created on demand
|
|
}
|
|
|
|
internal void AddChar(char ch) {
|
|
if (_numBytes > 0)
|
|
FlushBytes();
|
|
|
|
_charBuffer[_numChars++] = ch;
|
|
}
|
|
|
|
internal void AddByte(byte b) {
|
|
// if there are no pending bytes treat 7 bit bytes as characters
|
|
// this optimization is temp disable as it doesn't work for some encodings
|
|
/*
|
|
if (_numBytes == 0 && ((b & 0x80) == 0)) {
|
|
AddChar((char)b);
|
|
}
|
|
else
|
|
*/
|
|
{
|
|
if (_byteBuffer == null)
|
|
_byteBuffer = new byte[_bufferSize];
|
|
|
|
_byteBuffer[_numBytes++] = b;
|
|
}
|
|
}
|
|
|
|
internal String GetString() {
|
|
if (_numBytes > 0)
|
|
FlushBytes();
|
|
|
|
if (_numChars > 0)
|
|
return new String(_charBuffer, 0, _numChars);
|
|
else
|
|
return String.Empty;
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|