From 2882f05f2c7829d73254b145ae83ddd5903d2013 Mon Sep 17 00:00:00 2001 From: Nick Touran Date: Sat, 18 Jun 2016 09:57:18 -0700 Subject: [PATCH] Added template rendering to shell_command component (#2268) * Added template rendering to `shell_command` component * Security upgrades to template rendering in shell_command. * Added new unit tests for shell_command templates. Better failure when template is invalid in shell_command --- homeassistant/components/shell_command.py | 30 +++++++++++++++++++++-- tests/components/test_shell_command.py | 24 ++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shell_command.py b/homeassistant/components/shell_command.py index 88df938d38c..dec518db6ea 100644 --- a/homeassistant/components/shell_command.py +++ b/homeassistant/components/shell_command.py @@ -9,6 +9,8 @@ import subprocess import voluptuous as vol +from homeassistant.helpers import template +from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv DOMAIN = 'shell_command' @@ -30,14 +32,38 @@ def setup(hass, config): def service_handler(call): """Execute a shell command service.""" + cmd = conf[call.service] + cmd, shell = _parse_command(hass, cmd, call.data) + if cmd is None: + return try: - subprocess.call(conf[call.service], shell=True, + subprocess.call(cmd, shell=shell, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) except subprocess.SubprocessError: - _LOGGER.exception('Error running command') + _LOGGER.exception('Error running command: %s', cmd) for name in conf.keys(): hass.services.register(DOMAIN, name, service_handler, schema=SHELL_COMMAND_SCHEMA) return True + + +def _parse_command(hass, cmd, variables): + """Parse command and fill in any template arguments if necessary.""" + cmds = cmd.split() + prog = cmds[0] + args = ' '.join(cmds[1:]) + try: + rendered_args = template.render(hass, args, variables=variables) + except TemplateError as ex: + _LOGGER.exception('Error rendering command template: %s', ex) + return None, None + if rendered_args == args: + # no template used. default behavior + shell = True + else: + # template used. Must break into list and use shell=False for security + cmd = [prog] + rendered_args.split() + shell = False + return cmd, shell diff --git a/tests/components/test_shell_command.py b/tests/components/test_shell_command.py index a313a41b66a..0318ef4742a 100644 --- a/tests/components/test_shell_command.py +++ b/tests/components/test_shell_command.py @@ -51,6 +51,30 @@ class TestShellCommand(unittest.TestCase): } }) + def test_template_render_no_template(self): + """Ensure shell_commands without templates get rendered properly.""" + cmd, shell = shell_command._parse_command(self.hass, 'ls /bin', {}) + self.assertTrue(shell) + self.assertEqual(cmd, 'ls /bin') + + def test_template_render(self): + """Ensure shell_commands with templates get rendered properly.""" + self.hass.states.set('sensor.test_state', 'Works') + cmd, shell = shell_command._parse_command( + self.hass, + 'ls /bin {{ states.sensor.test_state.state }}', {} + ) + self.assertFalse(shell, False) + self.assertEqual(cmd[-1], 'Works') + + def test_invalid_template_fails(self): + """Test that shell_commands with invalid templates fail.""" + cmd, _shell = shell_command._parse_command( + self.hass, + 'ls /bin {{ states. .test_state.state }}', {} + ) + self.assertEqual(cmd, None) + @patch('homeassistant.components.shell_command.subprocess.call', side_effect=SubprocessError) @patch('homeassistant.components.shell_command._LOGGER.error')