mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
382 lines
14 KiB
Rust
382 lines
14 KiB
Rust
use std::borrow::Cow;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use failure::ResultExt;
|
|
use futures::stream::StreamExt;
|
|
use linear_map::LinearMap;
|
|
use rand::SeedableRng;
|
|
use tokio_tar as tar;
|
|
|
|
use crate::config::{ConfigRuleEntry, ConfigSpec};
|
|
use crate::manifest::{ImageConfig, Manifest};
|
|
use crate::util::{from_cbor_async_reader, from_json_async_reader, from_yaml_async_reader};
|
|
use crate::version::VersionT;
|
|
|
|
#[derive(Clone, Debug, Fail)]
|
|
pub enum Error {
|
|
#[fail(display = "Invalid Directory Name: {}", _0)]
|
|
InvalidDirectoryName(String),
|
|
#[fail(display = "Invalid File Name: {}", _0)]
|
|
InvalidFileName(String),
|
|
#[fail(display = "Invalid Output Path: {}", _0)]
|
|
InvalidOutputPath(String),
|
|
}
|
|
|
|
pub async fn pack(path: &str, output: &str) -> Result<(), failure::Error> {
|
|
let path = Path::new(path.trim_end_matches("/"));
|
|
let output = Path::new(output);
|
|
log::info!(
|
|
"Starting pack of {} to {}.",
|
|
path.file_name()
|
|
.and_then(|a| a.to_str())
|
|
.ok_or_else(|| Error::InvalidDirectoryName(format!("{}", path.display())))?,
|
|
output.display(),
|
|
);
|
|
let out_file = tokio::fs::File::create(output).await?;
|
|
let mut out = tar::Builder::new(out_file);
|
|
log::info!("Reading {}/manifest.yaml.", path.display());
|
|
let manifest: Manifest = crate::util::from_yaml_async_reader(
|
|
tokio::fs::File::open(path.join("manifest.yaml"))
|
|
.await
|
|
.with_context(|e| format!("{}: manifest.yaml", e))?,
|
|
)
|
|
.await?;
|
|
log::info!("Writing manifest to archive.");
|
|
let bin_manifest = serde_cbor::to_vec(&manifest)?;
|
|
let mut manifest_header = tar::Header::new_gnu();
|
|
manifest_header.set_size(bin_manifest.len() as u64);
|
|
out.append_data(
|
|
&mut manifest_header,
|
|
"manifest.cbor",
|
|
std::io::Cursor::new(bin_manifest),
|
|
)
|
|
.await?;
|
|
let manifest = manifest.into_latest();
|
|
ensure!(
|
|
crate::version::Current::new()
|
|
.semver()
|
|
.satisfies(&manifest.os_version_required),
|
|
"Unsupported AppMgr version: expected {}",
|
|
manifest.os_version_required
|
|
);
|
|
log::info!("Reading {}/config_spec.yaml.", path.display());
|
|
let config_spec: ConfigSpec = from_yaml_async_reader(
|
|
tokio::fs::File::open(path.join("config_spec.yaml"))
|
|
.await
|
|
.with_context(|e| format!("{}: config_spec.yaml", e))?,
|
|
)
|
|
.await?;
|
|
log::info!("Writing config spec to archive.");
|
|
let bin_config_spec = serde_cbor::to_vec(&config_spec)?;
|
|
let mut config_spec_header = tar::Header::new_gnu();
|
|
config_spec_header.set_size(bin_config_spec.len() as u64);
|
|
out.append_data(
|
|
&mut config_spec_header,
|
|
"config_spec.cbor",
|
|
std::io::Cursor::new(bin_config_spec),
|
|
)
|
|
.await?;
|
|
log::info!("Reading {}/config_rules.yaml.", path.display());
|
|
let config_rules: Vec<ConfigRuleEntry> = from_yaml_async_reader(
|
|
tokio::fs::File::open(path.join("config_rules.yaml"))
|
|
.await
|
|
.with_context(|e| format!("{}: config_rules.yaml", e))?,
|
|
)
|
|
.await?;
|
|
log::info!("Writing config rules to archive.");
|
|
let bin_config_rules = serde_cbor::to_vec(&config_rules)?;
|
|
let mut config_rules_header = tar::Header::new_gnu();
|
|
config_rules_header.set_size(bin_config_rules.len() as u64);
|
|
out.append_data(
|
|
&mut config_rules_header,
|
|
"config_rules.cbor",
|
|
std::io::Cursor::new(bin_config_rules),
|
|
)
|
|
.await?;
|
|
if manifest.has_instructions {
|
|
log::info!("Packing instructions.md");
|
|
out.append_path_with_name(path.join("instructions.md"), "instructions.md")
|
|
.await?;
|
|
}
|
|
log::info!("Copying over assets.");
|
|
for asset in &manifest.assets {
|
|
let src_path = Path::new("assets").join(&asset.src);
|
|
log::info!("Reading {}/{}.", path.display(), src_path.display());
|
|
let file_path = path.join(&src_path);
|
|
let src = tokio::fs::File::open(&file_path)
|
|
.await
|
|
.with_context(|e| format!("{}: {}", e, src_path.display()))?;
|
|
log::info!("Writing {} to archive.", src_path.display());
|
|
if src.metadata().await?.is_dir() {
|
|
out.append_dir_all(&asset.src, &file_path).await?;
|
|
let mut h = tar::Header::new_gnu();
|
|
h.set_size(0);
|
|
h.set_path(format!("APPMGR_DIR_END:{}", asset.src.display()))?;
|
|
h.set_cksum();
|
|
out.append(&h, tokio::io::empty()).await?;
|
|
} else {
|
|
out.append_path_with_name(&file_path, &asset.src).await?;
|
|
}
|
|
}
|
|
match manifest.image {
|
|
ImageConfig::Tar => {
|
|
log::info!("Reading {}/image.tar.", path.display());
|
|
let image = tokio::fs::File::open(path.join("image.tar"))
|
|
.await
|
|
.with_context(|e| format!("{}: image.tar", e))?;
|
|
log::info!("Writing image.tar to archive.");
|
|
let mut header = tar::Header::new_gnu();
|
|
header.set_size(image.metadata().await?.len());
|
|
out.append_data(&mut header, "image.tar", image).await?;
|
|
}
|
|
}
|
|
out.into_inner().await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn validate_path<P: AsRef<Path>>(p: P) -> Result<(), Error> {
|
|
let path = p.as_ref();
|
|
if path.is_absolute() {
|
|
return Err(Error::InvalidFileName(format!("{}", path.display())));
|
|
}
|
|
for seg in path {
|
|
if seg == ".." {
|
|
return Err(Error::InvalidFileName(format!("{}", path.display())));
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn verify(path: &str) -> Result<(), failure::Error> {
|
|
let path = Path::new(path.trim_end_matches("/"));
|
|
ensure!(
|
|
path.extension()
|
|
.and_then(|a| a.to_str())
|
|
.ok_or_else(|| Error::InvalidFileName(format!("{}", path.display())))?
|
|
== "s9pk",
|
|
"Extension Must Be '.s9pk'"
|
|
);
|
|
let name = path
|
|
.file_stem()
|
|
.and_then(|a| a.to_str())
|
|
.ok_or_else(|| Error::InvalidFileName(format!("{}", path.display())))?;
|
|
ensure!(
|
|
!name.starts_with("start9")
|
|
&& name
|
|
.chars()
|
|
.filter(|c| !c.is_alphanumeric() && c != &'-')
|
|
.next()
|
|
.is_none(),
|
|
"Invalid Application ID"
|
|
);
|
|
log::info!(
|
|
"Starting verification of {}.",
|
|
path.file_name()
|
|
.and_then(|a| a.to_str())
|
|
.ok_or_else(|| Error::InvalidFileName(format!("{}", path.display())))?,
|
|
);
|
|
{}
|
|
log::info!("Opening file.");
|
|
let r = tokio::fs::File::open(&path)
|
|
.await
|
|
.with_context(|e| format!("{}: {}", path.display(), e))?;
|
|
log::info!("Extracting archive.");
|
|
let mut pkg = tar::Archive::new(r);
|
|
let mut entries = pkg.entries()?;
|
|
log::info!("Opening manifest from archive.");
|
|
let manifest = entries
|
|
.next()
|
|
.await
|
|
.ok_or_else(|| format_err!("missing manifest"))??;
|
|
ensure!(
|
|
manifest.path()?.to_str() == Some("manifest.cbor"),
|
|
"Package File Invalid or Corrupted: expected manifest.cbor, got {}",
|
|
manifest.path()?.display()
|
|
);
|
|
log::trace!("Deserializing manifest.");
|
|
let manifest: Manifest = from_cbor_async_reader(manifest).await?;
|
|
let manifest = manifest.into_latest();
|
|
ensure!(
|
|
crate::version::Current::new()
|
|
.semver()
|
|
.satisfies(&manifest.os_version_required),
|
|
"Unsupported AppMgr Version: expected {}",
|
|
manifest.os_version_required
|
|
);
|
|
ensure!(manifest.id == name, "Package Name Does Not Match Expected",);
|
|
if let (Some(public), Some(shared)) = (&manifest.public, &manifest.shared) {
|
|
ensure!(
|
|
!public.starts_with(shared) && !shared.starts_with(public),
|
|
"Public Directory Conflicts With Shared Directory"
|
|
)
|
|
}
|
|
if let Some(public) = &manifest.public {
|
|
validate_path(public)?;
|
|
}
|
|
if let Some(shared) = &manifest.shared {
|
|
validate_path(shared)?;
|
|
}
|
|
log::info!("Opening config spec from archive.");
|
|
let config_spec = entries
|
|
.next()
|
|
.await
|
|
.ok_or_else(|| format_err!("missing config spec"))??;
|
|
ensure!(
|
|
config_spec.path()?.to_str() == Some("config_spec.cbor"),
|
|
"Package File Invalid or Corrupted: expected config_rules.cbor, got {}",
|
|
config_spec.path()?.display()
|
|
);
|
|
log::trace!("Deserializing config spec.");
|
|
let config_spec: ConfigSpec = from_cbor_async_reader(config_spec).await?;
|
|
log::trace!("Validating config spec.");
|
|
config_spec.validate(&manifest)?;
|
|
let config = config_spec.gen(&mut rand::rngs::StdRng::from_entropy(), &None)?;
|
|
config_spec.matches(&config)?;
|
|
log::info!("Opening config rules from archive.");
|
|
let config_rules = entries
|
|
.next()
|
|
.await
|
|
.ok_or_else(|| format_err!("missing config rules"))??;
|
|
ensure!(
|
|
config_rules.path()?.to_str() == Some("config_rules.cbor"),
|
|
"Package File Invalid or Corrupted: expected config_rules.cbor, got {}",
|
|
config_rules.path()?.display()
|
|
);
|
|
log::trace!("Deserializing config rules.");
|
|
let config_rules: Vec<ConfigRuleEntry> = from_cbor_async_reader(config_rules).await?;
|
|
log::trace!("Validating config rules against config spec.");
|
|
let mut cfgs = LinearMap::new();
|
|
cfgs.insert(name, Cow::Borrowed(&config));
|
|
for rule in &config_rules {
|
|
rule.check(&config, &cfgs)
|
|
.with_context(|e| format!("Default Config does not satisfy: {}", e))?;
|
|
}
|
|
if manifest.has_instructions {
|
|
let instructions = entries
|
|
.next()
|
|
.await
|
|
.ok_or_else(|| format_err!("missing instructions"))??;
|
|
ensure!(
|
|
instructions.path()?.to_str() == Some("instructions.md"),
|
|
"Package File Invalid or Corrupted: expected instructions.md, got {}",
|
|
instructions.path()?.display()
|
|
);
|
|
}
|
|
for asset_info in manifest.assets {
|
|
validate_path(&asset_info.src)?;
|
|
validate_path(&asset_info.dst)?;
|
|
let asset = entries
|
|
.next()
|
|
.await
|
|
.ok_or_else(|| format_err!("missing asset: {}", asset_info.src.display()))??;
|
|
if asset.header().entry_type().is_file() {
|
|
ensure!(
|
|
asset.path()?.to_str() == Some(&format!("{}", asset_info.src.display())),
|
|
"Package File Invalid or Corrupted: expected {}, got {}",
|
|
asset_info.src.display(),
|
|
asset.path()?.display()
|
|
);
|
|
} else if asset.header().entry_type().is_dir() {
|
|
ensure!(
|
|
asset.path()?.to_str() == Some(&format!("{}/", asset_info.src.display())),
|
|
"Package File Invalid or Corrupted: expected {}, got {}",
|
|
asset_info.src.display(),
|
|
asset.path()?.display()
|
|
);
|
|
loop {
|
|
let file = entries.next().await.ok_or_else(|| {
|
|
format_err!(
|
|
"missing directory end marker: APPMGR_DIR_END:{}",
|
|
asset_info.src.display()
|
|
)
|
|
})??;
|
|
if file
|
|
.path()?
|
|
.starts_with(format!("APPMGR_DIR_END:{}", asset_info.src.display()))
|
|
{
|
|
break;
|
|
} else {
|
|
ensure!(
|
|
file.path()?
|
|
.to_str()
|
|
.map(|p| p.starts_with(&format!("{}/", asset_info.src.display())))
|
|
.unwrap_or(false),
|
|
"Package File Invalid or Corrupted: expected {}, got {}",
|
|
asset_info.src.display(),
|
|
asset.path()?.display()
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
bail!("Asset Not Regular File: {}", asset_info.src.display());
|
|
}
|
|
}
|
|
match &manifest.image {
|
|
ImageConfig::Tar => {
|
|
#[derive(Clone, Debug, serde::Deserialize)]
|
|
#[serde(rename_all = "PascalCase")]
|
|
struct DockerManifest {
|
|
config: PathBuf,
|
|
repo_tags: Vec<String>,
|
|
layers: Vec<PathBuf>,
|
|
}
|
|
let image_name = format!("start9/{}", manifest.id);
|
|
log::debug!("Opening image.tar from archive.");
|
|
let image = entries
|
|
.next()
|
|
.await
|
|
.ok_or_else(|| format_err!("missing image.tar"))??;
|
|
let image_path = image.path()?;
|
|
if image_path != Path::new("image.tar") {
|
|
return Err(format_err!(
|
|
"Package File Invalid or Corrupted: expected image.tar, got {}",
|
|
image_path.display()
|
|
));
|
|
}
|
|
log::info!("Verifying image.tar.");
|
|
let mut image_tar = tar::Archive::new(image);
|
|
let image_manifest = image_tar
|
|
.entries()?
|
|
.map(|e| {
|
|
let e = e?;
|
|
Ok((e.path()?.to_path_buf(), e))
|
|
})
|
|
.filter_map(|res: Result<(PathBuf, tar::Entry<_>), std::io::Error>| {
|
|
futures::future::ready(match res {
|
|
Ok((path, e)) => {
|
|
if path == Path::new("manifest.json") {
|
|
Some(Ok(e))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
Err(e) => Some(Err(e)),
|
|
})
|
|
})
|
|
.next()
|
|
.await
|
|
.ok_or_else(|| format_err!("image.tar is missing manifest.json"))??;
|
|
let image_manifest: Vec<DockerManifest> =
|
|
from_json_async_reader(image_manifest).await?;
|
|
image_manifest
|
|
.into_iter()
|
|
.flat_map(|a| a.repo_tags)
|
|
.map(|t| {
|
|
if t.starts_with("start9/") {
|
|
if t.split(":").next().unwrap() != image_name {
|
|
Err(format_err!("Contains prohibited image tag: {}", t))
|
|
} else {
|
|
Ok(())
|
|
}
|
|
} else {
|
|
Ok(())
|
|
}
|
|
})
|
|
.collect::<Result<_, _>>()?;
|
|
}
|
|
};
|
|
|
|
Ok(())
|
|
}
|