mirror of
https://github.com/italicsjenga/valence.git
synced 2024-12-23 22:41:30 +11:00
Add Username<S>
type (#132)
There are a number of places where usernames are passed around. Using this type ensures that the contained string is actually a valid username and not some other kind of string. For instance you can use it as a function argument to indicate that only valid usernames are accepted, or return it from a function to indicate that only valid usernames are produced. This is analogous to the existing `Ident<S>` type.
This commit is contained in:
parent
bbbeb7ae28
commit
56ebcaf50d
|
@ -45,6 +45,7 @@ use crate::protocol::{BoundedInt, BoundedString, ByteAngle, RawBytes, SlotId, Va
|
|||
use crate::server::{C2sPacketChannels, NewClientData, S2cPlayMessage, SharedServer};
|
||||
use crate::slab_versioned::{Key, VersionedSlab};
|
||||
use crate::text::Text;
|
||||
use crate::username::Username;
|
||||
use crate::util::{chunks_in_view_distance, is_chunk_in_view_distance};
|
||||
use crate::world::{WorldId, Worlds};
|
||||
use crate::{ident, LIBRARY_NAMESPACE};
|
||||
|
@ -191,7 +192,7 @@ pub struct Client<C: Config> {
|
|||
send: SendOpt,
|
||||
recv: Receiver<C2sPlayPacket>,
|
||||
uuid: Uuid,
|
||||
username: String,
|
||||
username: Username<String>,
|
||||
textures: Option<SignedPlayerTextures>,
|
||||
/// World client is currently in. Default value is **invalid** and must
|
||||
/// be set by calling [`Client::spawn`].
|
||||
|
@ -331,8 +332,8 @@ impl<C: Config> Client<C> {
|
|||
}
|
||||
|
||||
/// Gets the username of this client.
|
||||
pub fn username(&self) -> &str {
|
||||
&self.username
|
||||
pub fn username(&self) -> Username<&str> {
|
||||
self.username.as_str_username()
|
||||
}
|
||||
|
||||
/// Gets the player textures of this client. If the client does not have
|
||||
|
|
|
@ -12,6 +12,7 @@ use crate::biome::Biome;
|
|||
use crate::dimension::Dimension;
|
||||
use crate::server::{NewClientData, Server, SharedServer};
|
||||
use crate::text::Text;
|
||||
use crate::username::Username;
|
||||
use crate::{Ticks, STANDARD_TPS};
|
||||
|
||||
/// A trait for the configuration of a server.
|
||||
|
@ -232,7 +233,7 @@ pub trait Config: Sized + Send + Sync + 'static {
|
|||
fn format_session_server_url(
|
||||
&self,
|
||||
server: &SharedServer<Self>,
|
||||
username: &str,
|
||||
username: Username<&str>,
|
||||
auth_digest: &str,
|
||||
player_ip: &IpAddr,
|
||||
) -> String {
|
||||
|
|
|
@ -151,7 +151,10 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<S: AsRef<str>> fmt::Display for Ident<S> {
|
||||
impl<S> fmt::Display for Ident<S>
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}:{}", self.namespace(), self.path())
|
||||
}
|
||||
|
|
|
@ -123,6 +123,7 @@ mod slab_rc;
|
|||
mod slab_versioned;
|
||||
pub mod spatial_index;
|
||||
pub mod text;
|
||||
pub mod username;
|
||||
pub mod util;
|
||||
pub mod world;
|
||||
|
||||
|
@ -145,6 +146,7 @@ pub mod prelude {
|
|||
pub use server::{NewClientData, Server, SharedServer, ShutdownResult};
|
||||
pub use spatial_index::{RaycastHit, SpatialIndex};
|
||||
pub use text::{Color, Text, TextFormat};
|
||||
pub use username::Username;
|
||||
pub use util::{
|
||||
chunks_in_view_distance, from_yaw_and_pitch, is_chunk_in_view_distance, to_yaw_and_pitch,
|
||||
};
|
||||
|
|
|
@ -23,6 +23,7 @@ use crate::protocol::{
|
|||
VarLong,
|
||||
};
|
||||
use crate::text::Text;
|
||||
use crate::username::Username;
|
||||
|
||||
/// Provides the name of a packet for debugging purposes.
|
||||
pub trait PacketName {
|
||||
|
|
|
@ -56,7 +56,7 @@ pub mod login {
|
|||
|
||||
def_struct! {
|
||||
LoginStart {
|
||||
username: BoundedString<3, 16>,
|
||||
username: Username<String>,
|
||||
sig_data: Option<PublicKeyData>,
|
||||
profile_id: Option<Uuid>,
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ pub mod login {
|
|||
def_struct! {
|
||||
LoginSuccess {
|
||||
uuid: Uuid,
|
||||
username: BoundedString<3, 16>,
|
||||
username: Username<String>,
|
||||
properties: Vec<Property>,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,8 +38,8 @@ use crate::protocol::packets::c2s::status::{PingRequest, StatusRequest};
|
|||
use crate::protocol::packets::s2c::login::{DisconnectLogin, LoginSuccess, SetCompression};
|
||||
use crate::protocol::packets::s2c::play::S2cPlayPacket;
|
||||
use crate::protocol::packets::s2c::status::{PingResponse, StatusResponse};
|
||||
use crate::protocol::{BoundedString, VarInt};
|
||||
use crate::util::valid_username;
|
||||
use crate::protocol::VarInt;
|
||||
use crate::username::Username;
|
||||
use crate::world::Worlds;
|
||||
use crate::{ident, Ticks, PROTOCOL_VERSION, VERSION_NAME};
|
||||
|
||||
|
@ -120,7 +120,7 @@ pub struct NewClientData {
|
|||
/// The UUID of the new client.
|
||||
pub uuid: Uuid,
|
||||
/// The username of the new client.
|
||||
pub username: String,
|
||||
pub username: Username<String>,
|
||||
/// The new client's player textures. May be `None` if the client does not
|
||||
/// have a skin or cape.
|
||||
pub textures: Option<SignedPlayerTextures>,
|
||||
|
@ -627,13 +627,11 @@ async fn handle_login(
|
|||
}
|
||||
|
||||
let LoginStart {
|
||||
username: BoundedString(username),
|
||||
username,
|
||||
sig_data: _, // TODO
|
||||
profile_id: _, // TODO
|
||||
} = c.dec.read_packet().await?;
|
||||
|
||||
ensure!(valid_username(&username), "invalid username '{username}'");
|
||||
|
||||
let ncd = match server.connection_mode() {
|
||||
ConnectionMode::Online => login::online(server, c, remote_addr, username).await?,
|
||||
ConnectionMode::Offline => login::offline(remote_addr, username)?,
|
||||
|
@ -660,7 +658,7 @@ async fn handle_login(
|
|||
c.enc
|
||||
.write_packet(&LoginSuccess {
|
||||
uuid: ncd.uuid,
|
||||
username: ncd.username.clone().into(),
|
||||
username: ncd.username.clone(),
|
||||
properties: Vec::new(),
|
||||
})
|
||||
.await?;
|
||||
|
|
|
@ -25,9 +25,10 @@ use crate::protocol::packets::s2c::login::{
|
|||
DisconnectLogin, EncryptionRequest, LoginPluginRequest,
|
||||
};
|
||||
use crate::protocol::packets::Property;
|
||||
use crate::protocol::{BoundedArray, BoundedString, Decode, RawBytes, VarInt};
|
||||
use crate::protocol::{BoundedArray, Decode, RawBytes, VarInt};
|
||||
use crate::server::{Codec, NewClientData, SharedServer};
|
||||
use crate::text::Text;
|
||||
use crate::username::Username;
|
||||
|
||||
/// Login sequence for
|
||||
/// [`ConnectionMode::Online`](crate::config::ConnectionMode).
|
||||
|
@ -35,7 +36,7 @@ pub(super) async fn online(
|
|||
server: &SharedServer<impl Config>,
|
||||
c: &mut Codec,
|
||||
remote_addr: SocketAddr,
|
||||
username: String,
|
||||
username: Username<String>,
|
||||
) -> anyhow::Result<NewClientData> {
|
||||
let my_verify_token: [u8; 16] = rand::random();
|
||||
|
||||
|
@ -86,7 +87,7 @@ pub(super) async fn online(
|
|||
#[derive(Debug, Deserialize)]
|
||||
struct AuthResponse {
|
||||
id: String,
|
||||
name: String,
|
||||
name: Username<String>,
|
||||
properties: Vec<Property>,
|
||||
}
|
||||
|
||||
|
@ -97,7 +98,7 @@ pub(super) async fn online(
|
|||
|
||||
let url = server.config().format_session_server_url(
|
||||
server,
|
||||
&username,
|
||||
username.as_str_username(),
|
||||
&auth_digest(&hash),
|
||||
&remote_addr.ip(),
|
||||
);
|
||||
|
@ -140,10 +141,13 @@ pub(super) async fn online(
|
|||
|
||||
/// Login sequence for
|
||||
/// [`ConnectionMode::Offline`](crate::config::ConnectionMode).
|
||||
pub(super) fn offline(remote_addr: SocketAddr, username: String) -> anyhow::Result<NewClientData> {
|
||||
pub(super) fn offline(
|
||||
remote_addr: SocketAddr,
|
||||
username: Username<String>,
|
||||
) -> anyhow::Result<NewClientData> {
|
||||
Ok(NewClientData {
|
||||
// Derive the client's UUID from a hash of their username.
|
||||
uuid: Uuid::from_slice(&Sha256::digest(&username)[..16])?,
|
||||
uuid: Uuid::from_slice(&Sha256::digest(username.as_str())[..16])?,
|
||||
username,
|
||||
textures: None,
|
||||
remote_addr: remote_addr.ip(),
|
||||
|
@ -152,7 +156,10 @@ pub(super) fn offline(remote_addr: SocketAddr, username: String) -> anyhow::Resu
|
|||
|
||||
/// Login sequence for
|
||||
/// [`ConnectionMode::BungeeCord`](crate::config::ConnectionMode).
|
||||
pub(super) fn bungeecord(server_address: &str, username: String) -> anyhow::Result<NewClientData> {
|
||||
pub(super) fn bungeecord(
|
||||
server_address: &str,
|
||||
username: Username<String>,
|
||||
) -> anyhow::Result<NewClientData> {
|
||||
// Get data from server_address field of the handshake
|
||||
let [_, client_ip, uuid, properties]: [&str; 4] = server_address
|
||||
.split('\0')
|
||||
|
@ -194,7 +201,7 @@ fn auth_digest(bytes: &[u8]) -> String {
|
|||
|
||||
pub(super) async fn velocity(
|
||||
c: &mut Codec,
|
||||
username: String,
|
||||
username: Username<String>,
|
||||
velocity_secret: &str,
|
||||
) -> anyhow::Result<NewClientData> {
|
||||
const VELOCITY_MIN_SUPPORTED_VERSION: u8 = 1;
|
||||
|
@ -245,8 +252,10 @@ pub(super) async fn velocity(
|
|||
let uuid = Uuid::decode(&mut data_without_signature)?;
|
||||
|
||||
// Get username and validate
|
||||
let velocity_username = BoundedString::<0, 16>::decode(&mut data_without_signature)?.0;
|
||||
ensure!(username == velocity_username, "mismatched usernames");
|
||||
ensure!(
|
||||
username == Username::decode(&mut data_without_signature)?,
|
||||
"mismatched usernames"
|
||||
);
|
||||
|
||||
// Read properties and get textures
|
||||
let mut textures = None;
|
||||
|
|
185
src/username.rs
Normal file
185
src/username.rs
Normal file
|
@ -0,0 +1,185 @@
|
|||
use std::borrow::Borrow;
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::fmt::Formatter;
|
||||
use std::io::Write;
|
||||
use std::str::FromStr;
|
||||
|
||||
use serde::de::Error as _;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
|
||||
use crate::protocol::{Decode, Encode};
|
||||
|
||||
/// A newtype wrapper around a string type `S` which guarantees the wrapped
|
||||
/// string meets the criteria for a valid Minecraft username.
|
||||
///
|
||||
/// A valid username is 3 to 16 characters long with only ASCII alphanumeric
|
||||
/// characters. The username must match the regex `^[a-zA-Z0-9_]{3,16}$` to be
|
||||
/// considered valid.
|
||||
///
|
||||
/// # Contract
|
||||
///
|
||||
/// The type `S` must meet the following criteria:
|
||||
/// - All calls to [`AsRef::as_ref`] and [`Borrow::borrow`] while the string is
|
||||
/// wrapped in `Username` must return the same value.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use valence::prelude::*;
|
||||
///
|
||||
/// assert!(Username::new("00a").is_ok());
|
||||
/// assert!(Username::new("jeb_").is_ok());
|
||||
///
|
||||
/// assert!(Username::new("notavalidusername").is_err());
|
||||
/// assert!(Username::new("NotValid!").is_err());
|
||||
/// ```
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize)]
|
||||
#[repr(transparent)]
|
||||
#[serde(transparent)]
|
||||
pub struct Username<S>(S);
|
||||
|
||||
impl<S: AsRef<str>> Username<S> {
|
||||
pub fn new(string: S) -> Result<Self, UsernameError<S>> {
|
||||
let s = string.as_ref();
|
||||
|
||||
if (3..=16).contains(&s.len()) && s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
|
||||
Ok(Self(string))
|
||||
} else {
|
||||
Err(UsernameError(string))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.0.as_ref()
|
||||
}
|
||||
|
||||
pub fn as_str_username(&self) -> Username<&str> {
|
||||
Username(self.0.as_ref())
|
||||
}
|
||||
|
||||
pub fn to_owned_username(&self) -> Username<S::Owned>
|
||||
where
|
||||
S: ToOwned,
|
||||
S::Owned: AsRef<str>,
|
||||
{
|
||||
Username(self.0.to_owned())
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> S {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> AsRef<str> for Username<S>
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Borrow<str> for Username<S>
|
||||
where
|
||||
S: Borrow<str>,
|
||||
{
|
||||
fn borrow(&self) -> &str {
|
||||
self.0.borrow()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Username<String> {
|
||||
type Err = UsernameError<String>;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Username::new(s.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for Username<String> {
|
||||
type Error = UsernameError<String>;
|
||||
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
Username::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> From<Username<S>> for String
|
||||
where
|
||||
S: Into<String> + AsRef<str>,
|
||||
{
|
||||
fn from(value: Username<S>) -> Self {
|
||||
value.0.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> fmt::Display for Username<S>
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
self.0.as_ref().fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Encode for Username<S>
|
||||
where
|
||||
S: Encode,
|
||||
{
|
||||
fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> {
|
||||
self.0.encode(w)
|
||||
}
|
||||
|
||||
fn encoded_len(&self) -> usize {
|
||||
self.0.encoded_len()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Decode for Username<S>
|
||||
where
|
||||
S: Decode + AsRef<str> + Send + Sync + 'static,
|
||||
{
|
||||
fn decode(r: &mut &[u8]) -> anyhow::Result<Self> {
|
||||
Ok(Username::new(S::decode(r)?)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, S> Deserialize<'de> for Username<S>
|
||||
where
|
||||
S: Deserialize<'de> + AsRef<str>,
|
||||
{
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Username::new(S::deserialize(deserializer)?).map_err(D::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
/// The error type created when a [`Username`] cannot be parsed from a string.
|
||||
/// Contains the offending string.
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct UsernameError<S>(pub S);
|
||||
|
||||
impl<S> fmt::Debug for UsernameError<S>
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.debug_tuple("UsernameError")
|
||||
.field(&self.0.as_ref())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> fmt::Display for UsernameError<S>
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "invalid username \"{}\"", self.0.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Error for UsernameError<S> where S: AsRef<str> {}
|
22
src/util.rs
22
src/util.rs
|
@ -8,28 +8,6 @@ use vek::{Aabb, Vec3};
|
|||
|
||||
use crate::chunk_pos::ChunkPos;
|
||||
|
||||
/// Returns true if the given string meets the criteria for a valid Minecraft
|
||||
/// username.
|
||||
///
|
||||
/// Usernames are valid if they match the regex `^[a-zA-Z0-9_]{3,16}$`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use valence::util::valid_username;
|
||||
///
|
||||
/// assert!(valid_username("00a"));
|
||||
/// assert!(valid_username("jeb_"));
|
||||
///
|
||||
/// assert!(!valid_username("notavalidusername"));
|
||||
/// assert!(!valid_username("NotValid!"));
|
||||
/// ```
|
||||
pub fn valid_username(s: &str) -> bool {
|
||||
(3..=16).contains(&s.len())
|
||||
&& s.chars()
|
||||
.all(|c| matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '_'))
|
||||
}
|
||||
|
||||
const EXTRA_RADIUS: i32 = 3;
|
||||
|
||||
/// Returns an iterator over all chunk positions within a view distance,
|
||||
|
|
Loading…
Reference in a new issue