feat: RDP profiles, sessions and viewer
- Protocol::Rdp with RdpConfig (domain, password mode, certificate policy, TLS-only fallback, fixed or tab-sized resolution, SOCKS5/HTTP proxy, SSH tunnel); migration 008 adds rdp_config and drops the protocol CHECK constraint so new protocols no longer need a rebuild. - Session manager: connect_rdp dials directly, via the shared proxy module (proxy password from the secret store) or an SSH tunnel; the server certificate goes through the known-hosts check (key type rdp-tls); a tab-sized desktop uses the tab's reported pixel size, and resizes wait until the server opens Display Control. Rejected NLA passwords count as auth failures, so a saved one is asked again. - The VNC viewer becomes DesktopView for both protocols; a small adapter turns its keysyms and pointer masks into RDP scancodes, Unicode keys, buttons and wheel events. For RDP, Paste Clipboard types the text. - Profile editor RDP section, Quick Connect, badges, docs. Verified against xrdp in the app: logon, desktop, input, and connections through SOCKS5 and HTTP CONNECT relays. Not verified here: Windows/NLA servers, the SSH tunnel route, and Display Control resizing (xrdp 0.9.24 never opened the channel).
This commit is contained in:
+4
-4
@@ -259,7 +259,7 @@ impl App {
|
||||
(Protocol::Raw, Some(p)) => View::Raw(Box::new(crate::raw_view::RawView::new(&info.id, p))),
|
||||
(Protocol::Irc, Some(p)) => View::Irc(Box::new(crate::irc_view::IrcView::new(&info.id, p))),
|
||||
(Protocol::Github, Some(p)) => View::Github(Box::new(crate::github_view::GithubView::new(&info.id, &p.display_name))),
|
||||
(Protocol::Vnc, Some(p)) => View::Vnc(Box::new(crate::vnc_view::VncView::new(&info.id, p))),
|
||||
(Protocol::Vnc | Protocol::Rdp, Some(p)) => View::Desktop(Box::new(crate::desktop_view::DesktopView::new(&info.id, p))),
|
||||
(_, p) => {
|
||||
let (scroll, size, cursor) = match p {
|
||||
Some(p) => (p.terminal_scrollback, p.terminal_font_size, p.terminal_cursor_style.clone()),
|
||||
@@ -522,12 +522,12 @@ impl App {
|
||||
Event::HostKeyVerify { host, key_type, fingerprint, stored, reply, .. } => {
|
||||
self.prompts.push_back(Prompt::HostKey { host, key_type, fingerprint, stored, reply });
|
||||
}
|
||||
Event::VncReady { session } => {
|
||||
if let Some(View::Vnc(v)) = self.s.panes.get_mut(&session).map(|p| &mut p.view) {
|
||||
Event::DesktopReady { session } => {
|
||||
if let Some(View::Desktop(v)) = self.s.panes.get_mut(&session).map(|p| &mut p.view) {
|
||||
v.on_ready();
|
||||
}
|
||||
}
|
||||
Event::VncFrame { .. } => {}
|
||||
Event::DesktopFrame { .. } => {}
|
||||
Event::VncClipboard { text, .. } => self.remote_clipboard(&text),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
//! VNC viewer: the remote framebuffer as a texture, with mouse, keyboard and clipboard input.
|
||||
//! Toolbar after the web build's `vnc.js` (status, Ctrl+Alt+Del, clipboard), plus refresh and a
|
||||
//! scale toggle.
|
||||
//! Remote desktop viewer for VNC and RDP: the remote framebuffer as a texture, with mouse,
|
||||
//! keyboard and clipboard input. Input is expressed in X keysyms and VNC pointer masks (VNC's own
|
||||
//! terms); `Remote` translates them for RDP. Toolbar after the web build's `vnc.js` (status,
|
||||
//! Ctrl+Alt+Del, clipboard), plus refresh and a scale toggle.
|
||||
|
||||
use crate::state::Shared;
|
||||
use crate::theme::{self, Tokens};
|
||||
use crate::widgets;
|
||||
use egui::{Event, Key, Modifiers, Pos2, Rect, RichText, Sense, TextureHandle, Ui, vec2};
|
||||
use egui_phosphor::regular as icon;
|
||||
use nexus_core::clients::rdp::{self, MouseButton, MousePosition, Operation, Scancode, WheelRotations};
|
||||
use nexus_core::clients::vnc::{self, keysym_for};
|
||||
use nexus_core::models::Profile;
|
||||
use nexus_core::models::{Profile, Protocol};
|
||||
use std::sync::Arc;
|
||||
|
||||
const XK_SHIFT: u32 = 0xffe1;
|
||||
@@ -19,12 +21,121 @@ const XK_DELETE: u32 = 0xffff;
|
||||
/// Wheel distance (points) per remote scroll notch for pixel-precise wheels.
|
||||
const NOTCH: f32 = 40.0;
|
||||
|
||||
pub struct VncView {
|
||||
/// The connected client a view drives.
|
||||
#[derive(Clone)]
|
||||
enum Remote {
|
||||
Vnc(Arc<vnc::Client>),
|
||||
Rdp(Arc<rdp::Client>),
|
||||
}
|
||||
|
||||
impl Remote {
|
||||
fn framebuffer(&self) -> Arc<parking_lot::Mutex<vnc::Framebuffer>> {
|
||||
match self {
|
||||
Remote::Vnc(c) => c.framebuffer(),
|
||||
Remote::Rdp(c) => c.framebuffer(),
|
||||
}
|
||||
}
|
||||
|
||||
/// `held` is the modifier keysyms down on the remote side (for RDP shortcuts).
|
||||
fn key(&self, sym: u32, down: bool, held: &[u32]) {
|
||||
match self {
|
||||
Remote::Vnc(c) => {
|
||||
let _ = c.key(sym, down);
|
||||
}
|
||||
Remote::Rdp(c) => {
|
||||
if let Some(op) = rdp_key(sym, down, held.iter().any(|&k| matches!(k, XK_CONTROL | XK_ALT | XK_SUPER))) {
|
||||
c.input(vec![op]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Bits 0–2 left, middle, right; 3/4 a wheel notch up/down (VNC's pointer mask).
|
||||
fn pointer(&self, x: u16, y: u16, mask: u8) {
|
||||
match self {
|
||||
Remote::Vnc(c) => {
|
||||
let _ = c.pointer(x, y, mask);
|
||||
}
|
||||
Remote::Rdp(c) => {
|
||||
let mut ops = vec![Operation::MouseMove(MousePosition { x, y })];
|
||||
for (bit, b) in [(0, MouseButton::Left), (1, MouseButton::Middle), (2, MouseButton::Right)] {
|
||||
ops.push(if mask & 1 << bit != 0 { Operation::MouseButtonPressed(b) } else { Operation::MouseButtonReleased(b) });
|
||||
}
|
||||
for (bit, units) in [(3, 120), (4, -120)] {
|
||||
if mask & 1 << bit != 0 {
|
||||
ops.push(Operation::WheelRotations(WheelRotations { is_vertical: true, rotation_units: units }));
|
||||
}
|
||||
}
|
||||
c.input(ops);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An X keysym as an RDP key operation: named keys (and letters/digits pressed with Ctrl, Alt or
|
||||
/// Win, so shortcuts work) as US-layout scancodes, everything else as a Unicode key.
|
||||
fn rdp_key(sym: u32, down: bool, chord: bool) -> Option<Operation> {
|
||||
let sc = |ext: bool, code: u8| {
|
||||
Some(if down { Operation::KeyPressed(Scancode::from_u8(ext, code)) } else { Operation::KeyReleased(Scancode::from_u8(ext, code)) })
|
||||
};
|
||||
let named = match sym {
|
||||
0xff1b => (false, 0x01),
|
||||
0xff08 => (false, 0x0e),
|
||||
0xff09 => (false, 0x0f),
|
||||
0xff0d => (false, 0x1c),
|
||||
0xffe1 => (false, 0x2a),
|
||||
0xffe2 => (false, 0x36),
|
||||
0xffe3 => (false, 0x1d),
|
||||
0xffe4 => (true, 0x1d),
|
||||
0xffe9 => (false, 0x38),
|
||||
0xffea | 0xfe03 => (true, 0x38),
|
||||
0xffeb => (true, 0x5b),
|
||||
0xffec => (true, 0x5c),
|
||||
0xffe5 => (false, 0x3a),
|
||||
0xff50 => (true, 0x47),
|
||||
0xff51 => (true, 0x4b),
|
||||
0xff52 => (true, 0x48),
|
||||
0xff53 => (true, 0x4d),
|
||||
0xff54 => (true, 0x50),
|
||||
0xff55 => (true, 0x49),
|
||||
0xff56 => (true, 0x51),
|
||||
0xff57 => (true, 0x4f),
|
||||
0xff63 => (true, 0x52),
|
||||
0xffff => (true, 0x53),
|
||||
f @ 0xffbe..=0xffc7 => (false, 0x3b + (f - 0xffbe) as u8),
|
||||
0xffc8 => (false, 0x57),
|
||||
0xffc9 => (false, 0x58),
|
||||
_ => (false, 0),
|
||||
};
|
||||
if named.1 != 0 {
|
||||
return sc(named.0, named.1);
|
||||
}
|
||||
let ch = char::from_u32(if sym >= 0x0100_0000 { sym - 0x0100_0000 } else { sym })?;
|
||||
if chord {
|
||||
const LETTERS: &[u8; 26] = b"\x1e\x30\x2e\x20\x12\x21\x22\x23\x17\x24\x25\x26\x32\x31\x18\x19\x10\x13\x1f\x14\x16\x2f\x11\x2d\x15\x2c";
|
||||
let lower = ch.to_ascii_lowercase();
|
||||
match lower {
|
||||
'a'..='z' => return sc(false, LETTERS[(lower as u8 - b'a') as usize]),
|
||||
'1'..='9' => return sc(false, lower as u8 - b'1' + 0x02),
|
||||
'0' => return sc(false, 0x0b),
|
||||
' ' => return sc(false, 0x39),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Some(if down { Operation::UnicodeKeyPressed(ch) } else { Operation::UnicodeKeyReleased(ch) })
|
||||
}
|
||||
|
||||
pub struct DesktopView {
|
||||
pub session: String,
|
||||
protocol: Protocol,
|
||||
view_only: bool,
|
||||
scale_to_fit: bool,
|
||||
/// RDP: ask the server to match the tab's size.
|
||||
dynamic_resolution: bool,
|
||||
/// Last tab size reported (pixels) and when it changed, for debounced RDP resizes.
|
||||
reported: ([u32; 2], [u32; 2], f64),
|
||||
ready: bool,
|
||||
client: Option<Arc<vnc::Client>>,
|
||||
client: Option<Remote>,
|
||||
texture: Option<TextureHandle>,
|
||||
fb_size: [usize; 2],
|
||||
name: String,
|
||||
@@ -105,13 +216,20 @@ fn to_remote(p: Pos2, img: Rect, fb: [usize; 2]) -> (u16, u16) {
|
||||
(x as u16, y as u16)
|
||||
}
|
||||
|
||||
impl VncView {
|
||||
pub fn new(session: &str, profile: &Profile) -> VncView {
|
||||
let cfg = profile.vnc.clone().unwrap_or_default();
|
||||
VncView {
|
||||
impl DesktopView {
|
||||
pub fn new(session: &str, profile: &Profile) -> DesktopView {
|
||||
let (view_only, scale_to_fit, dynamic_resolution) = match (&profile.vnc, &profile.rdp) {
|
||||
(Some(v), _) if profile.protocol == Protocol::Vnc => (v.view_only, v.scale_to_fit, false),
|
||||
(_, Some(r)) => (false, r.scale_to_fit, r.dynamic_resolution && r.width == 0),
|
||||
_ => (false, true, false),
|
||||
};
|
||||
DesktopView {
|
||||
session: session.into(),
|
||||
view_only: cfg.view_only,
|
||||
scale_to_fit: cfg.scale_to_fit,
|
||||
protocol: profile.protocol,
|
||||
view_only,
|
||||
scale_to_fit,
|
||||
dynamic_resolution,
|
||||
reported: ([0, 0], [0, 0], 0.0),
|
||||
ready: false,
|
||||
client: None,
|
||||
texture: None,
|
||||
@@ -125,7 +243,7 @@ impl VncView {
|
||||
}
|
||||
}
|
||||
|
||||
/// Called on `Event::VncReady` (the client is connected; fetch it via `s.svc.sessions.vnc(id)`).
|
||||
/// Called on `Event::DesktopReady` (the client is connected; the view fetches it next frame).
|
||||
pub fn on_ready(&mut self) {
|
||||
self.ready = true;
|
||||
self.client = None;
|
||||
@@ -133,7 +251,7 @@ impl VncView {
|
||||
|
||||
fn key(&self, sym: u32, down: bool) {
|
||||
if let Some(c) = &self.client {
|
||||
let _ = c.key(sym, down);
|
||||
c.key(sym, down, &self.held);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,23 +279,57 @@ impl VncView {
|
||||
if self.buttons != 0
|
||||
&& let Some(c) = &self.client
|
||||
{
|
||||
let _ = c.pointer(self.last_pos.0, self.last_pos.1, 0);
|
||||
c.pointer(self.last_pos.0, self.last_pos.1, 0);
|
||||
}
|
||||
self.buttons = 0;
|
||||
}
|
||||
|
||||
/// VNC: sets the remote clipboard. RDP (no clipboard channel yet): types the text.
|
||||
fn paste(&self, s: &mut Shared, text: &str) {
|
||||
if let Some(c) = &self.client
|
||||
&& let Err(e) = c.send_clipboard(text)
|
||||
{
|
||||
s.error(format!("Clipboard: {e}"));
|
||||
match &self.client {
|
||||
Some(Remote::Vnc(c)) => {
|
||||
if let Err(e) = c.send_clipboard(text) {
|
||||
s.error(format!("Clipboard: {e}"));
|
||||
}
|
||||
}
|
||||
Some(Remote::Rdp(c)) => c.input(
|
||||
text.chars()
|
||||
.filter(|c| !c.is_control() || *c == '\n')
|
||||
.flat_map(|ch| [Operation::UnicodeKeyPressed(ch), Operation::UnicodeKeyReleased(ch)])
|
||||
.collect(),
|
||||
),
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// RDP with dynamic resolution: report the tab's pixel size once it has settled.
|
||||
fn report_size(&mut self, ui: &Ui, s: &Shared, area: Rect) {
|
||||
if self.protocol != Protocol::Rdp || !self.dynamic_resolution {
|
||||
return;
|
||||
}
|
||||
let ppp = ui.ctx().pixels_per_point();
|
||||
let size = [(area.width() * ppp) as u32 & !1, (area.height() * ppp) as u32];
|
||||
let now = ui.input(|i| i.time);
|
||||
if size != self.reported.1 {
|
||||
self.reported.1 = size;
|
||||
self.reported.2 = now;
|
||||
}
|
||||
// ponytail: 0.4 s debounce; each resize is a server-side desktop reconfiguration.
|
||||
if size != self.reported.0 && now - self.reported.2 > 0.4 && size[0] >= 200 && size[1] >= 200 {
|
||||
self.reported.0 = size;
|
||||
let _ = s.svc.sessions.resize(&self.session, size[0].min(8192) as u16, size[1].min(8192) as u16);
|
||||
} else if size != self.reported.0 {
|
||||
ui.ctx().request_repaint_after(std::time::Duration::from_millis(450));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, ui: &mut Ui, s: &mut Shared) {
|
||||
if self.ready && self.client.is_none() {
|
||||
self.client = s.svc.sessions.vnc(&self.session).ok();
|
||||
if let Some(c) = &self.client {
|
||||
self.client = match self.protocol {
|
||||
Protocol::Rdp => s.svc.sessions.rdp(&self.session).ok().map(Remote::Rdp),
|
||||
_ => s.svc.sessions.vnc(&self.session).ok().map(Remote::Vnc),
|
||||
};
|
||||
if let Some(Remote::Vnc(c)) = &self.client {
|
||||
self.name = c.desktop_name();
|
||||
}
|
||||
}
|
||||
@@ -186,6 +338,7 @@ impl VncView {
|
||||
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let area = ui.available_rect_before_wrap();
|
||||
self.report_size(ui, s, area);
|
||||
ui.painter().rect_filled(area, 0.0, t.canvas);
|
||||
let Some(tex) = self.texture.clone().filter(|_| self.ready) else {
|
||||
ui.allocate_rect(area, Sense::hover());
|
||||
@@ -206,7 +359,7 @@ impl VncView {
|
||||
self.draw(ui, s, &tex, img);
|
||||
} else {
|
||||
let mut c = ui.new_child(egui::UiBuilder::new().max_rect(area));
|
||||
egui::ScrollArea::both().id_salt(("vnc", &self.session)).auto_shrink([false, false]).show(&mut c, |ui| {
|
||||
egui::ScrollArea::both().id_salt(("desktop", &self.session)).auto_shrink([false, false]).show(&mut c, |ui| {
|
||||
let (img, _) = ui.allocate_exact_size(size, Sense::hover());
|
||||
self.draw(ui, s, &tex, img);
|
||||
});
|
||||
@@ -230,7 +383,7 @@ impl VncView {
|
||||
self.fb_size = size;
|
||||
match &mut self.texture {
|
||||
Some(t) => t.set(img, egui::TextureOptions::LINEAR),
|
||||
None => self.texture = Some(ui.ctx().load_texture(format!("vnc-{}", self.session), img, egui::TextureOptions::LINEAR)),
|
||||
None => self.texture = Some(ui.ctx().load_texture(format!("desktop-{}", self.session), img, egui::TextureOptions::LINEAR)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,16 +415,18 @@ impl VncView {
|
||||
if widgets::icon_button(ui, g, tip).clicked() {
|
||||
self.scale_to_fit = !self.scale_to_fit;
|
||||
}
|
||||
if widgets::icon_button(ui, icon::ARROW_CLOCKWISE, "Refresh the screen").clicked()
|
||||
&& let Some(c) = &self.client
|
||||
if let Some(Remote::Vnc(c)) = &self.client
|
||||
&& widgets::icon_button(ui, icon::ARROW_CLOCKWISE, "Refresh the screen").clicked()
|
||||
{
|
||||
let _ = c.request_full_update();
|
||||
}
|
||||
if !self.view_only {
|
||||
if widgets::button(ui, icon::CLIPBOARD_TEXT, "Paste Clipboard")
|
||||
.on_hover_text("Send the local clipboard to the remote desktop")
|
||||
.clicked()
|
||||
{
|
||||
let tip = if self.protocol == Protocol::Rdp {
|
||||
"Type the local clipboard's text into the remote desktop"
|
||||
} else {
|
||||
"Send the local clipboard to the remote desktop"
|
||||
};
|
||||
if widgets::button(ui, icon::CLIPBOARD_TEXT, "Paste Clipboard").on_hover_text(tip).clicked() {
|
||||
match arboard::Clipboard::new().and_then(|mut c| c.get_text()) {
|
||||
Ok(text) if !text.is_empty() => {
|
||||
self.paste(s, &text);
|
||||
@@ -292,7 +447,7 @@ impl VncView {
|
||||
|
||||
fn draw(&mut self, ui: &mut Ui, s: &mut Shared, tex: &TextureHandle, img: Rect) {
|
||||
ui.painter().image(tex.id(), img, Rect::from_min_max(Pos2::ZERO, Pos2::new(1.0, 1.0)), egui::Color32::WHITE);
|
||||
let id = egui::Id::new(("vnc-input", &self.session));
|
||||
let id = egui::Id::new(("desktop-input", &self.session));
|
||||
let resp = ui.interact(img, id, Sense::click_and_drag());
|
||||
if resp.clicked() || resp.drag_started() || resp.secondary_clicked() || resp.middle_clicked() {
|
||||
resp.request_focus();
|
||||
@@ -328,7 +483,7 @@ impl VncView {
|
||||
let buttons = if resp.hovered() || resp.dragged() || self.buttons != 0 { button_mask(left, middle, right) } else { 0 };
|
||||
let at = to_remote(pos, img, self.fb_size);
|
||||
if at != self.last_pos || buttons != self.buttons {
|
||||
let _ = c.pointer(at.0, at.1, buttons);
|
||||
c.pointer(at.0, at.1, buttons);
|
||||
self.last_pos = at;
|
||||
self.buttons = buttons;
|
||||
}
|
||||
@@ -336,8 +491,8 @@ impl VncView {
|
||||
self.wheel += scroll;
|
||||
while self.wheel.abs() >= 1.0 {
|
||||
let bit = if self.wheel > 0.0 { 1 << 3 } else { 1 << 4 };
|
||||
let _ = c.pointer(at.0, at.1, buttons | bit);
|
||||
let _ = c.pointer(at.0, at.1, buttons);
|
||||
c.pointer(at.0, at.1, buttons | bit);
|
||||
c.pointer(at.0, at.1, buttons);
|
||||
self.wheel -= self.wheel.signum();
|
||||
}
|
||||
}
|
||||
@@ -394,6 +549,20 @@ mod tests {
|
||||
assert_eq!(modifier_syms(Modifiers::CTRL | Modifiers::SHIFT), vec![XK_SHIFT, XK_CONTROL]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keysyms_map_to_rdp_input() {
|
||||
let sc = |ext, code| format!("{:?}", Some(Operation::KeyPressed(Scancode::from_u8(ext, code))));
|
||||
let k = |sym, down, chord| format!("{:?}", rdp_key(sym, down, chord));
|
||||
assert_eq!(k(0xff0d, true, false), sc(false, 0x1c), "Enter");
|
||||
assert_eq!(k(0xff51, true, false), sc(true, 0x4b), "Left is extended");
|
||||
assert_eq!(k(0xffc9, true, false), sc(false, 0x58), "F12");
|
||||
assert_eq!(k(XK_DELETE, true, false), sc(true, 0x53));
|
||||
assert_eq!(k(u32::from(b'c'), true, false), format!("{:?}", Some(Operation::UnicodeKeyPressed('c'))), "typed text is Unicode");
|
||||
assert_eq!(k(u32::from(b'c'), true, true), sc(false, 0x2e), "Ctrl+C sends the C scancode");
|
||||
assert_eq!(k(u32::from(b'1'), false, true), format!("{:?}", Some(Operation::KeyReleased(Scancode::from_u8(false, 0x02)))));
|
||||
assert_eq!(k(0x0100_20ac, true, false), format!("{:?}", Some(Operation::UnicodeKeyPressed('€'))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pointer_mapping() {
|
||||
assert_eq!(button_mask(true, false, true), 0b101);
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
mod app;
|
||||
mod chrome;
|
||||
mod desktop_view;
|
||||
mod forwards;
|
||||
mod github_view;
|
||||
mod irc_view;
|
||||
@@ -25,7 +26,6 @@ mod theme;
|
||||
mod transfer;
|
||||
mod triggers;
|
||||
mod views;
|
||||
mod vnc_view;
|
||||
mod widgets;
|
||||
|
||||
use nexus_core::config::AppConfig;
|
||||
|
||||
@@ -156,6 +156,9 @@ impl ProfileEditor {
|
||||
if proto != Protocol::Vnc {
|
||||
p.vnc = None;
|
||||
}
|
||||
if proto != Protocol::Rdp {
|
||||
p.rdp = None;
|
||||
}
|
||||
if proto != Protocol::Irc {
|
||||
p.irc = None;
|
||||
}
|
||||
@@ -223,6 +226,7 @@ impl ProfileEditor {
|
||||
Protocol::Raw => self.raw(ui, &errs),
|
||||
Protocol::Github => self.github(ui, &errs),
|
||||
Protocol::Vnc => self.vnc(ui, &errs),
|
||||
Protocol::Rdp => self.rdp(ui, &errs),
|
||||
Protocol::Irc => self.irc(ui, &errs),
|
||||
Protocol::Serial => self.serial(ui, &errs),
|
||||
Protocol::Local => {}
|
||||
@@ -678,6 +682,67 @@ impl ProfileEditor {
|
||||
});
|
||||
}
|
||||
|
||||
fn rdp(&mut self, ui: &mut Ui, errs: &[ValidationError]) {
|
||||
let Self { p, keys, key_checks, secrets, .. } = self;
|
||||
let c = p.rdp.get_or_insert_with(RdpConfig::defaults);
|
||||
section(ui, "RDP", |ui| {
|
||||
text(ui, "Domain", &mut c.domain, "Optional (e.g. CORP)");
|
||||
mode(ui, "Password Storage", &mut c.secret_mode);
|
||||
if c.secret_mode == SecretMode::Saved {
|
||||
secret(ui, "Password", secrets, SecretKind::RdpPassword, "Leave blank to keep the saved password");
|
||||
}
|
||||
let opts: Vec<_> = HostKeyPolicy::ALL.iter().map(|k| (*k, k.label())).collect();
|
||||
combo(ui, "Certificate Policy", &mut c.host_key_policy, &opts);
|
||||
check(
|
||||
ui,
|
||||
"Allow servers without NLA",
|
||||
&mut c.allow_tls_only,
|
||||
"Plain TLS logon, e.g. xrdp. Less secure: the session is set up before you authenticate.",
|
||||
);
|
||||
});
|
||||
section(ui, "Display", |ui| {
|
||||
check(ui, "Scale to Fit", &mut c.scale_to_fit, "");
|
||||
num(ui, "Width (0 = fit tab)", &mut c.width, 0..=8192);
|
||||
err(ui, errs, "rdp.width");
|
||||
num(ui, "Height (0 = fit tab)", &mut c.height, 0..=8192);
|
||||
err(ui, errs, "rdp.height");
|
||||
if c.width == 0 {
|
||||
check(ui, "Resize With Tab", &mut c.dynamic_resolution, "Ask the server to match the tab's size as it changes");
|
||||
}
|
||||
});
|
||||
section(ui, "Proxy", |ui| {
|
||||
combo_s(ui, "Proxy Type", &mut c.proxy_type, &[("none", "None (direct connection)"), ("socks5", "SOCKS5"), ("http", "HTTP CONNECT")]);
|
||||
err(ui, errs, "rdp.proxyType");
|
||||
if c.proxy_type != "none" {
|
||||
text(ui, "Proxy Host", &mut c.proxy_host, "proxy.example.com");
|
||||
err(ui, errs, "rdp.proxyHost");
|
||||
num(ui, "Proxy Port", &mut c.proxy_port, 0..=65535);
|
||||
text(ui, "Proxy Username", &mut c.proxy_username, "Optional");
|
||||
secret(ui, "Proxy Password", secrets, SecretKind::ProxyPassword, "Optional; leave blank to keep the saved password");
|
||||
}
|
||||
});
|
||||
section(ui, "SSH Tunnel", |ui| {
|
||||
tunnel(
|
||||
ui,
|
||||
"rdp",
|
||||
Tunnel {
|
||||
on: &mut c.ssh_tunnel,
|
||||
host: &mut c.ssh_host,
|
||||
port: &mut c.ssh_port,
|
||||
user: &mut c.ssh_username,
|
||||
auth: &mut c.ssh_auth_method,
|
||||
key: &mut c.ssh_private_key,
|
||||
agent: &mut c.ssh_agent_auth,
|
||||
mode: &mut c.ssh_secret_mode,
|
||||
},
|
||||
keys,
|
||||
key_checks,
|
||||
secrets,
|
||||
errs,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn irc(&mut self, ui: &mut Ui, errs: &[ValidationError]) {
|
||||
let Self { p, keys, key_checks, secrets, rooms, .. } = self;
|
||||
let port = &mut p.port;
|
||||
@@ -1171,6 +1236,8 @@ fn tunnel(ui: &mut Ui, kind: &str, c: Tunnel, keys: &[String], checks: &mut KeyC
|
||||
let (l, k) = match (kind, c.auth.as_str()) {
|
||||
("vnc", "password") => ("SSH Password", SecretKind::VncSshPassword),
|
||||
("vnc", _) => ("SSH Key Passphrase", SecretKind::VncSshPassphrase),
|
||||
("rdp", "password") => ("SSH Password", SecretKind::RdpSshPassword),
|
||||
("rdp", _) => ("SSH Key Passphrase", SecretKind::RdpSshPassphrase),
|
||||
(_, "password") => ("SSH Password", SecretKind::IrcSshPassword),
|
||||
_ => ("SSH Key Passphrase", SecretKind::IrcSshPassphrase),
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ use egui_phosphor::regular as icon;
|
||||
use nexus_core::clients::serial;
|
||||
use nexus_core::models::{Profile, Protocol};
|
||||
|
||||
const PROTOCOLS: [Protocol; 5] = [Protocol::Ssh, Protocol::Telnet, Protocol::Raw, Protocol::Irc, Protocol::Serial];
|
||||
const PROTOCOLS: [Protocol; 6] = [Protocol::Ssh, Protocol::Rdp, Protocol::Telnet, Protocol::Raw, Protocol::Irc, Protocol::Serial];
|
||||
|
||||
pub struct QuickConnect {
|
||||
open: bool,
|
||||
@@ -67,7 +67,7 @@ impl QuickConnect {
|
||||
if self.protocol != Protocol::Serial {
|
||||
p.port = self.port;
|
||||
}
|
||||
if matches!(self.protocol, Protocol::Ssh | Protocol::Irc) {
|
||||
if matches!(self.protocol, Protocol::Ssh | Protocol::Rdp | Protocol::Irc) {
|
||||
p.username = self.username.trim().into();
|
||||
}
|
||||
if let Some(c) = &mut p.irc
|
||||
@@ -134,7 +134,7 @@ impl QuickConnect {
|
||||
label(ui, "Port");
|
||||
ui.add(DragValue::new(&mut self.port).range(1..=65535).speed(0.25));
|
||||
}
|
||||
if matches!(self.protocol, Protocol::Ssh | Protocol::Irc) {
|
||||
if matches!(self.protocol, Protocol::Ssh | Protocol::Rdp | Protocol::Irc) {
|
||||
label(ui, "Username");
|
||||
ui.add(TextEdit::singleline(&mut self.username).hint_text("root").desired_width(f32::INFINITY));
|
||||
}
|
||||
|
||||
@@ -339,7 +339,7 @@ pub fn pane(app: &mut App, ui: &mut Ui, id: &str, rect: Rect) {
|
||||
View::Raw(v) => v.ui(ui, &mut app.s),
|
||||
View::Irc(v) => v.ui(ui, &mut app.s),
|
||||
View::Github(v) => v.ui(ui, &mut app.s),
|
||||
View::Vnc(v) => v.ui(ui, &mut app.s),
|
||||
View::Desktop(v) => v.ui(ui, &mut app.s),
|
||||
}
|
||||
let state = app.s.session_state(id).unwrap_or(pane.info.state);
|
||||
let local_exited = pane.local_exited;
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
//! How a session is drawn. Terminal-style protocols share `TermView`; the rest have their own.
|
||||
|
||||
use crate::desktop_view::DesktopView;
|
||||
use crate::github_view::GithubView;
|
||||
use crate::irc_view::IrcView;
|
||||
use crate::raw_view::RawView;
|
||||
use crate::terminal::TermView;
|
||||
use crate::vnc_view::VncView;
|
||||
|
||||
pub enum View {
|
||||
Terminal(Box<TermView>),
|
||||
Raw(Box<RawView>),
|
||||
Irc(Box<IrcView>),
|
||||
Github(Box<GithubView>),
|
||||
Vnc(Box<VncView>),
|
||||
/// VNC or RDP.
|
||||
Desktop(Box<DesktopView>),
|
||||
}
|
||||
|
||||
/// Protocol badge colour (the web build's --protocol-* tokens).
|
||||
@@ -24,6 +25,7 @@ pub fn protocol_color(p: nexus_core::models::Protocol) -> egui::Color32 {
|
||||
Raw => rgb(0xab, 0x47, 0xbc),
|
||||
Github => rgb(0x7c, 0x8a, 0xff),
|
||||
Vnc => rgb(0xe8, 0x5d, 0x4a),
|
||||
Rdp => rgb(0x26, 0xa6, 0x9a),
|
||||
Irc => rgb(0xd8, 0xb2, 0x4c),
|
||||
Local => rgb(0x8b, 0xc3, 0x4a),
|
||||
Serial => rgb(0x4f, 0xa3, 0xe0),
|
||||
@@ -39,6 +41,7 @@ pub fn protocol_badge(p: nexus_core::models::Protocol) -> &'static str {
|
||||
Raw => "RAW",
|
||||
Github => "GitHub",
|
||||
Vnc => "VNC",
|
||||
Rdp => "RDP",
|
||||
Irc => "IRC",
|
||||
Local => "LOCAL",
|
||||
Serial => "SERIAL",
|
||||
|
||||
Reference in New Issue
Block a user