netget/client/webtransport/
actions.rs1use crate::llm::actions::{
2 client_trait::{Client, ClientActionResult},
3 protocol_trait::Protocol,
4 ActionDefinition, ParameterDefinition,
5};
6use crate::protocol::{ConnectContext, EventType};
7use crate::server::webtransport::actions::{
8 parameter, reply, session_actions, validate, DATAGRAM_EVENT, STREAM_EVENT, STREAM_REPLY_EVENT,
9};
10use anyhow::{bail, Result};
11use serde_json::{json, Value};
12use std::sync::LazyLock;
13
14#[derive(Default)]
15pub struct WebTransportClientProtocol;
16impl WebTransportClientProtocol {
17 pub fn new() -> Self {
18 Self
19 }
20}
21
22pub static CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
23 EventType::new(
24 "webtransport_connected",
25 "The server accepted the session; open streams, send datagrams or wait for the server",
26 json!({"type": "webtransport_open_bi", "data": "hello"}),
27 )
28 .with_parameters(vec![parameter(
29 "url",
30 "string",
31 "The session's URL, e.g. https://127.0.0.1:4433/chat",
32 true,
33 )])
34 .with_actions(session_actions())
35});
36
37impl Protocol for WebTransportClientProtocol {
38 fn protocol_name(&self) -> &'static str {
39 "WebTransport"
40 }
41 fn stack_name(&self) -> &'static str {
42 "ETH>IP>UDP>QUIC>HTTP3>WebTransport"
43 }
44 fn keywords(&self) -> Vec<&'static str> {
45 vec!["webtransport", "webtransport client", "web transport"]
46 }
47 fn description(&self) -> &'static str {
48 "WebTransport over HTTP/3 client: opens a session on a URL, then exchanges streams and datagrams with the server"
49 }
50 fn get_async_actions(&self, _: &crate::state::app_state::AppState) -> Vec<ActionDefinition> {
51 session_actions()
52 }
53 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
54 let mut all = vec![reply()];
55 all.extend(session_actions());
56 all
57 }
58 fn get_event_types(&self) -> Vec<EventType> {
59 vec![
60 CONNECTED_EVENT.clone(),
61 STREAM_EVENT.clone(),
62 DATAGRAM_EVENT.clone(),
63 STREAM_REPLY_EVENT.clone(),
64 ]
65 }
66 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
67 let p =
68 |name: &str, kind: &str, description: &str, example: Value, default: Option<Value>| {
69 ParameterDefinition {
70 name: name.into(),
71 type_hint: kind.into(),
72 description: description.into(),
73 required: false,
74 example,
75 default,
76 }
77 };
78 vec![
79 p("path", "string", "The session path requested from the server", json!("/chat"), Some(json!(super::DEFAULT_PATH))),
80 p("certificate_sha256", "string", "Trust exactly the server certificate with this SHA-256 (hex, as browsers' serverCertificateHashes; ECDSA P-256, valid at most 14 days)", json!("3f1a…64 hex digits"), None),
81 p("ca_cert_path", "string", "PEM CA or certificate to trust instead of the system roots", json!("ca.pem"), None),
82 p("headers", "object", "Extra request header fields, e.g. {\"origin\": \"https://example.com\"}", json!({"origin": "https://example.com"}), None),
83 p("idle_timeout_secs", "number", "Close the session when idle this long, 1 to 3600 seconds", json!(30), Some(json!(crate::server::webtransport::IDLE_TIMEOUT.as_secs()))),
84 ]
85 }
86 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
87 use crate::protocol::metadata::*;
88 ProtocolMetadataV2::builder()
89 .state(DevelopmentState::Experimental)
90 .privilege_requirement(PrivilegeRequirement::None)
91 .implementation("wtransport 0.7.2 (vendored with settings, driver and QPACK bounds) over quinn: certificate-pinned, CA or system-root TLS, extended CONNECT, streams read whole, datagrams")
92 .llm_control("What the client sends on streams and datagrams, its answers to server-opened streams, and when it closes")
93 .e2e_testing("tests/client/webtransport: an aioquic 1.3.0 (independent, Python) WebTransport server echoes the client's streams and datagrams and opens streams to it")
94 .notes("A refused session reports only that it was refused (wtransport does not surface the status). Streams are delivered whole, up to 1 MiB within 30 s.")
95 .build()
96 }
97 fn example_prompt(&self) -> &'static str {
98 "Open a WebTransport session to https://127.0.0.1:4433/echo and send hello on a stream"
99 }
100 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
101 let llm = json!({"type":"open_client","protocol":"webtransport","remote_addr":"127.0.0.1:4433","instruction":"Send hello on a bidirectional stream and report the answer","startup_params":{"path":"/echo","certificate_sha256":"<server certificate sha-256>"}});
102 let mut static_example = llm.clone();
103 static_example["event_handlers"] = json!([{"event_pattern":"webtransport_connected","handler":{"type":"static","actions":[{"type":"webtransport_open_bi","data":"hello"}]}}]);
104 let mut scripted = llm.clone();
105 scripted["event_handlers"] = json!([{"event_pattern":"webtransport_datagram","handler":{"type":"script","language":"python","code":"import json,sys\ne=json.load(sys.stdin)['event']\nprint(json.dumps({'actions':[{'type':'webtransport_send_datagram','data':'ack '+e['data']}]}))"}}]);
106 crate::llm::actions::StartupExamples::new(llm, scripted, static_example)
107 }
108 fn group_name(&self) -> &'static str {
109 "Application"
110 }
111}
112
113impl Client for WebTransportClientProtocol {
114 fn connect(
115 &self,
116 ctx: ConnectContext,
117 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
118 {
119 Box::pin(super::connect(ctx))
120 }
121 fn execute_action(&self, v: Value) -> Result<ClientActionResult> {
122 if matches!(
123 v["type"].as_str(),
124 Some("webtransport_accept" | "webtransport_reject")
125 ) {
126 bail!("a client does not admit sessions");
127 }
128 validate(&v)?;
129 Ok(ClientActionResult::Custom {
130 name: v["type"].as_str().unwrap_or_default().into(),
131 data: v,
132 })
133 }
134}