def translate_ttype(ttype): from x84.bbs.ini import CFG log = logging.getLogger(__name__) termcap_unknown = CFG.get('system', 'termcap-unknown') termcap_ansi = CFG.get('system', 'termcap-ansi') if termcap_unknown != 'no' and ttype == 'unknown': log.debug("terminal-type {0!r} => {1!r}".format( ttype, termcap_unknown)) return termcap_unknown elif termcap_ansi != 'no' and ttype.lower().startswith('ansi'): log.debug("terminal-type {0!r} => {1!r}".format(ttype, termcap_ansi)) return termcap_ansi log.info("terminal type is {0!r}".format(ttype)) return ttype
def queue_for_network(self): " Queue message for networks, hosting or sending. " from x84.bbs import get_ini log = logging.getLogger(__name__) # server networks this server is a member of, member_networks = get_ini(section='msg', key='network_tags', split=True, splitsep=',') # server networks offered by this server, my_networks = get_ini(section='msg', key='server_tags', split=True, splitsep=',') # check all tags of message; if they match a message network, # either record for hosting servers, or schedule for delivery. for tag in self.tags: section = 'msgnet_{tag}'.format(tag=tag) # message is for a network we host if tag in my_networks: section = 'msgnet_{tag}'.format(tag=tag) transdb_name = CFG.get(section, 'trans_db_name') transdb = DBProxy(transdb_name) with transdb: self.body = u''.join((self.body, format_origin_line())) self.save() transdb[self.idx] = self.idx log.info('[{tag}] Stored for network (msgid {self.idx}).' .format(tag=tag, self=self)) # message is for a another network, queue for delivery elif tag in member_networks: queuedb_name = CFG.get(section, 'queue_db_name') queuedb = DBProxy(queuedb_name) with queuedb: queuedb[self.idx] = tag log.info('[{tag}] Message (msgid {self.idx}) queued ' 'for delivery'.format(tag=tag, self=self))
def translate_ttype(ttype): from x84.bbs.ini import CFG log = logging.getLogger(__name__) termcap_unknown = CFG.get('system', 'termcap-unknown') termcap_ansi = CFG.get('system', 'termcap-ansi') if termcap_unknown != 'no' and ttype == 'unknown': log.debug("terminal-type {0!r} => {1!r}" .format(ttype, termcap_unknown)) return termcap_unknown elif termcap_ansi != 'no' and ttype.lower().startswith('ansi'): log.debug("terminal-type {0!r} => {1!r}" .format(ttype, termcap_ansi)) return termcap_ansi log.info("terminal type is {0!r}".format(ttype)) return ttype
def get_digestpw(): """ Returns singleton to password digest routine. """ global FN_PASSWORD_DIGEST if FN_PASSWORD_DIGEST is not None: return FN_PASSWORD_DIGEST from x84.bbs.ini import CFG FN_PASSWORD_DIGEST = { 'bcrypt': _digestpw_bcrypt, 'internal': _digestpw_internal, 'plaintext': _digestpw_plaintext, }.get(CFG.get('system', 'password_digest')) return FN_PASSWORD_DIGEST
def connect_bot(botname): """ Make a zombie telnet connection to the board as the given bot. """ def read_forever(client): client.read_all() import telnetlib from x84.bbs.ini import CFG from x84.bbs.session import BOTLOCK, BOTQUEUE telnet_addr = CFG.get('telnet', 'addr') telnet_port = CFG.getint('telnet', 'port') with BOTLOCK: client = telnetlib.Telnet() client.set_option_negotiation_callback(callback_cmdopt) client.open(telnet_addr, telnet_port) BOTQUEUE.put(botname) t = threading.Thread(target=read_forever, args=(client,)) t.daemon = True t.start()
def GET(self, num=10): """ Return last x callers """ num = int(num) callers = DBProxy('lastcalls', use_session=False).items() last = sorted(callers[-num:], reverse=True, key=lambda caller: caller[1][0]) # output JSON instead? if 'json' in web.input(_method='get'): return json.dumps(last) callers_html = '' board = CFG.get('system', 'bbsname', 'x/84') page_title = 'Last {num} Callers to {board}'.format( num=num, board=board) for caller in last: callers_html += ''.join(('<li><b>{who}</b> {affil} ', '<small>at {when}</small></li>')).format( who=caller[0], affil='(%s)' % caller[1][2] if caller[1][2] else '', when=datetime.fromtimestamp(caller[1][0])) web.header('Content-Type', 'text/html; charset=utf-8', unique=True) output = """ <!DOCTYPE html> <html lang="en-US"> <head> <meta charset="utf-8" /> <title>{page_title}</title> </head> <body> <h1>{page_title}</h1> <ul> {callers_html} </ul> </body> </html> """.format(page_title=page_title, callers_html=callers_html) return output
def GET(self, num=10): """ Return last x oneliners """ num = int(num) oneliners = DBProxy('oneliner', use_session=False).items() oneliners = [(int(k), v) for (k, v) in DBProxy('oneliner', use_session=False).items()] last = oneliners[-num:] # output JSON instead? if 'json' in web.input(_method='get'): return json.dumps(last) board = CFG.get('system', 'bbsname', 'x/84') page_title = 'Last {num} Oneliners on {board}'.format(num=num, board=board) oneliners_html = '' for line in last: val = line[1] oneliners_html += '<li><b>{alias}:</b> {oneliner}</li>'.format( alias=val['alias'], oneliner=val['oneliner']) web.header('Content-Type', 'text/html; charset=utf-8', unique=True) output = """ <!DOCTYPE html> <html lang="en-US"> <head> <meta charset="utf-8" /> <title>{page_title}</title> </head> <body> <h1>{page_title}</h1> <ul> {oneliners_html} </ul> </body> </html> """.format(page_title=page_title, oneliners_html=oneliners_html) return output
def GET(self, num=10): """ Return last x oneliners """ num = int(num) oneliners = DBProxy('oneliner', use_session=False).items() oneliners = [(int(k), v) for (k, v) in DBProxy('oneliner', use_session=False).items()] last = oneliners[-num:] # output JSON instead? if 'json' in web.input(_method='get'): return json.dumps(last) board = CFG.get('system', 'bbsname', 'x/84') page_title = 'Last {num} Oneliners on {board}'.format( num=num, board=board) oneliners_html = '' for line in last: val = line[1] oneliners_html += '<li><b>{alias}:</b> {oneliner}</li>'.format( alias=val['alias'], oneliner=val['oneliner']) web.header('Content-Type', 'text/html; charset=utf-8', unique=True) output = """ <!DOCTYPE html> <html lang="en-US"> <head> <meta charset="utf-8" /> <title>{page_title}</title> </head> <body> <h1>{page_title}</h1> <ul> {oneliners_html} </ul> </body> </html> """.format(page_title=page_title, oneliners_html=oneliners_html) return output
def start(web_modules): """ fire up a web server with the given modules as endpoints """ from threading import Thread import logging import sys import os from x84.bbs.ini import CFG logger = logging.getLogger() sys.path.insert(0, os.path.expanduser(CFG.get('system', 'scriptpath'))) urls = ('/favicon.ico', 'favicon') funcs = globals() funcs['favicon'] = Favicon for mod in web_modules: module = None # first check for it in the scripttpath's webmodules dir try: module = __import__('webmodules.%s' % mod, fromlist=('webmodules',)) except ImportError: pass # fallback to the engine's webmodules dir if module is None: module = __import__('x84.webmodules.%s' % mod, fromlist=('x84.webmodules',)) api = module.web_module() urls += api['urls'] for key in api['funcs']: funcs[key] = api['funcs'][key] t = Thread(target=server_thread, args=(urls, funcs,)) t.daemon = True t.start() logger.info(u'Web modules: %s' % u', '.join(web_modules))
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 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 _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)