diff --git a/.cargo/config.toml b/.cargo/config.toml index 640b867..f2a0a78 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,8 +1,8 @@ [target.'cfg(all(target_arch = "arm", target_os = "none"))'] -runner = "elf2uf2-rs -d -s -t" +runner = "sudo /home/arkitu/.cargo/bin/elf2uf2-rs -d -s" [build] -target = "thumbv6m-none-eabi" # Cortex-M0 and Cortex-M0+ +target = "thumbv6m-none-eabi" # Cortex-M0 and Cortex-M0+ [env] DEFMT_LOG = "debug" diff --git a/Cargo.lock b/Cargo.lock index f378d22..9ba2abc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -68,6 +68,12 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bit-set" version = "0.8.0" @@ -1098,6 +1104,7 @@ checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" name = "pico-website" version = "0.1.0" dependencies = [ + "base64", "cortex-m", "cortex-m-rt", "cyw43", @@ -1122,6 +1129,7 @@ dependencies = [ "ringbuffer", "serde", "serde-json-core", + "sha1", "static_cell", ] @@ -1420,6 +1428,17 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2-const-stable" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 960ff39..f90062a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,9 +7,11 @@ edition = "2024" wifi-connect = [ "dep:serde-json-core", "dep:serde", -] # you need to add a wifi.conf file for this to work +] # you need to add a wifi.json file for this to work dhcp = ["dep:dhcparse"] dns = ["dep:dnsparse"] +chat = ["dep:ringbuffer"] +ttt = [] default = ["dhcp", "dns"] [dependencies] @@ -58,5 +60,7 @@ serde = { version = "*", optional = true, default-features = false, features = [ ] } dhcparse = { version = "*", default-features = false, optional = true } dnsparse = { version = "*", optional = true } -ringbuffer = { version = "*", default-features = false } -percent-encoding = { version = "*", default-features = false } \ No newline at end of file +ringbuffer = { version = "*", default-features = false, optional = true } +percent-encoding = { version = "*", default-features = false } +sha1 = { version = "*", default-features = false } +base64 = { version = "*", default-features = false } diff --git a/src/apps/chat.rs b/src/apps/chat.rs index 66d9bb9..eb9c3f4 100644 --- a/src/apps/chat.rs +++ b/src/apps/chat.rs @@ -19,12 +19,12 @@ const MSG_SIZE: usize = 128; static MESSAGES: Mutex = Mutex::new(Messages::new()); pub struct ChatApp { - res_buf: Vec, + res_buf: String<1100>, } impl ChatApp { pub fn new() -> Self { Self { - res_buf: Vec::new(), + res_buf: String::new(), } } } @@ -34,13 +34,14 @@ impl App for ChatApp { } async fn handle_request<'a>( &'a mut self, + _host: &str, path: &str, req_type: HttpRequestType, content: &str, - ) -> (HttpResCode, &'static str, &'a [u8]) { + ) -> (HttpResCode, &'static str, &'a str) { match (req_type, path) { (HttpRequestType::Get, "/" | "/index" | "/index.html" | "/chat" | "/chat.html") => { - (HttpResCode::Ok, "html", include_bytes!("./chat.html")) + (HttpResCode::Ok, "html", include_str!("./chat.html")) } (_, path) => { let (path, args) = path.split_once('?').unwrap_or((path, "")); @@ -53,7 +54,7 @@ impl App for ChatApp { Some(("load", n)) => { let n: u16 = match n.parse() { Ok(v) => v, - Err(_) => return (HttpResCode::BadRequest, "", &[]), + Err(_) => return (HttpResCode::BadRequest, "", ""), }; if n > 0 { load = Some(n); @@ -63,11 +64,11 @@ impl App for ChatApp { let mut name = String::::new(); for c in percent_decode_str(u) { if let Err(_) = name.push(c as char) { - return (HttpResCode::BadRequest, "", &[]); + return (HttpResCode::BadRequest, "", ""); } } if u.len() < USERNAME_MIN_SIZE { - return (HttpResCode::BadRequest, "", &[]); + return (HttpResCode::BadRequest, "", ""); } username = Some(name); } @@ -83,7 +84,7 @@ impl App for ChatApp { Some(Ok(c)) => c, _ => { warn!("Invalid percent encoding of msg argument"); - return (HttpResCode::BadRequest, "", &[]); + return (HttpResCode::BadRequest, "", ""); } }; i += 2; @@ -92,7 +93,7 @@ impl App for ChatApp { m.as_bytes()[i] }; if let Err(_) = msg.push(c) { - return (HttpResCode::BadRequest, "", &[]); + return (HttpResCode::BadRequest, "", ""); } i += 1; } @@ -101,7 +102,7 @@ impl App for ChatApp { Ok(msg) => msg, Err(_) => { warn!("Invalid utf8 msg argument"); - return (HttpResCode::BadRequest, "", &[]); + return (HttpResCode::BadRequest, "", ""); } }); } @@ -168,17 +169,17 @@ impl App for ChatApp { } else if path.starts_with("/chat/message/") && path.len() > 14 { let msg_id: u16 = match path[14..].parse() { Ok(n) => n, - Err(_) => return (HttpResCode::BadRequest, "", &[]), + Err(_) => return (HttpResCode::BadRequest, "", ""), }; let msgs = MESSAGES.lock().await; if msg_id > msgs.next { - return (HttpResCode::BadRequest, "", &[]); + return (HttpResCode::BadRequest, "", ""); } self.res_buf.clear(); unwrap(write!(&mut self.res_buf, "
- + + +

Apps

diff --git a/src/apps/index.rs b/src/apps/index.rs index 59b02dd..ad6d393 100644 --- a/src/apps/index.rs +++ b/src/apps/index.rs @@ -9,15 +9,16 @@ impl App for IndexApp { } async fn handle_request<'a>( &'a mut self, + _host: &str, path: &str, _req_type: HttpRequestType, _content: &str, - ) -> (HttpResCode, &'static str, &'a [u8]) { + ) -> (HttpResCode, &'static str, &'a str) { match path { "/" | "/index" | "/index.html" => { - (HttpResCode::Ok, "html", include_bytes!("./index.html")) + (HttpResCode::Ok, "html", include_str!("./index.html")) } - _ => (HttpResCode::NotFound, "", &[]), + _ => (HttpResCode::NotFound, "", ""), } } } diff --git a/src/apps/mod.rs b/src/apps/mod.rs index d941728..8ff1aa4 100644 --- a/src/apps/mod.rs +++ b/src/apps/mod.rs @@ -1,15 +1,18 @@ use crate::socket::{HttpRequestType, HttpResCode}; +#[cfg(feature="chat")] pub mod chat; pub mod index; +#[cfg(feature = "ttt")] pub mod ttt; pub trait App { fn socket_name(&self) -> &'static str; async fn handle_request<'a>( &'a mut self, + host: &str, path: &str, req_type: HttpRequestType, content: &str, - ) -> (HttpResCode, &'static str, &'a [u8]); + ) -> (HttpResCode, &'static str, &'a str); } diff --git a/src/apps/ttt.rs b/src/apps/ttt.rs index beee258..73d07cd 100644 --- a/src/apps/ttt.rs +++ b/src/apps/ttt.rs @@ -1,7 +1,7 @@ use core::fmt::Write; use core::{ops::Not, sync::atomic::Ordering}; use embassy_time::{Duration, Instant}; -use heapless::Vec; +use heapless::String; use pico_website::unwrap; use portable_atomic::{AtomicBool, AtomicU32}; @@ -14,7 +14,7 @@ static TURN: AtomicBool = AtomicBool::new(false); static BOARD: AtomicU32 = AtomicU32::new(0); pub struct TttApp { - res_buf: Vec, + res_buf: String<2048>, /// State of the board last time it has been sent last_board: u32, team: Team, @@ -23,7 +23,7 @@ pub struct TttApp { impl TttApp { pub fn new(team: Team) -> Self { Self { - res_buf: Vec::new(), + res_buf: String::new(), last_board: 0, team, end: None, @@ -73,11 +73,11 @@ impl TttApp { board: u32, turn: Team, outer_html: bool, - ) -> &'a [u8] { + ) -> &'a str { self.res_buf.clear(); if outer_html { - unwrap(self.res_buf.extend_from_slice( - b"
unwrap(write!(self.res_buf, "

Draw!


",)).await, None => {} } - unwrap(self.res_buf.extend_from_slice(b"
")).await; + unwrap(self.res_buf.push_str("
")).await; for c in 0..=8 { let picked_by = if board & (1 << c) != 0 { Some(Team::Zero) @@ -133,18 +133,14 @@ impl TttApp { c )).await; } else { - unwrap( - self.res_buf - .extend_from_slice(b"
"), - ) - .await; + unwrap(self.res_buf.push_str("
")).await; } } }; } - unwrap(self.res_buf.extend_from_slice(b"
")).await; + unwrap(self.res_buf.push_str("
")).await; if outer_html { - unwrap(self.res_buf.extend_from_slice(b"
")).await; + unwrap(self.res_buf.push_str("
")).await; } &self.res_buf } @@ -156,13 +152,14 @@ impl App for TttApp { } async fn handle_request<'a>( &'a mut self, + _host: &str, path: &str, _req_type: HttpRequestType, _content: &str, - ) -> (HttpResCode, &'static str, &'a [u8]) { + ) -> (HttpResCode, &'static str, &'a str) { match path { "/" | "/index" | "/index.html" | "/ttt" | "/ttt.html" => { - (HttpResCode::Ok, "html", include_bytes!("./ttt.html")) + (HttpResCode::Ok, "html", include_str!("./ttt.html")) } "/ttt/initial_game" => { let board = BOARD.load(Ordering::Acquire); @@ -184,11 +181,11 @@ impl App for TttApp { unwrap(path.chars().nth(9).ok_or("no 9th char")).await, ) { Ok(c) => c, - Err(_) => return (HttpResCode::NotFound, "", &[]), + Err(_) => return (HttpResCode::NotFound, "", ""), }; if board & ((1 << (clicked_c as u32)) + (1 << (9 + clicked_c as u32))) != 0 { - return (HttpResCode::Forbidden, "", &[]); + return (HttpResCode::Forbidden, "", ""); } board = board | (1 << ((self.team as u32 * 9) + clicked_c as u32)); turn = (!self.team).into(); @@ -204,10 +201,10 @@ impl App for TttApp { self.generate_board_res(board, turn.into(), false).await, ) } else { - (HttpResCode::NoContent, "", &[]) + (HttpResCode::NoContent, "", "") } } else { - (HttpResCode::NotFound, "", &[]) + (HttpResCode::NotFound, "", "") } } } diff --git a/src/dns.rs b/src/dns.rs index 938025e..7e35e24 100644 --- a/src/dns.rs +++ b/src/dns.rs @@ -27,6 +27,7 @@ pub async fn dns_server(stack: Stack<'static>) { info!("Starting DNS server"); loop { + Timer::after_secs(0).await; let (n, meta) = unwrap(socket.recv_from(&mut buf).await).await; let msg = match dnsparse::Message::parse(&mut buf[..n]) { @@ -42,7 +43,6 @@ pub async fn dns_server(stack: Stack<'static>) { "Dns: Received unknown dns opcode ({:?}), ignoring", msg.header().opcode() ); - Timer::after_micros(10).await; continue; } let mut res = Message::builder(&mut res_buf) diff --git a/src/main.rs b/src/main.rs index 05a52b7..46bd2b8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,10 @@ #![allow(async_fn_in_trait)] #![feature(impl_trait_in_assoc_type)] #![feature(slice_split_once)] +#![feature(try_blocks)] + +#[cfg(feature = "wifi-connect")] +use core::net::Ipv4Addr; use cyw43_pio::{DEFAULT_CLOCK_DIVIDER, PioSpi}; use embassy_executor::Spawner; @@ -18,6 +22,8 @@ use pico_website::unwrap; use rand_core::RngCore; use static_cell::StaticCell; use {defmt_rtt as _, panic_probe as _}; +use log::info; +use embassy_time::Timer; #[cfg(feature = "dhcp")] mod dhcp; @@ -57,6 +63,13 @@ async fn main(spawner: Spawner) { spawner.spawn(logger_task(driver)).unwrap(); let mut rng = RoscRng; + // let mut i = 0; + // loop { + // info!("test{}", i); + // Timer::after_secs(1).await; + // i += 1; + // } + let fw = include_bytes!("../cyw43-firmware/43439A0.bin"); let clm = include_bytes!("../cyw43-firmware/43439A0_clm.bin"); let pwr = Output::new(p.PIN_23, Level::Low); @@ -106,7 +119,7 @@ async fn main(spawner: Spawner) { dns_servers: heapless::Vec::new(), gateway: None, }), - None => Config::dhcpv4(DhcpConfig::default()), + None => Config::dhcpv4(embassy_net::DhcpConfig::default()), }; (wifi_conf, config) }; @@ -170,11 +183,16 @@ async fn main(spawner: Spawner) { unwrap(spawner.spawn(dns::dns_server(stack))).await; unwrap(spawner.spawn(socket::index_listen_task(stack, 80))).await; - unwrap(spawner.spawn(socket::ttt_listen_task(stack, apps::ttt::Team::Zero, 8080))).await; - unwrap(spawner.spawn(socket::ttt_listen_task(stack, apps::ttt::Team::One, 8081))).await; + #[cfg(feature = "ttt")] + { + unwrap(spawner.spawn(socket::ttt_listen_task(stack, apps::ttt::Team::Zero, 8080))).await; + unwrap(spawner.spawn(socket::ttt_listen_task(stack, apps::ttt::Team::One, 8081))).await; + } + #[cfg(feature = "chat")] for _ in 0..4 { unwrap(spawner.spawn(socket::chat_listen_task(stack, 8082))).await; } + info!("All apps lauched!"); } #[cfg(feature = "wifi-connect")] diff --git a/src/socket.rs b/src/socket.rs index ae15454..2f2c700 100644 --- a/src/socket.rs +++ b/src/socket.rs @@ -1,29 +1,36 @@ +use base64::{EncodeSliceError, prelude::*}; use core::fmt::Write; use core::str::from_utf8; +use defmt::dbg; use embassy_net::tcp::TcpSocket; use embassy_time::{Duration, Timer}; use embedded_io_async::Write as _; -use heapless::Vec; +use heapless::{String, Vec}; use log::{info, warn}; +use sha1::{Digest, Sha1}; -use crate::apps::{App, chat, index::IndexApp, ttt}; +use crate::apps; +pub mod ws; + +#[cfg(feature = "ttt")] #[embassy_executor::task(pool_size = 2)] -pub async fn ttt_listen_task(stack: embassy_net::Stack<'static>, team: ttt::Team, port: u16) { - listen_task(stack, ttt::TttApp::new(team), port).await +pub async fn ttt_listen_task(stack: embassy_net::Stack<'static>, team: apps::ttt::Team, port: u16) { + listen_task(stack, apps::ttt::TttApp::new(team), port).await } #[embassy_executor::task(pool_size = 2)] pub async fn index_listen_task(stack: embassy_net::Stack<'static>, port: u16) { - listen_task(stack, IndexApp, port).await + listen_task(stack, apps::index::IndexApp, port).await } +#[cfg(feature = "chat")] #[embassy_executor::task(pool_size = 4)] pub async fn chat_listen_task(stack: embassy_net::Stack<'static>, port: u16) { - listen_task(stack, chat::ChatApp::new(), port).await + listen_task(stack, apps::chat::ChatApp::new(), port).await } -pub async fn listen_task(stack: embassy_net::Stack<'static>, mut app: impl App, port: u16) { +pub async fn listen_task(stack: embassy_net::Stack<'static>, mut app: impl apps::App, port: u16) { // loop { // info!("team:{:?}", team); // Timer::after_millis(0).await; @@ -31,7 +38,7 @@ pub async fn listen_task(stack: embassy_net::Stack<'static>, mut app: impl App, let mut rx_buffer = [0; 1024]; let mut tx_buffer = [0; 2048]; let mut buf = [0; 1024]; - let mut res_head_buf = Vec::::new(); + let mut res_head_buf = Vec::::new(); loop { Timer::after_secs(0).await; @@ -75,8 +82,10 @@ pub async fn listen_task(stack: embassy_net::Stack<'static>, mut app: impl App, } }; - // let mut headers = headers.split(|x| *x == b'\n'); - let (request_type, path) = match headers.lines().next() { + info!("\n{:?}\n", headers); + + let mut hl = headers.lines(); + let (request_type, path) = match hl.next() { None => { warn!("Empty request"); break; @@ -106,40 +115,100 @@ pub async fn listen_task(stack: embassy_net::Stack<'static>, mut app: impl App, ) } }; + let mut host = None; + let mut ws_handshake = false; + let mut ws_key = None; + for h in hl { + let Some((name, val)) = h.split_once(':') else { + continue; + }; + let name = name.trim(); + let val = val.trim(); + match (name, val) { + ("Host", _) => host = Some(val), + ("Upgrade", "websocket") => ws_handshake = true, + ("Sec-WebSocket-Key", _) => ws_key = Some(val), + _ => {} + } + } + let Some(host) = host else { + warn!("No host"); + break; + }; info!( - "Socket {}: {:?} request for {}", + "Socket {}: {:?}{} request for {}{}", app.socket_name(), request_type, - path + if ws_handshake { " websocket" } else { "" }, + host, + path, ); Timer::after_secs(0).await; - let (code, res_type, res_content): (HttpResCode, &str, &[u8]) = match path { - "/htmx.js" => ( - HttpResCode::Ok, - "javascript", - #[cfg(debug_assertions)] - include_bytes!("../static/htmx.js"), - #[cfg(not(debug_assertions))] - include_bytes!("../static/htmx.min.js"), - ), - p => app.handle_request(p, request_type, content).await, + res_head_buf.clear(); + let res_content: Result<&str, core::fmt::Error> = try { + if ws_handshake { + let Some(key) = ws_key else { + warn!("No ws key!"); + break; + }; + let accept = match compute_ws_accept(key).await { + Ok(a) => a, + Err(e) => { + warn!("compute ws accept error : {:?}", e); + break; + } + }; + write!( + &mut res_head_buf, + "{}\r\n\ + Upgrade: websocket\r\n\ + Connection: Upgrade\r\n\ + Sec-WebSocket-Accept: {}\r\n\r\n", + // Sec-WebSocket-Protocol: chat\r\n + Into::<&str>::into(HttpResCode::SwitchingProtocols), + accept + )?; + "" + } else { + let (code, res_type, res_content): (HttpResCode, &str, &str) = match path { + "/htmx.js" => ( + HttpResCode::Ok, + "javascript", + #[cfg(debug_assertions)] + include_str!("../static/htmx.js"), + #[cfg(not(debug_assertions))] + include_bytes!("../static/htmx.min.js"), + ), + _ => app.handle_request(host, path, request_type, content).await, + }; + + write!(&mut res_head_buf, "{}", Into::<&str>::into(code))?; + if res_type.len() > 0 { + write!( + &mut res_head_buf, + "\r\n\ + Content-Type: text/{}\r\n\ + Content-Length: {}\r\n", + res_type, + res_content.len() + )?; + } + write!(&mut res_head_buf, "\r\n\r\n")?; + res_content + } }; - res_head_buf.clear(); - if let Err(e) = write!( - &mut res_head_buf, - "{}\r\n\ - Content-Type: text/{}\r\n\ - Content-Length: {}\r\n\r\n", - Into::<&str>::into(code), - res_type, - res_content.len() - ) { - warn!("res buffer write error: {:?}", e); - break; - } + let res_content = match res_content { + Ok(rc) => rc, + Err(e) => { + warn!("res buffer write error: {:?}", e); + break; + } + }; + + info!("\n{}\n", from_utf8(&res_head_buf).unwrap()); match socket.write_all(&res_head_buf).await { Ok(()) => {} @@ -148,7 +217,7 @@ pub async fn listen_task(stack: embassy_net::Stack<'static>, mut app: impl App, break; } }; - match socket.write_all(&res_content).await { + match socket.write_all(res_content.as_bytes()).await { Ok(()) => {} Err(e) => { warn!("write error: {:?}", e); @@ -175,6 +244,7 @@ impl Into<&str> for HttpRequestType { #[derive(Debug, Clone, Copy)] pub enum HttpResCode { + SwitchingProtocols, Ok, NoContent, BadRequest, @@ -184,6 +254,7 @@ pub enum HttpResCode { impl Into<&str> for HttpResCode { fn into(self) -> &'static str { match self { + HttpResCode::SwitchingProtocols => "HTTP/1.1 101 Switching Protocols", HttpResCode::Ok => "HTTP/1.1 200 OK", HttpResCode::NoContent => "HTTP/1.1 204 NO CONTENT", HttpResCode::BadRequest => "HTTP/1.1 400 BAD REQUEST", @@ -192,3 +263,14 @@ impl Into<&str> for HttpResCode { } } } + +async fn compute_ws_accept(key: &str) -> Result, EncodeSliceError> { + let mut res = Vec::::new(); + res.extend_from_slice(&[0; 28]).unwrap(); + let mut hasher = Sha1::new(); + hasher.update(key.as_bytes()); + hasher.update(b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); + let hash = hasher.finalize(); + BASE64_STANDARD.encode_slice(hash, &mut res)?; + Ok(String::from_utf8(res).unwrap()) +} diff --git a/src/socket/ws.rs b/src/socket/ws.rs new file mode 100644 index 0000000..e9e75b6 --- /dev/null +++ b/src/socket/ws.rs @@ -0,0 +1,6 @@ +// pub struct Ws { + +// } +// impl Ws { +// pub fn handshake +// }