From 530658221daed94fd8eeb1fc7ed233ce46e1ca17 Mon Sep 17 00:00:00 2001 From: Vincent Ollivier Date: Sat, 21 May 2022 10:46:16 +0200 Subject: [PATCH] Add socket command (#341) * Add socket command * Add test.html page * Refactory sending code * Update documentation * Fix newlines * Add interval option * Replace newlines * Exit on end of transmission * Add listen mode * Fix arguments parsing * Remove debug output --- doc/network.md | 80 +++++++++++++-- src/sys/fs/device.rs | 1 + src/usr/dhcp.rs | 2 +- src/usr/disk.rs | 2 +- src/usr/http.rs | 6 +- src/usr/httpd.rs | 2 +- src/usr/mod.rs | 1 + src/usr/net.rs | 3 +- src/usr/read.rs | 2 +- src/usr/shell.rs | 5 +- src/usr/socket.rs | 237 +++++++++++++++++++++++++++++++++++++++++++ src/usr/tcp.rs | 28 +++-- www/test.html | 10 ++ 13 files changed, 354 insertions(+), 25 deletions(-) create mode 100644 src/usr/socket.rs create mode 100644 www/test.html diff --git a/doc/network.md b/doc/network.md index b0d4e97..134d3a1 100644 --- a/doc/network.md +++ b/doc/network.md @@ -49,7 +49,7 @@ Listen for packets transmitted on the network: The `dhcp` command configures the network automatically: - > dhcp -v + > dhcp --verbose DEBUG: DHCP Discover transmitted DEBUG: DHCP Offer received ip: 10.0.2.15/24 @@ -68,15 +68,14 @@ The `host` command performs DNS lookups: The `tcp` command connects to TCP sockets: - > tcp time.nist.gov 13 - Connecting to 129.6.15.30:13 + > tcp time.nist.gov 13 --verbose + DEBUG: Connecting to 129.6.15.30:13 58884 20-02-05 19:19:42 00 0 0 49.2 UTC(NIST) * This could also be done with the `read` command: > read /net/tcp/time.nist.gov:13 - Connecting to 129.6.15.30:13 58884 20-02-05 19:19:55 00 0 0 49.2 UTC(NIST) * @@ -85,12 +84,79 @@ This could also be done with the `read` command: Requesting a resource on a host: - > http example.com /articles/index.html + > http moros.cc /test.html Is equivalent to: - > read /net/http/example.com/articles + > read /net/http/moros.cc/test.html And: - > read /net/http/example.com:80/articles/index.html + > read /net/http/moros.cc:80/test.html + +## SOCKET + +The `socket` command is used to read and write to network connexions +like the `netcat` command on Unix. + +For example the request made with `tcp` above is equivalent to this: + + > socket time.nist.gov 13 --read-only + + 59710 22-05-11 21:44:52 50 0 0 359.3 UTC(NIST) * + +And the request made with `http` is equivalent to that: + + > socket moros.cc 80 --prompt + MOROS Socket v0.1.0 + + > GET /test.html HTTP/1.0 + > Host: moros.cc + > + HTTP/1.1 200 OK + Server: nginx + Date: Wed, 11 May 2022 21:46:34 GMT + Content-Type: text/html + Content-Length: 866 + Connection: close + Last-Modified: Fri, 29 Oct 2021 17:50:58 GMT + ETag: "617c3482-362" + Accept-Ranges: bytes + + + + + + MOROS: Obscure Rust Operating System + + +

MOROS

+ + + +Here's a connexion to a SMTP server to send a mail: + + > socket 10.0.2.2 2500 --prompt + MOROS Socket v0.1.0 + + 220 EventMachine SMTP Server + > EHLO moros.cc + 250-Ok EventMachine SMTP Server + 250-NO-SOLICITING + 250 SIZE 20000000 + > MAIL FROM: + > RCPT TO: + 250 Ok + 250 Ok + > DATA + 354 Send it + > Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum nec + > diam vitae ex blandit malesuada nec a turpis. + > . + > QUIT + 250 Message accepted + 221 Ok + +Sending a file to a server: + + > socket 10.0.2.2 1234 <= /tmp/alice.txt diff --git a/src/sys/fs/device.rs b/src/sys/fs/device.rs index a6f060d..2745689 100644 --- a/src/sys/fs/device.rs +++ b/src/sys/fs/device.rs @@ -86,6 +86,7 @@ impl FileIO for Device { Device::Null => Err(()), } } + fn write(&mut self, buf: &[u8]) -> Result { match self { Device::File(io) => io.write(buf), diff --git a/src/usr/dhcp.rs b/src/usr/dhcp.rs index 3020256..4f0679b 100644 --- a/src/usr/dhcp.rs +++ b/src/usr/dhcp.rs @@ -32,7 +32,7 @@ pub fn main(args: &[&str]) -> usr::shell::ExitCode { iface.remove_socket(dhcp_handle); return usr::shell::ExitCode::CommandError; } - if sys::console::end_of_text() { + if sys::console::end_of_text() || sys::console::end_of_transmission() { eprintln!(); iface.remove_socket(dhcp_handle); return usr::shell::ExitCode::CommandError; diff --git a/src/usr/disk.rs b/src/usr/disk.rs index 5baf52e..0c2fae6 100644 --- a/src/usr/disk.rs +++ b/src/usr/disk.rs @@ -60,7 +60,7 @@ fn erase(pathname: &str) -> usr::shell::ExitCode { let buf = vec![0; drive.block_size() as usize]; print!("\x1b[?25l"); // Disable cursor for i in 0..n { - if sys::console::end_of_text() { + if sys::console::end_of_text() || sys::console::end_of_transmission() { println!(); print!("\x1b[?25h"); // Enable cursor return usr::shell::ExitCode::CommandError; diff --git a/src/usr/http.rs b/src/usr/http.rs index b968cd9..8b04599 100644 --- a/src/usr/http.rs +++ b/src/usr/http.rs @@ -120,8 +120,8 @@ pub fn main(args: &[&str]) -> usr::shell::ExitCode { iface.remove_socket(tcp_handle); return usr::shell::ExitCode::CommandError; } - if sys::console::end_of_text() { - eprintln!(); // FIXME + if sys::console::end_of_text() || sys::console::end_of_transmission() { + eprintln!(); iface.remove_socket(tcp_handle); return usr::shell::ExitCode::CommandError; } @@ -220,6 +220,6 @@ fn help() -> usr::shell::ExitCode { println!("{}Usage:{} http {} {1}", csi_title, csi_reset, csi_option); println!(); println!("{}Options:{}", csi_title, csi_reset); - println!(" {0}--verbose{1} Increase verbosity", csi_option, csi_reset); + println!(" {0}-v{1}, {0}--verbose{1} Increase verbosity", csi_option, csi_reset); usr::shell::ExitCode::CommandSuccessful } diff --git a/src/usr/httpd.rs b/src/usr/httpd.rs index 9148efe..a4c9d9d 100644 --- a/src/usr/httpd.rs +++ b/src/usr/httpd.rs @@ -28,7 +28,7 @@ pub fn main(_args: &[&str]) -> usr::shell::ExitCode { let mut send_queue: VecDeque> = VecDeque::new(); loop { - if sys::console::end_of_text() { + if sys::console::end_of_text() || sys::console::end_of_transmission() { iface.remove_socket(tcp_handle); println!(); return usr::shell::ExitCode::CommandSuccessful; diff --git a/src/usr/mod.rs b/src/usr/mod.rs index eb63c22..e47da3d 100644 --- a/src/usr/mod.rs +++ b/src/usr/mod.rs @@ -34,6 +34,7 @@ pub mod read; pub mod reboot; pub mod shell; pub mod sleep; +pub mod socket; pub mod tcp; pub mod uptime; pub mod user; diff --git a/src/usr/net.rs b/src/usr/net.rs index 1870449..0234421 100644 --- a/src/usr/net.rs +++ b/src/usr/net.rs @@ -61,8 +61,9 @@ pub fn main(args: &[&str]) -> usr::shell::ExitCode { let tcp_handle = iface.add_socket(tcp_socket); loop { - if sys::console::end_of_text() { + if sys::console::end_of_text() || sys::console::end_of_transmission() { println!(); + iface.remove_socket(tcp_handle); return usr::shell::ExitCode::CommandSuccessful; } syscall::sleep(0.1); diff --git a/src/usr/read.rs b/src/usr/read.rs index a0e6575..a48bd7c 100644 --- a/src/usr/read.rs +++ b/src/usr/read.rs @@ -86,7 +86,7 @@ pub fn main(args: &[&str]) -> usr::shell::ExitCode { } else if info.is_device() { let is_console = info.size() == 4; // TODO: Improve device detection loop { - if sys::console::end_of_text() { + if sys::console::end_of_text() || sys::console::end_of_transmission() { println!(); return usr::shell::ExitCode::CommandSuccessful; } diff --git a/src/usr/shell.rs b/src/usr/shell.rs index 168ec78..1cd942e 100644 --- a/src/usr/shell.rs +++ b/src/usr/shell.rs @@ -8,11 +8,11 @@ use alloc::vec::Vec; use alloc::string::String; // TODO: Scan /bin -const AUTOCOMPLETE_COMMANDS: [&str; 38] = [ +const AUTOCOMPLETE_COMMANDS: [&str; 39] = [ "2048", "base64", "calc", "clear", "colors", "copy", "date", "delete", "dhcp", "disk", "edit", "env", "exit", "geotime", "goto", "halt", "help", "hex", "host", "http", "httpd", "install", "keyboard", "lisp", "list", "memory", "move", "net", "pci", "print", "read", "reboot", "shell", - "sleep", "tcp", "user", "vga", "write" + "sleep", "socket", "tcp", "user", "vga", "write" ]; #[repr(u8)] @@ -254,6 +254,7 @@ pub fn exec(cmd: &str) -> ExitCode { "dhcp" => usr::dhcp::main(&args), "http" => usr::http::main(&args), "httpd" => usr::httpd::main(&args), + "socket" => usr::socket::main(&args), "tcp" => usr::tcp::main(&args), "host" => usr::host::main(&args), "install" => usr::install::main(&args), diff --git a/src/usr/socket.rs b/src/usr/socket.rs new file mode 100644 index 0000000..e814ee1 --- /dev/null +++ b/src/usr/socket.rs @@ -0,0 +1,237 @@ +use crate::{sys, usr, debug}; +use crate::api::console::Style; +use crate::api::io; +use crate::api::syscall; +use crate::api::random; + +use alloc::format; +use alloc::string::String; +use alloc::vec; +use alloc::vec::Vec; +use core::str::{self, FromStr}; +use smoltcp::socket::{TcpSocket, TcpSocketBuffer, TcpState}; +use smoltcp::time::Instant; +use smoltcp::wire::IpAddress; + +pub fn main(args: &[&str]) -> usr::shell::ExitCode { + let mut listen = false; + let mut prompt = false; + let mut verbose = false; + let mut read_only = false; + let mut interval = 0.0; + let mut next_arg_is_interval = false; + let mut args: Vec<&str> = args.iter().filter_map(|arg| { + match *arg { + "-l" | "--listen" => { + listen = true; + None + } + "-p" | "--prompt" => { + prompt = true; + None + } + "-r" | "--read-only" => { + read_only = true; + None + } + "-v" | "--verbose" => { + verbose = true; + None + } + "-i" | "--interval" => { + next_arg_is_interval = true; + None + } + _ if next_arg_is_interval => { + next_arg_is_interval = false; + if let Ok(i) = arg.parse() { + interval = i; + } + None + } + _ => { + Some(*arg) + } + } + }).collect(); + if prompt { + println!("MOROS Socket v0.1.0\n"); + } + + let required_args_count = if listen { 2 } else { 3 }; + + if args.len() == required_args_count - 1 { + if let Some(i) = args[1].find(':') { // Split and + let arg = args[1].clone(); + let (host, path) = arg.split_at(i); + args[1] = host; + args.push(&path[1..]); + } + } + + if args.len() != required_args_count { + help(); + return usr::shell::ExitCode::CommandError; + } + + let host = if listen { "0.0.0.0" } else { &args[1] }; + let port: u16 = args[required_args_count - 1].parse().expect("Could not parse port"); + + let address = if host.ends_with(char::is_numeric) { + IpAddress::from_str(host).expect("invalid address format") + } else { + match usr::host::resolve(host) { + Ok(ip_addr) => { + ip_addr + } + Err(e) => { + error!("Could not resolve host: {:?}", e); + return usr::shell::ExitCode::CommandError; + } + } + }; + + let tcp_rx_buffer = TcpSocketBuffer::new(vec![0; 1024]); + let tcp_tx_buffer = TcpSocketBuffer::new(vec![0; 1024]); + let tcp_socket = TcpSocket::new(tcp_rx_buffer, tcp_tx_buffer); + + #[derive(Debug)] + enum State { Connecting, Sending, Receiving } + let mut state = State::Connecting; + + if let Some(ref mut iface) = *sys::net::IFACE.lock() { + let tcp_handle = iface.add_socket(tcp_socket); + + loop { + if sys::console::end_of_text() || sys::console::end_of_transmission() { + eprintln!(); + iface.remove_socket(tcp_handle); + return usr::shell::ExitCode::CommandError; + } + let timestamp = Instant::from_micros((syscall::realtime() * 1000000.0) as i64); + if let Err(e) = iface.poll(timestamp) { + error!("Network Error: {}", e); + } + + let (socket, cx) = iface.get_socket_and_context::(tcp_handle); + if verbose { + debug!("*********************************"); + debug!("APP State: {:?}", state); + debug!("TCP State: {:?}", socket.state()); + debug!("is active: {}", socket.is_active()); + debug!("is open: {}", socket.is_open()); + debug!("can recv: {}", socket.can_recv()); + debug!("can send: {}", socket.can_send()); + debug!("may recv: {}", socket.may_recv()); + debug!("may send: {}", socket.may_send()); + } + + state = match state { + State::Connecting if !socket.is_active() => { + if listen { // Listen to a local port + if !socket.is_open() { + if verbose { + debug!("Listening to {}", port); + } + socket.listen(port).unwrap(); + } + } else { // Connect to a remote port + let local_port = 49152 + random::get_u16() % 16384; + if verbose { + debug!("Connecting to {}:{}", address, port); + } + if socket.connect(cx, (address, port), local_port).is_err() { + error!("Could not connect to {}:{}", address, port); + return usr::shell::ExitCode::CommandError; + } + } + State::Receiving + } + State::Sending if socket.can_recv() => { + if verbose { + debug!("Sending -> Receiving"); + } + State::Receiving + } + State::Sending if socket.can_send() && socket.may_recv() => { + if !read_only { + if verbose { + debug!("Sending ..."); + } + if prompt { + // Print prompt + print!("{}>{} ", Style::color("Cyan"), Style::reset()); + } + let line = io::stdin().read_line(); + if line.is_empty() { + socket.close(); + } else { + let line = line.replace("\n", "\r\n"); + socket.send_slice(line.as_ref()).expect("cannot send"); + } + } + State::Receiving + } + State::Receiving if socket.can_recv() => { + if verbose { + debug!("Receiving ..."); + } + socket.recv(|data| { + let contents = String::from_utf8_lossy(data); + print!("{}", contents.replace("\r\n", "\n")); + (data.len(), ()) + }).unwrap(); + State::Receiving + } + _ if socket.state() == TcpState::SynSent || socket.state() == TcpState::SynReceived => { + state + } + State::Receiving if !socket.may_recv() && !listen => { + if verbose { + debug!("Break from response"); + } + break; + } + State::Receiving if socket.can_send() => { + if verbose { + debug!("Receiving -> Sending"); + } + State::Sending + } + _ if !socket.is_active() && !listen => { + if verbose { + debug!("Break from inactive"); + } + break; + } + _ => state + }; + + if interval > 0.0 { + syscall::sleep(interval); + } + if let Some(wait_duration) = iface.poll_delay(timestamp) { + syscall::sleep((wait_duration.total_micros() as f64) / 1000000.0); + } + } + iface.remove_socket(tcp_handle); + usr::shell::ExitCode::CommandSuccessful + } else { + usr::shell::ExitCode::CommandError + } +} + +fn help() -> usr::shell::ExitCode { + let csi_option = Style::color("LightCyan"); + let csi_title = Style::color("Yellow"); + let csi_reset = Style::reset(); + println!("{}Usage:{} socket {}[] {1}", csi_title, csi_reset, csi_option); + println!(); + println!("{}Options:{}", csi_title, csi_reset); + println!(" {0}-l{1}, {0}--listen{1} Listen to a local port", csi_option, csi_reset); + println!(" {0}-v{1}, {0}--verbose{1} Increase verbosity", csi_option, csi_reset); + println!(" {0}-p{1}, {0}--prompt{1} Display prompt", csi_option, csi_reset); + println!(" {0}-r{1}, {0}--read-only{1} Read only connexion", csi_option, csi_reset); + println!(" {0}-i{1}, {0}--interval