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:
Ryan Johnson 2022-10-22 20:17:06 -07:00 committed by GitHub
parent bbbeb7ae28
commit 56ebcaf50d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 224 additions and 46 deletions

View file

@ -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

View file

@ -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 {

View file

@ -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())
}

View file

@ -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,
};

View file

@ -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 {

View file

@ -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>,
}

View file

@ -48,7 +48,7 @@ pub mod login {
def_struct! {
LoginSuccess {
uuid: Uuid,
username: BoundedString<3, 16>,
username: Username<String>,
properties: Vec<Property>,
}
}

View file

@ -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?;

View file

@ -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
View 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> {}

View file

@ -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,