Source code for x84.rlogin

"""
rlogin server for x84.

This only exists to demonstrate alternative client protocols rather than
only ssh or telnet.  rlogin is a very insecure and not recommended!
"""
# http://www.ietf.org/rfc/rfc1282.txt

import logging
import select
import socket
import array
import errno
import time

# local
from x84.bbs.userbase import (
    check_new_user,
    check_bye_user,
    check_anonymous_user
)
from x84.bbs.exception import Disconnected
from x84.client import BaseClient, BaseConnect
from x84.server import BaseServer
from x84.terminal import spawn_client_session


[docs]class RLoginClient(BaseClient): """ rlogin protocol client handler. """ kind = 'rlogin' def __init__(self, sock, address_pair, on_naws=None): super(RLoginClient, self).__init__(sock, address_pair, on_naws) # Urgent send buffer (MSG_OOB) self.usend_buffer = array.array('c')
[docs] def recv_ready(self): """ Whether data is awaiting on the telnet socket. """ return (self.is_active() and bool( select.select([self.sock.fileno()], [], [], 0)[0]))
[docs] def send(self): """ Send any data buffered and return number of bytes send. :raises Disconnected: client has disconnected (cannot write to socket). """ if len(self.usend_buffer) > 0: ready_bytes = bytes(''.join(self.usend_buffer)) self.usend_buffer = array.array('c') def _send_urgent(send_bytes): """ Sent urgent (out of band) TCP packet. """ try: return self.sock.send(send_bytes, socket.MSG_OOB) except socket.error as err: if err.errno in (errno.EDEADLK, errno.EAGAIN): self.log.debug('{self.addrport}: {err} ' '(bandwidth exceed)' .format(self=self, err=err)) return 0 raise Disconnected('send: {0}'.format(err)) sent = _send_urgent(ready_bytes) if sent < len(ready_bytes): self.usend_buffer.fromstring(ready_bytes[sent:]) else: super(RLoginClient, self).send()
[docs] def send_ready(self): """ Whether any data is buffered for delivery. """ return bool(len(self.send_buffer) + len(self.usend_buffer))
[docs] def send_urgent_str(self, bstr): """ Buffer urgent (OOB) message to client from bytestring. """ self.usend_buffer.fromstring(bstr)
[docs]class ConnectRLogin(BaseConnect): """ rlogin protocol connection handler. Takes care of the (initial) handshake, terminal and session setup. """ #: maximum time elapsed allowed for on-connect negotiation TIME_NEGOTIATE = 5.0 #: poll interval for on-connect negotiation TIME_POLL = 0.10
[docs] def run(self): """ Perform rfc1282 (rlogin) connection establishment. Determine rlogin on-connect data, rlogin may only negotiate session user name and terminal type. """ try: self._set_socket_opts() self.banner() # Receive on-connect data-value pairs, may raise ValueError. data = self.get_connect_data() # parse into dict, parsed = self.parse_connect_data(data) for key, value in parsed.items(): if value: self.log.debug('{client.addrport}: {key}={value}' .format(client=self.client, key=key, value=value)) # and apply to session-local self.client.env. self.apply_environment(parsed) # The server returns a zero byte to indicate that it has received # these strings and is now in data transfer mode. if self.client.is_active(): self.client.send_str(bytes('\x00')) # The remote server indicates to the client that it can accept # window size change information by requesting a window size # message (as out of band data) just after connection # establishment and user identification exchange. The client # should reply to this request with the current window size. # # Disabled: neither SyncTERM or BSD rlogin honors this, and # we haven't got any code to parse it. Its in the RFC but .. self.client.send_urgent_str(bytes('\x80')) matrix_kwargs = {} username = parsed.get('server-user-name', 'new') if check_new_user(username): # new@ login may be allowed matrix_kwargs['new'] = True if check_bye_user(username): # rlogin as 'bye', 'logoff', etc. not allowed raise ValueError('Bye user {0!r} used by rlogin' .format(username)) if check_anonymous_user(username): # anonymous@ login may be allowed matrix_kwargs['anonymous'] = True if self.client.is_active(): return spawn_client_session(client=self.client, matrix_kwargs=matrix_kwargs) except socket.error as err: self.log.debug('{client.addrport}: connection closed: {err}' .format(client=self.client, err=err)) except EOFError: self.log.debug('{client.addrport}: EOF from client' .format(client=self.client)) except Exception as err: self.log.debug('{client.addrport}: connection closed: {err}' .format(client=self.client, err=err)) finally: self.stopped = True self.client.deactivate()
[docs] def get_connect_data(self): """ Receive four null-terminated strings transmitted by client on-connect. :return: bytes received, containing at least 4 NUL-terminated strings. :rtype: str :raises ValueError: on-connect data timeout or bandwidth exceeded. """ established_msg = ('{client.addrport}: rlogin connection established' .format(client=self.client)) data = array.array('c') #: maximum size of negotiation string MAXLEN = 4096 err = None st_time = time.time() while True: # allow time to pass for more data, time.sleep(self.TIME_POLL) if self.client.recv_ready(): self.client.socket_recv() if self.client.input_ready(): # data to be received, # read in data. data.fromstring(self.client.get_input()) n_nul = data.count('\x00') if n_nul >= 3: self.client.env['RLOGIN_CLIENT_NAME'] = { 3: 'SyncTERM', 4: 'BSD', }.get(n_nul, 'unknown:{0})'.format(n_nul)) if self.client.env['RLOGIN_CLIENT_NAME'] == 'SyncTERM': self.client.env['encoding'] = 'cp437' self.log.debug('{msg} ({env[RLOGIN_CLIENT_NAME]})' .format(msg=established_msg, env=self.client.env)) return data.tostring() elif time.time() - st_time >= self.TIME_NEGOTIATE: # too much time has elapsed, give up. err = 'rlogin on-connect timeout' elif len(data) >= MAXLEN: # client has sent an abusive number of bytes, disconnect. err = 'rlogin bandwidth exceeded' if err: raise ValueError(err)
[docs] def apply_environment(self, parsed): """ Cherry-pick rlogin values into client environment variables. :param dict parsed: values identified by class method ``parse_connect_data()`` :rtype: None """ # Only terminal-type environment variable is propagated from client, we # can also discern their USER by 'client-user-name', which we would # expect to be analogous to telnet environment value USER. self.client.env['TERM'] = parsed.get('terminal-type', 'vt220') if 'client-user-name' in parsed: self.client.env['USER'] = parsed.get('client-user-name')
[docs] def parse_connect_data(self, data): """ Parse and return raw data received by client on-connect. :param str data: bytes received by class method get_connect_data(). :return: dictionary containing pertinent key/values :rtype: dict """ parsed = dict() try: # Upon connection establishment, the client sends four # null-terminated strings to the server. The first is an empty # string (i.e., it consists solely of a single zero byte), followed # by three non-null strings: the client username, the server # username, and the terminal type and speed. More explicitly: # # <null> # client-user-name<null> # server-user-name<null> # terminal-type/speed<null> segs = data.split('\x00') # SyncTerm leaves a null-terminating byte. if len(segs) == 5 and segs[4] == '': segs.pop(4) for segname in ('null', 'client-user-name', 'server-user-name', 'terminal-type/speed'): if len(segs): parsed[segname] = segs.pop(0) parsed['terminal-type'], parsed['terminal-speed'] = ( parsed.pop('terminal-type/speed', 'unknown/0') .split('/', 2)) except ValueError as err: self.log.exception("ValueError in parse_connect_data: {err}" .format(err=err)) return parsed
def _set_sock_opts(self): """ Set the socket in non-blocking mode. """ self.client.sock.setblocking(0) self.client.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
[docs]class RLoginServer(BaseServer): """ RLogin/RSH protocol server. """ client_factory = RLoginClient connect_factory = ConnectRLogin def __init__(self, config): """ Class initializer. """ self.log = logging.getLogger(__name__) self.config = config self.addr = config.get('rlogin', 'addr') self.port = 513 if config.has_option('rlogin', 'port'): # rlogin is coded for port 513, though you could specify an # alternative port if you really wished. self.port = config.getint('rlogin', 'port') # bind self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: self.server_socket.bind((self.addr, self.port)) self.server_socket.listen(self.LISTEN_BACKLOG) except socket.error as err: self.log.error('unable to bind {self.addr}:{self.port}: {err}' .format(self=self, err=err)) exit(1) self.log.info('rlogin listening on {self.addr}:{self.port}/tcp' .format(self=self))
[docs] def client_fds(self): """ Return list of rlogin client file descriptors. """ fds = [client.fileno() for client in self.clients.values()] # pylint: disable=bad-builtin # You're drunk, pylint return filter(None, fds)