//------------------------------------------------------------------------------ // // Copyright (c) Microsoft Corporation. All rights reserved. // //------------------------------------------------------------------------------ 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; } } }