Newer
Older
This module persistently stores informion on tunnels in an in memory structure.
"""
import datetime
import yaml
class SshAgentException(Exception):
pass
class SSHSession:
"""Interfaces for working with processes forked from flask
in particular, we fork processes for ssh-agent and ssh tunnels and execution
Chris Hines
committed
def __init__(self,sshsessid,**kwargs):
self.pids = []
self.authtok = None
self.__dict__.update(kwargs)
Chris Hines
committed
self.sshagent = 'ssh-agent'
Chris Hines
committed
self.sshkeygen = 'ssh-keygen'
Chris Hines
committed
self.sshsessid = sshsessid
def start_agent(self):
import subprocess
from .. import app
import logging
import os
logger = logging.getLogger()
if app.config['ENABLELAUNCH'] and 'SSH_AUTH_SOCK' in os.environ and os.environ['SSH_AUTH_SOCK']:
self.socket = os.environ['SSH_AUTH_SOCK']
return
Chris Hines
committed
p = subprocess.Popen([self.sshagent],start_new_session=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
(stdout,stderr) = p.communicate()
for l in stdout.decode().split(';'):
if 'SSH_AUTH_SOCK=' in l:
socket = l.split('=')[1]
self.socket = socket
if 'SSH_AGENT_PID=' in l:
pid = l.split('=')[1]
self.pids.append(pid)
def get_certs(self):
env = os.environ.copy()
env['SSH_AUTH_SOCK'] = self.socket
cmd = [self.sshadd,'-L']
p = subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE, env=env)
(stdout,stderr) = p.communicate()
return stdout
def add_keycert(self,key,cert):
import tempfile
import os
import subprocess
import logging
logger = logging.getLogger()
try:
if self.socket is None:
self.start_agent()
except Exception as e:
self.lock.release()
raise e
keyf = tempfile.NamedTemporaryFile(mode='w',delete=False)
keyname = keyf.name
keyf.write(key)
keyf.close()
certf = open(keyname+'-cert.pub',mode='w')
certf.write(cert)
certf.close()
p = subprocess.Popen([self.sshkeygen,'-L','-f','-'],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
logger.debug('inspecting keycert pid {}'.format(p.pid))
# Examine the cert to determine its expiry. Use the -t flag to automatically remove from the ssh-agent when the cert expires
certcontents = SSHSession.parse_cert_contents(keygenout.decode().splitlines())
endtime = datetime.datetime.strptime(certcontents['Valid'][0].split()[3],"%Y-%m-%dT%H:%M:%S")
delta = endtime - datetime.datetime.now() # I *think* the output of ssh-keygen -L is in the current timezone even though I assume the certs validity is in UTC
env = os.environ.copy()
env['SSH_AUTH_SOCK'] = self.socket
cmd = [self.sshadd,'-t',"{}".format(int(delta.total_seconds()))]
if int(delta.total_seconds()) > 0:
p = subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE,env=env)
logger.debug('adding keycert pid {}'.format(p.pid))
(stdout,stderr) = p.communicate()
if p.returncode != 0:
logger.error("Couldn't add key and cert")
logger.error(stdout)
logger.error(stderr)
raise SshAgentException()
os.unlink(keyname+'-cert.pub')
os.unlink(keyname)
def rm_fp(self,fp):
cmd = [self.sshadd,'-L']
p = subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE,env=env)
(stdout,stderr) = p.communicate()
for l in stdout.splitlines():
p = subprocess.Popen([self.sshkeygen,'-l','-f','-'],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
keygenout,keygenerr = p.communicate(l)
if fp in keygenout:
self.rm_pub(self,l)
def rm_pub(self,pub):
import tempfile
pubf = tempfile.NamedTemporaryFile(mode='w',delete=False)
pubname = pubf.name
pubf.write(pub)
pubf.close()
cmd=[self.sshadd,'-d',pubname]
p = subprocess.Popen(cmd,stdin=None,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
(stderr,stdout) = p.communicate()
os.unlink(pubname)
Chris Hines
committed
def get_cert_contents(self):
import os
import subprocess
import logging
logger=logging.getLogger()
Chris Hines
committed
res=[]
Chris Hines
committed
env = os.environ.copy()
env['SSH_AUTH_SOCK'] = self.socket
cmd = [self.sshadd,'-L']
p = subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE,env=env)
(stdout,stderr) = p.communicate()
for l in stdout.splitlines():
if b'cert' in l:
p = subprocess.Popen([self.sshkeygen,'-L','-f','-'],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
keygenout,keygenerr = p.communicate(l)
certcontents = SSHSession.parse_cert_contents(keygenout.decode().splitlines())
res.append(certcontents)
Chris Hines
committed
return res
@staticmethod
def parse_cert_contents(lines):
key = None
values = []
res = {}
for l in lines:
l = l.rstrip().lstrip()
Chris Hines
committed
if ':' in l:
if key is not None:
res[key] = values
values = []
v = v.lstrip().rstrip()
Chris Hines
committed
values = [v]
else:
Chris Hines
committed
return res
def addkey(self,key,cert):
pass
def kill(self):
import os
import signal
logger=logging.getLogger()
logger.debug("shuting down ssh session for {} last seen at {}".format(self.authtok,self.last))
logger.debug("killing pid {}".format(pid))
Chris Hines
committed
os.killpg(os.getpgid(int(pid)), signal.SIGTERM) # Sometimes this fails and I don't know why
try:
os.kill(int(pid), 0) # If the first kill worked, this will raise a ProcessLookupError
time.sleep(2)
Chris Hines
committed
os.killpg(os.getpgid(int(pid)),signal.SIGKILL)
logger.error('resorting to sigkill for pid {}'.format(pid))
except ProcessLookupError:
Chris Hines
committed
logger.debug(f'sigterm succesfull for {pid}')
logger.debug("killed {}".format(pid))
for tunnel in self.tunnels:
tunnel.kill()
(stdout, stderr) = tunnel.communicate()
@staticmethod
def test_sshsession(sess):
import os
import subprocess
import logging
logger=logging.getLogger()
env = os.environ.copy()
try:
if sess.socket is None:
sess.start_agent()
except Exception as e:
sess.lock.release()
raise e
env['SSH_AUTH_SOCK'] = sess.socket
cmd = [sess.sshadd,'-l']
p = subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE,env=env)
(stdout,stderr) = p.communicate()
if p.returncode != 0:
"""
A non-zero return code can occur if the agent is running
but there are no keys loaded
This is actually not an error condition
"""
if b'The agent has no identities' in stdout:
return
logger.error("Couldn't communicate with the ssh agent")
logger.error(stdout)
logger.error(stderr)
raise SshAgentException()
def get_sshsession():
import random
import string
from .. import sshsessions
Chris Hines
committed
import uwsgi
uwsgi.lock()
sshsessid = session.get('sshsessid', None)
N = 8
while sshsessid is None:
key = ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(N))
if key not in session:
sshsessid = key
session['sshsessid'] = sshsessid
if sshsessid not in sshsessions:
logger.debug('creating a new SSH session ID {}'.format(sshsessid))
Chris Hines
committed
sshsessions[sshsessid] = SSHSession(sshsessid)
except SshAgentException:
logger = logging.getLogger()
logger.error('SSHAgentException, killing the agent and restarted. This is highly unexpected')
Chris Hines
committed
sshsessions[sshsessid] = SSHSession(sshsessid)
sshsession = sshsessions[sshsessid]
SSHSession.test_sshsession(sshsession)
Chris Hines
committed
uwsgi.unlock()
@staticmethod
def remove_sshsession():
import random
import string
from .. import sshsessions
from flask import session
sshsessid = session.get('sshsessid', None)
del sshsessions[sshsessid]