mod ado_net;
mod jdbc;
use std::path::PathBuf;
use std::time::Duration;
use super::AuthMethod;
use crate::EncryptionLevel;
use ado_net::*;
use jdbc::*;
const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(15);
#[derive(Clone, Debug)]
pub struct Config {
pub(crate) host: Option<String>,
pub(crate) port: Option<u16>,
pub(crate) database: Option<String>,
pub(crate) instance_name: Option<String>,
pub(crate) application_name: Option<String>,
pub(crate) encryption: EncryptionLevel,
pub(crate) trust: TrustConfig,
pub(crate) auth: AuthMethod,
pub(crate) readonly: bool,
pub(crate) packet_size: Option<u32>,
pub(crate) hostname_in_certificate: Option<String>,
pub(crate) client_name: Option<String>,
pub(crate) multi_subnet_failover: bool,
pub(crate) connect_timeout: Option<Duration>,
pub(crate) server_spn: Option<String>,
#[cfg(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
))]
pub(crate) client_cert: Option<ClientCertificate>,
}
#[derive(Clone, Debug)]
pub(crate) enum TrustConfig {
#[allow(dead_code)]
CaCertificateLocation(PathBuf),
TrustAll,
Default,
}
#[cfg(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
))]
#[derive(Clone, Debug)]
pub(crate) struct ClientCertificate {
pub(crate) source: ClientCertSource,
}
#[cfg(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
))]
#[derive(Clone)]
pub(crate) enum ClientCertSource {
CertAndKey { cert: PathBuf, key: PathBuf },
#[cfg(any(feature = "native-tls", feature = "vendored-openssl"))]
Pkcs12 {
path: PathBuf,
password: zeroize::Zeroizing<String>,
},
}
#[cfg(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
))]
impl std::fmt::Debug for ClientCertSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ClientCertSource::CertAndKey { cert, key } => f
.debug_struct("CertAndKey")
.field("cert", cert)
.field("key", key)
.finish(),
#[cfg(any(feature = "native-tls", feature = "vendored-openssl"))]
ClientCertSource::Pkcs12 { path, .. } => f
.debug_struct("Pkcs12")
.field("path", path)
.field("password", &"<redacted>")
.finish(),
}
}
}
impl Default for Config {
fn default() -> Self {
Self {
host: None,
port: None,
database: None,
instance_name: None,
application_name: None,
#[cfg(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
))]
encryption: EncryptionLevel::Required,
#[cfg(not(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
)))]
encryption: EncryptionLevel::NotSupported,
trust: TrustConfig::Default,
auth: AuthMethod::None,
readonly: false,
packet_size: None,
hostname_in_certificate: None,
client_name: None,
multi_subnet_failover: false,
connect_timeout: Some(DEFAULT_CONNECT_TIMEOUT),
server_spn: None,
#[cfg(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
))]
client_cert: None,
}
}
}
impl Drop for Config {
fn drop(&mut self) {
if let AuthMethod::AADToken(token) = &mut self.auth {
zeroize::Zeroize::zeroize(token);
}
}
}
impl Config {
pub fn new() -> Self {
Self::default()
}
pub fn builder() -> ConfigBuilder {
ConfigBuilder {
inner: Self::default(),
}
}
pub fn host(&mut self, host: impl ToString) {
self.host = Some(host.to_string());
}
pub fn port(&mut self, port: u16) {
self.port = Some(port);
}
pub fn database(&mut self, database: impl ToString) {
self.database = Some(database.to_string())
}
pub fn instance_name(&mut self, name: impl ToString) {
self.instance_name = Some(name.to_string());
}
pub fn application_name(&mut self, name: impl ToString) {
self.application_name = Some(name.to_string());
}
pub fn packet_size(&mut self, size: u32) {
self.packet_size = Some(size);
}
pub fn get_packet_size(&self) -> Option<u32> {
self.packet_size
}
pub fn encryption(&mut self, encryption: EncryptionLevel) {
self.encryption = encryption;
}
pub fn trust_cert(&mut self) {
if let TrustConfig::CaCertificateLocation(_) = &self.trust {
panic!("'trust_cert' and 'trust_cert_ca' are mutual exclusive! Only use one.")
}
self.trust = TrustConfig::TrustAll;
}
pub fn trust_cert_ca(&mut self, path: impl ToString) {
if let TrustConfig::TrustAll = &self.trust {
panic!("'trust_cert' and 'trust_cert_ca' are mutual exclusive! Only use one.")
} else {
self.trust = TrustConfig::CaCertificateLocation(PathBuf::from(path.to_string()))
}
}
pub fn hostname_in_certificate(&mut self, hostname: impl ToString) {
self.hostname_in_certificate = Some(hostname.to_string());
}
pub fn client_name(&mut self, name: impl ToString) {
self.client_name = Some(name.to_string());
}
pub fn authentication(&mut self, auth: AuthMethod) {
self.auth = auth;
}
pub fn readonly(&mut self, readnoly: bool) {
self.readonly = readnoly;
}
pub fn multi_subnet_failover(&mut self, multi_subnet_failover: bool) {
self.multi_subnet_failover = multi_subnet_failover;
}
pub fn get_multi_subnet_failover(&self) -> bool {
self.multi_subnet_failover
}
pub fn connect_timeout(&self) -> Option<Duration> {
self.connect_timeout
}
pub fn set_connect_timeout(&mut self, timeout: Option<Duration>) {
self.connect_timeout = timeout;
}
pub fn server_spn(&mut self, spn: impl ToString) {
self.server_spn = Some(spn.to_string());
}
pub(crate) fn get_server_spn(&self) -> Option<&str> {
self.server_spn
.as_deref()
.filter(|spn| !spn.trim().is_empty())
}
#[cfg(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
))]
#[cfg_attr(
docsrs,
doc(cfg(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
)))
)]
pub fn client_certificate(&mut self, cert: impl Into<PathBuf>, key: impl Into<PathBuf>) {
self.client_cert = Some(ClientCertificate {
source: ClientCertSource::CertAndKey {
cert: cert.into(),
key: key.into(),
},
});
}
#[cfg(any(feature = "native-tls", feature = "vendored-openssl"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "vendored-openssl")))
)]
pub fn client_certificate_pkcs12(
&mut self,
path: impl Into<PathBuf>,
password: impl Into<String>,
) {
self.client_cert = Some(ClientCertificate {
source: ClientCertSource::Pkcs12 {
path: path.into(),
password: zeroize::Zeroizing::new(password.into()),
},
});
}
#[cfg(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
))]
pub(crate) fn get_client_certificate(&self) -> Option<&ClientCertificate> {
self.client_cert.as_ref()
}
pub(crate) fn get_host(&self) -> &str {
self.host
.as_deref()
.filter(|v| v != &".")
.unwrap_or("localhost")
}
#[cfg(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
))]
pub(crate) fn get_hostname_in_certificate(&self) -> &str {
self.hostname_in_certificate
.as_deref()
.unwrap_or_else(|| self.get_host())
}
#[cfg(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
))]
pub(crate) fn tls_trust(&self) -> &TrustConfig {
static VALIDATE: TrustConfig = TrustConfig::Default;
match (&self.trust, self.encryption) {
(TrustConfig::TrustAll, EncryptionLevel::Strict) => {
tracing::warn!(
"TrustServerCertificate is ignored with strict encryption; validating the server certificate."
);
&VALIDATE
}
(trust, _) => trust,
}
}
pub(crate) fn get_port(&self) -> u16 {
match (self.port, self.instance_name.as_ref()) {
(Some(port), _) => port,
(None, Some(_)) => 1434,
(None, None) => 1433,
}
}
pub fn get_addr(&self) -> String {
format!("{}:{}", self.get_host(), self.get_port())
}
pub fn from_ado_string(s: &str) -> crate::Result<Self> {
let ado: AdoNetConfig = s.parse()?;
Self::from_config_string(ado)
}
pub fn from_jdbc_string(s: &str) -> crate::Result<Self> {
let jdbc: JdbcConfig = s.parse()?;
Self::from_config_string(jdbc)
}
fn from_config_string(s: impl ConfigString) -> crate::Result<Self> {
let mut builder = Self::new();
let server = s.server()?;
if let Some(host) = server.host {
builder.host(host);
}
if let Some(port) = server.port {
builder.port(port);
}
if let Some(instance) = server.instance {
builder.instance_name(instance);
}
builder.authentication(s.authentication()?);
if let Some(database) = s.database() {
builder.database(database);
}
if let Some(name) = s.application_name() {
builder.application_name(name);
}
let encryption = s.encrypt()?;
builder.encryption(encryption);
match (s.trust_cert()?, s.trust_cert_ca()) {
(true, ca) if encryption == EncryptionLevel::Strict => {
tracing::warn!(
"TrustServerCertificate is ignored with Encrypt=Strict; validating the server certificate."
);
if let Some(ca) = ca {
builder.trust_cert_ca(ca);
}
}
(true, Some(_)) => {
return Err(crate::Error::Conversion(
"Connection string: TrustServerCertificate=true and TrustServerCertificateCA are mutually exclusive"
.into(),
))
}
(true, None) => builder.trust_cert(),
(false, Some(ca)) => builder.trust_cert_ca(ca),
(false, None) => (),
}
if let Some(hostname_in_cert) = s.hostname_in_certificate() {
builder.hostname_in_certificate(hostname_in_cert);
}
builder.readonly(s.readonly());
if let Some(client_name) = s.client_name() {
builder.client_name(client_name);
}
builder.multi_subnet_failover(s.multi_subnet_failover()?);
if let Some(size) = s.packet_size()? {
builder.packet_size(size);
}
builder.set_connect_timeout(s.connect_timeout()?);
if let Some(spn) = s.server_spn() {
builder.server_spn(spn);
}
Ok(builder)
}
}
#[derive(Clone, Debug)]
pub struct ConfigBuilder {
inner: Config,
}
impl ConfigBuilder {
pub fn host(mut self, host: impl ToString) -> Self {
self.inner.host = Some(host.to_string());
self
}
pub fn port(mut self, port: u16) -> Self {
self.inner.port = Some(port);
self
}
pub fn database(mut self, database: impl ToString) -> Self {
self.inner.database = Some(database.to_string());
self
}
pub fn instance_name(mut self, name: impl ToString) -> Self {
self.inner.instance_name = Some(name.to_string());
self
}
pub fn application_name(mut self, name: impl ToString) -> Self {
self.inner.application_name = Some(name.to_string());
self
}
pub fn encryption(mut self, encryption: EncryptionLevel) -> Self {
self.inner.encryption = encryption;
self
}
pub fn trust_cert(mut self) -> Self {
if let TrustConfig::CaCertificateLocation(_) = &self.inner.trust {
panic!("'trust_cert' and 'trust_cert_ca' are mutual exclusive! Only use one.")
}
self.inner.trust = TrustConfig::TrustAll;
self
}
pub fn trust_cert_ca(mut self, path: impl ToString) -> Self {
if let TrustConfig::TrustAll = &self.inner.trust {
panic!("'trust_cert' and 'trust_cert_ca' are mutual exclusive! Only use one.")
} else {
self.inner.trust = TrustConfig::CaCertificateLocation(PathBuf::from(path.to_string()))
}
self
}
pub fn authentication(mut self, auth: AuthMethod) -> Self {
self.inner.auth = auth;
self
}
pub fn readonly(mut self, readonly: bool) -> Self {
self.inner.readonly = readonly;
self
}
pub fn connect_timeout(mut self, timeout: Option<Duration>) -> Self {
self.inner.connect_timeout = timeout;
self
}
pub fn server_spn(mut self, spn: impl ToString) -> Self {
self.inner.server_spn(spn);
self
}
#[cfg(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
))]
#[cfg_attr(
docsrs,
doc(cfg(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
)))
)]
pub fn client_certificate(mut self, cert: impl Into<PathBuf>, key: impl Into<PathBuf>) -> Self {
self.inner.client_certificate(cert, key);
self
}
#[cfg(any(feature = "native-tls", feature = "vendored-openssl"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "vendored-openssl")))
)]
pub fn client_certificate_pkcs12(
mut self,
path: impl Into<PathBuf>,
password: impl Into<String>,
) -> Self {
self.inner.client_certificate_pkcs12(path, password);
self
}
pub fn build(self) -> Config {
self.inner
}
}
impl From<Config> for ConfigBuilder {
fn from(config: Config) -> Self {
ConfigBuilder { inner: config }
}
}
impl From<ConfigBuilder> for Config {
fn from(builder: ConfigBuilder) -> Self {
builder.inner
}
}
pub(crate) struct ServerDefinition {
host: Option<String>,
port: Option<u16>,
instance: Option<String>,
}
pub(crate) trait ConfigString {
fn get(&self, key: &str) -> Option<&str>;
fn first(&self, keys: &[&str]) -> Option<&str> {
keys.iter().find_map(|key| self.get(key))
}
fn server(&self) -> crate::Result<ServerDefinition>;
fn connect_timeout(&self) -> crate::Result<Option<Duration>>;
fn packet_size(&self) -> crate::Result<Option<u32>> {
Ok(None)
}
fn integrated_security(&self) -> crate::Result<bool> {
match self.first(&["integrated security", "integratedsecurity"]) {
Some(v) if v.trim().eq_ignore_ascii_case("sspi") => Ok(true),
Some(v) => Self::parse_bool(v),
None => Ok(false),
}
}
fn authentication(&self) -> crate::Result<AuthMethod> {
let user = self.first(&["uid", "username", "user", "user id"]);
let pw = self.first(&["password", "pwd"]);
if !self.integrated_security()? {
return Ok(AuthMethod::sql_server(user.unwrap_or(""), pw.unwrap_or("")));
}
#[allow(unused)]
let missing = |what: &str| -> crate::Result<AuthMethod> {
Err(crate::Error::Conversion(
format!("Connection string: Integrated Security needs {what}").into(),
))
};
#[cfg(all(windows, feature = "winauth"))]
return match (user, pw) {
(None, None) => Ok(AuthMethod::Integrated),
_ => Ok(AuthMethod::windows(user.unwrap_or(""), pw.unwrap_or(""))),
};
#[cfg(all(windows, not(feature = "winauth")))]
return missing("the crate's `winauth` feature");
#[cfg(all(unix, feature = "sspi-rs"))]
return match (user, pw) {
(Some(user), Some(pw)) => Ok(AuthMethod::windows(user, pw)),
#[cfg(feature = "integrated-auth-gssapi")]
(None, None) => Ok(AuthMethod::Integrated),
#[cfg(not(feature = "integrated-auth-gssapi"))]
(None, None) => missing("a User ID, or the crate's `integrated-auth-gssapi` feature"),
_ => Ok(AuthMethod::windows(user.unwrap_or(""), pw.unwrap_or(""))),
};
#[cfg(all(unix, feature = "integrated-auth-gssapi", not(feature = "sspi-rs")))]
return Ok(AuthMethod::Integrated);
#[cfg(all(
unix,
not(any(feature = "integrated-auth-gssapi", feature = "sspi-rs"))
))]
return missing(
"the crate's `integrated-auth-gssapi` (Kerberos) or `sspi-rs` (NTLM) feature",
);
#[cfg(not(any(windows, unix)))]
return missing("Windows or Unix");
}
fn database(&self) -> Option<String> {
self.first(&["database", "initial catalog", "databasename"])
.map(|db| db.to_string())
}
fn application_name(&self) -> Option<String> {
self.first(&["application name", "applicationname"])
.map(|name| name.to_string())
}
fn trust_cert(&self) -> crate::Result<bool> {
self.first(&["trust server certificate", "trustservercertificate"])
.map(Self::parse_bool)
.unwrap_or(Ok(false))
}
fn trust_cert_ca(&self) -> Option<String> {
self.first(&["trustservercertificateca"])
.map(|ca| ca.to_string())
}
fn hostname_in_certificate(&self) -> Option<String> {
self.first(&[
"host name in certificate",
"hostnameincertificate",
"hostname in certificate",
])
.map(|host| host.to_string())
}
fn client_name(&self) -> Option<String> {
self.first(&["workstation id", "workstationid"])
.map(|name| name.to_string())
}
fn server_spn(&self) -> Option<String> {
self.first(&["server spn", "serverspn"]).map(str::to_owned)
}
#[cfg(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
))]
fn encrypt(&self) -> crate::Result<EncryptionLevel> {
self.first(&["encrypt"])
.map(|val| {
let val = val.trim();
let is = |s: &str| val.eq_ignore_ascii_case(s);
if is("true") || is("yes") || is("mandatory") {
Ok(EncryptionLevel::Required)
} else if is("false") || is("no") || is("optional") {
Ok(EncryptionLevel::Off)
} else if val == "DANGER_PLAINTEXT" {
Ok(EncryptionLevel::NotSupported)
} else if is("strict") && cfg!(feature = "tds80") {
Ok(EncryptionLevel::Strict)
} else if is("strict") {
Err(crate::Error::Conversion(
"encrypt=strict requires the crate's `tds80` feature to be enabled".into(),
))
} else {
Err(crate::Error::Conversion(
"Connection string: Encrypt must be true, false, yes, no, mandatory, optional or strict".into(),
))
}
})
.unwrap_or(Ok(EncryptionLevel::Required))
}
#[cfg(not(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
)))]
fn encrypt(&self) -> crate::Result<EncryptionLevel> {
Ok(EncryptionLevel::NotSupported)
}
fn parse_bool<T: AsRef<str>>(v: T) -> crate::Result<bool> {
match v.as_ref().trim().to_lowercase().as_str() {
"true" | "yes" => Ok(true),
"false" | "no" => Ok(false),
_ => Err(crate::Error::Conversion(
"Connection string: Not a valid boolean".into(),
)),
}
}
fn readonly(&self) -> bool {
self.first(&["application intent", "applicationintent"])
.filter(|val| val.trim().eq_ignore_ascii_case("ReadOnly"))
.is_some()
}
fn multi_subnet_failover(&self) -> crate::Result<bool> {
self.first(&["multi subnet failover", "multisubnetfailover"])
.map(Self::parse_bool)
.unwrap_or(Ok(false))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn config_builder_constructs_config() {
let config = Config::builder()
.host("db.example.com")
.port(4433)
.database("northwind")
.application_name("my-app")
.authentication(AuthMethod::sql_server("SA", "secret"))
.readonly(true)
.build();
assert_eq!("db.example.com", config.get_host());
assert_eq!(4433, config.get_port());
assert_eq!("db.example.com:4433", config.get_addr());
assert_eq!(Some("northwind"), config.database.as_deref());
assert_eq!(Some("my-app"), config.application_name.as_deref());
assert!(config.readonly);
assert!(matches!(config.auth, AuthMethod::SqlServer(_)));
assert!(matches!(config.trust, TrustConfig::Default));
}
#[test]
fn config_builder_roundtrips_via_from() {
let config = Config::builder().host("localhost").port(1433).build();
let builder: ConfigBuilder = config.into();
let config = builder.database("master").build();
assert_eq!("localhost:1433", config.get_addr());
assert_eq!(Some("master"), config.database.as_deref());
}
#[test]
fn config_from_builder_carries_builder_settings() {
let config: Config = Config::builder().host("db.internal").port(2020).into();
assert_eq!("db.internal", config.get_host());
assert_eq!(2020, config.get_port());
}
#[test]
fn get_packet_size_reflects_the_set_value() {
let mut config = Config::new();
assert_eq!(config.get_packet_size(), None);
config.packet_size(8192);
assert_eq!(config.get_packet_size(), Some(8192));
}
#[test]
fn server_spn_setters() {
let mut config = Config::new();
assert_eq!(config.get_server_spn(), None);
config.server_spn("MSSQLSvc/a:1");
assert_eq!(config.get_server_spn(), Some("MSSQLSvc/a:1"));
config.server_spn("");
assert_eq!(config.get_server_spn(), None);
let config = Config::builder().server_spn("MSSQLSvc/b:2").build();
assert_eq!(config.get_server_spn(), Some("MSSQLSvc/b:2"));
}
#[test]
fn from_jdbc_string_parses_host_and_port() {
let config =
Config::from_jdbc_string("jdbc:sqlserver://db.example.com:2345").expect("valid jdbc");
assert_eq!("db.example.com", config.get_host());
assert_eq!(2345, config.get_port());
}
#[cfg(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
))]
#[test]
fn get_hostname_in_certificate_falls_back_to_host() {
let mut config = Config::new();
config.host("real.host");
assert_eq!(config.get_hostname_in_certificate(), "real.host");
config.hostname_in_certificate("cert.host");
assert_eq!(config.get_hostname_in_certificate(), "cert.host");
}
#[cfg(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
))]
#[test]
fn client_certificate_sets_cert_and_key_source() {
let mut config = Config::new();
assert!(config.get_client_certificate().is_none());
config.client_certificate("/tmp/client.pem", "/tmp/client.key");
let cert = config
.get_client_certificate()
.expect("client certificate should be set");
match &cert.source {
ClientCertSource::CertAndKey { cert, key } => {
assert_eq!(cert, &PathBuf::from("/tmp/client.pem"));
assert_eq!(key, &PathBuf::from("/tmp/client.key"));
}
#[allow(unreachable_patterns)]
other => panic!("expected CertAndKey source, got {other:?}"),
}
}
#[cfg(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
))]
#[test]
fn config_builder_sets_client_certificate() {
let config = Config::builder()
.host("localhost")
.client_certificate("cert.der", "key.der")
.build();
match &config
.get_client_certificate()
.expect("client certificate should be set")
.source
{
ClientCertSource::CertAndKey { cert, key } => {
assert_eq!(cert, &PathBuf::from("cert.der"));
assert_eq!(key, &PathBuf::from("key.der"));
}
#[allow(unreachable_patterns)]
other => panic!("expected CertAndKey source, got {other:?}"),
}
}
#[cfg(any(feature = "native-tls", feature = "vendored-openssl"))]
#[test]
fn client_certificate_pkcs12_sets_bundle_source() {
let mut config = Config::new();
config.client_certificate_pkcs12("/tmp/identity.pfx", "s3cr3t");
match &config
.get_client_certificate()
.expect("client certificate should be set")
.source
{
ClientCertSource::Pkcs12 { path, password } => {
assert_eq!(path, &PathBuf::from("/tmp/identity.pfx"));
assert_eq!(password.as_str(), "s3cr3t");
}
other => panic!("expected Pkcs12 source, got {other:?}"),
}
}
#[cfg(any(feature = "native-tls", feature = "vendored-openssl"))]
#[test]
fn client_certificate_debug_redacts_pkcs12_password() {
let mut config = Config::new();
config.client_certificate_pkcs12("/tmp/identity.pfx", "topsecret");
let dbg = format!("{:?}", config.get_client_certificate().unwrap());
assert!(dbg.contains("<redacted>"));
assert!(!dbg.contains("topsecret"));
}
#[cfg(all(unix, feature = "sspi-rs"))]
#[test]
fn ado_integrated_security_sspi_with_credentials_uses_windows_ntlm() {
let config = Config::from_ado_string(
"server=tcp:localhost,1433;IntegratedSecurity=SSPI;uid=DOMAIN\\user;pwd=secret",
)
.unwrap();
match &config.auth {
AuthMethod::Windows(auth) => {
assert_eq!("user", auth.user);
assert_eq!(Some("DOMAIN"), auth.domain.as_deref());
}
other => panic!("expected Windows NTLM auth, got {other:?}"),
}
}
#[test]
fn config_direct_setters_populate_fields() {
let mut config = Config::new();
config.database("northwind");
config.instance_name("SQLEXPRESS");
config.client_name("workstation-7");
assert_eq!(Some("northwind"), config.database.as_deref());
assert_eq!(Some("SQLEXPRESS"), config.instance_name.as_deref());
assert_eq!(Some("workstation-7"), config.client_name.as_deref());
}
#[test]
fn get_port_defaults_without_port_or_instance() {
let config = Config::new();
assert_eq!(1433, config.get_port());
}
#[test]
fn get_port_uses_sql_browser_port_for_named_instance() {
let mut config = Config::new();
config.instance_name("SQLEXPRESS");
assert_eq!(1434, config.get_port());
}
#[test]
#[should_panic(expected = "mutual exclusive")]
fn trust_cert_after_trust_cert_ca_panics() {
let mut config = Config::new();
config.trust_cert_ca("/tmp/ca.crt");
config.trust_cert();
}
#[test]
#[should_panic(expected = "mutual exclusive")]
fn trust_cert_ca_after_trust_cert_panics() {
let mut config = Config::new();
config.trust_cert();
config.trust_cert_ca("/tmp/ca.crt");
}
#[test]
fn trust_cert_ca_sets_ca_location() {
let mut config = Config::new();
config.trust_cert_ca("/tmp/ca.crt");
assert!(matches!(
config.trust,
TrustConfig::CaCertificateLocation(_)
));
}
#[test]
fn config_builder_covers_all_setters() {
let config = Config::builder()
.host("localhost")
.instance_name("SQLEXPRESS")
.encryption(EncryptionLevel::Off)
.trust_cert_ca("/tmp/ca.crt")
.build();
assert_eq!(Some("SQLEXPRESS"), config.instance_name.as_deref());
assert!(matches!(config.encryption, EncryptionLevel::Off));
assert!(matches!(
config.trust,
TrustConfig::CaCertificateLocation(_)
));
}
#[test]
fn config_builder_trust_cert_sets_trust_all() {
let config = Config::builder().trust_cert().build();
assert!(matches!(config.trust, TrustConfig::TrustAll));
}
#[test]
#[should_panic(expected = "mutual exclusive")]
fn config_builder_trust_cert_after_ca_panics() {
Config::builder().trust_cert_ca("/tmp/ca.crt").trust_cert();
}
#[test]
#[should_panic(expected = "mutual exclusive")]
fn config_builder_trust_cert_ca_after_trust_cert_panics() {
Config::builder().trust_cert().trust_cert_ca("/tmp/ca.crt");
}
#[test]
fn from_ado_string_populates_optional_fields() {
let config = Config::from_ado_string(
"server=tcp:my-server.com\\SQLEXPRESS;database=northwind;\
HostNameInCertificate=cert.host;WorkstationID=ws-1",
)
.expect("valid ado string");
assert_eq!("my-server.com", config.get_host());
assert_eq!(Some("SQLEXPRESS"), config.instance_name.as_deref());
assert_eq!(Some("northwind"), config.database.as_deref());
assert_eq!(Some("cert.host"), config.hostname_in_certificate.as_deref());
assert_eq!(Some("ws-1"), config.client_name.as_deref());
}
#[cfg(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
))]
#[test]
fn client_cert_source_debug_formats_cert_and_key() {
let mut config = Config::new();
config.client_certificate("/tmp/client.pem", "/tmp/client.key");
let dbg = format!("{:?}", config.get_client_certificate().unwrap().source);
assert!(dbg.contains("CertAndKey"));
assert!(dbg.contains("client.pem"));
assert!(dbg.contains("client.key"));
}
#[cfg(any(feature = "native-tls", feature = "vendored-openssl"))]
#[test]
fn config_builder_sets_pkcs12_client_certificate() {
let config = Config::builder()
.client_certificate_pkcs12("/tmp/identity.pfx", "s3cr3t")
.build();
match &config
.get_client_certificate()
.expect("client certificate should be set")
.source
{
ClientCertSource::Pkcs12 { path, password } => {
assert_eq!(path, &PathBuf::from("/tmp/identity.pfx"));
assert_eq!(password.as_str(), "s3cr3t");
}
other => panic!("expected Pkcs12 source, got {other:?}"),
}
}
#[cfg(all(unix, feature = "sspi-rs"))]
#[test]
fn ado_integrated_security_sspi_with_partial_credentials_uses_windows() {
let config = Config::from_ado_string(
"server=tcp:localhost,1433;IntegratedSecurity=SSPI;uid=onlyuser",
)
.unwrap();
match &config.auth {
AuthMethod::Windows(auth) => {
assert_eq!("onlyuser", auth.user);
}
other => panic!("expected Windows auth, got {other:?}"),
}
}
#[test]
fn trust_cert_with_trust_cert_ca_is_an_error_not_a_panic() {
let res = Config::from_ado_string(
"server=tcp:h,1433;TrustServerCertificate=true;TrustServerCertificateCA=ca.pem",
);
assert!(matches!(res, Err(crate::Error::Conversion(_))));
let res = Config::from_jdbc_string(
"jdbc:sqlserver://h:1433;trustServerCertificate=true;TrustServerCertificateCA=ca.pem",
);
assert!(matches!(res, Err(crate::Error::Conversion(_))));
}
#[cfg(feature = "tds80")]
#[test]
fn strict_ignores_trust_server_certificate() {
let config =
Config::from_ado_string("server=tcp:h,1433;Encrypt=Strict;TrustServerCertificate=true")
.unwrap();
assert!(matches!(config.trust, TrustConfig::Default));
let config = Config::from_ado_string(
"server=tcp:h,1433;encrypt=strict;TrustServerCertificate=yes;TrustServerCertificateCA=ca.pem",
)
.unwrap();
assert!(matches!(
config.trust,
TrustConfig::CaCertificateLocation(_)
));
let mut config = Config::new();
config.trust_cert();
assert!(matches!(config.tls_trust(), TrustConfig::TrustAll));
config.encryption(EncryptionLevel::Strict);
assert!(matches!(config.tls_trust(), TrustConfig::Default));
}
#[test]
fn connect_timeout_defaults_and_setters() {
use std::time::Duration;
assert_eq!(
Config::new().connect_timeout(),
Some(Duration::from_secs(15))
);
let mut config = Config::new();
config.set_connect_timeout(None);
assert_eq!(config.connect_timeout(), None);
let config = Config::builder()
.connect_timeout(Some(Duration::from_secs(3)))
.build();
assert_eq!(config.connect_timeout(), Some(Duration::from_secs(3)));
let config = Config::from_ado_string("server=tcp:h,1433;Connect Timeout=0").unwrap();
assert_eq!(config.connect_timeout(), None);
let config = Config::from_jdbc_string("jdbc:sqlserver://h;loginTimeout=9").unwrap();
assert_eq!(config.connect_timeout(), Some(Duration::from_secs(9)));
}
}