Skip to content

Commit 5a9980d

Browse files
authored
Honor MySQL SSL mode in connection setup (#263)
* fix: honor MySQL SSL mode in connection setup * fix: align MySQL SSL mode aliases * fix: scope MySQL SSL pool key
1 parent 49f6f89 commit 5a9980d

4 files changed

Lines changed: 156 additions & 5 deletions

File tree

src-tauri/src/drivers/mysql/mod.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1313,8 +1313,16 @@ impl DatabaseDriver for MysqlDriver {
13131313
let connect_timeout =
13141314
mysql_numeric_setting("connectTimeout", DEFAULT_MYSQL_CONNECT_TIMEOUT_MS);
13151315
let timezone = mysql_string_setting("timezone", DEFAULT_MYSQL_TIMEZONE);
1316+
let ssl_mode = match params.ssl_mode.as_deref() {
1317+
Some("disabled") | Some("disable") => "disabled",
1318+
Some("preferred") | Some("prefer") => "preferred",
1319+
Some("required") | Some("require") => "required",
1320+
Some("verify_ca") => "verify_ca",
1321+
Some("verify_identity") => "verify_identity",
1322+
_ => "required",
1323+
};
13161324
Ok(format!(
1317-
"mysql://{}@{}:{}/{}?maxAllowedPacket={}&socketTimeout={}&connectTimeout={}&timezone={}",
1325+
"mysql://{}@{}:{}/{}?maxAllowedPacket={}&socketTimeout={}&connectTimeout={}&timezone={}&ssl-mode={}",
13181326
credentials,
13191327
params.host.as_deref().unwrap_or("localhost"),
13201328
params.port.unwrap_or(3306),
@@ -1323,6 +1331,7 @@ impl DatabaseDriver for MysqlDriver {
13231331
socket_timeout,
13241332
connect_timeout,
13251333
encode(&timezone),
1334+
ssl_mode,
13261335
))
13271336
}
13281337

src-tauri/src/drivers/mysql/tests.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,40 @@
11
use super::explain::parse_mysql_query_block;
2+
use super::MysqlDriver;
3+
use crate::drivers::driver_trait::DatabaseDriver;
24
use crate::models::ExplainNode;
5+
use crate::models::{ConnectionParams, DatabaseSelection};
6+
7+
#[test]
8+
fn build_connection_url_includes_disabled_ssl_mode() {
9+
let driver = MysqlDriver::new();
10+
let params = ConnectionParams {
11+
driver: "mysql".to_string(),
12+
host: Some("127.0.0.1".to_string()),
13+
port: Some(3306),
14+
username: Some("root".to_string()),
15+
password: Some("secret".to_string()),
16+
database: DatabaseSelection::Single("dec".to_string()),
17+
ssl_mode: Some("disabled".to_string()),
18+
ssl_ca: None,
19+
ssl_cert: None,
20+
ssl_key: None,
21+
ssh_enabled: None,
22+
ssh_connection_id: None,
23+
ssh_host: None,
24+
ssh_port: None,
25+
ssh_user: None,
26+
ssh_password: None,
27+
ssh_key_file: None,
28+
ssh_key_passphrase: None,
29+
save_in_keychain: None,
30+
connection_id: None,
31+
..Default::default()
32+
};
33+
34+
let url = driver.build_connection_url(&params).unwrap();
35+
36+
assert!(url.contains("ssl-mode=disabled"), "url was: {url}");
37+
}
338

439
/// Helper: parse a MariaDB ANALYZE FORMAT=JSON string and return the root node.
540
fn parse_json(json: &str) -> ExplainNode {

src-tauri/src/pool_manager.rs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,21 @@ fn mysql_numeric_setting(key: &str, default: u64) -> u64 {
7979
/// Build a stable connection key that works with SSH tunnels.
8080
/// If connection_id is provided (from saved connections), use it for stable pooling.
8181
/// Otherwise fall back to host:port:database (for ad-hoc connections).
82-
fn build_connection_key(params: &ConnectionParams, connection_id: Option<&str>) -> String {
83-
if let Some(conn_id) = connection_id {
82+
pub(crate) fn build_connection_key(
83+
params: &ConnectionParams,
84+
connection_id: Option<&str>,
85+
) -> String {
86+
let tls_key = (params.driver == "mysql").then(|| {
87+
format!(
88+
"ssl:{}:{}:{}:{}",
89+
params.ssl_mode.as_deref().unwrap_or("default"),
90+
params.ssl_ca.as_deref().unwrap_or(""),
91+
params.ssl_cert.as_deref().unwrap_or(""),
92+
params.ssl_key.as_deref().unwrap_or("")
93+
)
94+
});
95+
96+
let base_key = if let Some(conn_id) = connection_id {
8497
// Include database in key so different databases on the same connection use separate pools
8598
format!("{}:conn:{}:{}", params.driver, conn_id, params.database)
8699
} else {
@@ -92,10 +105,16 @@ fn build_connection_key(params: &ConnectionParams, connection_id: Option<&str>)
92105
params.port.unwrap_or(0),
93106
params.database
94107
)
108+
};
109+
110+
if let Some(tls_key) = tls_key {
111+
format!("{base_key}:{tls_key}")
112+
} else {
113+
base_key
95114
}
96115
}
97116

98-
fn build_mysql_options(
117+
pub(crate) fn build_mysql_options(
99118
params: &ConnectionParams,
100119
override_db: Option<&str>,
101120
) -> Result<sqlx::mysql::MySqlConnectOptions, String> {

src-tauri/src/pool_manager_tests.rs

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,42 @@
11
#[cfg(test)]
22
mod tests {
3-
use crate::pool_manager::format_error_chain;
3+
use crate::models::{ConnectionParams, DatabaseSelection};
4+
use crate::pool_manager::{build_connection_key, build_mysql_options, format_error_chain};
5+
use sqlx::mysql::MySqlSslMode;
6+
7+
fn connection_params(driver: &str, ssl_mode: Option<&str>) -> ConnectionParams {
8+
ConnectionParams {
9+
driver: driver.to_string(),
10+
host: Some("127.0.0.1".to_string()),
11+
port: Some(match driver {
12+
"postgres" => 5432,
13+
"mysql" => 3306,
14+
_ => 0,
15+
}),
16+
username: Some("dec".to_string()),
17+
password: Some("secret".to_string()),
18+
database: DatabaseSelection::Single("dec".to_string()),
19+
ssl_mode: ssl_mode.map(ToOwned::to_owned),
20+
ssl_ca: None,
21+
ssl_cert: None,
22+
ssl_key: None,
23+
ssh_enabled: Some(true),
24+
ssh_connection_id: Some("ssh-1".to_string()),
25+
ssh_host: Some("149.202.85.42".to_string()),
26+
ssh_port: Some(2222),
27+
ssh_user: Some("julien".to_string()),
28+
ssh_password: None,
29+
ssh_key_file: Some("/Users/julienbarbe/.ssh/id_rsa".to_string()),
30+
ssh_key_passphrase: None,
31+
save_in_keychain: None,
32+
connection_id: Some("conn-1".to_string()),
33+
..Default::default()
34+
}
35+
}
36+
37+
fn mysql_params(ssl_mode: &str) -> ConnectionParams {
38+
connection_params("mysql", Some(ssl_mode))
39+
}
440

541
#[test]
642
fn format_error_chain_walks_sources() {
@@ -34,6 +70,58 @@ mod tests {
3470
"outer message -> inner cause"
3571
);
3672
}
73+
74+
#[test]
75+
fn mysql_pool_key_changes_when_ssl_mode_changes() {
76+
let required = mysql_params("required");
77+
let disabled = mysql_params("disabled");
78+
79+
assert_ne!(
80+
build_connection_key(&required, Some("conn-1")),
81+
build_connection_key(&disabled, Some("conn-1"))
82+
);
83+
}
84+
85+
#[test]
86+
fn postgres_pool_key_ignores_mysql_ssl_key_fields() {
87+
let required = connection_params("postgres", Some("required"));
88+
let disabled = connection_params("postgres", Some("disabled"));
89+
90+
assert_eq!(
91+
build_connection_key(&required, Some("conn-1")),
92+
build_connection_key(&disabled, Some("conn-1"))
93+
);
94+
}
95+
96+
#[test]
97+
fn sqlite_pool_key_ignores_mysql_ssl_key_fields() {
98+
let required = connection_params("sqlite", Some("required"));
99+
let disabled = connection_params("sqlite", Some("disabled"));
100+
101+
assert_eq!(
102+
build_connection_key(&required, Some("conn-1")),
103+
build_connection_key(&disabled, Some("conn-1"))
104+
);
105+
}
106+
107+
#[test]
108+
fn mysql_options_accept_snake_case_verify_ssl_modes() {
109+
let verify_ca = mysql_params("verify_ca");
110+
let verify_identity = mysql_params("verify_identity");
111+
112+
assert!(matches!(
113+
build_mysql_options(&verify_ca, None)
114+
.unwrap()
115+
.get_ssl_mode(),
116+
MySqlSslMode::VerifyCa
117+
));
118+
assert!(matches!(
119+
build_mysql_options(&verify_identity, None)
120+
.unwrap()
121+
.get_ssl_mode(),
122+
MySqlSslMode::VerifyIdentity
123+
));
124+
}
37125
}
38126

39127
#[cfg(test)]

0 commit comments

Comments
 (0)