Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A brave attempt to fix the most voted issue - support |DataDirectory| with .NET Core client #284

Merged
merged 11 commits into from
Nov 14, 2019
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;

namespace Microsoft.Data.Common
{
Expand Down Expand Up @@ -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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -215,6 +217,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);
Expand Down Expand Up @@ -328,15 +332,31 @@ 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);
}
else
{
ValidateValueLength(_attachDBFileName, TdsEnums.MAXLEN_ATTACHDBFILE, KEY.AttachDBFilename);
}

_typeSystemAssemblyVersion = constTypeSystemAsmVersion10;

if (true == _userInstance && !string.IsNullOrEmpty(_failoverPartner))
Expand Down Expand Up @@ -467,6 +487,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;
Expand Down Expand Up @@ -525,6 +546,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<string, string> GetParseSynonyms()
Expand Down Expand Up @@ -629,6 +696,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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/Microsoft.Data.SqlClient/netcore/src/Resources/SR.resx
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,9 @@
<data name="ADP_InternalConnectionError" xml:space="preserve">
<value>Internal DbConnection Error: {0}</value>
</data>
<data name="ADP_InvalidDataDirectory" xml:space="preserve">
<value>The DataDirectory substitute is not a string.</value>
</data>
<data name="ADP_InvalidEnumerationValue" xml:space="preserve">
<value>The {0} enumeration value, {1}, is invalid.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,45 @@ public void UnexpectedKeywordRetrieval()
Assert.Throws<ArgumentException>(() => 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);
Expand Down
Loading