From 5009f2c227ab55d70022f24f7f32299e26fd11ea Mon Sep 17 00:00:00 2001 From: "Kali Kaneko (leap communications)" Date: Thu, 27 Apr 2017 19:13:22 +0200 Subject: [feature] streamline and move manhole into core --- src/leap/bitmask/core/manhole.py | 143 +++++++++++++++++++++++++ src/leap/bitmask/core/service.py | 32 +++++- src/leap/bitmask/mail/imap/service/__init__.py | 14 --- src/leap/bitmask/mail/imap/service/manhole.py | 130 ---------------------- 4 files changed, 174 insertions(+), 145 deletions(-) create mode 100644 src/leap/bitmask/core/manhole.py delete mode 100644 src/leap/bitmask/mail/imap/service/manhole.py (limited to 'src') diff --git a/src/leap/bitmask/core/manhole.py b/src/leap/bitmask/core/manhole.py new file mode 100644 index 00000000..1a1b10dc --- /dev/null +++ b/src/leap/bitmask/core/manhole.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +# manhole.py +# Copyright (C) 2014-2017 LEAP +# +# 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 . + +""" +Utilities for enabling the manhole administrative interface into Bitmask Core. +""" + +PORT = 2222 + + +def getManholeFactory(namespace, user, secret, keydir=None): + """ + Get an administrative manhole into the application. + + :param namespace: the namespace to show in the manhole + :type namespace: dict + :param user: the user to authenticate into the administrative shell. + :type user: str + :param secret: pass for this manhole + :type secret: str + """ + import string + + from twisted.cred import portal + from twisted.conch import manhole, manhole_ssh + from twisted.conch import recvline + from twisted.conch.insults import insults + from twisted.conch.ssh import keys + from twisted.cred.checkers import ( + InMemoryUsernamePasswordDatabaseDontUse as MemoryDB) + from twisted.python import filepath + + try: + from IPython.core.completer import Completer + except ImportError: + from rlcompleter import Completer + + class EnhancedColoredManhole(manhole.ColoredManhole): + """ + A nicer Manhole with some autocomplete support. + + See the patch in https://twistedmatrix.com/trac/ticket/6863 + Since you're reading this, it'd be good if *you* can help getting that + patch into twisted :) + """ + + completion = True + + def handle_TAB(self): + """ + If tab completion is available and enabled then perform some tab + completion. + """ + if not self.completion: + recvline.HistoricRecvLine.handle_TAB(self) + return + # If we only have whitespace characters on this line we pass + # through the tab + if set(self.lineBuffer).issubset(string.whitespace): + recvline.HistoricRecvLine.handle_TAB(self) + return + cp = Completer(namespace=self.namespace) + cp.limit_to__all__ = False + lineLeft, lineRight = self.currentLineBuffer() + + # Extract all the matches + matches = [] + n = 0 + while True: + match = cp.complete(lineLeft, n) + if match is None: + break + n += 1 + matches.append(match) + + if not matches: + return + + if len(matches) == 1: + # Found the match so replace the line. This is apparently how + # we replace a line + self.handle_HOME() + self.terminal.eraseToLineEnd() + + self.lineBuffer = [] + self._deliverBuffer(matches[0] + lineRight) + else: + # Must have more than one match, display them + matches.sort() + self.terminal.write("\n") + self.terminal.write(" ".join(matches)) + self.terminal.write("\n\n") + self.drawInputLine() + + def keystrokeReceived(self, keyID, modifier): + """ + Act upon any keystroke received. + """ + self.keyHandlers.update({'\b': self.handle_BACKSPACE}) + m = self.keyHandlers.get(keyID) + if m is not None: + m() + elif keyID in string.printable: + self.characterReceived(keyID, False) + + class chainedProtocolFactory: + def __init__(self, namespace): + self.namespace = namespace + + def __call__(self): + return insults.ServerProtocol( + EnhancedColoredManhole, self.namespace) + + sshRealm = manhole_ssh.TerminalRealm() + sshRealm.chainedProtocolFactory = chainedProtocolFactory(namespace) + + checker = MemoryDB(**{user: secret}) + sshPortal = portal.Portal(sshRealm, [checker]) + sshFactory = manhole_ssh.ConchFactory(sshPortal) + + if not keydir: + from twisted.python._appdirs import getDataDirectory + keydir = getDataDirectory() + + keyLocation = filepath.FilePath(keydir).child('id_rsa') + sshKey = keys._getPersistentRSAKey(keyLocation, 4096) + sshFactory.publicKeys[b"ssh-rsa"] = sshKey + sshFactory.privateKeys[b"ssh-rsa"] = sshKey + return sshFactory diff --git a/src/leap/bitmask/core/service.py b/src/leap/bitmask/core/service.py index 2972a51c..314c8899 100644 --- a/src/leap/bitmask/core/service.py +++ b/src/leap/bitmask/core/service.py @@ -20,18 +20,22 @@ Bitmask-core Service. import json import os import uuid +import tempfile try: import resource except ImportError: pass +from twisted.conch import manhole_tap from twisted.internet import reactor +from twisted.internet.endpoints import TCP4ServerEndpoint from twisted.logger import Logger from leap.bitmask import __version__ from leap.bitmask.core import configurable -from leap.bitmask.core import _zmq +from leap.bitmask.core import manhole from leap.bitmask.core import flags +from leap.bitmask.core import _zmq from leap.bitmask.core import _session from leap.bitmask.core.web.service import HTTPDispatcherService from leap.bitmask.vpn.service import VPNService @@ -80,6 +84,15 @@ class BitmaskBackend(configurable.ConfigurableService): def enabled(service): return self.get_config('services', service, False, boolean=True) + def with_manhole(): + user = self.get_config('manhole', 'user', None) + passwd = self.get_config('manhole', 'passwd', None) + port = self.get_config('manhole', 'port', None) + if user and passwd: + conf = {'user': user, 'passwd': passwd, 'port': port} + return conf + return None + on_start = reactor.callWhenRunning on_start(self.init_events) @@ -102,6 +115,10 @@ class BitmaskBackend(configurable.ConfigurableService): if enabled('websockets'): on_start(self._init_websockets) + manholecfg = with_manhole() + if manhole: + on_start(self._init_manhole, manholecfg) + def _touch_token_file(self): path = os.path.join(self.basedir, 'authtoken') with open(path, 'w') as f: @@ -209,6 +226,19 @@ class BitmaskBackend(configurable.ConfigurableService): service.setServiceParent(self) return service + def _init_manhole(self, cfg): + try: + port = int(cfg.get('port')) + except ValueError: + port = manhole.PORT + user, passwd = cfg['user'], cfg['passwd'] + sshFactory = manhole.getManholeFactory( + {'core': self}, user, passwd) + endpoint = TCP4ServerEndpoint(reactor, port) + endpoint.listen(sshFactory) + + log.info('Started manhole in PORT {0!s}'.format(port)) + def do_stats(self): return self.core_commands.do_stats() diff --git a/src/leap/bitmask/mail/imap/service/__init__.py b/src/leap/bitmask/mail/imap/service/__init__.py index b3673c2e..712a2d64 100644 --- a/src/leap/bitmask/mail/imap/service/__init__.py +++ b/src/leap/bitmask/mail/imap/service/__init__.py @@ -39,12 +39,8 @@ from leap.bitmask.mail.imap.server import LEAPIMAPServer log = Logger() -DO_MANHOLE = os.environ.get("LEAP_MAIL_MANHOLE", None) -if DO_MANHOLE: - from leap.bitmask.mail.imap.service import manhole # The default port in which imap service will run - IMAP_PORT = 1984 # @@ -180,16 +176,6 @@ def run_service(soledad_sessions, port=IMAP_PORT, factory=None): log.error("Error launching IMAP service: %r" % (exc,)) else: # all good. - - if DO_MANHOLE: - # TODO get pass from env var.too. - manhole_factory = manhole.getManholeFactory( - {'f': factory, - 'gm': factory.theAccount.getMailbox}, - "boss", "leap") - # TODO use Endpoints !!! - reactor.listenTCP(manhole.MANHOLE_PORT, manhole_factory, - interface="127.0.0.1") log.debug('IMAP4 Server is RUNNING in port %s' % (port,)) emit_async(catalog.IMAP_SERVICE_STARTED, str(port)) diff --git a/src/leap/bitmask/mail/imap/service/manhole.py b/src/leap/bitmask/mail/imap/service/manhole.py deleted file mode 100644 index c83ae899..00000000 --- a/src/leap/bitmask/mail/imap/service/manhole.py +++ /dev/null @@ -1,130 +0,0 @@ -# -*- coding: utf-8 -*- -# manhole.py -# Copyright (C) 2014 LEAP -# -# 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 . -""" -Utilities for enabling the manhole administrative interface into the -LEAP Mail application. -""" -MANHOLE_PORT = 2222 - - -def getManholeFactory(namespace, user, secret): - """ - Get an administrative manhole into the application. - - :param namespace: the namespace to show in the manhole - :type namespace: dict - :param user: the user to authenticate into the administrative shell. - :type user: str - :param secret: pass for this manhole - :type secret: str - """ - import string - - from twisted.cred.portal import Portal - from twisted.conch import manhole, manhole_ssh - from twisted.conch.insults import insults - from twisted.cred.checkers import ( - InMemoryUsernamePasswordDatabaseDontUse as MemoryDB) - - from rlcompleter import Completer - - class EnhancedColoredManhole(manhole.ColoredManhole): - """ - A Manhole with some primitive autocomplete support. - """ - # TODO use introspection to make life easier - - def find_common(self, l): - """ - find common parts in thelist items - ex: 'ab' for ['abcd','abce','abf'] - requires an ordered list - """ - if len(l) == 1: - return l[0] - - init = l[0] - for item in l[1:]: - for i, (x, y) in enumerate(zip(init, item)): - if x != y: - init = "".join(init[:i]) - break - - if not init: - return None - return init - - def handle_TAB(self): - """ - Trap the TAB keystroke. - """ - necessarypart = "".join(self.lineBuffer).split(' ')[-1] - completer = Completer(globals()) - if completer.complete(necessarypart, 0): - matches = list(set(completer.matches)) # has multiples - - if len(matches) == 1: - length = len(necessarypart) - self.lineBuffer = self.lineBuffer[:-length] - self.lineBuffer.extend(matches[0]) - self.lineBufferIndex = len(self.lineBuffer) - else: - matches.sort() - commons = self.find_common(matches) - if commons: - length = len(necessarypart) - self.lineBuffer = self.lineBuffer[:-length] - self.lineBuffer.extend(commons) - self.lineBufferIndex = len(self.lineBuffer) - - self.terminal.nextLine() - while matches: - matches, part = matches[4:], matches[:4] - for item in part: - self.terminal.write('%s' % item.ljust(30)) - self.terminal.write('\n') - self.terminal.nextLine() - - self.terminal.eraseLine() - self.terminal.cursorBackward(self.lineBufferIndex + 5) - self.terminal.write("%s %s" % ( - self.ps[self.pn], "".join(self.lineBuffer))) - - def keystrokeReceived(self, keyID, modifier): - """ - Act upon any keystroke received. - """ - self.keyHandlers.update({'\b': self.handle_BACKSPACE}) - m = self.keyHandlers.get(keyID) - if m is not None: - m() - elif keyID in string.printable: - self.characterReceived(keyID, False) - - sshRealm = manhole_ssh.TerminalRealm() - - def chainedProtocolFactory(): - return insults.ServerProtocol(EnhancedColoredManhole, namespace) - - sshRealm = manhole_ssh.TerminalRealm() - sshRealm.chainedProtocolFactory = chainedProtocolFactory - - portal = Portal( - sshRealm, [MemoryDB(**{user: secret})]) - - f = manhole_ssh.ConchFactory(portal) - return f -- cgit v1.2.3