Skip to main content

connectorx/sources/postgres/
connection.rs

1use crate::sources::postgres::errors::PostgresSourceError;
2use crate::utils::remove_query_params;
3use openssl::ssl::{SslConnector, SslFiletype, SslMethod, SslVerifyMode};
4use postgres::{config::SslMode, Config};
5use postgres_openssl::MakeTlsConnector;
6use std::collections::HashMap;
7use std::convert::TryFrom;
8use std::path::PathBuf;
9use url::Url;
10
11#[derive(Clone, Debug)]
12pub struct TlsConfig {
13    /// Postgres config, pg_config.sslmode (`sslmode`).
14    pub pg_config: Config,
15    /// Location of the client cert and key (`sslcert`, `sslkey`).
16    pub client_cert: Option<(PathBuf, PathBuf)>,
17    /// Location of the root certificate (`sslrootcert`).
18    pub root_cert: Option<PathBuf>,
19}
20
21impl TryFrom<TlsConfig> for MakeTlsConnector {
22    type Error = PostgresSourceError;
23    // The logic of this function adapted primarily from:
24    // https://github.com/sfackler/rust-postgres/pull/774
25    // We only support server side authentication (`sslrootcert`) for now
26    fn try_from(tls_config: TlsConfig) -> Result<Self, Self::Error> {
27        let mut builder = SslConnector::builder(SslMethod::tls_client())?;
28        let ssl_mode = tls_config.pg_config.get_ssl_mode();
29        let (verify_ca, verify_hostname) = match ssl_mode {
30            SslMode::Disable | SslMode::Prefer => (false, false),
31            SslMode::Require => match tls_config.root_cert {
32                // If a root CA file exists, the behavior of sslmode=require will be the same as
33                // that of verify-ca, meaning the server certificate is validated against the CA.
34                //
35                // For more details, check out the note about backwards compatibility in
36                // https://postgresql.org/docs/current/libpq-ssl.html#LIBQ-SSL-CERTIFICATES.
37                Some(_) => (true, false),
38                None => (false, false),
39            },
40            // These two modes will not work until upstream rust-postgres supports parsing
41            // them as part of the TLS config.
42            //
43            // SslMode::VerifyCa => (true, false),
44            // SslMode::VerifyFull => (true, true),
45            _ => panic!("unexpected sslmode {:?}", ssl_mode),
46        };
47
48        if let Some((cert, key)) = tls_config.client_cert {
49            builder.set_certificate_file(cert, SslFiletype::PEM)?;
50            builder.set_private_key_file(key, SslFiletype::PEM)?;
51        }
52
53        if let Some(root_cert) = tls_config.root_cert {
54            builder.set_ca_file(root_cert)?;
55        }
56
57        if !verify_ca {
58            builder.set_verify(SslVerifyMode::NONE); // do not verify CA
59        }
60
61        let mut tls_connector = MakeTlsConnector::new(builder.build());
62
63        if !verify_hostname {
64            tls_connector.set_callback(|connect, _| {
65                connect.set_verify_hostname(false);
66                Ok(())
67            });
68        }
69
70        Ok(tls_connector)
71    }
72}
73
74// Strip URL params not accepted by upstream rust-postgres
75fn strip_bad_opts(url: &Url) -> Url {
76    remove_query_params(url, &["sslkey", "sslcert", "sslrootcert"])
77}
78
79pub fn rewrite_tls_args(
80    conn: &Url,
81) -> Result<(Config, Option<MakeTlsConnector>), PostgresSourceError> {
82    // We parse the config, then strip unsupported SSL opts and rewrite the URI
83    // before calling conn.parse().
84    //
85    // For more details on this approach, see the conversation here:
86    // https://github.com/sfackler/rust-postgres/pull/774#discussion_r641784774
87
88    let params: HashMap<String, String> = conn.query_pairs().into_owned().collect();
89
90    let sslcert = params.get("sslcert").map(PathBuf::from);
91    let sslkey = params.get("sslkey").map(PathBuf::from);
92    let root_cert = params.get("sslrootcert").map(PathBuf::from);
93    let client_cert = match (sslcert, sslkey) {
94        (Some(a), Some(b)) => Some((a, b)),
95        _ => None,
96    };
97
98    let stripped_url = strip_bad_opts(conn);
99    let pg_config: Config = stripped_url.as_str().parse()?;
100
101    let tls_config = TlsConfig {
102        pg_config: pg_config.clone(),
103        client_cert,
104        root_cert,
105    };
106
107    let tls_connector = match pg_config.get_ssl_mode() {
108        SslMode::Disable => None,
109        _ => Some(MakeTlsConnector::try_from(tls_config)?),
110    };
111
112    Ok((pg_config, tls_connector))
113}