minecraft skins!
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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(_)
|
||||
));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user