# -*- coding: utf-8 -*- line endings: unix -*- #------------------------------------------------------------------------------ # miniboa/async.py # Copyright 2009 Jim Storch # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain a # copy of the License at http://www.apache.org/licenses/LICENSE-2.0 # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. #------------------------------------------------------------------------------ # Changes made by pR0Ps.CM[at]gmail[dot]com on 18/07/2012 # -Updated for use with Python 3.x # -Repackaged into a single file to simplify distribution # -Other misc fixes and changes # # Report any bugs in this implementation to me (email above) #------------------------------------------------------------------------------ # Additional changes by Quixadhal on 2014.06.16 # -Re-split code into multiple files, for ease of maintenance # -Rewrote terminal system #------------------------------------------------------------------------------ """ Handle Asynchronous Telnet Connections. """ import socket import select import sys from miniboa.telnet import TelnetClient from miniboa.telnet import ConnectionLost ## Cap sockets to 512 on Windows because winsock can only process 512 at time if sys.platform == 'win32': MAX_CONNECTIONS = 500 ## Cap sockets to 1000 on Linux because you can only have 1024 file descriptors else: MAX_CONNECTIONS = 1000 #--[ Telnet Server ]----------------------------------------------------------- ## Default connection handler def _on_connect(client): """ Placeholder new connection handler. """ logging.info("++ Opened connection to {}, sending greeting...".format(client.addrport())) client.send("Greetings from Miniboa-py3!\n") ## Default disconnection handler def _on_disconnect(client): """ Placeholder lost connection handler. """ logging.info("-- Lost connection to %s".format(client.addrport())) class TelnetServer(object): """ Poll sockets for new connections and sending/receiving data from clients. """ def __init__(self, port = 23, address = '', on_connect = _on_connect, on_disconnect = _on_disconnect, max_connections = MAX_CONNECTIONS, timeout = 0.1): """ Create a new Telnet Server. port -- Port to listen for new connection on. On UNIX-like platforms, you made need root access to use ports under 1025. address -- Address of the LOCAL network interface to listen on. You can usually leave this blank unless you want to restrict traffic to a specific network device. This will usually NOT be the same as the Internet address of your server. on_connect -- function to call with new telnet connections on_disconnect -- function to call when a client's connection dies, either through a terminated session or client.active being set to False. max_connections -- maximum simultaneous the server will accept at once timeout -- amount of time that Poll() will wait from user input before returning. Also frees a slice of CPU time. """ self.port = port self.address = address self.on_connect = on_connect self.on_disconnect = on_disconnect self.max_connections = min(max_connections, MAX_CONNECTIONS) self.timeout = timeout server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: server_socket.bind((address, port)) server_socket.listen(5) except socket.err as err: logging.critical("Unable to create the server socket: " + str(err)) raise self.server_socket = server_socket self.server_fileno = server_socket.fileno() ## Dictionary of active clients, ## key = file descriptor, value = TelnetClient instance self.clients = {} def stop(self): """ Disconnects the clients and shuts down the server """ for clients in self.client_list(): clients.sock.close() self.server_socket.close() ## TODO: Anything else need doing? def client_count(self): """ Returns the number of active connections. """ return len(self.clients) def client_list(self): """ Returns a list of connected clients. """ return self.clients.values() def poll(self): """ Perform a non-blocking scan of recv and send states on the server and client connection sockets. Process new connection requests, read incomming data, and send outgoing data. Sends and receives may be partial. """ ## Build a list of connections to test for receive data pending recv_list = [self.server_fileno] # always add the server del_list = [] # list of clients to delete after polling for client in self.clients.values(): if client.active: recv_list.append(client.fileno) else: self.on_disconnect(client) del_list.append(client.fileno) ## Delete inactive connections from the dictionary for client in del_list: del self.clients[client] ## Build a list of connections that need to send data send_list = [] for client in self.clients.values(): if client.send_pending: send_list.append(client.fileno) ## Get active socket file descriptors from select.select() try: rlist, slist, elist = select.select(recv_list, send_list, [], self.timeout) except select.error as err: ## If we can't even use select(), game over man, game over logging.critical("SELECT socket error '{}'".format(str(err))) raise ## Process socket file descriptors with data to recieve for sock_fileno in rlist: ## If it's coming from the server's socket then this is a new ## connection request. if sock_fileno == self.server_fileno: try: sock, addr_tup = self.server_socket.accept() except socket.error as err: logging.error("ACCEPT socket error '{}:{}'.".format(err[0], err[1])) continue #Check for maximum connections if self.client_count() >= self.max_connections: logging.warning("Refusing new connection, maximum already in use.") sock.close() continue ## Create the client instance new_client = TelnetClient(sock, addr_tup) ## Add the connection to our dictionary and call handler self.clients[new_client.fileno] = new_client self.on_connect(new_client) else: ## Call the connection's recieve method try: self.clients[sock_fileno].socket_recv() except ConnectionLost: self.clients[sock_fileno].deactivate() ## Process sockets with data to send for sock_fileno in slist: ## Call the connection's send method self.clients[sock_fileno].socket_send()