[Fix] WASM
This commit is contained in:
parent
3332aa621b
commit
cfc9cebf6a
14 changed files with 461 additions and 204 deletions
|
|
@ -4,6 +4,20 @@ use mtp_codec::{CommunicationValue, DataTypeId, DataValue, PROTOCOL_VERSION, Ver
|
||||||
use mtp_common::CommunicationError;
|
use mtp_common::CommunicationError;
|
||||||
use mtp_transport::{Policy, Receiver, Sender};
|
use mtp_transport::{Policy, Receiver, Sender};
|
||||||
|
|
||||||
|
#[cfg(feature = "crypto")]
|
||||||
|
fn unexpected_response_type_error(
|
||||||
|
context: &str,
|
||||||
|
expected_type: mtp_codec::CommunicationTypeId,
|
||||||
|
response: &CommunicationValue,
|
||||||
|
) -> CommunicationError {
|
||||||
|
CommunicationError::AuthenticationFailed(format!(
|
||||||
|
"unexpected response type during {context}: expected {:?}, got {:?}; parsed {}",
|
||||||
|
expected_type,
|
||||||
|
response.get_type(),
|
||||||
|
response
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
pub struct ClientConfig {
|
pub struct ClientConfig {
|
||||||
pub url: String,
|
pub url: String,
|
||||||
pub server_cert: Option<Vec<u8>>,
|
pub server_cert: Option<Vec<u8>>,
|
||||||
|
|
@ -124,6 +138,15 @@ impl MTPClient {
|
||||||
// 2. Receive host response (single message)
|
// 2. Receive host response (single message)
|
||||||
let response = receiver.receive().await?;
|
let response = receiver.receive().await?;
|
||||||
|
|
||||||
|
let expected_type = mtp_codec::CommunicationTypeId(16); // IdentificationResponse
|
||||||
|
if response.get_type() != expected_type {
|
||||||
|
return Err(unexpected_response_type_error(
|
||||||
|
"auth_connect",
|
||||||
|
expected_type,
|
||||||
|
&response,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let connected = response.get_data(DataTypeId(11));
|
let connected = response.get_data(DataTypeId(11));
|
||||||
match connected {
|
match connected {
|
||||||
DataValue::BoolTrue => {}
|
DataValue::BoolTrue => {}
|
||||||
|
|
@ -264,6 +287,15 @@ impl MTPClient {
|
||||||
// 2. Receive host response (single message)
|
// 2. Receive host response (single message)
|
||||||
let response = receiver.receive().await?;
|
let response = receiver.receive().await?;
|
||||||
|
|
||||||
|
let expected_type = mtp_codec::CommunicationTypeId(18); // RegisterResponse
|
||||||
|
if response.get_type() != expected_type {
|
||||||
|
return Err(unexpected_response_type_error(
|
||||||
|
"auth_register",
|
||||||
|
expected_type,
|
||||||
|
&response,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let connected = response.get_data(DataTypeId(11));
|
let connected = response.get_data(DataTypeId(11));
|
||||||
match connected {
|
match connected {
|
||||||
DataValue::BoolTrue => {}
|
DataValue::BoolTrue => {}
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,29 @@ mod auth;
|
||||||
mod messages;
|
mod messages;
|
||||||
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
use mtp::client::ClientConfig;
|
use mtp::client::ClientConfig;
|
||||||
use mtp::crypto::{KemPublicKey, PublicKeyBundle, SignaturePqPublicKey, SignaturePublicKey};
|
use mtp::crypto::{KemPublicKey, PublicKeyBundle, SignaturePqPublicKey, SignaturePublicKey};
|
||||||
|
|
||||||
|
fn dev_cert_path() -> String {
|
||||||
|
std::env::var("MTP_DEV_CERT").unwrap_or_else(|_| {
|
||||||
|
if Path::new("example-usage/dev-cert/cert.pem").exists() {
|
||||||
|
"example-usage/dev-cert/cert.pem".to_string()
|
||||||
|
} else {
|
||||||
|
"dev-cert/cert.pem".to_string()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let cert_pem = fs::read("server.pem").expect("Missing server.pem: run server first");
|
let cert_path = dev_cert_path();
|
||||||
|
let cert_pem = fs::read(&cert_path).unwrap_or_else(|e| {
|
||||||
|
panic!(
|
||||||
|
"Missing TLS certificate at {cert_path}: enter the Nix shell first or run the server to generate it: {e}"
|
||||||
|
)
|
||||||
|
});
|
||||||
let host_public_key = PublicKeyBundle::new(
|
let host_public_key = PublicKeyBundle::new(
|
||||||
KemPublicKey::new(
|
KemPublicKey::new(
|
||||||
fs::read("host_enc_kem_pk.bin")
|
fs::read("host_enc_kem_pk.bin")
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ fn derive_demo_key() -> [u8; 32] {
|
||||||
|
|
||||||
pub fn build_demo_message(client_id: u64, keyring: &Keyring) -> CommunicationValue {
|
pub fn build_demo_message(client_id: u64, keyring: &Keyring) -> CommunicationValue {
|
||||||
let cipher = ChaCha20Poly1305::new(derive_demo_key());
|
let cipher = ChaCha20Poly1305::new(derive_demo_key());
|
||||||
let signer = Ed25519Signer::new(&keyring.sig_cl_secret_key)
|
let signer =
|
||||||
.expect("Ed25519 signer from keyring");
|
Ed25519Signer::new(&keyring.sig_cl_secret_key).expect("Ed25519 signer from keyring");
|
||||||
|
|
||||||
let inner_enc = DataValue::Container(vec![
|
let inner_enc = DataValue::Container(vec![
|
||||||
(DataTypeId(1), DataValue::Str("secret inner data".into())),
|
(DataTypeId(1), DataValue::Str("secret inner data".into())),
|
||||||
|
|
@ -31,7 +31,10 @@ pub fn build_demo_message(client_id: u64, keyring: &Keyring) -> CommunicationVal
|
||||||
dv_sig.sign_container(SigAlgorithm::ED25519, &signer);
|
dv_sig.sign_container(SigAlgorithm::ED25519, &signer);
|
||||||
|
|
||||||
let inner_sec = DataValue::Container(vec![
|
let inner_sec = DataValue::Container(vec![
|
||||||
(DataTypeId(1), DataValue::Str("signed+encrypted payload".into())),
|
(
|
||||||
|
DataTypeId(1),
|
||||||
|
DataValue::Str("signed+encrypted payload".into()),
|
||||||
|
),
|
||||||
(DataTypeId(2), DataValue::UnsignedNumber(7)),
|
(DataTypeId(2), DataValue::UnsignedNumber(7)),
|
||||||
]);
|
]);
|
||||||
let mut dv_sec = inner_sec;
|
let mut dv_sec = inner_sec;
|
||||||
|
|
@ -42,13 +45,22 @@ pub fn build_demo_message(client_id: u64, keyring: &Keyring) -> CommunicationVal
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.as_secs();
|
.as_secs();
|
||||||
|
|
||||||
CommunicationValue::new(CommunicationType::Ping)
|
let msg = CommunicationValue::new(CommunicationType::Ping)
|
||||||
.add_typed_default(DataType::Description, DataValue::Str("MTP Data Type Demo".into()))
|
.add_typed_default(
|
||||||
.add_typed_default(DataType::Timestamp, DataValue::UnsignedNumber(timestamp as u128))
|
DataType::Description,
|
||||||
|
DataValue::Str("MTP Data Type Demo".into()),
|
||||||
|
)
|
||||||
|
.add_typed_default(
|
||||||
|
DataType::Timestamp,
|
||||||
|
DataValue::UnsignedNumber(timestamp as u128),
|
||||||
|
)
|
||||||
.add_typed_default(DataType::Data, DataValue::Str("Hello, MTP!".into()))
|
.add_typed_default(DataType::Data, DataValue::Str("Hello, MTP!".into()))
|
||||||
.add_typed_default(DataType::Flags, DataValue::BoolTrue)
|
.add_typed_default(DataType::Flags, DataValue::BoolTrue)
|
||||||
.add_typed_default(DataType::Value, DataValue::Float(2, 12345))
|
.add_typed_default(DataType::Value, DataValue::Float(2, 12345))
|
||||||
.add_typed_default(DataType::BinaryData, DataValue::Bytes(vec![0xDE, 0xAD, 0xBE, 0xEF, 0x42]))
|
.add_typed_default(
|
||||||
|
DataType::BinaryData,
|
||||||
|
DataValue::Bytes(vec![0xDE, 0xAD, 0xBE, 0xEF, 0x42]),
|
||||||
|
)
|
||||||
.add_typed_default(
|
.add_typed_default(
|
||||||
DataType::Items,
|
DataType::Items,
|
||||||
DataValue::Array(vec![
|
DataValue::Array(vec![
|
||||||
|
|
@ -60,7 +72,8 @@ pub fn build_demo_message(client_id: u64, keyring: &Keyring) -> CommunicationVal
|
||||||
.add_typed_default(DataType::EncryptedPayload, dv_enc)
|
.add_typed_default(DataType::EncryptedPayload, dv_enc)
|
||||||
.add_typed_default(DataType::SignedPayload, dv_sig)
|
.add_typed_default(DataType::SignedPayload, dv_sig)
|
||||||
.add_typed_default(DataType::SecurePayload, dv_sec)
|
.add_typed_default(DataType::SecurePayload, dv_sec)
|
||||||
.with_sender(client_id)
|
.with_sender(client_id);
|
||||||
|
msg
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_and_receive(
|
pub async fn send_and_receive(
|
||||||
|
|
@ -72,7 +85,9 @@ pub async fn send_and_receive(
|
||||||
conn.sender.send(&msg).await?;
|
conn.sender.send(&msg).await?;
|
||||||
|
|
||||||
match conn.receiver.receive().await {
|
match conn.receiver.receive().await {
|
||||||
Ok(resp) => println!("Received: {resp}"),
|
Ok(resp) => {
|
||||||
|
println!("Received: {resp}");
|
||||||
|
}
|
||||||
Err(e) => eprintln!("Receive error: {e}"),
|
Err(e) => eprintln!("Receive error: {e}"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,18 @@ pub fn load_client_db(
|
||||||
path: &str,
|
path: &str,
|
||||||
) -> Result<(Arc<Mutex<HashMap<u64, PublicKeyBundle>>>, Arc<Mutex<u64>>), Box<dyn std::error::Error>>
|
) -> Result<(Arc<Mutex<HashMap<u64, PublicKeyBundle>>>, Arc<Mutex<u64>>), Box<dyn std::error::Error>>
|
||||||
{
|
{
|
||||||
let clients: Arc<Mutex<HashMap<u64, PublicKeyBundle>>> =
|
let clients_map = match fs::read_to_string(path) {
|
||||||
Arc::new(Mutex::new(if let Ok(data) = fs::read_to_string(path) {
|
Ok(data) => match serde_json::from_str(&data) {
|
||||||
serde_json::from_str(&data).unwrap_or_default()
|
Ok(clients) => clients,
|
||||||
} else {
|
Err(e) => {
|
||||||
|
eprintln!("Failed to parse {path}; starting with empty client database: {e}");
|
||||||
HashMap::new()
|
HashMap::new()
|
||||||
}));
|
}
|
||||||
let next_id = Arc::new(Mutex::new(
|
},
|
||||||
clients.lock().unwrap().keys().max().unwrap_or(&999) + 1,
|
Err(_) => HashMap::new(),
|
||||||
));
|
};
|
||||||
|
let clients: Arc<Mutex<HashMap<u64, PublicKeyBundle>>> = Arc::new(Mutex::new(clients_map));
|
||||||
|
let next_value = clients.lock().unwrap().keys().max().unwrap_or(&999) + 1;
|
||||||
|
let next_id = Arc::new(Mutex::new(next_value));
|
||||||
Ok((clients, next_id))
|
Ok((clients, next_id))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataTypeId, DataValue, TypeMap};
|
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataTypeId, DataValue, TypeMap};
|
||||||
use mtp::crypto::{ChaCha20Poly1305, CryptoError, SignatureScheme, SignaturePublicKey, verify_ed25519};
|
use mtp::crypto::{
|
||||||
|
ChaCha20Poly1305, CryptoError, SignaturePublicKey, SignatureScheme, verify_ed25519,
|
||||||
|
};
|
||||||
|
|
||||||
struct Ed25519Verifier(SignaturePublicKey);
|
struct Ed25519Verifier(SignaturePublicKey);
|
||||||
|
|
||||||
|
|
@ -71,6 +73,7 @@ pub fn process_and_respond(
|
||||||
enc_status = format!("EncryptedPayload decrypted OK ({} entries)", entries.len());
|
enc_status = format!("EncryptedPayload decrypted OK ({} entries)", entries.len());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
eprintln!(" EncryptedPayload decryption failed");
|
||||||
enc_status = String::from("EncryptedPayload: decryption FAILED");
|
enc_status = String::from("EncryptedPayload: decryption FAILED");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -83,13 +86,14 @@ pub fn process_and_respond(
|
||||||
if dv.verify_into_container(&verifier).is_some() {
|
if dv.verify_into_container(&verifier).is_some() {
|
||||||
if let Some(entries) = dv.as_container() {
|
if let Some(entries) = dv.as_container() {
|
||||||
println!(" Verified SignedPayload: {:?}", entries);
|
println!(" Verified SignedPayload: {:?}", entries);
|
||||||
sig_status =
|
sig_status = format!("SignedPayload verified OK ({} entries)", entries.len());
|
||||||
format!("SignedPayload verified OK ({} entries)", entries.len());
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
eprintln!(" SignedPayload verification failed");
|
||||||
sig_status = String::from("SignedPayload: verification FAILED");
|
sig_status = String::from("SignedPayload: verification FAILED");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
eprintln!(" SignedPayload cannot be verified; no client public key available");
|
||||||
sig_status = String::from("SignedPayload: no client public key available");
|
sig_status = String::from("SignedPayload: no client public key available");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -99,7 +103,9 @@ pub fn process_and_respond(
|
||||||
if let Some(pk_bundle) = client_pk {
|
if let Some(pk_bundle) = client_pk {
|
||||||
let verifier = Ed25519Verifier(pk_bundle.sig_cl_public_key.clone());
|
let verifier = Ed25519Verifier(pk_bundle.sig_cl_public_key.clone());
|
||||||
let mut dv = secure.clone();
|
let mut dv = secure.clone();
|
||||||
if dv.decrypt_signed_encrypted_container(&cipher, b"demo-aad").is_some()
|
if dv
|
||||||
|
.decrypt_signed_encrypted_container(&cipher, b"demo-aad")
|
||||||
|
.is_some()
|
||||||
&& dv.verify_into_container(&verifier).is_some()
|
&& dv.verify_into_container(&verifier).is_some()
|
||||||
{
|
{
|
||||||
if let Some(entries) = dv.as_container() {
|
if let Some(entries) = dv.as_container() {
|
||||||
|
|
@ -110,9 +116,11 @@ pub fn process_and_respond(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
eprintln!(" SecurePayload decryption/verification failed");
|
||||||
secure_status = String::from("SecurePayload: decryption/verification FAILED");
|
secure_status = String::from("SecurePayload: decryption/verification FAILED");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
eprintln!(" SecurePayload cannot be verified; no client public key available");
|
||||||
secure_status = String::from("SecurePayload: no client public key available");
|
secure_status = String::from("SecurePayload: no client public key available");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
use mtp::crypto::{Ed25519Signer, Keyring, MlDsaSigner};
|
|
||||||
use mtp::crypto::kem::HybridKem;
|
use mtp::crypto::kem::HybridKem;
|
||||||
|
use mtp::crypto::{Ed25519Signer, Keyring, MlDsaSigner};
|
||||||
|
|
||||||
pub fn load_or_generate_host_keys(path: &str) -> Result<(u64, Keyring), Box<dyn std::error::Error>> {
|
pub fn load_or_generate_host_keys(
|
||||||
|
path: &str,
|
||||||
|
) -> Result<(u64, Keyring), Box<dyn std::error::Error>> {
|
||||||
if let Ok(data) = fs::read_to_string(path) {
|
if let Ok(data) = fs::read_to_string(path) {
|
||||||
let json: serde_json::Value = serde_json::from_str(&data)?;
|
let json: serde_json::Value = serde_json::from_str(&data)?;
|
||||||
let hid = json["host_id"].as_u64().unwrap_or(1);
|
let hid = json["host_id"].as_u64().unwrap_or(1);
|
||||||
|
|
@ -29,10 +31,7 @@ pub fn load_or_generate_host_keys(path: &str) -> Result<(u64, Keyring), Box<dyn
|
||||||
pub fn export_host_public_keys(host_keyring: &Keyring) -> Result<(), Box<dyn std::error::Error>> {
|
pub fn export_host_public_keys(host_keyring: &Keyring) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let public_key_bundle_hex = hex::encode(host_keyring.public_key_bundle().as_bytes());
|
let public_key_bundle_hex = hex::encode(host_keyring.public_key_bundle().as_bytes());
|
||||||
|
|
||||||
fs::write(
|
fs::write("host_public_key_bundle.hex", &public_key_bundle_hex)?;
|
||||||
"host_public_key_bundle.hex",
|
|
||||||
&public_key_bundle_hex,
|
|
||||||
)?;
|
|
||||||
fs::create_dir_all("web-client/public")?;
|
fs::create_dir_all("web-client/public")?;
|
||||||
fs::write(
|
fs::write(
|
||||||
"web-client/public/host_public_key_bundle.hex",
|
"web-client/public/host_public_key_bundle.hex",
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
|
||||||
let clients_for_get = clients.clone();
|
let clients_for_get = clients.clone();
|
||||||
let get_existing_user = Box::new(move |id: u64| -> Option<mtp::crypto::PublicKeyBundle> {
|
let get_existing_user = Box::new(move |id: u64| -> Option<mtp::crypto::PublicKeyBundle> {
|
||||||
clients_for_get.lock().unwrap().get(&id).cloned()
|
let result = clients_for_get.lock().unwrap().get(&id).cloned();
|
||||||
|
if result.is_some() {
|
||||||
|
println!("Auth lookup: client ID {id} found");
|
||||||
|
} else {
|
||||||
|
eprintln!("Auth lookup: unknown client ID {id}");
|
||||||
|
}
|
||||||
|
result
|
||||||
});
|
});
|
||||||
|
|
||||||
let clients_for_register = clients.clone();
|
let clients_for_register = clients.clone();
|
||||||
|
|
@ -52,7 +58,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let id = *nid;
|
let id = *nid;
|
||||||
*nid += 1;
|
*nid += 1;
|
||||||
db.insert(id, bundle);
|
db.insert(id, bundle);
|
||||||
std::fs::write(&clients_path, serde_json::to_string_pretty(&*db).unwrap()).ok();
|
match serde_json::to_string_pretty(&*db) {
|
||||||
|
Ok(json) => match std::fs::write(&clients_path, json) {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(e) => eprintln!("Failed to persist client database to {clients_path}: {e}"),
|
||||||
|
},
|
||||||
|
Err(e) => eprintln!("Failed to serialize client database after registering {id}: {e}"),
|
||||||
|
}
|
||||||
println!("Registered new client with ID: {}", id);
|
println!("Registered new client with ID: {}", id);
|
||||||
id
|
id
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import init, {
|
||||||
ed25519_generate,
|
ed25519_generate,
|
||||||
keyring_from_ed25519,
|
keyring_from_ed25519,
|
||||||
build_demo_message,
|
build_demo_message,
|
||||||
|
format_frame,
|
||||||
} from "mtp-wasm";
|
} from "mtp-wasm";
|
||||||
|
|
||||||
const STATUS = document.getElementById("status")!;
|
const STATUS = document.getElementById("status")!;
|
||||||
|
|
@ -57,7 +58,9 @@ function hexToBytes(value: string): Uint8Array {
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveKeys() {
|
function saveKeys() {
|
||||||
if (!keyringBytes) return;
|
if (!keyringBytes) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let hostPublicKey: number[] | undefined;
|
let hostPublicKey: number[] | undefined;
|
||||||
try {
|
try {
|
||||||
|
|
@ -106,7 +109,7 @@ async function loadHostPublicKey() {
|
||||||
|
|
||||||
HOST_PUBLIC_KEY.value = hostPublicKey;
|
HOST_PUBLIC_KEY.value = hostPublicKey;
|
||||||
saveKeys();
|
saveKeys();
|
||||||
log("Loaded host public key bundle from public file.");
|
log(`Loaded host public key bundle (${hostPublicKey.length / 2} bytes).`);
|
||||||
} catch {
|
} catch {
|
||||||
// Manual paste still works when the server has not exported the file yet.
|
// Manual paste still works when the server has not exported the file yet.
|
||||||
}
|
}
|
||||||
|
|
@ -138,11 +141,11 @@ function createClient(): WasmClient {
|
||||||
(state: number) =>
|
(state: number) =>
|
||||||
log(`[state] ${ConnectionState[state] ?? state}`, "state"),
|
log(`[state] ${ConnectionState[state] ?? state}`, "state"),
|
||||||
(data: Uint8Array) => {
|
(data: Uint8Array) => {
|
||||||
const decoder = new TextDecoder();
|
try {
|
||||||
log(
|
log(`Received: ${format_frame(data)}`, "received");
|
||||||
`[message] ${data.length} bytes: ${decoder.decode(data)}`,
|
} catch (e) {
|
||||||
"received",
|
log(`[message parse error] ${e}`, "error");
|
||||||
);
|
}
|
||||||
},
|
},
|
||||||
(err: any) => log(`[error] ${err}`, "error"),
|
(err: any) => log(`[error] ${err}`, "error"),
|
||||||
);
|
);
|
||||||
|
|
@ -181,7 +184,8 @@ async function connect() {
|
||||||
await loadDevCertHash();
|
await loadDevCertHash();
|
||||||
|
|
||||||
const client = createClient();
|
const client = createClient();
|
||||||
const config = new ConnectionConfig(SERVER_URL.value.trim());
|
const serverUrl = SERVER_URL.value.trim();
|
||||||
|
const config = new ConnectionConfig(serverUrl);
|
||||||
if (devCertHash) {
|
if (devCertHash) {
|
||||||
log(`Pinning WebTransport certificate hash: sha-256:${devCertHash}`);
|
log(`Pinning WebTransport certificate hash: sha-256:${devCertHash}`);
|
||||||
config.server_certificate_hashes = [`sha-256:${devCertHash}`];
|
config.server_certificate_hashes = [`sha-256:${devCertHash}`];
|
||||||
|
|
@ -210,6 +214,7 @@ async function connect() {
|
||||||
|
|
||||||
log("\nSending demo message...");
|
log("\nSending demo message...");
|
||||||
const frame = build_demo_message(activeClientId, keyringBytes);
|
const frame = build_demo_message(activeClientId, keyringBytes);
|
||||||
|
log(`Sending: ${format_frame(frame)}`, "state");
|
||||||
await client.send(frame);
|
await client.send(frame);
|
||||||
log(`Sent ${frame.length} bytes`);
|
log(`Sent ${frame.length} bytes`);
|
||||||
|
|
||||||
|
|
@ -236,6 +241,10 @@ GENERATE_KEYPAIR.addEventListener("click", () => {
|
||||||
CONNECT.addEventListener("click", () => {
|
CONNECT.addEventListener("click", () => {
|
||||||
connect().catch((e) => {
|
connect().catch((e) => {
|
||||||
log(`Fatal error: ${e}`, "error");
|
log(`Fatal error: ${e}`, "error");
|
||||||
|
log(
|
||||||
|
`[fatal context] clientId=${clientId?.toString() ?? "unregistered"}, server=${SERVER_URL.value.trim()}, hostPkChars=${HOST_PUBLIC_KEY.value.replace(/[^0-9a-fA-F]/g, "").length}, keyringBytes=${keyringBytes?.length ?? 0}, certHash=${devCertHash || "none"}`,
|
||||||
|
"error",
|
||||||
|
);
|
||||||
console.error(e);
|
console.error(e);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
34
flake.nix
34
flake.nix
|
|
@ -7,18 +7,16 @@
|
||||||
flake-utils.url = "github:numtide/flake-utils";
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs =
|
outputs = {
|
||||||
{
|
|
||||||
self,
|
self,
|
||||||
nixpkgs,
|
nixpkgs,
|
||||||
rust-overlay,
|
rust-overlay,
|
||||||
flake-utils,
|
flake-utils,
|
||||||
}:
|
}:
|
||||||
flake-utils.lib.eachDefaultSystem (
|
flake-utils.lib.eachDefaultSystem (
|
||||||
system:
|
system: let
|
||||||
let
|
overlays = [rust-overlay.overlays.default];
|
||||||
overlays = [ rust-overlay.overlays.default ];
|
pkgs = import nixpkgs {inherit system overlays;};
|
||||||
pkgs = import nixpkgs { inherit system overlays; };
|
|
||||||
|
|
||||||
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
|
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
|
||||||
extensions = [
|
extensions = [
|
||||||
|
|
@ -26,11 +24,11 @@
|
||||||
"clippy"
|
"clippy"
|
||||||
"rustfmt"
|
"rustfmt"
|
||||||
];
|
];
|
||||||
targets = [ "wasm32-unknown-unknown" ];
|
targets = ["wasm32-unknown-unknown"];
|
||||||
};
|
};
|
||||||
in
|
in {
|
||||||
{
|
devShells = {
|
||||||
devShells.default = pkgs.mkShell {
|
default = pkgs.mkShell {
|
||||||
name = "mtp-dev";
|
name = "mtp-dev";
|
||||||
|
|
||||||
buildInputs = with pkgs; [
|
buildInputs = with pkgs; [
|
||||||
|
|
@ -43,8 +41,9 @@
|
||||||
MTP_TYPE_MAPS = "${toString ./example-usage/type-maps.yaml}";
|
MTP_TYPE_MAPS = "${toString ./example-usage/type-maps.yaml}";
|
||||||
|
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
cert_dir="$PWD/example-usage/dev-cert"
|
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
||||||
public_dir="$PWD/example-usage/web-client/public"
|
cert_dir="$repo_root/example-usage/dev-cert"
|
||||||
|
public_dir="$repo_root/example-usage/web-client/public"
|
||||||
cert_key="$cert_dir/key.pem"
|
cert_key="$cert_dir/key.pem"
|
||||||
cert_pem="$cert_dir/cert.pem"
|
cert_pem="$cert_dir/cert.pem"
|
||||||
cert_hash="$cert_dir/sha256.txt"
|
cert_hash="$cert_dir/sha256.txt"
|
||||||
|
|
@ -89,6 +88,17 @@
|
||||||
echo " cert sha256: $MTP_DEV_CERT_HASH"
|
echo " cert sha256: $MTP_DEV_CERT_HASH"
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
autoStart = pkgs.mkShell {
|
||||||
|
name = "autoStart";
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
mprocs
|
||||||
|
];
|
||||||
|
shellHook = ''
|
||||||
|
nix develop --command bash -c "mprocs 'cd wasm && wasm-pack build --target web --out-dir pkg && cd ../example-usage/web-client && bun dev' 'cargo b && cd example-usage && cargo r --bin server'"
|
||||||
|
exit
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
# Ad-hoc WASM build using wasm-pack
|
# Ad-hoc WASM build using wasm-pack
|
||||||
apps.wasm-build = {
|
apps.wasm-build = {
|
||||||
|
|
|
||||||
|
|
@ -176,7 +176,9 @@ impl MTPHost {
|
||||||
_ => vec![],
|
_ => vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
let (assigned_id, client_bundle) = if msg.get_type() == mtp_codec::CommunicationTypeId(15) {
|
let (assigned_id, client_bundle, response_type) = if msg.get_type()
|
||||||
|
== mtp_codec::CommunicationTypeId(15)
|
||||||
|
{
|
||||||
// LOGIN
|
// LOGIN
|
||||||
let cid = match msg.get_data(DataTypeId(6)) {
|
let cid = match msg.get_data(DataTypeId(6)) {
|
||||||
DataValue::UnsignedNumber(n) => *n as u64,
|
DataValue::UnsignedNumber(n) => *n as u64,
|
||||||
|
|
@ -237,7 +239,11 @@ impl MTPHost {
|
||||||
}
|
}
|
||||||
/* ===== End Signature ===== */
|
/* ===== End Signature ===== */
|
||||||
|
|
||||||
(cid, bundle)
|
(
|
||||||
|
cid,
|
||||||
|
bundle,
|
||||||
|
mtp_codec::CommunicationType::IdentificationResponse,
|
||||||
|
)
|
||||||
} else if msg.get_type() == mtp_codec::CommunicationTypeId(17) {
|
} else if msg.get_type() == mtp_codec::CommunicationTypeId(17) {
|
||||||
// REGISTER
|
// REGISTER
|
||||||
let bundle = match msg.get_data(DataTypeId(9)) {
|
let bundle = match msg.get_data(DataTypeId(9)) {
|
||||||
|
|
@ -284,7 +290,11 @@ impl MTPHost {
|
||||||
/* ===== End Signature ===== */
|
/* ===== End Signature ===== */
|
||||||
|
|
||||||
let new_id = (self.config.complete_register)(bundle.clone());
|
let new_id = (self.config.complete_register)(bundle.clone());
|
||||||
(new_id, bundle)
|
(
|
||||||
|
new_id,
|
||||||
|
bundle,
|
||||||
|
mtp_codec::CommunicationType::RegisterResponse,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
sender.close();
|
sender.close();
|
||||||
return None;
|
return None;
|
||||||
|
|
@ -304,8 +314,7 @@ impl MTPHost {
|
||||||
/* ===== Signature ===== */
|
/* ===== Signature ===== */
|
||||||
let host_sig = host_signer.sign(&host_sig_payload).ok()?;
|
let host_sig = host_signer.sign(&host_sig_payload).ok()?;
|
||||||
|
|
||||||
let mut response =
|
let mut response = CommunicationValue::new(response_type)
|
||||||
CommunicationValue::new(mtp_codec::CommunicationType::IdentificationResponse)
|
|
||||||
.add_typed_default(DataType::Connected, DataValue::BoolTrue)
|
.add_typed_default(DataType::Connected, DataValue::BoolTrue)
|
||||||
.add_typed_default(
|
.add_typed_default(
|
||||||
DataType::ClientNonce,
|
DataType::ClientNonce,
|
||||||
|
|
@ -335,6 +344,7 @@ impl MTPHost {
|
||||||
/* ===== End Signature ===== */
|
/* ===== End Signature ===== */
|
||||||
|
|
||||||
sender.send(&response).await.ok()?;
|
sender.send(&response).await.ok()?;
|
||||||
|
sender.finish_stream().await.ok()?;
|
||||||
|
|
||||||
// 3. Version negotiation
|
// 3. Version negotiation
|
||||||
let negotiated = self.registry.negotiate(&[client_version])?;
|
let negotiated = self.registry.negotiate(&[client_version])?;
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ enum ReceivedFrame {
|
||||||
|
|
||||||
pub struct Sender {
|
pub struct Sender {
|
||||||
send_guard: Mutex<()>,
|
send_guard: Mutex<()>,
|
||||||
stream_guard: Mutex<Option<wtransport::SendStream>>,
|
stream_guard: Arc<Mutex<Option<wtransport::SendStream>>>,
|
||||||
handle: Arc<ConnectionHandle>,
|
handle: Arc<ConnectionHandle>,
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
policy: Arc<Policy>,
|
policy: Arc<Policy>,
|
||||||
|
|
@ -71,7 +71,7 @@ impl Sender {
|
||||||
pub fn new(connection: Connection, handle: Arc<ConnectionHandle>, policy: Arc<Policy>) -> Self {
|
pub fn new(connection: Connection, handle: Arc<ConnectionHandle>, policy: Arc<Policy>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
send_guard: Mutex::new(()),
|
send_guard: Mutex::new(()),
|
||||||
stream_guard: Mutex::new(None),
|
stream_guard: Arc::new(Mutex::new(None)),
|
||||||
handle,
|
handle,
|
||||||
connection,
|
connection,
|
||||||
policy,
|
policy,
|
||||||
|
|
@ -275,6 +275,18 @@ impl Sender {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn finish_stream(&self) -> Result<(), CommunicationError> {
|
||||||
|
let _send_lock = self.send_guard.lock().await;
|
||||||
|
let mut stream_opt = self.stream_guard.lock().await;
|
||||||
|
if let Some(mut stream) = stream_opt.take() {
|
||||||
|
timeout(self.policy.write_timeout, stream.finish())
|
||||||
|
.await
|
||||||
|
.map_err(|_| CommunicationError::StreamError)?
|
||||||
|
.map_err(|_| CommunicationError::StreamError)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn handle(&self) -> &Arc<ConnectionHandle> {
|
pub fn handle(&self) -> &Arc<ConnectionHandle> {
|
||||||
&self.handle
|
&self.handle
|
||||||
}
|
}
|
||||||
|
|
@ -283,6 +295,7 @@ impl Sender {
|
||||||
let connection = self.connection.clone();
|
let connection = self.connection.clone();
|
||||||
let handle = self.handle.clone();
|
let handle = self.handle.clone();
|
||||||
let policy = self.policy.clone();
|
let policy = self.policy.clone();
|
||||||
|
let stream_guard = self.stream_guard.clone();
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if connection.quic_connection().close_reason().is_some() || handle.is_closed() {
|
if connection.quic_connection().close_reason().is_some() || handle.is_closed() {
|
||||||
|
|
@ -290,6 +303,14 @@ impl Sender {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(mut stream) = stream_guard.lock().await.take() {
|
||||||
|
match timeout(policy.write_timeout, stream.finish()).await {
|
||||||
|
Ok(Ok(())) => {}
|
||||||
|
Ok(Err(e)) => log::warn!("[Sender] persistent stream finish failed: {e}"),
|
||||||
|
Err(_) => log::warn!("[Sender] persistent stream finish timed out"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let _ = Self::send_close_frame(&connection, &policy).await;
|
let _ = Self::send_close_frame(&connection, &policy).await;
|
||||||
|
|
||||||
handle.close(Some(CommunicationError::StreamClosed));
|
handle.close(Some(CommunicationError::StreamClosed));
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,7 @@ use std::rc::Rc;
|
||||||
|
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
use mtp_codec::{
|
use mtp_codec::{CommunicationType, CommunicationValue, DataType, DataValue, PROTOCOL_VERSION};
|
||||||
CommunicationType, CommunicationValue, DataType, DataValue, PROTOCOL_VERSION,
|
|
||||||
};
|
|
||||||
use mtp_type_map::{CommunicationTypeId, DataTypeId};
|
use mtp_type_map::{CommunicationTypeId, DataTypeId};
|
||||||
|
|
||||||
use mtp_crypto::SignatureScheme;
|
use mtp_crypto::SignatureScheme;
|
||||||
|
|
@ -13,6 +11,31 @@ use mtp_crypto::SignatureScheme;
|
||||||
use crate::error::js_error;
|
use crate::error::js_error;
|
||||||
use crate::transport::WasmTransport;
|
use crate::transport::WasmTransport;
|
||||||
|
|
||||||
|
fn raw_frame_preview(bytes: &[u8]) -> String {
|
||||||
|
let shown = bytes.len().min(256);
|
||||||
|
let mut preview = hex::encode(&bytes[..shown]);
|
||||||
|
if bytes.len() > shown {
|
||||||
|
preview.push_str("...");
|
||||||
|
}
|
||||||
|
format!("{} bytes, hex={preview}", bytes.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unexpected_response_type_error(
|
||||||
|
context: &str,
|
||||||
|
expected_type: CommunicationTypeId,
|
||||||
|
response_type: CommunicationTypeId,
|
||||||
|
response: &[u8],
|
||||||
|
parsed: &CommunicationValue,
|
||||||
|
) -> JsValue {
|
||||||
|
js_error(&format!(
|
||||||
|
"unexpected response type during {context}: expected {:?}, got {:?}; raw {}; parsed {}",
|
||||||
|
expected_type,
|
||||||
|
response_type,
|
||||||
|
raw_frame_preview(response),
|
||||||
|
parsed
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum ConnectionState {
|
pub enum ConnectionState {
|
||||||
|
|
@ -33,17 +56,27 @@ pub struct ConnectionConfig {
|
||||||
impl ConnectionConfig {
|
impl ConnectionConfig {
|
||||||
#[wasm_bindgen(constructor)]
|
#[wasm_bindgen(constructor)]
|
||||||
pub fn new(url: String) -> Self {
|
pub fn new(url: String) -> Self {
|
||||||
Self { url, server_certificate_hashes: None, client_id: 0 }
|
Self {
|
||||||
|
url,
|
||||||
|
server_certificate_hashes: None,
|
||||||
|
client_id: 0,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(getter)]
|
#[wasm_bindgen(getter)]
|
||||||
pub fn url(&self) -> String { self.url.clone() }
|
pub fn url(&self) -> String {
|
||||||
|
self.url.clone()
|
||||||
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(setter)]
|
#[wasm_bindgen(setter)]
|
||||||
pub fn set_client_id(&mut self, id: u64) { self.client_id = id; }
|
pub fn set_client_id(&mut self, id: u64) {
|
||||||
|
self.client_id = id;
|
||||||
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(getter)]
|
#[wasm_bindgen(getter)]
|
||||||
pub fn client_id(&self) -> u64 { self.client_id }
|
pub fn client_id(&self) -> u64 {
|
||||||
|
self.client_id
|
||||||
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(setter)]
|
#[wasm_bindgen(setter)]
|
||||||
pub fn set_server_certificate_hashes(&mut self, hashes: Vec<String>) {
|
pub fn set_server_certificate_hashes(&mut self, hashes: Vec<String>) {
|
||||||
|
|
@ -79,24 +112,29 @@ impl WasmClient {
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn is_supported() -> bool {
|
pub fn is_supported() -> bool {
|
||||||
js_sys::Reflect::has(&js_sys::global(), &JsValue::from_str("WebTransport"))
|
js_sys::Reflect::has(&js_sys::global(), &JsValue::from_str("WebTransport")).unwrap_or(false)
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(getter)]
|
#[wasm_bindgen(getter)]
|
||||||
pub fn state(&self) -> u8 { self.state.get() as u8 }
|
pub fn state(&self) -> u8 {
|
||||||
|
self.state.get() as u8
|
||||||
|
}
|
||||||
|
|
||||||
/// Unauthenticated connect (sends basic Identification, enables receive loop).
|
/// Unauthenticated connect (sends basic Identification, enables receive loop).
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub async fn connect(&mut self, config: &ConnectionConfig) -> Result<(), JsValue> {
|
pub async fn connect(&mut self, config: &ConnectionConfig) -> Result<(), JsValue> {
|
||||||
self.set_state(ConnectionState::Connecting);
|
self.set_state(ConnectionState::Connecting);
|
||||||
let transport = WasmTransport::connect(&config.url, config.server_certificate_hashes.clone()).await?;
|
let transport =
|
||||||
|
WasmTransport::connect(&config.url, config.server_certificate_hashes.clone()).await?;
|
||||||
let inner = transport.inner().clone();
|
let inner = transport.inner().clone();
|
||||||
|
|
||||||
let version_str = format!("{}", PROTOCOL_VERSION);
|
let version_str = format!("{}", PROTOCOL_VERSION);
|
||||||
let ident = CommunicationValue::new(CommunicationType::Identification)
|
let ident = CommunicationValue::new(CommunicationType::Identification)
|
||||||
.add_typed_default(DataType::Version, DataValue::Str(version_str))
|
.add_typed_default(DataType::Version, DataValue::Str(version_str))
|
||||||
.add_typed_default(DataType::Id, DataValue::UnsignedNumber(config.client_id as u128));
|
.add_typed_default(
|
||||||
|
DataType::Id,
|
||||||
|
DataValue::UnsignedNumber(config.client_id as u128),
|
||||||
|
);
|
||||||
transport.send_frame(&ident.to_bytes()).await?;
|
transport.send_frame(&ident.to_bytes()).await?;
|
||||||
|
|
||||||
self.transport = Some(transport);
|
self.transport = Some(transport);
|
||||||
|
|
@ -106,7 +144,9 @@ impl WasmClient {
|
||||||
let on_msg = self.on_message.clone();
|
let on_msg = self.on_message.clone();
|
||||||
let on_err = self.on_error.clone();
|
let on_err = self.on_error.clone();
|
||||||
wasm_bindgen_futures::spawn_local(async move {
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
WasmTransport::from_inner(inner).receive_loop(on_msg, on_err).await;
|
WasmTransport::from_inner(inner)
|
||||||
|
.receive_loop(on_msg, on_err)
|
||||||
|
.await;
|
||||||
state.set(ConnectionState::Disconnected);
|
state.set(ConnectionState::Disconnected);
|
||||||
});
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -137,8 +177,7 @@ impl WasmClient {
|
||||||
|
|
||||||
let version_str = format!("{}", PROTOCOL_VERSION);
|
let version_str = format!("{}", PROTOCOL_VERSION);
|
||||||
let mut nonce_bytes = [0u8; 16];
|
let mut nonce_bytes = [0u8; 16];
|
||||||
getrandom::fill(&mut nonce_bytes)
|
getrandom::fill(&mut nonce_bytes).map_err(|_| js_error("rng failed"))?;
|
||||||
.map_err(|_| js_error("rng failed"))?;
|
|
||||||
let client_nonce = u128::from_be_bytes(nonce_bytes);
|
let client_nonce = u128::from_be_bytes(nonce_bytes);
|
||||||
|
|
||||||
// Build signature payload: version || client_id || client_nonce
|
// Build signature payload: version || client_id || client_nonce
|
||||||
|
|
@ -149,17 +188,22 @@ impl WasmClient {
|
||||||
|
|
||||||
let signer = mtp_crypto::Ed25519Signer::new(&keyring.sig_cl_secret_key)
|
let signer = mtp_crypto::Ed25519Signer::new(&keyring.sig_cl_secret_key)
|
||||||
.map_err(|e| js_error(&format!("signer creation failed: {}", e)))?;
|
.map_err(|e| js_error(&format!("signer creation failed: {}", e)))?;
|
||||||
let signature = signer.sign(&sig_payload)
|
let signature = signer
|
||||||
|
.sign(&sig_payload)
|
||||||
.map_err(|e| js_error(&format!("signature failed: {}", e)))?;
|
.map_err(|e| js_error(&format!("signature failed: {}", e)))?;
|
||||||
|
|
||||||
let frame = CommunicationValue::new(CommunicationType::Identification)
|
let frame = CommunicationValue::new(CommunicationType::Identification)
|
||||||
.add_typed_default(DataType::Version, DataValue::Str(version_str))
|
.add_typed_default(DataType::Version, DataValue::Str(version_str))
|
||||||
.add_typed_default(DataType::Id, DataValue::UnsignedNumber(client_id as u128))
|
.add_typed_default(DataType::Id, DataValue::UnsignedNumber(client_id as u128))
|
||||||
.add_typed_default(DataType::ClientNonce, DataValue::UnsignedNumber(client_nonce))
|
.add_typed_default(
|
||||||
|
DataType::ClientNonce,
|
||||||
|
DataValue::UnsignedNumber(client_nonce),
|
||||||
|
)
|
||||||
.add_typed_default(DataType::Signature, DataValue::Bytes(signature))
|
.add_typed_default(DataType::Signature, DataValue::Bytes(signature))
|
||||||
.to_bytes();
|
.to_bytes();
|
||||||
|
|
||||||
let transport = WasmTransport::connect(&config.url, config.server_certificate_hashes.clone()).await?;
|
let transport =
|
||||||
|
WasmTransport::connect(&config.url, config.server_certificate_hashes.clone()).await?;
|
||||||
let inner = transport.inner().clone();
|
let inner = transport.inner().clone();
|
||||||
transport.send_frame(&frame).await?;
|
transport.send_frame(&frame).await?;
|
||||||
|
|
||||||
|
|
@ -171,7 +215,13 @@ impl WasmClient {
|
||||||
let resp_type = resp_comm.get_type();
|
let resp_type = resp_comm.get_type();
|
||||||
let expected_type = CommunicationTypeId(16); // IdentificationResponse
|
let expected_type = CommunicationTypeId(16); // IdentificationResponse
|
||||||
if resp_type != expected_type {
|
if resp_type != expected_type {
|
||||||
return Err(js_error("unexpected response type"));
|
return Err(unexpected_response_type_error(
|
||||||
|
"auth_connect",
|
||||||
|
expected_type,
|
||||||
|
resp_type,
|
||||||
|
&response,
|
||||||
|
&resp_comm,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp_comm.get_data(DataTypeId(11)) != &DataValue::BoolTrue {
|
if resp_comm.get_data(DataTypeId(11)) != &DataValue::BoolTrue {
|
||||||
|
|
@ -197,7 +247,9 @@ impl WasmClient {
|
||||||
let on_msg = self.on_message.clone();
|
let on_msg = self.on_message.clone();
|
||||||
let on_err = self.on_error.clone();
|
let on_err = self.on_error.clone();
|
||||||
wasm_bindgen_futures::spawn_local(async move {
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
WasmTransport::from_inner(inner).receive_loop(on_msg, on_err).await;
|
WasmTransport::from_inner(inner)
|
||||||
|
.receive_loop(on_msg, on_err)
|
||||||
|
.await;
|
||||||
state.set(ConnectionState::Disconnected);
|
state.set(ConnectionState::Disconnected);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -227,8 +279,7 @@ impl WasmClient {
|
||||||
|
|
||||||
let version_str = format!("{}", PROTOCOL_VERSION);
|
let version_str = format!("{}", PROTOCOL_VERSION);
|
||||||
let mut nonce_bytes = [0u8; 16];
|
let mut nonce_bytes = [0u8; 16];
|
||||||
getrandom::fill(&mut nonce_bytes)
|
getrandom::fill(&mut nonce_bytes).map_err(|_| js_error("rng failed"))?;
|
||||||
.map_err(|_| js_error("rng failed"))?;
|
|
||||||
let client_nonce = u128::from_be_bytes(nonce_bytes);
|
let client_nonce = u128::from_be_bytes(nonce_bytes);
|
||||||
|
|
||||||
let pk_bytes = keyring.public_key_bundle().as_bytes();
|
let pk_bytes = keyring.public_key_bundle().as_bytes();
|
||||||
|
|
@ -241,17 +292,22 @@ impl WasmClient {
|
||||||
|
|
||||||
let signer = mtp_crypto::Ed25519Signer::new(&keyring.sig_cl_secret_key)
|
let signer = mtp_crypto::Ed25519Signer::new(&keyring.sig_cl_secret_key)
|
||||||
.map_err(|e| js_error(&format!("signer creation failed: {}", e)))?;
|
.map_err(|e| js_error(&format!("signer creation failed: {}", e)))?;
|
||||||
let signature = signer.sign(&sig_payload)
|
let signature = signer
|
||||||
|
.sign(&sig_payload)
|
||||||
.map_err(|e| js_error(&format!("signature failed: {}", e)))?;
|
.map_err(|e| js_error(&format!("signature failed: {}", e)))?;
|
||||||
|
|
||||||
let frame = CommunicationValue::new(CommunicationType::Register)
|
let frame = CommunicationValue::new(CommunicationType::Register)
|
||||||
.add_typed_default(DataType::Version, DataValue::Str(version_str))
|
.add_typed_default(DataType::Version, DataValue::Str(version_str))
|
||||||
.add_typed_default(DataType::ClientNonce, DataValue::UnsignedNumber(client_nonce))
|
.add_typed_default(
|
||||||
|
DataType::ClientNonce,
|
||||||
|
DataValue::UnsignedNumber(client_nonce),
|
||||||
|
)
|
||||||
.add_typed_default(DataType::PublicKeys, DataValue::Bytes(pk_bytes))
|
.add_typed_default(DataType::PublicKeys, DataValue::Bytes(pk_bytes))
|
||||||
.add_typed_default(DataType::Signature, DataValue::Bytes(signature))
|
.add_typed_default(DataType::Signature, DataValue::Bytes(signature))
|
||||||
.to_bytes();
|
.to_bytes();
|
||||||
|
|
||||||
let transport = WasmTransport::connect(&config.url, config.server_certificate_hashes.clone()).await?;
|
let transport =
|
||||||
|
WasmTransport::connect(&config.url, config.server_certificate_hashes.clone()).await?;
|
||||||
let inner = transport.inner().clone();
|
let inner = transport.inner().clone();
|
||||||
transport.send_frame(&frame).await?;
|
transport.send_frame(&frame).await?;
|
||||||
|
|
||||||
|
|
@ -262,7 +318,13 @@ impl WasmClient {
|
||||||
let resp_type = resp_comm.get_type();
|
let resp_type = resp_comm.get_type();
|
||||||
let expected_type = CommunicationTypeId(18); // RegisterResponse
|
let expected_type = CommunicationTypeId(18); // RegisterResponse
|
||||||
if resp_type != expected_type {
|
if resp_type != expected_type {
|
||||||
return Err(js_error("unexpected response type"));
|
return Err(unexpected_response_type_error(
|
||||||
|
"auth_register",
|
||||||
|
expected_type,
|
||||||
|
resp_type,
|
||||||
|
&response,
|
||||||
|
&resp_comm,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp_comm.get_data(DataTypeId(11)) != &DataValue::BoolTrue {
|
if resp_comm.get_data(DataTypeId(11)) != &DataValue::BoolTrue {
|
||||||
|
|
@ -286,7 +348,9 @@ impl WasmClient {
|
||||||
let on_msg = self.on_message.clone();
|
let on_msg = self.on_message.clone();
|
||||||
let on_err = self.on_error.clone();
|
let on_err = self.on_error.clone();
|
||||||
wasm_bindgen_futures::spawn_local(async move {
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
WasmTransport::from_inner(inner).receive_loop(on_msg, on_err).await;
|
WasmTransport::from_inner(inner)
|
||||||
|
.receive_loop(on_msg, on_err)
|
||||||
|
.await;
|
||||||
state.set(ConnectionState::Disconnected);
|
state.set(ConnectionState::Disconnected);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -303,16 +367,17 @@ impl WasmClient {
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn disconnect(&mut self) {
|
pub fn disconnect(&mut self) {
|
||||||
if let Some(t) = &self.transport { t.close(); }
|
if let Some(t) = &self.transport {
|
||||||
|
t.close();
|
||||||
|
}
|
||||||
self.transport = None;
|
self.transport = None;
|
||||||
self.set_state(ConnectionState::Disconnected);
|
self.set_state(ConnectionState::Disconnected);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_state(&self, new_state: ConnectionState) {
|
fn set_state(&self, new_state: ConnectionState) {
|
||||||
self.state.set(new_state);
|
self.state.set(new_state);
|
||||||
let _ = self.on_state_change.call1(
|
let _ = self
|
||||||
&JsValue::NULL,
|
.on_state_change
|
||||||
&JsValue::from(new_state as u8),
|
.call1(&JsValue::NULL, &JsValue::from(new_state as u8));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,23 @@
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
use mtp_codec::{
|
use mtp_codec::{CommunicationType, CommunicationValue, DataType, DataTypeId, DataValue};
|
||||||
CommunicationType, CommunicationValue, DataType, DataTypeId, DataValue,
|
use mtp_crypto::{ChaCha20Poly1305, Ed25519Signer, Keyring, SigAlgorithm, derive_encryption_key};
|
||||||
};
|
|
||||||
use mtp_type_map::communication_type_name;
|
use mtp_type_map::communication_type_name;
|
||||||
use mtp_crypto::{
|
|
||||||
ChaCha20Poly1305, Ed25519Signer, Keyring, SigAlgorithm,
|
|
||||||
derive_encryption_key,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::error::js_error;
|
use crate::error::js_error;
|
||||||
|
|
||||||
/// Build a simple Ping frame with description, timestamp, and optional data.
|
/// Build a simple Ping frame with description, timestamp, and optional data.
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn build_ping_frame(
|
pub fn build_ping_frame(client_id: u64, description: &str, timestamp: u64, data: &[u8]) -> Vec<u8> {
|
||||||
client_id: u64,
|
|
||||||
description: &str,
|
|
||||||
timestamp: u64,
|
|
||||||
data: &[u8],
|
|
||||||
) -> Vec<u8> {
|
|
||||||
let mut msg = CommunicationValue::new(CommunicationType::Ping)
|
let mut msg = CommunicationValue::new(CommunicationType::Ping)
|
||||||
.add_typed_default(DataType::Description, DataValue::Str(description.to_string()))
|
.add_typed_default(
|
||||||
.add_typed_default(DataType::Timestamp, DataValue::UnsignedNumber(timestamp as u128))
|
DataType::Description,
|
||||||
|
DataValue::Str(description.to_string()),
|
||||||
|
)
|
||||||
|
.add_typed_default(
|
||||||
|
DataType::Timestamp,
|
||||||
|
DataValue::UnsignedNumber(timestamp as u128),
|
||||||
|
)
|
||||||
.with_sender(client_id);
|
.with_sender(client_id);
|
||||||
|
|
||||||
if !data.is_empty() {
|
if !data.is_empty() {
|
||||||
|
|
@ -55,7 +51,8 @@ pub fn build_demo_message(client_id: u64, keyring_bytes: &[u8]) -> Result<Vec<u8
|
||||||
(DataTypeId(2), DataValue::UnsignedNumber(42)),
|
(DataTypeId(2), DataValue::UnsignedNumber(42)),
|
||||||
]);
|
]);
|
||||||
let mut dv_enc = inner_enc;
|
let mut dv_enc = inner_enc;
|
||||||
dv_enc.encrypt_container(&cipher, b"demo-aad")
|
dv_enc
|
||||||
|
.encrypt_container(&cipher, b"demo-aad")
|
||||||
.ok_or_else(|| js_error("encryption failed"))?;
|
.ok_or_else(|| js_error("encryption failed"))?;
|
||||||
|
|
||||||
// Signed container
|
// Signed container
|
||||||
|
|
@ -64,23 +61,34 @@ pub fn build_demo_message(client_id: u64, keyring_bytes: &[u8]) -> Result<Vec<u8
|
||||||
(DataTypeId(2), DataValue::UnsignedNumber(99)),
|
(DataTypeId(2), DataValue::UnsignedNumber(99)),
|
||||||
]);
|
]);
|
||||||
let mut dv_sig = inner_sig;
|
let mut dv_sig = inner_sig;
|
||||||
dv_sig.sign_container(SigAlgorithm::ED25519, &signer)
|
dv_sig
|
||||||
|
.sign_container(SigAlgorithm::ED25519, &signer)
|
||||||
.ok_or_else(|| js_error("signing failed"))?;
|
.ok_or_else(|| js_error("signing failed"))?;
|
||||||
|
|
||||||
// Signed + encrypted container
|
// Signed + encrypted container
|
||||||
let inner_sec = DataValue::Container(vec![
|
let inner_sec = DataValue::Container(vec![
|
||||||
(DataTypeId(1), DataValue::Str("signed+encrypted payload".into())),
|
(
|
||||||
|
DataTypeId(1),
|
||||||
|
DataValue::Str("signed+encrypted payload".into()),
|
||||||
|
),
|
||||||
(DataTypeId(2), DataValue::UnsignedNumber(7)),
|
(DataTypeId(2), DataValue::UnsignedNumber(7)),
|
||||||
]);
|
]);
|
||||||
let mut dv_sec = inner_sec;
|
let mut dv_sec = inner_sec;
|
||||||
dv_sec.sign_and_encrypt_container(SigAlgorithm::ED25519, &signer, &cipher, b"demo-aad")
|
dv_sec
|
||||||
|
.sign_and_encrypt_container(SigAlgorithm::ED25519, &signer, &cipher, b"demo-aad")
|
||||||
.ok_or_else(|| js_error("sign+encrypt failed"))?;
|
.ok_or_else(|| js_error("sign+encrypt failed"))?;
|
||||||
|
|
||||||
let timestamp = js_sys::Date::now() as u64;
|
let timestamp = js_sys::Date::now() as u64;
|
||||||
|
|
||||||
let msg = CommunicationValue::new(CommunicationType::Ping)
|
let msg = CommunicationValue::new(CommunicationType::Ping)
|
||||||
.add_typed_default(DataType::Description, DataValue::Str("MTP WASM Demo".into()))
|
.add_typed_default(
|
||||||
.add_typed_default(DataType::Timestamp, DataValue::UnsignedNumber(timestamp as u128))
|
DataType::Description,
|
||||||
|
DataValue::Str("MTP WASM Demo".into()),
|
||||||
|
)
|
||||||
|
.add_typed_default(
|
||||||
|
DataType::Timestamp,
|
||||||
|
DataValue::UnsignedNumber(timestamp as u128),
|
||||||
|
)
|
||||||
.add_typed_default(DataType::Version, DataValue::Str("demo-wasm".into()))
|
.add_typed_default(DataType::Version, DataValue::Str("demo-wasm".into()))
|
||||||
.with_sender(client_id);
|
.with_sender(client_id);
|
||||||
|
|
||||||
|
|
@ -174,7 +182,11 @@ pub fn parse_response_frame(frame: &[u8]) -> Result<String, JsValue> {
|
||||||
|
|
||||||
let obj = js_sys::Object::new();
|
let obj = js_sys::Object::new();
|
||||||
|
|
||||||
let _ = js_sys::Reflect::set(&obj, &JsValue::from_str("_id"), &JsValue::from(comm.get_id()));
|
let _ = js_sys::Reflect::set(
|
||||||
|
&obj,
|
||||||
|
&JsValue::from_str("_id"),
|
||||||
|
&JsValue::from(comm.get_id()),
|
||||||
|
);
|
||||||
|
|
||||||
let type_name = communication_type_name(comm.get_type().0).unwrap_or("Unknown");
|
let type_name = communication_type_name(comm.get_type().0).unwrap_or("Unknown");
|
||||||
let _ = js_sys::Reflect::set(
|
let _ = js_sys::Reflect::set(
|
||||||
|
|
@ -200,12 +212,21 @@ pub fn parse_response_frame(frame: &[u8]) -> Result<String, JsValue> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let stringified = js_sys::JSON::stringify(&obj)
|
let stringified =
|
||||||
.map_err(|_| js_error("JSON stringify failed"))?;
|
js_sys::JSON::stringify(&obj).map_err(|_| js_error("JSON stringify failed"))?;
|
||||||
stringified.as_string()
|
stringified
|
||||||
|
.as_string()
|
||||||
.ok_or_else(|| js_error("JSON stringify result not a string"))
|
.ok_or_else(|| js_error("JSON stringify result not a string"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse any MTP frame into the human-readable CommunicationValue display form.
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn format_frame(frame: &[u8]) -> Result<String, JsValue> {
|
||||||
|
let comm = CommunicationValue::from_bytes(frame)
|
||||||
|
.map_err(|e| js_error(&format!("parse failed: {}", e)))?;
|
||||||
|
Ok(comm.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
@ -219,8 +240,14 @@ mod tests {
|
||||||
|
|
||||||
assert_eq!(cv.get_type(), CommunicationTypeId(19)); // Ping
|
assert_eq!(cv.get_type(), CommunicationTypeId(19)); // Ping
|
||||||
assert_eq!(cv.get_sender(), 42);
|
assert_eq!(cv.get_sender(), 42);
|
||||||
assert_eq!(cv.get_data(DataTypeId(4)), &DataValue::Str("test-ping".into()));
|
assert_eq!(
|
||||||
assert_eq!(cv.get_data(DataTypeId(5)), &DataValue::UnsignedNumber(1234567890));
|
cv.get_data(DataTypeId(4)),
|
||||||
|
&DataValue::Str("test-ping".into())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
cv.get_data(DataTypeId(5)),
|
||||||
|
&DataValue::UnsignedNumber(1234567890)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen_test]
|
#[wasm_bindgen_test]
|
||||||
|
|
@ -231,9 +258,15 @@ mod tests {
|
||||||
|
|
||||||
assert_eq!(cv.get_type(), CommunicationTypeId(19));
|
assert_eq!(cv.get_type(), CommunicationTypeId(19));
|
||||||
assert_eq!(cv.get_sender(), 99);
|
assert_eq!(cv.get_sender(), 99);
|
||||||
assert_eq!(cv.get_data(DataTypeId(4)), &DataValue::Str("with-data".into()));
|
assert_eq!(
|
||||||
|
cv.get_data(DataTypeId(4)),
|
||||||
|
&DataValue::Str("with-data".into())
|
||||||
|
);
|
||||||
assert_eq!(cv.get_data(DataTypeId(5)), &DataValue::UnsignedNumber(555));
|
assert_eq!(cv.get_data(DataTypeId(5)), &DataValue::UnsignedNumber(555));
|
||||||
assert_eq!(cv.get_data(DataTypeId(6)), &DataValue::Bytes(payload.to_vec()));
|
assert_eq!(
|
||||||
|
cv.get_data(DataTypeId(6)),
|
||||||
|
&DataValue::Bytes(payload.to_vec())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen_test]
|
#[wasm_bindgen_test]
|
||||||
|
|
@ -264,7 +297,10 @@ mod tests {
|
||||||
|
|
||||||
assert_eq!(cv.get_type(), CommunicationTypeId(19)); // Ping
|
assert_eq!(cv.get_type(), CommunicationTypeId(19)); // Ping
|
||||||
assert_eq!(cv.get_sender(), 7);
|
assert_eq!(cv.get_sender(), 7);
|
||||||
assert_eq!(cv.get_data(DataTypeId(4)), &DataValue::Str("MTP WASM Demo".into()));
|
assert_eq!(
|
||||||
|
cv.get_data(DataTypeId(4)),
|
||||||
|
&DataValue::Str("MTP WASM Demo".into())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen_test]
|
#[wasm_bindgen_test]
|
||||||
|
|
@ -287,11 +323,13 @@ mod tests {
|
||||||
let result = parse_auth_response(&resp).expect("parse failed");
|
let result = parse_auth_response(&resp).expect("parse failed");
|
||||||
|
|
||||||
let connected = js_sys::Reflect::get(&result, &"connected".into())
|
let connected = js_sys::Reflect::get(&result, &"connected".into())
|
||||||
.ok().and_then(|v| v.as_bool());
|
.ok()
|
||||||
|
.and_then(|v| v.as_bool());
|
||||||
assert_eq!(connected, Some(true));
|
assert_eq!(connected, Some(true));
|
||||||
|
|
||||||
let id = js_sys::Reflect::get(&result, &"assignedId".into())
|
let id = js_sys::Reflect::get(&result, &"assignedId".into())
|
||||||
.ok().and_then(|v| v.as_f64());
|
.ok()
|
||||||
|
.and_then(|v| v.as_f64());
|
||||||
assert_eq!(id, Some(42.0));
|
assert_eq!(id, Some(42.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -304,7 +342,8 @@ mod tests {
|
||||||
let result = parse_auth_response(&resp).expect("parse failed");
|
let result = parse_auth_response(&resp).expect("parse failed");
|
||||||
|
|
||||||
let connected = js_sys::Reflect::get(&result, &"connected".into())
|
let connected = js_sys::Reflect::get(&result, &"connected".into())
|
||||||
.ok().and_then(|v| v.as_bool());
|
.ok()
|
||||||
|
.and_then(|v| v.as_bool());
|
||||||
assert_eq!(connected, Some(false));
|
assert_eq!(connected, Some(false));
|
||||||
|
|
||||||
// rejected should have no assignedId
|
// rejected should have no assignedId
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ use web_sys::{WebTransport, WebTransportHash, WebTransportOptions};
|
||||||
|
|
||||||
use crate::error::js_error;
|
use crate::error::js_error;
|
||||||
|
|
||||||
|
const CLOSE_FRAME_LEN: u32 = u32::MAX;
|
||||||
|
|
||||||
/// Given a `SendStream` (old API with `.writable` or new API where stream IS a WritableStream),
|
/// Given a `SendStream` (old API with `.writable` or new API where stream IS a WritableStream),
|
||||||
/// return the object to call `.getWriter()` on.
|
/// return the object to call `.getWriter()` on.
|
||||||
fn resolve_stream_writable(send_stream: &JsValue) -> Result<JsValue, JsValue> {
|
fn resolve_stream_writable(send_stream: &JsValue) -> Result<JsValue, JsValue> {
|
||||||
|
|
@ -199,10 +201,17 @@ impl WasmTransport {
|
||||||
}
|
}
|
||||||
|
|
||||||
if buffer.len() >= 4 {
|
if buffer.len() >= 4 {
|
||||||
let frame_len =
|
let frame_len = u32::from_be_bytes([buffer[0], buffer[1], buffer[2], buffer[3]]);
|
||||||
u32::from_be_bytes([buffer[0], buffer[1], buffer[2], buffer[3]]) as usize;
|
if frame_len == CLOSE_FRAME_LEN {
|
||||||
if 4 + frame_len <= buffer.len() {
|
return Err(js_error("connection closed before frame"));
|
||||||
return Ok(buffer[4..4 + frame_len].to_vec());
|
}
|
||||||
|
|
||||||
|
let frame_len = frame_len as usize;
|
||||||
|
let Some(frame_end) = 4usize.checked_add(frame_len) else {
|
||||||
|
return Err(js_error("invalid frame length"));
|
||||||
|
};
|
||||||
|
if frame_end <= buffer.len() {
|
||||||
|
return Ok(buffer[4..frame_end].to_vec());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -230,13 +239,7 @@ impl WasmTransport {
|
||||||
let result = match read_fn.call0(&reader_val) {
|
let result = match read_fn.call0(&reader_val) {
|
||||||
Ok(p) => match JsFuture::from(p.unchecked_into::<js_sys::Promise>()).await {
|
Ok(p) => match JsFuture::from(p.unchecked_into::<js_sys::Promise>()).await {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(e) => {
|
Err(_) => break,
|
||||||
let _ = on_error.call1(
|
|
||||||
&JsValue::NULL,
|
|
||||||
&JsValue::from_str(&format!("read stream failed: {:?}", e)),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
Err(_) => break,
|
Err(_) => break,
|
||||||
};
|
};
|
||||||
|
|
@ -302,29 +305,43 @@ impl WasmTransport {
|
||||||
|
|
||||||
// Extract all complete frames from the buffer
|
// Extract all complete frames from the buffer
|
||||||
while buffer.len() >= 4 {
|
while buffer.len() >= 4 {
|
||||||
let frame_len =
|
let frame_len = u32::from_be_bytes([buffer[0], buffer[1], buffer[2], buffer[3]]);
|
||||||
u32::from_be_bytes([buffer[0], buffer[1], buffer[2], buffer[3]]) as usize;
|
if frame_len == CLOSE_FRAME_LEN {
|
||||||
if 4 + frame_len > buffer.len() {
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let frame_len = frame_len as usize;
|
||||||
|
let Some(frame_end) = 4usize.checked_add(frame_len) else {
|
||||||
|
return Err(js_error("invalid frame length"));
|
||||||
|
};
|
||||||
|
if frame_end > buffer.len() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let frame = buffer[4..4 + frame_len].to_vec();
|
let frame = buffer[4..frame_end].to_vec();
|
||||||
let arr = js_sys::Uint8Array::from(&frame[..]);
|
let arr = js_sys::Uint8Array::from(&frame[..]);
|
||||||
let _ = on_message.call1(&JsValue::NULL, &arr);
|
let _ = on_message.call1(&JsValue::NULL, &arr);
|
||||||
buffer.drain(..4 + frame_len);
|
buffer.drain(..frame_end);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process any remaining complete frames after stream closes
|
// Process any remaining complete frames after stream closes
|
||||||
while buffer.len() >= 4 {
|
while buffer.len() >= 4 {
|
||||||
let frame_len =
|
let frame_len = u32::from_be_bytes([buffer[0], buffer[1], buffer[2], buffer[3]]);
|
||||||
u32::from_be_bytes([buffer[0], buffer[1], buffer[2], buffer[3]]) as usize;
|
if frame_len == CLOSE_FRAME_LEN {
|
||||||
if 4 + frame_len > buffer.len() {
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let frame_len = frame_len as usize;
|
||||||
|
let Some(frame_end) = 4usize.checked_add(frame_len) else {
|
||||||
|
return Err(js_error("invalid frame length"));
|
||||||
|
};
|
||||||
|
if frame_end > buffer.len() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let frame = buffer[4..4 + frame_len].to_vec();
|
let frame = buffer[4..frame_end].to_vec();
|
||||||
let arr = js_sys::Uint8Array::from(&frame[..]);
|
let arr = js_sys::Uint8Array::from(&frame[..]);
|
||||||
let _ = on_message.call1(&JsValue::NULL, &arr);
|
let _ = on_message.call1(&JsValue::NULL, &arr);
|
||||||
buffer.drain(..4 + frame_len);
|
buffer.drain(..frame_end);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue