Merge branch 'next' of github.com:Start9Labs/start-os into rebase/integration/refactors
89
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.
|
||||
<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
|
After Width: | Height: | Size: 396 KiB |
BIN
assets/c-lightning.png
Normal file
|
After Width: | Height: | Size: 402 KiB |
BIN
assets/community.png
Normal file
|
After Width: | Height: | Size: 591 KiB |
BIN
assets/logs.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 386 KiB |
|
Before Width: | Height: | Size: 305 KiB After Width: | Height: | Size: 319 KiB |
BIN
assets/nostr.png
|
Before Width: | Height: | Size: 473 KiB |
BIN
assets/registry.png
Normal file
|
After Width: | Height: | Size: 521 KiB |
|
Before Width: | Height: | Size: 315 KiB After Width: | Height: | Size: 331 KiB |
BIN
assets/welcome.png
Normal file
|
After Width: | Height: | Size: 402 KiB |
7
backend/Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
@@ -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)]
|
||||
|
||||
@@ -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) -> ®ex::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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ iw
|
||||
jq
|
||||
libavahi-client3
|
||||
lm-sensors
|
||||
lshw
|
||||
lvm2
|
||||
magic-wormhole
|
||||
man-db
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -19,7 +19,7 @@ Check your versions
|
||||
|
||||
```sh
|
||||
node --version
|
||||
v16.10.0
|
||||
v18.15.0
|
||||
|
||||
npm --version
|
||||
v8.0.0
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -363,7 +363,6 @@ export class MockApiService extends ApiService {
|
||||
path: string,
|
||||
params: Record<string, string>,
|
||||
url: string,
|
||||
arch = '',
|
||||
): Promise<any> {
|
||||
await pauseFor(2000)
|
||||
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
7
system-images/compat/Cargo.lock
generated
@@ -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"
|
||||
|
||||