1use 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 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 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}