Begin work on higher-level network abstractions.
parent
389bafe89d
commit
9acc85be48
241
src/main.rs
241
src/main.rs
|
@ -4,12 +4,6 @@
|
|||
|
||||
mod net;
|
||||
|
||||
use crate::net::chat::Chat;
|
||||
use crate::net::packet_stream::{Client, PacketStream, PacketStreamMaps};
|
||||
use crate::net::protocol::state::handshake::Handshake;
|
||||
use crate::net::protocol::state::login::Login;
|
||||
use crate::net::protocol::state::play::Play;
|
||||
use crate::net::protocol::state::status::Status;
|
||||
use std::io;
|
||||
use std::net::IpAddr;
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
|
@ -54,202 +48,87 @@ async fn listen(mut listener: TcpListener) {
|
|||
}
|
||||
}
|
||||
|
||||
async fn accept_connection(socket: TcpStream) {
|
||||
let con = PacketStream::new(Box::new(socket));
|
||||
async fn accept_connection(socket: TcpStream) -> Option<!> {
|
||||
use crate::net::chat::Chat;
|
||||
use crate::net::listener::*;
|
||||
use crate::net::packet_stream::PacketStreamMaps;
|
||||
use crate::net::protocol::state::play::*;
|
||||
|
||||
eprintln!("Client connected.");
|
||||
match interact_handshake(con).await {
|
||||
Err(err) => {
|
||||
eprintln!("Client disconnected with error: {:?}", err);
|
||||
},
|
||||
Ok(_) => {
|
||||
eprintln!("Client disconnected without error.");
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn mk_err<A, S: std::borrow::Borrow<str>>(str: S) -> io::Result<A> {
|
||||
Err(io::Error::new(io::ErrorKind::Other, str.borrow().to_string()))
|
||||
}
|
||||
|
||||
async fn interact_handshake(mut con: PacketStream<Client, Handshake>) -> io::Result<()> {
|
||||
use crate::net::protocol::state::handshake::*;
|
||||
|
||||
match con.recieve().await? {
|
||||
Serverbound::HandshakePkt(handshake) => {
|
||||
use HandshakeNextState::*;
|
||||
|
||||
match handshake.next_state {
|
||||
Status => interact_status(con.into_status()).await,
|
||||
Login => interact_login(con.into_login()).await,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async fn interact_status(mut con: PacketStream<Client, Status>) -> io::Result<()> {
|
||||
use crate::net::protocol::state::status::*;
|
||||
|
||||
loop {
|
||||
match con.recieve().await? {
|
||||
Serverbound::Request(Request {}) => {
|
||||
con.send(&Clientbound::Response(Response {
|
||||
data: ResponseData {
|
||||
version: ResponseVersion {
|
||||
name: "1.16.1".to_string(),
|
||||
protocol: 736,
|
||||
},
|
||||
players: ResponsePlayers {
|
||||
max: 255,
|
||||
online: 0,
|
||||
sample: Vec::new(),
|
||||
},
|
||||
description: Chat {
|
||||
text: "Hello, world!".to_string(),
|
||||
},
|
||||
favicon: None,
|
||||
},
|
||||
}))
|
||||
.await?;
|
||||
},
|
||||
Serverbound::Ping(ping) => {
|
||||
con.send(&Clientbound::Pong(Pong { payload: ping.payload })).await?;
|
||||
|
||||
// The status ping is now over so the server ends the connection.
|
||||
return Ok(());
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn interact_login(mut con: PacketStream<Client, Login>) -> io::Result<()> {
|
||||
use crate::net::protocol::state::login::*;
|
||||
|
||||
let name = match con.recieve().await? {
|
||||
Serverbound::LoginStart(login_start) => login_start.name,
|
||||
_ => {
|
||||
con.send(&Clientbound::Disconnect(Disconnect {
|
||||
reason: Chat {
|
||||
text: "Unexpected packet (expected Login Start).".to_string(),
|
||||
},
|
||||
}))
|
||||
.await?;
|
||||
return mk_err("Unexpected packet (expected Login Start).");
|
||||
},
|
||||
};
|
||||
#[cfg(not(feature = "compression"))]
|
||||
let compression_threshold = None;
|
||||
#[cfg(feature = "compression")]
|
||||
let compression_threshold = Some(64);
|
||||
|
||||
#[cfg(not(feature = "encryption"))]
|
||||
let encryption_config = None;
|
||||
#[cfg(feature = "encryption")]
|
||||
{
|
||||
use rand::rngs::OsRng;
|
||||
use rand::Rng;
|
||||
use rsa::{PaddingScheme, RSAPrivateKey};
|
||||
|
||||
let encryption_config = Some({
|
||||
use rsa::RSAPrivateKey;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
let mut public_key = Vec::new();
|
||||
|
||||
let mut public_key_bytes = Vec::new();
|
||||
File::open("private/pub.der")
|
||||
.expect("missing public key")
|
||||
.read_to_end(&mut public_key)?;
|
||||
.read_to_end(&mut public_key_bytes)
|
||||
.unwrap();
|
||||
|
||||
let mut private_key = Vec::new();
|
||||
let mut private_key_bytes = Vec::new();
|
||||
File::open("private/priv.der")
|
||||
.expect("missing private key")
|
||||
.read_to_end(&mut private_key)?;
|
||||
.read_to_end(&mut private_key_bytes)
|
||||
.unwrap();
|
||||
|
||||
let key = RSAPrivateKey::from_pkcs1(&private_key).expect("Invalid private key.");
|
||||
let private_key = RSAPrivateKey::from_pkcs1(&private_key_bytes).expect("Invalid private key.");
|
||||
|
||||
let mut verify_token = Vec::new();
|
||||
verify_token.resize(4, 0u8);
|
||||
OsRng.fill(verify_token.as_mut_slice());
|
||||
|
||||
let server_id = "";
|
||||
|
||||
con.send(&Clientbound::EncryptionRequest(EncryptionRequest {
|
||||
server_id: server_id.to_string().into_boxed_str(),
|
||||
public_key: public_key.clone().into_boxed_slice(),
|
||||
verify_token: verify_token.clone().into_boxed_slice(),
|
||||
}))
|
||||
.await?;
|
||||
|
||||
let secret = match con.recieve().await? {
|
||||
Serverbound::EncryptionResponse(encryption_response) => {
|
||||
let token = key
|
||||
.decrypt(PaddingScheme::PKCS1v15Encrypt, &encryption_response.verify_token)
|
||||
.expect("Failed to decrypt verify token.");
|
||||
if token.as_slice() != verify_token.as_slice() {
|
||||
return mk_err("Incorrect verify token.");
|
||||
}
|
||||
|
||||
key.decrypt(PaddingScheme::PKCS1v15Encrypt, &encryption_response.shared_secret)
|
||||
.expect("Failed to decrypt shared secret.")
|
||||
},
|
||||
_ => {
|
||||
return mk_err("Unexpected packet (expected Encryption Response).");
|
||||
},
|
||||
};
|
||||
|
||||
con.enable_encryption(&secret).expect("Failed to set encryption.");
|
||||
|
||||
#[cfg(feature = "authentication")]
|
||||
{
|
||||
let server_hash = {
|
||||
let server_hash_bytes = {
|
||||
use sha1::{Digest, Sha1};
|
||||
let mut hasher = Sha1::new();
|
||||
hasher.update(server_id.as_bytes());
|
||||
hasher.update(&secret);
|
||||
hasher.update(&public_key);
|
||||
hasher.finalize()
|
||||
};
|
||||
|
||||
format!("{:x}", num_bigint::BigInt::from_signed_bytes_be(&server_hash_bytes))
|
||||
};
|
||||
|
||||
use reqwest::Client;
|
||||
|
||||
println!(
|
||||
"{:?}",
|
||||
Client::new()
|
||||
.get("https://sessionserver.mojang.com/session/minecraft/hasJoined")
|
||||
.header("Content-Type", "application/json")
|
||||
.query(&[("username", name.clone()), ("serverId", server_hash.into_boxed_str())])
|
||||
.send()
|
||||
.await
|
||||
.expect("Request failed.")
|
||||
.text()
|
||||
.await
|
||||
.unwrap()
|
||||
);
|
||||
EncryptionConfig {
|
||||
private_key,
|
||||
public_key_bytes: public_key_bytes.into_boxed_slice(),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
#[cfg(feature = "compression")]
|
||||
{
|
||||
con.send(&Clientbound::SetCompression(SetCompression {
|
||||
threshold: crate::net::serialize::VarInt(64),
|
||||
}))
|
||||
.await?;
|
||||
con.set_compression(Some(64));
|
||||
}
|
||||
#[cfg(not(feature = "authentication"))]
|
||||
let authentication_config = None;
|
||||
#[cfg(feature = "authentication")]
|
||||
let authentication_config = Some(AuthenticationConfig::default());
|
||||
|
||||
con.send(&Clientbound::LoginSuccess(LoginSuccess {
|
||||
uuid: uuid::Uuid::nil(),
|
||||
username: name,
|
||||
}))
|
||||
let config = ConnectionConfig {
|
||||
enforce_host: Some(vec!["localhost".to_string().into_boxed_str()].into_boxed_slice()),
|
||||
compression_threshold,
|
||||
encryption_config,
|
||||
authentication_config,
|
||||
};
|
||||
|
||||
let (mut con, login_state) = setup_connection(&config, Box::new(socket), || {
|
||||
use crate::net::protocol::state::status::{ResponseData, ResponsePlayers, ResponseVersion};
|
||||
|
||||
ResponseData {
|
||||
version: ResponseVersion {
|
||||
name: "1.16.1".to_string().into_boxed_str(),
|
||||
protocol: 736,
|
||||
},
|
||||
players: ResponsePlayers {
|
||||
max: 255,
|
||||
online: 0,
|
||||
sample: Vec::new(),
|
||||
},
|
||||
description: Chat {
|
||||
text: "Hello, world!".to_string().into_boxed_str(),
|
||||
},
|
||||
favicon: None,
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
|
||||
interact_play(con.into_play()).await
|
||||
}
|
||||
|
||||
async fn interact_play(mut con: PacketStream<Client, Play>) -> io::Result<()> {
|
||||
use crate::net::protocol::state::play::*;
|
||||
eprintln!("Client logged in as: {:?}", login_state);
|
||||
|
||||
con.send(&Clientbound::Disconnect(Disconnect {
|
||||
reason: Chat {
|
||||
text: "Goodbye!".to_string(),
|
||||
text: "Goodbye!".to_string().into_boxed_str(),
|
||||
},
|
||||
}))
|
||||
.await?;
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
Ok(())
|
||||
None
|
||||
}
|
||||
|
|
|
@ -3,5 +3,5 @@ use serde::{Deserialize, Serialize};
|
|||
// TODO: Support more features.
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Chat {
|
||||
pub text: String,
|
||||
pub text: Box<str>,
|
||||
}
|
||||
|
|
|
@ -0,0 +1,419 @@
|
|||
use crate::net::chat::Chat;
|
||||
pub use crate::net::packet_stream::CompressionThreshold;
|
||||
use crate::net::packet_stream::{Client, PacketStream, PacketStreamMaps};
|
||||
use crate::net::protocol::state::handshake;
|
||||
use crate::net::protocol::state::login;
|
||||
use crate::net::protocol::state::login::Login;
|
||||
use crate::net::protocol::state::play::Play;
|
||||
use crate::net::protocol::state::status;
|
||||
use crate::net::protocol::state::status::{ResponseData, Status};
|
||||
use crate::net::Stream;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// The configuration necessary to use Minecraft protocol encryption.
|
||||
#[cfg(feature = "encryption")]
|
||||
#[derive(Clone)]
|
||||
pub struct EncryptionConfig {
|
||||
/// An RSA key pair. The Notchian server uses a 1024-bit key.
|
||||
/// You don't need to worry about signing it or anything like that,
|
||||
/// because it isn't ever checked for authenticity.
|
||||
///
|
||||
/// The Notchian server generates a fresh key every time the server starts.
|
||||
/// The rsa crate allows doing something similar using [`rsa::RSAPrivateKey::new`],
|
||||
/// but this is unusable because you need a DER-formatted public key for [`public_key_bytes`],
|
||||
/// and currently the rsa crate does not allow creating one.
|
||||
/// Instead, I would recommend reading a private key using [`rsa::RSAPrivateKey::from_pkcs1`].
|
||||
///
|
||||
/// You can generate a suitable private key for this purpose using OpenSSL:
|
||||
///
|
||||
/// ```bash
|
||||
/// openssl genpkey -algorithm RSA -outform der -pkeyopt rsa_keygen_bits:1024 -out priv.der
|
||||
/// ```
|
||||
pub private_key: rsa::RSAPrivateKey,
|
||||
|
||||
/// The verbatim bytes of a DER-encoded RSA public key.
|
||||
///
|
||||
/// You can generate this from your private key using OpenSSL:
|
||||
///
|
||||
/// ```bash
|
||||
/// openssl rsa -pubout -inform der -outform der -in priv.der -out pub.der
|
||||
/// ```
|
||||
///
|
||||
/// There is no reason it shouldn't be possible to extract this from the `private_key`,
|
||||
/// but the crate I use for RSA doesn't currently support it.
|
||||
pub public_key_bytes: Box<[u8]>,
|
||||
}
|
||||
#[cfg(not(feature = "encryption"))]
|
||||
type EncryptionConfig = !;
|
||||
|
||||
/// The configuration necessary to allow the server to run in online mode.
|
||||
/// The default value will be suitable in most cases.
|
||||
#[cfg(feature = "authentication")]
|
||||
#[derive(Clone)]
|
||||
pub struct AuthenticationConfig {
|
||||
/// This is the base URL that the server will send the player's username
|
||||
/// and the shared server id hash to to check whether the user is who they claim to be.
|
||||
///
|
||||
/// There's no real need to know the technical details:
|
||||
/// if you don't know what this is, just use the default value,
|
||||
/// which will allow login using Mojang/minecraft.net accounts,
|
||||
/// which is probably what you want.
|
||||
pub has_joined_endpoint: reqwest::Url,
|
||||
|
||||
/// Enable a limited form of defense against
|
||||
/// proxied connections using the authentication server.
|
||||
/// This checks if the IP the client authenticated with
|
||||
/// is the same as the IP of the client connected to the server,
|
||||
/// and if it's not, the client is kicked.
|
||||
pub prevent_proxy_connections: bool,
|
||||
|
||||
/// Allow players to join the server even if authentication fails,
|
||||
/// treating them as though the server were in offline mode.
|
||||
/// **You probably want this to be false.**
|
||||
pub allow_unauthenticated_players: bool,
|
||||
}
|
||||
#[cfg(not(feature = "authentication"))]
|
||||
type AuthenticationConfig = !;
|
||||
|
||||
#[cfg(feature = "authentication")]
|
||||
impl std::default::Default for AuthenticationConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
has_joined_endpoint: reqwest::Url::parse("https://sessionserver.mojang.com/session/minecraft/hasJoined")
|
||||
.unwrap(),
|
||||
prevent_proxy_connections: false,
|
||||
allow_unauthenticated_players: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for how connections with clients should be set up.
|
||||
#[derive(Clone)]
|
||||
pub struct ConnectionConfig {
|
||||
/// Enforce that one of the correct hostnames or IP addresses is used to connect to the server.
|
||||
/// This gives you flexibility as a server admin
|
||||
/// (e.g. you can update your SRV record or IP address without losing players
|
||||
/// who are connecting directly via IP, because they never count),
|
||||
/// and adds security (to prevent someone from pointing
|
||||
/// to their name and then maliciously changing it later).
|
||||
pub enforce_host: Option<Box<[Box<str>]>>,
|
||||
|
||||
/// The protocol compression theshold (i.e. the minimum packet size to compress).
|
||||
/// Setting this to `None` disables compression.
|
||||
pub compression_threshold: Option<CompressionThreshold>,
|
||||
|
||||
/// The protocol encryption configuration.
|
||||
/// Setting this to `None` disables encryption *and authentication, which requires encryption*.
|
||||
pub encryption_config: Option<EncryptionConfig>,
|
||||
|
||||
/// The authentication configuration.
|
||||
/// Setting this to `None` disables authentication.
|
||||
/// *Authentication requires encryption, so if encryption is disabled, this will be ignored.*
|
||||
pub authentication_config: Option<AuthenticationConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LoginState {
|
||||
name: Box<str>,
|
||||
/// None if the user is logged in but not authenticated.
|
||||
uuid: Option<Uuid>,
|
||||
}
|
||||
|
||||
pub async fn setup_connection<F>(
|
||||
cfg: &ConnectionConfig,
|
||||
stream: Box<dyn Stream>,
|
||||
get_status: F,
|
||||
) -> Option<(PacketStream<Client, Play>, LoginState)>
|
||||
where
|
||||
F: FnOnce() -> ResponseData,
|
||||
{
|
||||
let mut con = PacketStream::new(stream);
|
||||
|
||||
let handshake::Serverbound::HandshakePkt(handshake_pkt) = con.receive().await.ok()?;
|
||||
|
||||
let mut con = match handshake_pkt.next_state {
|
||||
handshake::HandshakeNextState::Status => {
|
||||
respond_status(con.into_status(), get_status).await?;
|
||||
},
|
||||
handshake::HandshakeNextState::Login => con.into_login(),
|
||||
};
|
||||
|
||||
if let Some(hosts) = &cfg.enforce_host {
|
||||
enforce_hosts(&mut con, &hosts, &handshake_pkt.server_address).await?;
|
||||
}
|
||||
|
||||
use login::*;
|
||||
|
||||
let name = match con.receive().await.ok()? {
|
||||
Serverbound::LoginStart(LoginStart { name }) => name,
|
||||
_ => {
|
||||
con.send(&Clientbound::Disconnect(Disconnect {
|
||||
reason: Chat {
|
||||
text: "You're supposed to send a LoginStart packet!"
|
||||
.to_string()
|
||||
.into_boxed_str(),
|
||||
},
|
||||
}))
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
return None;
|
||||
},
|
||||
};
|
||||
|
||||
let uuid = if let Some(ecfg) = &cfg.encryption_config {
|
||||
let shared_secret = enable_encryption(&mut con, &ecfg).await?;
|
||||
if let Some(acfg) = &cfg.authentication_config {
|
||||
authenticate(&mut con, &ecfg, &acfg, &name, &shared_secret).await?
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(threshold) = cfg.compression_threshold {
|
||||
con.send(&Clientbound::SetCompression(SetCompression {
|
||||
threshold: (threshold as i32).into(),
|
||||
}))
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
con.set_compression(Some(threshold));
|
||||
}
|
||||
|
||||
con.send(&Clientbound::LoginSuccess(LoginSuccess {
|
||||
uuid: uuid.unwrap_or_else(Uuid::nil),
|
||||
username: name.clone(),
|
||||
}))
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
let con = con.into_play();
|
||||
|
||||
let login_state = LoginState { name, uuid };
|
||||
|
||||
Some((con, login_state))
|
||||
}
|
||||
|
||||
async fn respond_status<F>(mut con: PacketStream<Client, Status>, get_status: F) -> Option<!>
|
||||
where
|
||||
F: FnOnce() -> ResponseData,
|
||||
{
|
||||
use status::*;
|
||||
|
||||
// There's no reason theoretically that we couldn't
|
||||
// accept these packets repeated or out of order,
|
||||
// but the `setup_connection` function is supposed to return in finite time,
|
||||
// and looping here would violate that contract.
|
||||
|
||||
match con.receive().await.ok()? {
|
||||
Serverbound::Request(_) => {
|
||||
con.send(&Clientbound::Response(Response { data: get_status() }))
|
||||
.await
|
||||
.ok()?;
|
||||
},
|
||||
// That's not how the status ping is supposed to work!
|
||||
_ => return None,
|
||||
}
|
||||
|
||||
match con.receive().await.ok()? {
|
||||
Serverbound::Ping(status::Ping { payload }) => {
|
||||
con.send(&Clientbound::Pong(Pong { payload })).await.ok()?;
|
||||
},
|
||||
_ => return None,
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
async fn enforce_hosts(con: &mut PacketStream<Client, Login>, hosts: &[Box<str>], server_address: &str) -> Option<()> {
|
||||
use login::*;
|
||||
|
||||
if hosts.iter().any(|host| host.as_ref() == server_address) {
|
||||
return Some(());
|
||||
}
|
||||
|
||||
let mut reason = "You can't connect to this server through that IP or domain.\n\
|
||||
Please use one of these instead:\n"
|
||||
.to_string();
|
||||
for host in hosts {
|
||||
reason.push_str(host);
|
||||
reason.push('\n');
|
||||
}
|
||||
|
||||
con.send(&Clientbound::Disconnect(Disconnect {
|
||||
reason: Chat {
|
||||
text: reason.into_boxed_str(),
|
||||
},
|
||||
}))
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "encryption"))]
|
||||
async fn enable_encryption(_con: &mut PacketStream<Client, Login>, cfg: &EncryptionConfig) -> Option<Box<[u8]>> {
|
||||
*cfg
|
||||
}
|
||||
|
||||
#[cfg(feature = "encryption")]
|
||||
async fn enable_encryption(con: &mut PacketStream<Client, Login>, cfg: &EncryptionConfig) -> Option<Box<[u8]>> {
|
||||
use login::*;
|
||||
|
||||
use rand::rngs::OsRng;
|
||||
use rand::Rng;
|
||||
use rsa::padding::PaddingScheme;
|
||||
|
||||
let mut verify_token = Vec::new();
|
||||
verify_token.resize(4, 0u8);
|
||||
OsRng.fill(verify_token.as_mut_slice());
|
||||
|
||||
con.send(&Clientbound::EncryptionRequest(EncryptionRequest {
|
||||
server_id: "".to_string().into_boxed_str(),
|
||||
public_key: cfg.public_key_bytes.clone(),
|
||||
verify_token: verify_token.clone().into_boxed_slice(),
|
||||
}))
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
let shared_secret: Box<[u8]> = match con.receive().await.ok()? {
|
||||
Serverbound::EncryptionResponse(EncryptionResponse {
|
||||
shared_secret,
|
||||
verify_token: encrypted_verify_token,
|
||||
}) => {
|
||||
let decrypted_verify_token = cfg
|
||||
.private_key
|
||||
.decrypt(PaddingScheme::PKCS1v15Encrypt, &encrypted_verify_token)
|
||||
.ok()?;
|
||||
|
||||
if decrypted_verify_token != verify_token {
|
||||
con.send(&Clientbound::Disconnect(Disconnect {
|
||||
reason: Chat {
|
||||
text: "Verify token was not encrypted correctly.".to_string().into_boxed_str(),
|
||||
},
|
||||
}))
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
cfg.private_key
|
||||
.decrypt(PaddingScheme::PKCS1v15Encrypt, &shared_secret)
|
||||
.ok()?
|
||||
.into_boxed_slice()
|
||||
},
|
||||
_ => {
|
||||
con.send(&Clientbound::Disconnect(Disconnect {
|
||||
reason: Chat {
|
||||
text: "You're supposed to send an EncryptionResponse packet!"
|
||||
.to_string()
|
||||
.into_boxed_str(),
|
||||
},
|
||||
}))
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
return None;
|
||||
},
|
||||
};
|
||||
|
||||
con.enable_encryption(&shared_secret).ok()?;
|
||||
|
||||
Some(shared_secret)
|
||||
}
|
||||
|
||||
#[cfg(feature = "authentication")]
|
||||
#[derive(serde::Deserialize)]
|
||||
struct HasJoinedResponse {
|
||||
// The response also contains a signature,
|
||||
// but I don't need to check it because I use HTTPS.
|
||||
// There's also additional properties like the player's skin,
|
||||
// but I don't care about those for now.
|
||||
id: Uuid,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "authentication"))]
|
||||
async fn authenticate(
|
||||
_con: &mut PacketStream<Client, Login>,
|
||||
_ecfg: &EncryptionConfig,
|
||||
acfg: &AuthenticationConfig,
|
||||
_name: &str,
|
||||
_shared_secret: &[u8],
|
||||
) -> Option<Option<Uuid>> {
|
||||
*acfg
|
||||
}
|
||||
|
||||
#[cfg(feature = "authentication")]
|
||||
async fn authenticate(
|
||||
con: &mut PacketStream<Client, Login>,
|
||||
ecfg: &EncryptionConfig,
|
||||
acfg: &AuthenticationConfig,
|
||||
name: &str,
|
||||
shared_secret: &[u8],
|
||||
) -> Option<Option<Uuid>> {
|
||||
use login::*;
|
||||
|
||||
let result = try_authenticate(ecfg, acfg, name, shared_secret).await;
|
||||
|
||||
if acfg.allow_unauthenticated_players {
|
||||
return Some(result);
|
||||
}
|
||||
|
||||
match result {
|
||||
Some(uuid) => Some(Some(uuid)),
|
||||
None => {
|
||||
con.send(&Clientbound::Disconnect(Disconnect {
|
||||
reason: Chat {
|
||||
text: "Authentication failed.".to_string().into_boxed_str(),
|
||||
},
|
||||
}))
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
None
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "authentication")]
|
||||
async fn try_authenticate(
|
||||
ecfg: &EncryptionConfig,
|
||||
acfg: &AuthenticationConfig,
|
||||
name: &str,
|
||||
shared_secret: &[u8],
|
||||
) -> Option<Uuid> {
|
||||
use num_bigint::BigInt;
|
||||
use reqwest::Client;
|
||||
use sha1::{Digest, Sha1};
|
||||
|
||||
let server_hash = {
|
||||
let server_hash_bytes = {
|
||||
let mut hasher = Sha1::new();
|
||||
hasher.update(b"");
|
||||
hasher.update(shared_secret);
|
||||
hasher.update(ecfg.public_key_bytes.clone());
|
||||
hasher.finalize()
|
||||
};
|
||||
|
||||
format!("{:x}", BigInt::from_signed_bytes_be(&server_hash_bytes)).into_boxed_str()
|
||||
};
|
||||
|
||||
// TODO: Allow checking IPs for the anti-proxy feature.
|
||||
let response_body = Client::new()
|
||||
.get(acfg.has_joined_endpoint.clone())
|
||||
.header("Content-Type", "application/json")
|
||||
.query(&[("username", name), ("serverId", &server_hash)])
|
||||
.send()
|
||||
.await
|
||||
.ok()?
|
||||
.text()
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
let response: HasJoinedResponse = serde_json::from_str(&response_body).ok()?;
|
||||
|
||||
Some(response.id)
|
||||
}
|
|
@ -1,4 +1,10 @@
|
|||
pub mod chat;
|
||||
pub mod listener;
|
||||
pub mod packet_stream;
|
||||
pub mod protocol;
|
||||
pub mod serialize;
|
||||
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
|
||||
pub trait Stream: AsyncRead + AsyncWrite + Send + Unpin {}
|
||||
impl<S: AsyncRead + AsyncWrite + Send + Unpin> Stream for S {}
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
#[cfg(feature = "encryption")]
|
||||
mod encryption;
|
||||
mod packet_format;
|
||||
mod stream;
|
||||
|
||||
use crate::net::packet_stream::packet_format::{AutoPacketFormat, PacketFormat};
|
||||
use crate::net::packet_stream::stream::Stream;
|
||||
use crate::net::protocol::packet_map::PacketMap;
|
||||
use crate::net::protocol::state::handshake::Handshake;
|
||||
use crate::net::protocol::state::login::Login;
|
||||
use crate::net::protocol::state::play::Play;
|
||||
use crate::net::protocol::state::status::Status;
|
||||
use crate::net::protocol::state::ProtocolState;
|
||||
use crate::net::Stream;
|
||||
use async_trait::async_trait;
|
||||
use std::io;
|
||||
use std::marker::PhantomData;
|
||||
|
@ -47,7 +48,7 @@ pub type SharedSecret = !;
|
|||
/// A stream of packets.
|
||||
///
|
||||
/// The type parameters are used to ensure using the type system
|
||||
/// that you can only send and recieve the correct type of packets.
|
||||
/// that you can only send and receive the correct type of packets.
|
||||
pub struct PacketStream<Rem: Remote, St: ProtocolState> {
|
||||
inner: Box<dyn Stream>,
|
||||
compression_threshold: Option<CompressionThreshold>,
|
||||
|
@ -78,7 +79,7 @@ impl<Rem: Remote, St: ProtocolState> PacketStream<Rem, St> {
|
|||
/// so it should only be used internally.
|
||||
///
|
||||
/// The purpose of this function is to reduce code duplication
|
||||
/// while implementing [`PacketStreamMaps`]'s send and recieve;
|
||||
/// while implementing [`PacketStreamMaps`]'s send and receive;
|
||||
/// the sending and recieving code is always going to be the same,
|
||||
/// but due to some shortcomings of Rust's type system
|
||||
/// (specifically, because there's no way to approximate type-level functions;
|
||||
|
@ -93,22 +94,22 @@ impl<Rem: Remote, St: ProtocolState> PacketStream<Rem, St> {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Recieve any packet from the stream, ignoring the declared ProtocolState.
|
||||
/// Receive any packet from the stream, ignoring the declared ProtocolState.
|
||||
/// This can be used to break PacketStream's type-enforced correctness guarantees,
|
||||
/// so it should only be used internally.
|
||||
///
|
||||
/// The purpose of this function is to reduce code duplication
|
||||
/// while implementing [`PacketStreamMaps`]'s send and recieve;
|
||||
/// while implementing [`PacketStreamMaps`]'s send and receive;
|
||||
/// the sending and recieving code is always going to be the same,
|
||||
/// but due to some shortcomings of Rust's type system
|
||||
/// (specifically, because there's no way to approximate type-level functions;
|
||||
/// in Haskell you could use multi-parameter type classes or type families),
|
||||
/// we can't implement this generically safely over Remote/ProtocolState combinations.
|
||||
async fn recieve_generic<Pkt: PacketMap>(&mut self) -> io::Result<Pkt> {
|
||||
async fn receive_generic<Pkt: PacketMap>(&mut self) -> io::Result<Pkt> {
|
||||
use crate::net::serialize::VecPacketDeserializer;
|
||||
|
||||
let buf = AutoPacketFormat(self.compression_threshold)
|
||||
.recieve(&mut self.inner)
|
||||
.receive(&mut self.inner)
|
||||
.await?;
|
||||
|
||||
Pkt::read(&mut VecPacketDeserializer::new(buf.as_ref()))
|
||||
|
@ -127,13 +128,13 @@ impl<Rem: Remote, St: ProtocolState> PacketStream<Rem, St> {
|
|||
/// which are determined by the protocol state and remote.
|
||||
#[async_trait]
|
||||
pub trait PacketStreamMaps: Sealed {
|
||||
/// The kind of packets that can be recieved from the remote.
|
||||
/// The kind of packets that can be received from the remote.
|
||||
type Inbound: PacketMap;
|
||||
/// The kind of packets that can be sent to the remote.
|
||||
type Outbound: PacketMap;
|
||||
|
||||
/// Recieve a packet from the remote.
|
||||
async fn recieve(&mut self) -> io::Result<Self::Inbound>;
|
||||
/// Receive a packet from the remote.
|
||||
async fn receive(&mut self) -> io::Result<Self::Inbound>;
|
||||
|
||||
/// Send a packet to the remote.
|
||||
async fn send(&mut self, pkt: &Self::Outbound) -> io::Result<()>;
|
||||
|
@ -145,8 +146,8 @@ impl<St: ProtocolState> PacketStreamMaps for PacketStream<Client, St> {
|
|||
type Inbound = St::Serverbound;
|
||||
type Outbound = St::Clientbound;
|
||||
|
||||
async fn recieve(&mut self) -> io::Result<Self::Inbound> {
|
||||
self.recieve_generic().await
|
||||
async fn receive(&mut self) -> io::Result<Self::Inbound> {
|
||||
self.receive_generic().await
|
||||
}
|
||||
|
||||
async fn send(&mut self, pkt: &Self::Outbound) -> io::Result<()> {
|
||||
|
@ -160,8 +161,8 @@ impl<St: ProtocolState> PacketStreamMaps for PacketStream<Server, St> {
|
|||
type Inbound = St::Clientbound;
|
||||
type Outbound = St::Serverbound;
|
||||
|
||||
async fn recieve(&mut self) -> io::Result<Self::Inbound> {
|
||||
self.recieve_generic().await
|
||||
async fn receive(&mut self) -> io::Result<Self::Inbound> {
|
||||
self.receive_generic().await
|
||||
}
|
||||
|
||||
async fn send(&mut self, pkt: &Self::Outbound) -> io::Result<()> {
|
||||
|
@ -221,7 +222,7 @@ impl<Rem: Remote> PacketStream<Rem, Login> {
|
|||
|
||||
#[cfg(feature = "encryption")]
|
||||
{
|
||||
use crate::net::packet_stream::stream::encrypted::EncryptedStream;
|
||||
use crate::net::packet_stream::encryption::EncryptedStream;
|
||||
use cfb8::stream_cipher::NewStreamCipher;
|
||||
use cfb8::Cfb8;
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::net::packet_stream::stream::Stream;
|
||||
use crate::net::Stream;
|
||||
use aes::Aes128;
|
||||
use cfb8::stream_cipher::StreamCipher;
|
||||
use cfb8::Cfb8;
|
|
@ -14,9 +14,9 @@ pub type Writer = dyn AsyncWrite + Unpin + Send;
|
|||
/// A packet format describes how to read a packet header and retrieve its data.
|
||||
#[async_trait]
|
||||
pub trait PacketFormat: Send + Sync {
|
||||
/// Recieve the bytes of a packet's body (its id and header) from the provided stream.
|
||||
/// Receive the bytes of a packet's body (its id and header) from the provided stream.
|
||||
/// This involves reading the packet header and performing decompression if necessary.
|
||||
async fn recieve(&self, src: &mut Reader) -> io::Result<Box<[u8]>>;
|
||||
async fn receive(&self, src: &mut Reader) -> io::Result<Box<[u8]>>;
|
||||
|
||||
/// Send the bytes of a packet's body (its id and header) through the provided stream.
|
||||
/// This involves writing the packet header and performing compression if necessary.
|
||||
|
@ -27,16 +27,16 @@ pub struct AutoPacketFormat(pub Option<CompressionThreshold>);
|
|||
|
||||
#[async_trait]
|
||||
impl PacketFormat for AutoPacketFormat {
|
||||
async fn recieve(&self, src: &mut Reader) -> io::Result<Box<[u8]>> {
|
||||
async fn receive(&self, src: &mut Reader) -> io::Result<Box<[u8]>> {
|
||||
match self.0 {
|
||||
#[cfg(not(feature = "compression"))]
|
||||
Some(x) => x,
|
||||
#[cfg(feature = "compression")]
|
||||
Some(threshold) => {
|
||||
use crate::net::packet_stream::packet_format::compressed::CompressedPacketFormat;
|
||||
CompressedPacketFormat(threshold).recieve(src).await
|
||||
CompressedPacketFormat(threshold).receive(src).await
|
||||
},
|
||||
None => DefaultPacketFormat.recieve(src).await,
|
||||
None => DefaultPacketFormat.receive(src).await,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,7 +54,7 @@ impl PacketFormat for AutoPacketFormat {
|
|||
}
|
||||
}
|
||||
|
||||
/// A completely arbitrary limitation on the maximum size of a recieved packet.
|
||||
/// A completely arbitrary limitation on the maximum size of a received packet.
|
||||
pub const MAX_PACKET_SIZE: usize = 35565;
|
||||
|
||||
async fn read_varint(src: &mut Reader) -> io::Result<(usize, i32)> {
|
||||
|
|
|
@ -20,7 +20,7 @@ pub struct CompressedPacketFormat(pub usize);
|
|||
|
||||
#[async_trait]
|
||||
impl PacketFormat for CompressedPacketFormat {
|
||||
async fn recieve(&self, src: &mut Reader) -> io::Result<Box<[u8]>> {
|
||||
async fn receive(&self, src: &mut Reader) -> io::Result<Box<[u8]>> {
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
// First we read in the packet and uncompressed data lengths.
|
||||
|
@ -42,7 +42,7 @@ impl PacketFormat for CompressedPacketFormat {
|
|||
}
|
||||
let data_length = data_length as usize;
|
||||
|
||||
// Now we recieve the remainder of the packet's data.
|
||||
// Now we receive the remainder of the packet's data.
|
||||
let mut data = Vec::with_capacity(packet_length - data_length_size);
|
||||
data.resize(packet_length, 0);
|
||||
src.read_exact(data.as_mut_slice()).await?;
|
||||
|
|
|
@ -7,7 +7,7 @@ pub struct DefaultPacketFormat;
|
|||
|
||||
#[async_trait]
|
||||
impl PacketFormat for DefaultPacketFormat {
|
||||
async fn recieve(&self, src: &mut Reader) -> io::Result<Box<[u8]>> {
|
||||
async fn receive(&self, src: &mut Reader) -> io::Result<Box<[u8]>> {
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
let (_, length) = read_varint(src).await?;
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
#[cfg(feature = "encryption")]
|
||||
pub mod encrypted;
|
||||
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
|
||||
pub trait Stream: AsyncRead + AsyncWrite + Send + Unpin {}
|
||||
impl<S: AsyncRead + AsyncWrite + Send + Unpin> Stream for S {}
|
|
@ -33,7 +33,7 @@ impl PacketWritable for &HandshakeNextState {
|
|||
define_packets! {
|
||||
packet HandshakePkt {
|
||||
protocol_version: VarInt,
|
||||
server_address: String,
|
||||
server_address: Box<str>,
|
||||
server_port: u16,
|
||||
next_state: HandshakeNextState
|
||||
}
|
||||
|
|
|
@ -6,14 +6,14 @@ use uuid::Uuid;
|
|||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
pub struct ResponseVersion {
|
||||
pub name: String,
|
||||
pub name: Box<str>,
|
||||
pub protocol: u32,
|
||||
}
|
||||
impl PacketJson for ResponseVersion {}
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
pub struct ResponsePlayersSample {
|
||||
pub name: String,
|
||||
pub name: Box<str>,
|
||||
pub id: Uuid,
|
||||
}
|
||||
impl PacketJson for ResponsePlayersSample {}
|
||||
|
@ -32,7 +32,7 @@ pub struct ResponseData {
|
|||
pub players: ResponsePlayers,
|
||||
pub description: Chat,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub favicon: Option<String>,
|
||||
pub favicon: Option<Box<str>>,
|
||||
}
|
||||
impl PacketJson for ResponseData {}
|
||||
|
||||
|
|
Loading…
Reference in New Issue