1use crate::llm::actions::{
3 client_trait::{Client, ClientActionResult},
4 protocol_trait::Protocol,
5 ActionDefinition, ParameterDefinition,
6};
7use crate::protocol::{ConnectContext, EventType};
8use crate::server::inetd::{
9 actions::{action, parameter, Service},
10 wire,
11};
12use crate::state::app_state::AppState;
13use anyhow::{bail, ensure, Result};
14use serde_json::{json, Value};
15use std::sync::LazyLock;
16
17pub const DEFAULT_CHARGEN_READ: u64 = 1024;
19pub const MAX_CHARGEN_READ: u64 = 64 * 1024;
21
22pub fn query_action_name(service: Service) -> &'static str {
23 match service {
24 Service::Echo => "echo_send",
25 Service::Discard => "discard_send",
26 Service::Daytime => "daytime_query",
27 Service::Qotd => "qotd_query",
28 Service::Chargen => "chargen_query",
29 Service::Time => "time_query",
30 }
31}
32
33fn query_action(service: Service) -> ActionDefinition {
34 let name = query_action_name(service);
35 match service {
36 Service::Echo => action(
37 name,
38 "Send data to the echo service and read it back; the reply arrives as echo_response.",
39 vec![
40 parameter("data", "string", "Bytes to send, as text or hex per encoding", true),
41 parameter("encoding", "string", "Encoding of data: utf8 (default) or hex", false),
42 ],
43 json!({"type":name,"data":"ping"}),
44 ),
45 Service::Discard => action(
46 name,
47 "Send data to the discard service, which answers nothing; discard_sent confirms it left.",
48 vec![
49 parameter("data", "string", "Bytes to send, as text or hex per encoding", true),
50 parameter("encoding", "string", "Encoding of data: utf8 (default) or hex", false),
51 ],
52 json!({"type":name,"data":"into the void"}),
53 ),
54 Service::Chargen => action(
55 name,
56 "Read generated characters; the text arrives as chargen_response with a check against the RFC 864 pattern.",
57 vec![parameter("bytes", "number", "Bytes to read over TCP before closing (default 1024, at most 65536)", false)],
58 json!({"type":name}),
59 ),
60 Service::Daytime => action(name, "Ask for the date and time; the line arrives as daytime_response.", vec![], json!({"type":name})),
61 Service::Qotd => action(name, "Ask for the quote of the day; it arrives as qotd_response.", vec![], json!({"type":name})),
62 Service::Time => action(name, "Ask for the RFC 868 time; it arrives as time_response, decoded.", vec![], json!({"type":name})),
63 }
64}
65
66fn disconnect_action() -> ActionDefinition {
67 action(
68 "disconnect",
69 "Stop this client",
70 vec![],
71 json!({"type":"disconnect"}),
72 )
73}
74
75fn actions(service: Service) -> Vec<ActionDefinition> {
76 vec![query_action(service), disconnect_action()]
77}
78
79pub fn response_event_id(service: Service) -> &'static str {
80 match service {
81 Service::Echo => "echo_response",
82 Service::Discard => "discard_sent",
83 Service::Daytime => "daytime_response",
84 Service::Qotd => "qotd_response",
85 Service::Chargen => "chargen_response",
86 Service::Time => "time_response",
87 }
88}
89
90fn ready_event_id(service: Service) -> &'static str {
91 match service {
92 Service::Echo => "echo_ready",
93 Service::Discard => "discard_ready",
94 Service::Daytime => "daytime_ready",
95 Service::Qotd => "qotd_ready",
96 Service::Chargen => "chargen_ready",
97 Service::Time => "time_ready",
98 }
99}
100
101fn build_ready(service: Service) -> EventType {
102 EventType::new(
103 ready_event_id(service),
104 "The client is ready; each query is one TCP connection or one UDP datagram.",
105 query_action(service).example.clone(),
106 )
107 .with_parameters(vec![
108 parameter(
109 "remote_addr",
110 "string",
111 "Address of the service being queried",
112 true,
113 ),
114 parameter(
115 "transport",
116 "string",
117 "tcp or udp, from the transport startup parameter",
118 true,
119 ),
120 ])
121 .with_actions(actions(service))
122}
123
124fn build_response(service: Service) -> EventType {
125 let transport = parameter(
126 "transport",
127 "string",
128 "tcp or udp: how the exchange was made",
129 true,
130 );
131 let params = match service {
132 Service::Echo => vec![
133 transport,
134 parameter(
135 "data",
136 "string",
137 "What came back, as text or hex per encoding",
138 true,
139 ),
140 parameter(
141 "encoding",
142 "string",
143 "utf8 when the bytes are printable text, otherwise hex",
144 true,
145 ),
146 parameter("bytes", "number", "Number of bytes received", true),
147 parameter(
148 "matches",
149 "boolean",
150 "True when the reply equals what was sent",
151 true,
152 ),
153 ],
154 Service::Discard => vec![
155 transport,
156 parameter("bytes", "number", "Bytes sent and discarded", true),
157 ],
158 Service::Daytime => vec![
159 transport,
160 parameter(
161 "text",
162 "string",
163 "The date and time line, without its line ending",
164 true,
165 ),
166 ],
167 Service::Qotd => vec![
168 transport,
169 parameter(
170 "quote",
171 "string",
172 "The quote, line endings normalised to newlines",
173 true,
174 ),
175 ],
176 Service::Chargen => vec![
177 transport,
178 parameter("text", "string", "The characters received", true),
179 parameter("bytes", "number", "Number of bytes received", true),
180 parameter(
181 "conforms",
182 "boolean",
183 "True when the text follows RFC 864's rotating printable-ASCII pattern",
184 true,
185 ),
186 ],
187 Service::Time => vec![
188 transport,
189 parameter(
190 "seconds_since_1900",
191 "number",
192 "The raw 32-bit RFC 868 value",
193 true,
194 ),
195 parameter(
196 "unix_seconds",
197 "number",
198 "The same instant as Unix seconds",
199 true,
200 ),
201 parameter(
202 "iso8601",
203 "string",
204 "The same instant as an RFC 3339 timestamp",
205 true,
206 ),
207 ],
208 };
209 EventType::new(
210 response_event_id(service),
211 "The service's answer to one query.",
212 query_action(service).example.clone(),
213 )
214 .with_parameters(params)
215 .with_actions(actions(service))
216}
217
218macro_rules! events {
219 ($ready:ident, $response:ident, $service:expr) => {
220 pub static $ready: LazyLock<EventType> = LazyLock::new(|| build_ready($service));
221 pub static $response: LazyLock<EventType> = LazyLock::new(|| build_response($service));
222 };
223}
224events!(ECHO_READY, ECHO_RESPONSE, Service::Echo);
225events!(DISCARD_READY, DISCARD_RESPONSE, Service::Discard);
226events!(DAYTIME_READY, DAYTIME_RESPONSE, Service::Daytime);
227events!(QOTD_READY, QOTD_RESPONSE, Service::Qotd);
228events!(CHARGEN_READY, CHARGEN_RESPONSE, Service::Chargen);
229events!(TIME_READY, TIME_RESPONSE, Service::Time);
230
231pub fn ready_event(service: Service) -> &'static EventType {
232 match service {
233 Service::Echo => &ECHO_READY,
234 Service::Discard => &DISCARD_READY,
235 Service::Daytime => &DAYTIME_READY,
236 Service::Qotd => &QOTD_READY,
237 Service::Chargen => &CHARGEN_READY,
238 Service::Time => &TIME_READY,
239 }
240}
241
242pub fn response_event(service: Service) -> &'static EventType {
243 match service {
244 Service::Echo => &ECHO_RESPONSE,
245 Service::Discard => &DISCARD_RESPONSE,
246 Service::Daytime => &DAYTIME_RESPONSE,
247 Service::Qotd => &QOTD_RESPONSE,
248 Service::Chargen => &CHARGEN_RESPONSE,
249 Service::Time => &TIME_RESPONSE,
250 }
251}
252
253#[derive(Debug, Clone)]
255pub struct Query {
256 pub payload: Vec<u8>,
257 pub read_bytes: u64,
258}
259
260pub fn parse_query(service: Service, v: &Value) -> Result<Query> {
261 ensure!(
262 v["type"].as_str() == Some(query_action_name(service)),
263 "Unknown {} client action",
264 service.name()
265 );
266 let payload = match service {
267 Service::Echo | Service::Discard => {
268 let data = v["data"]
269 .as_str()
270 .ok_or_else(|| anyhow::anyhow!("data must be a string"))?;
271 let bytes = wire::decode(data, v["encoding"].as_str())?;
272 ensure!(
273 bytes.len() <= wire::READ_CHUNK,
274 "data exceeds {} bytes",
275 wire::READ_CHUNK
276 );
277 ensure!(
278 !bytes.is_empty() || service == Service::Discard,
279 "echo data must not be empty"
280 );
281 bytes
282 }
283 _ => Vec::new(),
284 };
285 let read_bytes = match v.get("bytes").filter(|b| !b.is_null()) {
286 None => DEFAULT_CHARGEN_READ,
287 Some(b) => {
288 let n = b
289 .as_u64()
290 .ok_or_else(|| anyhow::anyhow!("bytes must be a positive integer"))?;
291 ensure!(
292 (1..=MAX_CHARGEN_READ).contains(&n),
293 "bytes must be 1 to {MAX_CHARGEN_READ}"
294 );
295 n
296 }
297 };
298 Ok(Query {
299 payload,
300 read_bytes,
301 })
302}
303
304pub fn startup_parameters() -> Vec<ParameterDefinition> {
305 vec![ParameterDefinition {
306 name: "transport".into(),
307 type_hint: "string".into(),
308 description: "tcp (one connection per query) or udp (one datagram per query)".into(),
309 required: false,
310 example: json!("udp"),
311 default: Some(json!(super::DEFAULT_TRANSPORT)),
312 }]
313}
314
315pub fn metadata(service: Service) -> crate::protocol::metadata::ProtocolMetadataV2 {
316 use crate::protocol::metadata::*;
317 ProtocolMetadataV2::builder()
318 .state(DevelopmentState::Experimental)
319 .privilege_requirement(PrivilegeRequirement::None)
320 .well_known_port(match service {
321 Service::Echo => 7,
322 Service::Discard => 9,
323 Service::Daytime => 13,
324 Service::Qotd => 17,
325 Service::Chargen => 19,
326 Service::Time => 37,
327 })
328 .implementation("One query per TCP connection or UDP datagram, bounded and with deadlines")
329 .llm_control("When to query the service and what to do with each answer")
330 .e2e_testing("tests/client/inetd: xinetd's own built-in services (and an external QOTD program) as the independent server")
331 .notes("Replies are bounded to 64 KiB on TCP and 8 KiB per datagram; connect, write and reply deadlines are 30 s on TCP, 5 s for a UDP reply. A UDP query that gets no reply raises no event and is logged.")
332 .max_inbound_bytes(wire::MAX_STREAM_REPLY)
333 .build()
334}
335
336pub fn startup_examples(service: Service) -> crate::llm::actions::StartupExamples {
337 let name = service.feature_name();
338 let llm = json!({"type":"open_client","protocol":name,"remote_addr":format!("127.0.0.1:{}", 10000 + service.port()),"instruction":format!("Query the {} service once", service.name())});
339 let query = query_action(service).example;
340 let mut static_example = llm.clone();
341 static_example["event_handlers"] = json!([
342 {"event_pattern": ready_event_id(service), "handler": {"type":"static","actions":[query.clone()]}},
343 {"event_pattern": response_event_id(service), "handler": {"type":"static","actions":[{"type":"disconnect"}]}}
344 ]);
345 let mut scripted = static_example.clone();
346 scripted["event_handlers"][0]["handler"] = json!({"type":"script","language":"python","code":format!("import json\nprint(json.dumps({{'actions':[{query}]}}))")});
347 crate::llm::actions::StartupExamples::new(llm, scripted, static_example)
348}
349
350pub fn execute(service: Service, v: Value) -> Result<ClientActionResult> {
351 match v["type"].as_str() {
352 Some("disconnect") => Ok(ClientActionResult::Disconnect),
353 Some(name) if name == query_action_name(service) => {
354 parse_query(service, &v)?;
355 Ok(ClientActionResult::Custom {
356 name: name.to_string(),
357 data: v,
358 })
359 }
360 _ => bail!("Unknown {} client action", service.name()),
361 }
362}
363
364macro_rules! inetd_client {
365 ($ty:ident, $service:expr, $prompt:literal) => {
366 #[derive(Default)]
367 pub struct $ty;
368 impl $ty {
369 pub fn new() -> Self {
370 Self
371 }
372 }
373 impl Protocol for $ty {
374 fn protocol_name(&self) -> &'static str {
375 $service.name()
376 }
377 fn stack_name(&self) -> &'static str {
378 $service.stack_name()
379 }
380 fn keywords(&self) -> Vec<&'static str> {
381 $service.keywords()
382 }
383 fn description(&self) -> &'static str {
384 match $service {
385 Service::Echo => "Echo client (RFC 862) over TCP or UDP",
386 Service::Discard => "Discard client (RFC 863) over TCP or UDP",
387 Service::Daytime => "Daytime client (RFC 867) over TCP or UDP",
388 Service::Qotd => "Quote of the Day client (RFC 865) over TCP or UDP",
389 Service::Chargen => "Character Generator client (RFC 864) over TCP or UDP",
390 Service::Time => "Time client (RFC 868) over TCP or UDP",
391 }
392 }
393 fn get_async_actions(&self, _: &AppState) -> Vec<ActionDefinition> {
394 actions($service)
395 }
396 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
397 vec![]
398 }
399 fn get_event_types(&self) -> Vec<EventType> {
400 vec![
401 ready_event($service).clone(),
402 response_event($service).clone(),
403 ]
404 }
405 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
406 startup_parameters()
407 }
408 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
409 metadata($service)
410 }
411 fn example_prompt(&self) -> &'static str {
412 $prompt
413 }
414 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
415 startup_examples($service)
416 }
417 fn group_name(&self) -> &'static str {
418 "Network Services"
419 }
420 }
421 impl Client for $ty {
422 fn connect(
423 &self,
424 ctx: ConnectContext,
425 ) -> std::pin::Pin<
426 Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>,
427 > {
428 Box::pin(super::connect(ctx, $service))
429 }
430 fn execute_action(&self, v: Value) -> Result<ClientActionResult> {
431 execute($service, v)
432 }
433 }
434 };
435}
436
437inetd_client!(
438 EchoClientProtocol,
439 Service::Echo,
440 "Send 'hello' to the echo service at 127.0.0.1:10007 and check it comes back"
441);
442inetd_client!(
443 DiscardClientProtocol,
444 Service::Discard,
445 "Send a test line to the discard service at 127.0.0.1:10009"
446);
447inetd_client!(
448 DaytimeClientProtocol,
449 Service::Daytime,
450 "Ask the daytime service at 127.0.0.1:10013 for the time"
451);
452inetd_client!(
453 QotdClientProtocol,
454 Service::Qotd,
455 "Fetch the quote of the day from 127.0.0.1:10017"
456);
457inetd_client!(
458 ChargenClientProtocol,
459 Service::Chargen,
460 "Read 2 KB from the chargen service at 127.0.0.1:10019"
461);
462inetd_client!(
463 TimeClientProtocol,
464 Service::Time,
465 "Ask the RFC 868 time server at 127.0.0.1:10037 for the time"
466);
467
468pub fn protocol(service: Service) -> &'static dyn Client {
469 match service {
470 Service::Echo => &EchoClientProtocol,
471 Service::Discard => &DiscardClientProtocol,
472 Service::Daytime => &DaytimeClientProtocol,
473 Service::Qotd => &QotdClientProtocol,
474 Service::Chargen => &ChargenClientProtocol,
475 Service::Time => &TimeClientProtocol,
476 }
477}