diff --git a/README.md b/README.md
index 5bca6b0b1..125a956a4 100644
--- a/README.md
+++ b/README.md
@@ -1,26 +1,56 @@
-# StartOS
-[](https://github.com/Start9Labs/start-os/releases)
-[](https://github.com/Start9Labs/start-os/actions/workflows/startos-iso.yaml)
-[](https://matrix.to/#/#community:matrix.start9labs.com)
-[](https://t.me/start9_labs)
-[](https://docs.start9.com)
-[](https://matrix.to/#/#community-dev:matrix.start9labs.com)
-[](https://start9.com)
-
-[](http://mastodon.start9labs.com)
-[](https://twitter.com/start9labs)
-
-### _Welcome to the era of Sovereign Computing_ ###
-
-StartOS is a Debian-based Linux distro optimized for running a personal server. It facilitates the discovery, installation, network configuration, service configuration, data backup, dependency management, and health monitoring of self-hosted software services.
+
+
+
+
+ Welcome to the era of Sovereign Computing
+
+
+ StartOS is a Debian-based Linux distro optimized for running a personal server. It facilitates the discovery, installation, network configuration, service configuration, data backup, dependency management, and health monitoring of self-hosted software services.
+
+
+
+
+
+
+
## Running StartOS
-There are multiple ways to get your hands on StartOS.
+There are multiple ways to get started with StartOS:
-### :moneybag: Buy a Start9 server
+### 💰 Buy a Start9 server
This is the most convenient option. Simply [buy a server](https://store.start9.com) from Start9 and plug it in.
-### :construction_worker: Build your own server
+### 👷 Build your own server
This option is easier than you might imagine, and there are 4 reasons why you might prefer it:
1. You already have hardware
1. You want to save on shipping costs
@@ -29,20 +59,23 @@ This option is easier than you might imagine, and there are 4 reasons why you mi
To pursue this option, follow one of our [DIY guides](https://start9.com/latest/diy).
-## :heart: Contributing
+## ❤️ Contributing
There are multiple ways to contribute: work directly on StartOS, package a service for the marketplace, or help with documentation and guides. To learn more about contributing, see [here](https://start9.com/contribute/).
To report security issues, please email our security team - security@start9.com.
-## UI Screenshots
+## 🌎 Marketplace
+There are dozens of service available for StartOS, and new ones are being added all the time. Check out the full list of available services [here](https://marketplace.start9.com/marketplace). To read more about the Marketplace ecosystem, check out this [blog post](https://blog.start9.com/start9-marketplace-strategy/)
+
+## 🖥️ User Interface Screenshots
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/assets/btcpay.png b/assets/btcpay.png
new file mode 100644
index 000000000..9ceb2f1ca
Binary files /dev/null and b/assets/btcpay.png differ
diff --git a/assets/c-lightning.png b/assets/c-lightning.png
new file mode 100644
index 000000000..f0dcf660e
Binary files /dev/null and b/assets/c-lightning.png differ
diff --git a/assets/community.png b/assets/community.png
new file mode 100644
index 000000000..b0ec06d37
Binary files /dev/null and b/assets/community.png differ
diff --git a/assets/logs.png b/assets/logs.png
new file mode 100644
index 000000000..39788c0ff
Binary files /dev/null and b/assets/logs.png differ
diff --git a/assets/marketplace.png b/assets/marketplace.png
deleted file mode 100644
index fda737126..000000000
Binary files a/assets/marketplace.png and /dev/null differ
diff --git a/assets/nextcloud.png b/assets/nextcloud.png
index 1e48adc24..4dd4e7c31 100644
Binary files a/assets/nextcloud.png and b/assets/nextcloud.png differ
diff --git a/assets/nostr.png b/assets/nostr.png
deleted file mode 100644
index c60bb7b7c..000000000
Binary files a/assets/nostr.png and /dev/null differ
diff --git a/assets/registry.png b/assets/registry.png
new file mode 100644
index 000000000..4660a3c75
Binary files /dev/null and b/assets/registry.png differ
diff --git a/assets/system.png b/assets/system.png
index e824239fd..d24ba6dea 100644
Binary files a/assets/system.png and b/assets/system.png differ
diff --git a/assets/welcome.png b/assets/welcome.png
new file mode 100644
index 000000000..b3857383b
Binary files /dev/null and b/assets/welcome.png differ
diff --git a/backend/Cargo.lock b/backend/Cargo.lock
index 82c501f3f..508d65a9b 100644
--- a/backend/Cargo.lock
+++ b/backend/Cargo.lock
@@ -4782,6 +4782,7 @@ dependencies = [
"trust-dns-server",
"typed-builder",
"url",
+ "urlencoding",
"uuid",
"zeroize",
]
@@ -5950,6 +5951,12 @@ dependencies = [
"serde",
]
+[[package]]
+name = "urlencoding"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9"
+
[[package]]
name = "utf-8"
version = "0.7.6"
diff --git a/backend/Cargo.toml b/backend/Cargo.toml
index bd30771f5..6c6c706f9 100644
--- a/backend/Cargo.toml
+++ b/backend/Cargo.toml
@@ -159,6 +159,7 @@ tracing-subscriber = { version = "0.3.14", features = ["env-filter"] }
trust-dns-server = "0.22.0"
typed-builder = "0.10.0"
url = { version = "2.2.2", features = ["serde"] }
+urlencoding = "2.1.2"
uuid = { version = "1.1.2", features = ["v4"] }
zeroize = "1.5.7"
diff --git a/backend/src/context/rpc.rs b/backend/src/context/rpc.rs
index d453cd26e..f1ab1a79e 100644
--- a/backend/src/context/rpc.rs
+++ b/backend/src/context/rpc.rs
@@ -12,7 +12,7 @@ use josekit::jwk::Jwk;
use models::PackageId;
use patch_db::json_ptr::JsonPointer;
use patch_db::{DbHandle, LockReceipt, LockType, PatchDb};
-use reqwest::Url;
+use reqwest::{Client, Proxy, Url};
use rpc_toolkit::Context;
use serde::Deserialize;
use sqlx::postgres::PgConnectOptions;
@@ -36,7 +36,9 @@ use crate::net::wifi::WpaCli;
use crate::notifications::NotificationManager;
use crate::shutdown::Shutdown;
use crate::status::{MainStatus, Status};
+use crate::system::get_mem_info;
use crate::util::config::load_config_from_paths;
+use crate::util::lshw::{lshw, LshwDevice};
use crate::{Error, ErrorKind, ResultExt};
#[derive(Debug, Default, Deserialize)]
@@ -123,6 +125,13 @@ pub struct RpcContextSeed {
pub wifi_manager: Option>>,
pub current_secret: Arc,
pub config_hooks: Mutex>>,
+ pub client: Client,
+ pub hardware: Hardware,
+}
+
+pub struct Hardware {
+ pub devices: Vec,
+ pub ram: u64,
}
pub struct RpcCleanReceipts {
@@ -206,6 +215,9 @@ impl RpcContext {
let metrics_cache = RwLock::new(None);
let notification_manager = NotificationManager::new(secret_store.clone());
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 {
is_closed: AtomicBool::new(false),
datadir: base.datadir().to_path_buf(),
@@ -239,6 +251,17 @@ impl RpcContext {
})?,
),
config_hooks: Mutex::new(BTreeMap::new()),
+ 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);
diff --git a/backend/src/install/mod.rs b/backend/src/install/mod.rs
index 81794ddf2..e57e61afb 100644
--- a/backend/src/install/mod.rs
+++ b/backend/src/install/mod.rs
@@ -41,6 +41,7 @@ use crate::dependencies::{
};
use crate::install::cleanup::{cleanup, update_dependency_errors_of_dependents};
use crate::install::progress::{InstallProgress, InstallProgressTracker};
+use crate::marketplace::with_query_params;
use crate::notifications::NotificationLevel;
use crate::s9pk::manifest::{Manifest, PackageId};
use crate::s9pk::reader::S9pkReader;
@@ -151,35 +152,39 @@ pub async fn install(
let marketplace_url =
marketplace_url.unwrap_or_else(|| crate::DEFAULT_MARKETPLACE.parse().unwrap());
let version_priority = version_priority.unwrap_or_default();
- let man: Manifest = reqwest::get(format!(
- "{}/package/v0/manifest/{}?spec={}&version-priority={}&eos-version-compat={}&arch={}",
- marketplace_url,
- id,
- version,
- version_priority,
- Current::new().compat(),
- &*crate::ARCH,
- ))
- .await
- .with_kind(crate::ErrorKind::Registry)?
- .error_for_status()
- .with_kind(crate::ErrorKind::Registry)?
- .json()
- .await
- .with_kind(crate::ErrorKind::Registry)?;
- let s9pk = reqwest::get(format!(
- "{}/package/v0/{}.s9pk?spec=={}&version-priority={}&eos-version-compat={}&arch={}",
- marketplace_url,
- id,
- man.version,
- version_priority,
- Current::new().compat(),
- &*crate::ARCH,
- ))
- .await
- .with_kind(crate::ErrorKind::Registry)?
- .error_for_status()
- .with_kind(crate::ErrorKind::Registry)?;
+ let man: Manifest = ctx
+ .client
+ .get(with_query_params(
+ &ctx,
+ format!(
+ "{}/package/v0/manifest/{}?spec={}&version-priority={}",
+ marketplace_url, id, version, version_priority,
+ )
+ .parse()?,
+ ))
+ .send()
+ .await
+ .with_kind(crate::ErrorKind::Registry)?
+ .error_for_status()
+ .with_kind(crate::ErrorKind::Registry)?
+ .json()
+ .await
+ .with_kind(crate::ErrorKind::Registry)?;
+ let s9pk = ctx
+ .client
+ .get(with_query_params(
+ &ctx,
+ format!(
+ "{}/package/v0/{}.s9pk?spec=={}&version-priority={}",
+ marketplace_url, id, man.version, version_priority,
+ )
+ .parse()?,
+ ))
+ .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) {
return Err(Error::new(
@@ -200,16 +205,18 @@ pub async fn install(
async {
tokio::io::copy(
&mut response_to_reader(
- reqwest::get(format!(
- "{}/package/v0/license/{}?spec=={}&eos-version-compat={}&arch={}",
- marketplace_url,
- id,
- man.version,
- Current::new().compat(),
- &*crate::ARCH,
- ))
- .await?
- .error_for_status()?,
+ ctx.client
+ .get(with_query_params(
+ &ctx,
+ format!(
+ "{}/package/v0/license/{}?spec=={}",
+ marketplace_url, id, man.version,
+ )
+ .parse()?,
+ ))
+ .send()
+ .await?
+ .error_for_status()?,
),
&mut File::create(public_dir_path.join("LICENSE.md")).await?,
)
@@ -219,16 +226,18 @@ pub async fn install(
async {
tokio::io::copy(
&mut response_to_reader(
- reqwest::get(format!(
- "{}/package/v0/instructions/{}?spec=={}&eos-version-compat={}&arch={}",
- marketplace_url,
- id,
- man.version,
- Current::new().compat(),
- &*crate::ARCH,
- ))
- .await?
- .error_for_status()?,
+ ctx.client
+ .get(with_query_params(
+ &ctx,
+ format!(
+ "{}/package/v0/instructions/{}?spec=={}",
+ marketplace_url, id, man.version,
+ )
+ .parse()?,
+ ))
+ .send()
+ .await?
+ .error_for_status()?,
),
&mut File::create(public_dir_path.join("INSTRUCTIONS.md")).await?,
)
@@ -238,16 +247,18 @@ pub async fn install(
async {
tokio::io::copy(
&mut response_to_reader(
- reqwest::get(format!(
- "{}/package/v0/icon/{}?spec=={}&eos-version-compat={}&arch={}",
- marketplace_url,
- id,
- man.version,
- Current::new().compat(),
- &*crate::ARCH,
- ))
- .await?
- .error_for_status()?,
+ ctx.client
+ .get(with_query_params(
+ &ctx,
+ format!(
+ "{}/package/v0/icon/{}?spec=={}",
+ marketplace_url, id, man.version,
+ )
+ .parse()?,
+ ))
+ .send()
+ .await?
+ .error_for_status()?,
),
&mut File::create(public_dir_path.join(format!("icon.{}", icon_type))).await?,
)
@@ -943,17 +954,20 @@ pub async fn install_s9pk(
{
Some(local_man)
} else if let Some(marketplace_url) = &marketplace_url {
- match reqwest::get(format!(
- "{}/package/v0/manifest/{}?spec={}&eos-version-compat={}&arch={}",
- marketplace_url,
- dep,
- info.version,
- Current::new().compat(),
- &*crate::ARCH,
- ))
- .await
- .with_kind(crate::ErrorKind::Registry)?
- .error_for_status()
+ match ctx
+ .client
+ .get(with_query_params(
+ ctx,
+ format!(
+ "{}/package/v0/manifest/{}?spec={}",
+ marketplace_url, dep, info.version,
+ )
+ .parse()?,
+ ))
+ .send()
+ .await
+ .with_kind(crate::ErrorKind::Registry)?
+ .error_for_status()
{
Ok(a) => Ok(Some(
a.json()
@@ -978,16 +992,19 @@ pub async fn install_s9pk(
let icon_path = dir.join(format!("icon.{}", manifest.assets.icon_type()));
if tokio::fs::metadata(&icon_path).await.is_err() {
tokio::fs::create_dir_all(&dir).await?;
- let icon = reqwest::get(format!(
- "{}/package/v0/icon/{}?spec={}&eos-version-compat={}&arch={}",
- marketplace_url,
- dep,
- info.version,
- Current::new().compat(),
- &*crate::ARCH,
- ))
- .await
- .with_kind(crate::ErrorKind::Registry)?;
+ let icon = ctx
+ .client
+ .get(with_query_params(
+ ctx,
+ format!(
+ "{}/package/v0/icon/{}?spec={}",
+ marketplace_url, dep, info.version,
+ )
+ .parse()?,
+ ))
+ .send()
+ .await
+ .with_kind(crate::ErrorKind::Registry)?;
let mut dst = File::create(&icon_path).await?;
tokio::io::copy(&mut response_to_reader(icon), &mut dst).await?;
dst.sync_all().await?;
diff --git a/backend/src/marketplace.rs b/backend/src/marketplace.rs
index f616174d5..40b81d1cb 100644
--- a/backend/src/marketplace.rs
+++ b/backend/src/marketplace.rs
@@ -3,6 +3,8 @@ use reqwest::{StatusCode, Url};
use rpc_toolkit::command;
use serde_json::Value;
+use crate::context::RpcContext;
+use crate::version::VersionT;
use crate::{Error, ResultExt};
#[command(subcommands(get))]
@@ -10,9 +12,34 @@ pub fn marketplace() -> Result<(), Error> {
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]
-pub async fn get(#[arg] url: Url) -> Result {
- let mut response = reqwest::get(url)
+pub async fn get(#[context] ctx: RpcContext, #[arg] url: Url) -> Result {
+ let mut response = ctx
+ .client
+ .get(with_query_params(&ctx, url))
+ .send()
.await
.with_kind(crate::ErrorKind::Network)?;
let status = response.status();
diff --git a/backend/src/net/static_server.rs b/backend/src/net/static_server.rs
index e640d49e9..d191d7724 100644
--- a/backend/src/net/static_server.rs
+++ b/backend/src/net/static_server.rs
@@ -38,6 +38,8 @@ static NOT_AUTHORIZED: &[u8] = b"Not Authorized";
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 {
StatusCode::OK
}
@@ -236,15 +238,6 @@ pub async fn main_ui_server_router(ctx: RpcContext) -> Result, ui_mode: UiMode) -> Result, Error> {
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::>();
match &request_parts.method {
&Method::GET => {
let uri_path = ui_mode.path(
@@ -307,6 +300,40 @@ async fn main_embassy_ui(req: Request, ctx: RpcContext) -> Result 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"))) => {
match HasValidSession::from_request_parts(&request_parts, &ctx).await {
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()
)
}
-
-#[test]
-fn test_packed_html() {
- assert!(MainUi::get("index.html").is_some())
-}
diff --git a/backend/src/s9pk/manifest.rs b/backend/src/s9pk/manifest.rs
index 168cdc5dc..52b499375 100644
--- a/backend/src/s9pk/manifest.rs
+++ b/backend/src/s9pk/manifest.rs
@@ -1,3 +1,4 @@
+use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use color_eyre::eyre::eyre;
@@ -16,6 +17,7 @@ use crate::net::interface::Interfaces;
use crate::procedure::docker::DockerContainers;
use crate::procedure::PackageProcedure;
use crate::status::health_check::HealthChecks;
+use crate::util::serde::Regex;
use crate::util::Version;
use crate::version::{Current, VersionT};
use crate::volume::Volumes;
@@ -79,6 +81,9 @@ pub struct Manifest {
#[serde(default)]
pub replaces: Vec,
+
+ #[serde(default)]
+ pub hardware_requirements: HardwareRequirements,
}
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,
+ ram: Option,
+ arch: Option>,
+}
+
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct Assets {
diff --git a/backend/src/system.rs b/backend/src/system.rs
index 138e01fea..9e8e0e049 100644
--- a/backend/src/system.rs
+++ b/backend/src/system.rs
@@ -251,7 +251,7 @@ impl<'de> Deserialize<'de> for Percentage {
}
#[derive(Clone, Debug)]
-pub struct MebiBytes(f64);
+pub struct MebiBytes(pub f64);
impl Serialize for MebiBytes {
fn serialize(&self, serializer: S) -> Result
where
@@ -310,19 +310,19 @@ pub struct MetricsGeneral {
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct MetricsMemory {
#[serde(rename = "Percentage Used")]
- percentage_used: Percentage,
+ pub percentage_used: Percentage,
#[serde(rename = "Total")]
- total: MebiBytes,
+ pub total: MebiBytes,
#[serde(rename = "Available")]
- available: MebiBytes,
+ pub available: MebiBytes,
#[serde(rename = "Used")]
- used: MebiBytes,
+ pub used: MebiBytes,
#[serde(rename = "Swap Total")]
- swap_total: MebiBytes,
+ pub swap_total: MebiBytes,
#[serde(rename = "Swap Free")]
- swap_free: MebiBytes,
+ pub swap_free: MebiBytes,
#[serde(rename = "Swap Used")]
- swap_used: MebiBytes,
+ pub swap_used: MebiBytes,
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct MetricsCpu {
@@ -698,7 +698,7 @@ pub struct MemInfo {
swap_free: Option,
}
#[instrument(skip_all)]
-async fn get_mem_info() -> Result {
+pub async fn get_mem_info() -> Result {
let contents = tokio::fs::read_to_string("/proc/meminfo").await?;
let mut mem_info = MemInfo {
mem_total: None,
diff --git a/backend/src/update/mod.rs b/backend/src/update/mod.rs
index 4618d49d5..90ac2870c 100644
--- a/backend/src/update/mod.rs
+++ b/backend/src/update/mod.rs
@@ -19,6 +19,7 @@ use crate::db::model::UpdateProgress;
use crate::disk::mount::filesystem::bind::Bind;
use crate::disk::mount::filesystem::ReadWrite;
use crate::disk::mount::guard::MountGuard;
+use crate::marketplace::with_query_params;
use crate::notifications::NotificationLevel;
use crate::sound::{
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,
) -> Result>, Error> {
let mut db = ctx.db.handle();
- let latest_version: Version = reqwest::get(format!(
- "{}/eos/v0/latest?eos-version={}&arch={}",
- marketplace_url,
- Current::new().semver(),
- OS_ARCH,
- ))
- .await
- .with_kind(ErrorKind::Network)?
- .json::()
- .await
- .with_kind(ErrorKind::Network)?
- .version;
+ let latest_version: Version = ctx
+ .client
+ .get(with_query_params(
+ &ctx,
+ format!("{}/eos/v0/latest", marketplace_url,).parse()?,
+ ))
+ .send()
+ .await
+ .with_kind(ErrorKind::Network)?
+ .json::()
+ .await
+ .with_kind(ErrorKind::Network)?
+ .version;
crate::db::DatabaseModel::new()
.server_info()
.lock(&mut db, LockType::Write)
diff --git a/backend/src/util/lshw.rs b/backend/src/util/lshw.rs
new file mode 100644
index 000000000..4de4bf46e
--- /dev/null
+++ b/backend/src/util/lshw.rs
@@ -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, 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)
+}
diff --git a/backend/src/util/mod.rs b/backend/src/util/mod.rs
index 52686fd27..0b70604b0 100644
--- a/backend/src/util/mod.rs
+++ b/backend/src/util/mod.rs
@@ -27,6 +27,7 @@ pub mod config;
pub mod http_reader;
pub mod io;
pub mod logger;
+pub mod lshw;
pub mod serde;
#[derive(Clone, Copy, Debug)]
diff --git a/backend/src/util/serde.rs b/backend/src/util/serde.rs
index 75d8cc172..856923c09 100644
--- a/backend/src/util/serde.rs
+++ b/backend/src/util/serde.rs
@@ -793,3 +793,42 @@ impl> Serialize for Base64 {
serializer.serialize_str(&base64::encode(self.0.as_ref()))
}
}
+
+#[derive(Clone, Debug)]
+pub struct Regex(regex::Regex);
+impl From for regex::Regex {
+ fn from(value: Regex) -> Self {
+ value.0
+ }
+}
+impl From for Regex {
+ fn from(value: regex::Regex) -> Self {
+ Regex(value)
+ }
+}
+impl AsRef for Regex {
+ fn as_ref(&self) -> ®ex::Regex {
+ &self.0
+ }
+}
+impl AsMut for Regex {
+ fn as_mut(&mut self) -> &mut regex::Regex {
+ &mut self.0
+ }
+}
+impl<'de> Deserialize<'de> for Regex {
+ fn deserialize(deserializer: D) -> Result
+ where
+ D: Deserializer<'de>,
+ {
+ deserialize_from_str(deserializer).map(Self)
+ }
+}
+impl Serialize for Regex {
+ fn serialize(&self, serializer: S) -> Result
+ where
+ S: Serializer,
+ {
+ serialize_display(&self.0, serializer)
+ }
+}
diff --git a/build/lib/depends b/build/lib/depends
index fc35c5335..23d19660e 100644
--- a/build/lib/depends
+++ b/build/lib/depends
@@ -24,6 +24,7 @@ iw
jq
libavahi-client3
lm-sensors
+lshw
lvm2
magic-wormhole
man-db
diff --git a/build/raspberrypi/make-image.sh b/build/raspberrypi/make-image.sh
index 76db9e417..3a0bf5291 100755
--- a/build/raspberrypi/make-image.sh
+++ b/build/raspberrypi/make-image.sh
@@ -71,6 +71,7 @@ sudo losetup -d $OUTPUT_DEVICE
if [ "$ALLOW_VERSION_MISMATCH" != 1 ]; then
if [ "$(cat GIT_HASH.txt)" != "$REAL_GIT_HASH" ]; then
>&2 echo "startos.raspberrypi.squashfs GIT_HASH.txt mismatch"
+ >&2 echo "expected $REAL_GIT_HASH (dpkg) found $(cat GIT_HASH.txt) (repo)"
exit 1
fi
if [ "$(cat VERSION.txt)" != "$REAL_VERSION" ]; then
diff --git a/frontend/README.md b/frontend/README.md
index 369b9cfe6..2bce95a6b 100644
--- a/frontend/README.md
+++ b/frontend/README.md
@@ -19,7 +19,7 @@ Check your versions
```sh
node --version
-v16.10.0
+v18.15.0
npm --version
v8.0.0
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/server-specs/server-specs.module.ts b/frontend/projects/ui/src/app/apps/ui/pages/system/server-specs/server-specs.module.ts
index 2393527ac..eff288eb2 100644
--- a/frontend/projects/ui/src/app/apps/ui/pages/system/server-specs/server-specs.module.ts
+++ b/frontend/projects/ui/src/app/apps/ui/pages/system/server-specs/server-specs.module.ts
@@ -5,6 +5,7 @@ import { IonicModule } from '@ionic/angular'
import { ServerSpecsPage } from './server-specs.page'
import { EmverPipesModule } from '@start9labs/shared'
import { TuiLetModule } from '@taiga-ui/cdk'
+import { QRComponentModule } from 'src/app/components/qr/qr.component.module'
const routes: Routes = [
{
@@ -18,6 +19,7 @@ const routes: Routes = [
CommonModule,
IonicModule,
RouterModule.forChild(routes),
+ QRComponentModule,
EmverPipesModule,
TuiLetModule,
],
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/server-specs/server-specs.page.html b/frontend/projects/ui/src/app/apps/ui/pages/system/server-specs/server-specs.page.html
index 2cebf019e..b10851ea2 100644
--- a/frontend/projects/ui/src/app/apps/ui/pages/system/server-specs/server-specs.page.html
+++ b/frontend/projects/ui/src/app/apps/ui/pages/system/server-specs/server-specs.page.html
@@ -36,6 +36,9 @@
+
+
+
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/server-specs/server-specs.page.ts b/frontend/projects/ui/src/app/apps/ui/pages/system/server-specs/server-specs.page.ts
index d4efca83d..4e00863f8 100644
--- a/frontend/projects/ui/src/app/apps/ui/pages/system/server-specs/server-specs.page.ts
+++ b/frontend/projects/ui/src/app/apps/ui/pages/system/server-specs/server-specs.page.ts
@@ -1,7 +1,8 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
-import { ToastController } from '@ionic/angular'
+import { ModalController, ToastController } from '@ionic/angular'
import { PatchDB } from 'patch-db-client'
import { ConfigService } from 'src/app/services/config.service'
+import { QRComponent } from 'src/app/components/qr/qr.component'
import { copyToClipboard } from '@start9labs/shared'
import { DataModel } from 'src/app/services/patch-db/data-model'
@@ -16,6 +17,7 @@ export class ServerSpecsPage {
constructor(
private readonly toastCtrl: ToastController,
+ private readonly modalCtrl: ModalController,
private readonly patch: PatchDB,
private readonly config: ConfigService,
) {}
@@ -44,6 +46,17 @@ export class ServerSpecsPage {
await toast.present()
}
+ async showQR(text: string): Promise {
+ const modal = await this.modalCtrl.create({
+ component: QRComponent,
+ componentProps: {
+ text,
+ },
+ cssClass: 'qr-modal',
+ })
+ await modal.present()
+ }
+
asIsOrder(a: any, b: any) {
return 0
}
diff --git a/frontend/projects/ui/src/app/services/api/api.types.ts b/frontend/projects/ui/src/app/services/api/api.types.ts
index b877410f7..d51dd326b 100644
--- a/frontend/projects/ui/src/app/services/api/api.types.ts
+++ b/frontend/projects/ui/src/app/services/api/api.types.ts
@@ -310,19 +310,14 @@ export module RR {
// marketplace
- export type EnvInfo = {
- 'server-id': string
- 'eos-version': string
- }
- export type GetMarketplaceInfoReq = EnvInfo
+ export type GetMarketplaceInfoReq = { 'server-id': string }
export type GetMarketplaceInfoRes = StoreInfo
- export type GetMarketplaceEosReq = EnvInfo
+ export type GetMarketplaceEosReq = { 'server-id': string }
export type GetMarketplaceEosRes = MarketplaceEOS
export type GetMarketplacePackagesReq = {
ids?: { id: string; version: string }[]
- 'eos-version-compat': string
// iff !ids
category?: string
query?: string
diff --git a/frontend/projects/ui/src/app/services/api/embassy-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-api.service.ts
index 7ce599d4c..c41c87008 100644
--- a/frontend/projects/ui/src/app/services/api/embassy-api.service.ts
+++ b/frontend/projects/ui/src/app/services/api/embassy-api.service.ts
@@ -127,7 +127,6 @@ export abstract class ApiService {
path: string,
params: Record,
url: string,
- arch?: string,
): Promise
abstract getEos(): Promise
diff --git a/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts
index 5301d4817..be129d28c 100644
--- a/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts
+++ b/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts
@@ -233,10 +233,7 @@ export class LiveApiService extends ApiService {
path: string,
qp: Record,
baseUrl: string,
- arch: string = this.config.packageArch,
): Promise {
- // Object.assign(qp, { arch })
- qp['arch'] = arch
const fullUrl = `${baseUrl}${path}?${new URLSearchParams(qp).toString()}`
return this.rpcRequest({
method: 'marketplace.get',
@@ -245,17 +242,13 @@ export class LiveApiService extends ApiService {
}
async getEos(): Promise {
- const { id, version } = await getServerInfo(this.patch)
- const qp: RR.GetMarketplaceEosReq = {
- 'server-id': id,
- 'eos-version': version,
- }
+ const { id } = await getServerInfo(this.patch)
+ const qp: RR.GetMarketplaceEosReq = { 'server-id': id }
return this.marketplaceProxy(
'/eos/v0/latest',
qp,
this.config.marketplace.start9,
- this.config.osArch,
)
}
diff --git a/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts
index 70ff3f2c6..1d0357b74 100644
--- a/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts
+++ b/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts
@@ -363,7 +363,6 @@ export class MockApiService extends ApiService {
path: string,
params: Record,
url: string,
- arch = '',
): Promise {
await pauseFor(2000)
diff --git a/frontend/projects/ui/src/app/services/marketplace.service.ts b/frontend/projects/ui/src/app/services/marketplace.service.ts
index b714e89e7..721a69a34 100644
--- a/frontend/projects/ui/src/app/services/marketplace.service.ts
+++ b/frontend/projects/ui/src/app/services/marketplace.service.ts
@@ -215,10 +215,7 @@ export class MarketplaceService implements AbstractMarketplaceService {
return this.patch.watch$('server-info').pipe(
take(1),
switchMap(serverInfo => {
- const qp: RR.GetMarketplaceInfoReq = {
- 'server-id': serverInfo.id,
- 'eos-version': serverInfo.version,
- }
+ const qp: RR.GetMarketplaceInfoReq = { 'server-id': serverInfo.id }
return this.api.marketplaceProxy(
'/package/v0/info',
qp,
@@ -272,28 +269,21 @@ export class MarketplaceService implements AbstractMarketplaceService {
private fetchPackages$(
url: string,
- params: Omit<
- RR.GetMarketplacePackagesReq,
- 'eos-version-compat' | 'page' | 'per-page'
- > = {},
+ params: Omit = {},
): Observable {
- return this.patch.watch$('server-info', 'eos-version-compat').pipe(
- take(1),
- switchMap(versionCompat => {
- const qp: RR.GetMarketplacePackagesReq = {
- ...params,
- 'eos-version-compat': versionCompat,
- page: 1,
- 'per-page': 100,
- }
- if (qp.ids) qp.ids = JSON.stringify(qp.ids)
+ const qp: RR.GetMarketplacePackagesReq = {
+ ...params,
+ page: 1,
+ 'per-page': 100,
+ }
+ if (qp.ids) qp.ids = JSON.stringify(qp.ids)
- return this.api.marketplaceProxy(
- '/package/v0/index',
- qp,
- url,
- )
- }),
+ return from(
+ this.api.marketplaceProxy(
+ '/package/v0/index',
+ qp,
+ url,
+ ),
)
}
diff --git a/libs/models/src/errors.rs b/libs/models/src/errors.rs
index a9f3a943b..c429ccd7c 100644
--- a/libs/models/src/errors.rs
+++ b/libs/models/src/errors.rs
@@ -76,6 +76,7 @@ pub enum ErrorKind {
Systemd = 65,
OpenSsh = 66,
Zram = 67,
+ Lshw = 68,
}
impl ErrorKind {
pub fn as_str(&self) -> &'static str {
@@ -148,6 +149,7 @@ impl ErrorKind {
Systemd => "Systemd Error",
OpenSsh => "OpenSSH Error",
Zram => "Zram Error",
+ Lshw => "LSHW Error",
}
}
}
diff --git a/system-images/compat/Cargo.lock b/system-images/compat/Cargo.lock
index f7b8e4277..0d378082f 100644
--- a/system-images/compat/Cargo.lock
+++ b/system-images/compat/Cargo.lock
@@ -4315,6 +4315,7 @@ dependencies = [
"trust-dns-server",
"typed-builder",
"url",
+ "urlencoding",
"uuid 1.2.2",
"zeroize",
]
@@ -5040,6 +5041,12 @@ dependencies = [
"serde",
]
+[[package]]
+name = "urlencoding"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9"
+
[[package]]
name = "utf-8"
version = "0.7.6"