1use crate::llm::actions::{
4 client_trait::{Client, ClientActionResult},
5 protocol_trait::Protocol,
6 ActionDefinition, Parameter, ParameterDefinition,
7};
8use crate::protocol::log_template::LogTemplate;
9use crate::protocol::EventType;
10use crate::state::app_state::AppState;
11use anyhow::{Context, Result};
12use serde_json::json;
13use std::sync::LazyLock;
14
15pub static HTTP3_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
17 EventType::new(
18 "http3_connected",
19 "HTTP/3 client connected via QUIC and ready to send requests",
20 json!({
21 "type": "send_http3_request",
22 "method": "GET",
23 "path": "/api/status",
24 "priority": 5
25 }),
26 )
27 .with_parameters(vec![
28 Parameter {
29 name: "base_url".to_string(),
30 type_hint: "string".to_string(),
31 description: "Base URL for HTTP/3 requests".to_string(),
32 required: true,
33 },
34 Parameter {
35 name: "remote_addr".to_string(),
36 type_hint: "string".to_string(),
37 description: "Authenticated target of this reusable QUIC session".to_string(),
38 required: true,
39 },
40 ])
41 .with_actions(Http3ClientProtocol::new().get_sync_actions())
42});
43
44pub static HTTP3_CLIENT_RESPONSE_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
46 EventType::new(
47 "http3_response_received",
48 "HTTP/3 response received from server",
49 json!({
50 "type": "send_http3_request",
51 "method": "POST",
52 "path": "/api/data",
53 "body": "{\"key\": \"value\"}",
54 "priority": 3
55 }),
56 )
57 .with_parameters(vec![
58 Parameter {
59 name: "status_code".to_string(),
60 type_hint: "number".to_string(),
61 description: "HTTP status code".to_string(),
62 required: true,
63 },
64 Parameter {
65 name: "headers".to_string(),
66 type_hint: "object".to_string(),
67 description: "Response headers".to_string(),
68 required: true,
69 },
70 Parameter {
71 name: "body".to_string(),
72 type_hint: "string".to_string(),
73 description: "Complete UTF-8 response body, at most 8 MiB".to_string(),
74 required: true,
75 },
76 Parameter {
77 name: "trailers".into(),
78 type_hint: "object".into(),
79 description: "Trailing response headers".into(),
80 required: true,
81 },
82 Parameter {
83 name: "stream_id".to_string(),
84 type_hint: "number".to_string(),
85 description: "Index of this request stream on the reusable QUIC connection."
86 .to_string(),
87 required: true,
88 },
89 ])
90 .with_actions(Http3ClientProtocol::new().get_sync_actions())
91});
92
93pub static HTTP3_CLIENT_ERROR_EVENT: LazyLock<EventType> = LazyLock::new(|| {
94 EventType::new(
95 "http3_request_failed",
96 "HTTP/3 exchange failed",
97 json!({"type":"wait_for_more"}),
98 )
99 .with_parameters(vec![Parameter {
100 name: "error".into(),
101 type_hint: "string".into(),
102 description: "Local failure description".into(),
103 required: true,
104 }])
105 .with_actions(Http3ClientProtocol::new().get_sync_actions())
106});
107
108#[derive(Default)]
110pub struct Http3ClientProtocol;
111
112impl Http3ClientProtocol {
113 pub fn new() -> Self {
114 Self
115 }
116}
117
118impl Protocol for Http3ClientProtocol {
120 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
122 let mut parameters = crate::utils::quic::client_parameters();
123 parameters.push(ParameterDefinition {
124 name: "default_headers".to_string(),
125 description: "Default headers to include in all requests".to_string(),
126 type_hint: "object".to_string(),
127 required: false,
128 example: json!({
129 "User-Agent": "NetGet-HTTP3/1.0",
130 "Accept": "application/json"
131 }),
132 default: None,
133 });
134 parameters
135 }
136 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
137 self.get_sync_actions()
138 }
139 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
140 vec![
141 ActionDefinition {
142 name: "send_http3_request".to_string(),
143 description: "Send another HTTP/3 request in response to received data".to_string(),
144 parameters: vec![
145 Parameter {
146 name: "method".to_string(),
147 type_hint: "string".to_string(),
148 description: "HTTP request method, such as GET or POST".to_string(),
149 required: true,
150 },
151 Parameter {
152 name: "path".to_string(),
153 type_hint: "string".to_string(),
154 description: "Origin-form path and optional query string".to_string(),
155 required: true,
156 },
157 Parameter {
158 name: "headers".to_string(),
159 type_hint: "object".to_string(),
160 description: "Request headers".to_string(),
161 required: false,
162 },
163 Parameter {
164 name: "trailers".into(),
165 type_hint: "object".into(),
166 description: "Trailing request headers".into(),
167 required: false,
168 },
169 Parameter {
170 name: "body".to_string(),
171 type_hint: "string".to_string(),
172 description: "UTF-8 request body, at most 8 MiB".to_string(),
173 required: false,
174 },
175 Parameter {
176 name: "priority".to_string(),
177 type_hint: "number".to_string(),
178 description: "RFC 9218 urgency, 0-7, sent as the `priority: u=N` request \
179 header. **Lower is more urgent** (0 = most urgent, 7 = least); \
180 the RFC default is 3, and omitting this sends no header at all, \
181 which is not the same as sending u=3. It is a hint the server \
182 uses when scheduling responses; a server may ignore it."
183 .to_string(),
184 required: false,
185 },
186 ],
187 example: json!({
188 "type": "send_http3_request",
189 "method": "POST",
190 "path": "/api/data",
191 "body": "{\"key\": \"value\"}",
192 "priority": 3
193 }),
194 log_template: Some(
195 LogTemplate::new().with_info("-> HTTP3 {method} {path} body_bytes={body_len}"),
196 ),
197 },
198 ActionDefinition {
199 name: "disconnect".into(),
200 description: "Close the QUIC session and cancel active requests".into(),
201 parameters: vec![],
202 example: json!({"type":"disconnect"}),
203 log_template: Some(LogTemplate::new().with_info("-> HTTP3 disconnect")),
204 },
205 ActionDefinition {
206 name: "wait_for_more".to_string(),
207 description: "Take no action and wait for the next response. Use when nothing \
208 should be sent yet."
209 .to_string(),
210 parameters: vec![],
211 example: json!({ "type": "wait_for_more" }),
212 log_template: Some(
213 LogTemplate::new().with_info("HTTP3 waiting for another response"),
214 ),
215 },
216 ]
217 }
218 fn protocol_name(&self) -> &'static str {
219 "HTTP3"
220 }
221 fn get_event_types(&self) -> Vec<EventType> {
222 vec![
223 HTTP3_CLIENT_CONNECTED_EVENT.clone(),
224 HTTP3_CLIENT_RESPONSE_RECEIVED_EVENT.clone(),
225 HTTP3_CLIENT_ERROR_EVENT.clone(),
226 ]
227 }
228 fn stack_name(&self) -> &'static str {
229 "ETH>IP>UDP>QUIC>HTTP3"
230 }
231 fn keywords(&self) -> Vec<&'static str> {
232 vec!["http3", "http/3", "quic", "h3", "connect to http3"]
233 }
234 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
235 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
236
237 ProtocolMetadataV2::builder()
238 .state(DevelopmentState::Experimental)
239 .implementation("quinn 0.11 + rustls 0.23 + h3 0.0.8, authenticated reusable RFC 9114 session with an owned control/QPACK driver. h3-quinn 0.0.10 has a local pending-read cancellation patch.")
240 .llm_control("Method, origin-form path, headers, UTF-8 body, request trailers and RFC 9218 urgency 0..7. Injected and handler requests multiplex concurrently; up to 32 active exchanges and handlers, with four follow-up levels.")
241 .e2e_testing("Independent pinned aioquic 1.3.0 server tests GET/POST/header/priority/trailers, multiplexing, response bounds, trust/name failures, timeout and disconnect/removal. NetGet pair and real server tests cover both roles. No tests are ignored or skip missing peers.")
242 .notes("32 KiB field sections, 8 MiB UTF-8 bodies, bounded handshake/exchange/idle deadlines. System roots plus explicit ca_cert_path; optional server_name. No 0-RTT, server push, DATAGRAM, WebTransport or migration. Experimental: no second independent implementation, pcap oracle or fuzz target.")
243 .build()
244 }
245 fn description(&self) -> &'static str {
246 "HTTP/3 client for making web requests over QUIC"
247 }
248 fn example_prompt(&self) -> &'static str {
249 "Connect to localhost:4433 using a trusted certificate and fetch /api/status over HTTP/3"
250 }
251 fn group_name(&self) -> &'static str {
252 "Core"
253 }
254
255 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
256 use crate::llm::actions::StartupExamples;
257 use serde_json::json;
258
259 StartupExamples::new(
260 json!({
262 "type": "open_client",
263 "remote_addr": "localhost:4433",
264 "startup_params": {"ca_cert_path":"cert.pem","server_name":"localhost"},
265 "base_stack": "http3",
266 "instruction": "Fetch /api/status and display QUIC connection info"
267 }),
268 json!({
270 "type": "open_client",
271 "remote_addr": "localhost:4433",
272 "startup_params": {"ca_cert_path":"cert.pem","server_name":"localhost"},
273 "base_stack": "http3",
274 "event_handlers": [{
275 "event_pattern": "http3_response_received",
276 "handler": {
277 "type": "script",
278 "language": "python",
279 "code": "<http3_handler>"
280 }
281 }]
282 }),
283 json!({
285 "type": "open_client",
286 "remote_addr": "localhost:4433",
287 "startup_params": {"ca_cert_path":"cert.pem","server_name":"localhost"},
288 "base_stack": "http3",
289 "event_handlers": [{
290 "event_pattern": "http3_connected",
291 "handler": {
292 "type": "static",
293 "actions": [{
294 "type": "send_http3_request",
295 "method": "GET",
296 "path": "/api/status",
297 "priority": 5
298 }]
299 }
300 }]
301 }),
302 )
303 }
304}
305
306impl Client for Http3ClientProtocol {
308 fn connect(
309 &self,
310 ctx: crate::protocol::ConnectContext,
311 ) -> std::pin::Pin<
312 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
313 > {
314 Box::pin(async move {
315 use crate::client::http3::Http3Client;
316 Http3Client::connect(ctx).await
317 })
318 }
319 fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
320 let action_type = action
321 .get("type")
322 .and_then(|v| v.as_str())
323 .context("Missing 'type' field in action")?;
324
325 match action_type {
326 "send_http3_request" => {
327 let method = action
328 .get("method")
329 .and_then(|v| v.as_str())
330 .context("Missing 'method' field")?
331 .to_string();
332
333 let path = action
334 .get("path")
335 .and_then(|v| v.as_str())
336 .context("Missing 'path' field")?
337 .to_string();
338
339 let headers = match action.get("headers") {
340 None | Some(serde_json::Value::Null) => None,
341 Some(value) => Some(
342 value
343 .as_object()
344 .context("headers must be an object")?
345 .clone(),
346 ),
347 };
348
349 let body = match action.get("body") {
350 None | Some(serde_json::Value::Null) => None,
351 Some(value) => {
352 Some(value.as_str().context("body must be a string")?.to_owned())
353 }
354 };
355
356 let priority = action
357 .get("priority")
358 .filter(|value| !value.is_null())
359 .map(|_| crate::client::wire_values::number::<u8>(&action, "priority", 0))
360 .transpose()?;
361 anyhow::ensure!(priority.is_none_or(|p| p <= 7), "priority must be 0..7");
362
363 Ok(ClientActionResult::Custom {
365 name: "http3_request".to_string(),
366 data: json!({
367 "method": method,
368 "path": path,
369 "headers": headers,
370 "body": body,
371 "priority": priority,
372 "trailers": action.get("trailers"),
373 }),
374 })
375 }
376 "disconnect" => Ok(ClientActionResult::Disconnect),
377 "wait_for_more" => Ok(ClientActionResult::WaitForMore),
378 _ => Err(anyhow::anyhow!(
379 "Unknown HTTP/3 client action: {}",
380 action_type
381 )),
382 }
383 }
384}