diff --git a/.github/workflows/sqlx.yml b/.github/workflows/sqlx.yml index b2f81b75ad..5d523cbc64 100644 --- a/.github/workflows/sqlx.yml +++ b/.github/workflows/sqlx.yml @@ -263,7 +263,7 @@ jobs: --no-default-features --features any,postgres,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} env: - DATABASE_URL: postgres://postgres:password@localhost:5432/sqlx?sslmode=verify-ca&sslrootcert=.%2Ftests%2Fcerts%2Fca.crt + DATABASE_URL: postgres://postgres:password@sqlx.rs:5432/sqlx?hostaddr=127.0.0.1&sslmode=verify-full&sslrootcert=.%2Ftests%2Fcerts%2Fca.crt SQLX_OFFLINE_DIR: .sqlx RUSTFLAGS: -D warnings --cfg postgres="${{ matrix.postgres }}" @@ -322,7 +322,7 @@ jobs: --no-default-features --features any,postgres,macros,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} env: - DATABASE_URL: postgres://postgres@localhost:5432/sqlx?sslmode=verify-ca&sslrootcert=.%2Ftests%2Fcerts%2Fca.crt&sslkey=.%2Ftests%2Fcerts%2Fkeys%2Fclient.key&sslcert=.%2Ftests%2Fcerts%2Fclient.crt + DATABASE_URL: postgres://postgres@sqlx.rs:5432/sqlx?hostaddr=127.0.0.1&sslmode=verify-full&sslrootcert=.%2Ftests%2Fcerts%2Fca.crt&sslkey=.%2Ftests%2Fcerts%2Fkeys%2Fclient.key&sslcert=.%2Ftests%2Fcerts%2Fclient.crt RUSTFLAGS: -D warnings --cfg postgres="${{ matrix.postgres }}" mysql: diff --git a/sqlx-postgres/src/connection/stream.rs b/sqlx-postgres/src/connection/stream.rs index e8a1aedc47..035ebe238a 100644 --- a/sqlx-postgres/src/connection/stream.rs +++ b/sqlx-postgres/src/connection/stream.rs @@ -44,7 +44,14 @@ impl PgStream { pub(super) async fn connect(options: &PgConnectOptions) -> Result { let socket_result = match options.fetch_socket() { Some(ref path) => net::connect_uds(path, MaybeUpgradeTls(options)).await?, - None => net::connect_tcp(&options.host, options.port, MaybeUpgradeTls(options)).await?, + None => { + net::connect_tcp( + options.host_addr.as_ref().unwrap_or(&options.host), + options.port, + MaybeUpgradeTls(options), + ) + .await? + } }; let socket = socket_result?; diff --git a/sqlx-postgres/src/options/doc.md b/sqlx-postgres/src/options/doc.md index 33dd63b7a8..2c68e71f65 100644 --- a/sqlx-postgres/src/options/doc.md +++ b/sqlx-postgres/src/options/doc.md @@ -29,7 +29,7 @@ if a parameter is not passed in via URL, it is populated by reading | `password` | `PGPASSWORD` | Read from [`passfile`], if it exists. | | [`passfile`] | `PGPASSFILE` | `~/.pgpass` or `%APPDATA%\postgresql\pgpass.conf` (Windows) | | `host` | `PGHOST` | See [Note: Default Host](#note-default-host). | -| `hostaddr` | `PGHOSTADDR` | See [Note: Default Host](#note-default-host). | +| `hostaddr` | `PGHOSTADDR` | Numeric IP address, allows to overwrite DNS lookup | | `port` | `PGPORT` | `5432` | | `dbname` | `PGDATABASE` | Unset; defaults to the username server-side. | | `sslmode` | `PGSSLMODE` | `prefer`. See [`PgSslMode`] for details. | @@ -182,4 +182,4 @@ let pool = PgPool::connect_with(opts).await?; [libpq-params]: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS [libpq-envars]: https://www.postgresql.org/docs/current/libpq-envars.html [rfc7468]: https://datatracker.ietf.org/doc/html/rfc7468 -[`webpki-roots`]: https://docs.rs/webpki-roots \ No newline at end of file +[`webpki-roots`]: https://docs.rs/webpki-roots diff --git a/sqlx-postgres/src/options/mod.rs b/sqlx-postgres/src/options/mod.rs index efbc43989b..55236f6e9e 100644 --- a/sqlx-postgres/src/options/mod.rs +++ b/sqlx-postgres/src/options/mod.rs @@ -16,6 +16,7 @@ mod ssl_mode; #[derive(Debug, Clone)] pub struct PgConnectOptions { pub(crate) host: String, + pub(crate) host_addr: Option, pub(crate) port: u16, pub(crate) socket: Option, pub(crate) username: String, @@ -59,10 +60,9 @@ impl PgConnectOptions { .and_then(|v| v.parse().ok()) .unwrap_or(5432); - let host = var("PGHOSTADDR") - .ok() - .or_else(|| var("PGHOST").ok()) - .unwrap_or_else(|| default_host(port)); + let host = var("PGHOST").ok().unwrap_or_else(|| default_host(port)); + + let host_addr = var("PGHOSTADDR").ok(); let username = var("PGUSER").ok().unwrap_or_else(whoami::username); @@ -71,6 +71,7 @@ impl PgConnectOptions { PgConnectOptions { port, host, + host_addr, socket: None, username, password: var("PGPASSWORD").ok(), @@ -127,6 +128,33 @@ impl PgConnectOptions { self } + /// Sets the host address to connect to. + /// + /// This is different to the host parameter as it overwrites DNS lookups for TCP/IP + /// communication. This is particuarly useful when the DB port has to be + /// proxied to localhost for security reasons. + /// + /// # Example + /// + /// ```rust + /// # use sqlx_postgres::PgConnectOptions; + /// let options = PgConnectOptions::new() + /// .host("example.com"); + /// + /// // Initially, host_addr should be None (unless PGHOSTADDR env var is set) + /// // For this test, we assume it's not set + /// assert_eq!(options.get_host_addr(), None); + /// + /// let options = options.host_addr("127.0.0.1"); + /// + /// // After setting, host_addr should contain the specified value + /// assert_eq!(options.get_host_addr(), Some("127.0.0.1")); + /// ``` + pub fn host_addr(mut self, host_addr: &str) -> Self { + self.host_addr = Some(host_addr.to_string()); + self + } + /// Sets the port to connect to at the server host. /// /// The default port for PostgreSQL is `5432`. @@ -477,6 +505,28 @@ impl PgConnectOptions { &self.host } + /// Get the current host addr. + /// + /// # Example + /// + /// ```rust + /// # use sqlx_postgres::PgConnectOptions; + /// let options = PgConnectOptions::new() + /// .host("example.com"); + /// + /// // Initially, host_addr should be None (unless PGHOSTADDR env var is set) + /// // For this test, we assume it's not set + /// assert_eq!(options.get_host_addr(), None); + /// + /// let options = options.host_addr("127.0.0.1"); + /// + /// // After setting host_addr, it should return the configured value + /// assert_eq!(options.get_host_addr(), Some("127.0.0.1")); + /// ``` + pub fn get_host_addr(&self) -> Option<&str> { + self.host_addr.as_deref() + } + /// Get the server's port. /// /// # Example diff --git a/sqlx-postgres/src/options/parse.rs b/sqlx-postgres/src/options/parse.rs index e911305698..0ea2cf8fbd 100644 --- a/sqlx-postgres/src/options/parse.rs +++ b/sqlx-postgres/src/options/parse.rs @@ -76,7 +76,7 @@ impl PgConnectOptions { "hostaddr" => { value.parse::().map_err(Error::config)?; - options = options.host(&value) + options = options.host_addr(&value) } "port" => options = options.port(value.parse().map_err(Error::config)?), @@ -203,7 +203,28 @@ fn it_parses_hostaddr_correctly_from_parameter() { let opts = PgConnectOptions::from_str(url).unwrap(); assert_eq!(None, opts.socket); - assert_eq!("8.8.8.8", &opts.host); + assert_eq!("localhost", &opts.host); + assert_eq!(Some("8.8.8.8"), opts.host_addr.as_deref()); +} + +#[test] +fn it_parses_hostaddr_host_separately_from_parameter() { + let url = "postgres://example.com/?hostaddr=8.8.8.8"; + let opts = PgConnectOptions::from_str(url).unwrap(); + + assert_eq!(None, opts.socket); + assert_eq!("example.com", &opts.host); + assert_eq!(Some("8.8.8.8"), opts.host_addr.as_deref()); +} + +#[test] +fn it_parses_hostaddr_host_host_overwrite_from_query_from_parameter() { + let url = "postgres://example.com/?hostaddr=8.8.8.8&host=sqlx.rs"; + let opts = PgConnectOptions::from_str(url).unwrap(); + + assert_eq!(None, opts.socket); + assert_eq!("sqlx.rs", &opts.host); + assert_eq!(Some("8.8.8.8"), opts.host_addr.as_deref()); } #[test] diff --git a/tests/certs/README.md b/tests/certs/README.md index add100625b..3d5bd05c09 100644 --- a/tests/certs/README.md +++ b/tests/certs/README.md @@ -83,5 +83,5 @@ This adds a required x509 v3 extension: Create a signed certificate using our CA key and the CSR: ``` -openssl x509 -req -CA ca.crt -CAkey keys/ca.key -in server.csr -out server.crt -days 3650 -CAcreateserial +openssl x509 -req -CA ca.crt -CAkey keys/ca.key -in server.csr -out server.crt -days 3650 -CAcreateserial -copy_extensions copy ``` diff --git a/tests/certs/server.crt b/tests/certs/server.crt index 4c18ad1b6a..c38753f00b 100644 --- a/tests/certs/server.crt +++ b/tests/certs/server.crt @@ -1,11 +1,12 @@ -----BEGIN CERTIFICATE----- -MIIBnjCCAVCgAwIBAgIJALbHH0sRwKGPMAUGAytlcDBfMQswCQYDVQQGEwJ1czET -MBEGA1UECAwKY2FsaWZvcm5pYTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQ -dHkgTHRkMRgwFgYDVQQDDA9BdXN0aW4gQm9uYW5kZXIwHhcNMjUwNzAxMDUyMTU2 -WhcNMzUwNjI5MDUyMTU2WjBGMQswCQYDVQQGEwJ1czETMBEGA1UECAwKY2FsaWZv -cm5pYTEQMA4GA1UECgwHU1FMeC5yczEQMA4GA1UEAwwHc3FseC5yczAqMAUGAytl -cAMhAA33S2qsqpZssUcYrpleMXDj5/mhb56HPaO3CIIgY5c8o0IwQDAdBgNVHQ4E -FgQUPUpn95GHFuMe7+2pG5rbmJS55/wwHwYDVR0jBBgwFoAUCw2pVpGKz2xkIjbV -HYh0LnzdkW4wBQYDK2VwA0EAExEOza9IrSchoQs1NwPxfCdfXMHiXpsgMThDuig+ -9hauW+b1KlBR3ZeW8AOIwazMhdstBFOhumaWPQ/wZNUkCg== +MIIBvTCCAW+gAwIBAgIULNqAltiOEP5feQrjIBXYA6YC0fUwBQYDK2VwMF8xCzAJ +BgNVBAYTAnVzMRMwEQYDVQQIDApjYWxpZm9ybmlhMSEwHwYDVQQKDBhJbnRlcm5l +dCBXaWRnaXRzIFB0eSBMdGQxGDAWBgNVBAMMD0F1c3RpbiBCb25hbmRlcjAeFw0y +NTExMTIyMTQ5NThaFw0zNTExMTAyMTQ5NThaMEYxCzAJBgNVBAYTAnVzMRMwEQYD +VQQIDApjYWxpZm9ybmlhMRAwDgYDVQQKDAdTUUx4LnJzMRAwDgYDVQQDDAdzcWx4 +LnJzMCowBQYDK2VwAyEADfdLaqyqlmyxRxiumV4xcOPn+aFvnoc9o7cIgiBjlzyj +VjBUMBIGA1UdEQQLMAmCB3NxbHgucnMwHQYDVR0OBBYEFD1KZ/eRhxbjHu/tqRua +25iUuef8MB8GA1UdIwQYMBaAFAsNqVaRis9sZCI21R2IdC583ZFuMAUGAytlcANB +AKyosmZvuCIrWkvb4QN8k2Fwf09LICCNjh571XwNxp9eUEEwJOjl956o6SFxDlgK +Cr1llASvz5cPm6jUV2wlaQc= -----END CERTIFICATE----- diff --git a/tests/x.py b/tests/x.py index e1308f2fa4..40d8aa5612 100755 --- a/tests/x.py +++ b/tests/x.py @@ -188,7 +188,7 @@ def run(command, comment=None, env=None, service=None, tag=None, args=None, data run( f"cargo test --no-default-features --features any,postgres,macros,_unstable-all-types,runtime-{runtime},tls-{tls}", comment=f"test postgres {version} ssl", - database_url_args="sslmode=verify-ca&sslrootcert=.%2Ftests%2Fcerts%2Fca.crt", + database_url_args="host=sqlx.rs&hostaddr=127.0.0.1&sslmode=verify-full&sslrootcert=.%2Ftests%2Fcerts%2Fca.crt", service=f"postgres_{version}", tag=f"postgres_{version}_ssl" if runtime == "async-std" else f"postgres_{version}_ssl_{runtime}", ) @@ -197,7 +197,7 @@ def run(command, comment=None, env=None, service=None, tag=None, args=None, data run( f"cargo test --no-default-features --features any,postgres,macros,_unstable-all-types,runtime-{runtime},tls-{tls}", comment=f"test postgres {version}_client_ssl no-password", - database_url_args="sslmode=verify-ca&sslrootcert=.%2Ftests%2Fcerts%2Fca.crt&sslkey=%2Ftests%2Fcerts%2Fkeys%2Fclient.key&sslcert=.%2Ftests%2Fcerts%2Fclient.crt", + database_url_args="host=sqlx.rs&hostaddr=127.0.0.1&sslmode=verify-full&sslrootcert=.%2Ftests%2Fcerts%2Fca.crt&sslkey=%2Ftests%2Fcerts%2Fkeys%2Fclient.key&sslcert=.%2Ftests%2Fcerts%2Fclient.crt", service=f"postgres_{version}_client_ssl", tag=f"postgres_{version}_client_ssl_no_password" if runtime == "async-std" else f"postgres_{version}_client_ssl_no_password_{runtime}", )