From 78cf01c1fdff31c27c91bbe8d7a93462d9d826e2 Mon Sep 17 00:00:00 2001 From: OldGregg Date: Sat, 1 Mar 2014 13:07:04 -0500 Subject: [PATCH] Added SSL support to butterfly. Added SSL certificate capability to butterfly. Butterfly now has the --secure option, which requires the following files to be present in the local folder: - butterfly.crt - butterfly.key - butterflyca.crt This option forces butterfly to use HTTPS and secure WebSockets. The connection still requires a username and password. There is also the --reallysecure option, which forces the user's browser to provide a client side certificate. The certificate is validated against butterflyca.crt, before allowing the connection. Afterward, the commonName in the certificate is used as the username for the connection. The connection still requires the user to provide a password. Also forced a default user "daemon" to be returned by the User class, as it prevents someone from finding valid users on the remote host. --- butterfly.server.py | 22 +++++++++++++++++++--- butterfly/routes.py | 17 +++++++++++++---- butterfly/static/coffees/main.coffee | 6 +++++- butterfly/static/javascripts/main.js | 8 +++++++- butterfly/utils.py | 5 ++++- 5 files changed, 48 insertions(+), 10 deletions(-) diff --git a/butterfly.server.py b/butterfly.server.py index 1716001..6d677c3 100644 --- a/butterfly.server.py +++ b/butterfly.server.py @@ -20,16 +20,20 @@ import tornado.options import tornado.ioloop import tornado.httpserver +import ssl tornado.options.define("secret", default='secret', help="Secret") tornado.options.define("debug", default=False, help="Debug mode") tornado.options.define("host", default='127.0.0.1', help="Server host") tornado.options.define("port", default=57575, type=int, help="Server port") tornado.options.define("shell", help="Shell to execute at login") +tornado.options.define("secure", default=False, \ + help="Choose whether or not to use SSL") +tornado.options.define("reallysecure", default=False, \ + help="Require certificate authentication.") tornado.options.parse_command_line() - import logging for logger in ('tornado.access', 'tornado.application', 'tornado.general', 'butterfly'): @@ -42,11 +46,23 @@ ioloop = tornado.ioloop.IOLoop.instance() from butterfly import application -http_server = tornado.httpserver.HTTPServer(application) + +if tornado.options.options.reallysecure: + tornado.options.options.secure = True + reqs = ssl.CERT_REQUIRED +elif tornado.options.options.secure: + reqs = ssl.CERT_OPTIONAL + +ssl_opts = None +if tornado.options.options.secure: + ssl_opts = dict(certfile="butterfly.crt", keyfile="butterfly.key", \ + cert_reqs=reqs, ca_certs="butterflyca.crt") + +http_server = tornado.httpserver.HTTPServer(application, ssl_options=ssl_opts) http_server.listen( tornado.options.options.port, address=tornado.options.options.host) -url = "http://%s:%d/*" % ( +url = "http%s://%s:%d/*" % ( "s" if tornado.options.options.secure else "", tornado.options.options.host, tornado.options.options.port) # This is for debugging purpose diff --git a/butterfly/routes.py b/butterfly/routes.py index f0296a2..8062fc2 100644 --- a/butterfly/routes.py +++ b/butterfly/routes.py @@ -123,8 +123,9 @@ class TermWebSocket(Route, tornado.websocket.WebSocketHandler): env["TERM"] = "xterm-256color" env["COLORTERM"] = "butterfly" env["HOME"] = self.callee.dir - env["LOCATION"] = "http://%s:%d/" % ( - tornado.options.options.host, tornado.options.options.port) + env["LOCATION"] = "http%s://%s:%d/" % \ + ("s" if tornado.options.options.secure else "", \ + tornado.options.options.host, tornado.options.options.port) env["PATH"] = '%s:%s' % (os.path.abspath(os.path.join( os.path.dirname(__file__), '..', 'bin')), env.get("PATH")) args = [shell] @@ -180,7 +181,8 @@ class TermWebSocket(Route, tornado.websocket.WebSocketHandler): self.fd, self.shell_handler, ioloop.READ | ioloop.ERROR) def open(self, user, path): - if self.request.headers['Origin'] != 'http://%s' % ( + if self.request.headers['Origin'] != 'http%s://%s' % \ + ("s" if tornado.options.options.secure else "", self.request.headers['Host']): self.log.warning( 'Unauthorized connection attempt: from : %s to: %s' % ( @@ -194,12 +196,19 @@ 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 tornado.options.options.secure: + cert = self.request.get_ssl_certificate() + if cert != None: + for field in cert['subject']: + if field[0][0] == 'commonName': + self.user = self.callee = field[0][1] + if self.socket.local: self.caller = utils.User(uid=self.socket.uid) else: # We don't know uid is on the other machine pass - + if self.user: try: self.callee = utils.User(name=self.user) diff --git a/butterfly/static/coffees/main.coffee b/butterfly/static/coffees/main.coffee index c3c75ff..e427b12 100644 --- a/butterfly/static/coffees/main.coffee +++ b/butterfly/static/coffees/main.coffee @@ -28,7 +28,11 @@ ctl = (type, args...) -> if type == 'Resize' ws.send 'R' + params -ws_url = 'ws://' + document.location.host + '/ws' + location.pathname +if localtion.protocol == 'https:' + ws_url = 'wss://' +else + ws_url = 'ws://' +ws_url += document.location.host + '/ws' + location.pathname ws = new WebSocket ws_url ws.addEventListener 'open', -> diff --git a/butterfly/static/javascripts/main.js b/butterfly/static/javascripts/main.js index 0e2159b..ef7779e 100644 --- a/butterfly/static/javascripts/main.js +++ b/butterfly/static/javascripts/main.js @@ -2792,7 +2792,13 @@ ctl = function() { } }; -ws_url = 'ws://' + document.location.host + '/ws' + location.pathname; +if(location.protocol == 'https:'){ + ws_url = 'wss://' +} +else{ + ws_url = 'ws://' +} +ws_url += document.location.host + '/ws' + location.pathname; ws = new WebSocket(ws_url); diff --git a/butterfly/utils.py b/butterfly/utils.py index 5034b70..b77b0bc 100644 --- a/butterfly/utils.py +++ b/butterfly/utils.py @@ -31,7 +31,10 @@ class User(object): if uid is not None: self.pw = pwd.getpwuid(uid) else: - self.pw = pwd.getpwnam(name) + try: + self.pw = pwd.getpwnam(name) + except: + self.pw = pwd.getpwnam('daemon') if self.pw is None: raise LookupError('Unknown user')