connectorx/sources/mysql/
connection.rs1use super::errors::MySQLSourceError;
2use r2d2_mysql::mysql::{Opts, OptsBuilder, SslOpts};
3use std::collections::HashMap;
4use std::path::PathBuf;
5use url::Url;
6
7const SSL_PARAMS: &[&str] = &["ssl-mode", "sslmode", "ssl-ca"];
10
11pub fn build_opts(conn: &str) -> Result<OptsBuilder, MySQLSourceError> {
18 let url = Url::parse(conn)?;
19
20 let params: HashMap<String, String> = url
21 .query_pairs()
22 .map(|(k, v)| (k.to_ascii_lowercase(), v.into_owned()))
23 .collect();
24 let ssl_opts = ssl_opts_from_params(¶ms)?;
25
26 let opts = Opts::from_url(strip_ssl_params(&url).as_str())?;
27 Ok(OptsBuilder::from_opts(opts).ssl_opts(ssl_opts))
28}
29
30fn ssl_opts_from_params(
37 params: &HashMap<String, String>,
38) -> Result<Option<SslOpts>, MySQLSourceError> {
39 let mode = match params.get("ssl-mode").or_else(|| params.get("sslmode")) {
40 Some(mode) => mode,
41 None => return Ok(None),
42 };
43
44 let (accept_invalid_certs, skip_domain_validation) =
45 match mode.trim().to_ascii_uppercase().replace('-', "_").as_str() {
46 "DISABLED" => return Ok(None),
47 "PREFERRED" | "REQUIRED" => (true, true),
48 "VERIFY_CA" => (false, true),
49 "VERIFY_IDENTITY" => (false, false),
50 _ => return Err(MySQLSourceError::InvalidSslMode(mode.clone())),
51 };
52
53 let ssl_opts = SslOpts::default()
54 .with_danger_accept_invalid_certs(accept_invalid_certs)
55 .with_danger_skip_domain_validation(skip_domain_validation)
56 .with_root_cert_path(params.get("ssl-ca").map(PathBuf::from));
57 Ok(Some(ssl_opts))
58}
59
60fn strip_ssl_params(url: &Url) -> Url {
61 let kept: Vec<(String, String)> = url
62 .query_pairs()
63 .filter(|(k, _)| !SSL_PARAMS.contains(&k.to_ascii_lowercase().as_str()))
64 .map(|(k, v)| (k.into_owned(), v.into_owned()))
65 .collect();
66
67 let mut stripped = url.clone();
68 stripped.set_query(None);
69 for (k, v) in kept {
70 stripped.query_pairs_mut().append_pair(&k, &v);
71 }
72 stripped
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78
79 fn ssl_opts(conn: &str) -> Option<SslOpts> {
80 let opts: Opts = build_opts(conn).unwrap().into();
81 opts.get_ssl_opts().cloned()
82 }
83
84 #[test]
85 fn no_ssl_mode_disables_tls() {
86 assert!(ssl_opts("mysql://user:pass@host:3306/db").is_none());
87 assert!(ssl_opts("mysql://user:pass@host:3306/db?ssl-mode=DISABLED").is_none());
88 }
89
90 #[test]
91 fn required_encrypts_without_verification() {
92 let opts = ssl_opts("mysql://user:pass@host:3306/db?ssl-mode=REQUIRED").unwrap();
93 assert!(opts.accept_invalid_certs());
94 assert!(opts.skip_domain_validation());
95 assert!(opts.root_cert_path().is_none());
96 }
97
98 #[test]
99 fn preferred_is_treated_as_required() {
100 assert_eq!(
101 ssl_opts("mysql://user:pass@host:3306/db?ssl-mode=PREFERRED"),
102 ssl_opts("mysql://user:pass@host:3306/db?ssl-mode=REQUIRED"),
103 );
104 }
105
106 #[test]
107 fn verify_ca_checks_chain_but_not_hostname() {
108 let opts = ssl_opts("mysql://user:pass@host:3306/db?ssl-mode=VERIFY_CA&ssl-ca=/tmp/ca.pem")
109 .unwrap();
110 assert!(!opts.accept_invalid_certs());
111 assert!(opts.skip_domain_validation());
112 assert_eq!(
113 opts.root_cert_path(),
114 Some(std::path::Path::new("/tmp/ca.pem"))
115 );
116 }
117
118 #[test]
119 fn verify_identity_checks_chain_and_hostname() {
120 let opts = ssl_opts("mysql://user:pass@host:3306/db?ssl-mode=VERIFY_IDENTITY").unwrap();
121 assert!(!opts.accept_invalid_certs());
122 assert!(!opts.skip_domain_validation());
123 }
124
125 #[test]
126 fn ssl_mode_is_case_insensitive_and_accepts_sslmode_alias() {
127 assert!(ssl_opts("mysql://user:pass@host:3306/db?ssl-mode=required").is_some());
128 assert!(ssl_opts("mysql://user:pass@host:3306/db?sslmode=Required").is_some());
129 }
130
131 #[test]
132 fn invalid_ssl_mode_errors() {
133 assert!(matches!(
134 build_opts("mysql://user:pass@host:3306/db?ssl-mode=bogus"),
135 Err(MySQLSourceError::InvalidSslMode(_))
136 ));
137 }
138
139 #[test]
140 fn ssl_params_are_stripped_so_the_url_parses_and_other_params_survive() {
141 let opts: Opts = build_opts(
144 "mysql://user:pass@host:3306/db?ssl-mode=REQUIRED&ssl-ca=/tmp/ca.pem&prefer_socket=false",
145 )
146 .unwrap()
147 .into();
148 assert!(opts.get_ssl_opts().is_some());
149 assert!(!opts.get_prefer_socket());
150 }
151}