1#![allow(clippy::disallowed_types)]
13use std::convert::Infallible;
17use std::future::Future;
18use std::pin::Pin;
19use std::sync::Arc;
20use std::task::{Context, Poll};
21
22use bytes::Bytes;
23use http::StatusCode;
24use tower::Service;
25
26use crate::ffi::handles::ResponseHeadEntry;
27use crate::ffi::pumps::pump_hyper_body_to_channel;
28use crate::http::server::{
29 options::DEFAULT_MAX_REQUEST_BODY_BYTES, serve_with_events, ConnectionEventFn, RemoteNodeId,
30 ServeHandle, ServeOptions,
31};
32use crate::{Body, CoreError, IrohEndpoint, RequestPayload};
33
34fn internal_error(detail: &'static [u8]) -> hyper::Response<Body> {
37 hyper::Response::builder()
38 .status(StatusCode::INTERNAL_SERVER_ERROR)
39 .body(Body::full(Bytes::from_static(detail)))
40 .expect("static error response args are valid")
41}
42
43fn service_unavailable(detail: &'static [u8]) -> hyper::Response<Body> {
44 hyper::Response::builder()
45 .status(StatusCode::SERVICE_UNAVAILABLE)
46 .body(Body::full(Bytes::from_static(detail)))
47 .expect("static error response args are valid")
48}
49
50pub fn respond(
60 handles: &crate::ffi::handles::HandleStore,
61 req_handle: u64,
62 status: u16,
63 headers: Vec<(String, String)>,
64) -> Result<(), CoreError> {
65 StatusCode::from_u16(status)
66 .map_err(|_| CoreError::invalid_input(format!("invalid HTTP status code: {status}")))?;
67 for (name, value) in &headers {
68 http::HeaderName::from_bytes(name.as_bytes()).map_err(|_| {
69 CoreError::invalid_input(format!("invalid response header name {:?}", name))
70 })?;
71 http::HeaderValue::from_str(value).map_err(|_| {
72 CoreError::invalid_input(format!("invalid response header value for {:?}", name))
73 })?;
74 }
75
76 let sender = handles
77 .take_req_sender(req_handle)
78 .ok_or_else(|| CoreError::invalid_handle(req_handle))?;
79 sender
80 .send(ResponseHeadEntry { status, headers })
81 .map_err(|_| CoreError::internal("serve task dropped before respond"))
82}
83
84struct ReqHeadGuard {
91 endpoint: IrohEndpoint,
92 req_handle: u64,
93}
94
95impl Drop for ReqHeadGuard {
96 fn drop(&mut self) {
97 self.endpoint.handles().take_req_sender(self.req_handle);
98 }
99}
100
101pub(crate) struct FfiDispatcher {
108 on_request: Arc<dyn Fn(RequestPayload) + Send + Sync>,
109 endpoint: IrohEndpoint,
110 own_node_id: Arc<String>,
111 max_header_size: Option<usize>,
112 max_request_body_wire_bytes: Option<usize>,
113}
114
115#[derive(Clone)]
116pub(crate) struct IrohHttpService {
117 dispatcher: Arc<FfiDispatcher>,
118}
119
120impl IrohHttpService {
121 pub(crate) fn downgrade(&self) -> std::sync::Weak<FfiDispatcher> {
125 std::sync::Arc::downgrade(&self.dispatcher)
126 }
127
128 pub(crate) fn upgrade(weak: &std::sync::Weak<FfiDispatcher>) -> Option<Self> {
131 weak.upgrade()
132 .map(|dispatcher| IrohHttpService { dispatcher })
133 }
134}
135
136impl Service<hyper::Request<Body>> for IrohHttpService {
140 type Response = hyper::Response<Body>;
141 type Error = Infallible;
142 type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
143
144 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
145 Poll::Ready(Ok(()))
146 }
147
148 fn call(&mut self, req: hyper::Request<Body>) -> Self::Future {
149 let dispatcher = self.dispatcher.clone();
150 let remote_node_id = req
155 .extensions()
156 .get::<RemoteNodeId>()
157 .map(|r| r.0.clone())
158 .unwrap_or_else(|| Arc::new(String::new()));
159 Box::pin(async move { Ok(dispatcher.dispatch(req, remote_node_id).await) })
160 }
161}
162
163impl FfiDispatcher {
164 async fn dispatch(
165 self: Arc<Self>,
166 req: hyper::Request<Body>,
167 remote_node_id: Arc<String>,
168 ) -> hyper::Response<Body> {
169 let handles = self.endpoint.handles();
170 let own_node_id = &*self.own_node_id;
171 let max_header_size = self.max_header_size;
172 let max_request_body_wire_bytes = self.max_request_body_wire_bytes;
173
174 let method = req.method().to_string();
175 let path_and_query = req
176 .uri()
177 .path_and_query()
178 .map(|p| p.as_str())
179 .unwrap_or("/")
180 .to_string();
181
182 let path_only = req.uri().path();
185 tracing::debug!(
186 method = %method,
187 path = %path_only,
188 peer = %remote_node_id,
189 "iroh-http: incoming request",
190 );
191
192 if let Some(limit) = max_header_size {
201 let header_bytes: usize = req
202 .headers()
203 .iter()
204 .filter(|(k, _)| !k.as_str().eq_ignore_ascii_case("peer-id"))
205 .map(|(k, v)| {
206 k.as_str()
207 .len()
208 .saturating_add(v.as_bytes().len())
209 .saturating_add(4)
210 }) .fold(0usize, |acc, x| acc.saturating_add(x))
212 .saturating_add("peer-id".len())
213 .saturating_add(remote_node_id.len())
214 .saturating_add(4)
215 .saturating_add(req.uri().to_string().len())
216 .saturating_add(method.len())
217 .saturating_add(12); if header_bytes > limit {
219 let resp = hyper::Response::builder()
220 .status(StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE)
221 .body(Body::empty())
222 .expect("static response args are valid");
223 return resp;
224 }
225 }
226
227 let mut req_headers: Vec<(String, String)> = Vec::new();
229 for (k, v) in req.headers().iter() {
230 if k.as_str().eq_ignore_ascii_case("peer-id") {
231 continue;
232 }
233 match v.to_str() {
234 Ok(s) => req_headers.push((k.as_str().to_string(), s.to_string())),
235 Err(_) => {
236 let resp = hyper::Response::builder()
237 .status(StatusCode::BAD_REQUEST)
238 .body(Body::full(Bytes::from_static(b"non-UTF8 header value")))
239 .expect("static response args are valid");
240 return resp;
241 }
242 }
243 }
244 req_headers.push(("peer-id".to_string(), (*remote_node_id).clone()));
245
246 if let (Some(limit), Some(content_length)) = (
247 max_request_body_wire_bytes,
248 req.headers().get(hyper::header::CONTENT_LENGTH),
249 ) {
250 match content_length
251 .to_str()
252 .ok()
253 .and_then(|v| v.parse::<u64>().ok())
254 {
255 Some(len) if len > limit as u64 => {
256 drop(req.into_body());
257 return hyper::Response::builder()
258 .status(StatusCode::PAYLOAD_TOO_LARGE)
259 .body(Body::empty())
260 .expect("static response args are valid");
261 }
262 _ => {}
263 }
264 }
265
266 let url = format!("httpi://{own_node_id}{path_and_query}");
267
268 let mut guard = handles.insert_guard();
271 let (req_body_writer, req_body_reader) = handles.make_body_channel();
272 let req_body_handle = match guard.insert_reader(req_body_reader) {
273 Ok(h) => h,
274 Err(_) => return service_unavailable(b"server handle table full"),
275 };
276
277 let (res_body_writer, res_body_reader) = handles.make_body_channel();
278 let res_body_handle = match guard.insert_writer(res_body_writer) {
279 Ok(h) => h,
280 Err(_) => return service_unavailable(b"server handle table full"),
281 };
282
283 let (head_tx, head_rx) = tokio::sync::oneshot::channel::<ResponseHeadEntry>();
284 let req_handle = match guard.allocate_req_handle(head_tx) {
285 Ok(h) => h,
286 Err(_) => return service_unavailable(b"server handle table full"),
287 };
288
289 guard.commit();
290
291 let _req_head_guard = ReqHeadGuard {
292 endpoint: self.endpoint.clone(),
293 req_handle,
294 };
295
296 let body = req.into_body();
299 tokio::spawn(pump_hyper_body_to_channel(body, req_body_writer));
300
301 (self.on_request)(RequestPayload {
304 req_handle,
305 req_body_handle,
306 res_body_handle,
307 method,
308 url,
309 headers: req_headers,
310 remote_node_id: Arc::unwrap_or_clone(remote_node_id),
311 is_bidi: false,
312 });
313
314 let response_head = match head_rx.await {
316 Ok(h) => h,
317 Err(_) => return internal_error(b"JS handler dropped without responding"),
318 };
319
320 let mut resp_builder = hyper::Response::builder().status(response_head.status);
323 for (k, v) in &response_head.headers {
324 resp_builder = resp_builder.header(k.as_str(), v.as_str());
325 }
326
327 match resp_builder.body(Body::new(res_body_reader)) {
328 Ok(r) => r,
329 Err(_) => internal_error(b"failed to build response head from JS"),
330 }
331 }
332}
333
334pub fn ffi_serve_with_callback<F>(
351 endpoint: IrohEndpoint,
352 options: ServeOptions,
353 on_request: F,
354 on_connection_event: Option<ConnectionEventFn>,
355) -> ServeHandle
356where
357 F: Fn(RequestPayload) + Send + Sync + 'static,
358{
359 let max_header_size = endpoint.max_header_size();
360 let own_node_id = Arc::new(endpoint.node_id().to_string());
361 let on_request = Arc::new(on_request) as Arc<dyn Fn(RequestPayload) + Send + Sync>;
362
363 let dispatcher = Arc::new(FfiDispatcher {
364 on_request,
365 endpoint: endpoint.clone(),
366 own_node_id,
367 max_header_size: if max_header_size == 0 {
368 None
369 } else {
370 Some(max_header_size)
371 },
372 max_request_body_wire_bytes: options
373 .max_request_body_wire_bytes
374 .or(Some(DEFAULT_MAX_REQUEST_BODY_BYTES)),
375 });
376 let svc = IrohHttpService { dispatcher };
377
378 let handle = serve_with_events(endpoint.clone(), options, svc.clone(), on_connection_event);
379
380 endpoint.set_local_service_for(&handle, &svc);
385 handle
386}
387
388pub fn ffi_serve<F>(endpoint: IrohEndpoint, options: ServeOptions, on_request: F) -> ServeHandle
392where
393 F: Fn(RequestPayload) + Send + Sync + 'static,
394{
395 ffi_serve_with_callback(endpoint, options, on_request, None)
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401 use crate::ffi::handles::{HandleStore, StoreConfig};
402 use crate::{NetworkingOptions, NodeOptions};
403 use http::HeaderValue;
404 use std::sync::Mutex;
405
406 #[tokio::test]
409 async fn respond_rejects_invalid_status() {
410 let store = HandleStore::new(StoreConfig::default());
411 let err = respond(&store, 0, 9999, vec![]).unwrap_err();
413 assert_eq!(err.code, crate::ErrorCode::InvalidInput);
414 }
415
416 #[tokio::test]
417 async fn respond_rejects_invalid_header_name() {
418 let store = HandleStore::new(StoreConfig::default());
419 let err = respond(&store, 0, 200, vec![("bad name".into(), "v".into())]).unwrap_err();
420 assert_eq!(err.code, crate::ErrorCode::InvalidInput);
421 }
422
423 #[tokio::test]
424 async fn respond_rejects_invalid_header_value() {
425 let store = HandleStore::new(StoreConfig::default());
426 let err = respond(&store, 0, 200, vec![("x".into(), "bad\nvalue".into())]).unwrap_err();
428 assert_eq!(err.code, crate::ErrorCode::InvalidInput);
429 }
430
431 #[tokio::test]
432 async fn respond_unknown_handle_errors() {
433 let store = HandleStore::new(StoreConfig::default());
434 let err = respond(&store, 4242, 200, vec![("x".into(), "y".into())]).unwrap_err();
436 assert_eq!(err.code, crate::ErrorCode::InvalidInput);
437 assert!(err.message.contains("4242"));
438 }
439
440 #[tokio::test]
441 async fn respond_delivers_head_to_receiver() {
442 let store = HandleStore::new(StoreConfig::default());
443 let (tx, rx) = tokio::sync::oneshot::channel::<ResponseHeadEntry>();
444 let handle = store.allocate_req_handle(tx).unwrap();
445
446 respond(&store, handle, 201, vec![("x-test".into(), "yes".into())]).unwrap();
447
448 let head = rx.await.unwrap();
449 assert_eq!(head.status, 201);
450 assert_eq!(
451 head.headers,
452 vec![("x-test".to_string(), "yes".to_string())]
453 );
454 let err = respond(&store, handle, 200, vec![]).unwrap_err();
456 assert_eq!(err.code, crate::ErrorCode::InvalidInput);
457 }
458
459 async fn local_endpoint() -> IrohEndpoint {
462 IrohEndpoint::bind(NodeOptions {
463 networking: NetworkingOptions {
464 disabled: true,
465 bind_addrs: vec!["127.0.0.1:0".into()],
466 ..Default::default()
467 },
468 ..Default::default()
469 })
470 .await
471 .unwrap()
472 }
473
474 fn make_dispatcher(
475 endpoint: IrohEndpoint,
476 max_header_size: Option<usize>,
477 sink: Arc<Mutex<Vec<RequestPayload>>>,
478 ) -> Arc<FfiDispatcher> {
479 let own_node_id = Arc::new(endpoint.node_id().to_string());
480 Arc::new(FfiDispatcher {
481 on_request: Arc::new(move |p| sink.lock().unwrap().push(p)),
482 endpoint,
483 own_node_id,
484 max_header_size,
485 max_request_body_wire_bytes: None,
486 })
487 }
488
489 #[tokio::test]
492 async fn dispatch_rejects_non_utf8_header_value() {
493 let endpoint = local_endpoint().await;
494 let sink = Arc::new(Mutex::new(Vec::new()));
495 let dispatcher = make_dispatcher(endpoint, None, sink.clone());
496
497 let mut req = hyper::Request::builder()
498 .method("GET")
499 .uri("/")
500 .body(Body::empty())
501 .unwrap();
502 req.headers_mut()
503 .insert("x-bad", HeaderValue::from_bytes(&[0xff, 0xfe]).unwrap());
504
505 let resp = dispatcher.dispatch(req, Arc::new(String::new())).await;
506 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
507 assert!(
508 sink.lock().unwrap().is_empty(),
509 "callback must not fire on a rejected request",
510 );
511 }
512
513 #[tokio::test]
516 async fn dispatch_rejects_oversized_headers() {
517 let endpoint = local_endpoint().await;
518 let sink = Arc::new(Mutex::new(Vec::new()));
519 let dispatcher = make_dispatcher(endpoint, Some(50), sink.clone());
520
521 let req = hyper::Request::builder()
522 .method("GET")
523 .uri("/")
524 .header("x-big", "a".repeat(200))
525 .body(Body::empty())
526 .unwrap();
527
528 let resp = dispatcher.dispatch(req, Arc::new(String::new())).await;
529 assert_eq!(resp.status(), StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE);
530 assert!(
531 sink.lock().unwrap().is_empty(),
532 "callback must not fire on a rejected request",
533 );
534 }
535}