1use std::marker::PhantomData;
7use std::net::SocketAddr;
8
9use bytes::BytesMut;
10use mssql_codec::connection::Connection;
11#[cfg(feature = "tls")]
12use mssql_tls::{TlsConfig, TlsConnector, TlsNegotiationMode};
13use tds_protocol::login7::Login7;
14use tds_protocol::packet::MAX_PACKET_SIZE;
15use tds_protocol::packet::PacketType;
16use tds_protocol::prelogin::{EncryptionLevel, PreLogin};
17use tds_protocol::token::{EnvChange, EnvChangeType, Token, TokenParser};
18use tokio::net::TcpStream;
19use tokio::time::timeout;
20
21use crate::config::Config;
22use crate::error::{Error, Result};
23#[cfg(feature = "otel")]
24use crate::instrumentation::InstrumentationContext;
25use crate::state::{Disconnected, Ready};
26use crate::statement_cache::StatementCache;
27
28use super::{Client, ConnectionHandle};
29
30impl Client<Disconnected> {
31 pub async fn connect(config: Config) -> Result<Client<Ready>> {
47 match &config.credentials {
54 mssql_auth::Credentials::SqlServer { .. } => {}
55 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
56 mssql_auth::Credentials::Integrated => {}
57 _ => {
61 return Err(Error::Config(
62 "Azure AD / Entra and client certificate (FEDAUTH) authentication \
63 are not yet supported: the LOGIN7 FEDAUTH feature extension is not \
64 implemented (tracked in \
65 https://github.com/praxiomlabs/rust-mssql-driver/issues/155). \
66 Use SQL Server or integrated authentication."
67 .into(),
68 ));
69 }
70 }
71
72 let retry = config.retry.clone();
73 let max_redirects = config.redirect.max_redirects;
74 let follow_redirects = config.redirect.follow_redirects;
75 let per_attempt = config.timeouts.connect_timeout
77 + config.timeouts.tls_timeout
78 + config.timeouts.login_timeout;
79 let total_attempts = (retry.max_retries + 1) * (max_redirects as u32 + 1);
80 let overall = (per_attempt * total_attempts).min(std::time::Duration::from_secs(300));
81 let initial_host = config.host.clone();
82 let initial_port = config.port;
83
84 let result = timeout(overall, async {
85 let mut last_error: Option<Error> = None;
86
87 for retry_attempt in 0..=retry.max_retries {
88 if retry_attempt > 0 {
89 let backoff = retry.backoff_for_attempt(retry_attempt);
90 tracing::info!(
91 retry_attempt,
92 backoff_ms = backoff.as_millis() as u64,
93 "retrying connection after transient error"
94 );
95 tokio::time::sleep(backoff).await;
96 }
97
98 let mut current_config = config.clone();
100 let mut redirect_count: u8 = 0;
101
102 let attempt_result = loop {
103 redirect_count += 1;
104 if redirect_count > max_redirects + 1 {
105 break Err(Error::TooManyRedirects { max: max_redirects });
106 }
107
108 match Self::try_connect(¤t_config).await {
109 Ok(client) => break Ok(client),
110 Err(Error::Routing { host, port }) => {
111 if !follow_redirects {
112 break Err(Error::Routing { host, port });
113 }
114 tracing::info!(
115 host = %host,
116 port = port,
117 redirect = redirect_count,
118 max_redirects = max_redirects,
119 "following Azure SQL routing redirect"
120 );
121 current_config = current_config.with_host(&host).with_port(port);
122 continue;
123 }
124 Err(e) => break Err(e),
125 }
126 };
127
128 match attempt_result {
129 Ok(client) => return Ok(client),
130 Err(ref e) if e.is_transient() && retry.should_retry(retry_attempt) => {
131 tracing::warn!(
132 retry_attempt,
133 max_retries = retry.max_retries,
134 error = %e,
135 "transient connection error, will retry"
136 );
137 last_error = Some(attempt_result.unwrap_err());
138 }
139 Err(e) => return Err(e),
140 }
141 }
142
143 Err(last_error.expect("at least one attempt was made"))
145 })
146 .await;
147
148 match result {
149 Ok(inner) => inner,
150 Err(_elapsed) => Err(Error::ConnectTimeout {
151 host: initial_host,
152 port: initial_port,
153 }),
154 }
155 }
156
157 async fn try_connect(config: &Config) -> Result<Client<Ready>> {
158 let port = if let Some(ref instance) = config.instance {
160 let resolved = crate::browser::resolve_instance(
161 &config.host,
162 instance,
163 Some(config.timeouts.connect_timeout),
164 )
165 .await?;
166 tracing::info!(
167 host = %config.host,
168 instance = %instance,
169 resolved_port = resolved,
170 database = ?config.database,
171 "connecting to named SQL Server instance"
172 );
173 resolved
174 } else {
175 tracing::info!(
176 host = %config.host,
177 port = config.port,
178 database = ?config.database,
179 "connecting to SQL Server"
180 );
181 config.port
182 };
183
184 let host = if config.host == "." || config.host.eq_ignore_ascii_case("(local)") {
187 "127.0.0.1"
188 } else {
189 &config.host
190 };
191
192 let tcp_stream = if config.multi_subnet_failover {
194 Self::connect_parallel(host, port, config.timeouts.connect_timeout).await?
195 } else {
196 let addr = format!("{host}:{port}");
197 tracing::debug!("establishing TCP connection to {}", addr);
198 let stream = timeout(config.timeouts.connect_timeout, TcpStream::connect(&addr))
199 .await
200 .map_err(|_| Error::ConnectTimeout {
201 host: config.host.clone(),
202 port: config.port,
203 })?
204 .map_err(Error::from)?;
205 stream.set_nodelay(true).map_err(Error::from)?;
206 stream
207 };
208
209 #[cfg(feature = "tls")]
210 {
211 let tls_mode = TlsNegotiationMode::from_encrypt_mode(config.strict_mode);
213
214 if tls_mode.is_tls_first() {
216 return Self::connect_tds_8(config, tcp_stream).await;
217 }
218
219 Self::connect_tds_7x(config, tcp_stream).await
221 }
222
223 #[cfg(not(feature = "tls"))]
224 {
225 if config.strict_mode {
227 return Err(Error::Config(
228 "TDS 8.0 strict mode requires TLS. Enable the 'tls' feature or use Encrypt=no_tls".into()
229 ));
230 }
231
232 if !config.no_tls {
233 return Err(Error::Config(
234 "TLS encryption requires the 'tls' feature. Either enable the 'tls' feature \
235 or use Encrypt=no_tls in your connection string for unencrypted connections."
236 .into(),
237 ));
238 }
239
240 Self::connect_no_tls(config, tcp_stream).await
242 }
243 }
244
245 async fn connect_parallel(
250 host: &str,
251 port: u16,
252 connect_timeout: std::time::Duration,
253 ) -> Result<TcpStream> {
254 let addr_str = format!("{host}:{port}");
255 let addrs: Vec<SocketAddr> = tokio::net::lookup_host(&addr_str)
256 .await
257 .map_err(Error::from)?
258 .collect();
259
260 if addrs.is_empty() {
261 return Err(Error::from(std::io::Error::new(
262 std::io::ErrorKind::AddrNotAvailable,
263 format!("no addresses resolved for {host}:{port}"),
264 )));
265 }
266
267 if addrs.len() == 1 {
269 tracing::debug!(addr = %addrs[0], "MultiSubnetFailover: single address resolved");
270 let stream = timeout(connect_timeout, TcpStream::connect(addrs[0]))
271 .await
272 .map_err(|_| Error::ConnectTimeout {
273 host: host.to_string(),
274 port,
275 })?
276 .map_err(Error::from)?;
277 stream.set_nodelay(true).map_err(Error::from)?;
278 return Ok(stream);
279 }
280
281 let addr_count = addrs.len();
282 tracing::debug!(
283 host = host,
284 port = port,
285 resolved_count = addr_count,
286 "MultiSubnetFailover: racing parallel connections",
287 );
288
289 let mut join_set = tokio::task::JoinSet::new();
290
291 for addr in addrs {
292 let dur = connect_timeout;
293 join_set.spawn(async move {
294 let tcp = timeout(dur, TcpStream::connect(addr)).await.map_err(|_| {
295 std::io::Error::new(
296 std::io::ErrorKind::TimedOut,
297 format!("connection to {addr} timed out"),
298 )
299 })??;
300 tcp.set_nodelay(true)?;
301 Ok::<(TcpStream, SocketAddr), std::io::Error>((tcp, addr))
302 });
303 }
304
305 let mut last_error: Option<std::io::Error> = None;
306
307 while let Some(result) = join_set.join_next().await {
308 match result {
309 Ok(Ok((stream, addr))) => {
310 tracing::debug!(addr = %addr, "MultiSubnetFailover: connected");
311 join_set.abort_all();
312 return Ok(stream);
313 }
314 Ok(Err(e)) => {
315 tracing::debug!(error = %e, "MultiSubnetFailover: attempt failed");
316 last_error = Some(e);
317 }
318 Err(join_err) => {
319 tracing::debug!(error = %join_err, "MultiSubnetFailover: task failed");
320 last_error = Some(std::io::Error::other(join_err.to_string()));
321 }
322 }
323 }
324
325 Err(Error::from(last_error.unwrap_or_else(|| {
327 std::io::Error::new(
328 std::io::ErrorKind::ConnectionRefused,
329 format!("all {addr_count} parallel connection attempts failed for {host}:{port}"),
330 )
331 })))
332 }
333
334 #[cfg(feature = "tls")]
338 async fn connect_tds_8(config: &Config, tcp_stream: TcpStream) -> Result<Client<Ready>> {
339 tracing::debug!("using TDS 8.0 strict mode (TLS first)");
340
341 let tls_config = connection_tls_config(config, true);
344
345 let tls_connector = TlsConnector::new(tls_config)?;
346
347 let tls_stream = timeout(
349 config.timeouts.tls_timeout,
350 tls_connector.connect(tcp_stream, &config.host),
351 )
352 .await
353 .map_err(|_| Error::TlsTimeout {
354 host: config.host.clone(),
355 port: config.port,
356 })??;
357
358 tracing::debug!("TLS handshake completed (strict mode)");
359
360 let mut connection = Connection::new(tls_stream);
362 connection.set_max_message_size(config.max_response_size);
363
364 let prelogin = Self::build_prelogin(config, EncryptionLevel::Required);
366 Self::send_prelogin(&mut connection, &prelogin).await?;
367 let _prelogin_response = Self::receive_prelogin(&mut connection).await?;
368
369 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
371 let negotiator = Self::create_negotiator(config)?;
372 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
373 let sspi_token = match negotiator {
374 Some(ref neg) => Some(neg.initialize()?),
375 None => None,
376 };
377 #[cfg(not(any(feature = "integrated-auth", feature = "sspi-auth")))]
378 let sspi_token: Option<Vec<u8>> = None;
379
380 let login = Self::build_login7(config, sspi_token);
382 Self::send_login7(&mut connection, &login).await?;
383
384 let (server_version, current_database, routing, server_collation) = timeout(
386 config.timeouts.login_timeout,
387 Self::process_login_response(
388 &mut connection,
389 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
390 negotiator.as_deref(),
391 ),
392 )
393 .await
394 .map_err(|_| Error::LoginTimeout {
395 host: config.host.clone(),
396 port: config.port,
397 })??;
398
399 if let Some((host, port)) = routing {
401 return Err(Error::Routing { host, port });
402 }
403
404 Ok(Client {
405 config: config.clone(),
406 _state: PhantomData,
407 connection: Some(ConnectionHandle::Tls(connection)),
408 server_version,
409 current_database: current_database.clone(),
410 server_collation,
411 statement_cache: StatementCache::with_default_size(),
412 transaction_descriptor: 0, needs_reset: false, in_flight: false, #[cfg(feature = "otel")]
416 instrumentation: InstrumentationContext::new(config.host.clone(), config.port)
417 .with_database(current_database.clone().unwrap_or_default()),
418 #[cfg(feature = "always-encrypted")]
419 encryption_context: config.column_encryption.clone().map(|cfg| {
420 std::sync::Arc::new(crate::encryption::EncryptionContext::from_arc(cfg))
421 }),
422 })
423 }
424
425 #[cfg(feature = "tls")]
433 async fn connect_tds_7x(config: &Config, mut tcp_stream: TcpStream) -> Result<Client<Ready>> {
434 use bytes::BufMut;
435 use tds_protocol::packet::{PACKET_HEADER_SIZE, PacketHeader, PacketStatus};
436 use tokio::io::{AsyncReadExt, AsyncWriteExt};
437
438 tracing::debug!("using TDS 7.x flow (PreLogin first)");
439
440 let client_encryption = if config.no_tls {
443 tracing::warn!(
445 "⚠️ no_tls mode enabled. Connection will be UNENCRYPTED. \
446 Credentials and data will be transmitted in plaintext. \
447 This should only be used for development/testing with legacy SQL Server."
448 );
449 EncryptionLevel::NotSupported
450 } else if config.encrypt {
451 EncryptionLevel::On
452 } else {
453 EncryptionLevel::Off
454 };
455 let prelogin = Self::build_prelogin(config, client_encryption);
456 tracing::debug!(encryption = ?client_encryption, "sending PreLogin");
457 let prelogin_bytes = prelogin.encode();
458
459 let header = PacketHeader::new(
461 PacketType::PreLogin,
462 PacketStatus::END_OF_MESSAGE,
463 (PACKET_HEADER_SIZE + prelogin_bytes.len()) as u16,
464 );
465
466 let mut packet_buf = BytesMut::with_capacity(PACKET_HEADER_SIZE + prelogin_bytes.len());
467 header.encode(&mut packet_buf);
468 packet_buf.put_slice(&prelogin_bytes);
469
470 tcp_stream
471 .write_all(&packet_buf)
472 .await
473 .map_err(Error::from)?;
474
475 let mut header_buf = [0u8; PACKET_HEADER_SIZE];
477 tcp_stream
478 .read_exact(&mut header_buf)
479 .await
480 .map_err(Error::from)?;
481
482 let response_length = u16::from_be_bytes([header_buf[2], header_buf[3]]) as usize;
483 let payload_length = response_length.saturating_sub(PACKET_HEADER_SIZE);
484
485 let mut response_buf = vec![0u8; payload_length];
486 tcp_stream
487 .read_exact(&mut response_buf)
488 .await
489 .map_err(Error::from)?;
490
491 let prelogin_response = PreLogin::decode(&response_buf[..])?;
492
493 let client_tds_version = config.tds_version;
498 if let Some(ref server_version) = prelogin_response.server_version {
499 tracing::debug!(
500 requested_tds_version = %client_tds_version,
501 server_product_version = %server_version,
502 server_product = server_version.product_name(),
503 max_tds_version = %server_version.max_tds_version(),
504 "PreLogin response received"
505 );
506
507 let server_max_tds = server_version.max_tds_version();
509 if server_max_tds < client_tds_version && !client_tds_version.is_tds_8() {
510 tracing::warn!(
511 requested_tds_version = %client_tds_version,
512 server_max_tds_version = %server_max_tds,
513 server_product = server_version.product_name(),
514 "Server supports lower TDS version than requested. \
515 Connection will use server's maximum: {}",
516 server_max_tds
517 );
518 }
519
520 if server_max_tds.is_legacy() {
522 tracing::warn!(
523 server_product = server_version.product_name(),
524 server_max_tds_version = %server_max_tds,
525 "Server uses legacy TDS version. Some features may not be available."
526 );
527 }
528 } else {
529 tracing::debug!(
530 requested_tds_version = %client_tds_version,
531 "PreLogin response received (no version info)"
532 );
533 }
534
535 let server_encryption = prelogin_response.encryption;
537 tracing::debug!(encryption = ?server_encryption, "server encryption level");
538
539 let negotiated_encryption = match (client_encryption, server_encryption) {
545 (EncryptionLevel::NotSupported, EncryptionLevel::NotSupported) => {
546 EncryptionLevel::NotSupported
547 }
548 (EncryptionLevel::Off, EncryptionLevel::Off) => EncryptionLevel::Off,
549 (EncryptionLevel::On, EncryptionLevel::Off)
550 | (EncryptionLevel::On, EncryptionLevel::NotSupported) => {
551 return Err(Error::Protocol(
552 "Server does not support requested encryption level".to_string(),
553 ));
554 }
555 _ => EncryptionLevel::On,
556 };
557
558 let use_tls = negotiated_encryption != EncryptionLevel::NotSupported;
561
562 if use_tls {
563 let tls_config = connection_tls_config(config, false);
568
569 let tls_connector = TlsConnector::new(tls_config)?;
570
571 let mut tls_stream = timeout(
573 config.timeouts.tls_timeout,
574 tls_connector.connect_with_prelogin(tcp_stream, &config.host),
575 )
576 .await
577 .map_err(|_| Error::TlsTimeout {
578 host: config.host.clone(),
579 port: config.port,
580 })??;
581
582 tracing::debug!("TLS handshake completed (PreLogin wrapped)");
583
584 let login_only_encryption = negotiated_encryption == EncryptionLevel::Off;
586
587 if login_only_encryption {
588 use tokio::io::AsyncWriteExt;
596
597 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
602 let negotiator = Self::create_negotiator(config)?;
603 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
604 let sspi_token = match negotiator {
605 Some(ref neg) => Some(neg.initialize()?),
606 None => None,
607 };
608 #[cfg(not(any(feature = "integrated-auth", feature = "sspi-auth")))]
609 let sspi_token: Option<Vec<u8>> = None;
610
611 let login = Self::build_login7(config, sspi_token);
613 let login_payload = login.encode();
614
615 let max_packet = MAX_PACKET_SIZE;
617 let max_payload = max_packet - PACKET_HEADER_SIZE;
618 let chunks: Vec<_> = login_payload.chunks(max_payload).collect();
619 let total_chunks = chunks.len();
620
621 for (i, chunk) in chunks.into_iter().enumerate() {
622 let is_last = i == total_chunks - 1;
623 let status = if is_last {
624 PacketStatus::END_OF_MESSAGE
625 } else {
626 PacketStatus::NORMAL
627 };
628
629 let header = PacketHeader::new(
630 PacketType::Tds7Login,
631 status,
632 (PACKET_HEADER_SIZE + chunk.len()) as u16,
633 );
634
635 let mut packet_buf = BytesMut::with_capacity(PACKET_HEADER_SIZE + chunk.len());
636 header.encode(&mut packet_buf);
637 packet_buf.put_slice(chunk);
638
639 tls_stream
640 .write_all(&packet_buf)
641 .await
642 .map_err(Error::from)?;
643 }
644
645 tls_stream.flush().await.map_err(Error::from)?;
647
648 tracing::debug!("Login7 sent through TLS, switching to plaintext for response");
649
650 let (wrapper, _client_conn) = tls_stream.into_inner();
654 let tcp_stream = wrapper.into_inner();
655
656 let mut connection = Connection::new(tcp_stream);
658 connection.set_max_message_size(config.max_response_size);
659
660 let (server_version, current_database, routing, server_collation) = timeout(
662 config.timeouts.login_timeout,
663 Self::process_login_response(
664 &mut connection,
665 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
666 negotiator.as_deref(),
667 ),
668 )
669 .await
670 .map_err(|_| Error::LoginTimeout {
671 host: config.host.clone(),
672 port: config.port,
673 })??;
674
675 if let Some((host, port)) = routing {
677 return Err(Error::Routing { host, port });
678 }
679
680 Ok(Client {
682 config: config.clone(),
683 _state: PhantomData,
684 connection: Some(ConnectionHandle::Plain(connection)),
685 server_version,
686 current_database: current_database.clone(),
687 server_collation,
688 statement_cache: StatementCache::with_default_size(),
689 transaction_descriptor: 0, needs_reset: false, in_flight: false, #[cfg(feature = "otel")]
693 instrumentation: InstrumentationContext::new(config.host.clone(), config.port)
694 .with_database(current_database.clone().unwrap_or_default()),
695 #[cfg(feature = "always-encrypted")]
696 encryption_context: config.column_encryption.clone().map(|cfg| {
697 std::sync::Arc::new(crate::encryption::EncryptionContext::from_arc(cfg))
698 }),
699 })
700 } else {
701 let mut connection = Connection::new(tls_stream);
704 connection.set_max_message_size(config.max_response_size);
705
706 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
708 let negotiator = Self::create_negotiator(config)?;
709 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
710 let sspi_token = match negotiator {
711 Some(ref neg) => Some(neg.initialize()?),
712 None => None,
713 };
714 #[cfg(not(any(feature = "integrated-auth", feature = "sspi-auth")))]
715 let sspi_token: Option<Vec<u8>> = None;
716
717 let login = Self::build_login7(config, sspi_token);
719 Self::send_login7(&mut connection, &login).await?;
720
721 let (server_version, current_database, routing, server_collation) = timeout(
723 config.timeouts.login_timeout,
724 Self::process_login_response(
725 &mut connection,
726 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
727 negotiator.as_deref(),
728 ),
729 )
730 .await
731 .map_err(|_| Error::LoginTimeout {
732 host: config.host.clone(),
733 port: config.port,
734 })??;
735
736 if let Some((host, port)) = routing {
738 return Err(Error::Routing { host, port });
739 }
740
741 Ok(Client {
742 config: config.clone(),
743 _state: PhantomData,
744 connection: Some(ConnectionHandle::TlsPrelogin(connection)),
745 server_version,
746 current_database: current_database.clone(),
747 server_collation,
748 statement_cache: StatementCache::with_default_size(),
749 transaction_descriptor: 0, needs_reset: false, in_flight: false, #[cfg(feature = "otel")]
753 instrumentation: InstrumentationContext::new(config.host.clone(), config.port)
754 .with_database(current_database.clone().unwrap_or_default()),
755 #[cfg(feature = "always-encrypted")]
756 encryption_context: config.column_encryption.clone().map(|cfg| {
757 std::sync::Arc::new(crate::encryption::EncryptionContext::from_arc(cfg))
758 }),
759 })
760 }
761 } else {
762 tracing::warn!(
764 "Connecting without TLS encryption. This is insecure and should only be \
765 used for development/testing on trusted networks."
766 );
767
768 let mut connection = Connection::new(tcp_stream);
769
770 connection.set_max_message_size(config.max_response_size);
771
772 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
774 let negotiator = Self::create_negotiator(config)?;
775 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
776 let sspi_token = match negotiator {
777 Some(ref neg) => Some(neg.initialize()?),
778 None => None,
779 };
780 #[cfg(not(any(feature = "integrated-auth", feature = "sspi-auth")))]
781 let sspi_token: Option<Vec<u8>> = None;
782
783 let login = Self::build_login7(config, sspi_token);
785 Self::send_login7(&mut connection, &login).await?;
786
787 let (server_version, current_database, routing, server_collation) = timeout(
789 config.timeouts.login_timeout,
790 Self::process_login_response(
791 &mut connection,
792 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
793 negotiator.as_deref(),
794 ),
795 )
796 .await
797 .map_err(|_| Error::LoginTimeout {
798 host: config.host.clone(),
799 port: config.port,
800 })??;
801
802 if let Some((host, port)) = routing {
804 return Err(Error::Routing { host, port });
805 }
806
807 Ok(Client {
808 config: config.clone(),
809 _state: PhantomData,
810 connection: Some(ConnectionHandle::Plain(connection)),
811 server_version,
812 current_database: current_database.clone(),
813 server_collation,
814 statement_cache: StatementCache::with_default_size(),
815 transaction_descriptor: 0, needs_reset: false, in_flight: false, #[cfg(feature = "otel")]
819 instrumentation: InstrumentationContext::new(config.host.clone(), config.port)
820 .with_database(current_database.clone().unwrap_or_default()),
821 #[cfg(feature = "always-encrypted")]
822 encryption_context: config.column_encryption.clone().map(|cfg| {
823 std::sync::Arc::new(crate::encryption::EncryptionContext::from_arc(cfg))
824 }),
825 })
826 }
827 }
828
829 #[cfg(not(feature = "tls"))]
840 async fn connect_no_tls(config: &Config, mut tcp_stream: TcpStream) -> Result<Client<Ready>> {
841 use bytes::BufMut;
842 use tds_protocol::packet::{PACKET_HEADER_SIZE, PacketHeader, PacketStatus};
843 use tokio::io::{AsyncReadExt, AsyncWriteExt};
844
845 tracing::warn!(
846 "⚠️ Connecting without TLS (tls feature disabled). \
847 Credentials and data will be transmitted in plaintext."
848 );
849
850 let prelogin = Self::build_prelogin(config, EncryptionLevel::NotSupported);
852 let prelogin_bytes = prelogin.encode();
853
854 let header = PacketHeader::new(
856 PacketType::PreLogin,
857 PacketStatus::END_OF_MESSAGE,
858 (PACKET_HEADER_SIZE + prelogin_bytes.len()) as u16,
859 );
860
861 let mut packet_buf = BytesMut::with_capacity(PACKET_HEADER_SIZE + prelogin_bytes.len());
862 header.encode(&mut packet_buf);
863 packet_buf.put_slice(&prelogin_bytes);
864
865 tcp_stream
866 .write_all(&packet_buf)
867 .await
868 .map_err(Error::from)?;
869
870 let mut header_buf = [0u8; PACKET_HEADER_SIZE];
872 tcp_stream
873 .read_exact(&mut header_buf)
874 .await
875 .map_err(Error::from)?;
876
877 let response_length = u16::from_be_bytes([header_buf[2], header_buf[3]]) as usize;
878 let payload_length = response_length.saturating_sub(PACKET_HEADER_SIZE);
879
880 let mut response_buf = vec![0u8; payload_length];
881 tcp_stream
882 .read_exact(&mut response_buf)
883 .await
884 .map_err(Error::from)?;
885
886 let prelogin_response = PreLogin::decode(&response_buf[..])?;
887
888 let server_encryption = prelogin_response.encryption;
890 if server_encryption != EncryptionLevel::NotSupported {
891 return Err(Error::Config(format!(
892 "Server requires encryption (level: {:?}) but TLS feature is disabled. \
893 Either enable the 'tls' feature or configure the server to allow unencrypted connections.",
894 server_encryption
895 )));
896 }
897
898 tracing::debug!("Server accepted unencrypted connection");
899
900 let mut connection = Connection::new(tcp_stream);
901
902 connection.set_max_message_size(config.max_response_size);
903
904 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
906 let negotiator = Self::create_negotiator(config)?;
907 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
908 let sspi_token = match negotiator {
909 Some(ref neg) => Some(neg.initialize()?),
910 None => None,
911 };
912 #[cfg(not(any(feature = "integrated-auth", feature = "sspi-auth")))]
913 let sspi_token: Option<Vec<u8>> = None;
914
915 let login = Self::build_login7(config, sspi_token);
917 Self::send_login7(&mut connection, &login).await?;
918
919 let (server_version, current_database, routing, server_collation) = timeout(
921 config.timeouts.login_timeout,
922 Self::process_login_response(
923 &mut connection,
924 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
925 negotiator.as_deref(),
926 ),
927 )
928 .await
929 .map_err(|_| Error::LoginTimeout {
930 host: config.host.clone(),
931 port: config.port,
932 })??;
933
934 if let Some((host, port)) = routing {
936 return Err(Error::Routing { host, port });
937 }
938
939 Ok(Client {
940 config: config.clone(),
941 _state: PhantomData,
942 connection: Some(ConnectionHandle::Plain(connection)),
943 server_version,
944 current_database: current_database.clone(),
945 server_collation,
946 statement_cache: StatementCache::with_default_size(),
947 transaction_descriptor: 0,
948 needs_reset: false,
949 in_flight: false,
950 #[cfg(feature = "otel")]
951 instrumentation: InstrumentationContext::new(config.host.clone(), config.port)
952 .with_database(current_database.clone().unwrap_or_default()),
953 #[cfg(feature = "always-encrypted")]
954 encryption_context: config.column_encryption.clone().map(|cfg| {
955 std::sync::Arc::new(crate::encryption::EncryptionContext::from_arc(cfg))
956 }),
957 })
958 }
959
960 fn build_prelogin(config: &Config, encryption: EncryptionLevel) -> PreLogin {
962 let version = if config.strict_mode {
964 tds_protocol::version::TdsVersion::V8_0
965 } else {
966 config.tds_version
967 };
968
969 let mut prelogin = PreLogin::new()
970 .with_version(version)
971 .with_encryption(encryption);
972
973 if config.mars {
974 prelogin = prelogin.with_mars(true);
975 }
976
977 if let Some(ref instance) = config.instance {
978 prelogin = prelogin.with_instance(instance);
979 }
980
981 prelogin
982 }
983
984 fn resolve_workstation_id(config: &Config) -> String {
992 if let Some(ref id) = config.workstation_id {
993 return id.clone();
994 }
995 std::env::var("COMPUTERNAME")
998 .or_else(|_| std::env::var("HOSTNAME"))
999 .unwrap_or_default()
1000 }
1001
1002 fn build_login7(config: &Config, sspi_token: Option<Vec<u8>>) -> Login7 {
1007 let version = if config.strict_mode {
1009 tds_protocol::version::TdsVersion::V8_0
1010 } else {
1011 config.tds_version
1012 };
1013
1014 let mut login = Login7::new()
1015 .with_tds_version(version)
1016 .with_packet_size(config.packet_size as u32)
1017 .with_app_name(&config.application_name)
1018 .with_server_name(&config.host)
1019 .with_hostname(Self::resolve_workstation_id(config));
1020
1021 if let Some(ref database) = config.database {
1022 login = login.with_database(database);
1023 }
1024
1025 if config.application_intent == crate::config::ApplicationIntent::ReadOnly {
1027 login = login.with_read_only_intent(true);
1028 }
1029
1030 if let Some(ref lang) = config.language {
1032 login = login.with_language(lang);
1033 }
1034
1035 if let Some(token) = sspi_token {
1037 login = login.with_integrated_auth(token);
1039 } else if let mssql_auth::Credentials::SqlServer { username, password } =
1040 &config.credentials
1041 {
1042 login = login.with_sql_auth(username.as_ref(), password.as_ref());
1043 }
1044
1045 #[cfg(feature = "always-encrypted")]
1048 if config.column_encryption.is_some() {
1049 login = login.with_feature(tds_protocol::login7::FeatureExtension {
1050 feature_id: tds_protocol::login7::FeatureId::ColumnEncryption,
1051 data: bytes::Bytes::from_static(&[0x01]), });
1053 tracing::debug!("Login7: adding ColumnEncryption feature extension (version 1)");
1054 }
1055
1056 login
1057 }
1058
1059 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
1069 fn create_negotiator(config: &Config) -> Result<Option<Box<dyn mssql_auth::SspiNegotiator>>> {
1070 #[allow(clippy::match_like_matches_macro)]
1071 let is_integrated = match &config.credentials {
1072 mssql_auth::Credentials::Integrated => true,
1073 _ => false,
1074 };
1075
1076 if !is_integrated {
1077 return Ok(None);
1078 }
1079
1080 #[cfg(all(windows, feature = "sspi-auth"))]
1085 let negotiator: Box<dyn mssql_auth::SspiNegotiator> =
1086 Box::new(mssql_auth::NativeSspiAuth::new(&config.host, config.port)?);
1087
1088 #[cfg(all(not(windows), feature = "sspi-auth"))]
1090 let negotiator: Box<dyn mssql_auth::SspiNegotiator> =
1091 Box::new(mssql_auth::SspiAuth::new(&config.host, config.port)?);
1092
1093 #[cfg(all(feature = "integrated-auth", not(feature = "sspi-auth")))]
1094 let negotiator: Box<dyn mssql_auth::SspiNegotiator> =
1095 Box::new(mssql_auth::IntegratedAuth::new(&config.host, config.port));
1096
1097 Ok(Some(negotiator))
1098 }
1099
1100 #[cfg(feature = "tls")]
1102 async fn send_prelogin<T>(connection: &mut Connection<T>, prelogin: &PreLogin) -> Result<()>
1103 where
1104 T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
1105 {
1106 let payload = prelogin.encode();
1107 let max_packet = MAX_PACKET_SIZE;
1108
1109 connection
1110 .send_message(PacketType::PreLogin, payload, max_packet)
1111 .await?;
1112 Ok(())
1113 }
1114
1115 #[cfg(feature = "tls")]
1117 async fn receive_prelogin<T>(connection: &mut Connection<T>) -> Result<PreLogin>
1118 where
1119 T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
1120 {
1121 let message = connection
1122 .read_message()
1123 .await?
1124 .ok_or(Error::ConnectionClosed)?;
1125
1126 Ok(PreLogin::decode(&message.payload[..])?)
1127 }
1128
1129 async fn send_login7<T>(connection: &mut Connection<T>, login: &Login7) -> Result<()>
1131 where
1132 T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
1133 {
1134 let payload = login.encode();
1135 let max_packet = MAX_PACKET_SIZE;
1136
1137 connection
1138 .send_message(PacketType::Tds7Login, payload, max_packet)
1139 .await?;
1140 Ok(())
1141 }
1142
1143 #[allow(clippy::never_loop)] async fn process_login_response<T>(
1154 connection: &mut Connection<T>,
1155 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))] negotiator: Option<
1156 &dyn mssql_auth::SspiNegotiator,
1157 >,
1158 ) -> Result<(
1159 Option<u32>,
1160 Option<String>,
1161 Option<(String, u16)>,
1162 Option<tds_protocol::token::Collation>,
1163 )>
1164 where
1165 T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
1166 {
1167 let mut server_version = None;
1168 let mut database = None;
1169 let mut routing = None;
1170 let mut collation = None;
1171
1172 'outer: loop {
1173 let message = connection
1174 .read_message()
1175 .await?
1176 .ok_or(Error::ConnectionClosed)?;
1177
1178 let response_bytes = message.payload;
1179 let mut parser = TokenParser::new(response_bytes);
1180
1181 while let Some(token) = parser.next_token()? {
1182 match token {
1183 Token::LoginAck(ack) => {
1184 tracing::info!(
1185 version = ack.tds_version,
1186 interface = ack.interface,
1187 prog_name = %ack.prog_name,
1188 "login acknowledged"
1189 );
1190 server_version = Some(ack.tds_version);
1191 }
1192 Token::EnvChange(env) => {
1193 Self::process_env_change(&env, &mut database, &mut routing, &mut collation);
1194 }
1195 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
1196 Token::Sspi(sspi_token) => {
1197 let neg = negotiator.ok_or_else(|| {
1198 Error::Protocol(
1199 "server sent SSPI challenge but no negotiator is configured"
1200 .to_string(),
1201 )
1202 })?;
1203
1204 tracing::debug!(
1205 challenge_len = sspi_token.data.len(),
1206 "received SSPI challenge from server"
1207 );
1208
1209 if let Some(response) = neg.step(&sspi_token.data)? {
1210 tracing::debug!(response_len = response.len(), "sending SSPI response");
1211 connection
1212 .send_message(
1213 PacketType::Sspi,
1214 bytes::Bytes::from(response),
1215 tds_protocol::packet::MAX_PACKET_SIZE,
1216 )
1217 .await?;
1218 }
1219
1220 continue 'outer;
1222 }
1223 Token::Error(err) => {
1224 return Err(Error::Server {
1225 number: err.number,
1226 state: err.state,
1227 class: err.class,
1228 message: err.message.clone(),
1229 server: if err.server.is_empty() {
1230 None
1231 } else {
1232 Some(err.server.clone())
1233 },
1234 procedure: if err.procedure.is_empty() {
1235 None
1236 } else {
1237 Some(err.procedure.clone())
1238 },
1239 line: err.line as u32,
1240 });
1241 }
1242 Token::Info(info) => {
1243 tracing::info!(
1244 number = info.number,
1245 message = %info.message,
1246 "server info message"
1247 );
1248 }
1249 Token::Done(done) => {
1250 if done.status.error {
1251 return Err(Error::Protocol("login failed".to_string()));
1252 }
1253 break 'outer;
1254 }
1255 _ => {}
1256 }
1257 }
1258
1259 break;
1261 }
1262
1263 Ok((server_version, database, routing, collation))
1264 }
1265
1266 fn process_env_change(
1268 env: &EnvChange,
1269 database: &mut Option<String>,
1270 routing: &mut Option<(String, u16)>,
1271 collation: &mut Option<tds_protocol::token::Collation>,
1272 ) {
1273 use tds_protocol::token::EnvChangeValue;
1274
1275 match env.env_type {
1276 EnvChangeType::Database => {
1277 if let EnvChangeValue::String(ref new_value) = env.new_value {
1278 tracing::debug!(database = %new_value, "database changed");
1279 *database = Some(new_value.clone());
1280 }
1281 }
1282 EnvChangeType::Routing => {
1283 if let EnvChangeValue::Routing { ref host, port } = env.new_value {
1284 tracing::info!(host = %host, port = port, "routing redirect received");
1285 *routing = Some((host.clone(), port));
1286 }
1287 }
1288 EnvChangeType::SqlCollation => {
1289 if let EnvChangeValue::Binary(ref data) = env.new_value {
1290 if data.len() >= 5 {
1291 let c = tds_protocol::token::Collation::from_bytes(
1292 data[..5].try_into().unwrap(),
1293 );
1294 tracing::debug!(
1295 lcid = c.lcid,
1296 sort_id = c.sort_id,
1297 "server collation received"
1298 );
1299 *collation = Some(c);
1300 }
1301 }
1302 }
1303 _ => {
1304 if let EnvChangeValue::String(ref new_value) = env.new_value {
1305 tracing::debug!(
1306 env_type = ?env.env_type,
1307 new_value = %new_value,
1308 "environment change"
1309 );
1310 }
1311 }
1312 }
1313 }
1314}
1315
1316#[cfg(feature = "tls")]
1337fn connection_tls_config(config: &Config, strict: bool) -> TlsConfig {
1338 let tls = config
1339 .tls
1340 .clone()
1341 .trust_server_certificate(config.trust_server_certificate);
1342 if strict {
1343 tls.strict_mode(true)
1344 .with_alpn_protocols(vec![b"tds/8.0".to_vec()])
1345 } else {
1346 tls
1347 }
1348}
1349
1350#[cfg(all(test, feature = "tls"))]
1351mod tls_config_tests {
1352 use super::*;
1353 use mssql_tls::CertificateDer;
1354
1355 fn config_with_root(cert: Vec<u8>) -> Config {
1356 let mut config = Config::new();
1357 config.tls = config
1358 .tls
1359 .clone()
1360 .add_root_certificate(CertificateDer::from(cert));
1361 config
1362 }
1363
1364 #[test]
1365 fn custom_root_certificate_reaches_connector_config() {
1366 let config = config_with_root(vec![0xCA; 32]);
1370
1371 for strict in [true, false] {
1372 let tls = connection_tls_config(&config, strict);
1373 assert_eq!(
1374 tls.root_certificates.len(),
1375 1,
1376 "custom root must reach the connector (strict={strict})"
1377 );
1378 assert_eq!(tls.root_certificates[0].as_ref(), &[0xCA; 32][..]);
1379 }
1380 }
1381
1382 #[test]
1383 fn trust_server_certificate_taken_from_top_level_field() {
1384 let mut config = Config::new();
1387 config.trust_server_certificate = true;
1388 assert!(!config.tls.trust_server_certificate);
1390
1391 let tls = connection_tls_config(&config, false);
1392 assert!(
1393 tls.trust_server_certificate,
1394 "top-level trust flag must win"
1395 );
1396 }
1397
1398 #[test]
1399 fn strict_mode_adds_tds8_alpn() {
1400 let config = Config::new();
1401 let strict = connection_tls_config(&config, true);
1402 assert!(strict.strict_mode);
1403 assert!(strict.alpn_protocols.iter().any(|p| p == b"tds/8.0"));
1404
1405 let non_strict = connection_tls_config(&config, false);
1406 assert!(!non_strict.strict_mode);
1407 }
1408}