Merge branch 'next' of github.com:Start9Labs/start-os into rebase/integration/refactors

This commit is contained in:
Aiden McClelland
2023-08-08 10:08:59 -06:00
36 changed files with 430 additions and 190 deletions

View File

@@ -1,26 +1,56 @@
# StartOS
[![version](https://img.shields.io/github/v/tag/Start9Labs/start-os?color=success)](https://github.com/Start9Labs/start-os/releases)
[![build](https://github.com/Start9Labs/start-os/actions/workflows/startos-iso.yaml/badge.svg)](https://github.com/Start9Labs/start-os/actions/workflows/startos-iso.yaml)
[![community](https://img.shields.io/badge/community-matrix-yellow)](https://matrix.to/#/#community:matrix.start9labs.com)
[![community](https://img.shields.io/badge/community-telegram-informational)](https://t.me/start9_labs)
[![support](https://img.shields.io/badge/support-docs-important)](https://docs.start9.com)
[![developer](https://img.shields.io/badge/developer-matrix-blueviolet)](https://matrix.to/#/#community-dev:matrix.start9labs.com)
[![website](https://img.shields.io/website?down_color=lightgrey&down_message=offline&up_color=green&up_message=online&url=https%3A%2F%2Fstart9.com)](https://start9.com)
[![mastodon](https://img.shields.io/mastodon/follow/000000001?domain=https%3A%2F%2Fmastodon.start9labs.com&label=Follow&style=social)](http://mastodon.start9labs.com)
[![twitter](https://img.shields.io/twitter/follow/start9labs?label=Follow)](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.
<div align="center">
<img src="frontend/projects/shared/assets/img/icon_pwa.png" alt="StartOS Logo" width="16%" />
<h1 style="margin-top: 0;">StartOS</h1>
<a href="https://github.com/Start9Labs/start-os/releases">
<img src="https://img.shields.io/github/v/tag/Start9Labs/start-os?color=success" />
</a>
<a href="https://github.com/Start9Labs/start-os/actions/workflows/startos-iso.yaml">
<img src="https://github.com/Start9Labs/start-os/actions/workflows/startos-iso.yaml/badge.svg">
</a>
<a href="https://twitter.com/start9labs">
<img src="https://img.shields.io/twitter/follow/start9labs?label=Follow">
</a>
<a href="http://mastodon.start9labs.com">
<img src="https://img.shields.io/mastodon/follow/000000001?domain=https%3A%2F%2Fmastodon.start9labs.com&label=Follow&style=social">
</a>
<a href="https://matrix.to/#/#community:matrix.start9labs.com">
<img src="https://img.shields.io/badge/community-matrix-yellow">
</a>
<a href="https://t.me/start9_labs">
<img src="https://img.shields.io/badge/community-telegram-informational">
</a>
<a href="https://docs.start9.com">
<img src="https://img.shields.io/badge/support-docs-important">
</a>
<a href="https://matrix.to/#/#community-dev:matrix.start9labs.com">
<img src="https://img.shields.io/badge/developer-matrix-blueviolet">
</a>
<a href="https://start9.com">
<img src="https://img.shields.io/website?down_color=lightgrey&down_message=offline&up_color=green&up_message=online&url=https%3A%2F%2Fstart9.com">
</a>
</div>
<br />
<div align="center">
<h3>
Welcome to the era of Sovereign Computing
</h3>
<p>
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.
</p>
</div>
<br />
<p align="center">
<img src="assets/StartOS.png" alt="StartOS" width="85%">
</p>
<br />
## 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
<p align="center">
<img src="assets/StartOS.png" alt="StartOS" width="85%">
</p>
<p align="center">
<img src="assets/marketplace.png" alt="StartOS Marketplace" width="49%">
<img src="assets/nostr.png" alt="StartOS Nostr Service" width="49%">
</p>
<p align="center">
<img src="assets/nextcloud.png" alt="StartOS NextCloud Service" width="49%">
<img src="assets/registry.png" alt="StartOS Marketplace" width="49%">
<img src="assets/community.png" alt="StartOS Community Registry" width="49%">
<img src="assets/c-lightning.png" alt="StartOS NextCloud Service" width="49%">
<img src="assets/btcpay.png" alt="StartOS BTCPay Service" width="49%">
<img src="assets/nextcloud.png" alt="StartOS System Settings" width="49%">
<img src="assets/system.png" alt="StartOS System Settings" width="49%">
<img src="assets/welcome.png" alt="StartOS System Settings" width="49%">
<img src="assets/logs.png" alt="StartOS System Settings" width="49%">
</p>

BIN
assets/btcpay.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 KiB

BIN
assets/c-lightning.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 KiB

BIN
assets/community.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 591 KiB

BIN
assets/logs.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 386 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 305 KiB

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 473 KiB

BIN
assets/registry.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 315 KiB

After

Width:  |  Height:  |  Size: 331 KiB

BIN
assets/welcome.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 KiB

7
backend/Cargo.lock generated
View File

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

View File

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

View File

@@ -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<Arc<RwLock<WpaCli>>>,
pub current_secret: Arc<Jwk>,
pub config_hooks: Mutex<BTreeMap<PackageId, Vec<ConfigHook>>>,
pub client: Client,
pub hardware: Hardware,
}
pub struct Hardware {
pub devices: Vec<LshwDevice>,
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);

View File

@@ -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<R: AsyncRead + AsyncSeek + Unpin + Send + Sync>(
{
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<R: AsyncRead + AsyncSeek + Unpin + Send + Sync>(
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?;

View File

@@ -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<Value, Error> {
let mut response = reqwest::get(url)
pub async fn get(#[context] ctx: RpcContext, #[arg] url: Url) -> Result<Value, Error> {
let mut response = ctx
.client
.get(with_query_params(&ctx, url))
.send()
.await
.with_kind(crate::ErrorKind::Network)?;
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");
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<HttpHandler, Error
async fn alt_ui(req: Request<Body>, ui_mode: UiMode) -> Result<Response<Body>, 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::<Vec<_>>();
match &request_parts.method {
&Method::GET => {
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}")),
}
}
(&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())
}

View File

@@ -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<String>,
#[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<String, Regex>,
ram: Option<u64>,
arch: Option<Vec<String>>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct Assets {

View File

@@ -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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<u64>,
}
#[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 mut mem_info = MemInfo {
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::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<Option<Arc<Revision>>, 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::<LatestInformation>()
.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::<LatestInformation>()
.await
.with_kind(ErrorKind::Network)?
.version;
crate::db::DatabaseModel::new()
.server_info()
.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 io;
pub mod logger;
pub mod lshw;
pub mod serde;
#[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()))
}
}
#[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
libavahi-client3
lm-sensors
lshw
lvm2
magic-wormhole
man-db

View File

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

View File

@@ -19,7 +19,7 @@ Check your versions
```sh
node --version
v16.10.0
v18.15.0
npm --version
v8.0.0

View File

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

View File

@@ -36,6 +36,9 @@
<ion-button fill="clear" (click)="launch(torAddress)">
<ion-icon slot="icon-only" name="open-outline"></ion-icon>
</ion-button>
<ion-button fill="clear" (click)="showQR(torAddress)">
<ion-icon slot="icon-only" name="qr-code-outline"></ion-icon>
</ion-button>
<ion-button fill="clear" (click)="copy(torAddress)">
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
</ion-button>

View File

@@ -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<DataModel>,
private readonly config: ConfigService,
) {}
@@ -44,6 +46,17 @@ export class ServerSpecsPage {
await toast.present()
}
async showQR(text: string): Promise<void> {
const modal = await this.modalCtrl.create({
component: QRComponent,
componentProps: {
text,
},
cssClass: 'qr-modal',
})
await modal.present()
}
asIsOrder(a: any, b: any) {
return 0
}

View File

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

View File

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

View File

@@ -233,10 +233,7 @@ export class LiveApiService extends ApiService {
path: string,
qp: Record<string, string>,
baseUrl: string,
arch: string = this.config.packageArch,
): Promise<T> {
// 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<RR.GetMarketplaceEosRes> {
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,
)
}

View File

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

View File

@@ -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<RR.GetMarketplaceInfoRes>(
'/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<RR.GetMarketplacePackagesReq, 'page' | 'per-page'> = {},
): Observable<MarketplacePkg[]> {
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<RR.GetMarketplacePackagesRes>(
'/package/v0/index',
qp,
url,
)
}),
return from(
this.api.marketplaceProxy<RR.GetMarketplacePackagesRes>(
'/package/v0/index',
qp,
url,
),
)
}

View File

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

View File

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