diff --git a/backend/install-sdk.sh b/backend/install-sdk.sh index 72bf03c18..4b2978102 100755 --- a/backend/install-sdk.sh +++ b/backend/install-sdk.sh @@ -3,9 +3,9 @@ set -e shopt -s expand_aliases -if [ "$0" != "./build-sdk.sh" ]; then +if [ "$0" != "./install-sdk.sh" ]; then >&2 echo "Must be run from backend directory" exit 1 fi -cargo install --bin=embassy-sdk --path=. --no-default-features --verbose \ No newline at end of file +cargo install --bin=embassy-sdk --path=. --no-default-features diff --git a/backend/src/action/docker.rs b/backend/src/action/docker.rs index d6f4ba99f..8da24057b 100644 --- a/backend/src/action/docker.rs +++ b/backend/src/action/docker.rs @@ -1,5 +1,5 @@ use std::borrow::Cow; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::ffi::{OsStr, OsString}; use std::net::Ipv4Addr; use std::path::PathBuf; @@ -23,6 +23,17 @@ use crate::{Error, ResultExt, HOST_IP}; pub const NET_TLD: &str = "embassy"; +lazy_static::lazy_static! { + pub static ref SYSTEM_IMAGES: BTreeSet = { + let mut set = BTreeSet::new(); + + set.insert("compat".parse().unwrap()); + set.insert("utils".parse().unwrap()); + + set + }; +} + #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] pub struct DockerAction { @@ -44,6 +55,32 @@ pub struct DockerAction { pub sigterm_timeout: Option, } impl DockerAction { + pub fn validate( + &self, + volumes: &Volumes, + image_ids: &BTreeSet, + expected_io: bool, + ) -> Result<(), color_eyre::eyre::Report> { + for (volume, _) in &self.mounts { + if !volumes.contains_key(volume) { + color_eyre::eyre::bail!("unknown volume: {}", volume); + } + } + if self.system { + if !SYSTEM_IMAGES.contains(&self.image) { + color_eyre::eyre::bail!("unknown system image: {}", self.image); + } + } else { + if !image_ids.contains(&self.image) { + color_eyre::eyre::bail!("image for {} not contained in package", self.image); + } + } + if expected_io && self.io_format.is_none() { + color_eyre::eyre::bail!("expected io-format"); + } + Ok(()) + } + #[instrument(skip(ctx, input))] pub async fn execute Deserialize<'de>>( &self, diff --git a/backend/src/action/mod.rs b/backend/src/action/mod.rs index 99eba5d05..4fc4d3afe 100644 --- a/backend/src/action/mod.rs +++ b/backend/src/action/mod.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::path::Path; use std::str::FromStr; use std::time::Duration; @@ -14,7 +14,7 @@ use tracing::instrument; use self::docker::DockerAction; use crate::config::{Config, ConfigSpec}; use crate::context::RpcContext; -use crate::id::{Id, InvalidId}; +use crate::id::{Id, ImageId, InvalidId}; use crate::s9pk::manifest::PackageId; use crate::util::serde::{display_serializable, parse_stdin_deserializable, IoFormat}; use crate::util::Version; @@ -109,6 +109,18 @@ pub struct Action { pub input_spec: ConfigSpec, } impl Action { + #[instrument] + pub fn validate(&self, volumes: &Volumes, image_ids: &BTreeSet) -> Result<(), Error> { + self.implementation + .validate(volumes, image_ids, true) + .with_ctx(|_| { + ( + crate::ErrorKind::ValidateS9pk, + format!("Action {}", self.name), + ) + }) + } + #[instrument(skip(ctx))] pub async fn execute( &self, @@ -147,6 +159,20 @@ pub enum ActionImplementation { Docker(DockerAction), } impl ActionImplementation { + #[instrument] + pub fn validate( + &self, + volumes: &Volumes, + image_ids: &BTreeSet, + expected_io: bool, + ) -> Result<(), color_eyre::eyre::Report> { + match self { + ActionImplementation::Docker(action) => { + action.validate(volumes, image_ids, expected_io) + } + } + } + #[instrument(skip(ctx, input))] pub async fn execute Deserialize<'de>>( &self, diff --git a/backend/src/backup/mod.rs b/backend/src/backup/mod.rs index 6de98f03e..e09f1cdf2 100644 --- a/backend/src/backup/mod.rs +++ b/backend/src/backup/mod.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::path::Path; use chrono::{DateTime, Utc}; @@ -15,6 +15,7 @@ use self::target::PackageBackupInfo; use crate::action::{ActionImplementation, NoOutput}; use crate::context::RpcContext; use crate::dependencies::reconfigure_dependents_with_live_pointers; +use crate::id::ImageId; use crate::install::PKG_ARCHIVE_DIR; use crate::net::interface::{InterfaceId, Interfaces}; use crate::s9pk::manifest::PackageId; @@ -67,6 +68,16 @@ pub struct BackupActions { pub restore: ActionImplementation, } impl BackupActions { + pub fn validate(&self, volumes: &Volumes, image_ids: &BTreeSet) -> Result<(), Error> { + self.create + .validate(volumes, image_ids, false) + .with_ctx(|_| (crate::ErrorKind::ValidateS9pk, "Backup Create"))?; + self.restore + .validate(volumes, image_ids, false) + .with_ctx(|_| (crate::ErrorKind::ValidateS9pk, "Backup Restore"))?; + Ok(()) + } + #[instrument(skip(ctx))] pub async fn create( &self, diff --git a/backend/src/config/action.rs b/backend/src/config/action.rs index 339b18e12..1dced12f8 100644 --- a/backend/src/config/action.rs +++ b/backend/src/config/action.rs @@ -10,11 +10,12 @@ use super::{Config, ConfigSpec}; use crate::action::ActionImplementation; use crate::context::RpcContext; use crate::dependencies::Dependencies; +use crate::id::ImageId; use crate::s9pk::manifest::PackageId; use crate::status::health_check::HealthCheckId; use crate::util::Version; use crate::volume::Volumes; -use crate::Error; +use crate::{Error, ResultExt}; #[derive(Debug, Deserialize, Serialize, HasModel)] #[serde(rename_all = "kebab-case")] @@ -29,6 +30,16 @@ pub struct ConfigActions { pub set: ActionImplementation, } impl ConfigActions { + #[instrument] + pub fn validate(&self, volumes: &Volumes, image_ids: &BTreeSet) -> Result<(), Error> { + self.get + .validate(volumes, image_ids, true) + .with_ctx(|_| (crate::ErrorKind::ValidateS9pk, "Config Get"))?; + self.set + .validate(volumes, image_ids, true) + .with_ctx(|_| (crate::ErrorKind::ValidateS9pk, "Config Set"))?; + Ok(()) + } #[instrument(skip(ctx))] pub async fn get( &self, diff --git a/backend/src/id.rs b/backend/src/id.rs index f987569c4..e14c38fa5 100644 --- a/backend/src/id.rs +++ b/backend/src/id.rs @@ -1,5 +1,6 @@ use std::borrow::{Borrow, Cow}; use std::fmt::Debug; +use std::str::FromStr; use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -158,6 +159,12 @@ impl> ImageId { ) } } +impl FromStr for ImageId { + type Err = InvalidId; + fn from_str(s: &str) -> Result { + Ok(ImageId(Id::try_from(s.to_owned())?)) + } +} impl<'de, S> Deserialize<'de> for ImageId where S: AsRef, diff --git a/backend/src/migration.rs b/backend/src/migration.rs index 9209f77ab..01d567f63 100644 --- a/backend/src/migration.rs +++ b/backend/src/migration.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeSet; + use color_eyre::eyre::eyre; use emver::VersionRange; use futures::{Future, FutureExt}; @@ -8,10 +10,11 @@ use tracing::instrument; use crate::action::ActionImplementation; use crate::context::RpcContext; +use crate::id::ImageId; use crate::s9pk::manifest::PackageId; use crate::util::Version; use crate::volume::Volumes; -use crate::Error; +use crate::{Error, ResultExt}; #[derive(Clone, Debug, Default, Deserialize, Serialize, HasModel)] #[serde(rename_all = "kebab-case")] @@ -20,6 +23,27 @@ pub struct Migrations { pub to: IndexMap, } impl Migrations { + #[instrument] + pub fn validate(&self, volumes: &Volumes, image_ids: &BTreeSet) -> Result<(), Error> { + for (version, migration) in &self.from { + migration.validate(volumes, image_ids, true).with_ctx(|_| { + ( + crate::ErrorKind::ValidateS9pk, + format!("Migration from {}", version), + ) + })?; + } + for (version, migration) in &self.to { + migration.validate(volumes, image_ids, true).with_ctx(|_| { + ( + crate::ErrorKind::ValidateS9pk, + format!("Migration to {}", version), + ) + })?; + } + Ok(()) + } + #[instrument(skip(ctx))] pub fn from<'a>( &'a self, diff --git a/backend/src/net/interface.rs b/backend/src/net/interface.rs index b97f0cada..05f837ef4 100644 --- a/backend/src/net/interface.rs +++ b/backend/src/net/interface.rs @@ -14,12 +14,24 @@ use crate::db::model::{InterfaceAddressMap, InterfaceAddresses}; use crate::id::Id; use crate::s9pk::manifest::PackageId; use crate::util::serde::Port; -use crate::Error; +use crate::{Error, ResultExt}; #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] pub struct Interfaces(pub BTreeMap); // TODO impl Interfaces { + #[instrument] + pub fn validate(&self) -> Result<(), Error> { + for (_, interface) in &self.0 { + interface.validate().with_ctx(|_| { + ( + crate::ErrorKind::ValidateS9pk, + format!("Interface {}", interface.name), + ) + })?; + } + Ok(()) + } #[instrument(skip(secrets))] pub async fn install( &self, @@ -152,6 +164,21 @@ pub struct Interface { pub ui: bool, pub protocols: IndexSet, } +impl Interface { + #[instrument] + pub fn validate(&self) -> Result<(), color_eyre::eyre::Report> { + if self.tor_config.is_some() && !self.protocols.contains("tcp") { + color_eyre::eyre::bail!("must support tcp to set up a tor hidden service"); + } + if self.lan_config.is_some() && !self.protocols.contains("http") { + color_eyre::eyre::bail!("must support http to set up a lan service"); + } + if self.ui && !(self.protocols.contains("http") || self.protocols.contains("https")) { + color_eyre::eyre::bail!("must support http or https to serve a ui"); + } + Ok(()) + } +} #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] diff --git a/backend/src/s9pk/reader.rs b/backend/src/s9pk/reader.rs index 9e2044e81..0de06472d 100644 --- a/backend/src/s9pk/reader.rs +++ b/backend/src/s9pk/reader.rs @@ -1,19 +1,25 @@ +use std::collections::BTreeSet; use std::io::SeekFrom; use std::path::Path; use std::pin::Pin; +use std::str::FromStr; use std::task::{Context, Poll}; +use color_eyre::eyre::eyre; use digest::Output; use ed25519_dalek::PublicKey; +use futures::TryStreamExt; use sha2::{Digest, Sha512}; use tokio::fs::File; use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeek, AsyncSeekExt, ReadBuf, Take}; use tracing::instrument; use super::header::{FileSection, Header, TableOfContents}; -use super::manifest::Manifest; +use super::manifest::{Manifest, PackageId}; use super::SIG_CONTEXT; +use crate::id::ImageId; use crate::install::progress::InstallProgressTracker; +use crate::util::Version; use crate::{Error, ResultExt}; #[pin_project::pin_project] @@ -45,6 +51,66 @@ impl<'a, R: AsyncRead + AsyncSeek + Unpin> AsyncRead for ReadHandle<'a, R> { } } +#[derive(Debug)] +pub struct ImageTag { + pub package_id: PackageId, + pub image_id: ImageId, + pub version: Version, +} +impl ImageTag { + #[instrument] + pub fn validate(&self, id: &PackageId, version: &Version) -> Result<(), Error> { + if id != &self.package_id { + return Err(Error::new( + eyre!( + "Contains image for incorrect package: id {}", + self.package_id, + ), + crate::ErrorKind::ValidateS9pk, + )); + } + if id != &self.package_id { + return Err(Error::new( + eyre!( + "Contains image with incorrect version: expected {} received {}", + version, + self.version, + ), + crate::ErrorKind::ValidateS9pk, + )); + } + Ok(()) + } +} +impl FromStr for ImageTag { + type Err = Error; + fn from_str(s: &str) -> Result { + let rest = s.strip_prefix("start9/").ok_or_else(|| { + Error::new( + eyre!("Invalid image tag prefix: expected start9/"), + crate::ErrorKind::ValidateS9pk, + ) + })?; + let (package, rest) = rest.split_once("/").ok_or_else(|| { + Error::new( + eyre!("Image tag missing image id"), + crate::ErrorKind::ValidateS9pk, + ) + })?; + let (image, version) = rest.split_once(":").ok_or_else(|| { + Error::new( + eyre!("Image tag missing version"), + crate::ErrorKind::ValidateS9pk, + ) + })?; + Ok(ImageTag { + package_id: package.parse()?, + image_id: image.parse()?, + version: version.parse()?, + }) + } +} + pub struct S9pkReader { hash: Option>, hash_string: Option, @@ -71,8 +137,66 @@ impl S9pkReader> { impl S9pkReader { #[instrument(skip(self))] pub async fn validate(&mut self) -> Result<(), Error> { + let image_tags = self.image_tags().await?; + let man = self.manifest().await?; + let validated_image_ids = image_tags + .into_iter() + .map(|i| i.validate(&man.id, &man.version).map(|_| i.image_id)) + .collect::, _>>()?; + man.actions + .0 + .iter() + .map(|(_, action)| action.validate(&man.volumes, &validated_image_ids)) + .collect::>()?; + man.backup.validate(&man.volumes, &validated_image_ids)?; + if let Some(cfg) = &man.config { + cfg.validate(&man.volumes, &validated_image_ids)?; + } + man.health_checks + .validate(&man.volumes, &validated_image_ids)?; + man.interfaces.validate()?; + man.main + .validate(&man.volumes, &validated_image_ids, false) + .with_ctx(|_| (crate::ErrorKind::ValidateS9pk, "Main"))?; + man.migrations + .validate(&man.volumes, &validated_image_ids)?; + if let Some(props) = &man.properties { + props + .validate(&man.volumes, &validated_image_ids, true) + .with_ctx(|_| (crate::ErrorKind::ValidateS9pk, "Properties"))?; + } + man.volumes.validate(&man.interfaces)?; + Ok(()) } + #[instrument(skip(self))] + pub async fn image_tags(&mut self) -> Result, Error> { + let mut tar = tokio_tar::Archive::new(self.docker_images().await?); + let mut entries = tar.entries()?; + while let Some(mut entry) = entries.try_next().await? { + if &*entry.path()? != Path::new("manifest.json") { + continue; + } + let mut buf = Vec::with_capacity(entry.header().size()? as usize); + entry.read_to_end(&mut buf).await?; + #[derive(serde::Deserialize)] + struct ManEntry { + #[serde(rename = "RepoTags")] + tags: Vec, + } + let man_entries = serde_json::from_slice::>(&buf) + .with_ctx(|_| (crate::ErrorKind::Deserialization, "manifest.json"))?; + return man_entries + .iter() + .flat_map(|e| &e.tags) + .map(|t| t.parse()) + .collect(); + } + Err(Error::new( + eyre!("image.tar missing manifest.json"), + crate::ErrorKind::ParseS9pk, + )) + } #[instrument(skip(rdr))] pub async fn from_reader(mut rdr: R, check_sig: bool) -> Result { let header = Header::deserialize(&mut rdr).await?; diff --git a/backend/src/status/health_check.rs b/backend/src/status/health_check.rs index 628f09800..75185c880 100644 --- a/backend/src/status/health_check.rs +++ b/backend/src/status/health_check.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::path::Path; use chrono::{DateTime, Utc}; @@ -7,12 +7,12 @@ use tracing::instrument; use crate::action::{ActionImplementation, NoOutput}; use crate::context::RpcContext; -use crate::id::Id; +use crate::id::{Id, ImageId}; use crate::s9pk::manifest::PackageId; use crate::util::serde::Duration; use crate::util::Version; use crate::volume::Volumes; -use crate::Error; +use crate::{Error, ResultExt}; #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)] pub struct HealthCheckId = String>(Id); @@ -47,6 +47,21 @@ impl> AsRef for HealthCheckId { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct HealthChecks(pub BTreeMap); impl HealthChecks { + #[instrument] + pub fn validate(&self, volumes: &Volumes, image_ids: &BTreeSet) -> Result<(), Error> { + for (_, check) in &self.0 { + check + .implementation + .validate(&volumes, image_ids, false) + .with_ctx(|_| { + ( + crate::ErrorKind::ValidateS9pk, + format!("Health Check {}", check.name), + ) + })?; + } + Ok(()) + } pub async fn check_all( &self, ctx: &RpcContext, diff --git a/backend/src/volume.rs b/backend/src/volume.rs index 08cf7b311..aa8d63c37 100644 --- a/backend/src/volume.rs +++ b/backend/src/volume.rs @@ -3,15 +3,17 @@ use std::collections::BTreeMap; use std::ops::{Deref, DerefMut}; use std::path::{Path, PathBuf}; +use color_eyre::eyre::eyre; use patch_db::{HasModel, Map, MapModel}; use serde::{Deserialize, Deserializer, Serialize}; +use tracing::instrument; use crate::context::RpcContext; use crate::id::{Id, IdUnchecked}; -use crate::net::interface::InterfaceId; +use crate::net::interface::{InterfaceId, Interfaces}; use crate::s9pk::manifest::PackageId; use crate::util::Version; -use crate::Error; +use crate::{Error, ResultExt}; pub const PKG_VOLUME_DIR: &'static str = "package-data/volumes"; pub const BACKUP_DIR: &'static str = "/media/embassy-os/backups"; @@ -75,6 +77,16 @@ impl> Serialize for VolumeId { #[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct Volumes(BTreeMap); impl Volumes { + #[instrument] + pub fn validate(&self, interfaces: &Interfaces) -> Result<(), Error> { + for (id, volume) in &self.0 { + volume + .validate(interfaces) + .with_ctx(|_| (crate::ErrorKind::ValidateS9pk, format!("Volume {}", id)))?; + } + Ok(()) + } + #[instrument(skip(ctx))] pub async fn install( &self, ctx: &RpcContext, @@ -180,6 +192,18 @@ pub enum Volume { Backup { readonly: bool }, } impl Volume { + #[instrument] + pub fn validate(&self, interfaces: &Interfaces) -> Result<(), color_eyre::eyre::Report> { + match self { + Volume::Certificate { interface_id } => { + if !interfaces.0.contains_key(interface_id) { + color_eyre::eyre::bail!("unknown interface: {}", interface_id); + } + } + _ => (), + } + Ok(()) + } pub async fn install( &self, ctx: &RpcContext,