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