1pub mod actions;
5use crate::client::{command_support, llm_budget::call_llm_for_client};
6use crate::llm::actions::{
7 client_trait::{Client, ClientActionResult},
8 protocol_trait::Protocol,
9};
10use crate::logging::emit::Log;
11use crate::protocol::{ConnectContext, Event};
12use crate::server::http3::wire::*;
13use crate::state::{
14 client_handles::{ClientCommand, ClientSendOutcome},
15 AccessLogOwner, ClientStatus,
16};
17use crate::utils::quic::*;
18pub use actions::Http3ClientProtocol;
19use actions::{
20 HTTP3_CLIENT_CONNECTED_EVENT, HTTP3_CLIENT_ERROR_EVENT, HTTP3_CLIENT_RESPONSE_RECEIVED_EVENT,
21};
22use anyhow::{ensure, Context, Result};
23use bytes::{Buf, Bytes};
24type Sender = h3::client::SendRequest<h3_quinn::OpenStreams, Bytes>;
25use futures::{future::BoxFuture, stream::FuturesUnordered, FutureExt, StreamExt};
26use serde_json::{json, Value};
27use std::{net::SocketAddr, time::Duration};
28
29const MAX_FOLLOWUP_DEPTH: u8 = 4;
30type HandlerFuture = BoxFuture<'static, (u8, Result<Vec<Value>>)>;
31type QueryFuture = BoxFuture<'static, (u8, Event)>;
32
33pub struct Http3Client;
34impl Http3Client {
35 pub async fn connect(ctx: ConnectContext) -> Result<SocketAddr> {
36 let params = ctx.startup_params.as_ref();
37 let exchange =
38 Duration::from_secs(bounded_parameter(params, "exchange_timeout_secs", 30, 300)?);
39 let (endpoint, connection) =
40 crate::utils::quic::connect(&ctx.remote_addr, params, b"h3", 4).await?;
41 let endpoint = EndpointOwner(endpoint);
42 let connection = ConnectionOwner(connection);
43 let (driver, sender) = h3::client::builder()
44 .send_grease(false)
45 .max_field_section_size(MAX_HEADERS as u64)
46 .build(h3_quinn::Connection::new(connection.clone()))
47 .await?;
48 let local_addr = endpoint.local_addr()?;
49 let now = crate::utils::clock::Instant::now();
50 ctx.state
51 .with_client_mut(ctx.client_id, |client| {
52 client.connection = Some(crate::state::ClientConnectionState {
53 id: ctx.client_id,
54 remote_addr: ctx.remote_addr.clone(),
55 connected_addr: Some(connection.remote_address()),
56 local_addr: Some(local_addr),
57 bytes_sent: 0,
58 bytes_received: 0,
59 packets_sent: 0,
60 packets_received: 0,
61 last_activity: now,
62 status: ClientStatus::Connected,
63 status_changed_at: now,
64 protocol_info: crate::state::server::ProtocolConnectionInfo::new(
65 json!({"alpn":"h3"}),
66 ),
67 });
68 })
69 .await;
70 ctx.state
71 .update_client_status(ctx.client_id, ClientStatus::Connected)
72 .await;
73 let command_rx = command_support::register_command_channel(&ctx.state, ctx.client_id).await;
74 let state = ctx.state.clone();
75 let id = ctx.client_id;
76 let task = tokio::spawn(Self::run(
77 ctx, endpoint, connection, driver, sender, command_rx, exchange,
78 ));
79 state.register_client_task(id, task).await;
80 Ok(local_addr)
81 }
82
83 async fn run(
84 ctx: ConnectContext,
85 _endpoint: EndpointOwner,
86 connection: ConnectionOwner,
87 mut driver: h3::client::Connection<h3_quinn::Connection, Bytes>,
88 sender: Sender,
89 mut commands: tokio::sync::mpsc::Receiver<ClientCommand>,
90 exchange: Duration,
91 ) {
92 let mut handlers: FuturesUnordered<HandlerFuture> = FuturesUnordered::new();
93 let mut queries: FuturesUnordered<QueryFuture> = FuturesUnordered::new();
94 handlers.push(handle_event(
95 ctx.clone(),
96 Event::new(
97 &HTTP3_CLIENT_CONNECTED_EVENT,
98 json!({"base_url":format!("https://{}",ctx.remote_addr),"remote_addr":ctx.remote_addr}),
99 ),
100 0,
101 ));
102 let mut disconnect = false;
103 while !disconnect {
104 tokio::select! {
105 command = commands.recv() => {
106 let Some(command) = command else { break; };
107 disconnect = enqueue(&ctx,&sender,exchange,&mut queries,handlers.len(),command.action.clone(),Some(command),0).await;
108 }
109 Some((depth,event)) = queries.next(), if !queries.is_empty() => {
110 handlers.push(handle_event(ctx.clone(),event,depth));
113 }
114 Some((depth,result)) = handlers.next(), if !handlers.is_empty() => {
115 match result {
116 Ok(actions) if actions.len() <= MAX_STREAMS => {
117 for action in actions {
118 if depth >= MAX_FOLLOWUP_DEPTH && action["type"] != "disconnect" {
119 if action["type"] == "send_http3_request" { Log::new(Some(&ctx.status_tx)).warn("HTTP3 follow-up limit reached"); }
120 continue;
121 }
122 if enqueue(&ctx,&sender,exchange,&mut queries,handlers.len(),action,None,depth).await { disconnect = true; break; }
123 }
124 }
125 Ok(_) => Log::new(Some(&ctx.status_tx)).warn("HTTP3 handler returned more than 32 actions"),
126 Err(e) => Log::new(Some(&ctx.status_tx)).warn(format!("HTTP3 event handler failed: {e}")),
127 }
128 }
129 _ = std::future::poll_fn(|cx| driver.poll_close(cx)) => break,
130 _ = connection.closed() => break,
131 }
132 }
133 drop(queries);
134 drop(handlers);
135 connection.close(0x100u32.into(), b"client disconnected");
136 ctx.state
137 .with_client_mut(ctx.client_id, |client| {
138 if let Some(connection) = &mut client.connection {
139 connection.status = ClientStatus::Disconnected;
140 connection.status_changed_at = crate::utils::clock::Instant::now();
141 }
142 })
143 .await;
144 ctx.state.remove_client_handle(ctx.client_id).await;
145 ctx.state
146 .update_client_status(ctx.client_id, ClientStatus::Disconnected)
147 .await;
148 let _ = ctx.status_tx.send("__UPDATE_UI__".into());
149 }
150}
151
152fn handle_event(ctx: ConnectContext, event: Event, depth: u8) -> HandlerFuture {
153 async move {
154 let result = async {
155 let instruction = ctx
156 .state
157 .get_instruction_for_client(ctx.client_id)
158 .await
159 .unwrap_or_default();
160 let memory = ctx
161 .state
162 .get_memory_for_client(ctx.client_id)
163 .await
164 .unwrap_or_default();
165 let result = call_llm_for_client(
166 &ctx.llm_client,
167 &ctx.state,
168 ctx.client_id.to_string(),
169 &instruction,
170 &memory,
171 Some(&event),
172 &Http3ClientProtocol::new(),
173 &ctx.status_tx,
174 )
175 .await?;
176 if let Some(memory) = result.memory_updates {
177 ctx.state.set_memory_for_client(ctx.client_id, memory).await;
178 }
179 Ok(result.actions)
180 }
181 .await;
182 (depth, result)
183 }
184 .boxed()
185}
186
187async fn enqueue(
188 ctx: &ConnectContext,
189 sender: &Sender,
190 deadline: Duration,
191 queries: &mut FuturesUnordered<QueryFuture>,
192 handler_count: usize,
193 action: Value,
194 command: Option<ClientCommand>,
195 depth: u8,
196) -> bool {
197 let outcome = match Http3ClientProtocol::new().execute_action(action.clone()) {
198 Ok(ClientActionResult::Disconnect) => Ok(ClientSendOutcome::Disconnected),
199 Ok(ClientActionResult::WaitForMore | ClientActionResult::NoAction) => {
200 Ok(ClientSendOutcome::Executed {
201 detail: "Waiting for more queries".into(),
202 })
203 }
204 Ok(ClientActionResult::Custom { name, data }) if name == "http3_request" => {
205 if queries.len() + handler_count >= MAX_STREAMS {
208 Err(anyhow::anyhow!(
209 "HTTP3 client busy: 32 active exchanges or handlers"
210 ))
211 } else {
212 queries.push(query_future(
213 ctx.clone(),
214 sender.clone(),
215 deadline,
216 data,
217 action,
218 command,
219 depth + 1,
220 ));
221 return false;
222 }
223 }
224 Ok(_) => Err(anyhow::anyhow!("Unsupported HTTP3 client action")),
225 Err(e) => Ok(ClientSendOutcome::Rejected {
226 error: e.to_string(),
227 }),
228 };
229 let disconnect = matches!(outcome, Ok(ClientSendOutcome::Disconnected));
230 record_action(ctx, &action, &outcome).await;
231 if let Some(command) = command {
232 command_support::reply(command, outcome);
233 }
234 disconnect
235}
236
237async fn record_action(ctx: &ConnectContext, action: &Value, outcome: &Result<ClientSendOutcome>) {
238 let result = match outcome {
239 Ok(v) => serde_json::to_value(v).unwrap_or(Value::Null),
240 Err(e) => json!({"error":e.to_string()}),
241 };
242 ctx.state
243 .record_access_log(
244 AccessLogOwner::Client(ctx.client_id.as_u32()),
245 Http3ClientProtocol::new().protocol_name(),
246 None,
247 "injected_action",
248 action.clone(),
249 vec![result],
250 )
251 .await;
252}
253
254fn query_future(
255 ctx: ConnectContext,
256 sender: Sender,
257 deadline: Duration,
258 data: Value,
259 action: Value,
260 command: Option<ClientCommand>,
261 depth: u8,
262) -> QueryFuture {
263 async move {
264 let result = async {
265 exchange(sender, &ctx.remote_addr, ctx.startup_params.as_ref(), data, deadline).await
266 }
267 .await;
268 let (outcome, event) = match result {
269 Ok(response) => {
270 ctx.state.with_client_mut(ctx.client_id, |client| {
271 if let Some(connection) = &mut client.connection {
272 connection.bytes_sent += action["body"].as_str().unwrap_or("").len() as u64;
273 connection.bytes_received += response.body.len() as u64;
274 connection.packets_sent += 1;
275 connection.packets_received += 1;
276 connection.last_activity = crate::utils::clock::Instant::now();
277 }
278 }).await;
279 let event = Event::new(&HTTP3_CLIENT_RESPONSE_RECEIVED_EVENT, json!({
280 "status_code":response.status_code,"headers":response.headers,"body":response.body,
281 "trailers":response.trailers,"stream_id":response.stream_index,
282 }));
283 (Ok(ClientSendOutcome::Executed { detail: format!("http3_request {} {} -> {} ({} byte body)",
284 action["method"].as_str().unwrap_or("GET"),action["path"].as_str().unwrap_or("/"),response.status_code,response.body.len()) }),event)
285 }
286 Err(e) => {
287 Log::new(Some(&ctx.status_tx)).warn(format!("HTTP3 stream failed: {e:#}"));
288 let event = Event::new(&HTTP3_CLIENT_ERROR_EVENT, json!({"error":format!("{e:#}")}));
289 (Err(e), event)
290 }
291 };
292 record_action(&ctx, &action, &outcome).await;
293 if let Some(command) = command {
294 command_support::reply(command, outcome);
295 }
296 (depth, event)
297 }
298 .boxed()
299}
300
301#[derive(Debug)]
303pub struct Http3Exchange {
304 pub status_code: u16,
305 pub headers: serde_json::Map<String, Value>,
306 pub body: String,
307 pub trailers: serde_json::Map<String, Value>,
308 pub stream_index: u64,
309}
310pub async fn exchange(
312 mut sender: Sender,
313 remote: &str,
314 params: Option<&crate::protocol::StartupParams>,
315 data: Value,
316 deadline: Duration,
317) -> Result<Http3Exchange> {
318 let method = data["method"].as_str().context("Missing method")?;
319 let path = data["path"].as_str().context("Missing path")?;
320 ensure!(
321 path.starts_with('/') && !path.starts_with("//"),
322 "HTTP3 path must be an origin-form path"
323 );
324 ensure!(path.len() <= MAX_HEADERS, "HTTP3 path too long");
325 let body = data["body"].as_str().unwrap_or("");
326 ensure!(body.len() <= MAX_BODY, "HTTP3 request body exceeds 8 MiB");
327 let end = tokio::time::Instant::now() + deadline;
328 let mut headers = params
329 .map(|p| p.get_optional_object("default_headers"))
330 .transpose()?
331 .flatten()
332 .cloned()
333 .unwrap_or_default();
334 headers = headers
335 .into_iter()
336 .map(|(k, v)| (k.to_ascii_lowercase(), v))
337 .collect();
338 if let Some(h) = data["headers"].as_object() {
339 for (key, value) in h {
340 headers.insert(key.to_ascii_lowercase(), value.clone());
341 }
342 }
343 if let Some(priority) = data["priority"].as_u64() {
344 ensure!(priority <= 7, "priority must be 0..7");
345 headers.insert("priority".into(), json!(format!("u={priority}")));
346 }
347 let mut request = http::Request::builder()
348 .method(method)
349 .uri(format!("https://{remote}{path}"))
350 .body(())?;
351 *request.headers_mut() = parse_request_headers(&Value::Object(headers))?;
352 check_field_section(
353 request.headers(),
354 &[
355 (":method", request.method().as_str()),
356 (":scheme", "https"),
357 (
358 ":authority",
359 request
360 .uri()
361 .authority()
362 .context("Missing authority")?
363 .as_str(),
364 ),
365 (":path", path),
366 ],
367 )?;
368 validate_length(request.headers(), body.len())?;
369 let stream = tokio::time::timeout_at(end, sender.send_request(request))
370 .await
371 .context("HTTP3 request deadline")??;
372 let mut stream = ClientStreamGuard {
373 stream,
374 done: false,
375 };
376 let result = tokio::time::timeout_at(end, async {
377 if !body.is_empty() {
378 stream
379 .stream
380 .send_data(Bytes::copy_from_slice(body.as_bytes()))
381 .await?;
382 }
383 let trailers = parse_headers(&data["trailers"])?;
384 if !trailers.is_empty() {
385 stream.stream.send_trailers(trailers).await?;
386 }
387 stream.stream.finish().await?;
388 let mut informational = 0;
389 let response = loop {
390 let response = stream.stream.recv_response().await?;
391 check_headers(response.headers())?;
392 if response.status().is_informational() {
393 ensure!(
394 response.status().as_u16() != 101,
395 "HTTP3 forbids switching protocols"
396 );
397 informational += 1;
398 ensure!(
399 informational <= 16,
400 "HTTP3 too many informational responses"
401 );
402 continue;
403 }
404 break response;
405 };
406 let mut bytes = Vec::new();
407 while let Some(mut chunk) = stream.stream.recv_data().await? {
408 ensure!(
409 chunk.remaining() <= MAX_BODY.saturating_sub(bytes.len()),
410 "HTTP3 response body exceeds 8 MiB"
411 );
412 let n = chunk.remaining();
413 bytes.extend_from_slice(&chunk.copy_to_bytes(n));
414 }
415 let trailers = stream.stream.recv_trailers().await?.unwrap_or_default();
416 check_headers(&trailers)?;
417 if method == "HEAD" || matches!(response.status().as_u16(), 204 | 205 | 304) {
418 ensure!(bytes.is_empty(), "HTTP3 response must not contain a body");
419 } else {
420 validate_length(response.headers(), bytes.len())?;
421 }
422 Ok(Http3Exchange {
423 status_code: response.status().as_u16(),
424 headers: header_json(response.headers())?,
425 body: String::from_utf8(bytes).context("HTTP3 body is not UTF-8 text")?,
426 trailers: header_json(&trailers)?,
427 stream_index: stream.stream.id().index(),
428 })
429 })
430 .await
431 .context("HTTP3 exchange deadline exceeded")?;
432 if result.is_ok() {
433 stream.done = true;
434 }
435 result
436}
437struct ClientStreamGuard {
438 stream: h3::client::RequestStream<h3_quinn::BidiStream<Bytes>, Bytes>,
439 done: bool,
440}
441impl Drop for ClientStreamGuard {
442 fn drop(&mut self) {
443 if !self.done {
444 self.stream
445 .stop_sending(h3::error::Code::H3_REQUEST_CANCELLED);
446 self.stream
447 .stop_stream(h3::error::Code::H3_REQUEST_CANCELLED);
448 }
449 }
450}