use crate::error::Result;
use crate::native::io::{ClickHouseRead, ClickHouseWrite};
use crate::tcp::protocol::ServerHello;
use crate::tcp::{reader, writer};
#[derive(Debug, 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(),
}
}
}
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 default_handshake_config_uses_server_defaults() {
let cfg = HandshakeConfig::default();
assert_eq!(cfg.database, "default");
assert_eq!(cfg.user, "default");
assert!(cfg.password.is_empty());
assert!(cfg.quota_key.is_empty());
}
#[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"
);
}
}