use crate::error::Result;
use crate::native::io::{ClickHouseRead, ClickHouseWrite};
use crate::tcp::protocol::ServerHello;
use crate::tcp::{reader, writer};
#[derive(Clone)]
#[non_exhaustive]
pub struct HandshakeConfig {
pub database: String,
pub user: String,
pub password: String,
pub quota_key: String,
}
impl Default for HandshakeConfig {
fn default() -> Self {
Self {
database: "default".to_string(),
user: "default".to_string(),
password: String::new(),
quota_key: String::new(),
}
}
}
impl std::fmt::Debug for HandshakeConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HandshakeConfig")
.field("database", &self.database)
.field("user", &self.user)
.field("password", &"<redacted>")
.field("quota_key", &self.quota_key)
.finish()
}
}
pub(crate) async fn handshake<S>(stream: &mut S, cfg: &HandshakeConfig) -> Result<ServerHello>
where
S: ClickHouseRead + ClickHouseWrite,
{
writer::send_hello(stream, &cfg.database, &cfg.user, &cfg.password).await?;
let hello = reader::read_hello(stream).await?;
writer::send_addendum(stream, hello.revision, &cfg.quota_key).await?;
Ok(hello)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tcp::protocol::{
DBMS_MIN_PROTOCOL_VERSION_WITH_ADDENDUM, DBMS_TCP_PROTOCOL_VERSION, ServerPacketId,
};
use tokio::io::{AsyncWriteExt, duplex};
#[test]
fn debug_redacts_the_password_and_keeps_the_rest() {
let cfg = HandshakeConfig {
database: "analytics".to_string(),
user: "loader".to_string(),
password: "hunter2-not-a-real-password".to_string(),
quota_key: "q1".to_string(),
};
let rendered = format!("{cfg:?}");
assert!(
!rendered.contains("hunter2-not-a-real-password"),
"{rendered}"
);
assert!(rendered.contains("<redacted>"), "{rendered}");
assert!(rendered.contains("analytics"), "{rendered}");
assert!(rendered.contains("loader"), "{rendered}");
assert!(rendered.contains("q1"), "{rendered}");
}
#[test]
fn default_handshake_config_uses_server_defaults() {
let cfg = HandshakeConfig::default();
assert_eq!(cfg.database, "default");
assert_eq!(cfg.user, "default");
assert_eq!(cfg.password, "");
assert_eq!(cfg.quota_key, "");
}
#[tokio::test]
async fn handshake_roundtrip_on_duplex() {
let (mut client_side, mut server_side) = duplex(4096);
server_side
.write_var_uint(ServerPacketId::Hello as u64)
.await
.unwrap();
server_side
.write_string("ClickHouse test".as_bytes())
.await
.unwrap();
server_side.write_var_uint(25).await.unwrap();
server_side.write_var_uint(4).await.unwrap();
server_side
.write_var_uint(DBMS_TCP_PROTOCOL_VERSION)
.await
.unwrap();
server_side.write_var_uint(0).await.unwrap();
server_side
.write_string("Etc/UTC".as_bytes())
.await
.unwrap();
server_side
.write_string("ch-test".as_bytes())
.await
.unwrap();
server_side.write_var_uint(7).await.unwrap();
server_side
.write_string("notchunked".as_bytes())
.await
.unwrap();
server_side
.write_string("notchunked".as_bytes())
.await
.unwrap();
server_side.write_var_uint(0).await.unwrap();
server_side.write_all(&[0u8; 8]).await.unwrap();
server_side.flush().await.unwrap();
let cfg = HandshakeConfig {
database: "default".into(),
user: "user".into(),
password: "pw".into(),
quota_key: "qk".into(),
};
let hello = handshake(&mut client_side, &cfg).await.unwrap();
assert_eq!(hello.server_name, "ClickHouse test");
assert_eq!(hello.revision, DBMS_TCP_PROTOCOL_VERSION);
assert!(hello.revision >= DBMS_MIN_PROTOCOL_VERSION_WITH_ADDENDUM);
drop(client_side);
let packet_id = server_side.read_var_uint().await.unwrap();
assert_eq!(packet_id, 0); assert_eq!(
server_side.read_utf8_string().await.unwrap(),
crate::tcp::writer::CLIENT_NAME
);
let _major = server_side.read_var_uint().await.unwrap();
let _minor = server_side.read_var_uint().await.unwrap();
let _rev = server_side.read_var_uint().await.unwrap();
assert_eq!(server_side.read_utf8_string().await.unwrap(), "default");
assert_eq!(server_side.read_utf8_string().await.unwrap(), "user");
assert_eq!(server_side.read_utf8_string().await.unwrap(), "pw");
assert_eq!(server_side.read_utf8_string().await.unwrap(), "qk");
}
#[tokio::test]
async fn handshake_suppresses_addendum_below_gate() {
let (mut client_side, mut server_side) = duplex(4096);
let legacy_revision = DBMS_MIN_PROTOCOL_VERSION_WITH_ADDENDUM - 1;
server_side
.write_var_uint(ServerPacketId::Hello as u64)
.await
.unwrap();
server_side.write_string("legacy".as_bytes()).await.unwrap();
server_side.write_var_uint(24).await.unwrap();
server_side.write_var_uint(0).await.unwrap();
server_side.write_var_uint(legacy_revision).await.unwrap();
server_side.write_string("UTC".as_bytes()).await.unwrap();
server_side
.write_string("legacy-ch".as_bytes())
.await
.unwrap();
server_side.write_var_uint(0).await.unwrap();
server_side.flush().await.unwrap();
let cfg = HandshakeConfig {
database: "default".into(),
user: "user".into(),
password: String::new(),
quota_key: "should-not-be-sent".into(),
};
let hello = handshake(&mut client_side, &cfg).await.unwrap();
assert_eq!(hello.revision, legacy_revision);
drop(client_side);
let _ = server_side.read_var_uint().await.unwrap(); let _ = server_side.read_utf8_string().await.unwrap(); let _ = server_side.read_var_uint().await.unwrap(); let _ = server_side.read_var_uint().await.unwrap(); let _ = server_side.read_var_uint().await.unwrap(); let _ = server_side.read_utf8_string().await.unwrap(); let _ = server_side.read_utf8_string().await.unwrap(); let _ = server_side.read_utf8_string().await.unwrap();
let next = server_side.read_var_uint().await;
assert!(
next.is_err(),
"expected EOF after Hello when server revision is below addendum gate"
);
}
}