This commit is contained in:
Aiden McClelland
2021-07-01 10:51:22 -06:00
committed by Aiden McClelland
parent ffb9a72e18
commit 3f416dda1b
19 changed files with 258 additions and 49 deletions

2
appmgr/Cargo.lock generated
View File

@@ -698,7 +698,7 @@ dependencies = [
"patch-db",
"pin-project",
"prettytable-rs",
"rand 0.8.4",
"rand 0.7.3",
"regex",
"reqwest",
"rpassword",

View File

@@ -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"

View File

@@ -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),

View File

@@ -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 }
)
}

View File

@@ -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 }
)
}

View File

@@ -16,7 +16,7 @@ impl CharSet {
self.0.iter().any(|r| r.0.contains(c))
}
pub fn gen<R: Rng>(&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(

View File

@@ -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<Host>,
pub port: Option<u16>,
#[serde(deserialize_with = "crate::util::deserialize_from_str_opt")]
pub proxy: Option<Url>,
pub developer_key_path: Option<PathBuf>,
#[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<CliContextSeed>);
impl CliContext {
/// BLOCKING
pub fn init(matches: &ArgMatches) -> Result<Self, crate::Error> {
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<ed25519_dalek::Keypair, Error> {
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> {

View File

@@ -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)?;
}

View File

@@ -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(())
}

View File

@@ -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",
}
}
}

View File

@@ -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<St
Ok(message)
}
#[command(subcommands(config::config, version::git_info, echo, s9pk::pack, s9pk::verify))]
#[command(subcommands(
config::config,
version::git_info,
echo,
s9pk::pack,
s9pk::verify,
developer::init
))]
pub fn main_api(#[context] ctx: EitherContext) -> Result<EitherContext, RpcError> {
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<EitherContext, RpcError> {
Ok(ctx)
}

View File

@@ -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<S: AsRef<str>> AsRef<Path> for InterfaceId<S> {
#[serde(rename_all = "kebab-case")]
pub struct Interface {
pub tor_config: Option<TorConfig>,
pub lan_config: Option<IndexMap<u16, LanPortConfig>>,
pub lan_config: Option<IndexMap<Port, LanPortConfig>>,
pub ui: bool,
pub protocols: Vec<String>,
}
@@ -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<u16, u16>,
pub port_mapping: IndexMap<Port, Port>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]

View File

@@ -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::<Vec<_>>()
.iter(),

View File

@@ -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"))?;

View File

@@ -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<W: Write>(&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];

View File

@@ -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<PathBuf>,
@@ -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

View File

@@ -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<PathBuf>) -> Result<(), Error> {
pub fn pack(#[context] ctx: EitherContext, #[arg] path: Option<PathBuf>) -> 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<PathBuf>) -> 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(())

View File

@@ -67,7 +67,7 @@ impl<R: AsyncRead + AsyncSeek + Unpin> S9pkReader<InstallProgressTracker<R>> {
}
impl<R: AsyncRead + AsyncSeek + Unpin> S9pkReader<R> {
pub async fn validate(&mut self) -> Result<(), Error> {
todo!()
Ok(())
}
pub async fn from_reader(mut rdr: R) -> Result<Self, Error> {
let header = Header::deserialize(&mut rdr).await?;

View File

@@ -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<W: std::fmt::Write> std::io::Write for FmtWriter<W> {
}
#[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<R: std::io::Read, T: for<'de> 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<T> Container<T> {
*self.0.write().await = None;
}
}
pub struct HashWriter<H: Digest, W: std::io::Write> {
hasher: H,
writer: W,
}
impl<H: Digest, W: std::io::Write> HashWriter<H, W> {
pub fn new(hasher: H, writer: W) -> Self {
HashWriter { hasher, writer }
}
pub fn finish(self) -> (H, W) {
(self.hasher, self.writer)
}
}
impl<H: Digest, W: std::io::Write> std::io::Write for HashWriter<H, W> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let written = self.writer.write(buf)?;
self.hasher.update(&buf[..written]);
Ok(written)
}
fn flush(&mut self) -> std::io::Result<()> {
self.writer.flush()
}
}
impl<H: Digest, W: std::io::Write> std::ops::Deref for HashWriter<H, W> {
type Target = W;
fn deref(&self) -> &Self::Target {
&self.writer
}
}
impl<H: Digest, W: std::io::Write> std::ops::DerefMut for HashWriter<H, W> {
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<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserialize_from_str(deserializer).map(Port)
}
}
impl Serialize for Port {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serialize_display(&self.0, serializer)
}
}