1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
|
//------------------------------------------------------------------------------
// <copyright file="UriUtil.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
//------------------------------------------------------------------------------
namespace System.Web.Util {
using System;
using System.Linq;
using System.Text;
// Contains helpers for URI generation and parsing
internal static class UriUtil {
private static readonly char[] _queryFragmentSeparators = new char[] { '?', '#' };
// Similar to UriBuilder, but contains semantics specific to generation
// of the Request.Url property.
internal static Uri BuildUri(string scheme, string serverName, string port, string path, string queryString) {
return BuildUriImpl(scheme, serverName, port, path, queryString, AppSettings.UseLegacyRequestUrlGeneration);
}
// for unit testing
internal static Uri BuildUriImpl(string scheme, string serverName, string port, string path, string queryString, bool useLegacyRequestUrlGeneration) {
Debug.Assert(!String.IsNullOrEmpty(scheme));
Debug.Assert(!String.IsNullOrEmpty(serverName));
Debug.Assert(!String.IsNullOrEmpty(path));
if (!useLegacyRequestUrlGeneration) {
if (path != null) {
// The path that is provided to us is expected to be in an already-decoded
// state, but the Uri class expects encoded input, so we'll re-encode.
// This removes ambiguity that can lead to unintentional double-unescaping.
path = EscapeForPath(path);
}
if (queryString != null) {
// Need to replace any stray '#' characters that appear in the
// query string so that we don't end up accidentally generating
// a fragment in the resulting URI.
string reencodedQueryString = queryString.Replace("#", "%23");
queryString = reencodedQueryString;
}
}
if (port != null) {
port = ":" + port;
}
string uriString = scheme + "://" + serverName + port + path + queryString;
return new Uri(uriString);
}
private static string EscapeForPath(string unescaped) {
// DevDiv 762893: Applications might not call Uri.UnescapeDataString when looking
// at components of the URI, and they'll be broken if certain path-safe characters
// are now escaped.
if (String.IsNullOrEmpty(unescaped) || ContainsOnlyPathSafeCharacters(unescaped))
return unescaped;
string escaped = Uri.EscapeDataString(unescaped);
// If nothing was escaped, no need to decode
if (String.Equals(escaped, unescaped, StringComparison.Ordinal))
return unescaped;
// We're going to perform multiple replace operations.
// StringBuilder.Replace is much more memory-efficient than String.Replace
StringBuilder builder = new StringBuilder(escaped);
// Uri.EscapeDataString() is guaranteed to produce uppercase escape sequences.
// Path-safe characters are listed in RFC 3986, Appendix A. We also add '/' to
// this list since EscapeDataString may contain path segments.
builder.Replace("%21", "!");
builder.Replace("%24", "$");
builder.Replace("%26", "&");
builder.Replace("%27", "'");
builder.Replace("%28", "(");
builder.Replace("%29", ")");
builder.Replace("%2A", "*");
builder.Replace("%2B", "+");
builder.Replace("%2C", ",");
builder.Replace("%2F", "/");
builder.Replace("%3A", ":");
builder.Replace("%3B", ";");
builder.Replace("%3D", "=");
builder.Replace("%40", "@");
return builder.ToString();
}
private static bool ContainsOnlyPathSafeCharacters(string input) {
// See RFC 3986, Appendix A for the list of path-safe characters.
for (int i = 0; i < input.Length; i++) {
char c = input[i];
// unreserved = ALPHA / DIGIT / ...
if (('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || ('0' <= c && c <= '9')) {
continue;
}
switch (c) {
case '/': // path-abempty; path-absolute
case '-': case '.': case '_': case '~': // unreserved
case ':': case '@': // pchar
case '!': case '$': case '&': case '\'': case '(': case ')': // sub-delims
case '*': case '+': case ',': case ';': case '=': // sub-delims, cont.
continue;
default:
return false; // not path-safe
}
}
// no bad characters found
return true;
}
// Just extracts the query string and fragment from the input path by splitting on the separator characters.
// Doesn't perform any validation as to whether the input represents a valid URL.
// Concatenating the pieces back together will form the original input string.
internal static void ExtractQueryAndFragment(string input, out string path, out string queryAndFragment) {
int queryFragmentSeparatorPos = input.IndexOfAny(_queryFragmentSeparators);
if (queryFragmentSeparatorPos != -1) {
path = input.Substring(0, queryFragmentSeparatorPos);
queryAndFragment = input.Substring(queryFragmentSeparatorPos);
}
else {
// no query or fragment separator
path = input;
queryAndFragment = null;
}
}
// Schemes that are generally considered safe for the purposes of redirects or other places where URLs are rendered to the page.
internal static bool IsSafeScheme(String url) {
return url.IndexOf(":", StringComparison.Ordinal) == -1 ||
url.StartsWith("http:", StringComparison.OrdinalIgnoreCase) ||
url.StartsWith("https:", StringComparison.OrdinalIgnoreCase) ||
url.StartsWith("ftp:", StringComparison.OrdinalIgnoreCase) ||
url.StartsWith("file:", StringComparison.OrdinalIgnoreCase) ||
url.StartsWith("news:", StringComparison.OrdinalIgnoreCase);
}
// Attempts to split a URI into its constituent pieces.
// Even if this method returns true, one or more of the out parameters might contain a null or empty string, e.g. if there is no query / fragment.
// Concatenating the pieces back together will form the original input string.
internal static bool TrySplitUriForPathEncode(string input, out string schemeAndAuthority, out string path, out string queryAndFragment, bool checkScheme) {
// Strip off ?query and #fragment if they exist, since we're not going to look at them
string inputWithoutQueryFragment;
ExtractQueryAndFragment(input, out inputWithoutQueryFragment, out queryAndFragment);
// DevDiv #450404: UrlPathEncode shouldn't care about the scheme of the incoming URL when it is
// performing encoding; only Response.Redirect should.
bool isValidScheme = (checkScheme) ? IsSafeScheme(inputWithoutQueryFragment) : true;
// Use Uri class to parse the url into authority and path, use that to help decide
// where to split the string. Do not rebuild the url from the Uri instance, as that
// might have subtle changes from the original string (for example, see below about "://").
Uri uri;
if (isValidScheme && Uri.TryCreate(inputWithoutQueryFragment, UriKind.Absolute, out uri)) {
string authority = uri.Authority; // e.g. "foo:81" in "http://foo:81/bar"
if (!String.IsNullOrEmpty(authority)) {
// don't make any assumptions about the scheme or the "://" part.
// For example, the "//" could be missing, or there could be "///" as in "file:///C:\foo.txt"
// To retain the same string as originally given, find the authority in the original url and include
// everything up to that.
int authorityIndex = inputWithoutQueryFragment.IndexOf(authority, StringComparison.OrdinalIgnoreCase);
if (authorityIndex != -1) {
int schemeAndAuthorityLength = authorityIndex + authority.Length;
schemeAndAuthority = inputWithoutQueryFragment.Substring(0, schemeAndAuthorityLength);
path = inputWithoutQueryFragment.Substring(schemeAndAuthorityLength);
return true;
}
}
}
// Not a safe URL
schemeAndAuthority = null;
path = null;
queryAndFragment = null;
return false;
}
}
}
|