release: ship lesavka 0.16.1
This commit is contained in:
parent
9ec915f91c
commit
ec3edf525a
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.16.0"
|
version = "0.16.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
@ -1686,7 +1686,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.16.0"
|
version = "0.16.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
@ -1698,7 +1698,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.16.0"
|
version = "0.16.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
|||||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.16.0"
|
version = "0.16.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -1,4 +1,106 @@
|
|||||||
{
|
{
|
||||||
|
fn default_client_pki_dir() -> PathBuf {
|
||||||
|
if let Some(home) = std::env::var_os("HOME") {
|
||||||
|
return PathBuf::from(home).join(".config/lesavka/pki");
|
||||||
|
}
|
||||||
|
std::env::current_dir()
|
||||||
|
.unwrap_or_else(|_| PathBuf::from("."))
|
||||||
|
.join(".config/lesavka/pki")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_client_pki_bundle(bundle: &Path) -> Result<PathBuf, String> {
|
||||||
|
let target = default_client_pki_dir();
|
||||||
|
let scratch = std::env::temp_dir().join(format!(
|
||||||
|
"lesavka-client-pki-{}",
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_millis()
|
||||||
|
));
|
||||||
|
std::fs::create_dir_all(&scratch)
|
||||||
|
.map_err(|err| format!("could not create extraction folder: {err}"))?;
|
||||||
|
let extract = Command::new("tar")
|
||||||
|
.arg("-xzf")
|
||||||
|
.arg(bundle)
|
||||||
|
.arg("-C")
|
||||||
|
.arg(&scratch)
|
||||||
|
.status()
|
||||||
|
.map_err(|err| format!("tar is unavailable: {err}"))?;
|
||||||
|
if !extract.success() {
|
||||||
|
let _ = std::fs::remove_dir_all(&scratch);
|
||||||
|
return Err(format!("could not extract {}", bundle.display()));
|
||||||
|
}
|
||||||
|
for item in ["ca.crt", "client.crt", "client.key"] {
|
||||||
|
if !scratch.join(item).is_file() {
|
||||||
|
let _ = std::fs::remove_dir_all(&scratch);
|
||||||
|
return Err(format!("bundle is missing {item}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::fs::create_dir_all(&target)
|
||||||
|
.map_err(|err| format!("could not create {}: {err}", target.display()))?;
|
||||||
|
for item in ["ca.crt", "client.crt", "client.key"] {
|
||||||
|
std::fs::copy(scratch.join(item), target.join(item))
|
||||||
|
.map_err(|err| format!("could not install {item}: {err}"))?;
|
||||||
|
}
|
||||||
|
tighten_client_key_permissions(&target.join("client.key"));
|
||||||
|
let _ = std::fs::remove_dir_all(&scratch);
|
||||||
|
Ok(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn tighten_client_key_permissions(path: &Path) {
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
if let Ok(mut permissions) = std::fs::metadata(path).map(|metadata| metadata.permissions()) {
|
||||||
|
permissions.set_mode(0o600);
|
||||||
|
let _ = std::fs::set_permissions(path, permissions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
fn tighten_client_key_permissions(_path: &Path) {}
|
||||||
|
|
||||||
|
{
|
||||||
|
let widgets = widgets.clone();
|
||||||
|
let window = window.clone();
|
||||||
|
let certs_button = widgets.certs_button.clone();
|
||||||
|
certs_button.connect_clicked(move |_| {
|
||||||
|
let chooser = gtk::FileChooserNative::new(
|
||||||
|
Some("Choose Lesavka Client TLS Bundle"),
|
||||||
|
Some(&window),
|
||||||
|
gtk::FileChooserAction::Open,
|
||||||
|
Some("Install"),
|
||||||
|
Some("Cancel"),
|
||||||
|
);
|
||||||
|
chooser.set_modal(true);
|
||||||
|
let widgets = widgets.clone();
|
||||||
|
chooser.connect_response(move |dialog, response| {
|
||||||
|
if response == gtk::ResponseType::Accept {
|
||||||
|
if let Some(bundle) = dialog.file().and_then(|file| file.path()) {
|
||||||
|
widgets.status_label.set_text(&format!(
|
||||||
|
"Installing Lesavka client TLS bundle from {}...",
|
||||||
|
bundle.display()
|
||||||
|
));
|
||||||
|
match install_client_pki_bundle(&bundle) {
|
||||||
|
Ok(target) => widgets.status_label.set_text(&format!(
|
||||||
|
"Client TLS certs installed at {}. Reconnect the relay to use them.",
|
||||||
|
target.display()
|
||||||
|
)),
|
||||||
|
Err(err) => widgets
|
||||||
|
.status_label
|
||||||
|
.set_text(&format!("Client TLS bundle install failed: {err}")),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
widgets
|
||||||
|
.status_label
|
||||||
|
.set_text("Selected TLS bundle did not provide a filesystem path.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dialog.destroy();
|
||||||
|
});
|
||||||
|
chooser.show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let child_proc = Rc::clone(&child_proc);
|
let child_proc = Rc::clone(&child_proc);
|
||||||
let widgets = widgets.clone();
|
let widgets = widgets.clone();
|
||||||
|
|||||||
@ -85,6 +85,7 @@ pub fn build_launcher_view(
|
|||||||
let OperationsRailContext {
|
let OperationsRailContext {
|
||||||
server_entry,
|
server_entry,
|
||||||
start_button,
|
start_button,
|
||||||
|
certs_button,
|
||||||
clipboard_button,
|
clipboard_button,
|
||||||
usb_recover_button,
|
usb_recover_button,
|
||||||
uac_recover_button,
|
uac_recover_button,
|
||||||
|
|||||||
@ -124,6 +124,7 @@
|
|||||||
display_panes: [left_pane.clone(), right_pane.clone()],
|
display_panes: [left_pane.clone(), right_pane.clone()],
|
||||||
server_entry: server_entry.clone(),
|
server_entry: server_entry.clone(),
|
||||||
start_button: start_button.clone(),
|
start_button: start_button.clone(),
|
||||||
|
certs_button: certs_button.clone(),
|
||||||
camera_combo: camera_combo.clone(),
|
camera_combo: camera_combo.clone(),
|
||||||
camera_quality_combo: camera_quality_combo.clone(),
|
camera_quality_combo: camera_quality_combo.clone(),
|
||||||
microphone_combo: microphone_combo.clone(),
|
microphone_combo: microphone_combo.clone(),
|
||||||
|
|||||||
@ -55,6 +55,7 @@ struct DeviceControlsContext {
|
|||||||
struct OperationsRailContext {
|
struct OperationsRailContext {
|
||||||
server_entry: gtk::Entry,
|
server_entry: gtk::Entry,
|
||||||
start_button: gtk::Button,
|
start_button: gtk::Button,
|
||||||
|
certs_button: gtk::Button,
|
||||||
clipboard_button: gtk::Button,
|
clipboard_button: gtk::Button,
|
||||||
usb_recover_button: gtk::Button,
|
usb_recover_button: gtk::Button,
|
||||||
uac_recover_button: gtk::Button,
|
uac_recover_button: gtk::Button,
|
||||||
|
|||||||
@ -3,18 +3,27 @@
|
|||||||
let server_entry = gtk::Entry::new();
|
let server_entry = gtk::Entry::new();
|
||||||
server_entry.add_css_class("server-entry");
|
server_entry.add_css_class("server-entry");
|
||||||
server_entry.set_hexpand(true);
|
server_entry.set_hexpand(true);
|
||||||
server_entry.set_width_chars(14);
|
server_entry.set_width_chars(11);
|
||||||
server_entry.set_text(server_addr);
|
server_entry.set_text(server_addr);
|
||||||
server_entry.set_tooltip_text(Some("Relay host address."));
|
server_entry.set_tooltip_text(Some(
|
||||||
|
"Relay host address. If no scheme is provided, Lesavka uses https://.",
|
||||||
|
));
|
||||||
let relay_grid = gtk::Grid::new();
|
let relay_grid = gtk::Grid::new();
|
||||||
relay_grid.set_column_homogeneous(true);
|
relay_grid.set_column_homogeneous(false);
|
||||||
relay_grid.set_column_spacing(6);
|
relay_grid.set_column_spacing(6);
|
||||||
relay_grid.set_hexpand(true);
|
relay_grid.set_hexpand(true);
|
||||||
relay_grid.set_row_spacing(8);
|
relay_grid.set_row_spacing(8);
|
||||||
relay_grid.attach(&server_entry, 0, 0, 2, 1);
|
relay_grid.attach(&server_entry, 0, 0, 1, 1);
|
||||||
|
|
||||||
|
let certs_button = rail_button("Certs", "Install or replace the client TLS enrollment bundle.");
|
||||||
|
certs_button.set_hexpand(false);
|
||||||
|
stabilize_button(&certs_button, 66);
|
||||||
|
relay_grid.attach(&certs_button, 1, 0, 1, 1);
|
||||||
|
|
||||||
let start_button = rail_button("Connect", "Start or stop relay.");
|
let start_button = rail_button("Connect", "Start or stop relay.");
|
||||||
start_button.add_css_class("suggested-action");
|
start_button.add_css_class("suggested-action");
|
||||||
|
start_button.set_hexpand(false);
|
||||||
|
stabilize_button(&start_button, 76);
|
||||||
relay_grid.attach(&start_button, 2, 0, 1, 1);
|
relay_grid.attach(&start_button, 2, 0, 1, 1);
|
||||||
|
|
||||||
connection_body.append(&relay_grid);
|
connection_body.append(&relay_grid);
|
||||||
@ -24,7 +33,7 @@
|
|||||||
recovery_heading.set_halign(gtk::Align::Start);
|
recovery_heading.set_halign(gtk::Align::Start);
|
||||||
let recovery_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
let recovery_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||||
recovery_row.set_hexpand(true);
|
recovery_row.set_hexpand(true);
|
||||||
recovery_heading.set_width_chars(10);
|
recovery_heading.set_width_chars(RELAY_SUBGROUP_LABEL_WIDTH);
|
||||||
recovery_row.append(&recovery_heading);
|
recovery_row.append(&recovery_heading);
|
||||||
let recovery_buttons = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
let recovery_buttons = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||||
recovery_buttons.set_hexpand(true);
|
recovery_buttons.set_hexpand(true);
|
||||||
@ -39,10 +48,11 @@
|
|||||||
connection_body.append(&recovery_row);
|
connection_body.append(&recovery_row);
|
||||||
connection_body.append(>k::Separator::new(gtk::Orientation::Horizontal));
|
connection_body.append(>k::Separator::new(gtk::Orientation::Horizontal));
|
||||||
|
|
||||||
let calibration_heading = gtk::Label::new(Some("AV Upstream\nCalibration"));
|
let calibration_heading = gtk::Label::new(Some("Audio/Video\nUpstream\nCalibration"));
|
||||||
calibration_heading.add_css_class("subgroup-title");
|
calibration_heading.add_css_class("subgroup-title");
|
||||||
calibration_heading.set_halign(gtk::Align::Start);
|
calibration_heading.set_halign(gtk::Align::Start);
|
||||||
calibration_heading.set_width_chars(12);
|
calibration_heading.set_justify(gtk::Justification::Center);
|
||||||
|
calibration_heading.set_width_chars(RELAY_SUBGROUP_LABEL_WIDTH);
|
||||||
let calibration_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
let calibration_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||||
calibration_row.set_hexpand(true);
|
calibration_row.set_hexpand(true);
|
||||||
calibration_row.append(&calibration_heading);
|
calibration_row.append(&calibration_heading);
|
||||||
@ -93,7 +103,7 @@
|
|||||||
power_shell.set_halign(gtk::Align::Fill);
|
power_shell.set_halign(gtk::Align::Fill);
|
||||||
let power_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
let power_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||||
power_row.set_hexpand(true);
|
power_row.set_hexpand(true);
|
||||||
power_heading.set_width_chars(10);
|
power_heading.set_width_chars(RELAY_SUBGROUP_LABEL_WIDTH);
|
||||||
power_row.append(&power_heading);
|
power_row.append(&power_heading);
|
||||||
let power_buttons = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
let power_buttons = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||||
power_buttons.set_hexpand(true);
|
power_buttons.set_hexpand(true);
|
||||||
@ -127,7 +137,7 @@
|
|||||||
|
|
||||||
let routing_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
let routing_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||||
routing_row.set_hexpand(true);
|
routing_row.set_hexpand(true);
|
||||||
routing_heading.set_width_chars(10);
|
routing_heading.set_width_chars(RELAY_SUBGROUP_LABEL_WIDTH);
|
||||||
routing_row.append(&routing_heading);
|
routing_row.append(&routing_heading);
|
||||||
let routing_buttons = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
let routing_buttons = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||||
routing_buttons.set_hexpand(true);
|
routing_buttons.set_hexpand(true);
|
||||||
@ -150,7 +160,7 @@
|
|||||||
tools_heading.set_halign(gtk::Align::Start);
|
tools_heading.set_halign(gtk::Align::Start);
|
||||||
let tools_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
let tools_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||||
tools_row.set_hexpand(true);
|
tools_row.set_hexpand(true);
|
||||||
tools_heading.set_width_chars(10);
|
tools_heading.set_width_chars(RELAY_SUBGROUP_LABEL_WIDTH);
|
||||||
tools_row.append(&tools_heading);
|
tools_row.append(&tools_heading);
|
||||||
let tools_buttons = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
let tools_buttons = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||||
tools_buttons.set_hexpand(true);
|
tools_buttons.set_hexpand(true);
|
||||||
@ -196,6 +206,7 @@
|
|||||||
.child(&diagnostics_shell)
|
.child(&diagnostics_shell)
|
||||||
.build();
|
.build();
|
||||||
diagnostics_scroll.set_propagate_natural_width(false);
|
diagnostics_scroll.set_propagate_natural_width(false);
|
||||||
|
diagnostics_scroll.set_propagate_natural_height(false);
|
||||||
diagnostics_body.append(&diagnostics_toolbar);
|
diagnostics_body.append(&diagnostics_toolbar);
|
||||||
diagnostics_body.append(&diagnostics_scroll);
|
diagnostics_body.append(&diagnostics_scroll);
|
||||||
operations.append(&diagnostics_panel);
|
operations.append(&diagnostics_panel);
|
||||||
@ -252,6 +263,7 @@
|
|||||||
.child(&session_log_view)
|
.child(&session_log_view)
|
||||||
.build();
|
.build();
|
||||||
log_scroll.set_propagate_natural_width(false);
|
log_scroll.set_propagate_natural_width(false);
|
||||||
|
log_scroll.set_propagate_natural_height(false);
|
||||||
console_body.append(&console_toolbar);
|
console_body.append(&console_toolbar);
|
||||||
console_body.append(&log_scroll);
|
console_body.append(&log_scroll);
|
||||||
operations.append(&console_panel);
|
operations.append(&console_panel);
|
||||||
@ -277,6 +289,7 @@
|
|||||||
OperationsRailContext {
|
OperationsRailContext {
|
||||||
server_entry,
|
server_entry,
|
||||||
start_button,
|
start_button,
|
||||||
|
certs_button,
|
||||||
clipboard_button,
|
clipboard_button,
|
||||||
usb_recover_button,
|
usb_recover_button,
|
||||||
uac_recover_button,
|
uac_recover_button,
|
||||||
|
|||||||
@ -131,6 +131,7 @@ pub struct LauncherWidgets {
|
|||||||
pub display_panes: [DisplayPaneWidgets; 2],
|
pub display_panes: [DisplayPaneWidgets; 2],
|
||||||
pub server_entry: gtk::Entry,
|
pub server_entry: gtk::Entry,
|
||||||
pub start_button: gtk::Button,
|
pub start_button: gtk::Button,
|
||||||
|
pub certs_button: gtk::Button,
|
||||||
pub camera_combo: gtk::ComboBoxText,
|
pub camera_combo: gtk::ComboBoxText,
|
||||||
pub camera_quality_combo: gtk::ComboBoxText,
|
pub camera_quality_combo: gtk::ComboBoxText,
|
||||||
pub microphone_combo: gtk::ComboBoxText,
|
pub microphone_combo: gtk::ComboBoxText,
|
||||||
@ -212,6 +213,7 @@ const LESAVKA_ICON_SEARCH_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/ass
|
|||||||
const LAUNCHER_DEFAULT_WIDTH: i32 = 1540;
|
const LAUNCHER_DEFAULT_WIDTH: i32 = 1540;
|
||||||
const LAUNCHER_DEFAULT_HEIGHT: i32 = 880;
|
const LAUNCHER_DEFAULT_HEIGHT: i32 = 880;
|
||||||
const OPERATIONS_RAIL_WIDTH: i32 = 276;
|
const OPERATIONS_RAIL_WIDTH: i32 = 276;
|
||||||
|
const RELAY_SUBGROUP_LABEL_WIDTH: i32 = 11;
|
||||||
const CAMERA_PREVIEW_VIEWPORT_HEIGHT: i32 = 225;
|
const CAMERA_PREVIEW_VIEWPORT_HEIGHT: i32 = 225;
|
||||||
const CAMERA_PREVIEW_VIEWPORT_WIDTH: i32 = 400;
|
const CAMERA_PREVIEW_VIEWPORT_WIDTH: i32 = 400;
|
||||||
const EYE_PREVIEW_MIN_HEIGHT: i32 = 299;
|
const EYE_PREVIEW_MIN_HEIGHT: i32 = 299;
|
||||||
|
|||||||
@ -39,9 +39,22 @@ pub fn selected_server_addr(entry: >k::Entry, fallback: &str) -> String {
|
|||||||
let current = entry.text();
|
let current = entry.text();
|
||||||
let trimmed = current.trim();
|
let trimmed = current.trim();
|
||||||
if trimmed.is_empty() {
|
if trimmed.is_empty() {
|
||||||
fallback.to_string()
|
normalize_server_addr(fallback)
|
||||||
} else {
|
} else {
|
||||||
|
let normalized = normalize_server_addr(trimmed);
|
||||||
|
if normalized != trimmed {
|
||||||
|
entry.set_text(&normalized);
|
||||||
|
}
|
||||||
|
normalized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn normalize_server_addr(raw: &str) -> String {
|
||||||
|
let trimmed = raw.trim();
|
||||||
|
if trimmed.is_empty() || trimmed.contains("://") {
|
||||||
trimmed.to_string()
|
trimmed.to_string()
|
||||||
|
} else {
|
||||||
|
format!("https://{trimmed}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -39,30 +39,49 @@ fn client_tls_config(server_addr: &str) -> Result<ClientTlsConfig> {
|
|||||||
.domain_name(tls_domain(server_addr))
|
.domain_name(tls_domain(server_addr))
|
||||||
.with_enabled_roots();
|
.with_enabled_roots();
|
||||||
|
|
||||||
if let Some(ca_path) = env_path("LESAVKA_TLS_CA").or_else(|| default_pki_path("ca.crt"))
|
let ca_path = env_path("LESAVKA_TLS_CA").or_else(|| default_pki_path("ca.crt"));
|
||||||
&& ca_path.exists()
|
let cert_path = env_path("LESAVKA_TLS_CLIENT_CERT").or_else(|| default_pki_path("client.crt"));
|
||||||
{
|
let key_path = env_path("LESAVKA_TLS_CLIENT_KEY").or_else(|| default_pki_path("client.key"));
|
||||||
|
require_tls_enrollment(&ca_path, &cert_path, &key_path)?;
|
||||||
|
|
||||||
|
let ca_path = ca_path.expect("enrollment check guarantees CA path");
|
||||||
|
let cert_path = cert_path.expect("enrollment check guarantees cert path");
|
||||||
|
let key_path = key_path.expect("enrollment check guarantees key path");
|
||||||
|
|
||||||
let ca = std::fs::read(&ca_path)
|
let ca = std::fs::read(&ca_path)
|
||||||
.with_context(|| format!("reading TLS CA certificate {}", ca_path.display()))?;
|
.with_context(|| format!("reading TLS CA certificate {}", ca_path.display()))?;
|
||||||
tls = tls.ca_certificate(Certificate::from_pem(ca));
|
tls = tls.ca_certificate(Certificate::from_pem(ca));
|
||||||
}
|
|
||||||
|
|
||||||
let cert_path = env_path("LESAVKA_TLS_CLIENT_CERT").or_else(|| default_pki_path("client.crt"));
|
|
||||||
let key_path = env_path("LESAVKA_TLS_CLIENT_KEY").or_else(|| default_pki_path("client.key"));
|
|
||||||
if let (Some(cert_path), Some(key_path)) = (cert_path, key_path)
|
|
||||||
&& cert_path.exists()
|
|
||||||
&& key_path.exists()
|
|
||||||
{
|
|
||||||
let cert = std::fs::read(&cert_path)
|
let cert = std::fs::read(&cert_path)
|
||||||
.with_context(|| format!("reading TLS client certificate {}", cert_path.display()))?;
|
.with_context(|| format!("reading TLS client certificate {}", cert_path.display()))?;
|
||||||
let key = std::fs::read(&key_path)
|
let key = std::fs::read(&key_path)
|
||||||
.with_context(|| format!("reading TLS client key {}", key_path.display()))?;
|
.with_context(|| format!("reading TLS client key {}", key_path.display()))?;
|
||||||
tls = tls.identity(Identity::from_pem(cert, key));
|
tls = tls.identity(Identity::from_pem(cert, key));
|
||||||
}
|
|
||||||
|
|
||||||
Ok(tls)
|
Ok(tls)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn require_tls_enrollment(
|
||||||
|
ca_path: &Option<PathBuf>,
|
||||||
|
cert_path: &Option<PathBuf>,
|
||||||
|
key_path: &Option<PathBuf>,
|
||||||
|
) -> Result<()> {
|
||||||
|
if path_exists(ca_path) && path_exists(cert_path) && path_exists(key_path) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let pki_hint = default_pki_path("")
|
||||||
|
.map(|path| path.display().to_string())
|
||||||
|
.unwrap_or_else(|| "~/.config/lesavka/pki".to_string());
|
||||||
|
bail!(
|
||||||
|
"TLS enrollment is missing for the Lesavka relay. Run the server installer first, then run the client installer with the server-generated bundle, or let the client installer auto-fetch it from the configured SSH host. Expected ca.crt, client.crt, and client.key under {pki_hint}."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path_exists(path: &Option<PathBuf>) -> bool {
|
||||||
|
path.as_ref().is_some_and(|path| path.exists())
|
||||||
|
}
|
||||||
|
|
||||||
fn tls_domain(server_addr: &str) -> String {
|
fn tls_domain(server_addr: &str) -> String {
|
||||||
std::env::var("LESAVKA_TLS_DOMAIN")
|
std::env::var("LESAVKA_TLS_DOMAIN")
|
||||||
.ok()
|
.ok()
|
||||||
@ -254,4 +273,24 @@ mod tests {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn https_endpoint_fails_fast_without_client_enrollment() {
|
||||||
|
let dir = tempdir().expect("empty pki dir");
|
||||||
|
with_vars(
|
||||||
|
[
|
||||||
|
("HOME", Some(dir.path().to_string_lossy().as_ref())),
|
||||||
|
("LESAVKA_TLS_CA", None::<&str>),
|
||||||
|
("LESAVKA_TLS_CLIENT_CERT", None::<&str>),
|
||||||
|
("LESAVKA_TLS_CLIENT_KEY", None::<&str>),
|
||||||
|
("LESAVKA_TLS_DOMAIN", None::<&str>),
|
||||||
|
],
|
||||||
|
|| {
|
||||||
|
let err = endpoint("https://38.28.125.112:50051")
|
||||||
|
.expect_err("missing mTLS enrollment should be explicit");
|
||||||
|
assert!(err.to_string().contains("TLS enrollment is missing"));
|
||||||
|
assert!(err.to_string().contains("client.key"));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.16.0"
|
version = "0.16.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,13 @@
|
|||||||
This is the tracked inventory for `LESAVKA_*` knobs used by source, scripts, CI, and tests. The hygiene gate fails when a new variable is added without appearing here, which keeps operator-facing configuration from drifting into folklore.
|
This is the tracked inventory for `LESAVKA_*` knobs used by source, scripts, CI, and tests. The hygiene gate fails when a new variable is added without appearing here, which keeps operator-facing configuration from drifting into folklore.
|
||||||
|
|
||||||
Hardware-facing assumptions belong near the code that uses them; this file is the repo-wide index.
|
Hardware-facing assumptions belong near the code that uses them; this file is the repo-wide index.
|
||||||
|
|
||||||
|
Install security flow: the server installer creates `/etc/lesavka/lesavka-client-pki.tar.gz`
|
||||||
|
and makes it readable only by the installing account. The client installer first uses
|
||||||
|
`LESAVKA_CLIENT_PKI_BUNDLE` when supplied, otherwise it tries to fetch that same bundle
|
||||||
|
from `LESAVKA_CLIENT_PKI_SSH_SOURCE` over SSH. Runtime clients require the installed
|
||||||
|
`ca.crt`, `client.crt`, and `client.key` for HTTPS/mTLS relay connections.
|
||||||
|
|
||||||
| `LESAVKA_ALLOW_GADGET_CYCLE` | document near use before promoting to operator config |
|
| `LESAVKA_ALLOW_GADGET_CYCLE` | document near use before promoting to operator config |
|
||||||
| `LESAVKA_ALLOW_GADGET_RESET` | document near use before promoting to operator config |
|
| `LESAVKA_ALLOW_GADGET_RESET` | document near use before promoting to operator config |
|
||||||
| `LESAVKA_ALLOW_INSECURE` | client transport override; permits non-local `http://` relay URLs only for lab/debug use |
|
| `LESAVKA_ALLOW_INSECURE` | client transport override; permits non-local `http://` relay URLs only for lab/debug use |
|
||||||
@ -49,6 +56,7 @@ Hardware-facing assumptions belong near the code that uses them; this file is th
|
|||||||
| `LESAVKA_CAPTURE_REMOTE` | runtime/install/session override |
|
| `LESAVKA_CAPTURE_REMOTE` | runtime/install/session override |
|
||||||
| `LESAVKA_CLIENT_APP_SRC` | test/build contract variable; not runtime operator config |
|
| `LESAVKA_CLIENT_APP_SRC` | test/build contract variable; not runtime operator config |
|
||||||
| `LESAVKA_CLIENT_BUNDLE` | server installer output path for the generated client TLS enrollment bundle |
|
| `LESAVKA_CLIENT_BUNDLE` | server installer output path for the generated client TLS enrollment bundle |
|
||||||
|
| `LESAVKA_CLIENT_CAPTURE_DIR` | client installer capture folder override; defaults to `~/Pictures/lesavka` |
|
||||||
| `LESAVKA_CLIENT_CAMERA_SRC` | test/build contract variable; not runtime operator config |
|
| `LESAVKA_CLIENT_CAMERA_SRC` | test/build contract variable; not runtime operator config |
|
||||||
| `LESAVKA_CLIENT_INPUTS_SRC` | test/build contract variable; not runtime operator config |
|
| `LESAVKA_CLIENT_INPUTS_SRC` | test/build contract variable; not runtime operator config |
|
||||||
| `LESAVKA_CLIENT_KEYBOARD_SRC` | test/build contract variable; not runtime operator config |
|
| `LESAVKA_CLIENT_KEYBOARD_SRC` | test/build contract variable; not runtime operator config |
|
||||||
@ -59,7 +67,9 @@ Hardware-facing assumptions belong near the code that uses them; this file is th
|
|||||||
| `LESAVKA_CLIENT_OUTPUT_DISPLAY_SRC` | test/build contract variable; not runtime operator config |
|
| `LESAVKA_CLIENT_OUTPUT_DISPLAY_SRC` | test/build contract variable; not runtime operator config |
|
||||||
| `LESAVKA_CLIENT_OUTPUT_VIDEO_SRC` | test/build contract variable; not runtime operator config |
|
| `LESAVKA_CLIENT_OUTPUT_VIDEO_SRC` | test/build contract variable; not runtime operator config |
|
||||||
| `LESAVKA_CLIENT_PKI_BUNDLE` | client installer input path for a server-generated TLS enrollment bundle |
|
| `LESAVKA_CLIENT_PKI_BUNDLE` | client installer input path for a server-generated TLS enrollment bundle |
|
||||||
|
| `LESAVKA_CLIENT_PKI_AUTO_FETCH` | client installer toggle for SSH enrollment auto-fetch; defaults to enabled |
|
||||||
| `LESAVKA_CLIENT_PKI_DIR` | client installer/runtime TLS identity directory override |
|
| `LESAVKA_CLIENT_PKI_DIR` | client installer/runtime TLS identity directory override |
|
||||||
|
| `LESAVKA_CLIENT_PKI_SSH_SOURCE` | client installer SSH source for auto-fetching the server enrollment bundle; defaults to `theia:/etc/lesavka/lesavka-client-pki.tar.gz` |
|
||||||
| `LESAVKA_CLIENT_RELAYCTL_BIN_SRC` | test/build contract variable; not runtime operator config |
|
| `LESAVKA_CLIENT_RELAYCTL_BIN_SRC` | test/build contract variable; not runtime operator config |
|
||||||
| `LESAVKA_CLIENT_VIDEO_SUPPORT_SRC` | test/build contract variable; not runtime operator config |
|
| `LESAVKA_CLIENT_VIDEO_SUPPORT_SRC` | test/build contract variable; not runtime operator config |
|
||||||
| `LESAVKA_CLIPBOARD_CHORD` | input routing/clipboard override |
|
| `LESAVKA_CLIPBOARD_CHORD` | input routing/clipboard override |
|
||||||
|
|||||||
@ -382,23 +382,23 @@
|
|||||||
},
|
},
|
||||||
"client/src/launcher/ui/utility_button_bindings.rs": {
|
"client/src/launcher/ui/utility_button_bindings.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 0,
|
"doc_debt": 3,
|
||||||
"loc": 387
|
"loc": 489
|
||||||
},
|
},
|
||||||
"client/src/launcher/ui_components.rs": {
|
"client/src/launcher/ui_components.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 1,
|
"doc_debt": 1,
|
||||||
"loc": 124
|
"loc": 125
|
||||||
},
|
},
|
||||||
"client/src/launcher/ui_components/assemble_view.rs": {
|
"client/src/launcher/ui_components/assemble_view.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 0,
|
"doc_debt": 0,
|
||||||
"loc": 204
|
"loc": 205
|
||||||
},
|
},
|
||||||
"client/src/launcher/ui_components/build_contexts.rs": {
|
"client/src/launcher/ui_components/build_contexts.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 0,
|
"doc_debt": 0,
|
||||||
"loc": 87
|
"loc": 88
|
||||||
},
|
},
|
||||||
"client/src/launcher/ui_components/build_device_controls.rs": {
|
"client/src/launcher/ui_components/build_device_controls.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
@ -408,7 +408,7 @@
|
|||||||
"client/src/launcher/ui_components/build_operations_rail.rs": {
|
"client/src/launcher/ui_components/build_operations_rail.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 0,
|
"doc_debt": 0,
|
||||||
"loc": 310
|
"loc": 323
|
||||||
},
|
},
|
||||||
"client/src/launcher/ui_components/build_shell.rs": {
|
"client/src/launcher/ui_components/build_shell.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
@ -448,7 +448,7 @@
|
|||||||
"client/src/launcher/ui_components/types.rs": {
|
"client/src/launcher/ui_components/types.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 0,
|
"doc_debt": 0,
|
||||||
"loc": 221
|
"loc": 223
|
||||||
},
|
},
|
||||||
"client/src/launcher/ui_runtime.rs": {
|
"client/src/launcher/ui_runtime.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
@ -457,8 +457,8 @@
|
|||||||
},
|
},
|
||||||
"client/src/launcher/ui_runtime/control_paths.rs": {
|
"client/src/launcher/ui_runtime/control_paths.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 8,
|
"doc_debt": 9,
|
||||||
"loc": 244
|
"loc": 257
|
||||||
},
|
},
|
||||||
"client/src/launcher/ui_runtime/display_popouts.rs": {
|
"client/src/launcher/ui_runtime/display_popouts.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
@ -552,8 +552,8 @@
|
|||||||
},
|
},
|
||||||
"client/src/relay_transport.rs": {
|
"client/src/relay_transport.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 7,
|
"doc_debt": 9,
|
||||||
"loc": 257
|
"loc": 296
|
||||||
},
|
},
|
||||||
"client/src/sync_probe/analyze.rs": {
|
"client/src/sync_probe/analyze.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
|
|||||||
@ -189,8 +189,8 @@
|
|||||||
"loc": 82
|
"loc": 82
|
||||||
},
|
},
|
||||||
"client/src/relay_transport.rs": {
|
"client/src/relay_transport.rs": {
|
||||||
"line_percent": 95.54,
|
"line_percent": 97.0,
|
||||||
"loc": 257
|
"loc": 296
|
||||||
},
|
},
|
||||||
"client/src/sync_probe/analyze.rs": {
|
"client/src/sync_probe/analyze.rs": {
|
||||||
"line_percent": 97.92,
|
"line_percent": 97.92,
|
||||||
|
|||||||
@ -12,6 +12,9 @@ SRC=/var/src/lesavka
|
|||||||
export TMPDIR=${TMPDIR:-/var/tmp}
|
export TMPDIR=${TMPDIR:-/var/tmp}
|
||||||
USER_HOME=$(getent passwd "$ORIG_USER" | cut -d: -f6)
|
USER_HOME=$(getent passwd "$ORIG_USER" | cut -d: -f6)
|
||||||
CLIENT_PKI_DIR=${LESAVKA_CLIENT_PKI_DIR:-$USER_HOME/.config/lesavka/pki}
|
CLIENT_PKI_DIR=${LESAVKA_CLIENT_PKI_DIR:-$USER_HOME/.config/lesavka/pki}
|
||||||
|
CLIENT_PKI_AUTO_FETCH=${LESAVKA_CLIENT_PKI_AUTO_FETCH:-1}
|
||||||
|
CLIENT_PKI_SSH_SOURCE=${LESAVKA_CLIENT_PKI_SSH_SOURCE:-theia:/etc/lesavka/lesavka-client-pki.tar.gz}
|
||||||
|
CLIENT_CAPTURE_DIR=${LESAVKA_CLIENT_CAPTURE_DIR:-$USER_HOME/Pictures/lesavka}
|
||||||
|
|
||||||
log() {
|
log() {
|
||||||
printf '==> %s\n' "$*"
|
printf '==> %s\n' "$*"
|
||||||
@ -103,17 +106,44 @@ run_as_user() {
|
|||||||
sudo -u "$ORIG_USER" env HOME="$USER_HOME" SSH_AUTH_SOCK="${SSH_AUTH_SOCK:-}" "$@"
|
sudo -u "$ORIG_USER" env HOME="$USER_HOME" SSH_AUTH_SOCK="${SSH_AUTH_SOCK:-}" "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fetch_client_pki_bundle() {
|
||||||
|
[[ $CLIENT_PKI_AUTO_FETCH != 0 && $CLIENT_PKI_AUTO_FETCH != false && $CLIENT_PKI_AUTO_FETCH != no ]] || return 1
|
||||||
|
[[ $CLIENT_PKI_SSH_SOURCE == *:* ]] || return 1
|
||||||
|
|
||||||
|
local host=${CLIENT_PKI_SSH_SOURCE%%:*}
|
||||||
|
local remote_path=${CLIENT_PKI_SSH_SOURCE#*:}
|
||||||
|
local tmp_bundle
|
||||||
|
tmp_bundle=$(mktemp --suffix=.tar.gz)
|
||||||
|
if run_as_user scp -q -o BatchMode=yes -o ConnectTimeout=5 \
|
||||||
|
"$host:$remote_path" "$tmp_bundle" >/dev/null 2>&1; then
|
||||||
|
printf '%s\n' "$tmp_bundle"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
rm -f "$tmp_bundle"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
install_client_pki_bundle() {
|
install_client_pki_bundle() {
|
||||||
local bundle=${LESAVKA_CLIENT_PKI_BUNDLE:-}
|
local bundle=${LESAVKA_CLIENT_PKI_BUNDLE:-}
|
||||||
|
local fetched_bundle=0
|
||||||
if [[ -z $bundle ]]; then
|
if [[ -z $bundle ]]; then
|
||||||
if [[ -s "$CLIENT_PKI_DIR/ca.crt" && -s "$CLIENT_PKI_DIR/client.crt" && -s "$CLIENT_PKI_DIR/client.key" ]]; then
|
if [[ -s "$CLIENT_PKI_DIR/ca.crt" && -s "$CLIENT_PKI_DIR/client.crt" && -s "$CLIENT_PKI_DIR/client.key" ]]; then
|
||||||
echo " ↪ TLS client identity already present: $CLIENT_PKI_DIR"
|
echo " ↪ TLS client identity already present: $CLIENT_PKI_DIR"
|
||||||
else
|
|
||||||
echo "⚠️ no LESAVKA_CLIENT_PKI_BUNDLE supplied; HTTPS relay connections will need a trusted public cert or a bundle install later."
|
|
||||||
fi
|
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if bundle=$(fetch_client_pki_bundle); then
|
||||||
|
fetched_bundle=1
|
||||||
|
echo " ↪ fetched TLS client enrollment bundle from $CLIENT_PKI_SSH_SOURCE"
|
||||||
|
else
|
||||||
|
echo "⚠️ no TLS client identity installed."
|
||||||
|
echo " Rerun with LESAVKA_CLIENT_PKI_BUNDLE=/path/to/lesavka-client-pki.tar.gz,"
|
||||||
|
echo " or make $CLIENT_PKI_SSH_SOURCE readable over SSH and rerun the installer."
|
||||||
|
echo " HTTPS/mTLS relay connections will not work until this bundle is installed."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
log "5b. Installing TLS client identity"
|
log "5b. Installing TLS client identity"
|
||||||
local tmp
|
local tmp
|
||||||
tmp=$(mktemp -d)
|
tmp=$(mktemp -d)
|
||||||
@ -130,6 +160,9 @@ install_client_pki_bundle() {
|
|||||||
sudo install -m 0644 -o "$ORIG_USER" -g "$ORIG_USER" "$tmp/client.crt" "$CLIENT_PKI_DIR/client.crt"
|
sudo install -m 0644 -o "$ORIG_USER" -g "$ORIG_USER" "$tmp/client.crt" "$CLIENT_PKI_DIR/client.crt"
|
||||||
sudo install -m 0600 -o "$ORIG_USER" -g "$ORIG_USER" "$tmp/client.key" "$CLIENT_PKI_DIR/client.key"
|
sudo install -m 0600 -o "$ORIG_USER" -g "$ORIG_USER" "$tmp/client.key" "$CLIENT_PKI_DIR/client.key"
|
||||||
sudo rm -rf "$tmp"
|
sudo rm -rf "$tmp"
|
||||||
|
if [[ $fetched_bundle == 1 ]]; then
|
||||||
|
rm -f "$bundle"
|
||||||
|
fi
|
||||||
echo " ↪ installed TLS client identity: $CLIENT_PKI_DIR"
|
echo " ↪ installed TLS client identity: $CLIENT_PKI_DIR"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -233,6 +266,8 @@ log "5. Installing launchable client binaries"
|
|||||||
sudo install -Dm755 "$SRC/target/release/lesavka-client" /usr/local/bin/lesavka-client
|
sudo install -Dm755 "$SRC/target/release/lesavka-client" /usr/local/bin/lesavka-client
|
||||||
sudo ln -sf /usr/local/bin/lesavka-client /usr/local/bin/lesavka
|
sudo ln -sf /usr/local/bin/lesavka-client /usr/local/bin/lesavka
|
||||||
install_client_pki_bundle
|
install_client_pki_bundle
|
||||||
|
sudo install -d -m 0755 -o "$ORIG_USER" -g "$ORIG_USER" "$CLIENT_CAPTURE_DIR"
|
||||||
|
echo " ↪ capture folder: $CLIENT_CAPTURE_DIR"
|
||||||
|
|
||||||
log "6. Registering desktop application"
|
log "6. Registering desktop application"
|
||||||
sudo install -Dm644 "$SRC/client/assets/icons/hicolor/1024x1024/apps/lesavka.png" \
|
sudo install -Dm644 "$SRC/client/assets/icons/hicolor/1024x1024/apps/lesavka.png" \
|
||||||
@ -265,6 +300,7 @@ echo " Launch alias: /usr/local/bin/lesavka"
|
|||||||
echo " Desktop entry: /usr/share/applications/lesavka.desktop"
|
echo " Desktop entry: /usr/share/applications/lesavka.desktop"
|
||||||
echo " Build source: $SRC/target/release/lesavka-client"
|
echo " Build source: $SRC/target/release/lesavka-client"
|
||||||
echo " TLS identity: $CLIENT_PKI_DIR"
|
echo " TLS identity: $CLIENT_PKI_DIR"
|
||||||
|
echo " Captures: $CLIENT_CAPTURE_DIR"
|
||||||
echo "✅ Installed version: lesavka-client ${INSTALLED_VERSION:-unknown}${INSTALLED_SHA:+ ($INSTALLED_SHA)}"
|
echo "✅ Installed version: lesavka-client ${INSTALLED_VERSION:-unknown}${INSTALLED_SHA:+ ($INSTALLED_SHA)}"
|
||||||
echo
|
echo
|
||||||
echo "Quick start:"
|
echo "Quick start:"
|
||||||
|
|||||||
@ -167,7 +167,8 @@ ensure_server_tls_pki() {
|
|||||||
sudo cp "$LESAVKA_TLS_DIR/client.crt" "$bundle_tmp/client.crt"
|
sudo cp "$LESAVKA_TLS_DIR/client.crt" "$bundle_tmp/client.crt"
|
||||||
sudo cp "$LESAVKA_TLS_DIR/client.key" "$bundle_tmp/client.key"
|
sudo cp "$LESAVKA_TLS_DIR/client.key" "$bundle_tmp/client.key"
|
||||||
sudo tar -C "$bundle_tmp" -czf "$LESAVKA_CLIENT_BUNDLE" ca.crt client.crt client.key
|
sudo tar -C "$bundle_tmp" -czf "$LESAVKA_CLIENT_BUNDLE" ca.crt client.crt client.key
|
||||||
sudo chmod 0640 "$LESAVKA_CLIENT_BUNDLE"
|
sudo chown "$ORIG_USER":"$ORIG_USER" "$LESAVKA_CLIENT_BUNDLE"
|
||||||
|
sudo chmod 0600 "$LESAVKA_CLIENT_BUNDLE"
|
||||||
sudo rm -rf "$bundle_tmp"
|
sudo rm -rf "$bundle_tmp"
|
||||||
echo " ↪ client enrollment bundle: $LESAVKA_CLIENT_BUNDLE"
|
echo " ↪ client enrollment bundle: $LESAVKA_CLIENT_BUNDLE"
|
||||||
}
|
}
|
||||||
@ -1121,6 +1122,8 @@ fi
|
|||||||
if [[ -n $PERSISTED_UVC_CODEC ]]; then
|
if [[ -n $PERSISTED_UVC_CODEC ]]; then
|
||||||
echo "➡️ UVC codec: ${PERSISTED_UVC_CODEC}"
|
echo "➡️ UVC codec: ${PERSISTED_UVC_CODEC}"
|
||||||
fi
|
fi
|
||||||
|
echo "➡️ Client TLS bundle: ${LESAVKA_CLIENT_BUNDLE}"
|
||||||
|
echo "➡️ Client install can use: sudo env LESAVKA_CLIENT_PKI_BUNDLE=${LESAVKA_CLIENT_BUNDLE} ./scripts/install/client.sh"
|
||||||
echo "➡️ Status: sudo systemctl status lesavka-server --no-pager"
|
echo "➡️ Status: sudo systemctl status lesavka-server --no-pager"
|
||||||
echo "➡️ Logs: sudo journalctl -u lesavka-server -f --no-pager"
|
echo "➡️ Logs: sudo journalctl -u lesavka-server -f --no-pager"
|
||||||
echo "✅ Installed version: lesavka-server ${INSTALLED_VERSION:-unknown}${INSTALLED_SHA:+ ($INSTALLED_SHA)}"
|
echo "✅ Installed version: lesavka-server ${INSTALLED_VERSION:-unknown}${INSTALLED_SHA:+ ($INSTALLED_SHA)}"
|
||||||
|
|||||||
@ -10,7 +10,7 @@ bench = false
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.16.0"
|
version = "0.16.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
autobins = false
|
autobins = false
|
||||||
|
|
||||||
|
|||||||
@ -16,8 +16,14 @@ fn client_install_accepts_server_generated_tls_bundle() {
|
|||||||
"client.crt",
|
"client.crt",
|
||||||
"client.key",
|
"client.key",
|
||||||
"install_client_pki_bundle",
|
"install_client_pki_bundle",
|
||||||
"HTTPS relay connections will need a trusted public cert or a bundle install later",
|
"fetch_client_pki_bundle",
|
||||||
|
"LESAVKA_CLIENT_PKI_SSH_SOURCE",
|
||||||
|
"LESAVKA_CLIENT_CAPTURE_DIR",
|
||||||
|
"theia:/etc/lesavka/lesavka-client-pki.tar.gz",
|
||||||
|
"Pictures/lesavka",
|
||||||
|
"HTTPS/mTLS relay connections will not work until this bundle is installed",
|
||||||
"TLS identity:",
|
"TLS identity:",
|
||||||
|
"Captures:",
|
||||||
] {
|
] {
|
||||||
assert!(
|
assert!(
|
||||||
CLIENT_INSTALL.contains(expected),
|
CLIENT_INSTALL.contains(expected),
|
||||||
@ -32,4 +38,8 @@ fn client_install_accepts_server_generated_tls_bundle() {
|
|||||||
CLIENT_INSTALL.contains("0600"),
|
CLIENT_INSTALL.contains("0600"),
|
||||||
"client private key should be installed with private permissions"
|
"client private key should be installed with private permissions"
|
||||||
);
|
);
|
||||||
|
assert!(
|
||||||
|
CLIENT_INSTALL.contains("scp -q -o BatchMode=yes"),
|
||||||
|
"client installer should auto-fetch the server enrollment bundle without hanging"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -228,6 +228,13 @@ fn operations_column_fills_height_and_splits_extra_space_between_logs() {
|
|||||||
.count(),
|
.count(),
|
||||||
2
|
2
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
UI_LAYOUT_SRC
|
||||||
|
.matches(".set_propagate_natural_height(false)")
|
||||||
|
.count(),
|
||||||
|
2,
|
||||||
|
"Diagnostics and Log scrollports should split height by allocation rather than content"
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
UI_LAYOUT_SRC
|
UI_LAYOUT_SRC
|
||||||
.matches(".max_content_height(SIDE_LOG")
|
.matches(".max_content_height(SIDE_LOG")
|
||||||
@ -273,14 +280,20 @@ fn status_chip_text_is_centered_inside_each_pill() {
|
|||||||
fn relay_controls_keep_connect_inline_with_server_entry() {
|
fn relay_controls_keep_connect_inline_with_server_entry() {
|
||||||
assert!(UI_LAYOUT_SRC.contains("build_panel(\"Relay\")"));
|
assert!(UI_LAYOUT_SRC.contains("build_panel(\"Relay\")"));
|
||||||
assert_eq!(const_i32("OPERATIONS_RAIL_WIDTH"), 276);
|
assert_eq!(const_i32("OPERATIONS_RAIL_WIDTH"), 276);
|
||||||
|
assert_eq!(const_i32("RELAY_SUBGROUP_LABEL_WIDTH"), 11);
|
||||||
assert_eq!(const_i32("RAIL_BUTTON_WIDTH"), 86);
|
assert_eq!(const_i32("RAIL_BUTTON_WIDTH"), 86);
|
||||||
assert_eq!(const_i32("RAIL_BUTTON_LABEL_CHARS"), 11);
|
assert_eq!(const_i32("RAIL_BUTTON_LABEL_CHARS"), 11);
|
||||||
assert!(UI_LAYOUT_SRC.contains("let relay_grid = gtk::Grid::new();"));
|
assert!(UI_LAYOUT_SRC.contains("let relay_grid = gtk::Grid::new();"));
|
||||||
assert!(UI_LAYOUT_SRC.contains("relay_grid.set_column_homogeneous(true);"));
|
assert!(UI_LAYOUT_SRC.contains("server_entry.set_width_chars(11);"));
|
||||||
assert!(UI_LAYOUT_SRC.contains("relay_grid.attach(&server_entry, 0, 0, 2, 1);"));
|
assert!(UI_LAYOUT_SRC.contains("relay_grid.set_column_homogeneous(false);"));
|
||||||
|
assert!(UI_LAYOUT_SRC.contains("relay_grid.attach(&server_entry, 0, 0, 1, 1);"));
|
||||||
|
assert!(UI_LAYOUT_SRC.contains("let certs_button = rail_button(\"Certs\""));
|
||||||
assert!(UI_LAYOUT_SRC.contains("let start_button = rail_button(\"Connect\""));
|
assert!(UI_LAYOUT_SRC.contains("let start_button = rail_button(\"Connect\""));
|
||||||
assert!(UI_LAYOUT_SRC.contains("pub(crate) fn set_rail_button_label("));
|
assert!(UI_LAYOUT_SRC.contains("pub(crate) fn set_rail_button_label("));
|
||||||
|
assert!(UI_LAYOUT_SRC.contains("relay_grid.attach(&certs_button, 1, 0, 1, 1);"));
|
||||||
assert!(UI_LAYOUT_SRC.contains("relay_grid.attach(&start_button, 2, 0, 1, 1);"));
|
assert!(UI_LAYOUT_SRC.contains("relay_grid.attach(&start_button, 2, 0, 1, 1);"));
|
||||||
|
assert!(UI_LAYOUT_SRC.contains("stabilize_button(&certs_button, 66);"));
|
||||||
|
assert!(UI_LAYOUT_SRC.contains("stabilize_button(&start_button, 76);"));
|
||||||
assert!(UI_LAYOUT_SRC.contains("let recovery_heading = gtk::Label::new(Some(\"Recover\"));"));
|
assert!(UI_LAYOUT_SRC.contains("let recovery_heading = gtk::Label::new(Some(\"Recover\"));"));
|
||||||
assert!(
|
assert!(
|
||||||
UI_LAYOUT_SRC
|
UI_LAYOUT_SRC
|
||||||
@ -291,11 +304,16 @@ fn relay_controls_keep_connect_inline_with_server_entry() {
|
|||||||
.contains("let recovery_buttons = gtk::Box::new(gtk::Orientation::Horizontal, 8);")
|
.contains("let recovery_buttons = gtk::Box::new(gtk::Orientation::Horizontal, 8);")
|
||||||
);
|
);
|
||||||
assert!(UI_LAYOUT_SRC.contains("recovery_buttons.set_homogeneous(true);"));
|
assert!(UI_LAYOUT_SRC.contains("recovery_buttons.set_homogeneous(true);"));
|
||||||
assert!(UI_LAYOUT_SRC.contains("recovery_heading.set_width_chars(10);"));
|
assert!(
|
||||||
|
UI_LAYOUT_SRC.contains("recovery_heading.set_width_chars(RELAY_SUBGROUP_LABEL_WIDTH);")
|
||||||
|
);
|
||||||
assert!(UI_LAYOUT_SRC.contains(
|
assert!(UI_LAYOUT_SRC.contains(
|
||||||
"let calibration_heading = gtk::Label::new(Some(\"AV Upstream\\nCalibration\"));"
|
"let calibration_heading = gtk::Label::new(Some(\"Audio/Video\\nUpstream\\nCalibration\"));"
|
||||||
));
|
));
|
||||||
assert!(UI_LAYOUT_SRC.contains("calibration_heading.set_width_chars(12);"));
|
assert!(UI_LAYOUT_SRC.contains("calibration_heading.set_justify(gtk::Justification::Center);"));
|
||||||
|
assert!(
|
||||||
|
UI_LAYOUT_SRC.contains("calibration_heading.set_width_chars(RELAY_SUBGROUP_LABEL_WIDTH);")
|
||||||
|
);
|
||||||
assert!(UI_LAYOUT_SRC.contains("let calibration_buttons = gtk::Grid::new();"));
|
assert!(UI_LAYOUT_SRC.contains("let calibration_buttons = gtk::Grid::new();"));
|
||||||
assert!(UI_LAYOUT_SRC.contains("calibration_buttons.set_column_homogeneous(true);"));
|
assert!(UI_LAYOUT_SRC.contains("calibration_buttons.set_column_homogeneous(true);"));
|
||||||
assert!(UI_LAYOUT_SRC.contains("let calibration_default_button = rail_button("));
|
assert!(UI_LAYOUT_SRC.contains("let calibration_default_button = rail_button("));
|
||||||
@ -313,7 +331,7 @@ fn relay_controls_keep_connect_inline_with_server_entry() {
|
|||||||
.contains("let tools_buttons = gtk::Box::new(gtk::Orientation::Horizontal, 8);")
|
.contains("let tools_buttons = gtk::Box::new(gtk::Orientation::Horizontal, 8);")
|
||||||
);
|
);
|
||||||
assert!(UI_LAYOUT_SRC.contains("tools_buttons.set_homogeneous(true);"));
|
assert!(UI_LAYOUT_SRC.contains("tools_buttons.set_homogeneous(true);"));
|
||||||
assert!(UI_LAYOUT_SRC.contains("tools_heading.set_width_chars(10);"));
|
assert!(UI_LAYOUT_SRC.contains("tools_heading.set_width_chars(RELAY_SUBGROUP_LABEL_WIDTH);"));
|
||||||
assert!(UI_LAYOUT_SRC.contains("let clipboard_button = rail_button(\"Clipboard\""));
|
assert!(UI_LAYOUT_SRC.contains("let clipboard_button = rail_button(\"Clipboard\""));
|
||||||
assert!(UI_LAYOUT_SRC.contains("let usb_recover_button = rail_button(\"USB\""));
|
assert!(UI_LAYOUT_SRC.contains("let usb_recover_button = rail_button(\"USB\""));
|
||||||
assert!(UI_LAYOUT_SRC.contains("let uac_recover_button = rail_button(\"UAC\""));
|
assert!(UI_LAYOUT_SRC.contains("let uac_recover_button = rail_button(\"UAC\""));
|
||||||
@ -324,19 +342,23 @@ fn relay_controls_keep_connect_inline_with_server_entry() {
|
|||||||
assert!(
|
assert!(
|
||||||
source_index("let recovery_heading = gtk::Label::new(Some(\"Recover\"));")
|
source_index("let recovery_heading = gtk::Label::new(Some(\"Recover\"));")
|
||||||
< source_index(
|
< source_index(
|
||||||
"let calibration_heading = gtk::Label::new(Some(\"AV Upstream\\nCalibration\"));"
|
"let calibration_heading = gtk::Label::new(Some(\"Audio/Video\\nUpstream\\nCalibration\"));"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
source_index(
|
source_index(
|
||||||
"let calibration_heading = gtk::Label::new(Some(\"AV Upstream\\nCalibration\"));"
|
"let calibration_heading = gtk::Label::new(Some(\"Audio/Video\\nUpstream\\nCalibration\"));"
|
||||||
) < source_index("let power_heading = gtk::Label::new(Some(\"GPIO Power\"));")
|
) < source_index("let power_heading = gtk::Label::new(Some(\"GPIO Power\"));")
|
||||||
);
|
);
|
||||||
assert!(UI_LAYOUT_SRC.contains("tools_buttons.append(&clipboard_button);"));
|
assert!(UI_LAYOUT_SRC.contains("tools_buttons.append(&clipboard_button);"));
|
||||||
assert!(!UI_LAYOUT_SRC.contains("Gate Probe"));
|
assert!(!UI_LAYOUT_SRC.contains("Gate Probe"));
|
||||||
assert!(UI_LAYOUT_SRC.contains("text.set_ellipsize(pango::EllipsizeMode::End);"));
|
assert!(UI_LAYOUT_SRC.contains("text.set_ellipsize(pango::EllipsizeMode::End);"));
|
||||||
assert!(
|
assert!(
|
||||||
source_index("relay_grid.attach(&server_entry, 0, 0, 2, 1);")
|
source_index("relay_grid.attach(&server_entry, 0, 0, 1, 1);")
|
||||||
|
< source_index("relay_grid.attach(&certs_button, 1, 0, 1, 1);")
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
source_index("relay_grid.attach(&certs_button, 1, 0, 1, 1);")
|
||||||
< source_index("relay_grid.attach(&start_button, 2, 0, 1, 1);")
|
< source_index("relay_grid.attach(&start_button, 2, 0, 1, 1);")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -165,6 +165,10 @@ fn launcher_webcam_quality_selection_reaches_preview_and_relay_env() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn launcher_utility_buttons_still_bind_to_live_actions() {
|
fn launcher_utility_buttons_still_bind_to_live_actions() {
|
||||||
assert!(UI_SRC.contains("widgets.clipboard_button.connect_clicked"));
|
assert!(UI_SRC.contains("widgets.clipboard_button.connect_clicked"));
|
||||||
|
assert!(UI_SRC.contains("certs_button.connect_clicked"));
|
||||||
|
assert!(UI_SRC.contains("Choose Lesavka Client TLS Bundle"));
|
||||||
|
assert!(UI_SRC.contains("install_client_pki_bundle(&bundle)"));
|
||||||
|
assert!(UI_SRC.contains(".config/lesavka/pki"));
|
||||||
assert!(UI_SRC.contains("send_clipboard_text_to_remote(&server_addr, &text)"));
|
assert!(UI_SRC.contains("send_clipboard_text_to_remote(&server_addr, &text)"));
|
||||||
assert!(UI_SRC.contains("Start the relay before sending clipboard text."));
|
assert!(UI_SRC.contains("Start the relay before sending clipboard text."));
|
||||||
assert!(!UI_SRC.contains("widgets.probe_button.connect_clicked"));
|
assert!(!UI_SRC.contains("widgets.probe_button.connect_clicked"));
|
||||||
@ -181,6 +185,14 @@ fn launcher_utility_buttons_still_bind_to_live_actions() {
|
|||||||
assert!(UI_SRC.contains("Recover USB 2/3: relay acknowledged reset."));
|
assert!(UI_SRC.contains("Recover USB 2/3: relay acknowledged reset."));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn launcher_normalizes_bare_relay_addresses_to_https() {
|
||||||
|
assert!(UI_RUNTIME_SRC.contains("pub fn normalize_server_addr(raw: &str) -> String"));
|
||||||
|
assert!(UI_RUNTIME_SRC.contains("trimmed.contains(\"://\")"));
|
||||||
|
assert!(UI_RUNTIME_SRC.contains("format!(\"https://{trimmed}\")"));
|
||||||
|
assert!(UI_RUNTIME_SRC.contains("entry.set_text(&normalized);"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn server_chip_distinguishes_reachable_from_connected() {
|
fn server_chip_distinguishes_reachable_from_connected() {
|
||||||
assert!(UI_RUNTIME_SRC.contains("fn server_light_state("));
|
assert!(UI_RUNTIME_SRC.contains("fn server_light_state("));
|
||||||
|
|||||||
@ -221,6 +221,9 @@ fn server_install_generates_mtls_identity_and_client_bundle() {
|
|||||||
"extendedKeyUsage = clientAuth",
|
"extendedKeyUsage = clientAuth",
|
||||||
"LESAVKA_CLIENT_BUNDLE",
|
"LESAVKA_CLIENT_BUNDLE",
|
||||||
"lesavka-client-pki.tar.gz",
|
"lesavka-client-pki.tar.gz",
|
||||||
|
"Client TLS bundle:",
|
||||||
|
"Client install can use:",
|
||||||
|
"sudo chown \"$ORIG_USER\":\"$ORIG_USER\" \"$LESAVKA_CLIENT_BUNDLE\"",
|
||||||
"LESAVKA_REQUIRE_TLS=%s",
|
"LESAVKA_REQUIRE_TLS=%s",
|
||||||
"LESAVKA_TLS_CLIENT_CA=%s",
|
"LESAVKA_TLS_CLIENT_CA=%s",
|
||||||
] {
|
] {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user