Session security

This commit is contained in:
Florian Mounier
2015-10-02 17:24:25 +02:00
parent 5aa697381a
commit 6c5cbeaca5
4 changed files with 137 additions and 77 deletions

View File

@@ -65,46 +65,47 @@ tornado.options.define("theme", default=None,
if os.getuid() == 0:
conf_file = os.path.join(
os.path.abspath(os.sep), 'etc', 'butterfly', 'butterfly.conf')
ssl_dir = os.path.join(os.path.abspath(os.sep), 'etc', 'butterfly', 'ssl')
else:
conf_file = os.path.join(
os.path.expanduser('~'), '.butterfly', 'butterfly.conf')
ssl_dir = os.path.join(os.path.expanduser('~'), '.butterfly', 'ssl')
tornado.options.define("conf", default=conf_file,
help="Butterfly configuration file. "
"Contains the same options as command line.")
if os.path.exists(conf_file):
tornado.options.parse_config_file(conf_file)
tornado.options.define("ssl_dir", default=ssl_dir,
help="Force SSL directory location")
tornado.options.parse_command_line()
if os.path.exists(tornado.options.options.conf):
tornado.options.parse_config_file(tornado.options.options.conf)
options = tornado.options.options
for logger in ('tornado.access', 'tornado.application',
'tornado.general', 'butterfly'):
level = logging.WARNING
if tornado.options.options.debug:
if options.debug:
level = logging.INFO
if tornado.options.options.more:
if options.more:
level = logging.DEBUG
logging.getLogger(logger).setLevel(level)
log = logging.getLogger('butterfly')
log.info('Starting server')
host = tornado.options.options.host
port = tornado.options.options.port
host = options.host
port = options.port
if os.getuid() == 0:
ssl_dir = os.path.join(os.path.abspath(os.sep), 'etc', 'butterfly', 'ssl')
else:
ssl_dir = os.path.join(os.path.expanduser('~'), '.butterfly', 'ssl')
if not os.path.exists(ssl_dir):
os.makedirs(ssl_dir)
if not os.path.exists(options.ssl_dir):
os.makedirs(options.ssl_dir)
def to_abs(file):
return os.path.join(ssl_dir, file)
return os.path.join(options.ssl_dir, file)
ca, ca_key, cert, cert_key, pkcs12 = map(to_abs, [
'butterfly_ca.crt', 'butterfly_ca.key',
@@ -131,7 +132,7 @@ def read(file):
with open(file, 'rb') as fd:
return fd.read()
if tornado.options.options.generate_certs:
if options.generate_certs:
from OpenSSL import crypto
print('Generating certificates for %s (change it with --host)\n' % host)
@@ -180,8 +181,8 @@ if tornado.options.options.generate_certs:
sys.exit(0)
if (tornado.options.options.generate_current_user_pkcs or
tornado.options.options.generate_user_pkcs):
if (options.generate_current_user_pkcs or
options.generate_user_pkcs):
from butterfly import utils
try:
current_user = utils.User()
@@ -193,10 +194,10 @@ if (tornado.options.options.generate_current_user_pkcs or
print('Please generate certificates using --generate-certs before')
sys.exit(1)
if tornado.options.options.generate_current_user_pkcs:
if options.generate_current_user_pkcs:
user = current_user.name
else:
user = tornado.options.options.generate_user_pkcs
user = options.generate_user_pkcs
if user != current_user.name and current_user.uid != 0:
print('Cannot create certificate for another user with '
@@ -239,7 +240,7 @@ if (tornado.options.options.generate_current_user_pkcs or
sys.exit(0)
if tornado.options.options.unsecure:
if options.unsecure:
ssl_opts = None
else:
if not all(map(os.path.exists, [cert % host, cert_key % host, ca])):
@@ -260,15 +261,15 @@ else:
'ca_certs': ca,
'cert_reqs': ssl.CERT_REQUIRED
}
if tornado.options.options.ssl_version is not None:
if options.ssl_version is not None:
if not hasattr(
ssl, 'PROTOCOL_%s' % tornado.options.options.ssl_version):
ssl, 'PROTOCOL_%s' % options.ssl_version):
print(
"Unknown SSL protocol %s" %
tornado.options.options.ssl_version)
options.ssl_version)
sys.exit(1)
ssl_opts['ssl_version'] = getattr(
ssl, 'PROTOCOL_%s' % tornado.options.options.ssl_version)
ssl, 'PROTOCOL_%s' % options.ssl_version)
from butterfly import application
@@ -285,6 +286,6 @@ log.info('Starting loop')
ioloop = tornado.ioloop.IOLoop.instance()
url = "http%s://%s:%d/" % (
"s" if not tornado.options.options.unsecure else "", host, port)
"s" if not options.unsecure else "", host, port)
log.warn('Butterfly is ready, open your browser to: %s' % url)
ioloop.start()

View File

@@ -22,6 +22,7 @@ import tornado.options
import tornado.process
import tornado.web
import tornado.websocket
from collections import defaultdict
from butterfly import url, Route, utils, __version__
from butterfly.terminal import Terminal
@@ -98,11 +99,13 @@ class Font(Route):
'(?:/wd/(?P<path>.+))?')
class TermWebSocket(Route, tornado.websocket.WebSocketHandler):
session_history_size = 10000
# List of websockets per session
sessions = {}
# List of websockets per session per user
# dict: user -> dict: session -> [TermWebSocket]
sessions = defaultdict(dict)
# Terminal for session
terminals = {}
# Terminal for session per user
# dict: user -> dict: session -> Terminal
terminals = defaultdict(dict)
# All terminals sockets for systemd socket deactivation
sockets = []
@@ -113,6 +116,7 @@ class TermWebSocket(Route, tornado.websocket.WebSocketHandler):
def open(self, user, path, session):
self.session = session
self.closed = False
self.secure_user = None
# Prevent cross domain
if self.request.headers['Origin'] not in (
@@ -130,46 +134,93 @@ class TermWebSocket(Route, tornado.websocket.WebSocketHandler):
self.log.info('Websocket opened %r' % self)
self.set_nodelay(True)
socket = utils.Socket(self.ws_connection.stream.socket)
opts = tornado.options.options
if not opts.unsecure:
user = utils.parse_cert(
self.ws_connection.stream.socket.getpeercert())
assert user, 'No user in certificate'
try:
user = utils.User(name=user)
except LookupError:
raise Exception('Invalid user in certificate')
# Certificate authed user
self.secure_user = user
elif socket.local and socket.user == utils.User():
# Local to local returning browser user
self.secure_user = socket.user
# Handling terminal session
if session:
if session in TermWebSocket.sessions:
if session in self.user_sessions:
# Session already here, registering websocket
TermWebSocket.sessions[session].append(self)
self.user_sessions[session].append(self)
self.write_message(TermWebSocket.history[session])
# And returning, we don't want another terminal
return
else:
# New session, opening terminal
TermWebSocket.sessions[session] = [self]
self.user_sessions[session] = [self]
TermWebSocket.history[session] = ''
terminal = Terminal(
user, path, session,
self.ws_connection.stream.socket,
self.request.headers['Host'],
self.render_string,
self.write)
user, path, session, socket,
self.request.headers['Host'], self.render_string, self.write)
terminal.pty()
if session:
TermWebSocket.terminals[session] = terminal
if not self.secure_user:
self.log.error(
'No terminal session without secure authenticated user'
'or local user.')
self._terminal = terminal
self.session = None
else:
self.log.info('Openning session %s for secure user %r' % (
session, self.secure_user))
self.user_terminals[session] = terminal
else:
self._terminal = terminal
@classmethod
def close_all(cls, session):
for inst in TermWebSocket.sessions[session][:]:
inst.on_close()
inst.close()
del TermWebSocket.sessions[session]
del TermWebSocket.terminals[session]
@property
def user_sessions(self):
"""Return the dict session of socket lists"""
if not self.secure_user:
return {}
return TermWebSocket.sessions[self.secure_user.name]
@property
def user_terminals(self):
"""Return the dict session of terminal"""
if not self.secure_user:
return {}
return TermWebSocket.terminals[self.secure_user.name]
@classmethod
def broadcast(cls, session, message):
def close_all(cls, session, user):
sessions = TermWebSocket.sessions.get(user.name)
if sessions:
sockets = sessions[session]
for socket in sockets[:]:
socket.on_close()
socket.close()
del sessions[session]
terminals = TermWebSocket.terminals.get(user.name)
del terminals[session]
@classmethod
def broadcast(cls, session, message, user):
cls.history[session] += message
if len(cls.history) > cls.session_history_size:
cls.history[session] = cls.history[session][
-cls.session_history_size:]
for session in cls.sessions[session][:]:
sessions = cls.sessions.get(user.name, [])
for session in sessions[session]:
try:
session.write_message(message)
except Exception:
@@ -178,9 +229,10 @@ class TermWebSocket(Route, tornado.websocket.WebSocketHandler):
def write(self, message):
if self.session:
if message is None:
TermWebSocket.close_all(self.session)
TermWebSocket.close_all(self.session, self.secure_user)
else:
TermWebSocket.broadcast(self.session, message)
TermWebSocket.broadcast(
self.session, message, self.secure_user)
else:
if message is None:
self.on_close()
@@ -189,8 +241,8 @@ class TermWebSocket(Route, tornado.websocket.WebSocketHandler):
self.write_message(message)
def on_message(self, message):
if self.session:
term = TermWebSocket.terminals.get(self.session)
if self.session and self.secure_user:
term = self.user_terminals.get(self.session)
term and term.write(message)
else:
self._terminal.write(message)
@@ -202,7 +254,7 @@ class TermWebSocket(Route, tornado.websocket.WebSocketHandler):
self.log.info('Websocket closed %r' % self)
TermWebSocket.sockets.remove(self)
if self.session:
TermWebSocket.sessions[self.session].remove(self)
self.user_sessions[self.session].remove(self)
elif hasattr(self, '_terminal'):
self._terminal.close()
else:
@@ -212,10 +264,18 @@ class TermWebSocket(Route, tornado.websocket.WebSocketHandler):
sys.exit(0)
@url(r'/list(?:user/(.+))?/?(?:wd/(.+))?')
class List(Route):
"""List available terminals"""
def get(self, user, path):
if not tornado.options.options.unsecure and user:
raise tornado.web.HTTPError(400)
return self.render('list.html', sessions=TermWebSocket.sessions)
@url(r'/sessions')
class Sessions(Route):
"""List available sessions"""
def get(self):
if tornado.options.options.unsecure:
raise tornado.web.HTTPError(403)
cert = self.request.get_ssl_certificate()
user = utils.parse_cert(cert)
if not user:
raise tornado.web.HTTPError(403)
return self.render(
'list.html', sessions=TermWebSocket.sessions.get(user, []))

View File

@@ -10,14 +10,14 @@
<link rel="shortcut icon" href="{{ static_url('images/favicon.png') }}">
<title>Butterfly</title>
{# <link href="/style.css" rel="stylesheet"> #}
<link href="/style.css" rel="stylesheet">
</head>
<body>
<p>Currently open butterfly sessions :
<h1>Currently open butterfly sessions :</h1>
<ul>
{% for session in sessions %}
<li> <a target="_blank" href="/session/{{ session }}">{{ session }}</a></li>
<li><h2><a target="_blank" href="/session/{{ session }}">{{ session }}</a></h2></li>
{% end %}
</ul>
</body>

View File

@@ -53,7 +53,7 @@ class Terminal(object):
self.send = send
self.fd = None
self.closed = False
self.socket = utils.Socket(socket)
self.socket = socket
log.info('Terminal opening with session: %s and socket %r' % (
self.session, self.socket))
self.path = path
@@ -77,15 +77,10 @@ class Terminal(object):
# user as the one who opened the socket ie: the one
# openning a terminal in browser
if not self.callee and not self.user and self.socket.local:
self.callee = self.caller
self.user = self.callee = self.caller
else:
user = utils.parse_cert(socket.getpeercert())
assert user, 'No user in certificate'
self.user = user
try:
self.callee = utils.User(name=self.user)
except LookupError:
raise Exception('Invalid user in certificate')
# Authed user
self.callee = self.user
if tornado.options.options.motd != '':
motd = (render_string(
@@ -99,7 +94,7 @@ class Terminal(object):
.replace('\n', '\r\n'))
self.send(motd)
self.pty()
log.info('Forking pty for user %r' % self.user)
def pty(self):
# Make a "unique" id in 4 bytes
@@ -112,6 +107,8 @@ class Terminal(object):
self.pid, self.fd = pty.fork()
if self.pid == 0:
self.determine_user()
log.debug('Pty forked for user %r caller %r callee %r' % (
self.user, self.caller, self.callee))
self.shell()
else:
self.communicate()
@@ -201,6 +198,8 @@ class Terminal(object):
os.initgroups(self.callee.name, self.callee.gid)
os.setgid(self.callee.gid)
os.setuid(self.callee.uid)
# Apparently necessary for some cmd
env['LOGNAME'] = env['USER'] = self.callee.name
except Exception:
log.error(
'The server must be run as root '