// Copyright (C) 2011, Kenneth Skovhede // http://www.hexad.dk, opensource@hexad.dk // // This library is free software; you can redistribute it and/or modify // it under the terms of the GNU Lesser General Public License as // published by the Free Software Foundation; either version 2.1 of the // License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA using System; using System.Collections.Specialized; using System.Web; namespace Duplicati.Library.Utility { /// /// Represents a relaxed parsing of a URL. /// The goal is to cover as many types of url's as possible, /// without being ambiguos. /// The major limitations is that an embedded username may not contain a :, /// and the password may not contain a @. /// public struct Uri { /// /// A very lax version of a URL parser /// private static System.Text.RegularExpressions.Regex URL_PARSER = new System.Text.RegularExpressions.Regex(@"(?[^:]+)://(((?[^\:]+)(\:(?[^@]*))?\@))?((?[^/\?\:]+)(\:(?\d+))?)?(/(?[^\?]*))?(\?(?.+))?"); /// /// The URL scheme, e.g. http /// public readonly string Scheme; /// /// The server name, e.g. www.example.com /// public readonly string Host; /// /// The server path, e.g. index.html /// public readonly string Path; /// /// The server port, e.g. 80, is -1 if using the default port /// public readonly int Port; /// /// The querystring, e.g. ?id=1 /// public readonly string Query; /// /// The username, if any /// public readonly string Username; /// /// The password, if any /// public readonly string Password; /// /// The original URI. /// public readonly string OriginalUri; /// /// Cache for the query parameters. /// private NameValueCollection m_queryParams; /// /// Gets the paramters in the query string /// /// The query parameters. public NameValueCollection QueryParameters { get { if (m_queryParams == null) { if (Query == null) m_queryParams = new NameValueCollection(); else m_queryParams = ParseQueryString(Query); } return m_queryParams; } } /// /// Gets the host and path. /// /// The host and path. public string HostAndPath { get { if (string.IsNullOrEmpty(Path)) return Host; else if (string.IsNullOrEmpty(Host)) return Path; else return Host + (Path == null ? "" : "/" + Path); } } /// /// Gets the path and query. /// /// The path and query. public string PathAndQuery { get { return (Path ?? "") + (Query == null ? "" : "?" + Query); } } /// /// Initializes a new instance of the struct. /// /// The URL to parse public Uri(string url) { if (string.IsNullOrEmpty(url)) throw new ArgumentNullException("url"); m_queryParams = null; this.OriginalUri = url; var m = URL_PARSER.Match(url); if (!m.Success || m.Length != url.Length) { var path = url; if (path.StartsWith("file://", StringComparison.InvariantCultureIgnoreCase)) path = path.Substring("file://".Length); if (path.IndexOfAny(System.IO.Path.GetInvalidPathChars()) < 0) try { var fp = System.IO.Path.GetFullPath(path); this.Scheme = "file"; this.Host = null; this.Path = fp; this.Port = -1; this.Query = null; this.Username = null; this.Password = null; return; } catch { } throw new ArgumentException(string.Format(Strings.Uri.UriParseError, url), "url"); } this.Scheme = m.Groups["scheme"].Value; if (!m.Groups["hostname"].Success && m.Groups["path"].Success) { this.Host = m.Groups["path"].Value; this.Path = ""; } else { this.Host = m.Groups["hostname"].Value; this.Path = m.Groups["path"].Success ? m.Groups["path"].Value : ""; } this.Query = m.Groups["query"].Success ? m.Groups["query"].Value : null; this.Username = m.Groups["username"].Success ? UrlDecode(m.Groups["username"].Value) : null; this.Password = m.Groups["password"].Success ? UrlDecode(m.Groups["password"].Value) : null; if (m.Groups["port"].Success) this.Port = int.Parse(m.Groups["port"].Value); else this.Port = -1; } /// /// Constructs a free-form URI from components /// /// The url scheme, e.g. http /// The hostname, e.g. www.example.com /// The path, e.g. index.html /// The query string, e.g. id=1 /// The username /// The password /// The port public Uri(string scheme, string host, string path = null, string query = null, string username = null, string password = null, int port = -1) { m_queryParams = null; Scheme = scheme; Host = host; Path = path; Query = query; Username = username; Password = password; Port = port; OriginalUri = AsString(scheme, host, path, query, username, password, port); } /// /// Returns a that represents the current . /// /// A that represents the current . public override string ToString () { return AsString(Scheme, Host, Path, Query, Username, Password, Port); } /// /// Throws an exception if the host name is missing. /// public void RequireHost() { if (string.IsNullOrEmpty(Host)) throw new ArgumentException(string.Format(Strings.Uri.NoHostname, OriginalUri)); } /// /// Constructs an url-like string from components. /// /// An url-like string /// The url scheme, e.g. http /// The hostname, e.g. www.example.com /// The path, e.g. index.html /// The query string, e.g. id=1 /// The username /// The password /// The port private static string AsString(string scheme, string host, string path, string query, string username, string password, int port) { var s = scheme + "://"; if (!string.IsNullOrEmpty(username) || !string.IsNullOrEmpty(password)) { s += UrlEncode(username ?? ""); s += ":"; s += UrlEncode(password ?? ""); s += "@"; } if (!string.IsNullOrEmpty(host)) { s += host; if (port != -1) s += ":" + port.ToString(); } if (!string.IsNullOrEmpty(path)) { if (!string.IsNullOrEmpty(host)) s += "/"; s += path; } if (!string.IsNullOrEmpty(query)) s += "?" + query; return s; } /// /// Creates a new instance with another scheme /// /// A new instance /// The new scheme to use public Uri SetScheme(string scheme) { return new Uri(scheme, Host, Path, Query, Username, Password, Port); } /// /// Creates a new instance with another host /// /// A new instance /// The new hostname to use public Uri SetHost(string host) { return new Uri(Scheme, host, Path, Query, Username, Password, Port); } /// /// Creates a new instance with another path /// /// A new instance /// The new path to use public Uri SetPath(string path) { return new Uri(Scheme, Host, path, Query, Username, Password, Port); } /// /// Creates a new instance with another query /// /// A new instance /// The new query to use public Uri SetQuery(string query) { return new Uri(Scheme, Host, Path, query, Username, Password, Port); } /// /// Creates a new instance with other credentials /// /// A new instance /// The new username to use /// The new password to use public Uri SetCredentials(string username, string password) { return new Uri(Scheme, Host, Path, Query, username, password, Port); } /// /// Creates a new instance with another port /// /// A new instance /// The new port to use public Uri SetPort(int port) { return new Uri(Scheme, Host, Path, Query, Username, Password, port); } /// /// The regular expression that matches %20 type values in a querystring /// private static System.Text.RegularExpressions.Regex RE_ESCAPECHAR = new System.Text.RegularExpressions.Regex(@"[^0-9a-zA-Z]", System.Text.RegularExpressions.RegexOptions.Compiled); /// /// Encodes a URL, like System.Web.HttpUtility.UrlEncode /// /// The encoded URL /// The URL fragment to encode /// The encoding to use public static string UrlPathEncode(string value, System.Text.Encoding encoding = null) { return UrlEncode(value, encoding, "%20"); } /// /// Encodes a URL, like System.Web.HttpUtility.UrlEncode /// /// The encoded URL /// The URL fragment to encode /// The encoding to use public static string UrlEncode(string value, System.Text.Encoding encoding = null, string spacevalue = "+") { if (value == null) throw new ArgumentNullException("value"); encoding = encoding ?? System.Text.Encoding.UTF8; var encoder = encoding.GetEncoder(); var inbuf = new char[1]; var outbuf = new byte[2]; var shortout = new byte[1]; return RE_ESCAPECHAR.Replace(value, (m) => { if (m.Value == " ") return spacevalue; inbuf[0] = m.Value[0]; try { var len = encoder.GetBytes(inbuf, 0, 1, outbuf, 0, true); if (len == 1) { shortout[0] = outbuf[0]; return "%" + Utility.ByteArrayAsHexString(shortout).ToLower(); } else if (len == 2) return "%u" + Utility.ByteArrayAsHexString(outbuf).ToLower(); } catch { } //Fallback return m.Value; }); } /// /// The regular expression that matches %20 type values in a querystring /// private static System.Text.RegularExpressions.Regex RE_NUMBER = new System.Text.RegularExpressions.Regex(@"(\%(?([0-9]|[a-z]|[A-Z]){2}))|(\+)|(\%u(?([0-9]|[a-z]|[A-Z]){4}))", System.Text.RegularExpressions.RegexOptions.Compiled); /// /// A helper for converting byte arrays to hex, vice versa /// private const string HEX_DIGITS_UPPER = "0123456789ABCDEF"; /// /// Encodes a URL, like System.Web.HttpUtility.UrlEncode /// /// The decoded URL /// The URL fragment to decode /// The encoding to use public static string UrlDecode(string value, System.Text.Encoding encoding = null) { if (value == null) throw new ArgumentNullException("value"); encoding = encoding ?? System.Text.Encoding.UTF8; var decoder = encoding.GetDecoder(); var inbuf = new byte[2]; var outbuf = new char[1]; return RE_NUMBER.Replace(value, (m) => { if (m.Value == "+") return " "; try { var hex = m.Groups["number"].Value; Utility.HexStringAsByteArray(hex, inbuf); decoder.GetChars(inbuf, 0, hex.Length / 2, outbuf, 0); return outbuf[0].ToString(); } catch { } //Fallback return m.Value; }); } /// /// The regular expression that matches a=b type values in a querystring /// private static System.Text.RegularExpressions.Regex RE_URLPARAM = new System.Text.RegularExpressions.Regex(@"(?[^\=\&]+)(\=(?[^\&]*))?", System.Text.RegularExpressions.RegexOptions.Compiled); /// /// Parses the query string. /// This is a duplicate of the System.Web.HttpUtility.ParseQueryString that does not work well on Mono /// /// The parsed query string /// The query to parse public static NameValueCollection ParseQueryString(string query) { if (query == null) throw new ArgumentNullException("query"); if (query.StartsWith("?")) query = query.Substring(1); if (string.IsNullOrEmpty(query)) return new NameValueCollection(StringComparer.InvariantCultureIgnoreCase); var result = new NameValueCollection(StringComparer.InvariantCultureIgnoreCase); foreach(System.Text.RegularExpressions.Match m in RE_URLPARAM.Matches(query)) result.Add(UrlDecode(m.Groups["key"].Value), UrlDecode(m.Groups["value"].Success ? m.Groups["value"].Value : "")); return result; } } }