mirror of
https://github.com/paradoxxxzero/butterfly.git
synced 2026-05-26 07:08:08 +00:00
Session security
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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, []))
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 '
|
||||
|
||||
Reference in New Issue
Block a user