xous-core/services/shellchat/src/cmds/net_cmd.rs
2022-06-14 16:46:37 +08:00

685 lines
No EOL
34 KiB
Rust

use crate::{ShellCmdApi, CommonEnv};
use com::api::NET_MTU;
use xous_ipc::String;
#[cfg(any(target_os = "none", target_os = "xous"))]
use net::XousServerId;
use net::NetPingCallback;
use xous::MessageEnvelope;
use num_traits::*;
use std::net::{IpAddr, TcpStream, TcpListener};
use std::io::Write;
use std::io::Read;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use std::thread;
use std::sync::mpsc;
use dns::Dns; // necessary to work around https://github.com/rust-lang/rust/issues/94182
#[cfg(feature="ditherpunk")]
use std::str::FromStr;
#[cfg(feature="ditherpunk")]
use std::convert::TryInto;
use gam::{Img, RGB};
pub struct NetCmd {
callback_id: Option<u32>,
callback_conn: u32,
dns: Dns,
#[cfg(any(target_os = "none", target_os = "xous"))]
ping: Option<net::Ping>,
}
impl NetCmd {
pub fn new(xns: &xous_names::XousNames) -> Self {
NetCmd {
callback_id: None,
callback_conn: xns.request_connection_blocking(crate::SERVER_NAME_SHELLCHAT).unwrap(),
dns: dns::Dns::new(&xns).unwrap(),
#[cfg(any(target_os = "none", target_os = "xous"))]
ping: None,
}
}
}
#[derive(Debug, num_derive::FromPrimitive, num_derive::ToPrimitive)]
pub(crate) enum NetCmdDispatch {
UdpTest1 = 0x1_0000, // we're muxing our own dispatch + ping dispatch, so we need a custom discriminant
UdpTest2 = 0x1_0001,
}
impl<'a> ShellCmdApi<'a> for NetCmd {
cmd_api!(net); // inserts boilerplate for command API
fn process(&mut self, args: String::<1024>, env: &mut CommonEnv) -> Result<Option<String::<1024>>, xous::Error> {
if self.callback_id.is_none() {
let cb_id = env.register_handler(String::<256>::from_str(self.verb()));
log::trace!("hooking net callback with ID {}", cb_id);
self.callback_id = Some(cb_id);
}
use core::fmt::Write;
let mut ret = String::<1024>::new();
#[cfg(any(target_os = "none", target_os = "xous"))]
let helpstring = "net [udp [rx socket] [tx dest socket]] [ping [host] [count]] [tcpget host/path]";
// no ping in hosted mode -- why would you need it? we're using the host's network connection.
#[cfg(not(any(target_os = "none", target_os = "xous")))]
let helpstring = "net [udp [port]] [count]] [tcpget host/path]";
let mut tokens = args.as_str().unwrap().split(' ');
if let Some(sub_cmd) = tokens.next() {
match sub_cmd {
"unsub" => {
// this is just for testing the unsub call itself. It should result in the connection manager itself breaking.
match env.netmgr.wifi_state_unsubscribe() {
Ok(_) => write!(ret, "wifi unsub successful"),
Err(e) => write!(ret, "wifi unsub error: {:?}", e),
}.ok();
}
"tcpget" => {
// note: to keep shellchat lightweight, we do a very minimal parsing of the URL. We assume it always has
// a form such as:
// bunniefoo.com./bunnie/test.txt
// It will break on everything else. The `url` crate is nice but "large" for a demo.
// There is no https support, obvs.
if let Some(url) = tokens.next() {
match url.split_once('/') {
Some((host, path)) => {
match TcpStream::connect((host, 80)) {
Ok(mut stream) => {
log::trace!("stream open, setting timeouts");
stream.set_read_timeout(Some(Duration::from_millis(10_000))).unwrap();
stream.set_write_timeout(Some(Duration::from_millis(10_000))).unwrap();
log::debug!("read timeout: {:?}", stream.read_timeout().unwrap().unwrap().as_millis());
log::debug!("write timeout: {:?}", stream.write_timeout().unwrap().unwrap().as_millis());
log::info!("my socket: {:?}", stream.local_addr());
log::info!("peer addr: {:?}", stream.peer_addr());
log::info!("sending GET request");
match write!(stream, "GET /{} HTTP/1.1\r\n", path) {
Ok(_) => log::trace!("sent GET"),
Err(e) => {
log::error!("GET err {:?}", e);
write!(ret, "Error sending GET: {:?}", e).unwrap();
}
}
write!(stream, "Host: {}\r\nAccept: */*\r\nUser-Agent: Precursor/0.9.6\r\n", host).expect("stream error");
write!(stream, "Connection: close\r\n").expect("stream error");
write!(stream, "\r\n").expect("stream error");
log::info!("fetching response....");
let mut buf = [0u8; 512];
match stream.read(&mut buf) {
Ok(len) => {
log::trace!("raw response ({}): {:?}", len, &buf[..len]);
write!(ret, "{}", std::string::String::from_utf8_lossy(&buf[..len.min(buf.len())])).ok(); // let it run off the end
log::info!("{}NET.TCPGET,{},{}",
xous::BOOKEND_START,
std::string::String::from_utf8_lossy(&buf[..len.min(buf.len())]),
xous::BOOKEND_END);
}
Err(e) => write!(ret, "Didn't get response from host: {:?}", e).unwrap(),
}
}
Err(e) => write!(ret, "Couldn't connect to {}:80: {:?}", host, e).unwrap(),
}
}
_ => write!(ret, "Usage: tcpget bunniefoo.com/bunnie/test.txt").unwrap(),
}
} else {
write!(ret, "Usage: tcpget bunniefoo.com/bunnie/test.txt").unwrap();
}
}
"server" => {
// this is adapted from https://doc.rust-lang.org/book/ch20-03-graceful-shutdown-and-cleanup.html
thread::spawn({
let boot_instant = env.boot_instant.clone();
move || {
let listener = TcpListener::bind("0.0.0.0:80").unwrap();
// limit to 4 because we're a bit shy on space in shellchat right now; there is a 32-thread limit per process, and shellchat has the kitchen sink.
let pool = ThreadPool::new(4);
for stream in listener.incoming() {
let stream = match stream {
Ok(s) => s,
Err(e) => {
log::warn!("Listener returned error: {:?}", e);
continue;
}
};
pool.execute({
let bi = boot_instant.clone();
move || {
handle_connection(stream, bi);
}
});
}
log::info!("demo server shutting down.");
}
});
write!(ret, "TCP listener started on port 80").unwrap();
log::info!("{}NET.SERVER,{}", xous::BOOKEND_START, xous::BOOKEND_END);
}
"fountain" => {
// anything typed after fountain will cause this to be a short test
let short_test = tokens.next().is_some();
thread::spawn({
let short_test = short_test.clone();
move || {
let tp = threadpool::ThreadPool::new(4);
loop {
let listener = std::net::TcpListener::bind("0.0.0.0:3333");
let listener = match listener {
Ok(listener) => listener,
Err(_) => {
std::thread::sleep(std::time::Duration::from_millis(1000));
continue;
},
};
for i in listener.incoming() {
match i {
Err(error) => {
log::error!("error caught in listener.incoming(): {}", error);
},
Ok(mut stream) => {
tp.execute(move || {
let mut count = 0;
loop {
match std::io::Write::write(&mut stream, format!("hello! {}\n", count).as_bytes()) {
Err(e) => {
log::info!("fountain write failed with error {:?}", e);
break;
}
_ => {}
}
count += 1;
if count == 10 && short_test {
stream.flush().unwrap();
break;
}
}
});
}
}
}
}
}
});
write!(ret, "Fountain started on port 3333").unwrap();
}
// Testing of udp is done with netcat:
// to send packets run `netcat -u <precursor ip address> 6502` on a remote host, and then type some data
// to receive packets, use `netcat -u -l 6502`, on the same remote host, and it should show a packet of counts received
"udp" => {
let socket = if let Some(tok_str) = tokens.next() {
tok_str
} else {
// you could also pass e.g. 127.0.0.1 to check that udp doesn't respond to remote pings, etc.
write!(ret, "Usage: net udp 0.0.0.0:6502 [sender_ip:6502], where sender_ip is only necessary if you want the echo-back").unwrap();
return Ok(Some(ret));
}.to_string();
let (response_addr, do_response) = if let Some(r) = tokens.next() {
(r.to_string(), true)
} else {
(std::string::String::new(), false)
};
use std::net::UdpSocket;
let udp = match UdpSocket::bind(socket.clone()) {
Ok(udp) => udp,
Err(e) => {
write!(ret, "Couldn't bind UDP socket: {:?}\n", e).unwrap();
return Ok(Some(ret));
}
};
udp.set_write_timeout(Some(Duration::from_millis(500))).expect("couldn't set write timeout");
for index in 0..2 {
let _ = std::thread::spawn({
let self_cid = self.callback_conn;
let udp = udp.try_clone().expect("couldn't clone socket");
let response = response_addr.clone();
move || {
const ITERS: usize = 4;
let mut iters = 0;
let mut s = xous_ipc::String::<512>::new();
write!(s, "UDP server {} started", index).unwrap();
s.send(self_cid).unwrap();
loop {
s.clear();
let mut buf = [0u8; NET_MTU];
match udp.recv_from(&mut buf) {
Ok((bytes, addr)) => {
write!(s, "UDP server {} rx {} bytes: {:?}: {}", index, bytes, addr, std::str::from_utf8(&buf[..bytes]).unwrap()).unwrap();
s.send(self_cid).unwrap();
if do_response {
match udp.send_to(
format!("Server {} received {} bytes\r\n", index, bytes).as_bytes(),
&response,
) {
Ok(len) => log::info!("server {} sent response of {} bytes", index, len),
Err(e) => log::info!("server {} UDP tx err: {:?}", index, e),
}
}
}
Err(e) => {
log::error!("UDP rx failed with {:?}", e);
}
}
iters += 1;
if iters >= ITERS {
break;
}
}
s.clear();
write!(s, "UDP server {} rx closed after {} iters", index, iters).unwrap();
s.send(self_cid).unwrap();
}
});
}
write!(ret, "Started multi-threaded UDP responder").unwrap();
}
"dns" => {
if let Some(name) = tokens.next() {
match self.dns.lookup(name) {
Ok(ipaddr) => {
write!(ret, "DNS resolved {}->{:?}", name, ipaddr).unwrap();
}
Err(e) => {
write!(ret, "DNS lookup error: {:?}", e).unwrap();
}
}
}
}
#[cfg(feature="ditherpunk")]
"image" => {
const MAX_IMAGE_LEN: usize = 256 * 1024; // includes HTTP headers
if let Some(url) = tokens.next() {
match url.split_once('/') {
Some((host, path)) => {
match TcpStream::connect((host, 80)) {
Ok(mut stream) => {
let mut heap_image = match xous::syscall::map_memory(
None,
None,
MAX_IMAGE_LEN,
xous::MemoryFlags::R | xous::MemoryFlags::W,
) {
Ok(d) => d,
Err(e) => {
log::error!("couldn't allocate data for image, aborting");
write!(ret, "Couldn't allocate memory for image, aborting: {:?}", e).unwrap();
return Ok(Some(ret));
}
};
stream.set_read_timeout(Some(Duration::from_millis(10_000))).unwrap();
stream.set_write_timeout(Some(Duration::from_millis(10_000))).unwrap();
match write!(stream, "GET /{} HTTP/1.1\r\n", path) {
Ok(_) => log::trace!("sent GET"),
Err(e) => {
log::error!("GET err {:?}", e);
write!(ret, "Error sending GET: {:?}", e).unwrap();
}
}
write!(stream, "Host: {}\r\nAccept: */*\r\nUser-Agent: Precursor/0.9.6\r\n", host).expect("stream error");
write!(stream, "Connection: close\r\n").expect("stream error");
write!(stream, "\r\n").expect("stream error");
log::info!("fetching response....");
let init_len = match stream.read(heap_image.as_slice_mut::<u8>()) {
Ok(init_len) => {
log::info!("fetched {} bytes of http header data", init_len);
init_len
}
Err(e) => {
write!(ret, "Didn't get response from host: {:?}", e).unwrap();
return Ok(Some(ret));
},
};
if let Some(start) = find_subsequence(
heap_image.as_slice::<u8>(),
&[0x0d, 0x0a, 0x0d, 0x0a], // this is \r\n\r\n, which indicates end of header.
) {
log::info!("header is {} bytes long", start);
let header = std::string::String::from_utf8_lossy(&heap_image.as_slice::<u8>()[..start]);
let lines = header.split("\r\n");
let mut content_length = 0;
for line in lines {
let l_line_split = line.to_ascii_lowercase();
let l_line: Vec<&str> = l_line_split.split(':').collect();
if l_line.len() > 1 {
log::info!("attr: {}, {}", l_line[0], l_line[1]);
match l_line[0] {
"content-length" => {
content_length = usize::from_str(l_line[1].trim()).unwrap_or(0);
log::info!("found content-length of {}", content_length);
break;
}
_ => {}
}
}
}
if content_length > 0 {
let remaining_len = content_length - (init_len - start);
log::info!("fetching remaining length of {}", remaining_len);
match stream.read_exact(&mut heap_image.as_slice_mut::<u8>()[init_len..remaining_len]) {
Ok(_) => {
log::info!("read image data remaining len {}", remaining_len);
let modals = modals::Modals::new(&env.xns).unwrap();
let decoder = png::Decoder::new(&heap_image.as_slice::<u8>()[start..start+content_length]);
let mut reader = decoder.read_info().expect("failed to read png info");
let mut buf = vec![0; reader.output_buffer_size()];
let info = reader.next_frame(&mut buf).expect("failed to decode png");
let (width, height) = (info.width, info.height);
let img = Img::new(buf, width as usize);
modals.show_image(&img).expect("show image modal failed");
}
Err(e) => {
log::info!("couldn't read remaining image length: {:?}", e);
}
}
}
}
xous::syscall::unmap_memory(heap_image).expect("couldn't de-allocate image memory");
}
Err(e) => write!(ret, "Couldn't connect to {}:80: {:?}", host, e).unwrap(),
}
}
_ => write!(ret, "Usage: image bunniefoo.com/bunnie/bunny.png").unwrap(),
}
} else {
write!(ret, "Usage: image bunniefoo.com/bunnie/bunny.png").unwrap();
}
}
"tls" => {
write!(ret, "Work in progress. Please check back later!").unwrap();
}
#[cfg(any(target_os = "none", target_os = "xous"))]
"ping" => {
if let Some(name) = tokens.next() {
match self.dns.lookup(name) {
Ok(ipaddr) => {
log::debug!("sending ping to {:?}", ipaddr);
if self.ping.is_none() {
self.ping = Some(net::Ping::non_blocking_handle(
XousServerId::ServerName(xous_ipc::String::from_str(crate::SERVER_NAME_SHELLCHAT)),
self.callback_id.unwrap() as usize,
));
}
if let Some(count_str) = tokens.next() {
let count = count_str.parse::<u32>().unwrap();
if let Some(pinger) = &self.ping {
pinger.ping_spawn_thread(
IpAddr::from(ipaddr),
count as usize,
1000
);
write!(ret, "Sending {} pings to {} ({:?})", count, name, ipaddr).unwrap();
} else {
// this just shouldn't happen based on the structure of the code above.
write!(ret, "Can't ping, internal error.").unwrap();
}
} else {
if let Some(pinger) = &self.ping {
if pinger.ping(IpAddr::from(ipaddr)) {
write!(ret, "Sending a ping to {} ({:?})", name, ipaddr).unwrap();
} else {
write!(ret, "Couldn't send a ping to {}, maybe socket is busy?", name).unwrap();
}
} else {
write!(ret, "Can't ping, internal error.").unwrap();
}
};
}
Err(e) => {
write!(ret, "Can't ping, DNS lookup error: {:?}", e).unwrap();
}
}
} else {
write!(ret, "Missing host: net ping [host] [count]").unwrap();
}
}
_ => {
write!(ret, "{}", helpstring).unwrap();
}
}
} else {
write!(ret, "{}", helpstring).unwrap();
}
Ok(Some(ret))
}
fn callback(&mut self, msg: &MessageEnvelope, _env: &mut CommonEnv) -> Result<Option<String::<1024>>, xous::Error> {
use core::fmt::Write;
log::debug!("net callback");
let mut ret = String::<1024>::new();
match &msg.body {
xous::Message::Scalar(xous::ScalarMessage {id: _, arg1, arg2, arg3, arg4}) => {
let dispatch = *arg1;
match FromPrimitive::from_usize(dispatch) {
Some(NetCmdDispatch::UdpTest1) => {
// Not used after udp to libstd, but left in case we want to repurpose
},
Some(NetCmdDispatch::UdpTest2) => {
// Not used after udp to libstd
},
None => {
// rebind the scalar args to the Ping convention
let op = arg1;
let addr = IpAddr::from((*arg2 as u32).to_be_bytes());
let seq_or_addr = *arg3;
let timestamp = *arg4;
match FromPrimitive::from_usize(op & 0xFF) {
Some(NetPingCallback::Drop) => {
// write!(ret, "Info: All pending pings done").unwrap();
// ignore the message, just creates visual noise
return Ok(None);
}
Some(NetPingCallback::NoErr) => {
match addr {
IpAddr::V4(_) => {
write!(ret, "Pong from {:?} seq {} received: {} ms",
addr,
seq_or_addr,
timestamp).unwrap();
log::info!("{}NET.PONG,{:?},{},{},{}",
xous::BOOKEND_START,
addr,
seq_or_addr,
timestamp,
xous::BOOKEND_END
);
},
IpAddr::V6(_) => {
write!(ret, "Ipv6 pong received: {} ms", timestamp).unwrap();
},
}
}
Some(NetPingCallback::Timeout) => {
write!(ret, "Ping to {:?} timed out", addr).unwrap();
}
Some(NetPingCallback::Unreachable) => {
let code = net::Icmpv4DstUnreachable::from((op >> 24) as u8);
write!(ret, "Ping to {:?} unreachable: {:?}", addr, code).unwrap();
}
None => {
log::error!("Unknown opcode received in NetCmd callback: {:?}", op);
write!(ret, "Unknown opcode received in NetCmd callback: {:?}", op).unwrap();
}
}
},
}
},
xous::Message::Move(m) => {
let s = xous_ipc::String::<512>::from_message(m).unwrap();
write!(ret, "{}", s.as_str().unwrap()).unwrap();
}
_ => {
log::error!("got unecognized message type in callback handler")
}
}
Ok(Some(ret))
}
}
// yep. this is from stackoverflow copypasta
#[cfg(feature="ditherpunk")]
fn find_subsequence(haystack: &[u8], needle: &[u8]) -> Option<usize> {
haystack.windows(needle.len()).position(|window| window == needle)
}
enum Responses {
Uptime,
NotFound,
Buzz,
}
fn handle_connection(mut stream: TcpStream, boot_instant: Instant) {
// the result is implementation dependent, on Xous hardware, this is effectively the same as ticktimer.elapsed_ms()
let elapsed_time = Instant::now().duration_since(boot_instant);
let uptime = std::format!("Hello from Precursor!\n\rI have been up for {}:{:02}:{:02}.\n\r",
(elapsed_time.as_millis() / 3_600_000),
(elapsed_time.as_millis() / 60_000) % 60,
(elapsed_time.as_millis() / 1000) % 60,
);
let mut buffer = [0; 1024];
stream.read(&mut buffer).unwrap();
let get = b"GET / HTTP/1.1\r\n";
let sleep = b"GET /sleep HTTP/1.1\r\n";
let buzz = b"GET /buzz HTTP/1.1\r\n";
let (status_line, response_index) = if buffer.starts_with(get) {
("HTTP/1.1 200 OK", Responses::Uptime)
} else if buffer.starts_with(sleep) {
thread::sleep(Duration::from_secs(5));
("HTTP/1.1 200 OK", Responses::Uptime)
} else if buffer.starts_with(buzz) {
("HTTP/1.1 200 OK", Responses::Buzz)
} else {
("HTTP/1.1 404 NOT FOUND", Responses::NotFound)
};
let contents = match response_index {
Responses::Uptime => {
uptime.as_str()
},
Responses::Buzz => {
let xns = xous_names::XousNames::new().unwrap();
llio::Llio::new(&xns).vibe(llio::VibePattern::Double).ok();
"The motor on the Precursor goes bzz bzz"
}
Responses::NotFound => {
"Ceci n'est pas une page vide"
},
};
let response = format!(
"{}\r\nContent-Length: {}\r\n\r\n{}",
status_line,
contents.len(),
contents
);
stream.write(response.as_bytes()).unwrap();
stream.flush().unwrap();
}
pub struct ThreadPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Message>,
}
type Job = Box<dyn FnOnce() + Send + 'static>;
enum Message {
NewJob(Job),
Terminate,
}
impl ThreadPool {
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}
ThreadPool { workers, sender }
}
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let job = Box::new(f);
self.sender.send(Message::NewJob(job)).unwrap();
}
}
impl Drop for ThreadPool {
fn drop(&mut self) {
log::info!("Sending terminate message to all workers.");
for _ in &self.workers {
self.sender.send(Message::Terminate).unwrap();
}
log::info!("Shutting down all workers.");
for worker in &mut self.workers {
log::info!("Shutting down worker {}", worker.id);
if let Some(thread) = worker.thread.take() {
thread.join().unwrap();
}
}
}
}
struct Worker {
id: usize,
thread: Option<thread::JoinHandle<()>>,
}
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Message>>>) -> Worker {
let thread = thread::spawn(move || loop {
let message = receiver.lock().unwrap().recv().unwrap();
match message {
Message::NewJob(job) => {
log::debug!("Worker {} got a job; executing.", id);
job();
}
Message::Terminate => {
log::info!("Worker {} was told to terminate.", id);
break;
}
}
});
Worker {
id,
thread: Some(thread),
}
}
}