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