netget/server/mssql/mod.rs
1//! MSSQL server implementation using manual TDS protocol
2pub mod actions;
3
4use crate::llm::action_helper::call_llm;
5use crate::llm::actions::protocol_trait::ActionResult;
6use crate::llm::ollama_client::OllamaClient;
7use crate::logging::emit::Log;
8use crate::protocol::Event;
9use crate::server::connection::ConnectionId;
10use crate::state::app_state::AppState;
11use actions::{MssqlProtocol, MSSQL_QUERY_EVENT};
12use anyhow::Result;
13use std::net::SocketAddr;
14use std::sync::Arc;
15use tokio::io::{AsyncReadExt, AsyncWriteExt, ReadHalf};
16use tokio::net::{TcpListener, TcpStream};
17use tokio::sync::{mpsc, Mutex};
18use tracing::{debug, error, info, trace, warn};
19
20/// TDS packet size we advertise in the login ENVCHANGE and honour when writing.
21const TDS_PACKET_SIZE: usize = 4096;
22
23/// The write half of one connection, shared between the session loop and the dashboard's
24/// peer-command task (`server::peer_support`).
25///
26/// Every handler below that used to take `&mut TcpStream` only ever *wrote* through it — the
27/// reads all happen in the top-level loop — so they take this instead. The mutex is what keeps
28/// an injected write from landing between the continuation packets of a TDS message.
29type SharedWrite = Arc<Mutex<tokio::io::WriteHalf<TcpStream>>>;
30
31/// How long to wait for a peer's first TDS packet after it has connected.
32///
33/// TDS is client-speaks-first: PRELOGIN is the first packet and the server says nothing before
34/// it. `tiberius`, `sqlcmd` and every ADO.NET driver send it inside the connect path, so a peer
35/// that has connected and sent nothing has begun no session — and that is the state an
36/// unauthenticated flood lives in.
37const FIRST_PACKET_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
38
39/// How long to wait for a *further* packet once the peer has logged in.
40///
41/// SQL Server has no idle-disconnect default to copy, so the number is argued the same way
42/// MySQL's and PostgreSQL's are: this server holds nothing a reconnect would lose — no tables,
43/// no temp tables, no open transaction, no state the model carries between batches — so a
44/// pooled connection reaped here costs one transparent reconnect. Ten minutes is far above any
45/// connection pool's keepalive interval and still bounds the hold.
46const IDLE_BETWEEN_PACKETS_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(600);
47
48/// Concurrent connections this server admits.
49const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
50
51/// A peer over [`MAX_CONNECTIONS`] gets a plain close, which is what a real SQL Server does.
52///
53/// TDS has no message a server may send to a client still in PRELOGIN: every server packet is a
54/// typed response to a request, and a TABULAR_RESULT carrying an ERROR token read in that state
55/// is a framing violation, not a diagnosis. The refusal is in the log instead, tagged
56/// `decision=fail_closed_connection_cap`.
57const CONNECTION_CAP_REFUSAL: &[u8] = b"";
58
59/// SQL Server's own "Login failed for user '%.*ls'." error. Severity 14 is what a real
60/// instance sends, and every driver already maps it to an authentication failure.
61const LOGIN_FAILED_ERROR: u32 = 18456;
62
63/// Generic user-defined error number. Anything >= 50000 is a user error, which is what an
64/// LLM-driven server's failures actually are.
65const MSSQL_ERROR_GENERIC: u32 = 50000;
66
67/// "Cannot process request. Not enough resources to process request." On the standard
68/// transient-error list SqlClient's built-in retry logic keys off, so a client backs off and
69/// retries instead of surfacing the failure to the application.
70const MSSQL_ERROR_NOT_ENOUGH_RESOURCES: u32 = 49918;
71
72/// MSSQL server implementation
73pub struct MssqlServer {
74 llm_client: OllamaClient,
75 app_state: Arc<AppState>,
76 _status_tx: mpsc::UnboundedSender<String>,
77 server_id: Option<crate::state::ServerId>,
78}
79
80impl MssqlServer {
81 /// Create a new MSSQL server
82 pub fn new(
83 llm_client: OllamaClient,
84 app_state: Arc<AppState>,
85 status_tx: mpsc::UnboundedSender<String>,
86 server_id: Option<crate::state::ServerId>,
87 ) -> Self {
88 Self {
89 llm_client,
90 app_state,
91 _status_tx: status_tx,
92 server_id,
93 }
94 }
95
96 /// Spawn MSSQL server with LLM integration
97 pub async fn spawn_with_llm_actions(
98 listen_addr: SocketAddr,
99 llm_client: OllamaClient,
100 app_state: Arc<AppState>,
101 status_tx: mpsc::UnboundedSender<String>,
102 _send_first: bool,
103 server_id: crate::state::ServerId,
104 ) -> Result<SocketAddr> {
105 let listener = TcpListener::bind(listen_addr).await?;
106 let actual_addr = listener.local_addr()?;
107
108 Log::new(Some(&status_tx)).info(format!("MSSQL server listening on {}", actual_addr));
109
110 let server = Arc::new(MssqlServer::new(
111 llm_client,
112 app_state.clone(),
113 status_tx.clone(),
114 Some(server_id),
115 ));
116
117 let status_tx_clone = status_tx.clone();
118 let task_registrar = app_state.clone();
119
120 // Spawn the accept loop
121 let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
122 let accept_handle = tokio::spawn(async move {
123 loop {
124 match crate::server::accept_bounded::accept_bounded(
125 &listener,
126 &limiter,
127 CONNECTION_CAP_REFUSAL,
128 "MSSQL",
129 Some(&status_tx),
130 )
131 .await
132 {
133 Ok((stream, addr, permit)) => {
134 Log::new(Some(&status_tx)).info(format!("MSSQL connection from {}", addr));
135
136 let connection_id =
137 ConnectionId::new(app_state.get_next_unified_id().await);
138 let local_addr_conn = stream.local_addr().unwrap_or(actual_addr);
139
140 // Track the connection
141 if let Some(server_id) = server.server_id {
142 use crate::state::server::{
143 ConnectionState as ServerConnectionState, ConnectionStatus,
144 ProtocolConnectionInfo,
145 };
146 let now = crate::utils::clock::Instant::now();
147 let conn_state = ServerConnectionState {
148 id: connection_id,
149 remote_addr: addr,
150 local_addr: local_addr_conn,
151 bytes_sent: 0,
152 bytes_received: 0,
153 packets_sent: 0,
154 packets_received: 0,
155 last_activity: now,
156 status: ConnectionStatus::Active,
157 status_changed_at: now,
158 protocol_info: ProtocolConnectionInfo::empty(),
159 };
160 server
161 .app_state
162 .add_connection_to_server(server_id, conn_state)
163 .await;
164 }
165
166 let handler = MssqlHandler::new(
167 connection_id,
168 server.llm_client.clone(),
169 server.app_state.clone(),
170 status_tx.clone(),
171 server.server_id,
172 addr,
173 );
174
175 // Tracked, not detached: stop_server must abort this task too.
176 let task_owner = app_state.clone();
177 task_owner
178 .spawn_server_task(server_id, async move {
179 // Held for the life of the session, so the cap counts live
180 // connections rather than accepts.
181 let _permit = permit;
182 if let Err(e) = handler.handle_connection(stream).await {
183 error!("MSSQL connection error: {:?}", e);
184 }
185 })
186 .await;
187 }
188 Err(e) => {
189 Log::new(Some(&status_tx)).error(format!("MSSQL accept error: {}", e));
190 }
191 }
192 }
193 });
194
195 // Register the accept loop so stop_server can abort it and release the port.
196 task_registrar
197 .register_server_task(server_id, accept_handle)
198 .await;
199
200 let _ = status_tx_clone.send("__UPDATE_UI__".to_string());
201 Ok(actual_addr)
202 }
203}
204
205/// MSSQL connection handler
206pub struct MssqlHandler {
207 connection_id: ConnectionId,
208 llm_client: OllamaClient,
209 app_state: Arc<AppState>,
210 status_tx: mpsc::UnboundedSender<String>,
211 #[allow(dead_code)]
212 server_id: Option<crate::state::ServerId>,
213 #[allow(dead_code)]
214 remote_addr: SocketAddr,
215 /// MSSQL protocol handler for action execution
216 protocol: Arc<MssqlProtocol>,
217}
218
219impl MssqlHandler {
220 pub fn new(
221 connection_id: ConnectionId,
222 llm_client: OllamaClient,
223 app_state: Arc<AppState>,
224 status_tx: mpsc::UnboundedSender<String>,
225 server_id: Option<crate::state::ServerId>,
226 remote_addr: SocketAddr,
227 ) -> Self {
228 let protocol = Arc::new(MssqlProtocol::new(
229 connection_id,
230 app_state.clone(),
231 status_tx.clone(),
232 ));
233
234 Self {
235 connection_id,
236 llm_client,
237 app_state,
238 status_tx,
239 server_id,
240 remote_addr,
241 protocol,
242 }
243 }
244
245 /// Run the connection, then always mark it closed in `AppState`.
246 ///
247 /// The socket is split here rather than inside the loop, because the write half has two
248 /// owners: the loop's own TDS replies and the dashboard's peer-command task. Registration
249 /// happens **before the first read** — TDS is client-speaks-first, so this server says
250 /// nothing until PRELOGIN arrives, and a `manual` rule can then park the login (or any
251 /// later batch) for minutes. The operator has to be able to reach, or hang up, a
252 /// connection that is waiting on their own answer.
253 async fn handle_connection(self, stream: TcpStream) -> Result<()> {
254 let server_id = self.server_id;
255 let connection_id = self.connection_id;
256 let app_state = self.app_state.clone();
257
258 // Owning split, never a clone: the write half outlives this stack frame inside the
259 // peer-command task.
260 let (mut reader, write_half) = tokio::io::split(stream);
261 let write_half: SharedWrite = Arc::new(Mutex::new(write_half));
262
263 if let Some(server_id) = server_id {
264 let peer_rx = crate::server::peer_support::register_peer_channel(
265 &app_state,
266 server_id,
267 connection_id.as_u32(),
268 )
269 .await;
270 crate::server::peer_support::spawn_peer_command_task(
271 peer_rx,
272 self.protocol.clone(),
273 app_state.clone(),
274 server_id,
275 connection_id.as_u32(),
276 write_half.clone(),
277 self.status_tx.clone(),
278 );
279 }
280
281 let result = self.run_session(&mut reader, &write_half).await;
282
283 // Every exit path — EOF, a read timeout, a refused login, an attention, a write error,
284 // an injected disconnect — lands here, and none of them may `?` past it. Dropping the
285 // handle also ends the peer command task, which releases its clone of the write half;
286 // the explicit shutdown makes the FIN immediate rather than waiting on it.
287 if let Some(server_id) = server_id {
288 app_state
289 .remove_peer_handle(server_id, connection_id.as_u32())
290 .await;
291 }
292 let _ = write_half.lock().await.shutdown().await;
293
294 if let Some(server_id) = server_id {
295 app_state
296 .close_connection_on_server(server_id, connection_id)
297 .await;
298 }
299
300 result
301 }
302
303 /// The read/dispatch/reply loop. Reads come off `reader`; every write goes through the
304 /// shared `write_half`, which the peer-command task also holds.
305 async fn run_session(
306 &self,
307 reader: &mut ReadHalf<TcpStream>,
308 write_half: &SharedWrite,
309 ) -> Result<()> {
310 info!("MSSQL connection established");
311
312 // Set only by an `mssql_login_ack`. Nothing else in this loop may set it, so the
313 // admission decision has exactly one source.
314 //
315 // Without this flag the `mssql_login` decision was skippable: the loop dispatched
316 // packet types independently of one another, so a peer that simply never sent a
317 // LOGIN7 and opened with a SQL Batch (0x01) or an RPC (0x03) got its query answered
318 // and `mssql_login` never fired at all. An instruction like "only allow the user
319 // `reporting`" was then enforced against every client that asked to be checked and
320 // none that did not. Real drivers always log in; a hostile peer has no reason to.
321 let mut authenticated = false;
322
323 // Handle TDS protocol negotiation and queries
324 loop {
325 // Both reads below are bounded by this deadline, and nothing else in the loop is.
326 // The LLM round-trip that decides a login or answers a batch, and a `manual` rule
327 // parking either for a human (`src/state/intercepts.rs`, 300s by default), happen
328 // after a whole packet has been read — outside the deadline by construction, so
329 // neither can be cut short. What is bounded is a peer holding the connection while
330 // sending nothing, and one that announces a packet length and then stalls.
331 let read_timeout = if authenticated {
332 IDLE_BETWEEN_PACKETS_TIMEOUT
333 } else {
334 FIRST_PACKET_READ_TIMEOUT
335 };
336
337 // Read TDS packet header (8 bytes)
338 let header = match self.read_tds_header(reader, read_timeout).await {
339 Ok(h) => h,
340 Err(e) => {
341 debug!("Error reading TDS header: {}", e);
342 break;
343 }
344 };
345
346 if header.length < 8 {
347 debug!("Invalid TDS packet length: {}", header.length);
348 break;
349 }
350
351 // Read packet data
352 let data_len = header.length - 8;
353 let mut data = vec![0u8; data_len as usize];
354 match tokio::time::timeout(read_timeout, reader.read_exact(&mut data)).await {
355 Ok(read) => read?,
356 Err(_) => {
357 debug!(
358 "MSSQL connection {} announced a {}-byte packet and stalled for {}s \
359 without delivering it; closing",
360 self.connection_id,
361 header.length,
362 read_timeout.as_secs()
363 );
364 break;
365 }
366 };
367
368 // Only the *sent* side was counted before, so every MSSQL connection in the
369 // dashboard rail showed a growing ↑ against a permanently zero ↓, and
370 // `last_activity` was never refreshed by an inbound packet.
371 if let Some(server_id) = self.server_id {
372 self.app_state
373 .update_connection_stats(
374 server_id,
375 self.connection_id,
376 Some(u64::from(header.length)),
377 None,
378 Some(1),
379 None,
380 )
381 .await;
382 }
383
384 trace!(
385 "TDS packet type: 0x{:02x}, length: {}",
386 header.packet_type,
387 header.length
388 );
389
390 match header.packet_type {
391 0x12 => {
392 // Pre-Login
393 debug!("Received Pre-Login packet (length: {})", header.length);
394 self.send_prelogin_response(write_half).await?;
395 debug!("Pre-Login response sent");
396 }
397 0x10 => {
398 // TDS7/TDS8 Login
399 debug!("Received Login packet");
400 if !self.handle_login(write_half, &data).await? {
401 // Refused. The ERROR token is already on the wire; TDS has no way to
402 // continue a session whose login failed, so close.
403 break;
404 }
405 authenticated = true;
406 }
407 0x01 | 0x03 if !authenticated => {
408 // A statement before the session was admitted. Answer with the same 18456
409 // an outright refusal produces and close, rather than serving the query:
410 // the model was never asked about this peer, so nothing has admitted it.
411 Log::new(Some(&self.status_tx)).warn(
412 "MSSQL statement refused (decision=fail_closed_not_logged_in): the \
413 client sent a batch or RPC before any LOGIN7"
414 .to_string(),
415 );
416 self.send_error(
417 write_half,
418 LOGIN_FAILED_ERROR,
419 "Login failed. The login is from an untrusted domain and cannot be \
420 used with Windows authentication.",
421 14,
422 )
423 .await?;
424 break;
425 }
426 0x01 => {
427 // SQL Batch
428 debug!("Received SQL Batch packet");
429 let query = self.parse_sql_batch(&data)?;
430 debug!("SQL Query: {}", query);
431 if self.handle_query(write_half, &query).await? {
432 break;
433 }
434 }
435 0x03 => {
436 // RPC Request (sp_executesql, sp_prepare, etc.)
437 debug!("Received RPC Request");
438 let query = self.parse_rpc_request(&data)?;
439 if !query.is_empty() {
440 debug!("RPC Query: {}", query);
441 if self.handle_query(write_half, &query).await? {
442 break;
443 }
444 } else {
445 debug!("RPC call without extractable query (ignoring)");
446 // Send empty result set for RPCs we can't parse
447 self.send_empty_result(write_half).await?;
448 }
449 }
450 0x0E => {
451 // Bulk Load
452 debug!("Received Bulk Load (not implemented)");
453 self.send_error(write_half, 40002, "Bulk load not supported", 16)
454 .await?;
455 }
456 0x07 => {
457 // Attention (cancel)
458 debug!("Received Attention signal");
459 break;
460 }
461 _ => {
462 debug!("Unknown TDS packet type: 0x{:02x}", header.packet_type);
463 self.send_error(write_half, 40002, "Unknown packet type", 16)
464 .await?;
465 }
466 }
467 }
468
469 Ok(())
470 }
471
472 /// Read TDS packet header (8 bytes)
473 async fn read_tds_header(
474 &self,
475 reader: &mut ReadHalf<TcpStream>,
476 read_timeout: std::time::Duration,
477 ) -> Result<TdsHeader> {
478 let mut header_bytes = [0u8; 8];
479 match tokio::time::timeout(read_timeout, reader.read_exact(&mut header_bytes)).await {
480 Ok(read) => read?,
481 Err(_) => {
482 anyhow::bail!(
483 "peer sent nothing for {}s; closing idle connection",
484 read_timeout.as_secs()
485 )
486 }
487 };
488
489 Ok(TdsHeader {
490 packet_type: header_bytes[0],
491 status: header_bytes[1],
492 length: u16::from_be_bytes([header_bytes[2], header_bytes[3]]),
493 spid: u16::from_be_bytes([header_bytes[4], header_bytes[5]]),
494 packet_id: header_bytes[6],
495 window: header_bytes[7],
496 })
497 }
498
499 /// Send Pre-Login response
500 async fn send_prelogin_response(&self, write_half: &SharedWrite) -> Result<()> {
501 // Simplified Pre-Login response
502 // Version: 16.0.0.0 (SQL Server 2022)
503 // Encryption: NOT_SUP (0x02)
504 let mut response = Vec::new();
505
506 // Calculate offsets (all token headers = 3 tokens * 5 bytes + 1 terminator = 16 bytes)
507 let header_size = 16u16;
508 let version_offset = header_size; // 16 (0x10)
509 let version_length = 6u16;
510 let encryption_offset = version_offset + version_length; // 22 (0x16)
511 let encryption_length = 1u16;
512 let threadid_offset = encryption_offset + encryption_length; // 23 (0x17)
513 let threadid_length = 4u16;
514
515 // Version token (0x00)
516 response.push(0x00);
517 response.extend_from_slice(&version_offset.to_be_bytes()); // Offset: 0x00, 0x10
518 response.extend_from_slice(&version_length.to_be_bytes()); // Length: 0x00, 0x06
519
520 // Encryption token (0x01)
521 response.push(0x01);
522 response.extend_from_slice(&encryption_offset.to_be_bytes()); // Offset: 0x00, 0x16
523 response.extend_from_slice(&encryption_length.to_be_bytes()); // Length: 0x00, 0x01
524
525 // ThreadID token (0x03)
526 response.push(0x03);
527 response.extend_from_slice(&threadid_offset.to_be_bytes()); // Offset: 0x00, 0x17
528 response.extend_from_slice(&threadid_length.to_be_bytes()); // Length: 0x00, 0x04
529
530 // Terminator
531 response.push(0xFF);
532
533 // Version data (16.0.0.0)
534 response.extend_from_slice(&[0x10, 0x00, 0x00, 0x00, 0x00, 0x00]);
535
536 // Encryption: ENCRYPT_NOT_SUP (0x02) - encryption not supported
537 response.push(0x02);
538
539 // ThreadID: 0
540 response.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
541
542 self.send_tds_packet(write_half, 0x04, &response).await
543 }
544
545 /// Ask the model whether to admit this login, and refuse unless it says yes.
546 ///
547 /// Every TDS Login used to be accepted unconditionally: there was no `mssql_login` event
548 /// and no action that could decline one, so an operator instruction like "only allow the
549 /// user `reporting`" could not be enforced and the model was never consulted. Authentication
550 /// decided by default is the pattern the root CLAUDE.md calls the most dangerous here.
551 ///
552 /// Returns `Ok(true)` when the session may proceed. Accept requires an explicit
553 /// `mssql_login_ack`; a model rejection (`mssql_error_response`), an answer containing
554 /// neither, and a backend failure all refuse — and are kept apart in the log, because the
555 /// wire cannot distinguish them beyond the error number.
556 async fn handle_login(&self, write_half: &SharedWrite, data: &[u8]) -> Result<bool> {
557 let (username, database, app_name) = parse_login7(data);
558
559 let event = Event::new(
560 &actions::MSSQL_LOGIN_EVENT,
561 serde_json::json!({
562 "username": username,
563 "database": database,
564 "app_name": app_name,
565 }),
566 );
567
568 let server_id = self
569 .server_id
570 .unwrap_or_else(|| crate::state::ServerId::new(0));
571
572 let llm_result = call_llm(
573 &self.llm_client,
574 &self.app_state,
575 server_id,
576 Some(self.connection_id),
577 &event,
578 self.protocol.as_ref(),
579 )
580 .await;
581
582 match llm_result {
583 Ok(result) => {
584 for r in &result.protocol_results {
585 if let ActionResult::Custom { name, data } = r {
586 match name.as_str() {
587 "mssql_login_ack" => {
588 let db = data
589 .get("database")
590 .and_then(|v| v.as_str())
591 .unwrap_or("master");
592 Log::new(Some(&self.status_tx)).info(format!(
593 "MSSQL login accepted for user '{}' (decision=model_accept)",
594 username
595 ));
596 self.send_login_response_with_database(write_half, db)
597 .await?;
598 return Ok(true);
599 }
600 "mssql_error" => {
601 let number = data
602 .get("error_number")
603 .and_then(|v| v.as_u64())
604 .unwrap_or(LOGIN_FAILED_ERROR as u64)
605 as u32;
606 let message = data
607 .get("message")
608 .and_then(|v| v.as_str())
609 .unwrap_or("Login failed.");
610 Log::new(Some(&self.status_tx)).info(format!(
611 "MSSQL login refused for user '{}' (decision=model_reject): {}",
612 username, message
613 ));
614 self.send_error(write_half, number, message, 14).await?;
615 return Ok(false);
616 }
617 _ => {}
618 }
619 }
620 }
621
622 // The handler ran and produced neither verdict. Refuse: silence is not consent
623 // for an authentication decision.
624 Log::new(Some(&self.status_tx)).warn(format!(
625 "MSSQL login refused for user '{}' (decision=fail_closed_no_action): the \
626 handler produced no mssql_login_ack",
627 username
628 ));
629 self.send_error(
630 write_half,
631 LOGIN_FAILED_ERROR,
632 &format!("Login failed for user '{}'.", username),
633 14,
634 )
635 .await?;
636 Ok(false)
637 }
638 Err(e) => {
639 // netget could not reach a decision. Refuse, and tell the client only a
640 // category — never the backend error, which names the model and the URL.
641 let failure = crate::utils::WireFailure::classify(&e);
642 error!(
643 "MSSQL login for user '{}' decision=fail_closed_llm_error category={:?}: {:#}",
644 username, failure, e
645 );
646 Log::new(Some(&self.status_tx)).warn(format!(
647 "MSSQL login refused for user '{}' (decision=fail_closed_llm_error)",
648 username
649 ));
650 self.send_error(write_half, LOGIN_FAILED_ERROR, failure.text(), 14)
651 .await?;
652 Ok(false)
653 }
654 }
655 }
656
657 /// Send the LOGINACK/ENVCHANGE/DONE sequence that admits a session.
658 ///
659 /// Only reachable from `handle_login` once the model has explicitly accepted.
660 async fn send_login_response_with_database(
661 &self,
662 write_half: &SharedWrite,
663 db_name: &str,
664 ) -> Result<()> {
665 let mut response = Vec::new();
666
667 // ENVCHANGE: Database context
668
669 let db_name_utf16: Vec<u8> = db_name
670 .encode_utf16()
671 .flat_map(|c| c.to_le_bytes())
672 .collect();
673
674 let mut envchange_db = Vec::new();
675 envchange_db.push(0xE3); // ENVCHANGE token
676 let envchange_db_len = 1 + 1 + db_name_utf16.len() + 1 + db_name_utf16.len();
677 envchange_db.extend_from_slice(&(envchange_db_len as u16).to_le_bytes());
678 envchange_db.push(0x01); // Type: Database
679 envchange_db.push(db_name.len() as u8); // New value length (in characters)
680 envchange_db.extend_from_slice(&db_name_utf16);
681 envchange_db.push(db_name.len() as u8); // Old value length (in characters)
682 envchange_db.extend_from_slice(&db_name_utf16);
683 response.extend_from_slice(&envchange_db);
684
685 // ENVCHANGE: Language (us_english)
686 let lang = "us_english";
687 let lang_utf16: Vec<u8> = lang.encode_utf16().flat_map(|c| c.to_le_bytes()).collect();
688
689 let mut envchange_lang = Vec::new();
690 envchange_lang.push(0xE3); // ENVCHANGE token
691 let envchange_lang_len = 1 + 1 + lang_utf16.len() + 1 + 0; // old value is empty
692 envchange_lang.extend_from_slice(&(envchange_lang_len as u16).to_le_bytes());
693 envchange_lang.push(0x02); // Type: Language
694 envchange_lang.push(lang.len() as u8); // New value length (in characters)
695 envchange_lang.extend_from_slice(&lang_utf16);
696 envchange_lang.push(0x00); // Old value length (empty)
697 response.extend_from_slice(&envchange_lang);
698
699 // ENVCHANGE: Packet size ("4096" as string)
700 let pkt_size_new = "4096";
701 let pkt_size_old = "512"; // Default packet size
702 let pkt_size_new_utf16: Vec<u8> = pkt_size_new
703 .encode_utf16()
704 .flat_map(|c| c.to_le_bytes())
705 .collect();
706 let pkt_size_old_utf16: Vec<u8> = pkt_size_old
707 .encode_utf16()
708 .flat_map(|c| c.to_le_bytes())
709 .collect();
710
711 let mut envchange_pkt = Vec::new();
712 envchange_pkt.push(0xE3); // ENVCHANGE token
713 let envchange_pkt_len = 1 + 1 + pkt_size_new_utf16.len() + 1 + pkt_size_old_utf16.len();
714 envchange_pkt.extend_from_slice(&(envchange_pkt_len as u16).to_le_bytes());
715 envchange_pkt.push(0x04); // Type: Packet size
716 envchange_pkt.push(pkt_size_new.len() as u8); // New value length (in characters)
717 envchange_pkt.extend_from_slice(&pkt_size_new_utf16);
718 envchange_pkt.push(pkt_size_old.len() as u8); // Old value length (in characters)
719 envchange_pkt.extend_from_slice(&pkt_size_old_utf16);
720 response.extend_from_slice(&envchange_pkt);
721
722 // INFO message
723 let msg = "Login succeeded";
724 let msg_utf16: Vec<u8> = msg.encode_utf16().flat_map(|c| c.to_le_bytes()).collect();
725
726 response.push(0xAB); // INFO token
727 let info_len = 4 + 1 + 1 + 2 + msg_utf16.len() + 1 + 1 + 4;
728 response.extend_from_slice(&(info_len as u16).to_le_bytes());
729 response.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]); // Error number
730 response.push(0x01); // State
731 response.push(0x00); // Class (severity)
732 response.extend_from_slice(&(msg.len() as u16).to_le_bytes()); // Message length (character count)
733 response.extend_from_slice(&msg_utf16);
734 response.push(0x00); // Server name length
735 response.push(0x00); // Procedure name length
736 response.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Line number
737
738 // DONE token
739 response.push(0xFD); // DONE token
740 response.extend_from_slice(&[0x00, 0x00]); // Status
741 response.extend_from_slice(&[0x00, 0x00]); // CurCmd
742 response.extend_from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); // DoneRowCount
743
744 self.send_tds_packet(write_half, 0x04, &response).await
745 }
746
747 /// Parse SQL Batch packet
748 fn parse_sql_batch(&self, data: &[u8]) -> Result<String> {
749 // SQL Batch format:
750 // - Header (22 bytes for TDS 7.4+)
751 // - SQL text (Unicode UTF-16LE)
752
753 if data.len() < 22 {
754 return Ok(String::new());
755 }
756
757 // Skip header, extract SQL text
758 let sql_bytes = &data[22..];
759
760 // Decode UTF-16LE
761 let sql_u16: Vec<u16> = sql_bytes
762 .chunks_exact(2)
763 .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
764 .collect();
765
766 Ok(String::from_utf16_lossy(&sql_u16).trim().to_string())
767 }
768
769 /// Recover the SQL text from an RPC Request packet.
770 ///
771 /// RPC parameter marshalling is not decoded; the statement is found heuristically by
772 /// looking for a SQL keyword in the UTF-16LE payload. Two properties matter and both were
773 /// wrong before:
774 ///
775 /// * **One decode, not one per offset.** This used to slide a 2000-byte window forward two
776 /// bytes at a time, decoding and upper-casing it at every position: ~32,000 windows for a
777 /// maximum-size TDS packet, each allocating two strings and running eight substring
778 /// searches, with no `.await` anywhere to yield the worker. A single 64 KB packet was
779 /// seconds of CPU on a tokio worker thread, repeatable by one connection. Every window sat
780 /// on the same UTF-16 code-unit grid (the step is 2 bytes and so is a code unit), so
781 /// decoding the payload once covers exactly the same text.
782 /// * **`to_ascii_uppercase`, never `to_uppercase`.** The offset of the keyword was taken
783 /// from the upper-cased string and then used to slice the *original* one. Unicode
784 /// case mapping changes byte length - U+FB01 `fi` upper-cases to the two bytes `FI`, U+0149
785 /// `ʼn` to the three bytes `ʼN` - so a payload carrying one such character ahead of the
786 /// keyword sliced the original mid-character and panicked. That is reachable from the
787 /// wire, and the panic skipped `handle_connection`'s teardown, leaving the connection
788 /// `Active` in the dashboard for the life of the process. ASCII case folding never
789 /// changes a byte's width, so the offset stays valid in both strings.
790 fn parse_rpc_request(&self, data: &[u8]) -> Result<String> {
791 /// Keywords a statement may open with, searched for case-insensitively.
792 const SQL_KEYWORDS: [&str; 7] = [
793 "SELECT", "INSERT", "UPDATE", "DELETE", "CREATE", "DROP", "ALTER",
794 ];
795
796 if data.len() < 10 {
797 return Ok(String::new());
798 }
799
800 let preview_len = std::cmp::min(data.len(), 200);
801 let hex_preview: String = data[..preview_len]
802 .iter()
803 .map(|b| format!("{:02x}", b))
804 .collect::<Vec<_>>()
805 .join(" ");
806 debug!(
807 "RPC data preview (first {} bytes): {}",
808 preview_len, hex_preview
809 );
810
811 let units: Vec<u16> = data
812 .chunks_exact(2)
813 .map(|c| u16::from_le_bytes([c[0], c[1]]))
814 .collect();
815 let text = String::from_utf16_lossy(&units);
816 let upper = text.to_ascii_uppercase();
817
818 // The earliest keyword anywhere in the payload, not the first keyword in list order:
819 // a packet mentioning DELETE in a parameter name before its actual SELECT should not
820 // have the SELECT truncated away.
821 let Some(pos) = SQL_KEYWORDS.iter().filter_map(|kw| upper.find(kw)).min() else {
822 return Ok(String::new());
823 };
824
825 // `upper` is ASCII-folded `text`, so byte lengths and char boundaries are identical
826 // and `pos` indexes the same character in both.
827 let sql: String = text[pos..]
828 .chars()
829 .take_while(|c| {
830 c.is_ascii() && (*c == '\n' || *c == '\r' || *c == '\t' || !c.is_ascii_control())
831 })
832 .collect();
833 let sql = sql.trim().to_string();
834 if sql.is_empty() {
835 return Ok(String::new());
836 }
837
838 debug!("Extracted SQL from RPC at offset {}: {}", pos, sql);
839 Ok(sql)
840 }
841
842 /// Handle SQL query with LLM.
843 ///
844 /// Returns `true` when the LLM asked to close the connection.
845 async fn handle_query(&self, write_half: &SharedWrite, query: &str) -> Result<bool> {
846 trace!("Calling LLM for MSSQL query: {}", query);
847
848 // Create query event
849 let event = Event::new(
850 &MSSQL_QUERY_EVENT,
851 serde_json::json!({
852 "query": query,
853 }),
854 );
855
856 let server_id = self
857 .server_id
858 .unwrap_or_else(|| crate::state::ServerId::new(0));
859
860 let llm_result = call_llm(
861 &self.llm_client,
862 &self.app_state,
863 server_id,
864 Some(self.connection_id),
865 &event,
866 self.protocol.as_ref(),
867 )
868 .await;
869
870 match llm_result {
871 Ok(execution_result) => {
872 let close_requested = execution_result
873 .protocol_results
874 .iter()
875 .any(|r| matches!(r, ActionResult::CloseConnection));
876 let mut responded = false;
877
878 // Process action results to find MSSQL responses
879 for result in execution_result.protocol_results {
880 if responded {
881 // TDS is strictly one token stream per statement, so only the first
882 // response-producing action can reach the wire. Say which ones were
883 // dropped rather than discarding them silently.
884 if let ActionResult::Custom { name, .. } = &result {
885 warn!(
886 "MSSQL: query already answered, ignoring extra action result '{}'",
887 name
888 );
889 }
890 continue;
891 }
892 match result {
893 ActionResult::Custom { name, data } => match name.as_str() {
894 "mssql_query_response" => {
895 let columns = data
896 .get("columns")
897 .and_then(|v| v.as_array())
898 .cloned()
899 .unwrap_or_default();
900 let rows = data
901 .get("rows")
902 .and_then(|v| v.as_array())
903 .cloned()
904 .unwrap_or_default();
905
906 self.send_result_set(write_half, columns, rows).await?;
907 responded = true;
908 }
909 "mssql_error" => {
910 let error_number = data
911 .get("error_number")
912 .and_then(|v| v.as_u64())
913 .unwrap_or(50000)
914 as u32;
915 let message = data
916 .get("message")
917 .and_then(|v| v.as_str())
918 .unwrap_or("Unknown error");
919 let severity =
920 data.get("severity").and_then(|v| v.as_u64()).unwrap_or(16)
921 as u8;
922
923 // The model chose to refuse this statement. Tagged so an
924 // operator can tell a deliberate refusal from netget failing
925 // to obtain one; the two are indistinguishable on the wire.
926 Log::new(Some(&self.status_tx)).info(format!(
927 "MSSQL query decision=model_reject error={} severity={}",
928 error_number, severity
929 ));
930 self.send_error(write_half, error_number, message, severity)
931 .await?;
932 responded = true;
933 }
934 "mssql_ok" => {
935 let rows_affected = data
936 .get("rows_affected")
937 .and_then(|v| v.as_u64())
938 .unwrap_or(0);
939
940 self.send_done(write_half, rows_affected).await?;
941 responded = true;
942 }
943 other => {
944 warn!(
945 "MSSQL: no wire encoding for action result '{}', ignoring",
946 other
947 );
948 }
949 },
950 _ => {}
951 }
952 }
953
954 if !responded {
955 // TDS clients block until a DONE or an ERROR arrives, so something must be
956 // sent. It must not be a bare DONE: in SQL that reads as "the statement ran
957 // and matched nothing", which is a meaningful *successful* answer. Nobody
958 // answered this query, so the fail-closed token is ERROR.
959 //
960 // An explicit `close_this_connection` is the one exception - that is the
961 // model deciding, not the model going quiet - so it keeps the DONE it has
962 // always produced and is tagged as its own decision.
963 if close_requested {
964 Log::new(Some(&self.status_tx))
965 .info("MSSQL query decision=model_close (closing without a result)");
966 self.send_done(write_half, 0).await?;
967 } else {
968 let message = crate::utils::WireFailure::Unavailable.prefixed_text();
969 Log::new(Some(&self.status_tx)).warn(format!(
970 "MSSQL query decision=fail_closed_no_action; replying error {}: {}",
971 MSSQL_ERROR_GENERIC, message
972 ));
973 warn!("MSSQL: no response action produced for query {:?}", query);
974 self.send_error(write_half, MSSQL_ERROR_GENERIC, message, 16)
975 .await?;
976 }
977 }
978
979 Ok(close_requested)
980 }
981 Err(e) => {
982 // A TDS ERROR token is raised by every driver as a SqlException on the
983 // statement that produced it, so it cannot be mistaken for a result set - and
984 // an empty result set is a meaningful answer in SQL, which is why silence or a
985 // bare DONE would be worse than useless here.
986 //
987 // 49918 ("Cannot process request. Not enough resources") is on the standard
988 // transient-error list that SqlClient's own retry logic keys off, so capacity
989 // exhaustion gets retried rather than surfaced to the application. 50000 is
990 // the generic user-defined error number for everything else.
991 let overloaded = crate::llm::is_overload_error(&e);
992 let number = if overloaded {
993 MSSQL_ERROR_NOT_ENOUGH_RESOURCES
994 } else {
995 MSSQL_ERROR_GENERIC
996 };
997 let message = crate::utils::WireFailure::classify(&e).prefixed_text();
998 // Non-fatal: a wire fallback (TDS ERROR token) is still delivered and the
999 // connection stays open, so this is recovered rather than a hard failure.
1000 // `decision=fail_closed_llm_error` is never `model_reject`: the model said
1001 // nothing at all here. The full error goes to the log only.
1002 error!("MSSQL: LLM error answering query {:?}: {:#}", query, e);
1003 Log::new(Some(&self.status_tx)).warn(format!(
1004 "MSSQL query decision=fail_closed_llm_error ({}); replying error {number}: {message}",
1005 if overloaded { "overloaded" } else { "unavailable" }
1006 ));
1007 self.send_error(write_half, number, message, 16).await?;
1008 Ok(false)
1009 }
1010 }
1011 }
1012
1013 /// Send result set
1014 ///
1015 /// Every column is emitted as one of TDS's *nullable* (variable-length) types, because
1016 /// those are the only ones whose COLMETADATA carries a length byte and whose row values
1017 /// carry a length prefix - which is the shape this encoder produces. The previous mapping
1018 /// handed out FIXEDLENTYPE codes (INT4TYPE 0x38, INT8TYPE 0x7F, BITTYPE 0x32, FLT4TYPE
1019 /// 0x3B) while still writing length bytes, so anything other than a string column put
1020 /// malformed tokens on the wire.
1021 async fn send_result_set(
1022 &self,
1023 write_half: &SharedWrite,
1024 columns: Vec<serde_json::Value>,
1025 rows: Vec<serde_json::Value>,
1026 ) -> Result<()> {
1027 debug!(
1028 "send_result_set called: {} columns, {} rows",
1029 columns.len(),
1030 rows.len()
1031 );
1032
1033 let col_types: Vec<TdsColumnType> = columns
1034 .iter()
1035 .map(|col| {
1036 TdsColumnType::from_name(
1037 col.get("type")
1038 .and_then(|v| v.as_str())
1039 .unwrap_or("NVARCHAR"),
1040 )
1041 })
1042 .collect();
1043
1044 let mut response = Vec::new();
1045
1046 // COLMETADATA token
1047 response.push(0x81);
1048 response.extend_from_slice(&(columns.len() as u16).to_le_bytes());
1049
1050 for (col, col_type) in columns.iter().zip(&col_types) {
1051 let col_name = col.get("name").and_then(|v| v.as_str()).unwrap_or("column");
1052
1053 response.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // UserType
1054 // Flags is a little-endian USHORT and fNullable is bit 0, so the bytes are
1055 // 01 00. This used to be `[0x00, 0x02]`, i.e. bit 9 - which no TDS flag
1056 // occupies, so a real client (tiberius) rejected every result set with
1057 // "column metadata: invalid flags". It went unnoticed because NetGet's own
1058 // MSSQL client could not complete the handshake at all.
1059 response.extend_from_slice(&[0x01, 0x00]); // Flags: fNullable
1060 col_type.write_type_info(&mut response);
1061
1062 // Column name is a B_VARCHAR: length in UTF-16 code units, then UTF-16LE.
1063 let name_units: Vec<u16> = col_name.encode_utf16().take(128).collect();
1064 response.push(name_units.len() as u8);
1065 for unit in &name_units {
1066 response.extend_from_slice(&unit.to_le_bytes());
1067 }
1068 }
1069
1070 // ROW tokens
1071 for row in &rows {
1072 response.push(0xD1); // ROW token
1073
1074 let row_values = row.as_array().cloned().unwrap_or_default();
1075 // TDS requires exactly one value per described column: pad short rows with NULL
1076 // and drop extras rather than desynchronising the token stream.
1077 for (idx, col_type) in col_types.iter().enumerate() {
1078 let value = row_values.get(idx).unwrap_or(&serde_json::Value::Null);
1079 col_type.write_value(&mut response, value);
1080 }
1081 }
1082
1083 // DONE token. DONE_COUNT (0x0010) is what makes the client believe the row count.
1084 response.push(0xFD);
1085 response.extend_from_slice(&0x0010u16.to_le_bytes()); // Status: final + count valid
1086 response.extend_from_slice(&0x00C1u16.to_le_bytes()); // CurCmd: SELECT
1087 response.extend_from_slice(&(rows.len() as u64).to_le_bytes());
1088
1089 debug!("Sending result set: {} bytes", response.len());
1090 self.send_tds_packet(write_half, 0x04, &response).await
1091 }
1092
1093 /// Send empty result set (just DONE token)
1094 async fn send_empty_result(&self, write_half: &SharedWrite) -> Result<()> {
1095 let mut response = Vec::new();
1096
1097 // DONE token
1098 response.push(0xFD);
1099 response.extend_from_slice(&[0x00, 0x00]); // Status
1100 response.extend_from_slice(&[0x00, 0x00]); // CurCmd
1101 response.extend_from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); // DoneRowCount
1102
1103 self.send_tds_packet(write_half, 0x04, &response).await
1104 }
1105
1106 /// Send error response
1107 async fn send_error(
1108 &self,
1109 write_half: &SharedWrite,
1110 error_number: u32,
1111 message: &str,
1112 severity: u8,
1113 ) -> Result<()> {
1114 let mut response = Vec::new();
1115
1116 // ERROR token (0xAA)
1117 response.push(0xAA);
1118
1119 let msg_u16: Vec<u16> = message.encode_utf16().collect();
1120 let msg_bytes: Vec<u8> = msg_u16.iter().flat_map(|c| c.to_le_bytes()).collect();
1121
1122 let token_len = 4 + 1 + 1 + 2 + msg_bytes.len() + 1 + 1 + 4;
1123 response.extend_from_slice(&(token_len as u16).to_le_bytes());
1124
1125 response.extend_from_slice(&error_number.to_le_bytes());
1126 response.push(0x01); // State
1127 response.push(severity); // Class (severity)
1128 response.extend_from_slice(&(msg_u16.len() as u16).to_le_bytes());
1129 response.extend_from_slice(&msg_bytes);
1130 response.push(0x00); // Server name length
1131 response.push(0x00); // Procedure name length
1132 response.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Line number
1133
1134 // DONE token
1135 response.push(0xFD);
1136 response.extend_from_slice(&[0x00, 0x00]); // Status
1137 response.extend_from_slice(&[0x00, 0x00]); // CurCmd
1138 response.extend_from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
1139
1140 self.send_tds_packet(write_half, 0x04, &response).await
1141 }
1142
1143 /// Send DONE token
1144 async fn send_done(&self, write_half: &SharedWrite, rows_affected: u64) -> Result<()> {
1145 let mut response = Vec::new();
1146
1147 response.push(0xFD); // DONE token
1148 // DONE_COUNT (0x0010) must be set or the client ignores DoneRowCount entirely, which
1149 // silently discarded the `rows_affected` value the LLM supplied via mssql_ok_response.
1150 response.extend_from_slice(&0x0010u16.to_le_bytes()); // Status: final + count valid
1151 response.extend_from_slice(&0x00C1u16.to_le_bytes()); // CurCmd
1152 response.extend_from_slice(&rows_affected.to_le_bytes());
1153
1154 self.send_tds_packet(write_half, 0x04, &response).await
1155 }
1156
1157 /// Send a TDS message, split across packets of the negotiated size.
1158 ///
1159 /// The header length field is a `u16`, so a single oversized write used to wrap around and
1160 /// emit a packet whose declared length was far shorter than its payload. TDS solves this
1161 /// with continuation packets: every packet but the last carries status 0x00, the last
1162 /// carries 0x01 (EOM).
1163 ///
1164 /// The whole message is framed first and written under **one** lock on the shared write
1165 /// half. Locking per packet would let an injected write from the dashboard's peer task
1166 /// land between two continuation packets of the same message, which no TDS client can
1167 /// resynchronise from.
1168 async fn send_tds_packet(
1169 &self,
1170 write_half: &SharedWrite,
1171 packet_type: u8,
1172 data: &[u8],
1173 ) -> Result<()> {
1174 let max_payload = TDS_PACKET_SIZE - 8;
1175 let mut packet_id: u8 = 1;
1176 let mut offset = 0;
1177 let mut framed: Vec<u8> = Vec::with_capacity(data.len() + 8);
1178 let mut packets: u64 = 0;
1179
1180 loop {
1181 let end = std::cmp::min(offset + max_payload, data.len());
1182 let chunk = &data[offset..end];
1183 let is_last = end == data.len();
1184
1185 framed.push(packet_type); // Type
1186 framed.push(if is_last { 0x01 } else { 0x00 }); // Status: EOM on the last packet
1187 framed.extend_from_slice(&((8 + chunk.len()) as u16).to_be_bytes()); // Length
1188 framed.extend_from_slice(&[0x00, 0x00]); // SPID
1189 framed.push(packet_id); // PacketID
1190 framed.push(0x00); // Window
1191 framed.extend_from_slice(chunk);
1192 packets += 1;
1193
1194 if is_last {
1195 break;
1196 }
1197 offset = end;
1198 packet_id = packet_id.wrapping_add(1).max(1);
1199 }
1200
1201 write_counted(
1202 write_half,
1203 &framed,
1204 packets,
1205 &self.app_state,
1206 self.server_id,
1207 self.connection_id,
1208 )
1209 .await?;
1210 Ok(())
1211 }
1212}
1213
1214/// Write one framed TDS message to the shared write half and count it.
1215///
1216/// `packets` is the number of TDS packets inside `data`, which is why this takes a count
1217/// rather than assuming one: a message over [`TDS_PACKET_SIZE`] is several packets in a
1218/// single write, and the rail's `↑` packet counter has always counted TDS packets.
1219///
1220/// The guard is dropped before `update_connection_stats`, so nothing awaits `AppState` — let
1221/// alone an LLM call — while holding the write half.
1222async fn write_counted(
1223 write_half: &SharedWrite,
1224 data: &[u8],
1225 packets: u64,
1226 app_state: &AppState,
1227 server_id: Option<crate::state::ServerId>,
1228 connection_id: ConnectionId,
1229) -> std::io::Result<()> {
1230 {
1231 let mut writer = write_half.lock().await;
1232 writer.write_all(data).await?;
1233 writer.flush().await?;
1234 }
1235 if let Some(server_id) = server_id {
1236 app_state
1237 .update_connection_stats(
1238 server_id,
1239 connection_id,
1240 None,
1241 Some(data.len() as u64),
1242 None,
1243 Some(packets),
1244 )
1245 .await;
1246 }
1247 Ok(())
1248}
1249
1250/// TDS packet header
1251#[allow(dead_code)]
1252struct TdsHeader {
1253 packet_type: u8,
1254 #[allow(dead_code)]
1255 status: u8,
1256 length: u16,
1257 #[allow(dead_code)]
1258 spid: u16,
1259 #[allow(dead_code)]
1260 packet_id: u8,
1261 #[allow(dead_code)]
1262 window: u8,
1263}
1264
1265/// Maximum bytes of NVARCHAR payload a single column value may carry.
1266///
1267/// 4000 UTF-16 code units is the largest non-MAX `nvarchar`. Declaring `nvarchar(max)` instead
1268/// would oblige us to emit PLP-chunked row values, which this encoder does not do.
1269const NVARCHAR_MAX_BYTES: usize = 8000;
1270
1271/// The TDS wire representation chosen for one result-set column.
1272#[derive(Clone, Copy, PartialEq, Eq)]
1273enum TdsColumnType {
1274 /// INTNTYPE (0x26) with the given byte width: 1, 2, 4 or 8.
1275 Int(u8),
1276 /// BITNTYPE (0x68).
1277 Bit,
1278 /// FLTNTYPE (0x6D), 8-byte double.
1279 Float,
1280 /// NVARCHARTYPE (0xE7), UTF-16LE with a USHORT length prefix.
1281 NVarChar,
1282}
1283
1284impl TdsColumnType {
1285 /// Map an LLM-supplied SQL type name onto a wire type. Unknown names become NVARCHAR.
1286 fn from_name(type_name: &str) -> Self {
1287 match type_name.trim().to_uppercase().as_str() {
1288 "TINYINT" => TdsColumnType::Int(1),
1289 "SMALLINT" => TdsColumnType::Int(2),
1290 "INT" | "INTEGER" => TdsColumnType::Int(4),
1291 "BIGINT" => TdsColumnType::Int(8),
1292 "BIT" | "BOOL" | "BOOLEAN" => TdsColumnType::Bit,
1293 "FLOAT" | "REAL" | "DOUBLE" | "DECIMAL" | "NUMERIC" | "MONEY" => TdsColumnType::Float,
1294 _ => TdsColumnType::NVarChar,
1295 }
1296 }
1297
1298 /// Write the COLMETADATA TYPE_INFO for this column.
1299 fn write_type_info(&self, out: &mut Vec<u8>) {
1300 match self {
1301 TdsColumnType::Int(width) => {
1302 out.push(0x26); // INTNTYPE
1303 out.push(*width);
1304 }
1305 TdsColumnType::Bit => {
1306 out.push(0x68); // BITNTYPE
1307 out.push(0x01);
1308 }
1309 TdsColumnType::Float => {
1310 out.push(0x6D); // FLTNTYPE
1311 out.push(0x08);
1312 }
1313 TdsColumnType::NVarChar => {
1314 out.push(0xE7); // NVARCHARTYPE
1315 out.extend_from_slice(&(NVARCHAR_MAX_BYTES as u16).to_le_bytes());
1316 out.extend_from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00]); // COLLATION
1317 }
1318 }
1319 }
1320
1321 /// Write one row value, including its length prefix. JSON `null` becomes a real SQL NULL.
1322 fn write_value(&self, out: &mut Vec<u8>, value: &serde_json::Value) {
1323 match self {
1324 TdsColumnType::Int(width) => {
1325 match value_as_i64(value) {
1326 Some(v) => {
1327 out.push(*width);
1328 match width {
1329 1 => out.push(v as u8),
1330 2 => out.extend_from_slice(&(v as i16).to_le_bytes()),
1331 4 => out.extend_from_slice(&(v as i32).to_le_bytes()),
1332 _ => out.extend_from_slice(&v.to_le_bytes()),
1333 }
1334 }
1335 // NULL, or a value that is not an integer at all.
1336 None => out.push(0x00),
1337 }
1338 }
1339 TdsColumnType::Bit => match value_as_bool(value) {
1340 Some(b) => {
1341 out.push(0x01);
1342 out.push(u8::from(b));
1343 }
1344 None => out.push(0x00),
1345 },
1346 TdsColumnType::Float => match value_as_f64(value) {
1347 Some(f) => {
1348 out.push(0x08);
1349 out.extend_from_slice(&f.to_le_bytes());
1350 }
1351 None => out.push(0x00),
1352 },
1353 TdsColumnType::NVarChar => {
1354 if value.is_null() {
1355 // 0xFFFF is the NVARCHAR NULL marker. Writing the text "NULL" here, as the
1356 // previous encoder did, made every NULL arrive as a four-character string.
1357 out.extend_from_slice(&0xFFFFu16.to_le_bytes());
1358 return;
1359 }
1360 let text = json_to_string(value);
1361 let mut bytes: Vec<u8> = Vec::new();
1362 for unit in text.encode_utf16() {
1363 if bytes.len() + 2 > NVARCHAR_MAX_BYTES {
1364 break;
1365 }
1366 bytes.extend_from_slice(&unit.to_le_bytes());
1367 }
1368 out.extend_from_slice(&(bytes.len() as u16).to_le_bytes());
1369 out.extend_from_slice(&bytes);
1370 }
1371 }
1372 }
1373}
1374
1375fn value_as_i64(value: &serde_json::Value) -> Option<i64> {
1376 match value {
1377 serde_json::Value::Number(n) => n.as_i64().or_else(|| n.as_f64().map(|f| f as i64)),
1378 serde_json::Value::Bool(b) => Some(i64::from(*b)),
1379 serde_json::Value::String(s) => s.parse().ok(),
1380 _ => None,
1381 }
1382}
1383
1384fn value_as_f64(value: &serde_json::Value) -> Option<f64> {
1385 match value {
1386 serde_json::Value::Number(n) => n.as_f64(),
1387 serde_json::Value::Bool(b) => Some(if *b { 1.0 } else { 0.0 }),
1388 serde_json::Value::String(s) => s.parse().ok(),
1389 _ => None,
1390 }
1391}
1392
1393fn value_as_bool(value: &serde_json::Value) -> Option<bool> {
1394 match value {
1395 serde_json::Value::Bool(b) => Some(*b),
1396 serde_json::Value::Number(n) => n.as_i64().map(|v| v != 0),
1397 serde_json::Value::String(s) => match s.to_ascii_lowercase().as_str() {
1398 "true" | "1" | "yes" => Some(true),
1399 "false" | "0" | "no" => Some(false),
1400 _ => None,
1401 },
1402 _ => None,
1403 }
1404}
1405
1406/// Convert a JSON value to its NVARCHAR text form.
1407fn json_to_string(value: &serde_json::Value) -> String {
1408 match value {
1409 serde_json::Value::Null => String::new(),
1410 serde_json::Value::Bool(b) => if *b { "1" } else { "0" }.to_string(),
1411 serde_json::Value::Number(n) => n.to_string(),
1412 serde_json::Value::String(s) => s.clone(),
1413 serde_json::Value::Array(_) | serde_json::Value::Object(_) => value.to_string(),
1414 }
1415}
1416
1417/// Pull the username, database and application name out of a LOGIN7 packet body.
1418///
1419/// MS-TDS 2.2.6.4: a fixed 36-byte prelude, then a table of `(offset, length)` pairs in
1420/// UTF-16 code units, each offset measured from the start of the LOGIN7 data. Only three of
1421/// the fields matter for an access decision, and the password is deliberately *not* among
1422/// them — it is not needed to decide, and putting a credential into an event would send it
1423/// to the model and into the event log.
1424///
1425/// Everything is best-effort: a truncated or malformed packet yields empty strings rather
1426/// than an error, because the caller's job is to refuse a login it cannot justify, and an
1427/// unparseable login is exactly that. It must never be the reason a login is *granted*.
1428#[cfg(not(target_arch = "wasm32"))]
1429fn parse_login7(data: &[u8]) -> (String, String, String) {
1430 /// Offsets of the (offset, length) pairs within the LOGIN7 body, per MS-TDS 2.2.6.4.
1431 const IB_USERNAME: usize = 40;
1432 const IB_APPNAME: usize = 48;
1433 const IB_DATABASE: usize = 68;
1434
1435 let field = |pair_at: usize| -> String {
1436 // Each pair is two little-endian u16s: offset, then length in UTF-16 characters.
1437 let Some(bytes) = data.get(pair_at..pair_at + 4) else {
1438 return String::new();
1439 };
1440 let offset = u16::from_le_bytes([bytes[0], bytes[1]]) as usize;
1441 let chars = u16::from_le_bytes([bytes[2], bytes[3]]) as usize;
1442 if chars == 0 {
1443 return String::new();
1444 }
1445 let Some(raw) = data.get(offset..offset + chars * 2) else {
1446 return String::new();
1447 };
1448 let units: Vec<u16> = raw
1449 .chunks_exact(2)
1450 .map(|c| u16::from_le_bytes([c[0], c[1]]))
1451 .collect();
1452 String::from_utf16_lossy(&units)
1453 };
1454
1455 (field(IB_USERNAME), field(IB_DATABASE), field(IB_APPNAME))
1456}