feat: phase 2 — handshake, message writers, and server message dispatch
handshake.rs: - Config, PixelFormat, ServerInit, Session types - connect() walks all 11 handshake steps per aw.g() line 226 - Auth error mapping from aw.a(int) line 350 (7 error codes) msg.rs — client-to-server writers: - SetEncodings (type 2), FramebufferUpdateRequest (type 3) - KeyEvent (type 4), PointerEvent (type 5, 8 bytes) - PingResponse (type 149), BandwidthMarker (type 151) msg.rs — server message dispatch: - ServerMsg enum covering all 15 message types - Readers for ping, bandwidth probe, ack, debug string, RFB command, server cut text, server name update, layout/locale, RDP event, FB update header examples/handshake.rs: connects and prints session info. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
47
crates/ericrfb/examples/handshake.rs
Normal file
47
crates/ericrfb/examples/handshake.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use std::env;
|
||||
|
||||
use ericrfb::handshake::{Config, connect};
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
|
||||
let host = args
|
||||
.iter()
|
||||
.position(|a| a == "--host")
|
||||
.and_then(|i| args.get(i + 1))
|
||||
.expect("usage: --host <ip> --applet-id <token> [--port <port>]");
|
||||
|
||||
let applet_id = args
|
||||
.iter()
|
||||
.position(|a| a == "--applet-id")
|
||||
.and_then(|i| args.get(i + 1))
|
||||
.expect("usage: --host <ip> --applet-id <token> [--port <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);
|
||||
}
|
||||
}
|
||||
}
|
||||
254
crates/ericrfb/src/handshake.rs
Normal file
254
crates/ericrfb/src/handshake.rs
Normal file
@@ -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<String>, port: u16, applet_id: impl Into<String>) -> 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<PixelFormat, HandshakeError> {
|
||||
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<ServerInit, HandshakeError> {
|
||||
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<TcpStream>,
|
||||
pub writer: BufWriter<TcpStream>,
|
||||
}
|
||||
|
||||
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<Session, HandshakeError> {
|
||||
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,
|
||||
})
|
||||
}
|
||||
@@ -1 +1,3 @@
|
||||
pub mod handshake;
|
||||
pub mod msg;
|
||||
pub mod proto;
|
||||
|
||||
235
crates/ericrfb/src/msg.rs
Normal file
235
crates/ericrfb/src/msg.rs
Normal file
@@ -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<u8> 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<i32> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
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<i8> {
|
||||
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<u16> {
|
||||
let _pad = read_u8(r)?;
|
||||
read_u16_be(r)
|
||||
}
|
||||
Reference in New Issue
Block a user