Skip to content

Commit 9b94ca8

Browse files
author
Daniel Frankcom
committed
postgres: Allow configuration of SSL negotiation procedure
1 parent bab1b02 commit 9b94ca8

File tree

6 files changed

+152
-25
lines changed

6 files changed

+152
-25
lines changed

sqlx-postgres/src/connection/tls.rs

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use crate::net::tls::{self, TlsConfig};
33
use crate::net::{Socket, SocketIntoBox, WithSocket};
44

55
use crate::message::SslRequest;
6-
use crate::{PgConnectOptions, PgSslMode};
6+
use crate::{PgConnectOptions, PgSslMode, PgSslNegotiation};
77

88
pub struct MaybeUpgradeTls<'a>(pub &'a PgConnectOptions);
99

@@ -19,28 +19,52 @@ async fn maybe_upgrade<S: Socket>(
1919
mut socket: S,
2020
options: &PgConnectOptions,
2121
) -> Result<Box<dyn Socket>, Error> {
22-
// https://www.postgresql.org/docs/12/libpq-ssl.html#LIBPQ-SSL-SSLMODE-STATEMENTS
23-
match options.ssl_mode {
24-
// FIXME: Implement ALLOW
25-
PgSslMode::Allow | PgSslMode::Disable => return Ok(Box::new(socket)),
26-
27-
PgSslMode::Prefer => {
28-
if !tls::available() {
29-
return Ok(Box::new(socket));
30-
}
31-
32-
// try upgrade, but its okay if we fail
33-
if !request_upgrade(&mut socket, options).await? {
34-
return Ok(Box::new(socket));
22+
// https://www.postgresql.org/docs/17/libpq-connect.html#LIBPQ-CONNECT-SSLNEGOTIATION
23+
match options.ssl_negotiation {
24+
PgSslNegotiation::Postgres => {
25+
// https://www.postgresql.org/docs/12/libpq-ssl.html#LIBPQ-SSL-SSLMODE-STATEMENTS
26+
match options.ssl_mode {
27+
// FIXME: Implement ALLOW
28+
PgSslMode::Allow | PgSslMode::Disable => return Ok(Box::new(socket)),
29+
30+
PgSslMode::Prefer => {
31+
if !tls::available() {
32+
return Ok(Box::new(socket));
33+
}
34+
35+
// try upgrade, but its okay if we fail
36+
if !request_upgrade(&mut socket, options).await? {
37+
return Ok(Box::new(socket));
38+
}
39+
}
40+
41+
PgSslMode::Require | PgSslMode::VerifyFull | PgSslMode::VerifyCa => {
42+
tls::error_if_unavailable()?;
43+
44+
if !request_upgrade(&mut socket, options).await? {
45+
// upgrade failed, die
46+
return Err(Error::Tls("server does not support TLS".into()));
47+
}
48+
}
3549
}
3650
}
37-
38-
PgSslMode::Require | PgSslMode::VerifyFull | PgSslMode::VerifyCa => {
39-
tls::error_if_unavailable()?;
40-
41-
if !request_upgrade(&mut socket, options).await? {
42-
// upgrade failed, die
43-
return Err(Error::Tls("server does not support TLS".into()));
51+
PgSslNegotiation::Direct => {
52+
// Direct TLS negotiation without PostgreSQL handshake
53+
match options.ssl_mode {
54+
PgSslMode::Disable | PgSslMode::Allow | PgSslMode::Prefer => {
55+
return Err(Error::Tls(
56+
format!(
57+
"SSL mode {:?} is incompatible with direct SSL negotiation",
58+
options.ssl_mode
59+
)
60+
.into(),
61+
));
62+
}
63+
64+
PgSslMode::Require | PgSslMode::VerifyFull | PgSslMode::VerifyCa => {
65+
tls::error_if_unavailable()?;
66+
// No need to request an upgrade. We go straight to TLS handshake.
67+
}
4468
}
4569
}
4670
}

sqlx-postgres/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ pub use database::Postgres;
5454
pub use error::{PgDatabaseError, PgErrorPosition};
5555
pub use listener::{PgListener, PgNotification};
5656
pub use message::PgSeverity;
57-
pub use options::{PgConnectOptions, PgSslMode};
57+
pub use options::{PgConnectOptions, PgSslMode, PgSslNegotiation};
5858
pub use query_result::PgQueryResult;
5959
pub use row::PgRow;
6060
pub use statement::PgStatement;

sqlx-postgres/src/options/doc.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ if a parameter is not passed in via URL, it is populated by reading
3333
| `port` | `PGPORT` | `5432` |
3434
| `dbname` | `PGDATABASE` | Unset; defaults to the username server-side. |
3535
| `sslmode` | `PGSSLMODE` | `prefer`. See [`PgSslMode`] for details. |
36+
| `sslnegotiation` | `PGSSLNEGOTIATION` | `postgres`. See [`PgSslNegotiation`] for details. |
3637
| `sslrootcert` | `PGSSLROOTCERT` | Unset. See [Note: SSL](#note-ssl). |
3738
| `sslcert` | `PGSSLCERT` | Unset. See [Note: SSL](#note-ssl). |
3839
| `sslkey` | `PGSSLKEY` | Unset. See [Note: SSL](#note-ssl). |

sqlx-postgres/src/options/mod.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ use std::fmt::{Display, Write};
44
use std::path::{Path, PathBuf};
55

66
pub use ssl_mode::PgSslMode;
7+
pub use ssl_negotiation::PgSslNegotiation;
78

89
use crate::{connection::LogSettings, net::tls::CertificateInput};
910

1011
mod connect;
1112
mod parse;
1213
mod pgpass;
1314
mod ssl_mode;
15+
mod ssl_negotiation;
1416

1517
#[doc = include_str!("doc.md")]
1618
#[derive(Debug, Clone)]
@@ -22,6 +24,7 @@ pub struct PgConnectOptions {
2224
pub(crate) password: Option<String>,
2325
pub(crate) database: Option<String>,
2426
pub(crate) ssl_mode: PgSslMode,
27+
pub(crate) ssl_negotiation: PgSslNegotiation,
2528
pub(crate) ssl_root_cert: Option<CertificateInput>,
2629
pub(crate) ssl_client_cert: Option<CertificateInput>,
2730
pub(crate) ssl_client_key: Option<CertificateInput>,
@@ -85,6 +88,10 @@ impl PgConnectOptions {
8588
.ok()
8689
.and_then(|v| v.parse().ok())
8790
.unwrap_or_default(),
91+
ssl_negotiation: var("PGSSLNEGOTIATION")
92+
.ok()
93+
.and_then(|v| v.parse().ok())
94+
.unwrap_or_default(),
8895
statement_cache_capacity: 100,
8996
application_name: var("PGAPPNAME").ok(),
9097
extra_float_digits: Some("2".into()),
@@ -218,6 +225,26 @@ impl PgConnectOptions {
218225
self
219226
}
220227

228+
/// Sets the protocol with which the secure SSL TCP/IP connection will be negotiated with
229+
/// the server.
230+
///
231+
/// By default, the protocol is [`Postgres`](PgSslNegotiation::Postgres), and the client will
232+
/// first check whether the server supports SSL, and fallback to a non-SSL connection if not.
233+
///
234+
/// Ignored for Unix domain socket communication.
235+
///
236+
/// # Example
237+
///
238+
/// ```rust
239+
/// # use sqlx_postgres::{PgSslNegotiation, PgConnectOptions};
240+
/// let options = PgConnectOptions::new()
241+
/// .ssl_negotiation(PgSslNegotiation::Postgres);
242+
/// ```
243+
pub fn ssl_negotiation(mut self, procedure: PgSslNegotiation) -> Self {
244+
self.ssl_negotiation = procedure;
245+
self
246+
}
247+
221248
/// Sets the name of a file containing SSL certificate authority (CA) certificate(s).
222249
/// If the file exists, the server's certificate will be verified to be signed by
223250
/// one of these authorities.
@@ -542,6 +569,19 @@ impl PgConnectOptions {
542569
self.ssl_mode
543570
}
544571

572+
/// Get the SSL negotiation protocol.
573+
///
574+
/// # Example
575+
///
576+
/// ```rust
577+
/// # use sqlx_postgres::{PgConnectOptions, PgSslNegotiation};
578+
/// let options = PgConnectOptions::new();
579+
/// assert!(matches!(options.get_ssl_negotiation(), PgSslNegotiation::Postgres));
580+
/// ```
581+
pub fn get_ssl_negotiation(&self) -> PgSslNegotiation {
582+
self.ssl_negotiation
583+
}
584+
545585
/// Get the application name.
546586
///
547587
/// # Example

sqlx-postgres/src/options/parse.rs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::error::Error;
2-
use crate::{PgConnectOptions, PgSslMode};
2+
use crate::{PgConnectOptions, PgSslMode, PgSslNegotiation};
33
use sqlx_core::percent_encoding::{percent_decode_str, utf8_percent_encode, NON_ALPHANUMERIC};
44
use sqlx_core::Url;
55
use std::net::IpAddr;
@@ -53,6 +53,10 @@ impl PgConnectOptions {
5353
options = options.ssl_mode(value.parse().map_err(Error::config)?);
5454
}
5555

56+
"sslnegotiation" | "ssl-negotiation" => {
57+
options = options.ssl_negotiation(value.parse().map_err(Error::config)?);
58+
}
59+
5660
"sslrootcert" | "ssl-root-cert" | "ssl-ca" => {
5761
options = options.ssl_root_cert(&*value);
5862
}
@@ -146,6 +150,13 @@ impl PgConnectOptions {
146150
};
147151
url.query_pairs_mut().append_pair("sslmode", ssl_mode);
148152

153+
let ssl_negotiation = match self.ssl_negotiation {
154+
PgSslNegotiation::Postgres => "postgres",
155+
PgSslNegotiation::Direct => "direct",
156+
};
157+
url.query_pairs_mut()
158+
.append_pair("sslnegotiation", ssl_negotiation);
159+
149160
if let Some(ssl_root_cert) = &self.ssl_root_cert {
150161
url.query_pairs_mut()
151162
.append_pair("sslrootcert", &ssl_root_cert.to_string());
@@ -266,6 +277,22 @@ fn it_parses_password_with_non_ascii_chars_correctly() {
266277
assert_eq!(Some("p@ssw0rd".into()), opts.password);
267278
}
268279

280+
#[test]
281+
fn it_parses_sslmode_correctly_from_parameter() {
282+
let url = "postgres://?sslmode=verify-full";
283+
let opts = PgConnectOptions::from_str(url).unwrap();
284+
285+
assert!(matches!(opts.ssl_mode, PgSslMode::VerifyFull));
286+
}
287+
288+
#[test]
289+
fn it_parses_sslnegotiation_correctly_from_parameter() {
290+
let url = "postgres://?sslnegotiation=direct";
291+
let opts = PgConnectOptions::from_str(url).unwrap();
292+
293+
assert!(matches!(opts.ssl_negotiation, PgSslNegotiation::Direct));
294+
}
295+
269296
#[test]
270297
fn it_parses_socket_correctly_percent_encoded() {
271298
let url = "postgres://%2Fvar%2Flib%2Fpostgres/database";
@@ -310,7 +337,7 @@ fn it_returns_the_parsed_url_when_socket() {
310337

311338
let mut expected_url = Url::parse(url).unwrap();
312339
// PgConnectOptions defaults
313-
let query_string = "sslmode=prefer&statement-cache-capacity=100";
340+
let query_string = "sslmode=prefer&sslnegotiation=postgres&statement-cache-capacity=100";
314341
let port = 5432;
315342
expected_url.set_query(Some(query_string));
316343
let _ = expected_url.set_port(Some(port));
@@ -325,7 +352,7 @@ fn it_returns_the_parsed_url_when_host() {
325352

326353
let mut expected_url = Url::parse(url).unwrap();
327354
// PgConnectOptions defaults
328-
let query_string = "sslmode=prefer&statement-cache-capacity=100";
355+
let query_string = "sslmode=prefer&sslnegotiation=postgres&statement-cache-capacity=100";
329356
expected_url.set_query(Some(query_string));
330357

331358
assert_eq!(expected_url, opts.build_url());
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
use crate::error::Error;
2+
use std::str::FromStr;
3+
4+
/// Options for controlling the connection establishment procedure for PostgreSQL SSL connections.
5+
///
6+
/// It is used by the [`sslnegotiation`](super::PgConnectOptions::ssl_negotiation) method.
7+
#[derive(Debug, Clone, Copy, Default)]
8+
pub enum PgSslNegotiation {
9+
/// The client first asks the server if SSL is supported.
10+
///
11+
/// This is the default if no other mode is specified.
12+
#[default]
13+
Postgres,
14+
15+
/// The client starts the standard SSL handshake directly after establishing the TCP/IP
16+
/// connection.
17+
Direct,
18+
}
19+
20+
impl FromStr for PgSslNegotiation {
21+
type Err = Error;
22+
23+
fn from_str(s: &str) -> Result<Self, Error> {
24+
Ok(match &*s.to_ascii_lowercase() {
25+
"postgres" => PgSslNegotiation::Postgres,
26+
"direct" => PgSslNegotiation::Direct,
27+
28+
_ => {
29+
return Err(Error::Configuration(
30+
format!("unknown value {s:?} for `ssl_negotiation`").into(),
31+
));
32+
}
33+
})
34+
}
35+
}

0 commit comments

Comments
 (0)