Skip to main content

rustlavel_db/sqlserver/
connection.rs

1//! A single SQL Server connection.
2
3use super::auth::{self, Encryption, Negotiated, TdsStream, TlsOptions};
4use super::protocol::{
5    self, DEFAULT_PACKET_SIZE, EnvChange, HEADER_LEN, Login7, PacketHeader, ServerError,
6    Token, TokenStream, packet,
7};
8use crate::config::DatabaseConfig;
9use crate::dialect::Dialect;
10use crate::driver::{BoxFuture, Driver, DriverConnection, QueryResult};
11use crate::postgres::connection::log_bindings;
12use crate::row::{Columns, Row};
13use crate::tls::TlsMode;
14use crate::value::Value;
15use rustlavel_core::events::Event;
16use rustlavel_core::{Error, Result};
17use std::sync::Arc;
18use std::time::Instant;
19use tokio::net::TcpStream;
20
21/// Settings that are specific to SQL Server and have no home in
22/// [`DatabaseConfig`], which is shared with the other drivers.
23#[derive(Debug, Clone, Copy, Default)]
24pub struct SqlServerOptions {
25    pub encryption: Encryption,
26    pub tls: TlsOptions,
27}
28
29impl SqlServerOptions {
30    /// What the connection string asked for.
31    ///
32    /// `sslmode` was parsed and validated for `sqlserver://` URLs and then read
33    /// by nobody: every connection used `Default`, which is "encrypt, and trust
34    /// whatever certificate turns up". Somebody who wrote `sslmode=verify-full`
35    /// got no verification at all, and `sslrootcert=` was discarded — a silent
36    /// downgrade of exactly the kind the other two drivers are careful to avoid.
37    ///
38    /// `prefer`, the default, keeps the documented compromise: SQL Server
39    /// generates a version 1 self-signed certificate at startup that rustls will
40    /// not even parse, so verifying by default would refuse every stock
41    /// installation. Asking for verification, however, now gets it.
42    ///
43    /// One honest imprecision: `verify-ca` is served by the same verifier as
44    /// `verify-full`, so it checks the hostname too. That is stricter than the
45    /// mode promises rather than weaker, and it is said here rather than left
46    /// to be discovered.
47    pub fn from_config(config: &DatabaseConfig) -> SqlServerOptions {
48        let trust = !matches!(config.tls_mode, TlsMode::VerifyCa | TlsMode::VerifyFull);
49        SqlServerOptions {
50            encryption: match config.tls_mode {
51                TlsMode::Disable => Encryption::Disabled,
52                _ => Encryption::Required,
53            },
54            tls: TlsOptions { trust_server_certificate: trust },
55        }
56    }
57}
58
59pub struct SqlServerConnection {
60    stream: TdsStream,
61    /// Bytes read from the socket but not yet consumed as a packet.
62    buffer: Vec<u8>,
63    config: DatabaseConfig,
64    /// The largest packet either side may send, as the server settled it.
65    packet_size: usize,
66    /// The descriptor every request must quote; zero when no transaction is
67    /// open, which is also how `in_transaction` is answered.
68    transaction: u64,
69    /// Set when the connection is known to be unusable, so the pool discards it.
70    broken: bool,
71}
72
73impl SqlServerConnection {
74    /// Open a connection: pre-login, encryption, then login.
75    pub async fn connect(config: &DatabaseConfig) -> Result<SqlServerConnection> {
76        SqlServerConnection::connect_with(config, SqlServerOptions::from_config(config)).await
77    }
78
79    pub async fn connect_with(
80        config: &DatabaseConfig,
81        options: SqlServerOptions,
82    ) -> Result<SqlServerConnection> {
83        let address = format!("{}:{}", config.host, config.port);
84
85        let socket = tokio::time::timeout(config.connect_timeout, TcpStream::connect(&address))
86            .await
87            .map_err(|_| {
88                Error::msg(format!(
89                    "timed out connecting to {address}. Is SQL Server running, and is its TCP/IP \
90                     protocol enabled? It is off by default on Windows."
91                ))
92            })?
93            .map_err(|e| {
94                Error::msg(format!(
95                    "cannot connect to {}: {e}\n  \
96                     Check the host and port in DATABASE_URL; SQL Server listens on 1433 unless \
97                     it was configured otherwise.",
98                    config.redacted_url()
99                ))
100            })?;
101
102        let _ = socket.set_nodelay(true);
103
104        let mut connection = SqlServerConnection {
105            stream: TdsStream::Plain(socket),
106            buffer: Vec::with_capacity(8 * 1024),
107            config: config.clone(),
108            packet_size: DEFAULT_PACKET_SIZE,
109            transaction: 0,
110            broken: false,
111        };
112
113        let negotiated = connection.prelogin(options).await?;
114        connection.login(negotiated).await?;
115        Ok(connection)
116    }
117
118    pub fn is_broken(&self) -> bool {
119        self.broken
120    }
121
122    /// True while a transaction is open, so the pool never hands back a
123    /// connection that would leak one.
124    pub fn in_transaction(&self) -> bool {
125        self.transaction != 0
126    }
127
128    /// Exchange PRELOGIN packets and put TLS up if either side asked for it.
129    async fn prelogin(&mut self, options: SqlServerOptions) -> Result<Negotiated> {
130        self.write_message(packet::PRE_LOGIN, &protocol::prelogin(options.encryption.as_byte()))
131            .await?;
132
133        let response = protocol::parse_prelogin(&self.read_message().await?)?;
134        let negotiated = auth::negotiate(options.encryption, response.encryption)?;
135
136        if negotiated != Negotiated::None {
137            // The socket is handed to the handshake wrapper whole; nothing can
138            // be buffered here, because PRELOGIN is one message and it has
139            // already been consumed in full.
140            debug_assert!(self.buffer.is_empty());
141            let socket = match self.stream.take() {
142                TdsStream::Plain(socket) => socket,
143                _ => return Err(Error::Protocol("encryption was negotiated twice".into())),
144            };
145            let tls =
146                auth::start_tls(socket, &self.config.host, options.tls, self.packet_size).await?;
147            self.stream = TdsStream::Tls(Box::new(tls));
148        }
149
150        Ok(negotiated)
151    }
152
153    /// Send LOGIN7 and read the response through to its DONE.
154    async fn login(&mut self, negotiated: Negotiated) -> Result<()> {
155        let password = auth::obfuscate_password(&self.config.password);
156        let hostname = hostname();
157
158        let payload = protocol::login7(&Login7 {
159            hostname: &hostname,
160            username: &self.config.user,
161            password: &password,
162            application: &self.config.application_name,
163            server: &self.config.host,
164            library: "rustlavel-db",
165            language: "",
166            database: &self.config.database,
167            packet_size: self.packet_size,
168        });
169
170        self.write_message(packet::LOGIN7, &payload).await?;
171
172        // ENCRYPT_OFF means exactly this: the login packet was the only thing
173        // encrypted, and the response already comes back in the clear.
174        if negotiated == Negotiated::LoginOnly {
175            self.stream = self.stream.take().into_plain()?;
176        }
177
178        let message = self.read_message().await?;
179        let mut stream = TokenStream::new(&message);
180        let mut failure: Option<ServerError> = None;
181        let mut acknowledged = false;
182
183        while let Some(token) = stream.next_token()? {
184            match token {
185                Token::LoginAck(_) => acknowledged = true,
186                Token::EnvChange(change) => self.apply(change),
187                Token::Error(error) => failure = Some(error),
188                Token::Info(_) | Token::Done(_) | Token::Ignored(_) => {}
189                other => {
190                    return Err(Error::Protocol(format!(
191                        "unexpected token during login: {other:?}"
192                    )));
193                }
194            }
195        }
196
197        if let Some(error) = failure {
198            self.broken = true;
199            return Err(login_error(error, &self.config));
200        }
201        if !acknowledged {
202            self.broken = true;
203            return Err(Error::Protocol(
204                "the server ended the login exchange without accepting or refusing it. If it \
205                 requires Windows authentication, this driver implements SQL Server \
206                 authentication only."
207                    .into(),
208            ));
209        }
210
211        Ok(())
212    }
213
214    /// Run a statement with no parameters, as a batch.
215    ///
216    /// Used for DDL and for transaction control, both of which have to run
217    /// outside `sp_executesql` — a `begin transaction` inside a procedure ends
218    /// when the procedure does.
219    pub async fn simple_query(&mut self, sql: &str) -> Result<QueryResult> {
220        let started = Instant::now();
221        let payload = protocol::sql_batch(sql, self.transaction);
222        self.write_message(packet::SQL_BATCH, &payload).await?;
223
224        let result = self.collect(sql).await;
225        self.record(sql, &[], started, &result);
226        result
227    }
228
229    /// Run a statement through `sp_executesql`.
230    ///
231    /// Every statement takes this route, parameters or not, because it is the
232    /// route where a bound value is a value: the statement text and the
233    /// parameter declarations are themselves arguments to a stored procedure,
234    /// so nothing a caller binds is ever concatenated into SQL. A value cannot
235    /// change the shape of a statement it was never part of.
236    pub async fn query(&mut self, sql: &str, params: &[Value]) -> Result<QueryResult> {
237        let started = Instant::now();
238        let payload = protocol::execute_sql(sql, params, self.transaction);
239        self.write_message(packet::RPC, &payload).await?;
240
241        let result = self.collect(sql).await;
242        self.record(sql, params, started, &result);
243        result
244    }
245
246    /// Read the whole response and turn it into rows, a count and an error.
247    async fn collect(&mut self, sql: &str) -> Result<QueryResult> {
248        let message = self.read_message().await?;
249        let mut stream = TokenStream::new(&message);
250
251        let mut columns: Columns = Arc::new(Vec::new());
252        let mut result = QueryResult::default();
253        let mut failure: Option<ServerError> = None;
254        let mut changes = Vec::new();
255
256        while let Some(token) = stream.next_token()? {
257            match token {
258                Token::ColumnMetadata(described) => {
259                    columns = Arc::new(described.iter().map(|c| c.name.clone()).collect());
260                }
261                Token::Row(values) => result.rows.push(Row::new(Arc::clone(&columns), values)),
262                Token::Done(done) => {
263                    // Several DONE tokens arrive per call — one per statement
264                    // inside the procedure, one for the procedure. Only those
265                    // carrying DONE_COUNT have a number worth believing.
266                    if done.has_count() {
267                        result.affected = done.rows;
268                    }
269                }
270                Token::Error(error) => failure = Some(error),
271                // Applied after the loop, because `stream` borrows the message.
272                Token::EnvChange(change) => changes.push(change),
273                Token::Info(_) | Token::LoginAck(_) | Token::ReturnStatus(_)
274                | Token::Ignored(_) => {}
275            }
276        }
277
278        for change in changes {
279            self.apply(change);
280        }
281
282        match failure {
283            Some(error) => Err(error.into_error(Some(sql))),
284            None => {
285                result.last_insert_id = generated_key(sql, &result.rows);
286                Ok(result)
287            }
288        }
289    }
290
291    fn apply(&mut self, change: EnvChange) {
292        match change {
293            EnvChange::PacketSize(size) => {
294                self.packet_size = size.clamp(HEADER_LEN + 1, 32 * 1024)
295            }
296            EnvChange::BeginTransaction(descriptor) => self.transaction = descriptor,
297            EnvChange::CommitTransaction | EnvChange::RollbackTransaction => self.transaction = 0,
298            EnvChange::Database(_) | EnvChange::Other(_) => {}
299        }
300    }
301
302    /// Publish the query on the event bus for Telescope and slow-query logging.
303    fn record(&self, sql: &str, params: &[Value], started: Instant, result: &Result<QueryResult>) {
304        let elapsed = started.elapsed();
305
306        if rustlavel_core::events::has_subscribers() {
307            let bindings = if log_bindings() {
308                params.iter().map(Value::to_display).collect::<Vec<_>>().join(", ")
309            } else {
310                format!("{} value(s) hidden", params.len())
311            };
312            Event::new("db.query")
313                .with("sql", sql)
314                .with("bindings", bindings)
315                .with("rows", result.as_ref().map(|r| r.rows.len()).unwrap_or(0))
316                .with("ok", result.is_ok())
317                .took(elapsed)
318                .dispatch();
319        }
320
321        rustlavel_core::debug!("db: {sql} ({:.1}ms)", elapsed.as_secs_f64() * 1000.0);
322    }
323
324    /// Frame a payload into packets and send it.
325    ///
326    /// One write per packet, so an encrypted connection puts each packet in its
327    /// own TLS record — see [`protocol::split_message`] for why that matters.
328    async fn write_message(&mut self, kind: u8, payload: &[u8]) -> Result<()> {
329        for packet in protocol::split_message(kind, payload, self.packet_size) {
330            if let Err(e) = self.stream.write_all(&packet).await {
331                self.broken = true;
332                return Err(Error::Io(e));
333            }
334            if let Err(e) = self.stream.flush().await {
335                self.broken = true;
336                return Err(Error::Io(e));
337            }
338        }
339        Ok(())
340    }
341
342    /// Read packets until one carries the end-of-message bit, returning the
343    /// payloads joined back together.
344    async fn read_message(&mut self) -> Result<Vec<u8>> {
345        let mut payload = Vec::new();
346
347        loop {
348            self.fill_to(HEADER_LEN).await?;
349            let header = PacketHeader::parse(&self.buffer)?;
350            let total = header.length as usize;
351
352            if total < HEADER_LEN {
353                self.broken = true;
354                return Err(Error::Protocol("packet length is impossibly small".into()));
355            }
356
357            self.fill_to(total).await?;
358            payload.extend_from_slice(&self.buffer[HEADER_LEN..total]);
359            self.buffer.drain(..total);
360
361            if header.is_end_of_message() {
362                return Ok(payload);
363            }
364        }
365    }
366
367    /// Read from the socket until the buffer holds at least `wanted` bytes.
368    async fn fill_to(&mut self, wanted: usize) -> Result<()> {
369        while self.buffer.len() < wanted {
370            let mut chunk = [0u8; 8192];
371            let read = match self.stream.read(&mut chunk).await {
372                Ok(read) => read,
373                Err(e) => {
374                    self.broken = true;
375                    return Err(Error::Io(e));
376                }
377            };
378            if read == 0 {
379                self.broken = true;
380                return Err(Error::Protocol(
381                    "the database closed the connection unexpectedly".into(),
382                ));
383            }
384            self.buffer.extend_from_slice(&chunk[..read]);
385        }
386        Ok(())
387    }
388
389    /// Hang up.
390    ///
391    /// TDS has no goodbye token — MS-TDS says a client ends a session by
392    /// closing the transport — so there is nothing to send first.
393    pub async fn close(mut self) {
394        let _ = self.stream.shutdown().await;
395    }
396}
397
398/// Opens SQL Server connections.
399pub struct SqlServerDriver {
400    config: DatabaseConfig,
401    options: SqlServerOptions,
402    dialect: Arc<dyn Dialect>,
403}
404
405impl SqlServerDriver {
406    pub fn new(config: DatabaseConfig) -> Self {
407        let options = SqlServerOptions::from_config(&config);
408        SqlServerDriver::with_options(config, options)
409    }
410
411    pub fn with_options(config: DatabaseConfig, options: SqlServerOptions) -> Self {
412        SqlServerDriver {
413            config,
414            options,
415            dialect: Arc::new(crate::dialect::SqlServer),
416        }
417    }
418
419    pub fn config(&self) -> &DatabaseConfig {
420        &self.config
421    }
422
423    pub fn options(&self) -> &SqlServerOptions {
424        &self.options
425    }
426}
427
428impl Driver for SqlServerDriver {
429    fn generation(&self) -> u64 {
430        self.config.generation()
431    }
432
433    fn dialect(&self) -> Arc<dyn Dialect> {
434        Arc::clone(&self.dialect)
435    }
436
437    fn connect(&self) -> BoxFuture<'_, Result<Box<dyn DriverConnection>>> {
438        Box::pin(async move {
439            let connection = SqlServerConnection::connect_with(&self.config.resolved(), self.options).await?;
440            Ok(Box::new(connection) as Box<dyn DriverConnection>)
441        })
442    }
443
444    fn describe(&self) -> String {
445        self.config.redacted_url()
446    }
447
448    fn max_connections(&self) -> usize {
449        self.config.max_connections
450    }
451}
452
453impl DriverConnection for SqlServerConnection {
454    fn query<'a>(
455        &'a mut self,
456        sql: &'a str,
457        params: &'a [Value],
458    ) -> BoxFuture<'a, Result<QueryResult>> {
459        Box::pin(SqlServerConnection::query(self, sql, params))
460    }
461
462    fn simple_query<'a>(&'a mut self, sql: &'a str) -> BoxFuture<'a, Result<QueryResult>> {
463        Box::pin(SqlServerConnection::simple_query(self, sql))
464    }
465
466    fn is_broken(&self) -> bool {
467        SqlServerConnection::is_broken(self)
468    }
469
470    fn in_transaction(&self) -> bool {
471        SqlServerConnection::in_transaction(self)
472    }
473
474    fn close(self: Box<Self>) -> BoxFuture<'static, ()> {
475        Box::pin(async move { SqlServerConnection::close(*self).await })
476    }
477}
478
479/// The key an `output inserted` clause handed back.
480///
481/// SQL Server returns a generated key as an ordinary row rather than in the
482/// acknowledgement, so there is nothing on the wire that says "this is the
483/// identity". The statement is what says so: the dialect emits `output
484/// inserted.[id]`, and only a statement carrying that clause has a key to read.
485fn generated_key(sql: &str, rows: &[Row]) -> Option<i64> {
486    if !sql.to_ascii_lowercase().contains("output inserted") {
487        return None;
488    }
489    rows.first()?.get_at::<i64>(0).ok()
490}
491
492/// The host name to send in LOGIN7, which shows up in `sys.dm_exec_sessions`.
493fn hostname() -> String {
494    std::env::var("HOSTNAME")
495        .ok()
496        .filter(|name| !name.is_empty())
497        .unwrap_or_else(|| "rustlavel".to_string())
498}
499
500/// Turn a login failure into something the developer can act on.
501///
502/// SQL Server's login errors are famously terse — 18456 says "Login failed for
503/// user" and nothing else, because saying more would help an attacker — so the
504/// actionable half has to come from this side.
505fn login_error(error: ServerError, config: &DatabaseConfig) -> Error {
506    let number = error.number;
507    let base = error.into_error(None);
508
509    let advice = match number {
510        18456 => Some(format!(
511            "The password for `{}` was rejected, or that login does not exist. Check \
512             DATABASE_URL in your .env. SQL Server logs the real reason in its error log; the \
513             wire deliberately does not carry it.",
514            config.user
515        )),
516        4060 => Some(format!(
517            "Database `{}` cannot be opened by `{}`. Create it, grant access to it, or point \
518             DATABASE_URL at one that exists.",
519            config.database, config.user
520        )),
521        18452 => Some(
522            "The login is from an untrusted domain and cannot be used with Windows \
523             authentication. This driver implements SQL Server authentication only: give \
524             DATABASE_URL a username and password."
525                .to_string(),
526        ),
527        _ => None,
528    };
529
530    match advice {
531        Some(advice) => Error::msg(format!("{base}\n  {advice}")),
532        None => base,
533    }
534}
535
536#[cfg(test)]
537mod tests {
538    use super::*;
539
540    fn row(value: Value) -> Row {
541        Row::new(Arc::new(vec!["id".to_string()]), vec![value])
542    }
543
544    #[test]
545    fn a_generated_key_is_read_only_from_a_statement_that_asked_for_one() {
546        let rows = vec![row(Value::Int(42))];
547
548        assert_eq!(
549            generated_key("insert into [t] ([n]) output inserted.[id] values (@P1)", &rows),
550            Some(42)
551        );
552        // Case does not matter; the builder may emit either.
553        assert_eq!(generated_key("INSERT INTO [t] OUTPUT INSERTED.[id] ...", &rows), Some(42));
554
555        // A plain select returns rows too, and none of them is an identity.
556        assert_eq!(generated_key("select id from t", &rows), None);
557        // A statement that asked but got nothing back.
558        assert_eq!(generated_key("insert ... output inserted.[id] ...", &[]), None);
559        // A non-integer key, such as a uuid, is not an insert id.
560        assert_eq!(
561            generated_key("output inserted.[id]", &[row(Value::Text("abc".into()))]),
562            None
563        );
564    }
565
566    #[test]
567    fn a_rejected_password_says_where_to_look() {
568        let error = ServerError {
569            number: 18456,
570            severity: 14,
571            state: 1,
572            message: "Login failed for user 'ada'.".into(),
573            ..ServerError::default()
574        };
575        let config = DatabaseConfig { user: "ada".into(), ..DatabaseConfig::default() };
576
577        let rendered = login_error(error, &config).to_string();
578        assert!(rendered.contains("18456"), "{rendered}");
579        assert!(rendered.contains("severity 14"), "{rendered}");
580        assert!(rendered.contains("DATABASE_URL"), "{rendered}");
581        assert!(rendered.contains("`ada`"), "{rendered}");
582    }
583
584    #[test]
585    fn a_missing_database_names_the_database_that_is_missing() {
586        let error = ServerError {
587            number: 4060,
588            severity: 11,
589            message: "Cannot open database \"blog\" requested by the login.".into(),
590            ..ServerError::default()
591        };
592        let config = DatabaseConfig { database: "blog".into(), ..DatabaseConfig::default() };
593
594        let rendered = login_error(error, &config).to_string();
595        assert!(rendered.contains("`blog` cannot be opened"), "{rendered}");
596    }
597
598    #[test]
599    fn windows_authentication_is_refused_by_name_rather_than_retried() {
600        let error = ServerError { number: 18452, severity: 14, ..ServerError::default() };
601
602        let rendered = login_error(error, &DatabaseConfig::default()).to_string();
603        assert!(rendered.contains("SQL Server authentication only"), "{rendered}");
604    }
605
606    #[test]
607    fn an_error_with_no_advice_is_still_reported_verbatim() {
608        let error = ServerError {
609            number: 208,
610            severity: 16,
611            message: "Invalid object name 'nope'.".into(),
612            ..ServerError::default()
613        };
614
615        let rendered = login_error(error, &DatabaseConfig::default()).to_string();
616        assert!(rendered.contains("Invalid object name"), "{rendered}");
617    }
618
619    #[test]
620    fn the_driver_speaks_the_sql_server_dialect_and_never_prints_the_password() {
621        let config = DatabaseConfig {
622            driver: "sqlserver".into(),
623            user: "sa".into(),
624            password: "hunter2".into(),
625            port: 1433,
626            ..DatabaseConfig::default()
627        };
628        let driver = SqlServerDriver::new(config);
629
630        assert_eq!(driver.dialect().name(), "sqlserver");
631        assert_eq!(driver.options().encryption, Encryption::Required);
632        assert!(!driver.describe().contains("hunter2"), "{}", driver.describe());
633    }
634
635    #[tokio::test]
636    async fn connecting_to_a_closed_port_names_the_server_and_the_default_port() {
637        let config = DatabaseConfig {
638            driver: "sqlserver".into(),
639            port: 1,
640            password: "hunter2".into(),
641            ..DatabaseConfig::default()
642        };
643
644        let error = match SqlServerConnection::connect(&config).await {
645            Err(error) => error.to_string(),
646            Ok(_) => panic!("nothing should be listening on port 1"),
647        };
648
649        assert!(error.contains("127.0.0.1:1"), "{error}");
650        assert!(error.contains("1433"), "{error}");
651        // The password must never appear, even in a connection error.
652        assert!(!error.contains("hunter2"), "{error}");
653    }
654}