mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
Feature:支持 SQL Server 旧版兼容模式 (#2181)
* fix: 支持 SQL Server 旧版非加密重试 * fix: 优化 SQL Server 旧版兼容模式入口 --------- Co-authored-by: staff <staff@qimaos-MacBook-Pro.local>
This commit is contained in:
@@ -867,6 +867,28 @@ function connectionErrorWithDriverUpdateHint(config: ConnectionConfig, message:
|
||||
return appendAgentDriverUpdateHint(message, t("connection.agentDriverUpdateConnectionHint"));
|
||||
}
|
||||
|
||||
function isSqlServerLegacyUnencryptedMode(params: string | undefined): boolean {
|
||||
const normalized = (params || "").trim().replace(/^\?/, "").replace(/;/g, "&");
|
||||
if (!normalized) return false;
|
||||
const parsed = new URLSearchParams(normalized);
|
||||
for (const [key, value] of parsed.entries()) {
|
||||
if (key.trim().toLowerCase() === "sqlserverencryption") {
|
||||
return ["disabled", "disable", "false", "0", "off"].includes(value.trim().toLowerCase());
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function setSqlServerLegacyUnencryptedMode(params: string | undefined, enabled: boolean): string {
|
||||
const normalized = (params || "").trim().replace(/^\?/, "").replace(/;/g, "&");
|
||||
return setUrlParam(normalized, "sqlserverEncryption", enabled ? "disabled" : "");
|
||||
}
|
||||
|
||||
function isSqlServerTlsHandshakeFailure(message: string): boolean {
|
||||
const text = message.toLowerCase();
|
||||
return text.includes("sql server") && text.includes("tls") && (text.includes("handshake") || text.includes("eof") || text.includes("performing i/o"));
|
||||
}
|
||||
|
||||
async function testConnectionWithTimeout(config: ConnectionConfig, runId: number): Promise<string> {
|
||||
const timeoutMs = connectionAttemptTimeoutMs(config);
|
||||
const timeoutMessage = connectionAttemptTimeoutMessage(timeoutMs);
|
||||
@@ -1608,6 +1630,13 @@ const testResultMessage = computed(() => {
|
||||
if (!testResult.value) return "";
|
||||
return testResult.value.ok ? t("connection.testSuccess") : translateBackendError(t, testResult.value.message);
|
||||
});
|
||||
const sqlServerLegacyUnencryptedModeEnabled = computed({
|
||||
get: () => form.value.db_type === "sqlserver" && isSqlServerLegacyUnencryptedMode(form.value.url_params),
|
||||
set: (enabled: boolean) => {
|
||||
if (form.value.db_type !== "sqlserver") return;
|
||||
form.value.url_params = setSqlServerLegacyUnencryptedMode(form.value.url_params, enabled);
|
||||
},
|
||||
});
|
||||
const shouldUseWideConnectionDialog = computed(() => dialogStep.value === "config" && (canChooseVisibleDatabases.value || (canChooseVisibleSchemas.value && !visibleFilterUsesSchemas.value)));
|
||||
const connectionDialogContentClass = computed(() => {
|
||||
if (dialogStep.value === "select") return "sm:max-w-[760px]";
|
||||
@@ -1685,6 +1714,10 @@ async function testConnection() {
|
||||
const message = connectionErrorWithDriverUpdateHint(config, mongodbAuthFailureHint(String(e)));
|
||||
const fallbackMessage = await tryNacosDockerConsoleFallback(config, message, runId);
|
||||
if (runId !== testRunId) return;
|
||||
const shouldShowSqlServerLegacyMode = !fallbackMessage && config.db_type === "sqlserver" && !isSqlServerLegacyUnencryptedMode(config.url_params) && isSqlServerTlsHandshakeFailure(message);
|
||||
if (shouldShowSqlServerLegacyMode) {
|
||||
configTab.value = "advanced";
|
||||
}
|
||||
testResult.value = fallbackMessage ? { ok: true, message: fallbackMessage } : { ok: false, message };
|
||||
} finally {
|
||||
if (runId === testRunId) {
|
||||
@@ -4297,6 +4330,18 @@ function openExternalUrl(url: string) {
|
||||
<span class="text-xs text-muted-foreground">{{ t("connection.readOnlyHint") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div v-show="form.db_type === 'sqlserver'" class="grid grid-cols-4 items-start gap-4">
|
||||
<Label :class="connectionLabelSmallClass">{{ t("connection.sqlServerLegacyUnencryptedMode") }}</Label>
|
||||
<div class="col-span-3 flex flex-col gap-1">
|
||||
<label class="flex h-5 cursor-pointer items-center gap-2">
|
||||
<input type="checkbox" v-model="sqlServerLegacyUnencryptedModeEnabled" class="mr-0" />
|
||||
<span class="text-xs text-foreground">{{ t("connection.sqlServerLegacyUnencryptedModeEnable") }}</span>
|
||||
</label>
|
||||
<p class="m-0 whitespace-pre-line text-xs leading-5 text-muted-foreground">
|
||||
{{ t("connection.sqlServerLegacyUnencryptedModeHint") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="form.db_type === 'redis'" class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelSmallClass">{{ t("settings.redisScanPageSize") }}</Label>
|
||||
<div class="col-span-3 flex flex-col gap-1">
|
||||
@@ -4318,7 +4363,7 @@ function openExternalUrl(url: string) {
|
||||
|
||||
<TabsContent v-if="canUseTransportLayers" value="transport" class="m-0">
|
||||
<div class="connection-form-body grid gap-4 py-4 pr-2 max-h-[65vh] overflow-y-auto">
|
||||
<div class="grid grid-cols-4 items-start gap-4">
|
||||
<div class="connection-label-wide-grid grid grid-cols-4 items-start gap-4">
|
||||
<Label :class="connectionLabelSmallPaddedClass">{{ t("connection.sshHops") }}</Label>
|
||||
<div class="col-span-3 grid gap-3">
|
||||
<div class="flex flex-wrap items-center gap-1 text-[11px] text-muted-foreground">
|
||||
@@ -4629,4 +4674,8 @@ function openExternalUrl(url: string) {
|
||||
width: min(100%, 36rem);
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.connection-dialog-content .grid.grid-cols-4.connection-label-wide-grid {
|
||||
grid-template-columns: 7.75rem repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -283,6 +283,10 @@ export default {
|
||||
test: "Test",
|
||||
testing: "Testing...",
|
||||
copyTestResult: "Copy test result",
|
||||
sqlServerLegacyUnencryptedMode: "Legacy compatibility mode",
|
||||
sqlServerLegacyUnencryptedModeEnable: "Use SQL Server legacy TLS 1.0 unencrypted connection",
|
||||
sqlServerLegacyUnencryptedModeEnabled: "SQL Server legacy compatibility mode is enabled.",
|
||||
sqlServerLegacyUnencryptedModeHint: "Applies to unencrypted transport or login-only encryption. It will still fail if the server requires TLS 1.0 encryption.\nUse it only on trusted networks, VPNs, or SSH tunnels",
|
||||
saveAndConnect: "Save & Connect",
|
||||
save: "Save",
|
||||
editTitle: "Edit Connection",
|
||||
|
||||
@@ -286,6 +286,10 @@ export default withEnglishFallback({
|
||||
test: "测试",
|
||||
testing: "测试中...",
|
||||
copyTestResult: "复制测试结果",
|
||||
sqlServerLegacyUnencryptedMode: "旧版兼容模式",
|
||||
sqlServerLegacyUnencryptedModeEnable: "使用 SQL Server 旧版TLS 1.0非加密连接",
|
||||
sqlServerLegacyUnencryptedModeEnabled: "已启用 SQL Server 旧版兼容模式。",
|
||||
sqlServerLegacyUnencryptedModeHint: "适用于非加密或仅登录阶段加密的场景。若服务端强制 TLS 1.0 加密,此模式仍会失败\n请仅在可信内网、VPN 或 SSH 隧道中使用",
|
||||
saveAndConnect: "保存并连接",
|
||||
save: "保存",
|
||||
editTitle: "编辑连接",
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -636,6 +636,7 @@ pub async fn test_connection(state: State<'_, Arc<AppState>>, config: Connection
|
||||
&config.username,
|
||||
&config.password,
|
||||
config.database.as_deref(),
|
||||
config.url_params.as_deref(),
|
||||
connect_timeout,
|
||||
)
|
||||
.await
|
||||
@@ -924,6 +925,7 @@ pub async fn connect_db(state: State<'_, Arc<AppState>>, config: ConnectionConfi
|
||||
&db_config.username,
|
||||
&db_config.password,
|
||||
db_config.database.as_deref(),
|
||||
db_config.url_params.as_deref(),
|
||||
connect_timeout,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Reference in New Issue
Block a user