#!/usr/bin/env python3 # *-* coding: utf-8 *-* # This file is part of butterfly # # butterfly Copyright(C) 2015-2017 Florian Mounier # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import tornado.options import tornado.ioloop import tornado.httpserver try: from tornado_systemd import SystemdHTTPServer as HTTPServer except ImportError: from tornado.httpserver import HTTPServer import logging import webbrowser import uuid import ssl import getpass import os import shutil import stat import socket import sys tornado.options.define("debug", default=False, help="Debug mode") tornado.options.define("more", default=False, help="Debug mode with more verbosity") tornado.options.define("unminified", default=False, help="Use the unminified js (for development only)") tornado.options.define("host", default='localhost', help="Server host") tornado.options.define("port", default=57575, type=int, help="Server port") tornado.options.define("keepalive_interval", default=30, type=int, help="Interval between ping packets sent from server " "to client (in seconds)") tornado.options.define("one_shot", default=False, help="Run a one-shot instance. Quit at term close") tornado.options.define("shell", help="Shell to execute at login") tornado.options.define("motd", default='motd', help="Path to the motd file.") tornado.options.define("cmd", help="Command to run instead of shell, f.i.: 'ls -l'") tornado.options.define("unsecure", default=False, help="Don't use ssl not recommended") tornado.options.define("i_hereby_declare_i_dont_want_any_security_whatsoever", default=False, help="Remove all security and warnings. There are some " "use cases for that. Use this if you really know what " "you are doing.") tornado.options.define("login", default=False, help="Use login screen at start") tornado.options.define("pam_profile", default="", type=str, help="When --login=True provided and running as ROOT, " "use PAM with the specified PAM profile for " "authentication and then execute the user's default " "shell. Will override --shell.") tornado.options.define("force_unicode_width", default=False, help="Force all unicode characters to the same width." "Useful for avoiding layout mess.") tornado.options.define("ssl_version", default=None, help="SSL protocol version") tornado.options.define("generate_certs", default=False, help="Generate butterfly certificates") tornado.options.define("generate_current_user_pkcs", default=False, help="Generate current user pfx for client " "authentication") tornado.options.define("generate_user_pkcs", default='', help="Generate user pfx for client authentication " "(Must be root to create for another user)") tornado.options.define("uri_root_path", default='', help="Sets the servier root path: " "example.com//static/") if os.getuid() == 0: ev = os.getenv('XDG_CONFIG_DIRS', '/etc') else: ev = os.getenv( 'XDG_CONFIG_HOME', os.path.join( os.getenv('HOME', os.path.expanduser('~')), '.config')) butterfly_dir = os.path.join(ev, 'butterfly') conf_file = os.path.join(butterfly_dir, 'butterfly.conf') ssl_dir = os.path.join(butterfly_dir, 'ssl') tornado.options.define("conf", default=conf_file, help="Butterfly configuration file. " "Contains the same options as command line.") tornado.options.define("ssl_dir", default=ssl_dir, help="Force SSL directory location") # Do it once to get the conf path tornado.options.parse_command_line() if os.path.exists(tornado.options.options.conf): tornado.options.parse_config_file(tornado.options.options.conf) # Do it again to overwrite conf with args tornado.options.parse_command_line() # For next time, create them a conf file from template. # Need to do this after parsing options so we do not trigger # code import for butterfly module, in case that code is # dependent on the set of parsed options. if not os.path.exists(conf_file): try: import butterfly shutil.copy( os.path.join( os.path.abspath(os.path.dirname(butterfly.__file__)), 'butterfly.conf.default'), conf_file) print('butterfly.conf installed in %s' % conf_file) except: pass options = tornado.options.options for logger in ('tornado.access', 'tornado.application', 'tornado.general', 'butterfly'): level = logging.WARNING if options.debug: level = logging.INFO if options.more: level = logging.DEBUG logging.getLogger(logger).setLevel(level) log = logging.getLogger('butterfly') host = options.host port = options.port if options.i_hereby_declare_i_dont_want_any_security_whatsoever: options.unsecure = True if not os.path.exists(options.ssl_dir): os.makedirs(options.ssl_dir) def to_abs(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', 'butterfly_%s.crt', 'butterfly_%s.key', '%s.p12']) def fill_fields(subject): subject.C = 'WW' subject.O = 'Butterfly' subject.OU = 'Butterfly Terminal' subject.ST = 'World Wide' subject.L = 'Terminal' def write(file, content): with open(file, 'wb') as fd: fd.write(content) print('Writing %s' % file) def read(file): print('Reading %s' % file) with open(file, 'rb') as fd: return fd.read() def b(s): return s.encode('utf-8') if options.generate_certs: from OpenSSL import crypto print('Generating certificates for %s (change it with --host)\n' % host) if not os.path.exists(ca) and not os.path.exists(ca_key): print('Root certificate not found, generating it') ca_pk = crypto.PKey() ca_pk.generate_key(crypto.TYPE_RSA, 2048) ca_cert = crypto.X509() ca_cert.set_version(2) ca_cert.get_subject().CN = 'Butterfly CA on %s' % socket.gethostname() fill_fields(ca_cert.get_subject()) ca_cert.set_serial_number(uuid.uuid4().int) ca_cert.gmtime_adj_notBefore(0) # From now ca_cert.gmtime_adj_notAfter(315360000) # to 10y ca_cert.set_issuer(ca_cert.get_subject()) # Self signed ca_cert.set_pubkey(ca_pk) ca_cert.add_extensions([ crypto.X509Extension( b('basicConstraints'), True, b('CA:TRUE, pathlen:0')), crypto.X509Extension( b('keyUsage'), True, b('keyCertSign, cRLSign')), crypto.X509Extension( b('subjectKeyIdentifier'), False, b('hash'), subject=ca_cert), ]) ca_cert.add_extensions([ crypto.X509Extension( b('authorityKeyIdentifier'), False, b('issuer:always, keyid:always'), issuer=ca_cert, subject=ca_cert ) ]) ca_cert.sign(ca_pk, 'sha512') 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 else: print('Root certificate found, using it') ca_cert = crypto.load_certificate(crypto.FILETYPE_PEM, read(ca)) ca_pk = crypto.load_privatekey(crypto.FILETYPE_PEM, read(ca_key)) server_pk = crypto.PKey() server_pk.generate_key(crypto.TYPE_RSA, 2048) server_cert = crypto.X509() server_cert.set_version(2) server_cert.get_subject().CN = host server_cert.add_extensions([ crypto.X509Extension( b('basicConstraints'), False, b('CA:FALSE')), crypto.X509Extension( b('subjectKeyIdentifier'), False, b('hash'), subject=server_cert), crypto.X509Extension( b('subjectAltName'), False, b('DNS:%s' % host)), ]) server_cert.add_extensions([ crypto.X509Extension( b('authorityKeyIdentifier'), False, b('issuer:always, keyid:always'), issuer=ca_cert, subject=ca_cert ) ]) fill_fields(server_cert.get_subject()) server_cert.set_serial_number(uuid.uuid4().int) server_cert.gmtime_adj_notBefore(0) # From now server_cert.gmtime_adj_notAfter(315360000) # to 10y server_cert.set_issuer(ca_cert.get_subject()) # Signed by ca server_cert.set_pubkey(server_pk) server_cert.sign(ca_pk, 'sha512') write(cert % host, crypto.dump_certificate( crypto.FILETYPE_PEM, server_cert)) write(cert_key % host, crypto.dump_privatekey( crypto.FILETYPE_PEM, server_pk)) os.chmod(cert_key % host, stat.S_IRUSR | stat.S_IWUSR) # 0o600 perms print('\nNow you can run --generate-user-pkcs=user ' 'to generate user certificate.') sys.exit(0) if (options.generate_current_user_pkcs or options.generate_user_pkcs): from butterfly import utils try: current_user = utils.User() except Exception: current_user = None from OpenSSL import crypto if not all(map(os.path.exists, [ca, ca_key])): print('Please generate certificates using --generate-certs before') sys.exit(1) if options.generate_current_user_pkcs: user = current_user.name else: user = options.generate_user_pkcs if user != current_user.name and current_user.uid != 0: print('Cannot create certificate for another user with ' 'current privileges.') sys.exit(1) 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) client_cert = crypto.X509() client_cert.set_version(2) client_cert.get_subject().CN = user fill_fields(client_cert.get_subject()) client_cert.set_serial_number(uuid.uuid4().int) client_cert.gmtime_adj_notBefore(0) # From now client_cert.gmtime_adj_notAfter(315360000) # to 10y client_cert.set_issuer(ca_cert.get_subject()) # Signed by ca client_cert.set_pubkey(client_pk) client_cert.sign(client_pk, 'sha512') client_cert.sign(ca_pk, 'sha512') pfx = crypto.PKCS12() pfx.set_certificate(client_cert) pfx.set_privatekey(client_pk) pfx.set_ca_certificates([ca_cert]) pfx.set_friendlyname(('%s cert for butterfly' % user).encode('utf-8')) 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.') print('') 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 options.unsecure: ssl_opts = None else: if not all(map(os.path.exists, [cert % host, cert_key % host, ca])): print("Unable to find butterfly certificate for host %s" % host) print(cert % host) print(cert_key % host) print(ca) print("Can't run butterfly without certificate.\n") print("Either generate them using --generate-certs --host=host " "or run as --unsecure (NOT RECOMMENDED)\n") print("For more information go to http://paradoxxxzero.github.io/" "2014/03/21/butterfly-with-ssl-auth.html\n") sys.exit(1) ssl_opts = { 'certfile': cert % host, 'keyfile': cert_key % host, 'ca_certs': ca, 'cert_reqs': ssl.CERT_REQUIRED } if options.ssl_version is not None: if not hasattr( ssl, 'PROTOCOL_%s' % options.ssl_version): print( "Unknown SSL protocol %s" % options.ssl_version) sys.exit(1) ssl_opts['ssl_version'] = getattr( ssl, 'PROTOCOL_%s' % options.ssl_version) from butterfly import application application.butterfly_dir = butterfly_dir log.info('Starting server') http_server = HTTPServer(application, ssl_options=ssl_opts) http_server.listen(port, address=host) if getattr(http_server, 'systemd', False): os.environ.pop('LISTEN_PID') os.environ.pop('LISTEN_FDS') log.info('Starting loop') ioloop = tornado.ioloop.IOLoop.instance() if port == 0: port = list(http_server._sockets.values())[0].getsockname()[1] url = "http%s://%s:%d/%s" % ( "s" if not options.unsecure else "", host, port, (options.uri_root_path.strip('/') + '/') if options.uri_root_path else '' ) if not options.one_shot or not webbrowser.open(url): log.warn('Butterfly is ready, open your browser to: %s' % url) ioloop.start()