diff --git a/butterfly.server.py b/butterfly.server.py index 22c2657..211f7d6 100644 --- a/butterfly.server.py +++ b/butterfly.server.py @@ -22,13 +22,11 @@ import tornado.ioloop import tornado.httpserver import uuid import ssl +import getpass import os +import stat import sys -try: - input = raw_input -except NameError: - pass tornado.options.define("debug", default=False, help="Debug mode") tornado.options.define("more", default=False, @@ -60,18 +58,46 @@ for logger in ('tornado.access', 'tornado.application', log = logging.getLogger('butterfly') log.info('Starting server') ioloop = tornado.ioloop.IOLoop.instance() -ca, ca_key = 'butterfly_ca.crt', 'butterfly_ca.key' -cert, cert_key = 'butterfly.crt', 'butterfly.key' + +ssl_dir = os.path.join( + os.path.dirname(os.path.realpath(__file__)), 'ssl') + +if not os.path.exists(ssl_dir): + os.mkdir(ssl_dir) + + +def to_abs(file): + return os.path.join(ssl_dir, file) + +ca, ca_key, cert, cert_key, pkcs12 = map(to_abs, [ + 'butterfly_ca.crt', 'butterfly_ca.key', + 'butterfly.crt', 'butterfly.key', + '%s.p12']) + + +def write(file, content): + with open(file, 'wb') as fd: + fd.write(content) + print('Written %s' % file) + + +def read(file): + print('Reading %s' % file) + with open(file, 'rb') as fd: + return fd.read() from butterfly import application if tornado.options.options.generate_certs: + host = tornado.options.options.host + print('Generating certificates for %s (change it with --host)\n' % host) + from OpenSSL import crypto ca_pk = crypto.PKey() ca_pk.generate_key(crypto.TYPE_RSA, 2048) ca_cert = crypto.X509() - ca_cert.get_subject().CN = 'butterfly ca' + ca_cert.get_subject().CN = 'Butterfly CA' ca_cert.set_serial_number(100) ca_cert.gmtime_adj_notBefore(0) # From now ca_cert.gmtime_adj_notAfter(315360000) # to 10y @@ -79,17 +105,14 @@ if tornado.options.options.generate_certs: ca_cert.set_pubkey(ca_pk) ca_cert.sign(ca_pk, 'sha1') - with open(ca, "wb") as cf: - cf.write( - crypto.dump_certificate(crypto.FILETYPE_PEM, ca_cert)) - with open(ca_key, "wb") as cf: - cf.write( - crypto.dump_privatekey(crypto.FILETYPE_PEM, ca_pk)) + write(ca, crypto.dump_certificate(crypto.FILETYPE_PEM, ca_cert)) + write(ca_key, crypto.dump_privatekey(crypto.FILETYPE_PEM, ca_pk)) + os.chmod(ca_key, stat.S_IRUSR | stat.S_IWUSR) # 0o600 perms server_pk = crypto.PKey() server_pk.generate_key(crypto.TYPE_RSA, 2048) server_cert = crypto.X509() - server_cert.get_subject().CN = tornado.options.options.host + server_cert.get_subject().CN = host server_cert.set_serial_number(200) server_cert.gmtime_adj_notBefore(0) # From now server_cert.gmtime_adj_notAfter(315360000) # to 10y @@ -97,12 +120,12 @@ if tornado.options.options.generate_certs: server_cert.set_pubkey(server_pk) server_cert.sign(ca_pk, 'sha1') - with open(cert, "wb") as cf: - cf.write(crypto.dump_certificate(crypto.FILETYPE_PEM, server_cert)) + write(cert, crypto.dump_certificate(crypto.FILETYPE_PEM, server_cert)) + write(cert_key, crypto.dump_privatekey(crypto.FILETYPE_PEM, server_pk)) + os.chmod(cert_key, stat.S_IRUSR | stat.S_IWUSR) # 0o600 perms - with open(cert_key, "wb") as cf: - cf.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, server_pk)) - print('Done') + print('\nNow you can run --generate_user_pkcs=user ' + 'to generate user certificate.') sys.exit(0) @@ -114,10 +137,8 @@ if tornado.options.options.generate_user_pkcs: sys.exit(1) user = tornado.options.options.generate_user_pkcs - with open(ca, 'rb') as cf: - ca_cert = crypto.load_certificate(crypto.FILETYPE_PEM, cf.read()) - with open(ca_key, 'rb') as cf: - ca_pk = crypto.load_privatekey(crypto.FILETYPE_PEM, cf.read()) + ca_cert = crypto.load_certificate(crypto.FILETYPE_PEM, read(ca)) + ca_pk = crypto.load_privatekey(crypto.FILETYPE_PEM, read(ca_key)) client_pk = crypto.PKey() client_pk.generate_key(crypto.TYPE_RSA, 2048) @@ -138,23 +159,29 @@ if tornado.options.options.generate_user_pkcs: pfx.set_ca_certificates([ca_cert]) pfx.set_friendlyname(('%s cert for butterfly' % user).encode('utf-8')) - with open('%s.p12' % user, "wb") as cf: - cf.write(pfx.export(b'')) - print('%s.p12 written.' % user) + while True: + password = getpass.getpass('\nPKCS12 Password (can be blank): ') + password2 = getpass.getpass('Verify Password (can be blank): ') + if password == password2: + break + print('Passwords do not match.') + + write(pkcs12 % user, pfx.export(password.encode('utf-8'))) + os.chmod(pkcs12 % user, stat.S_IRUSR | stat.S_IWUSR) # 0o600 perms sys.exit(0) -if (tornado.options.options.unsecure or - tornado.options.options.host == '127.0.0.1'): +if tornado.options.options.unsecure: ssl_opts = None else: if not all(map(os.path.exists, [cert, cert_key, ca, ca_key])): - print("Unable to find butterfly certificate. " - "Can't run butterfly without certificate. " - "Either generate them or run as --unsecure " - "(NOT RECOMMENDED)") - sys.exit(1) + print("Unable to find butterfly certificate. " + "Can't run butterfly without certificate. " + "Either generate them using --generate-certs " + "or run as --unsecure " + "(NOT RECOMMENDED)") + sys.exit(1) ssl_opts = { 'certfile': cert, diff --git a/butterfly/routes.py b/butterfly/routes.py index eea6c5a..32ffce9 100644 --- a/butterfly/routes.py +++ b/butterfly/routes.py @@ -22,6 +22,7 @@ import io import struct import fcntl import termios +import tornado.web import tornado.websocket import tornado.process import tornado.ioloop @@ -71,8 +72,9 @@ R Y Y AFrom:R ! ! G%sX ''' + .replace('G', '\x1b[3%d;1m' % ( + 1 if tornado.options.options.unsecure else 2)) .replace('B', '\x1b[34;1m') - .replace('G', '\x1b[32;1m') .replace('R', '\x1b[37;1m') .replace('Z', '\x1b[33;1m') .replace('A', '\x1b[37;0m') @@ -86,6 +88,8 @@ R Y Y AFrom:R @url(r'/(?:user/(.+))?/?(?:wd/(.+))?') class Index(Route): def get(self, user, path): + if not tornado.options.options.unsecure and user: + raise tornado.web.HTTPError(400) return self.render('index.html') @@ -104,20 +108,26 @@ class TermWebSocket(Route, tornado.websocket.WebSocketHandler): self.communicate() def shell(self): - if self.callee is None: + if self.callee is None and tornado.options.options.unsecure: user = input('login: ') try: self.callee = utils.User(name=user) except: self.callee = utils.User(name='nobody') + assert self.callee is not None + try: os.chdir(self.path or self.callee.dir) except: pass env = os.environ - env.update(self.socket.env) + # If local and local user is the same as login user + # We set the env of the user from the browser + # Usefull when running as root + if self.caller == self.callee: + env.update(self.socket.env) env["TERM"] = "xterm-256color" env["COLORTERM"] = "butterfly" env["HOME"] = self.callee.dir @@ -127,23 +137,31 @@ class TermWebSocket(Route, tornado.websocket.WebSocketHandler): env["PATH"] = '%s:%s' % (os.path.abspath(os.path.join( os.path.dirname(__file__), '..', 'bin')), env.get("PATH")) - if self.socket.local: - # All users are the same -> launch shell - if self.caller == self.callee and server == self.callee: - args = [tornado.options.options.shell or self.callee.shell] - args.append('-i') - os.execvpe(args[0], args, env) - # This process has been replaced - return + if not tornado.options.options.unsecure or ( + self.socket.local and + self.caller == self.callee and + server == self.callee): + # User has been auth with ssl or is the same user as server + try: + os.setuid(self.callee.uid) + except PermissionError: + print('The server must be run as root ' + 'if you want to log as different user\n') + sys.exit(1) + args = [tornado.options.options.shell or self.callee.shell] + args.append('-i') + os.execvpe(args[0], args, env) + # This process has been replaced - if server.root: + # Unsecure connection with su + if server.root: + if self.socket.local: if self.callee != self.caller: # Force password prompt by dropping rights # to the daemon user os.setuid(daemon.uid) - else: - # We are not local so we should always get a password prompt - if server.root: + else: + # We are not local so we should always get a password prompt if self.callee == daemon: # No logging from daemon sys.exit(1) @@ -201,28 +219,33 @@ class TermWebSocket(Route, tornado.websocket.WebSocketHandler): self.path = path self.user = user.decode('utf-8') if user else None self.caller = self.callee = None - if not tornado.options.options.unsecure: - cert = self.request.get_ssl_certificate() - if cert is not None: - for field in cert['subject']: - if field[0][0] == 'commonName': - self.user = self.callee = field[0][1] # If local we have the user connecting if self.socket.local and self.socket.user is not None: self.caller = self.socket.user - if self.user: + if tornado.options.options.unsecure: + if self.user: + try: + self.callee = utils.User(name=self.user) + except LookupError: + self.callee = None + + # If no user where given and we are local, keep the same user + # as the one who opened the socket + # ie: the one openning a terminal in borwser + if not self.callee and not self.user and self.socket.local: + self.callee = self.caller + else: + issuer, user = utils.parse_cert(self.request.get_ssl_certificate()) + assert issuer == 'Butterfly CA', 'Invalid certificate issuer' + assert user, 'No user in certificate' + self.user = user try: self.callee = utils.User(name=self.user) except LookupError: - self.callee = None + raise Exception('Invalid user in certificate') - # If no user where given and we are local, keep the same user - # as the one who opened the socket - # ie: the one openning a terminal in borwser - if not self.callee and not self.user and self.socket.local: - self.callee = self.caller self.write_message(motd(self.socket)) self.pty() diff --git a/butterfly/utils.py b/butterfly/utils.py index 29c8ef6..44f6e12 100644 --- a/butterfly/utils.py +++ b/butterfly/utils.py @@ -25,6 +25,23 @@ import re log = getLogger('butterfly') +def parse_cert(cert): + issuer = None + user = None + + for elt in cert['issuer']: + issuer = dict(elt).get('commonName', None) + if issuer: + break + + for elt in cert['subject']: + user = dict(elt).get('commonName', None) + if user: + break + + return issuer, user + + class User(object): def __init__(self, uid=None, name=None): if uid is None and not name: