use serde::Deserialize;
use std::time::Duration;
use rmux_proto::{AttachedWindowsConsoleKey, ResizePaneAdjustment, SplitDirection, TerminalSize};
#[cfg(windows)]
use rmux_pty::WindowsConsoleKeyEvent;
use crate::input_keys::{encode_key, ExtendedKeyFormat};
use crate::keys::parse_key_code;
use crate::web::crypto::E2EE_CAPABILITY;
mod handshake;
mod inbound;
mod outbound;
#[cfg(test)]
mod tests;
pub(crate) use handshake::{
build_challenge, close_for_auth_error, read_auth_message, read_client_hello, send_text,
};
pub(crate) use inbound::{
handle_pane_client_text, handle_pane_operator_binary_frame, handle_session_client_text,
handle_session_operator_binary_frame,
};
pub(crate) use outbound::{
queue_output, queue_session_keyframe, queue_session_pane_frame, queue_session_view,
queue_snapshot, send_ready, send_revoked, send_viewer_count,
};
pub(crate) const PRE_AUTH_TIMEOUT: Duration = Duration::from_secs(5);
pub(crate) const AUTH_FRAME_TIMEOUT: Duration = Duration::from_secs(2);
pub(crate) const UNIFORM_AUTH_DELAY: Duration = Duration::from_millis(50);
pub(crate) const WEB_SHARE_PROTOCOL_VERSION: u16 = 1;
pub(crate) const HANDSHAKE_REJECTED: (u16, &str) = (4000, "handshake_rejected");
pub(crate) const PANE_FRAME_CAPABILITY: &str = "pane-frame-v1";
pub(crate) const PANE_RECOVERY_COVERAGE_CAPABILITY: &str = "pane-recovery-coverage-v1";
const SERVER_CAPABILITIES: &[&str] = &[
E2EE_CAPABILITY,
"terminal-palette-v1",
PANE_FRAME_CAPABILITY,
PANE_RECOVERY_COVERAGE_CAPABILITY,
];
const OPERATOR_INPUT_FRAME_MAX: usize = 4 * 1024;
const MAX_SESSION_RESIZE_DIMENSION: u16 = 4096;
const MAX_SESSION_RESIZE_CELLS: u32 = 1_000_000;
const MAX_PANE_RESIZE_CELLS: u16 = 10_000;
const WS_OUTPUT_RAW: u8 = 0x01;
const WS_RESIZE_NOTIFY: u8 = 0x02;
const WS_SNAPSHOT_FULL: u8 = 0x10;
const WS_SESSION_VIEW: u8 = 0x11;
const WS_SESSION_PANE_FRAME: u8 = 0x12;
const WS_PANE_RECOVERY_SNAPSHOT: u8 = 0x13;
const WS_INPUT_TEXT: u8 = 0x80;
const WS_INPUT_KEY: u8 = 0x81;
const WS_RESIZE_REQUEST: u8 = 0x82;
const WS_ATTACH_INPUT: u8 = 0x83;
const WS_SESSION_RESIZE_PANE: u8 = 0x84;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct SessionScrollRequest {
pub(crate) pane_id: u32,
pub(crate) delta: i32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SessionClientTextOutcome {
None,
Scroll(SessionScrollRequest),
Snapshot,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SessionOperatorBinaryOutcome {
None,
Resize,
Snapshot,
}
#[derive(Debug)]
pub(crate) struct AuthMessage {
pub(crate) pin: Option<String>,
pub(crate) supports_session_pane_frame: bool,
pub(crate) supports_pane_recovery_coverage: bool,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct AuthWireMessage {
#[serde(rename = "type")]
kind: String,
protocol_version: u16,
capabilities: Vec<String>,
#[serde(default)]
pin: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
#[serde(tag = "type", rename_all = "snake_case")]
enum ClientMessage {
Logout,
PaneScroll { pane_id: u32, delta: i32 },
SelectPane { pane_id: u32 },
SplitPane { direction: ClientSplitDirection },
NewWindow,
KillPane,
SelectWindow { window_index: u32 },
RenameWindow { window_index: u32, name: String },
KillWindow { window_index: u32 },
}
#[derive(Debug, Clone, Copy, Deserialize)]
#[serde(rename_all = "snake_case")]
enum ClientSplitDirection {
Horizontal,
Vertical,
}
impl From<ClientSplitDirection> for SplitDirection {
fn from(value: ClientSplitDirection) -> Self {
match value {
ClientSplitDirection::Horizontal => Self::Horizontal,
ClientSplitDirection::Vertical => Self::Vertical,
}
}
}
fn parse_resize_body(body: &[u8]) -> Option<TerminalSize> {
if body.len() != 4 {
return None;
}
let cols = u16::from_be_bytes([body[0], body[1]]);
let rows = u16::from_be_bytes([body[2], body[3]]);
is_valid_session_resize(cols, rows).then_some(TerminalSize { cols, rows })
}
fn is_valid_session_resize(cols: u16, rows: u16) -> bool {
cols > 0
&& rows > 0
&& cols <= MAX_SESSION_RESIZE_DIMENSION
&& rows <= MAX_SESSION_RESIZE_DIMENSION
&& u32::from(cols).saturating_mul(u32::from(rows)) <= MAX_SESSION_RESIZE_CELLS
}
fn parse_pane_resize_body(body: &[u8]) -> Option<(u32, ResizePaneAdjustment)> {
if body.len() != 7 {
return None;
}
let pane_id = u32::from_be_bytes([body[0], body[1], body[2], body[3]]);
let cells = u16::from_be_bytes([body[5], body[6]]);
if cells == 0 || cells > MAX_PANE_RESIZE_CELLS {
return None;
}
let adjustment = match body[4] {
0 => ResizePaneAdjustment::Left { cells },
1 => ResizePaneAdjustment::Right { cells },
2 => ResizePaneAdjustment::Up { cells },
3 => ResizePaneAdjustment::Down { cells },
_ => return None,
};
Some((pane_id, adjustment))
}
fn session_logout_allowed(is_operator: bool, controls: bool) -> bool {
is_operator && controls
}
fn valid_window_name(name: &str) -> bool {
!name.is_empty() && name.len() <= 128 && !name.chars().any(char::is_control)
}
fn encode_session_key(token: &str) -> Option<Vec<u8>> {
let key = parse_key_code(token)?;
encode_key(0, ExtendedKeyFormat::Xterm, key)
}
fn encode_session_windows_console_key(token: &str) -> Option<(Vec<u8>, AttachedWindowsConsoleKey)> {
#[cfg(not(windows))]
{
let _ = token;
None
}
#[cfg(windows)]
{
let key = rmux_core::key_code_lookup_bits(parse_key_code(token)?);
let (bytes, key_event) = session_windows_ctrl_letter_for_key(key)?;
Some((bytes, key_event))
}
}
#[cfg(windows)]
fn session_windows_ctrl_letter_for_key(
key: rmux_core::KeyCode,
) -> Option<(Vec<u8>, AttachedWindowsConsoleKey)> {
for letter in b'A'..=b'Z' {
let name = format!("C-{}", char::from(letter.to_ascii_lowercase()));
if Some(key)
== rmux_core::key_string_lookup_string(&name).map(rmux_core::key_code_lookup_bits)
{
let event = WindowsConsoleKeyEvent::ctrl_letter(letter)?;
return Some((
vec![letter - b'A' + 1],
AttachedWindowsConsoleKey::new(
event.virtual_key_code(),
event.virtual_scan_code(),
event.unicode_char(),
event.control_key_state(),
event.repeat_count(),
),
));
}
}
None
}