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
Next Next commit
A brave attempt to fix the most voted issue - support |DataDirectory|…
… with .NET Core client

Probably WIP!
  • Loading branch information
ErikEJ committed Oct 23, 2019
commit d712064ffd011b15f2e752049969ce158af1f056
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,12 @@ 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("The DataDirectory substitute is not a string.");
ErikEJ marked this conversation as resolved.
Show resolved Hide resolved
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), the use the already created options
ErikEJ marked this conversation as resolved.
Show resolved Hide resolved
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,87 @@ 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;
}
if (null == rootFolderPath)
{
rootFolderPath = "";
}
ErikEJ marked this conversation as resolved.
Show resolved Hide resolved

// We don't know if rootFolderpath ends with '\', and we don't know if the given name starts with onw
ErikEJ marked this conversation as resolved.
Show resolved Hide resolved
int fileNamePosition = DataDirectory.Length; // filename starts right after the '|datadirectory|' keyword
bool rootFolderEndsWith = (0 < rootFolderPath.Length) && rootFolderPath[rootFolderPath.Length - 1] == Path.DirectorySeparatorChar;
bool fileNameStartsWith = (fileNamePosition < value.Length) && value[fileNamePosition] == Path.DirectorySeparatorChar;

// replace |datadirectory| with root folder path
if (!rootFolderEndsWith && !fileNameStartsWith)
{
// need to insert '\'
fullPath = rootFolderPath + Path.DirectorySeparatorChar + value.Substring(fileNamePosition);
}
else if (rootFolderEndsWith && fileNameStartsWith)
{
// need to strip one out
fullPath = rootFolderPath + value.Substring(fileNamePosition + 1);
}
else
{
// simply concatenate the strings
fullPath = rootFolderPath + value.Substring(fileNamePosition);
}

// 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 @@ -215,6 +215,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 +330,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 +485,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 +544,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 +694,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))
saurabh500 marked this conversation as resolved.
Show resolved Hide resolved
{
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
ErikEJ marked this conversation as resolved.
Show resolved Hide resolved
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 = "." + System.Net.NetworkInformation.IPGlobalProperties.GetIPGlobalProperties().DomainName;
ErikEJ marked this conversation as resolved.
Show resolved Hide resolved
var hostName = System.Net.Dns.GetHostName();
ErikEJ marked this conversation as resolved.
Show resolved Hide resolved
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
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,67 @@ 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]
[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;
ErikEJ marked this conversation as resolved.
Show resolved Hide resolved
Assert.Equal(expected, cn.ConnectionString);
}

[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<ArgumentException>(() => 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<InvalidOperationException>(() => cn.ConnectionString = value);
}

[Fact]
public void ConnectionString_ConnectTimeout()
{
Expand Down