Skip to content

Commit 8eb9f32

Browse files
Add Feature Extension for User Agent (#3451)
* Add Feature Extension for User Agent * resolve conflicts * Add summary and review changes * Add unit tests for WriteUserAgentFeatureRequest * Update tests to avoid reflection
1 parent b69da75 commit 8eb9f32

File tree

6 files changed

+214
-1
lines changed

6 files changed

+214
-1
lines changed

src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,9 @@ internal bool IsDNSCachingBeforeRedirectSupported
207207
// Json Support Flag
208208
internal bool IsJsonSupportEnabled = false;
209209

210+
// User Agent Flag
211+
internal bool IsUserAgentEnabled = true;
212+
210213
// Vector Support Flag
211214
internal bool IsVectorSupportEnabled = false;
212215

@@ -1430,6 +1433,10 @@ private void Login(ServerInfo server, TimeoutTimer timeout, string newPassword,
14301433
requestedFeatures |= TdsEnums.FeatureExtension.JsonSupport;
14311434
requestedFeatures |= TdsEnums.FeatureExtension.VectorSupport;
14321435

1436+
#if DEBUG
1437+
requestedFeatures |= TdsEnums.FeatureExtension.UserAgent;
1438+
#endif
1439+
14331440
_parser.TdsLogin(login, requestedFeatures, _recoverySessionData, _fedAuthFeatureExtensionData, encrypt);
14341441
}
14351442

src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8579,6 +8579,49 @@ internal int WriteVectorSupportFeatureRequest(bool write)
85798579
return len;
85808580
}
85818581

8582+
/// <summary>
8583+
/// Writes the User Agent feature request to the physical state object.
8584+
/// The request includes the feature ID, feature data length, version number and encoded JSON payload.
8585+
/// </summary>
8586+
/// <param name="userAgentJsonPayload"> Byte array of UTF-8 encoded JSON payload for User Agent</param>
8587+
/// <param name="write">
8588+
/// If true, writes the feature request to the physical state object.
8589+
/// If false, just calculates the length.
8590+
/// </param>
8591+
/// <returns>The length of the feature request in bytes.</returns>
8592+
/// <remarks>
8593+
/// The feature request consists of:
8594+
/// - 1 byte for the feature ID.
8595+
/// - 4 bytes for the feature data length.
8596+
/// - 1 byte for the version number.
8597+
/// - N bytes for the JSON payload
8598+
/// </remarks>
8599+
internal int WriteUserAgentFeatureRequest(byte[] userAgentJsonPayload,
8600+
bool write)
8601+
{
8602+
// 1byte (Feature Version) + size of UTF-8 encoded JSON payload
8603+
int dataLen = 1 + userAgentJsonPayload.Length;
8604+
// 1byte (Feature ID) + 4bytes (Feature Data Length) + 1byte (Version) + N(JSON payload size)
8605+
int totalLen = 1 + 4 + dataLen;
8606+
8607+
if (write)
8608+
{
8609+
// Write Feature ID
8610+
_physicalStateObj.WriteByte(TdsEnums.FEATUREEXT_USERAGENT);
8611+
8612+
// Feature Data Length
8613+
WriteInt(dataLen, _physicalStateObj);
8614+
8615+
// Write Feature Version
8616+
_physicalStateObj.WriteByte(TdsEnums.SUPPORTED_USER_AGENT_VERSION);
8617+
8618+
// Write encoded JSON payload
8619+
_physicalStateObj.WriteByteArray(userAgentJsonPayload, userAgentJsonPayload.Length, 0);
8620+
}
8621+
8622+
return totalLen;
8623+
}
8624+
85828625
private void WriteLoginData(SqlLogin rec,
85838626
TdsEnums.FeatureExtension requestedFeatures,
85848627
SessionData recoverySessionData,

src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,9 @@ internal bool IsDNSCachingBeforeRedirectSupported
211211
// Vector Support Flag
212212
internal bool IsVectorSupportEnabled = false;
213213

214+
// User Agent Flag
215+
internal bool IsUserAgentEnabled = true;
216+
214217
// TCE flags
215218
internal byte _tceVersionSupported;
216219

@@ -1436,6 +1439,10 @@ private void Login(ServerInfo server, TimeoutTimer timeout, string newPassword,
14361439
requestedFeatures |= TdsEnums.FeatureExtension.JsonSupport;
14371440
requestedFeatures |= TdsEnums.FeatureExtension.VectorSupport;
14381441

1442+
#if DEBUG
1443+
requestedFeatures |= TdsEnums.FeatureExtension.UserAgent;
1444+
#endif
1445+
14391446
_parser.TdsLogin(login, requestedFeatures, _recoverySessionData, _fedAuthFeatureExtensionData, encrypt);
14401447
}
14411448

src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8762,6 +8762,49 @@ internal int WriteVectorSupportFeatureRequest(bool write)
87628762
return len;
87638763
}
87648764

8765+
/// <summary>
8766+
/// Writes the User Agent feature request to the physical state object.
8767+
/// The request includes the feature ID, feature data length, version number and encoded JSON payload.
8768+
/// </summary>
8769+
/// <param name="userAgentJsonPayload"> Byte array of UTF-8 encoded JSON payload for User Agent</param>
8770+
/// <param name="write">
8771+
/// If true, writes the feature request to the physical state object.
8772+
/// If false, just calculates the length.
8773+
/// </param>
8774+
/// <returns>The length of the feature request in bytes.</returns>
8775+
/// <remarks>
8776+
/// The feature request consists of:
8777+
/// - 1 byte for the feature ID.
8778+
/// - 4 bytes for the feature data length.
8779+
/// - 1 byte for the version number.
8780+
/// - N bytes for the JSON payload
8781+
/// </remarks>
8782+
internal int WriteUserAgentFeatureRequest(byte[] userAgentJsonPayload,
8783+
bool write)
8784+
{
8785+
// 1byte (Feature Version) + size of UTF-8 encoded JSON payload
8786+
int dataLen = 1 + userAgentJsonPayload.Length;
8787+
// 1byte (Feature ID) + 4bytes (Feature Data Length) + 1byte (Version) + N(JSON payload size)
8788+
int totalLen = 1 + 4 + dataLen;
8789+
8790+
if (write)
8791+
{
8792+
// Write Feature ID
8793+
_physicalStateObj.WriteByte(TdsEnums.FEATUREEXT_USERAGENT);
8794+
8795+
// Feature Data Length
8796+
WriteInt(dataLen, _physicalStateObj);
8797+
8798+
// Write Feature Version
8799+
_physicalStateObj.WriteByte(TdsEnums.SUPPORTED_USER_AGENT_VERSION);
8800+
8801+
// Write encoded JSON payload
8802+
_physicalStateObj.WriteByteArray(userAgentJsonPayload, userAgentJsonPayload.Length, 0);
8803+
}
8804+
8805+
return totalLen;
8806+
}
8807+
87658808
private void WriteLoginData(SqlLogin rec,
87668809
TdsEnums.FeatureExtension requestedFeatures,
87678810
SessionData recoverySessionData,

src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,8 @@ public enum EnvChangeType : byte
241241
public const byte FEATUREEXT_SQLDNSCACHING = 0x0B;
242242
public const byte FEATUREEXT_JSONSUPPORT = 0x0D;
243243
public const byte FEATUREEXT_VECTORSUPPORT = 0x0E;
244+
// TODO: re-verify if this byte competes with another feature
245+
public const byte FEATUREEXT_USERAGENT = 0x0F;
244246

245247
[Flags]
246248
public enum FeatureExtension : uint
@@ -255,7 +257,8 @@ public enum FeatureExtension : uint
255257
UTF8Support = 1 << (TdsEnums.FEATUREEXT_UTF8SUPPORT - 1),
256258
SQLDNSCaching = 1 << (TdsEnums.FEATUREEXT_SQLDNSCACHING - 1),
257259
JsonSupport = 1 << (TdsEnums.FEATUREEXT_JSONSUPPORT - 1),
258-
VectorSupport = 1 << (TdsEnums.FEATUREEXT_VECTORSUPPORT - 1)
260+
VectorSupport = 1 << (TdsEnums.FEATUREEXT_VECTORSUPPORT - 1),
261+
UserAgent = 1 << (TdsEnums.FEATUREEXT_USERAGENT - 1)
259262
}
260263

261264
public const uint UTF8_IN_TDSCOLLATION = 0x4000000;
@@ -985,6 +988,9 @@ internal enum FedAuthInfoId : byte
985988
internal const byte MAX_SUPPORTED_VECTOR_VERSION = 0x01;
986989
internal const int VECTOR_HEADER_SIZE = 8;
987990

991+
// User Agent constants
992+
internal const byte SUPPORTED_USER_AGENT_VERSION = 0x01;
993+
988994
// TCE Related constants
989995
internal const byte MAX_SUPPORTED_TCE_VERSION = 0x03; // max version
990996
internal const byte MIN_TCE_VERSION_WITH_ENCLAVE_SUPPORT = 0x02; // min version with enclave support
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
using System;
5+
using System.Linq;
6+
using System.Reflection;
7+
using System.Text;
8+
using Xunit;
9+
10+
#nullable enable
11+
12+
namespace Microsoft.Data.SqlClient.UnitTests
13+
{
14+
public class TdsParserInternalsTest
15+
{
16+
private readonly TdsParser _parser = new(false, false);
17+
18+
// TODO(ADO-37888): Avoid reflection by exposing a way for tests to intercept outbound TDS packets.
19+
// Helper function to extract private _physicalStateObj fields raw buffer and no. of bytes written so far
20+
private static (byte[] buffer, int count) ExtractOutputBuffer(TdsParser parser)
21+
{
22+
FieldInfo stateField = typeof(TdsParser)
23+
.GetField("_physicalStateObj", BindingFlags.Instance | BindingFlags.NonPublic)
24+
?? throw new InvalidOperationException("_physicalStateObj not found");
25+
26+
object stateObj = stateField.GetValue(parser)
27+
?? throw new InvalidOperationException("physical state object is null");
28+
29+
Type stateType = stateObj.GetType();
30+
31+
FieldInfo buffField = stateType
32+
.GetField("_outBuff", BindingFlags.Instance | BindingFlags.NonPublic)
33+
?? throw new InvalidOperationException("_outBuff not found");
34+
35+
byte[] buffer = (byte[])buffField.GetValue(stateObj)!;
36+
37+
FieldInfo usedField = stateType
38+
.GetField("_outBytesUsed", BindingFlags.Instance | BindingFlags.NonPublic)
39+
?? throw new InvalidOperationException("_outBytesUsed not found");
40+
41+
int count = (int)usedField.GetValue(stateObj)!;
42+
43+
return (buffer, count);
44+
}
45+
46+
[Fact]
47+
public void WriteUserAgentFeatureRequest_WriteFalse_LengthOnlyReturn()
48+
{
49+
byte[] payload = Encoding.UTF8.GetBytes("{\"kel\":\"sier\"}");
50+
var (_, countBefore) = ExtractOutputBuffer(_parser);
51+
52+
int lengthOnly = _parser.WriteUserAgentFeatureRequest(payload, write: false);
53+
54+
var (_, countAfter) = ExtractOutputBuffer(_parser);
55+
56+
// assert: total = 1 (feat-ID) + 4 (len field) + [1 (version) + payload.Length]
57+
int expectedDataLen = 1 + payload.Length;
58+
int expectedTotalLen = 1 + 4 + expectedDataLen;
59+
Assert.Equal(expectedTotalLen, lengthOnly);
60+
61+
// assert: no bytes were written when write == false
62+
Assert.Equal(countBefore, countAfter);
63+
}
64+
65+
[Fact]
66+
public void WriteUserAgentFeatureRequest_WriteTrue_AppendsOnlyExtensionBytes()
67+
{
68+
byte[] payload = Encoding.UTF8.GetBytes("{\"kel\":\"sier\"}");
69+
var (bufferBefore, countBefore) = ExtractOutputBuffer(_parser);
70+
71+
int returnedLength = _parser.WriteUserAgentFeatureRequest(payload, write: true);
72+
73+
var (bufferAfter, countAfter) = ExtractOutputBuffer(_parser);
74+
75+
// We expect both of these to be the same object
76+
Assert.Same(bufferBefore, bufferAfter);
77+
int appended = countAfter - countBefore;
78+
Assert.Equal(returnedLength, appended);
79+
80+
int start = countBefore;
81+
82+
Assert.Equal(
83+
TdsEnums.FEATUREEXT_USERAGENT,
84+
bufferAfter[start]);
85+
86+
int dataLenFromStream = BitConverter.ToInt32(bufferAfter, start + 1);
87+
int expectedDataLen = 1 + payload.Length;
88+
Assert.Equal(expectedDataLen, dataLenFromStream);
89+
90+
Assert.Equal(
91+
TdsEnums.SUPPORTED_USER_AGENT_VERSION,
92+
bufferAfter[start + 5]);
93+
94+
// slice into the existing buffer
95+
ReadOnlySpan<byte> writtenSpan = new ReadOnlySpan<byte>(
96+
bufferAfter,
97+
start + 6,
98+
appended - 6);
99+
100+
Assert.True(
101+
writtenSpan.SequenceEqual(payload),
102+
"Payload bytes did not match");
103+
104+
}
105+
106+
}
107+
}

0 commit comments

Comments
 (0)