fix(sqlserver): attribute login and catalog errors, keep sidebar search read-only
This commit is contained in:
16 files changed
+542
-35
No files matched your search
@@ -310,25 +310,86 @@ pub async fn connect_with_port_explicit(
|
||||
Err(encrypted_error) => {
|
||||
try_connect_legacy_sqlserver_encryption(host, port, port_explicit, 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 compatibility mode. It first behaves like encrypt=false and, \
|
||||
when explicitly enabled, DBX can also fall back to the SQL Server legacy compatibility \
|
||||
driver for TLS 1.0 encrypted transport. Only use this mode on trusted networks, VPNs, \
|
||||
or SSH tunnels.\n\n\
|
||||
Automatic native legacy fallback also failed: {plain_error}"
|
||||
)
|
||||
} else {
|
||||
plain_error
|
||||
}
|
||||
})
|
||||
.map_err(|fallback_errors| sqlserver_connect_failure_message(&encrypted_error, &fallback_errors))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SQLSERVER_LEGACY_TLS_HINT: &str = "This 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 compatibility mode. It first behaves like encrypt=false and, \
|
||||
when explicitly enabled, DBX can also fall back to the SQL Server legacy compatibility \
|
||||
driver for TLS 1.0 encrypted transport. Only use this mode on trusted networks, VPNs, \
|
||||
or SSH tunnels.";
|
||||
|
||||
/// Login/catalog error numbers reported by the server itself. They mean the TDS transport was
|
||||
/// established, so the actionable fix is the login or the configured database, not TLS.
|
||||
const SQLSERVER_LOGIN_OR_CATALOG_ERROR_NUMBERS: [u32; 8] = [4060, 18452, 18456, 18470, 18486, 18487, 18488, 18489];
|
||||
|
||||
fn sqlserver_legacy_fallback_summary(errors: &[(&'static str, String)]) -> String {
|
||||
errors.iter().map(|(label, error)| format!("{label} failed: {error}")).collect::<Vec<_>>().join("\n")
|
||||
}
|
||||
|
||||
/// Extracts the error number from a driver message such as
|
||||
/// `... on server dbx executing on line 1 (code: 4060, state: 1, class: 11)`.
|
||||
fn sqlserver_error_number(error: &str) -> Option<u32> {
|
||||
let lower = error.to_ascii_lowercase();
|
||||
let rest = lower.split_once("(code:")?.1;
|
||||
let digits = rest.trim_start().chars().take_while(char::is_ascii_digit).collect::<String>();
|
||||
digits.parse().ok()
|
||||
}
|
||||
|
||||
fn is_sqlserver_login_or_catalog_error(error: &str) -> bool {
|
||||
if sqlserver_error_number(error).is_some_and(|code| SQLSERVER_LOGIN_OR_CATALOG_ERROR_NUMBERS.contains(&code)) {
|
||||
return true;
|
||||
}
|
||||
let lower = error.to_ascii_lowercase();
|
||||
[
|
||||
"login failed",
|
||||
"cannot open database",
|
||||
"not allowed to access",
|
||||
"is not able to access the database",
|
||||
"token error",
|
||||
]
|
||||
.iter()
|
||||
.any(|needle| lower.contains(needle))
|
||||
}
|
||||
|
||||
/// Builds the final `connect` error for the encryption cascade.
|
||||
///
|
||||
/// A fallback that reached the login/catalog stage proves the transport works without modern
|
||||
/// encryption, so that error is the real cause and belongs on the first line: the TLS hint would
|
||||
/// otherwise send users to their TLS configuration while the actual failure is a database that
|
||||
/// cannot be opened (issue: SQL Server 2014 with a saved database that no longer exists).
|
||||
fn sqlserver_connect_failure_message(encrypted_error: &str, fallback_errors: &[(&'static str, String)]) -> String {
|
||||
let legacy_summary = sqlserver_legacy_fallback_summary(fallback_errors);
|
||||
if let Some((label, error)) =
|
||||
fallback_errors.iter().rev().find(|(_, error)| is_sqlserver_login_or_catalog_error(error))
|
||||
{
|
||||
let mut message = error.clone();
|
||||
if is_sqlserver_tls_handshake_error(encrypted_error) {
|
||||
message.push_str(&format!(
|
||||
"\n\nDBX reached the server through the SQL Server legacy compatibility fallbacks ({label}), \
|
||||
so this failure is not caused by TLS/encryption settings. Check the database name, the login, \
|
||||
and its permissions for this connection; enable SQL Server legacy compatibility mode to skip \
|
||||
the failing encrypted handshake.\n\nInitial encrypted connection failed with: {encrypted_error}"
|
||||
));
|
||||
}
|
||||
return message;
|
||||
}
|
||||
if is_sqlserver_login_or_catalog_error(encrypted_error) {
|
||||
return format!(
|
||||
"{encrypted_error}\n\nThe SQL Server legacy compatibility fallbacks also failed:\n{legacy_summary}"
|
||||
);
|
||||
}
|
||||
if is_sqlserver_tls_handshake_error(encrypted_error) {
|
||||
return format!(
|
||||
"{encrypted_error}\n\n{SQLSERVER_LEGACY_TLS_HINT}\n\nAutomatic native legacy fallback also failed: {legacy_summary}"
|
||||
);
|
||||
}
|
||||
legacy_summary
|
||||
}
|
||||
|
||||
async fn try_connect_legacy_sqlserver_encryption(
|
||||
host: &str,
|
||||
port: u16,
|
||||
@@ -337,16 +398,16 @@ async fn try_connect_legacy_sqlserver_encryption(
|
||||
pass: &str,
|
||||
database: Option<&str>,
|
||||
timeout: Duration,
|
||||
) -> Result<SqlServerClient, String> {
|
||||
) -> Result<SqlServerClient, Vec<(&'static str, String)>> {
|
||||
let mut errors = Vec::new();
|
||||
for (label, encryption) in SQLSERVER_LEGACY_ENCRYPTION_FALLBACKS {
|
||||
match try_connect(host, port, port_explicit, user, pass, database, encryption, timeout).await {
|
||||
Ok(client) => return Ok(client),
|
||||
Err(error) => errors.push(format!("{label} failed: {error}")),
|
||||
Err(error) => errors.push((label, error)),
|
||||
}
|
||||
}
|
||||
|
||||
Err(errors.join("\n"))
|
||||
Err(errors)
|
||||
}
|
||||
|
||||
pub fn sqlserver_native_encryption_disabled(url_params: Option<&str>) -> bool {
|
||||
@@ -4378,6 +4439,94 @@ mod tests {
|
||||
assert!(!super::is_sqlserver_tls_handshake_error("SQL Server connection failed: Login failed for user"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sqlserver_login_error_detection_covers_server_codes_and_messages() {
|
||||
assert!(super::is_sqlserver_login_or_catalog_error(
|
||||
"SQL Server connection failed: Token error: 'Cannot open database \"cwxt2025\" requested by the login. \
|
||||
The login failed.' on server dbx executing on line 1 (code: 4060, state: 1, class: 11)"
|
||||
));
|
||||
assert!(super::is_sqlserver_login_or_catalog_error(
|
||||
"SQL Server connection failed: Login failed for user 'sa'. (code: 18456, state: 1, class: 14)"
|
||||
));
|
||||
assert!(super::is_sqlserver_login_or_catalog_error("Login failed for user 'readonly'."));
|
||||
// Localized server text (zh-CN) keeps no english needle, so "token error" has to carry it.
|
||||
assert!(super::is_sqlserver_login_or_catalog_error(
|
||||
"SQL Server connection failed: Token error: '\u{65e0}\u{6cd5}\u{6253}\u{5f00}\u{767b}\u{5f55}\u{6240}\u{8bf7}\u{6c42}\u{7684}\u{6570}\u{636e}\u{5e93} \"cwxt2025\"\u{3002}\u{767b}\u{5f55}\u{5931}\u{8d25}\u{3002}'"
|
||||
));
|
||||
assert!(!super::is_sqlserver_login_or_catalog_error(
|
||||
"SQL Server connection failed: An error occured during the attempt of performing I/O: tls handshake eof"
|
||||
));
|
||||
assert!(!super::is_sqlserver_login_or_catalog_error("SQL Server connection timed out (30s)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sqlserver_login_failure_outranks_the_legacy_tls_hint() {
|
||||
// The reported case: the encrypted handshake fails, but the no-encryption fallback reaches
|
||||
// the login stage and the server rejects the configured database (4060). The catalog error is
|
||||
// the actionable one and must not be buried under the TLS hint. The same instance reports the
|
||||
// server text in its own language (zh-CN here), so both spellings have to win.
|
||||
let encrypted_error =
|
||||
"SQL Server connection failed: An error occured during the attempt of performing I/O: tls handshake eof";
|
||||
let catalog_failures = [
|
||||
"SQL Server connection failed: Token error: 'Cannot open database \"cwxt2025\" requested by the login. \
|
||||
The login failed.' on server dbx executing on line 1 (code: 4060, state: 1, class: 11)",
|
||||
"SQL Server connection failed: Token error: '\u{65e0}\u{6cd5}\u{6253}\u{5f00}\u{767b}\u{5f55}\u{6240}\u{8bf7}\u{6c42}\u{7684}\u{6570}\u{636e}\u{5e93} \"cwxt2025\"\u{3002}\u{767b}\u{5f55}\u{5931}\u{8d25}\u{3002}' on server iZw1wl8nyooomlZ executing on line 1 (code: 4060, state: 1, class: 11)",
|
||||
];
|
||||
|
||||
for catalog_failure in catalog_failures {
|
||||
let fallback_errors = vec![
|
||||
("login-only encryption", encrypted_error.to_string()),
|
||||
("no-encryption compatibility fallback", catalog_failure.to_string()),
|
||||
];
|
||||
|
||||
let message = super::sqlserver_connect_failure_message(encrypted_error, &fallback_errors);
|
||||
|
||||
assert!(message.starts_with(catalog_failure), "{message}");
|
||||
assert!(message.contains("not caused by TLS/encryption settings"));
|
||||
assert!(message.contains("Initial encrypted connection failed with:"));
|
||||
assert!(!message.contains("This may be caused by an old SQL Server TLS/encryption configuration"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sqlserver_initial_login_failure_keeps_its_priority_over_fallback_transport_errors() {
|
||||
let login_error =
|
||||
"SQL Server connection failed: Login failed for user 'sa'. (code: 18456, state: 1, class: 14)";
|
||||
let fallback_errors = vec![
|
||||
(
|
||||
"login-only encryption",
|
||||
"SQL Server connection failed: An error occured during the attempt of performing I/O: tls handshake eof"
|
||||
.to_string(),
|
||||
),
|
||||
(
|
||||
"no-encryption compatibility fallback",
|
||||
"SQL Server connection failed: An error occured during the attempt of performing I/O: tls handshake eof"
|
||||
.to_string(),
|
||||
),
|
||||
];
|
||||
|
||||
let message = super::sqlserver_connect_failure_message(login_error, &fallback_errors);
|
||||
|
||||
assert!(message.starts_with("SQL Server connection failed: Login failed for user 'sa'."));
|
||||
assert!(message.contains("legacy compatibility fallbacks also failed"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sqlserver_all_tls_failures_keep_the_legacy_compatibility_hint() {
|
||||
let encrypted_error =
|
||||
"SQL Server connection failed: An error occured during the attempt of performing I/O: tls handshake eof";
|
||||
let fallback_errors = vec![
|
||||
("login-only encryption", encrypted_error.to_string()),
|
||||
("no-encryption compatibility fallback", encrypted_error.to_string()),
|
||||
];
|
||||
|
||||
let message = super::sqlserver_connect_failure_message(encrypted_error, &fallback_errors);
|
||||
|
||||
assert!(message.starts_with(encrypted_error));
|
||||
assert!(message.contains("This may be caused by an old SQL Server TLS/encryption configuration"));
|
||||
assert!(message.contains("Automatic native legacy fallback also failed:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sqlserver_module_definitions_require_simple_query_batch() {
|
||||
assert!(requires_simple_query_batch("SET SHOWPLAN_XML ON;"));
|
||||
|
||||
Reference in new issue
Block a user