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