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
This commit is contained in:
Vincent Ollivier 2022-05-21 10:46:16 +02:00 committed by GitHub
parent 8d2448281d
commit 530658221d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 354 additions and 25 deletions

View File

@ -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
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>MOROS: Obscure Rust Operating System</title>
</head>
<body>
<h1>MOROS</h1>
</body>
</html>
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:<v@moros.cc>
> RCPT TO:<alice@example.com>
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

View File

@ -86,6 +86,7 @@ impl FileIO for Device {
Device::Null => Err(()),
}
}
fn write(&mut self, buf: &[u8]) -> Result<usize, ()> {
match self {
Device::File(io) => io.write(buf),

View File

@ -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;

View File

@ -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;

View File

@ -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 {}<options> <url>{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
}

View File

@ -28,7 +28,7 @@ pub fn main(_args: &[&str]) -> usr::shell::ExitCode {
let mut send_queue: VecDeque<Vec<u8>> = 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;

View File

@ -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;

View File

@ -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);

View File

@ -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;
}

View File

@ -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),

237
src/usr/socket.rs Normal file
View File

@ -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 <host> and <port>
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::<TcpSocket>(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 {}[<host>] <port>{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 <time>{1} Wait <time> between packets", csi_option, csi_reset);
usr::shell::ExitCode::CommandSuccessful
}

View File

@ -1,9 +1,8 @@
use crate::{sys, usr};
use crate::{sys, usr, debug};
use crate::api::console::Style;
use crate::api::syscall;
use crate::api::random;
use alloc::borrow::ToOwned;
use alloc::string::{String, ToString};
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use core::str::{self, FromStr};
@ -12,15 +11,26 @@ use smoltcp::time::Instant;
use smoltcp::wire::IpAddress;
pub fn main(args: &[&str]) -> usr::shell::ExitCode {
let mut args: Vec<String> = args.iter().map(ToOwned::to_owned).map(ToOwned::to_owned).collect();
let mut verbose = false;
let mut args: Vec<&str> = args.iter().filter_map(|arg| {
match *arg {
"-v" | "--verbose" => {
verbose = true;
None
}
_ => {
Some(*arg)
}
}
}).collect();
// Split <host> and <port>
if args.len() == 2 {
if let Some(i) = args[1].find(':') {
let arg = args[1].clone();
let (host, path) = arg.split_at(i);
args[1] = host.to_string();
args.push(path[1..].to_string());
args[1] = host;
args.push(&path[1..]);
}
}
@ -65,7 +75,7 @@ pub fn main(args: &[&str]) -> usr::shell::ExitCode {
iface.remove_socket(tcp_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(tcp_handle);
return usr::shell::ExitCode::CommandError;
@ -80,7 +90,9 @@ pub fn main(args: &[&str]) -> usr::shell::ExitCode {
state = match state {
State::Connect if !socket.is_active() => {
let local_port = 49152 + random::get_u16() % 16384;
println!("Connecting to {}:{}", address, port);
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;

10
www/test.html Normal file
View File

@ -0,0 +1,10 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>MOROS: Obscure Rust Operating System</title>
</head>
<body>
<h1>MOROS</h1>
</body>
</html>