Skip to main content

freeswitch_types/commands/
execute_on.rs

1//! The value of an `execute_on_*` channel variable: an application and its
2//! argument.
3
4use std::fmt;
5
6use crate::switch_passes::execute_on::{expansion_rewrites, opens_scope_variables, render, split};
7
8use super::originate::OriginateError;
9
10/// The part of a value a fault names.
11const APPLICATION: &str = "application name";
12const ARGUMENT: &str = "argument";
13
14/// What the switch does with an `execute_on_*` application name or argument that keeps it from
15/// reaching the application as written.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17#[non_exhaustive]
18pub enum ExecuteOnFault {
19    /// Empty, which names no application.
20    Empty,
21    /// Carries a space, a lone colon or a NUL, where the switch cuts the value.
22    Cut,
23    /// Carries a line break, which no command line holds.
24    LineBreak,
25    /// Carries an escape or a reference the expansion reads before the application does.
26    Expanded,
27    /// Opens a block of scope variables, which the application never receives.
28    OpensScopeVariables,
29}
30
31impl fmt::Display for ExecuteOnFault {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        match self {
34            Self::Empty => f.write_str("is empty, which names no application"),
35            Self::Cut => f.write_str(
36                "carries a space, a lone colon or a NUL, where the switch cuts the value",
37            ),
38            Self::LineBreak => f.write_str("carries a line break, which no command line holds"),
39            Self::Expanded => f.write_str(
40                "carries an escape or a reference the expansion reads before the \
41                             application does",
42            ),
43            Self::OpensScopeVariables => f.write_str(
44                "opens a block of scope variables, which the application never receives",
45            ),
46        }
47    }
48}
49
50/// An application for an `execute_on_*` hook such as
51/// [`ChannelVariable::ExecuteOnOriginate`](crate::ChannelVariable::ExecuteOnOriginate).
52///
53/// The switch splits the value at the first space or lone colon, expands what follows and runs the
54/// application on the channel; a name opening `perl` in any case, or one carrying `::`, is queued
55/// on the session instead of run where the hook fires. Only a name and argument the application
56/// receives as written are representable. Set as `execute_on_originate` in an originate's block,
57/// the hook runs on the new channel before that channel's session thread starts, so a variable it
58/// sets is on the channel before a SIP leg builds its INVITE. That is how a large or free-text
59/// value reaches a leg without crossing the dial string's tokenizer: the block carries paths, and
60/// the application reads the value from where they point. Not on a loopback leg, which the hook
61/// wedges in `CS_INIT`. `docs/dial-string-format.md` has the measurements and the traps.
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct ExecuteOn {
64    app: String,
65    arg: Option<String>,
66}
67
68impl ExecuteOn {
69    /// Refuses a name or argument the switch cuts, expands or reads as a block of scope variables,
70    /// and a line break the wire cannot carry.
71    pub fn new(
72        app: impl Into<String>,
73        arg: Option<impl Into<String>>,
74    ) -> Result<Self, OriginateError> {
75        let app = app.into();
76        let arg = arg.map(Into::into);
77        if app.is_empty() {
78            return Err(undeliverable(APPLICATION, ExecuteOnFault::Empty));
79        }
80        if app.contains(['\n', '\r']) {
81            return Err(undeliverable(APPLICATION, ExecuteOnFault::LineBreak));
82        }
83        if let Some(arg) = &arg {
84            if arg.contains(['\n', '\r']) {
85                return Err(undeliverable(ARGUMENT, ExecuteOnFault::LineBreak));
86            }
87            if opens_scope_variables(arg) {
88                return Err(undeliverable(ARGUMENT, ExecuteOnFault::OpensScopeVariables));
89            }
90            if expansion_rewrites(arg) {
91                return Err(undeliverable(ARGUMENT, ExecuteOnFault::Expanded));
92            }
93        }
94        let value = render(&app, arg.as_deref());
95        let hook = split(&value);
96        if hook.app != app {
97            return Err(undeliverable(APPLICATION, ExecuteOnFault::Cut));
98        }
99        if hook.arg != arg.as_deref() {
100            return Err(undeliverable(ARGUMENT, ExecuteOnFault::Cut));
101        }
102        Ok(Self { app, arg })
103    }
104
105    /// `lua <script> <args…>`. mod_lua splits its argument on spaces, so a
106    /// script path or argument carrying one is refused.
107    pub fn lua(
108        script: impl AsRef<str>,
109        args: impl IntoIterator<Item = impl AsRef<str>>,
110    ) -> Result<Self, OriginateError> {
111        let mut words = vec![script
112            .as_ref()
113            .to_string()];
114        words.extend(
115            args.into_iter()
116                .map(|a| {
117                    a.as_ref()
118                        .to_string()
119                }),
120        );
121        if words
122            .iter()
123            .any(|word| word.is_empty() || word.contains(' '))
124        {
125            return Err(OriginateError::ParseError(
126                "a lua argument is empty or carries a space, on which mod_lua splits its \
127                 argument list"
128                    .to_owned(),
129            ));
130        }
131        Self::new("lua", Some(words.join(" ")))
132    }
133
134    /// The application name.
135    pub fn app(&self) -> &str {
136        &self.app
137    }
138
139    /// The argument, as the application receives it.
140    pub fn arg(&self) -> Option<&str> {
141        self.arg
142            .as_deref()
143    }
144}
145
146fn undeliverable(part: &'static str, fault: ExecuteOnFault) -> OriginateError {
147    OriginateError::UndeliverableExecuteOn { part, fault }
148}
149
150impl fmt::Display for ExecuteOn {
151    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152        f.write_str(&render(
153            &self.app,
154            self.arg
155                .as_deref(),
156        ))
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn renders_app_and_argument() {
166        let hook = ExecuteOn::new("set", Some("probe=hooked")).unwrap();
167        assert_eq!(hook.to_string(), "set probe=hooked");
168        assert_eq!(hook.app(), "set");
169        assert_eq!(hook.arg(), Some("probe=hooked"));
170
171        let bare = ExecuteOn::new("answer", None::<String>).unwrap();
172        assert_eq!(bare.to_string(), "answer");
173        assert_eq!(bare.arg(), None);
174    }
175
176    #[test]
177    fn lua_joins_script_and_arguments() {
178        let hook = ExecuteOn::lua("/run/app/load.lua", ["/run/app/doc.xml", "pidf"]).unwrap();
179        assert_eq!(
180            hook.to_string(),
181            "lua /run/app/load.lua /run/app/doc.xml pidf"
182        );
183    }
184
185    /// The switch splits the name from the argument at the first space or
186    /// single colon, so a name carrying either names a different application.
187    #[test]
188    fn refuses_a_name_the_switch_would_split() {
189        assert!(ExecuteOn::new("lua:x", None::<String>).is_err());
190        assert!(ExecuteOn::new("my app", None::<String>).is_err());
191        assert!(ExecuteOn::new("", None::<String>).is_err());
192    }
193
194    #[test]
195    fn refuses_an_argument_the_switch_would_expand_or_the_wire_cannot_carry() {
196        assert!(ExecuteOn::new("set", Some("k=${v}")).is_err());
197        assert!(ExecuteOn::new("set", Some("k=a\nb")).is_err());
198    }
199
200    /// `switch_core_session_exec` expands the argument, and that pass drops the backslash of `\\`
201    /// and `\$` and the whole of `\'`. It runs only on a value carrying `\\`, `\n`, `\s`, `\t`,
202    /// `\'` or a reference, so a `\$` alone rides through and one beside such an escape does not.
203    #[test]
204    fn refuses_an_argument_the_expansion_unescapes() {
205        assert!(ExecuteOn::new("set", Some(r"k=a\\b")).is_err());
206        assert!(ExecuteOn::new("set", Some(r"k=it\'s")).is_err());
207        assert!(ExecuteOn::new("set", Some(r"k=\$10\\")).is_err());
208        assert!(ExecuteOn::new("set", Some(r"k=\$10")).is_ok());
209    }
210
211    /// An expanded argument opening `%[` or `%<delim>[` is read as a block of scope variables and
212    /// never reaches the application; a bare `%` leaves the switch reading past the terminator.
213    #[test]
214    fn refuses_an_argument_that_opens_scope_variables() {
215        assert!(ExecuteOn::new("set", Some("%[k=v]rest")).is_err());
216        assert!(ExecuteOn::new("set", Some("%|[k=v]rest")).is_err());
217        assert!(ExecuteOn::new("set", Some("%")).is_err());
218    }
219
220    /// The name rides the same wire as the argument, and a NUL ends what the switch reads of
221    /// either.
222    #[test]
223    fn refuses_a_name_or_argument_the_wire_cuts() {
224        assert!(ExecuteOn::new("se\nt", None::<String>).is_err());
225        assert!(ExecuteOn::new("se\0t", None::<String>).is_err());
226        assert!(ExecuteOn::new("set", Some("k=a\0b")).is_err());
227    }
228
229    #[test]
230    fn lua_refuses_a_path_with_a_space() {
231        assert!(ExecuteOn::lua("/run/my app/load.lua", ["/run/doc.xml"]).is_err());
232        assert!(ExecuteOn::lua("/run/load.lua", ["/run/my doc.xml"]).is_err());
233        assert!(ExecuteOn::lua("/run/load.lua", [""]).is_err());
234    }
235
236    /// A name the switch queues on the session is still the name it runs.
237    #[test]
238    fn a_perl_name_is_representable() {
239        let hook = ExecuteOn::new("perl", Some("/run/app/load.pl")).unwrap();
240        assert_eq!(hook.to_string(), "perl /run/app/load.pl");
241    }
242}