Skip to main content

tiberius/client/
config.rs

1mod ado_net;
2mod jdbc;
3
4use std::collections::HashMap;
5use std::path::PathBuf;
6
7use super::AuthMethod;
8use crate::EncryptionLevel;
9use ado_net::*;
10use jdbc::*;
11
12#[derive(Clone, Debug)]
13/// The `Config` struct contains all configuration information
14/// required for connecting to the database with a [`Client`]. It also provides
15/// the server address when connecting to a `TcpStream` via the
16/// [`get_addr`] method.
17///
18/// When using an [ADO.NET connection string], it can be
19/// constructed using the [`from_ado_string`] function.
20///
21/// Alternatively, a [`ConfigBuilder`] can be used for an ergonomic,
22/// chainable construction. Create one via [`builder`], call its
23/// setter methods and finalize it with [`build`].
24///
25/// [`Client`]: struct.Client.html
26/// [ADO.NET connection string]: https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/connection-strings
27/// [`from_ado_string`]: struct.Config.html#method.from_ado_string
28/// [`get_addr`]: struct.Config.html#method.get_addr
29/// [`ConfigBuilder`]: struct.ConfigBuilder.html
30/// [`builder`]: struct.Config.html#method.builder
31/// [`build`]: struct.ConfigBuilder.html#method.build
32pub struct Config {
33    pub(crate) host: Option<String>,
34    pub(crate) port: Option<u16>,
35    pub(crate) database: Option<String>,
36    pub(crate) instance_name: Option<String>,
37    pub(crate) application_name: Option<String>,
38    pub(crate) encryption: EncryptionLevel,
39    pub(crate) trust: TrustConfig,
40    pub(crate) auth: AuthMethod,
41    pub(crate) readonly: bool,
42    pub(crate) packet_size: Option<u32>,
43    pub(crate) hostname_in_certificate: Option<String>,
44    pub(crate) client_name: Option<String>,
45    pub(crate) multi_subnet_failover: bool,
46    #[cfg(any(
47        feature = "rustls",
48        feature = "native-tls",
49        feature = "vendored-openssl"
50    ))]
51    pub(crate) client_cert: Option<ClientCertificate>,
52}
53
54#[derive(Clone, Debug)]
55pub(crate) enum TrustConfig {
56    #[allow(dead_code)]
57    CaCertificateLocation(PathBuf),
58    TrustAll,
59    Default,
60}
61
62/// A client certificate and its private key, presented to the server during the
63/// TLS handshake to authenticate the *client* (mutual TLS / TDS 8.0
64/// `ENCRYPT_CLIENT_CERT`).
65///
66/// Construct one indirectly via [`Config::client_certificate`] (PEM/DER
67/// certificate + private-key files) or [`Config::client_certificate_pkcs12`]
68/// (a PKCS#12 / PFX bundle, `native-tls` and `vendored-openssl` only).
69#[cfg(any(
70    feature = "rustls",
71    feature = "native-tls",
72    feature = "vendored-openssl"
73))]
74#[derive(Clone, Debug)]
75pub(crate) struct ClientCertificate {
76    pub(crate) source: ClientCertSource,
77}
78
79#[cfg(any(
80    feature = "rustls",
81    feature = "native-tls",
82    feature = "vendored-openssl"
83))]
84#[derive(Clone)]
85pub(crate) enum ClientCertSource {
86    /// A certificate file and a separate private-key file. Both may be PEM
87    /// (`.pem`/`.crt` for the certificate, `.pem`/`.key` for the key) or DER
88    /// (`.der`); the concrete format is detected from the file extension by the
89    /// active TLS backend.
90    CertAndKey { cert: PathBuf, key: PathBuf },
91    /// A PKCS#12 / PFX bundle path together with its decryption password. Only
92    /// supported by the `native-tls` and `vendored-openssl` backends.
93    #[cfg(any(feature = "native-tls", feature = "vendored-openssl"))]
94    Pkcs12 {
95        path: PathBuf,
96        password: zeroize::Zeroizing<String>,
97    },
98}
99
100// Manual `Debug` so the PKCS#12 password is never printed.
101#[cfg(any(
102    feature = "rustls",
103    feature = "native-tls",
104    feature = "vendored-openssl"
105))]
106impl std::fmt::Debug for ClientCertSource {
107    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108        match self {
109            ClientCertSource::CertAndKey { cert, key } => f
110                .debug_struct("CertAndKey")
111                .field("cert", cert)
112                .field("key", key)
113                .finish(),
114            #[cfg(any(feature = "native-tls", feature = "vendored-openssl"))]
115            ClientCertSource::Pkcs12 { path, .. } => f
116                .debug_struct("Pkcs12")
117                .field("path", path)
118                .field("password", &"<redacted>")
119                .finish(),
120        }
121    }
122}
123
124impl Default for Config {
125    fn default() -> Self {
126        Self {
127            host: None,
128            port: None,
129            database: None,
130            instance_name: None,
131            application_name: None,
132            #[cfg(any(
133                feature = "rustls",
134                feature = "native-tls",
135                feature = "vendored-openssl"
136            ))]
137            encryption: EncryptionLevel::Required,
138            #[cfg(not(any(
139                feature = "rustls",
140                feature = "native-tls",
141                feature = "vendored-openssl"
142            )))]
143            encryption: EncryptionLevel::NotSupported,
144            trust: TrustConfig::Default,
145            auth: AuthMethod::None,
146            readonly: false,
147            packet_size: None,
148            hostname_in_certificate: None,
149            client_name: None,
150            multi_subnet_failover: false,
151            #[cfg(any(
152                feature = "rustls",
153                feature = "native-tls",
154                feature = "vendored-openssl"
155            ))]
156            client_cert: None,
157        }
158    }
159}
160
161impl Config {
162    /// Create a new `Config` with the default settings.
163    pub fn new() -> Self {
164        Self::default()
165    }
166
167    /// Create a new [`ConfigBuilder`] initialized with the default settings.
168    ///
169    /// This provides an ergonomic, chainable alternative to constructing a
170    /// [`Config`] via its individual setter methods.
171    ///
172    /// # Example
173    ///
174    /// ```
175    /// # use tiberius::{Config, AuthMethod};
176    /// let config = Config::builder()
177    ///     .host("localhost")
178    ///     .port(1433)
179    ///     .database("master")
180    ///     .authentication(AuthMethod::sql_server("SA", "<password>"))
181    ///     .build();
182    ///
183    /// assert_eq!("localhost:1433", config.get_addr());
184    /// ```
185    ///
186    /// [`ConfigBuilder`]: struct.ConfigBuilder.html
187    /// [`Config`]: struct.Config.html
188    pub fn builder() -> ConfigBuilder {
189        ConfigBuilder {
190            inner: Self::default(),
191        }
192    }
193
194    /// A host or ip address to connect to.
195    ///
196    /// - Defaults to `localhost`.
197    pub fn host(&mut self, host: impl ToString) {
198        self.host = Some(host.to_string());
199    }
200
201    /// The server port.
202    ///
203    /// - Defaults to `1433`.
204    pub fn port(&mut self, port: u16) {
205        self.port = Some(port);
206    }
207
208    /// The database to connect to.
209    ///
210    /// - Defaults to `master`.
211    pub fn database(&mut self, database: impl ToString) {
212        self.database = Some(database.to_string())
213    }
214
215    /// The instance name as defined in the SQL Browser. Only available on
216    /// Windows platforms.
217    ///
218    /// If specified, the port is replaced with the value returned from the
219    /// browser.
220    ///
221    /// - Defaults to no name specified.
222    pub fn instance_name(&mut self, name: impl ToString) {
223        self.instance_name = Some(name.to_string());
224    }
225
226    /// Sets the application name to the connection, queryable with the
227    /// `APP_NAME()` command.
228    ///
229    /// - Defaults to no name specified.
230    pub fn application_name(&mut self, name: impl ToString) {
231        self.application_name = Some(name.to_string());
232    }
233
234    /// Sets the TDS packet size for the connection.
235    ///
236    /// Larger packet sizes can improve bulk insert performance by reducing
237    /// the number of network round-trips. Valid values are 512 to 32767.
238    /// The server may negotiate a different size.
239    ///
240    /// - Defaults to 4096 bytes.
241    pub fn packet_size(&mut self, size: u32) {
242        self.packet_size = Some(size);
243    }
244
245    /// Gets the configured packet size, if set.
246    pub fn get_packet_size(&self) -> Option<u32> {
247        self.packet_size
248    }
249
250    /// Set the preferred encryption level.
251    ///
252    /// - With `tls` feature, defaults to `Required`.
253    /// - Without `tls` feature, defaults to `NotSupported`.
254    pub fn encryption(&mut self, encryption: EncryptionLevel) {
255        self.encryption = encryption;
256    }
257
258    /// If set, the server certificate will not be validated and it is accepted
259    /// as-is.
260    ///
261    /// On production setting, the certificate should be added to the local key
262    /// storage (or use `trust_cert_ca` instead), using this setting is potentially dangerous.
263    ///
264    /// # Panics
265    /// Will panic in case `trust_cert_ca` was called before.
266    ///
267    /// - Defaults to `default`, meaning server certificate is validated against system-truststore.
268    pub fn trust_cert(&mut self) {
269        if let TrustConfig::CaCertificateLocation(_) = &self.trust {
270            panic!("'trust_cert' and 'trust_cert_ca' are mutual exclusive! Only use one.")
271        }
272        self.trust = TrustConfig::TrustAll;
273    }
274
275    /// If set, the server certificate will be validated against the given CA certificate in
276    /// in addition to the system-truststore.
277    /// Useful when using self-signed certificates on the server without having to disable the
278    /// trust-chain.
279    ///
280    /// # Panics
281    /// Will panic in case `trust_cert` was called before.
282    ///
283    /// - Defaults to validating the server certificate is validated against system's certificate storage.
284    pub fn trust_cert_ca(&mut self, path: impl ToString) {
285        if let TrustConfig::TrustAll = &self.trust {
286            panic!("'trust_cert' and 'trust_cert_ca' are mutual exclusive! Only use one.")
287        } else {
288            self.trust = TrustConfig::CaCertificateLocation(PathBuf::from(path.to_string()))
289        }
290    }
291
292    /// Sets the hostname that the server certificate is validated against,
293    /// instead of the value given to [`host`].
294    ///
295    /// This is useful when connecting through an IP address, a tunnel, or a
296    /// load balancer whose certificate carries a different subject/SAN than the
297    /// address used to reach it (see issue #340).
298    ///
299    /// - Defaults to the value of [`host`].
300    ///
301    /// [`host`]: Config::host
302    pub fn hostname_in_certificate(&mut self, hostname: impl ToString) {
303        self.hostname_in_certificate = Some(hostname.to_string());
304    }
305
306    /// Sets the client / workstation name reported to the server in the login
307    /// record (queryable with `HOST_NAME()`).
308    ///
309    /// - Defaults to the local workstation id (the machine hostname).
310    pub fn client_name(&mut self, name: impl ToString) {
311        self.client_name = Some(name.to_string());
312    }
313
314    /// Sets the authentication method.
315    ///
316    /// - Defaults to `None`.
317    pub fn authentication(&mut self, auth: AuthMethod) {
318        self.auth = auth;
319    }
320
321    /// Sets ApplicationIntent readonly.
322    ///
323    /// - Defaults to `false`.
324    pub fn readonly(&mut self, readnoly: bool) {
325        self.readonly = readnoly;
326    }
327
328    /// Enable multi-subnet failover.
329    ///
330    /// When enabled and the server host name resolves to more than one IP
331    /// address (for example, an Always On availability group listener spread
332    /// across subnets), connections are attempted to all resolved addresses in
333    /// parallel and the first one to succeed is used. This mirrors the ADO.NET
334    /// `MultiSubnetFailover` connection-string keyword.
335    ///
336    /// - Defaults to `false`.
337    pub fn multi_subnet_failover(&mut self, multi_subnet_failover: bool) {
338        self.multi_subnet_failover = multi_subnet_failover;
339    }
340
341    /// Returns whether multi-subnet failover is enabled.
342    pub fn get_multi_subnet_failover(&self) -> bool {
343        self.multi_subnet_failover
344    }
345
346    /// Supplies a client certificate and private key used to authenticate the
347    /// client to the server during the TLS handshake (mutual TLS). This is
348    /// required for TDS 8.0 "strict" connections that use client-certificate
349    /// authentication (`ENCRYPT_CLIENT_CERT`), and may also be used with the
350    /// classic (pre-8.0) TLS handshake when the server requests a client
351    /// certificate.
352    ///
353    /// Both arguments are paths to files:
354    ///
355    /// - `cert`: the client certificate, PEM (`.pem`/`.crt`) or DER (`.der`).
356    /// - `key`: the matching private key, PEM (`.pem`/`.key`) or DER (`.der`,
357    ///   PKCS#8).
358    ///
359    /// Backend support:
360    ///
361    /// - `rustls`: PEM and DER certificate/key files.
362    /// - `native-tls`: PEM certificate + PEM PKCS#8 key only (DER files are
363    ///   rejected at connect time; use [`client_certificate_pkcs12`] for a
364    ///   bundled DER identity).
365    /// - `vendored-openssl` (opentls): does not support separate certificate/key
366    ///   files; use [`client_certificate_pkcs12`] instead.
367    ///
368    /// - Defaults to no client certificate.
369    ///
370    /// [`client_certificate_pkcs12`]: Config::client_certificate_pkcs12
371    #[cfg(any(
372        feature = "rustls",
373        feature = "native-tls",
374        feature = "vendored-openssl"
375    ))]
376    #[cfg_attr(
377        docsrs,
378        doc(cfg(any(
379            feature = "rustls",
380            feature = "native-tls",
381            feature = "vendored-openssl"
382        )))
383    )]
384    pub fn client_certificate(&mut self, cert: impl Into<PathBuf>, key: impl Into<PathBuf>) {
385        self.client_cert = Some(ClientCertificate {
386            source: ClientCertSource::CertAndKey {
387                cert: cert.into(),
388                key: key.into(),
389            },
390        });
391    }
392
393    /// Supplies a client identity from a PKCS#12 / PFX bundle (certificate,
394    /// private key and any chain, encrypted with `password`) used to
395    /// authenticate the client to the server during the TLS handshake (mutual
396    /// TLS).
397    ///
398    /// Only supported by the `native-tls` and `vendored-openssl` backends; the
399    /// `rustls` backend rejects PKCS#12 identities at connect time (supply
400    /// separate PEM/DER files via [`client_certificate`] instead).
401    ///
402    /// - Defaults to no client certificate.
403    ///
404    /// [`client_certificate`]: Config::client_certificate
405    #[cfg(any(feature = "native-tls", feature = "vendored-openssl"))]
406    #[cfg_attr(
407        docsrs,
408        doc(cfg(any(feature = "native-tls", feature = "vendored-openssl")))
409    )]
410    pub fn client_certificate_pkcs12(
411        &mut self,
412        path: impl Into<PathBuf>,
413        password: impl Into<String>,
414    ) {
415        self.client_cert = Some(ClientCertificate {
416            source: ClientCertSource::Pkcs12 {
417                path: path.into(),
418                password: zeroize::Zeroizing::new(password.into()),
419            },
420        });
421    }
422
423    #[cfg(any(
424        feature = "rustls",
425        feature = "native-tls",
426        feature = "vendored-openssl"
427    ))]
428    pub(crate) fn get_client_certificate(&self) -> Option<&ClientCertificate> {
429        self.client_cert.as_ref()
430    }
431
432    pub(crate) fn get_host(&self) -> &str {
433        self.host
434            .as_deref()
435            .filter(|v| v != &".")
436            .unwrap_or("localhost")
437    }
438
439    #[cfg(any(
440        feature = "rustls",
441        feature = "native-tls",
442        feature = "vendored-openssl"
443    ))]
444    pub(crate) fn get_hostname_in_certificate(&self) -> &str {
445        self.hostname_in_certificate
446            .as_deref()
447            .unwrap_or_else(|| self.get_host())
448    }
449
450    pub(crate) fn get_port(&self) -> u16 {
451        match (self.port, self.instance_name.as_ref()) {
452            // A user-defined port, we must use that.
453            (Some(port), _) => port,
454            // If using a named instance, we'll give the default port of SQL
455            // Browser.
456            (None, Some(_)) => 1434,
457            // Otherwise the defaulting to the default SQL Server port.
458            (None, None) => 1433,
459        }
460    }
461
462    /// Get the host address including port
463    pub fn get_addr(&self) -> String {
464        format!("{}:{}", self.get_host(), self.get_port())
465    }
466
467    /// Creates a new `Config` from an [ADO.NET connection string].
468    ///
469    /// # Supported parameters
470    ///
471    /// All parameter keys are handled case-insensitive.
472    ///
473    /// |Parameter|Allowed values|Description|
474    /// |--------|--------|--------|
475    /// |`server`|`<string>`|The name or network address of the instance of SQL Server to which to connect. The port number can be specified after the server name. The correct form of this parameter is either `tcp:host,port` or `tcp:host\\instance`|
476    /// |`IntegratedSecurity`|`true`,`false`,`yes`,`no`|Toggle between Windows/Kerberos authentication and SQL authentication.|
477    /// |`uid`,`username`,`user`,`user id`|`<string>`|The SQL Server login account.|
478    /// |`password`,`pwd`|`<string>`|The password for the SQL Server account logging on.|
479    /// |`database`|`<string>`|The name of the database.|
480    /// |`TrustServerCertificate`|`true`,`false`,`yes`,`no`|Specifies whether the driver trusts the server certificate when connecting using TLS. Cannot be used toghether with `TrustServerCertificateCA`|
481    /// |`TrustServerCertificateCA`|`<path>`|Path to a `pem`, `crt` or `der` certificate file. Cannot be used together with `TrustServerCertificate`|
482    /// |`encrypt`|`strict`,`true`,`false`,`yes`,`no`,`DANGER_PLAINTEXT`|Specifies whether the driver uses TLS to encrypt communication. `strict` (TDS 8.0) requires the `tds80` feature.|
483    /// |`Application Name`, `ApplicationName`|`<string>`|Sets the application name for the connection.|
484    /// |`HostNameInCertificate`, `HostName In Certificate`|`<string>`|The hostname the server certificate is validated against. Defaults to `server`.|
485    /// |`WorkstationID`, `Workstation ID`|`<string>`|The client / workstation name reported to the server.|
486    /// |`MultiSubnetFailover`|`true`,`false`,`yes`,`no`|When enabled, connections are attempted in parallel to all IP addresses the server resolves to, and the first to succeed is used.|
487    ///
488    /// [ADO.NET connection string]: https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/connection-strings
489    pub fn from_ado_string(s: &str) -> crate::Result<Self> {
490        let ado: AdoNetConfig = s.parse()?;
491        Self::from_config_string(ado)
492    }
493
494    /// Creates a new `Config` from a [JDBC connection string].
495    ///
496    /// See [`from_ado_string`] method for supported parameters.
497    ///
498    /// [JDBC connection string]: https://docs.microsoft.com/en-us/sql/connect/jdbc/building-the-connection-url?view=sql-server-ver15
499    /// [`from_ado_string`]: #method.from_ado_string
500    pub fn from_jdbc_string(s: &str) -> crate::Result<Self> {
501        let jdbc: JdbcConfig = s.parse()?;
502        Self::from_config_string(jdbc)
503    }
504
505    fn from_config_string(s: impl ConfigString) -> crate::Result<Self> {
506        let mut builder = Self::new();
507
508        let server = s.server()?;
509
510        if let Some(host) = server.host {
511            builder.host(host);
512        }
513
514        if let Some(port) = server.port {
515            builder.port(port);
516        }
517
518        if let Some(instance) = server.instance {
519            builder.instance_name(instance);
520        }
521
522        builder.authentication(s.authentication()?);
523
524        if let Some(database) = s.database() {
525            builder.database(database);
526        }
527
528        if let Some(name) = s.application_name() {
529            builder.application_name(name);
530        }
531
532        if s.trust_cert()? {
533            builder.trust_cert();
534        }
535
536        if let Some(ca) = s.trust_cert_ca() {
537            builder.trust_cert_ca(ca);
538        }
539
540        if let Some(hostname_in_cert) = s.hostname_in_certificate() {
541            builder.hostname_in_certificate(hostname_in_cert);
542        }
543
544        builder.encryption(s.encrypt()?);
545
546        builder.readonly(s.readonly());
547
548        if let Some(client_name) = s.client_name() {
549            builder.client_name(client_name);
550        }
551        builder.multi_subnet_failover(s.multi_subnet_failover()?);
552
553        Ok(builder)
554    }
555}
556
557/// A builder for [`Config`], providing an ergonomic, chainable way to
558/// construct a connection configuration.
559///
560/// Create a builder with [`Config::builder`], set the desired options by
561/// calling its methods (each returns the builder to allow chaining) and
562/// finalize it with [`build`].
563///
564/// # Example
565///
566/// ```
567/// # use tiberius::{Config, AuthMethod, EncryptionLevel};
568/// let config = Config::builder()
569///     .host("localhost")
570///     .port(1433)
571///     .database("master")
572///     .encryption(EncryptionLevel::NotSupported)
573///     .authentication(AuthMethod::sql_server("SA", "<password>"))
574///     .build();
575/// ```
576///
577/// [`Config`]: struct.Config.html
578/// [`Config::builder`]: struct.Config.html#method.builder
579/// [`build`]: struct.ConfigBuilder.html#method.build
580#[derive(Clone, Debug)]
581pub struct ConfigBuilder {
582    inner: Config,
583}
584
585impl ConfigBuilder {
586    /// A host or ip address to connect to.
587    ///
588    /// - Defaults to `localhost`.
589    pub fn host(mut self, host: impl ToString) -> Self {
590        self.inner.host = Some(host.to_string());
591        self
592    }
593
594    /// The server port.
595    ///
596    /// - Defaults to `1433`.
597    pub fn port(mut self, port: u16) -> Self {
598        self.inner.port = Some(port);
599        self
600    }
601
602    /// The database to connect to.
603    ///
604    /// - Defaults to `master`.
605    pub fn database(mut self, database: impl ToString) -> Self {
606        self.inner.database = Some(database.to_string());
607        self
608    }
609
610    /// The instance name as defined in the SQL Browser. Only available on
611    /// Windows platforms.
612    ///
613    /// If specified, the port is replaced with the value returned from the
614    /// browser.
615    ///
616    /// - Defaults to no name specified.
617    pub fn instance_name(mut self, name: impl ToString) -> Self {
618        self.inner.instance_name = Some(name.to_string());
619        self
620    }
621
622    /// Sets the application name to the connection, queryable with the
623    /// `APP_NAME()` command.
624    ///
625    /// - Defaults to no name specified.
626    pub fn application_name(mut self, name: impl ToString) -> Self {
627        self.inner.application_name = Some(name.to_string());
628        self
629    }
630
631    /// Set the preferred encryption level.
632    ///
633    /// - With `tls` feature, defaults to `Required`.
634    /// - Without `tls` feature, defaults to `NotSupported`.
635    pub fn encryption(mut self, encryption: EncryptionLevel) -> Self {
636        self.inner.encryption = encryption;
637        self
638    }
639
640    /// If set, the server certificate will not be validated and it is accepted
641    /// as-is.
642    ///
643    /// On production setting, the certificate should be added to the local key
644    /// storage (or use `trust_cert_ca` instead), using this setting is potentially dangerous.
645    ///
646    /// # Panics
647    /// Will panic in case `trust_cert_ca` was called before.
648    ///
649    /// - Defaults to `default`, meaning server certificate is validated against system-truststore.
650    pub fn trust_cert(mut self) -> Self {
651        if let TrustConfig::CaCertificateLocation(_) = &self.inner.trust {
652            panic!("'trust_cert' and 'trust_cert_ca' are mutual exclusive! Only use one.")
653        }
654        self.inner.trust = TrustConfig::TrustAll;
655        self
656    }
657
658    /// If set, the server certificate will be validated against the given CA certificate in
659    /// in addition to the system-truststore.
660    /// Useful when using self-signed certificates on the server without having to disable the
661    /// trust-chain.
662    ///
663    /// # Panics
664    /// Will panic in case `trust_cert` was called before.
665    ///
666    /// - Defaults to validating the server certificate is validated against system's certificate storage.
667    pub fn trust_cert_ca(mut self, path: impl ToString) -> Self {
668        if let TrustConfig::TrustAll = &self.inner.trust {
669            panic!("'trust_cert' and 'trust_cert_ca' are mutual exclusive! Only use one.")
670        } else {
671            self.inner.trust = TrustConfig::CaCertificateLocation(PathBuf::from(path.to_string()))
672        }
673        self
674    }
675
676    /// Sets the authentication method.
677    ///
678    /// - Defaults to `None`.
679    pub fn authentication(mut self, auth: AuthMethod) -> Self {
680        self.inner.auth = auth;
681        self
682    }
683
684    /// Sets ApplicationIntent readonly.
685    ///
686    /// - Defaults to `false`.
687    pub fn readonly(mut self, readonly: bool) -> Self {
688        self.inner.readonly = readonly;
689        self
690    }
691
692    /// Supplies a client certificate and private key for mutual TLS.
693    ///
694    /// See [`Config::client_certificate`] for details and backend support.
695    #[cfg(any(
696        feature = "rustls",
697        feature = "native-tls",
698        feature = "vendored-openssl"
699    ))]
700    #[cfg_attr(
701        docsrs,
702        doc(cfg(any(
703            feature = "rustls",
704            feature = "native-tls",
705            feature = "vendored-openssl"
706        )))
707    )]
708    pub fn client_certificate(mut self, cert: impl Into<PathBuf>, key: impl Into<PathBuf>) -> Self {
709        self.inner.client_certificate(cert, key);
710        self
711    }
712
713    /// Supplies a client identity from a PKCS#12 / PFX bundle for mutual TLS.
714    ///
715    /// See [`Config::client_certificate_pkcs12`] for details and backend
716    /// support.
717    #[cfg(any(feature = "native-tls", feature = "vendored-openssl"))]
718    #[cfg_attr(
719        docsrs,
720        doc(cfg(any(feature = "native-tls", feature = "vendored-openssl")))
721    )]
722    pub fn client_certificate_pkcs12(
723        mut self,
724        path: impl Into<PathBuf>,
725        password: impl Into<String>,
726    ) -> Self {
727        self.inner.client_certificate_pkcs12(path, password);
728        self
729    }
730
731    /// Produces the finalized [`Config`] from this builder.
732    ///
733    /// [`Config`]: struct.Config.html
734    pub fn build(self) -> Config {
735        self.inner
736    }
737}
738
739impl From<Config> for ConfigBuilder {
740    fn from(config: Config) -> Self {
741        ConfigBuilder { inner: config }
742    }
743}
744
745impl From<ConfigBuilder> for Config {
746    fn from(builder: ConfigBuilder) -> Self {
747        builder.inner
748    }
749}
750
751pub(crate) struct ServerDefinition {
752    host: Option<String>,
753    port: Option<u16>,
754    instance: Option<String>,
755}
756
757pub(crate) trait ConfigString {
758    fn dict(&self) -> &HashMap<String, String>;
759
760    fn server(&self) -> crate::Result<ServerDefinition>;
761
762    fn authentication(&self) -> crate::Result<AuthMethod> {
763        let user = self
764            .dict()
765            .get("uid")
766            .or_else(|| self.dict().get("username"))
767            .or_else(|| self.dict().get("user"))
768            .or_else(|| self.dict().get("user id"))
769            .map(|s| s.as_str());
770
771        let pw = self
772            .dict()
773            .get("password")
774            .or_else(|| self.dict().get("pwd"))
775            .map(|s| s.as_str());
776
777        match self
778            .dict()
779            .get("integratedsecurity")
780            .or_else(|| self.dict().get("integrated security"))
781        {
782            #[cfg(all(windows, feature = "winauth"))]
783            Some(val) if val.to_lowercase() == "sspi" || Self::parse_bool(val)? => match (user, pw)
784            {
785                (None, None) => Ok(AuthMethod::Integrated),
786                _ => Ok(AuthMethod::windows(user.unwrap_or(""), pw.unwrap_or(""))),
787            },
788            // On Unix with `sspi-rs`, `IntegratedSecurity=SSPI` (or a truthy
789            // value) uses NTLM when a username/password is supplied, and falls
790            // back to Kerberos (`Integrated`) only if `integrated-auth-gssapi`
791            // is also enabled and no credentials are given.
792            #[cfg(all(unix, feature = "sspi-rs"))]
793            Some(val) if val.to_lowercase() == "sspi" || Self::parse_bool(val)? => {
794                match (user, pw) {
795                    (Some(user), Some(pw)) => Ok(AuthMethod::windows(user, pw)),
796                    #[cfg(feature = "integrated-auth-gssapi")]
797                    (None, None) => Ok(AuthMethod::Integrated),
798                    _ => Ok(AuthMethod::windows(user.unwrap_or(""), pw.unwrap_or(""))),
799                }
800            }
801            #[cfg(all(
802                feature = "integrated-auth-gssapi",
803                not(all(unix, feature = "sspi-rs"))
804            ))]
805            Some(val) if val.to_lowercase() == "sspi" || Self::parse_bool(val)? => {
806                Ok(AuthMethod::Integrated)
807            }
808            _ => Ok(AuthMethod::sql_server(user.unwrap_or(""), pw.unwrap_or(""))),
809        }
810    }
811
812    fn database(&self) -> Option<String> {
813        self.dict()
814            .get("database")
815            .or_else(|| self.dict().get("initial catalog"))
816            .or_else(|| self.dict().get("databasename"))
817            .map(|db| db.to_string())
818    }
819
820    fn application_name(&self) -> Option<String> {
821        self.dict()
822            .get("application name")
823            .or_else(|| self.dict().get("applicationname"))
824            .map(|name| name.to_string())
825    }
826
827    fn trust_cert(&self) -> crate::Result<bool> {
828        self.dict()
829            .get("trustservercertificate")
830            .map(Self::parse_bool)
831            .unwrap_or(Ok(false))
832    }
833
834    fn trust_cert_ca(&self) -> Option<String> {
835        self.dict()
836            .get("trustservercertificateca")
837            .map(|ca| ca.to_string())
838    }
839
840    fn hostname_in_certificate(&self) -> Option<String> {
841        self.dict()
842            .get("hostnameincertificate")
843            .or_else(|| self.dict().get("hostname in certificate"))
844            .map(|host| host.to_string())
845    }
846
847    fn client_name(&self) -> Option<String> {
848        self.dict()
849            .get("workstationid")
850            .or_else(|| self.dict().get("workstation id"))
851            .map(|name| name.to_string())
852    }
853
854    #[cfg(any(
855        feature = "rustls",
856        feature = "native-tls",
857        feature = "vendored-openssl"
858    ))]
859    fn encrypt(&self) -> crate::Result<EncryptionLevel> {
860        self.dict()
861            .get("encrypt")
862            .map(|val| match Self::parse_bool(val) {
863                Ok(true) => Ok(EncryptionLevel::Required),
864                Ok(false) => Ok(EncryptionLevel::Off),
865                Err(_) if val == "DANGER_PLAINTEXT" => Ok(EncryptionLevel::NotSupported),
866                Err(_) if val.eq_ignore_ascii_case("strict") && cfg!(feature = "tds80") => {
867                    Ok(EncryptionLevel::Strict)
868                }
869                Err(_) if val.eq_ignore_ascii_case("strict") => Err(crate::Error::Conversion(
870                    "encrypt=strict requires the crate's `tds80` feature to be enabled".into(),
871                )),
872                Err(e) => Err(e),
873            })
874            // When the `encrypt` keyword is omitted, default to requiring
875            // encryption — matching `Config::default()` and modern ADO.NET
876            // (`Encrypt=Mandatory`). Callers who want an unencrypted connection
877            // must opt out explicitly with `encrypt=false` (or
878            // `encrypt=DANGER_PLAINTEXT`).
879            .unwrap_or(Ok(EncryptionLevel::Required))
880    }
881
882    #[cfg(not(any(
883        feature = "rustls",
884        feature = "native-tls",
885        feature = "vendored-openssl"
886    )))]
887    fn encrypt(&self) -> crate::Result<EncryptionLevel> {
888        Ok(EncryptionLevel::NotSupported)
889    }
890
891    fn parse_bool<T: AsRef<str>>(v: T) -> crate::Result<bool> {
892        match v.as_ref().trim().to_lowercase().as_str() {
893            "true" | "yes" => Ok(true),
894            "false" | "no" => Ok(false),
895            _ => Err(crate::Error::Conversion(
896                "Connection string: Not a valid boolean".into(),
897            )),
898        }
899    }
900
901    fn readonly(&self) -> bool {
902        self.dict()
903            .get("applicationintent")
904            .filter(|val| val.trim().eq_ignore_ascii_case("ReadOnly"))
905            .is_some()
906    }
907
908    fn multi_subnet_failover(&self) -> crate::Result<bool> {
909        self.dict()
910            .get("multisubnetfailover")
911            .map(Self::parse_bool)
912            .unwrap_or(Ok(false))
913    }
914}
915
916#[cfg(test)]
917mod tests {
918    use super::*;
919
920    #[test]
921    fn config_builder_constructs_config() {
922        let config = Config::builder()
923            .host("db.example.com")
924            .port(4433)
925            .database("northwind")
926            .application_name("my-app")
927            .authentication(AuthMethod::sql_server("SA", "secret"))
928            .readonly(true)
929            .build();
930
931        assert_eq!("db.example.com", config.get_host());
932        assert_eq!(4433, config.get_port());
933        assert_eq!("db.example.com:4433", config.get_addr());
934        assert_eq!(Some("northwind"), config.database.as_deref());
935        assert_eq!(Some("my-app"), config.application_name.as_deref());
936        assert!(config.readonly);
937        assert!(matches!(config.auth, AuthMethod::SqlServer(_)));
938        assert!(matches!(config.trust, TrustConfig::Default));
939    }
940
941    #[test]
942    fn config_builder_roundtrips_via_from() {
943        let config = Config::builder().host("localhost").port(1433).build();
944        let builder: ConfigBuilder = config.into();
945        let config = builder.database("master").build();
946
947        assert_eq!("localhost:1433", config.get_addr());
948        assert_eq!(Some("master"), config.database.as_deref());
949    }
950
951    #[test]
952    fn config_from_builder_carries_builder_settings() {
953        // `From<ConfigBuilder>` must return the built inner config, not a default.
954        let config: Config = Config::builder().host("db.internal").port(2020).into();
955        assert_eq!("db.internal", config.get_host());
956        assert_eq!(2020, config.get_port());
957    }
958
959    #[test]
960    fn get_packet_size_reflects_the_set_value() {
961        let mut config = Config::new();
962        assert_eq!(config.get_packet_size(), None);
963        config.packet_size(8192);
964        assert_eq!(config.get_packet_size(), Some(8192));
965    }
966
967    #[test]
968    fn from_jdbc_string_parses_host_and_port() {
969        let config =
970            Config::from_jdbc_string("jdbc:sqlserver://db.example.com:2345").expect("valid jdbc");
971        assert_eq!("db.example.com", config.get_host());
972        assert_eq!(2345, config.get_port());
973    }
974
975    #[cfg(any(
976        feature = "rustls",
977        feature = "native-tls",
978        feature = "vendored-openssl"
979    ))]
980    #[test]
981    fn get_hostname_in_certificate_falls_back_to_host() {
982        let mut config = Config::new();
983        config.host("real.host");
984        // Unset: falls back to the connection host.
985        assert_eq!(config.get_hostname_in_certificate(), "real.host");
986        // Set: returns the explicit certificate hostname.
987        config.hostname_in_certificate("cert.host");
988        assert_eq!(config.get_hostname_in_certificate(), "cert.host");
989    }
990
991    #[cfg(any(
992        feature = "rustls",
993        feature = "native-tls",
994        feature = "vendored-openssl"
995    ))]
996    #[test]
997    fn client_certificate_sets_cert_and_key_source() {
998        let mut config = Config::new();
999        assert!(config.get_client_certificate().is_none());
1000
1001        config.client_certificate("/tmp/client.pem", "/tmp/client.key");
1002
1003        let cert = config
1004            .get_client_certificate()
1005            .expect("client certificate should be set");
1006        match &cert.source {
1007            ClientCertSource::CertAndKey { cert, key } => {
1008                assert_eq!(cert, &PathBuf::from("/tmp/client.pem"));
1009                assert_eq!(key, &PathBuf::from("/tmp/client.key"));
1010            }
1011            #[allow(unreachable_patterns)]
1012            other => panic!("expected CertAndKey source, got {other:?}"),
1013        }
1014    }
1015
1016    #[cfg(any(
1017        feature = "rustls",
1018        feature = "native-tls",
1019        feature = "vendored-openssl"
1020    ))]
1021    #[test]
1022    fn config_builder_sets_client_certificate() {
1023        let config = Config::builder()
1024            .host("localhost")
1025            .client_certificate("cert.der", "key.der")
1026            .build();
1027
1028        match &config
1029            .get_client_certificate()
1030            .expect("client certificate should be set")
1031            .source
1032        {
1033            ClientCertSource::CertAndKey { cert, key } => {
1034                assert_eq!(cert, &PathBuf::from("cert.der"));
1035                assert_eq!(key, &PathBuf::from("key.der"));
1036            }
1037            #[allow(unreachable_patterns)]
1038            other => panic!("expected CertAndKey source, got {other:?}"),
1039        }
1040    }
1041
1042    #[cfg(any(feature = "native-tls", feature = "vendored-openssl"))]
1043    #[test]
1044    fn client_certificate_pkcs12_sets_bundle_source() {
1045        let mut config = Config::new();
1046        config.client_certificate_pkcs12("/tmp/identity.pfx", "s3cr3t");
1047
1048        match &config
1049            .get_client_certificate()
1050            .expect("client certificate should be set")
1051            .source
1052        {
1053            ClientCertSource::Pkcs12 { path, password } => {
1054                assert_eq!(path, &PathBuf::from("/tmp/identity.pfx"));
1055                assert_eq!(password.as_str(), "s3cr3t");
1056            }
1057            other => panic!("expected Pkcs12 source, got {other:?}"),
1058        }
1059    }
1060
1061    #[cfg(any(feature = "native-tls", feature = "vendored-openssl"))]
1062    #[test]
1063    fn client_certificate_debug_redacts_pkcs12_password() {
1064        let mut config = Config::new();
1065        config.client_certificate_pkcs12("/tmp/identity.pfx", "topsecret");
1066
1067        let dbg = format!("{:?}", config.get_client_certificate().unwrap());
1068        assert!(dbg.contains("<redacted>"));
1069        assert!(!dbg.contains("topsecret"));
1070    }
1071
1072    #[cfg(all(unix, feature = "sspi-rs"))]
1073    #[test]
1074    fn ado_integrated_security_sspi_with_credentials_uses_windows_ntlm() {
1075        let config = Config::from_ado_string(
1076            "server=tcp:localhost,1433;IntegratedSecurity=SSPI;uid=DOMAIN\\user;pwd=secret",
1077        )
1078        .unwrap();
1079
1080        match config.auth {
1081            AuthMethod::Windows(auth) => {
1082                assert_eq!("user", auth.user);
1083                assert_eq!(Some("DOMAIN"), auth.domain.as_deref());
1084            }
1085            other => panic!("expected Windows NTLM auth, got {other:?}"),
1086        }
1087    }
1088
1089    #[test]
1090    fn config_direct_setters_populate_fields() {
1091        let mut config = Config::new();
1092        config.database("northwind");
1093        config.instance_name("SQLEXPRESS");
1094        config.client_name("workstation-7");
1095
1096        assert_eq!(Some("northwind"), config.database.as_deref());
1097        assert_eq!(Some("SQLEXPRESS"), config.instance_name.as_deref());
1098        assert_eq!(Some("workstation-7"), config.client_name.as_deref());
1099    }
1100
1101    #[test]
1102    fn get_port_defaults_without_port_or_instance() {
1103        // No explicit port and no instance -> default SQL Server port.
1104        let config = Config::new();
1105        assert_eq!(1433, config.get_port());
1106    }
1107
1108    #[test]
1109    fn get_port_uses_sql_browser_port_for_named_instance() {
1110        // A named instance without an explicit port -> SQL Browser port.
1111        let mut config = Config::new();
1112        config.instance_name("SQLEXPRESS");
1113        assert_eq!(1434, config.get_port());
1114    }
1115
1116    #[test]
1117    #[should_panic(expected = "mutual exclusive")]
1118    fn trust_cert_after_trust_cert_ca_panics() {
1119        let mut config = Config::new();
1120        config.trust_cert_ca("/tmp/ca.crt");
1121        config.trust_cert();
1122    }
1123
1124    #[test]
1125    #[should_panic(expected = "mutual exclusive")]
1126    fn trust_cert_ca_after_trust_cert_panics() {
1127        let mut config = Config::new();
1128        config.trust_cert();
1129        config.trust_cert_ca("/tmp/ca.crt");
1130    }
1131
1132    #[test]
1133    fn trust_cert_ca_sets_ca_location() {
1134        let mut config = Config::new();
1135        config.trust_cert_ca("/tmp/ca.crt");
1136        assert!(matches!(
1137            config.trust,
1138            TrustConfig::CaCertificateLocation(_)
1139        ));
1140    }
1141
1142    #[test]
1143    fn config_builder_covers_all_setters() {
1144        let config = Config::builder()
1145            .host("localhost")
1146            .instance_name("SQLEXPRESS")
1147            .encryption(EncryptionLevel::Off)
1148            .trust_cert_ca("/tmp/ca.crt")
1149            .build();
1150
1151        assert_eq!(Some("SQLEXPRESS"), config.instance_name.as_deref());
1152        assert!(matches!(config.encryption, EncryptionLevel::Off));
1153        assert!(matches!(
1154            config.trust,
1155            TrustConfig::CaCertificateLocation(_)
1156        ));
1157    }
1158
1159    #[test]
1160    fn config_builder_trust_cert_sets_trust_all() {
1161        let config = Config::builder().trust_cert().build();
1162        assert!(matches!(config.trust, TrustConfig::TrustAll));
1163    }
1164
1165    #[test]
1166    #[should_panic(expected = "mutual exclusive")]
1167    fn config_builder_trust_cert_after_ca_panics() {
1168        Config::builder().trust_cert_ca("/tmp/ca.crt").trust_cert();
1169    }
1170
1171    #[test]
1172    #[should_panic(expected = "mutual exclusive")]
1173    fn config_builder_trust_cert_ca_after_trust_cert_panics() {
1174        Config::builder().trust_cert().trust_cert_ca("/tmp/ca.crt");
1175    }
1176
1177    #[test]
1178    fn from_ado_string_populates_optional_fields() {
1179        let config = Config::from_ado_string(
1180            "server=tcp:my-server.com\\SQLEXPRESS;database=northwind;\
1181             HostNameInCertificate=cert.host;WorkstationID=ws-1",
1182        )
1183        .expect("valid ado string");
1184
1185        assert_eq!("my-server.com", config.get_host());
1186        assert_eq!(Some("SQLEXPRESS"), config.instance_name.as_deref());
1187        assert_eq!(Some("northwind"), config.database.as_deref());
1188        assert_eq!(Some("cert.host"), config.hostname_in_certificate.as_deref());
1189        assert_eq!(Some("ws-1"), config.client_name.as_deref());
1190    }
1191
1192    #[cfg(any(
1193        feature = "rustls",
1194        feature = "native-tls",
1195        feature = "vendored-openssl"
1196    ))]
1197    #[test]
1198    fn client_cert_source_debug_formats_cert_and_key() {
1199        let mut config = Config::new();
1200        config.client_certificate("/tmp/client.pem", "/tmp/client.key");
1201
1202        let dbg = format!("{:?}", config.get_client_certificate().unwrap().source);
1203        assert!(dbg.contains("CertAndKey"));
1204        assert!(dbg.contains("client.pem"));
1205        assert!(dbg.contains("client.key"));
1206    }
1207
1208    #[cfg(any(feature = "native-tls", feature = "vendored-openssl"))]
1209    #[test]
1210    fn config_builder_sets_pkcs12_client_certificate() {
1211        let config = Config::builder()
1212            .client_certificate_pkcs12("/tmp/identity.pfx", "s3cr3t")
1213            .build();
1214
1215        match &config
1216            .get_client_certificate()
1217            .expect("client certificate should be set")
1218            .source
1219        {
1220            ClientCertSource::Pkcs12 { path, password } => {
1221                assert_eq!(path, &PathBuf::from("/tmp/identity.pfx"));
1222                assert_eq!(password.as_str(), "s3cr3t");
1223            }
1224            other => panic!("expected Pkcs12 source, got {other:?}"),
1225        }
1226    }
1227
1228    #[cfg(all(unix, feature = "sspi-rs"))]
1229    #[test]
1230    fn ado_integrated_security_sspi_with_partial_credentials_uses_windows() {
1231        // Only a username (no password) -> falls into the catch-all NTLM arm.
1232        let config = Config::from_ado_string(
1233            "server=tcp:localhost,1433;IntegratedSecurity=SSPI;uid=onlyuser",
1234        )
1235        .unwrap();
1236
1237        match config.auth {
1238            AuthMethod::Windows(auth) => {
1239                assert_eq!("onlyuser", auth.user);
1240            }
1241            other => panic!("expected Windows auth, got {other:?}"),
1242        }
1243    }
1244}