Skip to main content

freeswitch_log_parser/
peer.rs

1//! Peer-leg UUID harvesting: which channel variables name another leg of the
2//! same call, and how to pull those UUIDs out of a parsed entry.
3//!
4//! Distinct from [`SessionState::other_leg_uuid`](crate::SessionState::other_leg_uuid),
5//! which tracks the one authoritative peer of a session as it evolves. This
6//! harvests every peer a single entry mentions, which is what a "show me this
7//! call and everything it bridged to" search needs to seed itself.
8
9use std::str::FromStr;
10
11use freeswitch_types::variables::LoopbackVariable;
12use freeswitch_types::ChannelVariable;
13
14use crate::message::MessageKind;
15use crate::session::parse_bridge_args;
16use crate::stream::{Block, LogEntry};
17use crate::uuid::find_uuids;
18
19/// Channel variables whose value is, or contains, a peer leg's UUID.
20pub const PEER_UUID_VARS: &[ChannelVariable] = &[
21    ChannelVariable::BridgeUuid,
22    ChannelVariable::SignalBond,
23    ChannelVariable::SignalBridge,
24    ChannelVariable::LastBridgeTo,
25    ChannelVariable::OriginatingLegUuid,
26    ChannelVariable::OriginationUuid,
27    ChannelVariable::OriginatedLegs,
28    ChannelVariable::TransferSource,
29    ChannelVariable::TransferHistory,
30];
31
32/// mod_loopback variables whose value is another leg's UUID. Separate from
33/// [`PEER_UUID_VARS`] only because `freeswitch-types` gives them their own enum.
34pub const LOOPBACK_PEER_UUID_VARS: &[LoopbackVariable] = &[
35    LoopbackVariable::LoopbackFromUuid,
36    LoopbackVariable::OtherLoopbackFromUuid,
37    LoopbackVariable::OtherLoopbackLegUuid,
38    LoopbackVariable::OtherLegTrueId,
39    LoopbackVariable::LoopbackBowoutOtherUuid,
40];
41
42/// Whether `name` is one of [`PEER_UUID_VARS`] or [`LOOPBACK_PEER_UUID_VARS`].
43/// Accepts the bare variable name; strip any `variable_` prefix first.
44pub fn is_peer_uuid_var(name: &str) -> bool {
45    ChannelVariable::from_str(name)
46        .map(|v| PEER_UUID_VARS.contains(&v))
47        .unwrap_or(false)
48        || LoopbackVariable::from_str(name)
49            .map(|v| LOOPBACK_PEER_UUID_VARS.contains(&v))
50            .unwrap_or(false)
51}
52
53/// Call `f` with every peer-leg UUID `entry` mentions.
54///
55/// See [`for_each_peer_uuid_with`]; this recognizes only vanilla FreeSWITCH
56/// variables.
57pub fn for_each_peer_uuid<F: FnMut(&str)>(entry: &LogEntry, f: F) {
58    for_each_peer_uuid_with(entry, |_| false, f);
59}
60
61/// Call `f` with every peer-leg UUID `entry` mentions, treating a variable as
62/// peer-bearing when it is in [`PEER_UUID_VARS`] or `extra_var` accepts its name.
63///
64/// Walks only structured variable assignments — `CHANNEL_DATA` fields, standalone
65/// variable lines, `set`/`export`/`bridge` executions. Scanning the raw message
66/// and attached text instead would harvest shared-context variables (a FusionPBX
67/// `domain_uuid`, say) and pull in every unrelated call in the same tenant.
68pub fn for_each_peer_uuid_with<F: FnMut(&str)>(
69    entry: &LogEntry,
70    extra_var: impl Fn(&str) -> bool,
71    mut f: F,
72) {
73    let wanted = |name: &str| {
74        let name = name.strip_prefix("variable_").unwrap_or(name);
75        is_peer_uuid_var(name) || extra_var(name)
76    };
77    let mut harvest = |value: &str| {
78        for (_, uuid) in find_uuids(value) {
79            f(uuid);
80        }
81    };
82
83    if let Some(Block::ChannelData { variables, .. }) = &entry.block {
84        for (name, value) in variables {
85            if wanted(name) {
86                harvest(value);
87            }
88        }
89    }
90
91    match &entry.message_kind {
92        MessageKind::Variable { name, value } => {
93            if wanted(name) {
94                harvest(value);
95            }
96        }
97        MessageKind::Execute {
98            application,
99            arguments,
100            ..
101        } => match application.as_str() {
102            "set" | "export" => {
103                if let Some((name, value)) = arguments.split_once('=') {
104                    if wanted(name) {
105                        harvest(value);
106                    }
107                }
108            }
109            // The dial string's own `origination_uuid` names the leg this call is
110            // about to create, before that leg logs anything of its own.
111            "bridge" | "att_xfer" => {
112                if let Some(uuid) = parse_bridge_args(arguments).and_then(|i| i.origination_uuid) {
113                    f(&uuid);
114                }
115            }
116            _ => {}
117        },
118        _ => {}
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use crate::attached::AttachedLines;
126    use crate::line::LineKind;
127
128    const PEER: &str = "11111111-2222-3333-4444-555555555555";
129
130    fn entry(message_kind: MessageKind, block: Option<Block>) -> LogEntry {
131        LogEntry {
132            uuid: "self".to_string(),
133            timestamp: String::new(),
134            level: None,
135            idle_pct: None,
136            source: None,
137            message: String::new(),
138            kind: LineKind::Full,
139            message_kind,
140            block,
141            attached: AttachedLines::new(),
142            line_number: 0,
143            warnings: Vec::new(),
144        }
145    }
146
147    fn var(name: &str, value: &str) -> LogEntry {
148        entry(
149            MessageKind::Variable {
150                name: name.to_string(),
151                value: value.to_string(),
152            },
153            None,
154        )
155    }
156
157    fn collect(entry: &LogEntry) -> Vec<String> {
158        let mut out = Vec::new();
159        for_each_peer_uuid(entry, |u| out.push(u.to_string()));
160        out
161    }
162
163    #[test]
164    fn harvests_from_allowlisted_var() {
165        assert_eq!(collect(&var("bridge_uuid", PEER)), vec![PEER]);
166    }
167
168    #[test]
169    fn harvests_uppercase_hex() {
170        let upper = "AAAABBBB-2222-3333-4444-5555CCCCDDDD";
171        assert_eq!(collect(&var("bridge_uuid", upper)), vec![upper]);
172    }
173
174    #[test]
175    fn harvests_loopback_peer_var() {
176        assert_eq!(collect(&var("other_loopback_leg_uuid", PEER)), vec![PEER]);
177    }
178
179    #[test]
180    fn ignores_non_peer_loopback_var() {
181        assert!(collect(&var("loopback_initial_codec", PEER)).is_empty());
182    }
183
184    #[test]
185    fn ignores_shared_context_var() {
186        assert!(collect(&var("domain_uuid", PEER)).is_empty());
187    }
188
189    #[test]
190    fn strips_variable_prefix_in_channel_data() {
191        let block = Block::ChannelData {
192            fields: Vec::new(),
193            variables: vec![("variable_signal_bond".to_string(), PEER.to_string())],
194        };
195        assert_eq!(
196            collect(&entry(MessageKind::General, Some(block))),
197            vec![PEER]
198        );
199    }
200
201    #[test]
202    fn harvests_from_set_and_bridge() {
203        let set = entry(
204            MessageKind::Execute {
205                depth: 0,
206                channel: "sofia/internal/1001".to_string(),
207                application: "set".to_string(),
208                arguments: format!("last_bridge_to={PEER}"),
209            },
210            None,
211        );
212        assert_eq!(collect(&set), vec![PEER]);
213
214        let bridge = |args: String| {
215            entry(
216                MessageKind::Execute {
217                    depth: 0,
218                    channel: "sofia/internal/1001".to_string(),
219                    application: "bridge".to_string(),
220                    arguments: args,
221                },
222                None,
223            )
224        };
225        // Per-endpoint `[]` scope and all-endpoint `{}` scope both name the leg.
226        assert_eq!(
227            collect(&bridge(format!(
228                "[origination_uuid={PEER}]sofia/gateway/gw/5551234"
229            ))),
230            vec![PEER]
231        );
232        assert_eq!(
233            collect(&bridge(format!(
234                "{{origination_uuid={PEER}}}sofia/gateway/gw/5551234"
235            ))),
236            vec![PEER]
237        );
238    }
239
240    #[test]
241    fn att_xfer_names_its_leg_the_same_way() {
242        let e = entry(
243            MessageKind::Execute {
244                depth: 0,
245                channel: "sofia/internal/1001".to_string(),
246                application: "att_xfer".to_string(),
247                arguments: format!("[origination_uuid={PEER}]sofia/internal/1002"),
248            },
249            None,
250        );
251        assert_eq!(collect(&e), vec![PEER]);
252    }
253
254    #[test]
255    fn extra_var_extends_the_allowlist() {
256        let e = var("my_deployment_peer_id", PEER);
257        assert!(collect(&e).is_empty());
258
259        let mut out = Vec::new();
260        for_each_peer_uuid_with(
261            &e,
262            |n| n == "my_deployment_peer_id",
263            |u| out.push(u.to_string()),
264        );
265        assert_eq!(out, vec![PEER]);
266    }
267
268    #[test]
269    fn harvests_every_uuid_in_a_multi_valued_var() {
270        let second = "aaaabbbb-cccc-dddd-eeee-ffff00001111";
271        let e = var("originated_legs", &format!("{PEER},{second}"));
272        assert_eq!(collect(&e), vec![PEER, second]);
273    }
274}