Compare commits
No commits in common. 'wip/egui-replicon' and 'main' have entirely different histories.
wip/egui-r
...
main
10 changed files with 79 additions and 997 deletions
@ -1,144 +0,0 @@ |
||||
use { |
||||
crate::server_address::ServerAddress, |
||||
aeronet_replicon::client::AeronetRepliconClient, |
||||
aeronet_webtransport::{ |
||||
cert::{self, CertificateHash}, |
||||
client::{ClientConfig, WebTransportClient}, |
||||
}, |
||||
bevy::prelude::*, |
||||
bevy_egui::{egui, EguiContexts}, |
||||
bevy_replicon::prelude::*, |
||||
}; |
||||
|
||||
pub struct ConnectionUiPlugin; |
||||
|
||||
impl Plugin for ConnectionUiPlugin { |
||||
fn build(&self, app: &mut App) { |
||||
app.init_resource::<ConnectionUiState>().add_systems( |
||||
Update, |
||||
display_connection_ui.run_if(not(client_connected.or(client_connecting))), |
||||
); |
||||
} |
||||
} |
||||
|
||||
#[derive(Default, Resource)] |
||||
struct ConnectionUiState { |
||||
reveal: bool, |
||||
address: String, |
||||
cert_hash: String, |
||||
} |
||||
|
||||
fn display_connection_ui( |
||||
commands: Commands, |
||||
mut contexts: EguiContexts, |
||||
mut ui_state: ResMut<ConnectionUiState>, |
||||
) { |
||||
let default_address_str = format!("[::1]:{}", common::WEB_TRANSPORT_PORT); |
||||
let default_address = default_address_str.parse().unwrap(); |
||||
|
||||
egui::Window::new("Connect to Server") |
||||
.anchor(egui::Align2::CENTER_CENTER, [0., 0.]) |
||||
.collapsible(false) |
||||
.resizable(false) |
||||
.show(contexts.ctx_mut(), |ui| { |
||||
let password = !ui_state.reveal; |
||||
egui::Grid::new("connection_info_grid") |
||||
.num_columns(2) |
||||
.show(ui, |ui| { |
||||
ui.label("Server Address:"); |
||||
egui::TextEdit::singleline(&mut ui_state.address) |
||||
.hint_text(&default_address_str) |
||||
.password(password) |
||||
.show(ui); |
||||
ui.end_row(); |
||||
|
||||
ui.label("Certificate Hash:"); |
||||
egui::TextEdit::singleline(&mut ui_state.cert_hash) |
||||
.password(password) |
||||
.show(ui); |
||||
ui.end_row(); |
||||
}); |
||||
|
||||
ui.horizontal(|ui| { |
||||
ui.checkbox(&mut ui_state.reveal, "Reveal Address / Hash"); |
||||
|
||||
let address = if ui_state.address.is_empty() { |
||||
Some(default_address) |
||||
} else if let Ok(address) = ui_state.address.parse::<ServerAddress>() { |
||||
Some(address.with_default_port(common::WEB_TRANSPORT_PORT)) |
||||
} else { |
||||
None |
||||
}; |
||||
|
||||
let (cert_hash, cert_valid) = if ui_state.cert_hash.is_empty() { |
||||
(None, true) |
||||
} else if let Ok(cert_hash) = cert::hash_from_b64(&ui_state.cert_hash) { |
||||
(Some(cert_hash), true) |
||||
} else { |
||||
(None, false) |
||||
}; |
||||
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { |
||||
ui.horizontal(|ui| { |
||||
ui.add_enabled_ui(address.is_some() && cert_valid, |ui| { |
||||
if ui.button(" Connect ").clicked() { |
||||
connect_to_server(commands, &address.unwrap(), cert_hash); |
||||
} |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
fn connect_to_server( |
||||
mut commands: Commands, |
||||
address: &ServerAddress, |
||||
cert_hash: Option<CertificateHash>, |
||||
) { |
||||
// When using a self-signed cert from the server, 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: Add support for SRV records to `ServerAddress`.
|
||||
// TODO: (WASM) Auto-connect to address of the server hosting the webpage.
|
||||
// FIXME: (WASM) `serverCertificateHashes` not accepted in Firefox. https://github.com/BiagioFesta/wtransport/issues/241
|
||||
let config = web_transport_config(cert_hash); |
||||
let target = format!("https://{address}"); |
||||
commands |
||||
.spawn((Name::new(address.to_string()), AeronetRepliconClient)) |
||||
.queue(WebTransportClient::connect(config, target)); |
||||
} |
||||
|
||||
#[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() |
||||
} |
||||
|
||||
#[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() |
||||
} |
||||
} |
@ -1,236 +0,0 @@ |
||||
use std::net::{Ipv4Addr, Ipv6Addr}; |
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)] |
||||
pub struct ServerAddress { |
||||
host: ServerHost, |
||||
port: Option<u16>, |
||||
} |
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)] |
||||
pub enum ServerHost { |
||||
Hostname(String), |
||||
Ipv4Addr(Ipv4Addr), |
||||
Ipv6Addr(Ipv6Addr), |
||||
} |
||||
|
||||
impl ServerAddress { |
||||
pub fn host(&self) -> &ServerHost { |
||||
&self.host |
||||
} |
||||
|
||||
pub fn port(&self) -> Option<u16> { |
||||
self.port |
||||
} |
||||
|
||||
/// Returns a new `ServerAddress` with the specified port if
|
||||
/// this instance did not already have an explicit port set.
|
||||
pub fn with_default_port(self, default_port: u16) -> Self { |
||||
Self { |
||||
port: self.port.or(Some(default_port)), |
||||
..self |
||||
} |
||||
} |
||||
} |
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)] |
||||
pub enum ParseError { |
||||
/// The given string (or its host or port) was empty.
|
||||
Empty, |
||||
/// The given string was not a valid hostname.
|
||||
Hostname, |
||||
/// The given string looked like an IPv4 address (`#.#.#.#`) but turned out to be invalid.
|
||||
Ipv4, |
||||
/// The given string started with a '[' but did not contain a valid IPv6 address.
|
||||
Ipv6, |
||||
/// The given port was invalid or out of range.
|
||||
Port, |
||||
} |
||||
|
||||
impl std::str::FromStr for ServerAddress { |
||||
type Err = ParseError; |
||||
fn from_str(s: &str) -> Result<Self, Self::Err> { |
||||
let colon = match (s.rfind(':'), s.rfind(']')) { |
||||
// This doesn't match colons within IPv6 brackets (`[::1]:80`).
|
||||
(Some(colon), Some(bracket)) if colon > bracket => Some(colon), |
||||
(Some(colon), None) => Some(colon), |
||||
_ => None, |
||||
}; |
||||
|
||||
let host_slice = colon.map(|i| &s[..i]).unwrap_or(s); |
||||
let host = host_slice.parse()?; |
||||
|
||||
if colon == Some(s.len() - 1) { |
||||
return Err(ParseError::Empty); |
||||
} |
||||
|
||||
let port_slice = colon.map(|i| &s[(i + 1)..]); |
||||
let port_result = port_slice.map(str::parse).transpose(); |
||||
let port = port_result.map_err(|_| ParseError::Port)?; |
||||
|
||||
Ok(Self { host, port }) |
||||
} |
||||
} |
||||
|
||||
impl std::str::FromStr for ServerHost { |
||||
type Err = ParseError; |
||||
fn from_str(s: &str) -> Result<Self, Self::Err> { |
||||
if s.is_empty() { |
||||
Err(ParseError::Empty) |
||||
} else if s.chars().nth(0).unwrap() == '[' { |
||||
if s.chars().last().unwrap() == ']' { |
||||
let result = s[1..(s.len() - 1)].parse(); |
||||
result.map(Self::Ipv6Addr).map_err(|_| ParseError::Ipv6) |
||||
} else { |
||||
Err(ParseError::Ipv6) |
||||
} |
||||
} else if looks_like_ipv4(s) { |
||||
s.parse().map(Self::Ipv4Addr).map_err(|_| ParseError::Ipv4) |
||||
} else if hostname_validator::is_valid(s) { |
||||
Ok(Self::Hostname(s.to_string())) |
||||
} else { |
||||
Err(ParseError::Hostname) |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl std::fmt::Display for ServerAddress { |
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||
if let Some(port) = self.port { |
||||
write!(f, "{}:{}", self.host, port) |
||||
} else { |
||||
write!(f, "{}", self.host) |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl std::fmt::Display for ServerHost { |
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||
match self { |
||||
ServerHost::Hostname(host) => write!(f, "{}", host), |
||||
ServerHost::Ipv4Addr(ipv4) => write!(f, "{}", ipv4), |
||||
ServerHost::Ipv6Addr(ipv6) => write!(f, "[{}]", ipv6), |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// Returns whether the supplied string is in the dotted-decimal (`#.#.#.#`)
|
||||
/// form that IPv4 addresses are (has 4 digit-only sections seperated by dots).
|
||||
/// Does not verify the individual values are within valid ranges (0-255).
|
||||
fn looks_like_ipv4(s: &str) -> bool { |
||||
let mut num_dots = 0; |
||||
let mut previous_char = '.'; |
||||
for c in s.chars() { |
||||
match c { |
||||
// Dot at beginning or after another dot is invalid.
|
||||
'.' if previous_char == '.' => return false, |
||||
// More than 3 dots aren't valid, let's stop iterating.
|
||||
'.' if num_dots == 3 => return false, |
||||
// More dots!
|
||||
'.' => num_dots += 1, |
||||
// Digits are valid.
|
||||
'0'..'9' => {} |
||||
// Anything else isn't.
|
||||
_ => return false, |
||||
} |
||||
previous_char = c; |
||||
} |
||||
// Must contain 3 dots and not end with one.
|
||||
(num_dots == 3) && (previous_char != '.') |
||||
} |
||||
|
||||
#[cfg(test)] |
||||
mod tests { |
||||
use super::*; |
||||
|
||||
#[test] |
||||
fn valid_addresses() { |
||||
// No port.
|
||||
assert!(parse("localhost").is_ok()); |
||||
assert!(parse("0.1-example.net2").is_ok()); |
||||
assert!(parse("127.0.0.1").is_ok()); |
||||
assert!(parse("255.255.255.255").is_ok()); |
||||
assert!(parse("[::]").is_ok()); |
||||
assert!(parse("[2001:db8:85a3::8a2e:370:7334]").is_ok()); |
||||
assert!(parse("[2001:0db8:85a3:0000:0000:8a2e:0370:7334]").is_ok()); |
||||
|
||||
// Yes port.
|
||||
assert!(parse("localhost:0").is_ok()); |
||||
assert!(parse("0.1-example.net2:80").is_ok()); |
||||
assert!(parse("127.0.0.1:420").is_ok()); |
||||
assert!(parse("[::]:8080").is_ok()); |
||||
assert!(parse("[2001:db8:85a3::8a2e:370:7334]:25565").is_ok()); |
||||
assert!(parse("[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:65535").is_ok()); |
||||
|
||||
assert_host_and_port("localhost:80", "localhost", 80); |
||||
assert_host_and_port("0.0.0.0:69", "0.0.0.0", 69); |
||||
assert_host_and_port("[::1]:1337", "[::1]", 1337); |
||||
|
||||
// Sussy but technically valid hostnames??
|
||||
assert_is_hostname("12.34.56"); |
||||
assert_is_hostname("12.34.56.78.90:123"); |
||||
} |
||||
|
||||
#[test] |
||||
fn invalid_addresses() { |
||||
assert_eq!(parse(""), Err(ParseError::Empty)); |
||||
assert_eq!(parse(":"), Err(ParseError::Empty)); |
||||
assert_eq!(parse(":80"), Err(ParseError::Empty)); |
||||
assert_eq!(parse("abc:"), Err(ParseError::Empty)); |
||||
|
||||
assert_eq!(parse("[::]:a"), Err(ParseError::Port)); |
||||
assert_eq!(parse("[::]:20a"), Err(ParseError::Port)); |
||||
assert_eq!(parse("[::]:65536"), Err(ParseError::Port)); |
||||
assert_eq!(parse("example.net:42.0"), Err(ParseError::Port)); |
||||
|
||||
// Not necessary to verify many of these, as we don't parse them ourselves.
|
||||
assert_eq!(parse("["), Err(ParseError::Ipv6)); |
||||
assert_eq!(parse("[]"), Err(ParseError::Ipv6)); |
||||
assert_eq!(parse("[localhost]"), Err(ParseError::Ipv6)); |
||||
|
||||
// Bare IPv6 values are not supported, they must be wrapped in brackets.
|
||||
assert_eq!(parse("::"), Err(ParseError::Hostname)); |
||||
assert_eq!(parse("::1:1337"), Err(ParseError::Hostname)); |
||||
assert_eq!(parse("2001:4860:4860::8888"), Err(ParseError::Hostname)); |
||||
|
||||
// Anything that "looks like an IPv4 address" will attempt to parse as such.
|
||||
assert_eq!(parse("253.254.255.256"), Err(ParseError::Ipv4)); |
||||
assert_eq!(parse("1.22.333.4444"), Err(ParseError::Ipv4)); |
||||
assert_eq!(parse("01.02.03.04"), Err(ParseError::Ipv4)); |
||||
|
||||
assert_eq!(parse("..."), Err(ParseError::Hostname)); |
||||
assert_eq!(parse("a..b"), Err(ParseError::Hostname)); |
||||
assert_eq!(parse("example.net]"), Err(ParseError::Hostname)); |
||||
assert_eq!(parse("foo.-bar.example"), Err(ParseError::Hostname)); |
||||
assert_eq!(parse("admin@example.net"), Err(ParseError::Hostname)); |
||||
} |
||||
|
||||
#[test] |
||||
fn address_with_default_port() { |
||||
assert_default_port("localhost", 12345, 12345); |
||||
assert_default_port("localhost:80", 12345, 80); |
||||
assert_default_port("[::1]", 12345, 12345); |
||||
assert_default_port("[::1]:80", 12345, 80); |
||||
assert_default_port("1.2.3.4", 12345, 12345); |
||||
assert_default_port("1.2.3.4:80", 12345, 80); |
||||
} |
||||
|
||||
fn parse(s: &str) -> Result<ServerAddress, ParseError> { |
||||
s.parse::<ServerAddress>() |
||||
} |
||||
|
||||
fn assert_host_and_port(s: &str, host: &str, port: u16) { |
||||
let address = parse(s).unwrap(); |
||||
assert_eq!(address.host().to_string(), host); |
||||
assert_eq!(address.port(), Some(port)); |
||||
} |
||||
|
||||
fn assert_is_hostname(s: &str) { |
||||
let host = parse(s).unwrap().host().clone(); |
||||
assert!(matches!(host, ServerHost::Hostname(_))); |
||||
} |
||||
|
||||
fn assert_default_port(s: &str, default_port: u16, expected_port: u16) { |
||||
let address = parse(s).unwrap().with_default_port(default_port); |
||||
assert_eq!(address.port(), Some(expected_port)); |
||||
} |
||||
} |
Loading…
Reference in new issue