From 27a879595fb2ce588cecb4cc0d549109c3b50ab7 Mon Sep 17 00:00:00 2001 From: Saransh Sharma Date: Thu, 26 Jun 2025 17:46:31 -0700 Subject: [PATCH 1/9] Add Feature Extension for User Agent --- .../SqlClient/SqlInternalConnectionTds.cs | 7 +++++ .../src/Microsoft/Data/SqlClient/TdsParser.cs | 26 +++++++++++++++++++ .../SqlClient/SqlInternalConnectionTds.cs | 7 +++++ .../src/Microsoft/Data/SqlClient/TdsParser.cs | 26 +++++++++++++++++++ .../src/Microsoft/Data/SqlClient/TdsEnums.cs | 7 ++++- 5 files changed, 72 insertions(+), 1 deletion(-) 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 0d1da114cc..6f2c6a2432 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; + // TCE flags internal byte _tceVersionSupported; @@ -1426,6 +1429,10 @@ private void Login(ServerInfo server, TimeoutTimer timeout, string newPassword, requestedFeatures |= TdsEnums.FeatureExtension.SQLDNSCaching; requestedFeatures |= TdsEnums.FeatureExtension.JsonSupport; + #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 5320d1e51f..64031e5c24 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 @@ -8529,6 +8529,32 @@ internal int WriteFedAuthFeatureRequest(FederatedAuthenticationFeatureExtensionD return len; } + internal int WriteUserAgentFeatureRequest(byte[] userAgentJsonPayload, + bool write /* if false just calculates the length */) + { + // 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 d460c61619..644122e7b3 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 @@ -208,6 +208,9 @@ internal bool IsDNSCachingBeforeRedirectSupported // Json Support Flag internal bool IsJsonSupportEnabled = false; + // User Agent Flag + internal bool IsUserAgentEnabled = true; + // TCE flags internal byte _tceVersionSupported; @@ -1432,6 +1435,10 @@ private void Login(ServerInfo server, TimeoutTimer timeout, string newPassword, requestedFeatures |= TdsEnums.FeatureExtension.SQLDNSCaching; requestedFeatures |= TdsEnums.FeatureExtension.JsonSupport; + #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 d602928be8..7f4207d17c 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 @@ -8712,6 +8712,32 @@ internal int WriteFedAuthFeatureRequest(FederatedAuthenticationFeatureExtensionD return len; } + internal int WriteUserAgentFeatureRequest(byte[] userAgentJsonPayload, + bool write /* if false just calculates the length */) + { + // 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 280469a5e0..c08b6c5ad0 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs @@ -240,6 +240,7 @@ public enum EnvChangeType : byte public const byte FEATUREEXT_UTF8SUPPORT = 0x0A; public const byte FEATUREEXT_SQLDNSCACHING = 0x0B; public const byte FEATUREEXT_JSONSUPPORT = 0x0D; + public const byte FEATUREEXT_USERAGENT = 0x0F; [Flags] public enum FeatureExtension : uint @@ -253,7 +254,8 @@ public enum FeatureExtension : uint DataClassification = 1 << (TdsEnums.FEATUREEXT_DATACLASSIFICATION - 1), UTF8Support = 1 << (TdsEnums.FEATUREEXT_UTF8SUPPORT - 1), SQLDNSCaching = 1 << (TdsEnums.FEATUREEXT_SQLDNSCACHING - 1), - JsonSupport = 1 << (TdsEnums.FEATUREEXT_JSONSUPPORT - 1) + JsonSupport = 1 << (TdsEnums.FEATUREEXT_JSONSUPPORT - 1), + UserAgent = 1 << (TdsEnums.FEATUREEXT_USERAGENT - 1) } public const uint UTF8_IN_TDSCOLLATION = 0x4000000; @@ -978,6 +980,9 @@ internal enum FedAuthInfoId : byte // JSON Support constants internal const byte MAX_SUPPORTED_JSON_VERSION = 0x01; + // 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 From a27be5dad15e7486e809073d62cfee429b8430a4 Mon Sep 17 00:00:00 2001 From: Saransh Sharma Date: Fri, 27 Jun 2025 13:24:30 -0700 Subject: [PATCH 2/9] resolve conflicts --- .../SqlClient/SqlInternalConnectionTds.cs | 4 +- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 53 +++++++++---------- .../SqlClient/SqlInternalConnectionTds.cs | 8 ++- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 44 +++++++-------- .../src/Microsoft/Data/SqlClient/TdsEnums.cs | 19 +------ 5 files changed, 54 insertions(+), 74 deletions(-) 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 ded323cd51..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,13 +207,11 @@ internal bool IsDNSCachingBeforeRedirectSupported // Json Support Flag internal bool IsJsonSupportEnabled = false; -<<<<<<< HEAD // User Agent Flag internal bool IsUserAgentEnabled = true; -======= + // Vector Support Flag internal bool IsVectorSupportEnabled = false; ->>>>>>> origin/main // TCE flags internal byte _tceVersionSupported; 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 64981fb978..00b47114b4 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 @@ -8549,32 +8549,6 @@ internal int WriteFedAuthFeatureRequest(FederatedAuthenticationFeatureExtensionD return len; } -<<<<<<< HEAD - internal int WriteUserAgentFeatureRequest(byte[] userAgentJsonPayload, - bool write /* if false just calculates the length */) - { - // 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; -======= /// /// Writes the Vector Support feature request to the physical state object. /// The request includes the feature ID, feature data length, and version number. @@ -8603,7 +8577,32 @@ internal int WriteVectorSupportFeatureRequest(bool write) } return len; ->>>>>>> origin/main + } + + internal int WriteUserAgentFeatureRequest(byte[] userAgentJsonPayload, + bool write /* if false just calculates the length */) + { + // 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, 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 33966e40f5..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 @@ -208,13 +208,11 @@ internal bool IsDNSCachingBeforeRedirectSupported // Json Support Flag internal bool IsJsonSupportEnabled = false; -<<<<<<< HEAD - // User Agent Flag - internal bool IsUserAgentEnabled = true; -======= // Vector Support Flag internal bool IsVectorSupportEnabled = false; ->>>>>>> origin/main + + // User Agent Flag + internal bool IsUserAgentEnabled = true; // TCE flags internal byte _tceVersionSupported; 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 9040e30cc1..76c6c880da 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 @@ -8732,15 +8732,6 @@ internal int WriteFedAuthFeatureRequest(FederatedAuthenticationFeatureExtensionD return len; } -<<<<<<< HEAD - internal int WriteUserAgentFeatureRequest(byte[] userAgentJsonPayload, - bool write /* if false just calculates the length */) - { - // 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; -======= /// /// Writes the Vector Support feature request to the physical state object. /// The request includes the feature ID, feature data length, and version number. @@ -8756,12 +8747,32 @@ internal int WriteUserAgentFeatureRequest(byte[] userAgentJsonPayload, internal int WriteVectorSupportFeatureRequest(bool write) { const int len = 6; ->>>>>>> origin/main if (write) { // Write Feature ID -<<<<<<< HEAD + _physicalStateObj.WriteByte(TdsEnums.FEATUREEXT_VECTORSUPPORT); + + // Feature Data Length + WriteInt(1, _physicalStateObj); + + _physicalStateObj.WriteByte(TdsEnums.MAX_SUPPORTED_VECTOR_VERSION); + } + + return len; + } + + internal int WriteUserAgentFeatureRequest(byte[] userAgentJsonPayload, + bool write /* if false just calculates the length */) + { + // 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 @@ -8775,17 +8786,6 @@ internal int WriteVectorSupportFeatureRequest(bool write) } return totalLen; -======= - _physicalStateObj.WriteByte(TdsEnums.FEATUREEXT_VECTORSUPPORT); - - // Feature Data Length - WriteInt(1, _physicalStateObj); - - _physicalStateObj.WriteByte(TdsEnums.MAX_SUPPORTED_VECTOR_VERSION); - } - - return len; ->>>>>>> origin/main } private void WriteLoginData(SqlLogin rec, 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 a2501948a5..43e33471ac 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs @@ -240,11 +240,8 @@ public enum EnvChangeType : byte public const byte FEATUREEXT_UTF8SUPPORT = 0x0A; public const byte FEATUREEXT_SQLDNSCACHING = 0x0B; public const byte FEATUREEXT_JSONSUPPORT = 0x0D; -<<<<<<< HEAD - public const byte FEATUREEXT_USERAGENT = 0x0F; -======= public const byte FEATUREEXT_VECTORSUPPORT = 0x0E; ->>>>>>> origin/main + public const byte FEATUREEXT_USERAGENT = 0x0F; [Flags] public enum FeatureExtension : uint @@ -259,11 +256,8 @@ public enum FeatureExtension : uint UTF8Support = 1 << (TdsEnums.FEATUREEXT_UTF8SUPPORT - 1), SQLDNSCaching = 1 << (TdsEnums.FEATUREEXT_SQLDNSCACHING - 1), JsonSupport = 1 << (TdsEnums.FEATUREEXT_JSONSUPPORT - 1), -<<<<<<< HEAD + VectorSupport = 1 << (TdsEnums.FEATUREEXT_VECTORSUPPORT - 1), UserAgent = 1 << (TdsEnums.FEATUREEXT_USERAGENT - 1) -======= - VectorSupport = 1 << (TdsEnums.FEATUREEXT_VECTORSUPPORT - 1) ->>>>>>> origin/main } public const uint UTF8_IN_TDSCOLLATION = 0x4000000; @@ -989,15 +983,6 @@ internal enum FedAuthInfoId : byte // JSON Support constants internal const byte MAX_SUPPORTED_JSON_VERSION = 0x01; -<<<<<<< HEAD - // User Agent constants - internal const byte SUPPORTED_USER_AGENT_VERSION = 0x01; -======= - // Vector Support constants - internal const byte MAX_SUPPORTED_VECTOR_VERSION = 0x01; - internal const int VECTOR_HEADER_SIZE = 8; ->>>>>>> origin/main - // 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 From 9509553de42b69dee12f686e95781224c2c647b1 Mon Sep 17 00:00:00 2001 From: Saransh Sharma Date: Fri, 27 Jun 2025 14:24:31 -0700 Subject: [PATCH 3/9] Add summary and review changes --- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 21 +++++++++++++++++-- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 19 ++++++++++++++++- .../src/Microsoft/Data/SqlClient/TdsEnums.cs | 8 +++++++ 3 files changed, 45 insertions(+), 3 deletions(-) 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 00b47114b4..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,13 +8579,30 @@ 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 /* if false just calculates the length */) + 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; + int totalLen = 1 + 4 + dataLen; if (write) { 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 76c6c880da..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,8 +8762,25 @@ 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 /* if false just calculates the length */) + bool write) { // 1byte (Feature Version) + size of UTF-8 encoded JSON payload int dataLen = 1 + userAgentJsonPayload.Length; 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 43e33471ac..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,7 @@ 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] @@ -983,6 +984,13 @@ internal enum FedAuthInfoId : byte // JSON Support constants internal const byte MAX_SUPPORTED_JSON_VERSION = 0x01; + // Vector Support constants + 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 From 679baee601141949082d078aba4d424cd3d4ba5e Mon Sep 17 00:00:00 2001 From: Saransh Sharma Date: Tue, 1 Jul 2025 16:12:19 -0700 Subject: [PATCH 4/9] Adding functional test for User Agent --- .../SqlClient/SqlInternalConnectionTds.cs | 6 +- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 13 ++++- .../SqlClient/SqlInternalConnectionTds.cs | 24 ++++++-- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 11 +++- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 4 +- .../SqlConnectionBasicTests.cs | 57 +++++++++++++++++++ .../TDS/TDS.EndPoint/ITDSServerSession.cs | 6 ++ .../tools/TDS/TDS.Servers/GenericTDSServer.cs | 51 +++++++++++++++++ .../TDS.Servers/GenericTDSServerSession.cs | 5 ++ .../tests/tools/TDS/TDS/TDSFeatureID.cs | 5 ++ 10 files changed, 168 insertions(+), 14 deletions(-) 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 c75eb35a0e..fd345599f3 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 @@ -208,7 +208,7 @@ internal bool IsDNSCachingBeforeRedirectSupported internal bool IsJsonSupportEnabled = false; // User Agent Flag - internal bool IsUserAgentEnabled = true; + internal bool IsUserAgentSupportEnabled = true; // Vector Support Flag internal bool IsVectorSupportEnabled = false; @@ -1432,10 +1432,8 @@ 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); } 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 79228a3449..d0bbb32346 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 @@ -8878,8 +8878,9 @@ private void WriteLoginData(SqlLogin rec, _physicalStateObj.WriteByteArray(encryptedChangePassword, encryptedChangePasswordLengthInBytes, 0); } } - - ApplyFeatureExData(requestedFeatures, recoverySessionData, fedAuthFeatureExtensionData, useFeatureExt, length, true); + // TODO: User Agent Json Payload will go here + byte[] emptyBytes = new byte[0]; + ApplyFeatureExData(requestedFeatures, recoverySessionData, fedAuthFeatureExtensionData, emptyBytes, useFeatureExt, length, true); } catch (Exception e) { @@ -8898,12 +8899,13 @@ private void WriteLoginData(SqlLogin rec, private int ApplyFeatureExData(TdsEnums.FeatureExtension requestedFeatures, SessionData recoverySessionData, FederatedAuthenticationFeatureExtensionData fedAuthFeatureExtensionData, + byte[] userAgentJsonPayload, bool useFeatureExt, int length, bool write = false) { if (useFeatureExt) - { + { if ((requestedFeatures & TdsEnums.FeatureExtension.SessionRecovery) != 0) { length += WriteSessionRecoveryFeatureRequest(recoverySessionData, write); @@ -8950,6 +8952,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/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 1ef8f728a8..2cdf32f17a 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 @@ -212,7 +212,7 @@ internal bool IsDNSCachingBeforeRedirectSupported internal bool IsVectorSupportEnabled = false; // User Agent Flag - internal bool IsUserAgentEnabled = true; + internal bool IsUserAgentSupportEnabled = true; // TCE flags internal byte _tceVersionSupported; @@ -1438,10 +1438,8 @@ 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); } @@ -3072,6 +3070,24 @@ internal void OnFeatureExtAck(int featureId, byte[] data) break; } + case TdsEnums.FEATUREEXT_USERAGENT: + { + SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Received feature extension acknowledgement for USERAGENT", ObjectID); + if (data.Length != 1) + { + SqlClientEventSource.Log.TryTraceEvent(" {0}, Unknown token for USERAGENT", ObjectID); + throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream); + } + byte userAgentSupportVersion = data[0]; + if (userAgentSupportVersion == 0 || userAgentSupportVersion > TdsEnums.SUPPORTED_USER_AGENT_VERSION) + { + SqlClientEventSource.Log.TryTraceEvent(" {0}, Invalid version number {1} for USERAGENT, Max supported version is {2}", ObjectID, userAgentSupportVersion, TdsEnums.SUPPORTED_USER_AGENT_VERSION); + throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream); + } + IsUserAgentSupportEnabled = true; + break; + } + default: { // Unknown feature ack 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 f871da2c6c..f4accc52cb 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 @@ -9068,8 +9068,9 @@ private void WriteLoginData(SqlLogin rec, _physicalStateObj.WriteByteArray(encryptedChangePassword, encryptedChangePasswordLengthInBytes, 0); } } - - ApplyFeatureExData(requestedFeatures, recoverySessionData, fedAuthFeatureExtensionData, useFeatureExt, length, true); + // TODO: User Agent Json Payload will go here + byte[] emptyBytes = new byte[0]; + ApplyFeatureExData(requestedFeatures, recoverySessionData, fedAuthFeatureExtensionData, emptyBytes, useFeatureExt, length, true); } catch (Exception e) { @@ -9088,6 +9089,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) @@ -9142,6 +9144,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/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs index 7e0fd18503..b7cbfaf1ae 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -191,8 +191,10 @@ internal void TdsLogin( } int feOffset = length; + // TODO: User Agent Json Payload will go here + byte[] emptyBytes = new byte[0]; // calculate and reserve the required bytes for the featureEx - length = ApplyFeatureExData(requestedFeatures, recoverySessionData, fedAuthFeatureExtensionData, useFeatureExt, length); + length = ApplyFeatureExData(requestedFeatures, recoverySessionData, fedAuthFeatureExtensionData,emptyBytes, useFeatureExt, length); WriteLoginData(rec, requestedFeatures, diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs index 616a8fec6f..89901cd168 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionBasicTests.cs @@ -620,5 +620,62 @@ public void TestConnWithVectorFeatExtVersionNegotiation(bool expectedConnectionR Assert.Throws(() => connection.Open()); } } + + // Test to verify client sends a UserAgent version + // We do not receive any Ack for it from the server + [Fact] + public void TestConnWithUnackedUserAgentFeatureExtension() + { + using var server = TestTdsServer.StartTestServer(); + + // Configure the server to support UserAgent version 0x01 + server.ServerSupportedUserAgentFeatureExtVersion = 0x01; + server.EnableUserAgentFeatureExt = true; + // By design its response logic never emits an ACK + bool loginFound = false; + bool responseFound = false; + + // 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) + { + Assert.Equal((byte)TDSFeatureID.UserAgentSupport, (byte)token.FeatureID); + Assert.Equal(0x1, token.Data[0]); + loginFound = true; + } + }; + + // Inspect whether the server ever sends back an ACK + server.OnAuthenticationResponseCompleted = response => + { + var ack = response + .OfType() + .SelectMany(t => t.Options) + .OfType() + .FirstOrDefault(o => o.FeatureID == TDSFeatureID.UserAgentSupport); + if (ack != null) + { + responseFound = true; + } + }; + + // Open the connection (this triggers the LOGIN7 exchange) + using var connection = new SqlConnection(server.ConnectionString); + connection.Open(); + + // Verify client did offer UserAgent + Assert.True(loginFound, "Expected UserAgent extension in LOGIN7"); + + // Verify server never acknowledged it + Assert.False(responseFound, "Server should not acknowledge UserAgent"); + + // Verify the connection itself succeeded + Assert.Equal(ConnectionState.Open, connection.State); + } + } } diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.EndPoint/ITDSServerSession.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.EndPoint/ITDSServerSession.cs index 9b5b7804b4..8587f4b7c1 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.EndPoint/ITDSServerSession.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.EndPoint/ITDSServerSession.cs @@ -93,5 +93,11 @@ public interface ITDSServerSession /// Indicates whether the client supports Vector column type /// bool IsVectorSupportEnabled { get; set; } + + /// + /// Indicates whether the client supports Vector column type + /// + bool IsUserAgentSupportEnabled { get; set; } + } } 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 ac04fd2f57..c81d100225 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 @@ -49,16 +49,32 @@ public delegate void OnAuthenticationCompletedDelegate( /// public const byte DefaultSupportedVectorFeatureExtVersion = 0x01; + /// + /// Default feature extension version supported on the server for user agent. + /// + public const byte DefaultSupportedUserAgentFeatureExtVersion = 0x01; + /// /// Property for setting server version for vector feature extension. /// public bool EnableVectorFeatureExt { get; set; } = false; + /// + /// Property for setting server version for user agent feature extension. + /// + public bool EnableUserAgentFeatureExt { get; set; } = true; + /// /// 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. /// @@ -287,6 +303,15 @@ public virtual TDSMessageCollection OnLogin7Request(ITDSServerSession session, T } break; } + case TDSFeatureID.UserAgentSupport: + { + if (EnableUserAgentFeatureExt) + { + // Enable User Agent Support + session.IsUserAgentSupportEnabled = true; + } + break; + } default: { @@ -653,6 +678,32 @@ protected virtual TDSMessageCollection OnAuthenticationCompleted(ITDSServerSessi featureExtAckToken.Options.Add(vectorSupportOption); } } + // Note: there should be case + //// Check if UserAgent is supported + //if (session.IsUserAgentSupportEnabled) + //{ + // // Create ack data (1 byte: Version number) + // byte[] data = new byte[1]; + // data[0] = ServerSupportedUserAgentFeatureExtVersion; + + // // Create vector support as a generic feature extension option + // TDSFeatureExtAckGenericOption userAgentSupportOption = new TDSFeatureExtAckGenericOption(TDSFeatureID.UserAgentSupport, (uint)data.Length, data); + + // // Look for feature extension token + // TDSFeatureExtAckToken featureExtAckToken = (TDSFeatureExtAckToken)responseMessage.Where(t => t is TDSFeatureExtAckToken).FirstOrDefault(); + + // if (featureExtAckToken == null) + // { + // // Create feature extension ack token + // featureExtAckToken = new TDSFeatureExtAckToken(userAgentSupportOption); + // responseMessage.Add(featureExtAckToken); + // } + // else + // { + // // Update the existing token + // featureExtAckToken.Options.Add(userAgentSupportOption); + // } + //} // Create DONE token TDSDoneToken doneToken = new TDSDoneToken(TDSDoneTokenStatusType.Final); diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTDSServerSession.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTDSServerSession.cs index e9e65d5f8f..986b27a4dd 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTDSServerSession.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTDSServerSession.cs @@ -124,6 +124,11 @@ public class GenericTDSServerSession : ITDSServerSession /// public bool IsVectorSupportEnabled { get; set; } + /// + /// Indicates whether this session supports User Agent Feature Extension + /// + public bool IsUserAgentSupportEnabled { get; set; } + #region Session Options /// 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..258ad7e1f3 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 = 0x0F, + /// /// End of the list /// From 5fc8f705924b40fb1a4a2974a2bf018993478f22 Mon Sep 17 00:00:00 2001 From: Saransh Sharma Date: Wed, 16 Jul 2025 16:47:59 -0700 Subject: [PATCH 5/9] Add UserAgent Json payload --- .../Microsoft/Data/SqlClient/UserAgentInfo.cs | 387 ++++++++++++++++++ .../Data/SqlClient/UserAgentInfoDto.cs | 44 ++ .../Microsoft.Data.SqlClient.UnitTests.csproj | 3 + .../Data/SqlClient/UserAgentInfoTests.cs | 120 ++++++ 4 files changed, 554 insertions(+) create mode 100644 src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgentInfo.cs create mode 100644 src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgentInfoDto.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/UserAgentInfoTests.cs diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgentInfo.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgentInfo.cs new file mode 100644 index 0000000000..be91e47dfe --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgentInfo.cs @@ -0,0 +1,387 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using Microsoft.Data.Common; + +#if WINDOWS +using System.Management; +#endif + +namespace Microsoft.Data.SqlClient +{ + /// + /// Gathers driver + environment info, enforces size constraints, + /// and serializes into a UTF-8 JSON payload. + /// + public static class UserAgentInfo + { + public const int DriverNameMaxChars = 16; + public const int VersionMaxChars = 16; + public const int OsTypeMaxChars = 16; + public const int OsDetailsMaxChars = 128; + public const int ArchMaxChars = 16; + public const int RuntimeMaxChars = 128; + public const int JsonPayloadMaxBytesSpec = 2047; + public const int UserAgentPayloadMaxBytes = 10000; + + private const string DefaultJsonValue = "Unknown"; + private const string DefaultDriverName = "MS-MDS"; + + // JSON Payload for UserAgent + private static readonly string driverName; + private static readonly string version; + private static readonly string osType; + private static readonly string osDetails; + private static readonly string architecture; + private static readonly string runtime; + private static readonly byte[] _cachedPayload; + + private enum OsType + { + Windows, + Linux, + macOS, + FreeBSD, + Android, + Unknown + } + + // P/Invoke signature for glibc detection + [DllImport("libc", EntryPoint = "gnu_get_libc_version", CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr gnu_get_libc_version(); + + static UserAgentInfo() + { + /// Note: We serialize 6 fields in total: + // - 4 fields with up to 16 characters each + // - 2 fields with up to 128 characters each + // + // For estimating **on-the-wire UTF-8 size** of the serialized JSON: + // 1) For the 4 fields of 16 characters: + // - In worst case (all characters require escaping in JSON, e.g., quotes, backslashes, control chars), + // each character may expand to 2–6 bytes in the JSON string (e.g., \" = 2 bytes, \uXXXX = 6 bytes) + // - Assuming full escape with \uXXXX form (6 bytes per char): 4 × 16 × 6 = 384 bytes (extreme worst case) + // - For unescaped high-plane Unicode (e.g., emojis), UTF-8 uses up to 4 bytes per character: + // 4 × 16 × 4 = 256 bytes (UTF-8 max) + // + // Conservative max estimate for these fields = **384 bytes** + // + // 2) For the 2 fields of 128 characters: + // - Worst-case with \uXXXX escape sequences: 2 × 128 × 6 = 1,536 bytes + // - Worst-case with high Unicode: 2 × 128 × 4 = 1,024 bytes + // + // Conservative max estimate for these fields = **1,536 bytes** + // + // Combined worst-case for value content = 384 + 1536 = **1,920 bytes** + // + // 3) The rest of the serialized JSON payload (object braces, field names, quotes, colons, commas) is fixed. + // Based on measurements, it typically adds to about **81 bytes**. + // + // Final worst-case estimate for total payload on the wire (UTF-8 encoded): + // 1,920 + 81 = **2,001 bytes** + // + // This is still below our spec limit of 2,047 bytes. + // + // TDS Prelogin7 packets support up to 65,535 bytes (including headers), but many server versions impose + // stricter limits for prelogin payloads. + // + // As a safety measure: + // - If the serialized payload exceeds **10 KB**, we fallback to transmitting only essential fields: + // 'driver', 'version', and 'os.type' + // - If the payload exceeds 2,047 bytes but remains within sensible limits, we still send it, but note that + // some servers may silently drop or reject such packets — behavior we may use for future probing or diagnostics. + + driverName = TruncateOrDefault(DefaultDriverName, DriverNameMaxChars); + version = TruncateOrDefault(ADP.GetAssemblyVersion.ToString(), VersionMaxChars); + var osVal = DetectOsType(); + osType = TruncateOrDefault(osVal.ToString(), OsTypeMaxChars); + osDetails = TruncateOrDefault(DetectOsDetails(osVal), OsDetailsMaxChars); + architecture = TruncateOrDefault(DetectArchitecture(), ArchMaxChars); + runtime = TruncateOrDefault(DetectRuntime(), RuntimeMaxChars); + + // Instantiate DTO before serializing + var dto = new UserAgentInfoDto + { + Driver = driverName, + Version = version, + OS = new UserAgentInfoDto.OsInfo + { + Type = osType, + Details = osDetails + }, + Arch = architecture, + Runtime = runtime, + + }; + + // Check/Adjust payload before caching it + _cachedPayload = AdjustJsonPayloadSize(dto); + } + + /// + /// This function returns the appropriately sized json payload + /// We check the size of encoded json payload, if it is within limits we return the dto to be cached + /// other wise we drop some fields to reduce the size of the payload. + /// + /// Data Transfer Object for the json payload + /// Serialized UTF-8 encoded json payload version of DTO within size limit + private static byte[] AdjustJsonPayloadSize(UserAgentInfoDto dto) + { + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = null, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = false + }; + byte[] payload = JsonSerializer.SerializeToUtf8Bytes(dto, options); + + // Note: server will likely reject payloads larger than 2047 bytes + // Try if the payload fits the max allowed bytes + if (payload.Length <= JsonPayloadMaxBytesSpec) + { + return payload; + } + if (payload.Length > UserAgentPayloadMaxBytes) + { + // If the payload is over 10KB, we only send the bare minimum fields + dto.OS.Details = null; // drop OS.Details + dto.Runtime = null; // drop Runtime + dto.Arch = null; // drop Arch + payload = JsonSerializer.SerializeToUtf8Bytes(dto, options); + } +#if DEBUG + Debug.Assert(payload.Length <= JsonPayloadMaxBytesSpec, + $"UserAgent payload is {payload.Length} bytes (spec max {JsonPayloadMaxBytesSpec})."); +#endif + return payload; + + } + + /// + /// Truncates a string to the specified maximum length or returns a default value if input is null or empty. + /// + /// The string value to truncate + /// Maximum number of characters allowed + /// Truncated string or default value if input is invalid + private static string TruncateOrDefault(string jsonStringVal, int maxChars) + { + try + { + if (string.IsNullOrEmpty(jsonStringVal)) + { + return DefaultJsonValue; + } + + if (jsonStringVal.Length <= maxChars) + { + return jsonStringVal; + } + + return jsonStringVal.Substring(0, maxChars); + } + catch + { + // Silently consume all exceptions + return DefaultJsonValue; + } + } + + /// + /// Detects the OS platform and returns the matching OsType enum. + /// + private static OsType DetectOsType() + { + try + { + // first we try with built-in checks (Android and FreeBSD also report Linux so they are checked first) +#if NET6_0_OR_GREATER + if (OperatingSystem.IsAndroid()) return OsType.Android; + if (OperatingSystem.IsFreeBSD()) return OsType.FreeBSD; + if (OperatingSystem.IsWindows()) return OsType.Windows; + if (OperatingSystem.IsLinux()) return OsType.Linux; + if (OperatingSystem.IsMacOS()) return OsType.macOS; +#endif + // second we fallback to OSplatform checks + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return OsType.Windows; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + return OsType.Linux; + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + return OsType.macOS; + + // final fallback is inspecting OSdecription + var desc = RuntimeInformation.OSDescription?.ToLowerInvariant() ?? ""; + if (desc.Contains("android")) + return OsType.Android; + if (desc.Contains("freebsd")) + return OsType.FreeBSD; + if (desc.Contains("windows")) + return OsType.Windows; + if (desc.Contains("linux")) + return OsType.Linux; + if (desc.Contains("darwin") || desc.Contains("mac os")) + return OsType.macOS; + } + catch + { + // swallow any unexpected errors + } + + return OsType.Unknown; + } + + /// + /// Given an OsType enum, returns the edition/distro string. + /// passing the enum makes search less expensive + /// + private static string DetectOsDetails(OsType os) + { + try + { + switch (os) + { + case OsType.Windows: +#if WINDOWS + // WMI query for “Caption” + // https://learn.microsoft.com/en-us/windows/win32/wmisdk/about-wmi + using var searcher = + new ManagementObjectSearcher("SELECT Caption FROM Win32_OperatingSystem"); + foreach (var o in searcher.Get()) + { + var caption = o["Caption"]?.ToString()?.Trim(); + if (!string.IsNullOrEmpty(caption)) + return caption; + } +#endif + break; + + case OsType.Linux: + const string file = "/etc/os-release"; + if (File.Exists(file)) + { + foreach (var line in File.ReadAllLines(file)) + { + if (line.StartsWith("PRETTY_NAME=", StringComparison.Ordinal)) + { + var parts = line.Split('='); + if (parts.Length >= 2) + { + return parts[1].Trim().Trim('"'); + } + } + } + } + break; + + case OsType.macOS: + return "macOS " + RuntimeInformation.OSDescription; + + // FreeBSD, Android, Unknown fall through + } + + // fallback for FreeBSD, Android, Unknown or if above branches fail + var fallback = RuntimeInformation.OSDescription; + if (!string.IsNullOrWhiteSpace(fallback)) + return fallback; + } + catch + { + // swallow all exceptions + } + + return DefaultJsonValue; + } + + /// + /// Detects and reports whatever CPU architecture the guest OS exposes + /// + private static string DetectArchitecture() + { + try + { + // Returns “X86”, “X64”, “Arm”, “Arm64”, etc. + // This is the architecture of the guest process it's running in + // it does not see through to the physical host. + return RuntimeInformation.ProcessArchitecture.ToString(); + } + catch + { + // In case RuntimeInformation isn’t available or something unexpected happens + } + return DefaultJsonValue; + } + + /// + /// Reads the Microsoft.Data.SqlClient assembly’s informational version + /// or falls back to its AssemblyName.Version. + /// + private static string DetectRuntime() + { + // 1) Try the built-in .NET runtime description + try + { + string fw = RuntimeInformation.FrameworkDescription; + if (!string.IsNullOrWhiteSpace(fw)) + return fw.Trim(); + } + catch + { + // ignore and fall back + } + + // 2) On Linux, ask glibc what version it is + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + try + { + // P/Invoke into libc + IntPtr ptr = gnu_get_libc_version(); + string glibc = Marshal.PtrToStringAnsi(ptr); + if (!string.IsNullOrWhiteSpace(glibc)) + return "glibc " + glibc.Trim(); + } + catch + { + // ignore + } + } + + // 3) If running under Mono, grab its internal display name + try + { + var mono = Type.GetType("Mono.Runtime"); + if (mono != null) + { + // Mono.Runtime.GetDisplayName() is a private static method + var mi = mono.GetMethod( + "GetDisplayName", + BindingFlags.NonPublic | BindingFlags.Static + ); + if (mi != null) + { + string name = mi.Invoke(null, null) as string; + if (!string.IsNullOrWhiteSpace(name)) + return name.Trim(); + } + } + } + catch + { + // ignore + } + + // 4) Nothing matched, give up + return DefaultJsonValue; + } + + } +} + diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgentInfoDto.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgentInfoDto.cs new file mode 100644 index 0000000000..4a28a0d0d1 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgentInfoDto.cs @@ -0,0 +1,44 @@ +using System.Text.Json; + +namespace Microsoft.Data.SqlClient +{ + internal class UserAgentInfoDto + { + // Note: JSON key names are defined as constants to avoid reflection during serialization. + // This allows us to calculate their UTF-8 encoded byte sizes efficiently without instantiating + // the DTO or relying on JsonProperty attribute resolution at runtime. The small overhead of + // maintaining constants is justified by the performance and allocation savings. + public const string DriverJsonKey = "driver"; + public const string VersionJsonKey = "version"; + public const string OsJsonKey = "os"; + public const string ArchJsonKey = "arch"; + public const string RuntimeJsonKey = "runtime"; + + [JsonPropertyName(DriverJsonKey)] + public string Driver { get; set; } + + [JsonPropertyName(VersionJsonKey)] + public string Version { get; set; } + + [JsonPropertyName(OsJsonKey)] + public OsInfo OS { get; set; } + + [JsonPropertyName(ArchJsonKey)] + public string Arch { get; set; } + + [JsonPropertyName(RuntimeJsonKey)] + public string Runtime { get; set; } + + public class OsInfo + { + public const string TypeJsonKey = "type"; + public const string DetailsJsonKey = "details"; + + [JsonPropertyName(TypeJsonKey)] + public string Type { get; set; } + + [JsonPropertyName(DetailsJsonKey)] + public string Details { get; set; } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft.Data.SqlClient.UnitTests.csproj b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft.Data.SqlClient.UnitTests.csproj index 2f0e12c922..6d156b2bf3 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft.Data.SqlClient.UnitTests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft.Data.SqlClient.UnitTests.csproj @@ -48,4 +48,7 @@ xunit.runner.json + + + diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/UserAgentInfoTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/UserAgentInfoTests.cs new file mode 100644 index 0000000000..4aae276d2f --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/UserAgentInfoTests.cs @@ -0,0 +1,120 @@ +using System; +using System.Reflection; +using System.Text; +using System.Text.Json; +using Microsoft.Data.SqlClient; +using Xunit; + +namespace Microsoft.Data.SqlClient.UnitTests +{ + /// + /// Unit tests for and its companion DTO. + /// Focus areas: + /// 1. Field truncation logic + /// 2. Payload sizing and field‑dropping policy + /// 3. DTO JSON contract (key names) + /// 4. Cached payload invariants + /// + public class UserAgentInfoTests + { + // 1. Cached payload is within the 2,047‑byte spec and never null + [Fact] + public void CachedPayload_IsNotNull_And_WithinSpecLimit() + { + var field = typeof(UserAgentInfo).GetField( + name: "_cachedPayload", + bindingAttr: BindingFlags.NonPublic | BindingFlags.Static); + Assert.NotNull(field); + + byte[] payload = (byte[])field!.GetValue(null)!; + Assert.NotNull(payload); + Assert.InRange(payload.Length, 1, UserAgentInfo.JsonPayloadMaxBytesSpec); + } + + // 2. TruncateOrDefault respects null, empty, fit, and overflow cases + [Theory] + [InlineData(null, 5, "Unknown")] // null returns default + [InlineData("", 5, "Unknown")] // empty returns default + [InlineData("abc", 5, "abc")] // within limit unchanged + [InlineData("abcdef", 5, "abcde")] // overflow truncated + public void TruncateOrDefault_Behaviour(string? input, int max, string expected) + { + var mi = typeof(UserAgentInfo).GetMethod( + name: "TruncateOrDefault", + bindingAttr: BindingFlags.NonPublic | BindingFlags.Static); + Assert.NotNull(mi); + + string actual = (string)mi!.Invoke(null, new object?[] { input, max })!; + Assert.Equal(expected, actual); + } + + // 3. AdjustJsonPayloadSize drops low‑priority fields when required + [Fact] + public void AdjustJsonPayloadSize_StripsLowPriorityFields_When_PayloadTooLarge() + { + // Build an inflated DTO so the raw JSON exceeds 10 KB. + string huge = new string('x', 20_000); + var dto = new UserAgentInfoDto + { + Driver = huge, + Version = huge, + OS = new UserAgentInfoDto.OsInfo + { + Type = huge, + Details = huge + }, + Arch = huge, + Runtime = huge + }; + + var mi = typeof(UserAgentInfo).GetMethod( + name: "AdjustJsonPayloadSize", + bindingAttr: BindingFlags.NonPublic | BindingFlags.Static); + Assert.NotNull(mi); + + byte[] payload = (byte[])mi!.Invoke(null, new object?[] { dto })!; + + // Final payload must satisfy spec limit + Assert.InRange(payload.Length, 1, UserAgentInfo.JsonPayloadMaxBytesSpec); + + // Convert to string for field presence checks + string json = Encoding.UTF8.GetString(payload); + + // High‑priority fields remain + Assert.Contains(UserAgentInfoDto.DriverJsonKey, json); + Assert.Contains(UserAgentInfoDto.VersionJsonKey, json); + Assert.Contains(UserAgentInfoDto.OsJsonKey, json); + + // Low‑priority fields removed + Assert.DoesNotContain(UserAgentInfoDto.ArchJsonKey, json); + Assert.DoesNotContain(UserAgentInfoDto.RuntimeJsonKey, json); + Assert.DoesNotContain(UserAgentInfoDto.OsInfo.DetailsJsonKey, json); + } + + // 4. DTO serializes with expected JSON property names + [Fact] + public void Dto_JsonPropertyNames_MatchConstants() + { + var dto = new UserAgentInfoDto + { + Driver = "d", + Version = "v", + OS = new UserAgentInfoDto.OsInfo { Type = "t", Details = "dd" }, + Arch = "a", + Runtime = "r" + }; + + string json = JsonSerializer.Serialize(dto); + using JsonDocument doc = JsonDocument.Parse(json); + var root = doc.RootElement; + + Assert.True(root.TryGetProperty(UserAgentInfoDto.DriverJsonKey, out _)); + Assert.True(root.TryGetProperty(UserAgentInfoDto.VersionJsonKey, out _)); + Assert.True(root.TryGetProperty(UserAgentInfoDto.OsJsonKey, out var osElement)); + Assert.True(osElement.TryGetProperty(UserAgentInfoDto.OsInfo.TypeJsonKey, out _)); + Assert.True(osElement.TryGetProperty(UserAgentInfoDto.OsInfo.DetailsJsonKey, out _)); + Assert.True(root.TryGetProperty(UserAgentInfoDto.ArchJsonKey, out _)); + Assert.True(root.TryGetProperty(UserAgentInfoDto.RuntimeJsonKey, out _)); + } + } +} From 51478586541d1f04781e47bc7637e6ac60966d44 Mon Sep 17 00:00:00 2001 From: Saransh Sharma Date: Thu, 17 Jul 2025 13:28:30 -0700 Subject: [PATCH 6/9] Add unit tests and update UserAgentInfo payload checks --- .../src/Microsoft.Data.SqlClient.csproj | 2 + .../Microsoft/Data/SqlClient/UserAgentInfo.cs | 60 ++- .../Data/SqlClient/UserAgentInfoDto.cs | 2 +- .../netfx/src/Microsoft.Data.SqlClient.csproj | 2 + .../Microsoft/Data/SqlClient/UserAgentInfo.cs | 421 ++++++++++++++++++ .../Data/SqlClient/UserAgentInfoDto.cs | 45 ++ .../Data/SqlClient => }/UserAgentInfoTests.cs | 14 +- 7 files changed, 530 insertions(+), 16 deletions(-) rename src/Microsoft.Data.SqlClient/{ => netcore}/src/Microsoft/Data/SqlClient/UserAgentInfo.cs (89%) rename src/Microsoft.Data.SqlClient/{ => netcore}/src/Microsoft/Data/SqlClient/UserAgentInfoDto.cs (97%) create mode 100644 src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/UserAgentInfo.cs create mode 100644 src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/UserAgentInfoDto.cs rename src/Microsoft.Data.SqlClient/tests/UnitTests/{Microsoft/Data/SqlClient => }/UserAgentInfoTests.cs (91%) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj index 01f4de3352..587bcbddc6 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -1018,6 +1018,8 @@ True Strings.resx + + Resources\Strings.resx Microsoft.Data.SqlClient.Resources.Strings.resources diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgentInfo.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/UserAgentInfo.cs similarity index 89% rename from src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgentInfo.cs rename to src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/UserAgentInfo.cs index be91e47dfe..9d5ab9a7ce 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgentInfo.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/UserAgentInfo.cs @@ -23,15 +23,49 @@ namespace Microsoft.Data.SqlClient /// public static class UserAgentInfo { - public const int DriverNameMaxChars = 16; - public const int VersionMaxChars = 16; - public const int OsTypeMaxChars = 16; - public const int OsDetailsMaxChars = 128; - public const int ArchMaxChars = 16; - public const int RuntimeMaxChars = 128; + /// + /// Maximum number of characters allowed for the driver name. + /// + private const int DriverNameMaxChars = 16; + + /// + /// Maximum number of characters allowed for the driver version. + /// + private const int VersionMaxChars = 16; + + /// + /// Maximum number of characters allowed for the operating system type. + /// + private const int OsTypeMaxChars = 16; + + /// + /// Maximum number of characters allowed for the operating system details. + /// + private const int OsDetailsMaxChars = 128; + + /// + /// Maximum number of characters allowed for the system architecture. + /// + private const int ArchMaxChars = 16; + + /// + /// Maximum number of characters allowed for the driver runtime. + /// + private const int RuntimeMaxChars = 128; + + /// + /// Maximum number of bytes allowed for the user agent json payload. + /// payloads larger than this may be rejected by the server. + /// public const int JsonPayloadMaxBytesSpec = 2047; + + /// + /// Maximum number of bytes allowed before we drop multiple fields + /// and only send bare minimum useragent info. + /// public const int UserAgentPayloadMaxBytes = 10000; + private const string DefaultJsonValue = "Unknown"; private const string DefaultDriverName = "MS-MDS"; @@ -60,7 +94,7 @@ private enum OsType static UserAgentInfo() { - /// Note: We serialize 6 fields in total: + // Note: We serialize 6 fields in total: // - 4 fields with up to 16 characters each // - 2 fields with up to 128 characters each // @@ -100,7 +134,7 @@ static UserAgentInfo() // some servers may silently drop or reject such packets — behavior we may use for future probing or diagnostics. driverName = TruncateOrDefault(DefaultDriverName, DriverNameMaxChars); - version = TruncateOrDefault(ADP.GetAssemblyVersion.ToString(), VersionMaxChars); + version = TruncateOrDefault(ADP.GetAssemblyVersion().ToString(), VersionMaxChars); var osVal = DetectOsType(); osType = TruncateOrDefault(osVal.ToString(), OsTypeMaxChars); osDetails = TruncateOrDefault(DetectOsDetails(osVal), OsDetailsMaxChars); @@ -157,11 +191,11 @@ private static byte[] AdjustJsonPayloadSize(UserAgentInfoDto dto) dto.Arch = null; // drop Arch payload = JsonSerializer.SerializeToUtf8Bytes(dto, options); } -#if DEBUG - Debug.Assert(payload.Length <= JsonPayloadMaxBytesSpec, - $"UserAgent payload is {payload.Length} bytes (spec max {JsonPayloadMaxBytesSpec})."); -#endif - return payload; + + // Last check to ensure we are within the limits(in case remaining fields are still too large) + return payload.Length > UserAgentPayloadMaxBytes + ? JsonSerializer.SerializeToUtf8Bytes(new { }, options) + : payload; } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgentInfoDto.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/UserAgentInfoDto.cs similarity index 97% rename from src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgentInfoDto.cs rename to src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/UserAgentInfoDto.cs index 4a28a0d0d1..5f8046d62e 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgentInfoDto.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/UserAgentInfoDto.cs @@ -1,5 +1,5 @@ using System.Text.Json; - +using System.Text.Json.Serialization; namespace Microsoft.Data.SqlClient { internal class UserAgentInfoDto diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj index 8d178e11b6..75980c4ee0 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -936,6 +936,8 @@ True Strings.resx + + Resources\Strings.resx Microsoft.Data.SqlClient.Resources.Strings.resources diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/UserAgentInfo.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/UserAgentInfo.cs new file mode 100644 index 0000000000..9d5ab9a7ce --- /dev/null +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/UserAgentInfo.cs @@ -0,0 +1,421 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using Microsoft.Data.Common; + +#if WINDOWS +using System.Management; +#endif + +namespace Microsoft.Data.SqlClient +{ + /// + /// Gathers driver + environment info, enforces size constraints, + /// and serializes into a UTF-8 JSON payload. + /// + public static class UserAgentInfo + { + /// + /// Maximum number of characters allowed for the driver name. + /// + private const int DriverNameMaxChars = 16; + + /// + /// Maximum number of characters allowed for the driver version. + /// + private const int VersionMaxChars = 16; + + /// + /// Maximum number of characters allowed for the operating system type. + /// + private const int OsTypeMaxChars = 16; + + /// + /// Maximum number of characters allowed for the operating system details. + /// + private const int OsDetailsMaxChars = 128; + + /// + /// Maximum number of characters allowed for the system architecture. + /// + private const int ArchMaxChars = 16; + + /// + /// Maximum number of characters allowed for the driver runtime. + /// + private const int RuntimeMaxChars = 128; + + /// + /// Maximum number of bytes allowed for the user agent json payload. + /// payloads larger than this may be rejected by the server. + /// + public const int JsonPayloadMaxBytesSpec = 2047; + + /// + /// Maximum number of bytes allowed before we drop multiple fields + /// and only send bare minimum useragent info. + /// + public const int UserAgentPayloadMaxBytes = 10000; + + + private const string DefaultJsonValue = "Unknown"; + private const string DefaultDriverName = "MS-MDS"; + + // JSON Payload for UserAgent + private static readonly string driverName; + private static readonly string version; + private static readonly string osType; + private static readonly string osDetails; + private static readonly string architecture; + private static readonly string runtime; + private static readonly byte[] _cachedPayload; + + private enum OsType + { + Windows, + Linux, + macOS, + FreeBSD, + Android, + Unknown + } + + // P/Invoke signature for glibc detection + [DllImport("libc", EntryPoint = "gnu_get_libc_version", CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr gnu_get_libc_version(); + + static UserAgentInfo() + { + // Note: We serialize 6 fields in total: + // - 4 fields with up to 16 characters each + // - 2 fields with up to 128 characters each + // + // For estimating **on-the-wire UTF-8 size** of the serialized JSON: + // 1) For the 4 fields of 16 characters: + // - In worst case (all characters require escaping in JSON, e.g., quotes, backslashes, control chars), + // each character may expand to 2–6 bytes in the JSON string (e.g., \" = 2 bytes, \uXXXX = 6 bytes) + // - Assuming full escape with \uXXXX form (6 bytes per char): 4 × 16 × 6 = 384 bytes (extreme worst case) + // - For unescaped high-plane Unicode (e.g., emojis), UTF-8 uses up to 4 bytes per character: + // 4 × 16 × 4 = 256 bytes (UTF-8 max) + // + // Conservative max estimate for these fields = **384 bytes** + // + // 2) For the 2 fields of 128 characters: + // - Worst-case with \uXXXX escape sequences: 2 × 128 × 6 = 1,536 bytes + // - Worst-case with high Unicode: 2 × 128 × 4 = 1,024 bytes + // + // Conservative max estimate for these fields = **1,536 bytes** + // + // Combined worst-case for value content = 384 + 1536 = **1,920 bytes** + // + // 3) The rest of the serialized JSON payload (object braces, field names, quotes, colons, commas) is fixed. + // Based on measurements, it typically adds to about **81 bytes**. + // + // Final worst-case estimate for total payload on the wire (UTF-8 encoded): + // 1,920 + 81 = **2,001 bytes** + // + // This is still below our spec limit of 2,047 bytes. + // + // TDS Prelogin7 packets support up to 65,535 bytes (including headers), but many server versions impose + // stricter limits for prelogin payloads. + // + // As a safety measure: + // - If the serialized payload exceeds **10 KB**, we fallback to transmitting only essential fields: + // 'driver', 'version', and 'os.type' + // - If the payload exceeds 2,047 bytes but remains within sensible limits, we still send it, but note that + // some servers may silently drop or reject such packets — behavior we may use for future probing or diagnostics. + + driverName = TruncateOrDefault(DefaultDriverName, DriverNameMaxChars); + version = TruncateOrDefault(ADP.GetAssemblyVersion().ToString(), VersionMaxChars); + var osVal = DetectOsType(); + osType = TruncateOrDefault(osVal.ToString(), OsTypeMaxChars); + osDetails = TruncateOrDefault(DetectOsDetails(osVal), OsDetailsMaxChars); + architecture = TruncateOrDefault(DetectArchitecture(), ArchMaxChars); + runtime = TruncateOrDefault(DetectRuntime(), RuntimeMaxChars); + + // Instantiate DTO before serializing + var dto = new UserAgentInfoDto + { + Driver = driverName, + Version = version, + OS = new UserAgentInfoDto.OsInfo + { + Type = osType, + Details = osDetails + }, + Arch = architecture, + Runtime = runtime, + + }; + + // Check/Adjust payload before caching it + _cachedPayload = AdjustJsonPayloadSize(dto); + } + + /// + /// This function returns the appropriately sized json payload + /// We check the size of encoded json payload, if it is within limits we return the dto to be cached + /// other wise we drop some fields to reduce the size of the payload. + /// + /// Data Transfer Object for the json payload + /// Serialized UTF-8 encoded json payload version of DTO within size limit + private static byte[] AdjustJsonPayloadSize(UserAgentInfoDto dto) + { + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = null, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = false + }; + byte[] payload = JsonSerializer.SerializeToUtf8Bytes(dto, options); + + // Note: server will likely reject payloads larger than 2047 bytes + // Try if the payload fits the max allowed bytes + if (payload.Length <= JsonPayloadMaxBytesSpec) + { + return payload; + } + if (payload.Length > UserAgentPayloadMaxBytes) + { + // If the payload is over 10KB, we only send the bare minimum fields + dto.OS.Details = null; // drop OS.Details + dto.Runtime = null; // drop Runtime + dto.Arch = null; // drop Arch + payload = JsonSerializer.SerializeToUtf8Bytes(dto, options); + } + + // Last check to ensure we are within the limits(in case remaining fields are still too large) + return payload.Length > UserAgentPayloadMaxBytes + ? JsonSerializer.SerializeToUtf8Bytes(new { }, options) + : payload; + + } + + /// + /// Truncates a string to the specified maximum length or returns a default value if input is null or empty. + /// + /// The string value to truncate + /// Maximum number of characters allowed + /// Truncated string or default value if input is invalid + private static string TruncateOrDefault(string jsonStringVal, int maxChars) + { + try + { + if (string.IsNullOrEmpty(jsonStringVal)) + { + return DefaultJsonValue; + } + + if (jsonStringVal.Length <= maxChars) + { + return jsonStringVal; + } + + return jsonStringVal.Substring(0, maxChars); + } + catch + { + // Silently consume all exceptions + return DefaultJsonValue; + } + } + + /// + /// Detects the OS platform and returns the matching OsType enum. + /// + private static OsType DetectOsType() + { + try + { + // first we try with built-in checks (Android and FreeBSD also report Linux so they are checked first) +#if NET6_0_OR_GREATER + if (OperatingSystem.IsAndroid()) return OsType.Android; + if (OperatingSystem.IsFreeBSD()) return OsType.FreeBSD; + if (OperatingSystem.IsWindows()) return OsType.Windows; + if (OperatingSystem.IsLinux()) return OsType.Linux; + if (OperatingSystem.IsMacOS()) return OsType.macOS; +#endif + // second we fallback to OSplatform checks + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return OsType.Windows; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + return OsType.Linux; + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + return OsType.macOS; + + // final fallback is inspecting OSdecription + var desc = RuntimeInformation.OSDescription?.ToLowerInvariant() ?? ""; + if (desc.Contains("android")) + return OsType.Android; + if (desc.Contains("freebsd")) + return OsType.FreeBSD; + if (desc.Contains("windows")) + return OsType.Windows; + if (desc.Contains("linux")) + return OsType.Linux; + if (desc.Contains("darwin") || desc.Contains("mac os")) + return OsType.macOS; + } + catch + { + // swallow any unexpected errors + } + + return OsType.Unknown; + } + + /// + /// Given an OsType enum, returns the edition/distro string. + /// passing the enum makes search less expensive + /// + private static string DetectOsDetails(OsType os) + { + try + { + switch (os) + { + case OsType.Windows: +#if WINDOWS + // WMI query for “Caption” + // https://learn.microsoft.com/en-us/windows/win32/wmisdk/about-wmi + using var searcher = + new ManagementObjectSearcher("SELECT Caption FROM Win32_OperatingSystem"); + foreach (var o in searcher.Get()) + { + var caption = o["Caption"]?.ToString()?.Trim(); + if (!string.IsNullOrEmpty(caption)) + return caption; + } +#endif + break; + + case OsType.Linux: + const string file = "/etc/os-release"; + if (File.Exists(file)) + { + foreach (var line in File.ReadAllLines(file)) + { + if (line.StartsWith("PRETTY_NAME=", StringComparison.Ordinal)) + { + var parts = line.Split('='); + if (parts.Length >= 2) + { + return parts[1].Trim().Trim('"'); + } + } + } + } + break; + + case OsType.macOS: + return "macOS " + RuntimeInformation.OSDescription; + + // FreeBSD, Android, Unknown fall through + } + + // fallback for FreeBSD, Android, Unknown or if above branches fail + var fallback = RuntimeInformation.OSDescription; + if (!string.IsNullOrWhiteSpace(fallback)) + return fallback; + } + catch + { + // swallow all exceptions + } + + return DefaultJsonValue; + } + + /// + /// Detects and reports whatever CPU architecture the guest OS exposes + /// + private static string DetectArchitecture() + { + try + { + // Returns “X86”, “X64”, “Arm”, “Arm64”, etc. + // This is the architecture of the guest process it's running in + // it does not see through to the physical host. + return RuntimeInformation.ProcessArchitecture.ToString(); + } + catch + { + // In case RuntimeInformation isn’t available or something unexpected happens + } + return DefaultJsonValue; + } + + /// + /// Reads the Microsoft.Data.SqlClient assembly’s informational version + /// or falls back to its AssemblyName.Version. + /// + private static string DetectRuntime() + { + // 1) Try the built-in .NET runtime description + try + { + string fw = RuntimeInformation.FrameworkDescription; + if (!string.IsNullOrWhiteSpace(fw)) + return fw.Trim(); + } + catch + { + // ignore and fall back + } + + // 2) On Linux, ask glibc what version it is + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + try + { + // P/Invoke into libc + IntPtr ptr = gnu_get_libc_version(); + string glibc = Marshal.PtrToStringAnsi(ptr); + if (!string.IsNullOrWhiteSpace(glibc)) + return "glibc " + glibc.Trim(); + } + catch + { + // ignore + } + } + + // 3) If running under Mono, grab its internal display name + try + { + var mono = Type.GetType("Mono.Runtime"); + if (mono != null) + { + // Mono.Runtime.GetDisplayName() is a private static method + var mi = mono.GetMethod( + "GetDisplayName", + BindingFlags.NonPublic | BindingFlags.Static + ); + if (mi != null) + { + string name = mi.Invoke(null, null) as string; + if (!string.IsNullOrWhiteSpace(name)) + return name.Trim(); + } + } + } + catch + { + // ignore + } + + // 4) Nothing matched, give up + return DefaultJsonValue; + } + + } +} + diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/UserAgentInfoDto.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/UserAgentInfoDto.cs new file mode 100644 index 0000000000..5b139c7f3a --- /dev/null +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/UserAgentInfoDto.cs @@ -0,0 +1,45 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.Data.SqlClient +{ + internal class UserAgentInfoDto + { + // Note: JSON key names are defined as constants to avoid reflection during serialization. + // This allows us to calculate their UTF-8 encoded byte sizes efficiently without instantiating + // the DTO or relying on JsonProperty attribute resolution at runtime. The small overhead of + // maintaining constants is justified by the performance and allocation savings. + public const string DriverJsonKey = "driver"; + public const string VersionJsonKey = "version"; + public const string OsJsonKey = "os"; + public const string ArchJsonKey = "arch"; + public const string RuntimeJsonKey = "runtime"; + + [JsonPropertyName(DriverJsonKey)] + public string Driver { get; set; } + + [JsonPropertyName(VersionJsonKey)] + public string Version { get; set; } + + [JsonPropertyName(OsJsonKey)] + public OsInfo OS { get; set; } + + [JsonPropertyName(ArchJsonKey)] + public string Arch { get; set; } + + [JsonPropertyName(RuntimeJsonKey)] + public string Runtime { get; set; } + + public class OsInfo + { + public const string TypeJsonKey = "type"; + public const string DetailsJsonKey = "details"; + + [JsonPropertyName(TypeJsonKey)] + public string Type { get; set; } + + [JsonPropertyName(DetailsJsonKey)] + public string Details { get; set; } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/UserAgentInfoTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/UserAgentInfoTests.cs similarity index 91% rename from src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/UserAgentInfoTests.cs rename to src/Microsoft.Data.SqlClient/tests/UnitTests/UserAgentInfoTests.cs index 4aae276d2f..c49c83239d 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/UserAgentInfoTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/UserAgentInfoTests.cs @@ -5,6 +5,8 @@ using Microsoft.Data.SqlClient; using Xunit; +#nullable enable + namespace Microsoft.Data.SqlClient.UnitTests { /// @@ -74,12 +76,20 @@ public void AdjustJsonPayloadSize_StripsLowPriorityFields_When_PayloadTooLarge() byte[] payload = (byte[])mi!.Invoke(null, new object?[] { dto })!; - // Final payload must satisfy spec limit - Assert.InRange(payload.Length, 1, UserAgentInfo.JsonPayloadMaxBytesSpec); + // Final payload must satisfy limits + Assert.InRange(payload.Length, 1, UserAgentInfo.UserAgentPayloadMaxBytes); // Convert to string for field presence checks string json = Encoding.UTF8.GetString(payload); + // We either receive the minimal payload with only high‑priority fields, + // or we receive an empty payload in case of overflow despite dropping fields. + if (payload.Length <= 2) + { + Assert.Equal("{}", json.Trim()); + return; + } + // High‑priority fields remain Assert.Contains(UserAgentInfoDto.DriverJsonKey, json); Assert.Contains(UserAgentInfoDto.VersionJsonKey, json); From 7b2ad0ef01cf9baa8f342599569e7f40caa148cc Mon Sep 17 00:00:00 2001 From: Saransh Sharma Date: Fri, 18 Jul 2025 14:07:14 -0700 Subject: [PATCH 7/9] PR review changes 1 --- .../src/Microsoft.Data.SqlClient.csproj | 8 +- .../netfx/src/Microsoft.Data.SqlClient.csproj | 8 +- .../Microsoft/Data/SqlClient/UserAgentInfo.cs | 421 ------------------ .../Data/SqlClient/UserAgentInfoDto.cs | 45 -- .../SqlClient/UserAgent}/UserAgentInfo.cs | 12 +- .../SqlClient/UserAgent}/UserAgentInfoDto.cs | 2 +- .../Microsoft.Data.SqlClient.UnitTests.csproj | 3 - .../tests/UnitTests/UserAgentInfoTests.cs | 2 +- 8 files changed, 20 insertions(+), 481 deletions(-) delete mode 100644 src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/UserAgentInfo.cs delete mode 100644 src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/UserAgentInfoDto.cs rename src/Microsoft.Data.SqlClient/{netcore/src/Microsoft/Data/SqlClient => src/Microsoft/Data/SqlClient/UserAgent}/UserAgentInfo.cs (98%) rename src/Microsoft.Data.SqlClient/{netcore/src/Microsoft/Data/SqlClient => src/Microsoft/Data/SqlClient/UserAgent}/UserAgentInfoDto.cs (97%) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj index 587bcbddc6..90899f35fc 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -792,6 +792,12 @@ Microsoft\Data\SqlTypes\SqlVector.cs + + Microsoft\Data\SqlClient\UserAgent\UserAgentInfo.cs + + + Microsoft\Data\SqlClient\UserAgent\UserAgentInfoDto.cs + Resources\ResCategoryAttribute.cs @@ -1018,8 +1024,6 @@ True Strings.resx - - Resources\Strings.resx Microsoft.Data.SqlClient.Resources.Strings.resources diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj index 75980c4ee0..fbfcbe6e44 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -903,6 +903,12 @@ Microsoft\Data\SqlTypes\SqlVector.cs + + Microsoft\Data\SqlClient\UserAgent\UserAgentInfo.cs + + + Microsoft\Data\SqlClient\UserAgent\UserAgentInfoDto.cs + Resources\ResDescriptionAttribute.cs @@ -936,8 +942,6 @@ True Strings.resx - - Resources\Strings.resx Microsoft.Data.SqlClient.Resources.Strings.resources diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/UserAgentInfo.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/UserAgentInfo.cs deleted file mode 100644 index 9d5ab9a7ce..0000000000 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/UserAgentInfo.cs +++ /dev/null @@ -1,421 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Runtime.InteropServices; -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Text.RegularExpressions; -using Microsoft.Data.Common; - -#if WINDOWS -using System.Management; -#endif - -namespace Microsoft.Data.SqlClient -{ - /// - /// Gathers driver + environment info, enforces size constraints, - /// and serializes into a UTF-8 JSON payload. - /// - public static class UserAgentInfo - { - /// - /// Maximum number of characters allowed for the driver name. - /// - private const int DriverNameMaxChars = 16; - - /// - /// Maximum number of characters allowed for the driver version. - /// - private const int VersionMaxChars = 16; - - /// - /// Maximum number of characters allowed for the operating system type. - /// - private const int OsTypeMaxChars = 16; - - /// - /// Maximum number of characters allowed for the operating system details. - /// - private const int OsDetailsMaxChars = 128; - - /// - /// Maximum number of characters allowed for the system architecture. - /// - private const int ArchMaxChars = 16; - - /// - /// Maximum number of characters allowed for the driver runtime. - /// - private const int RuntimeMaxChars = 128; - - /// - /// Maximum number of bytes allowed for the user agent json payload. - /// payloads larger than this may be rejected by the server. - /// - public const int JsonPayloadMaxBytesSpec = 2047; - - /// - /// Maximum number of bytes allowed before we drop multiple fields - /// and only send bare minimum useragent info. - /// - public const int UserAgentPayloadMaxBytes = 10000; - - - private const string DefaultJsonValue = "Unknown"; - private const string DefaultDriverName = "MS-MDS"; - - // JSON Payload for UserAgent - private static readonly string driverName; - private static readonly string version; - private static readonly string osType; - private static readonly string osDetails; - private static readonly string architecture; - private static readonly string runtime; - private static readonly byte[] _cachedPayload; - - private enum OsType - { - Windows, - Linux, - macOS, - FreeBSD, - Android, - Unknown - } - - // P/Invoke signature for glibc detection - [DllImport("libc", EntryPoint = "gnu_get_libc_version", CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr gnu_get_libc_version(); - - static UserAgentInfo() - { - // Note: We serialize 6 fields in total: - // - 4 fields with up to 16 characters each - // - 2 fields with up to 128 characters each - // - // For estimating **on-the-wire UTF-8 size** of the serialized JSON: - // 1) For the 4 fields of 16 characters: - // - In worst case (all characters require escaping in JSON, e.g., quotes, backslashes, control chars), - // each character may expand to 2–6 bytes in the JSON string (e.g., \" = 2 bytes, \uXXXX = 6 bytes) - // - Assuming full escape with \uXXXX form (6 bytes per char): 4 × 16 × 6 = 384 bytes (extreme worst case) - // - For unescaped high-plane Unicode (e.g., emojis), UTF-8 uses up to 4 bytes per character: - // 4 × 16 × 4 = 256 bytes (UTF-8 max) - // - // Conservative max estimate for these fields = **384 bytes** - // - // 2) For the 2 fields of 128 characters: - // - Worst-case with \uXXXX escape sequences: 2 × 128 × 6 = 1,536 bytes - // - Worst-case with high Unicode: 2 × 128 × 4 = 1,024 bytes - // - // Conservative max estimate for these fields = **1,536 bytes** - // - // Combined worst-case for value content = 384 + 1536 = **1,920 bytes** - // - // 3) The rest of the serialized JSON payload (object braces, field names, quotes, colons, commas) is fixed. - // Based on measurements, it typically adds to about **81 bytes**. - // - // Final worst-case estimate for total payload on the wire (UTF-8 encoded): - // 1,920 + 81 = **2,001 bytes** - // - // This is still below our spec limit of 2,047 bytes. - // - // TDS Prelogin7 packets support up to 65,535 bytes (including headers), but many server versions impose - // stricter limits for prelogin payloads. - // - // As a safety measure: - // - If the serialized payload exceeds **10 KB**, we fallback to transmitting only essential fields: - // 'driver', 'version', and 'os.type' - // - If the payload exceeds 2,047 bytes but remains within sensible limits, we still send it, but note that - // some servers may silently drop or reject such packets — behavior we may use for future probing or diagnostics. - - driverName = TruncateOrDefault(DefaultDriverName, DriverNameMaxChars); - version = TruncateOrDefault(ADP.GetAssemblyVersion().ToString(), VersionMaxChars); - var osVal = DetectOsType(); - osType = TruncateOrDefault(osVal.ToString(), OsTypeMaxChars); - osDetails = TruncateOrDefault(DetectOsDetails(osVal), OsDetailsMaxChars); - architecture = TruncateOrDefault(DetectArchitecture(), ArchMaxChars); - runtime = TruncateOrDefault(DetectRuntime(), RuntimeMaxChars); - - // Instantiate DTO before serializing - var dto = new UserAgentInfoDto - { - Driver = driverName, - Version = version, - OS = new UserAgentInfoDto.OsInfo - { - Type = osType, - Details = osDetails - }, - Arch = architecture, - Runtime = runtime, - - }; - - // Check/Adjust payload before caching it - _cachedPayload = AdjustJsonPayloadSize(dto); - } - - /// - /// This function returns the appropriately sized json payload - /// We check the size of encoded json payload, if it is within limits we return the dto to be cached - /// other wise we drop some fields to reduce the size of the payload. - /// - /// Data Transfer Object for the json payload - /// Serialized UTF-8 encoded json payload version of DTO within size limit - private static byte[] AdjustJsonPayloadSize(UserAgentInfoDto dto) - { - var options = new JsonSerializerOptions - { - PropertyNamingPolicy = null, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - WriteIndented = false - }; - byte[] payload = JsonSerializer.SerializeToUtf8Bytes(dto, options); - - // Note: server will likely reject payloads larger than 2047 bytes - // Try if the payload fits the max allowed bytes - if (payload.Length <= JsonPayloadMaxBytesSpec) - { - return payload; - } - if (payload.Length > UserAgentPayloadMaxBytes) - { - // If the payload is over 10KB, we only send the bare minimum fields - dto.OS.Details = null; // drop OS.Details - dto.Runtime = null; // drop Runtime - dto.Arch = null; // drop Arch - payload = JsonSerializer.SerializeToUtf8Bytes(dto, options); - } - - // Last check to ensure we are within the limits(in case remaining fields are still too large) - return payload.Length > UserAgentPayloadMaxBytes - ? JsonSerializer.SerializeToUtf8Bytes(new { }, options) - : payload; - - } - - /// - /// Truncates a string to the specified maximum length or returns a default value if input is null or empty. - /// - /// The string value to truncate - /// Maximum number of characters allowed - /// Truncated string or default value if input is invalid - private static string TruncateOrDefault(string jsonStringVal, int maxChars) - { - try - { - if (string.IsNullOrEmpty(jsonStringVal)) - { - return DefaultJsonValue; - } - - if (jsonStringVal.Length <= maxChars) - { - return jsonStringVal; - } - - return jsonStringVal.Substring(0, maxChars); - } - catch - { - // Silently consume all exceptions - return DefaultJsonValue; - } - } - - /// - /// Detects the OS platform and returns the matching OsType enum. - /// - private static OsType DetectOsType() - { - try - { - // first we try with built-in checks (Android and FreeBSD also report Linux so they are checked first) -#if NET6_0_OR_GREATER - if (OperatingSystem.IsAndroid()) return OsType.Android; - if (OperatingSystem.IsFreeBSD()) return OsType.FreeBSD; - if (OperatingSystem.IsWindows()) return OsType.Windows; - if (OperatingSystem.IsLinux()) return OsType.Linux; - if (OperatingSystem.IsMacOS()) return OsType.macOS; -#endif - // second we fallback to OSplatform checks - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - return OsType.Windows; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - return OsType.Linux; - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - return OsType.macOS; - - // final fallback is inspecting OSdecription - var desc = RuntimeInformation.OSDescription?.ToLowerInvariant() ?? ""; - if (desc.Contains("android")) - return OsType.Android; - if (desc.Contains("freebsd")) - return OsType.FreeBSD; - if (desc.Contains("windows")) - return OsType.Windows; - if (desc.Contains("linux")) - return OsType.Linux; - if (desc.Contains("darwin") || desc.Contains("mac os")) - return OsType.macOS; - } - catch - { - // swallow any unexpected errors - } - - return OsType.Unknown; - } - - /// - /// Given an OsType enum, returns the edition/distro string. - /// passing the enum makes search less expensive - /// - private static string DetectOsDetails(OsType os) - { - try - { - switch (os) - { - case OsType.Windows: -#if WINDOWS - // WMI query for “Caption” - // https://learn.microsoft.com/en-us/windows/win32/wmisdk/about-wmi - using var searcher = - new ManagementObjectSearcher("SELECT Caption FROM Win32_OperatingSystem"); - foreach (var o in searcher.Get()) - { - var caption = o["Caption"]?.ToString()?.Trim(); - if (!string.IsNullOrEmpty(caption)) - return caption; - } -#endif - break; - - case OsType.Linux: - const string file = "/etc/os-release"; - if (File.Exists(file)) - { - foreach (var line in File.ReadAllLines(file)) - { - if (line.StartsWith("PRETTY_NAME=", StringComparison.Ordinal)) - { - var parts = line.Split('='); - if (parts.Length >= 2) - { - return parts[1].Trim().Trim('"'); - } - } - } - } - break; - - case OsType.macOS: - return "macOS " + RuntimeInformation.OSDescription; - - // FreeBSD, Android, Unknown fall through - } - - // fallback for FreeBSD, Android, Unknown or if above branches fail - var fallback = RuntimeInformation.OSDescription; - if (!string.IsNullOrWhiteSpace(fallback)) - return fallback; - } - catch - { - // swallow all exceptions - } - - return DefaultJsonValue; - } - - /// - /// Detects and reports whatever CPU architecture the guest OS exposes - /// - private static string DetectArchitecture() - { - try - { - // Returns “X86”, “X64”, “Arm”, “Arm64”, etc. - // This is the architecture of the guest process it's running in - // it does not see through to the physical host. - return RuntimeInformation.ProcessArchitecture.ToString(); - } - catch - { - // In case RuntimeInformation isn’t available or something unexpected happens - } - return DefaultJsonValue; - } - - /// - /// Reads the Microsoft.Data.SqlClient assembly’s informational version - /// or falls back to its AssemblyName.Version. - /// - private static string DetectRuntime() - { - // 1) Try the built-in .NET runtime description - try - { - string fw = RuntimeInformation.FrameworkDescription; - if (!string.IsNullOrWhiteSpace(fw)) - return fw.Trim(); - } - catch - { - // ignore and fall back - } - - // 2) On Linux, ask glibc what version it is - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - try - { - // P/Invoke into libc - IntPtr ptr = gnu_get_libc_version(); - string glibc = Marshal.PtrToStringAnsi(ptr); - if (!string.IsNullOrWhiteSpace(glibc)) - return "glibc " + glibc.Trim(); - } - catch - { - // ignore - } - } - - // 3) If running under Mono, grab its internal display name - try - { - var mono = Type.GetType("Mono.Runtime"); - if (mono != null) - { - // Mono.Runtime.GetDisplayName() is a private static method - var mi = mono.GetMethod( - "GetDisplayName", - BindingFlags.NonPublic | BindingFlags.Static - ); - if (mi != null) - { - string name = mi.Invoke(null, null) as string; - if (!string.IsNullOrWhiteSpace(name)) - return name.Trim(); - } - } - } - catch - { - // ignore - } - - // 4) Nothing matched, give up - return DefaultJsonValue; - } - - } -} - diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/UserAgentInfoDto.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/UserAgentInfoDto.cs deleted file mode 100644 index 5b139c7f3a..0000000000 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/UserAgentInfoDto.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Microsoft.Data.SqlClient -{ - internal class UserAgentInfoDto - { - // Note: JSON key names are defined as constants to avoid reflection during serialization. - // This allows us to calculate their UTF-8 encoded byte sizes efficiently without instantiating - // the DTO or relying on JsonProperty attribute resolution at runtime. The small overhead of - // maintaining constants is justified by the performance and allocation savings. - public const string DriverJsonKey = "driver"; - public const string VersionJsonKey = "version"; - public const string OsJsonKey = "os"; - public const string ArchJsonKey = "arch"; - public const string RuntimeJsonKey = "runtime"; - - [JsonPropertyName(DriverJsonKey)] - public string Driver { get; set; } - - [JsonPropertyName(VersionJsonKey)] - public string Version { get; set; } - - [JsonPropertyName(OsJsonKey)] - public OsInfo OS { get; set; } - - [JsonPropertyName(ArchJsonKey)] - public string Arch { get; set; } - - [JsonPropertyName(RuntimeJsonKey)] - public string Runtime { get; set; } - - public class OsInfo - { - public const string TypeJsonKey = "type"; - public const string DetailsJsonKey = "details"; - - [JsonPropertyName(TypeJsonKey)] - public string Type { get; set; } - - [JsonPropertyName(DetailsJsonKey)] - public string Details { get; set; } - } - } -} diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/UserAgentInfo.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfo.cs similarity index 98% rename from src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/UserAgentInfo.cs rename to src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfo.cs index 9d5ab9a7ce..cca0e13894 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/UserAgentInfo.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfo.cs @@ -15,13 +15,13 @@ using System.Management; #endif -namespace Microsoft.Data.SqlClient +namespace Microsoft.Data.SqlClient.UserAgent { /// /// Gathers driver + environment info, enforces size constraints, /// and serializes into a UTF-8 JSON payload. /// - public static class UserAgentInfo + internal static class UserAgentInfo { /// /// Maximum number of characters allowed for the driver name. @@ -90,7 +90,7 @@ private enum OsType // P/Invoke signature for glibc detection [DllImport("libc", EntryPoint = "gnu_get_libc_version", CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr gnu_get_libc_version(); + private static extern nint gnu_get_libc_version(); static UserAgentInfo() { @@ -132,7 +132,7 @@ static UserAgentInfo() // 'driver', 'version', and 'os.type' // - If the payload exceeds 2,047 bytes but remains within sensible limits, we still send it, but note that // some servers may silently drop or reject such packets — behavior we may use for future probing or diagnostics. - + // - If payload exceeds 10KB even after dropping fields , we send an empty payload. driverName = TruncateOrDefault(DefaultDriverName, DriverNameMaxChars); version = TruncateOrDefault(ADP.GetAssemblyVersion().ToString(), VersionMaxChars); var osVal = DetectOsType(); @@ -152,7 +152,7 @@ static UserAgentInfo() Details = osDetails }, Arch = architecture, - Runtime = runtime, + Runtime = runtime }; @@ -377,7 +377,7 @@ private static string DetectRuntime() try { // P/Invoke into libc - IntPtr ptr = gnu_get_libc_version(); + nint ptr = gnu_get_libc_version(); string glibc = Marshal.PtrToStringAnsi(ptr); if (!string.IsNullOrWhiteSpace(glibc)) return "glibc " + glibc.Trim(); diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/UserAgentInfoDto.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfoDto.cs similarity index 97% rename from src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/UserAgentInfoDto.cs rename to src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfoDto.cs index 5f8046d62e..8f1426fbff 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/UserAgentInfoDto.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfoDto.cs @@ -1,6 +1,6 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Microsoft.Data.SqlClient +namespace Microsoft.Data.SqlClient.UserAgent { internal class UserAgentInfoDto { diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft.Data.SqlClient.UnitTests.csproj b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft.Data.SqlClient.UnitTests.csproj index 6d156b2bf3..2f0e12c922 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft.Data.SqlClient.UnitTests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft.Data.SqlClient.UnitTests.csproj @@ -48,7 +48,4 @@ xunit.runner.json - - - diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/UserAgentInfoTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/UserAgentInfoTests.cs index c49c83239d..e2c60f2f0a 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/UserAgentInfoTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/UserAgentInfoTests.cs @@ -2,7 +2,7 @@ using System.Reflection; using System.Text; using System.Text.Json; -using Microsoft.Data.SqlClient; +using Microsoft.Data.SqlClient.UserAgent; using Xunit; #nullable enable From 1884906653a8c44fff42105249a5e640683f931a Mon Sep 17 00:00:00 2001 From: Saransh Sharma Date: Mon, 21 Jul 2025 14:34:23 -0700 Subject: [PATCH 8/9] Resolve conflicts --- .../Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs | 4 ---- .../Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs | 9 --------- 2 files changed, 13 deletions(-) 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 83eb8eb101..4e94c9eee3 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 @@ -208,11 +208,7 @@ internal bool IsDNSCachingBeforeRedirectSupported internal bool IsJsonSupportEnabled = false; // User Agent Flag -<<<<<<< HEAD internal bool IsUserAgentSupportEnabled = true; -======= - internal bool IsUserAgentEnabled = true; ->>>>>>> 81edbdf31b7cc1b6f01620be7eaa198799c08db7 // Vector Support Flag internal bool IsVectorSupportEnabled = false; 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 aeb6dee76e..8cd1fff219 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 @@ -212,11 +212,7 @@ internal bool IsDNSCachingBeforeRedirectSupported internal bool IsVectorSupportEnabled = false; // User Agent Flag -<<<<<<< HEAD internal bool IsUserAgentSupportEnabled = true; -======= - internal bool IsUserAgentEnabled = true; ->>>>>>> 81edbdf31b7cc1b6f01620be7eaa198799c08db7 // TCE flags internal byte _tceVersionSupported; @@ -1444,11 +1440,6 @@ private void Login(ServerInfo server, TimeoutTimer timeout, string newPassword, requestedFeatures |= TdsEnums.FeatureExtension.VectorSupport; requestedFeatures |= TdsEnums.FeatureExtension.UserAgent; - - #if DEBUG - requestedFeatures |= TdsEnums.FeatureExtension.UserAgent; - #endif - _parser.TdsLogin(login, requestedFeatures, _recoverySessionData, _fedAuthFeatureExtensionData, encrypt); } From e30f51b1575d37b3a68ae9f1813ebba9c77910ea Mon Sep 17 00:00:00 2001 From: Saransh Sharma Date: Mon, 21 Jul 2025 15:23:47 -0700 Subject: [PATCH 9/9] Enable writing Json payload for UserAgent --- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 15 +++++++-- .../SqlClient/SqlInternalConnectionTds.cs | 1 + .../src/Microsoft/Data/SqlClient/TdsParser.cs | 15 +++++++-- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 10 +++++- .../Data/SqlClient/UserAgent/UserAgentInfo.cs | 9 ++++++ .../tools/TDS/TDS.Servers/GenericTDSServer.cs | 31 +++---------------- 6 files changed, 47 insertions(+), 34 deletions(-) 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 a2222d6f1e..3fc636f557 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 @@ -30,6 +30,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 @@ -8875,9 +8877,16 @@ private void WriteLoginData(SqlLogin rec, _physicalStateObj.WriteByteArray(encryptedChangePassword, encryptedChangePasswordLengthInBytes, 0); } } - // TODO: User Agent Json Payload will go here - byte[] emptyBytes = new byte[0]; - ApplyFeatureExData(requestedFeatures, recoverySessionData, fedAuthFeatureExtensionData, emptyBytes, useFeatureExt, length, true); + + ApplyFeatureExData( + requestedFeatures, + recoverySessionData, + fedAuthFeatureExtensionData, + UserAgentInfo.GetCachedPayload(), + useFeatureExt, + length, + true + ); } catch (Exception e) { 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 8cd1fff219..c3d72923be 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 @@ -3071,6 +3071,7 @@ internal void OnFeatureExtAck(int featureId, byte[] data) case TdsEnums.FEATUREEXT_USERAGENT: { + // Note: We do not expect an ACK for USERAGENT feature extension, but if we receive it, we will log it. SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Received feature extension acknowledgement for USERAGENT", ObjectID); if (data.Length != 1) { 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 3c6f78b570..b57071832b 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 @@ -30,6 +30,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 @@ -9074,9 +9076,16 @@ private void WriteLoginData(SqlLogin rec, _physicalStateObj.WriteByteArray(encryptedChangePassword, encryptedChangePasswordLengthInBytes, 0); } } - // TODO: User Agent Json Payload will go here - byte[] emptyBytes = new byte[0]; - ApplyFeatureExData(requestedFeatures, recoverySessionData, fedAuthFeatureExtensionData, emptyBytes, useFeatureExt, length, true); + + ApplyFeatureExData( + requestedFeatures, + recoverySessionData, + fedAuthFeatureExtensionData, + UserAgentInfo.GetCachedPayload(), + useFeatureExt, + length, + true + ); } catch (Exception e) { 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 5b1c5c039f..68a0e7fac4 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.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 @@ -194,7 +195,14 @@ internal void TdsLogin( // TODO: User Agent Json Payload will go here byte[] emptyBytes = new byte[0]; // calculate and reserve the required bytes for the featureEx - length = ApplyFeatureExData(requestedFeatures, recoverySessionData, fedAuthFeatureExtensionData,emptyBytes, useFeatureExt, length); + length = ApplyFeatureExData( + requestedFeatures, + recoverySessionData, + fedAuthFeatureExtensionData, + UserAgentInfo.GetCachedPayload(), + useFeatureExt, + length + ); WriteLoginData(rec, requestedFeatures, diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfo.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfo.cs index cca0e13894..ca5c86fcd8 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfo.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfo.cs @@ -416,6 +416,15 @@ private static string DetectRuntime() return DefaultJsonValue; } + /// + /// Retrieves a copy of the cached payload. + /// + /// A byte array containing a copy of the cached payload. The caller receives a clone of the original data to + /// ensure data integrity. + public static byte[] GetCachedPayload() + { + return (byte[])_cachedPayload.Clone(); + } } } 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 c81d100225..6c86020aaf 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 @@ -52,7 +52,7 @@ public delegate void OnAuthenticationCompletedDelegate( /// /// Default feature extension version supported on the server for user agent. /// - public const byte DefaultSupportedUserAgentFeatureExtVersion = 0x01; + public const byte DefaultSupportedUserAgentFeatureExtVersion = 0x0F; /// /// Property for setting server version for vector feature extension. @@ -678,32 +678,9 @@ protected virtual TDSMessageCollection OnAuthenticationCompleted(ITDSServerSessi featureExtAckToken.Options.Add(vectorSupportOption); } } - // Note: there should be case - //// Check if UserAgent is supported - //if (session.IsUserAgentSupportEnabled) - //{ - // // Create ack data (1 byte: Version number) - // byte[] data = new byte[1]; - // data[0] = ServerSupportedUserAgentFeatureExtVersion; - - // // Create vector support as a generic feature extension option - // TDSFeatureExtAckGenericOption userAgentSupportOption = new TDSFeatureExtAckGenericOption(TDSFeatureID.UserAgentSupport, (uint)data.Length, data); - - // // Look for feature extension token - // TDSFeatureExtAckToken featureExtAckToken = (TDSFeatureExtAckToken)responseMessage.Where(t => t is TDSFeatureExtAckToken).FirstOrDefault(); - - // if (featureExtAckToken == null) - // { - // // Create feature extension ack token - // featureExtAckToken = new TDSFeatureExtAckToken(userAgentSupportOption); - // responseMessage.Add(featureExtAckToken); - // } - // else - // { - // // Update the existing token - // featureExtAckToken.Options.Add(userAgentSupportOption); - // } - //} + + // Note: there can be a case here handling User Agent support, but since server + // should not actually ack this feature extension, we don't handle it here. // Create DONE token TDSDoneToken doneToken = new TDSDoneToken(TDSDoneTokenStatusType.Final);