Skip to content

Commit 80ffa81

Browse files
authored
Send user agent info as a new feature extension to server (#2848)
* POC change to send user agent via feature ext. * Change FE identifier and encoding to UCS16 * Removed ApplicationAttributes logic * Changed sanitizing logic to not replace non-ascii chars * Send user agent FE as the first FE * Removed trailing $1 from the string template * Removed commented code * Removed unused constant * User agent string format changes * Removed unneded constant * Changed default user agent string to include FE version * Added more tests * Added comments around tests
1 parent 8f3ddc4 commit 80ffa81

File tree

3 files changed

+155
-1
lines changed

3 files changed

+155
-1
lines changed

src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,8 @@ final class TDS {
178178
static final byte TDS_FEATURE_EXT_JSONSUPPORT = 0x0D;
179179
static final byte JSONSUPPORT_NOT_SUPPORTED = 0x00;
180180
static final byte MAX_JSONSUPPORT_VERSION = 0x01;
181+
// User agent telemetry support
182+
static final byte TDS_FEATURE_EXT_USERAGENT = 0x10;
181183

182184
static final int TDS_TVP = 0xF3;
183185
static final int TVP_ROW = 0x01;
@@ -251,7 +253,9 @@ static final String getTokenName(int tdsTokenType) {
251253
return "TDS_FEATURE_EXT_VECTORSUPPORT (0x0E)";
252254
case TDS_FEATURE_EXT_JSONSUPPORT:
253255
return "TDS_FEATURE_EXT_JSONSUPPORT (0x0D)";
254-
256+
case TDS_FEATURE_EXT_USERAGENT:
257+
return "TDS_FEATURE_EXT_USERAGENT (0x10)";
258+
255259
default:
256260
return "unknown token (0x" + Integer.toHexString(tdsTokenType).toUpperCase() + ")";
257261
}

src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,79 @@ public String toString() {
313313
**/
314314
private static final Lock sLock = new ReentrantLock();
315315

316+
static final String USER_AGENT_TEMPLATE = "%s|%s|%s|%s|%s|%s|%s";
317+
static final String USER_AGENT_EXT_VERSION_AND_DRIVER_NAME = "1|MS-JDBC";
318+
static final String userAgentStr;
319+
320+
static {
321+
userAgentStr = getUserAgent();
322+
}
323+
324+
static String getUserAgent() {
325+
try {
326+
return String.format(
327+
USER_AGENT_TEMPLATE,
328+
"1",
329+
"MS-JDBC",
330+
getJDBCVersion(),
331+
getOSType(),
332+
getOSDetails(),
333+
getArchitecture(),
334+
getRuntimeDetails()
335+
);
336+
} catch(Exception e) {
337+
return USER_AGENT_EXT_VERSION_AND_DRIVER_NAME;
338+
}
339+
}
340+
341+
static String getJDBCVersion() {
342+
return sanitizeField(SQLJdbcVersion.MAJOR + "." + SQLJdbcVersion.MINOR + "." + SQLJdbcVersion.PATCH + "." + SQLJdbcVersion.BUILD + SQLJdbcVersion.RELEASE_EXT, 24);
343+
}
344+
345+
static String getOSType() {
346+
String osName = System.getProperty("os.name", "Unknown").trim().toLowerCase();
347+
String osNameToReturn = "Unknown";
348+
if (osName.startsWith("windows")) {
349+
osNameToReturn = "Windows";
350+
} else if (osName.startsWith("linux")) {
351+
osNameToReturn = "Linux";
352+
} else if (osName.startsWith("mac")) {
353+
osNameToReturn = "macOS";
354+
} else if (osName.startsWith("freebsd")) {
355+
osNameToReturn = "FreeBSD";
356+
} else if (osName.startsWith("android")) {
357+
osNameToReturn = "Android";
358+
}
359+
return sanitizeField(osNameToReturn, 10);
360+
}
361+
362+
static String getArchitecture() {
363+
return sanitizeField(System.getProperty("os.arch", "Unknown").trim(), 10);
364+
}
365+
366+
static String getOSDetails() {
367+
String osName = System.getProperty("os.name", "").trim();
368+
String osVersion = System.getProperty("os.version", "").trim();
369+
if (osName.isEmpty() && osVersion.isEmpty()) {
370+
return "Unknown";
371+
}
372+
return sanitizeField(osName + " " + osVersion, 44);
373+
}
374+
375+
static String getRuntimeDetails() {
376+
String javaVmName = System.getProperty("java.vm.name", "").trim();
377+
String javaVmVersion = System.getProperty("java.vm.version", "").trim();
378+
if (javaVmName.isEmpty() && javaVmVersion.isEmpty()) {
379+
return "Unknown";
380+
}
381+
return sanitizeField(javaVmName + " " + javaVmVersion, 44);
382+
}
383+
384+
static String sanitizeField(String field, int maxLength) {
385+
String sanitized = field.replaceAll("[^A-Za-z0-9 .+_-]", "").trim();
386+
return (sanitized == null || sanitized.isEmpty()) ? "Unknown" : sanitized.substring(0, Math.min(sanitized.length(), maxLength));
387+
}
388+
316389
/**
317390
* Generate a 6 byte random array for netAddress
318391
* 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
57895862
return len;
57905863
}
57915864

5865+
/**
5866+
* Writes the user agent telemetry feature request
5867+
* @param write
5868+
* If true, writes the feature request to the physical state object.
5869+
* @param tdsWriter
5870+
* @return
5871+
* The length of the feature request in bytes, or 0 if vectorTypeSupport is "off".
5872+
* @throws SQLServerException
5873+
*/
5874+
int writeUserAgentFeatureRequest(boolean write, /* if false just calculates the length */
5875+
TDSWriter tdsWriter) throws SQLServerException {
5876+
byte[] userAgentToSendBytes = toUCS16(userAgentStr);
5877+
int len = userAgentToSendBytes.length + 5; // 1byte = featureID, 1byte = version, 4byte = feature data length in bytes, remaining bytes: feature data
5878+
if (write) {
5879+
tdsWriter.writeByte(TDS.TDS_FEATURE_EXT_USERAGENT);
5880+
tdsWriter.writeInt(userAgentToSendBytes.length);
5881+
tdsWriter.writeBytes(userAgentToSendBytes);
5882+
}
5883+
return len;
5884+
}
5885+
57925886
/**
57935887
* Writes the Vector Support feature request to the physical state object,
57945888
* unless vectorTypeSupport is "off". The request includes the feature ID,
@@ -7043,6 +7137,14 @@ private void onFeatureExtAck(byte featureId, byte[] data) throws SQLServerExcept
70437137
break;
70447138
}
70457139

7140+
case TDS.TDS_FEATURE_EXT_USERAGENT: {
7141+
if (connectionlogger.isLoggable(Level.FINER)) {
7142+
connectionlogger.fine(
7143+
toString() + " Received feature extension acknowledgement for User agent feature extension. Received byte: " + data[0]);
7144+
}
7145+
break;
7146+
}
7147+
70467148
default: {
70477149
// Unknown feature ack
70487150
throw new SQLServerException(SQLServerException.getErrString("R_UnknownFeatureAck"), null);
@@ -7330,6 +7432,9 @@ final boolean complete(LogonCommand logonCommand, TDSReader tdsReader) throws SQ
73307432
}
73317433

73327434
int aeOffset = len;
7435+
7436+
len += writeUserAgentFeatureRequest(false, tdsWriter);
7437+
73337438
// AE is always ON
73347439
len += writeAEFeatureRequest(false, tdsWriter);
73357440
if (federatedAuthenticationInfoRequested || federatedAuthenticationRequested) {
@@ -7534,6 +7639,9 @@ final boolean complete(LogonCommand logonCommand, TDSReader tdsReader) throws SQ
75347639
tdsWriter.writeBytes(secBlob, 0, secBlob.length);
75357640
}
75367641

7642+
//Write user agent string
7643+
writeUserAgentFeatureRequest(true, tdsWriter);
7644+
75377645
// AE is always ON
75387646
writeAEFeatureRequest(true, tdsWriter);
75397647

src/test/java/com/microsoft/sqlserver/jdbc/SQLServerDriverTest.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import static org.junit.jupiter.api.Assertions.assertFalse;
66
import static org.junit.jupiter.api.Assertions.assertNotNull;
77
import static org.junit.jupiter.api.Assertions.assertTrue;
8+
import static org.mockito.Mockito.mockStatic;
89

910
import java.sql.Connection;
1011
import java.sql.Driver;
@@ -22,6 +23,7 @@
2223
import org.junit.jupiter.api.Test;
2324
import org.junit.platform.runner.JUnitPlatform;
2425
import org.junit.runner.RunWith;
26+
import org.mockito.MockedStatic;
2527

2628
import com.microsoft.sqlserver.testframework.AbstractTest;
2729
import com.microsoft.sqlserver.testframework.Constants;
@@ -266,4 +268,44 @@ public void testApplicationName() throws SQLException {
266268
// String defaultAppName = SQLServerDriver.getAppName();
267269
// assertEquals(SQLServerDriver.DEFAULT_APP_NAME, defaultAppName, "Application name should be the default one");
268270
// }
271+
272+
/**
273+
* test user agent string length
274+
*
275+
* @throws SQLException
276+
*/
277+
@Test
278+
public void testDriverUserAgentLength() throws SQLException {
279+
String userAgent = SQLServerConnection.getUserAgent();
280+
assertTrue(userAgent.length() <= 256, "User agent string length should not be more than 256 characters");
281+
}
282+
283+
/**
284+
* test user agent string OS
285+
*
286+
* @throws SQLException
287+
*/
288+
@Test
289+
public void testDriverUserAgentOS() throws SQLException {
290+
System.setProperty("os.name", "Linux");
291+
String userAgent = SQLServerConnection.getUserAgent();
292+
assertTrue(userAgent.contains("Linux"), "User agent string must contain Linux");
293+
294+
System.setProperty("os.name", "Mac");
295+
userAgent = SQLServerConnection.getUserAgent();
296+
assertTrue(userAgent.contains("macOS"), "User agent string must contain macOS");
297+
298+
System.setProperty("os.name", "FreeBSD");
299+
userAgent = SQLServerConnection.getUserAgent();
300+
assertTrue(userAgent.contains("FreeBSD"), "User agent string must contain FreeBSD");
301+
302+
System.setProperty("os.name", "Android");
303+
userAgent = SQLServerConnection.getUserAgent();
304+
assertTrue(userAgent.contains("Android"), "User agent string must contain Android");
305+
306+
System.setProperty("os.name", "Windows");
307+
userAgent = SQLServerConnection.getUserAgent();
308+
assertTrue(userAgent.contains("Windows"), "User agent string must contain Windows");
309+
310+
}
269311
}

0 commit comments

Comments
 (0)