diff options
Diffstat (limited to 'tcosmonitor/ftps_server.py')
-rw-r--r-- | tcosmonitor/ftps_server.py | 445 |
1 files changed, 445 insertions, 0 deletions
diff --git a/tcosmonitor/ftps_server.py b/tcosmonitor/ftps_server.py new file mode 100644 index 0000000..866f9dc --- /dev/null +++ b/tcosmonitor/ftps_server.py @@ -0,0 +1,445 @@ +# -*- coding: UTF-8 -*- +# +# http://svn.osafoundation.org/m2crypto/trunk/demo/medusa/ftps_server.py +# """An FTP/TLS server built on Medusa's ftp_server. +# +# Source taken from python-M2Crypto demo/medusa +# +# Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +# Python +import socket, string, sys, time + +# Medusa + +import asynchat, asyncore +from tcosmonitor.counter import counter +from medusa import ftp_server, logger + +# M2Crypto +from M2Crypto import SSL + +VERSION_STRING='0.09' + +class ftp_tls_channel(ftp_server.ftp_channel): + + """FTP/TLS server channel for Medusa.""" + + def __init__(self, server, ssl_ctx, conn, addr): + """Initialise the channel.""" + self.ssl_ctx = ssl_ctx + self.server = server + self.current_mode = 'a' + self.addr = addr + asynchat.async_chat.__init__(self, conn) + self.set_terminator('\r\n') + self.client_addr = (addr[0], 21) + self.client_dc = None + self.in_buffer = '' + self.closing = 0 + self.passive_acceptor = None + self.passive_connection = None + self.filesystem = None + self.authorized = 0 + self._ssl_accepting = 0 + self._ssl_accepted = 0 + self._pbsz = None + self._prot = None + resp = '220 %s M2Crypto (Medusa) FTP/TLS server v%s ready.' + self.respond(resp % (self.server.hostname, VERSION_STRING)) + + def writable(self): + return self._ssl_accepting or self._ssl_accepted + + def handle_read(self): + """Handle a read event.""" + if self._ssl_accepting: + self._ssl_accepted = self.socket.accept_ssl() + if self._ssl_accepted: + self._ssl_accepting = 0 + else: + try: + ftp_server.ftp_channel.handle_read(self) + except SSL.SSLError, what: + if str(what) == 'unexpected eof': + self.close() + else: + raise + + def handle_write(self): + """Handle a write event.""" + if self._ssl_accepting: + self._ssl_accepted = self.socket.accept_ssl() + if self._ssl_accepted: + self._ssl_accepting = 0 + else: + try: + ftp_server.ftp_channel.handle_write(self) + except SSL.SSLError, what: + if str(what) == 'unexpected eof': + self.close() + else: + raise + + def send(self, data): + """Send data over SSL.""" + try: + result = self.socket.send(data) + if result <= 0: + return 0 + else: + return result + except SSL.SSLError, what: + self.close() + self.log_info('send: closing channel %s %s' % (repr(self), what)) + return 0 + + def recv(self, buffer_size): + """Receive data over SSL.""" + try: + result = self.socket.recv(buffer_size) + if not result: + return '' + else: + return result + except SSL.SSLError, what: + self.close() + self.log_info('recv: closing channel %s %s' % (repr(self), what)) + return '' + + def found_terminator(self): + """Dispatch the FTP command.""" + line = self.in_buffer + if not len(line): + return + + sp = string.find(line, ' ') + if sp != -1: + line = [line[:sp], line[sp+1:]] + else: + line = [line] + + command = string.lower(line[0]) + if string.find(command, 'stor') != -1: + while command and command[0] not in string.letters: + command = command[1:] + + func_name = 'cmd_%s' % command + if command != 'pass': + self.log('<== %s' % repr(self.in_buffer)[1:-1]) + else: + self.log('<== %s' % line[0]+' <password>') + + self.in_buffer = '' + if not hasattr(self, func_name): + self.command_not_understood(line[0]) + return + + func = getattr(self, func_name) + if not self.check_command_authorization(command): + self.command_not_authorized(command) + else: + try: + result = apply(func, (line,)) + except: + self.server.total_exceptions.increment() + (file, func, line), t, v, tbinfo = asyncore.compact_traceback() + if self.client_dc: + try: + self.client_dc_close() + except: + pass + resp = '451 Server error: %s, %s: file %s line: %s' + self.respond(resp % (t, v, file, line)) + + def make_xmit_channel(self): + """Create a connection for sending data.""" + pa = self.passive_acceptor + if pa: + if pa.ready: + conn, addr = pa.ready + if self._prot: + cdc = tls_xmit_channel(self, conn, self.ssl_ctx, addr) + else: + cdc = ftp_server.xmit_channel(self, addr) + cdc.set_socket(conn) + cdc.connected = 1 + self.passive_acceptor.close() + self.passive_acceptor = None + else: + if self._prot: + cdc = tls_xmit_channel(self, None, self.ssl_ctx, None) + else: + cdc = ftp_server.xmit_channel(self) + else: + if self._prot: + cdc = tls_xmit_channel(self, None, self.ssl_ctx, self.client_addr) + else: + cdc = ftp_server.xmit_channel(self, self.client_addr) + cdc.create_socket(socket.AF_INET, socket.SOCK_STREAM) + if self.bind_local_minus_one: + cdc.bind(('', self.server.port - 1)) + try: + cdc.connect(self.client_addr) + except socket.error, what: + self.respond('425 Cannot build data connection') + self.client_dc = cdc + + def make_recv_channel(self, fd): + """Create a connection for receiving data.""" + pa = self.passive_acceptor + if pa: + if pa.ready: + conn, addr = pa.ready + if self._prot: + cdc = tls_recv_channel(self, conn, self.ssl_ctx, addr, fd) + else: + cdc = ftp_server.recv_channel(self, addr, fd) + cdc.set_socket(conn) + cdc.connected = 1 + self.passive_acceptor.close() + self.passive_acceptor = None + else: + if self._prot: + cdc = tls_recv_channel(self, None, self.ssl_ctx, None, fd) + else: + cdc = ftp_server.recv_channel(self, None, fd) + else: + if self._prot: + cdc = tls_recv_channel(self, None, self.ssl_ctx, self._prot, self.client_addr, fd) + else: + cdc = ftp_server.recv_channel(self, self.client_addr, fd) + cdc.create_socket(socket.AF_INET, socket.SOCK_STREAM) + try: + cdc.connect(self.client_addr) + except socket.error, what: + self.respond('425 Cannot build data connection') + self.client_dc = cdc + + def cmd_auth(self, line): + """Prepare for TLS operation.""" + # XXX Handle variations. + if line[1] != 'TLS': + self.command_not_understood (string.join(line)) + else: + self.respond('234 AUTH TLS successful') + self._ssl_accepting = 1 + self.socket = SSL.Connection(self.ssl_ctx, self.socket) + self.socket.setup_addr(self.addr) + self.socket.setup_ssl() + self.socket.set_accept_state() + self._ssl_accepted = self.socket.accept_ssl() + if self._ssl_accepted: + self._ssl_accepting = 0 + + def cmd_pbsz(self, line): + """Negotiate size of buffer for secure data transfer. For + FTP/TLS the only valid value for the parameter is '0'; any + other value is accepted but ignored.""" + if not (self._ssl_accepting or self._ssl_accepted): + return self.respond('503 AUTH TLS must be issued prior to PBSZ') + self._pbsz = 1 + self.respond('200 PBSZ=0 successful.') + + def cmd_prot(self, line): + """Negotiate the security level of the data connection.""" + if self._pbsz is None: + return self.respond('503 PBSZ must be issued prior to PROT') + if line[1] == 'C': + self.respond('200 Protection set to Clear') + self._pbsz = None + self._prot = None + elif line[1] == 'P': + self.respond('200 Protection set to Private') + self._prot = 1 + elif line[1] in ('S', 'E'): + self.respond('536 PROT %s unsupported' % line[1]) + else: + self.respond('504 PROT %s unsupported' % line[1]) + + +class ftp_tls_server(ftp_server.ftp_server): + + """FTP/TLS server for Medusa.""" + + SERVER_IDENT = 'M2Crypto FTP/TLS Server (v%s)' % VERSION_STRING + + ftp_channel_class = ftp_tls_channel + + def __init__(self, authz, ssl_ctx, host=None, ip='', port=21, resolver=None, log_obj=None): + """Initialise the server.""" + self.ssl_ctx = ssl_ctx + self.ip = ip + self.port = port + self.authorizer = authz + + if host is None: + self.hostname = socket.gethostname() + else: + self.hostname = host + + self.total_sessions = counter() + self.closed_sessions = counter() + self.total_files_out = counter() + self.total_files_in = counter() + self.total_bytes_out = counter() + self.total_bytes_in = counter() + self.total_exceptions = counter() + + asyncore.dispatcher.__init__(self) + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + self.set_reuse_addr() + self.bind((self.ip, self.port)) + self.listen(5) + + if log_obj is None: + log_obj = sys.stdout + + if resolver: + self.logger = logger.resolving_logger(resolver, log_obj) + else: + self.logger = logger.unresolving_logger(logger.file_logger(sys.stdout)) + + l = 'M2Crypto (Medusa) FTP/TLS server started at %s\n\tAuthz: %s\n\tHostname: %s\n\tPort: %d' + self.log_info(l % (time.ctime(time.time()), repr(self.authorizer), self.hostname, self.port)) + + def handle_accept(self): + """Accept a socket and dispatch a channel to handle it.""" + conn, addr = self.accept() + self.total_sessions.increment() + self.log_info('Connection from %s:%d' % addr) + self.ftp_channel_class(self, self.ssl_ctx, conn, addr) + + +class nbio_ftp_tls_actor: + + """TLS protocol negotiation mixin for FTP/TLS.""" + + def tls_init(self, sock, ssl_ctx, client_addr): + """Perform TLS protocol negotiation.""" + self.ssl_ctx = ssl_ctx + self.client_addr = client_addr + self._ssl_handshaking = 1 + self._ssl_handshake_ok = 0 + if sock: + self.socket = SSL.Connection(self.ssl_ctx, sock) + self.socket.setup_addr(self.client_addr) + self.socket.setup_ssl() + self._ssl_handshake_ok = self.socket.accept_ssl() + if self._ssl_handshake_ok: + self._ssl_handshaking = 0 + self.add_channel() + # else the client hasn't connected yet; when that happens, + # handle_connect() will be triggered. + + def tls_neg_ok(self): + """Return status of TLS protocol negotiation.""" + if self._ssl_handshaking: + self._ssl_handshake_ok = self.socket.accept_ssl() + if self._ssl_handshake_ok: + self._ssl_handshaking = 0 + return self._ssl_handshake_ok + + def handle_connect(self): + """Handle a data connection that occurs after this instance came + into being. When this handler is triggered, self.socket has been + created and refers to the underlying connected socket.""" + self.socket = SSL.Connection(self.ssl_ctx, self.socket) + self.socket.setup_addr(self.client_addr) + self.socket.setup_ssl() + self._ssl_handshake_ok = self.socket.accept_ssl() + if self._ssl_handshake_ok: + self._ssl_handshaking = 0 + self.add_channel() + + def send(self, data): + """Send data over SSL.""" + try: + result = self.socket.send(data) + if result <= 0: + return 0 + else: + return result + except SSL.SSLError, what: + self.close() + self.log_info('send: closing channel %s %s' % (repr(self), what)) + return 0 + + def recv(self, buffer_size): + """Receive data over SSL.""" + try: + result = self.socket.recv(buffer_size) + if not result: + return '' + else: + return result + except SSL.SSLError, what: + self.close() + self.log_info('recv: closing channel %s %s' % (repr(self), what)) + return '' + + +class tls_xmit_channel(nbio_ftp_tls_actor, ftp_server.xmit_channel): + + """TLS driver for a send-only data connection.""" + + def __init__(self, channel, conn, ssl_ctx, client_addr=None): + """Initialise the driver.""" + ftp_server.xmit_channel.__init__(self, channel, client_addr) + self.tls_init(conn, ssl_ctx, client_addr) + + def readable(self): + """This channel is readable iff TLS negotiation is in progress. + (Which implies a connected channel, of course.)""" + if not self.connected: + return 0 + else: + return self._ssl_handshaking + + def writable(self): + """This channel is writable iff TLS negotiation is in progress + or the application has data to send.""" + if self._ssl_handshaking: + return 1 + else: + return ftp_server.xmit_channel.writable(self) + + def handle_read(self): + """Handle a read event: either continue with TLS negotiation + or let the application handle this event.""" + if self.tls_neg_ok(): + ftp_server.xmit_channel.handle_read(self) + + def handle_write(self): + """Handle a write event: either continue with TLS negotiation + or let the application handle this event.""" + if self.tls_neg_ok(): + ftp_server.xmit_channel.handle_write(self) + + +class tls_recv_channel(nbio_ftp_tls_actor, ftp_server.recv_channel): + + """TLS driver for a receive-only data connection.""" + + def __init__(self, channel, conn, ssl_ctx, client_addr, fd): + """Initialise the driver.""" + ftp_server.recv_channel.__init__(self, channel, client_addr, fd) + self.tls_init(conn, ssl_ctx, client_addr) + + def writable(self): + """This channel is writable iff TLS negotiation is in progress.""" + return self._ssl_handshaking + + def handle_read(self): + """Handle a read event: either continue with TLS negotiation + or let the application handle this event.""" + if self.tls_neg_ok(): + ftp_server.recv_channel.handle_read(self) + + def handle_write(self): + """Handle a write event: either continue with TLS negotiation + or let the application handle this event.""" + if self.tls_neg_ok(): + ftp_server.recv_channel.handle_write(self) + + |