Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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() + ")";
}
Expand Down
108 changes: 108 additions & 0 deletions src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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");

}
}
Loading