fix(sqlserver): preserve legacy encryption fallback

This commit is contained in:
t8y2 2026-07-08 09:21:21 +08:00
parent f23894d816
commit 0747134dc2
1 changed files with 42 additions and 21 deletions

View File

@ -18,8 +18,14 @@ pub type SqlServerClient = Client<Compat<TcpStream>>;
pub const SQLSERVER_DRIVER_PANIC_ERROR_PREFIX: &str = "SQL Server driver panic:";
const SIMPLE_QUERY_MODULE_KEYWORDS: &[&str] = &["FUNCTION", "PROC", "PROCEDURE", "TRIGGER", "VIEW"];
// Match JDBC/tiberius `encrypt=false`: encrypt only login, then drop back to raw TDS.
// `NotSupported` sends ENCRYPT_NOT_SUP and is rejected by some legacy SQL Server setups.
const SQLSERVER_LEGACY_ENCRYPTION_LEVEL: tiberius::EncryptionLevel = tiberius::EncryptionLevel::Off;
// Some very old SQL Server setups only accepted DBX <= 0.5.48 because the fallback
// advertised no encryption support at all. Keep it as the last-resort compatibility path.
const SQLSERVER_UNSUPPORTED_ENCRYPTION_LEVEL: tiberius::EncryptionLevel = tiberius::EncryptionLevel::NotSupported;
const SQLSERVER_LEGACY_ENCRYPTION_FALLBACKS: [(&str, tiberius::EncryptionLevel); 2] = [
("login-only encryption", SQLSERVER_LEGACY_ENCRYPTION_LEVEL),
("no-encryption compatibility fallback", SQLSERVER_UNSUPPORTED_ENCRYPTION_LEVEL),
];
#[derive(Debug, PartialEq, Eq)]
struct SqlServerEndpoint<'a> {
@ -51,17 +57,17 @@ pub async fn connect(
timeout: Duration,
) -> Result<SqlServerClient, String> {
if sqlserver_legacy_encryption_disabled(url_params) {
return try_connect(host, port, user, pass, database, SQLSERVER_LEGACY_ENCRYPTION_LEVEL, timeout).await;
return try_connect_legacy_sqlserver_encryption(host, port, user, pass, database, timeout).await;
}
match try_connect(host, port, user, pass, database, tiberius::EncryptionLevel::Required, timeout).await {
Ok(client) => Ok(client),
Err(encrypted_error) => {
try_connect(host, port, user, pass, database, SQLSERVER_LEGACY_ENCRYPTION_LEVEL, timeout).await.map_err(
|plain_error| {
if is_sqlserver_tls_handshake_error(&encrypted_error) {
format!(
"{encrypted_error}\n\nThis may be caused by an old SQL Server TLS/encryption configuration. \
Err(encrypted_error) => try_connect_legacy_sqlserver_encryption(host, port, user, pass, database, timeout)
.await
.map_err(|plain_error| {
if is_sqlserver_tls_handshake_error(&encrypted_error) {
format!(
"{encrypted_error}\n\nThis may be caused by an old SQL Server TLS/encryption configuration. \
If you are connecting to SQL Server 2008/2008 R2/2012 or another legacy instance, \
try SQL Server legacy unencrypted mode. It behaves like encrypt=false and only helps \
when the server allows unencrypted transport or login-only encryption. It will still fail \
@ -69,14 +75,31 @@ pub async fn connect(
Only use this mode on trusted networks, VPNs, \
or SSH tunnels.\n\n\
Automatic legacy unencrypted fallback also failed: {plain_error}"
)
} else {
plain_error
}
},
)
)
} else {
plain_error
}
}),
}
}
async fn try_connect_legacy_sqlserver_encryption(
host: &str,
port: u16,
user: &str,
pass: &str,
database: Option<&str>,
timeout: Duration,
) -> Result<SqlServerClient, String> {
let mut errors = Vec::new();
for (label, encryption) in SQLSERVER_LEGACY_ENCRYPTION_FALLBACKS {
match try_connect(host, port, user, pass, database, encryption, timeout).await {
Ok(client) => return Ok(client),
Err(error) => errors.push(format!("{label} failed: {error}")),
}
}
Err(errors.join("\n"))
}
fn sqlserver_legacy_encryption_disabled(url_params: Option<&str>) -> bool {
@ -1876,17 +1899,15 @@ mod tests {
}
#[test]
fn sqlserver_legacy_encryption_mode_matches_jdbc_encrypt_false_semantics() {
fn sqlserver_legacy_encryption_modes_cover_jdbc_and_no_encryption_fallback() {
assert_eq!(super::SQLSERVER_LEGACY_ENCRYPTION_LEVEL, tiberius::EncryptionLevel::Off);
assert_eq!(super::SQLSERVER_UNSUPPORTED_ENCRYPTION_LEVEL, tiberius::EncryptionLevel::NotSupported);
}
#[test]
fn sqlserver_automatic_fallback_uses_legacy_encryption_mode() {
let source = include_str!("sqlserver.rs");
let connect = source.split("pub async fn connect").nth(1).unwrap();
let connect = connect.split("fn sqlserver_legacy_encryption_disabled").next().unwrap();
assert!(connect.contains("database, SQLSERVER_LEGACY_ENCRYPTION_LEVEL, timeout"));
assert!(!connect.contains("EncryptionLevel::NotSupported, timeout"));
fn sqlserver_automatic_fallback_preserves_v48_no_encryption_compatibility() {
let levels = super::SQLSERVER_LEGACY_ENCRYPTION_FALLBACKS.map(|(_, encryption)| encryption);
assert_eq!(levels, [tiberius::EncryptionLevel::Off, tiberius::EncryptionLevel::NotSupported]);
}
#[test]