Skip to content

Add Feature Extension for User Agent #3451

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

Merged
merged 8 commits into from
Jul 8, 2025
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8579,6 +8579,49 @@ internal int WriteVectorSupportFeatureRequest(bool write)
return len;
}

/// <summary>
/// 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.
/// </summary>
/// <param name="userAgentJsonPayload"> Byte array of UTF-8 encoded JSON payload for User Agent</param>
/// <param name="write">
/// If true, writes the feature request to the physical state object.
/// If false, just calculates the length.
/// </param>
/// <returns>The length of the feature request in bytes.</returns>
/// <remarks>
/// 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
/// </remarks>
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8762,6 +8762,49 @@ internal int WriteVectorSupportFeatureRequest(bool write)
return len;
}

/// <summary>
/// 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.
/// </summary>
/// <param name="userAgentJsonPayload"> Byte array of UTF-8 encoded JSON payload for User Agent</param>
/// <param name="write">
/// If true, writes the feature request to the physical state object.
/// If false, just calculates the length.
/// </param>
/// <returns>The length of the feature request in bytes.</returns>
/// <remarks>
/// 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
/// </remarks>
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// 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
{
// Selects and returns the first non-public instance constructor for TdsParser
private static TdsParser CreateParserInstance()
{
Type parserType = typeof(TdsParser);

ConstructorInfo ctor = parserType
.GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic)
.First();

// build default args for each parameter
object?[] ctorArgs = ctor.GetParameters()
.Select(p => p.ParameterType.IsValueType
? Activator.CreateInstance(p.ParameterType)
: null)
.ToArray();

return (TdsParser)ctor.Invoke(ctorArgs);
}

// 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 parser = CreateParserInstance();

int lengthOnly = parser.WriteUserAgentFeatureRequest(payload, write: false);

// 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);
}

[Fact]
public void WriteUserAgentFeatureRequest_WriteTrue_AppendsOnlyExtensionBytes()
{
byte[] payload = Encoding.UTF8.GetBytes("{\"kel\":\"sier\"}");
var parser = CreateParserInstance();

var (bufferBefore, countBefore) = ExtractOutputBuffer(parser);

int returnedLength = parser.WriteUserAgentFeatureRequest(payload, write: true);

var (bufferAfter, countAfter) = ExtractOutputBuffer(parser);

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]);

byte[] writtenPayload = bufferAfter
.Skip(start + 6)
.Take(payload.Length)
.ToArray();
Assert.Equal(payload, writtenPayload);

Assert.Equal(returnedLength, appended);
}

}
}
Loading