diff --git a/crates/ericrfb/examples/handshake.rs b/crates/ericrfb/examples/handshake.rs new file mode 100644 index 0000000..8f0e63a --- /dev/null +++ b/crates/ericrfb/examples/handshake.rs @@ -0,0 +1,47 @@ +use std::env; + +use ericrfb::handshake::{Config, connect}; + +fn main() { + let args: Vec = env::args().collect(); + + let host = args + .iter() + .position(|a| a == "--host") + .and_then(|i| args.get(i + 1)) + .expect("usage: --host --applet-id [--port ]"); + + let applet_id = args + .iter() + .position(|a| a == "--applet-id") + .and_then(|i| args.get(i + 1)) + .expect("usage: --host --applet-id [--port ]"); + + let port: u16 = args + .iter() + .position(|a| a == "--port") + .and_then(|i| args.get(i + 1)) + .and_then(|s| s.parse().ok()) + .unwrap_or(443); + + let cfg = Config::new(host, port, applet_id); + println!("Connecting to {}:{}...", cfg.host, cfg.port); + + match connect(&cfg) { + Ok(session) => { + println!( + "Connected: name={:?}, {}x{}, version={}.{}, format={}", + session.server_name, + session.width(), + session.height(), + session.server_version.0, + session.server_version.1, + session.pixel_format.label, + ); + } + Err(e) => { + eprintln!("Handshake failed: {e}"); + std::process::exit(1); + } + } +} diff --git a/crates/ericrfb/src/handshake.rs b/crates/ericrfb/src/handshake.rs new file mode 100644 index 0000000..58f80ac --- /dev/null +++ b/crates/ericrfb/src/handshake.rs @@ -0,0 +1,254 @@ +use std::io::{BufReader, BufWriter, Read, Write}; +use std::net::TcpStream; + +use thiserror::Error; + +use crate::proto::{self, read_exact, read_i32_be, read_modified_utf8, read_u8, read_u16_be}; + +// --------------------------------------------------------------------------- +// Errors +// --------------------------------------------------------------------------- + +#[derive(Debug, Error)] +pub enum HandshakeError { + #[error("protocol error: {0}")] + Protocol(#[from] proto::ProtoError), + #[error("i/o error: {0}")] + Io(#[from] std::io::Error), + #[error("auth rejected: {0}")] + AuthRejected(String), + #[error("invalid server banner")] + InvalidBanner, +} + +/// Map error status codes from aw.a(int), line 350. +fn auth_error_message(code: i32) -> &'static str { + match code { + 1 => "no permission", + 2 => "exclusive access active", + 3 => "manually rejected", + 4 => "server password disabled", + 5 => "loopback connection is senseless", + 6 => "authentication failed", + 7 => "access to this kvm port denied", + _ => "unknown error", + } +} + +// --------------------------------------------------------------------------- +// Config +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone)] +pub struct Config { + pub host: String, + pub port: u16, + pub applet_id: String, + pub protocol_version: String, + pub port_id: u8, + pub shared: bool, +} + +impl Config { + pub fn new(host: impl Into, port: u16, applet_id: impl Into) -> Self { + Self { + host: host.into(), + port, + applet_id: applet_id.into(), + protocol_version: "01.11".into(), + port_id: 0, + shared: true, + } + } +} + +// --------------------------------------------------------------------------- +// Pixel format (from aw.i(), line 519) +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone)] +pub struct PixelFormat { + pub exclusive: bool, + pub color_depth: u16, + pub label: String, +} + +fn read_pixel_format(r: &mut impl Read) -> Result { + let flag = read_u8(r)?; + let color_depth = read_u16_be(r)?; + let label_len = read_u16_be(r)? as usize; + let label_bytes = read_exact(r, label_len)?; + let label = String::from_utf8_lossy(&label_bytes).into_owned(); + Ok(PixelFormat { + exclusive: flag == 1, + color_depth, + label, + }) +} + +// --------------------------------------------------------------------------- +// ServerInit (from aw.k(), line 435) +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone)] +pub struct ServerInit { + pub supports_resize: bool, + pub width: u16, + pub height: u16, + pub bits_per_pixel: u8, + pub depth: u8, + pub big_endian: bool, + pub true_color: bool, + pub red_max: u16, + pub green_max: u16, + pub blue_max: u16, + pub red_shift: u8, + pub green_shift: u8, + pub blue_shift: u8, +} + +fn read_server_init(r: &mut impl Read) -> Result { + let supports_resize = read_u8(r)? != 0; + let width = read_u16_be(r)?; + let height = read_u16_be(r)?; + let bits_per_pixel = read_u8(r)?; + let depth = read_u8(r)?; + let big_endian = read_u8(r)? != 0; + let true_color = read_u8(r)? != 0; + let red_max = read_u16_be(r)?; + let green_max = read_u16_be(r)?; + let blue_max = read_u16_be(r)?; + let red_shift = read_u8(r)?; + let green_shift = read_u8(r)?; + let blue_shift = read_u8(r)?; + let _pad = read_exact(r, 3)?; + Ok(ServerInit { + supports_resize, + width, + height, + bits_per_pixel, + depth, + big_endian, + true_color, + red_max, + green_max, + blue_max, + red_shift, + green_shift, + blue_shift, + }) +} + +// --------------------------------------------------------------------------- +// Session — returned after successful handshake +// --------------------------------------------------------------------------- + +#[derive(Debug)] +pub struct Session { + pub server_version: (u8, u8), + pub server_name: String, + pub pixel_format: PixelFormat, + pub server_init: ServerInit, + pub reader: BufReader, + pub writer: BufWriter, +} + +impl Session { + pub fn width(&self) -> u16 { + self.server_init.width + } + + pub fn height(&self) -> u16 { + self.server_init.height + } +} + +// --------------------------------------------------------------------------- +// Handshake — aw.g(), line 226, steps 1–11 +// --------------------------------------------------------------------------- + +pub fn connect(cfg: &Config) -> Result { + let addr = format!("{}:{}", cfg.host, cfg.port); + let stream = TcpStream::connect(&addr)?; + let read_stream = stream.try_clone()?; + let mut r = BufReader::with_capacity(32768, read_stream); + let mut w = BufWriter::new(stream); + + // Step 1: C→S 75 bytes auth string, zero-padded, ISO-8859-1 + let auth_str = format!("e-RIC AUTH={}", cfg.applet_id); + let auth_bytes = auth_str.as_bytes(); + let mut auth_buf = [0u8; 75]; + let copy_len = auth_bytes.len().min(75); + auth_buf[..copy_len].copy_from_slice(&auth_bytes[..copy_len]); + w.write_all(&auth_buf)?; + w.flush()?; + + // Step 2: S→C 1 byte status + let status = read_u8(&mut r)?; + if status == 3 { + let error_code = read_i32_be(&mut r)?; + return Err(HandshakeError::AuthRejected( + auth_error_message(error_code).into(), + )); + } + + // Step 3: S→C 15 bytes banner "-RIC RFB MM.NN\n" + // The status byte (101 = 'e') is the first byte of "e-RIC RFB MM.NN\n". + let banner = read_exact(&mut r, 15)?; + if banner[0] != b'-' + || banner[1] != b'R' + || banner[2] != b'I' + || banner[3] != b'C' + || banner[4] != b' ' + || banner[5] != b'R' + || banner[6] != b'F' + || banner[7] != b'B' + || banner[8] != b' ' + || banner[14] != b'\n' + { + return Err(HandshakeError::InvalidBanner); + } + let major = (banner[9] - b'0') * 10 + (banner[10] - b'0'); + let minor = (banner[12] - b'0') * 10 + (banner[13] - b'0'); + + // Step 4: S→C 1 byte sync + let _sync1 = read_u8(&mut r)?; + + // Step 5: S→C server name (1 pad + modified-UTF-8 string) + let _pad = read_u8(&mut r)?; + let server_name = read_modified_utf8(&mut r)?; + + // Step 6: S→C 1 byte sync + let _sync2 = read_u8(&mut r)?; + + // Step 7: S→C pixel format struct (variable length) + let pixel_format = read_pixel_format(&mut r)?; + + // Step 8: C→S 16 bytes client version "e-RIC RFB 01.11\n" + let version_str = format!("e-RIC RFB {}\n", cfg.protocol_version); + let version_bytes = version_str.as_bytes(); + let mut version_buf = [0u8; 16]; + let copy_len = version_bytes.len().min(16); + version_buf[..copy_len].copy_from_slice(&version_bytes[..copy_len]); + w.write_all(&version_buf)?; + w.flush()?; + + // Step 9: C→S 2 bytes [shared_flag, port_id] + w.write_all(&[if cfg.shared { 1 } else { 0 }, cfg.port_id])?; + w.flush()?; + + // Step 10: S→C 1 byte sync + let _sync3 = read_u8(&mut r)?; + + // Step 11: S→C 19 bytes ServerInit + let server_init = read_server_init(&mut r)?; + + Ok(Session { + server_version: (major, minor), + server_name, + pixel_format, + server_init, + reader: r, + writer: w, + }) +} diff --git a/crates/ericrfb/src/lib.rs b/crates/ericrfb/src/lib.rs index febacec..21879be 100644 --- a/crates/ericrfb/src/lib.rs +++ b/crates/ericrfb/src/lib.rs @@ -1 +1,3 @@ +pub mod handshake; +pub mod msg; pub mod proto; diff --git a/crates/ericrfb/src/msg.rs b/crates/ericrfb/src/msg.rs new file mode 100644 index 0000000..7d4e760 --- /dev/null +++ b/crates/ericrfb/src/msg.rs @@ -0,0 +1,235 @@ +use std::io::Write; + +use crate::proto::{self, read_exact, read_i32_be, read_modified_utf8, read_u8, read_u16_be}; + +// --------------------------------------------------------------------------- +// Client-to-server message writers +// --------------------------------------------------------------------------- + +/// Msg type 2: SetEncodings — aw.a(int[], int), line 597. +/// Encoding IDs are i32 (negative values are pseudo-encodings). +pub fn write_set_encodings(w: &mut impl Write, encodings: &[i32]) -> proto::Result<()> { + let count = encodings.len() as u16; + let mut buf = vec![0u8; 4 + 4 * encodings.len()]; + buf[0] = 2; // msg type + // buf[1] = 0 (pad) + buf[2] = (count >> 8) as u8; + buf[3] = count as u8; + for (i, &enc) in encodings.iter().enumerate() { + let bytes = enc.to_be_bytes(); + buf[4 + i * 4..4 + i * 4 + 4].copy_from_slice(&bytes); + } + w.write_all(&buf)?; + w.flush()?; + Ok(()) +} + +/// Msg type 3: FramebufferUpdateRequest — aw.a(...), line 562. +pub fn write_fb_update_request( + w: &mut impl Write, + x: u16, + y: u16, + width: u16, + height: u16, + incremental: bool, +) -> proto::Result<()> { + let buf: [u8; 10] = [ + 3, // msg type + if incremental { 1 } else { 0 }, + (x >> 8) as u8, + x as u8, + (y >> 8) as u8, + y as u8, + (width >> 8) as u8, + width as u8, + (height >> 8) as u8, + height as u8, + ]; + w.write_all(&buf)?; + w.flush()?; + Ok(()) +} + +/// Msg type 4: KeyEvent — aw.a(byte), line 655. +/// Single scancode byte. +pub fn write_key_event(w: &mut impl Write, scancode: u8) -> proto::Result<()> { + w.write_all(&[4, scancode])?; + w.flush()?; + Ok(()) +} + +/// Msg type 5: PointerEvent — aw.a(boolean, int, int, int, int), line 612. +/// 8 bytes: [5, mask, x_u16, y_u16, extra_u16]. +pub fn write_pointer_event( + w: &mut impl Write, + x: u16, + y: u16, + button_mask: u8, +) -> proto::Result<()> { + let buf: [u8; 8] = [ + 5, // msg type (absolute mode) + button_mask, + (x >> 8) as u8, + x as u8, + (y >> 8) as u8, + y as u8, + 0, + 0, // extra_u16 = 0 in absolute mode + ]; + w.write_all(&buf)?; + w.flush()?; + Ok(()) +} + +/// Msg type 149: PingResponse — aw.if(int), line 636. +/// 8 bytes: [149(-107 signed), 0, 0, 0, n_i32]. +pub fn write_ping_response(w: &mut impl Write, payload: i32) -> proto::Result<()> { + let mut buf = [0u8; 8]; + buf[0] = 149u8; // -107 as u8 + // buf[1..4] = 0 (pad) + buf[4..8].copy_from_slice(&payload.to_be_bytes()); + w.write_all(&buf)?; + w.flush()?; + Ok(()) +} + +/// Msg type 151: bandwidth measurement bookend — aw.for(byte), line 649. +/// 2 bytes: [151, phase]. Phase 1 = start, 2 = done. +pub fn write_bandwidth_marker(w: &mut impl Write, phase: u8) -> proto::Result<()> { + w.write_all(&[151u8, phase])?; + w.flush()?; + Ok(()) +} + +// --------------------------------------------------------------------------- +// Server-to-client message types +// --------------------------------------------------------------------------- + +/// Server message type tag, read as the first byte of each server message. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ServerMsg { + FramebufferUpdate, // 0 + SetColourMapEntries, // 1 + Bell, // 2 + ServerCutText, // 3 + ServerNameUpdate, // 7 + PixelFormatChange, // 8 + LayoutLocale, // 9 + DesktopResize, // 16 + Ack, // 17 + ModeChange, // 128 + DebugString, // 131 + RfbCommand, // 132 + Ping, // 148 + BandwidthProbe, // 150 + RdpEvent, // 161 + Unknown(u8), +} + +impl From for ServerMsg { + fn from(b: u8) -> Self { + match b { + 0 => Self::FramebufferUpdate, + 1 => Self::SetColourMapEntries, + 2 => Self::Bell, + 3 => Self::ServerCutText, + 7 => Self::ServerNameUpdate, + 8 => Self::PixelFormatChange, + 9 => Self::LayoutLocale, + 16 => Self::DesktopResize, + 17 => Self::Ack, + 128 => Self::ModeChange, + 131 => Self::DebugString, + 132 => Self::RfbCommand, + 148 => Self::Ping, + 150 => Self::BandwidthProbe, + 161 => Self::RdpEvent, + other => Self::Unknown(other), + } + } +} + +// --------------------------------------------------------------------------- +// Server message readers (for dispatch loop) +// --------------------------------------------------------------------------- + +/// Read ping payload: 3 pad bytes + i32 — aw.b(), line 629. +pub fn read_ping(r: &mut impl std::io::Read) -> proto::Result { + let _pad = read_exact(r, 3)?; + read_i32_be(r) +} + +/// Read and discard bandwidth probe: 1 pad + u16 len + data — aw.do(), line 642. +pub fn read_bandwidth_probe(r: &mut impl std::io::Read) -> proto::Result<()> { + let _pad = read_u8(r)?; + let len = read_u16_be(r)? as usize; + let _data = read_exact(r, len)?; + Ok(()) +} + +/// Read 2-byte ack (no-op) — aw.for(), line 553. +pub fn read_ack(r: &mut impl std::io::Read) -> proto::Result<()> { + let _b1 = read_u8(r)?; + let _b2 = read_u8(r)?; + Ok(()) +} + +/// Read debug string — aw.d(), line 498. +/// 3 pad bytes + i32 length + string bytes. +pub fn read_debug_string(r: &mut impl std::io::Read) -> proto::Result { + let _pad = read_exact(r, 3)?; + let len = read_i32_be(r)? as usize; + let data = read_exact(r, len)?; + Ok(String::from_utf8_lossy(&data).into_owned()) +} + +/// Read RFB command — aw.long(), line 507. +/// 1 pad + u16 key_len + u16 val_len + key bytes + val bytes. +pub fn read_rfb_command(r: &mut impl std::io::Read) -> proto::Result<(String, String)> { + let _pad = read_u8(r)?; + let key_len = read_u16_be(r)? as usize; + let val_len = read_u16_be(r)? as usize; + let key_bytes = read_exact(r, key_len)?; + let val_bytes = read_exact(r, val_len)?; + Ok(( + String::from_utf8_lossy(&key_bytes).into_owned(), + String::from_utf8_lossy(&val_bytes).into_owned(), + )) +} + +/// Read server cut text — aw.goto(), line 464 (reads i32 error code, reused +/// for ServerCutText which reads the text via standard RFB: 3 pad + u32 len + text). +pub fn read_server_cut_text(r: &mut impl std::io::Read) -> proto::Result { + let _pad = read_exact(r, 3)?; + let len = read_i32_be(r)? as usize; + let data = read_exact(r, len)?; + Ok(String::from_utf8_lossy(&data).into_owned()) +} + +/// Read server name update — aw.l(), line 413. +/// 1 pad + modified-UTF-8 string. +pub fn read_server_name_update(r: &mut impl std::io::Read) -> proto::Result { + let _pad = read_u8(r)?; + read_modified_utf8(r) +} + +/// Read layout/locale string — aw.else(), line 529. +/// 1 pad + u16 len + string bytes. +pub fn read_layout_locale(r: &mut impl std::io::Read) -> proto::Result { + let _pad = read_u8(r)?; + let len = read_u16_be(r)? as usize; + let data = read_exact(r, len)?; + Ok(String::from_utf8_lossy(&data).into_owned()) +} + +/// Read RDP event type byte — aw.case(), line 558. +pub fn read_rdp_event(r: &mut impl std::io::Read) -> proto::Result { + proto::read_i8(r) +} + +/// Read framebuffer update header — aw.null(), line 459. +/// 1 pad byte + u16 num_rects. +pub fn read_fb_update_header(r: &mut impl std::io::Read) -> proto::Result { + let _pad = read_u8(r)?; + read_u16_be(r) +}