Skip to main content

netget/server/http3/
actions.rs

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}