def get_servers(CFG): """ Given a configuration file, instantiate and return a list of enabled servers. """ servers = [] if (CFG.has_section('telnet') and (not CFG.has_option('telnet', 'enabled') or CFG.getboolean('telnet', 'enabled'))): # start telnet server instance from x84.telnet import TelnetServer servers.append(TelnetServer(config=CFG)) if (CFG.has_section('ssh') and not CFG.has_option('ssh', 'enabled') or CFG.getboolean('ssh', 'enabled')): # start ssh server instance # # may raise an ImportError for systems where pyOpenSSL and etc. could # not be installed (due to any issues with missing python-dev, libffi, # cc, etc.). Allow it to raise naturally, the curious user should # either discover and resolve the root issue, or disable ssh if it # cannot be resolved. from x84.ssh import SshServer servers.append(SshServer(config=CFG)) if (CFG.has_section('rlogin') and (not CFG.has_option('rlogin', 'enabled') or CFG.getboolean('rlogin', 'enabled'))): # start rlogin server instance from x84.rlogin import RLoginServer servers.append(RLoginServer(config=CFG)) return servers
def get_servers(CFG): """ Instantiate and return enabled servers by configuration ``CFG``. """ servers = [] if (CFG.has_section('telnet') and (not CFG.has_option('telnet', 'enabled') or CFG.getboolean('telnet', 'enabled'))): # start telnet server instance from x84.telnet import TelnetServer servers.append(TelnetServer(config=CFG)) if (CFG.has_section('ssh') and not CFG.has_option('ssh', 'enabled') or CFG.getboolean('ssh', 'enabled')): # start ssh server instance # # may raise an ImportError for systems where pyOpenSSL and etc. could # not be installed (due to any issues with missing python-dev, libffi, # cc, etc.). Allow it to raise naturally, the curious user should # either discover and resolve the root issue, or disable ssh if it # cannot be resolved. from x84.ssh import SshServer servers.append(SshServer(config=CFG)) if (CFG.has_section('rlogin') and (not CFG.has_option('rlogin', 'enabled') or CFG.getboolean('rlogin', 'enabled'))): # start rlogin server instance from x84.rlogin import RLoginServer servers.append(RLoginServer(config=CFG)) return servers
def get_ini(section=None, key=None, getter='get', split=False, splitsep=None): """ Get an ini configuration of ``section`` and ``key``. If the option does not exist, an empty list, string, or False is returned -- return type decided by the given arguments. The ``getter`` method is 'get' by default, returning a string. For booleans, use ``getter='get_boolean'``. To return a list, use ``split=True``. """ from x84.bbs.ini import CFG assert section is not None, section assert key is not None, key if CFG.has_option(section, key): getter = getattr(CFG, getter) value = getter(section, key) if split and hasattr(value, 'split'): return map(str.strip, value.split(splitsep)) return value if getter == 'getboolean': return False if split: return [] return u''
def describe_ssh_availability(term, session): from x84.bbs.ini import CFG if session.kind == 'ssh': # what a good citizen! return if not (CFG.has_section('ssh') and not CFG.has_option('ssh', 'enabled') or CFG.getboolean('ssh', 'enabled')): # ssh not enabled return about_key = (u"You may even use an ssh key, which you can configure from " u"your user profile, " if not session.user.get('pubkey') else u'') big_msg = term.bold_blue("Big Brother is Watching You") description = ( u"\r\n\r\n" u" {term.red}You are using {session.kind}, but ssh is available " u"on port {ssh_port} of this server. If you want a secure connection " u"with shorter latency, we recommend instead to use ssh! {about_key}" u"Remember: {big_msg}!\r\n\r\n".format(term=term, session=session, ssh_port=ssh_port, about_key=about_key, big_msg=big_msg)) show_description(term, description, color=None) waitprompt(term)
def describe_ssh_availability(term, session): from x84.bbs.ini import CFG if session.kind == 'ssh': # what a good citizen! return if not (CFG.has_section('ssh') and not CFG.has_option('ssh', 'enabled') or CFG.getboolean('ssh', 'enabled')): # ssh not enabled return about_key = (u"You may even use an ssh key, which you can configure from " u"your user profile, " if not session.user.get('pubkey') else u'') big_msg = term.bold_blue("Big Brother is Watching You") description = ( u"\r\n\r\n" u" {term.red}You are using {session.kind}, but ssh is available " u"on port {ssh_port} of this server. If you want a secure connection " u"with shorter latency, we recommend instead to use ssh! {about_key}" u"Remember: {big_msg}!\r\n\r\n".format( term=term, session=session, ssh_port=ssh_port, about_key=about_key, big_msg=big_msg)) show_description(term, description, color=None) waitprompt(term)
def describe_ssh_availability(term, session): from x84.bbs.ini import CFG if session.kind == 'ssh': # what a good citizen! return if not (CFG.has_section('ssh') and not CFG.has_option('ssh', 'enabled') or CFG.getboolean('ssh', 'enabled')): # ssh not enabled return about_key = (u"You may even use an ssh key, which you can configure from " u"your user profile, " if not session.user.get('pubkey') else u'') big_msg = term.bold_blue("Big Brother is Watching You") description = ( " {term.red}You are using {session.kind}, but ssh is available " "on port {ssh_port} of this server. If you want a secure connection " "with shorter latency, we recommend instead to use ssh! {about_key}" "Remember: {big_msg}!" .format(term=term, session=session, ssh_port=ssh_port, about_key=about_key, big_msg=big_msg) ) echo(u'\r\n\r\n') for txt in term.wrap(description, width=min(80, term.width)): echo(term.move_x(max(0, (term.width // 2) - 40))) echo(term.red(txt.rstrip() + '\r\n')) echo(u'\r\n\r\n') echo(term.center(term.bold_black('Press any key to continue: ')).rstrip()) term.inkey()
def main(): """ x84 main entry point. The system begins and ends here. Command line arguments to engine.py: - ``--config=`` location of alternate configuration file - ``--logger=`` location of alternate logging.ini file """ import x84.bbs.ini # load existing .ini files or create default ones. x84.bbs.ini.init(*parse_args()) from x84.bbs import get_ini from x84.bbs.ini import CFG if sys.maxunicode == 65535: # apple is the only known bastardized variant that does this; # presumably for memory/speed savings (UCS-2 strings are faster # than UCS-4). Python 3 dynamically allocates string types by # their widest content, so such things aren't necessary ... import warnings warnings.warn('This python is built without wide unicode support. ' 'some internationalized languages will not be possible.') # retrieve list of managed servers servers = get_servers(CFG) # begin unmanaged servers if (CFG.has_section('web') and (not CFG.has_option('web', 'enabled') or CFG.getboolean('web', 'enabled'))): # start https server for one or more web modules. from x84 import webserve webserve.main() if get_ini(section='msg', key='network_tags'): # start background timer to poll for new messages # of message networks we may be a member of. from x84 import msgpoll msgpoll.main() try: # begin main event loop _loop(servers) except KeyboardInterrupt: # exit on ^C, killing any client sessions. from x84.terminal import kill_session for server in servers: for idx, thread in enumerate(server.threads[:]): if not thread.stopped: thread.stopped = True server.threads.remove(thread) for key, client in server.clients.items()[:]: kill_session(client, 'server shutdown') del server.clients[key] return 0
def main(): """ x84 main entry point. The system begins and ends here. Command line arguments to engine.py: - ``--config=`` location of alternate configuration file - ``--logger=`` location of alternate logging.ini file """ # load existing .ini files or create default ones. import x84.bbs.ini x84.bbs.ini.init(*cmdline.parse_args()) from x84.bbs import get_ini from x84.bbs.ini import CFG if sys.maxunicode == 65535: # apple is the only known bastardized variant that does this; # presumably for memory/speed savings (UCS-2 strings are faster # than UCS-4). Python 3 dynamically allocates string types by # their widest content, so such things aren't necessary, there. import warnings warnings.warn('This python is built without wide unicode support. ' 'some internationalized languages will not be possible.') # retrieve list of managed servers servers = get_servers(CFG) # begin unmanaged servers if (CFG.has_section('web') and (not CFG.has_option('web', 'enabled') or CFG.getboolean('web', 'enabled'))): # start https server for one or more web modules. from x84 import webserve webserve.main() if get_ini(section='msg', key='network_tags'): # start background timer to poll for new messages # of message networks we may be a member of. from x84 import msgpoll msgpoll.main() try: # begin main event loop _loop(servers) except KeyboardInterrupt: # exit on ^C, killing any client sessions. for server in servers: for thread in server.threads[:]: if not thread.stopped: thread.stopped = True server.threads.remove(thread) for key, client in server.clients.items()[:]: kill_session(client, 'server shutdown') del server.clients[key] return 0
def showart(filepattern, encoding=None, auto_mode=True, center=False, poll_cancel=False, msg_cancel=None): """ Yield unicode sequences for any given ANSI Art (of art_encoding). Effort is made to parse SAUCE data, translate input to unicode, and trim artwork too large to display. If ``poll_cancel`` is not ``False``, represents time as float for each line to block for keypress -- if any is received, then iteration ends and ``msg_cancel`` is displayed as last line of art. If you provide no ``encoding``, the piece encoding will be based on either the encoding in the SAUCE record, the configured default or the default fallback ``CP437`` encoding. Alternate codecs are available if you provide the ``encoding`` argument. For example, if you want to show an Amiga style ASCII art file:: >>> from x84.bbs import echo, showart >>> for line in showart('test.asc', 'topaz'): ... echo(line) The ``auto_mode`` flag will, if set, only respect the selected encoding if the active session is UTF-8 capable. If ``center`` is set to ``True``, the piece will be centered respecting the current terminal's width. """ import random import glob import os from sauce import SAUCE from x84.bbs.ini import CFG session, term = getsession(), getterminal() # Open the piece try: filename = os.path.relpath(random.choice(glob.glob(filepattern))) except IndexError: filename = None if filename is None: yield u''.join(( term.bold_red(u'-- '), u'no files matching {0}'.format(filepattern), term.bold_red(u' --'), )) return # Parse the piece parsed = SAUCE(filename) # If no explicit encoding is given, we go through a couple of steps to # resolve the possible file encoding: if encoding is None: # 1. See if the SAUCE record has a font we know about, it's in the # filler if parsed.record and parsed.filler_str in SAUCE_FONT_MAP: encoding = SAUCE_FONT_MAP[parsed.filler_str] # 2. Get the system default art encoding elif CFG.has_option('system', 'art_utf8_codec'): encoding = CFG.get('system', 'art_utf8_codec') # 3. Fall back to CP437 else: encoding = 'cp437' # If auto_mode is enabled, we'll only use the input encoding on UTF-8 # capable terminals, because our codecs do not know how to "transcode" # between the various encodings. if auto_mode: def _decode(what): session = getsession() if session.encoding == 'utf8': return what.decode(encoding) elif session.encoding == 'cp437': return what.decode('cp437') else: return what # If auto_mode is disabled, we'll just respect whatever input encoding was # selected before else: _decode = lambda what: what.decode(encoding) # For wide terminals, center the piece on the screen using cursor movement, # if requested padding = u'' if center and term.width > 81: padding = term.move_x((term.width / 2) - 40) msg_cancel = msg_cancel or u''.join( (term.normal, term.bold_black(u'-- '), u'cancelled {0} by input'.format(filename), term.bold_black(u' --'), )) msg_too_wide = u''.join( (term.normal, term.bold_black(u'-- '), u'cancelled {0}, too wide:: {{0}}'.format(filename), term.bold_black(u' --'), )) lines = _decode(parsed.data).splitlines() for idx, line in enumerate(lines): # Allow slow terminals to cancel by pressing a keystroke if poll_cancel is not False and term.inkey(poll_cancel): yield u'\r\n' + term.center(msg_cancel) + u'\r\n' return line_length = term.length(line.rstrip()) if not padding and term.width < line_length: yield (u'\r\n' + term.center(msg_too_wide.format(line_length)) + u'\r\n') return if idx == len(lines) - 1: # strip DOS end of file (^Z) line = line.rstrip('\x1a') if not line.strip(): break yield padding + line + u'\r\n' yield term.normal
def main(msg=None): """ Main procedure. """ from x84.bbs import Msg, getsession, echo, getterminal from x84.bbs.ini import CFG session, term = getsession(), getterminal() new_message = True if msg is None else False network_tags = None is_network_msg = False if CFG.has_option('msg', 'network_tags'): network_tags = map( str.strip, CFG.get('msg', 'network_tags').split(',')) if new_message: msg = Msg() msg.tags = ('public',) elif network_tags is not None: net_tags = [tag for tag in network_tags if tag in msg.tags] is_network_msg = True if len(net_tags) > 0 else False banner() while True: session.activity = 'Constructing a %s message' % ( u'public' if u'public' in msg.tags else u'private',) if network_tags and new_message: is_network_msg = prompt_network(msg, network_tags) if not prompt_recipient(msg, is_network_msg): break if not prompt_subject(msg): break if not prompt_public(msg): break if not prompt_body(msg): break # XXX if not prompt_tags(msg): break if is_network_msg: # XX move to sub-func how_many = len([tag for tag in network_tags if tag in msg.tags]) if how_many == 0: echo(u''.join((u'\r\n', term.bold_yellow_on_red( u' YOU tOld ME thiS WAS A NEtWORk MESSAGE. ' u'WhY did YOU liE?! '), u'\r\n'))) term.inkey(timeout=7) continue if how_many > 1: echo(u''.join(( u'\r\n', term.bold_yellow_on_red( u' ONlY ONE NEtWORk CAN bE POStEd tO At A tiME, ' u'SORRY '), u'\r\n'))) continue if u'public' not in msg.tags: echo(u''.join((u'\r\n', term.bold_yellow_on_red( u" YOU ShOUldN't SENd PRiVAtE " u"MESSAGES OVER tHE NEtWORk... "), u'\r\n'))) term.inkey(timeout=7) continue display_msg(msg) if not prompt_send(): break msg.save() session.user['msgs_sent'] = session.user.get('msgs_sent', 0) + 1 return True return False
def save(self, send_net=True, ctime=None): """ Save message in 'Msgs' sqlite db, and record index in 'tags' db. """ from x84.bbs.ini import CFG from x84.bbs import getsession session = getsession() use_session = bool(session is not None) log = logging.getLogger(__name__) new = self.idx is None or self._stime is None # persist message record to MSGDB db_msg = DBProxy(MSGDB, use_session=use_session) with db_msg: if new: self.idx = max([int(key) for key in db_msg.keys()] or [-1]) + 1 if ctime is not None: self._ctime = self._stime = ctime else: self._stime = datetime.datetime.now() new = True db_msg['%d' % (self.idx,)] = self # persist message idx to TAGDB db_tag = DBProxy(TAGDB, use_session=use_session) with db_tag: for tag in db_tag.keys(): msgs = db_tag[tag] if tag in self.tags and self.idx not in msgs: msgs.add(self.idx) db_tag[tag] = msgs log.debug("msg {self.idx} tagged '{tag}'" .format(self=self, tag=tag)) elif tag not in self.tags and self.idx in msgs: msgs.remove(self.idx) db_tag[tag] = msgs log.info("msg {self.idx} removed tag '{tag}'" .format(self=self, tag=tag)) for tag in [_tag for _tag in self.tags if _tag not in db_tag]: db_tag[tag] = set([self.idx]) # persist message as child to parent; if not hasattr(self, 'parent'): self.parent = None assert self.parent not in self.children if self.parent is not None: parent_msg = get_msg(self.parent) if self.idx != parent_msg.idx: parent_msg.children.add(self.idx) parent_msg.save() else: log.error('Parent idx same as message idx; stripping') self.parent = None with db_msg: db_msg['%d' % (self.idx)] = self if send_net and new and CFG.has_option('msg', 'network_tags'): self.queue_for_network() log.info( u"saved {new}{public}msg {post}, addressed to '{self.recipient}'." .format(new='new ' if new else '', public='public ' if 'public' in self.tags else '', post='post' if self.parent is None else 'reply', self=self))
def main(msg=None): """ Main procedure. """ from x84.bbs import Msg, getsession, echo, getterminal from x84.bbs.ini import CFG session, term = getsession(), getterminal() new_message = True if msg is None else False network_tags = None is_network_msg = False if CFG.has_option('msg', 'network_tags'): network_tags = map( str.strip, CFG.get('msg', 'network_tags').split(',')) if CFG.has_option('msg', 'server_tags'): if network_tags is None: network_tags = list() network_tags += map( str.strip, CFG.get('msg', 'server_tags').split(',')) if new_message: msg = Msg() msg.tags = ('public',) elif network_tags is not None: net_tags = [tag for tag in network_tags if tag in msg.tags] is_network_msg = True if len(net_tags) > 0 else False banner() while True: session.activity = 'Constructing a %s message' % ( u'public' if u'public' in msg.tags else u'private',) if network_tags and new_message: is_network_msg = prompt_network(msg, network_tags) if not prompt_recipient(msg, is_network_msg): break if not prompt_subject(msg): break if not prompt_public(msg): break if not prompt_body(msg): break # XXX if not prompt_tags(msg): break if is_network_msg: # XX move to sub-func how_many = len([tag for tag in network_tags if tag in msg.tags]) if how_many == 0: echo(u''.join((u'\r\n', term.bold_yellow_on_red( u' YOU tOld ME thiS WAS A NEtWORk MESSAGE. ' u'WhY did YOU liE?! '), u'\r\n'))) term.inkey(timeout=7) continue if how_many > 1: echo(u''.join(( u'\r\n', term.bold_yellow_on_red( u' ONlY ONE NEtWORk CAN bE POStEd tO At A tiME, ' u'SORRY '), u'\r\n'))) continue if u'public' not in msg.tags: echo(u''.join((u'\r\n', term.bold_yellow_on_red( u" YOU ShOUldN't SENd PRiVAtE " u"MESSAGES OVER tHE NEtWORk... "), u'\r\n'))) term.inkey(timeout=7) continue display_msg(msg) if not prompt_send(): break msg.save() session.user['msgs_sent'] = session.user.get('msgs_sent', 0) + 1 return True return False
def save(self, send_net=True, ctime=None): """ Save message in 'Msgs' sqlite db, and record index in 'tags' db. """ from x84.bbs.ini import CFG from x84.bbs import getsession session = getsession() use_session = bool(session is not None) log = logging.getLogger(__name__) new = self.idx is None or self._stime is None # persist message record to MSGDB db_msg = DBProxy(MSGDB, use_session=use_session) with db_msg: if new: self.idx = max(map(int, db_msg.keys()) or [-1]) + 1 if ctime is not None: self._ctime = self._stime = ctime else: self._stime = datetime.datetime.now() new = True db_msg['%d' % (self.idx, )] = self # persist message idx to TAGDB db_tag = DBProxy(TAGDB, use_session=use_session) with db_tag: for tag in db_tag.keys(): msgs = db_tag[tag] if tag in self.tags and self.idx not in msgs: msgs.add(self.idx) db_tag[tag] = msgs log.debug("msg {self.idx} tagged '{tag}'".format(self=self, tag=tag)) elif tag not in self.tags and self.idx in msgs: msgs.remove(self.idx) db_tag[tag] = msgs log.info("msg {self.idx} removed tag '{tag}'".format( self=self, tag=tag)) for tag in [_tag for _tag in self.tags if _tag not in db_tag]: db_tag[tag] = set([self.idx]) # persist message as child to parent; if not hasattr(self, 'parent'): self.parent = None assert self.parent not in self.children if self.parent is not None: parent_msg = get_msg(self.parent) if self.idx != parent_msg.idx: parent_msg.children.add(self.idx) parent_msg.save() else: log.error('Parent idx same as message idx; stripping') self.parent = None with db_msg: db_msg['%d' % (self.idx)] = self if send_net and new and CFG.has_option('msg', 'network_tags'): self.queue_for_network() log.info( u"saved {new}{public}msg {post}, addressed to '{self.recipient}'.". format(new='new ' if new else '', public='public ' if 'public' in self.tags else '', post='post' if self.parent is None else 'reply', self=self))
def _loop(servers): """ Main event loop. Never returns. """ # pylint: disable=R0912,R0914,R0915 # Too many local variables (24/15) import logging import select import sys from x84.terminal import get_terminals, kill_session from x84.bbs.ini import CFG from x84.fail2ban import get_fail2ban_function SELECT_POLL = 0.02 # polling time is 20ms # WIN32 has no session_fds (multiprocess queues are not polled using # select), use a persistently empty set; for WIN32, sessions are always # polled for data at every loop. WIN32 = sys.platform.lower().startswith('win32') session_fds = set() log = logging.getLogger('x84.engine') if not len(servers): raise ValueError("No servers configured for event loop! (ssh, telnet)") tap_events = CFG.getboolean('session', 'tap_events') check_ban = get_fail2ban_function() locks = dict() # message polling setup if CFG.has_option('msg', 'poll_interval'): from x84 import msgpoll msgpoll.start_polling() if CFG.has_section('web') and CFG.has_option('web', 'modules'): try: __import__("web") __import__("OpenSSL") import webserve module_names = CFG.get('web', 'modules', '').split(',') if module_names: web_modules = set(map(str.strip, module_names)) log.info('starting webmodules: {0!r}'.format(web_modules)) webserve.start(web_modules) except ImportError as err: log.error("section [web] enabled but not enabled: {0}".format(err)) while True: # shutdown, close & delete inactive clients, for server in servers: # bbs sessions that are no longer active on the socket # level -- send them a 'kill signal' for key, client in server.clients.items()[:]: if not client.is_active(): kill_session(client, 'socket shutdown') del server.clients[key] # on-connect negotiations that have completed or failed. # delete their thread instance from further evaluation for thread in [_thread for _thread in server.threads if _thread.stopped][:]: server.threads.remove(thread) server_fds = [server.server_socket.fileno() for server in servers] client_fds = [fd for fd in server.client_fds() for server in servers] check_r = server_fds + client_fds if not WIN32: session_fds = get_session_output_fds(servers) check_r += session_fds # We'd like to use timeout 'None', but the registration of # a new client in terminal.start_process surprises us with new # file descriptors for the session i/o. unless we loop for # additional `session_fds', a connecting client would block. ready_r, _, _ = select.select(check_r, [], [], SELECT_POLL) for fd in ready_r: # see if any new tcp connections were made server = find_server(servers, fd) if server is not None: accept(log, server, check_ban) # receive new data from tcp clients. client_recv(servers, log) terms = get_terminals() # receive new data from session terminals if WIN32 or set(session_fds) & set(ready_r): try: session_recv(locks, terms, log, tap_events) except IOError, err: # if the ipc closes while we poll, warn and continue log.warn(err) # send tcp data to clients client_send(terms, log) # send session data, poll for user-timeout and disconnect them session_send(terms)