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