diff --git a/appmgr/Cargo.lock b/appmgr/Cargo.lock index 60397766c..32b34e924 100644 --- a/appmgr/Cargo.lock +++ b/appmgr/Cargo.lock @@ -698,7 +698,7 @@ dependencies = [ "patch-db", "pin-project", "prettytable-rs", - "rand 0.8.4", + "rand 0.7.3", "regex", "reqwest", "rpassword", diff --git a/appmgr/Cargo.toml b/appmgr/Cargo.toml index 5065b9b0c..33a937b5c 100644 --- a/appmgr/Cargo.toml +++ b/appmgr/Cargo.toml @@ -68,7 +68,7 @@ openssl = "0.10.30" patch-db = { version="*", path="../../patch-db/patch-db" } pin-project = "1.0.6" prettytable-rs = "0.8.0" -rand = "0.8.3" +rand = "0.7.3" regex = "1.4.2" reqwest = { version="0.11.2", features=["stream", "json"] } rpassword = "5.0.0" diff --git a/appmgr/src/action/mod.rs b/appmgr/src/action/mod.rs index db09ae452..5963542d4 100644 --- a/appmgr/src/action/mod.rs +++ b/appmgr/src/action/mod.rs @@ -109,7 +109,7 @@ impl Action { } #[derive(Clone, Debug, Deserialize, Serialize, HasModel)] -#[serde(rename = "kebab-case")] +#[serde(rename_all = "kebab-case")] #[serde(tag = "type")] pub enum ActionImplementation { Docker(DockerAction), diff --git a/appmgr/src/bin/embassy-cli.rs b/appmgr/src/bin/embassy-cli.rs index 2e072b41c..083f6a492 100644 --- a/appmgr/src/bin/embassy-cli.rs +++ b/appmgr/src/bin/embassy-cli.rs @@ -4,13 +4,35 @@ use embassy::Error; use rpc_toolkit::run_cli; fn inner_main() -> Result<(), Error> { - simple_logging::log_to_stderr(log::LevelFilter::Info); run_cli!( embassy::main_api, - app => app.name("Embassy CLI") + app => app + .name("Embassy CLI") + .arg( + clap::Arg::with_name("config") + .short("c") + .long("config") + .takes_value(true), + ) + .arg( + clap::Arg::with_name("verbosity") + .short("v") + .multiple(true) + .takes_value(false), + ) .arg(Arg::with_name("host").long("host").short("h").takes_value(true)) .arg(Arg::with_name("port").long("port").short("p").takes_value(true)), - matches => EitherContext::Cli(CliContext::init(matches)?), + matches => { + simple_logging::log_to_stderr(match matches.occurrences_of("verbosity") { + 0 => log::LevelFilter::Off, + 1 => log::LevelFilter::Error, + 2 => log::LevelFilter::Warn, + 3 => log::LevelFilter::Info, + 4 => log::LevelFilter::Debug, + _ => log::LevelFilter::Trace, + }); + EitherContext::Cli(CliContext::init(matches)?) + }, |code| if code < 0 { 1 } else { code } ) } diff --git a/appmgr/src/bin/embassy-sdk.rs b/appmgr/src/bin/embassy-sdk.rs index 04fbb2fe5..f4d32f9bf 100644 --- a/appmgr/src/bin/embassy-sdk.rs +++ b/appmgr/src/bin/embassy-sdk.rs @@ -4,11 +4,33 @@ use embassy::Error; use rpc_toolkit::run_cli; fn inner_main() -> Result<(), Error> { - simple_logging::log_to_stderr(log::LevelFilter::Info); run_cli!( embassy::portable_api, - app => app.name("Embassy SDK"), - matches => EitherContext::Cli(CliContext::init(matches)?), + app => app + .name("Embassy SDK") + .arg( + clap::Arg::with_name("config") + .short("c") + .long("config") + .takes_value(true), + ) + .arg( + clap::Arg::with_name("verbosity") + .short("v") + .multiple(true) + .takes_value(false), + ), + matches => { + simple_logging::log_to_stderr(match matches.occurrences_of("verbosity") { + 0 => log::LevelFilter::Off, + 1 => log::LevelFilter::Error, + 2 => log::LevelFilter::Warn, + 3 => log::LevelFilter::Info, + 4 => log::LevelFilter::Debug, + _ => log::LevelFilter::Trace, + }); + EitherContext::Cli(CliContext::init(matches)?) + }, |code| if code < 0 { 1 } else { code } ) } diff --git a/appmgr/src/config/util.rs b/appmgr/src/config/util.rs index 8e29d6509..1a64b1a09 100644 --- a/appmgr/src/config/util.rs +++ b/appmgr/src/config/util.rs @@ -16,7 +16,7 @@ impl CharSet { self.0.iter().any(|r| r.0.contains(c)) } pub fn gen(&self, rng: &mut R) -> char { - let mut idx = rng.gen_range(0..self.1); + let mut idx = rng.gen_range(0, self.1); for r in &self.0 { if idx < r.1 { return std::convert::TryFrom::try_from( diff --git a/appmgr/src/context/cli.rs b/appmgr/src/context/cli.rs index 02fc88840..39db54522 100644 --- a/appmgr/src/context/cli.rs +++ b/appmgr/src/context/cli.rs @@ -1,8 +1,10 @@ use std::fs::File; +use std::io::Read; use std::net::IpAddr; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::sync::Arc; +use anyhow::anyhow; use clap::ArgMatches; use reqwest::Proxy; use rpc_toolkit::reqwest::{Client, Url}; @@ -11,15 +13,17 @@ use rpc_toolkit::Context; use serde::Deserialize; use super::rpc::RpcContextConfig; -use crate::ResultExt; +use crate::{Error, ResultExt}; #[derive(Debug, Default, Deserialize)] +#[serde(rename_all = "kebab-case")] pub struct CliContextConfig { #[serde(deserialize_with = "deserialize_host")] pub host: Option, pub port: Option, #[serde(deserialize_with = "crate::util::deserialize_from_str_opt")] pub proxy: Option, + pub developer_key_path: Option, #[serde(flatten)] pub server_config: RpcContextConfig, } @@ -29,13 +33,15 @@ pub struct CliContextSeed { pub host: Host, pub port: u16, pub client: Client, + pub developer_key_path: PathBuf, } #[derive(Debug, Clone)] pub struct CliContext(Arc); impl CliContext { + /// BLOCKING pub fn init(matches: &ArgMatches) -> Result { - let cfg_path = Path::new(crate::CONFIG_PATH); + let cfg_path = Path::new(matches.value_of("config").unwrap_or(crate::CONFIG_PATH)); let mut base = if cfg_path.exists() { serde_yaml::from_reader( File::open(cfg_path) @@ -76,8 +82,29 @@ impl CliContext { } else { Client::new() }, + developer_key_path: base.developer_key_path.unwrap_or_else(|| { + cfg_path + .parent() + .unwrap_or(Path::new("/")) + .join(".developer_key") + }), }))) } + /// BLOCKING + pub fn developer_key(&self) -> Result { + if !self.developer_key_path.exists() { + return Err(Error::new(anyhow!("Developer Key does not exist! Please run `embassy-sdk init` before running this command."), crate::ErrorKind::Uninitialized)); + } + let mut keypair_buf = [0; ed25519_dalek::KEYPAIR_LENGTH]; + File::open(&self.developer_key_path)?.read_exact(&mut keypair_buf)?; + Ok(ed25519_dalek::Keypair::from_bytes(&keypair_buf)?) + } +} +impl std::ops::Deref for CliContext { + type Target = CliContextSeed; + fn deref(&self) -> &Self::Target { + &*self.0 + } } impl Context for CliContext { fn host(&self) -> Host<&str> { diff --git a/appmgr/src/dependencies.rs b/appmgr/src/dependencies.rs index 04282e982..93346ae77 100644 --- a/appmgr/src/dependencies.rs +++ b/appmgr/src/dependencies.rs @@ -80,7 +80,7 @@ impl std::fmt::Display for DependencyError { if !comma { comma = true; } else { - write!(f, ", "); + write!(f, ", ")?; } write!(f, "{} @ {} {}", check, res.time, res.result)?; } diff --git a/appmgr/src/developer/mod.rs b/appmgr/src/developer/mod.rs new file mode 100644 index 000000000..667fdd9da --- /dev/null +++ b/appmgr/src/developer/mod.rs @@ -0,0 +1,29 @@ +use std::fs::File; +use std::io::Write; +use std::path::Path; + +use ed25519_dalek::Keypair; +use rpc_toolkit::command; + +use crate::context::EitherContext; +use crate::util::display_none; +use crate::{Error, ResultExt}; + +#[command(cli_only, blocking, display(display_none))] +pub fn init(#[context] ctx: EitherContext) -> Result<(), Error> { + let ctx = ctx.as_cli().unwrap(); + if !ctx.developer_key_path.exists() { + let parent = ctx.developer_key_path.parent().unwrap_or(Path::new("/")); + if !parent.exists() { + std::fs::create_dir_all(parent) + .with_ctx(|_| (crate::ErrorKind::Filesystem, parent.display().to_string()))?; + } + log::info!("Generating new developer key..."); + let keypair = Keypair::generate(&mut rand::thread_rng()); + log::info!("Writing key to {}", ctx.developer_key_path.display()); + let mut dev_key_file = File::create(&ctx.developer_key_path)?; + dev_key_file.write_all(&keypair.to_bytes())?; + dev_key_file.sync_all()?; + } + Ok(()) +} diff --git a/appmgr/src/error.rs b/appmgr/src/error.rs index e3ac5b88a..94a44fecd 100644 --- a/appmgr/src/error.rs +++ b/appmgr/src/error.rs @@ -45,6 +45,7 @@ pub enum ErrorKind { RateLimited = 37, InvalidRequest = 38, MigrationFailed = 39, + Uninitialized = 40, } impl ErrorKind { pub fn as_str(&self) -> &'static str { @@ -89,6 +90,7 @@ impl ErrorKind { RateLimited => "Rate Limited", InvalidRequest => "Invalid Request", MigrationFailed => "Migration Failed", + Uninitialized => "Uninitialized", } } } diff --git a/appmgr/src/lib.rs b/appmgr/src/lib.rs index b6b733f62..18102137e 100644 --- a/appmgr/src/lib.rs +++ b/appmgr/src/lib.rs @@ -22,6 +22,7 @@ pub mod config; pub mod context; pub mod db; pub mod dependencies; +pub mod developer; pub mod error; pub mod id; pub mod install; @@ -46,12 +47,19 @@ pub fn echo(#[context] _ctx: EitherContext, #[arg] message: String) -> Result Result { Ok(ctx) } -#[command(subcommands(version::git_info, s9pk::pack, s9pk::verify))] +#[command(subcommands(version::git_info, s9pk::pack, s9pk::verify, developer::init))] pub fn portable_api(#[context] ctx: EitherContext) -> Result { Ok(ctx) } diff --git a/appmgr/src/net/interface.rs b/appmgr/src/net/interface.rs index 64128f930..0f65f64e7 100644 --- a/appmgr/src/net/interface.rs +++ b/appmgr/src/net/interface.rs @@ -9,6 +9,7 @@ use torut::onion::TorSecretKeyV3; use crate::db::model::{InterfaceAddressMap, InterfaceAddresses, InterfaceInfo}; use crate::id::Id; use crate::s9pk::manifest::PackageId; +use crate::util::Port; use crate::Error; #[derive(Clone, Debug, Deserialize, Serialize)] @@ -99,7 +100,7 @@ impl> AsRef for InterfaceId { #[serde(rename_all = "kebab-case")] pub struct Interface { pub tor_config: Option, - pub lan_config: Option>, + pub lan_config: Option>, pub ui: bool, pub protocols: Vec, } @@ -107,7 +108,7 @@ pub struct Interface { #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] pub struct TorConfig { - pub port_mapping: IndexMap, + pub port_mapping: IndexMap, } #[derive(Clone, Debug, Deserialize, Serialize)] diff --git a/appmgr/src/net/tor.rs b/appmgr/src/net/tor.rs index aa115e3bf..047f885b4 100644 --- a/appmgr/src/net/tor.rs +++ b/appmgr/src/net/tor.rs @@ -147,7 +147,7 @@ impl TorControllerInner { .port_mapping .iter() .map(|(external, internal)| { - (*external, SocketAddr::from((config.ip, *internal))) + (external.0, SocketAddr::from((config.ip, internal.0))) }) .collect::>() .iter(), diff --git a/appmgr/src/s9pk/builder.rs b/appmgr/src/s9pk/builder.rs index f8a3ce1f1..033368c6d 100644 --- a/appmgr/src/s9pk/builder.rs +++ b/appmgr/src/s9pk/builder.rs @@ -1,9 +1,13 @@ use std::io::{Read, Seek, SeekFrom, Write}; +use digest::Digest; +use sha2::Sha512; use typed_builder::TypedBuilder; use super::header::{FileSection, Header}; use super::manifest::Manifest; +use super::SIG_CONTEXT; +use crate::util::HashWriter; use crate::{Error, ResultExt}; #[derive(TypedBuilder)] @@ -32,7 +36,7 @@ impl< > S9pkPacker<'a, W, RLicense, RInstructions, RIcon, RDockerImages> { /// BLOCKING - pub fn pack(mut self) -> Result<(), Error> { + pub fn pack(mut self, key: &ed25519_dalek::Keypair) -> Result<(), Error> { let header_pos = self.writer.stream_position()?; if header_pos != 0 { log::warn!("Appending to non-empty file."); @@ -45,57 +49,63 @@ impl< ) })?; let mut position = self.writer.stream_position()?; + + let mut writer = HashWriter::new(Sha512::new(), &mut self.writer); // manifest - serde_cbor::to_writer(&mut self.writer, self.manifest).with_ctx(|_| { + serde_cbor::to_writer(&mut writer, self.manifest).with_ctx(|_| { ( crate::ErrorKind::Serialization, "Serializing Manifest (CBOR)", ) })?; - let new_pos = self.writer.stream_position()?; + let new_pos = writer.stream_position()?; header.table_of_contents.manifest = FileSection { position, length: new_pos - position, }; position = new_pos; // license - std::io::copy(&mut self.license, &mut self.writer) + std::io::copy(&mut self.license, &mut writer) .with_ctx(|_| (crate::ErrorKind::Filesystem, "Copying License"))?; - let new_pos = self.writer.stream_position()?; + let new_pos = writer.stream_position()?; header.table_of_contents.license = FileSection { position, length: new_pos - position, }; position = new_pos; // instructions - std::io::copy(&mut self.instructions, &mut self.writer) + std::io::copy(&mut self.instructions, &mut writer) .with_ctx(|_| (crate::ErrorKind::Filesystem, "Copying Instructions"))?; - let new_pos = self.writer.stream_position()?; + let new_pos = writer.stream_position()?; header.table_of_contents.instructions = FileSection { position, length: new_pos - position, }; position = new_pos; // icon - std::io::copy(&mut self.icon, &mut self.writer) + std::io::copy(&mut self.icon, &mut writer) .with_ctx(|_| (crate::ErrorKind::Filesystem, "Copying Icon"))?; - let new_pos = self.writer.stream_position()?; + let new_pos = writer.stream_position()?; header.table_of_contents.icon = FileSection { position, length: new_pos - position, }; position = new_pos; // docker_images - std::io::copy(&mut self.docker_images, &mut self.writer) + std::io::copy(&mut self.docker_images, &mut writer) .with_ctx(|_| (crate::ErrorKind::Filesystem, "Copying App Image"))?; - let new_pos = self.writer.stream_position()?; + let new_pos = writer.stream_position()?; header.table_of_contents.docker_images = FileSection { position, length: new_pos - position, }; position = new_pos; + // header + let (hash, _) = writer.finish(); self.writer.seek(SeekFrom::Start(header_pos))?; + header.pubkey = key.public.clone(); + header.signature = key.sign_prehashed(hash, Some(SIG_CONTEXT))?; header .serialize(&mut self.writer) .with_ctx(|_| (crate::ErrorKind::Serialization, "Writing Header"))?; diff --git a/appmgr/src/s9pk/header.rs b/appmgr/src/s9pk/header.rs index 49376174e..76b7dffb1 100644 --- a/appmgr/src/s9pk/header.rs +++ b/appmgr/src/s9pk/header.rs @@ -10,6 +10,7 @@ use crate::Error; pub const MAGIC: [u8; 2] = [59, 59]; pub const VERSION: u8 = 1; +#[derive(Debug)] pub struct Header { pub pubkey: PublicKey, pub signature: Signature, @@ -76,14 +77,11 @@ pub struct TableOfContents { } impl TableOfContents { pub fn serialize(&self, mut writer: W) -> std::io::Result<()> { - let len: u32 = 16 // size of FileSection - * ( - 1 + // manifest - 1 + // license - 1 + // instructions - 1 + // icon - 1 // docker_images - ); + let len: u32 = ((1 + "manifest".len() + 16) + + (1 + "license".len() + 16) + + (1 + "instructions".len() + 16) + + (1 + "icon".len() + 16) + + (1 + "docker_images".len() + 16)) as u32; writer.write_all(&u32::to_be_bytes(len))?; self.manifest.serialize_entry("manifest", &mut writer)?; self.license.serialize_entry("license", &mut writer)?; @@ -153,7 +151,8 @@ impl FileSection { if read == 0 { return Ok(None); } - let label = vec![0; label_len[0] as usize]; + let mut label = vec![0; label_len[0] as usize]; + reader.read_exact(&mut label).await?; let mut pos = [0; 8]; reader.read_exact(&mut pos).await?; let mut len = [0; 8]; diff --git a/appmgr/src/s9pk/manifest.rs b/appmgr/src/s9pk/manifest.rs index 6be2ac997..4a511aa3b 100644 --- a/appmgr/src/s9pk/manifest.rs +++ b/appmgr/src/s9pk/manifest.rs @@ -134,6 +134,7 @@ pub struct Manifest { } #[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] pub struct Assets { #[serde(default)] pub license: Option, @@ -168,7 +169,7 @@ impl Assets { self.docker_images .as_ref() .map(|a| a.as_path()) - .unwrap_or(Path::new("images.tar")) + .unwrap_or(Path::new("image.tar")) } pub fn instructions_path(&self) -> &Path { self.instructions diff --git a/appmgr/src/s9pk/mod.rs b/appmgr/src/s9pk/mod.rs index cb25a6ea1..74dd8d44d 100644 --- a/appmgr/src/s9pk/mod.rs +++ b/appmgr/src/s9pk/mod.rs @@ -20,8 +20,11 @@ pub mod reader; pub const SIG_CONTEXT: &'static [u8] = b"s9pk"; #[command(cli_only, display(display_none), blocking)] -pub fn pack(#[context] _ctx: EitherContext, #[arg] path: Option) -> Result<(), Error> { +pub fn pack(#[context] ctx: EitherContext, #[arg] path: Option) -> Result<(), Error> { use std::fs::File; + use std::io::Read; + + let ctx = ctx.as_cli().unwrap(); let path = if let Some(path) = path { path @@ -44,12 +47,40 @@ pub fn pack(#[context] _ctx: EitherContext, #[arg] path: Option) -> Res S9pkPacker::builder() .manifest(&manifest) .writer(&mut outfile) - .license(File::open(path.join(manifest.assets.license_path()))?) - .icon(File::open(path.join(manifest.assets.icon_path()))?) - .instructions(File::open(path.join(manifest.assets.instructions_path()))?) - .docker_images(File::open(path.join(manifest.assets.docker_images_path()))?) + .license( + File::open(path.join(manifest.assets.license_path())).with_ctx(|_| { + ( + crate::ErrorKind::Filesystem, + manifest.assets.license_path().display().to_string(), + ) + })?, + ) + .icon( + File::open(path.join(manifest.assets.icon_path())).with_ctx(|_| { + ( + crate::ErrorKind::Filesystem, + manifest.assets.icon_path().display().to_string(), + ) + })?, + ) + .instructions( + File::open(path.join(manifest.assets.instructions_path())).with_ctx(|_| { + ( + crate::ErrorKind::Filesystem, + manifest.assets.instructions_path().display().to_string(), + ) + })?, + ) + .docker_images( + File::open(path.join(manifest.assets.docker_images_path())).with_ctx(|_| { + ( + crate::ErrorKind::Filesystem, + manifest.assets.docker_images_path().display().to_string(), + ) + })?, + ) .build() - .pack()?; + .pack(&ctx.developer_key()?)?; outfile.sync_all()?; Ok(()) diff --git a/appmgr/src/s9pk/reader.rs b/appmgr/src/s9pk/reader.rs index 791bf2799..a228831a6 100644 --- a/appmgr/src/s9pk/reader.rs +++ b/appmgr/src/s9pk/reader.rs @@ -67,7 +67,7 @@ impl S9pkReader> { } impl S9pkReader { pub async fn validate(&mut self) -> Result<(), Error> { - todo!() + Ok(()) } pub async fn from_reader(mut rdr: R) -> Result { let header = Header::deserialize(&mut rdr).await?; diff --git a/appmgr/src/util.rs b/appmgr/src/util.rs index 1aa044fa3..e94731aee 100644 --- a/appmgr/src/util.rs +++ b/appmgr/src/util.rs @@ -10,6 +10,7 @@ use std::time::Duration; use anyhow::anyhow; use async_trait::async_trait; use clap::ArgMatches; +use digest::Digest; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_json::Value; use sqlx::{Executor, Sqlite}; @@ -620,7 +621,7 @@ impl std::io::Write for FmtWriter { } #[derive(Clone, Copy, Debug, Deserialize, Serialize)] -#[serde(rename = "kebab-case")] +#[serde(rename_all = "kebab-case")] pub enum IoFormat { Json, JsonPretty, @@ -698,6 +699,7 @@ impl IoFormat { .with_kind(crate::ErrorKind::Serialization), } } + /// BLOCKING pub fn from_reader Deserialize<'de>>( &self, mut reader: R, @@ -714,7 +716,9 @@ impl IoFormat { } IoFormat::Toml | IoFormat::TomlPretty => { let mut s = String::new(); - reader.read_to_string(&mut s); + reader + .read_to_string(&mut s) + .with_kind(crate::ErrorKind::Deserialization)?; serde_toml::from_str(&s).with_kind(crate::ErrorKind::Deserialization) } } @@ -809,3 +813,56 @@ impl Container { *self.0.write().await = None; } } + +pub struct HashWriter { + hasher: H, + writer: W, +} +impl HashWriter { + pub fn new(hasher: H, writer: W) -> Self { + HashWriter { hasher, writer } + } + pub fn finish(self) -> (H, W) { + (self.hasher, self.writer) + } +} +impl std::io::Write for HashWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let written = self.writer.write(buf)?; + self.hasher.update(&buf[..written]); + Ok(written) + } + fn flush(&mut self) -> std::io::Result<()> { + self.writer.flush() + } +} +impl std::ops::Deref for HashWriter { + type Target = W; + fn deref(&self) -> &Self::Target { + &self.writer + } +} +impl std::ops::DerefMut for HashWriter { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.writer + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct Port(pub u16); +impl<'de> Deserialize<'de> for Port { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserialize_from_str(deserializer).map(Port) + } +} +impl Serialize for Port { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serialize_display(&self.0, serializer) + } +}