1use std::collections::HashMap;
23use std::io::{Read, Write};
24use std::net::TcpStream;
25#[cfg(feature = "console")]
26use std::sync::Arc;
27
28use sqlmodel_core::Error;
29use sqlmodel_core::error::{
30 ConnectionError, ConnectionErrorKind, ProtocolError, QueryError, QueryErrorKind,
31};
32
33#[cfg(feature = "console")]
34use sqlmodel_console::{ConsoleAware, SqlModelConsole};
35
36use crate::auth::ScramClient;
37use crate::config::PgConfig;
38#[cfg(not(feature = "tls"))]
39use crate::config::SslMode;
40use crate::protocol::{
41 BackendMessage, ErrorFields, FrontendMessage, MessageReader, MessageWriter, PROTOCOL_VERSION,
42 TransactionStatus,
43};
44
45#[cfg(feature = "tls")]
46use crate::tls;
47
48#[allow(clippy::large_enum_variant)]
51enum PgStream {
52 Plain(TcpStream),
53 #[cfg(feature = "tls")]
54 Tls(rustls::StreamOwned<rustls::ClientConnection, TcpStream>),
55 #[cfg(feature = "tls")]
56 Closed,
57}
58
59impl PgStream {
60 #[cfg(feature = "tls")]
61 fn read_exact(&mut self, buf: &mut [u8]) -> std::io::Result<()> {
62 match self {
63 PgStream::Plain(s) => s.read_exact(buf),
64 #[cfg(feature = "tls")]
65 PgStream::Tls(s) => s.read_exact(buf),
66 #[cfg(feature = "tls")]
67 PgStream::Closed => Err(std::io::Error::new(
68 std::io::ErrorKind::NotConnected,
69 "connection closed",
70 )),
71 }
72 }
73
74 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
75 match self {
76 PgStream::Plain(s) => s.read(buf),
77 #[cfg(feature = "tls")]
78 PgStream::Tls(s) => s.read(buf),
79 #[cfg(feature = "tls")]
80 PgStream::Closed => Err(std::io::Error::new(
81 std::io::ErrorKind::NotConnected,
82 "connection closed",
83 )),
84 }
85 }
86
87 fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> {
88 match self {
89 PgStream::Plain(s) => s.write_all(buf),
90 #[cfg(feature = "tls")]
91 PgStream::Tls(s) => s.write_all(buf),
92 #[cfg(feature = "tls")]
93 PgStream::Closed => Err(std::io::Error::new(
94 std::io::ErrorKind::NotConnected,
95 "connection closed",
96 )),
97 }
98 }
99
100 fn flush(&mut self) -> std::io::Result<()> {
101 match self {
102 PgStream::Plain(s) => s.flush(),
103 #[cfg(feature = "tls")]
104 PgStream::Tls(s) => s.flush(),
105 #[cfg(feature = "tls")]
106 PgStream::Closed => Err(std::io::Error::new(
107 std::io::ErrorKind::NotConnected,
108 "connection closed",
109 )),
110 }
111 }
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub enum ConnectionState {
117 Disconnected,
119 Connecting,
121 Authenticating,
123 Ready(TransactionStatusState),
125 InQuery,
127 InTransaction(TransactionStatusState),
129 Error,
131 Closed,
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
137pub enum TransactionStatusState {
138 #[default]
140 Idle,
141 InTransaction,
143 InFailed,
145}
146
147impl From<TransactionStatus> for TransactionStatusState {
148 fn from(status: TransactionStatus) -> Self {
149 match status {
150 TransactionStatus::Idle => TransactionStatusState::Idle,
151 TransactionStatus::Transaction => TransactionStatusState::InTransaction,
152 TransactionStatus::Error => TransactionStatusState::InFailed,
153 }
154 }
155}
156
157pub struct PgConnection {
168 stream: PgStream,
170 state: ConnectionState,
172 process_id: i32,
174 secret_key: i32,
176 parameters: HashMap<String, String>,
178 config: PgConfig,
180 reader: MessageReader,
182 writer: MessageWriter,
184 read_buf: Vec<u8>,
186 #[cfg(feature = "console")]
188 console: Option<Arc<SqlModelConsole>>,
189}
190
191impl std::fmt::Debug for PgConnection {
192 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193 f.debug_struct("PgConnection")
194 .field("state", &self.state)
195 .field("process_id", &self.process_id)
196 .field("host", &self.config.host)
197 .field("port", &self.config.port)
198 .field("database", &self.config.database)
199 .finish_non_exhaustive()
200 }
201}
202
203impl PgConnection {
204 #[allow(clippy::result_large_err)]
213 pub fn connect(config: PgConfig) -> Result<Self, Error> {
214 let stream = TcpStream::connect_timeout(
216 &config.socket_addr().parse().map_err(|e| {
217 Error::Connection(ConnectionError {
218 kind: ConnectionErrorKind::Connect,
219 message: format!("Invalid socket address: {}", e),
220 source: None,
221 })
222 })?,
223 config.connect_timeout,
224 )
225 .map_err(|e| {
226 let kind = if e.kind() == std::io::ErrorKind::ConnectionRefused {
227 ConnectionErrorKind::Refused
228 } else {
229 ConnectionErrorKind::Connect
230 };
231 Error::Connection(ConnectionError {
232 kind,
233 message: format!("Failed to connect to {}: {}", config.socket_addr(), e),
234 source: Some(Box::new(e)),
235 })
236 })?;
237
238 stream.set_nodelay(true).ok();
240 stream.set_read_timeout(Some(config.connect_timeout)).ok();
241 stream.set_write_timeout(Some(config.connect_timeout)).ok();
242
243 let mut conn = Self {
244 stream: PgStream::Plain(stream),
245 state: ConnectionState::Connecting,
246 process_id: 0,
247 secret_key: 0,
248 parameters: HashMap::new(),
249 config,
250 reader: MessageReader::new(),
251 writer: MessageWriter::new(),
252 read_buf: vec![0u8; 8192],
253 #[cfg(feature = "console")]
254 console: None,
255 };
256
257 if conn.config.ssl_mode.should_try_ssl() {
259 #[cfg(feature = "tls")]
260 conn.negotiate_ssl()?;
261
262 #[cfg(not(feature = "tls"))]
263 if conn.config.ssl_mode != SslMode::Prefer {
264 return Err(Error::Connection(ConnectionError {
265 kind: ConnectionErrorKind::Ssl,
266 message:
267 "TLS requested but 'sqlmodel-postgres' was built without feature 'tls'"
268 .to_string(),
269 source: None,
270 }));
271 }
272 }
273
274 conn.send_startup()?;
276 conn.state = ConnectionState::Authenticating;
277
278 conn.handle_auth()?;
280
281 conn.read_startup_messages()?;
283
284 Ok(conn)
285 }
286
287 pub fn state(&self) -> ConnectionState {
289 self.state
290 }
291
292 pub fn is_ready(&self) -> bool {
294 matches!(self.state, ConnectionState::Ready(_))
295 }
296
297 pub fn process_id(&self) -> i32 {
299 self.process_id
300 }
301
302 pub fn secret_key(&self) -> i32 {
304 self.secret_key
305 }
306
307 pub fn parameter(&self, name: &str) -> Option<&str> {
309 self.parameters.get(name).map(|s| s.as_str())
310 }
311
312 pub fn parameters(&self) -> &HashMap<String, String> {
314 &self.parameters
315 }
316
317 #[allow(clippy::result_large_err)]
319 pub fn close(&mut self) -> Result<(), Error> {
320 if matches!(
321 self.state,
322 ConnectionState::Closed | ConnectionState::Disconnected
323 ) {
324 return Ok(());
325 }
326
327 self.send_message(&FrontendMessage::Terminate)?;
329 self.state = ConnectionState::Closed;
330 Ok(())
331 }
332
333 #[allow(clippy::result_large_err)]
336 #[cfg(feature = "tls")]
337 fn negotiate_ssl(&mut self) -> Result<(), Error> {
338 self.send_message(&FrontendMessage::SSLRequest)?;
340
341 let mut buf = [0u8; 1];
343 self.stream.read_exact(&mut buf).map_err(|e| {
344 Error::Connection(ConnectionError {
345 kind: ConnectionErrorKind::Ssl,
346 message: format!("Failed to read SSL response: {}", e),
347 source: Some(Box::new(e)),
348 })
349 })?;
350
351 match buf[0] {
352 b'S' => {
353 #[cfg(feature = "tls")]
355 {
356 let plain = match std::mem::replace(&mut self.stream, PgStream::Closed) {
357 PgStream::Plain(s) => s,
358 other => {
359 self.stream = other;
360 return Err(Error::Connection(ConnectionError {
361 kind: ConnectionErrorKind::Ssl,
362 message: "TLS upgrade requires a plain TCP stream".to_string(),
363 source: None,
364 }));
365 }
366 };
367
368 let config = tls::build_client_config(self.config.ssl_mode)?;
369 let server_name = tls::server_name(&self.config.host)?;
370 let conn =
371 rustls::ClientConnection::new(std::sync::Arc::new(config), server_name)
372 .map_err(|e| {
373 Error::Connection(ConnectionError {
374 kind: ConnectionErrorKind::Ssl,
375 message: format!("Failed to create TLS connection: {e}"),
376 source: None,
377 })
378 })?;
379
380 let mut tls_stream = rustls::StreamOwned::new(conn, plain);
381 while tls_stream.conn.is_handshaking() {
382 tls_stream
383 .conn
384 .complete_io(&mut tls_stream.sock)
385 .map_err(|e| {
386 Error::Connection(ConnectionError {
387 kind: ConnectionErrorKind::Ssl,
388 message: format!("TLS handshake failed: {e}"),
389 source: Some(Box::new(e)),
390 })
391 })?;
392 }
393
394 self.stream = PgStream::Tls(tls_stream);
395 Ok(())
396 }
397
398 #[cfg(not(feature = "tls"))]
399 {
400 Err(Error::Connection(ConnectionError {
401 kind: ConnectionErrorKind::Ssl,
402 message:
403 "TLS requested but 'sqlmodel-postgres' was built without feature 'tls'"
404 .to_string(),
405 source: None,
406 }))
407 }
408 }
409 b'N' => {
410 if self.config.ssl_mode.is_required() {
412 return Err(Error::Connection(ConnectionError {
413 kind: ConnectionErrorKind::Ssl,
414 message: "Server does not support SSL".to_string(),
415 source: None,
416 }));
417 }
418 Ok(())
420 }
421 _ => Err(Error::Connection(ConnectionError {
422 kind: ConnectionErrorKind::Ssl,
423 message: format!("Unexpected SSL response: 0x{:02x}", buf[0]),
424 source: None,
425 })),
426 }
427 }
428
429 #[allow(clippy::result_large_err)]
432 fn send_startup(&mut self) -> Result<(), Error> {
433 let params = self.config.startup_params();
434 let msg = FrontendMessage::Startup {
435 version: PROTOCOL_VERSION,
436 params,
437 };
438 self.send_message(&msg)
439 }
440
441 #[allow(clippy::result_large_err)]
444 fn require_auth_value(&self, message: &'static str) -> Result<&str, Error> {
445 self.config
447 .password
448 .as_deref()
449 .ok_or_else(|| auth_error(message))
450 }
451
452 #[allow(clippy::result_large_err)]
453 fn handle_auth(&mut self) -> Result<(), Error> {
454 loop {
455 let msg = self.receive_message()?;
456
457 match msg {
458 BackendMessage::AuthenticationOk => {
459 return Ok(());
460 }
461 BackendMessage::AuthenticationCleartextPassword => {
462 let auth_value =
463 self.require_auth_value("Authentication value required but not provided")?;
464 self.send_message(&FrontendMessage::PasswordMessage(auth_value.to_string()))?;
465 }
466 BackendMessage::AuthenticationMD5Password(salt) => {
467 let auth_value =
468 self.require_auth_value("Authentication value required but not provided")?;
469 let hash = md5_password(&self.config.user, auth_value, salt);
470 self.send_message(&FrontendMessage::PasswordMessage(hash))?;
471 }
472 BackendMessage::AuthenticationSASL(mechanisms) => {
473 if mechanisms.contains(&"SCRAM-SHA-256".to_string()) {
474 self.scram_auth()?;
475 } else {
476 return Err(auth_error(format!(
477 "Unsupported SASL mechanisms: {:?}",
478 mechanisms
479 )));
480 }
481 }
482 BackendMessage::ErrorResponse(e) => {
483 self.state = ConnectionState::Error;
484 return Err(error_from_fields(&e));
485 }
486 _ => {
487 return Err(Error::Protocol(ProtocolError {
488 message: format!("Unexpected message during auth: {:?}", msg),
489 raw_data: None,
490 source: None,
491 }));
492 }
493 }
494 }
495 }
496
497 #[allow(clippy::result_large_err)]
498 fn scram_auth(&mut self) -> Result<(), Error> {
499 let auth_value =
500 self.require_auth_value("Authentication value required for SCRAM-SHA-256")?;
501
502 let mut client = ScramClient::new(&self.config.user, auth_value);
503
504 let client_first = client.client_first();
506 self.send_message(&FrontendMessage::SASLInitialResponse {
507 mechanism: "SCRAM-SHA-256".to_string(),
508 data: client_first,
509 })?;
510
511 let msg = self.receive_message()?;
513 let server_first_data = match msg {
514 BackendMessage::AuthenticationSASLContinue(data) => data,
515 BackendMessage::ErrorResponse(e) => {
516 self.state = ConnectionState::Error;
517 return Err(error_from_fields(&e));
518 }
519 _ => {
520 return Err(Error::Protocol(ProtocolError {
521 message: format!("Expected SASL continue, got: {:?}", msg),
522 raw_data: None,
523 source: None,
524 }));
525 }
526 };
527
528 let client_final = client.process_server_first(&server_first_data)?;
530 self.send_message(&FrontendMessage::SASLResponse(client_final))?;
531
532 let msg = self.receive_message()?;
534 let server_final_data = match msg {
535 BackendMessage::AuthenticationSASLFinal(data) => data,
536 BackendMessage::ErrorResponse(e) => {
537 self.state = ConnectionState::Error;
538 return Err(error_from_fields(&e));
539 }
540 _ => {
541 return Err(Error::Protocol(ProtocolError {
542 message: format!("Expected SASL final, got: {:?}", msg),
543 raw_data: None,
544 source: None,
545 }));
546 }
547 };
548
549 client.verify_server_final(&server_final_data)?;
551
552 let msg = self.receive_message()?;
554 match msg {
555 BackendMessage::AuthenticationOk => Ok(()),
556 BackendMessage::ErrorResponse(e) => {
557 self.state = ConnectionState::Error;
558 Err(error_from_fields(&e))
559 }
560 _ => Err(Error::Protocol(ProtocolError {
561 message: format!("Expected AuthenticationOk, got: {:?}", msg),
562 raw_data: None,
563 source: None,
564 })),
565 }
566 }
567
568 #[allow(clippy::result_large_err)]
571 fn read_startup_messages(&mut self) -> Result<(), Error> {
572 loop {
573 let msg = self.receive_message()?;
574
575 match msg {
576 BackendMessage::BackendKeyData {
577 process_id,
578 secret_key,
579 } => {
580 self.process_id = process_id;
581 self.secret_key = secret_key;
582 }
583 BackendMessage::ParameterStatus { name, value } => {
584 self.parameters.insert(name, value);
585 }
586 BackendMessage::ReadyForQuery(status) => {
587 self.state = ConnectionState::Ready(status.into());
588 return Ok(());
589 }
590 BackendMessage::ErrorResponse(e) => {
591 self.state = ConnectionState::Error;
592 return Err(error_from_fields(&e));
593 }
594 BackendMessage::NoticeResponse(_notice) => {
595 }
597 _ => {
598 return Err(Error::Protocol(ProtocolError {
599 message: format!("Unexpected startup message: {:?}", msg),
600 raw_data: None,
601 source: None,
602 }));
603 }
604 }
605 }
606 }
607
608 #[allow(clippy::result_large_err)]
611 fn send_message(&mut self, msg: &FrontendMessage) -> Result<(), Error> {
612 let data = self.writer.write(msg);
613 self.stream.write_all(data).map_err(|e| {
614 self.state = ConnectionState::Error;
615 Error::Io(e)
616 })?;
617 self.stream.flush().map_err(|e| {
618 self.state = ConnectionState::Error;
619 Error::Io(e)
620 })?;
621 Ok(())
622 }
623
624 #[allow(clippy::result_large_err)]
625 fn receive_message(&mut self) -> Result<BackendMessage, Error> {
626 loop {
628 match self.reader.next_message() {
629 Ok(Some(msg)) => return Ok(msg),
630 Ok(None) => {
631 let n = self.stream.read(&mut self.read_buf).map_err(|e| {
633 if e.kind() == std::io::ErrorKind::TimedOut
634 || e.kind() == std::io::ErrorKind::WouldBlock
635 {
636 Error::Timeout
637 } else {
638 self.state = ConnectionState::Error;
639 Error::Connection(ConnectionError {
640 kind: ConnectionErrorKind::Disconnected,
641 message: format!("Failed to read from server: {}", e),
642 source: Some(Box::new(e)),
643 })
644 }
645 })?;
646
647 if n == 0 {
648 self.state = ConnectionState::Disconnected;
649 return Err(Error::Connection(ConnectionError {
650 kind: ConnectionErrorKind::Disconnected,
651 message: "Connection closed by server".to_string(),
652 source: None,
653 }));
654 }
655
656 self.reader.feed(&self.read_buf[..n]).map_err(|e| {
658 Error::Protocol(ProtocolError {
659 message: format!("Protocol error: {}", e),
660 raw_data: None,
661 source: None,
662 })
663 })?;
664 }
665 Err(e) => {
666 self.state = ConnectionState::Error;
667 return Err(Error::Protocol(ProtocolError {
668 message: format!("Protocol error: {}", e),
669 raw_data: None,
670 source: None,
671 }));
672 }
673 }
674 }
675 }
676}
677
678impl Drop for PgConnection {
679 fn drop(&mut self) {
680 let _ = self.close();
682 }
683}
684
685#[cfg(feature = "console")]
688impl ConsoleAware for PgConnection {
689 fn set_console(&mut self, console: Option<Arc<SqlModelConsole>>) {
690 self.console = console;
691 }
692
693 fn console(&self) -> Option<&Arc<SqlModelConsole>> {
694 self.console.as_ref()
695 }
696
697 fn has_console(&self) -> bool {
698 self.console.is_some()
699 }
700}
701
702#[cfg(feature = "console")]
704#[derive(Debug, Clone, Copy, PartialEq, Eq)]
705pub enum ConnectionStage {
706 DnsResolve,
708 TcpConnect,
710 SslNegotiate,
712 SslEstablished,
714 Startup,
716 Authenticating,
718 Authenticated,
720 Ready,
722}
723
724#[cfg(feature = "console")]
725impl ConnectionStage {
726 #[must_use]
728 pub fn description(&self) -> &'static str {
729 match self {
730 Self::DnsResolve => "Resolving DNS",
731 Self::TcpConnect => "Connecting (TCP)",
732 Self::SslNegotiate => "Negotiating SSL",
733 Self::SslEstablished => "SSL established",
734 Self::Startup => "Sending startup",
735 Self::Authenticating => "Authenticating",
736 Self::Authenticated => "Authenticated",
737 Self::Ready => "Ready",
738 }
739 }
740}
741
742#[cfg(feature = "console")]
743impl PgConnection {
744 pub fn emit_progress(&self, stage: ConnectionStage, success: bool) {
748 if let Some(console) = &self.console {
749 let status = if success { "[OK]" } else { "[..] " };
750 let message = format!("{} {}", status, stage.description());
751 console.info(&message);
752 }
753 }
754
755 pub fn emit_connected(&self) {
757 if let Some(console) = &self.console {
758 let server_version = self
759 .parameters
760 .get("server_version")
761 .map_or("unknown", |s| s.as_str());
762 let message = format!(
763 "Connected to PostgreSQL {} at {}:{}",
764 server_version, self.config.host, self.config.port
765 );
766 console.success(&message);
767 }
768 }
769
770 pub fn emit_connected_plain(&self) -> String {
772 let server_version = self
773 .parameters
774 .get("server_version")
775 .map_or("unknown", |s| s.as_str());
776 format!(
777 "Connected to PostgreSQL {} at {}:{}",
778 server_version, self.config.host, self.config.port
779 )
780 }
781}
782
783fn md5_password(user: &str, password: &str, salt: [u8; 4]) -> String {
787 use std::fmt::Write;
788
789 let inner = format!("{}{}", password, user);
791 let inner_hash = md5::compute(inner.as_bytes());
792
793 let mut outer_input = format!("{:x}", inner_hash).into_bytes();
794 outer_input.extend_from_slice(&salt);
795 let outer_hash = md5::compute(&outer_input);
796
797 let mut result = String::with_capacity(35);
798 result.push_str("md5");
799 write!(&mut result, "{:x}", outer_hash).unwrap();
800 result
801}
802
803fn auth_error(msg: impl Into<String>) -> Error {
804 Error::Connection(ConnectionError {
805 kind: ConnectionErrorKind::Authentication,
806 message: msg.into(),
807 source: None,
808 })
809}
810
811fn error_from_fields(fields: &ErrorFields) -> Error {
812 let kind = match fields.code.get(..2) {
814 Some("08") => {
815 return Error::Connection(ConnectionError {
817 kind: ConnectionErrorKind::Connect,
818 message: fields.message.clone(),
819 source: None,
820 });
821 }
822 Some("28") => {
823 return Error::Connection(ConnectionError {
825 kind: ConnectionErrorKind::Authentication,
826 message: fields.message.clone(),
827 source: None,
828 });
829 }
830 Some("42") => QueryErrorKind::Syntax, Some("23") => QueryErrorKind::Constraint, Some("40") => {
833 if fields.code == "40001" {
834 QueryErrorKind::Serialization
835 } else {
836 QueryErrorKind::Deadlock
837 }
838 }
839 Some("57") => {
840 if fields.code == "57014" {
841 QueryErrorKind::Cancelled
842 } else {
843 QueryErrorKind::Timeout
844 }
845 }
846 _ => QueryErrorKind::Database,
847 };
848
849 Error::Query(QueryError {
850 kind,
851 sql: None,
852 sqlstate: Some(fields.code.clone()),
853 message: fields.message.clone(),
854 detail: fields.detail.clone(),
855 hint: fields.hint.clone(),
856 position: fields.position.map(|p| p as usize),
857 source: None,
858 })
859}
860
861#[cfg(test)]
862mod tests {
863 use super::*;
864
865 #[test]
866 fn test_md5_password() {
867 let hash = md5_password("postgres", "mysecretpassword", *b"abcd");
869 assert!(hash.starts_with("md5"));
870 assert_eq!(hash.len(), 35); }
872
873 #[test]
874 fn test_transaction_status_conversion() {
875 assert_eq!(
876 TransactionStatusState::from(TransactionStatus::Idle),
877 TransactionStatusState::Idle
878 );
879 assert_eq!(
880 TransactionStatusState::from(TransactionStatus::Transaction),
881 TransactionStatusState::InTransaction
882 );
883 assert_eq!(
884 TransactionStatusState::from(TransactionStatus::Error),
885 TransactionStatusState::InFailed
886 );
887 }
888
889 #[test]
890 fn test_error_classification() {
891 let fields = ErrorFields {
892 severity: "ERROR".to_string(),
893 code: "23505".to_string(),
894 message: "unique violation".to_string(),
895 ..Default::default()
896 };
897 let err = error_from_fields(&fields);
898 assert!(matches!(err, Error::Query(q) if q.kind == QueryErrorKind::Constraint));
899
900 let fields = ErrorFields {
901 severity: "FATAL".to_string(),
902 code: "28P01".to_string(),
903 message: "password authentication failed".to_string(),
904 ..Default::default()
905 };
906 let err = error_from_fields(&fields);
907 assert!(matches!(
908 err,
909 Error::Connection(c) if c.kind == ConnectionErrorKind::Authentication
910 ));
911 }
912}