freeswitch_types/variables/loopback.rs
1//! Typed mod_loopback channel variable names, channel names, and the bowout
2//! marker.
3
4sip_header::define_header_enum! {
5 tests_mod: loopback_variable_generated_tests,
6 error_type: ParseLoopbackVariableError => "unknown loopback variable",
7 /// mod_loopback channel variable names (the part after the `variable_` prefix).
8 ///
9 /// Use with [`HeaderLookup::variable()`](crate::HeaderLookup::variable) for
10 /// type-safe lookups.
11 pub enum LoopbackVariable {
12 // --- Set by mod_loopback ---
13 IsLoopback => "is_loopback",
14 LoopbackLeg => "loopback_leg",
15 LoopbackFromUuid => "loopback_from_uuid",
16 OtherLoopbackFromUuid => "other_loopback_from_uuid",
17 OtherLoopbackLegUuid => "other_loopback_leg_uuid",
18 /// Set on the *real* channel bridged to a loopback leg, not on the leg.
19 OtherLegTrueId => "other_leg_true_id",
20 /// Application named by the `loopback/app=<application>[:<args>]`
21 /// destination form, which runs instead of a dialplan lookup.
22 LoopbackApp => "loopback_app",
23 LoopbackAppArg => "loopback_app_arg",
24 /// Presence marks a bowout. See [`LoopbackResignation`].
25 LoopbackHangupCause => "loopback_hangup_cause",
26 /// The real channel that outlives the bowout.
27 LoopbackBowoutOtherUuid => "loopback_bowout_other_uuid",
28
29 // --- Set by the caller ---
30 /// Vetoes the bowout when false. Unset means enabled.
31 LoopbackBowout => "loopback_bowout",
32 /// Bows out as soon as the leg executes an application, instead of
33 /// waiting for audio to flow.
34 LoopbackBowoutOnExecute => "loopback_bowout_on_execute",
35 /// Space-separated variable names to copy onto the B leg.
36 LoopbackExport => "loopback_export",
37 LoopbackInitialCodec => "loopback_initial_codec",
38 }
39}
40
41wire_enum! {
42 /// Which mod_loopback path resigned, from the `loopback_hangup_cause` value.
43 ///
44 /// Both variants mean the same thing to a consumer: the loopback leg is
45 /// gone and the call continues elsewhere. Branch on this only to log or
46 /// to tell the two triggers apart -- never to decide whether a bowout
47 /// happened. That is [`LoopbackResignation`]'s job.
48 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49 pub enum LoopbackHangupCause {
50 /// Execute-time masquerade, triggered by `loopback_bowout_on_execute`
51 /// or by an application flagged `SAF_NO_LOOPBACK`.
52 Bowout => "bowout",
53 /// Frame-count path: both legs bridged and answered, so mod_loopback
54 /// `uuid_bridge`s the two real channels together.
55 Bridge => "bridge",
56 }
57 error ParseLoopbackHangupCauseError("loopback hangup cause");
58 tests: loopback_hangup_cause_generated_tests;
59}
60
61wire_enum! {
62 /// Which half of a loopback pair, from the `loopback_leg` variable.
63 ///
64 /// The A leg is the one an `originate` returns; the B leg is routed into the
65 /// dialplan. Each carries the partner's UUID in `other_loopback_leg_uuid`.
66 ///
67 /// The wire form is the variable's uppercase spelling. The lowercase suffix
68 /// a channel name carries is [`LoopbackChannelName`]'s business.
69 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
70 pub enum LoopbackLeg {
71 /// The leg an `originate` returns.
72 A => "A",
73 /// The leg routed into the dialplan.
74 B => "B",
75 }
76 error ParseLoopbackLegError("loopback leg");
77 tests: loopback_leg_generated_tests;
78}
79
80/// Borrowed view of a parsed loopback channel name. No allocation.
81///
82/// mod_loopback names its pair `loopback/<extension>-a` and `-b`, from the
83/// destination with the `/context/dialplan` tail stripped and an `app=` form
84/// reduced to the bare application token. `Some` only for names carrying a leg
85/// suffix; other drivers return `None`.
86///
87/// This is the only field that answers whether a loopback leg emitted an event.
88/// The execute-time resignation copies every channel variable onto the real
89/// channel that continues the call and clones the resigning leg's caller
90/// profile onto it, so `is_loopback`, `loopback_leg` and every `Caller-*` header
91/// -- `Caller-Channel-Name` included -- then describe a channel that is gone.
92/// Feed this parser
93/// [`HeaderLookup::channel_name()`](crate::HeaderLookup::channel_name), which
94/// reads `Channel-Name`, and nothing else. Mechanics are in
95/// `docs/loopback-bowout.md`.
96///
97/// A name is what a channel calls itself, not proof of its driver: a SIP peer
98/// can rename a sofia channel through the `X-FS-Channel-Name` header, which
99/// mod_sofia records as the `push_channel_name` variable. So a name answers what
100/// kind of channel this is, and never carries an authorization decision.
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub struct LoopbackChannelName<'a> {
103 extension: &'a str,
104 leg: LoopbackLeg,
105}
106
107impl<'a> LoopbackChannelName<'a> {
108 /// Parses a channel name; `Some` only for `loopback/<extension>-a|-b`.
109 pub fn parse(name: &'a str) -> Option<Self> {
110 let rest = name.strip_prefix("loopback/")?;
111 // The suffix is always present, and an extension may itself end in one,
112 // so exactly one is stripped from the end.
113 let (extension, leg) = match rest.strip_suffix("-a") {
114 Some(extension) => (extension, LoopbackLeg::A),
115 None => (rest.strip_suffix("-b")?, LoopbackLeg::B),
116 };
117 if extension.is_empty() {
118 return None;
119 }
120 Some(Self { extension, leg })
121 }
122
123 /// Dialled extension, or the application token of an `app=` destination.
124 pub fn extension(&self) -> &'a str {
125 self.extension
126 }
127
128 /// Which half of the pair this is.
129 pub fn leg(&self) -> LoopbackLeg {
130 self.leg
131 }
132}
133
134/// A loopback leg that resigned from a call which is still up.
135///
136/// mod_loopback takes itself out of the path once it can connect the two real
137/// channels directly, so the leg emits `CHANNEL_HANGUP_COMPLETE` for a call
138/// that is alive and connected on [`other_uuid()`](Self::other_uuid). Track
139/// that channel instead of treating the hangup as a teardown.
140///
141/// Obtained from
142/// [`HeaderLookup::loopback_resignation()`](crate::HeaderLookup::loopback_resignation),
143/// which returns `Some` whenever the marker variable is present regardless of
144/// its value -- mod_loopback writes a different token per path, and a third
145/// one would still be a resignation. [`cause()`](Self::cause) asks the separate
146/// question of which path it was.
147#[derive(Debug, Clone, Copy, PartialEq, Eq)]
148pub struct LoopbackResignation<'a> {
149 cause_raw: &'a str,
150 other_uuid: Option<&'a str>,
151}
152
153impl<'a> LoopbackResignation<'a> {
154 pub(crate) fn new(cause_raw: &'a str, other_uuid: Option<&'a str>) -> Self {
155 Self {
156 cause_raw,
157 other_uuid,
158 }
159 }
160
161 /// The `loopback_hangup_cause` value verbatim.
162 pub fn cause_raw(&self) -> &'a str {
163 self.cause_raw
164 }
165
166 /// Which path resigned.
167 ///
168 /// `Err` means mod_loopback used a path this crate does not know yet --
169 /// worth logging, but the resignation itself still stands.
170 pub fn cause(&self) -> Result<LoopbackHangupCause, ParseLoopbackHangupCauseError> {
171 self.cause_raw
172 .parse()
173 }
174
175 /// The real channel that outlives the loopback leg, from
176 /// `loopback_bowout_other_uuid`.
177 pub fn other_uuid(&self) -> Option<&'a str> {
178 self.other_uuid
179 }
180}
181
182#[cfg(test)]
183mod channel_name_tests {
184 use super::*;
185
186 #[test]
187 fn both_legs_of_a_dialled_extension() {
188 let a = LoopbackChannelName::parse("loopback/9199-a").expect("loopback name parses");
189 assert_eq!(a.extension(), "9199");
190 assert_eq!(a.leg(), LoopbackLeg::A);
191
192 let b = LoopbackChannelName::parse("loopback/9199-b").expect("loopback name parses");
193 assert_eq!(b.extension(), "9199");
194 assert_eq!(b.leg(), LoopbackLeg::B);
195 }
196
197 // `loopback/app=bridge:x/y` is reduced to the bare application token before
198 // the name is built.
199 #[test]
200 fn app_form_carries_the_application_token() {
201 let n = LoopbackChannelName::parse("loopback/bridge-a").expect("loopback name parses");
202 assert_eq!(n.extension(), "bridge");
203 }
204
205 // The suffix is always present, so exactly one is stripped.
206 #[test]
207 fn an_extension_may_itself_end_in_a_leg_suffix() {
208 let n = LoopbackChannelName::parse("loopback/park-a-b").expect("loopback name parses");
209 assert_eq!(n.extension(), "park-a");
210 assert_eq!(n.leg(), LoopbackLeg::B);
211 }
212
213 #[test]
214 fn non_loopback_and_malformed_are_none() {
215 for name in [
216 "loopback/9199",
217 "loopback/9199-A",
218 "loopback/9199-c",
219 "loopback/-a",
220 "loopback/",
221 "loopback",
222 "sofia/internal/1000@example.com",
223 "Loopback/9199-a",
224 "",
225 ] {
226 assert_eq!(LoopbackChannelName::parse(name), None, "{name:?}");
227 }
228 }
229
230 // The variable is uppercase; the name suffix is lowercase and never parses
231 // through FromStr.
232 #[test]
233 fn leg_wire_form_is_the_variable_spelling() {
234 assert_eq!(LoopbackLeg::A.as_str(), "A");
235 assert_eq!("B".parse(), Ok(LoopbackLeg::B));
236 assert!("a"
237 .parse::<LoopbackLeg>()
238 .is_err());
239 }
240}