1mod ado_net;
2mod jdbc;
3
4use std::path::PathBuf;
5use std::time::Duration;
6
7use super::AuthMethod;
8use crate::EncryptionLevel;
9use ado_net::*;
10use jdbc::*;
11
12const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(15);
14
15#[derive(Clone, Debug)]
16pub struct Config {
36 pub(crate) host: Option<String>,
37 pub(crate) port: Option<u16>,
38 pub(crate) database: Option<String>,
39 pub(crate) instance_name: Option<String>,
40 pub(crate) application_name: Option<String>,
41 pub(crate) encryption: EncryptionLevel,
42 pub(crate) trust: TrustConfig,
43 pub(crate) auth: AuthMethod,
44 pub(crate) readonly: bool,
45 pub(crate) packet_size: Option<u32>,
46 pub(crate) hostname_in_certificate: Option<String>,
47 pub(crate) client_name: Option<String>,
48 pub(crate) multi_subnet_failover: bool,
49 pub(crate) connect_timeout: Option<Duration>,
50 pub(crate) server_spn: Option<String>,
51 #[cfg(any(
52 feature = "rustls",
53 feature = "native-tls",
54 feature = "vendored-openssl"
55 ))]
56 pub(crate) client_cert: Option<ClientCertificate>,
57}
58
59#[derive(Clone, Debug)]
60pub(crate) enum TrustConfig {
61 #[allow(dead_code)]
62 CaCertificateLocation(PathBuf),
63 TrustAll,
64 Default,
65}
66
67#[cfg(any(
75 feature = "rustls",
76 feature = "native-tls",
77 feature = "vendored-openssl"
78))]
79#[derive(Clone, Debug)]
80pub(crate) struct ClientCertificate {
81 pub(crate) source: ClientCertSource,
82}
83
84#[cfg(any(
85 feature = "rustls",
86 feature = "native-tls",
87 feature = "vendored-openssl"
88))]
89#[derive(Clone)]
90pub(crate) enum ClientCertSource {
91 CertAndKey { cert: PathBuf, key: PathBuf },
96 #[cfg(any(feature = "native-tls", feature = "vendored-openssl"))]
99 Pkcs12 {
100 path: PathBuf,
101 password: zeroize::Zeroizing<String>,
102 },
103}
104
105#[cfg(any(
107 feature = "rustls",
108 feature = "native-tls",
109 feature = "vendored-openssl"
110))]
111impl std::fmt::Debug for ClientCertSource {
112 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113 match self {
114 ClientCertSource::CertAndKey { cert, key } => f
115 .debug_struct("CertAndKey")
116 .field("cert", cert)
117 .field("key", key)
118 .finish(),
119 #[cfg(any(feature = "native-tls", feature = "vendored-openssl"))]
120 ClientCertSource::Pkcs12 { path, .. } => f
121 .debug_struct("Pkcs12")
122 .field("path", path)
123 .field("password", &"<redacted>")
124 .finish(),
125 }
126 }
127}
128
129impl Default for Config {
130 fn default() -> Self {
131 Self {
132 host: None,
133 port: None,
134 database: None,
135 instance_name: None,
136 application_name: None,
137 #[cfg(any(
138 feature = "rustls",
139 feature = "native-tls",
140 feature = "vendored-openssl"
141 ))]
142 encryption: EncryptionLevel::Required,
143 #[cfg(not(any(
144 feature = "rustls",
145 feature = "native-tls",
146 feature = "vendored-openssl"
147 )))]
148 encryption: EncryptionLevel::NotSupported,
149 trust: TrustConfig::Default,
150 auth: AuthMethod::None,
151 readonly: false,
152 packet_size: None,
153 hostname_in_certificate: None,
154 client_name: None,
155 multi_subnet_failover: false,
156 connect_timeout: Some(DEFAULT_CONNECT_TIMEOUT),
157 server_spn: None,
158 #[cfg(any(
159 feature = "rustls",
160 feature = "native-tls",
161 feature = "vendored-openssl"
162 ))]
163 client_cert: None,
164 }
165 }
166}
167
168impl Drop for Config {
171 fn drop(&mut self) {
172 if let AuthMethod::AADToken(token) = &mut self.auth {
173 zeroize::Zeroize::zeroize(token);
174 }
175 }
176}
177
178impl Config {
179 pub fn new() -> Self {
181 Self::default()
182 }
183
184 pub fn builder() -> ConfigBuilder {
206 ConfigBuilder {
207 inner: Self::default(),
208 }
209 }
210
211 pub fn host(&mut self, host: impl ToString) {
215 self.host = Some(host.to_string());
216 }
217
218 pub fn port(&mut self, port: u16) {
222 self.port = Some(port);
223 }
224
225 pub fn database(&mut self, database: impl ToString) {
229 self.database = Some(database.to_string())
230 }
231
232 pub fn instance_name(&mut self, name: impl ToString) {
240 self.instance_name = Some(name.to_string());
241 }
242
243 pub fn application_name(&mut self, name: impl ToString) {
248 self.application_name = Some(name.to_string());
249 }
250
251 pub fn packet_size(&mut self, size: u32) {
259 self.packet_size = Some(size);
260 }
261
262 pub fn get_packet_size(&self) -> Option<u32> {
264 self.packet_size
265 }
266
267 pub fn encryption(&mut self, encryption: EncryptionLevel) {
272 self.encryption = encryption;
273 }
274
275 pub fn trust_cert(&mut self) {
286 if let TrustConfig::CaCertificateLocation(_) = &self.trust {
287 panic!("'trust_cert' and 'trust_cert_ca' are mutual exclusive! Only use one.")
288 }
289 self.trust = TrustConfig::TrustAll;
290 }
291
292 pub fn trust_cert_ca(&mut self, path: impl ToString) {
302 if let TrustConfig::TrustAll = &self.trust {
303 panic!("'trust_cert' and 'trust_cert_ca' are mutual exclusive! Only use one.")
304 } else {
305 self.trust = TrustConfig::CaCertificateLocation(PathBuf::from(path.to_string()))
306 }
307 }
308
309 pub fn hostname_in_certificate(&mut self, hostname: impl ToString) {
320 self.hostname_in_certificate = Some(hostname.to_string());
321 }
322
323 pub fn client_name(&mut self, name: impl ToString) {
328 self.client_name = Some(name.to_string());
329 }
330
331 pub fn authentication(&mut self, auth: AuthMethod) {
335 self.auth = auth;
336 }
337
338 pub fn readonly(&mut self, readnoly: bool) {
342 self.readonly = readnoly;
343 }
344
345 pub fn multi_subnet_failover(&mut self, multi_subnet_failover: bool) {
355 self.multi_subnet_failover = multi_subnet_failover;
356 }
357
358 pub fn get_multi_subnet_failover(&self) -> bool {
360 self.multi_subnet_failover
361 }
362
363 pub fn connect_timeout(&self) -> Option<Duration> {
394 self.connect_timeout
395 }
396
397 pub fn set_connect_timeout(&mut self, timeout: Option<Duration>) {
399 self.connect_timeout = timeout;
400 }
401
402 pub fn server_spn(&mut self, spn: impl ToString) {
408 self.server_spn = Some(spn.to_string());
409 }
410
411 pub(crate) fn get_server_spn(&self) -> Option<&str> {
414 self.server_spn
415 .as_deref()
416 .filter(|spn| !spn.trim().is_empty())
417 }
418
419 #[cfg(any(
445 feature = "rustls",
446 feature = "native-tls",
447 feature = "vendored-openssl"
448 ))]
449 #[cfg_attr(
450 docsrs,
451 doc(cfg(any(
452 feature = "rustls",
453 feature = "native-tls",
454 feature = "vendored-openssl"
455 )))
456 )]
457 pub fn client_certificate(&mut self, cert: impl Into<PathBuf>, key: impl Into<PathBuf>) {
458 self.client_cert = Some(ClientCertificate {
459 source: ClientCertSource::CertAndKey {
460 cert: cert.into(),
461 key: key.into(),
462 },
463 });
464 }
465
466 #[cfg(any(feature = "native-tls", feature = "vendored-openssl"))]
479 #[cfg_attr(
480 docsrs,
481 doc(cfg(any(feature = "native-tls", feature = "vendored-openssl")))
482 )]
483 pub fn client_certificate_pkcs12(
484 &mut self,
485 path: impl Into<PathBuf>,
486 password: impl Into<String>,
487 ) {
488 self.client_cert = Some(ClientCertificate {
489 source: ClientCertSource::Pkcs12 {
490 path: path.into(),
491 password: zeroize::Zeroizing::new(password.into()),
492 },
493 });
494 }
495
496 #[cfg(any(
497 feature = "rustls",
498 feature = "native-tls",
499 feature = "vendored-openssl"
500 ))]
501 pub(crate) fn get_client_certificate(&self) -> Option<&ClientCertificate> {
502 self.client_cert.as_ref()
503 }
504
505 pub(crate) fn get_host(&self) -> &str {
506 self.host
507 .as_deref()
508 .filter(|v| v != &".")
509 .unwrap_or("localhost")
510 }
511
512 #[cfg(any(
513 feature = "rustls",
514 feature = "native-tls",
515 feature = "vendored-openssl"
516 ))]
517 pub(crate) fn get_hostname_in_certificate(&self) -> &str {
518 self.hostname_in_certificate
519 .as_deref()
520 .unwrap_or_else(|| self.get_host())
521 }
522
523 #[cfg(any(
526 feature = "rustls",
527 feature = "native-tls",
528 feature = "vendored-openssl"
529 ))]
530 pub(crate) fn tls_trust(&self) -> &TrustConfig {
531 static VALIDATE: TrustConfig = TrustConfig::Default;
532
533 match (&self.trust, self.encryption) {
534 (TrustConfig::TrustAll, EncryptionLevel::Strict) => {
535 tracing::warn!(
536 "TrustServerCertificate is ignored with strict encryption; validating the server certificate."
537 );
538 &VALIDATE
539 }
540 (trust, _) => trust,
541 }
542 }
543
544 pub(crate) fn get_port(&self) -> u16 {
545 match (self.port, self.instance_name.as_ref()) {
546 (Some(port), _) => port,
548 (None, Some(_)) => 1434,
551 (None, None) => 1433,
553 }
554 }
555
556 pub fn get_addr(&self) -> String {
558 format!("{}:{}", self.get_host(), self.get_port())
559 }
560
561 pub fn from_ado_string(s: &str) -> crate::Result<Self> {
597 let ado: AdoNetConfig = s.parse()?;
598 Self::from_config_string(ado)
599 }
600
601 pub fn from_jdbc_string(s: &str) -> crate::Result<Self> {
611 let jdbc: JdbcConfig = s.parse()?;
612 Self::from_config_string(jdbc)
613 }
614
615 fn from_config_string(s: impl ConfigString) -> crate::Result<Self> {
616 let mut builder = Self::new();
617
618 let server = s.server()?;
619
620 if let Some(host) = server.host {
621 builder.host(host);
622 }
623
624 if let Some(port) = server.port {
625 builder.port(port);
626 }
627
628 if let Some(instance) = server.instance {
629 builder.instance_name(instance);
630 }
631
632 builder.authentication(s.authentication()?);
633
634 if let Some(database) = s.database() {
635 builder.database(database);
636 }
637
638 if let Some(name) = s.application_name() {
639 builder.application_name(name);
640 }
641
642 let encryption = s.encrypt()?;
643 builder.encryption(encryption);
644
645 match (s.trust_cert()?, s.trust_cert_ca()) {
646 (true, ca) if encryption == EncryptionLevel::Strict => {
648 tracing::warn!(
649 "TrustServerCertificate is ignored with Encrypt=Strict; validating the server certificate."
650 );
651 if let Some(ca) = ca {
652 builder.trust_cert_ca(ca);
653 }
654 }
655 (true, Some(_)) => {
656 return Err(crate::Error::Conversion(
657 "Connection string: TrustServerCertificate=true and TrustServerCertificateCA are mutually exclusive"
658 .into(),
659 ))
660 }
661 (true, None) => builder.trust_cert(),
662 (false, Some(ca)) => builder.trust_cert_ca(ca),
663 (false, None) => (),
664 }
665
666 if let Some(hostname_in_cert) = s.hostname_in_certificate() {
667 builder.hostname_in_certificate(hostname_in_cert);
668 }
669
670 builder.readonly(s.readonly());
671
672 if let Some(client_name) = s.client_name() {
673 builder.client_name(client_name);
674 }
675 builder.multi_subnet_failover(s.multi_subnet_failover()?);
676 if let Some(size) = s.packet_size()? {
677 builder.packet_size(size);
678 }
679 builder.set_connect_timeout(s.connect_timeout()?);
680 if let Some(spn) = s.server_spn() {
681 builder.server_spn(spn);
682 }
683
684 Ok(builder)
685 }
686}
687
688#[derive(Clone, Debug)]
712pub struct ConfigBuilder {
713 inner: Config,
714}
715
716impl ConfigBuilder {
717 pub fn host(mut self, host: impl ToString) -> Self {
721 self.inner.host = Some(host.to_string());
722 self
723 }
724
725 pub fn port(mut self, port: u16) -> Self {
729 self.inner.port = Some(port);
730 self
731 }
732
733 pub fn database(mut self, database: impl ToString) -> Self {
737 self.inner.database = Some(database.to_string());
738 self
739 }
740
741 pub fn instance_name(mut self, name: impl ToString) -> Self {
749 self.inner.instance_name = Some(name.to_string());
750 self
751 }
752
753 pub fn application_name(mut self, name: impl ToString) -> Self {
758 self.inner.application_name = Some(name.to_string());
759 self
760 }
761
762 pub fn encryption(mut self, encryption: EncryptionLevel) -> Self {
767 self.inner.encryption = encryption;
768 self
769 }
770
771 pub fn trust_cert(mut self) -> Self {
782 if let TrustConfig::CaCertificateLocation(_) = &self.inner.trust {
783 panic!("'trust_cert' and 'trust_cert_ca' are mutual exclusive! Only use one.")
784 }
785 self.inner.trust = TrustConfig::TrustAll;
786 self
787 }
788
789 pub fn trust_cert_ca(mut self, path: impl ToString) -> Self {
799 if let TrustConfig::TrustAll = &self.inner.trust {
800 panic!("'trust_cert' and 'trust_cert_ca' are mutual exclusive! Only use one.")
801 } else {
802 self.inner.trust = TrustConfig::CaCertificateLocation(PathBuf::from(path.to_string()))
803 }
804 self
805 }
806
807 pub fn authentication(mut self, auth: AuthMethod) -> Self {
811 self.inner.auth = auth;
812 self
813 }
814
815 pub fn readonly(mut self, readonly: bool) -> Self {
819 self.inner.readonly = readonly;
820 self
821 }
822
823 pub fn connect_timeout(mut self, timeout: Option<Duration>) -> Self {
825 self.inner.connect_timeout = timeout;
826 self
827 }
828
829 pub fn server_spn(mut self, spn: impl ToString) -> Self {
832 self.inner.server_spn(spn);
833 self
834 }
835
836 #[cfg(any(
840 feature = "rustls",
841 feature = "native-tls",
842 feature = "vendored-openssl"
843 ))]
844 #[cfg_attr(
845 docsrs,
846 doc(cfg(any(
847 feature = "rustls",
848 feature = "native-tls",
849 feature = "vendored-openssl"
850 )))
851 )]
852 pub fn client_certificate(mut self, cert: impl Into<PathBuf>, key: impl Into<PathBuf>) -> Self {
853 self.inner.client_certificate(cert, key);
854 self
855 }
856
857 #[cfg(any(feature = "native-tls", feature = "vendored-openssl"))]
862 #[cfg_attr(
863 docsrs,
864 doc(cfg(any(feature = "native-tls", feature = "vendored-openssl")))
865 )]
866 pub fn client_certificate_pkcs12(
867 mut self,
868 path: impl Into<PathBuf>,
869 password: impl Into<String>,
870 ) -> Self {
871 self.inner.client_certificate_pkcs12(path, password);
872 self
873 }
874
875 pub fn build(self) -> Config {
879 self.inner
880 }
881}
882
883impl From<Config> for ConfigBuilder {
884 fn from(config: Config) -> Self {
885 ConfigBuilder { inner: config }
886 }
887}
888
889impl From<ConfigBuilder> for Config {
890 fn from(builder: ConfigBuilder) -> Self {
891 builder.inner
892 }
893}
894
895pub(crate) struct ServerDefinition {
896 host: Option<String>,
897 port: Option<u16>,
898 instance: Option<String>,
899}
900
901pub(crate) trait ConfigString {
902 fn get(&self, key: &str) -> Option<&str>;
903
904 fn first(&self, keys: &[&str]) -> Option<&str> {
906 keys.iter().find_map(|key| self.get(key))
907 }
908
909 fn server(&self) -> crate::Result<ServerDefinition>;
910
911 fn connect_timeout(&self) -> crate::Result<Option<Duration>>;
912
913 fn packet_size(&self) -> crate::Result<Option<u32>> {
914 Ok(None)
915 }
916
917 fn integrated_security(&self) -> crate::Result<bool> {
920 match self.first(&["integrated security", "integratedsecurity"]) {
921 Some(v) if v.trim().eq_ignore_ascii_case("sspi") => Ok(true),
922 Some(v) => Self::parse_bool(v),
923 None => Ok(false),
924 }
925 }
926
927 fn authentication(&self) -> crate::Result<AuthMethod> {
928 let user = self.first(&["uid", "username", "user", "user id"]);
929
930 let pw = self.first(&["password", "pwd"]);
931
932 if !self.integrated_security()? {
933 return Ok(AuthMethod::sql_server(user.unwrap_or(""), pw.unwrap_or("")));
934 }
935
936 #[allow(unused)]
938 let missing = |what: &str| -> crate::Result<AuthMethod> {
939 Err(crate::Error::Conversion(
940 format!("Connection string: Integrated Security needs {what}").into(),
941 ))
942 };
943
944 #[cfg(all(windows, feature = "winauth"))]
945 return match (user, pw) {
946 (None, None) => Ok(AuthMethod::Integrated),
947 _ => Ok(AuthMethod::windows(user.unwrap_or(""), pw.unwrap_or(""))),
948 };
949 #[cfg(all(windows, not(feature = "winauth")))]
950 return missing("the crate's `winauth` feature");
951
952 #[cfg(all(unix, feature = "sspi-rs"))]
955 return match (user, pw) {
956 (Some(user), Some(pw)) => Ok(AuthMethod::windows(user, pw)),
957 #[cfg(feature = "integrated-auth-gssapi")]
958 (None, None) => Ok(AuthMethod::Integrated),
959 #[cfg(not(feature = "integrated-auth-gssapi"))]
960 (None, None) => missing("a User ID, or the crate's `integrated-auth-gssapi` feature"),
961 _ => Ok(AuthMethod::windows(user.unwrap_or(""), pw.unwrap_or(""))),
962 };
963 #[cfg(all(unix, feature = "integrated-auth-gssapi", not(feature = "sspi-rs")))]
964 return Ok(AuthMethod::Integrated);
965 #[cfg(all(
966 unix,
967 not(any(feature = "integrated-auth-gssapi", feature = "sspi-rs"))
968 ))]
969 return missing(
970 "the crate's `integrated-auth-gssapi` (Kerberos) or `sspi-rs` (NTLM) feature",
971 );
972
973 #[cfg(not(any(windows, unix)))]
974 return missing("Windows or Unix");
975 }
976
977 fn database(&self) -> Option<String> {
978 self.first(&["database", "initial catalog", "databasename"])
979 .map(|db| db.to_string())
980 }
981
982 fn application_name(&self) -> Option<String> {
983 self.first(&["application name", "applicationname"])
984 .map(|name| name.to_string())
985 }
986
987 fn trust_cert(&self) -> crate::Result<bool> {
988 self.first(&["trust server certificate", "trustservercertificate"])
989 .map(Self::parse_bool)
990 .unwrap_or(Ok(false))
991 }
992
993 fn trust_cert_ca(&self) -> Option<String> {
994 self.first(&["trustservercertificateca"])
995 .map(|ca| ca.to_string())
996 }
997
998 fn hostname_in_certificate(&self) -> Option<String> {
999 self.first(&[
1000 "host name in certificate",
1001 "hostnameincertificate",
1002 "hostname in certificate",
1003 ])
1004 .map(|host| host.to_string())
1005 }
1006
1007 fn client_name(&self) -> Option<String> {
1008 self.first(&["workstation id", "workstationid"])
1009 .map(|name| name.to_string())
1010 }
1011
1012 fn server_spn(&self) -> Option<String> {
1014 self.first(&["server spn", "serverspn"]).map(str::to_owned)
1015 }
1016
1017 #[cfg(any(
1018 feature = "rustls",
1019 feature = "native-tls",
1020 feature = "vendored-openssl"
1021 ))]
1022 fn encrypt(&self) -> crate::Result<EncryptionLevel> {
1023 self.first(&["encrypt"])
1024 .map(|val| {
1025 let val = val.trim();
1026 let is = |s: &str| val.eq_ignore_ascii_case(s);
1027 if is("true") || is("yes") || is("mandatory") {
1029 Ok(EncryptionLevel::Required)
1030 } else if is("false") || is("no") || is("optional") {
1031 Ok(EncryptionLevel::Off)
1032 } else if val == "DANGER_PLAINTEXT" {
1033 Ok(EncryptionLevel::NotSupported)
1034 } else if is("strict") && cfg!(feature = "tds80") {
1035 Ok(EncryptionLevel::Strict)
1036 } else if is("strict") {
1037 Err(crate::Error::Conversion(
1038 "encrypt=strict requires the crate's `tds80` feature to be enabled".into(),
1039 ))
1040 } else {
1041 Err(crate::Error::Conversion(
1042 "Connection string: Encrypt must be true, false, yes, no, mandatory, optional or strict".into(),
1043 ))
1044 }
1045 })
1046 .unwrap_or(Ok(EncryptionLevel::Required))
1052 }
1053
1054 #[cfg(not(any(
1055 feature = "rustls",
1056 feature = "native-tls",
1057 feature = "vendored-openssl"
1058 )))]
1059 fn encrypt(&self) -> crate::Result<EncryptionLevel> {
1060 Ok(EncryptionLevel::NotSupported)
1061 }
1062
1063 fn parse_bool<T: AsRef<str>>(v: T) -> crate::Result<bool> {
1064 match v.as_ref().trim().to_lowercase().as_str() {
1065 "true" | "yes" => Ok(true),
1066 "false" | "no" => Ok(false),
1067 _ => Err(crate::Error::Conversion(
1068 "Connection string: Not a valid boolean".into(),
1069 )),
1070 }
1071 }
1072
1073 fn readonly(&self) -> bool {
1074 self.first(&["application intent", "applicationintent"])
1075 .filter(|val| val.trim().eq_ignore_ascii_case("ReadOnly"))
1076 .is_some()
1077 }
1078
1079 fn multi_subnet_failover(&self) -> crate::Result<bool> {
1080 self.first(&["multi subnet failover", "multisubnetfailover"])
1081 .map(Self::parse_bool)
1082 .unwrap_or(Ok(false))
1083 }
1084}
1085
1086#[cfg(test)]
1087mod tests {
1088 use super::*;
1089
1090 #[test]
1091 fn config_builder_constructs_config() {
1092 let config = Config::builder()
1093 .host("db.example.com")
1094 .port(4433)
1095 .database("northwind")
1096 .application_name("my-app")
1097 .authentication(AuthMethod::sql_server("SA", "secret"))
1098 .readonly(true)
1099 .build();
1100
1101 assert_eq!("db.example.com", config.get_host());
1102 assert_eq!(4433, config.get_port());
1103 assert_eq!("db.example.com:4433", config.get_addr());
1104 assert_eq!(Some("northwind"), config.database.as_deref());
1105 assert_eq!(Some("my-app"), config.application_name.as_deref());
1106 assert!(config.readonly);
1107 assert!(matches!(config.auth, AuthMethod::SqlServer(_)));
1108 assert!(matches!(config.trust, TrustConfig::Default));
1109 }
1110
1111 #[test]
1112 fn config_builder_roundtrips_via_from() {
1113 let config = Config::builder().host("localhost").port(1433).build();
1114 let builder: ConfigBuilder = config.into();
1115 let config = builder.database("master").build();
1116
1117 assert_eq!("localhost:1433", config.get_addr());
1118 assert_eq!(Some("master"), config.database.as_deref());
1119 }
1120
1121 #[test]
1122 fn config_from_builder_carries_builder_settings() {
1123 let config: Config = Config::builder().host("db.internal").port(2020).into();
1125 assert_eq!("db.internal", config.get_host());
1126 assert_eq!(2020, config.get_port());
1127 }
1128
1129 #[test]
1130 fn get_packet_size_reflects_the_set_value() {
1131 let mut config = Config::new();
1132 assert_eq!(config.get_packet_size(), None);
1133 config.packet_size(8192);
1134 assert_eq!(config.get_packet_size(), Some(8192));
1135 }
1136
1137 #[test]
1138 fn server_spn_setters() {
1139 let mut config = Config::new();
1140 assert_eq!(config.get_server_spn(), None);
1141 config.server_spn("MSSQLSvc/a:1");
1142 assert_eq!(config.get_server_spn(), Some("MSSQLSvc/a:1"));
1143 config.server_spn("");
1144 assert_eq!(config.get_server_spn(), None);
1145
1146 let config = Config::builder().server_spn("MSSQLSvc/b:2").build();
1147 assert_eq!(config.get_server_spn(), Some("MSSQLSvc/b:2"));
1148 }
1149
1150 #[test]
1151 fn from_jdbc_string_parses_host_and_port() {
1152 let config =
1153 Config::from_jdbc_string("jdbc:sqlserver://db.example.com:2345").expect("valid jdbc");
1154 assert_eq!("db.example.com", config.get_host());
1155 assert_eq!(2345, config.get_port());
1156 }
1157
1158 #[cfg(any(
1159 feature = "rustls",
1160 feature = "native-tls",
1161 feature = "vendored-openssl"
1162 ))]
1163 #[test]
1164 fn get_hostname_in_certificate_falls_back_to_host() {
1165 let mut config = Config::new();
1166 config.host("real.host");
1167 assert_eq!(config.get_hostname_in_certificate(), "real.host");
1169 config.hostname_in_certificate("cert.host");
1171 assert_eq!(config.get_hostname_in_certificate(), "cert.host");
1172 }
1173
1174 #[cfg(any(
1175 feature = "rustls",
1176 feature = "native-tls",
1177 feature = "vendored-openssl"
1178 ))]
1179 #[test]
1180 fn client_certificate_sets_cert_and_key_source() {
1181 let mut config = Config::new();
1182 assert!(config.get_client_certificate().is_none());
1183
1184 config.client_certificate("/tmp/client.pem", "/tmp/client.key");
1185
1186 let cert = config
1187 .get_client_certificate()
1188 .expect("client certificate should be set");
1189 match &cert.source {
1190 ClientCertSource::CertAndKey { cert, key } => {
1191 assert_eq!(cert, &PathBuf::from("/tmp/client.pem"));
1192 assert_eq!(key, &PathBuf::from("/tmp/client.key"));
1193 }
1194 #[allow(unreachable_patterns)]
1195 other => panic!("expected CertAndKey source, got {other:?}"),
1196 }
1197 }
1198
1199 #[cfg(any(
1200 feature = "rustls",
1201 feature = "native-tls",
1202 feature = "vendored-openssl"
1203 ))]
1204 #[test]
1205 fn config_builder_sets_client_certificate() {
1206 let config = Config::builder()
1207 .host("localhost")
1208 .client_certificate("cert.der", "key.der")
1209 .build();
1210
1211 match &config
1212 .get_client_certificate()
1213 .expect("client certificate should be set")
1214 .source
1215 {
1216 ClientCertSource::CertAndKey { cert, key } => {
1217 assert_eq!(cert, &PathBuf::from("cert.der"));
1218 assert_eq!(key, &PathBuf::from("key.der"));
1219 }
1220 #[allow(unreachable_patterns)]
1221 other => panic!("expected CertAndKey source, got {other:?}"),
1222 }
1223 }
1224
1225 #[cfg(any(feature = "native-tls", feature = "vendored-openssl"))]
1226 #[test]
1227 fn client_certificate_pkcs12_sets_bundle_source() {
1228 let mut config = Config::new();
1229 config.client_certificate_pkcs12("/tmp/identity.pfx", "s3cr3t");
1230
1231 match &config
1232 .get_client_certificate()
1233 .expect("client certificate should be set")
1234 .source
1235 {
1236 ClientCertSource::Pkcs12 { path, password } => {
1237 assert_eq!(path, &PathBuf::from("/tmp/identity.pfx"));
1238 assert_eq!(password.as_str(), "s3cr3t");
1239 }
1240 other => panic!("expected Pkcs12 source, got {other:?}"),
1241 }
1242 }
1243
1244 #[cfg(any(feature = "native-tls", feature = "vendored-openssl"))]
1245 #[test]
1246 fn client_certificate_debug_redacts_pkcs12_password() {
1247 let mut config = Config::new();
1248 config.client_certificate_pkcs12("/tmp/identity.pfx", "topsecret");
1249
1250 let dbg = format!("{:?}", config.get_client_certificate().unwrap());
1251 assert!(dbg.contains("<redacted>"));
1252 assert!(!dbg.contains("topsecret"));
1253 }
1254
1255 #[cfg(all(unix, feature = "sspi-rs"))]
1256 #[test]
1257 fn ado_integrated_security_sspi_with_credentials_uses_windows_ntlm() {
1258 let config = Config::from_ado_string(
1259 "server=tcp:localhost,1433;IntegratedSecurity=SSPI;uid=DOMAIN\\user;pwd=secret",
1260 )
1261 .unwrap();
1262
1263 match &config.auth {
1264 AuthMethod::Windows(auth) => {
1265 assert_eq!("user", auth.user);
1266 assert_eq!(Some("DOMAIN"), auth.domain.as_deref());
1267 }
1268 other => panic!("expected Windows NTLM auth, got {other:?}"),
1269 }
1270 }
1271
1272 #[test]
1273 fn config_direct_setters_populate_fields() {
1274 let mut config = Config::new();
1275 config.database("northwind");
1276 config.instance_name("SQLEXPRESS");
1277 config.client_name("workstation-7");
1278
1279 assert_eq!(Some("northwind"), config.database.as_deref());
1280 assert_eq!(Some("SQLEXPRESS"), config.instance_name.as_deref());
1281 assert_eq!(Some("workstation-7"), config.client_name.as_deref());
1282 }
1283
1284 #[test]
1285 fn get_port_defaults_without_port_or_instance() {
1286 let config = Config::new();
1288 assert_eq!(1433, config.get_port());
1289 }
1290
1291 #[test]
1292 fn get_port_uses_sql_browser_port_for_named_instance() {
1293 let mut config = Config::new();
1295 config.instance_name("SQLEXPRESS");
1296 assert_eq!(1434, config.get_port());
1297 }
1298
1299 #[test]
1300 #[should_panic(expected = "mutual exclusive")]
1301 fn trust_cert_after_trust_cert_ca_panics() {
1302 let mut config = Config::new();
1303 config.trust_cert_ca("/tmp/ca.crt");
1304 config.trust_cert();
1305 }
1306
1307 #[test]
1308 #[should_panic(expected = "mutual exclusive")]
1309 fn trust_cert_ca_after_trust_cert_panics() {
1310 let mut config = Config::new();
1311 config.trust_cert();
1312 config.trust_cert_ca("/tmp/ca.crt");
1313 }
1314
1315 #[test]
1316 fn trust_cert_ca_sets_ca_location() {
1317 let mut config = Config::new();
1318 config.trust_cert_ca("/tmp/ca.crt");
1319 assert!(matches!(
1320 config.trust,
1321 TrustConfig::CaCertificateLocation(_)
1322 ));
1323 }
1324
1325 #[test]
1326 fn config_builder_covers_all_setters() {
1327 let config = Config::builder()
1328 .host("localhost")
1329 .instance_name("SQLEXPRESS")
1330 .encryption(EncryptionLevel::Off)
1331 .trust_cert_ca("/tmp/ca.crt")
1332 .build();
1333
1334 assert_eq!(Some("SQLEXPRESS"), config.instance_name.as_deref());
1335 assert!(matches!(config.encryption, EncryptionLevel::Off));
1336 assert!(matches!(
1337 config.trust,
1338 TrustConfig::CaCertificateLocation(_)
1339 ));
1340 }
1341
1342 #[test]
1343 fn config_builder_trust_cert_sets_trust_all() {
1344 let config = Config::builder().trust_cert().build();
1345 assert!(matches!(config.trust, TrustConfig::TrustAll));
1346 }
1347
1348 #[test]
1349 #[should_panic(expected = "mutual exclusive")]
1350 fn config_builder_trust_cert_after_ca_panics() {
1351 Config::builder().trust_cert_ca("/tmp/ca.crt").trust_cert();
1352 }
1353
1354 #[test]
1355 #[should_panic(expected = "mutual exclusive")]
1356 fn config_builder_trust_cert_ca_after_trust_cert_panics() {
1357 Config::builder().trust_cert().trust_cert_ca("/tmp/ca.crt");
1358 }
1359
1360 #[test]
1361 fn from_ado_string_populates_optional_fields() {
1362 let config = Config::from_ado_string(
1363 "server=tcp:my-server.com\\SQLEXPRESS;database=northwind;\
1364 HostNameInCertificate=cert.host;WorkstationID=ws-1",
1365 )
1366 .expect("valid ado string");
1367
1368 assert_eq!("my-server.com", config.get_host());
1369 assert_eq!(Some("SQLEXPRESS"), config.instance_name.as_deref());
1370 assert_eq!(Some("northwind"), config.database.as_deref());
1371 assert_eq!(Some("cert.host"), config.hostname_in_certificate.as_deref());
1372 assert_eq!(Some("ws-1"), config.client_name.as_deref());
1373 }
1374
1375 #[cfg(any(
1376 feature = "rustls",
1377 feature = "native-tls",
1378 feature = "vendored-openssl"
1379 ))]
1380 #[test]
1381 fn client_cert_source_debug_formats_cert_and_key() {
1382 let mut config = Config::new();
1383 config.client_certificate("/tmp/client.pem", "/tmp/client.key");
1384
1385 let dbg = format!("{:?}", config.get_client_certificate().unwrap().source);
1386 assert!(dbg.contains("CertAndKey"));
1387 assert!(dbg.contains("client.pem"));
1388 assert!(dbg.contains("client.key"));
1389 }
1390
1391 #[cfg(any(feature = "native-tls", feature = "vendored-openssl"))]
1392 #[test]
1393 fn config_builder_sets_pkcs12_client_certificate() {
1394 let config = Config::builder()
1395 .client_certificate_pkcs12("/tmp/identity.pfx", "s3cr3t")
1396 .build();
1397
1398 match &config
1399 .get_client_certificate()
1400 .expect("client certificate should be set")
1401 .source
1402 {
1403 ClientCertSource::Pkcs12 { path, password } => {
1404 assert_eq!(path, &PathBuf::from("/tmp/identity.pfx"));
1405 assert_eq!(password.as_str(), "s3cr3t");
1406 }
1407 other => panic!("expected Pkcs12 source, got {other:?}"),
1408 }
1409 }
1410
1411 #[cfg(all(unix, feature = "sspi-rs"))]
1412 #[test]
1413 fn ado_integrated_security_sspi_with_partial_credentials_uses_windows() {
1414 let config = Config::from_ado_string(
1416 "server=tcp:localhost,1433;IntegratedSecurity=SSPI;uid=onlyuser",
1417 )
1418 .unwrap();
1419
1420 match &config.auth {
1421 AuthMethod::Windows(auth) => {
1422 assert_eq!("onlyuser", auth.user);
1423 }
1424 other => panic!("expected Windows auth, got {other:?}"),
1425 }
1426 }
1427
1428 #[test]
1429 fn trust_cert_with_trust_cert_ca_is_an_error_not_a_panic() {
1430 let res = Config::from_ado_string(
1431 "server=tcp:h,1433;TrustServerCertificate=true;TrustServerCertificateCA=ca.pem",
1432 );
1433 assert!(matches!(res, Err(crate::Error::Conversion(_))));
1434
1435 let res = Config::from_jdbc_string(
1436 "jdbc:sqlserver://h:1433;trustServerCertificate=true;TrustServerCertificateCA=ca.pem",
1437 );
1438 assert!(matches!(res, Err(crate::Error::Conversion(_))));
1439 }
1440
1441 #[cfg(feature = "tds80")]
1442 #[test]
1443 fn strict_ignores_trust_server_certificate() {
1444 let config =
1445 Config::from_ado_string("server=tcp:h,1433;Encrypt=Strict;TrustServerCertificate=true")
1446 .unwrap();
1447 assert!(matches!(config.trust, TrustConfig::Default));
1448
1449 let config = Config::from_ado_string(
1451 "server=tcp:h,1433;encrypt=strict;TrustServerCertificate=yes;TrustServerCertificateCA=ca.pem",
1452 )
1453 .unwrap();
1454 assert!(matches!(
1455 config.trust,
1456 TrustConfig::CaCertificateLocation(_)
1457 ));
1458
1459 let mut config = Config::new();
1461 config.trust_cert();
1462 assert!(matches!(config.tls_trust(), TrustConfig::TrustAll));
1463 config.encryption(EncryptionLevel::Strict);
1464 assert!(matches!(config.tls_trust(), TrustConfig::Default));
1465 }
1466
1467 #[test]
1468 fn connect_timeout_defaults_and_setters() {
1469 use std::time::Duration;
1470 assert_eq!(
1471 Config::new().connect_timeout(),
1472 Some(Duration::from_secs(15))
1473 );
1474
1475 let mut config = Config::new();
1476 config.set_connect_timeout(None);
1477 assert_eq!(config.connect_timeout(), None);
1478
1479 let config = Config::builder()
1480 .connect_timeout(Some(Duration::from_secs(3)))
1481 .build();
1482 assert_eq!(config.connect_timeout(), Some(Duration::from_secs(3)));
1483
1484 let config = Config::from_ado_string("server=tcp:h,1433;Connect Timeout=0").unwrap();
1485 assert_eq!(config.connect_timeout(), None);
1486 let config = Config::from_jdbc_string("jdbc:sqlserver://h;loginTimeout=9").unwrap();
1487 assert_eq!(config.connect_timeout(), Some(Duration::from_secs(9)));
1488 }
1489}