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
126    const PEER: &str = "11111111-2222-3333-4444-555555555555";
127
128    fn entry(message_kind: MessageKind, block: Option<Block>) -> LogEntry {
129        LogEntry {
130            uuid: Some("self".to_string()),
131            message_kind,
132            block,
133            ..LogEntry::synthetic(String::new())
134        }
135    }
136
137    fn var(name: &str, value: &str) -> LogEntry {
138        entry(
139            MessageKind::Variable {
140                name: name.to_string(),
141                value: value.to_string(),
142            },
143            None,
144        )
145    }
146
147    fn collect(entry: &LogEntry) -> Vec<String> {
148        let mut out = Vec::new();
149        for_each_peer_uuid(entry, |u| out.push(u.to_string()));
150        out
151    }
152
153    #[test]
154    fn harvests_from_allowlisted_var() {
155        assert_eq!(collect(&var("bridge_uuid", PEER)), vec![PEER]);
156    }
157
158    #[test]
159    fn harvests_uppercase_hex() {
160        let upper = "AAAABBBB-2222-3333-4444-5555CCCCDDDD";
161        assert_eq!(collect(&var("bridge_uuid", upper)), vec![upper]);
162    }
163
164    #[test]
165    fn harvests_loopback_peer_var() {
166        assert_eq!(collect(&var("other_loopback_leg_uuid", PEER)), vec![PEER]);
167    }
168
169    #[test]
170    fn ignores_non_peer_loopback_var() {
171        assert!(collect(&var("loopback_initial_codec", PEER)).is_empty());
172    }
173
174    #[test]
175    fn ignores_shared_context_var() {
176        assert!(collect(&var("domain_uuid", PEER)).is_empty());
177    }
178
179    #[test]
180    fn strips_variable_prefix_in_channel_data() {
181        let block = Block::ChannelData {
182            fields: Vec::new(),
183            variables: vec![("variable_signal_bond".to_string(), PEER.to_string())],
184        };
185        assert_eq!(
186            collect(&entry(MessageKind::General, Some(block))),
187            vec![PEER]
188        );
189    }
190
191    #[test]
192    fn harvests_from_set_and_bridge() {
193        let set = entry(
194            MessageKind::Execute {
195                depth: 0,
196                channel: "sofia/internal/1001".to_string(),
197                application: "set".to_string(),
198                arguments: format!("last_bridge_to={PEER}"),
199            },
200            None,
201        );
202        assert_eq!(collect(&set), vec![PEER]);
203
204        let bridge = |args: String| {
205            entry(
206                MessageKind::Execute {
207                    depth: 0,
208                    channel: "sofia/internal/1001".to_string(),
209                    application: "bridge".to_string(),
210                    arguments: args,
211                },
212                None,
213            )
214        };
215        // Per-endpoint `[]` scope and all-endpoint `{}` scope both name the leg.
216        assert_eq!(
217            collect(&bridge(format!(
218                "[origination_uuid={PEER}]sofia/gateway/gw/5551234"
219            ))),
220            vec![PEER]
221        );
222        assert_eq!(
223            collect(&bridge(format!(
224                "{{origination_uuid={PEER}}}sofia/gateway/gw/5551234"
225            ))),
226            vec![PEER]
227        );
228    }
229
230    #[test]
231    fn att_xfer_names_its_leg_the_same_way() {
232        let e = entry(
233            MessageKind::Execute {
234                depth: 0,
235                channel: "sofia/internal/1001".to_string(),
236                application: "att_xfer".to_string(),
237                arguments: format!("[origination_uuid={PEER}]sofia/internal/1002"),
238            },
239            None,
240        );
241        assert_eq!(collect(&e), vec![PEER]);
242    }
243
244    #[test]
245    fn extra_var_extends_the_allowlist() {
246        let e = var("my_deployment_peer_id", PEER);
247        assert!(collect(&e).is_empty());
248
249        let mut out = Vec::new();
250        for_each_peer_uuid_with(
251            &e,
252            |n| n == "my_deployment_peer_id",
253            |u| out.push(u.to_string()),
254        );
255        assert_eq!(out, vec![PEER]);
256    }
257
258    #[test]
259    fn harvests_every_uuid_in_a_multi_valued_var() {
260        let second = "aaaabbbb-cccc-dddd-eeee-ffff00001111";
261        let e = var("originated_legs", &format!("{PEER},{second}"));
262        assert_eq!(collect(&e), vec![PEER, second]);
263    }
264}