Compare commits

...

5 Commits

Author SHA1 Message Date
Conrad Ludgate 6062cd180c proxy: skip 1rtt during auth (experiment) 2024-06-24 09:11:00 +01:00
Conrad Ludgate d2a6cd1182 update to neon branch 2024-06-20 11:40:14 +01:00
Conrad Ludgate 75bfd22084 set utf8 for http flow 2024-06-17 17:48:03 +01:00
Conrad Ludgate 3cb3dce34e add regression test 2024-06-17 16:15:31 +01:00
Conrad Ludgate c7e43f40ba proxy: update tokio-postgres to allow arbitrary config params 2024-06-17 15:54:34 +01:00
7 changed files with 124 additions and 97 deletions
Generated
+4 -4
View File
@@ -4002,7 +4002,7 @@ dependencies = [
[[package]] [[package]]
name = "postgres" name = "postgres"
version = "0.19.4" version = "0.19.4"
source = "git+https://github.com/neondatabase/rust-postgres.git?branch=neon#20031d7a9ee1addeae6e0968e3899ae6bf01cee2" source = "git+https://github.com/neondatabase/rust-postgres.git?branch=skip-auth-1rtt#42784ef44fe62b6edca9813ca47bfc1c52c60a73"
dependencies = [ dependencies = [
"bytes", "bytes",
"fallible-iterator", "fallible-iterator",
@@ -4015,7 +4015,7 @@ dependencies = [
[[package]] [[package]]
name = "postgres-protocol" name = "postgres-protocol"
version = "0.6.4" version = "0.6.4"
source = "git+https://github.com/neondatabase/rust-postgres.git?branch=neon#20031d7a9ee1addeae6e0968e3899ae6bf01cee2" source = "git+https://github.com/neondatabase/rust-postgres.git?branch=skip-auth-1rtt#42784ef44fe62b6edca9813ca47bfc1c52c60a73"
dependencies = [ dependencies = [
"base64 0.20.0", "base64 0.20.0",
"byteorder", "byteorder",
@@ -4034,7 +4034,7 @@ dependencies = [
[[package]] [[package]]
name = "postgres-types" name = "postgres-types"
version = "0.2.4" version = "0.2.4"
source = "git+https://github.com/neondatabase/rust-postgres.git?branch=neon#20031d7a9ee1addeae6e0968e3899ae6bf01cee2" source = "git+https://github.com/neondatabase/rust-postgres.git?branch=skip-auth-1rtt#42784ef44fe62b6edca9813ca47bfc1c52c60a73"
dependencies = [ dependencies = [
"bytes", "bytes",
"fallible-iterator", "fallible-iterator",
@@ -6204,7 +6204,7 @@ dependencies = [
[[package]] [[package]]
name = "tokio-postgres" name = "tokio-postgres"
version = "0.7.7" version = "0.7.7"
source = "git+https://github.com/neondatabase/rust-postgres.git?branch=neon#20031d7a9ee1addeae6e0968e3899ae6bf01cee2" source = "git+https://github.com/neondatabase/rust-postgres.git?branch=skip-auth-1rtt#42784ef44fe62b6edca9813ca47bfc1c52c60a73"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"byteorder", "byteorder",
+5 -5
View File
@@ -198,10 +198,10 @@ env_logger = "0.10"
log = "0.4" log = "0.4"
## Libraries from neondatabase/ git forks, ideally with changes to be upstreamed ## Libraries from neondatabase/ git forks, ideally with changes to be upstreamed
postgres = { git = "https://github.com/neondatabase/rust-postgres.git", branch="neon" } postgres = { git = "https://github.com/neondatabase/rust-postgres.git", branch="skip-auth-1rtt" }
postgres-protocol = { git = "https://github.com/neondatabase/rust-postgres.git", branch="neon" } postgres-protocol = { git = "https://github.com/neondatabase/rust-postgres.git", branch="skip-auth-1rtt" }
postgres-types = { git = "https://github.com/neondatabase/rust-postgres.git", branch="neon" } postgres-types = { git = "https://github.com/neondatabase/rust-postgres.git", branch="skip-auth-1rtt" }
tokio-postgres = { git = "https://github.com/neondatabase/rust-postgres.git", branch="neon" } tokio-postgres = { git = "https://github.com/neondatabase/rust-postgres.git", branch="skip-auth-1rtt" }
## Other git libraries ## Other git libraries
heapless = { default-features=false, features=[], git = "https://github.com/japaric/heapless.git", rev = "644653bf3b831c6bb4963be2de24804acf5e5001" } # upstream release pending heapless = { default-features=false, features=[], git = "https://github.com/japaric/heapless.git", rev = "644653bf3b831c6bb4963be2de24804acf5e5001" } # upstream release pending
@@ -240,7 +240,7 @@ tonic-build = "0.9"
[patch.crates-io] [patch.crates-io]
# Needed to get `tokio-postgres-rustls` to depend on our fork. # Needed to get `tokio-postgres-rustls` to depend on our fork.
tokio-postgres = { git = "https://github.com/neondatabase/rust-postgres.git", branch="neon" } tokio-postgres = { git = "https://github.com/neondatabase/rust-postgres.git", branch="skip-auth-1rtt" }
# bug fixes for UUID # bug fixes for UUID
parquet = { git = "https://github.com/apache/arrow-rs", branch = "master" } parquet = { git = "https://github.com/apache/arrow-rs", branch = "master" }
+23 -27
View File
@@ -144,20 +144,7 @@ impl PgConnectionConfig {
// implement and this function is hardly a bottleneck. The function is only called around // implement and this function is hardly a bottleneck. The function is only called around
// establishing a new connection. // establishing a new connection.
#[allow(unstable_name_collisions)] #[allow(unstable_name_collisions)]
config.options( config.options(&encode_options(&self.options));
&self
.options
.iter()
.map(|s| {
if s.contains(['\\', ' ']) {
Cow::Owned(s.replace('\\', "\\\\").replace(' ', "\\ "))
} else {
Cow::Borrowed(s.as_str())
}
})
.intersperse(Cow::Borrowed(" ")) // TODO: use impl from std once it's stabilized
.collect::<String>(),
);
} }
config config
} }
@@ -178,6 +165,21 @@ impl PgConnectionConfig {
} }
} }
#[allow(unstable_name_collisions)]
fn encode_options(options: &[String]) -> String {
options
.iter()
.map(|s| {
if s.contains(['\\', ' ']) {
Cow::Owned(s.replace('\\', "\\\\").replace(' ', "\\ "))
} else {
Cow::Borrowed(s.as_str())
}
})
.intersperse(Cow::Borrowed(" ")) // TODO: use impl from std once it's stabilized
.collect::<String>()
}
impl fmt::Display for PgConnectionConfig { impl fmt::Display for PgConnectionConfig {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// The password is intentionally hidden and not part of this display string. // The password is intentionally hidden and not part of this display string.
@@ -206,7 +208,7 @@ impl fmt::Debug for PgConnectionConfig {
#[cfg(test)] #[cfg(test)]
mod tests_pg_connection_config { mod tests_pg_connection_config {
use crate::PgConnectionConfig; use crate::{encode_options, PgConnectionConfig};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use url::Host; use url::Host;
@@ -255,18 +257,12 @@ mod tests_pg_connection_config {
#[test] #[test]
fn test_with_options() { fn test_with_options() {
let cfg = PgConnectionConfig::new_host_port(STUB_HOST.clone(), 123).extend_options([ let options = encode_options(&[
"hello", "hello".to_owned(),
"world", "world".to_owned(),
"with space", "with space".to_owned(),
"and \\ backslashes", "and \\ backslashes".to_owned(),
]); ]);
assert_eq!(cfg.host(), &*STUB_HOST); assert_eq!(options, "hello world with\\ space and\\ \\\\\\ backslashes");
assert_eq!(cfg.port(), 123);
assert_eq!(cfg.raw_address(), "stub.host.example:123");
assert_eq!(
cfg.to_tokio_postgres_config().get_options(),
Some("hello world with\\ space and\\ \\\\\\ backslashes")
);
} }
} }
+68 -61
View File
@@ -103,12 +103,8 @@ impl ConnCfg {
/// Reuse password or auth keys from the other config. /// Reuse password or auth keys from the other config.
pub fn reuse_password(&mut self, other: Self) { pub fn reuse_password(&mut self, other: Self) {
if let Some(password) = other.get_password() { if let Some(password) = other.get_auth() {
self.password(password); self.auth(password);
}
if let Some(keys) = other.get_auth_keys() {
self.auth_keys(keys);
} }
} }
@@ -124,48 +120,64 @@ impl ConnCfg {
/// Apply startup message params to the connection config. /// Apply startup message params to the connection config.
pub fn set_startup_params(&mut self, params: &StartupMessageParams) { pub fn set_startup_params(&mut self, params: &StartupMessageParams) {
// Only set `user` if it's not present in the config. let mut client_encoding = false;
// Link auth flow takes username from the console's response. for (k, v) in params.iter() {
if let (None, Some(user)) = (self.get_user(), params.get("user")) { match k {
self.user(user); "user" => {
} // Only set `user` if it's not present in the config.
// Link auth flow takes username from the console's response.
// Only set `dbname` if it's not present in the config. if self.get_user().is_none() {
// Link auth flow takes dbname from the console's response. self.user(v);
if let (None, Some(dbname)) = (self.get_dbname(), params.get("database")) { }
self.dbname(dbname);
}
// Don't add `options` if they were only used for specifying a project.
// Connection pools don't support `options`, because they affect backend startup.
if let Some(options) = filtered_options(params) {
self.options(&options);
}
if let Some(app_name) = params.get("application_name") {
self.application_name(app_name);
}
// TODO: This is especially ugly...
if let Some(replication) = params.get("replication") {
use tokio_postgres::config::ReplicationMode;
match replication {
"true" | "on" | "yes" | "1" => {
self.replication_mode(ReplicationMode::Physical);
} }
"database" => { "database" => {
self.replication_mode(ReplicationMode::Logical); // Only set `dbname` if it's not present in the config.
// Link auth flow takes dbname from the console's response.
if self.get_dbname().is_none() {
self.dbname(v);
}
}
"options" => {
// Don't add `options` if they were only used for specifying a project.
// Connection pools don't support `options`, because they affect backend startup.
if let Some(options) = filtered_options(v) {
self.options(&options);
}
}
// the special ones in tokio-postgres that we don't want being set by the user
"dbname" => {}
"password" => {}
"sslmode" => {}
"host" => {}
"port" => {}
"connect_timeout" => {}
"keepalives" => {}
"keepalives_idle" => {}
"keepalives_interval" => {}
"keepalives_retries" => {}
"target_session_attrs" => {}
"channel_binding" => {}
"max_backend_message_size" => {}
"client_encoding" => {
client_encoding = true;
// only error should be from bad null bytes,
// but we've already checked for those.
_ = self.param("client_encoding", v);
}
_ => {
// only error should be from bad null bytes,
// but we've already checked for those.
_ = self.param(k, v);
} }
_other => {}
} }
} }
if !client_encoding {
// TODO: extend the list of the forwarded startup parameters. // for compatibility since we removed it from tokio-postgres
// Currently, tokio-postgres doesn't allow us to pass self.param("client_encoding", "UTF8").unwrap();
// arbitrary parameters, but the ones above are a good start. }
//
// This and the reverse params problem can be better addressed
// in a bespoke connection machinery (a new library for that sake).
} }
} }
@@ -338,10 +350,9 @@ impl ConnCfg {
} }
/// Retrieve `options` from a startup message, dropping all proxy-secific flags. /// Retrieve `options` from a startup message, dropping all proxy-secific flags.
fn filtered_options(params: &StartupMessageParams) -> Option<String> { fn filtered_options(options: &str) -> Option<String> {
#[allow(unstable_name_collisions)] #[allow(unstable_name_collisions)]
let options: String = params let options: String = StartupMessageParams::parse_options_raw(options)
.options_raw()?
.filter(|opt| parse_endpoint_param(opt).is_none() && neon_option(opt).is_none()) .filter(|opt| parse_endpoint_param(opt).is_none() && neon_option(opt).is_none())
.intersperse(" ") // TODO: use impl from std once it's stabilized .intersperse(" ") // TODO: use impl from std once it's stabilized
.collect(); .collect();
@@ -413,27 +424,23 @@ mod tests {
#[test] #[test]
fn test_filtered_options() { fn test_filtered_options() {
// Empty options is unlikely to be useful anyway. // Empty options is unlikely to be useful anyway.
let params = StartupMessageParams::new([("options", "")]); assert_eq!(filtered_options(""), None);
assert_eq!(filtered_options(&params), None);
// It's likely that clients will only use options to specify endpoint/project. // It's likely that clients will only use options to specify endpoint/project.
let params = StartupMessageParams::new([("options", "project=foo")]); let params = "project=foo";
assert_eq!(filtered_options(&params), None); assert_eq!(filtered_options(params), None);
// Same, because unescaped whitespaces are no-op. // Same, because unescaped whitespaces are no-op.
let params = StartupMessageParams::new([("options", " project=foo ")]); let params = " project=foo ";
assert_eq!(filtered_options(&params).as_deref(), None); assert_eq!(filtered_options(params), None);
let params = StartupMessageParams::new([("options", r"\ project=foo \ ")]); let params = r"\ project=foo \ ";
assert_eq!(filtered_options(&params).as_deref(), Some(r"\ \ ")); assert_eq!(filtered_options(params).as_deref(), Some(r"\ \ "));
let params = StartupMessageParams::new([("options", "project = foo")]); let params = "project = foo";
assert_eq!(filtered_options(&params).as_deref(), Some("project = foo")); assert_eq!(filtered_options(params).as_deref(), Some("project = foo"));
let params = StartupMessageParams::new([( let params = "project = foo neon_endpoint_type:read_write neon_lsn:0/2";
"options", assert_eq!(filtered_options(params).as_deref(), Some("project = foo"));
"project = foo neon_endpoint_type:read_write neon_lsn:0/2",
)]);
assert_eq!(filtered_options(&params).as_deref(), Some("project = foo"));
} }
} }
+4
View File
@@ -231,6 +231,10 @@ impl ConnectMechanism for TokioMechanism {
.dbname(&self.conn_info.dbname) .dbname(&self.conn_info.dbname)
.connect_timeout(timeout); .connect_timeout(timeout);
config
.param("client_encoding", "UTF8")
.expect("client encoding UTF8 is always valid");
let pause = ctx.latency_timer.pause(crate::metrics::Waiting::Compute); let pause = ctx.latency_timer.pause(crate::metrics::Waiting::Compute);
let res = config.connect(tokio_postgres::NoTls).await; let res = config.connect(tokio_postgres::NoTls).await;
drop(pause); drop(pause);
+1
View File
@@ -202,6 +202,7 @@ fn get_conn_info(
options = Some(NeonOptions::parse_options_raw(&value)); options = Some(NeonOptions::parse_options_raw(&value));
} }
} }
ctx.set_db_options(params.freeze());
let user_info = ComputeUserInfo { let user_info = ComputeUserInfo {
endpoint, endpoint,
+19
View File
@@ -53,6 +53,25 @@ def test_proxy_select_1(static_proxy: NeonProxy):
assert out[0][0] == 42 assert out[0][0] == 42
def test_proxy_server_params(static_proxy: NeonProxy):
"""
Test that server params are passing through to postgres
"""
out = static_proxy.safe_psql(
"select to_json('0 seconds'::interval)", options="-c intervalstyle=iso_8601"
)
assert out[0][0] == "PT0S"
out = static_proxy.safe_psql(
"select to_json('0 seconds'::interval)", options="-c intervalstyle=sql_standard"
)
assert out[0][0] == "0"
out = static_proxy.safe_psql(
"select to_json('0 seconds'::interval)", options="-c intervalstyle=postgres"
)
assert out[0][0] == "00:00:00"
def test_password_hack(static_proxy: NeonProxy): def test_password_hack(static_proxy: NeonProxy):
""" """
Check the PasswordHack auth flow: an alternative to SCRAM auth for Check the PasswordHack auth flow: an alternative to SCRAM auth for