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