Feature:支持 SQL Server 旧版兼容模式 (#2181)
* fix: 支持 SQL Server 旧版非加密重试 * fix: 优化 SQL Server 旧版兼容模式入口 --------- Co-authored-by: staff <staff@qimaos-MacBook-Pro.local>
This commit is contained in:
1 parent
8747a0e8f9
commit
d04463ebeb
7 files changed
+136
-14
No files matched your search
@@ -881,6 +881,7 @@ impl AppState {
|
||||
&db_config.username,
|
||||
&db_config.password,
|
||||
db_config.database.as_deref(),
|
||||
db_config.url_params.as_deref(),
|
||||
connect_timeout,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -17,6 +17,7 @@ use tokio_util::sync::CancellationToken;
|
||||
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"];
|
||||
const SQLSERVER_LEGACY_ENCRYPTION_LEVEL: tiberius::EncryptionLevel = tiberius::EncryptionLevel::Off;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct SqlServerEndpoint<'a> {
|
||||
@@ -44,12 +45,51 @@ pub async fn connect(
|
||||
user: &str,
|
||||
pass: &str,
|
||||
database: Option<&str>,
|
||||
url_params: Option<&str>,
|
||||
timeout: Duration,
|
||||
) -> Result<SqlServerClient, String> {
|
||||
match try_connect(host, port, user, pass, database, true, timeout).await {
|
||||
Ok(client) => Ok(client),
|
||||
Err(_) => try_connect(host, port, user, pass, database, false, timeout).await,
|
||||
if sqlserver_legacy_encryption_disabled(url_params) {
|
||||
return try_connect(host, port, user, pass, database, SQLSERVER_LEGACY_ENCRYPTION_LEVEL, 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, tiberius::EncryptionLevel::NotSupported, 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 configuration. \
|
||||
If you are connecting to SQL Server 2008/2008 R2 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 \
|
||||
if the server requires TLS 1.0 encryption. Only use this mode on trusted networks, VPNs, \
|
||||
or SSH tunnels.\n\n\
|
||||
Automatic unencrypted fallback also failed: {plain_error}"
|
||||
)
|
||||
} else {
|
||||
plain_error
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn sqlserver_legacy_encryption_disabled(url_params: Option<&str>) -> bool {
|
||||
let Some(params) = url_params.map(str::trim).filter(|params| !params.is_empty()) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
params.trim_start_matches('?').split(['&', ';']).filter_map(|pair| pair.split_once('=')).any(|(key, value)| {
|
||||
key.trim().eq_ignore_ascii_case("sqlserverEncryption")
|
||||
&& matches!(value.trim().to_ascii_lowercase().as_str(), "disabled" | "disable" | "false" | "0" | "off")
|
||||
})
|
||||
}
|
||||
|
||||
fn is_sqlserver_tls_handshake_error(error: &str) -> bool {
|
||||
let error = error.to_ascii_lowercase();
|
||||
error.contains("tls") && (error.contains("handshake") || error.contains("eof") || error.contains("performing i/o"))
|
||||
}
|
||||
|
||||
async fn try_connect(
|
||||
@@ -58,7 +98,7 @@ async fn try_connect(
|
||||
user: &str,
|
||||
pass: &str,
|
||||
database: Option<&str>,
|
||||
use_encryption: bool,
|
||||
encryption: tiberius::EncryptionLevel,
|
||||
timeout: Duration,
|
||||
) -> Result<SqlServerClient, String> {
|
||||
let mut config = Config::new();
|
||||
@@ -74,9 +114,7 @@ async fn try_connect(
|
||||
config.database(db);
|
||||
}
|
||||
config.trust_cert();
|
||||
if !use_encryption {
|
||||
config.encryption(tiberius::EncryptionLevel::NotSupported);
|
||||
}
|
||||
config.encryption(encryption);
|
||||
|
||||
let tcp = if endpoint.instance_name.is_some() {
|
||||
tokio::time::timeout(timeout, TcpStream::connect_named(&config))
|
||||
@@ -1817,6 +1855,29 @@ mod tests {
|
||||
assert!(try_connect.contains("connect_named(&config)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sqlserver_legacy_encryption_flag_is_opt_in() {
|
||||
assert!(!super::sqlserver_legacy_encryption_disabled(None));
|
||||
assert!(!super::sqlserver_legacy_encryption_disabled(Some("encrypt=false")));
|
||||
assert!(super::sqlserver_legacy_encryption_disabled(Some("sqlserverEncryption=disabled")));
|
||||
assert!(super::sqlserver_legacy_encryption_disabled(Some("applicationName=dbx;sqlserverEncryption=off")));
|
||||
assert!(super::sqlserver_legacy_encryption_disabled(Some("?sqlserverEncryption=false&applicationName=dbx")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sqlserver_legacy_encryption_mode_matches_jdbc_encrypt_false_semantics() {
|
||||
assert_eq!(super::SQLSERVER_LEGACY_ENCRYPTION_LEVEL, tiberius::EncryptionLevel::Off);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sqlserver_tls_handshake_error_detection_matches_legacy_hint_cases() {
|
||||
assert!(super::is_sqlserver_tls_handshake_error(
|
||||
"SQL Server connection failed: An error occured during the attempt of performing I/O: tls handshake eof"
|
||||
));
|
||||
assert!(super::is_sqlserver_tls_handshake_error("TLS handshake failed: unexpected EOF"));
|
||||
assert!(!super::is_sqlserver_tls_handshake_error("SQL Server connection failed: Login failed for user"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sqlserver_module_definitions_require_simple_query_batch() {
|
||||
assert!(requires_simple_query_batch("CREATE SCHEMA [analytics];"));
|
||||
|
||||
@@ -63,7 +63,7 @@ async fn live_sqlserver_execute_query_creates_schema() {
|
||||
let user = std::env::var("DBX_LIVE_SQLSERVER_USER").unwrap_or_else(|_| "sa".to_string());
|
||||
let password = std::env::var("DBX_LIVE_SQLSERVER_PASSWORD").expect("DBX_LIVE_SQLSERVER_PASSWORD");
|
||||
let mut client =
|
||||
dbx_core::db::sqlserver::connect(&host, port, &user, &password, Some(&database), Duration::from_secs(10))
|
||||
dbx_core::db::sqlserver::connect(&host, port, &user, &password, Some(&database), None, Duration::from_secs(10))
|
||||
.await
|
||||
.expect("connect SQL Server");
|
||||
|
||||
@@ -94,7 +94,7 @@ async fn live_sqlserver_stream_first_result_set_exports_cte_query_rows() {
|
||||
let user = std::env::var("DBX_LIVE_SQLSERVER_USER").unwrap_or_else(|_| "sa".to_string());
|
||||
let password = std::env::var("DBX_LIVE_SQLSERVER_PASSWORD").expect("DBX_LIVE_SQLSERVER_PASSWORD");
|
||||
let mut client =
|
||||
dbx_core::db::sqlserver::connect(&host, port, &user, &password, Some(&database), Duration::from_secs(10))
|
||||
dbx_core::db::sqlserver::connect(&host, port, &user, &password, Some(&database), None, Duration::from_secs(10))
|
||||
.await
|
||||
.expect("connect SQL Server");
|
||||
|
||||
@@ -152,11 +152,11 @@ async fn live_sqlserver_query_result_export_streams_cte_query_to_csv() {
|
||||
let user = std::env::var("DBX_LIVE_SQLSERVER_USER").unwrap_or_else(|_| "sa".to_string());
|
||||
let password = std::env::var("DBX_LIVE_SQLSERVER_PASSWORD").expect("DBX_LIVE_SQLSERVER_PASSWORD");
|
||||
let mut setup_client =
|
||||
dbx_core::db::sqlserver::connect(&host, port, &user, &password, Some(&database), Duration::from_secs(10))
|
||||
dbx_core::db::sqlserver::connect(&host, port, &user, &password, Some(&database), None, Duration::from_secs(10))
|
||||
.await
|
||||
.expect("connect SQL Server");
|
||||
let export_client =
|
||||
dbx_core::db::sqlserver::connect(&host, port, &user, &password, Some(&database), Duration::from_secs(10))
|
||||
dbx_core::db::sqlserver::connect(&host, port, &user, &password, Some(&database), None, Duration::from_secs(10))
|
||||
.await
|
||||
.expect("connect export SQL Server");
|
||||
|
||||
@@ -240,7 +240,7 @@ async fn live_sqlserver_transfer_table_skips_rowversion_insert_column() {
|
||||
let user = std::env::var("DBX_LIVE_SQLSERVER_USER").unwrap_or_else(|_| "sa".to_string());
|
||||
let password = std::env::var("DBX_LIVE_SQLSERVER_PASSWORD").expect("DBX_LIVE_SQLSERVER_PASSWORD");
|
||||
let mut setup_client =
|
||||
dbx_core::db::sqlserver::connect(&host, port, &user, &password, Some(&database), Duration::from_secs(10))
|
||||
dbx_core::db::sqlserver::connect(&host, port, &user, &password, Some(&database), None, Duration::from_secs(10))
|
||||
.await
|
||||
.expect("connect SQL Server");
|
||||
let mut target_client = dbx_core::db::sqlserver::connect(
|
||||
@@ -249,6 +249,7 @@ async fn live_sqlserver_transfer_table_skips_rowversion_insert_column() {
|
||||
&user,
|
||||
&password,
|
||||
Some(&target_database),
|
||||
None,
|
||||
Duration::from_secs(10),
|
||||
)
|
||||
.await
|
||||
@@ -329,7 +330,7 @@ async fn live_sqlserver_completion_assistant_searches_metadata_before_limiting()
|
||||
let user = std::env::var("DBX_LIVE_SQLSERVER_USER").unwrap_or_else(|_| "sa".to_string());
|
||||
let password = std::env::var("DBX_LIVE_SQLSERVER_PASSWORD").expect("DBX_LIVE_SQLSERVER_PASSWORD");
|
||||
let mut client =
|
||||
dbx_core::db::sqlserver::connect(&host, port, &user, &password, Some(&database), Duration::from_secs(10))
|
||||
dbx_core::db::sqlserver::connect(&host, port, &user, &password, Some(&database), None, Duration::from_secs(10))
|
||||
.await
|
||||
.expect("connect SQL Server");
|
||||
|
||||
|
||||
Reference in new issue
Block a user