misc improvements to cli (#2827)

* misc improvements to cli

* switch host shorthand to H

* simplify macro
This commit is contained in:
Aiden McClelland
2025-02-12 12:20:18 -07:00
committed by GitHub
parent 3047dae703
commit 6dc9a11a89
13 changed files with 339 additions and 87 deletions

View File

@@ -38,9 +38,10 @@ impl AccountInfo {
let root_ca_key = generate_key()?;
let root_ca_cert = make_root_cert(&root_ca_key, &hostname, start_time)?;
let ssh_key = ssh_key::PrivateKey::from(ssh_key::private::Ed25519Keypair::random(
&mut rand::thread_rng(),
&mut ssh_key::rand_core::OsRng::default(),
));
let compat_s9pk_key = ed25519_dalek::SigningKey::generate(&mut rand::thread_rng());
let compat_s9pk_key =
ed25519_dalek::SigningKey::generate(&mut ssh_key::rand_core::OsRng::default());
Ok(Self {
server_id,
hostname,

View File

@@ -82,11 +82,13 @@ impl OsBackupV0 {
root_ca_key: self.root_ca_key.0,
root_ca_cert: self.root_ca_cert.0,
ssh_key: ssh_key::PrivateKey::random(
&mut rand::thread_rng(),
&mut ssh_key::rand_core::OsRng::default(),
ssh_key::Algorithm::Ed25519,
)?,
tor_keys: vec![TorSecretKeyV3::from(self.tor_key.0)],
compat_s9pk_key: ed25519_dalek::SigningKey::generate(&mut rand::thread_rng()),
compat_s9pk_key: ed25519_dalek::SigningKey::generate(
&mut ssh_key::rand_core::OsRng::default(),
),
},
ui: self.ui,
})

View File

@@ -13,6 +13,7 @@ use crate::disk::OsPartitionInfo;
use crate::init::init_postgres;
use crate::prelude::*;
use crate::util::serde::IoFormat;
use crate::version::VersionT;
use crate::MAIN_DATA;
pub const DEVICE_CONFIG_PATH: &str = "/media/startos/config/config.yaml"; // "/media/startos/config/config.yaml";
@@ -57,18 +58,20 @@ pub trait ContextConfig: DeserializeOwned + Default {
#[derive(Debug, Default, Deserialize, Serialize, Parser)]
#[serde(rename_all = "kebab-case")]
#[command(rename_all = "kebab-case")]
#[command(name = "start-cli")]
#[command(version = crate::version::Current::default().semver().to_string())]
pub struct ClientConfig {
#[arg(short = 'c', long = "config")]
#[arg(short = 'c', long)]
pub config: Option<PathBuf>,
#[arg(short = 'h', long = "host")]
#[arg(short = 'H', long)]
pub host: Option<Url>,
#[arg(short = 'r', long = "registry")]
#[arg(short = 'r', long)]
pub registry: Option<Url>,
#[arg(short = 'p', long = "proxy")]
#[arg(short = 'p', long)]
pub proxy: Option<Url>,
#[arg(long = "cookie-path")]
#[arg(long)]
pub cookie_path: Option<PathBuf>,
#[arg(long = "developer-key-path")]
#[arg(long)]
pub developer_key_path: Option<PathBuf>,
}
impl ContextConfig for ClientConfig {

View File

@@ -32,5 +32,7 @@ pub struct Private {
}
pub fn generate_compat_key() -> Pem<ed25519_dalek::SigningKey> {
Pem(ed25519_dalek::SigningKey::generate(&mut rand::thread_rng()))
Pem(ed25519_dalek::SigningKey::generate(
&mut ssh_key::rand_core::OsRng::default(),
))
}

View File

@@ -20,7 +20,7 @@ pub fn init(ctx: CliContext) -> Result<(), Error> {
.with_ctx(|_| (crate::ErrorKind::Filesystem, parent.display().to_string()))?;
}
tracing::info!("Generating new developer key...");
let secret = SigningKey::generate(&mut rand::thread_rng());
let secret = SigningKey::generate(&mut ssh_key::rand_core::OsRng::default());
tracing::info!("Writing key to {}", ctx.developer_key_path.display());
let keypair_bytes = ed25519::KeypairBytes {
secret_key: secret.to_bytes(),

View File

@@ -51,7 +51,7 @@ fn test(files: Vec<(PathBuf, String)>) -> Result<(), Error> {
check_set.insert(path.to_owned(), content);
}
}
let key = SigningKey::generate(&mut rand::thread_rng());
let key = SigningKey::generate(&mut ssh_key::rand_core::OsRng::default());
let mut a1 = MerkleArchive::new(root, key, "test");
tokio::runtime::Builder::new_current_thread()
.enable_io()

View File

@@ -7,8 +7,6 @@ use clap::Parser;
use color_eyre::eyre::eyre;
use futures::FutureExt;
use imbl::vector;
use mail_send::mail_builder::{self, MessageBuilder};
use mail_send::SmtpClientBuilder;
use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler};
use rustls::crypto::CryptoProvider;
use rustls::RootCertStore;
@@ -906,64 +904,74 @@ pub async fn test_smtp(
password,
}: TestSmtpParams,
) -> Result<(), Error> {
use rustls_pki_types::pem::PemObject;
#[cfg(feature = "mail-send")]
{
use mail_send::mail_builder::{self, MessageBuilder};
use mail_send::SmtpClientBuilder;
use rustls_pki_types::pem::PemObject;
let Some(pass_val) = password else {
return Err(Error::new(
eyre!("mail-send requires a password"),
ErrorKind::InvalidRequest,
));
};
let Some(pass_val) = password else {
return Err(Error::new(
eyre!("mail-send requires a password"),
ErrorKind::InvalidRequest,
));
};
let mut root_cert_store = RootCertStore::empty();
let pem = tokio::fs::read("/etc/ssl/certs/ca-certificates.crt").await?;
for cert in CertificateDer::pem_slice_iter(&pem) {
root_cert_store.add_parsable_certificates([cert.with_kind(ErrorKind::OpenSsl)?]);
}
let cfg = Arc::new(
rustls::ClientConfig::builder_with_provider(Arc::new(
rustls::crypto::ring::default_provider(),
))
.with_safe_default_protocol_versions()?
.with_root_certificates(root_cert_store)
.with_no_client_auth(),
);
let client = SmtpClientBuilder::new_with_tls_config(server, port, cfg)
.implicit_tls(false)
.credentials((login.split("@").next().unwrap().to_owned(), pass_val));
fn parse_address<'a>(addr: &'a str) -> mail_builder::headers::address::Address<'a> {
if addr.find("<").map_or(false, |start| {
addr.find(">").map_or(false, |end| start < end)
}) {
addr.split_once("<")
.map(|(name, addr)| (name.trim(), addr.strip_suffix(">").unwrap_or(addr)))
.unwrap()
.into()
} else {
addr.into()
let mut root_cert_store = RootCertStore::empty();
let pem = tokio::fs::read("/etc/ssl/certs/ca-certificates.crt").await?;
for cert in CertificateDer::pem_slice_iter(&pem) {
root_cert_store.add_parsable_certificates([cert.with_kind(ErrorKind::OpenSsl)?]);
}
}
let message = MessageBuilder::new()
.from(parse_address(&from))
.to(parse_address(&to))
.subject("StartOS Test Email")
.text_body("This is a test email sent from your StartOS Server");
client
.connect()
.await
.map_err(|e| {
Error::new(
eyre!("mail-send connection error: {:?}", e),
ErrorKind::Unknown,
)
})?
.send(message)
.await
.map_err(|e| Error::new(eyre!("mail-send send error: {:?}", e), ErrorKind::Unknown))?;
Ok(())
let cfg = Arc::new(
rustls::ClientConfig::builder_with_provider(Arc::new(
rustls::crypto::ring::default_provider(),
))
.with_safe_default_protocol_versions()?
.with_root_certificates(root_cert_store)
.with_no_client_auth(),
);
let client = SmtpClientBuilder::new_with_tls_config(server, port, cfg)
.implicit_tls(false)
.credentials((login.split("@").next().unwrap().to_owned(), pass_val));
fn parse_address<'a>(addr: &'a str) -> mail_builder::headers::address::Address<'a> {
if addr.find("<").map_or(false, |start| {
addr.find(">").map_or(false, |end| start < end)
}) {
addr.split_once("<")
.map(|(name, addr)| (name.trim(), addr.strip_suffix(">").unwrap_or(addr)))
.unwrap()
.into()
} else {
addr.into()
}
}
let message = MessageBuilder::new()
.from(parse_address(&from))
.to(parse_address(&to))
.subject("StartOS Test Email")
.text_body("This is a test email sent from your StartOS Server");
client
.connect()
.await
.map_err(|e| {
Error::new(
eyre!("mail-send connection error: {:?}", e),
ErrorKind::Unknown,
)
})?
.send(message)
.await
.map_err(|e| Error::new(eyre!("mail-send send error: {:?}", e), ErrorKind::Unknown))?;
Ok(())
}
#[cfg(not(feature = "mail-send"))]
Err(Error::new(
eyre!("test-smtp requires mail-send feature to be enabled"),
ErrorKind::InvalidRequest,
))
}
#[tokio::test]

View File

@@ -648,7 +648,7 @@ impl<'a, T> From<&'a T> for MaybeOwned<'a, T> {
pub fn new_guid() -> InternedString {
use rand::RngCore;
let mut buf = [0; 20];
rand::thread_rng().fill_bytes(&mut buf);
rand::rng().fill_bytes(&mut buf);
InternedString::intern(base32::encode(
base32::Alphabet::Rfc4648 { padding: false },
&buf,

View File

@@ -1,7 +1,12 @@
use std::collections::VecDeque;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::AtomicUsize;
use std::sync::{Arc, Weak};
use std::task::{Poll, Waker};
use futures::stream::BoxStream;
use futures::Stream;
#[derive(Debug, Default)]
pub struct SyncMutex<T>(std::sync::Mutex<T>);
impl<T> SyncMutex<T> {
@@ -160,3 +165,149 @@ impl<T: Clone> futures::Stream for Watch<T> {
(1, None)
}
}
struct DupState<T, Upstream = BoxStream<'static, T>>
where
T: Clone,
Upstream: Stream<Item = T> + Unpin,
{
buffer: VecDeque<T>,
upstream: Upstream,
pos: usize,
pos_refs: Vec<Weak<AtomicUsize>>,
wakers: Vec<Waker>,
}
impl<T: Clone, Upstream: Stream<Item = T> + Unpin> DupState<T, Upstream> {
fn poll_next(&mut self, cx: &mut std::task::Context<'_>) -> Poll<Option<T>> {
use futures::stream::StreamExt;
let Some(next) = futures::ready!(self.upstream.poll_next_unpin(cx)) else {
return Poll::Ready(None);
};
self.pos += 1;
self.buffer.push_back(next.clone());
for waker in self.wakers.drain(..) {
if !waker.will_wake(cx.waker()) {
waker.wake();
}
}
Poll::Ready(Some(next))
}
}
pub struct DupStream<T, Upstream = BoxStream<'static, T>>
where
T: Clone,
Upstream: Stream<Item = T> + Unpin,
{
state: Arc<SyncMutex<DupState<T, Upstream>>>,
pos: Arc<AtomicUsize>,
}
impl<T: Clone, Upstream: Stream<Item = T> + Unpin> DupStream<T, Upstream> {
pub fn new(upstream: Upstream) -> Self {
let pos = Arc::new(AtomicUsize::new(0));
Self {
state: Arc::new(SyncMutex::new(DupState {
buffer: VecDeque::new(),
upstream,
pos: 0,
pos_refs: vec![Arc::downgrade(&pos)],
wakers: Vec::new(),
})),
pos,
}
}
}
impl<T: Clone, Upstream: Stream<Item = T> + Unpin> Clone for DupStream<T, Upstream> {
fn clone(&self) -> Self {
let pos = self.state.mutate(|state| {
let pos = Arc::new(AtomicUsize::new(
self.pos.load(std::sync::atomic::Ordering::Relaxed),
));
state.pos_refs.push(Arc::downgrade(&pos));
state.pos_refs.retain(|ptr| ptr.strong_count() > 0);
pos
});
Self {
state: self.state.clone(),
pos,
}
}
}
impl<T: Clone, Upstream: Stream<Item = T> + Unpin> Stream for DupStream<T, Upstream> {
type Item = T;
fn poll_next(
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> Poll<Option<Self::Item>> {
self.state.mutate(|state| {
let pos = self.pos.load(std::sync::atomic::Ordering::Relaxed);
if pos < state.pos {
self.pos.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if state
.pos_refs
.iter()
.filter_map(|ptr| ptr.upgrade())
.all(|ptr| ptr.load(std::sync::atomic::Ordering::Relaxed) > pos)
{
while let Some(next) = state.buffer.pop_front() {
if state.buffer.len() + 1 == state.pos - pos {
return Poll::Ready(Some(next));
}
}
Poll::Ready(None)
} else {
Poll::Ready(
state
.buffer
.get(state.buffer.len() + pos - state.pos)
.cloned(),
)
}
} else {
let res = state.poll_next(cx);
if res.is_ready() {
self.pos
.store(state.pos, std::sync::atomic::Ordering::Relaxed);
} else {
let waker = cx.waker();
if state.wakers.iter().all(|w| !w.will_wake(waker)) {
state.wakers.push(waker.clone());
}
}
res
}
})
}
}
#[tokio::test]
async fn test_dup_stream() {
use std::time::Duration;
use futures::StreamExt;
let stream = async_stream::stream! {
for i in 0..100 {
tokio::time::sleep(Duration::from_nanos(rand::random_range(0..=10000000))).await;
yield i;
}
}
.boxed();
let n = rand::random_range(3..10);
let mut tasks = Vec::with_capacity(n);
for mut dup_stream in std::iter::repeat_n(DupStream::new(stream), n) {
tasks.push(tokio::spawn(async move {
let mut ctr = 0;
while let Some(i) = dup_stream.next().await {
assert_eq!(ctr, i);
ctr += 1;
tokio::time::sleep(Duration::from_nanos(rand::random_range(0..=10000000))).await;
}
assert_eq!(ctr, 100);
}));
}
futures::future::try_join_all(tasks).await.unwrap();
}

View File

@@ -464,9 +464,9 @@ async fn previous_account_info(pg: &sqlx::Pool<sqlx::Postgres>) -> Result<Accoun
.as_bytes(),
)
.with_ctx(|_| (ErrorKind::Database, "X509::from_pem"))?,
compat_s9pk_key: SigningKey::generate(&mut rand::thread_rng()),
compat_s9pk_key: SigningKey::generate(&mut ssh_key::rand_core::OsRng::default()),
ssh_key: ssh_key::PrivateKey::random(
&mut rand::thread_rng(),
&mut ssh_key::rand_core::OsRng::default(),
ssh_key::Algorithm::Ed25519,
)
.with_ctx(|_| (ErrorKind::Database, "X509::ssh_key::PrivateKey::random"))?,