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, VARIABLE_PREFIX};
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.
44fn 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 a variable assignment in `entry` carries.
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 a variable assignment in `entry` carries,
62/// treating a variable as peer-bearing when it is in [`PEER_UUID_VARS`] or
63/// `extra_var` accepts its name.
64///
65/// Walks only structured variable assignments — `CHANNEL_DATA` fields, standalone
66/// variable lines, `set`/`export`/`bridge` executions. Scanning the raw message
67/// and attached text instead would harvest shared-context variables (a FusionPBX
68/// `domain_uuid`, say) and pull in every unrelated call in the same tenant.
69///
70/// A peer another message kind names — an originate-success line's `Peer UUID:`
71/// — is not one of those assignments and is not walked;
72/// [`SessionState::other_leg_uuid`](crate::SessionState::other_leg_uuid) is
73/// where the per-session layer puts it.
74pub fn for_each_peer_uuid_with<F: FnMut(&str)>(
75    entry: &LogEntry,
76    extra_var: impl Fn(&str) -> bool,
77    mut f: F,
78) {
79    let wanted = |name: &str| {
80        let name = name.strip_prefix(VARIABLE_PREFIX).unwrap_or(name);
81        is_peer_uuid_var(name) || extra_var(name)
82    };
83    let mut harvest = |value: &str| {
84        for (_, uuid) in find_uuids(value) {
85            f(uuid);
86        }
87    };
88
89    if let Some(Block::ChannelData { variables, .. }) = &entry.block {
90        for (name, value) in variables {
91            if wanted(name.bare()) {
92                harvest(value);
93            }
94        }
95    }
96
97    match &entry.message_kind {
98        MessageKind::Variable { name, value } => {
99            if wanted(name.bare()) {
100                harvest(value);
101            }
102        }
103        MessageKind::Execute {
104            application,
105            arguments,
106            ..
107        } => match application.as_str() {
108            "set" | "export" => {
109                if let Some((name, value)) = arguments.split_once('=') {
110                    if wanted(name) {
111                        harvest(value);
112                    }
113                }
114            }
115            // The dial string's own `origination_uuid` names the leg this call is
116            // about to create, before that leg logs anything of its own.
117            "bridge" | "att_xfer" => {
118                if let Some(uuid) = parse_bridge_args(arguments).and_then(|i| i.origination_uuid) {
119                    f(&uuid);
120                }
121            }
122            _ => {}
123        },
124        _ => {}
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use crate::message::VarName;
132
133    const PEER: &str = "11111111-2222-3333-4444-555555555555";
134
135    fn entry(message_kind: MessageKind, block: Option<Block>) -> LogEntry {
136        LogEntry {
137            uuid: Some("self".to_string()),
138            message_kind,
139            block,
140            ..LogEntry::synthetic(String::new())
141        }
142    }
143
144    fn var(name: &str, value: &str) -> LogEntry {
145        entry(
146            MessageKind::Variable {
147                name: VarName::new(name),
148                value: value.to_string(),
149            },
150            None,
151        )
152    }
153
154    fn collect(entry: &LogEntry) -> Vec<String> {
155        let mut out = Vec::new();
156        for_each_peer_uuid(entry, |u| out.push(u.to_string()));
157        out
158    }
159
160    #[test]
161    fn harvests_from_allowlisted_var() {
162        assert_eq!(collect(&var("bridge_uuid", PEER)), vec![PEER]);
163    }
164
165    #[test]
166    fn harvests_uppercase_hex() {
167        let upper = "AAAABBBB-2222-3333-4444-5555CCCCDDDD";
168        assert_eq!(collect(&var("bridge_uuid", upper)), vec![upper]);
169    }
170
171    #[test]
172    fn harvests_loopback_peer_var() {
173        assert_eq!(collect(&var("other_loopback_leg_uuid", PEER)), vec![PEER]);
174    }
175
176    #[test]
177    fn ignores_non_peer_loopback_var() {
178        assert!(collect(&var("loopback_initial_codec", PEER)).is_empty());
179    }
180
181    #[test]
182    fn ignores_shared_context_var() {
183        assert!(collect(&var("domain_uuid", PEER)).is_empty());
184    }
185
186    #[test]
187    fn harvests_from_a_channel_data_dump() {
188        let block = Block::ChannelData {
189            fields: Vec::new(),
190            variables: vec![(
191                VarName::from_prefixed("variable_signal_bond"),
192                PEER.to_string(),
193            )],
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: Some(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: Some(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: Some(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}