1#!/usr/bin/env python 2# *-* coding: utf-8 *-* 3 4# This file is part of butterfly 5# 6# butterfly Copyright (C) 2015 Florian Mounier 7# This program is free software: you can redistribute it and/or modify 8# it under the terms of the GNU General Public License as published by 9# the Free Software Foundation, either version 3 of the License, or 10# (at your option) any later version. 11# 12# This program is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15# GNU General Public License for more details. 16# 17# You should have received a copy of the GNU General Public License 18# along with this program. If not, see <http://www.gnu.org/licenses/>. 19 20import tornado.options 21import tornado.ioloop 22import tornado.httpserver 23#import tornado_systemd 24import logging 25import webbrowser 26import uuid 27import ssl 28import getpass 29import os 30import shutil 31import stat 32import socket 33import sys 34 35tornado.options.define("debug", default=False, help="Debug mode") 36tornado.options.define("more", default=False, 37 help="Debug mode with more verbosity") 38tornado.options.define("unminified", default=False, 39 help="Use the unminified js (for development only)") 40 41tornado.options.define("host", default='localhost', help="Server host") 42tornado.options.define("port", default=57575, type=int, help="Server port") 43tornado.options.define("one_shot", default=False, 44 help="Run a one-shot instance. Quit at term close") 45tornado.options.define("shell", help="Shell to execute at login") 46tornado.options.define("motd", default='motd', help="Path to the motd file.") 47tornado.options.define("cmd", 48 help="Command to run instead of shell, f.i.: 'ls -l'") 49tornado.options.define("unsecure", default=False, 50 help="Don't use ssl not recommended") 51tornado.options.define("login", default=True, 52 help="Use login screen at start") 53tornado.options.define("force_unicode_width", 54 default=False, 55 help="Force all unicode characters to the same width." 56 "Useful for avoiding layout mess.") 57tornado.options.define("ssl_version", default=None, 58 help="SSL protocol version") 59tornado.options.define("generate_certs", default=False, 60 help="Generate butterfly certificates") 61tornado.options.define("generate_current_user_pkcs", default=False, 62 help="Generate current user pfx for client " 63 "authentication") 64tornado.options.define("generate_user_pkcs", default='', 65 help="Generate user pfx for client authentication " 66 "(Must be root to create for another user)") 67 68if os.getuid() == 0: 69 ev = os.getenv('XDG_CONFIG_DIRS', '/etc') 70else: 71 ev = os.getenv( 72 'XDG_CONFIG_HOME', os.path.join( 73 os.getenv('HOME', os.path.expanduser('~')), 74 '.config')) 75 76butterfly_dir = os.path.join(ev, 'butterfly') 77conf_file = os.path.join(butterfly_dir, 'butterfly.conf') 78ssl_dir = os.path.join(butterfly_dir, 'ssl') 79 80if not os.path.exists(conf_file): 81 try: 82 import butterfly 83 shutil.copy( 84 os.path.join( 85 os.path.abspath(os.path.dirname(butterfly.__file__)), 86 'butterfly.conf.default'), conf_file) 87 print('butterfly.conf installed in %s' % conf_file) 88 except: 89 pass 90 91tornado.options.define("conf", default=conf_file, 92 help="Butterfly configuration file. " 93 "Contains the same options as command line.") 94 95tornado.options.define("ssl_dir", default=ssl_dir, 96 help="Force SSL directory location") 97 98# Do it once to get the conf path 99tornado.options.parse_command_line() 100 101if os.path.exists(tornado.options.options.conf): 102 tornado.options.parse_config_file(tornado.options.options.conf) 103 104# Do it again to overwrite conf with args 105tornado.options.parse_command_line() 106 107options = tornado.options.options 108 109for logger in ('tornado.access', 'tornado.application', 110 'tornado.general', 'butterfly'): 111 level = logging.WARNING 112 if options.debug: 113 level = logging.INFO 114 if options.more: 115 level = logging.DEBUG 116 logging.getLogger(logger).setLevel(level) 117 118log = logging.getLogger('butterfly') 119 120host = options.host 121port = options.port 122 123 124if not os.path.exists(options.ssl_dir): 125 os.makedirs(options.ssl_dir) 126 127 128def to_abs(file): 129 return os.path.join(options.ssl_dir, file) 130 131ca, ca_key, cert, cert_key, pkcs12 = map(to_abs, [ 132 'butterfly_ca.crt', 'butterfly_ca.key', 133 'butterfly_%s.crt', 'butterfly_%s.key', 134 '%s.p12']) 135 136 137def fill_fields(subject): 138 subject.C = 'WW' 139 subject.O = 'Butterfly' 140 subject.OU = 'Butterfly Terminal' 141 subject.ST = 'World Wide' 142 subject.L = 'Terminal' 143 144 145def write(file, content): 146 with open(file, 'wb') as fd: 147 fd.write(content) 148 print('Writing %s' % file) 149 150 151def read(file): 152 print('Reading %s' % file) 153 with open(file, 'rb') as fd: 154 return fd.read() 155 156if options.generate_certs: 157 from OpenSSL import crypto 158 print('Generating certificates for %s (change it with --host)\n' % host) 159 160 if not os.path.exists(ca) and not os.path.exists(ca_key): 161 print('Root certificate not found, generating it') 162 ca_pk = crypto.PKey() 163 ca_pk.generate_key(crypto.TYPE_RSA, 2048) 164 ca_cert = crypto.X509() 165 ca_cert.get_subject().CN = 'Butterfly CA on %s' % socket.gethostname() 166 fill_fields(ca_cert.get_subject()) 167 ca_cert.set_serial_number(uuid.uuid4().int) 168 ca_cert.gmtime_adj_notBefore(0) # From now 169 ca_cert.gmtime_adj_notAfter(315360000) # to 10y 170 ca_cert.set_issuer(ca_cert.get_subject()) # Self signed 171 ca_cert.set_pubkey(ca_pk) 172 ca_cert.sign(ca_pk, 'sha512') 173 174 write(ca, crypto.dump_certificate(crypto.FILETYPE_PEM, ca_cert)) 175 write(ca_key, crypto.dump_privatekey(crypto.FILETYPE_PEM, ca_pk)) 176 os.chmod(ca_key, stat.S_IRUSR | stat.S_IWUSR) # 0o600 perms 177 else: 178 print('Root certificate found, using it') 179 ca_cert = crypto.load_certificate(crypto.FILETYPE_PEM, read(ca)) 180 ca_pk = crypto.load_privatekey(crypto.FILETYPE_PEM, read(ca_key)) 181 182 server_pk = crypto.PKey() 183 server_pk.generate_key(crypto.TYPE_RSA, 2048) 184 server_cert = crypto.X509() 185 server_cert.get_subject().CN = host 186 fill_fields(server_cert.get_subject()) 187 server_cert.set_serial_number(uuid.uuid4().int) 188 server_cert.gmtime_adj_notBefore(0) # From now 189 server_cert.gmtime_adj_notAfter(315360000) # to 10y 190 server_cert.set_issuer(ca_cert.get_subject()) # Signed by ca 191 server_cert.set_pubkey(server_pk) 192 server_cert.sign(ca_pk, 'sha512') 193 194 write(cert % host, crypto.dump_certificate( 195 crypto.FILETYPE_PEM, server_cert)) 196 write(cert_key % host, crypto.dump_privatekey( 197 crypto.FILETYPE_PEM, server_pk)) 198 os.chmod(cert_key % host, stat.S_IRUSR | stat.S_IWUSR) # 0o600 perms 199 200 print('\nNow you can run --generate-user-pkcs=user ' 201 'to generate user certificate.') 202 sys.exit(0) 203 204 205if (options.generate_current_user_pkcs or 206 options.generate_user_pkcs): 207 from butterfly import utils 208 try: 209 current_user = utils.User() 210 except Exception: 211 current_user = None 212 213 from OpenSSL import crypto 214 if not all(map(os.path.exists, [ca, ca_key])): 215 print('Please generate certificates using --generate-certs before') 216 sys.exit(1) 217 218 if options.generate_current_user_pkcs: 219 user = current_user.name 220 else: 221 user = options.generate_user_pkcs 222 223 if user != current_user.name and current_user.uid != 0: 224 print('Cannot create certificate for another user with ' 225 'current privileges.') 226 sys.exit(1) 227 228 ca_cert = crypto.load_certificate(crypto.FILETYPE_PEM, read(ca)) 229 ca_pk = crypto.load_privatekey(crypto.FILETYPE_PEM, read(ca_key)) 230 231 client_pk = crypto.PKey() 232 client_pk.generate_key(crypto.TYPE_RSA, 2048) 233 234 client_cert = crypto.X509() 235 client_cert.get_subject().CN = user 236 fill_fields(client_cert.get_subject()) 237 client_cert.set_serial_number(uuid.uuid4().int) 238 client_cert.gmtime_adj_notBefore(0) # From now 239 client_cert.gmtime_adj_notAfter(315360000) # to 10y 240 client_cert.set_issuer(ca_cert.get_subject()) # Signed by ca 241 client_cert.set_pubkey(client_pk) 242 client_cert.sign(client_pk, 'sha512') 243 client_cert.sign(ca_pk, 'sha512') 244 245 pfx = crypto.PKCS12() 246 pfx.set_certificate(client_cert) 247 pfx.set_privatekey(client_pk) 248 pfx.set_ca_certificates([ca_cert]) 249 pfx.set_friendlyname(('%s cert for butterfly' % user).encode('utf-8')) 250 251 while True: 252 password = getpass.getpass('\nPKCS12 Password (can be blank): ') 253 password2 = getpass.getpass('Verify Password (can be blank): ') 254 if password == password2: 255 break 256 print('Passwords do not match.') 257 258 print('') 259 write(pkcs12 % user, pfx.export(password.encode('utf-8'))) 260 os.chmod(pkcs12 % user, stat.S_IRUSR | stat.S_IWUSR) # 0o600 perms 261 sys.exit(0) 262 263 264if options.unsecure: 265 ssl_opts = None 266else: 267 if not all(map(os.path.exists, [cert % host, cert_key % host, ca])): 268 print("Unable to find butterfly certificate for host %s" % host) 269 print(cert % host) 270 print(cert_key % host) 271 print(ca) 272 print("Can't run butterfly without certificate.\n") 273 print("Either generate them using --generate-certs --host=host " 274 "or run as --unsecure (NOT RECOMMENDED)\n") 275 print("For more information go to http://paradoxxxzero.github.io/" 276 "2014/03/21/butterfly-with-ssl-auth.html\n") 277 sys.exit(1) 278 279 ssl_opts = { 280 'certfile': cert % host, 281 'keyfile': cert_key % host, 282 'ca_certs': ca, 283 'cert_reqs': ssl.CERT_REQUIRED 284 } 285 if options.ssl_version is not None: 286 if not hasattr( 287 ssl, 'PROTOCOL_%s' % options.ssl_version): 288 print( 289 "Unknown SSL protocol %s" % 290 options.ssl_version) 291 sys.exit(1) 292 ssl_opts['ssl_version'] = getattr( 293 ssl, 'PROTOCOL_%s' % options.ssl_version) 294 295from butterfly import application 296application.butterfly_dir = butterfly_dir 297log.info('Starting server') 298http_server = tornado.httpserver.HTTPServer( 299 application, ssl_options=ssl_opts) 300http_server.listen(port, address=host) 301 302#if http_server.systemd: 303# os.environ.pop('LISTEN_PID') 304# os.environ.pop('LISTEN_FDS') 305 306log.info('Starting loop') 307 308ioloop = tornado.ioloop.IOLoop.instance() 309 310if port == 0: 311 port = list(http_server._sockets.values())[0].getsockname()[1] 312 313url = "http%s://%s:%d/" % ( 314 "s" if not options.unsecure else "", host, port) 315 316if not options.one_shot or not webbrowser.open(url): 317 log.warn('Butterfly is ready, open your browser to: %s' % url) 318 319ioloop.start() 320