tokio-postgres-rustls icon indicating copy to clipboard operation
tokio-postgres-rustls copied to clipboard

Example to replace tokio_postgres_rustls || postgres_native_tls for this library

Open snspinn opened this issue 11 months ago • 4 comments

Both tokio_postgres_rustls and postgres_native_tls are interchangeable. When I try to swap this library, however, the returned types don't match up. A more complete example would be great.

Happy to submit a PR, if I can get some guidance and if this repo is still being maintained.

snspinn avatar May 22 '25 09:05 snspinn

For example, if I try to swap this library in, I get an issue where I would need to return a private struct.

error[E0308]: mismatched types
  --> services/lynxfleetcloud-iotpass-pairing/src/db/mod.rs:69:28
   |
69 |         return Ok((client, connection));
   |                            ^^^^^^^^^^ expected `Connection<Socket, MakeRustlsConnect>`, found `Connection<Socket, RustlsStream<...>>`
   |
   = note: expected struct `tokio_postgres::Connection<_, MakeRustlsConnect>`
              found struct `tokio_postgres::Connection<_, tokio_postgres_rustls::private::RustlsStream<Socket>>`

snspinn avatar May 22 '25 09:05 snspinn

There is a maintained fork with a comprehensive test suite, fixes for SASL/SCRAM channel binding, and unsafe code removed that would welcome any contributions: https://github.com/khorsolutions/tokio-postgres-rustls-improved

A full usage example is:

src/main.rs

use rustls::pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject};
use tokio_postgres::config::{Config, SslMode};
use tokio_postgres_rustls_improved::MakeRustlsConnect;

#[tokio::main]
async fn main() {
    let mut roots = rustls::RootCertStore::empty();
    roots
        .add(CertificateDer::from_pem_file("ca.crt").expect("load ca"))
        .unwrap();

    let tls_config = rustls::ClientConfig::builder()
        .with_root_certificates(roots)
        .with_client_auth_cert(
            vec![CertificateDer::from_pem_file("client.crt").unwrap()],
            PrivateKeyDer::from_pem_file("client.key").unwrap(),
        )
        .unwrap();

    let tls = MakeRustlsConnect::new(tls_config);

    let mut pg_config = Config::new();
    pg_config
        .host("localhost")
        .port(5432)
        .dbname("postgres")
        .user("ssl_user")
        .ssl_mode(SslMode::Require);
    let (client, conn) = pg_config.connect(tls).await.expect("connect");
    tokio::spawn(async move { conn.await.map_err(|e| panic!("{:?}", e)) });

    let stmt = client.prepare("SELECT 1::INT4").await.expect("prepare");
    let rows = client.query(&stmt, &[]).await.expect("query");
    assert_eq!(1, rows.len());
    let res: i32 = (&rows[0]).get(0);
    assert_eq!(1, res);
    println!("SELECT 1 = {}", res)
}

Cargo.toml

[package]
name = "tokio-postgres-rustls-improved-example"
version = "0.1.0"
edition = "2024"

[dependencies]
tokio-postgres = "0.7"
tokio-postgres-rustls-improved = "0.15"
rustls = { version = "0.23" }
tokio = { version = "1", features = ["full"] }

dsykes16 avatar Sep 02 '25 17:09 dsykes16

@snspinn, based on your error, I think you're trying to abstract out the actual connection. You'd want to keep that generic, like this:

async fn connect<T>(
    pg_config: Config,
    tls: T,
) -> Result<(Client, Connection<Socket, T::Stream>), Error>
where
    T: MakeTlsConnect<Socket>,
{
    pg_config.connect(tls).await
}

Full example again (compiles and runs):

use rustls::pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject};
use tokio_postgres::config::{Config, SslMode};
use tokio_postgres::{Client, Connection, Error, Socket, tls::MakeTlsConnect};
use tokio_postgres_rustls_improved::MakeRustlsConnect;

async fn connect<T>(
    pg_config: Config,
    tls: T,
) -> Result<(Client, Connection<Socket, T::Stream>), Error>
where
    T: MakeTlsConnect<Socket>,
{
    pg_config.connect(tls).await
}

#[tokio::main]
async fn main() {
    let mut roots = rustls::RootCertStore::empty();
    roots
        .add(CertificateDer::from_pem_file("ca.crt").expect("load ca"))
        .unwrap();

    let tls_config = rustls::ClientConfig::builder()
        .with_root_certificates(roots)
        .with_client_auth_cert(
            vec![CertificateDer::from_pem_file("client.crt").unwrap()],
            PrivateKeyDer::from_pem_file("client.key").unwrap(),
        )
        .unwrap();

    let tls = MakeRustlsConnect::new(tls_config);

    let mut pg_config = Config::new();
    pg_config
        .host("localhost")
        .port(32799)
        .dbname("postgres")
        .user("ssl_user")
        .ssl_mode(SslMode::Require);
    let (client, conn) = connect(pg_config, tls).await.unwrap();
    tokio::spawn(async move { conn.await.map_err(|e| panic!("{:?}", e)) });

    let stmt = client.prepare("SELECT 1::INT4").await.expect("prepare");
    let rows = client.query(&stmt, &[]).await.expect("query");
    assert_eq!(1, rows.len());
    let res: i32 = (&rows[0]).get(0);
    assert_eq!(1, res);
    println!("SELECT 1 = {}", res)
}

dsykes16 avatar Sep 02 '25 18:09 dsykes16

@snspinn, if you want to support either backend with feature flags, a crude (but working) example is:

src/main.rs

use tokio_postgres::config::{Config, SslMode};
use tokio_postgres::{Client, Connection, Error, Socket, tls::MakeTlsConnect};

#[cfg(feature = "native-tls")]
use native_tls::{Certificate, Identity, TlsConnector};
#[cfg(feature = "native-tls")]
use postgres_native_tls::MakeTlsConnector;
#[cfg(feature = "native-tls")]
use std::fs;

#[cfg(feature = "rustls")]
use rustls::pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject};
#[cfg(feature = "rustls")]
use tokio_postgres_rustls_improved::MakeRustlsConnect;

#[cfg(all(feature = "native-tls", feature = "rustls"))]
compile_error!("feature \"native-tls\" and \"rustls\" cannot be enabled at the same time");

#[cfg(feature = "native-tls")]
async fn build_tls_connector() -> MakeTlsConnector {
    let ca = fs::read("ca.crt").unwrap();
    let ca = Certificate::from_pem(&ca).unwrap();

    let cert = fs::read("client.crt").unwrap();
    let key = fs::read("client.key").unwrap();
    let identity = Identity::from_pkcs8(&cert, &key).unwrap();

    let connector = TlsConnector::builder()
        .add_root_certificate(ca)
        .identity(identity)
        .build()
        .unwrap();
    MakeTlsConnector::new(connector)
}

#[cfg(feature = "rustls")]
async fn build_tls_connector() -> MakeRustlsConnect {
    let mut roots = rustls::RootCertStore::empty();
    roots
        .add(CertificateDer::from_pem_file("ca.crt").expect("load ca"))
        .unwrap();

    let tls_config = rustls::ClientConfig::builder()
        .with_root_certificates(roots)
        .with_client_auth_cert(
            vec![CertificateDer::from_pem_file("client.crt").unwrap()],
            PrivateKeyDer::from_pem_file("client.key").unwrap(),
        )
        .unwrap();

    MakeRustlsConnect::new(tls_config)
}

async fn connect<T>(
    pg_config: Config,
    tls: T,
) -> Result<(Client, Connection<Socket, T::Stream>), Error>
where
    T: MakeTlsConnect<Socket>,
{
    pg_config.connect(tls).await
}

#[tokio::main]
async fn main() {
    let mut pg_config = Config::new();
    pg_config
        .host("127.0.0.1")
        .port(32827)
        .dbname("postgres")
        .user("ssl_user")
        .ssl_mode(SslMode::Require);

    let tls = build_tls_connector().await;
    let (client, conn) = connect(pg_config, tls).await.unwrap();

    tokio::spawn(async move { conn.await.map_err(|e| panic!("{:?}", e)) });

    let stmt = client.prepare("SELECT 1::INT4").await.expect("prepare");
    let rows = client.query(&stmt, &[]).await.expect("query");
    assert_eq!(1, rows.len());
    let res: i32 = (&rows[0]).get(0);
    assert_eq!(1, res);
    println!("SELECT 1 = {}", res)
}

Cargo.toml

[package]
name = "tokio-postgres-rustls-improved-example"
version = "0.1.0"
edition = "2024"

[features]
default = ["rustls"]
rustls = ["dep:tokio-postgres-rustls-improved", "dep:rustls"]
native-tls = ["dep:postgres-native-tls", "dep:native-tls"]

[dependencies]
tokio-postgres = "0.7"
postgres-native-tls = { version = "0.5.1", optional = true }
tokio-postgres-rustls-improved = { version = "0.15", optional = true }
rustls = { version = "0.23", optional = true }
native-tls = { version = "0.2.14", optional = true }
tokio = { version = "1", features = ["full"] }

The key is making use of traits, not binding to concrete types. The connect function isn't necessary there, it's just included to show the trait bound return type on a function like that.

dsykes16 avatar Sep 02 '25 20:09 dsykes16