|
| 1 | +//! Async TLS "Hello, world" HTTP/1.0 server using `EspAsyncTls::negotiate_server` |
| 2 | +//! (ESP-IDF >= 5.5.0, mbedTLS). |
| 3 | +//! |
| 4 | +//! Multiple connections are served concurrently, each by its own task spawned |
| 5 | +//! on a local executor. |
| 6 | +//! |
| 7 | +//! Add your own ssid and password, flash, then point your browser to |
| 8 | +//! `https://<esp-ip>` (the demo cert is self-signed, so accept the browser |
| 9 | +//! warning), or run: |
| 10 | +//! |
| 11 | +//! ```text |
| 12 | +//! curl -k https://<esp-ip> |
| 13 | +//! ``` |
| 14 | +
|
| 15 | +#![allow(unknown_lints)] |
| 16 | +#![allow(unexpected_cfgs)] |
| 17 | + |
| 18 | +#[cfg(all( |
| 19 | + not(any(esp32h2, esp32h4, esp32p4)), |
| 20 | + esp_idf_esp_tls_using_mbedtls, |
| 21 | + esp_idf_version_at_least_5_5_0, |
| 22 | +))] |
| 23 | +fn main() -> anyhow::Result<()> { |
| 24 | + example::main() |
| 25 | +} |
| 26 | + |
| 27 | +#[cfg(any(esp32h2, esp32h4, esp32p4))] |
| 28 | +fn main() -> anyhow::Result<()> { |
| 29 | + panic!("ESP32-H2, ESP32-H4 and ESP32-P4 do not have a Wifi radio (but you could enable the esp-wifi-remote component to use them with a WiFi co-processor)"); |
| 30 | +} |
| 31 | + |
| 32 | +#[cfg(all( |
| 33 | + not(any(esp32h2, esp32h4, esp32p4)), |
| 34 | + not(all(esp_idf_esp_tls_using_mbedtls, esp_idf_version_at_least_5_5_0)), |
| 35 | +))] |
| 36 | +fn main() -> anyhow::Result<()> { |
| 37 | + panic!( |
| 38 | + "This example requires ESP-IDF ≥ 5.5.0 with the mbedTLS ESP-TLS stack \ |
| 39 | + (CONFIG_ESP_TLS_USING_MBEDTLS=y) for EspAsyncTls::negotiate_server." |
| 40 | + ); |
| 41 | +} |
| 42 | + |
| 43 | +#[cfg(all( |
| 44 | + not(any(esp32h2, esp32h4, esp32p4)), |
| 45 | + esp_idf_esp_tls_using_mbedtls, |
| 46 | + esp_idf_version_at_least_5_5_0, |
| 47 | +))] |
| 48 | +mod example { |
| 49 | + use core::cell::Cell; |
| 50 | + use core::pin::pin; |
| 51 | + |
| 52 | + use std::net::{TcpListener, TcpStream}; |
| 53 | + use std::os::fd::{AsRawFd, IntoRawFd}; |
| 54 | + use std::rc::Rc; |
| 55 | + use std::time::Duration; |
| 56 | + |
| 57 | + use async_io::Async; |
| 58 | + |
| 59 | + use futures::executor::{LocalPool, LocalSpawner}; |
| 60 | + use futures::future::{select, Either}; |
| 61 | + use futures::task::LocalSpawnExt; |
| 62 | + |
| 63 | + use esp_idf_svc::log::EspLogger; |
| 64 | + use esp_idf_svc::sys::EspError; |
| 65 | + use esp_idf_svc::timer::EspTaskTimerService; |
| 66 | + use esp_idf_svc::tls::{EspAsyncTls, ServerConfig, X509}; |
| 67 | + |
| 68 | + use log::{info, warn}; |
| 69 | + |
| 70 | + const SSID: &str = env!("WIFI_SSID"); |
| 71 | + const PASSWORD: &str = env!("WIFI_PASS"); |
| 72 | + |
| 73 | + const PORT: u16 = 443; |
| 74 | + |
| 75 | + /// Each TLS session allocates tens of KBs of heap (mbedTLS buffers), so |
| 76 | + /// too many concurrent sessions exhaust the heap and fail with |
| 77 | + /// `MBEDTLS_ERR_SSL_ALLOC_FAILED` (-0x7F00); connections beyond the cap |
| 78 | + /// are dropped at accept time. Browsers easily hit this by opening |
| 79 | + /// several speculative sockets at once. |
| 80 | + const MAX_SESSIONS: usize = 2; |
| 81 | + |
| 82 | + /// Drop peers that never finish the handshake. `tls_handshake_timeout_ms` |
| 83 | + /// is only honored by the blocking `EspTls::negotiate_server`, so the async |
| 84 | + /// handshake is raced against a timer instead. |
| 85 | + const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10); |
| 86 | + |
| 87 | + // Demo self-signed cert/key. Generate your own with: |
| 88 | + // openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem \ |
| 89 | + // -days 3650 -nodes -subj "/CN=esp32.local" |
| 90 | + // Trailing "\0": ESP-TLS expects NUL-terminated PEM. |
| 91 | + const SERVER_CERT: &[u8] = concat!(include_str!("tls_server_cert.pem"), "\0").as_bytes(); |
| 92 | + const SERVER_KEY: &[u8] = concat!(include_str!("tls_server_key.pem"), "\0").as_bytes(); |
| 93 | + |
| 94 | + /// The canned response served to every request. HTTP/1.0 with |
| 95 | + /// `Connection: close`, so the connection is simply closed after the |
| 96 | + /// response and no keep-alive/chunked machinery is necessary. |
| 97 | + const RESPONSE: &[u8] = b"HTTP/1.0 200 OK\r\n\ |
| 98 | + Connection: close\r\n\ |
| 99 | + Content-Type: text/plain\r\n\ |
| 100 | + Content-Length: 13\r\n\ |
| 101 | + \r\n\ |
| 102 | + Hello, world!"; |
| 103 | + |
| 104 | + pub fn main() -> anyhow::Result<()> { |
| 105 | + esp_idf_svc::sys::link_patches(); |
| 106 | + EspLogger::initialize_default(); |
| 107 | + |
| 108 | + // This thread is necessary because the ESP IDF main task thread is running with a very low priority that cannot be raised |
| 109 | + // (lower than the hidden posix thread in `async-io-mini`) |
| 110 | + // As a result, the main thread is constantly starving because of the higher prio `async-io-mini` thread |
| 111 | + // |
| 112 | + // To use async networking IO, make your `main()` minimal by just spawning all work in a new thread |
| 113 | + std::thread::Builder::new() |
| 114 | + // Enough for the mbedTLS handshake processing; the large TLS buffers |
| 115 | + // live on the heap. Note that on ESP-IDF thread stacks are allocated |
| 116 | + // from the heap too, so an over-sized stack directly reduces the |
| 117 | + // memory available for TLS sessions |
| 118 | + .stack_size(20000) |
| 119 | + .spawn(run_main) |
| 120 | + .unwrap() |
| 121 | + .join() |
| 122 | + .unwrap() |
| 123 | + } |
| 124 | + |
| 125 | + fn run_main() -> anyhow::Result<()> { |
| 126 | + // `async-io-mini` uses the ESP IDF `eventfd` syscall to implement async IO. |
| 127 | + // If you use `tokio`, you still have to do the same as it also uses the `eventfd` syscall |
| 128 | + let _mounted_eventfs = esp_idf_svc::io::vfs::MountedEventfs::mount(5)?; |
| 129 | + |
| 130 | + // A local executor rather than a plain `block_on`, so that each accepted |
| 131 | + // connection can be served concurrently by its own spawned task |
| 132 | + let mut pool = LocalPool::new(); |
| 133 | + let spawner = pool.spawner(); |
| 134 | + |
| 135 | + pool.run_until(async move { |
| 136 | + let wifi = wifi_create().await?; // Keep it around so that the wifi connection is not dropped |
| 137 | + |
| 138 | + let ip_info = wifi.sta_netif().get_ip_info()?; |
| 139 | + |
| 140 | + run(&spawner, ip_info.ip).await |
| 141 | + }) |
| 142 | + } |
| 143 | + |
| 144 | + async fn run(spawner: &LocalSpawner, ip: impl core::fmt::Display) -> anyhow::Result<()> { |
| 145 | + let listener = Async::<TcpListener>::bind(([0, 0, 0, 0], PORT))?; |
| 146 | + |
| 147 | + info!( |
| 148 | + "TLS Hello-World server on port {PORT}; point your browser to https://{ip}:{PORT} \ |
| 149 | + (accept the self-signed cert warning) or run `curl -k https://{ip}:{PORT}`" |
| 150 | + ); |
| 151 | + |
| 152 | + // All tasks run on the same thread, so a plain `Rc<Cell>` is enough |
| 153 | + // to track the number of active sessions |
| 154 | + let active = Rc::new(Cell::new(0)); |
| 155 | + |
| 156 | + loop { |
| 157 | + let (stream, peer) = listener.accept().await?; |
| 158 | + |
| 159 | + if active.get() >= MAX_SESSIONS { |
| 160 | + warn!("Too many sessions, dropping {peer}"); |
| 161 | + continue; |
| 162 | + } |
| 163 | + |
| 164 | + info!("Accepted {peer}"); |
| 165 | + |
| 166 | + active.set(active.get() + 1); |
| 167 | + |
| 168 | + let active = active.clone(); |
| 169 | + |
| 170 | + spawner.spawn_local(async move { |
| 171 | + if let Err(e) = handle(stream).await { |
| 172 | + warn!("Connection to {peer} failed: {e:?}"); |
| 173 | + } |
| 174 | + |
| 175 | + active.set(active.get() - 1); |
| 176 | + })?; |
| 177 | + } |
| 178 | + } |
| 179 | + |
| 180 | + async fn handle(stream: Async<TcpStream>) -> anyhow::Result<()> { |
| 181 | + let mut tls = EspAsyncTls::adopt(EspTlsSocket::new(stream))?; |
| 182 | + |
| 183 | + let cfg = ServerConfig { |
| 184 | + server_cert: Some(X509::pem_until_nul(SERVER_CERT)), |
| 185 | + server_key: Some(X509::pem_until_nul(SERVER_KEY)), |
| 186 | + ..ServerConfig::new() |
| 187 | + }; |
| 188 | + |
| 189 | + let mut timer = EspTaskTimerService::new()?.timer_async()?; |
| 190 | + |
| 191 | + { |
| 192 | + let tls_task = pin!(tls.negotiate_server(&cfg)); |
| 193 | + let timer_task = pin!(timer.after(HANDSHAKE_TIMEOUT)); |
| 194 | + |
| 195 | + match select(tls_task, timer_task).await { |
| 196 | + Either::Left((res, _)) => res?, |
| 197 | + Either::Right(_) => anyhow::bail!("Handshake timed out"), |
| 198 | + } |
| 199 | + } |
| 200 | + |
| 201 | + info!("Handshake complete"); |
| 202 | + |
| 203 | + // Read the request up to the end of its headers (an empty line); |
| 204 | + // this demo replies the same to every request, so the contents are ignored. |
| 205 | + // Requests with a body (e.g. POST) are not handled, as the body would |
| 206 | + // arrive after the headers and is simply never read. |
| 207 | + let mut buf = [0; 1024]; |
| 208 | + let mut len = 0; |
| 209 | + |
| 210 | + loop { |
| 211 | + let n = tls.read(&mut buf[len..]).await?; |
| 212 | + if n == 0 { |
| 213 | + info!("Peer closed the connection mid-request"); |
| 214 | + return Ok(()); |
| 215 | + } |
| 216 | + |
| 217 | + len += n; |
| 218 | + |
| 219 | + if buf[..len].windows(4).any(|w| w == b"\r\n\r\n") { |
| 220 | + break; |
| 221 | + } |
| 222 | + |
| 223 | + if len == buf.len() { |
| 224 | + // Request headers bigger than our buffer; just respond anyway |
| 225 | + break; |
| 226 | + } |
| 227 | + } |
| 228 | + |
| 229 | + tls.write_all(RESPONSE).await?; |
| 230 | + |
| 231 | + info!("Request served"); |
| 232 | + |
| 233 | + Ok(()) |
| 234 | + } |
| 235 | + |
| 236 | + async fn wifi_create() -> Result<esp_idf_svc::wifi::EspWifi<'static>, EspError> { |
| 237 | + use esp_idf_svc::eventloop::*; |
| 238 | + use esp_idf_svc::hal::peripherals::Peripherals; |
| 239 | + use esp_idf_svc::nvs::*; |
| 240 | + use esp_idf_svc::timer::*; |
| 241 | + use esp_idf_svc::wifi::*; |
| 242 | + |
| 243 | + let sys_loop = EspSystemEventLoop::take().unwrap(); |
| 244 | + let timer_service = EspTimerService::new().unwrap(); |
| 245 | + let nvs = EspDefaultNvsPartition::take().unwrap(); |
| 246 | + |
| 247 | + let peripherals = Peripherals::take()?; |
| 248 | + |
| 249 | + let mut esp_wifi = EspWifi::new(peripherals.modem, sys_loop.clone(), Some(nvs.clone()))?; |
| 250 | + let mut wifi = AsyncWifi::wrap(&mut esp_wifi, sys_loop.clone(), timer_service.clone())?; |
| 251 | + |
| 252 | + wifi.set_configuration(&Configuration::Client(ClientConfiguration { |
| 253 | + ssid: SSID.try_into().unwrap(), |
| 254 | + password: PASSWORD.try_into().unwrap(), |
| 255 | + ..Default::default() |
| 256 | + }))?; |
| 257 | + |
| 258 | + wifi.start().await?; |
| 259 | + info!("Wifi started"); |
| 260 | + |
| 261 | + wifi.connect().await?; |
| 262 | + info!("Wifi connected"); |
| 263 | + |
| 264 | + wifi.wait_netif_up().await?; |
| 265 | + info!("Wifi netif up"); |
| 266 | + |
| 267 | + Ok(esp_wifi) |
| 268 | + } |
| 269 | + |
| 270 | + // |
| 271 | + // Implement `esp_idf_svc::tls::PollableSocket` for `async-io` sockets |
| 272 | + // you can do the same for e.g. `tokio` if you plan to use `tokio` instead of `async-io` |
| 273 | + // |
| 274 | + |
| 275 | + pub struct EspTlsSocket(Option<async_io::Async<TcpStream>>); |
| 276 | + |
| 277 | + impl EspTlsSocket { |
| 278 | + pub const fn new(socket: async_io::Async<TcpStream>) -> Self { |
| 279 | + Self(Some(socket)) |
| 280 | + } |
| 281 | + |
| 282 | + pub fn handle(&self) -> i32 { |
| 283 | + self.0.as_ref().unwrap().as_raw_fd() |
| 284 | + } |
| 285 | + |
| 286 | + pub fn poll_readable( |
| 287 | + &self, |
| 288 | + ctx: &mut core::task::Context, |
| 289 | + ) -> core::task::Poll<Result<(), esp_idf_svc::sys::EspError>> { |
| 290 | + self.0 |
| 291 | + .as_ref() |
| 292 | + .unwrap() |
| 293 | + .poll_readable(ctx) |
| 294 | + .map_err(|_| EspError::from_infallible::<{ esp_idf_svc::sys::ESP_FAIL }>()) |
| 295 | + } |
| 296 | + |
| 297 | + pub fn poll_writeable( |
| 298 | + &self, |
| 299 | + ctx: &mut core::task::Context, |
| 300 | + ) -> core::task::Poll<Result<(), esp_idf_svc::sys::EspError>> { |
| 301 | + self.0 |
| 302 | + .as_ref() |
| 303 | + .unwrap() |
| 304 | + .poll_writable(ctx) |
| 305 | + .map_err(|_| EspError::from_infallible::<{ esp_idf_svc::sys::ESP_FAIL }>()) |
| 306 | + } |
| 307 | + |
| 308 | + fn release(&mut self) -> Result<(), esp_idf_svc::sys::EspError> { |
| 309 | + let socket = self.0.take().unwrap(); |
| 310 | + let _ = socket.into_inner().unwrap().into_raw_fd(); |
| 311 | + |
| 312 | + Ok(()) |
| 313 | + } |
| 314 | + } |
| 315 | + |
| 316 | + impl esp_idf_svc::tls::Socket for EspTlsSocket { |
| 317 | + fn handle(&self) -> i32 { |
| 318 | + EspTlsSocket::handle(self) |
| 319 | + } |
| 320 | + |
| 321 | + fn release(&mut self) -> Result<(), esp_idf_svc::sys::EspError> { |
| 322 | + EspTlsSocket::release(self) |
| 323 | + } |
| 324 | + } |
| 325 | + |
| 326 | + impl esp_idf_svc::tls::PollableSocket for EspTlsSocket { |
| 327 | + fn poll_readable( |
| 328 | + &self, |
| 329 | + ctx: &mut core::task::Context, |
| 330 | + ) -> core::task::Poll<Result<(), esp_idf_svc::sys::EspError>> { |
| 331 | + EspTlsSocket::poll_readable(self, ctx) |
| 332 | + } |
| 333 | + |
| 334 | + fn poll_writable( |
| 335 | + &self, |
| 336 | + ctx: &mut core::task::Context, |
| 337 | + ) -> core::task::Poll<Result<(), esp_idf_svc::sys::EspError>> { |
| 338 | + EspTlsSocket::poll_writeable(self, ctx) |
| 339 | + } |
| 340 | + } |
| 341 | +} |
0 commit comments