diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java index 2bbdca66e..1e61e56a9 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java @@ -178,6 +178,8 @@ final class TDS { static final byte TDS_FEATURE_EXT_JSONSUPPORT = 0x0D; static final byte JSONSUPPORT_NOT_SUPPORTED = 0x00; static final byte MAX_JSONSUPPORT_VERSION = 0x01; + // User agent telemetry support + static final byte TDS_FEATURE_EXT_USERAGENT = 0x10; static final int TDS_TVP = 0xF3; static final int TVP_ROW = 0x01; @@ -251,7 +253,9 @@ static final String getTokenName(int tdsTokenType) { return "TDS_FEATURE_EXT_VECTORSUPPORT (0x0E)"; case TDS_FEATURE_EXT_JSONSUPPORT: return "TDS_FEATURE_EXT_JSONSUPPORT (0x0D)"; - + case TDS_FEATURE_EXT_USERAGENT: + return "TDS_FEATURE_EXT_USERAGENT (0x10)"; + default: return "unknown token (0x" + Integer.toHexString(tdsTokenType).toUpperCase() + ")"; } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index 8ff2069a1..9e6953023 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -313,6 +313,79 @@ public String toString() { **/ private static final Lock sLock = new ReentrantLock(); + static final String USER_AGENT_TEMPLATE = "%s|%s|%s|%s|%s|%s|%s"; + static final String USER_AGENT_EXT_VERSION_AND_DRIVER_NAME = "1|MS-JDBC"; + static final String userAgentStr; + + static { + userAgentStr = getUserAgent(); + } + + static String getUserAgent() { + try { + return String.format( + USER_AGENT_TEMPLATE, + "1", + "MS-JDBC", + getJDBCVersion(), + getOSType(), + getOSDetails(), + getArchitecture(), + getRuntimeDetails() + ); + } catch(Exception e) { + return USER_AGENT_EXT_VERSION_AND_DRIVER_NAME; + } + } + + static String getJDBCVersion() { + return sanitizeField(SQLJdbcVersion.MAJOR + "." + SQLJdbcVersion.MINOR + "." + SQLJdbcVersion.PATCH + "." + SQLJdbcVersion.BUILD + SQLJdbcVersion.RELEASE_EXT, 24); + } + + static String getOSType() { + String osName = System.getProperty("os.name", "Unknown").trim().toLowerCase(); + String osNameToReturn = "Unknown"; + if (osName.startsWith("windows")) { + osNameToReturn = "Windows"; + } else if (osName.startsWith("linux")) { + osNameToReturn = "Linux"; + } else if (osName.startsWith("mac")) { + osNameToReturn = "macOS"; + } else if (osName.startsWith("freebsd")) { + osNameToReturn = "FreeBSD"; + } else if (osName.startsWith("android")) { + osNameToReturn = "Android"; + } + return sanitizeField(osNameToReturn, 10); + } + + static String getArchitecture() { + return sanitizeField(System.getProperty("os.arch", "Unknown").trim(), 10); + } + + static String getOSDetails() { + String osName = System.getProperty("os.name", "").trim(); + String osVersion = System.getProperty("os.version", "").trim(); + if (osName.isEmpty() && osVersion.isEmpty()) { + return "Unknown"; + } + return sanitizeField(osName + " " + osVersion, 44); + } + + static String getRuntimeDetails() { + String javaVmName = System.getProperty("java.vm.name", "").trim(); + String javaVmVersion = System.getProperty("java.vm.version", "").trim(); + if (javaVmName.isEmpty() && javaVmVersion.isEmpty()) { + return "Unknown"; + } + return sanitizeField(javaVmName + " " + javaVmVersion, 44); + } + + static String sanitizeField(String field, int maxLength) { + String sanitized = field.replaceAll("[^A-Za-z0-9 .+_-]", "").trim(); + return (sanitized == null || sanitized.isEmpty()) ? "Unknown" : sanitized.substring(0, Math.min(sanitized.length(), maxLength)); + } + /** * Generate a 6 byte random array for netAddress * As per TDS spec this is a unique clientID (MAC address) used to identify the client. @@ -5789,6 +5862,27 @@ int writeDNSCacheFeatureRequest(boolean write, /* if false just calculates the l return len; } + /** + * Writes the user agent telemetry feature request + * @param write + * If true, writes the feature request to the physical state object. + * @param tdsWriter + * @return + * The length of the feature request in bytes, or 0 if vectorTypeSupport is "off". + * @throws SQLServerException + */ + int writeUserAgentFeatureRequest(boolean write, /* if false just calculates the length */ + TDSWriter tdsWriter) throws SQLServerException { + byte[] userAgentToSendBytes = toUCS16(userAgentStr); + int len = userAgentToSendBytes.length + 5; // 1byte = featureID, 1byte = version, 4byte = feature data length in bytes, remaining bytes: feature data + if (write) { + tdsWriter.writeByte(TDS.TDS_FEATURE_EXT_USERAGENT); + tdsWriter.writeInt(userAgentToSendBytes.length); + tdsWriter.writeBytes(userAgentToSendBytes); + } + return len; + } + /** * Writes the Vector Support feature request to the physical state object, * unless vectorTypeSupport is "off". The request includes the feature ID, @@ -7043,6 +7137,14 @@ private void onFeatureExtAck(byte featureId, byte[] data) throws SQLServerExcept break; } + case TDS.TDS_FEATURE_EXT_USERAGENT: { + if (connectionlogger.isLoggable(Level.FINER)) { + connectionlogger.fine( + toString() + " Received feature extension acknowledgement for User agent feature extension. Received byte: " + data[0]); + } + break; + } + default: { // Unknown feature ack throw new SQLServerException(SQLServerException.getErrString("R_UnknownFeatureAck"), null); @@ -7330,6 +7432,9 @@ final boolean complete(LogonCommand logonCommand, TDSReader tdsReader) throws SQ } int aeOffset = len; + + len += writeUserAgentFeatureRequest(false, tdsWriter); + // AE is always ON len += writeAEFeatureRequest(false, tdsWriter); if (federatedAuthenticationInfoRequested || federatedAuthenticationRequested) { @@ -7534,6 +7639,9 @@ final boolean complete(LogonCommand logonCommand, TDSReader tdsReader) throws SQ tdsWriter.writeBytes(secBlob, 0, secBlob.length); } + //Write user agent string + writeUserAgentFeatureRequest(true, tdsWriter); + // AE is always ON writeAEFeatureRequest(true, tdsWriter); diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerDriverTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerDriverTest.java index be9333e24..6290b5904 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerDriverTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerDriverTest.java @@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mockStatic; import java.sql.Connection; import java.sql.Driver; @@ -22,6 +23,7 @@ import org.junit.jupiter.api.Test; import org.junit.platform.runner.JUnitPlatform; import org.junit.runner.RunWith; +import org.mockito.MockedStatic; import com.microsoft.sqlserver.testframework.AbstractTest; import com.microsoft.sqlserver.testframework.Constants; @@ -266,4 +268,44 @@ public void testApplicationName() throws SQLException { // String defaultAppName = SQLServerDriver.getAppName(); // assertEquals(SQLServerDriver.DEFAULT_APP_NAME, defaultAppName, "Application name should be the default one"); // } + + /** + * test user agent string length + * + * @throws SQLException + */ + @Test + public void testDriverUserAgentLength() throws SQLException { + String userAgent = SQLServerConnection.getUserAgent(); + assertTrue(userAgent.length() <= 256, "User agent string length should not be more than 256 characters"); + } + + /** + * test user agent string OS + * + * @throws SQLException + */ + @Test + public void testDriverUserAgentOS() throws SQLException { + System.setProperty("os.name", "Linux"); + String userAgent = SQLServerConnection.getUserAgent(); + assertTrue(userAgent.contains("Linux"), "User agent string must contain Linux"); + + System.setProperty("os.name", "Mac"); + userAgent = SQLServerConnection.getUserAgent(); + assertTrue(userAgent.contains("macOS"), "User agent string must contain macOS"); + + System.setProperty("os.name", "FreeBSD"); + userAgent = SQLServerConnection.getUserAgent(); + assertTrue(userAgent.contains("FreeBSD"), "User agent string must contain FreeBSD"); + + System.setProperty("os.name", "Android"); + userAgent = SQLServerConnection.getUserAgent(); + assertTrue(userAgent.contains("Android"), "User agent string must contain Android"); + + System.setProperty("os.name", "Windows"); + userAgent = SQLServerConnection.getUserAgent(); + assertTrue(userAgent.contains("Windows"), "User agent string must contain Windows"); + + } }