Skip to main content

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}