feat: phase 2 — handshake, message writers, and server message dispatch
All checks were successful
CI / fmt (push) Successful in 30s
CI / check (push) Successful in 1m1s
CI / clippy (push) Successful in 1m4s

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:
2026-05-06 14:11:31 +03:00
parent 07db90094d
commit 1bd43fc1f9
4 changed files with 538 additions and 0 deletions

View 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);
}
}
}

View 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 111
// ---------------------------------------------------------------------------
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,
})
}

View File

@@ -1 +1,3 @@
pub mod handshake;
pub mod msg;
pub mod proto; pub mod proto;

235
crates/ericrfb/src/msg.rs Normal file
View 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)
}