use std::sync::Arc;
use bytes::Bytes;
use tokio::sync::{Notify, mpsc, oneshot};
use tokio_util::sync::CancellationToken;
use wasmtime::component::{Accessor, AccessorTask, HasSelf, Linker, Resource, ResourceTable};
use camel_api::security_policy::{AccessMode, RouteSecurityPlan};
use camel_api::{Body, CamelError, Exchange, ExchangePattern, Message, StreamBody, Value};
use camel_auth::AuthenticatedPrincipal;
use camel_auth::ProviderRegistry;
use camel_component_api::SecurityContext;
use camel_component_api::consumer::ConsumerContext;
use crate::return_stream::{
DEFAULT_DRAIN_CHANNEL_BOUND, DrainCoord, DrainEvent, StreamReturnable, TerminalSlot,
drain_guest_stream, receiver_to_body_stream, write_terminal,
};
use crate::source_auth_edge::{EdgeAuthOutcome, authenticate_edge};
use crate::source_bindings::camel::plugin::source_host::{HttpRequest, SubmitOutcome};
use crate::source_bindings::camel::plugin::types::{
WasmBody, WasmExchange, WasmMessage, WasmPattern,
};
pub(crate) struct WasmSourceKernelAuth {
pub(crate) plan: RouteSecurityPlan,
pub(crate) providers: Arc<ProviderRegistry>,
}
impl WasmSourceKernelAuth {
pub(crate) fn from_security_context(ctx: &SecurityContext) -> Option<Self> {
Some(Self {
plan: ctx.plan.clone()?,
providers: ctx.providers.clone()?,
})
}
}
pub struct HttpListenerHandle;
pub struct HttpMeta {
pub method: String,
pub path: String,
pub headers: Vec<(String, String)>,
principal: Option<AuthenticatedPrincipal>,
}
type BodyChunkRx = mpsc::Receiver<Result<Bytes, CamelError>>;
type RequestChannelItem = (HttpMeta, BodyChunkRx);
pub const REQUEST_CHANNEL_CAPACITY: usize = 1;
pub const EXCHANGE_CHANNEL_CAPACITY: usize = 1;
pub struct SourceChannels {
pub request_tx: mpsc::Sender<RequestChannelItem>,
pub request_rx: Arc<tokio::sync::Mutex<mpsc::Receiver<RequestChannelItem>>>,
pub exchange_tx: mpsc::Sender<(Exchange, oneshot::Sender<SubmitOutcome>)>,
pub exchange_rx: mpsc::Receiver<(Exchange, oneshot::Sender<SubmitOutcome>)>,
}
impl SourceChannels {
pub fn new() -> Self {
let (request_tx, request_rx) = mpsc::channel(REQUEST_CHANNEL_CAPACITY);
let (exchange_tx, exchange_rx) = mpsc::channel(EXCHANGE_CHANNEL_CAPACITY);
Self {
request_tx,
request_rx: Arc::new(tokio::sync::Mutex::new(request_rx)),
exchange_tx,
exchange_rx,
}
}
}
impl Default for SourceChannels {
fn default() -> Self {
Self::new()
}
}
pub const DEFAULT_MAX_REQUEST_BODY_BYTES: u64 = 10 * 1024 * 1024;
pub struct SourceHostState {
pub table: ResourceTable,
pub wasi: wasmtime_wasi::WasiCtx,
pub request_rx: Arc<tokio::sync::Mutex<mpsc::Receiver<RequestChannelItem>>>,
pub exchange_tx: mpsc::Sender<(Exchange, oneshot::Sender<SubmitOutcome>)>,
pub cancel_token: CancellationToken,
pub max_request_body_bytes: u64,
pub pending_principal: Option<AuthenticatedPrincipal>,
pub accept_outstanding: bool,
}
impl wasmtime_wasi::WasiView for SourceHostState {
fn ctx(&mut self) -> wasmtime_wasi::WasiCtxView<'_> {
wasmtime_wasi::WasiCtxView {
ctx: &mut self.wasi,
table: &mut self.table,
}
}
}
impl SourceHostState {
fn stash_pending_principal(
&mut self,
principal: Option<AuthenticatedPrincipal>,
) -> Result<(), &'static str> {
if self.accept_outstanding {
return Err(
"accept-http rejected: a previous request is outstanding; submit-exchange it before accepting again",
);
}
self.accept_outstanding = true;
self.pending_principal = principal;
Ok(())
}
fn install_pending_carrier(&mut self, exchange: &mut Exchange) {
if let Some(principal) = self.pending_principal.take() {
camel_auth::install_carrier(exchange, &principal);
}
self.accept_outstanding = false;
}
fn abort_pending_accept(&mut self) {
self.pending_principal = None;
self.accept_outstanding = false;
}
}
type SourceWasmError = crate::source_bindings::camel::plugin::types::WasmError;
impl crate::source_bindings::camel::plugin::source_host::HostHttpListener for SourceHostState {
fn drop(
&mut self,
_resource: wasmtime::component::Resource<HttpListenerHandle>,
) -> wasmtime::Result<()> {
Ok(())
}
}
impl crate::source_bindings::camel::plugin::source_host::Host for SourceHostState {
fn is_cancelled(&mut self) -> bool {
self.cancel_token.is_cancelled()
}
}
impl crate::source_bindings::camel::plugin::source_host::HostWithStore<SourceHostState>
for wasmtime::component::HasSelf<SourceHostState>
{
async fn accept_http(
accessor: &Accessor<SourceHostState, wasmtime::component::HasSelf<SourceHostState>>,
_listener: Resource<HttpListenerHandle>,
) -> Result<Option<HttpRequest>, SourceWasmError> {
let (request_rx, cancel_token, max_body) = accessor.with(|mut view| {
let state = view.get();
(
state.request_rx.clone(),
state.cancel_token.clone(),
state.max_request_body_bytes,
)
});
let mut guard = request_rx.lock().await;
let meta_and_body = tokio::select! {
r = guard.recv() => r,
_ = cancel_token.cancelled() => {
return Ok(None);
}
};
drop(guard);
let Some((mut meta, mut body_rx)) = meta_and_body else {
return Ok(None);
};
if let Err(msg) =
accessor.with(|mut view| view.get().stash_pending_principal(meta.principal.take()))
{
tracing::warn!("source: accept-http invariant: {msg}");
return Err(SourceWasmError::ProcessorError(msg.to_string()));
}
let box_stream: futures::stream::BoxStream<'static, Result<Bytes, CamelError>> =
Box::pin(futures::stream::poll_fn(move |cx| body_rx.poll_recv(cx)));
let cancel = cancel_token.clone();
let handle = crate::stream_bridge::assemble_stream_body_source(
accessor, box_stream, cancel, max_body,
);
let request = match handle {
Ok(body) => Some(HttpRequest {
method: meta.method,
path: meta.path,
headers: meta.headers,
body,
}),
Err(e) => {
tracing::error!("failed to assemble stream body for accept-http: {e}");
accessor.with(|mut view| view.get().abort_pending_accept());
None
}
};
Ok(request)
}
async fn submit_exchange(
accessor: &Accessor<SourceHostState, wasmtime::component::HasSelf<SourceHostState>>,
mut exchange: WasmExchange,
) -> Result<SubmitOutcome, SourceWasmError> {
let (exchange_tx, cancel_token, stream_parts) = accessor.with(|mut view| {
let state = view.get();
let exchange_tx = state.exchange_tx.clone();
let cancel_token = state.cancel_token.clone();
let stream_parts = exchange.take_stream();
(exchange_tx, cancel_token, stream_parts)
});
let mut native = source_exchange_to_native(exchange);
accessor.with(|mut view| view.get().install_pending_carrier(&mut native));
let (reply_tx, reply_rx) = oneshot::channel();
if let Some((stream_reader, terminal, stream_metadata)) = stream_parts {
let (chunk_tx, chunk_rx) = mpsc::channel::<DrainEvent>(DEFAULT_DRAIN_CHANNEL_BOUND);
let source_terminal: TerminalSlot = Arc::new(std::sync::Mutex::new(None));
let coord = DrainCoord {
cancel: cancel_token.clone(),
progress: Arc::new(Notify::new()),
receiver_gone: Arc::new(Notify::new()),
terminal_slot: source_terminal.clone(),
};
if let Err(spawn_err) = accessor.spawn(SubmitExchangeDrain {
stream_reader,
terminal,
chunk_tx,
coord,
}) {
write_terminal(
&source_terminal,
Err(CamelError::ProcessorError(format!(
"stream drain spawn failed: {spawn_err}"
))),
);
}
native.input.body = Body::Stream(StreamBody {
stream: Arc::new(tokio::sync::Mutex::new(Some(Box::pin(
receiver_to_body_stream(crate::return_stream::DrainReceiver {
rx: chunk_rx,
terminal: source_terminal,
}),
)))),
metadata: stream_metadata,
});
}
let outcome = tokio::select! {
res = async {
if exchange_tx.send((native, reply_tx)).await.is_err() {
return SubmitOutcome::Stopped;
}
reply_rx.await.unwrap_or(SubmitOutcome::Stopped)
} => res,
_ = cancel_token.cancelled() => SubmitOutcome::Stopped,
};
Ok(outcome)
}
}
struct SubmitExchangeDrain {
stream_reader: wasmtime::component::StreamReader<u8>,
terminal: wasmtime::component::FutureReader<Result<(), SourceWasmError>>,
chunk_tx: mpsc::Sender<DrainEvent>,
coord: DrainCoord,
}
impl AccessorTask<SourceHostState, HasSelf<SourceHostState>> for SubmitExchangeDrain {
async fn run(
self,
accessor: &Accessor<SourceHostState, HasSelf<SourceHostState>>,
) -> wasmtime::Result<()> {
drain_guest_stream::<SourceWasmError, SourceHostState>(
accessor,
self.stream_reader,
self.terminal,
self.chunk_tx,
self.coord,
)
.await;
Ok(())
}
}
fn source_exchange_to_native(wasm: WasmExchange) -> Exchange {
let input = source_message_to_native(wasm.input);
let mut exchange = Exchange::new(input);
if !wasm.correlation_id.is_empty() {
exchange.correlation_id = wasm.correlation_id;
}
if let Some(out) = wasm.output {
exchange.output = Some(source_message_to_native(out));
}
for (key, raw) in wasm.properties {
let value = serde_json::from_str::<Value>(&raw).unwrap_or(Value::String(raw));
exchange.properties.insert(key, value);
}
exchange.pattern = match wasm.pattern {
WasmPattern::InOnly => ExchangePattern::InOnly,
WasmPattern::InOut => ExchangePattern::InOut,
};
exchange
}
fn source_message_to_native(msg: WasmMessage) -> Message {
let headers = msg
.headers
.into_iter()
.map(|(k, raw)| {
let value = serde_json::from_str::<Value>(&raw).unwrap_or(Value::String(raw));
(k, value)
})
.collect();
Message {
headers,
body: source_body_to_native(msg.body),
}
}
fn source_body_to_native(body: WasmBody) -> Body {
match body {
WasmBody::Empty => Body::Empty,
WasmBody::Text(s) => Body::Text(s),
WasmBody::Bytes(v) => Body::Bytes(Bytes::from(v)),
WasmBody::Json(s) => serde_json::from_str::<Value>(&s)
.map(Body::Json)
.unwrap_or(Body::Text(s)),
WasmBody::Xml(s) => Body::Xml(s),
WasmBody::Stream(_) => {
tracing::warn!("source: undrained stream body reached conversion — dropped");
Body::Empty
}
}
}
pub fn add_to_linker(linker: &mut Linker<SourceHostState>) -> Result<(), wasmtime::Error> {
crate::wasi_surface::register_command_adapter_wasi(linker)?;
crate::source_bindings::camel::plugin::source_host::add_to_linker::<_, HasSelf<_>>(
linker,
|state| state,
)?;
Ok(())
}
pub(crate) async fn run_http_listener(
listener: tokio::net::TcpListener,
path_filter: Option<String>,
request_tx: mpsc::Sender<RequestChannelItem>,
cancel: CancellationToken,
kernel: Option<Arc<WasmSourceKernelAuth>>,
plan_access: Option<AccessMode>,
) -> Result<(), CamelError> {
use axum::Router;
use axum::extract::State;
use axum::http::Request;
use axum::response::Response;
use axum::routing::any;
use http_body_util::BodyExt;
struct ListenerState {
tx: mpsc::Sender<RequestChannelItem>,
cancel: CancellationToken,
kernel: Option<Arc<WasmSourceKernelAuth>>,
plan_access: Option<AccessMode>,
}
async fn handler(
State(state): State<Arc<ListenerState>>,
req: Request<axum::body::Body>,
) -> Response {
let (parts, body) = req.into_parts();
let principal = match authenticate_edge(
state.plan_access.as_ref(),
state.kernel.as_deref(),
&parts.headers,
&parts.uri,
)
.await
{
Ok(EdgeAuthOutcome::PassThrough) => None,
Ok(EdgeAuthOutcome::Authenticated(principal)) => Some(*principal),
Err(status) => {
return Response::builder()
.status(status)
.body(axum::body::Body::from("unauthenticated"))
.unwrap(); }
};
let (body_tx, body_rx) = mpsc::channel::<Result<Bytes, CamelError>>(
crate::return_stream::DEFAULT_DRAIN_CHANNEL_BOUND,
);
let http_meta = HttpMeta {
method: parts.method.to_string(),
path: parts.uri.path().to_string(),
headers: parts
.headers
.iter()
.map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
.collect(),
principal,
};
if state.tx.send((http_meta, body_rx)).await.is_err() {
return Response::builder()
.status(503)
.body(axum::body::Body::from("service unavailable"))
.unwrap(); }
let drain_cancel = state.cancel.clone();
tokio::spawn(async move {
let mut body = body;
loop {
tokio::select! {
frame = body.frame() => {
match frame {
Some(Ok(frame)) => {
if let Ok(data) = frame.into_data()
&& body_tx.send(Ok(data)).await.is_err()
{
break;
}
}
Some(Err(e)) => {
let _ = body_tx
.send(Err(CamelError::Io(format!("body read: {e}"))))
.await;
break;
}
None => break,
}
}
_ = drain_cancel.cancelled() => break,
}
}
});
Response::builder()
.status(202)
.body(axum::body::Body::from("accepted"))
.unwrap() }
let state = Arc::new(ListenerState {
tx: request_tx,
cancel: cancel.clone(),
kernel,
plan_access,
});
let route_path = path_filter
.filter(|path| !path.is_empty())
.map(|path| {
if path.starts_with('/') {
path
} else {
format!("/{path}")
}
})
.unwrap_or_else(|| "/{*path}".to_string());
let app = Router::new()
.route(&route_path, any(handler))
.with_state(state);
let local = listener.local_addr().ok();
if let Some(addr) = &local {
tracing::info!(%addr, "source HTTP listener started");
}
axum::serve(listener, app)
.with_graceful_shutdown(async move { cancel.cancelled().await })
.await
.map_err(|e| CamelError::Io(format!("HTTP listener error: {e}")))?;
if let Some(addr) = &local {
tracing::info!(%addr, "source HTTP listener stopped");
}
Ok(())
}
pub async fn run_pipeline_bridge(
mut exchange_rx: mpsc::Receiver<(Exchange, oneshot::Sender<SubmitOutcome>)>,
ctx: ConsumerContext,
) -> Result<(), CamelError> {
while let Some((exchange, reply_tx)) = exchange_rx.recv().await {
let outcome = match ctx.send(exchange).await {
Ok(()) => SubmitOutcome::Accepted,
Err(_) => SubmitOutcome::Stopped,
};
let _ = reply_tx.send(outcome);
}
Ok(())
}
#[cfg(test)]
mod tests;