From 819a6dc1b69b330d165e081d645c470da7271f2a Mon Sep 17 00:00:00 2001 From: John Doty Date: Wed, 27 Sep 2023 16:44:30 -0700 Subject: [PATCH] [oden] More debugger! More more! All the way through websockets and JSON RPC (kinda) --- src/script/debugger.rs | 678 ++++++++++++++++++++++++++++++++++------- 1 file changed, 568 insertions(+), 110 deletions(-) diff --git a/src/script/debugger.rs b/src/script/debugger.rs index 4f5a5c3a..214e8b52 100644 --- a/src/script/debugger.rs +++ b/src/script/debugger.rs @@ -1,114 +1,31 @@ +use base64::Engine; +use bytes::buf::BufMut; +use oden_js::{Context, Runtime, ValueRef}; +use sha1::{Digest, Sha1}; use std::collections::HashMap; use std::io; -use std::io::{BufRead, BufReader, Write}; +use std::io::{BufRead, BufReader, Read, Write}; use std::net::{TcpListener, TcpStream}; use std::thread; +use thiserror::Error; pub struct Debugger { _thread: thread::JoinHandle<()>, } -fn write_http_response( - stream: &mut TcpStream, - code: u16, - phrase: &str, - content_type: &str, - data: &[u8], -) -> io::Result<()> { - let length = data.len(); - let buffer = format!( - "HTTP/1.1 {code} {phrase}\r\n\ - content-length: {length}\r\n\ - content-type: {content_type}\r\n\ - \r\n" - ); - stream.write_all(buffer.as_bytes())?; - stream.write_all(data) -} - -fn write_http_error(stream: &mut TcpStream, code: u16, phrase: &str) -> io::Result<()> { - write_http_response(stream, code, phrase, "text/plain", phrase.as_bytes()) -} - -fn write_http_ok(stream: &mut TcpStream, content_type: &str, data: &[u8]) -> io::Result<()> { - write_http_response(stream, 200, "OK", content_type, data) -} - -fn handle_connection(mut stream: TcpStream) -> io::Result<()> { - let mut buf_reader = BufReader::new(&mut stream); - - let mut buffer = String::new(); - buf_reader.read_line(&mut buffer)?; - - let parts: Vec<_> = buffer.trim().split(" ").collect(); - if parts.len() != 3 { - eprintln!("Invalid request line: {buffer}"); - write_http_error(&mut stream, 400, "Invalid request")?; - return Ok(()); - } - - let method = parts[0]; - let path = parts[1]; - eprintln!("Debugger: {method} {path}"); - - let mut headers = HashMap::new(); - let mut header_line_buffer = String::new(); - loop { - header_line_buffer.clear(); - buf_reader.read_line(&mut header_line_buffer)?; - let header_line = header_line_buffer.trim(); - if header_line.is_empty() { - break; +#[allow(dead_code)] +pub fn start_debugger() -> Debugger { + let thread = thread::spawn(move || { + if let Err(e) = debugger_listener() { + eprintln!("ERROR STARTING DEBUGGER: {:?}", e); } + }); - let sep_idx = match header_line.find(":") { - Some(idx) => idx, - None => { - write_http_error(&mut stream, 400, "Invalid request")?; - return Ok(()); - } - }; - let header = &header_line[0..sep_idx]; - let value = &header_line[sep_idx + 1..]; - eprintln!("HEADER: {header} {value}"); - headers.insert(header.trim().to_string(), value.trim().to_string()); - } - - if method == "GET" { - if path == "/json/version" { - // NOTE: Deno spits out a V8 version here but we're not running v8. - write_http_ok( - &mut stream, - "application/json", - "{\"Browser\": \"Oden/0.0.1\", \"Protocol-Version\": \"1.3\"}".as_bytes(), - )?; - } else if path == "/json" || path == "/json/list" { - // [ - // { - // "description": "deno", - // "devtoolsFrontendUrl": "devtools://devtools/bundled/js_app.html?ws=127.0.0.1:9229/ws/01b9bd23-8810-43ed-86f7-5feef5d120fa&experiments=true&v8only=true", - // "faviconUrl": "https://deno.land/favicon.ico", - // "id": "01b9bd23-8810-43ed-86f7-5feef5d120fa", - // "title": "deno - main [pid: 40483]", - // "type": "node", - // "url": "file:///Users/doty/src/ioniq/status.ts", - // "webSocketDebuggerUrl": "ws://127.0.0.1:9229/ws/01b9bd23-8810-43ed-86f7-5feef5d120fa" - // } - // ] - } else if path == "/ws/dd5cfe85-f0b1-4241-a643-8ed81e436188" { - // I don't feel like making a new guid for each thing. - // Websocket upgrade and then debugger messages. - } else { - write_http_error(&mut stream, 404, "Not Found")?; - } - } else { - write_http_error(&mut stream, 404, "Not Found")?; - } - Ok(()) + Debugger { _thread: thread } } -pub fn debugger_listener() -> io::Result<()> { - let listener = TcpListener::bind("127.0.0.1:0")?; +pub fn debugger_listener() -> Result<()> { + let listener = TcpListener::bind("127.0.0.1:9229")?; let port = listener.local_addr()?.port(); println!("Debugger listening on http://127.0.0.1:{port}"); for stream in listener.incoming() { @@ -120,20 +37,561 @@ pub fn debugger_listener() -> io::Result<()> { } }; - // TODO: Thread pool extraction here. - if let Err(e) = handle_connection(stream) { - eprintln!("Error handling incoming connection: {e}"); - } + DebuggerConnection::start(stream); } Ok(()) } -#[allow(dead_code)] -pub fn start_debugger() -> Debugger { - let thread = thread::spawn(move || { - if let Err(e) = debugger_listener() { - eprintln!("ERROR STARTING DEBUGGER: {:?}", e); - } - }); - Debugger { _thread: thread } +struct DebuggerConnection { + stream: TcpStream, + context: Context, +} + +impl DebuggerConnection { + fn start(stream: TcpStream) { + thread::spawn(move || { + let mut connection = DebuggerConnection { + stream, + context: Context::new(Runtime::new()), + }; + loop { + if let Err(e) = connection.handle_connection() { + eprintln!("Closing connection: {e}"); + break; + } + } + }); + } + + // ============================================================================ + // Dumb HTTP stuff + // ============================================================================ + fn handle_connection(&mut self) -> Result<()> { + let port = self.stream.local_addr()?.port(); + let id = "dd5cfe85-f0b1-4241-a643-8ed81e436188"; + + let mut buf_reader = BufReader::new(&mut self.stream); + + let mut buffer = String::new(); + while buffer.trim().is_empty() { + buf_reader.read_line(&mut buffer)?; + } + + let parts: Vec<_> = buffer.trim().split(" ").collect(); + if parts.len() != 3 { + eprintln!("Invalid request line: {buffer}"); + self.write_http_error(400, "Invalid request")?; + return Ok(()); + } + + let method = parts[0]; + let path = parts[1]; + eprintln!("Debugger: {method} {path}"); + + let mut headers = HashMap::new(); + let mut header_line_buffer = String::new(); + loop { + header_line_buffer.clear(); + buf_reader.read_line(&mut header_line_buffer)?; + let header_line = header_line_buffer.trim(); + if header_line.is_empty() { + break; + } + + let sep_idx = match header_line.find(":") { + Some(idx) => idx, + None => { + self.write_http_error(400, "Invalid request")?; + return Ok(()); + } + }; + let header = &header_line[0..sep_idx]; + let value = &header_line[sep_idx + 1..]; + // eprintln!("HEADER: {header} {value}"); + headers.insert(header.trim().to_lowercase(), value.trim().to_string()); + } + + if method == "GET" { + if path == "/json/version" { + // NOTE: Deno spits out a V8 version here but we're not running v8. + self.write_http_ok( + "application/json", + r#" +{ + "Browser": "Oden/0.0.1", + "Protocol-Version": "1.3" +}"# + .as_bytes(), + )?; + } else if path == "/json" || path == "/json/list" { + // TODO: title should reflect the game being loaded + // TODO: url should go to the main .ts file + // TODO: faciconUrl? + let result = format!( + r#" +[{{ + "description": "oden", + "devtoolsFrontendUrl": "devtools://devtools/bundled/js_app.html?ws=127.0.0.1:9229/ws/01b9bd23-8810-43ed-86f7-5feef5d120fa&experiments=true&v8only=true", + "id": "{id}", + "title": "oden game", + "url": "oden://game", + "webSocketDebuggerUrl": "ws://127.0.0.1:{port}/ws/{id}" +}}] +"# + ); + self.write_http_ok("application/json", result.as_bytes())?; + } else if path == "/ws/dd5cfe85-f0b1-4241-a643-8ed81e436188" { + // I don't feel like making a new guid for each thing. + // Websocket upgrade and then debugger messages. + + let mut key = match headers.get("sec-websocket-key") { + Some(v) => v.clone(), + None => { + self.write_http_error(400, "Invalid request")?; + return Ok(()); + } + }; + key.push_str("258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); + let mut hasher = Sha1::new(); + hasher.update(key.as_bytes()); + let result = base64::engine::general_purpose::STANDARD.encode(hasher.finalize()); + let response = format!( + "HTTP/1.1 101 Switching Protocols\r\n\ + Upgrade: websocket\r\n\ + Connection: Upgrade\r\n\ + Sec-Websocket-Accept: {result}\r\n\ + \r\n" + ); + self.stream.write_all(response.as_bytes())?; + + self.handle_websocket_connection()?; + } else { + self.write_http_error(404, "Not Found")?; + } + } else { + self.write_http_error(404, "Not Found")?; + } + Ok(()) + } + + fn write_http_response( + &mut self, + code: u16, + phrase: &str, + content_type: &str, + data: &[u8], + ) -> Result<()> { + let length = data.len(); + let buffer = format!( + "HTTP/1.1 {code} {phrase}\r\n\ + content-length: {length}\r\n\ + content-type: {content_type}\r\n\ + \r\n" + ); + self.stream.write_all(buffer.as_bytes())?; + self.stream.write_all(data)?; + Ok(()) + } + + fn write_http_error(&mut self, code: u16, phrase: &str) -> Result<()> { + self.write_http_response(code, phrase, "text/plain", phrase.as_bytes()) + } + + fn write_http_ok(&mut self, content_type: &str, data: &[u8]) -> Result<()> { + self.write_http_response(200, "OK", content_type, data) + } + + // ============================================================================ + // Dumb Websocket stuff + // ============================================================================ + fn handle_websocket_connection(&mut self) -> Result<()> { + // BLARG: https://datatracker.ietf.org/doc/html/rfc6455 + + let mut payload: Vec = Vec::new(); + let mut opcode: WebsocketOp = WebsocketOp::Continue; + + loop { + let frame = WebsocketFrameHeader::read(&mut self.stream)?; + eprintln!("FRAME: {:?}", frame); + let mask = match frame.mask { + Some(m) => m, + None => { + eprintln!("Client sent unmasked frame"); + return Ok(()); // TODO: _Fail the WebSocket Connection_ + } + }; + + // OK what are we dealing with here? Check the opcode before we decide + // where to read the data. + if frame.opcode.is_control() { + // This is a control frame, handle it on its own. + if frame.length > 125 { + eprintln!("Control frame too big ({})", frame.length); + return Ok(()); // TODO: _Fail the WebSocket Connection_ + } + let mut buffer: [u8; 125] = [0; 125]; + self.stream.read_exact(&mut buffer[0..frame.length])?; + + if frame.opcode == WebsocketOp::Close { + self.send_websocket_message(frame.opcode, &buffer[..frame.length])?; + return Ok(()); + } else if frame.opcode == WebsocketOp::Ping { + self.send_websocket_message(WebsocketOp::Pong, &buffer[..frame.length])?; + } else if frame.opcode == WebsocketOp::Pong { + // got a pong + } + } else { + // This is a user data frame, it goes into `payload`. + let last_tail = payload.len(); + if last_tail + frame.length > 1 * 1024 * 1024 { + eprintln!("Message too big ({})", frame.length); + return Ok(()); // TODO: _Fail the WebSocket Connection_ + } + payload.resize(last_tail + frame.length, 0); + let slice = payload.as_mut_slice(); + let mut slice = &mut slice[last_tail..]; + // eprintln!("Reading {} bytes", slice.len()); + self.stream.read_exact(&mut slice)?; + for i in 0..frame.length { + slice[i] = slice[i] ^ mask[i % 4]; + } + // eprintln!("Done"); + + if opcode == WebsocketOp::Continue { + opcode = frame.opcode; + } + + if frame.fin { + // Dispatch. + // eprintln!("Dispatching: {:?} {}", opcode, payload.len()); + self.handle_websocket_message(opcode, &payload)?; + payload.clear(); + opcode = WebsocketOp::Continue; + } + } + } + } + + fn send_websocket_message(&mut self, opcode: WebsocketOp, payload: &[u8]) -> Result<()> { + WebsocketFrameHeader { + fin: true, + opcode, + length: payload.len(), + mask: None, + } + .write(&mut self.stream)?; + self.stream.write_all(payload)?; + Ok(()) + } + + fn send_websocket_close(&mut self, close_code: u16, reason: &str) -> Result<()> { + if reason.len() > 123 { + return Err("reason doesn't fit in close packet".into()); + } + + let mut body: [u8; 125] = [0; 125]; + let mut dst = &mut body[..]; + dst.put_u16(close_code); + dst.put_slice(reason.as_bytes()); + let remaining = dst.remaining_mut(); + + let body_len = body.len() - remaining; + self.send_websocket_message(WebsocketOp::Close, &body[0..body_len]) + } + + // ============================================================================ + // Actual debugger stuff? + // ============================================================================ + fn handle_websocket_message(&mut self, _op: WebsocketOp, data: &[u8]) -> Result<()> { + let text = String::from_utf8_lossy(data); + eprintln!("Received: {text}"); + + let message = match self.context.parse_json(&text, "") { + Ok(v) => v, + Err(e) => { + eprintln!("error parsing json {e}"); + self.send_websocket_close(1007, "invalid json")?; + return Err("invalid json".into()); + } + }; + + // Oh no. + let _ = message.get_property(&self.context, "sessionId")?; + let id = message.get_property(&self.context, "id")?; + let method = { + let method = message.get_property(&self.context, "method")?; + method.to_string(&self.context)? + }; + let _ = message.get_property(&self.context, "params")?; + + match method.as_str() { + "Profiler.enable" => { + self.send_json_response(&id, self.context.new_object()?.as_ref())?; + } + + "Runtime.enable" => { + self.send_json_event( + "Runtime.executionContextCreated", + self.context + .new_object_props([( + "context", + self.context.new_object_props([ + ("id", self.context.new_i32(7)?), + ("origin", self.context.new_string("something")?), + ("name", self.context.new_string("main")?), + ("uniqueId", self.context.new_string("aaa-aaa-aaa")?), + ])?, + )])? + .as_ref(), + )?; + self.send_json_response(&id, self.context.new_object()?.as_ref())?; + } + + "Runtime.evaluate" => { + // OHHHHHHHHHH + eprintln!("****** EVALUATE *******"); + self.send_debugger_error(&id, -32601, "method does not exist")? + } + + "Runtime.runIfWaitingForDebugger" => { + self.send_json_response(&id, self.context.new_object()?.as_ref())?; + } + + "Debugger.enable" => { + self.send_json_response(&id, self.context.new_object()?.as_ref())?; + } + + "Debugger.setAsyncCallStackDepth" => { + // TODO: Do I need to respect this? + self.send_json_response(&id, self.context.new_object()?.as_ref())?; + } + + "Debugger.setBlackboxPatterns" => { + // TODO: Do I need to respect this? + self.send_json_response(&id, self.context.new_object()?.as_ref())?; + } + + "Debugger.setPauseOnExceptions" => { + // TODO: Respect this. + self.send_json_response(&id, self.context.new_object()?.as_ref())?; + } + + _ => { + eprintln!("Unsupported method: {method}"); + self.send_debugger_error(&id, -32601, "method does not exist")? + } + } + + Ok(()) + } + + fn send_debugger_error(&mut self, id: &ValueRef, code: i32, msg: &str) -> Result<()> { + self.send_json_message( + self.context + .new_object_props([ + ("id", id), + ( + "error", + self.context + .new_object_props([ + ("code", self.context.new_i32(code)?), + ("message", self.context.new_string(msg)?), + ])? + .as_ref(), + ), + ])? + .as_ref(), + ) + } + + fn send_json_event(&mut self, event: &str, params: &ValueRef) -> Result<()> { + self.send_json_message( + self.context + .new_object_props([ + ("method", self.context.new_string(event)?.as_ref()), + ("params", params), + ])? + .as_ref(), + ) + } + + fn send_json_response(&mut self, id: &ValueRef, result: &ValueRef) -> Result<()> { + let response = self + .context + .new_object_props([("id", id), ("result", result)])?; + self.send_json_message(&response) + } + + fn send_json_message(&mut self, value: &ValueRef) -> Result<()> { + let txt = self + .context + .json_stringify(&value)? + .to_string(&self.context)?; + eprintln!("Send: {txt}"); + self.send_websocket_message(WebsocketOp::Text, txt.as_bytes()) + } +} + +#[derive(Debug)] +struct WebsocketFrameHeader { + pub fin: bool, + pub opcode: WebsocketOp, + pub length: usize, + pub mask: Option<[u8; 4]>, +} + +impl WebsocketFrameHeader { + fn read(stream: &mut TcpStream) -> Result { + let mut buffer: [u8; 2] = [0, 0]; + stream.read_exact(&mut buffer)?; + if (buffer[0] & 0x70) != 0 { + eprintln!("Frame has non-zero RSV* ({:x})", buffer[0]); + return Err("unknown extensions".into()); + } + let has_mask = (buffer[1] & 0x80) != 0; + + let fin = (buffer[0] & 0x80) != 0; + let opcode = buffer[0] & 0x0F; + let mut length: u64 = (buffer[1] & !0x80).into(); + // eprintln!("Decoded length {} from {}", length, buffer[1]); + if length == 126 { + let mut len_buffer: [u8; 2] = [0, 0]; + stream.read_exact(&mut len_buffer)?; + length = u16::from_be_bytes(len_buffer).into(); + } else if length == 127 { + let mut len_buffer: [u8; 8] = [0, 0, 0, 0, 0, 0, 0, 0]; + stream.read_exact(&mut len_buffer)?; + length = u64::from_be_bytes(len_buffer); + } + if length > 1 * 1024 * 1024 { + eprintln!("Frame too big ({length})"); + return Err(io::Error::new(io::ErrorKind::InvalidData, "frame too big").into()); + } + + let mask = if has_mask { + let mut mask: [u8; 4] = [0, 0, 0, 0]; + stream.read_exact(&mut mask)?; + Some(mask) + } else { + None + }; + + let length: usize = length.try_into().unwrap(); + Ok(WebsocketFrameHeader { + fin, + opcode: opcode.try_into()?, + length, + mask, + }) + } + + fn write(&self, stream: &mut TcpStream) -> Result<()> { + let mut header: [u8; 16] = [0; 16]; + let mut dst = &mut header[..]; + + let byte: u8 = self.opcode.into(); + dst.put_u8(byte | if self.fin { 0x80 } else { 0x00 }); + + let mask = if self.mask.is_some() { 0x80 } else { 0x00 }; + if self.length < 126 { + let len: u8 = self.length.try_into().unwrap(); + dst.put_u8(len | mask); + } else if self.length <= u16::max_value().into() { + dst.put_u8(126 | mask); + dst.put_u16(self.length.try_into().unwrap()); + } else { + dst.put_u8(127 | mask); + dst.put_u64(self.length.try_into().unwrap()); + } + if let Some(bytes) = self.mask { + dst.put_slice(&bytes); + } + let remaining = dst.remaining_mut(); + + let header_len = header.len() - remaining; + stream.write_all(&header[0..header_len])?; + Ok(()) + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] +enum WebsocketOp { + Continue, + Text, + Binary, + Close, + Ping, + Pong, +} + +impl WebsocketOp { + fn is_control(&self) -> bool { + let byte: u8 = (*self).into(); + byte & 0x08 != 0 + } +} + +impl TryFrom for WebsocketOp { + type Error = DebuggerError; + + fn try_from(value: u8) -> Result { + match value { + 0x0 => Ok(WebsocketOp::Continue), + 0x1 => Ok(WebsocketOp::Text), + 0x2 => Ok(WebsocketOp::Binary), + 0x8 => Ok(WebsocketOp::Close), + 0x9 => Ok(WebsocketOp::Ping), + 0xA => Ok(WebsocketOp::Pong), + _ => Err(format!("unknown opcode {value}").into()), + } + } +} + +impl From for u8 { + fn from(value: WebsocketOp) -> Self { + match value { + WebsocketOp::Continue => 0x0, + WebsocketOp::Text => 0x1, + WebsocketOp::Binary => 0x2, + WebsocketOp::Close => 0x8, + WebsocketOp::Ping => 0x9, + WebsocketOp::Pong => 0xA, + } + } +} + +#[derive(Debug, Error)] +pub enum DebuggerError { + #[error("an unknown error has occurred: {0}")] + MiscError(String), + #[error("a javascript error has occurred: {0}")] + JsError(oden_js::Error), + #[error("an io error has occurred: {0}")] + IoError(io::Error), +} + +type Result = core::result::Result; + +impl From<&str> for DebuggerError { + fn from(value: &str) -> Self { + DebuggerError::MiscError(value.to_string()) + } +} + +impl From for DebuggerError { + fn from(value: String) -> Self { + DebuggerError::MiscError(value) + } +} + +impl From for DebuggerError { + fn from(value: oden_js::Error) -> Self { + DebuggerError::JsError(value) + } +} + +impl From for DebuggerError { + fn from(value: io::Error) -> Self { + DebuggerError::IoError(value) + } }