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]]
|
||||
name = "lesavka_client"
|
||||
version = "0.16.0"
|
||||
version = "0.16.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@ -1686,7 +1686,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_common"
|
||||
version = "0.16.0"
|
||||
version = "0.16.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
@ -1698,7 +1698,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_server"
|
||||
version = "0.16.0"
|
||||
version = "0.16.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
|
||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
||||
|
||||
[package]
|
||||
name = "lesavka_client"
|
||||
version = "0.16.0"
|
||||
version = "0.16.1"
|
||||
edition = "2024"
|
||||
|
||||
[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 widgets = widgets.clone();
|
||||
|
||||
@ -85,6 +85,7 @@ pub fn build_launcher_view(
|
||||
let OperationsRailContext {
|
||||
server_entry,
|
||||
start_button,
|
||||
certs_button,
|
||||
clipboard_button,
|
||||
usb_recover_button,
|
||||
uac_recover_button,
|
||||
|
||||
@ -124,6 +124,7 @@
|
||||
display_panes: [left_pane.clone(), right_pane.clone()],
|
||||
server_entry: server_entry.clone(),
|
||||
start_button: start_button.clone(),
|
||||
certs_button: certs_button.clone(),
|
||||
camera_combo: camera_combo.clone(),
|
||||
camera_quality_combo: camera_quality_combo.clone(),
|
||||
microphone_combo: microphone_combo.clone(),
|
||||
|
||||
@ -55,6 +55,7 @@ struct DeviceControlsContext {
|
||||
struct OperationsRailContext {
|
||||
server_entry: gtk::Entry,
|
||||
start_button: gtk::Button,
|
||||
certs_button: gtk::Button,
|
||||
clipboard_button: gtk::Button,
|
||||
usb_recover_button: gtk::Button,
|
||||
uac_recover_button: gtk::Button,
|
||||
|
||||
@ -3,18 +3,27 @@
|
||||
let server_entry = gtk::Entry::new();
|
||||
server_entry.add_css_class("server-entry");
|
||||
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_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();
|
||||
relay_grid.set_column_homogeneous(true);
|
||||
relay_grid.set_column_homogeneous(false);
|
||||
relay_grid.set_column_spacing(6);
|
||||
relay_grid.set_hexpand(true);
|
||||
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.");
|
||||
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);
|
||||
|
||||
connection_body.append(&relay_grid);
|
||||
@ -24,7 +33,7 @@
|
||||
recovery_heading.set_halign(gtk::Align::Start);
|
||||
let recovery_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||
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);
|
||||
let recovery_buttons = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||
recovery_buttons.set_hexpand(true);
|
||||
@ -39,10 +48,11 @@
|
||||
connection_body.append(&recovery_row);
|
||||
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.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);
|
||||
calibration_row.set_hexpand(true);
|
||||
calibration_row.append(&calibration_heading);
|
||||
@ -93,7 +103,7 @@
|
||||
power_shell.set_halign(gtk::Align::Fill);
|
||||
let power_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||
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);
|
||||
let power_buttons = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||
power_buttons.set_hexpand(true);
|
||||
@ -127,7 +137,7 @@
|
||||
|
||||
let routing_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||
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);
|
||||
let routing_buttons = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||
routing_buttons.set_hexpand(true);
|
||||
@ -150,7 +160,7 @@
|
||||
tools_heading.set_halign(gtk::Align::Start);
|
||||
let tools_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||
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);
|
||||
let tools_buttons = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||
tools_buttons.set_hexpand(true);
|
||||
@ -196,6 +206,7 @@
|
||||
.child(&diagnostics_shell)
|
||||
.build();
|
||||
diagnostics_scroll.set_propagate_natural_width(false);
|
||||
diagnostics_scroll.set_propagate_natural_height(false);
|
||||
diagnostics_body.append(&diagnostics_toolbar);
|
||||
diagnostics_body.append(&diagnostics_scroll);
|
||||
operations.append(&diagnostics_panel);
|
||||
@ -252,6 +263,7 @@
|
||||
.child(&session_log_view)
|
||||
.build();
|
||||
log_scroll.set_propagate_natural_width(false);
|
||||
log_scroll.set_propagate_natural_height(false);
|
||||
console_body.append(&console_toolbar);
|
||||
console_body.append(&log_scroll);
|
||||
operations.append(&console_panel);
|
||||
@ -277,6 +289,7 @@
|
||||
OperationsRailContext {
|
||||
server_entry,
|
||||
start_button,
|
||||
certs_button,
|
||||
clipboard_button,
|
||||
usb_recover_button,
|
||||
uac_recover_button,
|
||||
|
||||
@ -131,6 +131,7 @@ pub struct LauncherWidgets {
|
||||
pub display_panes: [DisplayPaneWidgets; 2],
|
||||
pub server_entry: gtk::Entry,
|
||||
pub start_button: gtk::Button,
|
||||
pub certs_button: gtk::Button,
|
||||
pub camera_combo: gtk::ComboBoxText,
|
||||
pub camera_quality_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_HEIGHT: i32 = 880;
|
||||
const OPERATIONS_RAIL_WIDTH: i32 = 276;
|
||||
const RELAY_SUBGROUP_LABEL_WIDTH: i32 = 11;
|
||||
const CAMERA_PREVIEW_VIEWPORT_HEIGHT: i32 = 225;
|
||||
const CAMERA_PREVIEW_VIEWPORT_WIDTH: i32 = 400;
|
||||
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 trimmed = current.trim();
|
||||
if trimmed.is_empty() {
|
||||
fallback.to_string()
|
||||
normalize_server_addr(fallback)
|
||||
} 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()
|
||||
} else {
|
||||
format!("https://{trimmed}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -39,30 +39,49 @@ fn client_tls_config(server_addr: &str) -> Result<ClientTlsConfig> {
|
||||
.domain_name(tls_domain(server_addr))
|
||||
.with_enabled_roots();
|
||||
|
||||
if let Some(ca_path) = env_path("LESAVKA_TLS_CA").or_else(|| default_pki_path("ca.crt"))
|
||||
&& ca_path.exists()
|
||||
{
|
||||
let ca_path = env_path("LESAVKA_TLS_CA").or_else(|| default_pki_path("ca.crt"));
|
||||
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)
|
||||
.with_context(|| format!("reading TLS CA certificate {}", ca_path.display()))?;
|
||||
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)
|
||||
.with_context(|| format!("reading TLS client certificate {}", cert_path.display()))?;
|
||||
let key = std::fs::read(&key_path)
|
||||
.with_context(|| format!("reading TLS client key {}", key_path.display()))?;
|
||||
tls = tls.identity(Identity::from_pem(cert, key));
|
||||
}
|
||||
|
||||
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 {
|
||||
std::env::var("LESAVKA_TLS_DOMAIN")
|
||||
.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]
|
||||
name = "lesavka_common"
|
||||
version = "0.16.0"
|
||||
version = "0.16.1"
|
||||
edition = "2024"
|
||||
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.
|
||||
|
||||
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_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 |
|
||||
@ -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_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_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_INPUTS_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_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_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_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_VIDEO_SUPPORT_SRC` | test/build contract variable; not runtime operator config |
|
||||
| `LESAVKA_CLIPBOARD_CHORD` | input routing/clipboard override |
|
||||
|
||||
@ -382,23 +382,23 @@
|
||||
},
|
||||
"client/src/launcher/ui/utility_button_bindings.rs": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 0,
|
||||
"loc": 387
|
||||
"doc_debt": 3,
|
||||
"loc": 489
|
||||
},
|
||||
"client/src/launcher/ui_components.rs": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 1,
|
||||
"loc": 124
|
||||
"loc": 125
|
||||
},
|
||||
"client/src/launcher/ui_components/assemble_view.rs": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 0,
|
||||
"loc": 204
|
||||
"loc": 205
|
||||
},
|
||||
"client/src/launcher/ui_components/build_contexts.rs": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 0,
|
||||
"loc": 87
|
||||
"loc": 88
|
||||
},
|
||||
"client/src/launcher/ui_components/build_device_controls.rs": {
|
||||
"clippy_warnings": 0,
|
||||
@ -408,7 +408,7 @@
|
||||
"client/src/launcher/ui_components/build_operations_rail.rs": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 0,
|
||||
"loc": 310
|
||||
"loc": 323
|
||||
},
|
||||
"client/src/launcher/ui_components/build_shell.rs": {
|
||||
"clippy_warnings": 0,
|
||||
@ -448,7 +448,7 @@
|
||||
"client/src/launcher/ui_components/types.rs": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 0,
|
||||
"loc": 221
|
||||
"loc": 223
|
||||
},
|
||||
"client/src/launcher/ui_runtime.rs": {
|
||||
"clippy_warnings": 0,
|
||||
@ -457,8 +457,8 @@
|
||||
},
|
||||
"client/src/launcher/ui_runtime/control_paths.rs": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 8,
|
||||
"loc": 244
|
||||
"doc_debt": 9,
|
||||
"loc": 257
|
||||
},
|
||||
"client/src/launcher/ui_runtime/display_popouts.rs": {
|
||||
"clippy_warnings": 0,
|
||||
@ -552,8 +552,8 @@
|
||||
},
|
||||
"client/src/relay_transport.rs": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 7,
|
||||
"loc": 257
|
||||
"doc_debt": 9,
|
||||
"loc": 296
|
||||
},
|
||||
"client/src/sync_probe/analyze.rs": {
|
||||
"clippy_warnings": 0,
|
||||
|
||||
@ -189,8 +189,8 @@
|
||||
"loc": 82
|
||||
},
|
||||
"client/src/relay_transport.rs": {
|
||||
"line_percent": 95.54,
|
||||
"loc": 257
|
||||
"line_percent": 97.0,
|
||||
"loc": 296
|
||||
},
|
||||
"client/src/sync_probe/analyze.rs": {
|
||||
"line_percent": 97.92,
|
||||
|
||||
@ -12,6 +12,9 @@ SRC=/var/src/lesavka
|
||||
export TMPDIR=${TMPDIR:-/var/tmp}
|
||||
USER_HOME=$(getent passwd "$ORIG_USER" | cut -d: -f6)
|
||||
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() {
|
||||
printf '==> %s\n' "$*"
|
||||
@ -103,17 +106,44 @@ run_as_user() {
|
||||
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() {
|
||||
local bundle=${LESAVKA_CLIENT_PKI_BUNDLE:-}
|
||||
local fetched_bundle=0
|
||||
if [[ -z $bundle ]]; 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"
|
||||
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
|
||||
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"
|
||||
local tmp
|
||||
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 0600 -o "$ORIG_USER" -g "$ORIG_USER" "$tmp/client.key" "$CLIENT_PKI_DIR/client.key"
|
||||
sudo rm -rf "$tmp"
|
||||
if [[ $fetched_bundle == 1 ]]; then
|
||||
rm -f "$bundle"
|
||||
fi
|
||||
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 ln -sf /usr/local/bin/lesavka-client /usr/local/bin/lesavka
|
||||
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"
|
||||
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 " Build source: $SRC/target/release/lesavka-client"
|
||||
echo " TLS identity: $CLIENT_PKI_DIR"
|
||||
echo " Captures: $CLIENT_CAPTURE_DIR"
|
||||
echo "✅ Installed version: lesavka-client ${INSTALLED_VERSION:-unknown}${INSTALLED_SHA:+ ($INSTALLED_SHA)}"
|
||||
echo
|
||||
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.key" "$bundle_tmp/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"
|
||||
echo " ↪ client enrollment bundle: $LESAVKA_CLIENT_BUNDLE"
|
||||
}
|
||||
@ -1121,6 +1122,8 @@ fi
|
||||
if [[ -n $PERSISTED_UVC_CODEC ]]; then
|
||||
echo "➡️ UVC codec: ${PERSISTED_UVC_CODEC}"
|
||||
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 "➡️ Logs: sudo journalctl -u lesavka-server -f --no-pager"
|
||||
echo "✅ Installed version: lesavka-server ${INSTALLED_VERSION:-unknown}${INSTALLED_SHA:+ ($INSTALLED_SHA)}"
|
||||
|
||||
@ -10,7 +10,7 @@ bench = false
|
||||
|
||||
[package]
|
||||
name = "lesavka_server"
|
||||
version = "0.16.0"
|
||||
version = "0.16.1"
|
||||
edition = "2024"
|
||||
autobins = false
|
||||
|
||||
|
||||
@ -16,8 +16,14 @@ fn client_install_accepts_server_generated_tls_bundle() {
|
||||
"client.crt",
|
||||
"client.key",
|
||||
"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:",
|
||||
"Captures:",
|
||||
] {
|
||||
assert!(
|
||||
CLIENT_INSTALL.contains(expected),
|
||||
@ -32,4 +38,8 @@ fn client_install_accepts_server_generated_tls_bundle() {
|
||||
CLIENT_INSTALL.contains("0600"),
|
||||
"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(),
|
||||
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!(
|
||||
UI_LAYOUT_SRC
|
||||
.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() {
|
||||
assert!(UI_LAYOUT_SRC.contains("build_panel(\"Relay\")"));
|
||||
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_LABEL_CHARS"), 11);
|
||||
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("relay_grid.attach(&server_entry, 0, 0, 2, 1);"));
|
||||
assert!(UI_LAYOUT_SRC.contains("server_entry.set_width_chars(11);"));
|
||||
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("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("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
|
||||
@ -291,11 +304,16 @@ fn relay_controls_keep_connect_inline_with_server_entry() {
|
||||
.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_heading.set_width_chars(10);"));
|
||||
assert!(
|
||||
UI_LAYOUT_SRC.contains("recovery_heading.set_width_chars(RELAY_SUBGROUP_LABEL_WIDTH);")
|
||||
);
|
||||
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("calibration_buttons.set_column_homogeneous(true);"));
|
||||
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);")
|
||||
);
|
||||
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 usb_recover_button = rail_button(\"USB\""));
|
||||
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!(
|
||||
source_index("let recovery_heading = gtk::Label::new(Some(\"Recover\"));")
|
||||
< source_index(
|
||||
"let calibration_heading = gtk::Label::new(Some(\"AV Upstream\\nCalibration\"));"
|
||||
"let calibration_heading = gtk::Label::new(Some(\"Audio/Video\\nUpstream\\nCalibration\"));"
|
||||
)
|
||||
);
|
||||
assert!(
|
||||
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\"));")
|
||||
);
|
||||
assert!(UI_LAYOUT_SRC.contains("tools_buttons.append(&clipboard_button);"));
|
||||
assert!(!UI_LAYOUT_SRC.contains("Gate Probe"));
|
||||
assert!(UI_LAYOUT_SRC.contains("text.set_ellipsize(pango::EllipsizeMode::End);"));
|
||||
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);")
|
||||
);
|
||||
}
|
||||
|
||||
@ -165,6 +165,10 @@ fn launcher_webcam_quality_selection_reaches_preview_and_relay_env() {
|
||||
#[test]
|
||||
fn launcher_utility_buttons_still_bind_to_live_actions() {
|
||||
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("Start the relay before sending clipboard text."));
|
||||
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."));
|
||||
}
|
||||
|
||||
#[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]
|
||||
fn server_chip_distinguishes_reachable_from_connected() {
|
||||
assert!(UI_RUNTIME_SRC.contains("fn server_light_state("));
|
||||
|
||||
@ -221,6 +221,9 @@ fn server_install_generates_mtls_identity_and_client_bundle() {
|
||||
"extendedKeyUsage = clientAuth",
|
||||
"LESAVKA_CLIENT_BUNDLE",
|
||||
"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_TLS_CLIENT_CA=%s",
|
||||
] {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user