Feature/hw filtering (#2368)

* update deno

* add proxy

* remove query params, now auto added by BE

* add hardware requirements and BE reg query params

* update query params for BE requests

* allow multiple arches in hw reqs

* explain git hash mismatch

* require lshw

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
This commit is contained in:
Aiden McClelland
2023-08-02 09:52:38 -06:00
committed by GitHub
parent 32ca91a7c9
commit 73229501c2
21 changed files with 349 additions and 160 deletions

7
backend/Cargo.lock generated
View File

@@ -4760,6 +4760,7 @@ dependencies = [
"trust-dns-server", "trust-dns-server",
"typed-builder", "typed-builder",
"url", "url",
"urlencoding",
"uuid", "uuid",
"zeroize", "zeroize",
] ]
@@ -5928,6 +5929,12 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "urlencoding"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9"
[[package]] [[package]]
name = "utf-8" name = "utf-8"
version = "0.7.6" version = "0.7.6"

View File

@@ -154,6 +154,7 @@ tracing-subscriber = { version = "0.3.14", features = ["env-filter"] }
trust-dns-server = "0.22.0" trust-dns-server = "0.22.0"
typed-builder = "0.10.0" typed-builder = "0.10.0"
url = { version = "2.2.2", features = ["serde"] } url = { version = "2.2.2", features = ["serde"] }
urlencoding = "2.1.2"
uuid = { version = "1.1.2", features = ["v4"] } uuid = { version = "1.1.2", features = ["v4"] }
zeroize = "1.5.7" zeroize = "1.5.7"

View File

@@ -11,7 +11,7 @@ use helpers::to_tmp_path;
use josekit::jwk::Jwk; use josekit::jwk::Jwk;
use patch_db::json_ptr::JsonPointer; use patch_db::json_ptr::JsonPointer;
use patch_db::{DbHandle, LockReceipt, LockType, PatchDb}; use patch_db::{DbHandle, LockReceipt, LockType, PatchDb};
use reqwest::Url; use reqwest::{Client, Proxy, Url};
use rpc_toolkit::Context; use rpc_toolkit::Context;
use serde::Deserialize; use serde::Deserialize;
use sqlx::postgres::PgConnectOptions; use sqlx::postgres::PgConnectOptions;
@@ -34,7 +34,9 @@ use crate::net::wifi::WpaCli;
use crate::notifications::NotificationManager; use crate::notifications::NotificationManager;
use crate::shutdown::Shutdown; use crate::shutdown::Shutdown;
use crate::status::{MainStatus, Status}; use crate::status::{MainStatus, Status};
use crate::system::get_mem_info;
use crate::util::config::load_config_from_paths; use crate::util::config::load_config_from_paths;
use crate::util::lshw::{lshw, LshwDevice};
use crate::{Error, ErrorKind, ResultExt}; use crate::{Error, ErrorKind, ResultExt};
#[derive(Debug, Default, Deserialize)] #[derive(Debug, Default, Deserialize)]
@@ -120,6 +122,13 @@ pub struct RpcContextSeed {
pub rpc_stream_continuations: Mutex<BTreeMap<RequestGuid, RpcContinuation>>, pub rpc_stream_continuations: Mutex<BTreeMap<RequestGuid, RpcContinuation>>,
pub wifi_manager: Option<Arc<RwLock<WpaCli>>>, pub wifi_manager: Option<Arc<RwLock<WpaCli>>>,
pub current_secret: Arc<Jwk>, pub current_secret: Arc<Jwk>,
pub client: Client,
pub hardware: Hardware,
}
pub struct Hardware {
pub devices: Vec<LshwDevice>,
pub ram: u64,
} }
pub struct RpcCleanReceipts { pub struct RpcCleanReceipts {
@@ -203,6 +212,9 @@ impl RpcContext {
let metrics_cache = RwLock::new(None); let metrics_cache = RwLock::new(None);
let notification_manager = NotificationManager::new(secret_store.clone()); let notification_manager = NotificationManager::new(secret_store.clone());
tracing::info!("Initialized Notification Manager"); tracing::info!("Initialized Notification Manager");
let tor_proxy_url = format!("socks5h://{tor_proxy}");
let devices = lshw().await?;
let ram = get_mem_info().await?.total.0 as u64 * 1024 * 1024;
let seed = Arc::new(RpcContextSeed { let seed = Arc::new(RpcContextSeed {
is_closed: AtomicBool::new(false), is_closed: AtomicBool::new(false),
datadir: base.datadir().to_path_buf(), datadir: base.datadir().to_path_buf(),
@@ -235,6 +247,17 @@ impl RpcContext {
) )
})?, })?,
), ),
client: Client::builder()
.proxy(Proxy::custom(move |url| {
if url.host_str().map_or(false, |h| h.ends_with(".onion")) {
Some(tor_proxy_url.clone())
} else {
None
}
}))
.build()
.with_kind(crate::ErrorKind::ParseUrl)?,
hardware: Hardware { devices, ram },
}); });
let res = Self(seed); let res = Self(seed);

View File

@@ -40,6 +40,7 @@ use crate::dependencies::{
}; };
use crate::install::cleanup::{cleanup, update_dependency_errors_of_dependents}; use crate::install::cleanup::{cleanup, update_dependency_errors_of_dependents};
use crate::install::progress::{InstallProgress, InstallProgressTracker}; use crate::install::progress::{InstallProgress, InstallProgressTracker};
use crate::marketplace::with_query_params;
use crate::notifications::NotificationLevel; use crate::notifications::NotificationLevel;
use crate::s9pk::manifest::{Manifest, PackageId}; use crate::s9pk::manifest::{Manifest, PackageId};
use crate::s9pk::reader::S9pkReader; use crate::s9pk::reader::S9pkReader;
@@ -136,35 +137,39 @@ pub async fn install(
let marketplace_url = let marketplace_url =
marketplace_url.unwrap_or_else(|| crate::DEFAULT_MARKETPLACE.parse().unwrap()); marketplace_url.unwrap_or_else(|| crate::DEFAULT_MARKETPLACE.parse().unwrap());
let version_priority = version_priority.unwrap_or_default(); let version_priority = version_priority.unwrap_or_default();
let man: Manifest = reqwest::get(format!( let man: Manifest = ctx
"{}/package/v0/manifest/{}?spec={}&version-priority={}&eos-version-compat={}&arch={}", .client
marketplace_url, .get(with_query_params(
id, &ctx,
version, format!(
version_priority, "{}/package/v0/manifest/{}?spec={}&version-priority={}",
Current::new().compat(), marketplace_url, id, version, version_priority,
&*crate::ARCH, )
)) .parse()?,
.await ))
.with_kind(crate::ErrorKind::Registry)? .send()
.error_for_status() .await
.with_kind(crate::ErrorKind::Registry)? .with_kind(crate::ErrorKind::Registry)?
.json() .error_for_status()
.await .with_kind(crate::ErrorKind::Registry)?
.with_kind(crate::ErrorKind::Registry)?; .json()
let s9pk = reqwest::get(format!( .await
"{}/package/v0/{}.s9pk?spec=={}&version-priority={}&eos-version-compat={}&arch={}", .with_kind(crate::ErrorKind::Registry)?;
marketplace_url, let s9pk = ctx
id, .client
man.version, .get(with_query_params(
version_priority, &ctx,
Current::new().compat(), format!(
&*crate::ARCH, "{}/package/v0/{}.s9pk?spec=={}&version-priority={}",
)) marketplace_url, id, man.version, version_priority,
.await )
.with_kind(crate::ErrorKind::Registry)? .parse()?,
.error_for_status() ))
.with_kind(crate::ErrorKind::Registry)?; .send()
.await
.with_kind(crate::ErrorKind::Registry)?
.error_for_status()
.with_kind(crate::ErrorKind::Registry)?;
if man.id.as_str() != id || !man.version.satisfies(&version) { if man.id.as_str() != id || !man.version.satisfies(&version) {
return Err(Error::new( return Err(Error::new(
@@ -185,16 +190,18 @@ pub async fn install(
async { async {
tokio::io::copy( tokio::io::copy(
&mut response_to_reader( &mut response_to_reader(
reqwest::get(format!( ctx.client
"{}/package/v0/license/{}?spec=={}&eos-version-compat={}&arch={}", .get(with_query_params(
marketplace_url, &ctx,
id, format!(
man.version, "{}/package/v0/license/{}?spec=={}",
Current::new().compat(), marketplace_url, id, man.version,
&*crate::ARCH, )
)) .parse()?,
.await? ))
.error_for_status()?, .send()
.await?
.error_for_status()?,
), ),
&mut File::create(public_dir_path.join("LICENSE.md")).await?, &mut File::create(public_dir_path.join("LICENSE.md")).await?,
) )
@@ -204,16 +211,18 @@ pub async fn install(
async { async {
tokio::io::copy( tokio::io::copy(
&mut response_to_reader( &mut response_to_reader(
reqwest::get(format!( ctx.client
"{}/package/v0/instructions/{}?spec=={}&eos-version-compat={}&arch={}", .get(with_query_params(
marketplace_url, &ctx,
id, format!(
man.version, "{}/package/v0/instructions/{}?spec=={}",
Current::new().compat(), marketplace_url, id, man.version,
&*crate::ARCH, )
)) .parse()?,
.await? ))
.error_for_status()?, .send()
.await?
.error_for_status()?,
), ),
&mut File::create(public_dir_path.join("INSTRUCTIONS.md")).await?, &mut File::create(public_dir_path.join("INSTRUCTIONS.md")).await?,
) )
@@ -223,16 +232,18 @@ pub async fn install(
async { async {
tokio::io::copy( tokio::io::copy(
&mut response_to_reader( &mut response_to_reader(
reqwest::get(format!( ctx.client
"{}/package/v0/icon/{}?spec=={}&eos-version-compat={}&arch={}", .get(with_query_params(
marketplace_url, &ctx,
id, format!(
man.version, "{}/package/v0/icon/{}?spec=={}",
Current::new().compat(), marketplace_url, id, man.version,
&*crate::ARCH, )
)) .parse()?,
.await? ))
.error_for_status()?, .send()
.await?
.error_for_status()?,
), ),
&mut File::create(public_dir_path.join(format!("icon.{}", icon_type))).await?, &mut File::create(public_dir_path.join(format!("icon.{}", icon_type))).await?,
) )
@@ -928,17 +939,20 @@ pub async fn install_s9pk<R: AsyncRead + AsyncSeek + Unpin + Send + Sync>(
{ {
Some(local_man) Some(local_man)
} else if let Some(marketplace_url) = &marketplace_url { } else if let Some(marketplace_url) = &marketplace_url {
match reqwest::get(format!( match ctx
"{}/package/v0/manifest/{}?spec={}&eos-version-compat={}&arch={}", .client
marketplace_url, .get(with_query_params(
dep, ctx,
info.version, format!(
Current::new().compat(), "{}/package/v0/manifest/{}?spec={}",
&*crate::ARCH, marketplace_url, dep, info.version,
)) )
.await .parse()?,
.with_kind(crate::ErrorKind::Registry)? ))
.error_for_status() .send()
.await
.with_kind(crate::ErrorKind::Registry)?
.error_for_status()
{ {
Ok(a) => Ok(Some( Ok(a) => Ok(Some(
a.json() a.json()
@@ -963,16 +977,19 @@ pub async fn install_s9pk<R: AsyncRead + AsyncSeek + Unpin + Send + Sync>(
let icon_path = dir.join(format!("icon.{}", manifest.assets.icon_type())); let icon_path = dir.join(format!("icon.{}", manifest.assets.icon_type()));
if tokio::fs::metadata(&icon_path).await.is_err() { if tokio::fs::metadata(&icon_path).await.is_err() {
tokio::fs::create_dir_all(&dir).await?; tokio::fs::create_dir_all(&dir).await?;
let icon = reqwest::get(format!( let icon = ctx
"{}/package/v0/icon/{}?spec={}&eos-version-compat={}&arch={}", .client
marketplace_url, .get(with_query_params(
dep, ctx,
info.version, format!(
Current::new().compat(), "{}/package/v0/icon/{}?spec={}",
&*crate::ARCH, marketplace_url, dep, info.version,
)) )
.await .parse()?,
.with_kind(crate::ErrorKind::Registry)?; ))
.send()
.await
.with_kind(crate::ErrorKind::Registry)?;
let mut dst = File::create(&icon_path).await?; let mut dst = File::create(&icon_path).await?;
tokio::io::copy(&mut response_to_reader(icon), &mut dst).await?; tokio::io::copy(&mut response_to_reader(icon), &mut dst).await?;
dst.sync_all().await?; dst.sync_all().await?;

View File

@@ -3,6 +3,8 @@ use reqwest::{StatusCode, Url};
use rpc_toolkit::command; use rpc_toolkit::command;
use serde_json::Value; use serde_json::Value;
use crate::context::RpcContext;
use crate::version::VersionT;
use crate::{Error, ResultExt}; use crate::{Error, ResultExt};
#[command(subcommands(get))] #[command(subcommands(get))]
@@ -10,9 +12,34 @@ pub fn marketplace() -> Result<(), Error> {
Ok(()) Ok(())
} }
pub fn with_query_params(ctx: &RpcContext, mut url: Url) -> Url {
url.query_pairs_mut()
.append_pair(
"os.version",
&crate::version::Current::new().semver().to_string(),
)
.append_pair(
"os.compat",
&crate::version::Current::new().compat().to_string(),
)
.append_pair("os.arch", crate::OS_ARCH)
.append_pair("hardware.arch", &*crate::ARCH)
.append_pair("hardware.ram", &ctx.hardware.ram.to_string());
for hw in &ctx.hardware.devices {
url.query_pairs_mut()
.append_pair(&format!("hardware.device.{}", hw.class()), hw.product());
}
url
}
#[command] #[command]
pub async fn get(#[arg] url: Url) -> Result<Value, Error> { pub async fn get(#[context] ctx: RpcContext, #[arg] url: Url) -> Result<Value, Error> {
let mut response = reqwest::get(url) let mut response = ctx
.client
.get(with_query_params(&ctx, url))
.send()
.await .await
.with_kind(crate::ErrorKind::Network)?; .with_kind(crate::ErrorKind::Network)?;
let status = response.status(); let status = response.status();

View File

@@ -38,6 +38,8 @@ static NOT_AUTHORIZED: &[u8] = b"Not Authorized";
static EMBEDDED_UIS: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../frontend/dist/static"); static EMBEDDED_UIS: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../frontend/dist/static");
const PROXY_STRIP_HEADERS: &[&str] = &["cookie", "host", "origin", "referer", "user-agent"];
fn status_fn(_: i32) -> StatusCode { fn status_fn(_: i32) -> StatusCode {
StatusCode::OK StatusCode::OK
} }
@@ -236,15 +238,6 @@ pub async fn main_ui_server_router(ctx: RpcContext) -> Result<HttpHandler, Error
async fn alt_ui(req: Request<Body>, ui_mode: UiMode) -> Result<Response<Body>, Error> { async fn alt_ui(req: Request<Body>, ui_mode: UiMode) -> Result<Response<Body>, Error> {
let (request_parts, _body) = req.into_parts(); let (request_parts, _body) = req.into_parts();
let accept_encoding = request_parts
.headers
.get_all(ACCEPT_ENCODING)
.into_iter()
.filter_map(|h| h.to_str().ok())
.flat_map(|s| s.split(","))
.filter_map(|s| s.split(";").next())
.map(|s| s.trim())
.collect::<Vec<_>>();
match &request_parts.method { match &request_parts.method {
&Method::GET => { &Method::GET => {
let uri_path = ui_mode.path( let uri_path = ui_mode.path(
@@ -307,6 +300,40 @@ async fn main_embassy_ui(req: Request<Body>, ctx: RpcContext) -> Result<Response
Err(e) => un_authorized(e, &format!("public/{path}")), Err(e) => un_authorized(e, &format!("public/{path}")),
} }
} }
(&Method::GET, Some(("proxy", target))) => {
match HasValidSession::from_request_parts(&request_parts, &ctx).await {
Ok(_) => {
let target = urlencoding::decode(target)?;
let res = ctx
.client
.get(target.as_ref())
.headers(
request_parts
.headers
.iter()
.filter(|(h, _)| {
!PROXY_STRIP_HEADERS
.iter()
.any(|bad| h.as_str().eq_ignore_ascii_case(bad))
})
.map(|(h, v)| (h.clone(), v.clone()))
.collect(),
)
.send()
.await
.with_kind(crate::ErrorKind::Network)?;
let mut hres = Response::builder().status(res.status());
for (h, v) in res.headers().clone() {
if let Some(h) = h {
hres = hres.header(h, v);
}
}
hres.body(Body::wrap_stream(res.bytes_stream()))
.with_kind(crate::ErrorKind::Network)
}
Err(e) => un_authorized(e, &format!("proxy/{target}")),
}
}
(&Method::GET, Some(("eos", "local.crt"))) => { (&Method::GET, Some(("eos", "local.crt"))) => {
match HasValidSession::from_request_parts(&request_parts, &ctx).await { match HasValidSession::from_request_parts(&request_parts, &ctx).await {
Ok(_) => cert_send(&ctx.account.read().await.root_ca_cert), Ok(_) => cert_send(&ctx.account.read().await.root_ca_cert),
@@ -550,8 +577,3 @@ fn e_tag(path: &Path, metadata: Option<&Metadata>) -> String {
base32::encode(base32::Alphabet::RFC4648 { padding: false }, res.as_slice()).to_lowercase() base32::encode(base32::Alphabet::RFC4648 { padding: false }, res.as_slice()).to_lowercase()
) )
} }
#[test]
fn test_packed_html() {
assert!(MainUi::get("index.html").is_some())
}

View File

@@ -1,3 +1,4 @@
use std::collections::BTreeMap;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use color_eyre::eyre::eyre; use color_eyre::eyre::eyre;
@@ -16,6 +17,7 @@ use crate::net::interface::Interfaces;
use crate::procedure::docker::DockerContainers; use crate::procedure::docker::DockerContainers;
use crate::procedure::PackageProcedure; use crate::procedure::PackageProcedure;
use crate::status::health_check::HealthChecks; use crate::status::health_check::HealthChecks;
use crate::util::serde::Regex;
use crate::util::Version; use crate::util::Version;
use crate::version::{Current, VersionT}; use crate::version::{Current, VersionT};
use crate::volume::Volumes; use crate::volume::Volumes;
@@ -79,6 +81,9 @@ pub struct Manifest {
#[serde(default)] #[serde(default)]
pub replaces: Vec<String>, pub replaces: Vec<String>,
#[serde(default)]
pub hardware_requirements: HardwareRequirements,
} }
impl Manifest { impl Manifest {
@@ -109,6 +114,15 @@ impl Manifest {
} }
} }
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct HardwareRequirements {
#[serde(default)]
device: BTreeMap<String, Regex>,
ram: Option<u64>,
arch: Option<Vec<String>>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)] #[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct Assets { pub struct Assets {

View File

@@ -251,7 +251,7 @@ impl<'de> Deserialize<'de> for Percentage {
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct MebiBytes(f64); pub struct MebiBytes(pub f64);
impl Serialize for MebiBytes { impl Serialize for MebiBytes {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where where
@@ -310,19 +310,19 @@ pub struct MetricsGeneral {
#[derive(Deserialize, Serialize, Clone, Debug)] #[derive(Deserialize, Serialize, Clone, Debug)]
pub struct MetricsMemory { pub struct MetricsMemory {
#[serde(rename = "Percentage Used")] #[serde(rename = "Percentage Used")]
percentage_used: Percentage, pub percentage_used: Percentage,
#[serde(rename = "Total")] #[serde(rename = "Total")]
total: MebiBytes, pub total: MebiBytes,
#[serde(rename = "Available")] #[serde(rename = "Available")]
available: MebiBytes, pub available: MebiBytes,
#[serde(rename = "Used")] #[serde(rename = "Used")]
used: MebiBytes, pub used: MebiBytes,
#[serde(rename = "Swap Total")] #[serde(rename = "Swap Total")]
swap_total: MebiBytes, pub swap_total: MebiBytes,
#[serde(rename = "Swap Free")] #[serde(rename = "Swap Free")]
swap_free: MebiBytes, pub swap_free: MebiBytes,
#[serde(rename = "Swap Used")] #[serde(rename = "Swap Used")]
swap_used: MebiBytes, pub swap_used: MebiBytes,
} }
#[derive(Deserialize, Serialize, Clone, Debug)] #[derive(Deserialize, Serialize, Clone, Debug)]
pub struct MetricsCpu { pub struct MetricsCpu {
@@ -698,7 +698,7 @@ pub struct MemInfo {
swap_free: Option<u64>, swap_free: Option<u64>,
} }
#[instrument(skip_all)] #[instrument(skip_all)]
async fn get_mem_info() -> Result<MetricsMemory, Error> { pub async fn get_mem_info() -> Result<MetricsMemory, Error> {
let contents = tokio::fs::read_to_string("/proc/meminfo").await?; let contents = tokio::fs::read_to_string("/proc/meminfo").await?;
let mut mem_info = MemInfo { let mut mem_info = MemInfo {
mem_total: None, mem_total: None,

View File

@@ -19,6 +19,7 @@ use crate::db::model::UpdateProgress;
use crate::disk::mount::filesystem::bind::Bind; use crate::disk::mount::filesystem::bind::Bind;
use crate::disk::mount::filesystem::ReadWrite; use crate::disk::mount::filesystem::ReadWrite;
use crate::disk::mount::guard::MountGuard; use crate::disk::mount::guard::MountGuard;
use crate::marketplace::with_query_params;
use crate::notifications::NotificationLevel; use crate::notifications::NotificationLevel;
use crate::sound::{ use crate::sound::{
CIRCLE_OF_5THS_SHORT, UPDATE_FAILED_1, UPDATE_FAILED_2, UPDATE_FAILED_3, UPDATE_FAILED_4, CIRCLE_OF_5THS_SHORT, UPDATE_FAILED_1, UPDATE_FAILED_2, UPDATE_FAILED_3, UPDATE_FAILED_4,
@@ -81,18 +82,19 @@ async fn maybe_do_update(
marketplace_url: Url, marketplace_url: Url,
) -> Result<Option<Arc<Revision>>, Error> { ) -> Result<Option<Arc<Revision>>, Error> {
let mut db = ctx.db.handle(); let mut db = ctx.db.handle();
let latest_version: Version = reqwest::get(format!( let latest_version: Version = ctx
"{}/eos/v0/latest?eos-version={}&arch={}", .client
marketplace_url, .get(with_query_params(
Current::new().semver(), &ctx,
OS_ARCH, format!("{}/eos/v0/latest", marketplace_url,).parse()?,
)) ))
.await .send()
.with_kind(ErrorKind::Network)? .await
.json::<LatestInformation>() .with_kind(ErrorKind::Network)?
.await .json::<LatestInformation>()
.with_kind(ErrorKind::Network)? .await
.version; .with_kind(ErrorKind::Network)?
.version;
crate::db::DatabaseModel::new() crate::db::DatabaseModel::new()
.server_info() .server_info()
.lock(&mut db, LockType::Write) .lock(&mut db, LockType::Write)

49
backend/src/util/lshw.rs Normal file
View File

@@ -0,0 +1,49 @@
use models::{Error, ResultExt};
use serde::{Deserialize, Serialize};
use tokio::process::Command;
use crate::util::Invoke;
const KNOWN_CLASSES: &[&str] = &["processor", "display"];
#[derive(Debug, Deserialize, Serialize)]
#[serde(tag = "class")]
#[serde(rename_all = "kebab-case")]
pub enum LshwDevice {
Processor(LshwProcessor),
Display(LshwDisplay),
}
impl LshwDevice {
pub fn class(&self) -> &'static str {
match self {
Self::Processor(_) => "processor",
Self::Display(_) => "display",
}
}
pub fn product(&self) -> &str {
match self {
Self::Processor(hw) => hw.product.as_str(),
Self::Display(hw) => hw.product.as_str(),
}
}
}
#[derive(Debug, Deserialize, Serialize)]
pub struct LshwProcessor {
pub product: String,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct LshwDisplay {
pub product: String,
}
pub async fn lshw() -> Result<Vec<LshwDevice>, Error> {
let mut cmd = Command::new("lshw");
cmd.arg("-json");
for class in KNOWN_CLASSES {
cmd.arg("-class").arg(*class);
}
serde_json::from_slice(&cmd.invoke(crate::ErrorKind::Lshw).await?)
.with_kind(crate::ErrorKind::Deserialization)
}

View File

@@ -27,6 +27,7 @@ pub mod config;
pub mod http_reader; pub mod http_reader;
pub mod io; pub mod io;
pub mod logger; pub mod logger;
pub mod lshw;
pub mod serde; pub mod serde;
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]

View File

@@ -793,3 +793,42 @@ impl<T: AsRef<[u8]>> Serialize for Base64<T> {
serializer.serialize_str(&base64::encode(self.0.as_ref())) serializer.serialize_str(&base64::encode(self.0.as_ref()))
} }
} }
#[derive(Clone, Debug)]
pub struct Regex(regex::Regex);
impl From<Regex> for regex::Regex {
fn from(value: Regex) -> Self {
value.0
}
}
impl From<regex::Regex> for Regex {
fn from(value: regex::Regex) -> Self {
Regex(value)
}
}
impl AsRef<regex::Regex> for Regex {
fn as_ref(&self) -> &regex::Regex {
&self.0
}
}
impl AsMut<regex::Regex> for Regex {
fn as_mut(&mut self) -> &mut regex::Regex {
&mut self.0
}
}
impl<'de> Deserialize<'de> for Regex {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserialize_from_str(deserializer).map(Self)
}
}
impl Serialize for Regex {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serialize_display(&self.0, serializer)
}
}

View File

@@ -24,6 +24,7 @@ iw
jq jq
libavahi-client3 libavahi-client3
lm-sensors lm-sensors
lshw
lvm2 lvm2
magic-wormhole magic-wormhole
man-db man-db

View File

@@ -71,6 +71,7 @@ sudo losetup -d $OUTPUT_DEVICE
if [ "$ALLOW_VERSION_MISMATCH" != 1 ]; then if [ "$ALLOW_VERSION_MISMATCH" != 1 ]; then
if [ "$(cat GIT_HASH.txt)" != "$REAL_GIT_HASH" ]; then if [ "$(cat GIT_HASH.txt)" != "$REAL_GIT_HASH" ]; then
>&2 echo "startos.raspberrypi.squashfs GIT_HASH.txt mismatch" >&2 echo "startos.raspberrypi.squashfs GIT_HASH.txt mismatch"
>&2 echo "expected $REAL_GIT_HASH (dpkg) found $(cat GIT_HASH.txt) (repo)"
exit 1 exit 1
fi fi
if [ "$(cat VERSION.txt)" != "$REAL_VERSION" ]; then if [ "$(cat VERSION.txt)" != "$REAL_VERSION" ]; then

View File

@@ -259,19 +259,14 @@ export module RR {
// marketplace // marketplace
export type EnvInfo = { export type GetMarketplaceInfoReq = { 'server-id': string }
'server-id': string
'eos-version': string
}
export type GetMarketplaceInfoReq = EnvInfo
export type GetMarketplaceInfoRes = StoreInfo export type GetMarketplaceInfoRes = StoreInfo
export type GetMarketplaceEosReq = EnvInfo export type GetMarketplaceEosReq = { 'server-id': string }
export type GetMarketplaceEosRes = MarketplaceEOS export type GetMarketplaceEosRes = MarketplaceEOS
export type GetMarketplacePackagesReq = { export type GetMarketplacePackagesReq = {
ids?: { id: string; version: string }[] ids?: { id: string; version: string }[]
'eos-version-compat': string
// iff !ids // iff !ids
category?: string category?: string
query?: string query?: string

View File

@@ -125,7 +125,6 @@ export abstract class ApiService {
path: string, path: string,
params: Record<string, unknown>, params: Record<string, unknown>,
url: string, url: string,
arch?: string,
): Promise<T> ): Promise<T>
abstract getEos(): Promise<RR.GetMarketplaceEosRes> abstract getEos(): Promise<RR.GetMarketplaceEosRes>

View File

@@ -212,10 +212,7 @@ export class LiveApiService extends ApiService {
path: string, path: string,
qp: Record<string, string>, qp: Record<string, string>,
baseUrl: string, baseUrl: string,
arch: string = this.config.packageArch,
): Promise<T> { ): Promise<T> {
// Object.assign(qp, { arch })
qp['arch'] = arch
const fullUrl = `${baseUrl}${path}?${new URLSearchParams(qp).toString()}` const fullUrl = `${baseUrl}${path}?${new URLSearchParams(qp).toString()}`
return this.rpcRequest({ return this.rpcRequest({
method: 'marketplace.get', method: 'marketplace.get',
@@ -224,17 +221,13 @@ export class LiveApiService extends ApiService {
} }
async getEos(): Promise<RR.GetMarketplaceEosRes> { async getEos(): Promise<RR.GetMarketplaceEosRes> {
const { id, version } = await getServerInfo(this.patch) const { id } = await getServerInfo(this.patch)
const qp: RR.GetMarketplaceEosReq = { const qp: RR.GetMarketplaceEosReq = { 'server-id': id }
'server-id': id,
'eos-version': version,
}
return this.marketplaceProxy( return this.marketplaceProxy(
'/eos/v0/latest', '/eos/v0/latest',
qp, qp,
this.config.marketplace.start9, this.config.marketplace.start9,
this.config.osArch,
) )
} }

View File

@@ -352,7 +352,6 @@ export class MockApiService extends ApiService {
path: string, path: string,
params: Record<string, string>, params: Record<string, string>,
url: string, url: string,
arch = '',
): Promise<any> { ): Promise<any> {
await pauseFor(2000) await pauseFor(2000)

View File

@@ -217,10 +217,7 @@ export class MarketplaceService implements AbstractMarketplaceService {
return this.patch.watch$('server-info').pipe( return this.patch.watch$('server-info').pipe(
take(1), take(1),
switchMap(serverInfo => { switchMap(serverInfo => {
const qp: RR.GetMarketplaceInfoReq = { const qp: RR.GetMarketplaceInfoReq = { 'server-id': serverInfo.id }
'server-id': serverInfo.id,
'eos-version': serverInfo.version,
}
return this.api.marketplaceProxy<RR.GetMarketplaceInfoRes>( return this.api.marketplaceProxy<RR.GetMarketplaceInfoRes>(
'/package/v0/info', '/package/v0/info',
qp, qp,
@@ -274,28 +271,21 @@ export class MarketplaceService implements AbstractMarketplaceService {
private fetchPackages$( private fetchPackages$(
url: string, url: string,
params: Omit< params: Omit<RR.GetMarketplacePackagesReq, 'page' | 'per-page'> = {},
RR.GetMarketplacePackagesReq,
'eos-version-compat' | 'page' | 'per-page'
> = {},
): Observable<MarketplacePkg[]> { ): Observable<MarketplacePkg[]> {
return this.patch.watch$('server-info', 'eos-version-compat').pipe( const qp: RR.GetMarketplacePackagesReq = {
take(1), ...params,
switchMap(versionCompat => { page: 1,
const qp: RR.GetMarketplacePackagesReq = { 'per-page': 100,
...params, }
'eos-version-compat': versionCompat, if (qp.ids) qp.ids = JSON.stringify(qp.ids)
page: 1,
'per-page': 100,
}
if (qp.ids) qp.ids = JSON.stringify(qp.ids)
return this.api.marketplaceProxy<RR.GetMarketplacePackagesRes>( return from(
'/package/v0/index', this.api.marketplaceProxy<RR.GetMarketplacePackagesRes>(
qp, '/package/v0/index',
url, qp,
) url,
}), ),
) )
} }

View File

@@ -76,6 +76,7 @@ pub enum ErrorKind {
Systemd = 65, Systemd = 65,
OpenSsh = 66, OpenSsh = 66,
Zram = 67, Zram = 67,
Lshw = 68,
} }
impl ErrorKind { impl ErrorKind {
pub fn as_str(&self) -> &'static str { pub fn as_str(&self) -> &'static str {
@@ -148,6 +149,7 @@ impl ErrorKind {
Systemd => "Systemd Error", Systemd => "Systemd Error",
OpenSsh => "OpenSSH Error", OpenSsh => "OpenSSH Error",
Zram => "Zram Error", Zram => "Zram Error",
Lshw => "LSHW Error",
} }
} }
} }

View File

@@ -4296,6 +4296,7 @@ dependencies = [
"trust-dns-server", "trust-dns-server",
"typed-builder", "typed-builder",
"url", "url",
"urlencoding",
"uuid 1.2.2", "uuid 1.2.2",
"zeroize", "zeroize",
] ]
@@ -5021,6 +5022,12 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "urlencoding"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9"
[[package]] [[package]]
name = "utf-8" name = "utf-8"
version = "0.7.6" version = "0.7.6"