minecraft skins!

This commit is contained in:
2026-08-29 01:41:59 +08:00
parent 9d695e704e
commit b6401a79a3
29 changed files with 1466 additions and 383 deletions
+5
View File
@@ -161,6 +161,7 @@ mod tests {
profile: Profile {
id: public_key.clone(),
display_name: "Source".to_owned(),
skin_hash: None,
},
friends: vec!["target".to_owned(), "one-sided".to_owned()],
sender: source_sender,
@@ -174,6 +175,7 @@ mod tests {
profile: Profile {
id: "target".to_owned(),
display_name: "Target".to_owned(),
skin_hash: None,
},
friends: vec![public_key.clone()],
sender: target_sender.clone(),
@@ -187,6 +189,7 @@ mod tests {
profile: Profile {
id: "one-sided".to_owned(),
display_name: "One-sided".to_owned(),
skin_hash: None,
},
friends: Vec::new(),
sender: one_sided_sender,
@@ -305,6 +308,7 @@ mod tests {
profile: Profile {
id: public_key.clone(),
display_name: "Source".to_owned(),
skin_hash: None,
},
friends: vec!["target".to_owned()],
sender: source_sender,
@@ -318,6 +322,7 @@ mod tests {
profile: Profile {
id: "target".to_owned(),
display_name: "Target".to_owned(),
skin_hash: None,
},
friends: vec![public_key.clone()],
sender: target_sender,
+51
View File
@@ -18,6 +18,7 @@ use wyd_common::{ClientMessage, Profile, ServerMessage, message_bytes, register_
mod interactions;
mod presence;
mod profiles;
mod skins;
type Clients = Arc<Mutex<HashMap<String, Client>>>;
@@ -70,6 +71,13 @@ async fn connected(mut socket: WebSocket, clients: Clients) {
else {
return;
};
if profile
.skin_hash
.as_deref()
.is_some_and(|hash| !wyd_common::is_skin_hash(hash))
{
return;
}
let Ok(key) = key(&profile.id) else { return };
if !verify(
&key,
@@ -208,6 +216,42 @@ async fn connected(mut socket: WebSocket, clients: Clients) {
break;
}
}
Ok(ClientMessage::RequestSkin { request_id, user_id, skin_hash }) => {
match skins::request(
&clients,
&public_key,
connection_id,
request_id,
user_id,
skin_hash,
).await {
skins::RequestOutcome::StaleSession => break,
skins::RequestOutcome::Forwarded => {}
skins::RequestOutcome::Unavailable(response) => {
if writer.send(response).await.is_err() {
break;
}
}
}
}
Ok(ClientMessage::ProvideSkin {
request_id,
requester_id,
skin_hash,
data,
}) => {
if !skins::provide(
&clients,
&public_key,
connection_id,
request_id,
requester_id,
skin_hash,
data,
).await {
break;
}
}
_ => break,
}
Some(Ok(Message::Ping(data))) => {
@@ -301,6 +345,7 @@ mod tests {
let profile = Profile {
id: URL_SAFE_NO_PAD.encode(signing_key.verifying_key().to_bytes()),
display_name: "Wind".to_owned(),
skin_hash: None,
};
let friends = vec!["friend".to_owned()];
let registration = register_bytes("challenge", &profile, &friends);
@@ -347,6 +392,7 @@ mod tests {
profile: Profile {
id: public_key.clone(),
display_name: "Wind".to_owned(),
skin_hash: None,
},
friends: Vec::new(),
sender,
@@ -390,6 +436,7 @@ mod tests {
profile: Profile {
id: "source".to_owned(),
display_name: "Source".to_owned(),
skin_hash: None,
},
friends: vec!["mutual".to_owned()],
sender: source_sender,
@@ -403,6 +450,7 @@ mod tests {
profile: Profile {
id: "mutual".to_owned(),
display_name: "Mutual".to_owned(),
skin_hash: None,
},
friends: vec!["source".to_owned()],
sender: mutual_sender,
@@ -416,6 +464,7 @@ mod tests {
profile: Profile {
id: "one-way".to_owned(),
display_name: "One way".to_owned(),
skin_hash: None,
},
friends: vec!["source".to_owned()],
sender: one_way_sender,
@@ -494,6 +543,7 @@ mod tests {
profile: Profile {
id: public_key.clone(),
display_name: "Source".to_owned(),
skin_hash: None,
},
friends: vec!["mutual".to_owned(), "sender-only".to_owned()],
sender: source_sender,
@@ -516,6 +566,7 @@ mod tests {
profile: Profile {
id: id.to_owned(),
display_name: id.to_owned(),
skin_hash: None,
},
friends,
sender,
+21 -1
View File
@@ -19,7 +19,13 @@ pub(super) async fn update(
else {
return false;
};
if profile.id != public_key || !verify(&client.key, &profile_bytes(&profile), signature) {
if profile.id != public_key
|| profile
.skin_hash
.as_deref()
.is_some_and(|hash| !wyd_common::is_skin_hash(hash))
|| !verify(&client.key, &profile_bytes(&profile), signature)
{
return false;
}
client.profile = profile.clone();
@@ -122,6 +128,7 @@ mod tests {
profile: Profile {
id: id.to_owned(),
display_name: display_name.to_owned(),
skin_hash: None,
},
friends,
sender,
@@ -164,6 +171,7 @@ mod tests {
let profile = Profile {
id: public_key.clone(),
display_name: "New".to_owned(),
skin_hash: Some("a".repeat(64)),
};
let signature =
URL_SAFE_NO_PAD.encode(signing_key.sign(&profile_bytes(&profile)).to_bytes());
@@ -179,6 +187,17 @@ mod tests {
.display_name,
"New"
);
assert_eq!(
clients
.lock()
.await
.get(&public_key)
.unwrap()
.profile
.skin_hash
.as_deref(),
Some("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
);
let Message::Text(announcement) = friend_receiver
.recv()
.await
@@ -197,6 +216,7 @@ mod tests {
let stale_profile = Profile {
id: public_key.clone(),
display_name: "Stale".to_owned(),
skin_hash: None,
};
let stale_signature =
URL_SAFE_NO_PAD.encode(signing_key.sign(&profile_bytes(&stale_profile)).to_bytes());
+327
View File
@@ -0,0 +1,327 @@
use axum::extract::ws::Message;
use uuid::Uuid;
use wyd_common::{MAX_SKIN_B64_SIZE, ServerMessage};
use super::{Clients, are_mutual_friends};
pub(super) enum RequestOutcome {
StaleSession,
Forwarded,
Unavailable(Message),
}
pub(super) async fn request(
clients: &Clients,
requester_id: &str,
connection_id: Uuid,
request_id: String,
user_id: String,
skin_hash: String,
) -> RequestOutcome {
let clients = clients.lock().await;
let Some(requester) = clients
.get(requester_id)
.filter(|client| client.connection_id == connection_id)
else {
return RequestOutcome::StaleSession;
};
let target = clients.get(&user_id).filter(|target| {
are_mutual_friends(requester_id, requester, &user_id, target)
&& target.profile.skin_hash.as_deref() == Some(&skin_hash)
});
let Some(target) = target else {
return RequestOutcome::Unavailable(resolved(request_id, user_id, skin_hash, None));
};
let request = Message::Text(
serde_json::to_string(&ServerMessage::SkinRequested {
request_id: request_id.clone(),
requester_id: requester_id.to_owned(),
skin_hash: skin_hash.clone(),
})
.expect("skin request serializes")
.into(),
);
match target.sender.try_send(request) {
Ok(()) => RequestOutcome::Forwarded,
Err(_) => RequestOutcome::Unavailable(resolved(request_id, user_id, skin_hash, None)),
}
}
pub(super) async fn provide(
clients: &Clients,
provider_id: &str,
connection_id: Uuid,
request_id: String,
requester_id: String,
skin_hash: String,
data: Option<String>,
) -> bool {
if data
.as_ref()
.is_some_and(|data| data.len() > MAX_SKIN_B64_SIZE)
{
return false;
}
let clients = clients.lock().await;
let Some(provider) = clients
.get(provider_id)
.filter(|client| client.connection_id == connection_id)
.filter(|client| client.profile.skin_hash.as_deref() == Some(&skin_hash))
else {
return false;
};
let Some(requester) = clients
.get(&requester_id)
.filter(|requester| are_mutual_friends(provider_id, provider, &requester_id, requester))
else {
return true;
};
let _ = requester.sender.try_send(resolved(
request_id,
provider_id.to_owned(),
skin_hash,
data,
));
true
}
fn resolved(
request_id: String,
user_id: String,
skin_hash: String,
data: Option<String>,
) -> Message {
Message::Text(
serde_json::to_string(&ServerMessage::SkinResolved {
request_id,
user_id,
skin_hash,
data,
})
.expect("skin response serializes")
.into(),
)
}
#[cfg(test)]
mod tests {
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use ed25519_dalek::SigningKey;
use tokio::sync::mpsc;
use wyd_common::Profile;
use super::*;
use crate::network::Client;
fn client(
id: &str,
connection_id: Uuid,
friends: Vec<String>,
skin_hash: Option<String>,
sender: mpsc::Sender<Message>,
) -> Client {
let signing_key = SigningKey::from_bytes(&[7; 32]);
Client {
connection_id,
key: signing_key.verifying_key(),
profile: Profile {
id: id.to_owned(),
display_name: id.to_owned(),
skin_hash,
},
friends,
sender,
}
}
#[tokio::test]
async fn mutual_friends_can_round_trip_the_advertised_skin() {
let requester_id = URL_SAFE_NO_PAD.encode([1_u8; 32]);
let provider_id = URL_SAFE_NO_PAD.encode([2_u8; 32]);
let skin_hash = "a".repeat(64);
let requester_connection = Uuid::new_v4();
let provider_connection = Uuid::new_v4();
let (requester_sender, mut requester_receiver) = mpsc::channel(2);
let (provider_sender, mut provider_receiver) = mpsc::channel(2);
let clients = Clients::default();
{
let mut clients = clients.lock().await;
clients.insert(
requester_id.clone(),
client(
&requester_id,
requester_connection,
vec![provider_id.clone()],
None,
requester_sender,
),
);
clients.insert(
provider_id.clone(),
client(
&provider_id,
provider_connection,
vec![requester_id.clone()],
Some(skin_hash.clone()),
provider_sender,
),
);
}
assert!(matches!(
request(
&clients,
&requester_id,
requester_connection,
"request".to_owned(),
provider_id.clone(),
skin_hash.clone(),
)
.await,
RequestOutcome::Forwarded
));
let Message::Text(requested) = provider_receiver.recv().await.unwrap() else {
panic!("expected skin request")
};
assert!(matches!(
serde_json::from_str(&requested).unwrap(),
ServerMessage::SkinRequested { request_id, .. } if request_id == "request"
));
assert!(
provide(
&clients,
&provider_id,
provider_connection,
"request".to_owned(),
requester_id,
skin_hash,
Some("png".to_owned()),
)
.await
);
let Message::Text(resolved) = requester_receiver.recv().await.unwrap() else {
panic!("expected skin response")
};
assert!(matches!(
serde_json::from_str(&resolved).unwrap(),
ServerMessage::SkinResolved { data: Some(data), .. } if data == "png"
));
}
#[tokio::test]
async fn request_requires_the_exact_advertised_hash() {
let (requester_sender, _requester_receiver) = mpsc::channel(1);
let (provider_sender, mut provider_receiver) = mpsc::channel(1);
let requester_connection = Uuid::new_v4();
let requester_id = "requester".to_owned();
let provider_id = "provider".to_owned();
let clients = Clients::default();
{
let mut clients = clients.lock().await;
clients.insert(
requester_id.clone(),
client(
&requester_id,
requester_connection,
vec![provider_id.clone()],
None,
requester_sender,
),
);
clients.insert(
provider_id.clone(),
client(
&provider_id,
Uuid::new_v4(),
vec![requester_id.clone()],
Some("a".repeat(64)),
provider_sender,
),
);
}
let response = request(
&clients,
&requester_id,
requester_connection,
"request".to_owned(),
provider_id,
"b".repeat(64),
)
.await;
assert!(matches!(response, RequestOutcome::Unavailable(_)));
assert!(provider_receiver.try_recv().is_err());
}
async fn request_with_provider_sender(
provider_sender: mpsc::Sender<Message>,
) -> RequestOutcome {
let (requester_sender, _requester_receiver) = mpsc::channel(1);
let requester_connection = Uuid::new_v4();
let requester_id = "requester".to_owned();
let provider_id = "provider".to_owned();
let skin_hash = "a".repeat(64);
let clients = Clients::default();
{
let mut clients = clients.lock().await;
clients.insert(
requester_id.clone(),
client(
&requester_id,
requester_connection,
vec![provider_id.clone()],
None,
requester_sender,
),
);
clients.insert(
provider_id.clone(),
client(
&provider_id,
Uuid::new_v4(),
vec![requester_id.clone()],
Some(skin_hash.clone()),
provider_sender,
),
);
}
request(
&clients,
&requester_id,
requester_connection,
"request".to_owned(),
provider_id,
skin_hash,
)
.await
}
#[tokio::test]
async fn closed_target_queue_returns_an_immediate_response() {
let (provider_sender, provider_receiver) = mpsc::channel(1);
drop(provider_receiver);
assert!(matches!(
request_with_provider_sender(provider_sender).await,
RequestOutcome::Unavailable(_)
));
}
#[tokio::test]
async fn full_target_queue_returns_an_immediate_response() {
let (provider_sender, _provider_receiver) = mpsc::channel(1);
provider_sender
.try_send(Message::Text("occupied".into()))
.unwrap();
assert!(matches!(
request_with_provider_sender(provider_sender).await,
RequestOutcome::Unavailable(_)
));
}
}