freeswitch_log_parser/
peer.rs1use 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
19pub 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
32pub const LOOPBACK_PEER_UUID_VARS: &[LoopbackVariable] = &[
35 LoopbackVariable::LoopbackFromUuid,
36 LoopbackVariable::OtherLoopbackFromUuid,
37 LoopbackVariable::OtherLoopbackLegUuid,
38 LoopbackVariable::OtherLegTrueId,
39 LoopbackVariable::LoopbackBowoutOtherUuid,
40];
41
42pub 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
53pub fn for_each_peer_uuid<F: FnMut(&str)>(entry: &LogEntry, f: F) {
58 for_each_peer_uuid_with(entry, |_| false, f);
59}
60
61pub 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 "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 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}