diff --git a/test/scripts/debug_main_node.sh b/scripts/debug_main_node.sh similarity index 100% rename from test/scripts/debug_main_node.sh rename to scripts/debug_main_node.sh diff --git a/test/scripts/debug_subnode_1.sh b/scripts/debug_subnode_1.sh similarity index 100% rename from test/scripts/debug_subnode_1.sh rename to scripts/debug_subnode_1.sh diff --git a/test/scripts/no-timeout.yml b/scripts/no-timeout.yml similarity index 100% rename from test/scripts/no-timeout.yml rename to scripts/no-timeout.yml diff --git a/test/scripts/run_20_no_timeout.sh b/scripts/run_20_no_timeout.sh similarity index 100% rename from test/scripts/run_20_no_timeout.sh rename to scripts/run_20_no_timeout.sh diff --git a/test/scripts/run_2_no_timeout.sh b/scripts/run_2_no_timeout.sh similarity index 100% rename from test/scripts/run_2_no_timeout.sh rename to scripts/run_2_no_timeout.sh diff --git a/test/scripts/run_4_no_timeout.sh b/scripts/run_4_no_timeout.sh similarity index 100% rename from test/scripts/run_4_no_timeout.sh rename to scripts/run_4_no_timeout.sh diff --git a/test/scripts/run_local_test.py b/scripts/run_local_test.py similarity index 97% rename from test/scripts/run_local_test.py rename to scripts/run_local_test.py index 3195414b..8557bbfd 100755 --- a/test/scripts/run_local_test.py +++ b/scripts/run_local_test.py @@ -15,10 +15,10 @@ if sys.version_info < (3, 0, 0): sys.exit(1) script_dir = os.path.dirname(os.path.realpath(__file__)) -veilid_server_exe_debug = os.path.join(script_dir, '..', 'veilid-server', +veilid_server_exe_debug = os.path.join(script_dir, '..', 'target', 'debug', 'veilid-server') veilid_server_exe_release = os.path.join( - script_dir, '..', 'veilid-server', 'target', 'release', 'veilid-server') + script_dir, '..', 'target', 'release', 'veilid-server') main_process = None subindex_processes = [] diff --git a/veilid-core/src/intf/native/network/mod.rs b/veilid-core/src/intf/native/network/mod.rs index 1ea71f29..f63f343f 100644 --- a/veilid-core/src/intf/native/network/mod.rs +++ b/veilid-core/src/intf/native/network/mod.rs @@ -860,11 +860,11 @@ impl Network { pub async fn start_ws_listeners(&self) -> Result<(), String> { let routing_table = self.routing_table(); - let (listen_address, public_address, path) = { + let (listen_address, url, path) = { let c = self.config.get(); ( c.network.protocol.ws.listen_address.clone(), - c.network.protocol.ws.public_address.clone(), + c.network.protocol.ws.url.clone(), c.network.protocol.ws.path.clone(), ) }; @@ -888,21 +888,20 @@ impl Network { ); // Add static public dialinfo if it's configured - if let Some(public_address) = public_address.as_ref() { - let (public_fqdn, public_port) = split_port(public_address).map_err(|_| { - "invalid WS public address, port not specified correctly".to_owned() - })?; - let public_port = public_port - .ok_or_else(|| "port must be specified for public WS address".to_owned())?; - + if let Some(url) = url.as_ref() { + let split_url = SplitUrl::from_str(url)?; + if split_url.scheme.to_ascii_lowercase() != "ws" { + return Err("WS URL must use 'ws://' scheme".to_owned()); + } routing_table.register_global_dial_info( - DialInfo::ws(fqdn, public_port, public_fqdn), - Some(NetworkClass::Server), - DialInfoOrigin::Static, - ); - } else { - routing_table.register_global_dial_info( - DialInfo::ws(fqdn, port, path.clone()), + DialInfo::ws( + split_url.host, + split_url.port.unwrap_or(80), + split_url + .path + .map(|p| p.to_string()) + .unwrap_or_else(|| "/".to_string()), + ), Some(NetworkClass::Server), DialInfoOrigin::Static, ); @@ -914,11 +913,11 @@ impl Network { pub async fn start_wss_listeners(&self) -> Result<(), String> { let routing_table = self.routing_table(); - let (listen_address, public_address, path) = { + let (listen_address, url, path) = { let c = self.config.get(); ( c.network.protocol.wss.listen_address.clone(), - c.network.protocol.wss.public_address.clone(), + c.network.protocol.wss.url.clone(), c.network.protocol.wss.path.clone(), ) }; @@ -943,24 +942,25 @@ impl Network { ); // Add static public dialinfo if it's configured - if let Some(public_address) = public_address.as_ref() { - let (public_fqdn, public_port) = split_port(public_address).map_err(|_| { - "invalid WSS public address, port not specified correctly".to_owned() - })?; - let public_port = public_port - .ok_or_else(|| "port must be specified for public WSS address".to_owned())?; - + if let Some(url) = url.as_ref() { + let split_url = SplitUrl::from_str(url)?; + if split_url.scheme.to_ascii_lowercase() != "wss" { + return Err("WSS URL must use 'wss://' scheme".to_owned()); + } routing_table.register_global_dial_info( - DialInfo::wss(fqdn, public_port, public_fqdn), - None, + DialInfo::wss( + split_url.host, + split_url.port.unwrap_or(443), + split_url + .path + .map(|p| p.to_string()) + .unwrap_or_else(|| "/".to_string()), + ), + Some(NetworkClass::Server), DialInfoOrigin::Static, ); } else { - routing_table.register_global_dial_info( - DialInfo::wss(fqdn, port, path.clone()), - None, - DialInfoOrigin::Static, - ); + return Err("WSS URL must be specified due to TLS requirements".to_owned()); } self.inner.lock().wss_listen = true; diff --git a/veilid-core/src/network_manager.rs b/veilid-core/src/network_manager.rs index cd6c783d..f9e70dcb 100644 --- a/veilid-core/src/network_manager.rs +++ b/veilid-core/src/network_manager.rs @@ -227,10 +227,11 @@ impl NetworkManager { } pub async fn tick(&self) -> Result<(), String> { - let (net, lease_manager, receipt_manager) = { + let (routing_table, net, lease_manager, receipt_manager) = { let inner = self.inner.lock(); let components = inner.components.as_ref().unwrap(); ( + inner.routing_table.as_ref().unwrap().clone(), components.net.clone(), components.lease_manager.clone(), components.receipt_manager.clone(), @@ -244,6 +245,9 @@ impl NetworkManager { net.startup().await?; } + // Run the routing table tick + routing_table.tick().await?; + // Run the low level network tick net.tick().await?; diff --git a/veilid-core/src/routing_table/mod.rs b/veilid-core/src/routing_table/mod.rs index dad6581d..ac8ab4da 100644 --- a/veilid-core/src/routing_table/mod.rs +++ b/veilid-core/src/routing_table/mod.rs @@ -205,14 +205,14 @@ impl RoutingTable { }); info!( - "Local Dial Info: {} ({:?})", + "Local Dial Info: {}", NodeDialInfoSingle { node_id: NodeId::new(inner.node_id), dial_info } .to_string(), - origin, ); + debug!(" Origin: {:?}", origin); } pub fn clear_local_dial_info(&self) { @@ -281,15 +281,15 @@ impl RoutingTable { }); info!( - "Public Dial Info: {} ({:?}#{:?})", + "Public Dial Info: {}", NodeDialInfoSingle { node_id: NodeId::new(inner.node_id), dial_info } .to_string(), - origin, - network_class, ); + debug!(" Origin: {:?}", origin); + debug!(" Network Class: {:?}", network_class); } pub fn clear_global_dial_info(&self) { @@ -613,6 +613,8 @@ impl RoutingTable { c.network.bootstrap.clone() }; + trace!("Bootstrap task with: {:?}", bootstrap); + // Map all bootstrap entries to a single key with multiple dialinfo let mut bsmap: BTreeMap> = BTreeMap::new(); for b in bootstrap { diff --git a/veilid-core/src/tests/common/test_host_interface.rs b/veilid-core/src/tests/common/test_host_interface.rs index 844f5369..3cb1615b 100644 --- a/veilid-core/src/tests/common/test_host_interface.rs +++ b/veilid-core/src/tests/common/test_host_interface.rs @@ -311,6 +311,131 @@ pub async fn test_sleep() { } } +macro_rules! assert_split_url { + ($url:expr, $scheme:expr, $host:expr) => { + assert_eq!( + SplitUrl::from_str($url), + Ok(SplitUrl::new($scheme, None, $host, None, None)) + ); + }; + ($url:expr, $scheme:expr, $host:expr, $port:expr) => { + assert_eq!( + SplitUrl::from_str($url), + Ok(SplitUrl::new($scheme, None, $host, $port, None)) + ); + }; + ($url:expr, $scheme:expr, $host:expr, $port:expr, $path:expr) => { + assert_eq!( + SplitUrl::from_str($url), + Ok(SplitUrl::new( + $scheme, + None, + $host, + $port, + Some(SplitUrlPath::new( + $path, + Option::::None, + Option::::None + )) + )) + ); + }; + ($url:expr, $scheme:expr, $host:expr, $port:expr, $path:expr, $frag:expr, $query:expr) => { + assert_eq!( + SplitUrl::from_str($url), + Ok(SplitUrl::new( + $scheme, + None, + $host, + $port, + Some(SplitUrlPath::new($path, $frag, $query)) + )) + ); + }; +} + +macro_rules! assert_split_url_parse { + ($url:expr) => { + let url = $url; + let su1 = SplitUrl::from_str(url).expect("should parse"); + assert_eq!(su1.to_string(), url); + }; +} +macro_rules! assert_err { + ($ex:expr) => { + if let Ok(v) = $ex { + panic!("assertion failed, expected Err(..), got {:?}", v); + } + }; +} + +pub async fn test_split_url() { + info!("testing split_url"); + + assert_split_url!("http://foo", "http", "foo"); + assert_split_url!("http://foo:1234", "http", "foo", Some(1234)); + assert_split_url!("http://foo:1234/", "http", "foo", Some(1234), ""); + assert_split_url!( + "http://foo:1234/asdf/qwer", + "http", + "foo", + Some(1234), + "asdf/qwer" + ); + assert_split_url!("http://foo/", "http", "foo", None, ""); + assert_split_url!("http://foo/asdf/qwer", "http", "foo", None, "asdf/qwer"); + assert_split_url!( + "http://foo/asdf/qwer#3", + "http", + "foo", + None, + "asdf/qwer", + Some("3"), + Option::::None + ); + assert_split_url!( + "http://foo/asdf/qwer?xxx", + "http", + "foo", + None, + "asdf/qwer", + Option::::None, + Some("xxx") + ); + assert_split_url!( + "http://foo/asdf/qwer#yyy?xxx", + "http", + "foo", + None, + "asdf/qwer", + Some("yyy"), + Some("xxx") + ); + assert_err!(SplitUrl::from_str("://asdf")); + assert_err!(SplitUrl::from_str("")); + assert_err!(SplitUrl::from_str("::")); + assert_err!(SplitUrl::from_str("://:")); + assert_err!(SplitUrl::from_str("a://:")); + assert_err!(SplitUrl::from_str("a://:1243")); + assert_err!(SplitUrl::from_str("a://:65536")); + assert_err!(SplitUrl::from_str("a://:-16")); + assert_err!(SplitUrl::from_str("a:///")); + assert_err!(SplitUrl::from_str("a:///qwer:")); + assert_err!(SplitUrl::from_str("a:///qwer://")); + assert_err!(SplitUrl::from_str("a://qwer://")); + + assert_split_url_parse!("sch://foo:bar@baz.com:1234/fnord#qux?zuz"); + assert_split_url_parse!("sch://foo:bar@baz.com:1234/fnord#qux"); + assert_split_url_parse!("sch://foo:bar@baz.com:1234/fnord?zuz"); + assert_split_url_parse!("sch://foo:bar@baz.com:1234/fnord/"); + assert_split_url_parse!("sch://foo:bar@baz.com:1234//"); + assert_split_url_parse!("sch://foo:bar@baz.com:1234"); + assert_split_url_parse!("sch://@baz.com:1234"); + assert_split_url_parse!("sch://baz.com/asdf/asdf"); + assert_split_url_parse!("sch://baz.com/"); + assert_split_url_parse!("s://s"); +} + pub async fn test_protected_store() { info!("testing protected store"); @@ -518,6 +643,7 @@ pub async fn test_all() { test_log().await; test_get_timestamp().await; test_tools().await; + test_split_url().await; test_get_random_u64().await; test_get_random_u32().await; test_sleep().await; diff --git a/veilid-core/src/tests/common/test_veilid_config.rs b/veilid-core/src/tests/common/test_veilid_config.rs index 417a604d..d95442ed 100644 --- a/veilid-core/src/tests/common/test_veilid_config.rs +++ b/veilid-core/src/tests/common/test_veilid_config.rs @@ -189,13 +189,16 @@ pub fn config_callback(key: String) -> Result, String> { "network.tls.certificate_path" => Ok(Box::new(get_certfile_path())), "network.tls.private_key_path" => Ok(Box::new(get_keyfile_path())), "network.tls.connection_initial_timeout" => Ok(Box::new(2_000_000u64)), - "network.application.path" => Ok(Box::new(String::from("/app"))), - "network.application.https.enabled" => Ok(Box::new(true)), + "network.application.https.enabled" => Ok(Box::new(false)), "network.application.https.listen_address" => Ok(Box::new(String::from("[::1]:5150"))), - "network.application.http.enabled" => Ok(Box::new(true)), + "network.application.https.path" => Ok(Box::new(String::from("app"))), + "network.application.https.url" => Ok(Box::new(Option::::None)), + "network.application.http.enabled" => Ok(Box::new(false)), "network.application.http.listen_address" => Ok(Box::new(String::from("[::1]:5150"))), + "network.application.http.path" => Ok(Box::new(String::from("app"))), + "network.application.http.url" => Ok(Box::new(Option::::None)), "network.protocol.udp.enabled" => Ok(Box::new(true)), - "network.protocol.udp.socket_pool_size" => Ok(Box::new(0u32)), + "network.protocol.udp.socket_pool_size" => Ok(Box::new(16u32)), "network.protocol.udp.listen_address" => Ok(Box::new(String::from("[::1]:5150"))), "network.protocol.udp.public_address" => Ok(Box::new(Option::::None)), "network.protocol.tcp.connect" => Ok(Box::new(true)), @@ -203,23 +206,27 @@ pub fn config_callback(key: String) -> Result, String> { "network.protocol.tcp.max_connections" => Ok(Box::new(32u32)), "network.protocol.tcp.listen_address" => Ok(Box::new(String::from("[::1]:5150"))), "network.protocol.tcp.public_address" => Ok(Box::new(Option::::None)), - "network.protocol.ws.connect" => Ok(Box::new(true)), - "network.protocol.ws.listen" => Ok(Box::new(true)), + "network.protocol.ws.connect" => Ok(Box::new(false)), + "network.protocol.ws.listen" => Ok(Box::new(false)), "network.protocol.ws.max_connections" => Ok(Box::new(16u32)), "network.protocol.ws.listen_address" => Ok(Box::new(String::from("[::1]:5150"))), - "network.protocol.ws.path" => Ok(Box::new(String::from("/ws"))), - "network.protocol.ws.public_address" => Ok(Box::new(Option::::None)), - "network.protocol.wss.connect" => Ok(Box::new(true)), - "network.protocol.wss.listen" => Ok(Box::new(true)), + "network.protocol.ws.path" => Ok(Box::new(String::from("ws"))), + "network.protocol.ws.url" => Ok(Box::new(Option::::None)), + "network.protocol.wss.connect" => Ok(Box::new(false)), + "network.protocol.wss.listen" => Ok(Box::new(false)), "network.protocol.wss.max_connections" => Ok(Box::new(16u32)), "network.protocol.wss.listen_address" => Ok(Box::new(String::from("[::1]:5150"))), - "network.protocol.wss.path" => Ok(Box::new(String::from("/ws"))), - "network.protocol.wss.public_address" => Ok(Box::new(Option::::None)), + "network.protocol.wss.path" => Ok(Box::new(String::from("ws"))), + "network.protocol.wss.url" => Ok(Box::new(Option::::None)), "network.leases.max_server_signal_leases" => Ok(Box::new(256u32)), "network.leases.max_server_relay_leases" => Ok(Box::new(8u32)), "network.leases.max_client_signal_leases" => Ok(Box::new(2u32)), "network.leases.max_client_relay_leases" => Ok(Box::new(2u32)), - _ => Err(format!("config key '{}' doesn't exist", key)), + _ => { + let err = format!("config key '{}' doesn't exist", key); + debug!("{}", err); + Err(err) + } } } @@ -278,13 +285,16 @@ pub async fn test_config() { assert_eq!(inner.network.tls.private_key_path, get_keyfile_path()); assert_eq!(inner.network.tls.connection_initial_timeout, 2_000_000u64); - assert_eq!(inner.network.application.path, "/app"); - assert_eq!(inner.network.application.https.enabled, true); + assert_eq!(inner.network.application.https.enabled, false); assert_eq!(inner.network.application.https.listen_address, "[::1]:5150"); - assert_eq!(inner.network.application.http.enabled, true); + assert_eq!(inner.network.application.https.path, "app"); + assert_eq!(inner.network.application.https.url, None); + assert_eq!(inner.network.application.http.enabled, false); assert_eq!(inner.network.application.http.listen_address, "[::1]:5150"); + assert_eq!(inner.network.application.http.path, "app"); + assert_eq!(inner.network.application.http.url, None); assert_eq!(inner.network.protocol.udp.enabled, true); - assert_eq!(inner.network.protocol.udp.socket_pool_size, 0u32); + assert_eq!(inner.network.protocol.udp.socket_pool_size, 16u32); assert_eq!(inner.network.protocol.udp.listen_address, "[::1]:5150"); assert_eq!(inner.network.protocol.udp.public_address, None); assert_eq!(inner.network.protocol.tcp.connect, true); @@ -292,18 +302,18 @@ pub async fn test_config() { assert_eq!(inner.network.protocol.tcp.max_connections, 32u32); assert_eq!(inner.network.protocol.tcp.listen_address, "[::1]:5150"); assert_eq!(inner.network.protocol.tcp.public_address, None); - assert_eq!(inner.network.protocol.ws.connect, true); - assert_eq!(inner.network.protocol.ws.listen, true); + assert_eq!(inner.network.protocol.ws.connect, false); + assert_eq!(inner.network.protocol.ws.listen, false); assert_eq!(inner.network.protocol.ws.max_connections, 16u32); assert_eq!(inner.network.protocol.ws.listen_address, "[::1]:5150"); - assert_eq!(inner.network.protocol.ws.path, "/ws"); - assert_eq!(inner.network.protocol.ws.public_address, None); - assert_eq!(inner.network.protocol.wss.connect, true); - assert_eq!(inner.network.protocol.wss.listen, true); + assert_eq!(inner.network.protocol.ws.path, "ws"); + assert_eq!(inner.network.protocol.ws.url, None); + assert_eq!(inner.network.protocol.wss.connect, false); + assert_eq!(inner.network.protocol.wss.listen, false); assert_eq!(inner.network.protocol.wss.max_connections, 16u32); assert_eq!(inner.network.protocol.wss.listen_address, "[::1]:5150"); - assert_eq!(inner.network.protocol.wss.path, "/ws"); - assert_eq!(inner.network.protocol.wss.public_address, None); + assert_eq!(inner.network.protocol.wss.path, "ws"); + assert_eq!(inner.network.protocol.wss.url, None); } pub async fn test_all() { diff --git a/veilid-core/src/veilid_api.rs b/veilid-core/src/veilid_api.rs index 1ea84ebb..7d65a2eb 100644 --- a/veilid-core/src/veilid_api.rs +++ b/veilid-core/src/veilid_api.rs @@ -385,7 +385,7 @@ impl DialInfo { let addr: IpAddr = di .fqdn .parse() - .map_err(|e| format!("Failed to parse WS fqdn: {}", e))?; + .map_err(|e| format!("Failed to parse WSS fqdn: {}", e))?; Ok(addr) } } @@ -896,7 +896,7 @@ impl fmt::Debug for VeilidAPIInner { impl Drop for VeilidAPIInner { fn drop(&mut self) { if let Some(core) = self.core.take() { - intf::spawn_local(core.internal_shutdown()).detach(); + intf::spawn_local(core.shutdown()).detach(); } } } @@ -953,7 +953,7 @@ impl VeilidAPI { pub async fn shutdown(self) { let core = { self.inner.lock().core.take() }; if let Some(core) = core { - core.internal_shutdown().await; + core.shutdown().await; } } diff --git a/veilid-core/src/veilid_config.rs b/veilid-core/src/veilid_config.rs index c3782196..a8e1e6cf 100644 --- a/veilid-core/src/veilid_config.rs +++ b/veilid-core/src/veilid_config.rs @@ -14,17 +14,20 @@ cfg_if! { pub struct VeilidConfigHTTPS { pub enabled: bool, pub listen_address: String, + pub path: String, + pub url: Option, // Fixed URL is not optional for TLS-based protocols and is dynamically validated } #[derive(Default, Clone)] pub struct VeilidConfigHTTP { pub enabled: bool, pub listen_address: String, + pub path: String, + pub url: Option, } #[derive(Default, Clone)] pub struct VeilidConfigApplication { - pub path: String, pub https: VeilidConfigHTTPS, pub http: VeilidConfigHTTP, } @@ -53,7 +56,7 @@ pub struct VeilidConfigWS { pub max_connections: u32, pub listen_address: String, pub path: String, - pub public_address: Option, + pub url: Option, } #[derive(Default, Clone)] @@ -63,7 +66,7 @@ pub struct VeilidConfigWSS { pub max_connections: u32, pub listen_address: String, pub path: String, - pub public_address: Option, + pub url: Option, // Fixed URL is not optional for TLS-based protocols and is dynamically validated } #[derive(Default, Clone)] @@ -184,9 +187,11 @@ impl VeilidConfig { macro_rules! get_config { ($key:expr) => { let keyname = &stringify!($key)[6..]; - $key = *cb(keyname.to_owned())? - .downcast() - .map_err(|_| format!("incorrect type for key: {}", keyname))?; + $key = *cb(keyname.to_owned())?.downcast().map_err(|_| { + let err = format!("incorrect type for key: {}", keyname); + debug!("{}", err); + err + })?; }; } @@ -232,11 +237,14 @@ impl VeilidConfig { get_config!(inner.network.tls.certificate_path); get_config!(inner.network.tls.private_key_path); get_config!(inner.network.tls.connection_initial_timeout); - get_config!(inner.network.application.path); get_config!(inner.network.application.https.enabled); get_config!(inner.network.application.https.listen_address); + get_config!(inner.network.application.https.path); + get_config!(inner.network.application.https.url); get_config!(inner.network.application.http.enabled); get_config!(inner.network.application.http.listen_address); + get_config!(inner.network.application.http.path); + get_config!(inner.network.application.http.url); get_config!(inner.network.protocol.udp.enabled); get_config!(inner.network.protocol.udp.socket_pool_size); get_config!(inner.network.protocol.udp.listen_address); @@ -251,13 +259,13 @@ impl VeilidConfig { get_config!(inner.network.protocol.ws.max_connections); get_config!(inner.network.protocol.ws.listen_address); get_config!(inner.network.protocol.ws.path); - get_config!(inner.network.protocol.ws.public_address); + get_config!(inner.network.protocol.ws.url); get_config!(inner.network.protocol.wss.connect); get_config!(inner.network.protocol.wss.listen); get_config!(inner.network.protocol.wss.max_connections); get_config!(inner.network.protocol.wss.listen_address); get_config!(inner.network.protocol.wss.path); - get_config!(inner.network.protocol.wss.public_address); + get_config!(inner.network.protocol.wss.url); get_config!(inner.network.leases.max_server_signal_leases); get_config!(inner.network.leases.max_server_relay_leases); get_config!(inner.network.leases.max_client_signal_leases); @@ -266,7 +274,12 @@ impl VeilidConfig { // Initialize node id as early as possible because it is used // for encryption purposes all over the program - self.init_node_id().await + self.init_node_id().await?; + + // Validate settings + self.validate().await?; + + Ok(()) } pub async fn terminate(&self) { @@ -277,6 +290,85 @@ impl VeilidConfig { self.inner.read() } + async fn validate(&self) -> Result<(), String> { + let inner = self.inner.read(); + if inner.network.protocol.udp.enabled { + // Validate UDP settings + if inner.network.protocol.udp.socket_pool_size == 0 { + return Err("UDP socket pool size must be > 0 in config key 'network.protocol.udp.socket_pool_size'".to_owned()); + } + } + if inner.network.protocol.tcp.listen { + // Validate TCP settings + if inner.network.protocol.tcp.max_connections == 0 { + return Err("TCP max connections must be > 0 in config key 'network.protocol.tcp.max_connections'".to_owned()); + } + } + if inner.network.protocol.ws.listen { + // Validate WS settings + if inner.network.protocol.ws.max_connections == 0 { + return Err("WS max connections must be > 0 in config key 'network.protocol.ws.max_connections'".to_owned()); + } + if inner.network.application.https.enabled + && inner.network.application.https.path == inner.network.protocol.ws.path + { + return Err("WS path conflicts with HTTPS application path in config key 'network.protocol.ws.path'".to_owned()); + } + if inner.network.application.http.enabled + && inner.network.application.http.path == inner.network.protocol.ws.path + { + return Err("WS path conflicts with HTTP application path in config key 'network.protocol.ws.path'".to_owned()); + } + } + if inner.network.protocol.wss.listen { + // Validate WSS settings + if inner.network.protocol.wss.max_connections == 0 { + return Err("WSS max connections must be > 0 in config key 'network.protocol.wss.max_connections'".to_owned()); + } + if inner + .network + .protocol + .wss + .url + .as_ref() + .map(|u| u.is_empty()) + .unwrap_or_default() + { + return Err( + "WSS URL must be specified in config key 'network.protocol.wss.url'".to_owned(), + ); + } + if inner.network.application.https.enabled + && inner.network.application.https.path == inner.network.protocol.wss.path + { + return Err("WSS path conflicts with HTTPS application path in config key 'network.protocol.ws.path'".to_owned()); + } + if inner.network.application.http.enabled + && inner.network.application.http.path == inner.network.protocol.wss.path + { + return Err("WSS path conflicts with HTTP application path in config key 'network.protocol.ws.path'".to_owned()); + } + } + if inner.network.application.https.enabled { + // Validate HTTPS settings + if inner + .network + .application + .https + .url + .as_ref() + .map(|u| u.is_empty()) + .unwrap_or_default() + { + return Err( + "HTTPS URL must be specified in config key 'network.application.https.url'" + .to_owned(), + ); + } + } + Ok(()) + } + // Get the node id from config if one is specified async fn init_node_id(&self) -> Result<(), String> { let mut inner = self.inner.write(); diff --git a/veilid-core/src/veilid_core.rs b/veilid-core/src/veilid_core.rs index 4f938abd..6856a046 100644 --- a/veilid-core/src/veilid_core.rs +++ b/veilid-core/src/veilid_core.rs @@ -163,15 +163,13 @@ impl VeilidCore { match self.internal_startup(&mut *inner, setup).await { Ok(v) => Ok(v), Err(e) => { - self.clone().internal_shutdown().await; + Self::internal_shutdown(&mut *inner).await; Err(e) } } } - // stop the node gracefully because the veilid api was dropped - pub(crate) async fn internal_shutdown(self) { - let mut inner = self.inner.lock(); + async fn internal_shutdown(inner: &mut VeilidCoreInner) { trace!("VeilidCore::internal_shutdown starting"); // Detach the API object @@ -204,5 +202,11 @@ impl VeilidCore { trace!("VeilidCore::shutdown complete"); } + // stop the node gracefully because the veilid api was dropped + pub(crate) async fn shutdown(self) { + let mut inner = self.inner.lock(); + Self::internal_shutdown(&mut *inner); + } + // } diff --git a/veilid-core/src/xx/mod.rs b/veilid-core/src/xx/mod.rs index c0f3533d..aa906297 100644 --- a/veilid-core/src/xx/mod.rs +++ b/veilid-core/src/xx/mod.rs @@ -7,12 +7,14 @@ mod ip_addr_port; mod ip_extra; mod single_future; mod single_shot_eventual; +mod split_url; mod tick_task; mod tools; pub use cfg_if::*; pub use log::*; pub use parking_lot::*; +pub use split_url::*; pub use static_assertions::*; pub type PinBox = Pin>; diff --git a/veilid-core/src/xx/split_url.rs b/veilid-core/src/xx/split_url.rs new file mode 100644 index 00000000..6dfc4e02 --- /dev/null +++ b/veilid-core/src/xx/split_url.rs @@ -0,0 +1,325 @@ +// Loose subset interpretation of the URL standard +// Not using full Url crate here for no_std compatibility +// +// Caveats: +// No support for query string parsing +// No support for paths with ';' parameters +// URLs must convert to UTF8 +// Only IP address and DNS hostname host fields are supported + +use super::IpAddr; +use core::fmt; +use core::str::FromStr; + +fn is_alphanum(c: u8) -> bool { + matches!(c, + b'A'..=b'Z' + | b'a'..=b'z' + | b'0'..=b'9' + ) +} +fn is_mark(c: u8) -> bool { + matches!( + c, + b'-' | b'_' | b'.' | b'!' | b'~' | b'*' | b'\'' | b'(' | b')' + ) +} +fn is_unreserved(c: u8) -> bool { + is_alphanum(c) || is_mark(c) +} + +fn must_encode_userinfo(c: u8) -> bool { + !(is_unreserved(c) || matches!(c, b'%' | b':' | b';' | b'&' | b'=' | b'+' | b'$' | b',')) +} + +fn must_encode_path(c: u8) -> bool { + !(is_unreserved(c) + || matches!( + c, + b'%' | b'/' | b':' | b'@' | b'&' | b'=' | b'+' | b'$' | b',' + )) +} + +fn is_valid_host>(host: H) -> bool { + if host.as_ref().is_empty() { + return false; + } + if IpAddr::from_str(host.as_ref()).is_err() { + for ch in host.as_ref().chars() { + if !matches!(ch, + 'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '.' ) + { + return false; + } + } + } + true +} + +fn is_valid_scheme>(host: H) -> bool { + let mut chars = host.as_ref().chars(); + if let Some(ch) = chars.next() { + if !matches!(ch, 'A'..='Z' | 'a'..='z') { + return false; + } + } else { + return false; + } + for ch in chars { + if !matches!(ch, + 'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '+' | '.' ) + { + return false; + } + } + true +} + +fn hex_decode(h: u8) -> Result { + match h { + b'0'..=b'9' => Ok(h - b'0'), + b'A'..=b'F' => Ok(h - b'A' + 10), + b'a'..=b'f' => Ok(h - b'a' + 10), + _ => Err("Unexpected character in percent encoding".to_owned()), + } +} + +fn hex_encode(c: u8) -> (char, char) { + let c0 = c >> 4; + let c1 = c & 15; + ( + if c0 < 10 { + char::from_u32((b'0' + c0) as u32).unwrap() + } else { + char::from_u32((b'A' + c0 - 10) as u32).unwrap() + }, + if c1 < 10 { + char::from_u32((b'0' + c1) as u32).unwrap() + } else { + char::from_u32((b'A' + c1 - 10) as u32).unwrap() + }, + ) +} + +fn url_decode>(s: S) -> Result { + let url = s.as_ref().to_owned(); + if !url.is_ascii() { + return Err("URL is not in ASCII encoding".to_owned()); + } + let url_bytes = url.as_bytes(); + let mut dec_bytes: Vec = Vec::with_capacity(url_bytes.len()); + let mut i = 0; + let end = url_bytes.len(); + while i < end { + let mut b = url_bytes[i]; + i += 1; + if b == b'%' { + if (i + 1) >= end { + return Err("Invalid URL encoding".to_owned()); + } + b = hex_decode(url_bytes[i])? << 4 | hex_decode(url_bytes[i + 1])?; + i += 2; + } + dec_bytes.push(b); + } + String::from_utf8(dec_bytes).map_err(|e| format!("Decoded URL is not valid UTF-8: {}", e)) +} + +fn url_encode>(s: S, must_encode: impl Fn(u8) -> bool) -> String { + let bytes = s.as_ref().as_bytes(); + let mut out = String::new(); + for b in bytes { + if must_encode(*b) { + let (c0, c1) = hex_encode(*b); + out.push('%'); + out.push(c0); + out.push(c1); + } else { + out.push(char::from_u32(*b as u32).unwrap()) + } + } + out +} + +fn convert_port(port_str: N) -> Result +where + N: AsRef, +{ + port_str + .as_ref() + .parse::() + .map_err(|e| format!("Invalid port: {}", e)) +} + +/////////////////////////////////////////////////////////////////////////////// + +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct SplitUrlPath { + pub path: String, + pub fragment: Option, + pub query: Option, +} + +impl SplitUrlPath { + pub fn new(path: P, fragment: Option, query: Option) -> Self + where + P: AsRef, + F: AsRef, + Q: AsRef, + { + Self { + path: path.as_ref().to_owned(), + fragment: fragment.map(|f| f.as_ref().to_owned()), + query: query.map(|f| f.as_ref().to_owned()), + } + } +} + +impl FromStr for SplitUrlPath { + type Err = String; + fn from_str(s: &str) -> Result { + Ok(if let Some((p, q)) = s.split_once('?') { + if let Some((p, f)) = p.split_once('#') { + SplitUrlPath::new(url_decode(p)?, Some(url_decode(f)?), Some(q)) + } else { + SplitUrlPath::new(url_decode(p)?, Option::::None, Some(q)) + } + } else if let Some((p, f)) = s.split_once('#') { + SplitUrlPath::new(url_decode(p)?, Some(url_decode(f)?), Option::::None) + } else { + SplitUrlPath::new( + url_decode(s)?, + Option::::None, + Option::::None, + ) + }) + } +} + +impl fmt::Display for SplitUrlPath { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(fragment) = &self.fragment { + if let Some(query) = &self.query { + write!( + f, + "{}#{}?{}", + url_encode(&self.path, must_encode_path), + url_encode(fragment, must_encode_path), + query + ) + } else { + write!(f, "{}#{}", self.path, fragment) + } + } else if let Some(query) = &self.query { + write!(f, "{}?{}", url_encode(&self.path, must_encode_path), query) + } else { + write!(f, "{}", url_encode(&self.path, must_encode_path)) + } + } +} + +/////////////////////////////////////////////////////////////////////////////// + +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct SplitUrl { + pub scheme: String, + pub userinfo: Option, + pub host: String, + pub port: Option, + pub path: Option, +} + +impl SplitUrl { + pub fn new( + scheme: S, + userinfo: Option, + host: H, + port: Option, + path: Option, + ) -> Self + where + S: AsRef, + H: AsRef, + { + Self { + scheme: scheme.as_ref().to_owned(), + userinfo, + host: host.as_ref().to_owned(), + port, + path, + } + } +} + +impl FromStr for SplitUrl { + type Err = String; + fn from_str(s: &str) -> Result { + if let Some((scheme, mut rest)) = s.split_once("://") { + if !is_valid_scheme(scheme) { + return Err("Invalid scheme specified".to_owned()); + } + let userinfo = { + if let Some((userinfo_str, after)) = rest.split_once("@") { + rest = after; + Some(url_decode(userinfo_str)?) + } else { + None + } + }; + if let Some((host, rest)) = rest.rsplit_once(':') { + if !is_valid_host(host) { + return Err("Invalid host specified".to_owned()); + } + if let Some((portstr, path)) = rest.split_once('/') { + let port = convert_port(portstr)?; + let path = SplitUrlPath::from_str(path)?; + Ok(SplitUrl::new( + scheme, + userinfo, + host, + Some(port), + Some(path), + )) + } else { + let port = convert_port(rest)?; + Ok(SplitUrl::new(scheme, userinfo, host, Some(port), None)) + } + } else if let Some((host, path)) = rest.split_once('/') { + if !is_valid_host(host) { + return Err("Invalid host specified".to_owned()); + } + let path = SplitUrlPath::from_str(path)?; + Ok(SplitUrl::new(scheme, userinfo, host, None, Some(path))) + } else { + if !is_valid_host(rest) { + return Err("Invalid host specified".to_owned()); + } + Ok(SplitUrl::new(scheme, userinfo, rest, None, None)) + } + } else { + Err("No scheme specified".to_owned()) + } + } +} + +impl fmt::Display for SplitUrl { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let hostname = { + if let Some(userinfo) = &self.userinfo { + let userinfo = url_encode(userinfo, must_encode_userinfo); + if let Some(port) = self.port { + format!("{}@{}:{}", userinfo, self.host, port) + } else { + format!("{}@{}", userinfo, self.host) + } + } else { + self.host.clone() + } + }; + if let Some(path) = &self.path { + write!(f, "{}://{}/{}", self.scheme, hostname, path) + } else { + write!(f, "{}://{}", self.scheme, hostname) + } + } +} diff --git a/veilid-server/src/settings.rs b/veilid-server/src/settings.rs index 06250410..469018be 100644 --- a/veilid-server/src/settings.rs +++ b/veilid-server/src/settings.rs @@ -70,13 +70,16 @@ core: private_key_path: "/etc/veilid/private/server.key" connection_initial_timeout: 2000000 application: - path: "app" https: - enabled: true + enabled: false listen_address: "[::]:5150" + path: "app" + # url: "https://localhost:5150" http: - enabled: true + enabled: false listen_address: "[::]:5150" + path: "app" + # url: "http://localhost:5150" protocol: udp: enabled: true @@ -94,15 +97,15 @@ core: listen: true max_connections: 16 listen_address: "[::]:5150" - path: "/ws" - # "public_address": "" + path: "ws" + # url: "ws://localhost:5150/ws" wss: connect: true - listen: true + listen: false max_connections: 16 listen_address: "[::]:5150" - path: "/ws" - # "public_address": "" + path: "ws" + # url: "" leases: max_server_signal_leases: 256 max_server_relay_leases: 8 @@ -173,30 +176,47 @@ pub fn convert_loglevel(log_level: LogLevel) -> LevelFilter { } #[derive(Debug, Clone, PartialEq)] -pub struct ParsedURL { +pub struct ParsedUrl { pub urlstring: String, pub url: Url, } -impl FromStr for ParsedURL { - type Err = url::ParseError; - fn from_str(s: &str) -> Result { - let url = Url::parse(s)?; +impl ParsedUrl { + pub fn offset_port(&mut self, offset: u16) -> Result<(), ()> { + // Bump port on url + self.url.set_port(Some(self.url.port().unwrap() + offset))?; + self.urlstring = self.url.to_string(); + Ok(()) + } +} +impl FromStr for ParsedUrl { + type Err = url::ParseError; + fn from_str(s: &str) -> Result { + let mut url = Url::parse(s)?; + if url.scheme().to_lowercase() == "http" && url.port().is_none() { + url.set_port(Some(80)) + .map_err(|_| url::ParseError::InvalidPort)? + } + if url.scheme().to_lowercase() == "https" && url.port().is_none() { + url.set_port(Some(443)) + .map_err(|_| url::ParseError::InvalidPort)?; + } + let parsed_urlstring = url.to_string(); Ok(Self { - urlstring: s.to_string(), + urlstring: parsed_urlstring, url, }) } } -impl<'de> serde::Deserialize<'de> for ParsedURL { +impl<'de> serde::Deserialize<'de> for ParsedUrl { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let s = String::deserialize(deserializer)?; - ParsedURL::from_str(s.as_str()).map_err(serde::de::Error::custom) + ParsedUrl::from_str(s.as_str()).map_err(serde::de::Error::custom) } } @@ -279,17 +299,20 @@ pub struct Logging { pub struct Https { pub enabled: bool, pub listen_address: NamedSocketAddrs, + pub path: PathBuf, + pub url: Option, } #[derive(Debug, Deserialize)] pub struct Http { pub enabled: bool, pub listen_address: NamedSocketAddrs, + pub path: PathBuf, + pub url: Option, } #[derive(Debug, Deserialize)] pub struct Application { - pub path: PathBuf, pub https: Https, pub http: Http, } @@ -317,8 +340,8 @@ pub struct Ws { pub listen: bool, pub max_connections: u32, pub listen_address: NamedSocketAddrs, - pub path: String, - pub public_address: Option, + pub path: PathBuf, + pub url: Option, } #[derive(Debug, Deserialize)] @@ -327,8 +350,8 @@ pub struct Wss { pub listen: bool, pub max_connections: u32, pub listen_address: NamedSocketAddrs, - pub path: String, - pub public_address: Option, + pub path: PathBuf, + pub url: Option, } #[derive(Debug, Deserialize)] @@ -387,7 +410,7 @@ pub struct Network { pub connection_initial_timeout: u64, pub node_id: veilid_core::DHTKey, pub node_id_secret: veilid_core::DHTKeySecret, - pub bootstrap: Vec, + pub bootstrap: Vec, pub rpc: Rpc, pub dht: Dht, pub upnp: bool, @@ -450,8 +473,12 @@ impl Settings { load_config(&mut cfg, config_file_path)?; } + // Generate config + let inner: SettingsInner = cfg.try_into()?; + + // Ok(Self { - inner: Arc::new(RwLock::new(cfg.try_into()?)), + inner: Arc::new(RwLock::new(inner)), }) } pub fn read(&self) -> RwLockReadGuard { @@ -493,6 +520,9 @@ impl Settings { .ws .listen_address .offset_port(idx)?; + if let Some(url) = &mut (*settingsrw).core.network.protocol.ws.url { + url.offset_port(idx)?; + } (*settingsrw) .core .network @@ -500,6 +530,9 @@ impl Settings { .wss .listen_address .offset_port(idx)?; + if let Some(url) = &mut (*settingsrw).core.network.protocol.wss.url { + url.offset_port(idx)?; + } // bump application ports (*settingsrw) .core @@ -508,6 +541,9 @@ impl Settings { .http .listen_address .offset_port(idx)?; + if let Some(url) = &mut (*settingsrw).core.network.application.http.url { + url.offset_port(idx)?; + } (*settingsrw) .core .network @@ -515,7 +551,9 @@ impl Settings { .https .listen_address .offset_port(idx)?; - + if let Some(url) = &mut (*settingsrw).core.network.application.https.url { + url.offset_port(idx)?; + } Ok(()) } @@ -665,15 +703,6 @@ impl Settings { "network.tls.connection_initial_timeout" => { Ok(Box::new(inner.core.network.tls.connection_initial_timeout)) } - "network.application.path" => Ok(Box::new( - inner - .core - .network - .application - .path - .to_string_lossy() - .to_string(), - )), "network.application.https.enabled" => { Ok(Box::new(inner.core.network.application.https.enabled)) } @@ -687,6 +716,26 @@ impl Settings { .name .clone(), )), + "network.application.https.path" => Ok(Box::new( + inner + .core + .network + .application + .https + .path + .to_string_lossy() + .to_string(), + )), + "network.application.https.url" => Ok(Box::new( + inner + .core + .network + .application + .https + .url + .as_ref() + .map(|a| a.urlstring.clone()), + )), "network.application.http.enabled" => { Ok(Box::new(inner.core.network.application.http.enabled)) } @@ -700,6 +749,26 @@ impl Settings { .name .clone(), )), + "network.application.http.path" => Ok(Box::new( + inner + .core + .network + .application + .http + .path + .to_string_lossy() + .to_string(), + )), + "network.application.http.url" => Ok(Box::new( + inner + .core + .network + .application + .http + .url + .as_ref() + .map(|a| a.urlstring.clone()), + )), "network.protocol.udp.enabled" => { Ok(Box::new(inner.core.network.protocol.udp.enabled)) } @@ -751,18 +820,25 @@ impl Settings { "network.protocol.ws.listen_address" => Ok(Box::new( inner.core.network.protocol.ws.listen_address.name.clone(), )), - "network.protocol.ws.path" => { - Ok(Box::new(inner.core.network.protocol.ws.path.clone())) - } - "network.protocol.ws.public_address" => Ok(Box::new( + "network.protocol.ws.path" => Ok(Box::new( inner .core .network .protocol .ws - .public_address + .path + .to_string_lossy() + .to_string(), + )), + "network.protocol.ws.url" => Ok(Box::new( + inner + .core + .network + .protocol + .ws + .url .as_ref() - .map(|a| a.name.clone()), + .map(|a| a.urlstring.clone()), )), "network.protocol.wss.connect" => { Ok(Box::new(inner.core.network.protocol.wss.connect)) @@ -776,19 +852,19 @@ impl Settings { "network.protocol.wss.listen_address" => Ok(Box::new( inner.core.network.protocol.wss.listen_address.name.clone(), )), - "network.protocol.wss.path" => { - Ok(Box::new(inner.core.network.protocol.wss.path.clone())) - } - "network.protocol.wss.public_address" => Ok(Box::new( + "network.protocol.wss.path" => Ok(Box::new( inner .core .network .protocol .wss - .public_address - .as_ref() - .map(|a| a.name.clone()), + .path + .to_string_lossy() + .to_string(), )), + "network.protocol.wss.url" => { + Ok(Box::new(inner.core.network.protocol.wss.url.clone())) + } "network.leases.max_server_signal_leases" => { Ok(Box::new(inner.core.network.leases.max_server_signal_leases)) } @@ -899,11 +975,7 @@ mod tests { ); assert_eq!(s.core.network.tls.connection_initial_timeout, 2_000_000u64); // - assert_eq!( - s.core.network.application.path, - std::path::PathBuf::from("app") - ); - assert_eq!(s.core.network.application.https.enabled, true); + assert_eq!(s.core.network.application.https.enabled, false); assert_eq!( s.core.network.application.https.listen_address.name, "[::]:5150" @@ -915,7 +987,12 @@ mod tests { .unwrap() .collect::>() ); - assert_eq!(s.core.network.application.http.enabled, true); + assert_eq!( + s.core.network.application.https.path, + std::path::PathBuf::from("app") + ); + assert_eq!(s.core.network.application.https.url, None); + assert_eq!(s.core.network.application.http.enabled, false); assert_eq!( s.core.network.application.http.listen_address.name, "[::]:5150" @@ -927,6 +1004,11 @@ mod tests { .unwrap() .collect::>() ); + assert_eq!( + s.core.network.application.http.path, + std::path::PathBuf::from("app") + ); + assert_eq!(s.core.network.application.http.url, None); // assert_eq!(s.core.network.protocol.udp.enabled, true); assert_eq!(s.core.network.protocol.udp.socket_pool_size, 0); @@ -966,11 +1048,14 @@ mod tests { .unwrap() .collect::>() ); - assert_eq!(s.core.network.protocol.ws.path, "/ws"); - assert_eq!(s.core.network.protocol.ws.public_address, None); + assert_eq!( + s.core.network.protocol.ws.path, + std::path::PathBuf::from("ws") + ); + assert_eq!(s.core.network.protocol.ws.url, None); // assert_eq!(s.core.network.protocol.wss.connect, true); - assert_eq!(s.core.network.protocol.wss.listen, true); + assert_eq!(s.core.network.protocol.wss.listen, false); assert_eq!(s.core.network.protocol.wss.max_connections, 16); assert_eq!(s.core.network.protocol.wss.listen_address.name, "[::]:5150"); assert_eq!( @@ -980,9 +1065,12 @@ mod tests { .unwrap() .collect::>() ); - assert_eq!(s.core.network.protocol.wss.path, "/ws"); - assert_eq!(s.core.network.protocol.wss.public_address, None); - + assert_eq!( + s.core.network.protocol.wss.path, + std::path::PathBuf::from("ws") + ); + assert_eq!(s.core.network.protocol.wss.url, None); + // assert_eq!(s.core.network.leases.max_server_signal_leases, 256); assert_eq!(s.core.network.leases.max_server_relay_leases, 8); assert_eq!(s.core.network.leases.max_client_signal_leases, 2); diff --git a/veilid-server/src/unix.rs b/veilid-server/src/unix.rs index f56c3792..1f919011 100644 --- a/veilid-server/src/unix.rs +++ b/veilid-server/src/unix.rs @@ -162,13 +162,12 @@ pub async fn main() -> Result<(), String> { let bootstrap = match matches.value_of("bootstrap") { Some(x) => { println!("Overriding bootstrap with: "); - let mut out: Vec = Vec::new(); + let mut out: Vec = Vec::new(); for x in x.split(',') { println!(" {}", x); - out.push( - settings::ParsedURL::from_str(x) - .map_err(|e| format!("unable to parse url in bootstrap list: {}", e))?, - ); + out.push(settings::ParsedUrl::from_str(x).map_err(|e| { + format!("unable to parse url in bootstrap list: {} for {}", e, x) + })?); } out }