Skip to content

Commit 4f41157

Browse files
authored
Merge pull request #945 from trungda/tdinh/hostaddr
Add hostaddr support
2 parents 790af54 + b0596f7 commit 4f41157

File tree

4 files changed

+213
-10
lines changed

4 files changed

+213
-10
lines changed

postgres/src/config.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use crate::connection::Connection;
66
use crate::Client;
77
use log::info;
88
use std::fmt;
9+
use std::net::IpAddr;
910
use std::path::Path;
1011
use std::str::FromStr;
1112
use std::sync::Arc;
@@ -39,6 +40,19 @@ use tokio_postgres::{Error, Socket};
3940
/// path to the directory containing Unix domain sockets. Otherwise, it is treated as a hostname. Multiple hosts
4041
/// can be specified, separated by commas. Each host will be tried in turn when connecting. Required if connecting
4142
/// with the `connect` method.
43+
/// * `hostaddr` - Numeric IP address of host to connect to. This should be in the standard IPv4 address format,
44+
/// e.g., 172.28.40.9. If your machine supports IPv6, you can also use those addresses.
45+
/// If this parameter is not specified, the value of `host` will be looked up to find the corresponding IP address,
46+
/// - or if host specifies an IP address, that value will be used directly.
47+
/// Using `hostaddr` allows the application to avoid a host name look-up, which might be important in applications
48+
/// with time constraints. However, a host name is required for verify-full SSL certificate verification.
49+
/// Specifically:
50+
/// * If `hostaddr` is specified without `host`, the value for `hostaddr` gives the server network address.
51+
/// The connection attempt will fail if the authentication method requires a host name;
52+
/// * If `host` is specified without `hostaddr`, a host name lookup occurs;
53+
/// * If both `host` and `hostaddr` are specified, the value for `hostaddr` gives the server network address.
54+
/// The value for `host` is ignored unless the authentication method requires it,
55+
/// in which case it will be used as the host name.
4256
/// * `port` - The port to connect to. Multiple ports can be specified, separated by commas. The number of ports must be
4357
/// either 1, in which case it will be used for all hosts, or the same as the number of hosts. Defaults to 5432 if
4458
/// omitted or the empty string.
@@ -70,6 +84,10 @@ use tokio_postgres::{Error, Socket};
7084
/// ```
7185
///
7286
/// ```not_rust
87+
/// host=host1,host2,host3 port=1234,,5678 hostaddr=127.0.0.1,127.0.0.2,127.0.0.3 user=postgres target_session_attrs=read-write
88+
/// ```
89+
///
90+
/// ```not_rust
7391
/// host=host1,host2,host3 port=1234,,5678 user=postgres target_session_attrs=read-write
7492
/// ```
7593
///
@@ -207,6 +225,7 @@ impl Config {
207225
///
208226
/// Multiple hosts can be specified by calling this method multiple times, and each will be tried in order. On Unix
209227
/// systems, a host starting with a `/` is interpreted as a path to a directory containing Unix domain sockets.
228+
/// There must be either no hosts, or the same number of hosts as hostaddrs.
210229
pub fn host(&mut self, host: &str) -> &mut Config {
211230
self.config.host(host);
212231
self
@@ -217,6 +236,11 @@ impl Config {
217236
self.config.get_hosts()
218237
}
219238

239+
/// Gets the hostaddrs that have been added to the configuration with `hostaddr`.
240+
pub fn get_hostaddrs(&self) -> &[IpAddr] {
241+
self.config.get_hostaddrs()
242+
}
243+
220244
/// Adds a Unix socket host to the configuration.
221245
///
222246
/// Unlike `host`, this method allows non-UTF8 paths.
@@ -229,6 +253,15 @@ impl Config {
229253
self
230254
}
231255

256+
/// Adds a hostaddr to the configuration.
257+
///
258+
/// Multiple hostaddrs can be specified by calling this method multiple times, and each will be tried in order.
259+
/// There must be either no hostaddrs, or the same number of hostaddrs as hosts.
260+
pub fn hostaddr(&mut self, hostaddr: IpAddr) -> &mut Config {
261+
self.config.hostaddr(hostaddr);
262+
self
263+
}
264+
232265
/// Adds a port to the configuration.
233266
///
234267
/// Multiple ports can be specified by calling this method multiple times. There must either be no ports, in which

tokio-postgres/src/config.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ use crate::{Client, Connection, Error};
1414
use std::borrow::Cow;
1515
#[cfg(unix)]
1616
use std::ffi::OsStr;
17+
use std::net::IpAddr;
18+
use std::ops::Deref;
1719
#[cfg(unix)]
1820
use std::os::unix::ffi::OsStrExt;
1921
#[cfg(unix)]
@@ -92,6 +94,19 @@ pub enum Host {
9294
/// path to the directory containing Unix domain sockets. Otherwise, it is treated as a hostname. Multiple hosts
9395
/// can be specified, separated by commas. Each host will be tried in turn when connecting. Required if connecting
9496
/// with the `connect` method.
97+
/// * `hostaddr` - Numeric IP address of host to connect to. This should be in the standard IPv4 address format,
98+
/// e.g., 172.28.40.9. If your machine supports IPv6, you can also use those addresses.
99+
/// If this parameter is not specified, the value of `host` will be looked up to find the corresponding IP address,
100+
/// - or if host specifies an IP address, that value will be used directly.
101+
/// Using `hostaddr` allows the application to avoid a host name look-up, which might be important in applications
102+
/// with time constraints. However, a host name is required for verify-full SSL certificate verification.
103+
/// Specifically:
104+
/// * If `hostaddr` is specified without `host`, the value for `hostaddr` gives the server network address.
105+
/// The connection attempt will fail if the authentication method requires a host name;
106+
/// * If `host` is specified without `hostaddr`, a host name lookup occurs;
107+
/// * If both `host` and `hostaddr` are specified, the value for `hostaddr` gives the server network address.
108+
/// The value for `host` is ignored unless the authentication method requires it,
109+
/// in which case it will be used as the host name.
95110
/// * `port` - The port to connect to. Multiple ports can be specified, separated by commas. The number of ports must be
96111
/// either 1, in which case it will be used for all hosts, or the same as the number of hosts. Defaults to 5432 if
97112
/// omitted or the empty string.
@@ -126,6 +141,10 @@ pub enum Host {
126141
/// ```
127142
///
128143
/// ```not_rust
144+
/// host=host1,host2,host3 port=1234,,5678 hostaddr=127.0.0.1,127.0.0.2,127.0.0.3 user=postgres target_session_attrs=read-write
145+
/// ```
146+
///
147+
/// ```not_rust
129148
/// host=host1,host2,host3 port=1234,,5678 user=postgres target_session_attrs=read-write
130149
/// ```
131150
///
@@ -162,6 +181,7 @@ pub struct Config {
162181
pub(crate) application_name: Option<String>,
163182
pub(crate) ssl_mode: SslMode,
164183
pub(crate) host: Vec<Host>,
184+
pub(crate) hostaddr: Vec<IpAddr>,
165185
pub(crate) port: Vec<u16>,
166186
pub(crate) connect_timeout: Option<Duration>,
167187
pub(crate) tcp_user_timeout: Option<Duration>,
@@ -189,6 +209,7 @@ impl Config {
189209
application_name: None,
190210
ssl_mode: SslMode::Prefer,
191211
host: vec![],
212+
hostaddr: vec![],
192213
port: vec![],
193214
connect_timeout: None,
194215
tcp_user_timeout: None,
@@ -288,6 +309,7 @@ impl Config {
288309
///
289310
/// Multiple hosts can be specified by calling this method multiple times, and each will be tried in order. On Unix
290311
/// systems, a host starting with a `/` is interpreted as a path to a directory containing Unix domain sockets.
312+
/// There must be either no hosts, or the same number of hosts as hostaddrs.
291313
pub fn host(&mut self, host: &str) -> &mut Config {
292314
#[cfg(unix)]
293315
{
@@ -305,6 +327,11 @@ impl Config {
305327
&self.host
306328
}
307329

330+
/// Gets the hostaddrs that have been added to the configuration with `hostaddr`.
331+
pub fn get_hostaddrs(&self) -> &[IpAddr] {
332+
self.hostaddr.deref()
333+
}
334+
308335
/// Adds a Unix socket host to the configuration.
309336
///
310337
/// Unlike `host`, this method allows non-UTF8 paths.
@@ -317,6 +344,15 @@ impl Config {
317344
self
318345
}
319346

347+
/// Adds a hostaddr to the configuration.
348+
///
349+
/// Multiple hostaddrs can be specified by calling this method multiple times, and each will be tried in order.
350+
/// There must be either no hostaddrs, or the same number of hostaddrs as hosts.
351+
pub fn hostaddr(&mut self, hostaddr: IpAddr) -> &mut Config {
352+
self.hostaddr.push(hostaddr);
353+
self
354+
}
355+
320356
/// Adds a port to the configuration.
321357
///
322358
/// Multiple ports can be specified by calling this method multiple times. There must either be no ports, in which
@@ -484,6 +520,14 @@ impl Config {
484520
self.host(host);
485521
}
486522
}
523+
"hostaddr" => {
524+
for hostaddr in value.split(',') {
525+
let addr = hostaddr
526+
.parse()
527+
.map_err(|_| Error::config_parse(Box::new(InvalidValue("hostaddr"))))?;
528+
self.hostaddr(addr);
529+
}
530+
}
487531
"port" => {
488532
for port in value.split(',') {
489533
let port = if port.is_empty() {
@@ -635,6 +679,7 @@ impl fmt::Debug for Config {
635679
.field("application_name", &self.application_name)
636680
.field("ssl_mode", &self.ssl_mode)
637681
.field("host", &self.host)
682+
.field("hostaddr", &self.hostaddr)
638683
.field("port", &self.port)
639684
.field("connect_timeout", &self.connect_timeout)
640685
.field("tcp_user_timeout", &self.tcp_user_timeout)
@@ -1025,3 +1070,41 @@ impl<'a> UrlParser<'a> {
10251070
.map_err(|e| Error::config_parse(e.into()))
10261071
}
10271072
}
1073+
1074+
#[cfg(test)]
1075+
mod tests {
1076+
use std::net::IpAddr;
1077+
1078+
use crate::{config::Host, Config};
1079+
1080+
#[test]
1081+
fn test_simple_parsing() {
1082+
let s = "user=pass_user dbname=postgres host=host1,host2 hostaddr=127.0.0.1,127.0.0.2 port=26257";
1083+
let config = s.parse::<Config>().unwrap();
1084+
assert_eq!(Some("pass_user"), config.get_user());
1085+
assert_eq!(Some("postgres"), config.get_dbname());
1086+
assert_eq!(
1087+
[
1088+
Host::Tcp("host1".to_string()),
1089+
Host::Tcp("host2".to_string())
1090+
],
1091+
config.get_hosts(),
1092+
);
1093+
1094+
assert_eq!(
1095+
[
1096+
"127.0.0.1".parse::<IpAddr>().unwrap(),
1097+
"127.0.0.2".parse::<IpAddr>().unwrap()
1098+
],
1099+
config.get_hostaddrs(),
1100+
);
1101+
1102+
assert_eq!(1, 1);
1103+
}
1104+
1105+
#[test]
1106+
fn test_invalid_hostaddr_parsing() {
1107+
let s = "user=pass_user dbname=postgres host=host1 hostaddr=127.0.0 port=26257";
1108+
s.parse::<Config>().err().unwrap();
1109+
}
1110+
}

tokio-postgres/src/connect.rs

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ use crate::connect_socket::connect_socket;
55
use crate::tls::{MakeTlsConnect, TlsConnect};
66
use crate::{Client, Config, Connection, Error, SimpleQueryMessage, Socket};
77
use futures_util::{future, pin_mut, Future, FutureExt, Stream};
8-
use std::io;
98
use std::task::Poll;
9+
use std::{cmp, io};
1010

1111
pub async fn connect<T>(
1212
mut tls: T,
@@ -15,35 +15,70 @@ pub async fn connect<T>(
1515
where
1616
T: MakeTlsConnect<Socket>,
1717
{
18-
if config.host.is_empty() {
19-
return Err(Error::config("host missing".into()));
18+
if config.host.is_empty() && config.hostaddr.is_empty() {
19+
return Err(Error::config("both host and hostaddr are missing".into()));
2020
}
2121

22-
if config.port.len() > 1 && config.port.len() != config.host.len() {
22+
if !config.host.is_empty()
23+
&& !config.hostaddr.is_empty()
24+
&& config.host.len() != config.hostaddr.len()
25+
{
26+
let msg = format!(
27+
"number of hosts ({}) is different from number of hostaddrs ({})",
28+
config.host.len(),
29+
config.hostaddr.len(),
30+
);
31+
return Err(Error::config(msg.into()));
32+
}
33+
34+
// At this point, either one of the following two scenarios could happen:
35+
// (1) either config.host or config.hostaddr must be empty;
36+
// (2) if both config.host and config.hostaddr are NOT empty; their lengths must be equal.
37+
let num_hosts = cmp::max(config.host.len(), config.hostaddr.len());
38+
39+
if config.port.len() > 1 && config.port.len() != num_hosts {
2340
return Err(Error::config("invalid number of ports".into()));
2441
}
2542

2643
let mut error = None;
27-
for (i, host) in config.host.iter().enumerate() {
44+
for i in 0..num_hosts {
45+
let host = config.host.get(i);
46+
let hostaddr = config.hostaddr.get(i);
2847
let port = config
2948
.port
3049
.get(i)
3150
.or_else(|| config.port.first())
3251
.copied()
3352
.unwrap_or(5432);
3453

54+
// The value of host is used as the hostname for TLS validation,
55+
// if it's not present, use the value of hostaddr.
3556
let hostname = match host {
36-
Host::Tcp(host) => host.as_str(),
57+
Some(Host::Tcp(host)) => host.clone(),
3758
// postgres doesn't support TLS over unix sockets, so the choice here doesn't matter
3859
#[cfg(unix)]
39-
Host::Unix(_) => "",
60+
Some(Host::Unix(_)) => "".to_string(),
61+
None => hostaddr.map_or("".to_string(), |ipaddr| ipaddr.to_string()),
4062
};
41-
4263
let tls = tls
43-
.make_tls_connect(hostname)
64+
.make_tls_connect(&hostname)
4465
.map_err(|e| Error::tls(e.into()))?;
4566

46-
match connect_once(host, port, tls, config).await {
67+
// Try to use the value of hostaddr to establish the TCP connection,
68+
// fallback to host if hostaddr is not present.
69+
let addr = match hostaddr {
70+
Some(ipaddr) => Host::Tcp(ipaddr.to_string()),
71+
None => {
72+
if let Some(host) = host {
73+
host.clone()
74+
} else {
75+
// This is unreachable.
76+
return Err(Error::config("both host and hostaddr are empty".into()));
77+
}
78+
}
79+
};
80+
81+
match connect_once(&addr, port, tls, config).await {
4782
Ok((client, connection)) => return Ok((client, connection)),
4883
Err(e) => error = Some(e),
4984
}

tokio-postgres/tests/test/runtime.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,58 @@ async fn target_session_attrs_err() {
6666
.unwrap();
6767
}
6868

69+
#[tokio::test]
70+
async fn host_only_ok() {
71+
let _ = tokio_postgres::connect(
72+
"host=localhost port=5433 user=pass_user dbname=postgres password=password",
73+
NoTls,
74+
)
75+
.await
76+
.unwrap();
77+
}
78+
79+
#[tokio::test]
80+
async fn hostaddr_only_ok() {
81+
let _ = tokio_postgres::connect(
82+
"hostaddr=127.0.0.1 port=5433 user=pass_user dbname=postgres password=password",
83+
NoTls,
84+
)
85+
.await
86+
.unwrap();
87+
}
88+
89+
#[tokio::test]
90+
async fn hostaddr_and_host_ok() {
91+
let _ = tokio_postgres::connect(
92+
"hostaddr=127.0.0.1 host=localhost port=5433 user=pass_user dbname=postgres password=password",
93+
NoTls,
94+
)
95+
.await
96+
.unwrap();
97+
}
98+
99+
#[tokio::test]
100+
async fn hostaddr_host_mismatch() {
101+
let _ = tokio_postgres::connect(
102+
"hostaddr=127.0.0.1,127.0.0.2 host=localhost port=5433 user=pass_user dbname=postgres password=password",
103+
NoTls,
104+
)
105+
.await
106+
.err()
107+
.unwrap();
108+
}
109+
110+
#[tokio::test]
111+
async fn hostaddr_host_both_missing() {
112+
let _ = tokio_postgres::connect(
113+
"port=5433 user=pass_user dbname=postgres password=password",
114+
NoTls,
115+
)
116+
.await
117+
.err()
118+
.unwrap();
119+
}
120+
69121
#[tokio::test]
70122
async fn cancel_query() {
71123
let client = connect("host=localhost port=5433 user=postgres").await;

0 commit comments

Comments
 (0)