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 f4f51f9b56..c75eb35a0e 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 @@ -207,6 +207,9 @@ internal bool IsDNSCachingBeforeRedirectSupported // Json Support Flag internal bool IsJsonSupportEnabled = false; + // User Agent Flag + internal bool IsUserAgentEnabled = true; + // Vector Support Flag internal bool IsVectorSupportEnabled = false; @@ -1430,6 +1433,10 @@ private void Login(ServerInfo server, TimeoutTimer timeout, string newPassword, requestedFeatures |= TdsEnums.FeatureExtension.JsonSupport; requestedFeatures |= TdsEnums.FeatureExtension.VectorSupport; + #if DEBUG + requestedFeatures |= TdsEnums.FeatureExtension.UserAgent; + #endif + _parser.TdsLogin(login, requestedFeatures, _recoverySessionData, _fedAuthFeatureExtensionData, encrypt); } diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs index e02711fe8d..79228a3449 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -8579,6 +8579,49 @@ internal int WriteVectorSupportFeatureRequest(bool write) return len; } + /// + /// Writes the User Agent feature request to the physical state object. + /// The request includes the feature ID, feature data length, version number and encoded JSON payload. + /// + /// Byte array of UTF-8 encoded JSON payload for User Agent + /// + /// If true, writes the feature request to the physical state object. + /// If false, just calculates the length. + /// + /// The length of the feature request in bytes. + /// + /// The feature request consists of: + /// - 1 byte for the feature ID. + /// - 4 bytes for the feature data length. + /// - 1 byte for the version number. + /// - N bytes for the JSON payload + /// + internal int WriteUserAgentFeatureRequest(byte[] userAgentJsonPayload, + bool write) + { + // 1byte (Feature Version) + size of UTF-8 encoded JSON payload + int dataLen = 1 + userAgentJsonPayload.Length; + // 1byte (Feature ID) + 4bytes (Feature Data Length) + 1byte (Version) + N(JSON payload size) + int totalLen = 1 + 4 + dataLen; + + if (write) + { + // Write Feature ID + _physicalStateObj.WriteByte(TdsEnums.FEATUREEXT_USERAGENT); + + // Feature Data Length + WriteInt(dataLen, _physicalStateObj); + + // Write Feature Version + _physicalStateObj.WriteByte(TdsEnums.SUPPORTED_USER_AGENT_VERSION); + + // Write encoded JSON payload + _physicalStateObj.WriteByteArray(userAgentJsonPayload, userAgentJsonPayload.Length, 0); + } + + return totalLen; + } + private void WriteLoginData(SqlLogin rec, TdsEnums.FeatureExtension requestedFeatures, SessionData recoverySessionData, 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 9da128fb6d..1ef8f728a8 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 @@ -211,6 +211,9 @@ internal bool IsDNSCachingBeforeRedirectSupported // Vector Support Flag internal bool IsVectorSupportEnabled = false; + // User Agent Flag + internal bool IsUserAgentEnabled = true; + // TCE flags internal byte _tceVersionSupported; @@ -1436,6 +1439,10 @@ private void Login(ServerInfo server, TimeoutTimer timeout, string newPassword, requestedFeatures |= TdsEnums.FeatureExtension.JsonSupport; requestedFeatures |= TdsEnums.FeatureExtension.VectorSupport; + #if DEBUG + requestedFeatures |= TdsEnums.FeatureExtension.UserAgent; + #endif + _parser.TdsLogin(login, requestedFeatures, _recoverySessionData, _fedAuthFeatureExtensionData, encrypt); } diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs index 0715bd8205..f871da2c6c 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -8762,6 +8762,49 @@ internal int WriteVectorSupportFeatureRequest(bool write) return len; } + /// + /// Writes the User Agent feature request to the physical state object. + /// The request includes the feature ID, feature data length, version number and encoded JSON payload. + /// + /// Byte array of UTF-8 encoded JSON payload for User Agent + /// + /// If true, writes the feature request to the physical state object. + /// If false, just calculates the length. + /// + /// The length of the feature request in bytes. + /// + /// The feature request consists of: + /// - 1 byte for the feature ID. + /// - 4 bytes for the feature data length. + /// - 1 byte for the version number. + /// - N bytes for the JSON payload + /// + internal int WriteUserAgentFeatureRequest(byte[] userAgentJsonPayload, + bool write) + { + // 1byte (Feature Version) + size of UTF-8 encoded JSON payload + int dataLen = 1 + userAgentJsonPayload.Length; + // 1byte (Feature ID) + 4bytes (Feature Data Length) + 1byte (Version) + N(JSON payload size) + int totalLen = 1 + 4 + dataLen; + + if (write) + { + // Write Feature ID + _physicalStateObj.WriteByte(TdsEnums.FEATUREEXT_USERAGENT); + + // Feature Data Length + WriteInt(dataLen, _physicalStateObj); + + // Write Feature Version + _physicalStateObj.WriteByte(TdsEnums.SUPPORTED_USER_AGENT_VERSION); + + // Write encoded JSON payload + _physicalStateObj.WriteByteArray(userAgentJsonPayload, userAgentJsonPayload.Length, 0); + } + + return totalLen; + } + private void WriteLoginData(SqlLogin rec, TdsEnums.FeatureExtension requestedFeatures, SessionData recoverySessionData, 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 630633a6b4..cf8ae33606 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs @@ -241,6 +241,8 @@ public enum EnvChangeType : byte public const byte FEATUREEXT_SQLDNSCACHING = 0x0B; 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; [Flags] public enum FeatureExtension : uint @@ -255,7 +257,8 @@ public enum FeatureExtension : uint UTF8Support = 1 << (TdsEnums.FEATUREEXT_UTF8SUPPORT - 1), SQLDNSCaching = 1 << (TdsEnums.FEATUREEXT_SQLDNSCACHING - 1), JsonSupport = 1 << (TdsEnums.FEATUREEXT_JSONSUPPORT - 1), - VectorSupport = 1 << (TdsEnums.FEATUREEXT_VECTORSUPPORT - 1) + VectorSupport = 1 << (TdsEnums.FEATUREEXT_VECTORSUPPORT - 1), + UserAgent = 1 << (TdsEnums.FEATUREEXT_USERAGENT - 1) } public const uint UTF8_IN_TDSCOLLATION = 0x4000000; @@ -985,6 +988,9 @@ internal enum FedAuthInfoId : byte internal const byte MAX_SUPPORTED_VECTOR_VERSION = 0x01; internal const int VECTOR_HEADER_SIZE = 8; + // User Agent constants + internal const byte SUPPORTED_USER_AGENT_VERSION = 0x01; + // TCE Related constants internal const byte MAX_SUPPORTED_TCE_VERSION = 0x03; // max version internal const byte MIN_TCE_VERSION_WITH_ENCLAVE_SUPPORT = 0x02; // min version with enclave support diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/TdsParserInternalsTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/TdsParserInternalsTest.cs new file mode 100644 index 0000000000..f0b3729d6f --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/TdsParserInternalsTest.cs @@ -0,0 +1,107 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. +using System; +using System.Linq; +using System.Reflection; +using System.Text; +using Xunit; + +#nullable enable + +namespace Microsoft.Data.SqlClient.UnitTests +{ + public class TdsParserInternalsTest + { + private readonly TdsParser _parser = new(false, false); + + // TODO(ADO-37888): Avoid reflection by exposing a way for tests to intercept outbound TDS packets. + // Helper function to extract private _physicalStateObj fields raw buffer and no. of bytes written so far + private static (byte[] buffer, int count) ExtractOutputBuffer(TdsParser parser) + { + FieldInfo stateField = typeof(TdsParser) + .GetField("_physicalStateObj", BindingFlags.Instance | BindingFlags.NonPublic) + ?? throw new InvalidOperationException("_physicalStateObj not found"); + + object stateObj = stateField.GetValue(parser) + ?? throw new InvalidOperationException("physical state object is null"); + + Type stateType = stateObj.GetType(); + + FieldInfo buffField = stateType + .GetField("_outBuff", BindingFlags.Instance | BindingFlags.NonPublic) + ?? throw new InvalidOperationException("_outBuff not found"); + + byte[] buffer = (byte[])buffField.GetValue(stateObj)!; + + FieldInfo usedField = stateType + .GetField("_outBytesUsed", BindingFlags.Instance | BindingFlags.NonPublic) + ?? throw new InvalidOperationException("_outBytesUsed not found"); + + int count = (int)usedField.GetValue(stateObj)!; + + return (buffer, count); + } + + [Fact] + public void WriteUserAgentFeatureRequest_WriteFalse_LengthOnlyReturn() + { + byte[] payload = Encoding.UTF8.GetBytes("{\"kel\":\"sier\"}"); + var (_, countBefore) = ExtractOutputBuffer(_parser); + + int lengthOnly = _parser.WriteUserAgentFeatureRequest(payload, write: false); + + var (_, countAfter) = ExtractOutputBuffer(_parser); + + // assert: total = 1 (feat-ID) + 4 (len field) + [1 (version) + payload.Length] + int expectedDataLen = 1 + payload.Length; + int expectedTotalLen = 1 + 4 + expectedDataLen; + Assert.Equal(expectedTotalLen, lengthOnly); + + // assert: no bytes were written when write == false + Assert.Equal(countBefore, countAfter); + } + + [Fact] + public void WriteUserAgentFeatureRequest_WriteTrue_AppendsOnlyExtensionBytes() + { + byte[] payload = Encoding.UTF8.GetBytes("{\"kel\":\"sier\"}"); + var (bufferBefore, countBefore) = ExtractOutputBuffer(_parser); + + int returnedLength = _parser.WriteUserAgentFeatureRequest(payload, write: true); + + var (bufferAfter, countAfter) = ExtractOutputBuffer(_parser); + + // We expect both of these to be the same object + Assert.Same(bufferBefore, bufferAfter); + int appended = countAfter - countBefore; + Assert.Equal(returnedLength, appended); + + int start = countBefore; + + Assert.Equal( + TdsEnums.FEATUREEXT_USERAGENT, + bufferAfter[start]); + + int dataLenFromStream = BitConverter.ToInt32(bufferAfter, start + 1); + int expectedDataLen = 1 + payload.Length; + Assert.Equal(expectedDataLen, dataLenFromStream); + + Assert.Equal( + TdsEnums.SUPPORTED_USER_AGENT_VERSION, + bufferAfter[start + 5]); + + // slice into the existing buffer + ReadOnlySpan writtenSpan = new ReadOnlySpan( + bufferAfter, + start + 6, + appended - 6); + + Assert.True( + writtenSpan.SequenceEqual(payload), + "Payload bytes did not match"); + + } + + } +}