diff --git a/CHANGELOG.md b/CHANGELOG.md index 0125fcd..4d56929 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,17 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.20.0-alpha.13] + ### Added - Support for `--shell` with optional path to an explicit shell as an option when executing `distant spawn` in order to run the command within a shell rather than directly +- `semver` crate to be used for version information in protocol and manager +- `is_compatible_with` function to root of `distant-protocol` crate that checks + if a provided version is compatible with the protocol ### Changed - `distant_protocol::PROTOCOL_VERSION` now uses the crate's major, minor, and patch version at compile-time (parsed via `const-str` crate) to streamline version handling between crate and protocol +- Protocol and manager now supply a version request instead of capabilities and + the capabilities of protocol are now a `Vec` to contain a set of more + broad capabilities instead of every possible request type ### Fixed @@ -30,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `Cmd::program` and `Cmd::arguments` functions as they were misleading (didn't do what `distant-local` or `distant-ssh2` do) +- Removed `Capability` and `Capabilities` from protocol and manager ## [0.20.0-alpha.12] @@ -600,7 +609,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 pending upon full channel and no longer locks up - stdout, stderr, and stdin of `RemoteProcess` no longer cause deadlock -[Unreleased]: https://github.com/chipsenkbeil/distant/compare/v0.20.0-alpha.12...HEAD +[Unreleased]: https://github.com/chipsenkbeil/distant/compare/v0.20.0-alpha.13...HEAD +[0.20.0-alpha.13]: https://github.com/chipsenkbeil/distant/compare/v0.20.0-alpha.12...v0.20.0-alpha.13 [0.20.0-alpha.12]: https://github.com/chipsenkbeil/distant/compare/v0.20.0-alpha.11...v0.20.0-alpha.12 [0.20.0-alpha.11]: https://github.com/chipsenkbeil/distant/compare/v0.20.0-alpha.10...v0.20.0-alpha.11 [0.20.0-alpha.10]: https://github.com/chipsenkbeil/distant/compare/v0.20.0-alpha.9...v0.20.0-alpha.10 diff --git a/Cargo.lock b/Cargo.lock index 3fd371b..38c5dbc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -813,7 +813,7 @@ dependencies = [ [[package]] name = "distant" -version = "0.20.0-alpha.12" +version = "0.20.0-alpha.13" dependencies = [ "anyhow", "assert_cmd", @@ -859,7 +859,7 @@ dependencies = [ [[package]] name = "distant-auth" -version = "0.20.0-alpha.12" +version = "0.20.0-alpha.13" dependencies = [ "async-trait", "derive_more", @@ -872,7 +872,7 @@ dependencies = [ [[package]] name = "distant-core" -version = "0.20.0-alpha.12" +version = "0.20.0-alpha.13" dependencies = [ "async-trait", "bitflags 2.3.1", @@ -898,7 +898,7 @@ dependencies = [ [[package]] name = "distant-local" -version = "0.20.0-alpha.12" +version = "0.20.0-alpha.13" dependencies = [ "assert_fs", "async-trait", @@ -926,11 +926,12 @@ dependencies = [ [[package]] name = "distant-net" -version = "0.20.0-alpha.12" +version = "0.20.0-alpha.13" dependencies = [ "async-trait", "bytes", "chacha20poly1305", + "const-str", "derive_more", "distant-auth", "dyn-clone", @@ -944,6 +945,7 @@ dependencies = [ "rand", "rmp", "rmp-serde", + "semver 1.0.17", "serde", "serde_bytes", "serde_json", @@ -956,7 +958,7 @@ dependencies = [ [[package]] name = "distant-protocol" -version = "0.20.0-alpha.12" +version = "0.20.0-alpha.13" dependencies = [ "bitflags 2.3.1", "const-str", @@ -964,6 +966,7 @@ dependencies = [ "regex", "rmp", "rmp-serde", + "semver 1.0.17", "serde", "serde_bytes", "serde_json", @@ -972,7 +975,7 @@ dependencies = [ [[package]] name = "distant-ssh2" -version = "0.20.0-alpha.12" +version = "0.20.0-alpha.13" dependencies = [ "anyhow", "assert_fs", @@ -2776,6 +2779,9 @@ name = "semver" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" +dependencies = [ + "serde", +] [[package]] name = "semver-parser" diff --git a/Cargo.toml b/Cargo.toml index 6a2943a..f05814d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "distant" description = "Operate on a remote computer through file and process manipulation" categories = ["command-line-utilities"] keywords = ["cli"] -version = "0.20.0-alpha.12" +version = "0.20.0-alpha.13" authors = ["Chip Senkbeil "] edition = "2021" homepage = "https://github.com/chipsenkbeil/distant" @@ -40,8 +40,8 @@ clap_complete = "4.3.0" config = { version = "0.13.3", default-features = false, features = ["toml"] } derive_more = { version = "0.99.17", default-features = false, features = ["display", "from", "error", "is_variant"] } dialoguer = { version = "0.10.4", default-features = false } -distant-core = { version = "=0.20.0-alpha.12", path = "distant-core" } -distant-local = { version = "=0.20.0-alpha.12", path = "distant-local" } +distant-core = { version = "=0.20.0-alpha.13", path = "distant-core" } +distant-local = { version = "=0.20.0-alpha.13", path = "distant-local" } directories = "5.0.1" file-mode = "0.1.2" flexi_logger = "0.25.5" @@ -65,7 +65,7 @@ winsplit = "0.1.0" whoami = "1.4.0" # Optional native SSH functionality -distant-ssh2 = { version = "=0.20.0-alpha.12", path = "distant-ssh2", default-features = false, features = ["serde"], optional = true } +distant-ssh2 = { version = "=0.20.0-alpha.13", path = "distant-ssh2", default-features = false, features = ["serde"], optional = true } [target.'cfg(unix)'.dependencies] fork = "0.1.21" diff --git a/distant-auth/Cargo.toml b/distant-auth/Cargo.toml index 85d4778..291042e 100644 --- a/distant-auth/Cargo.toml +++ b/distant-auth/Cargo.toml @@ -3,7 +3,7 @@ name = "distant-auth" description = "Authentication library for distant, providing various implementations" categories = ["authentication"] keywords = ["auth", "authentication", "async"] -version = "0.20.0-alpha.12" +version = "0.20.0-alpha.13" authors = ["Chip Senkbeil "] edition = "2021" homepage = "https://github.com/chipsenkbeil/distant" diff --git a/distant-core/Cargo.toml b/distant-core/Cargo.toml index 5368329..568abbc 100644 --- a/distant-core/Cargo.toml +++ b/distant-core/Cargo.toml @@ -3,7 +3,7 @@ name = "distant-core" description = "Core library for distant, enabling operation on a remote computer through file and process manipulation" categories = ["network-programming"] keywords = ["api", "async"] -version = "0.20.0-alpha.12" +version = "0.20.0-alpha.13" authors = ["Chip Senkbeil "] edition = "2021" homepage = "https://github.com/chipsenkbeil/distant" @@ -16,8 +16,8 @@ async-trait = "0.1.68" bitflags = "2.3.1" bytes = "1.4.0" derive_more = { version = "0.99.17", default-features = false, features = ["as_mut", "as_ref", "deref", "deref_mut", "display", "from", "error", "into", "into_iterator", "is_variant", "try_into"] } -distant-net = { version = "=0.20.0-alpha.12", path = "../distant-net" } -distant-protocol = { version = "=0.20.0-alpha.12", path = "../distant-protocol" } +distant-net = { version = "=0.20.0-alpha.13", path = "../distant-net" } +distant-protocol = { version = "=0.20.0-alpha.13", path = "../distant-protocol" } futures = "0.3.28" hex = "0.4.3" log = "0.4.18" diff --git a/distant-core/src/client/ext.rs b/distant-core/src/client/ext.rs index f65f0f0..bedbc64 100644 --- a/distant-core/src/client/ext.rs +++ b/distant-core/src/client/ext.rs @@ -44,8 +44,12 @@ pub trait DistantChannelExt { /// Creates a remote directory, optionally creating all parent components if specified fn create_dir(&mut self, path: impl Into, all: bool) -> AsyncReturn<'_, ()>; + /// Checks whether the `path` exists on the remote machine fn exists(&mut self, path: impl Into) -> AsyncReturn<'_, bool>; + /// Checks whether this client is compatible with the remote server + fn is_compatible(&mut self) -> AsyncReturn<'_, bool>; + /// Retrieves metadata about a path on a remote machine fn metadata( &mut self, @@ -136,6 +140,9 @@ pub trait DistantChannelExt { /// Retrieves server version information fn version(&mut self) -> AsyncReturn<'_, Version>; + /// Returns version of protocol that the client uses + fn protocol_version(&self) -> protocol::semver::Version; + /// Writes a remote file with the data from a collection of bytes fn write_file( &mut self, @@ -232,6 +239,15 @@ impl DistantChannelExt ) } + fn is_compatible(&mut self) -> AsyncReturn<'_, bool> { + make_body!(self, protocol::Request::Version {}, |data| match data { + protocol::Response::Version(version) => + Ok(protocol::is_compatible_with(&version.protocol_version)), + protocol::Response::Error(x) => Err(io::Error::from(x)), + _ => Err(mismatched_response()), + }) + } + fn metadata( &mut self, path: impl Into, @@ -453,6 +469,10 @@ impl DistantChannelExt }) } + fn protocol_version(&self) -> protocol::semver::Version { + protocol::PROTOCOL_VERSION + } + fn write_file( &mut self, path: impl Into, diff --git a/distant-core/tests/api_tests.rs b/distant-core/tests/api_tests.rs index 516b3f3..7b81fdb 100644 --- a/distant-core/tests/api_tests.rs +++ b/distant-core/tests/api_tests.rs @@ -7,8 +7,9 @@ use distant_core::{ }; use distant_net::auth::{DummyAuthHandler, Verifier}; use distant_net::client::Client; -use distant_net::common::{InmemoryTransport, OneshotListener}; +use distant_net::common::{InmemoryTransport, OneshotListener, Version}; use distant_net::server::{Server, ServerRef}; +use distant_protocol::PROTOCOL_VERSION; /// Stands up an inmemory client and server using the given api. async fn setup(api: impl DistantApi + Send + Sync + 'static) -> (DistantClient, ServerRef) { @@ -17,12 +18,22 @@ async fn setup(api: impl DistantApi + Send + Sync + 'static) -> (DistantClient, let server = Server::new() .handler(DistantApiServerHandler::new(api)) .verifier(Verifier::none()) + .version(Version::new( + PROTOCOL_VERSION.major, + PROTOCOL_VERSION.minor, + PROTOCOL_VERSION.patch, + )) .start(OneshotListener::from_value(t2)) .expect("Failed to start server"); let client: DistantClient = Client::build() .auth_handler(DummyAuthHandler) .connector(t1) + .version(Version::new( + PROTOCOL_VERSION.major, + PROTOCOL_VERSION.minor, + PROTOCOL_VERSION.patch, + )) .connect() .await .expect("Failed to connect to server"); diff --git a/distant-local/Cargo.toml b/distant-local/Cargo.toml index e4f9946..a66ac30 100644 --- a/distant-local/Cargo.toml +++ b/distant-local/Cargo.toml @@ -2,7 +2,7 @@ name = "distant-local" description = "Library implementing distant API for local interactions" categories = ["network-programming"] -version = "0.20.0-alpha.12" +version = "0.20.0-alpha.13" authors = ["Chip Senkbeil "] edition = "2021" homepage = "https://github.com/chipsenkbeil/distant" @@ -21,7 +21,7 @@ macos-kqueue = ["notify/macos_kqueue"] [dependencies] async-trait = "0.1.68" -distant-core = { version = "=0.20.0-alpha.12", path = "../distant-core" } +distant-core = { version = "=0.20.0-alpha.13", path = "../distant-core" } grep = "0.2.12" ignore = "0.4.20" log = "0.4.18" diff --git a/distant-local/src/api.rs b/distant-local/src/api.rs index fd589bc..a0560e0 100644 --- a/distant-local/src/api.rs +++ b/distant-local/src/api.rs @@ -3,10 +3,10 @@ use std::time::SystemTime; use std::{env, io}; use async_trait::async_trait; +use distant_core::protocol::semver; use distant_core::protocol::{ - Capabilities, ChangeKind, ChangeKindSet, DirEntry, Environment, FileType, Metadata, - Permissions, ProcessId, PtySize, SearchId, SearchQuery, SetPermissionsOptions, SystemInfo, - Version, PROTOCOL_VERSION, + ChangeKind, ChangeKindSet, DirEntry, Environment, FileType, Metadata, Permissions, ProcessId, + PtySize, SearchId, SearchQuery, SetPermissionsOptions, SystemInfo, Version, PROTOCOL_VERSION, }; use distant_core::{DistantApi, DistantCtx}; use ignore::{DirEntry as WalkDirEntry, WalkBuilder}; @@ -635,10 +635,32 @@ impl DistantApi for Api { async fn version(&self, ctx: DistantCtx) -> io::Result { debug!("[Conn {}] Querying version", ctx.connection_id); + // Parse our server's version + let mut server_version: semver::Version = env!("CARGO_PKG_VERSION") + .parse() + .map_err(|x| io::Error::new(io::ErrorKind::Other, x))?; + + // Add the package name to the version information + if server_version.build.is_empty() { + server_version.build = semver::BuildMetadata::new(env!("CARGO_PKG_NAME")) + .map_err(|x| io::Error::new(io::ErrorKind::Other, x))?; + } else { + let raw_build_str = format!( + "{}.{}", + server_version.build.as_str(), + env!("CARGO_PKG_NAME") + ); + server_version.build = semver::BuildMetadata::new(&raw_build_str) + .map_err(|x| io::Error::new(io::ErrorKind::Other, x))?; + } + Ok(Version { - server_version: format!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")), + server_version, protocol_version: PROTOCOL_VERSION, - capabilities: Capabilities::all(), + capabilities: Version::capabilities() + .iter() + .map(ToString::to_string) + .collect(), }) } } diff --git a/distant-net/Cargo.toml b/distant-net/Cargo.toml index cf2ed2b..d5e0662 100644 --- a/distant-net/Cargo.toml +++ b/distant-net/Cargo.toml @@ -3,7 +3,7 @@ name = "distant-net" description = "Network library for distant, providing implementations to support client/server architecture" categories = ["network-programming"] keywords = ["api", "async"] -version = "0.20.0-alpha.12" +version = "0.20.0-alpha.13" authors = ["Chip Senkbeil "] edition = "2021" homepage = "https://github.com/chipsenkbeil/distant" @@ -15,8 +15,9 @@ license = "MIT OR Apache-2.0" async-trait = "0.1.68" bytes = "1.4.0" chacha20poly1305 = "0.10.1" +const-str = "0.5.6" derive_more = { version = "0.99.17", default-features = false, features = ["as_mut", "as_ref", "deref", "deref_mut", "display", "from", "error", "into", "into_iterator", "is_variant", "try_into"] } -distant-auth = { version = "=0.20.0-alpha.12", path = "../distant-auth" } +distant-auth = { version = "=0.20.0-alpha.13", path = "../distant-auth" } dyn-clone = "1.0.11" flate2 = "1.0.26" hex = "0.4.3" @@ -28,6 +29,7 @@ rand = { version = "0.8.5", features = ["getrandom"] } rmp = "0.8.11" rmp-serde = "1.1.1" sha2 = "0.10.6" +semver = { version = "1.0.17", features = ["serde"] } serde = { version = "1.0.163", features = ["derive"] } serde_bytes = "0.11.9" serde_json = "1.0.96" @@ -35,7 +37,7 @@ strum = { version = "0.24.1", features = ["derive"] } tokio = { version = "1.28.2", features = ["full"] } [dev-dependencies] -distant-auth = { version = "=0.20.0-alpha.12", path = "../distant-auth", features = ["tests"] } +distant-auth = { version = "=0.20.0-alpha.13", path = "../distant-auth", features = ["tests"] } env_logger = "0.10.0" serde_json = "1.0.96" tempfile = "3.5.0" diff --git a/distant-net/src/client/builder.rs b/distant-net/src/client/builder.rs index 10d64b1..9e02495 100644 --- a/distant-net/src/client/builder.rs +++ b/distant-net/src/client/builder.rs @@ -20,7 +20,7 @@ pub use windows::*; use super::ClientConfig; use crate::client::{Client, UntypedClient}; -use crate::common::{Connection, Transport}; +use crate::common::{Connection, Transport, Version}; /// Interface that performs the connection to produce a [`Transport`] for use by the [`Client`]. #[async_trait] @@ -46,6 +46,7 @@ pub struct ClientBuilder { connector: C, config: ClientConfig, connect_timeout: Option, + version: Version, } impl ClientBuilder { @@ -56,6 +57,7 @@ impl ClientBuilder { config: self.config, connector: self.connector, connect_timeout: self.connect_timeout, + version: self.version, } } @@ -66,6 +68,7 @@ impl ClientBuilder { config, connector: self.connector, connect_timeout: self.connect_timeout, + version: self.version, } } @@ -76,6 +79,7 @@ impl ClientBuilder { config: self.config, connector, connect_timeout: self.connect_timeout, + version: self.version, } } @@ -86,6 +90,18 @@ impl ClientBuilder { config: self.config, connector: self.connector, connect_timeout: connect_timeout.into(), + version: self.version, + } + } + + /// Configure the version of the client. + pub fn version(self, version: Version) -> Self { + Self { + auth_handler: self.auth_handler, + config: self.config, + connector: self.connector, + connect_timeout: self.connect_timeout, + version, } } } @@ -97,6 +113,7 @@ impl ClientBuilder<(), ()> { config: Default::default(), connector: (), connect_timeout: None, + version: Default::default(), } } } @@ -119,6 +136,7 @@ where let auth_handler = self.auth_handler; let config = self.config; let connect_timeout = self.connect_timeout; + let version = self.version; let f = async move { let transport = match connect_timeout { @@ -128,7 +146,7 @@ where .and_then(convert::identity)?, None => self.connector.connect().await?, }; - let connection = Connection::client(transport, auth_handler).await?; + let connection = Connection::client(transport, auth_handler, version).await?; Ok(UntypedClient::spawn(connection, config)) }; diff --git a/distant-net/src/common.rs b/distant-net/src/common.rs index 5f793c8..2f24ba4 100644 --- a/distant-net/src/common.rs +++ b/distant-net/src/common.rs @@ -9,6 +9,7 @@ mod packet; mod port; mod transport; pub(crate) mod utils; +mod version; pub use any::*; pub(crate) use connection::Connection; @@ -21,3 +22,4 @@ pub use map::*; pub use packet::*; pub use port::*; pub use transport::*; +pub use version::*; diff --git a/distant-net/src/common/connection.rs b/distant-net/src/common/connection.rs index acb434b..ef6efe4 100644 --- a/distant-net/src/common/connection.rs +++ b/distant-net/src/common/connection.rs @@ -11,6 +11,7 @@ use tokio::sync::oneshot; use crate::common::InmemoryTransport; use crate::common::{ Backup, FramedTransport, HeapSecretKey, Keychain, KeychainResult, Reconnectable, Transport, + TransportExt, Version, }; /// Id of the connection @@ -110,6 +111,19 @@ where debug!("[Conn {id}] Re-establishing connection"); Reconnectable::reconnect(transport).await?; + // Wait for exactly version bytes (24 where 8 bytes for major, minor, patch) + // but with a reconnect we don't actually validate it because we did that + // the first time we connected + // + // NOTE: We do this with the raw transport and not the framed version! + debug!("[Conn {id}] Waiting for server version"); + if transport.as_mut_inner().read_exact(&mut [0u8; 24]).await? != 24 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "Wrong version byte len received", + )); + } + // Perform a handshake to ensure that the connection is properly established and encrypted debug!("[Conn {id}] Performing handshake"); transport.client_handshake().await?; @@ -190,13 +204,42 @@ where /// Transforms a raw [`Transport`] into an established [`Connection`] from the client-side by /// performing the following: /// - /// 1. Handshakes to derive the appropriate [`Codec`](crate::Codec) to use - /// 2. Authenticates the established connection to ensure it is valid - /// 3. Restores pre-existing state using the provided backup, replaying any missing frames and + /// 1. Performs a version check with the server + /// 2. Handshakes to derive the appropriate [`Codec`](crate::Codec) to use + /// 3. Authenticates the established connection to ensure it is valid + /// 4. Restores pre-existing state using the provided backup, replaying any missing frames and /// receiving any frames from the other side - pub async fn client(transport: T, handler: H) -> io::Result { + pub async fn client( + transport: T, + handler: H, + version: Version, + ) -> io::Result { let id: ConnectionId = rand::random(); + // Wait for exactly version bytes (24 where 8 bytes for major, minor, patch) + debug!("[Conn {id}] Waiting for server version"); + let mut version_bytes = [0u8; 24]; + if transport.read_exact(&mut version_bytes).await? != 24 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "Wrong version byte len received", + )); + } + + // Compare versions for compatibility and drop the connection if incompatible + let server_version = Version::from_be_bytes(version_bytes); + debug!( + "[Conn {id}] Checking compatibility between client {version} & server {server_version}" + ); + if !version.is_compatible_with(&server_version) { + return Err(io::Error::new( + io::ErrorKind::Other, + format!( + "Client version {version} is incompatible with server version {server_version}" + ), + )); + } + // Perform a handshake to ensure that the connection is properly established and encrypted debug!("[Conn {id}] Performing handshake"); let mut transport: FramedTransport = @@ -238,19 +281,25 @@ where /// Transforms a raw [`Transport`] into an established [`Connection`] from the server-side by /// performing the following: /// - /// 1. Handshakes to derive the appropriate [`Codec`](crate::Codec) to use - /// 2. Authenticates the established connection to ensure it is valid by either using the + /// 1. Performs a version check with the client + /// 2. Handshakes to derive the appropriate [`Codec`](crate::Codec) to use + /// 3. Authenticates the established connection to ensure it is valid by either using the /// given `verifier` or, if working with an existing client connection, will validate an OTP /// from our database - /// 3. Restores pre-existing state using the provided backup, replaying any missing frames and + /// 4. Restores pre-existing state using the provided backup, replaying any missing frames and /// receiving any frames from the other side pub async fn server( transport: T, verifier: &Verifier, keychain: Keychain>, + version: Version, ) -> io::Result { let id: ConnectionId = rand::random(); + // Write the version as bytes + debug!("[Conn {id}] Sending version {version}"); + transport.write_all(&version.to_be_bytes()).await?; + // Perform a handshake to ensure that the connection is properly established and encrypted debug!("[Conn {id}] Performing handshake"); let mut transport: FramedTransport = @@ -464,6 +513,60 @@ mod tests { use super::*; use crate::common::Frame; + macro_rules! server_version { + () => { + Version::new(1, 2, 3) + }; + } + + macro_rules! send_server_version { + ($transport:expr, $version:expr) => {{ + ($transport) + .as_mut_inner() + .write_all(&$version.to_be_bytes()) + .await + .unwrap(); + }}; + ($transport:expr) => { + send_server_version!($transport, server_version!()); + }; + } + + macro_rules! receive_version { + ($transport:expr) => {{ + let mut bytes = [0u8; 24]; + assert_eq!( + ($transport) + .as_mut_inner() + .read_exact(&mut bytes) + .await + .unwrap(), + 24, + "Wrong version len received" + ); + Version::from_be_bytes(bytes) + }}; + } + + #[test(tokio::test)] + async fn client_should_fail_when_server_sends_incompatible_version() { + let (mut t1, t2) = FramedTransport::pair(100); + + // Spawn a task to perform the client connection so we don't deadlock while simulating the + // server actions on the other side + let task = tokio::spawn(async move { + Connection::client(t2.into_inner(), DummyAuthHandler, Version::new(1, 2, 3)) + .await + .unwrap() + }); + + // Send invalid version to fail the handshake + send_server_version!(t1, Version::new(2, 0, 0)); + + // Client should fail + task.await.unwrap_err(); + } + #[test(tokio::test)] async fn client_should_fail_if_codec_handshake_fails() { let (mut t1, t2) = FramedTransport::pair(100); @@ -471,11 +574,14 @@ mod tests { // Spawn a task to perform the client connection so we don't deadlock while simulating the // server actions on the other side let task = tokio::spawn(async move { - Connection::client(t2.into_inner(), DummyAuthHandler) + Connection::client(t2.into_inner(), DummyAuthHandler, server_version!()) .await .unwrap() }); + // Send server version for client to confirm + send_server_version!(t1); + // Send garbage to fail the handshake t1.write_frame(Frame::new(b"invalid")).await.unwrap(); @@ -490,11 +596,14 @@ mod tests { // Spawn a task to perform the client connection so we don't deadlock while simulating the // server actions on the other side let task = tokio::spawn(async move { - Connection::client(t2.into_inner(), DummyAuthHandler) + Connection::client(t2.into_inner(), DummyAuthHandler, server_version!()) .await .unwrap() }); + // Send server version for client to confirm + send_server_version!(t1); + // Perform first step of connection by establishing the codec t1.server_handshake().await.unwrap(); @@ -519,11 +628,14 @@ mod tests { // Spawn a task to perform the client connection so we don't deadlock while simulating the // server actions on the other side let task = tokio::spawn(async move { - Connection::client(t2.into_inner(), DummyAuthHandler) + Connection::client(t2.into_inner(), DummyAuthHandler, server_version!()) .await .unwrap() }); + // Send server version for client to confirm + send_server_version!(t1); + // Perform first step of connection by establishing the codec t1.server_handshake().await.unwrap(); @@ -559,11 +671,14 @@ mod tests { // Spawn a task to perform the client connection so we don't deadlock while simulating the // server actions on the other side let task = tokio::spawn(async move { - Connection::client(t2.into_inner(), DummyAuthHandler) + Connection::client(t2.into_inner(), DummyAuthHandler, server_version!()) .await .unwrap() }); + // Send server version for client to confirm + send_server_version!(t1); + // Perform first step of connection by establishing the codec t1.server_handshake().await.unwrap(); @@ -597,11 +712,14 @@ mod tests { // Spawn a task to perform the client connection so we don't deadlock while simulating the // server actions on the other side let task = tokio::spawn(async move { - Connection::client(t2.into_inner(), DummyAuthHandler) + Connection::client(t2.into_inner(), DummyAuthHandler, server_version!()) .await .unwrap() }); + // Send server version for client to confirm + send_server_version!(t1); + // Perform first step of connection by establishing the codec t1.server_handshake().await.unwrap(); @@ -629,6 +747,30 @@ mod tests { assert_eq!(client.otp(), Some(&otp)); } + #[test(tokio::test)] + async fn server_should_fail_if_client_drops_due_to_version() { + let (mut t1, t2) = FramedTransport::pair(100); + let verifier = Verifier::none(); + let keychain = Keychain::new(); + + // Spawn a task to perform the server connection so we don't deadlock while simulating the + // client actions on the other side + let task = tokio::spawn(async move { + Connection::server(t2.into_inner(), &verifier, keychain, server_version!()) + .await + .unwrap() + }); + + // Receive the version from the server + let _ = receive_version!(t1); + + // Drop client connection as a result of an "incompatible version" + drop(t1); + + // Server should fail + task.await.unwrap_err(); + } + #[test(tokio::test)] async fn server_should_fail_if_codec_handshake_fails() { let (mut t1, t2) = FramedTransport::pair(100); @@ -638,11 +780,14 @@ mod tests { // Spawn a task to perform the server connection so we don't deadlock while simulating the // client actions on the other side let task = tokio::spawn(async move { - Connection::server(t2.into_inner(), &verifier, keychain) + Connection::server(t2.into_inner(), &verifier, keychain, server_version!()) .await .unwrap() }); + // Receive the version from the server + let _ = receive_version!(t1); + // Send garbage to fail the handshake t1.write_frame(Frame::new(b"invalid")).await.unwrap(); @@ -659,11 +804,14 @@ mod tests { // Spawn a task to perform the server connection so we don't deadlock while simulating the // client actions on the other side let task = tokio::spawn(async move { - Connection::server(t2.into_inner(), &verifier, keychain) + Connection::server(t2.into_inner(), &verifier, keychain, server_version!()) .await .unwrap() }); + // Receive the version from the server + let _ = receive_version!(t1); + // Perform first step of completing client-side of handshake t1.client_handshake().await.unwrap(); @@ -683,11 +831,14 @@ mod tests { // Spawn a task to perform the server connection so we don't deadlock while simulating the // client actions on the other side let task = tokio::spawn(async move { - Connection::server(t2.into_inner(), &verifier, keychain) + Connection::server(t2.into_inner(), &verifier, keychain, server_version!()) .await .unwrap() }); + // Receive the version from the server + let _ = receive_version!(t1); + // Perform first step of completing client-side of handshake t1.client_handshake().await.unwrap(); @@ -717,11 +868,14 @@ mod tests { // Spawn a task to perform the server connection so we don't deadlock while simulating the // client actions on the other side let task = tokio::spawn(async move { - Connection::server(t2.into_inner(), &verifier, keychain) + Connection::server(t2.into_inner(), &verifier, keychain, server_version!()) .await .unwrap() }); + // Receive the version from the server + let _ = receive_version!(t1); + // Perform first step of completing client-side of handshake t1.client_handshake().await.unwrap(); @@ -750,11 +904,14 @@ mod tests { // Spawn a task to perform the server connection so we don't deadlock while simulating the // client actions on the other side let task = tokio::spawn(async move { - Connection::server(t2.into_inner(), &verifier, keychain) + Connection::server(t2.into_inner(), &verifier, keychain, server_version!()) .await .unwrap() }); + // Receive the version from the server + let _ = receive_version!(t1); + // Perform first step of completing client-side of handshake t1.client_handshake().await.unwrap(); @@ -790,11 +947,14 @@ mod tests { // Spawn a task to perform the server connection so we don't deadlock while simulating the // client actions on the other side let task = tokio::spawn(async move { - Connection::server(t2.into_inner(), &verifier, keychain) + Connection::server(t2.into_inner(), &verifier, keychain, server_version!()) .await .unwrap() }); + // Receive the version from the server + let _ = receive_version!(t1); + // Perform first step of completing client-side of handshake t1.client_handshake().await.unwrap(); @@ -828,11 +988,14 @@ mod tests { // Spawn a task to perform the server connection so we don't deadlock while simulating the // client actions on the other side let task = tokio::spawn(async move { - Connection::server(t2.into_inner(), &verifier, keychain) + Connection::server(t2.into_inner(), &verifier, keychain, server_version!()) .await .unwrap() }); + // Receive the version from the server + let _ = receive_version!(t1); + // Perform first step of completing client-side of handshake t1.client_handshake().await.unwrap(); @@ -866,11 +1029,14 @@ mod tests { // Spawn a task to perform the server connection so we don't deadlock while simulating the // client actions on the other side let task = tokio::spawn(async move { - Connection::server(t2.into_inner(), &verifier, keychain) + Connection::server(t2.into_inner(), &verifier, keychain, server_version!()) .await .unwrap() }); + // Receive the version from the server + let _ = receive_version!(t1); + // Perform first step of completing client-side of handshake t1.client_handshake().await.unwrap(); @@ -904,12 +1070,15 @@ mod tests { let task = tokio::spawn({ let keychain = keychain.clone(); async move { - Connection::server(t2.into_inner(), &verifier, keychain) + Connection::server(t2.into_inner(), &verifier, keychain, server_version!()) .await .unwrap() } }); + // Receive the version from the server + let _ = receive_version!(t1); + // Perform first step of completing client-side of handshake t1.client_handshake().await.unwrap(); @@ -969,12 +1138,15 @@ mod tests { let task = tokio::spawn({ let keychain = keychain.clone(); async move { - Connection::server(t2.into_inner(), &verifier, keychain) + Connection::server(t2.into_inner(), &verifier, keychain, server_version!()) .await .unwrap() } }); + // Receive the version from the server + let _ = receive_version!(t1); + // Perform first step of completing client-side of handshake t1.client_handshake().await.unwrap(); @@ -1029,13 +1201,13 @@ mod tests { // Spawn a task to perform the server connection so we don't deadlock let task = tokio::spawn(async move { - Connection::server(t2, &verifier, keychain) + Connection::server(t2, &verifier, keychain, server_version!()) .await .expect("Failed to connect from server") }); // Perform the client-side of the connection - let mut client = Connection::client(t1, DummyAuthHandler) + let mut client = Connection::client(t1, DummyAuthHandler, server_version!()) .await .expect("Failed to connect from client"); let mut server = task.await.unwrap(); @@ -1063,14 +1235,14 @@ mod tests { let verifier = Arc::clone(&verifier); let keychain = keychain.clone(); tokio::spawn(async move { - Connection::server(t2, &verifier, keychain) + Connection::server(t2, &verifier, keychain, server_version!()) .await .expect("Failed to connect from server") }) }; // Perform the client-side of the connection - let mut client = Connection::client(t1, DummyAuthHandler) + let mut client = Connection::client(t1, DummyAuthHandler, server_version!()) .await .expect("Failed to connect from client"); @@ -1093,6 +1265,9 @@ mod tests { // Spawn a task to perform the client reconnection so we don't deadlock let task = tokio::spawn(async move { client.reconnect().await.unwrap() }); + // Send a version, although it'll be ignored by a reconnecting client + send_server_version!(transport); + // Send garbage to fail handshake from server-side transport.write_frame(b"hello").await.unwrap(); @@ -1108,6 +1283,9 @@ mod tests { // Spawn a task to perform the client reconnection so we don't deadlock let task = tokio::spawn(async move { client.reconnect().await.unwrap() }); + // Send a version, although it'll be ignored by a reconnecting client + send_server_version!(transport); + // Perform first step of completing server-side of handshake transport.server_handshake().await.unwrap(); @@ -1126,6 +1304,9 @@ mod tests { // Spawn a task to perform the client reconnection so we don't deadlock let task = tokio::spawn(async move { client.reconnect().await.unwrap() }); + // Send a version, although it'll be ignored by a reconnecting client + send_server_version!(transport); + // Perform first step of completing server-side of handshake transport.server_handshake().await.unwrap(); @@ -1162,6 +1343,9 @@ mod tests { // Spawn a task to perform the client reconnection so we don't deadlock let task = tokio::spawn(async move { client.reconnect().await.unwrap() }); + // Send a version, although it'll be ignored by a reconnecting client + send_server_version!(transport); + // Perform first step of completing server-side of handshake transport.server_handshake().await.unwrap(); @@ -1205,6 +1389,9 @@ mod tests { client }); + // Send a version, although it'll be ignored by a reconnecting client + send_server_version!(transport); + // Perform first step of completing server-side of handshake transport.server_handshake().await.unwrap(); @@ -1275,7 +1462,7 @@ mod tests { // Spawn a task to perform the server reconnection so we don't deadlock let task = tokio::spawn(async move { - Connection::server(transport, &verifier, keychain) + Connection::server(transport, &verifier, keychain, server_version!()) .await .expect("Failed to connect from server") }); diff --git a/distant-net/src/common/transport/framed/codec/plain.rs b/distant-net/src/common/transport/framed/codec/plain.rs index 9a5295a..e1f4786 100644 --- a/distant-net/src/common/transport/framed/codec/plain.rs +++ b/distant-net/src/common/transport/framed/codec/plain.rs @@ -8,7 +8,7 @@ pub struct PlainCodec; impl PlainCodec { pub fn new() -> Self { - Self::default() + Self } } diff --git a/distant-net/src/common/version.rs b/distant-net/src/common/version.rs new file mode 100644 index 0000000..5aa5bbb --- /dev/null +++ b/distant-net/src/common/version.rs @@ -0,0 +1,132 @@ +use semver::{Comparator, Op, Prerelease, Version as SemVer}; +use std::fmt; + +/// Represents a version and compatibility rules. +#[derive(Clone, Debug)] +pub struct Version { + inner: SemVer, + lower: Comparator, + upper: Comparator, +} + +impl Version { + /// Creates a new version in the form `major.minor.patch` with a ruleset that is used to check + /// other versions such that `>=0.1.2, <0.2.0` or `>=1.2.3, <2` depending on whether or not the + /// major version is `0`. + /// + /// ``` + /// use distant_net::common::Version; + /// + /// // Matching versions are compatible + /// let a = Version::new(1, 2, 3); + /// let b = Version::new(1, 2, 3); + /// assert!(a.is_compatible_with(&b)); + /// + /// // Version 1.2.3 is compatible with 1.2.4, but not the other way + /// let a = Version::new(1, 2, 3); + /// let b = Version::new(1, 2, 4); + /// assert!(a.is_compatible_with(&b)); + /// assert!(!b.is_compatible_with(&a)); + /// + /// // Version 1.2.3 is compatible with 1.3.0, but not 2 + /// let a = Version::new(1, 2, 3); + /// assert!(a.is_compatible_with(&Version::new(1, 3, 0))); + /// assert!(!a.is_compatible_with(&Version::new(2, 0, 0))); + /// + /// // Version 0.1.2 is compatible with 0.1.3, but not the other way + /// let a = Version::new(0, 1, 2); + /// let b = Version::new(0, 1, 3); + /// assert!(a.is_compatible_with(&b)); + /// assert!(!b.is_compatible_with(&a)); + /// + /// // Version 0.1.2 is not compatible with 0.2 + /// let a = Version::new(0, 1, 2); + /// let b = Version::new(0, 2, 0); + /// assert!(!a.is_compatible_with(&b)); + /// assert!(!b.is_compatible_with(&a)); + /// ``` + pub const fn new(major: u64, minor: u64, patch: u64) -> Self { + Self { + inner: SemVer::new(major, minor, patch), + lower: Comparator { + op: Op::GreaterEq, + major, + minor: Some(minor), + patch: Some(patch), + pre: Prerelease::EMPTY, + }, + upper: Comparator { + op: Op::Less, + major: if major == 0 { 0 } else { major + 1 }, + minor: if major == 0 { Some(minor + 1) } else { None }, + patch: None, + pre: Prerelease::EMPTY, + }, + } + } + + /// Returns true if this version is compatible with another version. + pub fn is_compatible_with(&self, other: &Self) -> bool { + self.lower.matches(&other.inner) && self.upper.matches(&other.inner) + } + + /// Converts from a collection of bytes into a version using the byte form major/minor/patch + /// using big endian. + pub const fn from_be_bytes(bytes: [u8; 24]) -> Self { + Self::new( + u64::from_be_bytes([ + bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], + ]), + u64::from_be_bytes([ + bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], + bytes[15], + ]), + u64::from_be_bytes([ + bytes[16], bytes[17], bytes[18], bytes[19], bytes[20], bytes[21], bytes[22], + bytes[23], + ]), + ) + } + + /// Converts the version into a byte form of major/minor/patch using big endian. + pub const fn to_be_bytes(&self) -> [u8; 24] { + let major = self.inner.major.to_be_bytes(); + let minor = self.inner.minor.to_be_bytes(); + let patch = self.inner.patch.to_be_bytes(); + + [ + major[0], major[1], major[2], major[3], major[4], major[5], major[6], major[7], + minor[0], minor[1], minor[2], minor[3], minor[4], minor[5], minor[6], minor[7], + patch[0], patch[1], patch[2], patch[3], patch[4], patch[5], patch[6], patch[7], + ] + } +} + +impl Default for Version { + /// Default version is `0.0.0`. + fn default() -> Self { + Self::new(0, 0, 0) + } +} + +impl fmt::Display for Version { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.inner) + } +} + +impl From for Version { + /// Creates a new [`Version`] using the major, minor, and patch information from + /// [`semver::Version`]. + fn from(version: semver::Version) -> Self { + let mut this = Self::new(version.major, version.minor, version.patch); + this.inner = version; + this + } +} + +impl From for semver::Version { + fn from(version: Version) -> Self { + version.inner + } +} diff --git a/distant-net/src/manager.rs b/distant-net/src/manager.rs index 6a9bf19..0201d71 100644 --- a/distant-net/src/manager.rs +++ b/distant-net/src/manager.rs @@ -5,3 +5,12 @@ mod server; pub use client::*; pub use data::*; pub use server::*; + +use crate::common::Version; + +/// Represents the version associated with the manager's protocol. +pub const PROTOCOL_VERSION: Version = Version::new( + const_str::parse!(env!("CARGO_PKG_VERSION_MAJOR"), u64), + const_str::parse!(env!("CARGO_PKG_VERSION_MINOR"), u64), + const_str::parse!(env!("CARGO_PKG_VERSION_PATCH"), u64), +); diff --git a/distant-net/src/manager/client.rs b/distant-net/src/manager/client.rs index fe3d038..f20c700 100644 --- a/distant-net/src/manager/client.rs +++ b/distant-net/src/manager/client.rs @@ -7,7 +7,7 @@ use log::*; use crate::client::Client; use crate::common::{ConnectionId, Destination, Map, Request}; use crate::manager::data::{ - ConnectionInfo, ConnectionList, ManagerCapabilities, ManagerRequest, ManagerResponse, + ConnectionInfo, ConnectionList, ManagerRequest, ManagerResponse, SemVer, }; mod channel; @@ -231,12 +231,12 @@ impl ManagerClient { RawChannel::spawn(connection_id, self).await } - /// Retrieves a list of supported capabilities - pub async fn capabilities(&mut self) -> io::Result { - trace!("capabilities()"); - let res = self.send(ManagerRequest::Capabilities).await?; + /// Retrieves the version of the manager. + pub async fn version(&mut self) -> io::Result { + trace!("version()"); + let res = self.send(ManagerRequest::Version).await?; match res.payload { - ManagerResponse::Capabilities { supported } => Ok(supported), + ManagerResponse::Version { version } => Ok(version), ManagerResponse::Error { description } => { Err(io::Error::new(io::ErrorKind::Other, description)) } diff --git a/distant-net/src/manager/data.rs b/distant-net/src/manager/data.rs index 3a9cc80..757c4bb 100644 --- a/distant-net/src/manager/data.rs +++ b/distant-net/src/manager/data.rs @@ -1,8 +1,6 @@ pub type ManagerChannelId = u32; pub type ManagerAuthenticationId = u32; - -mod capabilities; -pub use capabilities::*; +pub use semver::Version as SemVer; mod info; pub use info::*; diff --git a/distant-net/src/manager/data/capabilities.rs b/distant-net/src/manager/data/capabilities.rs deleted file mode 100644 index fdab311..0000000 --- a/distant-net/src/manager/data/capabilities.rs +++ /dev/null @@ -1,189 +0,0 @@ -use std::cmp::Ordering; -use std::collections::HashSet; -use std::hash::{Hash, Hasher}; -use std::ops::{BitAnd, BitOr, BitXor}; -use std::str::FromStr; - -use derive_more::{From, Into, IntoIterator}; -use serde::{Deserialize, Serialize}; -use strum::{EnumMessage, IntoEnumIterator}; - -use super::ManagerCapabilityKind; - -/// Set of supported capabilities for a manager -#[derive(Clone, Debug, From, Into, PartialEq, Eq, IntoIterator, Serialize, Deserialize)] -#[serde(transparent)] -pub struct ManagerCapabilities(#[into_iterator(owned, ref)] HashSet); - -impl ManagerCapabilities { - /// Return set of capabilities encompassing all possible capabilities - pub fn all() -> Self { - Self( - ManagerCapabilityKind::iter() - .map(ManagerCapability::from) - .collect(), - ) - } - - /// Return empty set of capabilities - pub fn none() -> Self { - Self(HashSet::new()) - } - - /// Returns true if the capability with described kind is included - pub fn contains(&self, kind: impl AsRef) -> bool { - let cap = ManagerCapability { - kind: kind.as_ref().to_string(), - description: String::new(), - }; - self.0.contains(&cap) - } - - /// Adds the specified capability to the set of capabilities - /// - /// * If the set did not have this capability, returns `true` - /// * If the set did have this capability, returns `false` - pub fn insert(&mut self, cap: impl Into) -> bool { - self.0.insert(cap.into()) - } - - /// Removes the capability with the described kind, returning the capability - pub fn take(&mut self, kind: impl AsRef) -> Option { - let cap = ManagerCapability { - kind: kind.as_ref().to_string(), - description: String::new(), - }; - self.0.take(&cap) - } - - /// Removes the capability with the described kind, returning true if it existed - pub fn remove(&mut self, kind: impl AsRef) -> bool { - let cap = ManagerCapability { - kind: kind.as_ref().to_string(), - description: String::new(), - }; - self.0.remove(&cap) - } - - /// Converts into vec of capabilities sorted by kind - pub fn into_sorted_vec(self) -> Vec { - let mut this = self.0.into_iter().collect::>(); - - this.sort_unstable(); - - this - } -} - -impl BitAnd for &ManagerCapabilities { - type Output = ManagerCapabilities; - - fn bitand(self, rhs: Self) -> Self::Output { - ManagerCapabilities(self.0.bitand(&rhs.0)) - } -} - -impl BitOr for &ManagerCapabilities { - type Output = ManagerCapabilities; - - fn bitor(self, rhs: Self) -> Self::Output { - ManagerCapabilities(self.0.bitor(&rhs.0)) - } -} - -impl BitOr for &ManagerCapabilities { - type Output = ManagerCapabilities; - - fn bitor(self, rhs: ManagerCapability) -> Self::Output { - let mut other = ManagerCapabilities::none(); - other.0.insert(rhs); - - self.bitor(&other) - } -} - -impl BitXor for &ManagerCapabilities { - type Output = ManagerCapabilities; - - fn bitxor(self, rhs: Self) -> Self::Output { - ManagerCapabilities(self.0.bitxor(&rhs.0)) - } -} - -impl FromIterator for ManagerCapabilities { - fn from_iter>(iter: I) -> Self { - let mut this = ManagerCapabilities::none(); - - for capability in iter { - this.0.insert(capability); - } - - this - } -} - -/// ManagerCapability tied to a manager. A capability is equivalent based on its kind and not -/// description. -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "snake_case", deny_unknown_fields)] -pub struct ManagerCapability { - /// Label describing the kind of capability - pub kind: String, - - /// Information about the capability - pub description: String, -} - -impl ManagerCapability { - /// Will convert the [`ManagerCapability`]'s `kind` into a known [`ManagerCapabilityKind`] if - /// possible, returning None if the capability is unknown - pub fn to_capability_kind(&self) -> Option { - ManagerCapabilityKind::from_str(&self.kind).ok() - } - - /// Returns true if the described capability is unknown - pub fn is_unknown(&self) -> bool { - self.to_capability_kind().is_none() - } -} - -impl PartialEq for ManagerCapability { - fn eq(&self, other: &Self) -> bool { - self.kind.eq_ignore_ascii_case(&other.kind) - } -} - -impl Eq for ManagerCapability {} - -impl PartialOrd for ManagerCapability { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for ManagerCapability { - fn cmp(&self, other: &Self) -> Ordering { - self.kind - .to_ascii_lowercase() - .cmp(&other.kind.to_ascii_lowercase()) - } -} - -impl Hash for ManagerCapability { - fn hash(&self, state: &mut H) { - self.kind.to_ascii_lowercase().hash(state); - } -} - -impl From for ManagerCapability { - /// Creates a new capability using the kind's default message - fn from(kind: ManagerCapabilityKind) -> Self { - Self { - kind: kind.to_string(), - description: kind - .get_message() - .map(ToString::to_string) - .unwrap_or_default(), - } - } -} diff --git a/distant-net/src/manager/data/request.rs b/distant-net/src/manager/data/request.rs index ebfc77c..c04cc0c 100644 --- a/distant-net/src/manager/data/request.rs +++ b/distant-net/src/manager/data/request.rs @@ -1,36 +1,17 @@ -use derive_more::IsVariant; use distant_auth::msg::AuthenticationResponse; use serde::{Deserialize, Serialize}; -use strum::{AsRefStr, EnumDiscriminants, EnumIter, EnumMessage, EnumString}; use super::{ManagerAuthenticationId, ManagerChannelId}; use crate::common::{ConnectionId, Destination, Map, UntypedRequest}; #[allow(clippy::large_enum_variant)] -#[derive(Clone, Debug, EnumDiscriminants, Serialize, Deserialize)] -#[strum_discriminants(derive( - AsRefStr, - strum::Display, - EnumIter, - EnumMessage, - EnumString, - Hash, - PartialOrd, - Ord, - IsVariant, - Serialize, - Deserialize -))] -#[strum_discriminants(name(ManagerCapabilityKind))] -#[strum_discriminants(strum(serialize_all = "snake_case"))] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "snake_case", deny_unknown_fields, tag = "type")] pub enum ManagerRequest { - /// Retrieve information about the server's capabilities - #[strum_discriminants(strum(message = "Supports retrieving capabilities"))] - Capabilities, + /// Retrieve information about the manager's version. + Version, /// Launch a server using the manager - #[strum_discriminants(strum(message = "Supports launching a server on remote machines"))] Launch { // NOTE: Boxed per clippy's large_enum_variant warning destination: Box, @@ -40,7 +21,6 @@ pub enum ManagerRequest { }, /// Initiate a connection through the manager - #[strum_discriminants(strum(message = "Supports connecting to remote servers"))] Connect { // NOTE: Boxed per clippy's large_enum_variant warning destination: Box, @@ -50,7 +30,6 @@ pub enum ManagerRequest { }, /// Submit some authentication message for the manager to use with an active connection - #[strum_discriminants(strum(message = "Supports authenticating with a remote server"))] Authenticate { /// Id of the authentication request that is being responded to id: ManagerAuthenticationId, @@ -60,16 +39,12 @@ pub enum ManagerRequest { }, /// Opens a channel for communication with an already-connected server - #[strum_discriminants(strum(message = "Supports opening a channel with a remote server"))] OpenChannel { /// Id of the connection id: ConnectionId, }, /// Sends data through channel - #[strum_discriminants(strum( - message = "Supports sending data through a channel with a remote server" - ))] Channel { /// Id of the channel id: ManagerChannelId, @@ -79,21 +54,17 @@ pub enum ManagerRequest { }, /// Closes an open channel - #[strum_discriminants(strum(message = "Supports closing a channel with a remote server"))] CloseChannel { /// Id of the channel to close id: ManagerChannelId, }, /// Retrieve information about a specific connection - #[strum_discriminants(strum(message = "Supports retrieving connection-specific information"))] Info { id: ConnectionId }, /// Kill a specific connection - #[strum_discriminants(strum(message = "Supports killing a remote connection"))] Kill { id: ConnectionId }, /// Retrieve list of connections being managed - #[strum_discriminants(strum(message = "Supports retrieving a list of managed connections"))] List, } diff --git a/distant-net/src/manager/data/response.rs b/distant-net/src/manager/data/response.rs index 3ee84b1..b48cde5 100644 --- a/distant-net/src/manager/data/response.rs +++ b/distant-net/src/manager/data/response.rs @@ -1,9 +1,7 @@ use distant_auth::msg::Authentication; use serde::{Deserialize, Serialize}; -use super::{ - ConnectionInfo, ConnectionList, ManagerAuthenticationId, ManagerCapabilities, ManagerChannelId, -}; +use super::{ConnectionInfo, ConnectionList, ManagerAuthenticationId, ManagerChannelId, SemVer}; use crate::common::{ConnectionId, Destination, UntypedResponse}; #[derive(Clone, Debug, Serialize, Deserialize)] @@ -15,8 +13,8 @@ pub enum ManagerResponse { /// Indicates that some error occurred during a request Error { description: String }, - /// Response to retrieving information about the manager's capabilities - Capabilities { supported: ManagerCapabilities }, + /// Information about the manager's version. + Version { version: SemVer }, /// Confirmation of a server being launched Launched { diff --git a/distant-net/src/manager/server.rs b/distant-net/src/manager/server.rs index c223ee4..bf9c504 100644 --- a/distant-net/src/manager/server.rs +++ b/distant-net/src/manager/server.rs @@ -9,8 +9,8 @@ use tokio::sync::{oneshot, RwLock}; use crate::common::{ConnectionId, Destination, Map}; use crate::manager::{ - ConnectionInfo, ConnectionList, ManagerAuthenticationId, ManagerCapabilities, ManagerChannelId, - ManagerRequest, ManagerResponse, + ConnectionInfo, ConnectionList, ManagerAuthenticationId, ManagerChannelId, ManagerRequest, + ManagerResponse, SemVer, }; use crate::server::{RequestCtx, Server, ServerHandler}; @@ -138,9 +138,11 @@ impl ManagerServer { Ok(id) } - /// Retrieves the list of supported capabilities for this manager - async fn capabilities(&self) -> io::Result { - Ok(ManagerCapabilities::all()) + /// Retrieves the manager's version. + async fn version(&self) -> io::Result { + env!("CARGO_PKG_VERSION") + .parse() + .map_err(|x| io::Error::new(io::ErrorKind::Other, x)) } /// Retrieves information about the connection to the server with the specified `id` @@ -196,10 +198,10 @@ impl ServerHandler for ManagerServer { } = ctx; let response = match request.payload { - ManagerRequest::Capabilities {} => { - debug!("Looking up capabilities"); - match self.capabilities().await { - Ok(supported) => ManagerResponse::Capabilities { supported }, + ManagerRequest::Version {} => { + debug!("Looking up version"); + match self.version().await { + Ok(version) => ManagerResponse::Version { version }, Err(x) => ManagerResponse::from(x), } } diff --git a/distant-net/src/server.rs b/distant-net/src/server.rs index 6f5c677..218b996 100644 --- a/distant-net/src/server.rs +++ b/distant-net/src/server.rs @@ -9,7 +9,7 @@ use serde::de::DeserializeOwned; use serde::Serialize; use tokio::sync::{broadcast, RwLock}; -use crate::common::{ConnectionId, Listener, Response, Transport}; +use crate::common::{ConnectionId, Listener, Response, Transport, Version}; mod builder; pub use builder::*; @@ -45,6 +45,9 @@ pub struct Server { /// Performs authentication using various methods verifier: Verifier, + + /// Version associated with the server used by clients to verify compatibility + version: Version, } /// Interface for a handler that receives connections and requests @@ -81,6 +84,7 @@ impl Server<()> { config: Default::default(), handler: (), verifier: Verifier::empty(), + version: Default::default(), } } @@ -115,6 +119,7 @@ impl Server { config, handler: self.handler, verifier: self.verifier, + version: self.version, } } @@ -124,6 +129,7 @@ impl Server { config: self.config, handler, verifier: self.verifier, + version: self.version, } } @@ -133,6 +139,17 @@ impl Server { config: self.config, handler: self.handler, verifier, + version: self.version, + } + } + + /// Consumes the current server, replacing its version with `version` and returning it. + pub fn version(self, version: Version) -> Self { + Self { + config: self.config, + handler: self.handler, + verifier: self.verifier, + version, } } } @@ -172,6 +189,7 @@ where config, handler, verifier, + version, } = self; let handler = Arc::new(handler); @@ -221,6 +239,7 @@ where .sleep_duration(config.connection_sleep) .heartbeat_duration(config.connection_heartbeat) .verifier(Arc::downgrade(&verifier)) + .version(version.clone()) .spawn(), ); @@ -253,6 +272,12 @@ mod tests { use super::*; use crate::common::{Connection, InmemoryTransport, MpscListener, Request, Response}; + macro_rules! server_version { + () => { + Version::new(1, 2, 3) + }; + } + pub struct TestServerHandler; #[async_trait] @@ -275,6 +300,7 @@ mod tests { config, handler: TestServerHandler, verifier: Verifier::new(methods), + version: server_version!(), } } @@ -304,7 +330,7 @@ mod tests { .expect("Failed to start server"); // Perform handshake and authentication with the server before beginning to send data - let mut connection = Connection::client(transport, DummyAuthHandler) + let mut connection = Connection::client(transport, DummyAuthHandler, server_version!()) .await .expect("Failed to connect to server"); diff --git a/distant-net/src/server/builder/tcp.rs b/distant-net/src/server/builder/tcp.rs index 2129d69..be35590 100644 --- a/distant-net/src/server/builder/tcp.rs +++ b/distant-net/src/server/builder/tcp.rs @@ -5,7 +5,7 @@ use distant_auth::Verifier; use serde::de::DeserializeOwned; use serde::Serialize; -use crate::common::{PortRange, TcpListener}; +use crate::common::{PortRange, TcpListener, Version}; use crate::server::{Server, ServerConfig, ServerHandler, TcpServerRef}; pub struct TcpServerBuilder(Server); @@ -35,6 +35,10 @@ impl TcpServerBuilder { pub fn verifier(self, verifier: Verifier) -> Self { Self(self.0.verifier(verifier)) } + + pub fn version(self, version: Version) -> Self { + Self(self.0.version(version)) + } } impl TcpServerBuilder diff --git a/distant-net/src/server/builder/unix.rs b/distant-net/src/server/builder/unix.rs index 4bddc1c..1037b65 100644 --- a/distant-net/src/server/builder/unix.rs +++ b/distant-net/src/server/builder/unix.rs @@ -5,7 +5,7 @@ use distant_auth::Verifier; use serde::de::DeserializeOwned; use serde::Serialize; -use crate::common::UnixSocketListener; +use crate::common::{UnixSocketListener, Version}; use crate::server::{Server, ServerConfig, ServerHandler, UnixSocketServerRef}; pub struct UnixSocketServerBuilder(Server); @@ -35,6 +35,10 @@ impl UnixSocketServerBuilder { pub fn verifier(self, verifier: Verifier) -> Self { Self(self.0.verifier(verifier)) } + + pub fn version(self, version: Version) -> Self { + Self(self.0.version(version)) + } } impl UnixSocketServerBuilder diff --git a/distant-net/src/server/builder/windows.rs b/distant-net/src/server/builder/windows.rs index 5d506fa..eb7f3d3 100644 --- a/distant-net/src/server/builder/windows.rs +++ b/distant-net/src/server/builder/windows.rs @@ -5,7 +5,7 @@ use distant_auth::Verifier; use serde::de::DeserializeOwned; use serde::Serialize; -use crate::common::WindowsPipeListener; +use crate::common::{Version, WindowsPipeListener}; use crate::server::{Server, ServerConfig, ServerHandler, WindowsPipeServerRef}; pub struct WindowsPipeServerBuilder(Server); @@ -35,6 +35,10 @@ impl WindowsPipeServerBuilder { pub fn verifier(self, verifier: Verifier) -> Self { Self(self.0.verifier(verifier)) } + + pub fn version(self, version: Version) -> Self { + Self(self.0.version(version)) + } } impl WindowsPipeServerBuilder diff --git a/distant-net/src/server/connection.rs b/distant-net/src/server/connection.rs index 43658f9..540836e 100644 --- a/distant-net/src/server/connection.rs +++ b/distant-net/src/server/connection.rs @@ -14,7 +14,7 @@ use tokio::task::JoinHandle; use super::{ConnectionState, RequestCtx, ServerHandler, ServerReply, ServerState, ShutdownTimer}; use crate::common::{ - Backup, Connection, Frame, Interest, Keychain, Response, Transport, UntypedRequest, + Backup, Connection, Frame, Interest, Keychain, Response, Transport, UntypedRequest, Version, }; pub type ServerKeychain = Keychain>; @@ -65,6 +65,7 @@ pub(super) struct ConnectionTaskBuilder { sleep_duration: Duration, heartbeat_duration: Duration, verifier: Weak, + version: Version, } impl ConnectionTaskBuilder<(), (), ()> { @@ -80,6 +81,7 @@ impl ConnectionTaskBuilder<(), (), ()> { sleep_duration: SLEEP_DURATION, heartbeat_duration: MINIMUM_HEARTBEAT_DURATION, verifier: Weak::new(), + version: Version::default(), } } } @@ -96,6 +98,7 @@ impl ConnectionTaskBuilder { sleep_duration: self.sleep_duration, heartbeat_duration: self.heartbeat_duration, verifier: self.verifier, + version: self.version, } } @@ -110,6 +113,7 @@ impl ConnectionTaskBuilder { sleep_duration: self.sleep_duration, heartbeat_duration: self.heartbeat_duration, verifier: self.verifier, + version: self.version, } } @@ -124,6 +128,7 @@ impl ConnectionTaskBuilder { sleep_duration: self.sleep_duration, heartbeat_duration: self.heartbeat_duration, verifier: self.verifier, + version: self.version, } } @@ -138,6 +143,7 @@ impl ConnectionTaskBuilder { sleep_duration: self.sleep_duration, heartbeat_duration: self.heartbeat_duration, verifier: self.verifier, + version: self.version, } } @@ -152,6 +158,7 @@ impl ConnectionTaskBuilder { sleep_duration: self.sleep_duration, heartbeat_duration: self.heartbeat_duration, verifier: self.verifier, + version: self.version, } } @@ -169,6 +176,7 @@ impl ConnectionTaskBuilder { sleep_duration: self.sleep_duration, heartbeat_duration: self.heartbeat_duration, verifier: self.verifier, + version: self.version, } } @@ -183,6 +191,7 @@ impl ConnectionTaskBuilder { sleep_duration, heartbeat_duration: self.heartbeat_duration, verifier: self.verifier, + version: self.version, } } @@ -200,6 +209,7 @@ impl ConnectionTaskBuilder { sleep_duration: self.sleep_duration, heartbeat_duration, verifier: self.verifier, + version: self.version, } } @@ -214,6 +224,22 @@ impl ConnectionTaskBuilder { sleep_duration: self.sleep_duration, heartbeat_duration: self.heartbeat_duration, verifier, + version: self.version, + } + } + + pub fn version(self, version: Version) -> ConnectionTaskBuilder { + ConnectionTaskBuilder { + handler: self.handler, + state: self.state, + keychain: self.keychain, + transport: self.transport, + shutdown: self.shutdown, + shutdown_timer: self.shutdown_timer, + sleep_duration: self.sleep_duration, + heartbeat_duration: self.heartbeat_duration, + verifier: self.verifier, + version, } } } @@ -240,6 +266,7 @@ where sleep_duration, heartbeat_duration, verifier, + version, } = self; // NOTE: This exists purely to make the compiler happy for macro_rules declaration order. @@ -408,7 +435,8 @@ where match await_or_shutdown!(Box::pin(Connection::server( transport, verifier.as_ref(), - keychain + keychain, + version ))) { Ok(connection) => connection, Err(x) => { @@ -627,6 +655,12 @@ mod tests { }}; } + macro_rules! server_version { + () => { + Version::new(1, 2, 3) + }; + } + #[test(tokio::test)] async fn should_terminate_if_fails_access_verifier() { let handler = Arc::new(TestServerHandler); @@ -671,11 +705,12 @@ mod tests { .transport(t1) .shutdown_timer(Arc::downgrade(&shutdown_timer)) .verifier(Arc::downgrade(&verifier)) + .version(server_version!()) .spawn(); // Spawn a task to handle establishing connection from client-side tokio::spawn(async move { - let _client = Connection::client(t2, DummyAuthHandler) + let _client = Connection::client(t2, DummyAuthHandler, server_version!()) .await .expect("Fail to establish client-side connection"); }); @@ -704,11 +739,12 @@ mod tests { .transport(t1) .shutdown_timer(Arc::downgrade(&shutdown_timer)) .verifier(Arc::downgrade(&verifier)) + .version(server_version!()) .spawn(); // Spawn a task to handle establishing connection from client-side tokio::spawn(async move { - let _client = Connection::client(t2, DummyAuthHandler) + let _client = Connection::client(t2, DummyAuthHandler, server_version!()) .await .expect("Fail to establish client-side connection"); }); @@ -754,12 +790,13 @@ mod tests { .transport(t1) .shutdown_timer(Arc::downgrade(&shutdown_timer)) .verifier(Arc::downgrade(&verifier)) + .version(server_version!()) .spawn(); // Spawn a task to handle establishing connection from client-side, and then closes to // trigger the server-side to close tokio::spawn(async move { - let _client = Connection::client(t2, DummyAuthHandler) + let _client = Connection::client(t2, DummyAuthHandler, server_version!()) .await .expect("Fail to establish client-side connection"); }); @@ -828,12 +865,13 @@ mod tests { }) .shutdown_timer(Arc::downgrade(&shutdown_timer)) .verifier(Arc::downgrade(&verifier)) + .version(server_version!()) .spawn(); // Spawn a task to handle establishing connection from client-side, set ready to fail // for the server-side after client connection completes, and wait a bit tokio::spawn(async move { - let _client = Connection::client(t2, DummyAuthHandler) + let _client = Connection::client(t2, DummyAuthHandler, server_version!()) .await .expect("Fail to establish client-side connection"); @@ -872,12 +910,13 @@ mod tests { .transport(t1) .shutdown_timer(Arc::downgrade(&shutdown_timer)) .verifier(Arc::downgrade(&verifier)) + .version(server_version!()) .spawn(); // Spawn a task to handle establishing connection from client-side, and then closes to // trigger the server-side to close tokio::spawn(async move { - let _client = Connection::client(t2, DummyAuthHandler) + let _client = Connection::client(t2, DummyAuthHandler, server_version!()) .await .expect("Fail to establish client-side connection"); }); @@ -902,11 +941,12 @@ mod tests { .transport(t1) .shutdown_timer(Arc::downgrade(&shutdown_timer)) .verifier(Arc::downgrade(&verifier)) + .version(server_version!()) .spawn(); // Spawn a task to handle establishing connection from client-side let task = tokio::spawn(async move { - let mut client = Connection::client(t2, DummyAuthHandler) + let mut client = Connection::client(t2, DummyAuthHandler, server_version!()) .await .expect("Fail to establish client-side connection"); @@ -939,11 +979,12 @@ mod tests { .shutdown_timer(Arc::downgrade(&shutdown_timer)) .heartbeat_duration(Duration::from_millis(200)) .verifier(Arc::downgrade(&verifier)) + .version(server_version!()) .spawn(); // Spawn a task to handle establishing connection from client-side let task = tokio::spawn(async move { - let mut client = Connection::client(t2, DummyAuthHandler) + let mut client = Connection::client(t2, DummyAuthHandler, server_version!()) .await .expect("Fail to establish client-side connection"); @@ -1047,10 +1088,12 @@ mod tests { .shutdown_timer(Arc::downgrade(&shutdown_timer)) .heartbeat_duration(Duration::from_millis(200)) .verifier(Arc::downgrade(&verifier)) + .version(server_version!()) .spawn(); // Spawn a task to handle the client-side establishment of a full connection - let _client_task = tokio::spawn(Connection::client(t2, DummyAuthHandler)); + let _client_task = + tokio::spawn(Connection::client(t2, DummyAuthHandler, server_version!())); // Shutdown server connection task while it is accepting the connection, verifying that we // do not get an error in return @@ -1099,10 +1142,12 @@ mod tests { .shutdown_timer(Arc::downgrade(&shutdown_timer)) .heartbeat_duration(Duration::from_millis(200)) .verifier(Arc::downgrade(&verifier)) + .version(server_version!()) .spawn(); // Spawn a task to handle the client-side establishment of a full connection - let _client_task = tokio::spawn(Connection::client(t2, DummyAuthHandler)); + let _client_task = + tokio::spawn(Connection::client(t2, DummyAuthHandler, server_version!())); // Wait to ensure we complete the accept call first let _ = rx.recv().await; diff --git a/distant-protocol/Cargo.toml b/distant-protocol/Cargo.toml index e10d357..71fec46 100644 --- a/distant-protocol/Cargo.toml +++ b/distant-protocol/Cargo.toml @@ -3,7 +3,7 @@ name = "distant-protocol" description = "Protocol library for distant, providing data structures used between the client and server" categories = ["data-structures"] keywords = ["protocol"] -version = "0.20.0-alpha.12" +version = "0.20.0-alpha.13" authors = ["Chip Senkbeil "] edition = "2021" homepage = "https://github.com/chipsenkbeil/distant" @@ -20,6 +20,7 @@ bitflags = "2.3.1" const-str = "0.5.6" derive_more = { version = "0.99.17", default-features = false, features = ["deref", "deref_mut", "display", "from", "error", "into", "into_iterator", "is_variant"] } regex = "1.8.3" +semver = { version = "1.0.17", features = ["serde"] } serde = { version = "1.0.163", features = ["derive"] } serde_bytes = "0.11.9" strum = { version = "0.24.1", features = ["derive"] } diff --git a/distant-protocol/src/common.rs b/distant-protocol/src/common.rs index 8e891cd..24ee0dc 100644 --- a/distant-protocol/src/common.rs +++ b/distant-protocol/src/common.rs @@ -1,4 +1,3 @@ -mod capabilities; mod change; mod cmd; mod error; @@ -10,7 +9,6 @@ mod search; mod system; mod version; -pub use capabilities::*; pub use change::*; pub use cmd::*; pub use error::*; @@ -24,6 +22,3 @@ pub use version::*; /// Id for a remote process pub type ProcessId = u32; - -/// Version indicated by the tuple of (major, minor, patch). -pub type SemVer = (u8, u8, u8); diff --git a/distant-protocol/src/common/capabilities.rs b/distant-protocol/src/common/capabilities.rs deleted file mode 100644 index 7a9d7c1..0000000 --- a/distant-protocol/src/common/capabilities.rs +++ /dev/null @@ -1,380 +0,0 @@ -use std::cmp::Ordering; -use std::collections::HashSet; -use std::hash::{Hash, Hasher}; -use std::ops::{BitAnd, BitOr, BitXor, Deref, DerefMut}; -use std::str::FromStr; - -use derive_more::{From, Into, IntoIterator}; -use serde::{Deserialize, Serialize}; -use strum::{EnumMessage, IntoEnumIterator}; - -/// Represents the kinds of capabilities available. -pub use crate::request::RequestKind as CapabilityKind; - -/// Set of supported capabilities for a server -#[derive(Clone, Debug, From, Into, PartialEq, Eq, IntoIterator, Serialize, Deserialize)] -#[serde(transparent)] -pub struct Capabilities(#[into_iterator(owned, ref)] HashSet); - -impl Capabilities { - /// Return set of capabilities encompassing all possible capabilities - pub fn all() -> Self { - Self(CapabilityKind::iter().map(Capability::from).collect()) - } - - /// Return empty set of capabilities - pub fn none() -> Self { - Self(HashSet::new()) - } - - /// Returns true if the capability with described kind is included - pub fn contains(&self, kind: impl AsRef) -> bool { - let cap = Capability { - kind: kind.as_ref().to_string(), - description: String::new(), - }; - self.0.contains(&cap) - } - - /// Adds the specified capability to the set of capabilities - /// - /// * If the set did not have this capability, returns `true` - /// * If the set did have this capability, returns `false` - pub fn insert(&mut self, cap: impl Into) -> bool { - self.0.insert(cap.into()) - } - - /// Removes the capability with the described kind, returning the capability - pub fn take(&mut self, kind: impl AsRef) -> Option { - let cap = Capability { - kind: kind.as_ref().to_string(), - description: String::new(), - }; - self.0.take(&cap) - } - - /// Removes the capability with the described kind, returning true if it existed - pub fn remove(&mut self, kind: impl AsRef) -> bool { - let cap = Capability { - kind: kind.as_ref().to_string(), - description: String::new(), - }; - self.0.remove(&cap) - } - - /// Converts into vec of capabilities sorted by kind - pub fn into_sorted_vec(self) -> Vec { - let mut this = self.0.into_iter().collect::>(); - - this.sort_unstable(); - - this - } -} - -impl AsRef> for Capabilities { - fn as_ref(&self) -> &HashSet { - &self.0 - } -} - -impl AsMut> for Capabilities { - fn as_mut(&mut self) -> &mut HashSet { - &mut self.0 - } -} - -impl Deref for Capabilities { - type Target = HashSet; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for Capabilities { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl BitAnd for &Capabilities { - type Output = Capabilities; - - fn bitand(self, rhs: Self) -> Self::Output { - Capabilities(self.0.bitand(&rhs.0)) - } -} - -impl BitOr for &Capabilities { - type Output = Capabilities; - - fn bitor(self, rhs: Self) -> Self::Output { - Capabilities(self.0.bitor(&rhs.0)) - } -} - -impl BitOr for &Capabilities { - type Output = Capabilities; - - fn bitor(self, rhs: Capability) -> Self::Output { - let mut other = Capabilities::none(); - other.0.insert(rhs); - - self.bitor(&other) - } -} - -impl BitXor for &Capabilities { - type Output = Capabilities; - - fn bitxor(self, rhs: Self) -> Self::Output { - Capabilities(self.0.bitxor(&rhs.0)) - } -} - -impl FromIterator for Capabilities { - fn from_iter>(iter: I) -> Self { - let mut this = Capabilities::none(); - - for capability in iter { - this.0.insert(capability); - } - - this - } -} - -/// Capability tied to a server. A capability is equivalent based on its kind and not description. -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "snake_case", deny_unknown_fields)] -pub struct Capability { - /// Label describing the kind of capability - pub kind: String, - - /// Information about the capability - pub description: String, -} - -impl Capability { - /// Will convert the [`Capability`]'s `kind` into a known [`CapabilityKind`] if possible, - /// returning None if the capability is unknown - pub fn to_capability_kind(&self) -> Option { - CapabilityKind::from_str(&self.kind).ok() - } - - /// Returns true if the described capability is unknown - pub fn is_unknown(&self) -> bool { - self.to_capability_kind().is_none() - } -} - -impl PartialEq for Capability { - fn eq(&self, other: &Self) -> bool { - self.kind.eq_ignore_ascii_case(&other.kind) - } -} - -impl Eq for Capability {} - -impl PartialOrd for Capability { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for Capability { - fn cmp(&self, other: &Self) -> Ordering { - self.kind - .to_ascii_lowercase() - .cmp(&other.kind.to_ascii_lowercase()) - } -} - -impl Hash for Capability { - fn hash(&self, state: &mut H) { - self.kind.to_ascii_lowercase().hash(state); - } -} - -impl From for Capability { - /// Creates a new capability using the kind's default message - fn from(kind: CapabilityKind) -> Self { - Self { - kind: kind.to_string(), - description: kind - .get_message() - .map(ToString::to_string) - .unwrap_or_default(), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - mod capabilities { - use super::*; - - #[test] - fn should_be_able_to_serialize_to_json() { - let capabilities: Capabilities = [Capability { - kind: "some kind".to_string(), - description: "some description".to_string(), - }] - .into_iter() - .collect(); - - let value = serde_json::to_value(capabilities).unwrap(); - assert_eq!( - value, - serde_json::json!([ - { - "kind": "some kind", - "description": "some description", - } - ]) - ); - } - - #[test] - fn should_be_able_to_deserialize_from_json() { - let value = serde_json::json!([ - { - "kind": "some kind", - "description": "some description", - } - ]); - - let capabilities: Capabilities = serde_json::from_value(value).unwrap(); - assert_eq!( - capabilities, - [Capability { - kind: "some kind".to_string(), - description: "some description".to_string(), - }] - .into_iter() - .collect() - ); - } - - #[test] - fn should_be_able_to_serialize_to_msgpack() { - let capabilities: Capabilities = [Capability { - kind: "some kind".to_string(), - description: "some description".to_string(), - }] - .into_iter() - .collect(); - - // NOTE: We don't actually check the output here because it's an implementation detail - // and could change as we change how serialization is done. This is merely to verify - // that we can serialize since there are times when serde fails to serialize at - // runtime. - let _ = rmp_serde::encode::to_vec_named(&capabilities).unwrap(); - } - - #[test] - fn should_be_able_to_deserialize_from_msgpack() { - // NOTE: It may seem odd that we are serializing just to deserialize, but this is to - // verify that we are not corrupting or preventing issues when serializing on a - // client/server and then trying to deserialize on the other side. This has happened - // enough times with minor changes that we need tests to verify. - let buf = rmp_serde::encode::to_vec_named( - &[Capability { - kind: "some kind".to_string(), - description: "some description".to_string(), - }] - .into_iter() - .collect::(), - ) - .unwrap(); - - let capabilities: Capabilities = rmp_serde::decode::from_slice(&buf).unwrap(); - assert_eq!( - capabilities, - [Capability { - kind: "some kind".to_string(), - description: "some description".to_string(), - }] - .into_iter() - .collect() - ); - } - } - - mod capability { - use super::*; - - #[test] - fn should_be_able_to_serialize_to_json() { - let capability = Capability { - kind: "some kind".to_string(), - description: "some description".to_string(), - }; - - let value = serde_json::to_value(capability).unwrap(); - assert_eq!( - value, - serde_json::json!({ - "kind": "some kind", - "description": "some description", - }) - ); - } - - #[test] - fn should_be_able_to_deserialize_from_json() { - let value = serde_json::json!({ - "kind": "some kind", - "description": "some description", - }); - - let capability: Capability = serde_json::from_value(value).unwrap(); - assert_eq!( - capability, - Capability { - kind: "some kind".to_string(), - description: "some description".to_string(), - } - ); - } - - #[test] - fn should_be_able_to_serialize_to_msgpack() { - let capability = Capability { - kind: "some kind".to_string(), - description: "some description".to_string(), - }; - - // NOTE: We don't actually check the output here because it's an implementation detail - // and could change as we change how serialization is done. This is merely to verify - // that we can serialize since there are times when serde fails to serialize at - // runtime. - let _ = rmp_serde::encode::to_vec_named(&capability).unwrap(); - } - - #[test] - fn should_be_able_to_deserialize_from_msgpack() { - // NOTE: It may seem odd that we are serializing just to deserialize, but this is to - // verify that we are not corrupting or causing issues when serializing on a - // client/server and then trying to deserialize on the other side. This has happened - // enough times with minor changes that we need tests to verify. - let buf = rmp_serde::encode::to_vec_named(&Capability { - kind: "some kind".to_string(), - description: "some description".to_string(), - }) - .unwrap(); - - let capability: Capability = rmp_serde::decode::from_slice(&buf).unwrap(); - assert_eq!( - capability, - Capability { - kind: "some kind".to_string(), - description: "some description".to_string(), - } - ); - } - } -} diff --git a/distant-protocol/src/common/version.rs b/distant-protocol/src/common/version.rs index 92d79ca..7e7c95f 100644 --- a/distant-protocol/src/common/version.rs +++ b/distant-protocol/src/common/version.rs @@ -1,48 +1,80 @@ use serde::{Deserialize, Serialize}; -use crate::common::{Capabilities, SemVer}; +use crate::semver; /// Represents version information. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Version { - /// General version of server (arbitrary format) - pub server_version: String, + /// Server version. + pub server_version: semver::Version, - /// Protocol version - pub protocol_version: SemVer, + /// Protocol version. + pub protocol_version: semver::Version, - /// Capabilities of the server - pub capabilities: Capabilities, + /// Additional features available. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub capabilities: Vec, +} + +impl Version { + /// Supports executing processes. + pub const CAP_EXEC: &'static str = "exec"; + + /// Supports reading and writing via filesystem IO. + pub const CAP_FS_IO: &'static str = "fs_io"; + + /// Supports modifying permissions of filesystem. + pub const CAP_FS_PERM: &'static str = "fs_perm"; + + /// Supports searching filesystem. + pub const CAP_FS_SEARCH: &'static str = "fs_search"; + + /// Supports watching filesystem for changes. + pub const CAP_FS_WATCH: &'static str = "fs_watch"; + + /// Supports TCP tunneling. + // pub const CAP_TCP_TUNNEL: &'static str = "tcp_tunnel"; + + /// Supports TCP reverse tunneling. + // pub const CAP_TCP_REV_TUNNEL: &'static str = "tcp_rev_tunnel"; + + /// Supports retrieving system information. + pub const CAP_SYS_INFO: &'static str = "sys_info"; + + pub const fn capabilities() -> &'static [&'static str] { + &[ + Self::CAP_EXEC, + Self::CAP_FS_IO, + Self::CAP_FS_PERM, + Self::CAP_FS_SEARCH, + Self::CAP_FS_WATCH, + /* Self::CAP_TCP_TUNNEL, + Self::CAP_TCP_REV_TUNNEL, */ + Self::CAP_SYS_INFO, + ] + } } #[cfg(test)] mod tests { use super::*; - use crate::common::Capability; + use semver::Version as SemVer; #[test] fn should_be_able_to_serialize_to_json() { let version = Version { - server_version: String::from("some version"), - protocol_version: (1, 2, 3), - capabilities: [Capability { - kind: String::from("some kind"), - description: String::from("some description"), - }] - .into_iter() - .collect(), + server_version: "123.456.789-rc+build".parse().unwrap(), + protocol_version: SemVer::new(1, 2, 3), + capabilities: vec![String::from("cap")], }; let value = serde_json::to_value(version).unwrap(); assert_eq!( value, serde_json::json!({ - "server_version": "some version", - "protocol_version": [1, 2, 3], - "capabilities": [{ - "kind": "some kind", - "description": "some description", - }] + "server_version": "123.456.789-rc+build", + "protocol_version": "1.2.3", + "capabilities": ["cap"] }) ); } @@ -50,26 +82,18 @@ mod tests { #[test] fn should_be_able_to_deserialize_from_json() { let value = serde_json::json!({ - "server_version": "some version", - "protocol_version": [1, 2, 3], - "capabilities": [{ - "kind": "some kind", - "description": "some description", - }] + "server_version": "123.456.789-rc+build", + "protocol_version": "1.2.3", + "capabilities": ["cap"] }); let version: Version = serde_json::from_value(value).unwrap(); assert_eq!( version, Version { - server_version: String::from("some version"), - protocol_version: (1, 2, 3), - capabilities: [Capability { - kind: String::from("some kind"), - description: String::from("some description"), - }] - .into_iter() - .collect(), + server_version: "123.456.789-rc+build".parse().unwrap(), + protocol_version: SemVer::new(1, 2, 3), + capabilities: vec![String::from("cap")], } ); } @@ -77,14 +101,9 @@ mod tests { #[test] fn should_be_able_to_serialize_to_msgpack() { let version = Version { - server_version: String::from("some version"), - protocol_version: (1, 2, 3), - capabilities: [Capability { - kind: String::from("some kind"), - description: String::from("some description"), - }] - .into_iter() - .collect(), + server_version: "123.456.789-rc+build".parse().unwrap(), + protocol_version: SemVer::new(1, 2, 3), + capabilities: vec![String::from("cap")], }; // NOTE: We don't actually check the output here because it's an implementation detail @@ -101,14 +120,9 @@ mod tests { // client/server and then trying to deserialize on the other side. This has happened // enough times with minor changes that we need tests to verify. let buf = rmp_serde::encode::to_vec_named(&Version { - server_version: String::from("some version"), - protocol_version: (1, 2, 3), - capabilities: [Capability { - kind: String::from("some kind"), - description: String::from("some description"), - }] - .into_iter() - .collect(), + server_version: "123.456.789-rc+build".parse().unwrap(), + protocol_version: SemVer::new(1, 2, 3), + capabilities: vec![String::from("cap")], }) .unwrap(); @@ -116,14 +130,9 @@ mod tests { assert_eq!( version, Version { - server_version: String::from("some version"), - protocol_version: (1, 2, 3), - capabilities: [Capability { - kind: String::from("some kind"), - description: String::from("some description"), - }] - .into_iter() - .collect(), + server_version: "123.456.789-rc+build".parse().unwrap(), + protocol_version: SemVer::new(1, 2, 3), + capabilities: vec![String::from("cap")], } ); } diff --git a/distant-protocol/src/lib.rs b/distant-protocol/src/lib.rs index 8f0907c..cc213a9 100644 --- a/distant-protocol/src/lib.rs +++ b/distant-protocol/src/lib.rs @@ -14,14 +14,95 @@ pub use common::*; pub use msg::*; pub use request::*; pub use response::*; +pub use semver; -/// Protocol version indicated by the tuple of (major, minor, patch). +/// Protocol version of major/minor/patch. /// /// This should match the version of this crate such that any significant change to the crate /// version will also be reflected in this constant that can be used to verify compatibility across /// the wire. -pub const PROTOCOL_VERSION: SemVer = ( - const_str::parse!(env!("CARGO_PKG_VERSION_MAJOR"), u8), - const_str::parse!(env!("CARGO_PKG_VERSION_MINOR"), u8), - const_str::parse!(env!("CARGO_PKG_VERSION_PATCH"), u8), +pub const PROTOCOL_VERSION: semver::Version = semver::Version::new( + const_str::parse!(env!("CARGO_PKG_VERSION_MAJOR"), u64), + const_str::parse!(env!("CARGO_PKG_VERSION_MINOR"), u64), + const_str::parse!(env!("CARGO_PKG_VERSION_PATCH"), u64), ); + +/// Comparators used to indicate the [lower, upper) bounds of supported protocol versions. +const PROTOCOL_VERSION_COMPAT: (semver::Comparator, semver::Comparator) = ( + semver::Comparator { + op: semver::Op::GreaterEq, + major: const_str::parse!(env!("CARGO_PKG_VERSION_MAJOR"), u64), + minor: Some(const_str::parse!(env!("CARGO_PKG_VERSION_MINOR"), u64)), + patch: Some(const_str::parse!(env!("CARGO_PKG_VERSION_PATCH"), u64)), + pre: semver::Prerelease::EMPTY, + }, + semver::Comparator { + op: semver::Op::Less, + major: { + let major = const_str::parse!(env!("CARGO_PKG_VERSION_MAJOR"), u64); + + // If we have a version like 0.20, then the upper bound is 0.21, + // otherwise if we have a version like 1.2, then the upper bound is 2.0 + // + // So only increment the major if it is greater than 0 + if major > 0 { + major + 1 + } else { + major + } + }, + minor: { + let major = const_str::parse!(env!("CARGO_PKG_VERSION_MAJOR"), u64); + let minor = const_str::parse!(env!("CARGO_PKG_VERSION_MINOR"), u64); + + // If we have a version like 0.20, then the upper bound is 0.21, + // otherwise if we have a version like 1.2, then the upper bound is 2.0 + // + // So only increment the minor if major is 0 + if major > 0 { + None + } else { + Some(minor + 1) + } + }, + patch: None, + pre: semver::Prerelease::EMPTY, + }, +); + +/// Returns true if the provided version is compatible with the protocol version. +/// +/// ``` +/// use distant_protocol::{is_compatible_with, PROTOCOL_VERSION}; +/// use distant_protocol::semver::Version; +/// +/// // The current protocol version tied to this crate is always compatible +/// assert!(is_compatible_with(&PROTOCOL_VERSION)); +/// +/// // Major bumps in distant's protocol version are always considered incompatible +/// assert!(!is_compatible_with(&Version::new( +/// PROTOCOL_VERSION.major + 1, +/// PROTOCOL_VERSION.minor, +/// PROTOCOL_VERSION.patch, +/// ))); +/// +/// // While distant's protocol is being stabilized, minor version bumps +/// // are also considered incompatible! +/// assert!(!is_compatible_with(&Version::new( +/// PROTOCOL_VERSION.major, +/// PROTOCOL_VERSION.minor + 1, +/// PROTOCOL_VERSION.patch, +/// ))); +/// +/// // Patch bumps in distant's protocol are always considered compatible +/// assert!(is_compatible_with(&Version::new( +/// PROTOCOL_VERSION.major, +/// PROTOCOL_VERSION.minor, +/// PROTOCOL_VERSION.patch + 1, +/// ))); +/// ``` +pub fn is_compatible_with(version: &semver::Version) -> bool { + let (lower, upper) = PROTOCOL_VERSION_COMPAT; + + lower.matches(version) && upper.matches(version) +} diff --git a/distant-protocol/src/request.rs b/distant-protocol/src/request.rs index 9d3509e..b725044 100644 --- a/distant-protocol/src/request.rs +++ b/distant-protocol/src/request.rs @@ -3,7 +3,6 @@ use std::path::PathBuf; use derive_more::IsVariant; use serde::{Deserialize, Serialize}; -use strum::{AsRefStr, EnumDiscriminants, EnumIter, EnumMessage, EnumString}; use crate::common::{ ChangeKind, Cmd, Permissions, ProcessId, PtySize, SearchId, SearchQuery, SetPermissionsOptions, @@ -14,26 +13,10 @@ use crate::utils; pub type Environment = HashMap; /// Represents the payload of a request to be performed on the remote machine -#[derive(Clone, Debug, PartialEq, Eq, EnumDiscriminants, IsVariant, Serialize, Deserialize)] -#[strum_discriminants(derive( - AsRefStr, - strum::Display, - EnumIter, - EnumMessage, - EnumString, - Hash, - PartialOrd, - Ord, - IsVariant, - Serialize, - Deserialize -))] -#[strum_discriminants(name(RequestKind))] -#[strum_discriminants(strum(serialize_all = "snake_case"))] +#[derive(Clone, Debug, PartialEq, Eq, IsVariant, Serialize, Deserialize)] #[serde(rename_all = "snake_case", deny_unknown_fields, tag = "type")] pub enum Request { /// Reads a file from the specified path on the remote machine - #[strum_discriminants(strum(message = "Supports reading binary file"))] FileRead { /// The path to the file on the remote machine path: PathBuf, @@ -41,7 +24,6 @@ pub enum Request { /// Reads a file from the specified path on the remote machine /// and treats the contents as text - #[strum_discriminants(strum(message = "Supports reading text file"))] FileReadText { /// The path to the file on the remote machine path: PathBuf, @@ -49,7 +31,6 @@ pub enum Request { /// Writes a file, creating it if it does not exist, and overwriting any existing content /// on the remote machine - #[strum_discriminants(strum(message = "Supports writing binary file"))] FileWrite { /// The path to the file on the remote machine path: PathBuf, @@ -61,7 +42,6 @@ pub enum Request { /// Writes a file using text instead of bytes, creating it if it does not exist, /// and overwriting any existing content on the remote machine - #[strum_discriminants(strum(message = "Supports writing text file"))] FileWriteText { /// The path to the file on the remote machine path: PathBuf, @@ -71,7 +51,6 @@ pub enum Request { }, /// Appends to a file, creating it if it does not exist, on the remote machine - #[strum_discriminants(strum(message = "Supports appending to binary file"))] FileAppend { /// The path to the file on the remote machine path: PathBuf, @@ -82,7 +61,6 @@ pub enum Request { }, /// Appends text to a file, creating it if it does not exist, on the remote machine - #[strum_discriminants(strum(message = "Supports appending to text file"))] FileAppendText { /// The path to the file on the remote machine path: PathBuf, @@ -92,7 +70,6 @@ pub enum Request { }, /// Reads a directory from the specified path on the remote machine - #[strum_discriminants(strum(message = "Supports reading directory"))] DirRead { /// The path to the directory on the remote machine path: PathBuf, @@ -126,7 +103,6 @@ pub enum Request { }, /// Creates a directory on the remote machine - #[strum_discriminants(strum(message = "Supports creating directory"))] DirCreate { /// The path to the directory on the remote machine path: PathBuf, @@ -137,7 +113,6 @@ pub enum Request { }, /// Removes a file or directory on the remote machine - #[strum_discriminants(strum(message = "Supports removing files, directories, and symlinks"))] Remove { /// The path to the file or directory on the remote machine path: PathBuf, @@ -149,7 +124,6 @@ pub enum Request { }, /// Copies a file or directory on the remote machine - #[strum_discriminants(strum(message = "Supports copying files, directories, and symlinks"))] Copy { /// The path to the file or directory on the remote machine src: PathBuf, @@ -159,7 +133,6 @@ pub enum Request { }, /// Moves/renames a file or directory on the remote machine - #[strum_discriminants(strum(message = "Supports renaming files, directories, and symlinks"))] Rename { /// The path to the file or directory on the remote machine src: PathBuf, @@ -169,7 +142,6 @@ pub enum Request { }, /// Watches a path for changes - #[strum_discriminants(strum(message = "Supports watching filesystem for changes"))] Watch { /// The path to the file, directory, or symlink on the remote machine path: PathBuf, @@ -189,23 +161,18 @@ pub enum Request { }, /// Unwatches a path for changes, meaning no additional changes will be reported - #[strum_discriminants(strum(message = "Supports unwatching filesystem for changes"))] Unwatch { /// The path to the file, directory, or symlink on the remote machine path: PathBuf, }, /// Checks whether the given path exists - #[strum_discriminants(strum(message = "Supports checking if a path exists"))] Exists { /// The path to the file or directory on the remote machine path: PathBuf, }, /// Retrieves filesystem metadata for the specified path on the remote machine - #[strum_discriminants(strum( - message = "Supports retrieving metadata about a file, directory, or symlink" - ))] Metadata { /// The path to the file, directory, or symlink on the remote machine path: PathBuf, @@ -222,9 +189,6 @@ pub enum Request { }, /// Sets permissions on a file, directory, or symlink on the remote machine - #[strum_discriminants(strum( - message = "Supports setting permissions on a file, directory, or symlink" - ))] SetPermissions { /// The path to the file, directory, or symlink on the remote machine path: PathBuf, @@ -238,23 +202,18 @@ pub enum Request { }, /// Searches filesystem using the provided query - #[strum_discriminants(strum(message = "Supports searching filesystem using queries"))] Search { /// Query to perform against the filesystem query: SearchQuery, }, /// Cancels an active search being run against the filesystem - #[strum_discriminants(strum( - message = "Supports canceling an active search against the filesystem" - ))] CancelSearch { /// Id of the search to cancel id: SearchId, }, /// Spawns a new process on the remote machine - #[strum_discriminants(strum(message = "Supports spawning a process"))] ProcSpawn { /// The full command to run including arguments cmd: Cmd, @@ -273,14 +232,12 @@ pub enum Request { }, /// Kills a process running on the remote machine - #[strum_discriminants(strum(message = "Supports killing a spawned process"))] ProcKill { /// Id of the actively-running process id: ProcessId, }, /// Sends additional data to stdin of running process - #[strum_discriminants(strum(message = "Supports sending stdin to a spawned process"))] ProcStdin { /// Id of the actively-running process to send stdin data id: ProcessId, @@ -291,7 +248,6 @@ pub enum Request { }, /// Resize pty of remote process - #[strum_discriminants(strum(message = "Supports resizing the pty of a spawned process"))] ProcResizePty { /// Id of the actively-running process whose pty to resize id: ProcessId, @@ -301,11 +257,9 @@ pub enum Request { }, /// Retrieve information about the server and the system it is on - #[strum_discriminants(strum(message = "Supports retrieving system information"))] SystemInfo {}, /// Retrieve information about the server's protocol version - #[strum_discriminants(strum(message = "Supports retrieving version"))] Version {}, } diff --git a/distant-protocol/src/response.rs b/distant-protocol/src/response.rs index c73812e..2ce3a54 100644 --- a/distant-protocol/src/response.rs +++ b/distant-protocol/src/response.rs @@ -2013,19 +2013,14 @@ mod tests { mod version { use super::*; - use crate::common::{Capabilities, Capability}; + use crate::semver::Version as SemVer; #[test] fn should_be_able_to_serialize_to_json() { let payload = Response::Version(Version { - server_version: String::from("some version"), - protocol_version: (1, 2, 3), - capabilities: [Capability { - kind: String::from("some kind"), - description: String::from("some description"), - }] - .into_iter() - .collect(), + server_version: "123.456.789-rc+build".parse().unwrap(), + protocol_version: SemVer::new(1, 2, 3), + capabilities: vec![String::from("cap")], }); let value = serde_json::to_value(payload).unwrap(); @@ -2033,12 +2028,9 @@ mod tests { value, serde_json::json!({ "type": "version", - "server_version": "some version", - "protocol_version": [1, 2, 3], - "capabilities": [{ - "kind": "some kind", - "description": "some description", - }], + "server_version": "123.456.789-rc+build", + "protocol_version": "1.2.3", + "capabilities": ["cap"], }) ); } @@ -2047,18 +2039,18 @@ mod tests { fn should_be_able_to_deserialize_from_json() { let value = serde_json::json!({ "type": "version", - "server_version": "some version", - "protocol_version": [1, 2, 3], - "capabilities": Capabilities::all(), + "server_version": "123.456.789-rc+build", + "protocol_version": "1.2.3", + "capabilities": ["cap"], }); let payload: Response = serde_json::from_value(value).unwrap(); assert_eq!( payload, Response::Version(Version { - server_version: String::from("some version"), - protocol_version: (1, 2, 3), - capabilities: Capabilities::all(), + server_version: "123.456.789-rc+build".parse().unwrap(), + protocol_version: SemVer::new(1, 2, 3), + capabilities: vec![String::from("cap")], }) ); } @@ -2066,9 +2058,9 @@ mod tests { #[test] fn should_be_able_to_serialize_to_msgpack() { let payload = Response::Version(Version { - server_version: String::from("some version"), - protocol_version: (1, 2, 3), - capabilities: Capabilities::all(), + server_version: "123.456.789-rc+build".parse().unwrap(), + protocol_version: SemVer::new(1, 2, 3), + capabilities: vec![String::from("cap")], }); // NOTE: We don't actually check the errput here because it's an implementation detail @@ -2085,9 +2077,9 @@ mod tests { // client/server and then trying to deserialize on the other side. This has happened // enough times with minor changes that we need tests to verify. let buf = rmp_serde::encode::to_vec_named(&Response::Version(Version { - server_version: String::from("some version"), - protocol_version: (1, 2, 3), - capabilities: Capabilities::all(), + server_version: "123.456.789-rc+build".parse().unwrap(), + protocol_version: SemVer::new(1, 2, 3), + capabilities: vec![String::from("cap")], })) .unwrap(); @@ -2095,9 +2087,9 @@ mod tests { assert_eq!( payload, Response::Version(Version { - server_version: String::from("some version"), - protocol_version: (1, 2, 3), - capabilities: Capabilities::all(), + server_version: "123.456.789-rc+build".parse().unwrap(), + protocol_version: SemVer::new(1, 2, 3), + capabilities: vec![String::from("cap")], }) ); } diff --git a/distant-ssh2/Cargo.toml b/distant-ssh2/Cargo.toml index 457201f..2beb63f 100644 --- a/distant-ssh2/Cargo.toml +++ b/distant-ssh2/Cargo.toml @@ -2,7 +2,7 @@ name = "distant-ssh2" description = "Library to enable native ssh-2 protocol for use with distant sessions" categories = ["network-programming"] -version = "0.20.0-alpha.12" +version = "0.20.0-alpha.13" authors = ["Chip Senkbeil "] edition = "2021" homepage = "https://github.com/chipsenkbeil/distant" @@ -20,7 +20,7 @@ async-compat = "0.2.1" async-once-cell = "0.5.2" async-trait = "0.1.68" derive_more = { version = "0.99.17", default-features = false, features = ["display", "error"] } -distant-core = { version = "=0.20.0-alpha.12", path = "../distant-core" } +distant-core = { version = "=0.20.0-alpha.13", path = "../distant-core" } futures = "0.3.28" hex = "0.4.3" log = "0.4.18" diff --git a/distant-ssh2/src/api.rs b/distant-ssh2/src/api.rs index 093388b..b75bcc9 100644 --- a/distant-ssh2/src/api.rs +++ b/distant-ssh2/src/api.rs @@ -7,9 +7,10 @@ use std::time::Duration; use async_compat::CompatExt; use async_once_cell::OnceCell; use async_trait::async_trait; +use distant_core::protocol::semver; use distant_core::protocol::{ - Capabilities, CapabilityKind, DirEntry, Environment, FileType, Metadata, Permissions, - ProcessId, PtySize, SetPermissionsOptions, SystemInfo, UnixMetadata, Version, PROTOCOL_VERSION, + DirEntry, Environment, FileType, Metadata, Permissions, ProcessId, PtySize, + SetPermissionsOptions, SystemInfo, UnixMetadata, Version, PROTOCOL_VERSION, }; use distant_core::{DistantApi, DistantCtx}; use log::*; @@ -936,18 +937,33 @@ impl DistantApi for SshDistantApi { async fn version(&self, ctx: DistantCtx) -> io::Result { debug!("[Conn {}] Querying capabilities", ctx.connection_id); - let mut capabilities = Capabilities::all(); - - // Searching is not supported by ssh implementation - // TODO: Could we have external search using ripgrep's JSON lines API? - capabilities.take(CapabilityKind::Search); - capabilities.take(CapabilityKind::CancelSearch); - - // Broken via wezterm-ssh, so not supported right now - capabilities.take(CapabilityKind::SetPermissions); + let capabilities = vec![ + Version::CAP_EXEC.to_string(), + Version::CAP_FS_IO.to_string(), + Version::CAP_SYS_INFO.to_string(), + ]; + + // Parse our server's version + let mut server_version: semver::Version = env!("CARGO_PKG_VERSION") + .parse() + .map_err(|x| io::Error::new(io::ErrorKind::Other, x))?; + + // Add the package name to the version information + if server_version.build.is_empty() { + server_version.build = semver::BuildMetadata::new(env!("CARGO_PKG_NAME")) + .map_err(|x| io::Error::new(io::ErrorKind::Other, x))?; + } else { + let raw_build_str = format!( + "{}.{}", + server_version.build.as_str(), + env!("CARGO_PKG_NAME") + ); + server_version.build = semver::BuildMetadata::new(&raw_build_str) + .map_err(|x| io::Error::new(io::ErrorKind::Other, x))?; + } Ok(Version { - server_version: format!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")), + server_version, protocol_version: PROTOCOL_VERSION, capabilities, }) diff --git a/distant-ssh2/src/lib.rs b/distant-ssh2/src/lib.rs index aefe3be..94381eb 100644 --- a/distant-ssh2/src/lib.rs +++ b/distant-ssh2/src/lib.rs @@ -19,8 +19,9 @@ use async_compat::CompatExt; use async_trait::async_trait; use distant_core::net::auth::{AuthHandlerMap, DummyAuthHandler, Verifier}; use distant_core::net::client::{Client, ClientConfig}; -use distant_core::net::common::{Host, InmemoryTransport, OneshotListener}; +use distant_core::net::common::{Host, InmemoryTransport, OneshotListener, Version}; use distant_core::net::server::{Server, ServerRef}; +use distant_core::protocol::PROTOCOL_VERSION; use distant_core::{DistantApiServerHandler, DistantClient, DistantSingleKeyCredentials}; use log::*; use smol::channel::Receiver as SmolReceiver; @@ -588,6 +589,11 @@ impl Ssh { match Client::tcp(addr) .auth_handler(AuthHandlerMap::new().with_static_key(key.clone())) .connect_timeout(timeout) + .version(Version::new( + PROTOCOL_VERSION.major, + PROTOCOL_VERSION.minor, + PROTOCOL_VERSION.patch, + )) .connect() .await { diff --git a/src/cli/commands/client.rs b/src/cli/commands/client.rs index 84981c8..a043d81 100644 --- a/src/cli/commands/client.rs +++ b/src/cli/commands/client.rs @@ -7,10 +7,10 @@ use std::time::Duration; use anyhow::Context; use distant_core::net::common::{ConnectionId, Host, Map, Request, Response}; use distant_core::net::manager::ManagerClient; +use distant_core::protocol::semver; use distant_core::protocol::{ - self, Capabilities, ChangeKind, ChangeKindSet, FileType, Permissions, SearchQuery, - SearchQueryContentsMatch, SearchQueryMatch, SearchQueryPathMatch, SetPermissionsOptions, - SystemInfo, + self, ChangeKind, ChangeKindSet, FileType, Permissions, SearchQuery, SearchQueryContentsMatch, + SearchQueryMatch, SearchQueryPathMatch, SetPermissionsOptions, SystemInfo, Version, }; use distant_core::{DistantChannel, DistantChannelExt, RemoteCommand, Searcher, Watcher}; use log::*; @@ -581,32 +581,51 @@ async fn async_run(cmd: ClientSubcommand) -> CliResult { match format { Format::Shell => { - let (major, minor, patch) = distant_core::protocol::PROTOCOL_VERSION; + let mut client_version: semver::Version = env!("CARGO_PKG_VERSION") + .parse() + .context("Failed to parse client version")?; + + // Add the package name to the version information + if client_version.build.is_empty() { + client_version.build = semver::BuildMetadata::new(env!("CARGO_PKG_NAME")) + .context("Failed to define client build metadata")?; + } else { + let raw_build_str = format!( + "{}.{}", + client_version.build.as_str(), + env!("CARGO_PKG_NAME") + ); + client_version.build = semver::BuildMetadata::new(&raw_build_str) + .context("Failed to define client build metadata")?; + } + println!( - "Client: {} {} (Protocol {major}.{minor}.{patch})", - env!("CARGO_PKG_NAME"), - env!("CARGO_PKG_VERSION") + "Client: {client_version} (Protocol {})", + distant_core::protocol::PROTOCOL_VERSION ); - let (major, minor, patch) = version.protocol_version; println!( - "Server: {} (Protocol {major}.{minor}.{patch})", - version.server_version + "Server: {} (Protocol {})", + version.server_version, version.protocol_version ); // Build a complete set of capabilities to show which ones we support - let client_capabilities = Capabilities::all(); - let server_capabilities = version.capabilities; - let mut capabilities: Vec = client_capabilities - .union(server_capabilities.as_ref()) - .map(|cap| { - let kind = &cap.kind; - if client_capabilities.contains(kind) - && server_capabilities.contains(kind) - { - format!("+{kind}") + let mut capabilities: HashMap = Version::capabilities() + .iter() + .map(|cap| (cap.to_string(), 1)) + .collect(); + + for cap in version.capabilities { + *capabilities.entry(cap).or_default() += 1; + } + + let mut capabilities: Vec = capabilities + .into_iter() + .map(|(cap, cnt)| { + if cnt > 1 { + format!("+{cap}") } else { - format!("-{kind}") + format!("-{cap}") } }) .collect(); diff --git a/src/cli/commands/manager.rs b/src/cli/commands/manager.rs index 3eca61d..a75394f 100644 --- a/src/cli/commands/manager.rs +++ b/src/cli/commands/manager.rs @@ -228,41 +228,24 @@ async fn async_run(cmd: ManagerSubcommand) -> CliResult { Ok(()) } - ManagerSubcommand::Capabilities { format, network } => { + ManagerSubcommand::Version { format, network } => { debug!("Connecting to manager"); let mut client = connect_to_manager(format, network).await?; - debug!("Getting list of capabilities"); - let caps = client - .capabilities() - .await - .context("Failed to get list of capabilities")?; - debug!("Got capabilities: {caps:?}"); + debug!("Getting version"); + let version = client.version().await.context("Failed to get version")?; + debug!("Got version: {version}"); match format { Format::Json => { println!( "{}", - serde_json::to_string(&caps) - .context("Failed to format capabilities as json")? + serde_json::to_string(&serde_json::json!({ "version": version })) + .context("Failed to format version as json")? ); } Format::Shell => { - #[derive(Tabled)] - struct CapabilityRow { - kind: String, - description: String, - } - - println!( - "{}", - Table::new(caps.into_sorted_vec().into_iter().map(|cap| { - CapabilityRow { - kind: cap.kind, - description: cap.description, - } - })) - ); + println!("{version}"); } } diff --git a/src/cli/commands/manager/handlers.rs b/src/cli/commands/manager/handlers.rs index 9179166..74d7200 100644 --- a/src/cli/commands/manager/handlers.rs +++ b/src/cli/commands/manager/handlers.rs @@ -11,8 +11,9 @@ use distant_core::net::auth::{ StaticKeyAuthMethodHandler, }; use distant_core::net::client::{Client, ClientConfig, ReconnectStrategy, UntypedClient}; -use distant_core::net::common::{Destination, Map, SecretKey32}; +use distant_core::net::common::{Destination, Map, SecretKey32, Version}; use distant_core::net::manager::{ConnectHandler, LaunchHandler}; +use distant_core::protocol::PROTOCOL_VERSION; use log::*; use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::process::{Child, Command}; @@ -247,6 +248,11 @@ impl DistantConnectHandler { ..Default::default() }) .connect_timeout(Duration::from_secs(180)) + .version(Version::new( + PROTOCOL_VERSION.major, + PROTOCOL_VERSION.minor, + PROTOCOL_VERSION.patch, + )) .connect_untyped() .await { diff --git a/src/cli/commands/server.rs b/src/cli/commands/server.rs index 81f1d1b..d3d3740 100644 --- a/src/cli/commands/server.rs +++ b/src/cli/commands/server.rs @@ -2,8 +2,9 @@ use std::io::{self, Read, Write}; use anyhow::Context; use distant_core::net::auth::Verifier; -use distant_core::net::common::{Host, SecretKey32}; +use distant_core::net::common::{Host, SecretKey32, Version}; use distant_core::net::server::{Server, ServerConfig as NetServerConfig}; +use distant_core::protocol::PROTOCOL_VERSION; use distant_core::DistantSingleKeyCredentials; use distant_local::{Config as LocalConfig, WatchConfig as LocalWatchConfig}; use log::*; @@ -159,6 +160,11 @@ async fn async_run(cmd: ServerSubcommand, _is_forked: bool) -> CliResult { }) .handler(handler) .verifier(Verifier::static_key(key.clone())) + .version(Version::new( + PROTOCOL_VERSION.major, + PROTOCOL_VERSION.minor, + PROTOCOL_VERSION.patch, + )) .start(addr, port) .await .with_context(|| format!("Failed to start server @ {addr} with {port}"))?; diff --git a/src/cli/common/client.rs b/src/cli/common/client.rs index 73dd481..3f590cf 100644 --- a/src/cli/common/client.rs +++ b/src/cli/common/client.rs @@ -7,7 +7,7 @@ use distant_core::net::auth::{ AuthHandler, AuthMethodHandler, PromptAuthMethodHandler, SingleAuthHandler, }; use distant_core::net::client::{Client as NetClient, ClientConfig, ReconnectStrategy}; -use distant_core::net::manager::ManagerClient; +use distant_core::net::manager::{ManagerClient, PROTOCOL_VERSION}; use log::*; use crate::cli::common::{MsgReceiver, MsgSender}; @@ -71,6 +71,7 @@ impl Client { }, ..Default::default() }) + .version(PROTOCOL_VERSION) .connect() .await { @@ -113,6 +114,7 @@ impl Client { }, ..Default::default() }) + .version(PROTOCOL_VERSION) .connect() .await { diff --git a/src/cli/common/manager.rs b/src/cli/common/manager.rs index 5aa4e25..de3c928 100644 --- a/src/cli/common/manager.rs +++ b/src/cli/common/manager.rs @@ -1,6 +1,6 @@ use anyhow::Context; use distant_core::net::auth::Verifier; -use distant_core::net::manager::{Config as ManagerConfig, ManagerServer}; +use distant_core::net::manager::{Config as ManagerConfig, ManagerServer, PROTOCOL_VERSION}; use distant_core::net::server::ServerRef; use log::*; @@ -18,6 +18,9 @@ impl Manager { pub async fn listen(self) -> anyhow::Result { let user = self.config.user; + // Version we'll use to report compatibility in talking to the manager + let version = PROTOCOL_VERSION; + #[cfg(unix)] { use distant_core::net::common::UnixSocketListener; @@ -38,6 +41,7 @@ impl Manager { let server = ManagerServer::new(self.config) .verifier(Verifier::none()) + .version(version) .start( UnixSocketListener::bind_with_permissions(socket_path, self.access.into_mode()) .await?, @@ -59,6 +63,7 @@ impl Manager { let server = ManagerServer::new(self.config) .verifier(Verifier::none()) + .version(version) .start(WindowsPipeListener::bind_local(pipe_name)?) .with_context(|| format!("Failed to start manager at pipe {pipe_name:?}"))?; diff --git a/src/lib.rs b/src/lib.rs index 81017da..57adf26 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -122,18 +122,27 @@ impl Termination for MainResult { CliError::Exit(code) => ExitCode::from(code), CliError::Error(x) => { match self.format { - Format::Shell => eprintln!("{x}"), + // For anyhow, we want to print with debug information, which includes the + // full stack of information that anyhow collects; otherwise, we would only + // include the top-level context. + Format::Shell => eprintln!("{x:?}"), + Format::Json => println!( "{}", serde_json::to_string(&serde_json::json!({ "type": "error", - "msg": x.to_string(), + "msg": format!("{x:?}"), }),) .expect("Failed to format error to JSON") ), } + + // For anyhow, we want to log with debug information, which includes the full + // stack of information that anyhow collects; otherwise, we would only include + // the top-level context. ::log::error!("{x:?}"); ::log::logger().flush(); + ExitCode::FAILURE } }, diff --git a/src/options.rs b/src/options.rs index d77821b..67f3fe9 100644 --- a/src/options.rs +++ b/src/options.rs @@ -171,7 +171,7 @@ impl Options { DistantSubcommand::Manager(cmd) => { update_logging!(manager); match cmd { - ManagerSubcommand::Capabilities { network, .. } => { + ManagerSubcommand::Version { network, .. } => { network.merge(config.manager.network); } ManagerSubcommand::Info { network, .. } => { @@ -1054,7 +1054,7 @@ pub enum ManagerSubcommand { }, /// Retrieve a list of capabilities that the manager supports - Capabilities { + Version { #[clap(short, long, default_value_t, value_enum)] format: Format, @@ -1111,7 +1111,7 @@ impl ManagerSubcommand { Self::Select { format, .. } => *format, Self::Service(_) => Format::Shell, Self::Listen { .. } => Format::Shell, - Self::Capabilities { format, .. } => *format, + Self::Version { format, .. } => *format, Self::Info { format, .. } => *format, Self::List { format, .. } => *format, Self::Kill { format, .. } => *format, @@ -3505,7 +3505,7 @@ mod tests { log_file: None, log_level: None, }, - command: DistantSubcommand::Manager(ManagerSubcommand::Capabilities { + command: DistantSubcommand::Manager(ManagerSubcommand::Version { format: Format::Json, network: NetworkSettings { unix_socket: None, @@ -3537,7 +3537,7 @@ mod tests { log_file: Some(PathBuf::from("config-log-file")), log_level: Some(LogLevel::Trace), }, - command: DistantSubcommand::Manager(ManagerSubcommand::Capabilities { + command: DistantSubcommand::Manager(ManagerSubcommand::Version { format: Format::Json, network: NetworkSettings { unix_socket: Some(PathBuf::from("config-unix-socket")), @@ -3556,7 +3556,7 @@ mod tests { log_file: Some(PathBuf::from("cli-log-file")), log_level: Some(LogLevel::Info), }, - command: DistantSubcommand::Manager(ManagerSubcommand::Capabilities { + command: DistantSubcommand::Manager(ManagerSubcommand::Version { format: Format::Json, network: NetworkSettings { unix_socket: Some(PathBuf::from("cli-unix-socket")), @@ -3588,7 +3588,7 @@ mod tests { log_file: Some(PathBuf::from("cli-log-file")), log_level: Some(LogLevel::Info), }, - command: DistantSubcommand::Manager(ManagerSubcommand::Capabilities { + command: DistantSubcommand::Manager(ManagerSubcommand::Version { format: Format::Json, network: NetworkSettings { unix_socket: Some(PathBuf::from("cli-unix-socket")), diff --git a/tests/cli/api/version.rs b/tests/cli/api/version.rs index 2dd9972..b930695 100644 --- a/tests/cli/api/version.rs +++ b/tests/cli/api/version.rs @@ -1,4 +1,5 @@ -use distant_core::protocol::{Capabilities, Capability, SemVer, PROTOCOL_VERSION}; +use distant_core::protocol::semver::Version as SemVer; +use distant_core::protocol::{Version, PROTOCOL_VERSION}; use rstest::*; use serde_json::json; use test_log::test; @@ -25,17 +26,17 @@ async fn should_support_json_capabilities(mut api_process: CtxCommand = res["payload"]["capabilities"] .as_array() - .expect("Field 'supported' was not an array") + .expect("Field 'capabilities' was not an array") .iter() .map(|value| { - serde_json::from_value::(value.clone()) - .expect("Could not read array value as capability") + serde_json::from_value::(value.clone()) + .expect("Could not read array value as string") }) .collect(); // NOTE: Our local server api should always support all capabilities since it is the reference // implementation for our api - assert_eq!(capabilities, Capabilities::all()); + assert_eq!(capabilities, Version::capabilities()); } diff --git a/tests/cli/client/version.rs b/tests/cli/client/version.rs index f822cf8..1cb0a0c 100644 --- a/tests/cli/client/version.rs +++ b/tests/cli/client/version.rs @@ -1,3 +1,4 @@ +use distant_core::protocol::semver; use distant_core::protocol::PROTOCOL_VERSION; use rstest::*; @@ -8,22 +9,40 @@ use crate::common::utils::TrimmedLinesMatchPredicate; #[test_log::test] fn should_output_capabilities(ctx: DistantManagerCtx) { // Because all of our crates have the same version, we can expect it to match - let package_name = "distant-local"; - let package_version = env!("CARGO_PKG_VERSION"); - let (major, minor, patch) = PROTOCOL_VERSION; + let version: semver::Version = env!("CARGO_PKG_VERSION").parse().unwrap(); + + // Add the package name to the client version information + let client_version = if version.build.is_empty() { + let mut version = version.clone(); + version.build = semver::BuildMetadata::new(env!("CARGO_PKG_NAME")).unwrap(); + version + } else { + let mut version = version.clone(); + let raw_build_str = format!("{}.{}", version.build.as_str(), env!("CARGO_PKG_NAME")); + version.build = semver::BuildMetadata::new(&raw_build_str).unwrap(); + version + }; + + // Add the distant-local to the server version information + let server_version = if version.build.is_empty() { + let mut version = version; + version.build = semver::BuildMetadata::new("distant-local").unwrap(); + version + } else { + let raw_build_str = format!("{}.{}", version.build.as_str(), "distant-local"); + let mut version = version; + version.build = semver::BuildMetadata::new(&raw_build_str).unwrap(); + version + }; // Since our client and server are built the same, all capabilities should be listed with + // and using 4 columns since we are not using a tty let expected = indoc::formatdoc! {" - Client: distant {package_version} (Protocol {major}.{minor}.{patch}) - Server: {package_name} {package_version} (Protocol {major}.{minor}.{patch}) + Client: {client_version} (Protocol {PROTOCOL_VERSION}) + Server: {server_version} (Protocol {PROTOCOL_VERSION}) Capabilities supported (+) or not (-): - +cancel_search +copy +dir_create +dir_read - +exists +file_append +file_append_text +file_read - +file_read_text +file_write +file_write_text +metadata - +proc_kill +proc_resize_pty +proc_spawn +proc_stdin - +remove +rename +search +set_permissions - +system_info +unwatch +version +watch + +exec +fs_io +fs_perm +fs_search + +fs_watch +sys_info "}; ctx.cmd("version") diff --git a/tests/cli/manager/capabilities.rs b/tests/cli/manager/capabilities.rs deleted file mode 100644 index 2908222..0000000 --- a/tests/cli/manager/capabilities.rs +++ /dev/null @@ -1,41 +0,0 @@ -use indoc::indoc; -use rstest::*; - -use crate::common::fixtures::*; - -const EXPECTED_TABLE: &str = indoc! {" -+---------------+--------------------------------------------------------------+ -| kind | description | -+---------------+--------------------------------------------------------------+ -| authenticate | Supports authenticating with a remote server | -+---------------+--------------------------------------------------------------+ -| capabilities | Supports retrieving capabilities | -+---------------+--------------------------------------------------------------+ -| channel | Supports sending data through a channel with a remote server | -+---------------+--------------------------------------------------------------+ -| close_channel | Supports closing a channel with a remote server | -+---------------+--------------------------------------------------------------+ -| connect | Supports connecting to remote servers | -+---------------+--------------------------------------------------------------+ -| info | Supports retrieving connection-specific information | -+---------------+--------------------------------------------------------------+ -| kill | Supports killing a remote connection | -+---------------+--------------------------------------------------------------+ -| launch | Supports launching a server on remote machines | -+---------------+--------------------------------------------------------------+ -| list | Supports retrieving a list of managed connections | -+---------------+--------------------------------------------------------------+ -| open_channel | Supports opening a channel with a remote server | -+---------------+--------------------------------------------------------------+ -"}; - -#[rstest] -#[test_log::test] -fn should_output_capabilities(ctx: DistantManagerCtx) { - // distant action capabilities - ctx.new_assert_cmd(vec!["manager", "capabilities"]) - .assert() - .success() - .stdout(EXPECTED_TABLE) - .stderr(""); -} diff --git a/tests/cli/manager/mod.rs b/tests/cli/manager/mod.rs index 98a3830..81c2f21 100644 --- a/tests/cli/manager/mod.rs +++ b/tests/cli/manager/mod.rs @@ -1 +1 @@ -mod capabilities; +mod version; diff --git a/tests/cli/manager/version.rs b/tests/cli/manager/version.rs new file mode 100644 index 0000000..40038fb --- /dev/null +++ b/tests/cli/manager/version.rs @@ -0,0 +1,13 @@ +use rstest::*; + +use crate::common::fixtures::*; + +#[rstest] +#[test_log::test] +fn should_output_version(ctx: DistantManagerCtx) { + ctx.new_assert_cmd(vec!["manager", "version"]) + .assert() + .success() + .stdout(format!("{}\n", env!("CARGO_PKG_VERSION"))) + .stderr(""); +}