valence_network/
connect.rs

1//! Handles new connections to the server and the log-in process.
2
3use std::io;
4use std::net::SocketAddr;
5use std::time::Duration;
6
7use anyhow::{bail, ensure, Context};
8use base64::prelude::*;
9use hmac::digest::Update;
10use hmac::{Hmac, Mac};
11use num_bigint::BigInt;
12use reqwest::StatusCode;
13use rsa::Pkcs1v15Encrypt;
14use serde::Deserialize;
15use serde_json::{json, Value};
16use sha1::Sha1;
17use sha2::{Digest, Sha256};
18use tokio::net::{TcpListener, TcpStream};
19use tracing::{error, info, trace, warn};
20use uuid::Uuid;
21use valence_lang::keys;
22use valence_protocol::profile::Property;
23use valence_protocol::Decode;
24use valence_server::client::Properties;
25use valence_server::protocol::packets::handshaking::handshake_c2s::HandshakeNextState;
26use valence_server::protocol::packets::handshaking::HandshakeC2s;
27use valence_server::protocol::packets::login::{
28    LoginCompressionS2c, LoginDisconnectS2c, LoginHelloC2s, LoginHelloS2c, LoginKeyC2s,
29    LoginQueryRequestS2c, LoginQueryResponseC2s, LoginSuccessS2c,
30};
31use valence_server::protocol::packets::status::{
32    QueryPingC2s, QueryPongS2c, QueryRequestC2s, QueryResponseS2c,
33};
34use valence_server::protocol::{PacketDecoder, PacketEncoder, RawBytes, VarInt};
35use valence_server::text::{Color, IntoText};
36use valence_server::{ident, Text, MINECRAFT_VERSION, PROTOCOL_VERSION};
37
38use crate::legacy_ping::try_handle_legacy_ping;
39use crate::packet_io::PacketIo;
40use crate::{CleanupOnDrop, ConnectionMode, NewClientInfo, ServerListPing, SharedNetworkState};
41
42/// Accepts new connections to the server as they occur.
43pub(super) async fn do_accept_loop(shared: SharedNetworkState) {
44    let listener = match TcpListener::bind(shared.0.address).await {
45        Ok(listener) => listener,
46        Err(e) => {
47            error!("failed to start TCP listener: {e}");
48            return;
49        }
50    };
51
52    let timeout = Duration::from_secs(5);
53
54    loop {
55        match shared.0.connection_sema.clone().acquire_owned().await {
56            Ok(permit) => match listener.accept().await {
57                Ok((stream, remote_addr)) => {
58                    let shared = shared.clone();
59
60                    tokio::spawn(async move {
61                        if let Err(e) = tokio::time::timeout(
62                            timeout,
63                            handle_connection(shared, stream, remote_addr),
64                        )
65                        .await
66                        {
67                            warn!("initial connection timed out: {e}");
68                        }
69
70                        drop(permit);
71                    });
72                }
73                Err(e) => {
74                    error!("failed to accept incoming connection: {e}");
75                }
76            },
77            // Closed semaphore indicates server shutdown.
78            Err(_) => return,
79        }
80    }
81}
82
83async fn handle_connection(
84    shared: SharedNetworkState,
85    mut stream: TcpStream,
86    remote_addr: SocketAddr,
87) {
88    trace!("handling connection");
89
90    if let Err(e) = stream.set_nodelay(true) {
91        error!("failed to set TCP_NODELAY: {e}");
92    }
93
94    match try_handle_legacy_ping(&shared, &mut stream, remote_addr).await {
95        Ok(true) => return, // Legacy ping succeeded.
96        Ok(false) => {}     // No legacy ping.
97        Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {}
98        Err(e) => {
99            warn!("legacy ping ended with error: {e:#}");
100        }
101    }
102
103    let io = PacketIo::new(stream, PacketEncoder::new(), PacketDecoder::new());
104
105    if let Err(e) = handle_handshake(shared, io, remote_addr).await {
106        // EOF can happen if the client disconnects while joining, which isn't
107        // very erroneous.
108        if let Some(e) = e.downcast_ref::<io::Error>() {
109            if e.kind() == io::ErrorKind::UnexpectedEof {
110                return;
111            }
112        }
113        warn!("connection ended with error: {e:#}");
114    }
115}
116
117/// Basic information about a client, provided at the beginning of the
118/// connection
119#[derive(Default, Debug)]
120pub struct HandshakeData {
121    /// The protocol version of the client.
122    pub protocol_version: i32,
123    /// The address that the client used to connect.
124    pub server_address: String,
125    /// The port that the client used to connect.
126    pub server_port: u16,
127}
128
129async fn handle_handshake(
130    shared: SharedNetworkState,
131    mut io: PacketIo,
132    remote_addr: SocketAddr,
133) -> anyhow::Result<()> {
134    let handshake = io.recv_packet::<HandshakeC2s>().await?;
135
136    let next_state = handshake.next_state;
137
138    let handshake = HandshakeData {
139        protocol_version: handshake.protocol_version.0,
140        server_address: handshake.server_address.0.to_owned(),
141        server_port: handshake.server_port,
142    };
143
144    // TODO: this is borked.
145    ensure!(
146        shared.0.connection_mode == ConnectionMode::BungeeCord
147            || handshake.server_address.encode_utf16().count() <= 255,
148        "handshake server address is too long"
149    );
150
151    match next_state {
152        HandshakeNextState::Status => handle_status(shared, io, remote_addr, handshake)
153            .await
154            .context("handling status"),
155        HandshakeNextState::Login => {
156            match handle_login(&shared, &mut io, remote_addr, handshake)
157                .await
158                .context("handling login")?
159            {
160                Some((info, cleanup)) => {
161                    let client = io.into_client_args(
162                        info,
163                        shared.0.incoming_byte_limit,
164                        shared.0.outgoing_byte_limit,
165                        cleanup,
166                    );
167
168                    let _ = shared.0.new_clients_send.send_async(client).await;
169
170                    Ok(())
171                }
172                None => Ok(()),
173            }
174        }
175    }
176}
177
178async fn handle_status(
179    shared: SharedNetworkState,
180    mut io: PacketIo,
181    remote_addr: SocketAddr,
182    handshake: HandshakeData,
183) -> anyhow::Result<()> {
184    io.recv_packet::<QueryRequestC2s>().await?;
185
186    match shared
187        .0
188        .callbacks
189        .inner
190        .server_list_ping(&shared, remote_addr, &handshake)
191        .await
192    {
193        ServerListPing::Respond {
194            online_players,
195            max_players,
196            player_sample,
197            mut description,
198            favicon_png,
199            version_name,
200            protocol,
201        } => {
202            // For pre-1.16 clients, replace all webcolors with their closest
203            // normal colors Because webcolor support was only
204            // added at 1.16.
205            if handshake.protocol_version < 735 {
206                fn fallback_webcolors(txt: &mut Text) {
207                    if let Some(Color::Rgb(color)) = txt.color {
208                        txt.color = Some(Color::Named(color.to_named_lossy()));
209                    }
210                    for child in &mut txt.extra {
211                        fallback_webcolors(child);
212                    }
213                }
214
215                fallback_webcolors(&mut description);
216            }
217
218            let mut json = json!({
219                "version": {
220                    "name": version_name,
221                    "protocol": protocol,
222                },
223                "players": {
224                    "online": online_players,
225                    "max": max_players,
226                    "sample": player_sample,
227                },
228                "description": description,
229            });
230
231            if !favicon_png.is_empty() {
232                let mut buf = "data:image/png;base64,".to_owned();
233                BASE64_STANDARD.encode_string(favicon_png, &mut buf);
234                json["favicon"] = Value::String(buf);
235            }
236
237            io.send_packet(&QueryResponseS2c {
238                json: &json.to_string(),
239            })
240            .await?;
241        }
242        ServerListPing::Ignore => return Ok(()),
243    }
244
245    let QueryPingC2s { payload } = io.recv_packet().await?;
246
247    io.send_packet(&QueryPongS2c { payload }).await?;
248
249    Ok(())
250}
251
252/// Handle the login process and return the new client's data if successful.
253async fn handle_login(
254    shared: &SharedNetworkState,
255    io: &mut PacketIo,
256    remote_addr: SocketAddr,
257    handshake: HandshakeData,
258) -> anyhow::Result<Option<(NewClientInfo, CleanupOnDrop)>> {
259    if handshake.protocol_version != PROTOCOL_VERSION {
260        io.send_packet(&LoginDisconnectS2c {
261            // TODO: use correct translation key.
262            reason: format!("Mismatched Minecraft version (server is on {MINECRAFT_VERSION})")
263                .color(Color::RED)
264                .into(),
265        })
266        .await?;
267
268        return Ok(None);
269    }
270
271    let LoginHelloC2s {
272        username,
273        .. // TODO: profile_id
274    } = io.recv_packet().await?;
275
276    let username = username.0.to_owned();
277
278    let info = match shared.connection_mode() {
279        ConnectionMode::Online { .. } => login_online(shared, io, remote_addr, username).await?,
280        ConnectionMode::Offline => login_offline(remote_addr, username)?,
281        ConnectionMode::BungeeCord => {
282            login_bungeecord(remote_addr, &handshake.server_address, username)?
283        }
284        ConnectionMode::Velocity { secret } => login_velocity(io, username, secret).await?,
285    };
286
287    if shared.0.threshold.0 > 0 {
288        io.send_packet(&LoginCompressionS2c {
289            threshold: shared.0.threshold.0.into(),
290        })
291        .await?;
292
293        io.set_compression(shared.0.threshold);
294    }
295
296    let cleanup = match shared.0.callbacks.inner.login(shared, &info).await {
297        Ok(f) => CleanupOnDrop(Some(f)),
298        Err(reason) => {
299            info!("disconnect at login: \"{reason}\"");
300            io.send_packet(&LoginDisconnectS2c {
301                reason: reason.into(),
302            })
303            .await?;
304            return Ok(None);
305        }
306    };
307
308    io.send_packet(&LoginSuccessS2c {
309        uuid: info.uuid,
310        username: info.username.as_str().into(),
311        properties: Default::default(),
312    })
313    .await?;
314
315    Ok(Some((info, cleanup)))
316}
317
318/// Login procedure for online mode.
319async fn login_online(
320    shared: &SharedNetworkState,
321    io: &mut PacketIo,
322    remote_addr: SocketAddr,
323    username: String,
324) -> anyhow::Result<NewClientInfo> {
325    let my_verify_token: [u8; 16] = rand::random();
326
327    io.send_packet(&LoginHelloS2c {
328        server_id: "".into(), // Always empty
329        public_key: &shared.0.public_key_der,
330        verify_token: &my_verify_token,
331    })
332    .await?;
333
334    let LoginKeyC2s {
335        shared_secret,
336        verify_token: encrypted_verify_token,
337    } = io.recv_packet().await?;
338
339    let shared_secret = shared
340        .0
341        .rsa_key
342        .decrypt(Pkcs1v15Encrypt, shared_secret)
343        .context("failed to decrypt shared secret")?;
344
345    let verify_token = shared
346        .0
347        .rsa_key
348        .decrypt(Pkcs1v15Encrypt, encrypted_verify_token)
349        .context("failed to decrypt verify token")?;
350
351    ensure!(
352        my_verify_token.as_slice() == verify_token,
353        "verify tokens do not match"
354    );
355
356    let crypt_key: [u8; 16] = shared_secret
357        .as_slice()
358        .try_into()
359        .context("shared secret has the wrong length")?;
360
361    io.enable_encryption(&crypt_key);
362
363    let hash = Sha1::new()
364        .chain(&shared_secret)
365        .chain(&shared.0.public_key_der)
366        .finalize();
367
368    let url = shared
369        .0
370        .callbacks
371        .inner
372        .session_server(
373            shared,
374            username.as_str(),
375            &auth_digest(&hash),
376            &remote_addr.ip(),
377        )
378        .await;
379
380    let resp = shared.0.http_client.get(url).send().await?;
381
382    match resp.status() {
383        StatusCode::OK => {}
384        StatusCode::NO_CONTENT => {
385            let reason = Text::translate(keys::MULTIPLAYER_DISCONNECT_UNVERIFIED_USERNAME, []);
386            io.send_packet(&LoginDisconnectS2c {
387                reason: reason.into(),
388            })
389            .await?;
390            bail!("session server could not verify username");
391        }
392        status => {
393            bail!("session server GET request failed (status code {status})");
394        }
395    }
396
397    #[derive(Deserialize)]
398    struct GameProfile {
399        id: Uuid,
400        name: String,
401        properties: Vec<Property>,
402    }
403
404    let profile: GameProfile = resp.json().await.context("parsing game profile")?;
405
406    ensure!(profile.name == username, "usernames do not match");
407
408    Ok(NewClientInfo {
409        uuid: profile.id,
410        username,
411        ip: remote_addr.ip(),
412        properties: Properties(profile.properties),
413    })
414}
415
416fn auth_digest(bytes: &[u8]) -> String {
417    BigInt::from_signed_bytes_be(bytes).to_str_radix(16)
418}
419
420fn offline_uuid(username: &str) -> anyhow::Result<Uuid> {
421    Uuid::from_slice(&Sha256::digest(username)[..16]).map_err(Into::into)
422}
423
424/// Login procedure for offline mode.
425fn login_offline(remote_addr: SocketAddr, username: String) -> anyhow::Result<NewClientInfo> {
426    Ok(NewClientInfo {
427        // Derive the client's UUID from a hash of their username.
428        uuid: offline_uuid(username.as_str())?,
429        username,
430        properties: Default::default(),
431        ip: remote_addr.ip(),
432    })
433}
434
435/// Login procedure for `BungeeCord`.
436fn login_bungeecord(
437    remote_addr: SocketAddr,
438    server_address: &str,
439    username: String,
440) -> anyhow::Result<NewClientInfo> {
441    // Get data from server_address field of the handshake
442    let data = server_address.split('\0').take(4).collect::<Vec<_>>();
443
444    // Ip of player, only given if ip_forward on bungee is true
445    let ip = match data.get(1) {
446        Some(ip) => ip.parse()?,
447        None => remote_addr.ip(),
448    };
449
450    // Uuid of player, only given if ip_forward on bungee is true
451    let uuid = match data.get(2) {
452        Some(uuid) => uuid.parse()?,
453        None => offline_uuid(username.as_str())?,
454    };
455
456    // Read properties and get textures
457    // Properties of player's game profile, only given if ip_forward and online_mode
458    // on bungee both are true
459    let properties: Vec<Property> = match data.get(3) {
460        Some(properties) => serde_json::from_str(properties)
461            .context("failed to parse BungeeCord player properties")?,
462        None => vec![],
463    };
464
465    Ok(NewClientInfo {
466        uuid,
467        username,
468        properties: Properties(properties),
469        ip,
470    })
471}
472
473/// Login procedure for Velocity.
474async fn login_velocity(
475    io: &mut PacketIo,
476    username: String,
477    velocity_secret: &str,
478) -> anyhow::Result<NewClientInfo> {
479    const VELOCITY_MIN_SUPPORTED_VERSION: u8 = 1;
480    const VELOCITY_MODERN_FORWARDING_WITH_KEY_V2: i32 = 3;
481
482    let message_id: i32 = 0; // TODO: make this random?
483
484    // Send Player Info Request into the Plugin Channel
485    io.send_packet(&LoginQueryRequestS2c {
486        message_id: VarInt(message_id),
487        channel: ident!("velocity:player_info").into(),
488        data: RawBytes(&[VELOCITY_MIN_SUPPORTED_VERSION]).into(),
489    })
490    .await?;
491
492    // Get Response
493    let plugin_response: LoginQueryResponseC2s = io.recv_packet().await?;
494
495    ensure!(
496        plugin_response.message_id.0 == message_id,
497        "mismatched plugin response ID (got {}, expected {message_id})",
498        plugin_response.message_id.0,
499    );
500
501    let data = plugin_response
502        .data
503        .context("missing plugin response data")?
504        .0;
505
506    ensure!(data.len() >= 32, "invalid plugin response data length");
507    let (signature, mut data_without_signature) = data.split_at(32);
508
509    // Verify signature
510    let mut mac = Hmac::<Sha256>::new_from_slice(velocity_secret.as_bytes())?;
511    Mac::update(&mut mac, data_without_signature);
512    mac.verify_slice(signature)?;
513
514    // Check Velocity version
515    let version = VarInt::decode(&mut data_without_signature)
516        .context("failed to decode velocity version")?
517        .0;
518
519    // Get client address
520    let remote_addr = String::decode(&mut data_without_signature)?.parse()?;
521
522    // Get UUID
523    let uuid = Uuid::decode(&mut data_without_signature)?;
524
525    // Get username and validate
526    ensure!(
527        username == <&str>::decode(&mut data_without_signature)?,
528        "mismatched usernames"
529    );
530
531    // Read game profile properties
532    let properties = Vec::<Property>::decode(&mut data_without_signature)
533        .context("decoding velocity game profile properties")?;
534
535    if version >= VELOCITY_MODERN_FORWARDING_WITH_KEY_V2 {
536        // TODO
537    }
538
539    Ok(NewClientInfo {
540        uuid,
541        username,
542        properties: Properties(properties),
543        ip: remote_addr,
544    })
545}
546
547#[cfg(test)]
548mod tests {
549    use sha1::Digest;
550
551    use super::*;
552
553    #[test]
554    fn auth_digest_usernames() {
555        assert_eq!(
556            auth_digest(&Sha1::digest("Notch")),
557            "4ed1f46bbe04bc756bcb17c0c7ce3e4632f06a48"
558        );
559        assert_eq!(
560            auth_digest(&Sha1::digest("jeb_")),
561            "-7c9d5b0044c130109a5d7b5fb5c317c02b4e28c1"
562        );
563        assert_eq!(
564            auth_digest(&Sha1::digest("simon")),
565            "88e16a1019277b15d58faf0541e11910eb756f6"
566        );
567    }
568}