use std::time::Instant;
use deepslate_protocol::codec;
use deepslate_protocol::packet::Packet;
use deepslate_protocol::packet::handshake::{HandshakeIntent, HandshakePacket};
use deepslate_protocol::packet::login::{
LoginAcknowledgedPacket, LoginPluginRequestPacket, LoginPluginResponsePacket, LoginStartPacket,
SetCompressionPacket,
};
use deepslate_protocol::types::GameProfile;
use tokio::net::TcpStream;
use tracing::{debug, warn};
use crate::config::Config;
use crate::connection::ConnectionError;
use crate::connection::MinecraftConnection;
use crate::connection::forwarding::{self, VELOCITY_FORWARDING_CHANNEL};
use crate::event::PlayerInfo;
use crate::metrics;
#[allow(clippy::too_many_lines, clippy::missing_panics_doc)]
#[expect(
clippy::large_futures,
reason = "MinecraftConnection carries large framing buffers through login handshakes"
)]
pub async fn connect_and_login(
addr: &str,
profile: &GameProfile,
player_info: &PlayerInfo,
protocol_version: i32,
config: &Config,
) -> Result<MinecraftConnection, ConnectionError> {
let connect_start = Instant::now();
let backend_stream = tokio::time::timeout(config.connect_timeout, TcpStream::connect(addr))
.await
.map_err(|_| {
metrics::counter!("backend_connect_failures_total", "server" => addr.to_string());
ConnectionError::Timeout
})?
.map_err(|e| {
metrics::counter!("backend_connect_failures_total", "server" => addr.to_string());
ConnectionError::from(e)
})?;
metrics::histogram!("backend_connect_duration_seconds", connect_start.elapsed().as_secs_f64(), "server" => addr.to_string());
backend_stream.set_nodelay(true)?;
let mut conn = MinecraftConnection::new(
backend_stream,
libdeflater::CompressionLvl::new(config.compression_level)
.expect("validated in Config::validate"),
config.read_timeout,
);
conn.write_packet(&HandshakePacket {
protocol_version,
server_address: addr.to_string(),
server_port: 0,
next_state: HandshakeIntent::Login,
})
.await?;
conn.write_packet(&LoginStartPacket {
username: profile.name.clone(),
uuid: profile.id,
})
.await?;
let mut forwarding_done = false;
loop {
let Some(frame) = conn.read_frame_timeout().await? else {
return Err(ConnectionError::BackendFailed {
reason: "backend closed during login".to_string(),
});
};
let mut cursor = &frame[..];
let packet_id = codec::read_packet_id(&mut cursor)?;
match packet_id {
LoginPluginRequestPacket::PACKET_ID => {
let plugin_msg = LoginPluginRequestPacket::decode(&mut cursor)?;
if plugin_msg.channel == VELOCITY_FORWARDING_CHANNEL {
let requested_version = if plugin_msg.data.len() == 1 {
i32::from(plugin_msg.data[0])
} else {
forwarding::version::MODERN_DEFAULT
};
let forwarding_data = forwarding::create_forwarding_data(
&config.forwarding_secret,
&player_info.remote_addr,
profile,
requested_version,
);
conn.write_packet(&LoginPluginResponsePacket {
message_id: plugin_msg.message_id,
successful: true,
data: forwarding_data.into(),
})
.await?;
forwarding_done = true;
debug!("sent Velocity forwarding data to backend");
} else {
conn.write_packet(&LoginPluginResponsePacket {
message_id: plugin_msg.message_id,
successful: false,
data: bytes::Bytes::new(),
})
.await?;
}
}
SetCompressionPacket::PACKET_ID => {
let set_comp = SetCompressionPacket::decode(&mut cursor)?;
debug!(threshold = set_comp.threshold, "backend set compression");
conn.enable_compression(set_comp.threshold);
}
0x02 => {
if !forwarding_done {
return Err(ConnectionError::BackendFailed {
reason: "backend sent LoginSuccess without Velocity forwarding — \
is the backend configured for velocity modern forwarding?"
.to_string(),
});
}
debug!("backend login success, sending LoginAcknowledged");
conn.write_packet(&LoginAcknowledgedPacket).await?;
return Ok(conn);
}
0x00 => {
let reason = deepslate_protocol::types::read_string(&mut cursor)
.unwrap_or_else(|_| "unknown".to_string());
warn!(reason, "backend disconnected during login");
return Err(ConnectionError::BackendFailed {
reason: format!("backend rejected login: {reason}"),
});
}
0x01 => {
return Err(ConnectionError::BackendFailed {
reason: "backend is in online-mode — it must be in offline-mode \
for proxy forwarding"
.to_string(),
});
}
other => {
warn!(packet_id = other, "unexpected packet during backend login");
}
}
}
}