1use std::borrow::Cow;
4use std::io;
5use std::net::SocketAddr;
6use std::time::Duration;
7
8use anyhow::{bail, ensure, Context};
9use base64::prelude::*;
10use chunkedge_binary::{Bounded, Decode, RawBytes};
11use chunkedge_lang::keys;
12use chunkedge_protocol::packets::configuration::select_known_packs_s2c::KnownPack;
13use chunkedge_protocol::packets::configuration::{
14 ClientInformationC2s, CustomPayloadC2s, CustomPayloadS2c, FinishConfigurationC2s,
15 FinishConfigurationS2c, RegistryDataS2c, SelectKnownPacksC2s, SelectKnownPacksS2c,
16 UpdateEnabledFeaturesS2c, UpdateTagsS2c,
17};
18use chunkedge_protocol::packets::login::{LoginAcknowledgedC2s, LoginFinishedS2c};
19use chunkedge_protocol::packets::status::{
20 PingRequestC2s, PongResponseS2c, StatusRequestC2s, StatusResponseS2c,
21};
22use chunkedge_protocol::profile::Property;
23use chunkedge_protocol::JsonText;
24use chunkedge_server::client::Properties;
25use chunkedge_server::nbt::serde::ser::CompoundSerializer;
26use chunkedge_server::protocol::packets::handshake::intention_c2s::HandShakeIntent;
27use chunkedge_server::protocol::packets::handshake::IntentionC2s;
28use chunkedge_server::protocol::packets::login::{
29 CustomQueryAnswerC2s, CustomQueryS2c, HelloC2s, HelloS2c, KeyC2s, LoginCompressionS2c,
30 LoginDisconnectS2c,
31};
32use chunkedge_server::protocol::{PacketDecoder, PacketEncoder, VarInt};
33use chunkedge_server::registry::{BiomeRegistry, DimensionTypeRegistry, RegistryCodec};
34use chunkedge_server::text::{Color, IntoText};
35use chunkedge_server::{ident, Ident, Text, MINECRAFT_VERSION, PROTOCOL_VERSION};
36use hmac::digest::Update;
37use hmac::{Hmac, Mac};
38use num_bigint::BigInt;
39use reqwest::StatusCode;
40use rsa::Pkcs1v15Encrypt;
41use serde::{Deserialize, Serialize};
42use serde_json::{json, Value};
43use sha1::Sha1;
44use sha2::{Digest, Sha256};
45use tokio::net::{TcpListener, TcpStream};
46use tracing::{error, info, trace, warn};
47use uuid::Uuid;
48
49use crate::legacy_ping::try_handle_legacy_ping;
50use crate::packet_io::PacketIo;
51use crate::{
52 CleanupOnDrop, ConnectionMode, NewClientInfo, ServerListPing, SharedNetworkState,
53 WorldLoginState,
54};
55
56const VELOCITY_MIN_MAX_SUPPORTED_VERSION: u8 = 3;
57
58pub(super) async fn do_accept_loop(shared: SharedNetworkState, world_state: WorldLoginState) {
60 let listener = match TcpListener::bind(shared.0.address).await {
61 Ok(listener) => listener,
62 Err(e) => {
63 error!("failed to start TCP listener: {e}");
64 return;
65 }
66 };
67
68 let timeout = Duration::from_secs(5);
69
70 loop {
71 let world_state = world_state.clone();
72 match shared.0.connection_sema.clone().acquire_owned().await {
73 Ok(permit) => match listener.accept().await {
74 Ok((stream, remote_addr)) => {
75 let shared = shared.clone();
76 tokio::spawn(async move {
77 if let Err(e) = tokio::time::timeout(
78 timeout,
79 handle_connection(shared, stream, remote_addr, world_state),
80 )
81 .await
82 {
83 warn!("initial connection timed out: {e}");
84 }
85
86 drop(permit);
87 });
88 }
89 Err(e) => {
90 error!("failed to accept incoming connection: {e}");
91 }
92 },
93 Err(_) => return,
95 }
96 }
97}
98
99async fn handle_connection(
100 shared: SharedNetworkState,
101 mut stream: TcpStream,
102 remote_addr: SocketAddr,
103 world_state: WorldLoginState,
104) {
105 trace!("handling connection");
106
107 if let Err(e) = stream.set_nodelay(true) {
108 error!("failed to set TCP_NODELAY: {e}");
109 }
110
111 match try_handle_legacy_ping(&shared, &mut stream, remote_addr).await {
112 Ok(true) => return, Ok(false) => {} Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {}
115 Err(e) => {
116 warn!("legacy ping ended with error: {e:#}");
117 }
118 }
119
120 let io = PacketIo::new(stream, PacketEncoder::new(), PacketDecoder::new());
121
122 if let Err(e) = handle_handshake(shared, io, remote_addr, world_state).await {
123 if let Some(e) = e.downcast_ref::<io::Error>() {
126 if e.kind() == io::ErrorKind::UnexpectedEof {
127 return;
128 }
129 }
130 warn!("connection ended with error: {e:#}");
131 }
132}
133
134#[derive(Default, Debug)]
137pub struct HandshakeData {
138 pub protocol_version: i32,
140 pub server_address: String,
142 pub server_port: u16,
144}
145
146async fn handle_handshake(
147 shared: SharedNetworkState,
148 mut io: PacketIo,
149 remote_addr: SocketAddr,
150 world_state: WorldLoginState,
151) -> anyhow::Result<()> {
152 let handshake = io.recv_packet::<IntentionC2s>().await?;
153
154 let next_state = handshake.intent;
155
156 let handshake = HandshakeData {
157 protocol_version: handshake.protocol_version.0,
158 server_address: handshake.server_address.0.to_owned(),
159 server_port: handshake.server_port,
160 };
161
162 ensure!(
164 shared.0.connection_mode == ConnectionMode::BungeeCord
165 || handshake.server_address.encode_utf16().count() <= 255,
166 "handshake server address is too long"
167 );
168
169 match next_state {
170 HandShakeIntent::Status => handle_status(shared, io, remote_addr, handshake)
171 .await
172 .context("handling status"),
173 HandShakeIntent::Login => {
174 match handle_login(&shared, &mut io, remote_addr, handshake, world_state)
175 .await
176 .context("handling login")?
177 {
178 Some((info, cleanup)) => {
179 let client = io.into_client_args(
180 info,
181 shared.0.incoming_byte_limit,
182 shared.0.outgoing_byte_limit,
183 cleanup,
184 );
185
186 let _ = shared.0.new_clients_send.send_async(client).await;
187
188 Ok(())
189 }
190 None => Ok(()),
191 }
192 }
193 HandShakeIntent::Transfer => {
194 bail!("transfer state is not yet implemented");
196 }
197 }
198}
199
200async fn handle_status(
201 shared: SharedNetworkState,
202 mut io: PacketIo,
203 remote_addr: SocketAddr,
204 handshake: HandshakeData,
205) -> anyhow::Result<()> {
206 io.recv_packet::<StatusRequestC2s>().await?;
207
208 match shared
209 .0
210 .callbacks
211 .inner
212 .server_list_ping(&shared, remote_addr, &handshake)
213 .await
214 {
215 ServerListPing::Respond {
216 online_players,
217 max_players,
218 player_sample,
219 mut description,
220 favicon_png,
221 version_name,
222 protocol,
223 } => {
224 if handshake.protocol_version < 735 {
228 fn fallback_webcolors(txt: &mut Text) {
229 if let Some(Color::Rgb(color)) = txt.color {
230 txt.color = Some(Color::Named(color.to_named_lossy()));
231 }
232 for child in &mut txt.extra {
233 fallback_webcolors(child);
234 }
235 }
236
237 fallback_webcolors(&mut description);
238 }
239
240 let mut json = json!({
241 "version": {
242 "name": version_name,
243 "protocol": protocol,
244 },
245 "players": {
246 "online": online_players,
247 "max": max_players,
248 "sample": player_sample,
249 },
250 "description": description,
251 });
252
253 if !favicon_png.is_empty() {
254 let mut buf = "data:image/png;base64,".to_owned();
255 BASE64_STANDARD.encode_string(favicon_png, &mut buf);
256 json["favicon"] = Value::String(buf);
257 }
258
259 io.send_packet(&StatusResponseS2c {
260 json: &json.to_string(),
261 })
262 .await?;
263 }
264 ServerListPing::Ignore => return Ok(()),
265 }
266
267 let PingRequestC2s { timestamp: payload } = io.recv_packet().await?;
268
269 io.send_packet(&PongResponseS2c { timestamp: payload })
270 .await?;
271
272 Ok(())
273}
274
275async fn handle_login(
277 shared: &SharedNetworkState,
278 io: &mut PacketIo,
279 remote_addr: SocketAddr,
280 handshake: HandshakeData,
281 world_state: WorldLoginState,
282) -> anyhow::Result<Option<(NewClientInfo, CleanupOnDrop)>> {
283 if handshake.protocol_version != PROTOCOL_VERSION {
284 io.send_packet(&LoginDisconnectS2c {
285 reason: Cow::Owned(JsonText(
287 format!("Mismatched Minecraft version (server is on {MINECRAFT_VERSION})")
288 .color(Color::RED),
289 )),
290 })
291 .await?;
292
293 return Ok(None);
294 }
295
296 let HelloC2s {
297 username,
298 .. } = io.recv_packet().await?;
300
301 let username = username.0.to_owned();
302
303 let mut info = match shared.connection_mode() {
304 ConnectionMode::Online { .. } => login_online(shared, io, remote_addr, username).await?,
305 ConnectionMode::Offline => login_offline(remote_addr, username)?,
306 ConnectionMode::BungeeCord => {
307 login_bungeecord(remote_addr, &handshake.server_address, username)?
308 }
309 ConnectionMode::Velocity { secret } => login_velocity(io, username, secret).await?,
310 };
311
312 if shared.0.threshold.0 > 0 {
313 io.send_packet(&LoginCompressionS2c {
314 threshold: shared.0.threshold.0.into(),
315 })
316 .await?;
317
318 io.set_compression(shared.0.threshold);
319 }
320
321 let cleanup = match shared.0.callbacks.inner.login(shared, &info).await {
322 Ok(f) => CleanupOnDrop(Some(f)),
323 Err(reason) => {
324 info!("disconnect at login: \"{reason}\"");
325 io.send_packet(&LoginDisconnectS2c {
326 reason: Cow::Owned(JsonText(reason)),
327 })
328 .await?;
329 return Ok(None);
330 }
331 };
332
333 io.send_packet(&LoginFinishedS2c {
334 uuid: info.uuid,
335 username: info.username.as_str().into(),
336 properties: Default::default(),
337 })
338 .await?;
339
340 let LoginAcknowledgedC2s {} = io.recv_packet().await?;
341 if !matches!(shared.connection_mode(), ConnectionMode::Velocity { .. }) {
342 let _: CustomPayloadC2s = io.recv_packet().await?;
343 }
344 let client_info: ClientInformationC2s = io.recv_packet().await?;
345
346 info.view_distance = client_info.view_distance;
347 info.locale = client_info.locale.0.to_owned();
348 info.chat_mode = client_info.chat_mode;
349 info.chat_colors = client_info.chat_colors;
350 info.displayed_skin_parts = client_info.displayed_skin_parts;
351 info.main_arm = client_info.main_arm;
352 info.enable_text_filtering = client_info.enable_text_filtering;
353 info.allow_server_listings = client_info.allow_server_listings;
354 info.particle_mode = client_info.particle_mode;
355
356 io.send_packet(&CustomPayloadS2c {
357 channel: Ident::new("minecraft:brand").unwrap(),
358 data: Bounded(RawBytes(&[&[0x07], "vanilla".as_bytes()].concat())),
359 })
360 .await?;
361
362 io.send_packet(&UpdateEnabledFeaturesS2c {
363 features: vec![ident!("minecraft:vanilla").into()],
364 })
365 .await?;
366
367 io.send_packet(&SelectKnownPacksS2c {
368 packs: vec![KnownPack {
369 namespace: "minecraft".into(),
370 id: "core".into(),
371 version: MINECRAFT_VERSION.into(),
372 }],
373 })
374 .await?;
375
376 let _: SelectKnownPacksC2s = io.recv_packet().await?;
377
378 io.send_packet(&RegistryDataS2c {
385 id: BiomeRegistry::KEY.into(),
386 entries: world_state
387 .biome_registry
388 .iter()
389 .map(|(_, biome_ident, biome)| {
390 (
391 biome_ident.into(),
392 Some(
393 biome
394 .serialize(CompoundSerializer)
395 .expect("failed to serialize biome"),
396 ),
397 )
398 })
399 .collect(),
400 })
401 .await?;
402
403 io.send_packet(&RegistryDataS2c {
405 id: DimensionTypeRegistry::KEY.into(),
406 entries: world_state
407 .dimension_registry
408 .iter()
409 .map(|(_, dimension_ident, dimension_type)| {
410 (
411 dimension_ident.into(),
412 Some(
413 dimension_type
414 .serialize(CompoundSerializer)
415 .expect("failed to serialize dimension type"),
416 ),
417 )
418 })
419 .collect(),
420 })
421 .await?;
422
423 for (id, entries) in RegistryCodec::default().registries {
430 if id == ident!("worldgen/biome") || id == ident!("dimension_type") {
431 continue;
433 }
434
435 io.send_packet(&RegistryDataS2c {
436 id: id.into(),
437 entries: entries
438 .into_iter()
439 .map(|value| (value.name.into(), Some(value.element)))
440 .collect(),
441 })
442 .await?;
443 }
444
445 io.send_packet(&UpdateTagsS2c {
447 groups: Cow::Owned(world_state.tag_registry),
448 })
449 .await?;
450
451 io.send_packet(&FinishConfigurationS2c {}).await?;
452
453 if matches!(shared.connection_mode(), ConnectionMode::Velocity { .. }) {
454 let _: CustomPayloadC2s = io.recv_packet().await?;
455 }
456 let _: FinishConfigurationC2s = io.recv_packet().await?;
457
458 Ok(Some((info, cleanup)))
459}
460
461async fn login_online(
463 shared: &SharedNetworkState,
464 io: &mut PacketIo,
465 remote_addr: SocketAddr,
466 username: String,
467) -> anyhow::Result<NewClientInfo> {
468 let my_verify_token: [u8; 16] = rand::random();
469
470 io.send_packet(&HelloS2c {
471 server_id: "".into(), public_key: &shared.0.public_key_der,
473 verify_token: &my_verify_token,
474 should_authenticate: true,
475 })
476 .await?;
477
478 let KeyC2s {
479 shared_secret,
480 verify_token: encrypted_verify_token,
481 } = io.recv_packet().await?;
482
483 let shared_secret = shared
484 .0
485 .rsa_key
486 .decrypt(Pkcs1v15Encrypt, shared_secret)
487 .context("failed to decrypt shared secret")?;
488
489 let verify_token = shared
490 .0
491 .rsa_key
492 .decrypt(Pkcs1v15Encrypt, encrypted_verify_token)
493 .context("failed to decrypt verify token")?;
494
495 ensure!(
496 my_verify_token.as_slice() == verify_token,
497 "verify tokens do not match"
498 );
499
500 let crypt_key: [u8; 16] = shared_secret
501 .as_slice()
502 .try_into()
503 .context("shared secret has the wrong length")?;
504
505 io.enable_encryption(&crypt_key);
506
507 let hash = Sha1::new()
508 .chain(&shared_secret)
509 .chain(&shared.0.public_key_der)
510 .finalize();
511
512 let url = shared
513 .0
514 .callbacks
515 .inner
516 .session_server(
517 shared,
518 username.as_str(),
519 &auth_digest(&hash),
520 &remote_addr.ip(),
521 )
522 .await;
523
524 let resp = shared.0.http_client.get(url).send().await?;
525
526 match resp.status() {
527 StatusCode::OK => {}
528 StatusCode::NO_CONTENT => {
529 let reason =
530 Text::translate(keys::MULTIPLAYER_DISCONNECT_UNVERIFIED_USERNAME, [], None);
531 io.send_packet(&LoginDisconnectS2c {
532 reason: Cow::Owned(JsonText(reason)),
533 })
534 .await?;
535 bail!("session server could not verify username");
536 }
537 status => {
538 bail!("session server GET request failed (status code {status})");
539 }
540 }
541
542 #[derive(Deserialize)]
543 struct GameProfile {
544 id: Uuid,
545 name: String,
546 properties: Vec<Property>,
547 }
548
549 let profile: GameProfile = resp.json().await.context("parsing game profile")?;
550
551 ensure!(profile.name == username, "usernames do not match");
552
553 Ok(NewClientInfo {
554 uuid: profile.id,
555 username,
556 ip: remote_addr.ip(),
557 properties: Properties(profile.properties),
558 view_distance: 0, locale: String::new(),
560 chat_mode: Default::default(),
561 chat_colors: false,
562 displayed_skin_parts: Default::default(),
563 main_arm: Default::default(),
564 enable_text_filtering: false,
565 allow_server_listings: false,
566 particle_mode: Default::default(),
567 })
568}
569
570fn auth_digest(bytes: &[u8]) -> String {
571 BigInt::from_signed_bytes_be(bytes).to_str_radix(16)
572}
573
574fn offline_uuid(username: &str) -> anyhow::Result<Uuid> {
575 Uuid::from_slice(&Sha256::digest(username)[..16]).map_err(Into::into)
576}
577
578fn login_offline(remote_addr: SocketAddr, username: String) -> anyhow::Result<NewClientInfo> {
580 Ok(NewClientInfo {
581 uuid: offline_uuid(username.as_str())?,
583 username,
584 properties: Default::default(),
585 ip: remote_addr.ip(),
586 view_distance: 0, locale: String::new(),
588 chat_mode: Default::default(),
589 chat_colors: false,
590 displayed_skin_parts: Default::default(),
591 main_arm: Default::default(),
592 enable_text_filtering: false,
593 allow_server_listings: false,
594 particle_mode: Default::default(),
595 })
596}
597
598fn login_bungeecord(
600 remote_addr: SocketAddr,
601 server_address: &str,
602 username: String,
603) -> anyhow::Result<NewClientInfo> {
604 let data = server_address.split('\0').take(4).collect::<Vec<_>>();
606
607 let ip = match data.get(1) {
609 Some(ip) => ip.parse()?,
610 None => remote_addr.ip(),
611 };
612
613 let uuid = match data.get(2) {
615 Some(uuid) => uuid.parse()?,
616 None => offline_uuid(username.as_str())?,
617 };
618
619 let properties: Vec<Property> = match data.get(3) {
623 Some(properties) => serde_json::from_str(properties)
624 .context("failed to parse BungeeCord player properties")?,
625 None => vec![],
626 };
627
628 Ok(NewClientInfo {
629 uuid,
630 username,
631 properties: Properties(properties),
632 ip,
633 view_distance: 0, locale: String::new(),
635 chat_mode: Default::default(),
636 chat_colors: false,
637 displayed_skin_parts: Default::default(),
638 main_arm: Default::default(),
639 enable_text_filtering: false,
640 allow_server_listings: false,
641 particle_mode: Default::default(),
642 })
643}
644
645async fn login_velocity(
647 io: &mut PacketIo,
648 username: String,
649 velocity_secret: &str,
650) -> anyhow::Result<NewClientInfo> {
651 let message_id: i32 = 0; io.send_packet(&CustomQueryS2c {
655 message_id: VarInt(message_id),
656 channel: ident!("velocity:player_info").into(),
657 data: RawBytes(&[VELOCITY_MIN_MAX_SUPPORTED_VERSION]).into(),
658 })
659 .await?;
660
661 let plugin_response: CustomQueryAnswerC2s = io.recv_packet().await?;
663
664 ensure!(
665 plugin_response.message_id.0 == message_id,
666 "mismatched plugin response ID (got {}, expected {message_id})",
667 plugin_response.message_id.0,
668 );
669
670 let data = plugin_response
671 .data
672 .context("missing plugin response data")?;
673 let payload = data.0;
674
675 parse_velocity_player_info(payload.0, username, velocity_secret)
676}
677
678fn parse_velocity_player_info(
679 data: &[u8],
680 username: String,
681 velocity_secret: &str,
682) -> anyhow::Result<NewClientInfo> {
683 ensure!(data.len() >= 32, "invalid plugin response data length");
684 let (signature, mut data_without_signature) = data.split_at(32);
685
686 let mut mac = Hmac::<Sha256>::new_from_slice(velocity_secret.as_bytes())?;
688 Mac::update(&mut mac, data_without_signature);
689 mac.verify_slice(signature)?;
690
691 let version = VarInt::decode(&mut data_without_signature)
693 .context("failed to decode velocity version")?
694 .0;
695
696 ensure!(version != i32::from(VELOCITY_MIN_MAX_SUPPORTED_VERSION), "Client tried to connect with an unsupported Velocity version: {version}. While we only support version {VELOCITY_MIN_MAX_SUPPORTED_VERSION}.");
697
698 let remote_addr = String::decode(&mut data_without_signature)?.parse()?;
700
701 let uuid = Uuid::decode(&mut data_without_signature)?;
703
704 ensure!(
706 username == <&str>::decode(&mut data_without_signature)?,
707 "mismatched usernames"
708 );
709
710 let properties = Vec::<Property>::decode(&mut data_without_signature)
712 .context("decoding velocity game profile properties")?;
713
714 Ok(NewClientInfo {
715 uuid,
716 username,
717 properties: Properties(properties),
718 ip: remote_addr,
719 view_distance: 0, locale: String::new(),
721 chat_mode: Default::default(),
722 chat_colors: false,
723 displayed_skin_parts: Default::default(),
724 main_arm: Default::default(),
725 enable_text_filtering: false,
726 allow_server_listings: false,
727 particle_mode: Default::default(),
728 })
729}
730
731#[cfg(test)]
732mod tests {
733 use sha1::Digest;
734
735 use super::*;
736
737 #[test]
738 fn auth_digest_usernames() {
739 assert_eq!(
740 auth_digest(&Sha1::digest("Notch")),
741 "4ed1f46bbe04bc756bcb17c0c7ce3e4632f06a48"
742 );
743 assert_eq!(
744 auth_digest(&Sha1::digest("jeb_")),
745 "-7c9d5b0044c130109a5d7b5fb5c317c02b4e28c1"
746 );
747 assert_eq!(
748 auth_digest(&Sha1::digest("simon")),
749 "88e16a1019277b15d58faf0541e11910eb756f6"
750 );
751 }
752}