1use super::wire::*;
2use crate::llm::actions::{
3 protocol_trait::{ActionResult, Protocol, Server},
4 ActionDefinition, Parameter, ParameterDefinition, StartupExamples,
5};
6use crate::protocol::log_template::LogTemplate;
7use crate::protocol::{EventType, SpawnContext};
8use crate::state::AppState;
9use crate::utils::quic::*;
10use anyhow::{bail, ensure, Result};
11use serde_json::{json, Value};
12use std::sync::LazyLock;
13pub fn field(name: &str, hint: &str, description: &str, required: bool) -> Parameter {
14 Parameter {
15 name: name.into(),
16 type_hint: hint.into(),
17 description: description.into(),
18 required,
19 }
20}
21pub static HTTP3_REQUEST_EVENT: LazyLock<EventType> = LazyLock::new(|| {
22 EventType::new(
23 "http3_request_received",
24 "Complete HTTP/3 request",
25 json!({"type":"send_http3_response","status":200,"body":"hello"}),
26 )
27 .with_parameters(vec![
28 field(
29 "method",
30 "string",
31 "HTTP request method, such as GET or POST",
32 true,
33 ),
34 field(
35 "path",
36 "string",
37 "Request path and optional query string",
38 true,
39 ),
40 field(
41 "headers",
42 "object",
43 "Headers; repeated values are arrays",
44 true,
45 ),
46 field(
47 "body",
48 "string",
49 "Complete UTF-8 request body, at most 8 MiB",
50 true,
51 ),
52 field("trailers", "object", "Request trailers", true),
53 field("stream_id", "number", "QUIC stream index", true),
54 field("peer_addr", "string", "Remote IP and UDP port", true),
55 ])
56 .with_actions(Http3Protocol.get_sync_actions())
57});
58#[derive(Default)]
59pub struct Http3Protocol;
60impl Http3Protocol {
61 pub fn new() -> Self {
62 Self
63 }
64}
65impl Protocol for Http3Protocol {
66 fn protocol_name(&self) -> &'static str {
67 "HTTP3"
68 }
69 fn stack_name(&self) -> &'static str {
70 "ETH>IP>UDP>QUIC>HTTP3"
71 }
72 fn description(&self) -> &'static str {
73 "HTTP/3 requests and responses over authenticated QUIC"
74 }
75 fn keywords(&self) -> Vec<&'static str> {
76 vec!["http3", "http/3", "h3"]
77 }
78 fn group_name(&self) -> &'static str {
79 "Core"
80 }
81 fn example_prompt(&self) -> &'static str {
82 "Serve HTTP/3 on UDP 4433 and answer requests with hello"
83 }
84 fn get_async_actions(&self, _: &AppState) -> Vec<ActionDefinition> {
85 vec![]
86 }
87 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
88 vec![
89 ActionDefinition {
90 name: "send_http3_response".into(),
91 description: "Send a final HTTP/3 response on the requesting stream".into(),
92 parameters: vec![
93 field("status", "number", "Final status code, 200..599", true),
94 field("headers", "object", "Response headers", false),
95 field(
96 "body",
97 "string",
98 "UTF-8 response body, at most 8 MiB",
99 false,
100 ),
101 field("trailers", "object", "Trailing headers", false),
102 ],
103 example: json!({"type":"send_http3_response","status":200,"headers":{"content-type":"text/plain"},"body":"hello"}),
104 log_template: Some(
105 LogTemplate::new().with_info("-> HTTP3 status={status} body_bytes={body_len}"),
106 ),
107 },
108 ActionDefinition {
109 name: "cancel_http3_request".into(),
110 description: "Reset this request with H3_REQUEST_CANCELLED".into(),
111 parameters: vec![],
112 example: json!({"type":"cancel_http3_request"}),
113 log_template: Some(
114 LogTemplate::new()
115 .with_info("HTTP3 request cancelled with H3_REQUEST_CANCELLED"),
116 ),
117 },
118 ]
119 }
120 fn get_event_types(&self) -> Vec<EventType> {
121 vec![HTTP3_REQUEST_EVENT.clone()]
122 }
123 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
124 vec![
125 parameter(
126 "cert_path",
127 "PEM certificate chain; requires key_path",
128 json!("cert.pem"),
129 None,
130 ),
131 parameter(
132 "key_path",
133 "PEM key; requires cert_path",
134 json!("key.pem"),
135 None,
136 ),
137 parameter(
138 "handshake_timeout_secs",
139 "TLS handshake deadline, 1..60 seconds",
140 json!(10),
141 Some(json!(HANDSHAKE_TIMEOUT.as_secs())),
142 ),
143 parameter(
144 "exchange_timeout_secs",
145 "Request headers/body, handler and response deadline, 1..300 seconds",
146 json!(30),
147 Some(json!(EXCHANGE_TIMEOUT.as_secs())),
148 ),
149 parameter(
150 "idle_timeout_secs",
151 "QUIC idle deadline, 1..3600 seconds",
152 json!(300),
153 Some(json!(IDLE_TIMEOUT.as_secs())),
154 ),
155 parameter(
156 "max_connections",
157 "Simultaneous connections including pending TLS, 1..256",
158 json!(64),
159 Some(json!(MAX_CONNECTIONS)),
160 ),
161 parameter(
162 "max_streams",
163 "Active requests per connection, 1..32",
164 json!(32),
165 Some(json!(MAX_STREAMS)),
166 ),
167 ]
168 }
169 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
170 use crate::protocol::metadata::*;
171 ProtocolMetadataV2::builder().state(DevelopmentState::Experimental).max_inbound_bytes(MAX_INBOUND_BYTES).privilege_requirement(PrivilegeRequirement::PrivilegedPort(443)).well_known_udp_port(443).implementation("h3 0.0.8, h3-quinn 0.0.10 with local cancellation fix, quinn 0.11 and rustls 0.23; RFC 9114 HEADERS/DATA/QPACK/control streams").llm_control("Structured status, headers, UTF-8 body and trailers per request; explicit cancellation").e2e_testing("Loopback aioquic 1.3.0 clients and servers, NetGet pair, authentication, bounds and removal regressions in tests/server/http3 and tests/client/http3").notes("32 KiB fields, 8 MiB UTF-8 bodies; 64 connections, 32 requests per connection; no server push, DATAGRAM, WebTransport, migration or 0-RTT. TLS certificates require explicit client trust.").build()
172 }
173 fn get_startup_examples(&self) -> StartupExamples {
174 StartupExamples::new(
175 json!({"type":"open_server","base_stack":"http3","port":4433,"instruction":"Answer HTTP/3 requests with status 200 and the UTF-8 body hello"}),
176 json!({"type":"open_server","base_stack":"http3","port":4433,"event_handlers":[{"event_pattern":"http3_request_received","handler":{"type":"script","language":"python","code":"import json,sys\njson.load(sys.stdin)\nprint(json.dumps({'actions':[{'type':'send_http3_response','status':200,'body':'hello'}]}))"}}]}),
177 json!({"type":"open_server","base_stack":"http3","port":4433,"event_handlers":[{"event_pattern":"http3_request_received","handler":{"type":"static","actions":[{"type":"send_http3_response","status":200,"body":"hello"}]}}]}),
178 )
179 }
180}
181impl Server for Http3Protocol {
182 fn spawn(
183 &self,
184 ctx: SpawnContext,
185 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
186 {
187 Box::pin(super::Http3Server::spawn(ctx))
188 }
189 fn execute_action(&self, action: Value) -> Result<ActionResult> {
190 match action["type"].as_str().unwrap_or("") {
191 "send_http3_response" => {
192 let status = action["status"]
193 .as_u64()
194 .ok_or_else(|| anyhow::anyhow!("Missing integer status"))?;
195 ensure!(
196 (200..=599).contains(&status),
197 "Final status must be 200..599"
198 );
199 let body = action
200 .get("body")
201 .map(|b| {
202 b.as_str()
203 .ok_or_else(|| anyhow::anyhow!("body must be a string"))
204 })
205 .transpose()?
206 .unwrap_or("");
207 ensure!(body.len() <= MAX_BODY, "HTTP3 response body exceeds 8 MiB");
208 let headers = parse_headers(&action["headers"])?;
209 check_field_section(
210 &headers,
211 &[(
212 ":status",
213 http::StatusCode::from_u16(status as u16)?.as_str(),
214 )],
215 )?;
216 validate_length(&headers, body.len())?;
217 parse_headers(&action["trailers"])?;
218 ensure!(
219 !matches!(status, 204 | 205 | 304) || body.is_empty(),
220 "This status forbids a response body"
221 );
222 Ok(ActionResult::Custom {
223 name: "http3_response".into(),
224 data: action,
225 })
226 }
227 "cancel_http3_request" => Ok(ActionResult::CloseConnection),
228 _ => bail!("Unknown HTTP3 action"),
229 }
230 }
231}