From 3c7561bef8c2dcea2aa609593cf64e06f6a1f9e1 Mon Sep 17 00:00:00 2001 From: Chip Senkbeil Date: Tue, 27 Jul 2021 00:14:35 -0500 Subject: [PATCH] Implemented broken framed logic --- Cargo.lock | 147 ++++++++++++++++++++++++++++++++ Cargo.toml | 4 + src/data.rs | 12 +++ src/lib.rs | 2 + src/net/codec.rs | 84 ++++++++++++++++++ src/net/mod.rs | 80 +++++++++++++++++ src/opt.rs | 4 + src/subcommand/clear_session.rs | 7 ++ src/subcommand/execute.rs | 62 +++++--------- src/subcommand/launch.rs | 21 +++-- src/subcommand/listen.rs | 48 +++++++++-- src/subcommand/mod.rs | 1 + src/utils.rs | 126 +++++++++++++++++++++++++++ 13 files changed, 537 insertions(+), 61 deletions(-) create mode 100644 src/net/codec.rs create mode 100644 src/net/mod.rs create mode 100644 src/subcommand/clear_session.rs create mode 100644 src/utils.rs diff --git a/Cargo.lock b/Cargo.lock index 82d1eb4..d80d4cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -121,9 +121,11 @@ dependencies = [ name = "distant" version = "0.1.0" dependencies = [ + "bytes", "derive_more", "directories", "fork", + "futures", "hex", "lazy_static", "log", @@ -134,6 +136,8 @@ dependencies = [ "stderrlog", "structopt", "tokio", + "tokio-stream", + "tokio-util", "whoami", ] @@ -146,6 +150,100 @@ dependencies = [ "libc", ] +[[package]] +name = "futures" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1adc00f486adfc9ce99f77d717836f0c5aa84965eb0b4f051f4e83f7cab53f8b" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74ed2411805f6e4e3d9bc904c95d5d423b89b3b25dc0250aa74729de20629ff9" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99" + +[[package]] +name = "futures-executor" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d0d535a57b87e1ae31437b892713aee90cd2d7b0ee48727cd11fc72ef54761c" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b0e06c393068f3a6ef246c75cdca793d6a46347e75286933e5e75fd2fd11582" + +[[package]] +name = "futures-macro" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c54913bae956fb8df7f4dc6fc90362aa72e69148e3f39041fbe8742d21e0ac57" +dependencies = [ + "autocfg", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f30aaa67363d119812743aa5f33c201a7a66329f97d1a887022971feea4b53" + +[[package]] +name = "futures-task" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe54a98670017f3be909561f6ad13e810d9a51f3f061b902062ca3da80799f2" + +[[package]] +name = "futures-util" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eb846bfd58e44a8481a00049e82c43e0ccb5d61f8dc071057cb19249dd4d78" +dependencies = [ + "autocfg", + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "proc-macro-hack", + "proc-macro-nested", + "slab", +] + [[package]] name = "getrandom" version = "0.2.3" @@ -356,6 +454,12 @@ version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -380,6 +484,18 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + +[[package]] +name = "proc-macro-nested" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" + [[package]] name = "proc-macro2" version = "1.0.28" @@ -479,6 +595,12 @@ dependencies = [ "libc", ] +[[package]] +name = "slab" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527" + [[package]] name = "smallvec" version = "1.6.1" @@ -613,6 +735,31 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-stream" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2f3f698253f03119ac0102beaa64f67a67e08074d03a22d18784104543727f" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1caa0b0c8d94a049db56b5acf8cba99dc0623aab1b26d5b5f5e2d945846b3592" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "tokio", +] + [[package]] name = "unicode-segmentation" version = "1.8.0" diff --git a/Cargo.toml b/Cargo.toml index 665ca53..b8873c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,9 +11,11 @@ lto = true codegen-units = 1 [dependencies] +bytes = "1.0.1" derive_more = { version = "0.99.16", default-features = false, features = ["display", "from", "error"] } directories = "3.0.2" fork = "0.1.18" +futures = "0.3.16" hex = "0.4.3" log = "0.4.14" orion = "0.16.0" @@ -21,6 +23,8 @@ serde = { version = "1.0.126", features = ["derive"] } serde_cbor = "0.11.1" serde_json = "1.0.64" tokio = { version = "1.9.0", features = ["full"] } +tokio-stream = "0.1.7" +tokio-util = { version = "0.6.7", features = ["codec"] } # Binary-specific dependencies lazy_static = "1.4.0" diff --git a/src/data.rs b/src/data.rs index d116dbd..484ec5f 100644 --- a/src/data.rs +++ b/src/data.rs @@ -110,6 +110,18 @@ pub enum Operation { detach: bool, }, + /// Re-connects to a detached process on the remote machine (to receive stdout/stderr) + ProcConnect { + /// Id of the actively-running process + id: usize, + }, + + /// Kills a process running on the remote machine + ProcKill { + /// Id of the actively-running process + id: usize, + }, + /// Sends additional data to stdin of running process ProcStdin { /// Id of the actively-running process to send stdin data diff --git a/src/lib.rs b/src/lib.rs index a624b3c..715e532 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,8 @@ mod data; +mod net; mod opt; mod subcommand; +mod utils; pub use opt::Opt; use std::path::PathBuf; diff --git a/src/net/codec.rs b/src/net/codec.rs new file mode 100644 index 0000000..71460db --- /dev/null +++ b/src/net/codec.rs @@ -0,0 +1,84 @@ +use bytes::{Buf, BufMut, Bytes, BytesMut}; +use derive_more::{Display, Error, From}; +use tokio::io; +use tokio_util::codec::{Decoder, Encoder}; + +/// Represents a marker to indicate the beginning of the next message +static MSG_START: &'static [u8] = b";start;"; + +/// Represents a marker to indicate the end of the next message +static MSG_END: &'static [u8] = b";end;"; + +#[inline] +fn packet_size(msg_size: usize) -> usize { + MSG_START.len() + msg_size + MSG_END.len() +} + +/// Possible errors that can occur during encoding and decoding +#[derive(Debug, Display, Error, From)] +pub enum DistantCodecError { + #[display(fmt = "Corrupt Marker: {:?}", _0)] + CorruptMarker(#[error(not(source))] Bytes), + IoError(io::Error), +} + +/// Represents the codec to encode and decode data for transmission +pub struct DistantCodec; + +impl<'a> Encoder<&'a [u8]> for DistantCodec { + type Error = DistantCodecError; + + fn encode(&mut self, item: &'a [u8], dst: &mut BytesMut) -> Result<(), Self::Error> { + // Add our full packet to the bytes + dst.reserve(packet_size(item.len())); + dst.put(MSG_START); + dst.put(item); + dst.put(MSG_END); + + Ok(()) + } +} + +impl Decoder for DistantCodec { + type Item = Vec; + type Error = DistantCodecError; + + fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { + // First, check if we have more data than just our markers, if not we say that it's okay + // but that we're waiting + if src.len() <= (MSG_START.len() + MSG_END.len()) { + return Ok(None); + } + + // Second, verify that our first N bytes match our start marker + let marker_start = &src[..MSG_START.len()]; + if marker_start != MSG_START { + return Err(DistantCodecError::CorruptMarker(Bytes::copy_from_slice( + marker_start, + ))); + } + + // Third, find end of message marker by scanning the available bytes, and + // consume a full packet of bytes + let mut maybe_frame = None; + for i in (MSG_START.len() + 1)..(src.len() - MSG_END.len()) { + let marker_end = &src[i..(i + MSG_END.len())]; + if marker_end == MSG_END { + maybe_frame = Some(src.split_to(i + MSG_END.len())); + break; + } + } + + // Fourth, return our msg if it's available, stripping it of the start and end markers + if let Some(frame) = maybe_frame { + let data = &frame[MSG_START.len()..(frame.len() - MSG_END.len())]; + + // Advance so frame is no longer kept around + src.advance(frame.len()); + + Ok(Some(data.to_vec())) + } else { + Ok(None) + } + } +} diff --git a/src/net/mod.rs b/src/net/mod.rs new file mode 100644 index 0000000..4d03f35 --- /dev/null +++ b/src/net/mod.rs @@ -0,0 +1,80 @@ +use crate::utils::Session; +use codec::{DistantCodec, DistantCodecError}; +use derive_more::{Display, Error, From}; +use futures::SinkExt; +use orion::{ + aead::{self, SecretKey}, + errors::UnknownCryptoError, +}; +use serde::{de::DeserializeOwned, Serialize}; +use std::sync::Arc; +use tokio::{io, net::TcpStream}; +use tokio_stream::StreamExt; +use tokio_util::codec::Framed; + +mod codec; + +#[derive(Debug, Display, Error, From)] +pub enum TransportError { + CodecError(DistantCodecError), + EncryptError(UnknownCryptoError), + IoError(io::Error), + SerializeError(serde_cbor::Error), +} + +/// Represents a transport of data across the network +pub struct Transport { + inner: Framed, + key: Arc, +} + +impl Transport { + /// Wraps a `TcpStream` and associated credentials in a transport layer + pub fn new(stream: TcpStream, key: Arc) -> Self { + Self { + inner: Framed::new(stream, DistantCodec), + key, + } + } + + /// Establishes a connection using the provided session + pub async fn connect(session: Session) -> io::Result { + let stream = TcpStream::connect(session.to_socket_addr().await?).await?; + Ok(Self::new(stream, Arc::new(session.key))) + } + + /// Sends some data across the wire + pub async fn send(&mut self, data: T) -> Result<(), TransportError> { + // Serialize, encrypt, and then (TODO) sign + let data = serde_cbor::ser::to_vec_packed(&data)?; + let data = aead::seal(&self.key, &data)?; + + self.inner + .send(&data) + .await + .map_err(TransportError::CodecError) + } + + /// Receives some data from out on the wire, waiting until it's available + pub async fn receive(&mut self) -> Result { + loop { + if let Some(data) = self.try_receive().await? { + break Ok(data); + } + } + } + + /// Attempts to receive some data from out on the wire, returning that data if available + /// or none if unavailable + pub async fn try_receive(&mut self) -> Result, TransportError> { + if let Some(data) = self.inner.next().await { + // Validate (TODO), decrypt, and then deserialize + let data = data?; + let data = aead::open(&self.key, &data)?; + let data = serde_cbor::from_slice(&data)?; + Ok(Some(data)) + } else { + Ok(None) + } + } +} diff --git a/src/opt.rs b/src/opt.rs index fd206b2..2f47f58 100644 --- a/src/opt.rs +++ b/src/opt.rs @@ -45,6 +45,9 @@ pub struct CommonOpt { #[derive(Debug, StructOpt)] pub enum Subcommand { + /// Clears the global session file + ClearSession, + #[structopt(visible_aliases = &["exec", "x"])] Execute(ExecuteSubcommand), Launch(LaunchSubcommand), @@ -55,6 +58,7 @@ impl Subcommand { /// Runs the subcommand, returning the result pub fn run(self) -> Result<(), Box> { match self { + Self::ClearSession => subcommand::clear_session::run()?, Self::Execute(cmd) => subcommand::execute::run(cmd)?, Self::Launch(cmd) => subcommand::launch::run(cmd)?, Self::Listen(cmd) => subcommand::listen::run(cmd)?, diff --git a/src/subcommand/clear_session.rs b/src/subcommand/clear_session.rs new file mode 100644 index 0000000..81eb4f3 --- /dev/null +++ b/src/subcommand/clear_session.rs @@ -0,0 +1,7 @@ +use crate::utils::Session; +use tokio::io; + +pub fn run() -> Result<(), io::Error> { + let rt = tokio::runtime::Runtime::new()?; + rt.block_on(async { Session::clear().await }) +} diff --git a/src/subcommand/execute.rs b/src/subcommand/execute.rs index 91b975d..ddb7ccc 100644 --- a/src/subcommand/execute.rs +++ b/src/subcommand/execute.rs @@ -1,26 +1,17 @@ -use crate::{opt::ExecuteSubcommand, SESSION_PATH}; +use crate::{ + data::Response, + net::{Transport, TransportError}, + opt::ExecuteSubcommand, + utils::{Session, SessionError}, +}; use derive_more::{Display, Error, From}; -use orion::aead::SecretKey; use tokio::io; #[derive(Debug, Display, Error, From)] pub enum Error { - #[display(fmt = "Invalid key for session")] - InvalidSessionKey, - - #[display(fmt = "Invalid port for session")] - InvalidSessionPort, - IoError(io::Error), - - #[display(fmt = "Missing key for session")] - MissingSessionKey, - - #[display(fmt = "Missing port for session")] - MissingSessionPort, - - #[display(fmt = "No session file: {:?}", SESSION_PATH.as_path())] - NoSessionFile, + SessionError(SessionError), + TransportError(TransportError), } pub fn run(cmd: ExecuteSubcommand) -> Result<(), Error> { @@ -30,34 +21,19 @@ pub fn run(cmd: ExecuteSubcommand) -> Result<(), Error> { } async fn run_async(cmd: ExecuteSubcommand) -> Result<(), Error> { - let (port, key) = load_session().await?; + let session = Session::load().await?; + let mut transport = Transport::connect(session).await?; - println!( - "PORT:{}; KEY:{}", - port, - hex::encode(key.unprotected_as_bytes()) - ); + // Send our operation + transport.send(cmd.operation).await?; - println!("FORMAT: {}", cmd.format); - println!("OPERATION: {:?}", cmd.operation); + // Continue to receive and process responses as long as we get them or we decide to end + loop { + let response = transport.receive::().await?; + println!("RESPONSE: {:?}", response); + } - Ok(()) -} + println!("DONE"); -async fn load_session() -> Result<(u16, SecretKey), Error> { - let text = tokio::fs::read_to_string(SESSION_PATH.as_path()) - .await - .map_err(|_| Error::NoSessionFile)?; - let mut tokens = text.split(' ').take(2); - let port = tokens - .next() - .ok_or(Error::MissingSessionPort)? - .parse::() - .map_err(|_| Error::InvalidSessionPort)?; - let key = SecretKey::from_slice( - &hex::decode(tokens.next().ok_or(Error::MissingSessionKey)?.to_string()) - .map_err(|_| Error::InvalidSessionKey)?, - ) - .map_err(|_| Error::InvalidSessionKey)?; - Ok((port, key)) + Ok(()) } diff --git a/src/subcommand/launch.rs b/src/subcommand/launch.rs index 0810d7c..e446113 100644 --- a/src/subcommand/launch.rs +++ b/src/subcommand/launch.rs @@ -1,4 +1,4 @@ -use crate::{opt::LaunchSubcommand, PROJECT_DIRS, SESSION_PATH}; +use crate::{opt::LaunchSubcommand, utils::Session}; use derive_more::{Display, Error, From}; use hex::FromHexError; use orion::{aead::SecretKey, errors::UnknownCryptoError}; @@ -30,7 +30,7 @@ async fn run_async(cmd: LaunchSubcommand) -> Result<(), Error> { "{} -o StrictHostKeyChecking=no ssh://{}@{}:{} {} {}", cmd.ssh_program, cmd.username, - cmd.host, + cmd.host.as_str(), cmd.port, cmd.identity_file .map(|f| format!("-i {}", f.as_path().display())) @@ -75,18 +75,17 @@ async fn run_async(cmd: LaunchSubcommand) -> Result<(), Error> { // Write a session file containing our data for use in subsequent calls let (port, key) = result?; - let key_hex_str = hex::encode(key.unprotected_as_bytes()); + let session = Session { + host: cmd.host, + port, + key, + }; + + session.save().await?; if cmd.print_startup_data { - println!("DISTANT DATA {} {}", port, key_hex_str); + println!("DISTANT DATA {} {}", port, session.to_hex_key()); } - // Ensure our cache directory exists - let cache_dir = PROJECT_DIRS.cache_dir(); - tokio::fs::create_dir_all(cache_dir).await?; - - // Write our session file - tokio::fs::write(SESSION_PATH.as_path(), format!("{} {}", port, key_hex_str)).await?; - Ok(()) } diff --git a/src/subcommand/listen.rs b/src/subcommand/listen.rs index 7bdcd84..5952245 100644 --- a/src/subcommand/listen.rs +++ b/src/subcommand/listen.rs @@ -1,9 +1,13 @@ -use crate::opt::{ConvertToIpAddrError, ListenSubcommand}; +use crate::{ + data::{Operation, Response, ResponsePayload}, + net::{Transport, TransportError}, + opt::{ConvertToIpAddrError, ListenSubcommand}, +}; use derive_more::{Display, Error, From}; use fork::{daemon, Fork}; use orion::aead::SecretKey; -use std::string::FromUtf8Error; -use tokio::io; +use std::{string::FromUtf8Error, sync::Arc}; +use tokio::{io, net::TcpListener}; pub type Result = std::result::Result<(), Error>; @@ -36,16 +40,15 @@ pub fn run(cmd: ListenSubcommand) -> Result { rt.block_on(async { run_async(cmd, false).await })?; } - // MAC -> Decrypt Ok(()) } async fn run_async(cmd: ListenSubcommand, is_forked: bool) -> Result { let addr = cmd.host.to_ip_addr()?; let socket_addrs = cmd.port.make_socket_addrs(addr); - let listener = tokio::net::TcpListener::bind(socket_addrs.as_slice()).await?; + let listener = TcpListener::bind(socket_addrs.as_slice()).await?; let port = listener.local_addr()?.port(); - let key = SecretKey::default(); + let key = Arc::new(SecretKey::default()); // Print information about port, key, etc. unless told not to if !cmd.no_print_startup_data { @@ -59,7 +62,38 @@ async fn run_async(cmd: ListenSubcommand, is_forked: bool) -> Result { } } - // TODO: Implement server logic + // Begin our listen loop + loop { + // Wait for a client connection + let (client, _) = listener.accept().await?; + + // Build a transport around the client + let mut transport = Transport::new(client, Arc::clone(&key)); + + // Spawn a new task that loops to handle requests from the client + tokio::spawn(async move { + loop { + match transport.receive::().await { + Ok(_request) => { + let response = Response::Error { + msg: String::from("Unimplemented"), + }; + + if let Err(x) = transport.send(response).await { + eprintln!("ERROR: {:?}", x); + break; + } + } + Err(x) => { + eprintln!("ERROR: {:?}", x); + break; + } + } + } + }); + } + + #[allow(unreachable_code)] Ok(()) } diff --git a/src/subcommand/mod.rs b/src/subcommand/mod.rs index 1eeaafb..2a86c25 100644 --- a/src/subcommand/mod.rs +++ b/src/subcommand/mod.rs @@ -1,3 +1,4 @@ +pub mod clear_session; pub mod execute; pub mod launch; pub mod listen; diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..2f617c3 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,126 @@ +use crate::{PROJECT_DIRS, SESSION_PATH}; +use derive_more::{Display, Error, From}; +use orion::aead::SecretKey; +use std::net::{IpAddr, SocketAddr}; +use tokio::{io, net::lookup_host}; + +#[derive(Debug, Display, Error, From)] +pub enum SessionError { + #[display(fmt = "Bad hex key for session")] + BadSessionHexKey, + + #[display(fmt = "Invalid address for session")] + InvalidSessionAddr, + + #[display(fmt = "Invalid key for session")] + InvalidSessionKey, + + #[display(fmt = "Invalid port for session")] + InvalidSessionPort, + + IoError(io::Error), + + #[display(fmt = "Missing address for session")] + MissingSessionAddr, + + #[display(fmt = "Missing key for session")] + MissingSessionKey, + + #[display(fmt = "Missing port for session")] + MissingSessionPort, + + #[display(fmt = "No session file: {:?}", SESSION_PATH.as_path())] + NoSessionFile, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct Session { + pub host: String, + pub port: u16, + pub key: SecretKey, +} + +impl Session { + /// Returns a string representing the secret key as hex + pub fn to_hex_key(&self) -> String { + hex::encode(self.key.unprotected_as_bytes()) + } + + /// Returns the ip address associated with the session based on the host + pub async fn to_ip_addr(&self) -> io::Result { + let addr = match self.host.parse::() { + Ok(addr) => addr, + Err(_) => lookup_host((self.host.as_str(), self.port)) + .await? + .next() + .ok_or_else(|| { + io::Error::new(io::ErrorKind::NotFound, SessionError::InvalidSessionAddr) + })? + .ip(), + }; + + Ok(addr) + } + + /// Returns socket address associated with the session + pub async fn to_socket_addr(&self) -> io::Result { + let addr = self.to_ip_addr().await?; + Ok(SocketAddr::from((addr, self.port))) + } + + /// Clears the global session file + pub async fn clear() -> io::Result<()> { + tokio::fs::remove_file(SESSION_PATH.as_path()).await + } + + /// Saves a session to disk + pub async fn save(&self) -> io::Result<()> { + let key_hex_str = self.to_hex_key(); + + // Ensure our cache directory exists + let cache_dir = PROJECT_DIRS.cache_dir(); + tokio::fs::create_dir_all(cache_dir).await?; + + // Write our session file + let addr = self.to_ip_addr().await?; + tokio::fs::write( + SESSION_PATH.as_path(), + format!("{} {} {}", addr, self.port, key_hex_str), + ) + .await?; + + Ok(()) + } + + /// Loads a session's information into memory + pub async fn load() -> Result { + let text = tokio::fs::read_to_string(SESSION_PATH.as_path()) + .await + .map_err(|_| SessionError::NoSessionFile)?; + let mut tokens = text.split(' ').take(3); + + // First, load up the address without parsing it + let host = tokens + .next() + .ok_or(SessionError::MissingSessionAddr)? + .trim() + .to_string(); + + // Second, load up the port and parse it into a number + let port = tokens + .next() + .ok_or(SessionError::MissingSessionPort)? + .trim() + .parse::() + .map_err(|_| SessionError::InvalidSessionPort)?; + + // Third, load up the key and convert it back into a secret key from a hex slice + let key = SecretKey::from_slice( + &hex::decode(tokens.next().ok_or(SessionError::MissingSessionKey)?.trim()) + .map_err(|_| SessionError::BadSessionHexKey)?, + ) + .map_err(|_| SessionError::InvalidSessionKey)?; + + Ok(Session { host, port, key }) + } +}