lshell-0.9.17/0000755000175000017500000000000012563352273013331 5ustar ghantoosghantooslshell-0.9.17/README.md0000644000175000017500000001064512563352273014616 0ustar ghantoosghantooslshell - limited shell ====================== All this information (and more) is in the man file. Installation: ---------------- You have 3 options: * Use the setup.py present in the source tar.gz. It uses python distutils to install everything in the right place: 1. Install from source # extract source tar xvfz lshell-{version}.tar.gz # on Linux: python setup.py install --no-compile --install-scripts=/usr/bin/ # on *BSD: python setup.py install --no-compile --install-data=/usr/{pkg,local}/ 2. Install the rpm yum install lshell # or rpm -Uvh lshell-x.x-x.noarch.rpm 3. Install the .deb apt-get install lshell # or dpkg -i lshell-x.x-x.deb Configuration: ------------------------ lshell.conf presents a template configuration file. See etc/lshell.conf or man file for more information. A [default] profile is available for all users using lshell. Nevertheless, you can create a [username] section or a [grp:groupname] section to customize users' preferences. Order of priority when loading preferences is the following: 1. User configuration 2. Group configuration 3. Default configuration The primary goal of lshell, was to be able to create shell accounts with ssh access and restrict their environment to a couple a needed commands. For example User 'foo' and user 'bar' both belong to the 'users' UNIX group: - User 'foo': - must be able to access /usr and /var but not /usr/local - user all command in his PATH but 'su' - has a warning counter set to 5 - has his home path set to '/home/users' - User 'bar': - must be able to access /etc and /usr but not /usr/local - is allowed default commands plus 'ping' minus 'ls' - strictness is set to 1 (meaning he is not allowed to type an unknown command) In this case, my configuration file will look something like this: # CONFIGURATION START [global] logpath : /var/log/lshell/ loglevel : 2 [default] allowed : ['ls','pwd'] forbidden : [';', '&', '|'] warning_counter : 2 timer : 0 path : ['/etc', '/usr'] env_path : ':/sbin:/usr/foo' scp : 1 # or 0 sftp : 1 # or 0 overssh : ['rsync','ls'] aliases : {'ls':'ls --color=auto','ll':'ls -l'} [grp:users] warning_counter : 5 overssh : - ['ls'] [foo] allowed : 'all' - ['su'] path : ['/var', '/usr'] - ['/usr/local'] home_path : '/home/users' [bar] allowed : + ['ping'] - ['ls'] path : - ['/usr/local'] strict : 1 scpforce : '/home/bar/uploads/' # CONFIGURATION END Usage: -------------- To launch lshell, just execute lshell specifying the location of your configuration file: lshell --config /path/to/configuration/file By default lshell will try to launch using /${CONFPATH}/lshell.conf unless specified otherwise (using --config), where ${CONFPATH} is : - "/etc/" for Linux - "/usr/{pkg,local}/etc/" for *BSD In order to log a user, you will have to add him to the lshell group: usermod -aG lshell username Use case 1: /etc/passwd ---------------------------------------- In order to configure a user account to use lshell by default, you must: - On Linux: chsh -s /usr/bin/lshell user_name - On *BSD: chsh -s /usr/{pkg,local}/bin/lshell user_name After this, whichever method is used by the user to log into his account, he will end up using the limited shell you configured for him! Use case 2: OpenSSH & authorized_keys ----------------------------------------------------------------- In order to launch lshell limited to the 'ssh' command, I used ssh's authorized_keys: # vi /home/foo/.ssh/authorized_keys # and add : command="/usr/bin/lshell --config /path/to/lshell.conf",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty just before the public key part. This will have the effect of executing lshell upon user's SSH connection. Contact ---------------- If you want to contribute to this project, please do not hesitate. Open an issue and, if possible, send a patch! I would be glad to take a look at it! You can use the interface on github: ghantoos/lshell/issues Cheers, Ignace Mouzannar lshell-0.9.17/bin/0000755000175000017500000000000012563352273014101 5ustar ghantoosghantooslshell-0.9.17/bin/lshell0000755000175000017500000000323212563352273015312 0ustar ghantoosghantoos#!/usr/bin/env python # # $Id: lshell,v 1.5 2009-07-28 14:31:26 ghantoos Exp $ # # Copyright (C) 2008-2009 Ignace Mouzannar (ghantoos) # # This file is part of lshell # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ calls lshell function """ import os import sys from lshell.checkconfig import CheckConfig from lshell.shellcmd import ShellCmd, LshellTimeOut def main(): """ main function """ # set SHELL and get LSHELL_ARGS env variables os.environ['SHELL'] = os.path.realpath(sys.argv[0]) if 'LSHELL_ARGS' in os.environ: args = sys.argv[1:] + eval(os.environ['LSHELL_ARGS']) else: args = sys.argv[1:] userconf = CheckConfig(args).returnconf() try: cli = ShellCmd(userconf, args) cli.cmdloop() except (KeyboardInterrupt, EOFError): sys.stdout.write('\nExited on user request\n') sys.exit(0) except LshellTimeOut: userconf['logpath'].error('Timer expired') sys.stdout.write('\nTime is up.\n') if __name__ == '__main__': main() lshell-0.9.17/MANIFEST.in0000644000175000017500000000021712563352273015067 0ustar ghantoosghantoosinclude COPYING include README include CHANGES include etc/lshell.conf include etc/logrotate.d/lshell include man/lshell.1 include MANIFEST.in lshell-0.9.17/rpm/0000755000175000017500000000000012563352273014127 5ustar ghantoosghantooslshell-0.9.17/rpm/lshell.spec0000644000175000017500000000662512563352273016277 0ustar ghantoosghantoos%define name lshell %define version 0.9.16 %define release 1 %define python_sitelib %(python -c "from distutils.sysconfig import get_python_lib; print get_python_lib()") Summary: Limited Shell Name: %{name} Version: %{version} Release: %{release} Source0: %{name}-%{version}.tar.gz License: GPL Group: System Environment/Shells BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot Prefix: %{_prefix} BuildRequires: python >= 2.4 Requires: python >= 2.4 BuildArch: noarch Vendor: Ignace Mouzannar (ghantoos) Url: http://lshell.ghantoos.org %description lshell is a shell coded in Python that lets you restrict a user's environment to limited sets of commands, choose to enable/disable any command over SSH (e.g. SCP, SFTP, rsync, etc.), log user's commands, implement timing restrictions, and more. %prep %setup -q %build %{__python} setup.py build %install %{__python} setup.py install --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES --skip-build %clean rm -rf $RPM_BUILD_ROOT %post #!/bin/sh # # $Id: lshell.spec,v 1.14 2010-10-17 15:47:21 ghantoos Exp $ # # RPM build postinstall script # case of installation if [ "$1" = "1" ] ; then if ! getent group lshell 2>&1 > /dev/null; then # thank you Michael Mansour for your suggestion to use groupadd # instead of addgroup groupadd -r lshell fi mkdir -p /var/log/lshell/ chown root:lshell /var/log/lshell/ chmod -R 770 /var/log/lshell/ ##### # This part is taken from debian add-shell(8) script ##### lshell=/usr/bin/lshell file=/etc/shells tmpfile=${file}.tmp set -o noclobber trap "rm -f ${tmpfile}" EXIT if ! cat ${file} > ${tmpfile} then cat 1>&2 <> ${tmpfile} fi chmod --reference=${file} ${tmpfile} chown --reference=${file} ${tmpfile} mv ${tmpfile} ${file} trap "" EXIT exit 0 # case of upgrade else mkdir -p /var/log/lshell/ chown root:lshell /var/log/lshell/ chmod -R 774 /var/log/lshell/ exit 0 fi %postun #!/bin/sh # # $Id: lshell.spec,v 1.14 2010-10-17 15:47:21 ghantoos Exp $ # # RPM build postuninstall script if [ -x /usr/sbin/remove-shell ] && [ -f /etc/shells ]; then ##### # This part is taken from debian remove-shell(8) script ##### lshell=/usr/bin/lshell file=/etc/shells # I want this to be GUARANTEED to be on the same filesystem as $file tmpfile=${file}.tmp otmpfile=${file}.tmp2 set -o noclobber trap "rm -f ${tmpfile} ${otmpfile}" EXIT if ! cat ${file} > ${tmpfile} then cat 1>&2 < ${otmpfile} || true mv ${otmpfile} ${tmpfile} chmod --reference=${file} ${tmpfile} chown --reference=${file} ${tmpfile} mv ${tmpfile} ${file} trap "" EXIT exit 0 fi %files %defattr(644,root,root,755) %doc /usr/share/doc/lshell/* %config(noreplace) %verify(not md5 mtime size) %{_sysconfdir}/* %attr(755,root,root) %{_bindir}/lshell %{python_sitelib}/* %{_mandir}/man1/lshell.1* lshell-0.9.17/rpm/postinstall0000644000175000017500000000255712563352273016437 0ustar ghantoosghantoos#!/bin/sh # # $Id: postinstall,v 1.7 2009-07-28 18:33:02 ghantoos Exp $ # # RPM build postinstall script # Check if rpm is being _installed_ (as opposed to _upgraded_) # if installation process, then proceed # source: http://www.ibm.com/developerworks/library/l-rpm3.html # case of installation if [ "$1" = "1" ] ; then if ! getent group lshell 2>&1 > /dev/null; then addgroup --system lshell fi chown root:lshell /var/log/lshell/ chmod 770 /var/log/lshell/ ##### # This part is taken from debian add-shell(8) script ##### lshell=/usr/bin/lshell file=/etc/shells tmpfile=${file}.tmp set -o noclobber trap "rm -f ${tmpfile}" EXIT if ! cat ${file} > ${tmpfile} then cat 1>&2 <> ${tmpfile} fi chmod --reference=${file} ${tmpfile} chown --reference=${file} ${tmpfile} mv ${tmpfile} ${file} trap "" EXIT exit 0 # case of upgrade else chown root:lshell /var/log/lshell/ chmod -R 770 /var/log/lshell/ mv /etc/lshell.conf /etc/lshell.conf-rpm mv /etc/lshell.conf-preinstall /etc/lshell.conf exit 0 fi lshell-0.9.17/rpm/preinstall0000644000175000017500000000034312563352273016227 0ustar ghantoosghantoos#!/bin/sh # # $Id: preinstall,v 1.2 2009-02-15 18:46:58 ghantoos Exp $ # # RPM build preinstall script # Save the configuration if [ -f "/etc/lshell.conf" ]; then cp /etc/lshell.conf /etc/lshell.conf-preinstall fi exit 0 lshell-0.9.17/rpm/postuninstall0000644000175000017500000000232412563352273016772 0ustar ghantoosghantoos#!/bin/sh # # $Id: postuninstall,v 1.6 2009-03-09 13:59:40 ghantoos Exp $ # # RPM build postuninstall script # Check if rpm is being _removed_ (as opposed to _upgraded_) # if deletion process, then proceed, else, exit 0 # source: http://www.ibm.com/developerworks/library/l-rpm3.html if [ "$1" != "0" ] ; then if [ -f "/etc/lshell.conf" ]; then cp /etc/lshell.conf /etc/lshell.conf-preinstall fi exit 0 fi #groupdel lshellg rm -f /etc/lshell.conf-rpm ##### # This part is taken from debian remove-shell(8) script ##### lshell=/usr/bin/lshell file=/etc/shells # I want this to be GUARANTEED to be on the same filesystem as $file tmpfile=${file}.tmp otmpfile=${file}.tmp2 set -o noclobber trap "rm -f ${tmpfile} ${otmpfile}" EXIT if ! cat ${file} > ${tmpfile} then cat 1>&2 < ${otmpfile} || true mv ${otmpfile} ${tmpfile} chmod --reference=${file} ${tmpfile} chown --reference=${file} ${tmpfile} mv ${tmpfile} ${file} trap "" EXIT exit 0 lshell-0.9.17/test/0000755000175000017500000000000012563352273014310 5ustar ghantoosghantooslshell-0.9.17/test/test_unit.py0000644000175000017500000001131612563352273016702 0ustar ghantoosghantoosimport unittest import lshell from lshell.shellcmd import ShellCmd, LshellTimeOut from lshell.checkconfig import CheckConfig from lshell.utils import get_aliases import os TOPDIR="%s/../" % os.path.dirname(os.path.realpath(__file__)) class TestFunctions(unittest.TestCase): args = ['--config=%s/etc/lshell.conf' % TOPDIR, "--quiet=1"] userconf = CheckConfig(args).returnconf() shell = ShellCmd(userconf, args) def test_checksecure_doublequote(self): """ quoted text should not be forbidden """ INPUT = 'ls -E "1|2" tmp/test' return self.assertEqual(self.shell.check_secure(INPUT), 0) def test_checksecure_simplequote(self): """ quoted text should not be forbidden """ INPUT = "ls -E '1|2' tmp/test" return self.assertEqual(self.shell.check_secure(INPUT), 0) def test_checksecure_doublepipe(self): """ double pipes should be allowed, even if pipe is forbidden """ args = self.args + ["--forbidden=['|']"] userconf = CheckConfig(args).returnconf() shell = ShellCmd(userconf, args) INPUT = "ls || ls" return self.assertEqual(shell.check_secure(INPUT), 0) def test_checksecure_forbiddenpipe(self): """ forbid pipe, should return 1 """ args = self.args + ["--forbidden=['|']"] userconf = CheckConfig(args).returnconf() shell = ShellCmd(userconf, args) INPUT = "ls | ls" return self.assertEqual(shell.check_secure(INPUT), 1) def test_checksecure_forbiddenchar(self): """ forbid character, should return 1 """ args = self.args + ["--forbidden=['l']"] userconf = CheckConfig(args).returnconf() shell = ShellCmd(userconf, args) INPUT = "ls" return self.assertEqual(shell.check_secure(INPUT), 1) def test_checksecure_sudo_command(self): """ quoted text should not be forbidden """ INPUT = "sudo ls" return self.assertEqual(self.shell.check_secure(INPUT), 1) def test_checksecure_notallowed_command(self): """ forbidden command, should return 1 """ args = self.args + ["--allowed=['ls']"] userconf = CheckConfig(args).returnconf() shell = ShellCmd(userconf, args) INPUT = "ll" return self.assertEqual(shell.check_secure(INPUT), 1) def test_checkpath_notallowed_path(self): """ forbidden command, should return 1 """ args = self.args + ["--path=['/home', '/var']"] userconf = CheckConfig(args).returnconf() shell = ShellCmd(userconf, args) INPUT = "cd /tmp" return self.assertEqual(shell.check_path(INPUT), 1) def test_checkpath_notallowed_path_completion(self): """ forbidden command, should return 1 """ args = self.args + ["--path=['/home', '/var']"] userconf = CheckConfig(args).returnconf() shell = ShellCmd(userconf, args) INPUT = "cd /tmp/" return self.assertEqual(shell.check_path(INPUT, completion=1), 1) def test_checkpath_dollarparenthesis(self): """ when $() is allowed, return 0 if path allowed """ args = self.args + ["--forbidden=[';', '&', '|','`','>','<', '${']"] userconf = CheckConfig(args).returnconf() shell = ShellCmd(userconf, args) INPUT = "echo $(echo aze)" return self.assertEqual(shell.check_path(INPUT), 0) def test_checkconfig_configoverwrite(self): """ forbid ';', then check_secure should return 1 """ args = ['--config=%s/etc/lshell.conf' % TOPDIR, '--strict=123'] userconf = CheckConfig(args).returnconf() return self.assertEqual(userconf['strict'], 123) def test_overssh(self): """ test command over ssh """ args = self.args + ["--overssh=['exit']", '-c exit'] os.environ['SSH_CLIENT'] = '8.8.8.8 36000 22' if os.environ.has_key('SSH_TTY'): os.environ.pop('SSH_TTY') with self.assertRaises(SystemExit) as cm: userconf = CheckConfig(args).returnconf() return self.assertEqual(cm.exception.code, 0) def test_multiple_aliases_with_separator(self): """ multiple aliases using &&, || and ; separators """ # enable &, | and ; characters aliases={'foo':'foo -l', 'bar':'open'} INPUT = "foo; fooo ;bar&&foo && foo | bar||bar || foo" return self.assertEqual(get_aliases(INPUT, aliases), ' foo -l; fooo ; open&& foo -l && foo -l | open|| open || foo -l') def test_sudo_all_commands_expansion(self): """ sudo_commands set to 'all' should be equal to allowed variable """ args = self.args + ["--sudo_commands=all"] userconf = CheckConfig(args).returnconf() # exclude internal and sudo(8) commands exclude = ['exit','lpath','lsudo','history','clear','export','sudo'] allowed = [x for x in userconf['allowed'] if x not in exclude] # sort lists to compare userconf['sudo_commands'].sort() allowed.sort() return self.assertEqual(allowed, userconf['sudo_commands']) if __name__ == "__main__": unittest.main() lshell-0.9.17/test/test_functional.py0000644000175000017500000002774212563352273020077 0ustar ghantoosghantoosimport unittest import pexpect import os import subprocess from getpass import getuser TOPDIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) class TestFunctions(unittest.TestCase): user = getuser() def setUp(self): """ spawn lshell with pexpext and return the child """ self.child = pexpect.spawn('%s/bin/lshell ' '--config %s/etc/lshell.conf --strict 1' % (TOPDIR, TOPDIR)) self.child.expect('%s:~\$' % self.user) def tearDown(self): self.child.close() def test_01(self): """ 01 - test lshell welcome message """ expected = "You are in a limited shell.\r\nType '?' or 'help' to get" \ " the list of allowed commands\r\n" result = self.child.before self.assertEqual(expected, result) def test_02(self): """ 02 - get the output of ls """ p = subprocess.Popen("ls ~", shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE) cout = p.stdout expected = cout.read(-1) self.child.sendline('ls') self.child.expect('%s:~\$' % self.user) output = self.child.before.split('ls\r', 1)[1] self.assertEqual(len(expected.strip().split()), len(output.strip().split())) def test_03(self): """ 03 - echo number """ expected = "32" self.child.sendline('echo 32') self.child.expect("%s:~\$" % self.user) result = self.child.before.split()[2] self.assertEqual(expected, result) def test_04(self): """ 04 - echo anything """ expected = "bla blabla 32 blibli! plop." self.child.sendline('echo "%s"' % expected) self.child.expect('%s:~\$' % self.user) result = self.child.before.split('\n', 1)[1].strip() self.assertEqual(expected, result) def test_05(self): """ 05 - echo $(uptime) """ expected = "*** forbidden syntax -> \"echo $(uptime)\"\r\n*** You " \ "have 1 warning(s) left, before getting kicked out.\r\nThis " \ "incident has been reported.\r\n" self.child.sendline('echo $(uptime)') self.child.expect('%s:~\$' % self.user) result = self.child.before.split('\n', 1)[1] self.assertEqual(expected, result) def test_06_0(self): """ 06.0 - change directory """ expected = "" home = os.path.expanduser('~') dirpath = None for path in os.listdir(home): dirpath = os.path.join(home, path) if os.path.isdir(dirpath): break if dirpath: self.child.sendline('cd %s' % path) self.child.expect('%s:~/%s\$' % (self.user, path)) self.child.sendline('cd ..') self.child.expect('%s:~\$' % self.user) result = self.child.before.split('\n', 1)[1] self.assertEqual(expected, result) def test_06_1(self): """ 06.1 - tilda bug """ expected = "*** forbidden path -> \"/etc/passwd\"\r\n*** You have" \ " 1 warning(s) left, before getting kicked out.\r\nThis " \ "incident has been reported.\r\n" self.child.sendline('ls ~/../../etc/passwd') self.child.expect("%s:~\$" % self.user) result = self.child.before.split('\n', 1)[1] self.assertEqual(expected, result) def test_07(self): """ 07 - quotes in cd "/" """ expected = "*** forbidden path -> \"/\"\r\n*** You have" \ " 1 warning(s) left, before getting kicked out.\r\nThis " \ "incident has been reported.\r\n" self.child.sendline('ls -ld "/"') self.child.expect('%s:~\$' % self.user) result = self.child.before.split('\n', 1)[1] self.assertEqual(expected, result) def test_08(self): """ 08 - ls ~root """ expected = "*** forbidden path -> \"/root/\"\r\n*** You have" \ " 1 warning(s) left, before getting kicked out.\r\nThis " \ "incident has been reported.\r\n" self.child.sendline('ls ~root') self.child.expect('%s:~\$' % self.user) result = self.child.before.split('\n', 1)[1] self.assertEqual(expected, result) def test_09(self): """ 09 - cd ~root """ expected = "*** forbidden path -> \"/root/\"\r\n*** You have" \ " 1 warning(s) left, before getting kicked out.\r\nThis " \ "incident has been reported.\r\n" self.child.sendline('cd ~root') self.child.expect('%s:~\$' % self.user) result = self.child.before.split('\n', 1)[1] self.assertEqual(expected, result) def test_10(self): """ 10 - empty variable 'ls "$a"/etc/passwd' """ expected = "*** forbidden path -> \"/etc/passwd\"\r\n*** You have" \ " 1 warning(s) left, before getting kicked out.\r\nThis " \ "incident has been reported.\r\n" self.child.sendline('ls "$a"/etc/passwd') self.child.expect('%s:~\$' % self.user) result = self.child.before.split('\n', 1)[1] self.assertEqual(expected, result) def test_11(self): """ 11 - empty variable 'ls -l .*./.*./etc/passwd' """ expected = "*** forbidden path -> \"/etc/passwd\"\r\n*** You have" \ " 1 warning(s) left, before getting kicked out.\r\nThis " \ "incident has been reported.\r\n" self.child.sendline('ls -l .*./.*./etc/passwd') self.child.expect('%s:~\$' % self.user) result = self.child.before.split('\n', 1)[1] self.assertEqual(expected, result) def test_12(self): """ 12 - empty variable 'ls -l .?/.?/etc/passwd' """ expected = "*** forbidden path -> \"/etc/passwd\"\r\n*** You have" \ " 1 warning(s) left, before getting kicked out.\r\nThis " \ "incident has been reported.\r\n" self.child.sendline('ls -l .?/.?/etc/passwd') self.child.expect('%s:~\$' % self.user) result = self.child.before.split('\n', 1)[1] self.assertEqual(expected, result) def test_13(self): """ 13 - completion with ~/ """ p = subprocess.Popen("ls -F ~/", shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE) cout = p.stdout expected = cout.read(-1) self.child.sendline('cd ~/\t\t') self.child.expect('%s:~\$' % self.user) output = self.child.before.split('\n', 1)[1] self.assertEqual(len(expected.strip().split()), len(output.strip().split())) # def test_14(self): # """ 14 - command over ssh """ def test_15(self): """ 15 - tab to list commands """ expected = '\x07\r\ncd echo help ll ls \r\n'\ 'clear exit history lpath lsudo' self.child.sendline('\t\t') self.child.expect('%s:~\$' % self.user) result = self.child.before.strip() self.assertEqual(expected, result) # def test_exit(self): # expected = '' # self.child.sendline('exit') # self.child.expect('$') # result = self.child.before # self.assertEqual(expected, result) def test_16_exitcode_with_separator(self): """ 16 - test exit codes with separator """ self.child = pexpect.spawn('%s/bin/lshell ' '--config %s/etc/lshell.conf --forbidden "[]"' % (TOPDIR, TOPDIR)) self.child.expect('%s:~\$' % self.user) expected = "2" self.child.sendline('ls nRVmmn8RGypVneYIp8HxyVAvaEaD55; echo $?') self.child.expect('%s:~\$' % self.user) result = self.child.before.split('\n')[2].strip() self.assertEqual(expected, result) def test_17_exitcode_without_separator(self): """ 17 - test exit codes without separator """ self.child = pexpect.spawn('%s/bin/lshell ' '--config %s/etc/lshell.conf --forbidden "[]"' % (TOPDIR, TOPDIR)) self.child.expect('%s:~\$' % self.user) expected = "2" self.child.sendline('ls nRVmmn8RGypVneYIp8HxyVAvaEaD55') self.child.expect('%s:~\$' % self.user) self.child.sendline('echo $?') self.child.expect('%s:~\$' % self.user) result = self.child.before.split('\n')[1].strip() self.assertEqual(expected, result) def test_18_allow_slash(self): """ 18 - user should able to allow / access minus some directory (e.g. /var) """ self.child = pexpect.spawn('%s/bin/lshell ' '--config %s/etc/lshell.conf --path "[\'/\'] - [\'/var\']"' % (TOPDIR, TOPDIR)) self.child.expect('%s:~\$' % self.user) expected = "*** forbidden path: /var/" self.child.sendline('cd /') self.child.expect('%s:/\$' % self.user) self.child.sendline('cd var') self.child.expect('%s:/\$' % self.user) result = self.child.before.split('\n')[1].strip() self.assertEqual(expected, result) def test_19_expand_env_variables(self): """ 19 - test expanding of environment variables """ self.child = pexpect.spawn('%s/bin/lshell ' '--config %s/etc/lshell.conf --allowed "+ [\'export\']"' % (TOPDIR, TOPDIR)) self.child.expect('%s:~\$' % self.user) expected = "%s/test" % os.path.expanduser('~') self.child.sendline('export A=test') self.child.expect('%s:~\$' % self.user) self.child.sendline('echo $HOME/$A') self.child.expect('%s:~\$' % self.user) result = self.child.before.split('\n')[1].strip() self.assertEqual(expected, result) def test_20_expand_env_variables_cd(self): """ 20 - test expanding of environment variables when using cd """ self.child = pexpect.spawn('%s/bin/lshell ' '--config %s/etc/lshell.conf --allowed "+ [\'export\']"' % (TOPDIR, TOPDIR)) self.child.expect('%s:~\$' % self.user) import random import string random = ''.join([random.choice(string.ascii_letters + string.digits) for n in xrange(32)]) expected = "lshell: %s/random_%s: No such file or directory" \ % (os.path.expanduser('~'),random) self.child.sendline('export A=random_%s' % random) self.child.expect('%s:~\$' % self.user) self.child.sendline('cd $HOME/$A') self.child.expect('%s:~\$' % self.user) result = self.child.before.split('\n')[1].strip() self.assertEqual(expected, result) def test_21_cd_and_command(self): """ 07 - cd && command should not be interpreted by internal function """ self.child = pexpect.spawn('%s/bin/lshell ' '--config %s/etc/lshell.conf' % (TOPDIR, TOPDIR)) self.child.expect('%s:~\$' % self.user) expected = "OK" self.child.sendline('cd ~ && echo "OK"') self.child.expect('%s:~\$' % self.user) result = self.child.before.split('\n')[1].strip() self.assertEqual(expected, result) def test_22_KeyboardInterrupt(self): """ 07 - test cat(1) with KeyboardInterrupt, should not exit """ self.child = pexpect.spawn('%s/bin/lshell ' '--config %s/etc/lshell.conf --allowed "+ [\'cat\']"' % (TOPDIR, TOPDIR)) self.child.expect('%s:~\$' % self.user) expected = "^C" self.child.sendline('cat') self.child.sendcontrol('c'); self.child.expect('%s:~\$' % self.user) result = self.child.before.split('\n')[1].strip() self.assertEqual(expected, result) if __name__ == '__main__': unittest.main() lshell-0.9.17/lshell/0000755000175000017500000000000012563352273014614 5ustar ghantoosghantooslshell-0.9.17/lshell/utils.py0000644000175000017500000000501412563352273016326 0ustar ghantoosghantoos# # Limited command Shell (lshell) # # Copyright (C) 2008-2013 Ignace Mouzannar (ghantoos) # # This file is part of lshell # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import re import subprocess try: from os import urandom except: def urandom(n): try: _urandomfd = open("/dev/urandom", 'r') except Exception,e: print e raise NotImplementedError("/dev/urandom (or equivalent) not found") bytes = "" while len(bytes) < n: bytes += _urandomfd.read(n - len(bytes)) _urandomfd.close() return bytes def get_aliases(line, aliases): """ Replace all configured aliases in the line """ for item in aliases.keys(): reg1 = '(^|;|&&|\|\||\|)\s*%s([ ;&\|]+|$)(.*)' % item reg2 = '(^|;|&&|\|\||\|)\s*%s([ ;&\|]+|$)' % item # in case aliase bigin with the same command # (this is until i find a proper regex solution..) aliaskey = urandom(10) while re.findall(reg1, line): (before, after, rest) = re.findall(reg1, line)[0] linesave = line cmd = "%s %s" % (item, rest) line = re.sub(reg2, "%s %s%s" % (before, aliaskey, \ after), line, 1) # if line does not change after sub, exit loop if linesave == line: break # replace the key by the actual alias line = line.replace(aliaskey, aliases[item]) for char in [';']: # remove all remaining double char line = line.replace('%s%s' %(char, char), '%s' %char) return line def exec_cmd(cmd): """ execute a command, locally catching the signals """ try: retcode = subprocess.call("%s" % cmd, shell=True) except KeyboardInterrupt: # exit code for user terminated scripts is 130 retcode = 130 return retcode lshell-0.9.17/lshell/shellcmd.py0000644000175000017500000007501312563352273016767 0ustar ghantoosghantoos# # Limited command Shell (lshell) # # Copyright (C) 2008-2013 Ignace Mouzannar (ghantoos) # # This file is part of lshell # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import cmd import sys import os from getpass import getuser import re import signal import readline import glob from utils import get_aliases,exec_cmd class ShellCmd(cmd.Cmd, object): """ Main lshell CLI class """ def __init__(self, userconf, args, stdin=None, stdout=None, stderr=None, g_cmd=None, g_line=None): if stdin is None: self.stdin = sys.stdin else: self.stdin = stdin if stdout is None: self.stdout = sys.stdout else: self.stdout = stdout if stderr is None: self.stderr = sys.stderr else: self.stderr = stderr self.args = args self.conf = userconf self.log = self.conf['logpath'] # Set timer if self.conf['timer'] > 0: self.mytimer(self.conf['timer']) self.identchars = self.identchars + '+./-' self.log.error('Logged in') cmd.Cmd.__init__(self) if 'prompt' in self.conf: self.promptbase = self.conf['prompt'] self.promptbase = self.promptbase.replace('%u', getuser()) self.promptbase = self.promptbase.replace( '%h', os.uname()[1].split('.')[0]) else: self.promptbase = getuser() self.prompt = '%s:~$ ' % self.promptbase self.intro = self.conf['intro'] # initialize oldpwd variable to home directory self.oldpwd = self.conf['home_path'] # initialize cli variables self.g_cmd = g_cmd self.g_line = g_line # initialize return code self.retcode = None def __getattr__(self, attr): """ This method actually takes care of all the called method that are not resolved (i.e not existing methods). It actually will simulate the existance of any method entered in the 'allowed' variable list. e.g. You just have to add 'uname' in list of allowed commands in the 'allowed' variable, and lshell will react as if you had added a do_uname in the ShellCmd class! """ # expand environment variables in command line self.g_cmd = os.path.expandvars(self.g_cmd) self.g_line = os.path.expandvars(self.g_line) self.g_arg = os.path.expandvars(self.g_arg) # in case the configuration file has been modified, reload it if self.conf['config_mtime'] != os.path.getmtime(self.conf['configfile']): from lshell.checkconfig import CheckConfig self.conf = CheckConfig(['--config', \ self.conf['configfile']]).returnconf() self.prompt = '%s:~$ ' % self.setprompt(self.conf) self.log = self.conf['logpath'] if self.g_cmd in ['quit', 'exit', 'EOF']: self.log.error('Exited') if self.g_cmd == 'EOF': self.stdout.write('\n') sys.exit(0) if self.check_secure(self.g_line, self.conf['strict']) == 1: return object.__getattribute__(self, attr) if self.check_path(self.g_line, strict = self.conf['strict']) == 1: return object.__getattribute__(self, attr) if self.g_cmd in self.conf['allowed']: if self.conf['timer'] > 0: self.mytimer(0) self.g_arg = re.sub('^~$|^~/', '%s/' %self.conf['home_path'], \ self.g_arg) self.g_arg = re.sub(' ~/', ' %s/' %self.conf['home_path'], \ self.g_arg) # replace previous command exit code # in case multiple commands (using separators), only replace first command # regex replaces all occureces of $?, before ;,&,| if re.search('[;&\|]', self.g_line): p = re.compile("(\s|^)(\$\?)([\s|$]?[;&|].*)") else: p = re.compile("(\s|^)(\$\?)(\s|$)") self.g_line = p.sub(r' %s \3' % self.retcode, self.g_line) if type(self.conf['aliases']) == dict: self.g_line = get_aliases(self.g_line, self.conf['aliases']) self.log.info('CMD: "%s"' %self.g_line) if self.g_cmd == 'cd': if re.search('[;&\|]', self.g_line): # ignore internal cd function in case more than one command self.retcode = exec_cmd(self.g_line) else: # builtin cd function self.retcode = self.cd() # builtin lpath function: list all allowed path elif self.g_cmd == 'lpath': self.retcode = self.lpath() # builtin lsudo function: list all allowed sudo commands elif self.g_cmd == 'lsudo': self.retcode = self.lsudo() # builtin history function: print command history elif self.g_cmd == 'history': self.retcode = self.history() # builtin export function elif self.g_cmd == 'export': self.retcode = self.export() # case 'cd' is in an alias e.g. {'toto':'cd /var/tmp'} elif self.g_line[0:2] == 'cd': self.g_cmd = self.g_line.split()[0] self.g_arg = ' '.join(self.g_line.split()[1:]) self.retcode = self.cd() else: self.retcode = exec_cmd(self.g_line) elif self.g_cmd not in ['', '?', 'help', None]: self.log.warn('INFO: unknown syntax -> "%s"' %self.g_line) self.stderr.write('*** unknown syntax: %s\n' %self.g_cmd) self.g_cmd, self.g_arg, self.g_line = ['', '', ''] if self.conf['timer'] > 0: self.mytimer(self.conf['timer']) return object.__getattribute__(self, attr) def setprompt(self, conf): """ set prompt used by the shell """ if conf.has_key('prompt'): promptbase = conf['prompt'] promptbase = promptbase.replace('%u', getuser()) promptbase = promptbase.replace('%h', os.uname()[1].split('.')[0]) else: promptbase = getuser() return promptbase def lpath(self): """ lists allowed and forbidden path """ if self.conf['path'][0]: sys.stdout.write("Allowed:\n") lpath_allowed = self.conf['path'][0].split('|') lpath_allowed.sort() for path in lpath_allowed: if path: sys.stdout.write(" %s\n" % path[:-2]) if self.conf['path'][1]: sys.stdout.write("Denied:\n") lpath_denied = self.conf['path'][1].split('|') lpath_denied.sort() for path in lpath_denied: if path: sys.stdout.write(" %s\n" % path[:-2]) return 0 def lsudo(self): """ lists allowed sudo commands """ if self.conf.has_key('sudo_commands'): sys.stdout.write("Allowed sudo commands:\n") for command in self.conf['sudo_commands']: sys.stdout.write(" - %s\n" % command) return 0 def history(self): """ print the commands history """ try: try: readline.write_history_file(self.conf['history_file']) except IOError: self.log.error('WARN: couldn\'t write history ' \ 'to file %s\n' % self.conf['history_file']) return 1 f = open(self.conf['history_file'], 'r') i = 1 for item in f.readlines(): sys.stdout.write("%d: %s" % (i, item)) i += 1 except: self.log.critical('** Unable to read the history file.') return 1 return 0 def export(self): """ export environment variables """ # if command contains at least 1 space if self.g_line.count(' '): env = self.g_line.split(" ", 1)[1] # if it conatins the equal sign, consider only the first one if env.count('='): var, value = env.split(' ')[0].split('=')[0:2] os.environ.update({var: value}) return 0 def cd(self): """ implementation of the "cd" command """ if len(self.g_arg) >= 1: # add wildcard completion support to cd if self.g_arg.find('*'): # get all files and directories matching wildcard wildall = glob.glob(self.g_arg) wilddir = [] # filter to only directories for item in wildall: if os.path.isdir(item): wilddir.append(item) # sort results wilddir.sort() # if any results are returned, pick first one if len(wilddir) >= 1: self.g_arg = wilddir[0] # go previous directory if self.g_arg == '-': self.g_arg = self.oldpwd # store current directory in oldpwd variable self.oldpwd = os.getcwd() # change directory try: os.chdir(os.path.realpath(self.g_arg)) self.updateprompt(os.getcwd()) except OSError, (ErrorNumber, ErrorMessage): sys.stdout.write("lshell: %s: %s\n" %(self.g_arg, ErrorMessage)) return ErrorNumber else: os.chdir(self.conf['home_path']) self.updateprompt(os.getcwd()) return 0 def check_secure(self, line, strict=None, ssh=None): """This method is used to check the content on the typed command. \ Its purpose is to forbid the user to user to override the lshell \ command restrictions. The forbidden characters are placed in the 'forbidden' variable. Feel free to update the list. Emptying it would be quite useless..: ) A warining counter has been added, to kick out of lshell a user if he \ is warned more than X time (X beeing the 'warning_counter' variable). """ # store original string oline = line # strip all spaces/tabs line = " ".join(line.split()) # ignore quoted text line = re.sub(r'\"(.+?)\"', '', line) line = re.sub(r'\'(.+?)\'', '', line) if re.findall('[:cntrl:].*\n', line): if not ssh: if strict: self.counter_update('syntax') else: self.log.critical('*** forbidden syntax -> %s' % oline) return 1 for item in self.conf['forbidden']: # allow '&&' and '||' even if singles are forbidden if item in ['&', '|']: if re.findall("[^\%s]\%s[^\%s]" %(item, item, item), line): return self.warn_count('syntax', oline, strict, ssh) else: if item in line: return self.warn_count('syntax', oline, strict, ssh) returncode = 0 # check if the line contains $(foo) executions, and check them executions = re.findall('\$\([^)]+[)]', line) for item in executions: returncode += self.check_path(item[2:-1].strip(), strict = strict) returncode += self.check_secure(item[2:-1].strip(), strict = strict) # check fot executions using back quotes '`' executions = re.findall('\`[^`]+[`]', line) for item in executions: returncode += self.check_secure(item[1:-1].strip(), strict = strict) # check if the line contains ${foo=bar}, and check them curly = re.findall('\$\{[^}]+[}]', line) for item in curly: # split to get variable only, and remove last character "}" if re.findall(r'=|\+|\?|\-', item): variable = re.split('=|\+|\?|\-', item, 1) else: variable = item returncode += self.check_path(variable[1][:-1], strict = strict) # if unknown commands where found, return 1 and don't execute the line if returncode > 0: return 1 # in case the $(foo) or `foo` command passed the above tests elif line.startswith('$(') or line.startswith('`'): return 0 # in case ';', '|' or '&' are not forbidden, check if in line lines = [] # corrected by Alojzij Blatnik #48 # test first character if line[0] in ["&", "|", ";"]: start = 1 else: start = 0 # split remaining command line for i in range(1, len(line)): # in case \& or \| or \; don't split it if line[i] in ["&", "|", ";"] and line[i-1] != "\\": # if there is more && or || skip it if start != i: lines.append(line[start:i]) start = i+1 # append remaining command line if start != len(line): lines.append(line[start:len(line)]) # remove trailing parenthesis line = re.sub('\)$', '', line) for separate_line in lines: separate_line = " ".join(separate_line.split()) splitcmd = separate_line.strip().split(' ') command = splitcmd[0] if len(splitcmd) > 1: cmdargs = splitcmd else: cmdargs = None # in case of a sudo command, check in sudo_commands list if allowed if command == 'sudo': if type(cmdargs) == list: # allow the -u (user) flag if cmdargs[1] == '-u' and cmdargs: sudocmd = cmdargs[3] else: sudocmd = cmdargs[1] if sudocmd not in self.conf['sudo_commands'] and cmdargs: return self.warn_count('sudo command', oline, strict, ssh) # if over SSH, replaced allowed list with the one of overssh if ssh: self.conf['allowed'] = self.conf['overssh'] # for all other commands check in allowed list if command not in self.conf['allowed'] and command: return self.warn_count('command', oline, strict, ssh, command) return 0 def warn_count(self, messagetype, line=None, strict=None, ssh=None, command=None): """ Update the warning_counter, log and display a warning to the user """ if not line: line = self.g_line if command: line = command if not ssh: if strict: self.conf['warning_counter'] -= 1 if self.conf['warning_counter'] < 0: self.log.critical('*** forbidden %s -> "%s"' \ % (messagetype ,line)) self.log.critical('*** Kicked out') sys.exit(1) else: self.log.critical('*** forbidden %s -> "%s"' \ % (messagetype ,line)) self.stderr.write('*** You have %s warning(s) left,' \ ' before getting kicked out.\n' \ %(self.conf['warning_counter'])) self.stderr.write('This incident has been reported.\n') else: if not self.conf['quiet']: self.log.critical('*** forbidden %s: %s' % (messagetype, line)) # if you are here, means that you did something wrong. Return 1. return 1 def counter_update(self, messagetype, path=None): """ Update the warning_counter, log and display a warning to the user """ if path: line = path else: line = self.g_line # if warning_counter is set to -1, just warn, don't kick if self.conf['warning_counter'] == -1: self.log.critical('*** forbidden %s -> "%s"' \ % (messagetype ,line)) else: self.conf['warning_counter'] -= 1 if self.conf['warning_counter'] < 0: self.log.critical('*** forbidden %s -> "%s"' \ % (messagetype ,line)) self.log.critical('*** Kicked out') sys.exit(1) else: self.log.critical('*** forbidden %s -> "%s"' \ % (messagetype ,line)) self.stderr.write('*** You have %s warning(s) left,' \ ' before getting kicked out.\n' \ %(self.conf['warning_counter'])) self.stderr.write('This incident has been reported.\n') def check_path(self, line, completion=None, ssh=None, strict=None): """ Check if a path is entered in the line. If so, it checks if user \ are allowed to see this path. If user is not allowed, it calls \ self.counter_update. I case of completion, it only returns 0 or 1. """ allowed_path_re = str(self.conf['path'][0]) denied_path_re = str(self.conf['path'][1][:-1]) # split line depending on the operators sep=re.compile(r'\ |;|\||&') line = line.strip() line = sep.split(line) for item in line: # remove potential quotes or backticks item = re.sub(r'^["\'`]|["\'`]$', '', item) # remove potential $(), ${}, `` item = re.sub(r'^\$[\(\{]|[\)\}]$', '', item) # if item has been converted to somthing other than a string # or an int, reconvert it to a string if type(item) not in ['str', 'int']: item = str(item) # replace "~" with home path item = os.path.expanduser(item) # expand shell wildcards using "echo" # i know, this a bit nasty... if re.findall('\$|\*|\?', item): # remove quotes if available item = re.sub("\"|\'", "", item) import subprocess p = subprocess.Popen( "`which echo` %s" % item, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE ) (cin, cout) = (p.stdin, p.stdout) item = cout.readlines()[0].split(' ')[0].strip() item = os.path.expandvars(item) tomatch = os.path.realpath(item) if os.path.isdir(tomatch) and tomatch[-1] != '/': tomatch += '/' match_allowed = re.findall(allowed_path_re, tomatch) if denied_path_re: match_denied = re.findall(denied_path_re, tomatch) else: match_denied = None # if path not allowed # case path executed: warn, and return 1 # case completion: return 1 if not match_allowed or match_denied: if not completion: self.warn_count('path', tomatch, strict, ssh) return 1 if not completion: if not re.findall(allowed_path_re, os.getcwd()+'/'): if not ssh: if strict: self.counter_update('path', os.getcwd()) os.chdir(self.conf['home_path']) self.updateprompt(os.getcwd()) else: self.log.critical('*** Forbidden path: %s' \ %os.getcwd()) return 1 return 0 def updateprompt(self, path): """ Update prompt when changing directory """ if path is self.conf['home_path']: self.prompt = '%s:~$ ' % self.promptbase elif self.conf['prompt_short'] == 1: self.prompt = '%s: %s$ ' % (self.promptbase, path.split('/')[-1]) elif re.findall(self.conf['home_path'], path): self.prompt = '%s:~%s$ ' % ( self.promptbase, \ path.split(self.conf['home_path'])[1]) else: self.prompt = '%s:%s$ ' % (self.promptbase, path) def cmdloop(self, intro=None): """Repeatedly issue a prompt, accept input, parse an initial prefix \ off the received input, and dispatch to action methods, passing them \ the remainder of the line as argument. """ self.preloop() if self.use_rawinput and self.completekey: try: readline.read_history_file(self.conf['history_file']) readline.set_history_length(self.conf['history_size']) except IOError: # if history file does not exist try: open(self.conf['history_file'], 'w').close() readline.read_history_file(self.conf['history_file']) except IOError: pass readline.set_completer_delims(readline.get_completer_delims().replace('-', '')) self.old_completer = readline.get_completer() readline.set_completer(self.complete) readline.parse_and_bind(self.completekey+": complete") try: if self.intro and isinstance(self.intro, str): self.stdout.write("%s\n" % self.intro) if self.conf['login_script']: retcode = exec_cmd(self.conf['login_script']) stop = None while not stop: if self.cmdqueue: line = self.cmdqueue.pop(0) else: if self.use_rawinput: try: line = raw_input(self.prompt) except EOFError: line = 'EOF' except KeyboardInterrupt: self.stdout.write('\n') line = '' else: self.stdout.write(self.prompt) self.stdout.flush() line = self.stdin.readline() if not len(line): line = 'EOF' else: line = line[:-1] # chop \n line = self.precmd(line) stop = self.onecmd(line) stop = self.postcmd(stop, line) self.postloop() finally: if self.use_rawinput and self.completekey: try: readline.set_completer_delims(readline.get_completer_delims().replace('-', '')) readline.set_completer(self.old_completer) except ImportError: pass try: readline.write_history_file(self.conf['history_file']) except IOError: self.log.error('WARN: couldn\'t write history ' \ 'to file %s\n' % self.conf['history_file']) def complete(self, text, state): """Return the next possible completion for 'text'. If a command has not been entered, then complete against command list. Otherwise try to call complete_ to get list of completions. """ if state == 0: origline = readline.get_line_buffer() line = origline.lstrip() # in case '|', ';', '&' used, take last part of line to complete line = re.split('&|\||;', line)[-1].lstrip() stripped = len(origline) - len(line) begidx = readline.get_begidx() - stripped endidx = readline.get_endidx() - stripped if line.split(' ')[0] == 'sudo' and len(line.split(' ')) <= 2: compfunc = self.completesudo elif len (line.split(' ')) > 1 \ and line.split(' ')[0] in self.conf['allowed']: compfunc = self.completechdir elif begidx > 0: cmd, args, foo = self.parseline(line) if cmd == '': compfunc = self.completedefault else: try: compfunc = getattr(self, 'complete_' + cmd) except AttributeError: compfunc = self.completedefault else: compfunc = self.completenames self.completion_matches = compfunc(text, line, begidx, endidx) try: return self.completion_matches[state] except IndexError: return None def default(self, line): """ This method overrides the original default method. It was originally used to warn when an unknown command was entered \ (e.g. *** Unknown syntax: blabla). It has been implemented in the __getattr__ method. So it has no use here. Its output is now empty. """ self.stdout.write('') def completenames(self, text, *ignored): """ This method overrides the original completenames method to overload\ it's output with the command available in the 'allowed' variable \ This is useful when typing 'tab-tab' in the command prompt """ dotext = 'do_'+text names = self.get_names() for command in self.conf['allowed']: names.append('do_' + command) return [a[3:] for a in names if a.startswith(dotext)] def completesudo(self, text, line, begidx, endidx): """ complete sudo command """ return [a for a in self.conf['sudo_commands'] if a.startswith(text)] def completechdir(self, text, line, begidx, endidx): """ complete directories """ toreturn = [] tocomplete = line.split()[-1] # replace "~" with home path tocomplete = re.sub('^~', self.conf['home_path'], tocomplete) try: directory = os.path.realpath(tocomplete) except: directory = os.getcwd() if not os.path.isdir(directory): directory = directory.rsplit('/', 1)[0] if directory == '': directory = '/' if not os.path.isdir(directory): directory = os.getcwd() if self.check_path(directory, 1) == 0: for instance in os.listdir(directory): if os.path.isdir(os.path.join(directory, instance)): instance = instance + '/' else: instance = instance + ' ' if instance.startswith('.'): if text.startswith('.'): toreturn.append(instance) else: pass else: toreturn.append(instance) return [a for a in toreturn if a.startswith(text)] else: return None def onecmd(self, line): """ This method overrides the original onecomd method, to put the cmd, \ arg and line variables in class global variables: self.g_cmd, \ self.g_arg and self.g_line. Thos variables are then used by the __getattr__ method """ cmd, arg, line = self.parseline(line) self.g_cmd, self.g_arg, self.g_line = [cmd, arg, line] if not line: return self.emptyline() if cmd is None: return self.default(line) self.lastcmd = line if cmd == '': return self.default(line) else: try: func = getattr(self, 'do_' + cmd) except AttributeError: return self.default(line) return func(arg) def emptyline(self): """ This method overrides the original emptyline method, so it doesn't \ repeat the last command if last command was empty. I just found this annoying.. """ if self.lastcmd: return 0 def do_help(self, arg): """ This method overrides the original do_help method. Instead of printing out the that are documented or not, it returns the \ list of allowed commands when '?' or 'help' is entered. Of course, it doesn't override the help function: any help_* method \ will be called (e.g. help_help(self) ) """ if arg: try: func = getattr(self, 'help_' + arg) except AttributeError: try: doc = getattr(self, 'do_' + arg).__doc__ if doc: self.stdout.write("%s\n"%str(doc)) return except AttributeError: pass self.stdout.write("%s\n"%str(self.nohelp % (arg,))) return func() else: # Get list of allowed commands, remove duplicate 'help' then sort it list_tmp = dict.fromkeys(self.completenames('')).keys() list_tmp.sort() self.columnize(list_tmp) def help_help(self): """ Print Help on Help """ self.stdout.write(help_help) def mytimer(self, timeout): """ This function is kicks you out the the lshell after \ the 'timer' variable exprires. 'timer' is set in seconds. """ # set timer signal.signal(signal.SIGALRM, self._timererror) signal.alarm(timeout) def _timererror(self, signum, frame): raise LshellTimeOut("lshell timer timeout") class LshellTimeOut(Exception): """ Custum exception used for timer timeout """ def __init__(self, value="Timed Out"): self.value = value def __str__(self): return repr(self.value) lshell-0.9.17/lshell/__init__.py0000644000175000017500000000147112563352273016730 0ustar ghantoosghantoos# # Limited command Shell (lshell) # # Copyright (C) 2008-2013 Ignace Mouzannar (ghantoos) # # This file is part of lshell # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . lshell-0.9.17/lshell/checkconfig.py0000644000175000017500000010171412563352273017435 0ustar ghantoosghantoos# # Limited command Shell (lshell) # # Copyright (C) 2008-2013 Ignace Mouzannar (ghantoos) # # This file is part of lshell # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import sys import os import ConfigParser from getpass import getpass, getuser import string import re import getopt import logging import grp import time import glob from utils import get_aliases,exec_cmd __version__ = "0.9.17" # Required config variable list per user required_config = ['allowed', 'forbidden', 'warning_counter'] # 'timer', 'scp', 'sftp'] # set configuration file path depending on sys.exec_prefix # on *Linux sys.exec_prefix = '/usr' and default path must be in '/etc' # on *BSD sys.exec_prefix = '/usr/{pkg,local}/' and default path # is '/usr/{pkg,local}/etc' if sys.exec_prefix != '/usr': # for *BSD conf_prefix = sys.exec_prefix else: # for *Linux conf_prefix = '' configfile = conf_prefix + '/etc/lshell.conf' # history file history_file = ".lhistory" # lock_file lock_file = ".lshell_lock" # help text usage = """Usage: lshell [OPTIONS] --config : Config file location (default %s) -- : where is *any* config file parameter -h, --help : Show this help message --version : Show version """ % configfile help_help = """Limited Shell (lshell) limited help. Cheers. """ # Intro Text intro = """You are in a limited shell. Type '?' or 'help' to get the list of allowed commands""" # configuration parameters configparams = [ 'config=', 'help', 'version', 'quiet=', 'log=', 'logpath=', 'loglevel=', 'logfilename=', 'syslogname=', 'allowed=', 'forbidden=', 'sudo_commands=', 'warning_counter=', 'aliases=', 'intro=', 'prompt=', 'prompt_short=', 'timer=', 'path=', 'home_path=', 'env_path=', 'allowed_cmd_path=', 'env_vars=', 'scp=', 'scp_upload=', 'scp_download=', 'sftp=', 'overssh=', 'strict=', 'scpforce=', 'history_size=', 'history_file=', 'include_dir='] class CheckConfig: """ Check the configuration file. """ def __init__(self, args, stdin=None, stdout=None, stderr=None): """ Force the calling of the methods below """ if stdin is None: self.stdin = sys.stdin else: self.stdin = stdin if stdout is None: self.stdout = sys.stdout else: self.stdout = stdout if stderr is None: self.stderr = sys.stderr else: self.stderr = stderr self.conf = {} self.conf, self.arguments = self.getoptions(args, self.conf) configfile = self.conf['configfile'] self.conf['config_mtime'] = self.get_config_mtime(configfile) self.check_file(configfile) self.get_global() self.check_log() self.get_config() self.check_user_integrity() self.get_config_user() self.check_env() self.check_scp_sftp() self.check_passwd() def getoptions(self, arguments, conf): """ This method checks the usage. lshell.py must be called with a \ configuration file. If no configuration file is specified, it will set the configuration \ file path to /etc/lshell.confelf.conf['allowed'].append('exit') """ # uncomment the following to set the -c/--config as mandatory argument #if '-c' not in arguments and '--config' not in arguments: # usage() # set configfile as default configuration file conf['configfile'] = configfile try: optlist, args = getopt.getopt(arguments, 'hc:', configparams) except getopt.GetoptError: self.stderr.write('Missing or unknown argument(s)\n') self.usage() for option, value in optlist: if option in ['--config']: conf['configfile'] = os.path.realpath(value) if option in ['--log']: conf['logpath'] = os.path.realpath(value) if "%s=" %option[2:] in configparams: conf[option[2:]] = value if option in ['-c']: conf['ssh'] = value if option in ['-h', '--help']: self.usage() if option in ['--version']: self.version() # put the expanded path of configfile and logpath (if exists) in # LSHELL_ARGS environment variable args = ['--config', conf['configfile']] if conf.has_key('logpath'): args += ['--log', conf['logpath']] os.environ['LSHELL_ARGS'] = str(args) # if lshell is invoked using shh autorized_keys file e.g. # command="/usr/bin/lshell", ssh-dss .... if os.environ.has_key('SSH_ORIGINAL_COMMAND'): conf['ssh'] = os.environ['SSH_ORIGINAL_COMMAND'] return conf, args def usage(self): """ Prints the usage """ sys.stderr.write(usage) sys.exit(0) def version(self): """ Prints the version """ sys.stderr.write('lshell-%s - Limited Shell\n' % __version__) sys.exit(0) def check_env(self): """ Load environment variable set in configuration file """ if self.conf.has_key('env_vars'): env_vars = self.conf['env_vars'] for key in env_vars.keys(): os.environ[key] = str(env_vars[key]) def check_file(self, file): """ This method checks the existence of the "argumently" given \ configuration file. """ if not os.path.exists(file): self.stderr.write("Error: Config file doesn't exist\n") self.stderr.write(usage) sys.exit(0) else: self.config = ConfigParser.ConfigParser() def get_global(self): """ Loads the [global] parameters from the configuration file """ try: self.config.read(self.conf['configfile']) except (ConfigParser.MissingSectionHeaderError, \ ConfigParser.ParsingError), argument: self.stderr.write('ERR: %s\n' %argument) sys.exit(0) if not self.config.has_section('global'): self.stderr.write('Config file missing [global] section\n') sys.exit(0) for item in self.config.items('global'): if not self.conf.has_key(item[0]): self.conf[item[0]] = item[1] def check_log(self): """ Sets the log level and log file """ # define log levels dict self.levels = { 1 : logging.CRITICAL, 2 : logging.ERROR, 3 : logging.WARNING, 4 : logging.DEBUG } # create logger for lshell application if self.conf.has_key('syslogname'): try: logname = eval(self.conf['syslogname']) except: logname = self.conf['syslogname'] else: logname = 'lshell' logger = logging.getLogger("%s.%s" % (logname, \ self.conf['config_mtime'])) # close any logger handler/filters if exists # this is useful if configuration is reloaded for loghandler in logger.handlers: try: logging.shutdown(logger.handlers) except TypeError: pass for logfilter in logger.filters: logger.removeFilter(logfilter) formatter = logging.Formatter('%%(asctime)s (%s): %%(message)s' \ % getuser() ) syslogformatter = logging.Formatter('%s[%s]: %s: %%(message)s' \ % (logname, os.getpid(), getuser() )) logger.setLevel(logging.DEBUG) # set log to output error on stderr logsterr = logging.StreamHandler() logger.addHandler(logsterr) logsterr.setFormatter(logging.Formatter('%(message)s')) logsterr.setLevel(logging.CRITICAL) # log level must be 1, 2, 3 , 4 or 0 if not self.conf.has_key('loglevel'): self.conf['loglevel'] = 0 try: self.conf['loglevel'] = int(self.conf['loglevel']) except ValueError: self.conf['loglevel'] = 0 if self.conf['loglevel'] > 4: self.conf['loglevel'] = 4 elif self.conf['loglevel'] < 0: self.conf['loglevel'] = 0 # read logfilename is exists, and set logfilename if self.conf.has_key('logfilename'): try: logfilename = eval(self.conf['logfilename']) except: logfilename = self.conf['logfilename'] currentime = time.localtime() logfilename = logfilename.replace('%y','%s' %currentime[0]) logfilename = logfilename.replace('%m','%02d' %currentime[1]) logfilename = logfilename.replace('%d','%02d' %currentime[2]) logfilename = logfilename.replace('%h','%02d%02d' % (currentime[3] \ , currentime[4])) logfilename = logfilename.replace('%u', getuser()) else: logfilename = getuser() if self.conf['loglevel'] > 0: try: if logfilename == "syslog": from logging.handlers import SysLogHandler syslog = SysLogHandler(address='/dev/log') syslog.setFormatter(syslogformatter) syslog.setLevel(self.levels[self.conf['loglevel']]) logger.addHandler(syslog) else: # if log file is writable add new log file handler logfile = os.path.join(self.conf['logpath'], \ logfilename+'.log') # create log file if it does not exist, and set permissions fp = open(logfile,'a').close() os.chmod(logfile, 0600) # set logging handler self.logfile = logging.FileHandler(logfile) self.logfile.setFormatter(formatter) self.logfile.setLevel(self.levels[self.conf['loglevel']]) logger.addHandler(self.logfile) except IOError: # uncomment the 2 following lines to warn if log file is not \ # writable #sys.stderr.write('Warning: Cannot write in log file: ' # 'Permission denied.\n') #sys.stderr.write('Warning: Actions will not be logged.\n') pass self.conf['logpath'] = logger self.log = logger def get_config(self): """ Load default, group and user configuation. Then merge them all. The loadpriority is done in the following order: 1- User section 2- Group section 3- Default section """ self.config.read(self.conf['configfile']) # list the include_dir directory and read configuration files if self.conf.has_key('include_dir'): import glob self.conf['include_dir_conf'] = glob.glob("%s*" % self.conf['include_dir']) self.config.read(self.conf['include_dir_conf']) self.user = getuser() self.conf_raw = {} # get 'default' configuration if any self.get_config_sub('default') # get groups configuration if any. # for each group the user belongs to, check if specific configuration \ # exists. The primary group has the highest priority. grplist = os.getgroups() grplist.reverse() for gid in grplist: try: grpname = grp.getgrgid(gid)[0] section = 'grp:' + grpname self.get_config_sub(section) except KeyError: pass # get user configuration if any self.get_config_sub(self.user) def get_config_sub(self, section): """ this function is used to interpret the configuration +/-, 'all' etc. """ # convert commandline options from dict to list of tuples, in order to # merge them with the output of the config parser conf = [] for key in self.conf: if key not in ['config_mtime', 'logpath']: conf.append((key,self.conf[key])) if self.config.has_section(section): conf = self.config.items(section) + conf for item in conf: key = item[0] value = item[1] # if string, then split if isinstance(value, str): split = re.split('([\+\-\s]+\[[^\]]+\])', value.replace(' ', '')) if len(split) > 1 and key in ['path', \ 'overssh', \ 'allowed', \ 'forbidden']: for stuff in split: if stuff.startswith('-') or stuff.startswith('+'): self.conf_raw.update(self.minusplus(self.conf_raw, \ key,stuff)) elif stuff == "'all'": self.conf_raw.update({key:self.expand_all()}) elif stuff and key == 'path': liste = ['', ''] for path in eval(stuff): for item in glob.glob(path): liste[0] += os.path.realpath(item) + '/.*|' # remove double slashes liste[0] = liste[0].replace("//","/") self.conf_raw.update({key:str(liste)}) elif stuff and type(eval(stuff)) is list: self.conf_raw.update({key:stuff}) # case allowed is set to 'all' elif key == 'allowed' and split[0] == "'all'": self.conf_raw.update({key:self.expand_all()}) elif key == 'path': liste = ['', ''] for path in self.myeval(value, 'path'): for item in glob.glob(path): liste[0] += os.path.realpath(item) + '/.*|' # remove double slashes liste[0] = liste[0].replace("//","/") self.conf_raw.update({key:str(liste)}) else: self.conf_raw.update(dict([item])) def minusplus(self, confdict, key, extra): """ update configuration lists containing -/+ operators """ if confdict.has_key(key): liste = self.myeval(confdict[key]) elif key == 'path': liste = ['', ''] else: liste = [] sublist = self.myeval(extra[1:], key) if extra.startswith('+'): if key == 'path': for path in sublist: liste[0] += os.path.realpath(path) + '/.*|' else: for item in sublist: liste.append(item) elif extra.startswith('-'): if key == 'path': for path in sublist: liste[1] += os.path.realpath(path) + '/.*|' else: for item in sublist: if item in liste: liste.remove(item) else: self.log.error("CONF: -['%s'] ignored in '%s' list." \ %(item,key)) return {key:str(liste)} def expand_all(self): """ expand allowed, if set to 'all' """ # initialize list to common shell builtins expanded_all = ['bg', 'break', 'case', 'cd', 'continue', 'eval', \ 'exec', 'exit', 'fg', 'if', 'jobs', 'kill', 'login', \ 'logout', 'set', 'shift', 'stop', 'suspend', 'umask', \ 'unset', 'wait', 'while' ] for directory in os.environ['PATH'].split(':'): if os.path.exists(directory): for item in os.listdir(directory): if os.access(os.path.join(directory, item), os.X_OK): expanded_all.append(item) else: self.log.error('CONF: PATH entry "%s" does not exist' \ % directory) return str(expanded_all) def myeval(self, value, info=''): """ if eval returns SyntaxError, log it as critical iconf missing """ try: evaluated = eval(value) return evaluated except SyntaxError: self.log.critical('CONF: Incomplete %s field in configuration file'\ % info) sys.exit(1) def check_user_integrity(self): """ This method checks if all the required fields by user are present \ for the present user. In case fields are missing, the user is notified and exited from lshell. """ for item in required_config: if item not in self.conf_raw.keys(): self.log.critical('ERROR: Missing parameter \'' \ + item + '\'') self.log.critical('ERROR: Add it in the in the [%s] ' 'or [default] section of conf file.' % self.user) sys.exit(0) def get_config_user(self): """ Once all the checks above have passed, the configuration files \ values are entered in a dict to be used by the command line it self. The lshell command line is then launched! """ # first, check user's loglevel if self.conf_raw.has_key('loglevel'): try: self.conf['loglevel'] = int(self.conf_raw['loglevel']) except ValueError: pass if self.conf['loglevel'] > 4: self.conf['loglevel'] = 4 elif self.conf['loglevel'] < 0: self.conf['loglevel'] = 0 # if log file exists: try: self.logfile.setLevel(self.levels[self.conf['loglevel']]) except AttributeError: pass for item in ['allowed', 'forbidden', 'sudo_commands', 'warning_counter', 'env_vars', 'timer', 'scp', 'scp_upload', 'scp_download', 'sftp', 'overssh', 'strict', 'aliases', 'prompt', 'prompt_short', 'allowed_cmd_path', 'history_size', 'login_script', 'quiet']: try: if len(self.conf_raw[item]) == 0: self.conf[item] = "" else: self.conf[item] = self.myeval(self.conf_raw[item], item) except KeyError: if item in ['allowed', 'overssh', 'sudo_commands']: self.conf[item] = [] elif item in ['history_size']: self.conf[item] = -1 # default scp is allowed elif item in ['scp_upload', 'scp_download']: self.conf[item] = 1 elif item in ['aliases','env_vars']: self.conf[item] = {} # do not set the variable elif item in ['prompt']: continue else: self.conf[item] = 0 except TypeError: self.log.critical('ERR: in the -%s- field. Check the' \ ' configuration file.' %item ) sys.exit(0) self.conf['username'] = self.user if self.conf_raw.has_key('home_path'): self.conf_raw['home_path'] = self.conf_raw['home_path'].replace( \ "%u", self.conf['username']) self.conf['home_path'] = os.path.normpath(self.myeval(self.conf_raw\ ['home_path'],'home_path')) else: self.conf['home_path'] = os.environ['HOME'] if self.conf_raw.has_key('path'): self.conf['path'] = eval(self.conf_raw['path']) self.conf['path'][0] += self.conf['home_path'] + '.*' else: self.conf['path'] = ['', ''] self.conf['path'][0] = self.conf['home_path'] + '.*' if self.conf_raw.has_key('env_path'): self.conf['env_path'] = self.myeval(self.conf_raw['env_path'], \ 'env_path') else: self.conf['env_path'] = '' if self.conf_raw.has_key('scpforce'): self.conf_raw['scpforce'] = self.myeval( \ self.conf_raw['scpforce']) try: if os.path.exists(self.conf_raw['scpforce']): self.conf['scpforce'] = self.conf_raw['scpforce'] else: self.log.error('CONF: scpforce no such directory: %s' \ % self.conf_raw['scpforce']) except TypeError: self.log.error('CONF: scpforce must be a string!') if self.conf_raw.has_key('intro'): self.conf['intro'] = self.myeval(self.conf_raw['intro']) else: self.conf['intro'] = intro # check if user account if locked if self.conf_raw.has_key('lock_counter'): self.conf['lock_counter'] = self.conf_raw['lock_counter'] self.account_lock(self.user, self.conf['lock_counter'], 1) if os.path.isdir(self.conf['home_path']): os.chdir(self.conf['home_path']) else: self.log.critical('ERR: home directory "%s" does not exist.' \ % self.conf['home_path']) sys.exit(0) if self.conf_raw.has_key('history_file'): try: self.conf['history_file'] = \ eval(self.conf_raw['history_file'].replace( \ "%u", self.conf['username'])) except: self.log.error('CONF: history file error: %s' \ % self.conf['history_file']) else: self.conf['history_file'] = history_file if not self.conf['history_file'].startswith('/'): self.conf['history_file'] = "%s/%s" % ( self.conf['home_path'], \ self.conf['history_file']) os.environ['PATH'] = os.environ['PATH'] + self.conf['env_path'] # append default commands to allowed list self.conf['allowed'].append('exit') self.conf['allowed'].append('lpath') self.conf['allowed'].append('lsudo') self.conf['allowed'].append('history') self.conf['allowed'].append('clear') # in case sudo_commands is not empty, append sudo(8) to allowed commands if self.conf['sudo_commands']: self.conf['allowed'].append('sudo') # add all commands present in allowed_cmd_path if specified if self.conf['allowed_cmd_path']: for path in self.conf['allowed_cmd_path']: # add path to PATH env variable os.environ['PATH'] += ":%s" % path # find executable file, and add them to allowed commands for item in os.listdir(path): cmd = os.path.join(path, item) if os.access(cmd, os.X_OK): self.conf['allowed'].append(item) # case sudo_commands set to 'all', expand to all 'allowed' commands if self.conf_raw.has_key('sudo_commands') and \ self.conf_raw['sudo_commands'] == 'all': # exclude native commands and sudo(8) exclude = ['exit','lpath','lsudo','history','clear','export','sudo'] self.conf['sudo_commands'] = \ [x for x in self.conf['allowed'] if x not in exclude] # sort lsudo commands self.conf['sudo_commands'].sort() def account_lock(self, user, lock_counter, check=None): """ check if user account is locked, in which case, exit """ ### TODO ### # check if account is locked if check: pass # increment account lock else: pass def check_scp_sftp(self): """ This method checks if the user is trying to SCP a file onto the \ server. If this is the case, it checks if the user is allowed to use \ SCP or not, and acts as requested. : ) """ if self.conf.has_key('ssh'): if os.environ.has_key('SSH_CLIENT') \ and not os.environ.has_key('SSH_TTY'): # check if sftp is requested and allowed if 'sftp-server' in self.conf['ssh']: if self.conf['sftp'] is 1: self.log.error('SFTP connect') exec_cmd(self.conf['ssh']) self.log.error('SFTP disconnect') sys.exit(0) else: self.log.error('*** forbidden SFTP connection') sys.exit(0) # initialise cli session from lshell.shellcmd import ShellCmd cli = ShellCmd(self.conf, None, None, None, None, \ self.conf['ssh']) if cli.check_path(self.conf['ssh'], None, ssh=1) == 1: self.ssh_warn('path over SSH', self.conf['ssh']) # check if scp is requested and allowed if self.conf['ssh'].startswith('scp '): if self.conf['scp'] is 1 or 'scp' in self.conf['overssh']: if ' -f ' in self.conf['ssh']: # case scp download is allowed if self.conf['scp_download']: self.log.error('SCP: GET "%s"' \ % self.conf['ssh']) # case scp download is forbidden else: self.log.error('SCP: download forbidden: "%s"' \ % self.conf['ssh']) sys.exit(0) elif ' -t ' in self.conf['ssh']: # case scp upload is allowed if self.conf['scp_upload']: if self.conf.has_key('scpforce'): cmdsplit = self.conf['ssh'].split(' ') scppath = os.path.realpath(cmdsplit[-1]) forcedpath = os.path.realpath(self.conf ['scpforce']) if scppath != forcedpath: self.log.error('SCP: forced SCP ' \ + 'directory: %s' \ %scppath) cmdsplit.pop(-1) cmdsplit.append(forcedpath) self.conf['ssh'] = string.join(cmdsplit) self.log.error('SCP: PUT "%s"' \ %self.conf['ssh']) # case scp upload is forbidden else: self.log.error('SCP: upload forbidden: "%s"' \ % self.conf['ssh']) sys.exit(0) exec_cmd(self.conf['ssh']) self.log.error('SCP disconnect') sys.exit(0) else: self.ssh_warn('SCP connection', self.conf['ssh'], 'scp') # check if command is in allowed overssh commands elif self.conf['ssh']: # replace aliases self.conf['ssh'] = get_aliases(self.conf['ssh'], \ self.conf['aliases']) # if command is not "secure", exit if cli.check_secure(self.conf['ssh'], strict=1, ssh=1): self.ssh_warn('char/command over SSH', self.conf['ssh']) # else self.log.error('Over SSH: "%s"' %self.conf['ssh']) # if command is "help" if self.conf['ssh'] == "help": cli.do_help(None) else: exec_cmd(self.conf['ssh']) self.log.error('Exited') sys.exit(0) # else warn and log else: self.ssh_warn('command over SSH', self.conf['ssh']) else : # case of shell escapes self.ssh_warn('shell escape', self.conf['ssh']) def ssh_warn(self, message, command='', key=''): """ log and warn if forbidden action over SSH """ if key == 'scp': self.log.critical('*** forbidden %s' %message) self.log.error('*** SCP command: %s' %command) else: self.log.critical('*** forbidden %s: "%s"' %(message, command)) self.stderr.write('This incident has been reported.\n') self.log.error('Exited') sys.exit(0) def check_passwd(self): """ As a passwd field is required by user. This method checks in the \ configuration file if the password is empty, in wich case, no password \ check is required. In the other case, the password is asked to be \ entered. If the entered password is wrong, the user is exited from lshell. """ if self.config.has_section(self.user): if self.config.has_option(self.user, 'passwd'): passwd = self.config.get(self.user, 'passwd') else: passwd = None else: passwd = None if passwd: password = getpass("Enter "+self.user+"'s password: ") if password != passwd: self.stderr.write('Error: Wrong password \nExiting..\n') self.log.critical('WARN: Wrong password') sys.exit(0) else: return 0 def get_config_mtime(self, configfile): """ get configuration file modification time, and store in the \ configuration dict. This should then be used to reload the configuration dynamically upon file changes """ return os.path.getmtime(configfile) def returnconf(self): """ returns the configuration dict """ return self.conf lshell-0.9.17/etc/0000755000175000017500000000000012563352273014104 5ustar ghantoosghantooslshell-0.9.17/etc/logrotate.d/0000755000175000017500000000000012563352273016326 5ustar ghantoosghantooslshell-0.9.17/etc/logrotate.d/lshell0000644000175000017500000000022512563352273017533 0ustar ghantoosghantoos# $Id: lshell,v 1.1 2010-03-02 00:05:58 ghantoos Exp $ /var/log/lshell/*.log { rotate 12 weekly compress missingok notifempty } lshell-0.9.17/etc/lshell.conf0000644000175000017500000000675412563352273016252 0ustar ghantoosghantoos# lshell.py configuration file # # $Id: lshell.conf,v 1.27 2010-10-18 19:05:17 ghantoos Exp $ [global] ## log directory (default /var/log/lshell/ ) logpath : /var/log/lshell/ ## set log level to 0, 1, 2, 3 or 4 (0: no logs, 1: least verbose, ## 4: log all commands) loglevel : 2 ## configure log file name (default is %u i.e. username.log) #logfilename : %y%m%d-%u #logfilename : syslog ## in case you are using syslog, you can choose your logname #syslogname : myapp ## include a directory containing multiple configuration files. These files ## can only contain default/user/group configuration. The global configuration will ## only be loaded from the default configuration file. ## e.g. splitting users into separate files #include_dir : /etc/lshell.d/*.conf [default] ## a list of the allowed commands or 'all' to allow all commands in user's PATH allowed : ['ls','echo','cd','ll'] ## a list of forbidden character or commands -- deny vim, as it allows to escape lshell forbidden : [';', '&', '|','`','>','<', '$(', '${'] ## a list of allowed command to use with sudo(8) ## if set to ´all', all the 'allowed' commands will be accessible through sudo(8) #sudo_commands : ['ls', 'more'] ## number of warnings when user enters a forbidden value before getting ## exited from lshell, set to -1 to disable. warning_counter : 2 ## command aliases list (similar to bash’s alias directive) aliases : {'ll':'ls -l', 'vim':'rvim'} ## introduction text to print (when entering lshell) #intro : "== My personal intro ==\nWelcome to lshell\nType '?' or 'help' to get the list of allowed commands" ## configure your promt using %u or %h (default: username) #prompt : "%u@%h" ## set sort prompt current directory update (default: 0) #prompt_short : 0 ## a value in seconds for the session timer #timer : 5 ## list of path to restrict the user "geographicaly" #path : ['/home/bla/','/etc'] ## set the home folder of your user. If not specified the home_path is set to ## the $HOME environment variable #home_path : '/home/bla/' ## update the environment variable $PATH of the user #env_path : ':/usr/local/bin:/usr/sbin' ## a list of path; all executable files inside these path will be allowed #allowed_cmd_path: ['/home/bla/bin','/home/bla/stuff/libexec'] ## add environment variables #env_vars : {'foo':1, 'bar':'helloworld'} ## allow or forbid the use of scp (set to 1 or 0) #scp : 1 ## forbid scp upload #scp_upload : 0 ## forbid scp download #scp_download : 0 ## allow of forbid the use of sftp (set to 1 or 0) ## this option will not work if you are using OpenSSH's internal-sftp service #sftp : 1 ## list of command allowed to execute over ssh (e.g. rsync, rdiff-backup, etc.) #overssh : ['ls', 'rsync'] ## logging strictness. If set to 1, any unknown command is considered as ## forbidden, and user's warning counter is decreased. If set to 0, command is ## considered as unknown, and user is only warned (i.e. *** unknown synthax) strict : 0 ## force files sent through scp to a specific directory #scpforce : '/home/bla/uploads/' ## history file maximum size #history_size : 100 ## set history file name (default is /home/%u/.lhistory) #history_file : "/home/%u/.lshell_history" ## define the script to run at user login #login_script : "/path/to/myscript.sh" lshell-0.9.17/TODO0000644000175000017500000000060112563352273014016 0ustar ghantoosghantoos* ability to lock user * iMil: add the possibility to use jokers in allowed commands * possibility to execute some command at login (like .bashrc) * add bash_completion like to lshell * check out syslog in python 2.7 (hint: 15 characters) * make warnings configurable * test commands with parameters (should work) * Traceback when command exists in allowed command (fill a bug report) lshell-0.9.17/Makefile0000644000175000017500000000237612563352273015001 0ustar ghantoosghantoos# Limited Shell (lshell) Makefile # # $Id: Makefile,v 1.16 2010-03-06 23:11:38 ghantoos Exp $ # PYTHON=`which python` DESTDIR=/ BUILDIR=$(CURDIR)/debian/lshell PROJECT=lshell all: @echo "make source - Create source package" @echo "make sourcedeb - Create source package (.orig.tar.gz)" @echo "make install - Install on local system" @echo "make buildrpm - Generate a rpm package" @echo "make builddeb - Generate a deb package" @echo "make clean - Get rid of scratch and byte files" source: $(PYTHON) setup.py sdist sourcedeb: $(PYTHON) setup.py sdist --dist-dir=../ --prune rename -f 's/$(PROJECT)-(.*)\.tar\.gz/$(PROJECT)_$$1\.orig\.tar\.gz/' ../* install: $(PYTHON) setup.py install --root=$(DESTDIR) --no-compile buildrpm: $(PYTHON) setup.py bdist_rpm --pre-install=rpm/preinstall --post-install=rpm/postinstall --post-uninstall=rpm/postuninstall builddeb: # build the source package in the parent directory # then rename it to project_version.orig.tar.gz $(PYTHON) setup.py sdist --dist-dir=../ --prune rename -f 's/$(PROJECT)-(.*)\.tar\.gz/$(PROJECT)_$$1\.orig\.tar\.gz/' ../* # build the package dpkg-buildpackage -i -I -rfakeroot clean: $(PYTHON) setup.py clean rm -rf build/ MANIFEST dist/ find . -name '*.pyc' -delete lshell-0.9.17/setup.py0000755000175000017500000000473612563352273015060 0ustar ghantoosghantoos#!/usr/bin/env python # # $Id: setup.py,v 1.32 2010-10-17 15:47:21 ghantoos Exp $ # # Copyright (C) 2008-2009 Ignace Mouzannar (ghantoos) # # This file is part of lshell # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from distutils.core import setup if __name__ == '__main__': setup(name='lshell', version='0.9.17', description='Limited Shell', long_description="""Limited Shell (lshell) is lets you restrict the \ environment of any user. It provides an easily configurable shell: just \ choose a list of allowed commands for every limited account.""", author='Ignace Mouzannar (ghantoos)', author_email='ghantoos@ghantoos.org', maintainer='Ignace Mouzannar (ghantoos)', maintainer_email='ghantoos@ghantoos.org', keywords=['limited','shell','security','python'], url='http://ghantoos.org/limited-shell-lshell/', license='GPL', platforms='UNIX', scripts = ['bin/lshell'], package_dir = {'lshell':'lshell'}, packages = ['lshell'], data_files = [('/etc', ['etc/lshell.conf']), ('/etc/logrotate.d', ['etc/logrotate.d/lshell']), ('share/doc/lshell',['README.md', 'COPYING', 'CHANGES']), ('share/man/man1/', ['man/lshell.1']) ], classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Console' 'Intended Audience :: Advanced End Users', 'Intended Audience :: System Administrators', 'License :: OSI Approved :: GNU General Public License v3', 'Operating System :: POSIX', 'Programming Language :: Python', 'Topic :: Security', 'Topic :: System Shells', 'Topic :: Terminals' ], ) lshell-0.9.17/CHANGES0000755000175000017500000003113112563352273014326 0ustar ghantoosghantoos##################################### # LSHELL - Limited Shell - CHANGES # ##################################### # # $Id: CHANGES,v 1.65 2010-10-26 22:39:36 ghantoos Exp $ Contact: ghantoos@ghantoos.org http://sourceforge.net/projects/lshell/ ##################################### === v0.9.17 14/08/2015 === * Added include_dir directive to include split configuration files from a directory. * Added possibility of using 'all' for sudo commands * Replaced os.system by subprocess (python) * Added support for sudo -u * Corrected shell variable expansion * Corrected bugs in aliases support * Fixed timer (idle session) * Added exit code support * Fixed wrong group reference for logging * Replaced Python os.system with subprocess === v0.9.16 14/08/2013 === * Added support to login script. Thank you Laurent Debacker for the patch. * Fixed auto-complete failing with "-" * Fixed bug where forbidden commands still execute if strict=1 * Fixed auto-completion complete of forbidden paths * Fixed wrong parsing &, | or ; characters * Added urandom function definition for python 2.3 compat * Corrected env variable expansion * Add support for cd command in aliases * Split lshellmodule in multiple files under the lshell directory * Fixed check_secure function to ignore quoted text * Fixed multiple spaces escaping forbidden filtering * Fixed log file permissions 644 -> 600 * Added possibility to override config file option via command-line * Enabled job control when executing command * Code cleanup === v0.9.15.2 08/05/2012 === * Corrected mismatch in aliaskey variable. === v0.9.15.1 15/03/2012 === * Corrected security bug allowing user to get out of the restricted shell. Thank you bui from NBS System for reporting this grave issue! === v0.9.15 13/03/2012 === * Set the hostname to the "short hostname" in the prompt. * Corrected traceback when "sudo" command was entered alone. Thank you Kiran Reddy for reporting this. * Added support for python2.3 as subprocess is not included by default. * Corrected the 'strict' behavior when entering a forbidden path. * Added short path promp support using the 'prompt_short' variable. * Corrected stacktrace when group did not exist. * Add support for empty prompt. * Fixed bugs when using $() and ``. * Corrected strict behavior to apply to forbidden path. * Added support for wildcard '*' when using 'cd'. * Added support for "cd -" to return to previous directory. * Updated security issue with non printable characters permitting user to get out of the limited shell. * Now lshell automatically reload its configuration if the configuration file is modified. * Added possibility to have no "intro" when user logs in. (by setting the intro configuration field to "") * Corrected multiple commands over ssh, and aliases interpretation. * Added possibility to use wildcards in path definitions. * Finally corrected the alias replacement loop. === v0.9.14 27/10/2010 === * Corrected get_aliases function, as it was looping when aliases were "recursive" (e.g. 'ls':'ls --color=auto') * Added lsudo built-in command to list allowed sudo commands. * Corrected completion function when 2 strings collided (e.g. ls and lsudo) * Corrected the README's installation part (adding --prefix). * Added possibility to log via syslog. * Corrected warning counter (was counting minus 1). * Added the possibility to disable the counter, and just warn the user (withouht kicking him). * Added possibility to configure prompt. Thank you bapt for the patch. * Added possibility to set environment variables to users. Thank you bapt for the patch. * Added the 'history' built-in function. === v0.9.13 02/09/2010 === * Switched from deprecated popen2 to subprocess to be python2.6 compatible. Thank you Greg Orlowski for the patch. * Added missing builin commands when 'allowed' list was set to 'all'. For example, the "cd" command was then missing. * Added the "export" builtin function to export shell variables. Thank you Chris for reporting this issue. === v0.9.12 04/05/2010 === * A minor bug was inserted in version 0.9.11 with the sudo command. It has been corrected in this version. === v0.9.11 27/04/2010 === * Corrects traceback when executing a command that had a python homonym (e.g. "print foo" or "set"). (Closes: SF#2969631) * Corrected completion error when using "~/". Thanks to Piotr Minkina for reporting this. * Corrected the get_aliases function. * Corrected interpretation of ~user. Thank you Adrien Urban for reporting this. * The 'home_path' variable is being deprecated from this version and on. Please use your system's tools to set a user's home directory. It will be completely removed in the next version of lshell. * Corrected shell variable and wildcards expansions when checking a command. Thank you Adrien Urban for reporting this. * Added possibility to allow/forbid scp upload/download using scp_upload and scp_download variables. * Corrected bug when using the "command=" in openSSH's authorized_keys. lshell now takes into account the SSH_ORIGINAL_COMMAND environment variable. Thank you Jason Heiss for reporting this. * Corrected traceback when aliases is not defined in configuration, and command is sent over SSH. Thank you Jason Heiss for reporting this. === v0.9.10 08/03/2010 === * Corrected minor bug in the aliases function that appeared in the previous version. Thank you Piotr Minkina for reporting this. === v0.9.9 07/03/2010 === * Added the possibility to configure introduction prompt. * Replaced "joker" by "warnings" (more elegant) * Possibility of limiting the history file size. * Added lpath built-in command to list allowed and denied path. Thanks to Adrien Urban. * Corrected bug when using "~" was not parsed as "home directory" when used in a command other than "cd". Thank you Adrien Urban finding this. * Corrected minor typo when warning for a forbidden path. * If $(foo) is present in the line, check if foo is allowed before executing the line. Thank you Adrien Urban for pointing this out! * Added the possibility to list commands allowed to be executed using sudo. The new configuration field is sudo_commands. * Added the clear(1) command as a built-in command. * Added '$(' and '${' in the forbidden list by default in the configuration file. * Now check the content of curly braces with variables '${}'. Thank you Adrien Urban for reporting this. * Added possibility to set history file name using history_file in the configuration file. * Corrected the bug when using '|', '&' or ';' over ssh. Over ssh forbidden characters refers now to the list provided in the "forbidden" field. Thank you Jools Wills for reporting this! * It now possible to use "&&" and "||" even if "&" and/or "|" are in the forbidden list. In order to forbid them too, you must add them explicitely in the forbidden list. Thank you Adrien Urban for this suggestion. * Fixed aliases bug that replaced part of commands rendering them unusable. e.g. aliase vi:vim replaced the view command by vimew. * Added a logrotate file for lshell log files. * Corrected parsing of commands overssh to be checked by the same function used by the lshell CLI. Thank you Adrien Urban for you security audit and excellent ideas! === v0.9.8 30/11/2009 === * Major bug fix. lshell did not launch on python 2.4 and 2.5 (https://sourceforge.net/projects/lshell/forums/forum/778301/topic/3474668) * Added aliases for commands over SSH. === v0.9.7 25/11/2009 === * Cleaned up the Python code * Corrected crash when directory permission denied (Closes: https://sourceforge.net/tracker/?func=detail&aid=2875374&group_id=215792&atid=1035093) * Added possibility to set the home_path option using the '%u' flag. (e.g. '/var/chroot/%u' where '%u' will be replaced by the user's username) * Now replaces "~" by user's home directory. === v0.9.6 9/09/2009 === * Major security fix. User had access to all files located in forbidden directories (Closes: https://sourceforge.net/tracker/?func=detail&aid=2838542&group_id=215792&atid=1035093) * Corrects RPM generation bug (Closes: https://sourceforge.net/tracker/index.php?func=detail&aid=2838283&group_id=215792&atid=1035093) * lshell exits gracefully when user home directory doesn't exist === v0.9.5 28/07/2009 === * Minor release * Changed lshell's group from lshellg to lshell (this should not have an \ impact on older installations) * Minor typo correction in the lshell.py code === v0.9.4 09/06/2009 === * Log file name is now configurable using 'logfilename' variable inside the\ configuration file * Corrected aliases in lshell.conf to work with *BSD === v0.9.3 13/04/2009 === * corrected major bug (alias related) === v0.9.2 05/04/2009 === * added Force SCP directory feature * added command alias feature === v0.9.1 24/03/2009 === * loglevel can now be defined on global, group or user level * corrected sftp support (broken since in 0.9.0) === v0.9.0 20/03/2009 === * As lshell has reached the point where it can be considered as a nearly \ stable software. I decided to make a version jump to 0.9.0 * corrected bug in case PATH does not exist and allowed set to 'all' * added support for UNIX groups in configuration file * cleaned up code * corrected major security bug * corrected path completion, to complete only allowed path simplified the \ check_secure and check_path functions * added escape code handling (tested with ftp, gdb, vi) * added flexible +/- possibilities in configuration file * now supports completion after '|', ';' and '&' * Command test are also done after '|', ';' and '&' * Doesn't list hidden directories by default * There are now 4 logging levels (4: logs absolutely everything user types) * added 'strict' behaviour. If set to 1, any unknown command is considered \ as forbidden, as warning counter is decreased. === v0.2.6 02/03/2009 === * added 'all' to allow all commands to a user * added backticks in lshell.conf * changes made to setup.py in version 0.2.5 were undone + added classifiers === v0.2.5 15/02/2009 === * corrected import readline [bug] * added log directory instead of a logfile * created log levels (0 to 3) * setup.py is now BSD compatible (using --install-data flag) === v0.2.4 27/01/2009 === * NEW: "overssh" in config file. Allows to set commands allowed to execute \ over ssh (e.g. rsync) * fixed timer * added python logging method * cleaned code * cleaner "over ssh commands" support (e.g. scp, sftp, rsync, etc.) === v0.2.3 03/12/2008 === * corrected completion * added [global] section in configuration file === v0.2.2 29/10/2008 === * corrected SCP functionnality * added SFTP support * passwd in not mandatory in configuration file (deprecated) * lshell is now added to /etc/shells using `add-shell` === v0.2.1 20/10/2008 === * Corrected rpm & deb builds * added a manpage === v0.2 18/10/2008 === * Initial debian packaging === v0.2 17/10/2008 === * Added config and log option on command line (-c|--config and -l|--log) * Initial source packaging using distutils * Initial rpm packaging using distutils === v0.2 07/10/2008 === * Added file completion * Added a history file per user * Added a logging for warnings and log in/out * Added prompt update when user changes directory (bash like) * Corrected the check_path function * Changed user setting from global variable to dict * Added a default profile used when a parameter is not set for a user === 06/05/2008 === * Added an shell script usefull to install and manage lshell users === 08/04/2008 === * Added evironment path (env_path) update support * Added home path (home_path) variable === 29/03/2008 === * Corrected class declaration bug and configuration file location * Updated the README file with another usage of lshell === 05/02/2008 === * added a path variable to restrict the user's geographic actions * MAJOR: added SCP support (also configurable through the config file) === 31/01/2008 === * MAJOR: Added the 'help' method * Did some code cleanup === 28/01/2008 === * Initial release of lshell lshell-0.9.17/man/0000755000175000017500000000000012563352273014104 5ustar ghantoosghantooslshell-0.9.17/man/lshell.10000644000175000017500000001774712563352273015471 0ustar ghantoosghantoos.\" .\" Man page for the Limited Shell (lshell) project. .\" .TH lshell 1 "July, 2015" "v0.9.17" .SH NAME lshell \- Limited Shell .SH SYNOPSIS .B lshell [\fIOPTIONS\fR] .SH DESCRIPTION \fBlshell\fR provides a limited shell configured per user. The configuration is done quite simply using a configuration file. Coupled with ssh's .I authorized_keys or with .I /etc/shells and .I /etc/passwd , it becomes very easy to restrict user's access to a limited set of command. .SH OPTIONS .TP .B \--config \fI\fR Specify config file .TP .B \--log \fI\fR Specify the log directory .TP .B \-- \fI\fR where is *any* config file parameter .TP .B \-h, --help Show help message .TP .B \--version Show version .SH CONFIGURATION You can configure lshell through its configuration file: .RS .ft 3 .nf .sp On Linux \-> /etc/lshell.conf On *BSD \-> /usr/{pkg,local}/etc/lshell.conf .ft .LP .RE .fi \fBlshell\fR configuration has 4 types of sections: .RS .ft 3 .nf .sp [global] -> lshell system configuration (only 1) [default] -> lshell default user configuration (only 1) [foo] -> UNIX username "foo" specific configuration [grp:bar] -> UNIX groupname "bar" specific configuration .ft .LP .RE .fi Order of priority when loading preferences is the following: .RS .ft 3 .nf .sp 1- User configuration 2- Group configuration 3- Default configuration .ft .LP .RE .fi .SS [global] .TP .I logpath config path (default is /var/log/lshell/) .TP .I loglevel 0, 1, 2, 3 or 4 (0: no logs -> 4: logs everything) .TP .I logfilename \- set to \fBsyslog\fR in order to log to syslog .RS \- set log file name, e.g. %u-%y%m%d (i.e foo-20091009.log): .BR \ \ \ \ %u -> username .RE .RS .BR \ \ \ \ %d -> day [1..31] .RE .RS .BR \ \ \ \ %m -> month [1..12] .RE .RS .BR \ \ \ \ %y -> year [00..99] .RE .RS .BR \ \ \ \ %h -> time [00:00..23:59] .RE .TP .I syslogname in case you are using syslog, set your logname (default: lshell) .TP .I include_dir include a directory containing multiple configuration files. These files can only contain default/user/group configuration. The global configuration will only be loaded from the default configuration file. This variable will be expanded (e.g. /path/*.conf). .RS .SS [default] and/or [username] and/or [grp:groupname] .TP .TP .I aliases command aliases list (similar to bash's alias directive) .TP .I allowed a list of the allowed commands or set to 'all' to allow all commands in user's \ PATH .TP .I allowed_cmd_path a list of path; all executable files inside these path will be allowed .TP .I env_path update the environment variable $PATH of the user (optional) .TP .I env_vars set environment variables (optional) .TP .I forbidden a list of forbidden characters or commands .TP .I history_file set the history filename. A wildcard can be used: .RS .BR \ \ \ \ %u -> username (e.g. '/home/%u/.lhistory') .RE .TP .I history_size set the maximum size (in lines) of the history file .TP .I home_path (deprecated) set the home folder of your user. If not specified, the home directory is set \ to the $HOME environment variable. This variable will be removed in the next \ version of lshell, please use your system's tools to set a user's home \ directory. A wildcard can be used: .RS .BR \ \ \ \ %u -> username (e.g. '/home/%u') .RE .TP .I intro set the introduction to print at login .TP .I login_script define the script to run at user login .TP .I passwd password of specific user (default is empty) .TP .I path list of path to restrict the user geographically. It is possible to use \ wildcards (e.g. '/var/log/ap*'). .TP .I prompt set the user's prompt format (default: username) .RS .BR \ \ \ \ %u -> username .RE .RS .BR \ \ \ \ %h -> hostname .RE .TP .I prompt_short set sort prompt current directory update - set to 1 or 0 .I overssh list of command allowed to execute over ssh (e.g. rsync, rdiff-backup, scp, \ etc.) .TP .I scp allow or forbid the use of scp connection - set to 1 or 0 .TP .I scpforce force files sent through scp to a specific directory .TP .I scp_download set to 0 to forbid scp downloads (default is 1) .TP .I scp_upload set to 0 to forbid scp uploads (default is 1) .TP .I sftp allow or forbid the use of sftp connection - set to 1 or 0. WARNING: This option will not work if you are using OpenSSH's \ internal-sftp service (e.g. when configured in chroot) .TP .I sudo_commands a list of the allowed commands that can be used with sudo(8). If set to \ \'all', all the 'allowed' commands will be accessible through sudo(8). It is possible to use the -u sudo flag in order to run a command as a \ different user than the default root. .TP .I timer a value in seconds for the session timer .TP .I strict logging strictness. If set to 1, any unknown command is considered as \ forbidden, and user's warning counter is decreased. If set to 0, command is \ considered as unknown, and user is only warned (i.e. *** unknown synthax) .TP .I warning_counter number of warnings when user enters a forbidden value before getting exited \ from lshell. Set to \fB\-1\fR to disable the counter, and just warn the user. .SH SHELL BUILTIN COMMANDS Here is the set of commands that are always available with lshell: .TP .I clear clears the terminal .TP .I help, ? print the list of allowed commands .TP . I history print the commands history .TP . I lpath lists all allowed and forbidden path .TP . I lsudo lists all sudo allowed commands .SH EXAMPLES .TP .B $ lshell .RS Tries to run lshell using default ${PREFIX}/etc/lshell.conf as configuration \ file. If it fails a warning is printed and lshell is interrupted. lshell options are loaded from the configuration file .RE .TP .B $ lshell --config /path/to/myconf.file --log /path/to/mylog.log .RS This will override the default options specified for configuration and/or log \ file .RE .SH USE CASE The primary goal of lshell, was to be able to create shell accounts \ with ssh access and restrict their environment to a couple a needed \ commands. In this example, User 'foo' and user 'bar' both belong to the 'users' UNIX \ group: .TP .B User foo: .RS - must be able to access /usr and /var but not /usr/local - user all command in his PATH but 'su' - has a warning counter set to 5 - has his home path set to '/home/users' .RE .TP .B User bar: .RS - must be able to access /etc and /usr but not /usr/local - is allowed default commands plus 'ping' minus 'ls' - strictness is set to 1 (meaning he is not allowed to type an unknown command) .RE In this case, my configuration file will look something like this: .RS .ft 3 .nf .sp # CONFIURATION START [global] logpath : /var/log/lshell/ loglevel : 2 [default] allowed : ['ls','pwd'] forbidden : [';', '&', '|'] warning_counter : 2 timer : 0 path : ['/etc', '/usr'] env_path : ':/sbin:/usr/bin/' scp : 1 # or 0 sftp : 1 # or 0 overssh : ['rsync','ls'] aliases : {'ls':'ls \-\-color=auto','ll':'ls \-l'} [grp:users] warning_counter : 5 overssh : - ['ls'] [foo] allowed : 'all' - ['su'] path : ['/var', '/usr'] - ['/usr/local'] home_path : '/home/users' [bar] allowed : + ['ping'] - ['ls'] path : - ['/usr/local'] strict : 1 scpforce : '/home/bar/uploads/' # CONFIURATION END .ft .LP .RE .fi .SH NOTES .TP In order to log a user's warnings into the logging directory (default \ \fI/var/log/lshell/\fR) , you must firt create the folder (if it doesn't \ exist yet) and chown it to lshell group: .RS .ft 3 .nf .sp # addgroup \-\-system lshell # mkdir /var/log/lshell # chown :lshell /var/log/lshell # chmod 770 /var/log/lshell .ft .LP .RE .fi then add the user to the \fIlshell\fR group: .RS .ft 3 .nf .sp # usermod \-aG lshell user_name .ft .LP .RE .fi In order to set lshell as default shell for a user: .RS .ft 3 .nf .sp On Linux: # chsh \-s /usr/bin/lshell user_name On *BSD: # chsh \-s /usr/{pkg,local}/bin/lshell user_name .ft .LP .RE .fi .SH AUTHOR Currently maintained by Ignace Mouzannar (ghantoos) .SH EMAIL Feel free to send me your recommendations at lshell-0.9.17/source0000644000175000017500000000025612563352273014557 0ustar ghantoosghantoos# source me in order to update your python path # and test using the local lshell files # this is useful when you have multiple lshell installations export PYTHONPATH=$PWD/ lshell-0.9.17/COPYING0000644000175000017500000010451312563352273014370 0ustar ghantoosghantoos GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read .