2019-01-18 14:39:45 -05:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
# Because:
|
|
|
|
# - SSH timeout doesn't work with byobu/screen/tmux
|
|
|
|
# - SSH timeout can be overridden client-side
|
|
|
|
# - $TMOUT can be overridden user-side
|
|
|
|
# we need to actually kill the sshd process attached to the SSH session.
|
2019-01-18 15:12:17 -05:00
|
|
|
# This does have some limitations, though. Namely, it doesn't work for screen/tmux sessions.
|
|
|
|
# specifically if they happen to be using byobu with a status bar that updates, because that process
|
|
|
|
# gathering information for the status bar counts as "activity". Go figure.
|
2019-01-18 14:39:45 -05:00
|
|
|
|
|
|
|
import datetime
|
|
|
|
import os
|
|
|
|
import psutil
|
|
|
|
import subprocess
|
|
|
|
|
|
|
|
# in seconds. 5 minutes = 300 seconds.
|
|
|
|
# if "auto", we'll try checking $TMOUT in the system bashrc and sshd_config, in that order.
|
|
|
|
timeout = 'auto'
|
|
|
|
# only apply to ssh connections instead of ssh + local.
|
|
|
|
# THIS WILL KILL SCREEN/TMUX CONNECTIONS. USE WITH CAUTION.
|
|
|
|
only_ssh = True
|
|
|
|
# send a closing message.
|
|
|
|
goodbye = True
|
|
|
|
# the message to send to the user if goodbye == True.
|
|
|
|
# can use the following for substitution:
|
|
|
|
# pid - The PID if the user's login process.
|
|
|
|
# terminal - The terminal they're logged in on.
|
|
|
|
# loginlength - How long they've been logged in (in minutes).
|
|
|
|
# logintime - When they logged in.
|
|
|
|
# timeout - The allowed length of time for inactivity until a timeout.
|
2019-01-18 16:00:47 -05:00
|
|
|
goodbye_mesg = ('You have been logged in for {loginlength} (since {logintime}) on {terminal} ({pid}).\n'
|
2019-01-18 14:39:45 -05:00
|
|
|
'However, as per security policy, you have exceeded the allowed idle timeout ({timeout}).\n'
|
|
|
|
'As such, your session will now be terminated. Please feel free to reconnect.')
|
|
|
|
# exclude these usernames
|
|
|
|
exclude_users = []
|
|
|
|
|
|
|
|
|
|
|
|
# Get the SSHD PIDs.
|
2019-01-18 15:12:17 -05:00
|
|
|
ssh_pids = [p.pid for p in psutil.process_iter() if p.name() == 'sshd']
|
2019-01-18 14:39:45 -05:00
|
|
|
# If the timeout is set to auto, try to find it.
|
|
|
|
if timeout == 'auto':
|
|
|
|
import re
|
|
|
|
#tmout_re = re.compile('^\s*#*(export\s*)?TMOUT=([0-9]+).*$')
|
|
|
|
tmout_re = re.compile('^\s*(export\s*)?TMOUT=([0-9]+).*$')
|
|
|
|
# We don't bother with factoring in ClientAliveCountMax.
|
|
|
|
# sshd_re = re.compile('^\s*#*ClientAliveCountMax\s+([0-9+]).*$')
|
|
|
|
sshd_re = re.compile('^\s*ClientAliveInterval\s+([0-9+]).*$')
|
|
|
|
for f in ('/etc/bashrc', '/etc/bash.bashrc'):
|
|
|
|
if not os.path.isfile(f):
|
|
|
|
continue
|
|
|
|
with open(f, 'r') as fh:
|
2019-01-18 14:42:37 -05:00
|
|
|
conf = fh.read()
|
2019-01-18 14:39:45 -05:00
|
|
|
for line in conf.splitlines():
|
|
|
|
if tmout_re.search(line):
|
|
|
|
try:
|
|
|
|
timeout = int(tmout_re.sub('\g<2>', line))
|
|
|
|
break
|
|
|
|
except ValueError:
|
|
|
|
continue
|
|
|
|
if not isinstance(timeout, int): # keep going; check sshd_config
|
|
|
|
with open('/etc/ssh/sshd_config', 'r') as f:
|
|
|
|
conf = f.read()
|
|
|
|
for line in conf.splitlines():
|
|
|
|
if sshd_re.search(line):
|
|
|
|
try:
|
|
|
|
timeout = int(tmout_re.sub('\g<1>', line))
|
|
|
|
break
|
|
|
|
except ValueError:
|
|
|
|
continue
|
|
|
|
# Finally, set a default. 5 minutes is sensible.
|
|
|
|
timeout = 300
|
2019-01-18 16:00:47 -05:00
|
|
|
pretty_timeout = datetime.timedelta(seconds = timeout)
|
2019-01-18 14:39:45 -05:00
|
|
|
|
|
|
|
def get_idle(user):
|
|
|
|
idle_time = None
|
2019-01-18 14:50:53 -05:00
|
|
|
try:
|
2019-01-18 14:39:45 -05:00
|
|
|
# https://unix.stackexchange.com/a/332704/284004
|
|
|
|
last_used = datetime.datetime.fromtimestamp(os.stat('/dev/{0}'.format(user.terminal)).st_atime)
|
|
|
|
idle_time = datetime.datetime.utcnow() - last_used
|
2019-01-18 14:50:53 -05:00
|
|
|
except FileNotFoundError:
|
|
|
|
# It's probably a graphical login (e.g. gnome uses ::1) - you're on your own.
|
|
|
|
pass
|
2019-01-18 14:39:45 -05:00
|
|
|
return(idle_time)
|
|
|
|
|
|
|
|
|
|
|
|
for user in psutil.users():
|
|
|
|
if user.name in exclude_users:
|
|
|
|
continue
|
2019-01-18 18:53:49 -05:00
|
|
|
try:
|
|
|
|
login_pid = user.pid
|
|
|
|
except AttributeError:
|
|
|
|
continue # Doesn't have a PID
|
2019-01-18 14:39:45 -05:00
|
|
|
login_length = (datetime.datetime.utcnow() - datetime.datetime.fromtimestamp(user.started))
|
|
|
|
if login_length.total_seconds() < timeout:
|
|
|
|
continue # they haven't even been logged in for long enough yet.
|
|
|
|
idle_time = get_idle(user)
|
2019-01-18 15:56:13 -05:00
|
|
|
parent_pid = psutil.Process(user.pid).ppid()
|
2019-01-18 14:50:53 -05:00
|
|
|
try:
|
|
|
|
diff = idle_time.total_seconds() >= timeout
|
|
|
|
except AttributeError:
|
|
|
|
# Something went wrong when getting idle_time. probably a graphical desktop login.
|
|
|
|
diff = False
|
|
|
|
if diff:
|
2019-01-18 14:45:46 -05:00
|
|
|
fmt_vals = {'pid': user.pid,
|
|
|
|
'terminal': user.terminal,
|
|
|
|
'loginlength': login_length,
|
|
|
|
'logintime': datetime.datetime.fromtimestamp(user.started),
|
2019-01-18 16:00:47 -05:00
|
|
|
'timeout': pretty_timeout}
|
2019-01-18 14:45:46 -05:00
|
|
|
fmtd_goodbye = goodbye_mesg.format(**fmt_vals)
|
2019-01-18 14:39:45 -05:00
|
|
|
if only_ssh:
|
2019-01-18 15:56:13 -05:00
|
|
|
if parent_pid in ssh_pids:
|
2019-01-18 14:39:45 -05:00
|
|
|
if goodbye:
|
|
|
|
subprocess.run(['write',
|
|
|
|
user.name,
|
|
|
|
user.terminal],
|
|
|
|
input = fmtd_goodbye.encode('utf-8'))
|
2019-01-18 15:56:13 -05:00
|
|
|
psutil.Process(parent_pid).terminate()
|
2019-01-18 14:39:45 -05:00
|
|
|
else:
|
|
|
|
if goodbye:
|
|
|
|
subprocess.run(['write',
|
|
|
|
user.name,
|
|
|
|
user.terminal],
|
|
|
|
input = fmtd_goodbye.encode('utf-8'))
|
2019-01-18 15:56:13 -05:00
|
|
|
psutil.Process(parent_pid).terminate()
|