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.ini import CFG if sys.maxunicode == 65535: import warnings warnings.warn('Python not built with wide unicode support!') # retrieve list of managed servers servers = get_servers(CFG) 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]
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 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: import warnings warnings.warn('Python not built with wide unicode support!') # retrieve list of managed servers servers = get_servers(CFG) # begin unmanaged servers if get_ini(section='web', key='modules'): # start https server for one or more web modules. # # 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 web modules if # it cannot be resolved. 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]
def client_recv(servers, ready_fds, log): """ Test all clients for recv_ready(). If any data is available, then socket_recv() is called, buffering the data for the session which is exhausted in session_send(). """ from x84.bbs.exception import Disconnected from x84.terminal import kill_session for server in servers: for client in server.clients_ready(ready_fds): try: client.socket_recv() except Disconnected as err: log.debug('{client.addrport}: disconnect on recv: {err}' .format(client=client, err=err)) kill_session(client, 'disconnected: {err}'.format(err=err))
def client_send(terminals, log): """ Test all clients for send_ready(). If any data is available, then tty.client.send() is called. This is data sent from the session to the tcp client. """ from x84.bbs.exception import Disconnected from x84.terminal import kill_session # nothing to send until tty is registered. for sid, tty in terminals: if tty.client.send_ready(): try: tty.client.send() except Disconnected as err: log.debug('{client.addrport}: disconnect on send: {err}' .format(client=tty.client, err=err)) kill_session(tty.client, 'disconnected: {err}'.format(err=err))
def client_recv(servers, log): """ Test all clients for recv_ready(). If any data is available, then socket_recv() is called, buffering the data for the session which is exhausted in session_send(). """ from x84.bbs.exception import Disconnected from x84.terminal import kill_session for server in servers: for client in server.clients.values(): if client.recv_ready(): try: client.socket_recv() except Disconnected as err: log.debug('{client.addrport}: disconnect on recv: {err}' .format(client=client, err=err)) kill_session(client, 'disconnected: {err}'.format(err=err))
def client_recv(servers, ready_fds, log): """ Test all clients for recv_ready(). If any data is available, then ``client.socket_recv()`` is called, buffering the data for the session which is exhausted by :func:`session_send`. """ from x84.bbs.exception import Disconnected for server in servers: for client in server.clients_ready(ready_fds): try: client.socket_recv() except Disconnected as err: log.debug('{client.addrport}: disconnect on recv: {err}' .format(client=client, err=err)) kill_session(client, 'disconnected: {err}'.format(err=err))
def session_send(terminals): """ Test all tty clients for input_ready(). Meaning, tcp data has been buffered to be received by the tty session, and send it to the tty input queue (tty.master_write). Also, test all sessions for idle timeout, signaling exit to subprocess when reached. """ for _, tty in terminals: if tty.client.input_ready(): try: tty.master_write.send(('input', tty.client.get_input())) except IOError: # this may happen if a sub-process crashes, or more often, # because the subprocess has logged off, but the user kept # banging the keyboard before we have had the opportunity # to close their telnet socket. kill_session(tty.client, 'no tty for socket data') # poll about and kick off idle users elif tty.timeout and tty.client.idle() > tty.timeout: kill_session(tty.client, 'timeout')
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() 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)
def session_recv(locks, terminals, log, tap_events): """ receive data waiting for session; all data received from subprocess is in form (event, data), and is handled by ipc_recv. if stale is not None, the number of seconds elapsed since lock was first held is consider stale after that period of time, and is acquire anyway. """ # No actual Lock instances are held or released, just a simple dictionary # state/time tracking system. from x84.terminal import kill_session from x84.db import DBHandler for sid, tty in terminals: while tty.master_read.poll(): try: event, data = tty.master_read.recv() except (EOFError, IOError) as err: # sub-process unexpectedly closed msg_err = 'master_read pipe: {err}'.format(err=err) log.exception(msg_err) kill_session(tty.client, msg_err) break except TypeError: msg_err = 'unpickling error' log.exception(msg_err) break # 'exit' event, unregisters client if event == 'exit': kill_session(tty.client, 'client exit') break # 'logger' event, prefix log message with handle and IP address elif event == 'logger': data.msg = ('{data.handle}[{tty.sid}] {data.msg}' .format(data=data, tty=tty)) log.handle(data) # 'output' event, buffer for tcp socket elif event == 'output': tty.client.send_unicode(ucs=data[0], encoding=data[1]) # 'remote-disconnect' event, hunt and destroy elif event == 'remote-disconnect': send_to = data[0] reason = 'remote-disconnect by {sid.tty}'.format(sid=sid) for _sid, _tty in terminals: if send_to == _sid: kill_session(tty.client, reason) break # 'route': message passing directly from one session to another elif event == 'route': if tap_events: log.debug('route {0!r}'.format(data)) tgt_sid, send_event, send_val = data[0], data[1], data[2:] for _sid, _tty in terminals: if tgt_sid == _sid: _tty.master_write.send((send_event, send_val)) break # 'global': message broadcasting to all sessions elif event == 'global': if tap_events: log.debug('broadcast: {data!r}'.format(data=data)) for _sid, _tty in terminals: if sid != _sid: _tty.master_write.send((event, data,)) # 'set-timeout': set user-preferred timeout elif event == 'set-timeout': if tap_events: log.debug('[{tty.sid}] set-timeout {data}' .format(tty=tty, data=data)) tty.timeout = data # 'db*': access DBProxy API for shared sqlitedict elif event.startswith('db'): thread = DBHandler(tty.master_write, event, data) thread.start() # 'lock': access fine-grained bbs-global locking elif event.startswith('lock'): handle_lock(locks, tty, event, data, tap_events, log) else: log.error('[{tty.sid}] unhandled event, data: ' '({event}, {data})' .format(tty=tty, event=event, data=data))
def session_recv(locks, terminals, log, tap_events): """ Receive data waiting for terminal sessions. All data received from subprocess is handled here. """ for sid, tty in terminals: while tty.master_read.poll(): try: event, data = tty.master_read.recv() except (EOFError, IOError) as err: # sub-process unexpectedly closed log.exception('master_read pipe: {0}'.format(err)) kill_session(tty.client, 'master_read pipe: {0}'.format(err)) break except TypeError as err: log.exception('unpickling error: {0}'.format(err)) break # 'exit' event, unregisters client if event == 'exit': kill_session(tty.client, 'client exit') break # 'logger' event, prefix log message with handle and IP address elif event == 'logger': data.msg = ('{data.handle}[{tty.sid}] {data.msg}' .format(data=data, tty=tty)) log.handle(data) # 'output' event, buffer for tcp socket elif event == 'output': tty.client.send_unicode(ucs=data[0], encoding=data[1]) # 'remote-disconnect' event, hunt and destroy elif event == 'remote-disconnect': for _sid, _tty in terminals: # data[0] is 'send-to' address. if data[0] == _sid: kill_session( tty.client, 'remote-disconnect by {0}'.format(sid)) break # 'route': message passing directly from one session to another elif event == 'route': if tap_events: log.debug('route {0!r}'.format(data)) tgt_sid, send_event, send_val = data[0], data[1], data[2:] for _sid, _tty in terminals: if tgt_sid == _sid: _tty.master_write.send((send_event, send_val)) break # 'global': message broadcasting to all sessions elif event == 'global': if tap_events: log.debug('broadcast: {data!r}'.format(data=data)) for _sid, _tty in terminals: if sid != _sid: _tty.master_write.send((event, data,)) # 'set-timeout': set user-preferred timeout elif event == 'set-timeout': if tap_events: log.debug('[{tty.sid}] set-timeout {data}' .format(tty=tty, data=data)) tty.timeout = data # 'db*': access DBProxy API for shared sqlitedict elif event.startswith('db'): DBHandler(tty.master_write, event, data).start() # 'lock': access fine-grained bbs-global locking elif event.startswith('lock'): handle_lock(locks, tty, event, data, tap_events, log) else: log.error('[{tty.sid}] unhandled event, data: ' '({event}, {data})' .format(tty=tty, event=event, data=data))