// Copyright (c) 2018, 2021, Oracle and/or its affiliates. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License, version 2.0, as // published by the Free Software Foundation. // // This program is also distributed with certain software (including // but not limited to OpenSSL) that is licensed under separate terms, // as designated in a particular file or component or in included license // documentation. The authors of MySQL hereby grant you an // additional permission to link the program and your derivative works // with the separately licensed software that they have included with // MySQL. // // Without limiting anything contained in the foregoing, this file, // which is part of MySQL Connector/NET, is also subject to the // Universal FOSS Exception, version 1.0, a copy of which can be found at // http://oss.oracle.com/licenses/universal-foss-exception. // // This program 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 General Public License, version 2.0, for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA using MySql.Data.common; using MySql.Data.Common; using System; using System.Collections.Generic; using System.ComponentModel; using System.Data.Common; using System.Globalization; using System.Linq; using System.Runtime.CompilerServices; using System.Security.Authentication; using System.Text; using Sog.Properties; using static MySql.Data.common.MySqlConnectionStringOption; namespace MySql.Data.MySqlClient { /// /// Abstract class that provides common functionality for connection options that apply for all protocols. /// public abstract class MySqlBaseConnectionStringBuilder : DbConnectionStringBuilder { /// /// Readonly field containing a collection of protocol shared connection options. /// internal static readonly MySqlConnectionStringOptionCollection Options = new MySqlConnectionStringOptionCollection(); static MySqlBaseConnectionStringBuilder() { // Server options. Options.Add(new MySqlConnectionStringOption("server", "host,data source,datasource", typeof(string), "" /*"localhost"*/, false)); Options.Add(new MySqlConnectionStringOption("database", "initial catalog", typeof(string), string.Empty, false)); Options.Add(new MySqlConnectionStringOption("protocol", "connection protocol,connectionprotocol", typeof(MySqlConnectionProtocol), MySqlConnectionProtocol.Sockets, false, (SetterDelegate)((msb, sender, value) => { #if !NETFRAMEWORK MySqlConnectionProtocol enumValue; if (Enum.TryParse(value.ToString(), true, out enumValue)) { if (!Platform.IsWindows() && (enumValue == MySqlConnectionProtocol.Memory || enumValue == MySqlConnectionProtocol.Pipe)) { throw new PlatformNotSupportedException(string.Format(Resources.OptionNotCurrentlySupported, $"Protocol={value}")); } } #endif msb.SetValue("protocol", value); }), (GetterDelegate)((msb, sender) => msb.ConnectionProtocol))); Options.Add(new MySqlConnectionStringOption("port", null, typeof(uint), (uint)3306, false)); Options.Add(new MySqlConnectionStringOption("dns-srv", "dnssrv", typeof(bool), false, false)); // Authentication options. Options.Add(new MySqlConnectionStringOption("user id", "uid,username,user name,user,userid", typeof(string), "", false)); Options.Add(new MySqlConnectionStringOption("password", "pwd", typeof(string), "", false)); Options.Add(new MySqlConnectionStringOption("certificatefile", "certificate file", typeof(string), null, false)); Options.Add(new MySqlConnectionStringOption("certificatepassword", "certificate password,ssl-ca-pwd", typeof(string), null, false)); Options.Add(new MySqlConnectionStringOption("certificatestorelocation", "certificate store location", typeof(MySqlCertificateStoreLocation), MySqlCertificateStoreLocation.None, false)); Options.Add(new MySqlConnectionStringOption("certificatethumbprint", "certificate thumb print", typeof(string), null, false)); Options.Add(new MySqlConnectionStringOption("sslmode", "ssl mode,ssl-mode", typeof(MySqlSslMode), MySqlSslMode.Preferred, false, (SetterDelegate)((msb, sender, value) => { MySqlSslMode newValue = (MySqlSslMode)Enum.Parse(typeof(MySqlSslMode), value.ToString(), true); if (newValue == MySqlSslMode.None && msb.TlsVersion != null) { throw new ArgumentException(Resources.InvalidTlsVersionAndSslModeOption, nameof(TlsVersion)); } msb.SetValue("sslmode", newValue); }), (GetterDelegate)((msb, sender) => { return msb.SslMode; }))); Options.Add(new MySqlConnectionStringOption("sslca", "ssl-ca", typeof(string), null, false, (SetterDelegate)((msb, sender, value) => { msb.SslCa = value as string; }), (GetterDelegate)((msb, sender) => { return msb.SslCa; }))); Options.Add(new MySqlConnectionStringOption("sslkey", "ssl-key", typeof(string), null, false)); Options.Add(new MySqlConnectionStringOption("sslcert", "ssl-cert", typeof(string), null, false)); Options.Add(new MySqlConnectionStringOption("tlsversion", "tls-version,tls version", typeof(string), null, false, (SetterDelegate)((msb, sender, value) => { if (value == null || string.IsNullOrWhiteSpace((string)value)) { msb.SetValue("tlsversion", null); return; } if (msb.SslMode == MySqlSslMode.None) { throw new ArgumentException(Resources.InvalidTlsVersionAndSslModeOption, nameof(TlsVersion)); } string strValue = ((string)value).TrimStart('[', '(').TrimEnd(']', ')').Replace(" ", string.Empty); if (string.IsNullOrWhiteSpace(strValue) || strValue == ",") { throw new ArgumentException(Resources.TlsVersionNotSupported); } SslProtocols protocols = SslProtocols.None; foreach (string opt in strValue.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) { string tls = opt.ToLowerInvariant().Replace("v", "").Replace(".", ""); if (tls.Equals("tls1") || tls.Equals("tls10")) { tls = "tls"; } SslProtocols protocol; if (!tls.StartsWith("tls", StringComparison.OrdinalIgnoreCase) || (!Enum.TryParse(tls, true, out protocol) && !tls.Equals("tls13", StringComparison.OrdinalIgnoreCase))) { string info = string.Empty; #if NET48 || NETSTANDARD2_1 info = ", TLSv1.3"; #endif throw new ArgumentException(string.Format(Resources.InvalidTlsVersionOption, opt, info), nameof(TlsVersion)); } protocols |= protocol; } string strProtocols = protocols == SslProtocols.None ? string.Empty : Enum.Format(typeof(SslProtocols), protocols, "G"); strProtocols = (value.ToString().Equals("Tls13", StringComparison.OrdinalIgnoreCase) || value.ToString().Equals("Tlsv1.3", StringComparison.OrdinalIgnoreCase)) ? "Tls13" : strProtocols; msb.SetValue("tlsversion", strProtocols); }), (GetterDelegate)((msb, sender) => { return msb.TlsVersion; }))); // Other properties. Options.Add(new MySqlConnectionStringOption("keepalive", "keep alive", typeof(uint), (uint)0, false)); // Language and charset options. Options.Add(new MySqlConnectionStringOption("characterset", "character set,charset", typeof(string), "", false)); } /// /// Gets or sets a flag indicating if the object has access to procedures. /// internal bool HasProcAccess { get; set; } /// /// Gets or sets a dictionary representing key-value pairs for each connection option. /// internal Dictionary values { get; set; } #region Server Properties /// /// Gets or sets the name of the server. /// /// The server. [Category("Connection")] [Description("Server to connect to")] [RefreshProperties(RefreshProperties.All)] public string Server { get { return this["server"] as string; } set { this.SetValue("server", value); } } /// /// Gets or sets the name of the database for the initial connection. /// [Category("Connection")] [Description("Database to use initially")] [RefreshProperties(RefreshProperties.All)] public string Database { get { return this.values["database"] as string; } set { this.SetValue("database", value); } } /// /// Gets or sets the protocol that should be used for communicating /// with MySQL. /// [Category("Connection")] [DisplayName("Connection Protocol")] [Description("Protocol to use for connection to MySQL")] [RefreshProperties(RefreshProperties.All)] public MySqlConnectionProtocol ConnectionProtocol { get { return (MySqlConnectionProtocol)this.values["protocol"]; } set { this.SetValue("protocol", value); } } /// /// Gets or sets the port number that is used when the socket /// protocol is being used. /// [Category("Connection")] [Description("Port to use for TCP/IP connections")] [RefreshProperties(RefreshProperties.All)] public uint Port { get { return (uint)this.values["port"]; } set { this.SetValue("port", value); } } /// /// Gets or sets a boolean value that indicates whether this connection /// should resolve DNS SRV records. /// [Category("Connection")] [DisplayName("DNS SRV")] [Description("The connection should resolve DNS SRV records.")] [RefreshProperties(RefreshProperties.All)] public bool DnsSrv { get { return (bool)this.values["dns-srv"]; } set { this.SetValue("dns-srv", value); } } #endregion #region Authentication Properties /// /// Gets or sets the user ID that should be used to connect with. /// [Category("Security")] [DisplayName("User ID")] [Description("Indicates the user ID to be used when connecting to the data source.")] [RefreshProperties(RefreshProperties.All)] public string UserID { get { return (string)this.values["user id"]; } set { this.SetValue("user id", value); } } /// /// Gets or sets the password that should be used to make a connection. /// [Category("Security")] [Description("Indicates the password to be used when connecting to the data source.")] [RefreshProperties(RefreshProperties.All)] [PasswordPropertyText(true)] public string Password { get { return (string)this.values["password"]; } set { this.SetValue("password", value); } } /// /// Gets or sets the path to the certificate file to be used. /// [Category("Authentication")] [DisplayName("Certificate File")] [Description("Certificate file in PKCS#12 format (.pfx) or path to a local file that " + "contains a list of trusted TLS/SSL CAs (.pem).")] public string CertificateFile { get { return (string)this.values["certificatefile"]; } set { this.SetValue("certificatefile", value); } } /// /// Gets or sets the password to be used in conjunction with the certificate file. /// [Category("Authentication")] [DisplayName("Certificate Password")] [Description("Password for certificate file")] public string CertificatePassword { get { return (string)this.values["certificatepassword"]; } set { this.SetValue("certificatepassword", value); } } /// /// Gets or sets the location to a personal store where a certificate is held. /// [Category("Authentication")] [DisplayName("Certificate Store Location")] [Description("Certificate Store Location for client certificates")] [DefaultValue(MySqlCertificateStoreLocation.None)] public MySqlCertificateStoreLocation CertificateStoreLocation { get { return (MySqlCertificateStoreLocation)this.values["certificatestorelocation"]; } set { this.SetValue("certificatestorelocation", value); } } /// /// Gets or sets a certificate thumbprint to ensure correct identification of a certificate contained within a personal store. /// [Category("Authentication")] [DisplayName("Certificate Thumbprint")] [Description("Certificate thumbprint. Can be used together with Certificate " + "Store Location parameter to uniquely identify the certificate to be used " + "for SSL authentication.")] public string CertificateThumbprint { get { return (string)this.values["certificatethumbprint"]; } set { this.SetValue("certificatethumbprint", value); } } /// /// Indicates whether to use SSL connections and how to handle server certificate errors. /// [DisplayName("Ssl Mode")] [Category("Authentication")] [Description("SSL properties for connection.")] [DefaultValue(MySqlSslMode.None)] public MySqlSslMode SslMode { get { return (MySqlSslMode)this.values["sslmode"]; } set { this.SetValue("sslmode", value); } } [DisplayName("Ssl Ca")] [Category("Authentication")] [Description("Path to a local file that contains a list of trusted TLS/SSL CAs.")] public string SslCa { get { return this.CertificateFile; } set { this.CertificateFile = value; } } /// /// Sets the TLS versions to use in a SSL connection to the server. /// /// /// Tls version=TLSv1.1,TLSv1.2; /// [DisplayName("TLS version")] [Category("Security")] [Description("TLS versions to use in a SSL connection to the server.")] public string TlsVersion { get { return (string)this.values["tlsversion"]; } set { this.SetValue("tlsversion", value); } } /// /// Gets or sets the path to a local key file in PEM format to use for establishing an encrypted connection. /// [DisplayName("Ssl Key")] [Category("Authentication")] [Description("Name of the SSL key file in PEM format to use for establishing an encrypted connection.")] public string SslKey { get { return (string)this.values["sslkey"]; } set { this.SetValue("sslkey", value); } } /// /// Gets or sets the path to a local certificate file in PEM format to use for establishing an encrypted connection. /// [DisplayName("Ssl Cert")] [Category("Authentication")] [Description("Name of the SSL certificate file in PEM format to use for establishing an encrypted connection.")] public string SslCert { get { return (string)this.values["sslcert"]; } set { this.SetValue("sslcert", value); } } #endregion #region Other Properties /// /// Gets or sets the idle connection time(seconds) for TCP connections. /// [DisplayName("Keep Alive")] [Description("For TCP connections, the idle connection time (in seconds) before the first keepalive packet is sent." + "A value of 0 indicates that keepalive is not used.")] [DefaultValue(0)] public uint Keepalive { get { return (uint)this.values["keepalive"]; } set { this.SetValue("keepalive", value); } } #endregion #region Language and Character Set Properties /// /// Gets or sets the character set that should be used for sending queries to the server. /// [DisplayName("Character Set")] [Category("Advanced")] [Description("Character set this connection should use.")] [RefreshProperties(RefreshProperties.All)] [DefaultValue("")] public string CharacterSet { get { return (string)this.values["characterset"]; } set { this.SetValue("characterset", value); } } #endregion /// /// Analyzes the connection string for potential duplicated or invalid connection options. /// /// Connection string. /// Flag that indicates if the connection is using X Protocol. /// Flag that indicates if the default port is used. internal void AnalyzeConnectionString(string connectionString, bool isXProtocol, bool isDefaultPort = true) { string[] queries = connectionString.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); var usedSslOptions = new List(); bool sslModeIsNone = false; bool isDnsSrv = false; if (queries.FirstOrDefault(q => q.ToLowerInvariant().Contains("dns-srv=true")) != null || queries.FirstOrDefault(q => q.ToLowerInvariant().Contains("dnssrv=true")) != null) { isDnsSrv = true; } foreach (string query in queries) { string[] keyValue = query.Split('='); if (keyValue.Length % 2 != 0) { continue; } var keyword = keyValue[0].ToLowerInvariant().Trim(); var value = query.Contains(",") ? query.Replace(keyword, "") : keyValue[1].ToLowerInvariant(); MySqlConnectionStringOption option = Options.Options.Where(o => o.Keyword == keyword || (o.Synonyms != null && o.Synonyms.Contains(keyword))).FirstOrDefault(); // DNS SRV option can't be used if Port, Unix Socket or Multihost are specified if (isDnsSrv) { if (option.Keyword == "port" && !isDefaultPort) { throw new ArgumentException(Resources.DnsSrvInvalidConnOptionPort); } if (option.Keyword == "server" && ((value.Contains("address") && value.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries).Length > 2) || value.Contains(","))) { throw new ArgumentException(Resources.DnsSrvInvalidConnOptionMultihost); } if (option.Keyword == "protocol" && (value.ToLowerInvariant().Contains("unix") || value.ToLowerInvariant().Contains("unixsocket"))) { throw new ArgumentException(Resources.DnsSrvInvalidConnOptionUnixSocket); } } if (option == null || (option.Keyword != "sslmode" && option.Keyword != "certificatefile" && option.Keyword != "certificatepassword" && option.Keyword != "sslcrl" && option.Keyword != "sslca" && option.Keyword != "sslcert" && option.Keyword != "sslkey" && option.Keyword != "server" && option.Keyword != "tlsversion" && option.Keyword != "dns-srv")) { continue; } // SSL connection options can't be duplicated. if (usedSslOptions.Contains(option.Keyword) && option.Keyword != "server" && option.Keyword != "tlsversion" && option.Keyword != "dns-srv") { throw new ArgumentException(string.Format(Resources.DuplicatedSslConnectionOption, keyword)); } else if (usedSslOptions.Contains(option.Keyword)) { throw new ArgumentException(string.Format(Resources.DuplicatedConnectionOption, keyword)); } // SSL connection options can't be used if sslmode=None. if (option.Keyword == "sslmode" && (value == "none" || value == "disabled")) { sslModeIsNone = true; } if (sslModeIsNone && (option.Keyword == "certificatefile" || option.Keyword == "certificatepassword" || option.Keyword == "sslcrl" || option.Keyword == "sslca" || option.Keyword == "sslcert" || option.Keyword == "sslkey")) { throw new ArgumentException(Resources.InvalidOptionWhenSslDisabled); } // Preferred is not allowed for the X Protocol. if (isXProtocol && option.Keyword == "sslmode" && (value == "preferred" || value == "prefered")) { throw new ArgumentException(string.Format(Resources.InvalidSslMode, keyValue[1])); } if (option.Keyword == "sslca" || option.Keyword == "certificatefile") { usedSslOptions.Add("sslca"); usedSslOptions.Add("certificatefile"); } else { usedSslOptions.Add(option.Keyword); } } } public string GetConnectionString(bool includePass) { if (includePass) { return this.ConnectionString; } var conn = new StringBuilder(); string delimiter = ""; foreach (string key in this.Keys) { if (string.Compare(key, "password", StringComparison.OrdinalIgnoreCase) == 0 || string.Compare(key, "pwd", StringComparison.OrdinalIgnoreCase) == 0) { continue; } conn.AppendFormat(CultureInfo.CurrentCulture, "{0}{1}={2}", delimiter, key, this[key]); delimiter = ";"; } return conn.ToString(); } internal abstract MySqlConnectionStringOption GetOption(string key); public override int GetHashCode() { return base.GetHashCode(); } internal void SetValue(string keyword, object value, [CallerMemberName] string callerName = "") { MySqlConnectionStringOption option = this.GetOption(keyword); if (callerName != ".cctor" && option.IsCustomized) { this[keyword] = value; } else { this.SetInternalValue(keyword, value); } } internal abstract void SetInternalValue(string keyword, object value); } }