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 b80174d9a0..19a678a767 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 @@ -210,9 +210,6 @@ internal bool IsDNSCachingBeforeRedirectSupported // Json Support Flag internal bool IsJsonSupportEnabled = false; - // User Agent Flag - internal bool IsUserAgentEnabled = true; - // Vector Support Flag internal bool IsVectorSupportEnabled = false; @@ -1412,10 +1409,7 @@ private void Login(ServerInfo server, TimeoutTimer timeout, string newPassword, requestedFeatures |= TdsEnums.FeatureExtension.SQLDNSCaching; requestedFeatures |= TdsEnums.FeatureExtension.JsonSupport; requestedFeatures |= TdsEnums.FeatureExtension.VectorSupport; - - #if DEBUG requestedFeatures |= TdsEnums.FeatureExtension.UserAgent; - #endif _parser.TdsLogin(login, requestedFeatures, _recoverySessionData, _fedAuthFeatureExtensionData, encrypt); } @@ -3019,6 +3013,12 @@ internal void OnFeatureExtAck(int featureId, byte[] data) IsVectorSupportEnabled = true; break; } + case TdsEnums.FEATUREEXT_USERAGENT: + { + // Unexpected ack from server but we ignore it entirely + SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Received feature extension acknowledgement for USERAGENTSUPPORT (ignored)", ObjectID); + break; + } default: { diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 8ebb341f44..ca86ef1863 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -214,9 +214,6 @@ internal bool IsDNSCachingBeforeRedirectSupported // Vector Support Flag internal bool IsVectorSupportEnabled = false; - // User Agent Flag - internal bool IsUserAgentEnabled = true; - // TCE flags internal byte _tceVersionSupported; @@ -3062,7 +3059,18 @@ internal void OnFeatureExtAck(int featureId, byte[] data) IsVectorSupportEnabled = true; break; } + case TdsEnums.FEATUREEXT_USERAGENT: + { + // TODO: Verify that the server sends an acknowledgment (Ack) + // using this log message in the future. + // This Ack from the server is unexpected and is ignored completely. + // According to the TDS specification, an Ack is not defined/expected + // for this scenario. We handle it only for completeness + // and to support testing. + SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Received feature extension acknowledgement for USERAGENTSUPPORT (ignored)", ObjectID); + break; + } default: { // Unknown feature ack diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs index 3113e19625..5101c51649 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs @@ -242,7 +242,7 @@ public enum EnvChangeType : byte public const byte FEATUREEXT_JSONSUPPORT = 0x0D; public const byte FEATUREEXT_VECTORSUPPORT = 0x0E; // TODO: re-verify if this byte competes with another feature - public const byte FEATUREEXT_USERAGENT = 0x0F; + public const byte FEATUREEXT_USERAGENT = 0x10; [Flags] public enum FeatureExtension : uint diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.SSPI.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.SSPI.cs index 6226f958a5..2881f70ad4 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.SSPI.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.SSPI.cs @@ -2,6 +2,7 @@ using System.Buffers; using System.Diagnostics; using System.Text; +using Microsoft.Data.SqlClient.UserAgent; using Microsoft.Data.SqlClient.Utilities; #nullable enable @@ -192,7 +193,14 @@ internal void TdsLogin( int feOffset = length; // calculate and reserve the required bytes for the featureEx - length = ApplyFeatureExData(requestedFeatures, recoverySessionData, fedAuthFeatureExtensionData, useFeatureExt, length); + length = ApplyFeatureExData( + requestedFeatures, + recoverySessionData, + fedAuthFeatureExtensionData, + UserAgentInfo.UserAgentCachedJsonPayload.ToArray(), + useFeatureExt, + length + ); WriteLoginData(rec, requestedFeatures, diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs index 35df415a7c..b1e933f6e2 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -29,6 +29,8 @@ using Microsoft.Data.SqlClient.DataClassification; using Microsoft.Data.SqlClient.LocalDb; using Microsoft.Data.SqlClient.Server; +using Microsoft.Data.SqlClient.UserAgent; + #if NETFRAMEWORK using Microsoft.Data.SqlTypes; #endif @@ -9233,7 +9235,15 @@ private void WriteLoginData(SqlLogin rec, } } - ApplyFeatureExData(requestedFeatures, recoverySessionData, fedAuthFeatureExtensionData, useFeatureExt, length, true); + ApplyFeatureExData( + requestedFeatures, + recoverySessionData, + fedAuthFeatureExtensionData, + UserAgentInfo.UserAgentCachedJsonPayload.ToArray(), + useFeatureExt, + length, + true + ); } catch (Exception e) { @@ -9252,6 +9262,7 @@ private void WriteLoginData(SqlLogin rec, private int ApplyFeatureExData(TdsEnums.FeatureExtension requestedFeatures, SessionData recoverySessionData, FederatedAuthenticationFeatureExtensionData fedAuthFeatureExtensionData, + byte[] userAgentJsonPayload, bool useFeatureExt, int length, bool write = false) @@ -9306,6 +9317,11 @@ private int ApplyFeatureExData(TdsEnums.FeatureExtension requestedFeatures, length += WriteVectorSupportFeatureRequest(write); } + if ((requestedFeatures & TdsEnums.FeatureExtension.UserAgent) != 0) + { + length += WriteUserAgentFeatureRequest(userAgentJsonPayload, write); + } + length++; // for terminator if (write) { diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionTests.cs index 3b038beb7d..0a1e83bb2e 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionTests.cs @@ -828,5 +828,86 @@ public void TestConnWithVectorFeatExtVersionNegotiation(bool expectedConnectionR Assert.Throws(() => connection.Open()); } } + + // Test to verify that the client sends a UserAgent version + // and driver behaves correctly even if server sent an Ack + [Theory] + [InlineData(false)] // We do not force test server to send an Ack + [InlineData(true)] // Server is forced to send an Ack + public void TestConnWithUserAgentFeatureExtension(bool forceAck) + { + using var server = new TdsServer(); + server.Start(); + + // Configure the server to support UserAgent version 0x01 + server.ServerSupportedUserAgentFeatureExtVersion = 0x01; + + // Opt in to forced ACK for UserAgentSupport (no negotiation) + server.EmitUserAgentFeatureExtAck = forceAck; + + bool loginFound = false; + + // Captured from LOGIN7 as parsed by the test server + byte observedVersion = 0; + byte[] observedJsonBytes = Array.Empty(); + + // Inspect what the client sends in the LOGIN7 packet + server.OnLogin7Validated = loginToken => + { + var token = loginToken.FeatureExt + .OfType() + .FirstOrDefault(t => t.FeatureID == TDSFeatureID.UserAgentSupport); + if (token == null) + { + return; + } + + var data = token.Data; + if (data == null || data.Length < 2) + { + return; + } + + observedVersion = data[0]; + observedJsonBytes = data.AsSpan(1).ToArray(); + loginFound = true; + }; + + // Connect to the test TDS server. + var connStr = new SqlConnectionStringBuilder + { + DataSource = $"localhost,{server.EndPoint.Port}", + Encrypt = SqlConnectionEncryptOption.Optional, + }.ConnectionString; + + // TODO: Confirm the server sent an Ack by reading log message from SqlInternalConnectionTds + using var connection = new SqlConnection(connStr); + connection.Open(); + + // Verify the connection itself succeeded + Assert.Equal(ConnectionState.Open, connection.State); + + // Verify client did offer UserAgent + Assert.True(loginFound, "Expected UserAgent extension in LOGIN7"); + Assert.Equal(0x1, observedVersion); + + // Note: Accessing UserAgentInfo via Reflection. + // We cannot use InternalsVisibleTo here because making internals visible to FunctionalTests + // causes the *.TestHarness.cs stubs to clash with the real internal types in SqlClient. + var asm = typeof(SqlConnection).Assembly; + var userAgentInfoType = + asm.GetTypes().FirstOrDefault(t => string.Equals(t.Name, "UserAgentInfo", StringComparison.Ordinal)) ?? + asm.GetTypes().FirstOrDefault(t => t.FullName?.EndsWith(".UserAgentInfo", StringComparison.Ordinal) == true); + + Assert.NotNull(userAgentInfoType); + + // Try to get the property + var prop = userAgentInfoType.GetProperty("UserAgentCachedJsonPayload", + BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + Assert.NotNull(prop); + + ReadOnlyMemory cachedPayload = (ReadOnlyMemory)prop.GetValue(null)!; + Assert.Equal(cachedPayload.ToArray(), observedJsonBytes.ToArray()); + } } } diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServer.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServer.cs index c17726fb8b..c3581753d2 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServer.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServer.cs @@ -50,11 +50,36 @@ public delegate void OnAuthenticationCompletedDelegate( /// public const byte DefaultSupportedVectorFeatureExtVersion = 0x01; + /// + /// Property for setting server version for vector feature extension. + /// + public bool EnableVectorFeatureExt { get; set; } = false; + + /// + /// Property for setting server version for vector feature extension. + /// + public byte ServerSupportedVectorFeatureExtVersion { get; set; } = DefaultSupportedVectorFeatureExtVersion; + + /// + /// Property for setting server version for user agent feature extension. + /// + public byte ServerSupportedUserAgentFeatureExtVersion { get; set; } = DefaultSupportedUserAgentFeatureExtVersion; + /// /// Client version for vector FeatureExtension. /// private byte _clientSupportedVectorFeatureExtVersion = 0; + /// + /// Server will ACK UserAgentSupport in the login response when this property is set to true. + /// + public bool EmitUserAgentFeatureExtAck { get; set; } = false; + + /// + /// Default feature extension version supported on the server for user agent. + /// + public const byte DefaultSupportedUserAgentFeatureExtVersion = 0x01; + /// /// Session counter /// @@ -107,16 +132,6 @@ public GenericTdsServer(T arguments, QueryEngine queryEngine) /// public int PreLoginCount => _preLoginCount; - /// - /// Property for setting server version for vector feature extension. - /// - public bool EnableVectorFeatureExt { get; set; } = false; - - /// - /// Property for setting server version for vector feature extension. - /// - public byte ServerSupportedVectorFeatureExtVersion { get; set; } = DefaultSupportedVectorFeatureExtVersion; - public OnAuthenticationCompletedDelegate OnAuthenticationResponseCompleted { private get; set; } public OnLogin7ValidatedDelegate OnLogin7Validated { private get; set; } @@ -691,6 +706,30 @@ protected virtual TDSMessageCollection OnAuthenticationCompleted(ITDSServerSessi responseMessage.Add(envChange); } + // If tests request it, force an ACK for UserAgentSupport with no negotiation + if (EmitUserAgentFeatureExtAck) + { + byte ackVersion = ServerSupportedUserAgentFeatureExtVersion; + + var data = new byte[] { ackVersion }; + var uaAck = new TDSFeatureExtAckGenericOption( + TDSFeatureID.UserAgentSupport, + (uint)data.Length, + data); + + // Reuse an existing FeatureExtAck token if present, otherwise add a new one + var featureExtAckToken = responseMessage.OfType().FirstOrDefault(); + if (featureExtAckToken == null) + { + featureExtAckToken = new TDSFeatureExtAckToken(uaAck); + responseMessage.Add(featureExtAckToken); + } + else + { + featureExtAckToken.Options.Add(uaAck); + } + } + // Create DONE token TDSDoneToken doneToken = new TDSDoneToken(TDSDoneTokenStatusType.Final); diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/TDSFeatureID.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/TDSFeatureID.cs index 6bb6fbc8d2..7681b72ac1 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/TDSFeatureID.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/TDSFeatureID.cs @@ -29,6 +29,11 @@ public enum TDSFeatureID : byte /// VectorSupport = 0x0E, + /// + /// User Agent Support + /// + UserAgentSupport = 0x10, + /// /// End of the list ///