1use std::collections::HashSet;
22use std::{collections::HashMap, net::SocketAddr, path::Path, sync::Arc};
23
24use crate::engine::workload::{ResolvedWorkload, WorkloadComponent};
25use crate::wit::WitInterface;
26use crate::wit::WitWorld;
27use crate::{engine::ctx::Ctx, plugin::HostPlugin};
28use anyhow::{Context, bail, ensure};
29use hyper::server::conn::http1;
30use tokio::net::TcpListener;
31use tracing::{debug, error, info, warn};
32use wasmtime::component::InstancePre;
33use wasmtime::{AsContextMut, StoreContextMut};
34use wasmtime_wasi_http::{
35 WasiHttpView,
36 bindings::{ProxyPre, http::types::Scheme},
37 body::HyperOutgoingBody,
38 io::TokioIo,
39};
40
41use rustls::{ServerConfig, pki_types::CertificateDer};
42use rustls_pemfile::{certs, private_key};
43use tokio::sync::{RwLock, mpsc};
44use tokio_rustls::TlsAcceptor;
45
46const HTTP_SERVER_ID: &str = "http-server";
47
48#[derive(Clone, Debug)]
49struct HttpWorkloadConfig {
50 host_header: String,
51}
52
53pub type WorkloadHandles =
55 Arc<RwLock<HashMap<String, (ResolvedWorkload, InstancePre<Ctx>, String)>>>;
56
57pub struct HttpServer {
63 addr: SocketAddr,
64 workload_handles: WorkloadHandles,
66 workload_configs: Arc<RwLock<HashMap<String, HttpWorkloadConfig>>>,
68 shutdown_tx: Arc<RwLock<Option<mpsc::Sender<()>>>>,
69 tls_acceptor: Option<TlsAcceptor>,
70}
71
72impl HttpServer {
73 pub fn new(addr: SocketAddr) -> Self {
81 Self {
82 addr,
83 workload_handles: Arc::default(),
84 workload_configs: Arc::default(),
85 shutdown_tx: Arc::new(RwLock::new(None)),
86 tls_acceptor: None,
87 }
88 }
89
90 pub async fn new_with_tls(
104 addr: SocketAddr,
105 cert_path: &Path,
106 key_path: &Path,
107 ca_path: Option<&Path>,
108 ) -> anyhow::Result<Self> {
109 let tls_config = load_tls_config(cert_path, key_path, ca_path).await?;
110 let tls_acceptor = TlsAcceptor::from(Arc::new(tls_config));
111
112 Ok(Self {
113 addr,
114 workload_handles: Arc::default(),
115 workload_configs: Arc::default(),
116 shutdown_tx: Arc::new(RwLock::new(None)),
117 tls_acceptor: Some(tls_acceptor),
118 })
119 }
120}
121
122#[async_trait::async_trait]
123impl HostPlugin for HttpServer {
124 fn id(&self) -> &'static str {
125 HTTP_SERVER_ID
126 }
127
128 fn world(&self) -> WitWorld {
129 WitWorld {
130 imports: HashSet::from([WitInterface::from(
131 "wasi:http/incoming-handler,outgoing-handler",
132 )]),
133 ..Default::default()
134 }
135 }
136
137 async fn start(&self) -> anyhow::Result<()> {
138 let addr = self.addr;
139 let (shutdown_tx, mut shutdown_rx) = mpsc::channel::<()>(1);
140 let shutdown_tx_clone = self.shutdown_tx.clone();
141 let workload_handles = self.workload_handles.clone();
142 let tls_acceptor = self.tls_acceptor.clone();
143
144 *shutdown_tx_clone.write().await = Some(shutdown_tx);
146
147 let listener = TcpListener::bind(addr).await?;
148 debug!(addr = ?addr, "HTTP server listening");
149 tokio::spawn(async move {
152 if let Err(e) =
153 run_http_server(listener, workload_handles, &mut shutdown_rx, tls_acceptor).await
154 {
155 error!(err = ?e, addr = ?addr, "HTTP server error");
156 }
157 });
158
159 let protocol = if self.tls_acceptor.is_some() {
160 "HTTPS"
161 } else {
162 "HTTP"
163 };
164 debug!(addr = ?addr, protocol = protocol, "HTTP server starting");
165 Ok(())
166 }
167
168 async fn on_component_bind(
169 &self,
170 component: &mut WorkloadComponent,
171 interfaces: std::collections::HashSet<crate::wit::WitInterface>,
172 ) -> anyhow::Result<()> {
173 let Some(http_iface) = interfaces.iter().find(|iface| {
174 iface.namespace == "wasi"
175 && iface.package == "http"
176 && iface.interfaces.contains("incoming-handler")
177 }) else {
178 bail!(
179 "No wasi:http/incoming-handler interface found, plugin should not be bound to this workload"
180 );
181 };
182
183 if interfaces.len() > 1 {
185 warn!(
186 interfaces = ?interfaces,
187 "ignoring non-wasi:http/incoming-handler interfaces",
188 );
189 } else if http_iface.interfaces.len() > 1 {
190 warn!(
191 interfaces = ?http_iface.interfaces,
192 "ignoring non-incoming-handler interfaces",
193 );
194 }
195
196 let host_header = http_iface
198 .config
199 .get("host")
200 .cloned()
201 .unwrap_or_else(|| "*".to_string());
202
203 let id = component.id();
204
205 debug!(host = %host_header, workload_id = id, "binding HTTP config for workload");
206
207 let config = HttpWorkloadConfig { host_header };
209 self.workload_configs
210 .write()
211 .await
212 .insert(id.to_string(), config);
213
214 Ok(())
219 }
220
221 async fn on_workload_resolved(
222 &self,
223 resolved_handle: &ResolvedWorkload,
224 component_id: &str,
225 ) -> anyhow::Result<()> {
226 let config = self
228 .workload_configs
229 .read()
230 .await
231 .get(component_id)
232 .cloned()
233 .ok_or_else(|| anyhow::anyhow!("No HTTP config found for workload {component_id}"))?;
234
235 debug!(host = %config.host_header, workload_id = resolved_handle.id(), component_id, "storing resolved workload handle");
236
237 let instance_pre = resolved_handle.instantiate_pre(component_id).await?;
239
240 self.workload_handles.write().await.insert(
242 config.host_header,
243 (
244 resolved_handle.clone(),
245 instance_pre,
246 component_id.to_string(),
247 ),
248 );
249
250 Ok(())
251 }
252
253 async fn on_workload_unbind(
254 &self,
255 workload: &ResolvedWorkload,
256 _interfaces: HashSet<WitInterface>,
257 ) -> anyhow::Result<()> {
258 debug!(workload_id = workload.id(), "removing HTTP workload handle");
259
260 let mut handles_guard = self.workload_handles.write().await;
262 handles_guard.retain(|_, (handle, _, _)| handle.id() != workload.id());
263
264 self.workload_configs.write().await.remove(workload.id());
266
267 Ok(())
268 }
269
270 async fn stop(&self) -> anyhow::Result<()> {
271 info!(addr = ?self.addr, "HTTP server stopping");
272 let mut shutdown_guard = self.shutdown_tx.write().await;
274 if let Some(tx) = shutdown_guard.take() {
275 let _ = tx.send(()).await;
276 }
277 Ok(())
278 }
279}
280
281async fn run_http_server(
283 listener: TcpListener,
284 workload_handles: WorkloadHandles,
285 shutdown_rx: &mut mpsc::Receiver<()>,
286 tls_acceptor: Option<TlsAcceptor>,
287) -> anyhow::Result<()> {
288 loop {
289 tokio::select! {
290 _ = shutdown_rx.recv() => {
292 info!("HTTP server received shutdown signal");
293 break;
294 }
295 result = listener.accept() => {
297 match result {
298 Ok((client, client_addr)) => {
299 debug!(addr = ?client_addr, "new HTTP client connection");
300
301 let handles_clone = workload_handles.clone();
302 let tls_acceptor_clone = tls_acceptor.clone();
303 tokio::spawn(async move {
304 let service = hyper::service::service_fn(move |req| {
305 let handles = handles_clone.clone();
306 async move {
307 handle_http_request(req, handles).await
308 }
309 });
310
311 let result = if let Some(acceptor) = tls_acceptor_clone {
312 match acceptor.accept(client).await {
314 Ok(tls_stream) => {
315 http1::Builder::new()
316 .keep_alive(true)
317 .serve_connection(TokioIo::new(tls_stream), service)
318 .await
319 }
320 Err(e) => {
321 error!(addr = ?client_addr, err = ?e, "TLS handshake failed");
322 return;
323 }
324 }
325 } else {
326 http1::Builder::new()
328 .keep_alive(true)
329 .serve_connection(TokioIo::new(client), service)
330 .await
331 };
332
333 if let Err(e) = result {
334 error!(addr = ?client_addr, err = ?e, "error serving HTTP client");
335 }
336 });
337 }
338 Err(e) => {
339 error!(err = ?e, "failed to accept HTTP connection");
340 }
341 }
342 }
343 }
344 }
345
346 Ok(())
347}
348
349async fn handle_http_request(
351 req: hyper::Request<hyper::body::Incoming>,
352 workload_handles: WorkloadHandles,
353) -> Result<hyper::Response<HyperOutgoingBody>, hyper::Error> {
354 let method = req.method().clone();
355 let uri = req.uri().clone();
356
357 let host_header = req
359 .headers()
360 .get("host")
361 .and_then(|h| h.to_str().ok())
362 .unwrap_or("<no host header>")
363 .to_string(); debug!(
366 method = %method,
367 uri = %uri,
368 host = %host_header,
369 "HTTP request received"
370 );
371
372 let workload_handle = {
374 let handles = workload_handles.read().await;
375 debug!(host = %host_header, "looking up workload handle for host header");
376
377 if let Some(handle) = handles.get(&host_header) {
379 Some(handle.clone())
380 } else {
381 debug!("No exact match for host header, trying wildcard '*'");
383 handles.get("*").cloned()
384 }
385 };
386
387 let response = match workload_handle {
388 Some((handle, instance_pre, component_id)) => {
389 match invoke_component_handler(handle, instance_pre, &component_id, req).await {
390 Ok(resp) => resp,
391 Err(e) => {
392 error!(err = ?e, host = %host_header, "failed to invoke component");
393 hyper::Response::builder()
395 .status(500)
396 .body(HyperOutgoingBody::default())
397 .unwrap()
398 }
399 }
400 }
401 None => {
402 warn!(host = %host_header, "No workload bound to host header or wildcard '*'");
403 hyper::Response::builder()
404 .status(404)
405 .body(HyperOutgoingBody::default())
406 .unwrap()
407 }
408 };
409
410 Ok(response)
411}
412
413async fn invoke_component_handler(
415 workload_handle: ResolvedWorkload,
416 instance_pre: InstancePre<Ctx>,
417 component_id: &str,
418 req: hyper::Request<hyper::body::Incoming>,
419) -> anyhow::Result<hyper::Response<HyperOutgoingBody>> {
420 let mut store = workload_handle.new_store(component_id).await?;
422
423 handle_component_request(store.as_context_mut(), instance_pre, req).await
424}
425
426pub async fn handle_component_request<'a>(
428 mut store: StoreContextMut<'a, Ctx>,
429 pre: InstancePre<Ctx>,
430 req: hyper::Request<hyper::body::Incoming>,
431) -> anyhow::Result<hyper::Response<HyperOutgoingBody>> {
432 let (sender, receiver) = tokio::sync::oneshot::channel();
433 let req = store.data_mut().new_incoming_request(Scheme::Http, req)?;
435 let out = store.data_mut().new_response_outparam(sender)?;
436 let pre = ProxyPre::new(pre).context("failed to instantiate proxy pre")?;
437
438 let proxy = pre.instantiate_async(&mut store).await?;
440
441 proxy
442 .wasi_http_incoming_handler()
443 .call_handle(&mut store, req, out)
444 .await?;
445
446 match receiver.await {
447 Ok(Ok(resp)) => Ok(resp),
450 Ok(Err(e)) => Err(e.into()),
451
452 Err(e) => {
455 error!(err = ?e, "error receiving http response");
456 Err(anyhow::anyhow!(
457 "oneshot channel closed but no response was sent"
458 ))
459 }
460 }
461}
462
463async fn load_tls_config(
466 cert_path: &Path,
467 key_path: &Path,
468 ca_path: Option<&Path>,
469) -> anyhow::Result<ServerConfig> {
470 let cert_data = tokio::fs::read(cert_path)
472 .await
473 .with_context(|| format!("Failed to read certificate file: {}", cert_path.display()))?;
474 let mut cert_reader = std::io::Cursor::new(cert_data);
475 let cert_chain: Vec<CertificateDer<'static>> = certs(&mut cert_reader)
476 .collect::<Result<Vec<_>, _>>()
477 .with_context(|| format!("Failed to parse certificate file: {}", cert_path.display()))?;
478
479 ensure!(
480 !cert_chain.is_empty(),
481 "No certificates found in file: {}",
482 cert_path.display()
483 );
484
485 let key_data = tokio::fs::read(key_path)
487 .await
488 .with_context(|| format!("Failed to read private key file: {}", key_path.display()))?;
489 let mut key_reader = std::io::Cursor::new(key_data);
490 let key = private_key(&mut key_reader)
491 .with_context(|| format!("Failed to parse private key file: {}", key_path.display()))?
492 .ok_or_else(|| anyhow::anyhow!("No private key found in file: {}", key_path.display()))?;
493
494 let config = ServerConfig::builder()
496 .with_no_client_auth()
497 .with_single_cert(cert_chain, key)
498 .with_context(|| "Failed to create TLS configuration")?;
499
500 if let Some(ca_path) = ca_path {
502 let ca_data = tokio::fs::read(ca_path)
503 .await
504 .with_context(|| format!("Failed to read CA file: {}", ca_path.display()))?;
505 let mut ca_reader = std::io::Cursor::new(ca_data);
506 let ca_certs: Vec<CertificateDer<'static>> = certs(&mut ca_reader)
507 .collect::<Result<Vec<_>, _>>()
508 .with_context(|| format!("Failed to parse CA file: {}", ca_path.display()))?;
509
510 ensure!(
511 !ca_certs.is_empty(),
512 "No CA certificates found in file: {}",
513 ca_path.display()
514 );
515
516 debug!("CA certificate loaded, but client certificate verification not yet implemented");
519 }
520
521 Ok(config)
522}