chunkedge_network/
connect.rs

1//! Handles new connections to the server and the log-in process.
2
3use 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
58/// Accepts new connections to the server as they occur.
59pub(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            // Closed semaphore indicates server shutdown.
94            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, // Legacy ping succeeded.
113        Ok(false) => {}     // No legacy ping.
114        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        // EOF can happen if the client disconnects while joining, which isn't
124        // very erroneous.
125        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/// Basic information about a client, provided at the beginning of the
135/// connection
136#[derive(Default, Debug)]
137pub struct HandshakeData {
138    /// The protocol version of the client.
139    pub protocol_version: i32,
140    /// The address that the client used to connect.
141    pub server_address: String,
142    /// The port that the client used to connect.
143    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    // TODO: this is borked.
163    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            // TODO: Implement
195            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            // For pre-1.16 clients, replace all webcolors with their closest
225            // normal colors Because webcolor support was only
226            // added at 1.16.
227            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
275/// Handle the login process and return the new client's data if successful.
276async 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            // TODO: use correct translation key.
286            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        .. // TODO: profile_id
299    } = 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    // We have chunkedge support for the `worldgen/biome` and `dimension_type`
379    // registries, therefore we use the current state of these registries here
380    // (instead of the default values) This means the server can add/remove
381    // biomes and dimensions at runtime.
382
383    // BiomeRegistry
384    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    // DimensionTypeRegistry
404    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    // Send all other registries.
424    //
425    // Even if the remote end acknowledges the vanilla known pack, send the full
426    // element data. Some protocol translators forward registry entries to newer
427    // clients, and omitted entries force the client to resolve them from local
428    // resources for the server's pack version.
429    for (id, entries) in RegistryCodec::default().registries {
430        if id == ident!("worldgen/biome") || id == ident!("dimension_type") {
431            // We already sent these registries.
432            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    // TagsRegistry
446    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
461/// Login procedure for online mode.
462async 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(), // Always empty
472        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, // Will be changed later.
559        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
578/// Login procedure for offline mode.
579fn login_offline(remote_addr: SocketAddr, username: String) -> anyhow::Result<NewClientInfo> {
580    Ok(NewClientInfo {
581        // Derive the client's UUID from a hash of their username.
582        uuid: offline_uuid(username.as_str())?,
583        username,
584        properties: Default::default(),
585        ip: remote_addr.ip(),
586        view_distance: 0, // Will be changed later.
587        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
598/// Login procedure for `BungeeCord`.
599fn login_bungeecord(
600    remote_addr: SocketAddr,
601    server_address: &str,
602    username: String,
603) -> anyhow::Result<NewClientInfo> {
604    // Get data from server_address field of the handshake
605    let data = server_address.split('\0').take(4).collect::<Vec<_>>();
606
607    // Ip of player, only given if ip_forward on bungee is true
608    let ip = match data.get(1) {
609        Some(ip) => ip.parse()?,
610        None => remote_addr.ip(),
611    };
612
613    // Uuid of player, only given if ip_forward on bungee is true
614    let uuid = match data.get(2) {
615        Some(uuid) => uuid.parse()?,
616        None => offline_uuid(username.as_str())?,
617    };
618
619    // Read properties and get textures
620    // Properties of player's game profile, only given if ip_forward and online_mode
621    // on bungee both are true
622    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, // Will be changed later.
634        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
645/// Login procedure for Velocity.
646async fn login_velocity(
647    io: &mut PacketIo,
648    username: String,
649    velocity_secret: &str,
650) -> anyhow::Result<NewClientInfo> {
651    let message_id: i32 = 0; // TODO: make this random?
652
653    // Send Player Info Request into the Plugin Channel
654    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    // Get Response
662    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    // Verify signature
687    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    // Check Velocity version
692    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    // Get client address
699    let remote_addr = String::decode(&mut data_without_signature)?.parse()?;
700
701    // Get UUID
702    let uuid = Uuid::decode(&mut data_without_signature)?;
703
704    // Get username and validate
705    ensure!(
706        username == <&str>::decode(&mut data_without_signature)?,
707        "mismatched usernames"
708    );
709
710    // Read game profile properties
711    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, // Will be changed later.
720        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}