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;
}
}
}
|