diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Common/src/Microsoft/Data/Common/AdapterUtil.cs b/src/Microsoft.Data.SqlClient/netcore/src/Common/src/Microsoft/Data/Common/AdapterUtil.cs index 8d16044e06..10f277f020 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Common/src/Microsoft/Data/Common/AdapterUtil.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Common/src/Microsoft/Data/Common/AdapterUtil.cs @@ -305,6 +305,11 @@ internal static Exception InvalidConnectionOptionValue(string key, Exception inn { return Argument(System.SRHelper.Format(SR.ADP_InvalidConnectionOptionValue, key), inner); } + static internal InvalidOperationException InvalidDataDirectory() + { + InvalidOperationException e = new InvalidOperationException(SR.ADP_InvalidDataDirectory); + return e; + } // // Generic Data Provider Collection diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Common/src/Microsoft/Data/Common/DbConnectionOptions.Common.cs b/src/Microsoft.Data.SqlClient/netcore/src/Common/src/Microsoft/Data/Common/DbConnectionOptions.Common.cs index 2cab0ba6ed..470425d3fb 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Common/src/Microsoft/Data/Common/DbConnectionOptions.Common.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Common/src/Microsoft/Data/Common/DbConnectionOptions.Common.cs @@ -85,6 +85,7 @@ private static class KEY internal const string Password = "password"; internal const string Persist_Security_Info = "persist security info"; internal const string User_ID = "user id"; + internal const string AttachDBFileName = "attachdbfilename"; } // known connection string common synonyms diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Common/src/Microsoft/Data/ProviderBase/DbConnectionFactory.cs b/src/Microsoft.Data.SqlClient/netcore/src/Common/src/Microsoft/Data/ProviderBase/DbConnectionFactory.cs index aa08c388c2..619f681c0b 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Common/src/Microsoft/Data/ProviderBase/DbConnectionFactory.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Common/src/Microsoft/Data/ProviderBase/DbConnectionFactory.cs @@ -205,7 +205,18 @@ internal DbConnectionPoolGroup GetConnectionPoolGroup(DbConnectionPoolKey key, D if (null == userConnectionOptions) { // we only allow one expansion on the connection string + userConnectionOptions = connectionOptions; + string expandedConnectionString = connectionOptions.Expand(); + + // if the expanded string is same instance (default implementation), then use the already created options + if ((object)expandedConnectionString != (object)key.ConnectionString) + { + // CONSIDER: caching the original string to reduce future parsing + DbConnectionPoolKey newKey = (DbConnectionPoolKey)((ICloneable)key).Clone(); + newKey.ConnectionString = expandedConnectionString; + return GetConnectionPoolGroup(newKey, null, ref userConnectionOptions); + } } // We don't support connection pooling on Win9x diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/Common/DbConnectionOptions.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/Common/DbConnectionOptions.cs index 647a5aee94..8d1a7043ba 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/Common/DbConnectionOptions.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/Common/DbConnectionOptions.cs @@ -5,6 +5,8 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.IO; +using System.Text; namespace Microsoft.Data.Common { @@ -105,5 +107,70 @@ public bool ContainsKey(string keyword) { return _parsetable.ContainsKey(keyword); } + + protected internal virtual string Expand() + { + return _usersConnectionString; + } + + // SxS notes: + // * this method queries "DataDirectory" value from the current AppDomain. + // This string is used for to replace "!DataDirectory!" values in the connection string, it is not considered as an "exposed resource". + // * This method uses GetFullPath to validate that root path is valid, the result is not exposed out. + internal static string ExpandDataDirectory(string keyword, string value) + { + string fullPath = null; + if ((null != value) && value.StartsWith(DataDirectory, StringComparison.OrdinalIgnoreCase)) + { + // find the replacement path + object rootFolderObject = AppDomain.CurrentDomain.GetData("DataDirectory"); + var rootFolderPath = (rootFolderObject as string); + if ((null != rootFolderObject) && (null == rootFolderPath)) + { + throw ADP.InvalidDataDirectory(); + } + else if (string.IsNullOrEmpty(rootFolderPath)) + { + rootFolderPath = AppDomain.CurrentDomain.BaseDirectory ?? string.Empty; + } + + var fileName = value.Substring(DataDirectory.Length); + + if (Path.IsPathRooted(fileName)) + { + fileName = fileName.TrimStart(new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }); + } + + fullPath = Path.Combine(rootFolderPath, fileName); + + // verify root folder path is a real path without unexpected "..\" + if (!Path.GetFullPath(fullPath).StartsWith(rootFolderPath, StringComparison.Ordinal)) + { + throw ADP.InvalidConnectionOptionValue(keyword); + } + } + return fullPath; + } + + internal string ExpandAttachDbFileName(string replacementValue) + { + int copyPosition = 0; + + StringBuilder builder = new StringBuilder(_usersConnectionString.Length); + for (NameValuePair current = _keyChain; null != current; current = current.Next) + { + if (current.Name == KEY.AttachDBFileName) + { + builder.Append($"{KEY.AttachDBFileName}={replacementValue};"); + } + else + { + builder.Append(_usersConnectionString, copyPosition, current.Length); + } + copyPosition += current.Length; + } + + return builder.ToString(); + } } } diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionString.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionString.cs index f642e84a64..b4ffa82e81 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionString.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionString.cs @@ -5,6 +5,8 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Net; +using System.Net.NetworkInformation; using System.Threading; using Microsoft.Data.Common; @@ -218,6 +220,8 @@ internal static class TRANSACTIONBINDING private static readonly Version constTypeSystemAsmVersion10 = new Version("10.0.0.0"); private static readonly Version constTypeSystemAsmVersion11 = new Version("11.0.0.0"); + private readonly string _expandedAttachDBFilename; // expanded during construction so that CreatePermissionSet & Expand are consistent + internal SqlConnectionString(string connectionString) : base(connectionString, GetParseSynonyms()) { ThrowUnsupportedIfKeywordSet(KEY.AsynchronousProcessing); @@ -332,7 +336,24 @@ internal SqlConnectionString(string connectionString) : base(connectionString, G } } - if (0 <= _attachDBFileName.IndexOf('|')) + // expand during construction so that CreatePermissionSet and Expand are consistent + _expandedAttachDBFilename = ExpandDataDirectory(KEY.AttachDBFilename, _attachDBFileName); + if (null != _expandedAttachDBFilename) + { + if (0 <= _expandedAttachDBFilename.IndexOf('|')) + { + throw ADP.InvalidConnectionOptionValue(KEY.AttachDBFilename); + } + ValidateValueLength(_expandedAttachDBFilename, TdsEnums.MAXLEN_ATTACHDBFILE, KEY.AttachDBFilename); + if (_localDBInstance == null) + { + // fail fast to verify LocalHost when using |DataDirectory| + // still must check again at connect time + string host = _dataSource; + VerifyLocalHostAndFixup(ref host, true, false /*don't fix-up*/); + } + } + else if (0 <= _attachDBFileName.IndexOf('|')) { throw ADP.InvalidConnectionOptionValue(KEY.AttachDBFilename); } @@ -340,7 +361,6 @@ internal SqlConnectionString(string connectionString) : base(connectionString, G { ValidateValueLength(_attachDBFileName, TdsEnums.MAXLEN_ATTACHDBFILE, KEY.AttachDBFilename); } - _typeSystemAssemblyVersion = constTypeSystemAsmVersion10; if (true == _userInstance && !string.IsNullOrEmpty(_failoverPartner)) @@ -471,6 +491,7 @@ internal SqlConnectionString(SqlConnectionString connectionOptions, string dataS _password = connectionOptions._password; _userID = connectionOptions._userID; _workstationId = connectionOptions._workstationId; + _expandedAttachDBFilename = connectionOptions._expandedAttachDBFilename; _typeSystemVersion = connectionOptions._typeSystemVersion; _transactionBinding = connectionOptions._transactionBinding; _applicationIntent = connectionOptions._applicationIntent; @@ -531,6 +552,52 @@ internal SqlConnectionString(SqlConnectionString connectionOptions, string dataS internal TransactionBindingEnum TransactionBinding { get { return _transactionBinding; } } + internal bool EnforceLocalHost + { + get + { + // so tdsparser.connect can determine if SqlConnection.UserConnectionOptions + // needs to enfoce local host after datasource alias lookup + return (null != _expandedAttachDBFilename) && (null == _localDBInstance); + } + } + + protected internal override string Expand() + { + if (null != _expandedAttachDBFilename) + { + return ExpandAttachDbFileName(_expandedAttachDBFilename); + } + else + { + return base.Expand(); + } + } + + private static bool CompareHostName(ref string host, string name, bool fixup) + { + // same computer name or same computer name + "\named instance" + bool equal = false; + + if (host.Equals(name, StringComparison.OrdinalIgnoreCase)) + { + if (fixup) + { + host = "."; + } + equal = true; + } + else if (host.StartsWith(name + @"\", StringComparison.OrdinalIgnoreCase)) + { + if (fixup) + { + host = "." + host.Substring(name.Length); + } + equal = true; + } + return equal; + } + // This dictionary is meant to be read-only translation of parsed string // keywords/synonyms to a known keyword string. internal static Dictionary GetParseSynonyms() @@ -636,6 +703,49 @@ private void ValidateValueLength(string value, int limit, string key) } } + internal static void VerifyLocalHostAndFixup(ref string host, bool enforceLocalHost, bool fixup) + { + if (string.IsNullOrEmpty(host)) + { + if (fixup) + { + host = "."; + } + } + else if (!CompareHostName(ref host, @".", fixup) && + !CompareHostName(ref host, @"(local)", fixup)) + { + // Fix-up completed in CompareHostName if return value true. + string name = GetComputerNameDnsFullyQualified(); // i.e, machine.location.corp.company.com + if (!CompareHostName(ref host, name, fixup)) + { + int separatorPos = name.IndexOf('.'); // to compare just 'machine' part + if ((separatorPos <= 0) || !CompareHostName(ref host, name.Substring(0, separatorPos), fixup)) + { + if (enforceLocalHost) + { + throw ADP.InvalidConnectionOptionValue(KEY.AttachDBFilename); + } + } + } + } + } + + private static string GetComputerNameDnsFullyQualified() + { + try + { + var domainName = "." + IPGlobalProperties.GetIPGlobalProperties().DomainName; + var hostName = Dns.GetHostName(); + if (domainName != "." && !hostName.EndsWith(domainName)) + hostName += domainName; + return hostName; + } + catch (System.Net.Sockets.SocketException) + { + return Environment.MachineName; + } + } internal ApplicationIntent ConvertValueToApplicationIntent() { diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 069e2b2d72..c4b3fcd910 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -1703,6 +1703,12 @@ private void ResolveExtendedServerName(ServerInfo serverInfo, bool aliasLookup, string host = serverInfo.UserServerName; string protocol = serverInfo.UserProtocol; + //TODO: fix local host enforcement with datadirectory and failover + if (options.EnforceLocalHost) + { + // verify LocalHost for |DataDirectory| usage + SqlConnectionString.VerifyLocalHostAndFixup(ref host, true, true /*fix-up to "."*/); + } serverInfo.SetDerivedNames(protocol, host); } diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Resources/SR.Designer.cs b/src/Microsoft.Data.SqlClient/netcore/src/Resources/SR.Designer.cs index 336b21c1f5..f7a9088e44 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Resources/SR.Designer.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Resources/SR.Designer.cs @@ -339,6 +339,15 @@ internal class SR { } } + /// + /// Looks up a localized string similar to The DataDirectory substitute is not a string.. + /// + internal static string ADP_InvalidDataDirectory { + get { + return ResourceManager.GetString("ADP_InvalidDataDirectory", resourceCulture); + } + } + /// /// Looks up a localized string similar to Specified length '{0}' is out of range.. /// diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Resources/SR.resx b/src/Microsoft.Data.SqlClient/netcore/src/Resources/SR.resx index fbdf5d8e67..f41b408e6a 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Resources/SR.resx +++ b/src/Microsoft.Data.SqlClient/netcore/src/Resources/SR.resx @@ -165,6 +165,9 @@ Internal DbConnection Error: {0} + + The DataDirectory substitute is not a string. + The {0} enumeration value, {1}, is invalid. diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/ProviderBase/DbConnectionFactory.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/ProviderBase/DbConnectionFactory.cs index ddb3dfd2a2..53de68caf1 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/ProviderBase/DbConnectionFactory.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/ProviderBase/DbConnectionFactory.cs @@ -454,7 +454,7 @@ internal DbConnectionPoolGroup GetConnectionPoolGroup(DbConnectionPoolKey key, D userConnectionOptions = connectionOptions; expandedConnectionString = connectionOptions.Expand(); - // if the expanded string is same instance (default implementation), the use the already created options + // if the expanded string is same instance (default implementation), then use the already created options if ((object)expandedConnectionString != (object)key.ConnectionString) { // CONSIDER: caching the original string to reduce future parsing diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionStringBuilderTest.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionStringBuilderTest.cs index 2ce50c0f29..553727b03a 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionStringBuilderTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionStringBuilderTest.cs @@ -48,6 +48,45 @@ public void UnexpectedKeywordRetrieval() Assert.Throws(() => builder["RandomKeyword"]); } + [Theory] + [InlineData(@"C:\test\attach.mdf", "AttachDbFilename=C:\\test\\attach.mdf")] + [InlineData(@"C:\test\attach.mdf;", "AttachDbFilename=\"C:\\test\\attach.mdf;\"")] + public void ConnectionString_AttachDbFileName_Plain(string value, string expected) + { + var builder = new SqlConnectionStringBuilder(); + builder.AttachDBFilename = value; + Assert.Equal(expected, builder.ConnectionString); + } + + [Theory] + [PlatformSpecific(TestPlatforms.Windows)] + [InlineData(@"|DataDirectory|\attach.mdf", + @"AttachDbFilename=|DataDirectory|\attach.mdf", + @"C:\test\")] + [InlineData(@"|DataDirectory|\attach.mdf", + @"AttachDbFilename=|DataDirectory|\attach.mdf", + @"C:\test")] + [InlineData(@"|DataDirectory|attach.mdf", + @"AttachDbFilename=|DataDirectory|attach.mdf", + @"C:\test")] + [InlineData(@"|DataDirectory|attach.mdf", + @"AttachDbFilename=|DataDirectory|attach.mdf", + @"C:\test\")] + [InlineData(@" |DataDirectory|attach.mdf", + "AttachDbFilename=\" |DataDirectory|attach.mdf\"", + @"C:\test\")] + [InlineData(@"|DataDirectory|attach.mdf ", + "AttachDbFilename=\"|DataDirectory|attach.mdf \"", + @"C:\test\")] + public void ConnectionStringBuilder_AttachDbFileName_DataDirectory(string value, string expected, string dataDirectory) + { + AppDomain.CurrentDomain.SetData("DataDirectory", dataDirectory); + + var builder = new SqlConnectionStringBuilder(); + builder.AttachDBFilename = value; + Assert.Equal(expected, builder.ConnectionString); + } + private void ExecuteConnectionStringTests(string connectionString) { SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(connectionString); diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionTest.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionTest.cs index d2c761c96a..05035da6ba 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionTest.cs @@ -547,6 +547,95 @@ public void GetSchema_Connection_Closed() } } + [Theory] + [InlineData(@"AttachDbFileName=C:\test\attach.mdf", @"AttachDbFileName=C:\test\attach.mdf")] + [InlineData(@"AttachDbFileName=C:\test\attach.mdf;", @"AttachDbFileName=C:\test\attach.mdf;")] + public void ConnectionString_AttachDbFileName_Plain(string value, string expected) + { + SqlConnection cn = new SqlConnection(); + cn.ConnectionString = value; + Assert.Equal(expected, cn.ConnectionString); + } + + [Theory] + [PlatformSpecific(TestPlatforms.Windows)] + [InlineData(@"Data Source=.;AttachDbFileName=|DataDirectory|\attach.mdf", + @"Data Source=.;AttachDbFileName=|DataDirectory|\attach.mdf", + @"C:\test\")] + [InlineData(@"Data Source=.;AttachDbFileName=|DataDirectory|\attach.mdf", + @"Data Source=.;AttachDbFileName=|DataDirectory|\attach.mdf", + @"C:\test")] + [InlineData(@"Data Source=.;AttachDbFileName=|DataDirectory|attach.mdf", + @"Data Source=.;AttachDbFileName=|DataDirectory|attach.mdf", + @"C:\test")] + [InlineData(@"Data Source=.;AttachDbFileName=|DataDirectory|attach.mdf", + @"Data Source=.;AttachDbFileName=|DataDirectory|attach.mdf", + @"C:\test\")] + [InlineData(@"Data Source=.;AttachDbFileName=C:\test\attach.mdf;AttachDbFileName=|DataDirectory|attach.mdf", + @"Data Source=.;AttachDbFileName=C:\test\attach.mdf;AttachDbFileName=|DataDirectory|attach.mdf", + null)] + public void ConnectionString_AttachDbFileName_DataDirectory(string value, string expected, string dataDirectory) + { + AppDomain.CurrentDomain.SetData("DataDirectory", dataDirectory); + + SqlConnection cn = new SqlConnection(); + cn.ConnectionString = value; + Assert.Equal(expected, cn.ConnectionString); + } + + [Fact] + [PlatformSpecific(TestPlatforms.Linux)] + public void ConnectionString_AttachDbFileName_DataDirectory_NoLinuxRootFolder() + { + AppDomain.CurrentDomain.SetData("DataDirectory", @"C:\test\"); + + SqlConnection cn = new SqlConnection(); + Assert.Throws(() => cn.ConnectionString = @"Data Source=.;AttachDbFileName=|DataDirectory|\attach.mdf"); + } + + [Theory] + [InlineData(@"Data Source=.;AttachDbFileName=|DataDirectory|attach.mdf", @"..\test\")] + [InlineData(@"Data Source=(local);AttachDbFileName=|DataDirectory|attach.mdf", @"c:\temp\..\test")] + [InlineData(@"Data Source=.;AttachDbFileName=|DataDirectory|attach.mdf", @"c:\temp\..\test\")] + [InlineData(@"Data Source=Random12344321;AttachDbFileName=|DataDirectory|attach.mdf", @"C:\\test\\")] + [InlineData(@"Data Source=local;AttachDbFileName=|DataDirectory|attach.mdf", @"C:\\test\\")] + [InlineData(@"Data Source=..;AttachDbFileName=|DataDirectory|attach.mdf", @"C:\\test\\")] + public void ConnectionString_AttachDbFileName_DataDirectory_Fails(string value, string dataDirectory) + { + AppDomain.CurrentDomain.SetData("DataDirectory", dataDirectory); + + SqlConnection cn = new SqlConnection(); + Assert.Throws(() => cn.ConnectionString = value); + } + + [Theory] + [InlineData(@"Data Source=.;AttachDbFileName=|DataDirectory|attach.mdf", 1)] + [InlineData(@"Data Source=.;AttachDbFileName=|DataDirectory|attach.mdf", 1.5)] + public void ConnectionString_AttachDbFileName_DataDirectory_Throws(string value, object dataDirectory) + { + AppDomain.CurrentDomain.SetData("DataDirectory", dataDirectory); + + SqlConnection cn = new SqlConnection(); + Assert.Throws(() => cn.ConnectionString = value); + } + + [Fact] + public void ConnectionString_AttachDbFileName_DataDirectory_Long_Throws() + { + AppDomain.CurrentDomain.SetData("DataDirectory", @"C:\test\" + new string('x', 261)); + + SqlConnection cn = new SqlConnection(); + var exception = Record.Exception(() => cn.ConnectionString = @"Data Source=.;AttachDbFileName=|DataDirectory|attach.mdf"); + Assert.NotNull(exception); + } + + [Fact] + public void ConnectionString_AttachDbFileName_Long_Throws() + { + SqlConnection cn = new SqlConnection(); + Assert.Throws(() => cn.ConnectionString = @"Data Source=.;AttachDbFileName=C:\test\" + new string('x', 261)); + } + [Fact] public void ConnectionString_ConnectTimeout() {