client server connection loop
This commit is contained in:
Generated
+119
-8
@@ -241,6 +241,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
|
checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum-core",
|
"axum-core",
|
||||||
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
@@ -259,8 +260,10 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_path_to_error",
|
"serde_path_to_error",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
|
"sha1",
|
||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-tungstenite 0.29.0",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
@@ -823,6 +826,12 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "data-encoding"
|
||||||
|
version = "2.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dbus"
|
name = "dbus"
|
||||||
version = "0.9.11"
|
version = "0.9.11"
|
||||||
@@ -2451,7 +2460,7 @@ dependencies = [
|
|||||||
"num-integer",
|
"num-integer",
|
||||||
"num-iter",
|
"num-iter",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"rand",
|
"rand 0.8.7",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
@@ -3130,8 +3139,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a"
|
checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"rand_chacha",
|
"rand_chacha 0.3.1",
|
||||||
"rand_core",
|
"rand_core 0.6.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
|
||||||
|
dependencies = [
|
||||||
|
"rand_chacha 0.9.0",
|
||||||
|
"rand_core 0.9.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3141,7 +3160,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ppv-lite86",
|
"ppv-lite86",
|
||||||
"rand_core",
|
"rand_core 0.6.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core 0.9.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3153,6 +3182,15 @@ dependencies = [
|
|||||||
"getrandom 0.2.17",
|
"getrandom 0.2.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.3.4",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "raw-window-handle"
|
name = "raw-window-handle"
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
@@ -3310,7 +3348,7 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
"pkcs1",
|
"pkcs1",
|
||||||
"pkcs8",
|
"pkcs8",
|
||||||
"rand_core",
|
"rand_core 0.6.4",
|
||||||
"signature 2.2.0",
|
"signature 2.2.0",
|
||||||
"spki",
|
"spki",
|
||||||
"subtle",
|
"subtle",
|
||||||
@@ -3723,7 +3761,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"digest 0.10.7",
|
"digest 0.10.7",
|
||||||
"rand_core",
|
"rand_core 0.6.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3996,7 +4034,7 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"rand",
|
"rand 0.8.7",
|
||||||
"rsa",
|
"rsa",
|
||||||
"serde",
|
"serde",
|
||||||
"sha1",
|
"sha1",
|
||||||
@@ -4034,7 +4072,7 @@ dependencies = [
|
|||||||
"md-5",
|
"md-5",
|
||||||
"memchr",
|
"memchr",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rand",
|
"rand 0.8.7",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2 0.10.9",
|
"sha2 0.10.9",
|
||||||
@@ -4681,6 +4719,30 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-tungstenite"
|
||||||
|
version = "0.28.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857"
|
||||||
|
dependencies = [
|
||||||
|
"futures-util",
|
||||||
|
"log",
|
||||||
|
"tokio",
|
||||||
|
"tungstenite 0.28.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-tungstenite"
|
||||||
|
version = "0.29.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c"
|
||||||
|
dependencies = [
|
||||||
|
"futures-util",
|
||||||
|
"log",
|
||||||
|
"tokio",
|
||||||
|
"tungstenite 0.29.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.18"
|
version = "0.7.18"
|
||||||
@@ -4931,6 +4993,39 @@ version = "0.2.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tungstenite"
|
||||||
|
version = "0.28.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"data-encoding",
|
||||||
|
"http",
|
||||||
|
"httparse",
|
||||||
|
"log",
|
||||||
|
"rand 0.9.5",
|
||||||
|
"sha1",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"utf-8",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tungstenite"
|
||||||
|
version = "0.29.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"data-encoding",
|
||||||
|
"http",
|
||||||
|
"httparse",
|
||||||
|
"log",
|
||||||
|
"rand 0.9.5",
|
||||||
|
"sha1",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typeid"
|
name = "typeid"
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
@@ -5958,6 +6053,7 @@ dependencies = [
|
|||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"device_query",
|
"device_query",
|
||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
|
"futures-util",
|
||||||
"getrandom 0.4.3",
|
"getrandom 0.4.3",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"objc2",
|
"objc2",
|
||||||
@@ -5973,8 +6069,17 @@ dependencies = [
|
|||||||
"tauri-plugin-opener",
|
"tauri-plugin-opener",
|
||||||
"tauri-specta",
|
"tauri-specta",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-tungstenite 0.28.0",
|
||||||
"uuid",
|
"uuid",
|
||||||
"windows 0.58.0",
|
"windows 0.58.0",
|
||||||
|
"wyd-common",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wyd-common"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5982,7 +6087,13 @@ name = "wyd-server"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
|
"base64 0.22.1",
|
||||||
|
"ed25519-dalek",
|
||||||
|
"futures-util",
|
||||||
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"uuid",
|
||||||
|
"wyd-common",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = ["src-tauri", "src-server"]
|
members = ["src-common", "src-tauri", "src-server"]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[package]
|
||||||
|
name = "wyd-common"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub const VERSION: u8 = 1;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Profile {
|
||||||
|
pub id: String,
|
||||||
|
pub display_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "type", rename_all = "camelCase")]
|
||||||
|
pub enum ClientMessage {
|
||||||
|
Register { profile: Profile, signature: String },
|
||||||
|
Signed { payload: String, signature: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "type", rename_all = "camelCase")]
|
||||||
|
pub enum ServerMessage {
|
||||||
|
Challenge { version: u8, challenge: String },
|
||||||
|
Registered,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register_bytes(challenge: &str, profile: &Profile) -> Vec<u8> {
|
||||||
|
format!(
|
||||||
|
"wyd-register-v{VERSION}\n{challenge}\n{}\n{}",
|
||||||
|
profile.id, profile.display_name
|
||||||
|
)
|
||||||
|
.into_bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn message_bytes(payload: &str) -> Vec<u8> {
|
||||||
|
format!("wyd-message-v{VERSION}\n{payload}").into_bytes()
|
||||||
|
}
|
||||||
@@ -5,5 +5,11 @@ edition = "2024"
|
|||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = "0.8"
|
axum = { version = "0.8", features = ["ws"] }
|
||||||
tokio = { version = "1.53.1", features = ["macros", "net", "rt-multi-thread"] }
|
tokio = { version = "1.53.1", features = ["macros", "net", "rt-multi-thread", "sync", "time"] }
|
||||||
|
base64 = "0.22.1"
|
||||||
|
ed25519-dalek = "3.0.0"
|
||||||
|
futures-util = "0.3"
|
||||||
|
serde_json = "1"
|
||||||
|
uuid = { version = "1", features = ["v4"] }
|
||||||
|
wyd-common = { path = "../src-common" }
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use axum::{Router, http::StatusCode, routing::get};
|
use axum::{Router, http::StatusCode, routing::get};
|
||||||
|
|
||||||
|
mod network;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
let bind_addr = std::env::var("BIND_ADDR").unwrap_or_else(|_| "127.0.0.1:3000".to_string());
|
let bind_addr = std::env::var("BIND_ADDR").unwrap_or_else(|_| "127.0.0.1:3000".to_string());
|
||||||
@@ -7,6 +9,7 @@ async fn main() -> std::io::Result<()> {
|
|||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/", get("ok"))
|
.route("/", get("ok"))
|
||||||
.route("/health", get("ok"))
|
.route("/health", get("ok"))
|
||||||
|
.merge(network::routes())
|
||||||
.fallback((StatusCode::NOT_FOUND, "not found"));
|
.fallback((StatusCode::NOT_FOUND, "not found"));
|
||||||
|
|
||||||
println!("wyd-server listening on http://{bind_addr}");
|
println!("wyd-server listening on http://{bind_addr}");
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use axum::Router;
|
||||||
|
use axum::extract::State;
|
||||||
|
use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade};
|
||||||
|
use axum::response::Response;
|
||||||
|
use axum::routing::get;
|
||||||
|
use base64::Engine;
|
||||||
|
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
||||||
|
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
|
||||||
|
use futures_util::{SinkExt, StreamExt};
|
||||||
|
use tokio::sync::{Mutex, mpsc};
|
||||||
|
use uuid::Uuid;
|
||||||
|
use wyd_common::{ClientMessage, Profile, ServerMessage, message_bytes, register_bytes};
|
||||||
|
|
||||||
|
type Clients = Arc<Mutex<HashMap<String, Client>>>;
|
||||||
|
|
||||||
|
struct Client {
|
||||||
|
connection_id: Uuid,
|
||||||
|
key: VerifyingKey,
|
||||||
|
#[allow(dead_code)] // Used when presence and profile lookup are exposed.
|
||||||
|
profile: Profile,
|
||||||
|
#[allow(dead_code)] // Used when server-side message routing is added.
|
||||||
|
sender: mpsc::Sender<Message>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn routes() -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/v1/ws", get(upgrade))
|
||||||
|
.with_state(Clients::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn upgrade(ws: WebSocketUpgrade, State(clients): State<Clients>) -> Response {
|
||||||
|
ws.on_upgrade(move |socket| connected(socket, clients))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn connected(mut socket: WebSocket, clients: Clients) {
|
||||||
|
let challenge = Uuid::new_v4().to_string();
|
||||||
|
if send(
|
||||||
|
&mut socket,
|
||||||
|
&ServerMessage::Challenge {
|
||||||
|
version: wyd_common::VERSION,
|
||||||
|
challenge: challenge.clone(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(Ok(Message::Text(text))) = socket.recv().await else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(ClientMessage::Register { profile, signature }) = serde_json::from_str(&text) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(key) = key(&profile.id) else { return };
|
||||||
|
if !verify(&key, ®ister_bytes(&challenge, &profile), &signature) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let connection_id = Uuid::new_v4();
|
||||||
|
let public_key = profile.id.clone();
|
||||||
|
let (sender, mut outgoing) = mpsc::channel(32);
|
||||||
|
clients.lock().await.insert(
|
||||||
|
public_key.clone(),
|
||||||
|
Client {
|
||||||
|
connection_id,
|
||||||
|
key,
|
||||||
|
profile,
|
||||||
|
sender,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if send(&mut socket, &ServerMessage::Registered).await.is_err() {
|
||||||
|
remove(&clients, &public_key, connection_id).await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (mut writer, mut reader) = socket.split();
|
||||||
|
let mut ping = tokio::time::interval(Duration::from_secs(20));
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
message = outgoing.recv() => {
|
||||||
|
let Some(message) = message else { break };
|
||||||
|
if writer.send(message).await.is_err() { break; }
|
||||||
|
}
|
||||||
|
_ = ping.tick() => {
|
||||||
|
if writer.send(Message::Ping(Vec::new().into())).await.is_err() { break; }
|
||||||
|
}
|
||||||
|
message = reader.next() => match message {
|
||||||
|
Some(Ok(Message::Text(text))) => {
|
||||||
|
let Ok(ClientMessage::Signed { payload, signature }) = serde_json::from_str(&text) else { break };
|
||||||
|
let registered_key = clients.lock().await.get(&public_key)
|
||||||
|
.filter(|client| client.connection_id == connection_id)
|
||||||
|
.map(|client| client.key);
|
||||||
|
let Some(registered_key) = registered_key else { break };
|
||||||
|
if !verify(®istered_key, &message_bytes(&payload), &signature) { break; }
|
||||||
|
// The message is authenticated. Domain routing comes next.
|
||||||
|
}
|
||||||
|
Some(Ok(Message::Ping(data))) => {
|
||||||
|
if writer.send(Message::Pong(data)).await.is_err() { break; }
|
||||||
|
}
|
||||||
|
Some(Ok(Message::Close(_))) | None | Some(Err(_)) => break,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(&clients, &public_key, connection_id).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove(clients: &Clients, public_key: &str, connection_id: Uuid) {
|
||||||
|
let mut clients = clients.lock().await;
|
||||||
|
if clients
|
||||||
|
.get(public_key)
|
||||||
|
.is_some_and(|client| client.connection_id == connection_id)
|
||||||
|
{
|
||||||
|
clients.remove(public_key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send(socket: &mut WebSocket, message: &ServerMessage) -> Result<(), axum::Error> {
|
||||||
|
socket
|
||||||
|
.send(Message::Text(
|
||||||
|
serde_json::to_string(message).unwrap().into(),
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn key(public_key: &str) -> Result<VerifyingKey, ()> {
|
||||||
|
let bytes = URL_SAFE_NO_PAD.decode(public_key).map_err(|_| ())?;
|
||||||
|
let bytes: [u8; 32] = bytes.try_into().map_err(|_| ())?;
|
||||||
|
VerifyingKey::from_bytes(&bytes).map_err(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify(key: &VerifyingKey, bytes: &[u8], signature: &str) -> bool {
|
||||||
|
let Ok(signature) = URL_SAFE_NO_PAD.decode(signature) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Ok(signature) = Signature::from_slice(&signature) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
key.verify(bytes, &signature).is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use ed25519_dalek::{Signer, SigningKey};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn verifies_registration_and_message_signatures() {
|
||||||
|
let signing_key = SigningKey::from_bytes(&[7; 32]);
|
||||||
|
let profile = Profile {
|
||||||
|
id: URL_SAFE_NO_PAD.encode(signing_key.verifying_key().to_bytes()),
|
||||||
|
display_name: "Wind".to_owned(),
|
||||||
|
};
|
||||||
|
let registration = register_bytes("challenge", &profile);
|
||||||
|
let signature = URL_SAFE_NO_PAD.encode(signing_key.sign(®istration).to_bytes());
|
||||||
|
|
||||||
|
assert!(verify(
|
||||||
|
&signing_key.verifying_key(),
|
||||||
|
®istration,
|
||||||
|
&signature
|
||||||
|
));
|
||||||
|
assert!(!verify(
|
||||||
|
&signing_key.verifying_key(),
|
||||||
|
®ister_bytes("another challenge", &profile),
|
||||||
|
&signature,
|
||||||
|
));
|
||||||
|
|
||||||
|
let message = message_bytes("hello");
|
||||||
|
let signature = URL_SAFE_NO_PAD.encode(signing_key.sign(&message).to_bytes());
|
||||||
|
assert!(verify(&signing_key.verifying_key(), &message, &signature));
|
||||||
|
assert!(!verify(
|
||||||
|
&signing_key.verifying_key(),
|
||||||
|
&message_bytes("tampered"),
|
||||||
|
&signature,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,12 +27,15 @@ lazy_static = "1.5.0"
|
|||||||
ed25519-dalek = "3.0.0"
|
ed25519-dalek = "3.0.0"
|
||||||
getrandom = "0.4.3"
|
getrandom = "0.4.3"
|
||||||
device_query = "4.0.1"
|
device_query = "4.0.1"
|
||||||
tokio = "1.53.1"
|
tokio = { version = "1.53.1", features = ["macros", "sync", "time"] }
|
||||||
|
tokio-tungstenite = "0.28"
|
||||||
|
futures-util = "0.3"
|
||||||
sqlx = { version = "0.8.6", features = ["runtime-tokio-rustls", "sqlite", "migrate"] }
|
sqlx = { version = "0.8.6", features = ["runtime-tokio-rustls", "sqlite", "migrate"] }
|
||||||
tauri-specta = { version = "2.0.0-rc.21", features = ["derive", "typescript"] }
|
tauri-specta = { version = "2.0.0-rc.21", features = ["derive", "typescript"] }
|
||||||
specta = "2.0.0-rc.21"
|
specta = "2.0.0-rc.21"
|
||||||
specta-typescript = { version = "0.0.9" }
|
specta-typescript = { version = "0.0.9" }
|
||||||
uuid = { version = "1", features = ["v4"] }
|
uuid = { version = "1", features = ["v4"] }
|
||||||
|
wyd-common = { path = "../src-common" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { version = "1.53.1", features = ["macros", "rt-multi-thread"] }
|
tokio = { version = "1.53.1", features = ["macros", "rt-multi-thread"] }
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ impl StoredKeypair {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)] // The signing key will be read by the pending WebSocket sender.
|
#[derive(Clone)]
|
||||||
pub struct AppKeypair {
|
pub struct AppKeypair {
|
||||||
signing_key: SigningKey,
|
signing_key: SigningKey,
|
||||||
public_key: String,
|
public_key: String,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ mod cursor;
|
|||||||
mod db;
|
mod db;
|
||||||
mod friends;
|
mod friends;
|
||||||
mod keypair;
|
mod keypair;
|
||||||
|
mod network;
|
||||||
mod profile;
|
mod profile;
|
||||||
mod remotes;
|
mod remotes;
|
||||||
mod ufa;
|
mod ufa;
|
||||||
@@ -12,6 +13,7 @@ async fn launch_app(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>>
|
|||||||
let handle = app.handle();
|
let handle = app.handle();
|
||||||
db::init(handle).await?;
|
db::init(handle).await?;
|
||||||
keypair::init(handle).await?;
|
keypair::init(handle).await?;
|
||||||
|
network::init(handle).await?;
|
||||||
ufa::init();
|
ufa::init();
|
||||||
cursor::init(handle);
|
cursor::init(handle);
|
||||||
windowing::init(handle);
|
windowing::init(handle);
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use futures_util::{SinkExt, StreamExt};
|
||||||
|
use tauri::{AppHandle, Manager};
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
use tokio_tungstenite::tungstenite::Message;
|
||||||
|
use wyd_common::{ClientMessage, Profile, ServerMessage, message_bytes, register_bytes};
|
||||||
|
|
||||||
|
use crate::db::AppDatabase;
|
||||||
|
use crate::keypair::AppKeypair;
|
||||||
|
use crate::remotes::{self, Remote};
|
||||||
|
|
||||||
|
pub struct Network {
|
||||||
|
senders: Mutex<HashMap<String, mpsc::Sender<String>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Network {
|
||||||
|
#[allow(dead_code)] // Ready for the first domain message sender.
|
||||||
|
pub fn send(&self, remote_id: &str, payload: String) -> Result<(), String> {
|
||||||
|
let senders = self.senders.lock().map_err(|error| error.to_string())?;
|
||||||
|
let sender = senders
|
||||||
|
.get(remote_id)
|
||||||
|
.ok_or_else(|| "remote is not configured".to_string())?;
|
||||||
|
sender
|
||||||
|
.try_send(payload)
|
||||||
|
.map_err(|error| format!("remote is not ready: {error}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn init(handle: &AppHandle) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let database = handle.state::<AppDatabase>();
|
||||||
|
let keypair = handle.state::<AppKeypair>().inner().clone();
|
||||||
|
let profile = crate::profile::get(&database, keypair.public_key()).await?;
|
||||||
|
let remotes = remotes::all(&database).await?;
|
||||||
|
let mut senders = HashMap::new();
|
||||||
|
|
||||||
|
for remote in remotes {
|
||||||
|
let (sender, receiver) = mpsc::channel(32);
|
||||||
|
senders.insert(remote.id.clone(), sender);
|
||||||
|
tauri::async_runtime::spawn(run(remote, profile.clone(), keypair.clone(), receiver));
|
||||||
|
}
|
||||||
|
|
||||||
|
handle.manage(Network {
|
||||||
|
senders: Mutex::new(senders),
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run(
|
||||||
|
remote: Remote,
|
||||||
|
profile: crate::user::User,
|
||||||
|
keypair: AppKeypair,
|
||||||
|
mut outgoing: mpsc::Receiver<String>,
|
||||||
|
) {
|
||||||
|
loop {
|
||||||
|
if let Err(error) = connect(&remote, &profile, &keypair, &mut outgoing).await {
|
||||||
|
eprintln!("remote {} disconnected: {error}", remote.id);
|
||||||
|
}
|
||||||
|
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn connect(
|
||||||
|
remote: &Remote,
|
||||||
|
profile: &crate::user::User,
|
||||||
|
keypair: &AppKeypair,
|
||||||
|
outgoing: &mut mpsc::Receiver<String>,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
let (socket, _) = tokio_tungstenite::connect_async(url(remote)).await?;
|
||||||
|
let (mut writer, mut reader) = socket.split();
|
||||||
|
|
||||||
|
let challenge = match recv(&mut reader).await? {
|
||||||
|
ServerMessage::Challenge { version, challenge } if version == wyd_common::VERSION => {
|
||||||
|
challenge
|
||||||
|
}
|
||||||
|
_ => return Err("server did not send a compatible challenge".into()),
|
||||||
|
};
|
||||||
|
let profile = Profile {
|
||||||
|
id: profile.id.clone(),
|
||||||
|
display_name: profile.display_name.clone(),
|
||||||
|
};
|
||||||
|
send(
|
||||||
|
&mut writer,
|
||||||
|
&ClientMessage::Register {
|
||||||
|
signature: keypair.sign(®ister_bytes(&challenge, &profile)),
|
||||||
|
profile,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !matches!(recv(&mut reader).await?, ServerMessage::Registered) {
|
||||||
|
return Err("server rejected registration".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
payload = outgoing.recv() => {
|
||||||
|
let payload = payload.ok_or("network sender closed")?;
|
||||||
|
send(&mut writer, &ClientMessage::Signed {
|
||||||
|
signature: keypair.sign(&message_bytes(&payload)),
|
||||||
|
payload,
|
||||||
|
}).await?;
|
||||||
|
}
|
||||||
|
message = reader.next() => match message.ok_or("server closed the socket")?? {
|
||||||
|
Message::Ping(data) => writer.send(Message::Pong(data)).await?,
|
||||||
|
Message::Close(_) => return Ok(()),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send<S>(
|
||||||
|
writer: &mut S,
|
||||||
|
message: &ClientMessage,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>
|
||||||
|
where
|
||||||
|
S: futures_util::Sink<Message> + Unpin,
|
||||||
|
S::Error: std::error::Error + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
writer
|
||||||
|
.send(Message::Text(serde_json::to_string(message)?.into()))
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn recv<S>(reader: &mut S) -> Result<ServerMessage, Box<dyn std::error::Error + Send + Sync>>
|
||||||
|
where
|
||||||
|
S: futures_util::Stream<Item = Result<Message, tokio_tungstenite::tungstenite::Error>> + Unpin,
|
||||||
|
{
|
||||||
|
while let Some(message) = reader.next().await {
|
||||||
|
if let Message::Text(text) = message? {
|
||||||
|
return Ok(serde_json::from_str(&text)?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err("server closed the socket".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn url(remote: &Remote) -> String {
|
||||||
|
let address = remote.address.trim_end_matches('/');
|
||||||
|
let address = address
|
||||||
|
.strip_prefix("http://")
|
||||||
|
.or_else(|| address.strip_prefix("https://"))
|
||||||
|
.or_else(|| address.strip_prefix("ws://"))
|
||||||
|
.or_else(|| address.strip_prefix("wss://"))
|
||||||
|
.unwrap_or(address);
|
||||||
|
let scheme = if remote.address.starts_with("https://") || remote.address.starts_with("wss://") {
|
||||||
|
"wss"
|
||||||
|
} else {
|
||||||
|
"ws"
|
||||||
|
};
|
||||||
|
let port = remote
|
||||||
|
.port
|
||||||
|
.map(|port| format!(":{port}"))
|
||||||
|
.unwrap_or_default();
|
||||||
|
format!("{scheme}://{address}{port}/v1/ws")
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ pub struct ProfileChanged {
|
|||||||
pub profile: User,
|
pub profile: User,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get(database: &AppDatabase, public_key: &str) -> Result<User, sqlx::Error> {
|
pub async fn get(database: &AppDatabase, public_key: &str) -> Result<User, sqlx::Error> {
|
||||||
let display_name =
|
let display_name =
|
||||||
sqlx::query_scalar::<_, String>("SELECT display_name FROM profile WHERE id = ?1")
|
sqlx::query_scalar::<_, String>("SELECT display_name FROM profile WHERE id = ?1")
|
||||||
.bind(PROFILE_ID)
|
.bind(PROFILE_ID)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ pub struct RemotesChanged {
|
|||||||
pub remotes: Vec<Remote>,
|
pub remotes: Vec<Remote>,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn all(database: &AppDatabase) -> Result<Vec<Remote>, sqlx::Error> {
|
pub async fn all(database: &AppDatabase) -> Result<Vec<Remote>, sqlx::Error> {
|
||||||
sqlx::query_as::<_, Remote>(
|
sqlx::query_as::<_, Remote>(
|
||||||
"SELECT id, address, name, port FROM remotes \
|
"SELECT id, address, name, port FROM remotes \
|
||||||
ORDER BY COALESCE(name, address) COLLATE NOCASE, id",
|
ORDER BY COALESCE(name, address) COLLATE NOCASE, id",
|
||||||
|
|||||||
Reference in New Issue
Block a user