1use crate::llm::actions::{
6 client_trait::{Client, ClientActionResult},
7 protocol_trait::Protocol,
8 ActionDefinition, ParameterDefinition, StartupExamples,
9};
10use crate::protocol::log_template::LogTemplate;
11use crate::protocol::{ConnectContext, EventType};
12use crate::server::h323::actions::{codecs_parameter, display_of, p};
13use crate::server::h323::{call, gatekeeper, h225};
14use crate::state::app_state::AppState;
15use anyhow::{anyhow, bail, Context, Result};
16use serde_json::{json, Value};
17use std::net::SocketAddr;
18use std::sync::LazyLock;
19
20pub const PLACE_CALL: &str = "h323_place_call";
21pub const HANGUP: &str = "h323_hangup";
22pub const REGISTER: &str = "h323_register";
23pub const UNREGISTER: &str = "h323_unregister";
24
25pub const DEFAULT_ALIAS: &str = "h323-ID:NetGet";
27pub const DEFAULT_MODE: &str = "auto";
29pub const DEFAULT_DISCOVER: bool = true;
31
32#[derive(Debug, Clone)]
33pub enum ClientCommand {
34 PlaceCall {
35 destination: String,
36 codecs: Vec<crate::server::h323::h245::Codec>,
37 display: Option<String>,
38 },
39 Hangup {
40 cause: u8,
41 reason: Option<String>,
42 },
43 Register {
44 aliases: Option<Vec<Value>>,
45 ttl: Option<u64>,
46 },
47 Unregister,
48}
49
50impl ClientCommand {
51 pub fn name(&self) -> &'static str {
52 match self {
53 ClientCommand::PlaceCall { .. } => PLACE_CALL,
54 ClientCommand::Hangup { .. } => HANGUP,
55 ClientCommand::Register { .. } => REGISTER,
56 ClientCommand::Unregister => UNREGISTER,
57 }
58 }
59}
60
61pub fn parse_command(a: &Value) -> Result<ClientCommand> {
62 let kind = a
63 .get("type")
64 .and_then(|t| t.as_str())
65 .ok_or_else(|| anyhow!("missing 'type'"))?;
66 Ok(match kind {
67 PLACE_CALL => {
68 let destination = a
69 .get("destination")
70 .and_then(|d| d.as_str())
71 .context("destination (an alias, a number or ip:port) is required")?
72 .to_string();
73 if destination.parse::<SocketAddr>().is_err() {
74 h225::alias(&destination)?;
75 }
76 ClientCommand::PlaceCall {
77 destination,
78 codecs: call::codecs_from(a.get("codec"))?,
79 display: display_of(a)?,
80 }
81 }
82 HANGUP => {
83 let (cause, reason) = crate::server::h323::actions::release_of(a)?;
84 ClientCommand::Hangup { cause, reason }
85 }
86 REGISTER => ClientCommand::Register {
87 aliases: match a.get("aliases") {
88 None | Some(Value::Null) => None,
89 Some(v) => Some(h225::aliases(v)?),
90 },
91 ttl: match a.get("time_to_live") {
92 None | Some(Value::Null) => None,
93 Some(t) => Some(
94 t.as_u64()
95 .filter(|t| (1..=gatekeeper::MAX_TTL_SECS).contains(t))
96 .ok_or_else(|| {
97 anyhow!("time_to_live is 1 to {} seconds", gatekeeper::MAX_TTL_SECS)
98 })?,
99 ),
100 },
101 },
102 UNREGISTER => ClientCommand::Unregister,
103 other => bail!("{other} is not an H.323 client action"),
104 })
105}
106
107fn action(
108 name: &str,
109 description: &str,
110 parameters: Vec<crate::llm::actions::Parameter>,
111 example: Value,
112) -> ActionDefinition {
113 ActionDefinition {
114 name: name.into(),
115 description: description.into(),
116 parameters,
117 example,
118 log_template: Some(
119 LogTemplate::new()
120 .with_info(format!("H.323 {name}"))
121 .with_debug(format!("H.323 {name}: {{json(.)}}")),
122 ),
123 }
124}
125
126pub fn client_actions() -> Vec<ActionDefinition> {
127 vec![
128 action(
129 PLACE_CALL,
130 "Place a call. Registered with a gatekeeper, NetGet asks admission first (ARQ) and \
131 calls the address the ACF names; otherwise it calls the remote endpoint directly. \
132 It offers G.711 by fastStart and tunnels H.245. The outcome arrives as \
133 h323_call_connected, h323_call_released, h323_call_forwarded or h323_call_failed. \
134 One call at a time.",
135 vec![
136 p(
137 "destination",
138 "string",
139 "Who to call: an alias (h323-ID:bob), a number (1002) or ip:port",
140 true,
141 ),
142 p(
143 "codec",
144 "string",
145 "g711u or g711a, or a list in order of preference (default both)",
146 false,
147 ),
148 p(
149 "display",
150 "string",
151 "Display name sent in the Setup (ASCII, up to 80 characters)",
152 false,
153 ),
154 ],
155 json!({"type": PLACE_CALL, "destination": "1002"}),
156 ),
157 action(
158 HANGUP,
159 "Hang up the current call with Release Complete (and tell the gatekeeper, DRQ).",
160 vec![
161 p(
162 "cause",
163 "number",
164 "Q.850 cause (default 16, normal clearing)",
165 false,
166 ),
167 p(
168 "reason",
169 "string",
170 "H.225.0 release reason, e.g. undefinedReason",
171 false,
172 ),
173 ],
174 json!({"type": HANGUP}),
175 ),
176 action(
177 REGISTER,
178 "Register (again) with the gatekeeper (RRQ), optionally under other aliases. The \
179 result arrives as h323_registration.",
180 vec![
181 p(
182 "aliases",
183 "array",
184 "Aliases as kind:value or bare values (default: the configured ones)",
185 false,
186 ),
187 p(
188 "time_to_live",
189 "number",
190 "Time-to-live to ask for, in seconds",
191 false,
192 ),
193 ],
194 json!({"type": REGISTER, "aliases": ["h323-ID:netget", "1003"]}),
195 ),
196 action(
197 UNREGISTER,
198 "Unregister from the gatekeeper (URQ). The result arrives as h323_registration.",
199 vec![],
200 json!({"type": UNREGISTER}),
201 ),
202 ]
203}
204
205pub static READY_EVENT: LazyLock<EventType> = LazyLock::new(|| {
206 EventType::new(
207 "h323_client_ready",
208 "The client is up. With a gatekeeper, registration has run and its result is here \
209 (confirmed or rejected, with the reason). Place a call with h323_place_call, \
210 (re)register, or do nothing",
211 json!({"type": PLACE_CALL, "destination": "1002"}),
212 )
213 .with_parameters(vec![
214 p(
215 "mode",
216 "string",
217 "gatekeeper or endpoint: what the remote address is",
218 true,
219 ),
220 p("remote", "string", "The remote address", true),
221 p(
222 "gatekeeper",
223 "object",
224 "GCF/GRJ: the gatekeeper's identifier and RAS address, or the reject reason",
225 false,
226 ),
227 p(
228 "registration",
229 "object",
230 "RCF/RRJ: endpoint identifier, aliases, time-to-live, or the reject reason",
231 false,
232 ),
233 p(
234 "local_call_signal_address",
235 "string",
236 "Where this endpoint takes calls",
237 false,
238 ),
239 ])
240 .with_actions(client_actions())
241 .with_log_template(
242 LogTemplate::new()
243 .with_info("H.323 client ready ({mode}) at {remote}")
244 .with_debug("H.323 client ready: {json(.)}"),
245 )
246});
247
248pub static REGISTRATION_EVENT: LazyLock<EventType> = LazyLock::new(|| {
249 EventType::new(
250 "h323_registration",
251 "The gatekeeper answered a registration or unregistration request (RCF/RRJ, UCF/URJ), \
252 or unregistered this endpoint itself (URQ)",
253 json!({"type": PLACE_CALL, "destination": "1002"}),
254 )
255 .with_parameters(vec![
256 p(
257 "request",
258 "string",
259 "RRQ, URQ, or gatekeeper_urq when the gatekeeper ended it",
260 true,
261 ),
262 p("result", "string", "confirmed, rejected or timeout", true),
263 p("reason", "string", "The reject reason, if rejected", false),
264 p(
265 "endpoint_id",
266 "string",
267 "The endpoint identifier assigned",
268 false,
269 ),
270 p("aliases", "array", "The aliases registered", false),
271 p(
272 "time_to_live",
273 "number",
274 "Seconds before NetGet must refresh it",
275 false,
276 ),
277 ])
278 .with_actions(client_actions())
279 .with_log_template(LogTemplate::new().with_info("H.323 {request}: {result} {reason}"))
280});
281
282pub static CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
283 EventType::new(
284 "h323_call_connected",
285 "The called endpoint answered (Connect). The codec and media addresses agreed and how \
286 are in media. Hang up with h323_hangup, or leave the call up",
287 json!({"type": HANGUP}),
288 )
289 .with_parameters(vec![
290 p(
291 "destination",
292 "string",
293 "The destination the call was placed to: an alias, a number or ip:port",
294 true,
295 ),
296 p(
297 "address",
298 "string",
299 "The call signalling address called",
300 true,
301 ),
302 p(
303 "progress",
304 "array",
305 "Messages the callee sent before Connect",
306 true,
307 ),
308 p(
309 "call",
310 "object",
311 "crv, call and conference identifiers, H.245 state and media",
312 true,
313 ),
314 p(
315 "admission",
316 "object",
317 "The gatekeeper's ACF, when admission ran",
318 false,
319 ),
320 ])
321 .with_actions(client_actions())
322 .with_log_template(LogTemplate::new().with_info("H.323 call to {destination} connected"))
323});
324
325pub static RELEASED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
326 EventType::new(
327 "h323_call_released",
328 "The call ended: the callee released it (Release Complete, with its Q.850 cause and \
329 H.225.0 reason), this client hung up, or the connection closed",
330 json!({"type": PLACE_CALL, "destination": "1002"}),
331 )
332 .with_parameters(vec![
333 p(
334 "destination",
335 "string",
336 "The destination the call was placed to: an alias, a number or ip:port",
337 true,
338 ),
339 p("released_by", "string", "remote, local or connection", true),
340 p("cause", "number", "Q.850 cause, if any", false),
341 p("reason", "string", "H.225.0 release reason, if any", false),
342 p(
343 "reached",
344 "string",
345 "How far the call got: offered, proceeding, alerting, connected",
346 true,
347 ),
348 p("progress", "array", "Messages the callee sent", true),
349 p(
350 "call",
351 "object",
352 "The call's identifiers, H.245 state and media",
353 true,
354 ),
355 p(
356 "disengage",
357 "string",
358 "The gatekeeper's answer to DRQ, when registered",
359 false,
360 ),
361 ])
362 .with_actions(client_actions())
363 .with_log_template(
364 LogTemplate::new()
365 .with_info("H.323 call to {destination} released by {released_by}, cause {cause}"),
366 )
367});
368
369pub static FAILED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
370 EventType::new(
371 "h323_call_failed",
372 "A call could not be placed: the gatekeeper refused admission (ARJ, with its reason), \
373 did not answer, or the destination could not be reached",
374 json!({"type": PLACE_CALL, "destination": "1002"}),
375 )
376 .with_parameters(vec![
377 p("destination", "string", "The destination the call was placed to: an alias, a number or ip:port", true),
378 p("stage", "string", "admission, connect or signalling", true),
379 p("reason", "string", "Why the call could not be placed: the ARJ reason, no answer from the gatekeeper, or the connect error", true),
380 ])
381 .with_actions(client_actions())
382 .with_log_template(LogTemplate::new().with_info("H.323 call to {destination} failed at {stage}: {reason}"))
383});
384
385pub static FORWARDED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
386 EventType::new(
387 "h323_call_forwarded",
388 "The callee deflected the call (Facility callForwarded / routeCallToGatekeeper) and \
389 named somewhere else to call. Call it with h323_place_call, or do nothing",
390 json!({"type": PLACE_CALL, "destination": "1002"}),
391 )
392 .with_parameters(vec![
393 p(
394 "destination",
395 "string",
396 "The destination the call was placed to: an alias, a number or ip:port",
397 true,
398 ),
399 p(
400 "forward_address",
401 "string",
402 "The address named, if any",
403 false,
404 ),
405 p(
406 "forward_aliases",
407 "array",
408 "The aliases named, if any",
409 true,
410 ),
411 ])
412 .with_actions(client_actions())
413 .with_log_template(LogTemplate::new().with_info("H.323 call to {destination} forwarded"))
414});
415
416pub static ERROR_EVENT: LazyLock<EventType> = LazyLock::new(|| {
417 EventType::new(
418 "h323_error",
419 "An action could not be carried out; nothing was sent for it",
420 json!({"type": PLACE_CALL, "destination": "1002"}),
421 )
422 .with_parameters(vec![
423 p("action", "string", "The action's type", true),
424 p("message", "string", "Why it was refused", true),
425 ])
426 .with_actions(client_actions())
427 .with_log_template(LogTemplate::new().with_info("H.323 {action} refused: {message}"))
428});
429
430pub static DISCONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
431 EventType::new(
432 "h323_client_stopped",
433 "The client stopped. Nothing more can be sent",
434 json!({"type": "show_message", "message": "H.323 client stopped"}),
435 )
436 .with_parameters(vec![p(
437 "reason",
438 "string",
439 "Why the client stopped: the operator stopped it, or its session ended",
440 true,
441 )])
442 .with_no_actions()
443 .with_log_template(LogTemplate::new().with_info("H.323 client stopped: {reason}"))
444});
445
446#[derive(Default)]
447pub struct H323ClientProtocol;
448
449impl H323ClientProtocol {
450 pub fn new() -> Self {
451 Self
452 }
453}
454
455impl Protocol for H323ClientProtocol {
456 fn protocol_name(&self) -> &'static str {
457 "H323"
458 }
459 fn stack_name(&self) -> &'static str {
460 "ETH>IP>TCP>TPKT>Q.931>H.225.0"
461 }
462 fn keywords(&self) -> Vec<&'static str> {
463 vec![
464 "h323",
465 "h.323",
466 "h.225",
467 "q.931",
468 "gatekeeper",
469 "h323 client",
470 ]
471 }
472 fn description(&self) -> &'static str {
473 "H.323 endpoint that registers with a gatekeeper (GRQ/RRQ), asks admission (ARQ), places calls with fastStart G.711 and tunnelled H.245, and hangs up, reporting every result decoded"
474 }
475 fn get_async_actions(&self, _: &AppState) -> Vec<ActionDefinition> {
476 client_actions()
477 }
478 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
479 vec![]
480 }
481 fn get_event_types(&self) -> Vec<EventType> {
482 vec![
483 READY_EVENT.clone(),
484 REGISTRATION_EVENT.clone(),
485 CONNECTED_EVENT.clone(),
486 RELEASED_EVENT.clone(),
487 FAILED_EVENT.clone(),
488 FORWARDED_EVENT.clone(),
489 ERROR_EVENT.clone(),
490 DISCONNECTED_EVENT.clone(),
491 ]
492 }
493 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
494 vec![
495 ParameterDefinition {
496 name: "mode".into(),
497 type_hint: "string".into(),
498 description: "What remote_addr is: gatekeeper (register and ask admission, RAS on \
499 UDP), endpoint (call it directly), or auto (gatekeeper on port 1719, \
500 endpoint otherwise)"
501 .into(),
502 required: false,
503 example: json!("gatekeeper"),
504 default: Some(json!(DEFAULT_MODE)),
505 },
506 ParameterDefinition {
507 name: "aliases".into(),
508 type_hint: "array".into(),
509 description: "Aliases to register under and call from, as kind:value \
510 (h323-ID:alice, dialledDigits:1001) or bare values"
511 .into(),
512 required: false,
513 example: json!(["h323-ID:netget", "1003"]),
514 default: Some(json!([DEFAULT_ALIAS])),
515 },
516 ParameterDefinition {
517 name: "time_to_live".into(),
518 type_hint: "number".into(),
519 description: "Registration time-to-live to ask the gatekeeper for, in seconds"
520 .into(),
521 required: false,
522 example: json!(120),
523 default: Some(json!(gatekeeper::DEFAULT_TTL_SECS)),
524 },
525 ParameterDefinition {
526 name: "discover".into(),
527 type_hint: "boolean".into(),
528 description: "Send GRQ before registering (some gatekeepers require discovery)"
529 .into(),
530 required: false,
531 example: json!(false),
532 default: Some(json!(DEFAULT_DISCOVER)),
533 },
534 codecs_parameter(),
535 ]
536 }
537 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
538 use crate::protocol::metadata::*;
539 ProtocolMetadataV2::builder()
540 .state(DevelopmentState::Experimental)
541 .privilege_requirement(PrivilegeRequirement::None)
542 .implementation("The H.323 server's PER codec, Q.931 framing and call leg as the calling side: GRQ, RRQ (full, then keep-alive before the time-to-live runs out), ARQ, DRQ and URQ over a RAS socket, Setup with fastStart G.711 proposals and tunnelled H.245 over TPKT, Release Complete; IRQ from the gatekeeper answered with IRR, its URQ with UCF. Incoming calls on the registered call signalling address are refused")
543 .llm_control("When to register and under which aliases, who to call (alias, number or address) in which codec, when to hang up, and what to do after a reject, a forward or a release")
544 .e2e_testing("tests/client/h323: GNU Gatekeeper (H323Plus) confirms NetGet's GRQ and RRQ, lists it in its status port's registrations and admits its ARQ; Asterisk's chan_ooh323 (ooh323c) and H323Plus's simph323 answer NetGet's Setup and the call connects with fastStart media; tshark's h225, q931 and h245 dissectors read every message both ways")
545 .notes("Experimental: one gatekeeper implementation (GNU Gatekeeper) and two call peers (ooh323c, H323Plus) drive it in tests that fail rather than skip, but nobody has reviewed it against H.225.0 and its pair job had not run in CI when it landed. One call at a time. No H.235 security, no separate H.245 channel, G.711 only, no media sent.")
546 .build()
547 }
548 fn example_prompt(&self) -> &'static str {
549 "Register with the H.323 gatekeeper at 127.0.0.1:1719 as 1003, call 1002, and hang up once it connects"
550 }
551 fn get_startup_examples(&self) -> StartupExamples {
552 let llm = json!({"type": "open_client", "protocol": "H323", "remote_addr": "127.0.0.1:1719",
553 "startup_params": {"aliases": ["1003"]},
554 "instruction": "Once registered, call 1002. When it connects, hang up and report the codec agreed."});
555 let mut static_example = llm.clone();
556 static_example["event_handlers"] = json!([
557 {"event_pattern": "h323_client_ready", "handler": {"type": "static", "actions": [
558 {"type": PLACE_CALL, "destination": "1002"}
559 ]}},
560 {"event_pattern": "h323_call_connected", "handler": {"type": "static", "actions": [
561 {"type": HANGUP}
562 ]}},
563 {"event_pattern": "*", "handler": {"type": "static", "actions": []}}
564 ]);
565 let mut scripted = static_example.clone();
566 scripted["event_handlers"][0] = json!({"event_pattern": "h323_client_ready", "handler": {"type": "script", "language": "python",
567 "code": "import json,sys\ne=json.load(sys.stdin)['event']\nreg=(e.get('registration') or {})\na=[{'type':'h323_place_call','destination':'1002'}] if reg.get('result')!='rejected' else [{'type':'h323_register','aliases':['1004']}]\nprint(json.dumps({'actions':a}))"}});
568 StartupExamples::new(llm, scripted, static_example)
569 }
570 fn group_name(&self) -> &'static str {
571 "VoIP & Media"
572 }
573}
574
575impl Client for H323ClientProtocol {
576 fn connect(
577 &self,
578 ctx: ConnectContext,
579 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<SocketAddr>> + Send>> {
580 Box::pin(super::connect(ctx))
581 }
582
583 fn execute_action(&self, v: Value) -> Result<ClientActionResult> {
584 parse_command(&v)?;
585 Ok(ClientActionResult::Custom {
586 name: v["type"].as_str().unwrap_or_default().to_string(),
587 data: v,
588 })
589 }
590}