Compare commits

...

2 Commits

  1. 2766
      Cargo.lock
  2. 5
      Cargo.toml
  3. 4
      client/Cargo.toml
  4. 142
      client/src/main.rs
  5. 15
      common/src/lib.rs
  6. 4
      server/Cargo.toml
  7. 90
      server/src/main.rs

2766
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -8,4 +8,7 @@ members = [
[workspace.dependencies]
# TODO: Disable default features and enable what's needed in specific crates.
bevy = "0.14.*"
bevy = "0.15.*"
aeronet = "0.11.*"
aeronet_webtransport = "0.11.*"

@ -5,4 +5,8 @@ edition = "2021"
[dependencies]
common = { package = "gaemstone-common", path = "../common" }
bevy = { workspace = true }
aeronet = { workspace = true }
aeronet_webtransport = { workspace = true, features = ["client"] }

@ -1,29 +1,133 @@
use bevy::{prelude::*, window::WindowResolution};
use {
aeronet::{
io::{
connection::{DisconnectReason, Disconnected},
Session, SessionEndpoint,
},
transport::TransportConfig,
},
aeronet_webtransport::{
cert,
cert::CertificateHash,
client::{ClientConfig, WebTransportClient, WebTransportClientPlugin},
},
bevy::{prelude::*, window::WindowResolution},
};
fn main() -> AppExit {
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "gæmstone Client".into(),
// Steam Deck: DPI appears pretty high, causing everything to be scaled up.
// Setting scale factor override prevents this from happening.
resolution: WindowResolution::new(1280., 720.).with_scale_factor_override(1.0),
// WASM: Fit canvas to parent element, so `Window` resizes automatically.
fit_canvas_to_parent: true,
// WASM: Don't override default event handling like browser hotkeys while focused.
prevent_default_event_handling: false,
.add_plugins((
DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "gæmstone Client".into(),
// Steam Deck: DPI appears pretty high, causing everything to be scaled up.
// Setting scale factor override prevents this from happening.
resolution: WindowResolution::new(1280., 720.).with_scale_factor_override(1.0),
// WASM: Fit canvas to parent element, so `Window` resizes automatically.
fit_canvas_to_parent: true,
// WASM: Don't override default event handling like browser hotkeys while focused.
prevent_default_event_handling: false,
..default()
}),
..default()
}),
..default()
}))
.add_systems(Startup, setup)
WebTransportClientPlugin,
))
.add_systems(Startup, (setup, connect_to_server))
.add_observer(on_connecting)
.add_observer(on_connected)
.add_observer(on_disconnected)
.run()
}
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn(Camera2dBundle::default());
commands.spawn(SpriteBundle {
texture: asset_server.load("heck.png"),
..default()
});
commands.spawn(Camera2d::default());
commands.spawn(Sprite::from_image(asset_server.load("heck.png")));
}
fn connect_to_server(mut commands: Commands) {
// When using a self-signed cert, its hash needs to be provided to the
// client for it to accept it. Works on native and WASM. On WASM we have
// the option to just accept the HTTPS server's certificate. On native we
// could theoretically disable cert checking entirely, but that kind of
// defeats the point.
// TODO: Do not hardcode the server's certificate hash. Enter in a UI of some sort?
// For now, just manually replace the `todo!` with the hash that gets
// logged in the server output. It looks something like this:
// ************************
// SPKI FINGERPRINT
// 1YLqE3c3ZsRBos35nUrMETfZtCUVIxyIjcskEq0LFYE=
// CERTIFICATE HASH
// z3cWU+Pc209kffV440ksqcWxMcCTi9QO6qI7VjVOQfU=
// ************************
let target = format!("https://[::1]:{}", common::WEB_TRANSPORT_PORT);
let cert_hash = todo!();
let cert_hash = cert::hash_from_b64(cert_hash).unwrap();
let config = web_transport_config(Some(cert_hash));
commands
.spawn(Name::new(target.clone()))
.queue(WebTransportClient::connect(config, target));
}
#[cfg(target_family = "wasm")]
fn web_transport_config(cert_hash: Option<CertificateHash>) -> ClientConfig {
use aeronet_webtransport::xwt_web_sys::{CertificateHash, HashAlgorithm};
let server_certificate_hashes = cert_hash
.map(|hash| CertificateHash {
algorithm: HashAlgorithm::Sha256,
value: Vec::from(hash),
})
.into_iter()
.collect::<Vec<_>>();
ClientConfig {
server_certificate_hashes,
..Default::default()
}
}
#[cfg(not(target_family = "wasm"))]
fn web_transport_config(cert_hash: Option<CertificateHash>) -> ClientConfig {
use {aeronet_webtransport::wtransport::tls::Sha256Digest, core::time::Duration};
let server_certificate_hashes = cert_hash
.map(Sha256Digest::new)
.into_iter()
.collect::<Vec<_>>();
ClientConfig::builder()
.with_bind_default()
.with_server_certificate_hashes(server_certificate_hashes)
.keep_alive_interval(Some(Duration::from_secs(1)))
.max_idle_timeout(Some(Duration::from_secs(5)))
.unwrap()
.build()
}
fn on_connecting(trigger: Trigger<OnAdd, SessionEndpoint>, names: Query<&Name>) {
let session = trigger.entity();
let name = names.get(session).unwrap();
info!("Connecting to '{name}' ...");
}
fn on_connected(trigger: Trigger<OnAdd, Session>, mut commands: Commands) {
let session = trigger.entity();
info!("Connected!");
commands.entity(session).insert((TransportConfig {
max_memory_usage: 64 * 1024,
send_bytes_per_sec: 4 * 1024,
},));
}
fn on_disconnected(trigger: Trigger<Disconnected>) {
match &trigger.event().reason {
DisconnectReason::User(reason) => {
info!("Disconnected (user): {reason}")
}
DisconnectReason::Peer(reason) => {
info!("Disconnected (server): {reason}")
}
DisconnectReason::Error(err) => {
info!("Disconnected (error): {err:#}")
}
};
}

@ -1,14 +1 @@
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
pub const WEB_TRANSPORT_PORT: u16 = 25565;

@ -5,4 +5,8 @@ edition = "2021"
[dependencies]
common = { package = "gaemstone-common", path = "../common" }
bevy = { workspace = true }
aeronet = { workspace = true }
aeronet_webtransport = { workspace = true, features = ["server"] }

@ -1,12 +1,96 @@
use bevy::{log::LogPlugin, prelude::*};
use {
aeronet::io::{
connection::{DisconnectReason, Disconnected, LocalAddr},
server::Server,
Session,
},
aeronet_webtransport::{
cert,
server::{SessionRequest, SessionResponse, WebTransportServer, WebTransportServerPlugin},
wtransport::{Identity, ServerConfig},
},
bevy::{log::LogPlugin, prelude::*},
std::time::Duration,
};
fn main() -> AppExit {
App::new()
.add_plugins((LogPlugin::default(), MinimalPlugins))
.add_systems(Startup, setup)
.add_plugins((
LogPlugin::default(),
MinimalPlugins,
WebTransportServerPlugin,
))
.add_systems(Startup, (setup, open_web_transport_server))
.add_observer(on_server_opened)
.add_observer(on_client_session_request)
.add_observer(on_client_connected)
.add_observer(on_client_disconnected)
.run()
}
fn setup() {
info!("Hello from gæmstone server!");
}
fn open_web_transport_server(mut commands: Commands) {
let identity = Identity::self_signed(["localhost", "127.0.0.1", "::1"]).unwrap();
let cert = &identity.certificate_chain().as_slice()[0];
let spki_fingerprint = cert::spki_fingerprint_b64(cert).unwrap();
let cert_hash = cert::hash_to_b64(cert.hash());
info!("************************");
info!("SPKI FINGERPRINT");
info!(" {spki_fingerprint}");
info!("CERTIFICATE HASH");
info!(" {cert_hash}");
info!("************************");
let config = ServerConfig::builder()
.with_bind_default(common::WEB_TRANSPORT_PORT)
.with_identity(&identity)
.keep_alive_interval(Some(Duration::from_secs(1)))
.max_idle_timeout(Some(Duration::from_secs(5)))
.unwrap()
.build();
commands
.spawn_empty()
.queue(WebTransportServer::open(config));
info!("Starting WebTransport server ...");
}
fn on_server_opened(trigger: Trigger<OnAdd, Server>, servers: Query<&LocalAddr>) {
let server = trigger.entity();
let local_addr = servers.get(server).unwrap();
info!("Server listening on {}", **local_addr);
}
fn on_client_session_request(mut trigger: Trigger<SessionRequest>) {
let client = trigger.entity();
let request = trigger.event_mut();
request.respond(SessionResponse::Accepted);
info!("{client} connecting with headers:");
for (header_key, header_value) in &request.headers {
info!(" {header_key}: {header_value}");
}
}
fn on_client_connected(trigger: Trigger<OnAdd, Session>) {
let client = trigger.entity();
info!("{client} connected");
}
fn on_client_disconnected(trigger: Trigger<Disconnected>) {
let client = trigger.entity();
match &trigger.event().reason {
DisconnectReason::User(reason) => {
info!("{client} disconnected (server): {reason}");
}
DisconnectReason::Peer(reason) => {
info!("{client} disconnected (client): {reason}");
}
DisconnectReason::Error(err) => {
warn!("{client} disconnected (error): {err:#}");
}
}
}

Loading…
Cancel
Save