├── .gitignore ├── miniboa ├── __init__.py ├── xterm.py ├── async.py └── telnet.py ├── hello_demo.py ├── handler_demo.py ├── README.md └── chat_demo.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .py[cod] 3 | -------------------------------------------------------------------------------- /miniboa/__init__.py: -------------------------------------------------------------------------------- 1 | from .async import TelnetServer 2 | -------------------------------------------------------------------------------- /hello_demo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | As simple as it gets. 5 | 6 | Launch the Telnet server on the default port and greet visitors using the 7 | placeholder 'on_connect()' function. Does nothing else. 8 | """ 9 | 10 | import logging 11 | from miniboa import TelnetServer 12 | 13 | if __name__ == "__main__": 14 | logging.basicConfig(level=logging.DEBUG) 15 | 16 | server = TelnetServer() 17 | 18 | logging.info("Starting server on port {}. CTRL-C to interrupt.".format(server.port)) 19 | while True: 20 | server.poll() 21 | -------------------------------------------------------------------------------- /handler_demo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Example of using on_connect and on_disconnect handlers. 5 | """ 6 | 7 | import logging 8 | from miniboa import TelnetServer 9 | 10 | CLIENTS = [] 11 | 12 | def my_on_connect(client): 13 | """ 14 | Example on_connect handler. 15 | """ 16 | client.send('You connected from %s\n' % client.addrport()) 17 | if CLIENTS: 18 | client.send('Also connected are:\n') 19 | for neighbor in CLIENTS: 20 | client.send('%s\n' % neighbor.addrport()) 21 | else: 22 | client.send('Sadly, you are alone.\n') 23 | CLIENTS.append(client) 24 | 25 | 26 | def my_on_disconnect(client): 27 | """ 28 | Example on_disconnect handler. 29 | """ 30 | CLIENTS.remove(client) 31 | 32 | 33 | if __name__ == "__main__": 34 | logging.basicConfig(level=logging.DEBUG) 35 | 36 | server = TelnetServer() 37 | server.on_connect=my_on_connect 38 | server.on_disconnect=my_on_disconnect 39 | 40 | logging.info("Starting server on port {}. CTRL-C to interrupt.".format(server.port)) 41 | while True: 42 | server.poll() 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------------------------- 2 | 3 | This project is outdated. For a more active fork see https://github.com/shmup/miniboa 4 | 5 | ------------------------------------------------------------------------------------- 6 | 7 | Miniboa-py3 8 | =========== 9 | 10 | Miniboa-py3 is an asynchronous, single-threaded, poll-based Telnet server 11 | written in Python. It supports many users (512 on Windows, 1000 on Unix) and 12 | is fully cross-platform. 13 | 14 | This module is ideal for everything from MUD servers to services requiring an 15 | administration interface. 16 | 17 | Miniboa-py3 is a fork of [Jim Storch's miniboa](https://code.google.com/p/miniboa/) 18 | updated with full support for Python 3. 19 | 20 | For full documentation, see the miniboa project page. 21 | 22 | License 23 | ======= 24 | ``` 25 | Copyright 2009 Jim Storch 26 | Copyright 2015 Carey Metcalfe 27 | 28 | Licensed under the Apache License, Version 2.0 (the "License"); 29 | you may not use this file except in compliance with the License. 30 | You may obtain a copy of the License at 31 | 32 | http://www.apache.org/licenses/LICENSE-2.0 33 | 34 | Unless required by applicable law or agreed to in writing, software 35 | distributed under the License is distributed on an "AS IS" BASIS, 36 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 37 | See the License for the specific language governing permissions and 38 | limitations under the License. 39 | ``` 40 | -------------------------------------------------------------------------------- /miniboa/xterm.py: -------------------------------------------------------------------------------- 1 | """ 2 | Support for color and formatting for Xterm style clients. 3 | """ 4 | 5 | import re 6 | 7 | _PARA_BREAK = re.compile(r"(\n\s*\n)", re.MULTILINE) 8 | 9 | #--[ Caret Code to ANSI TABLE ]------------------------------------------------ 10 | 11 | _ANSI_CODES = ( 12 | ( '^k', '\x1b[22;30m' ), # black 13 | ( '^K', '\x1b[1;30m' ), # bright black (grey) 14 | ( '^r', '\x1b[22;31m' ), # red 15 | ( '^R', '\x1b[1;31m' ), # bright red 16 | ( '^g', '\x1b[22;32m' ), # green 17 | ( '^G', '\x1b[1;32m' ), # bright green 18 | ( '^y', '\x1b[22;33m' ), # yellow 19 | ( '^Y', '\x1b[1;33m' ), # bright yellow 20 | ( '^b', '\x1b[22;34m' ), # blue 21 | ( '^B', '\x1b[1;34m' ), # bright blue 22 | ( '^m', '\x1b[22;35m' ), # magenta 23 | ( '^M', '\x1b[1;35m' ), # bright magenta 24 | ( '^c', '\x1b[22;36m' ), # cyan 25 | ( '^C', '\x1b[1;36m' ), # bright cyan 26 | ( '^w', '\x1b[22;37m' ), # white 27 | ( '^W', '\x1b[1;37m' ), # bright white 28 | ( '^0', '\x1b[40m' ), # black background 29 | ( '^1', '\x1b[41m' ), # red background 30 | ( '^2', '\x1b[42m' ), # green background 31 | ( '^3', '\x1b[43m' ), # yellow background 32 | ( '^4', '\x1b[44m' ), # blue background 33 | ( '^5', '\x1b[45m' ), # magenta background 34 | ( '^6', '\x1b[46m' ), # cyan background 35 | ( '^d', '\x1b[39m' ), # default (should be white on black) 36 | ( '^I', '\x1b[7m' ), # inverse text on 37 | ( '^i', '\x1b[27m' ), # inverse text off 38 | ( '^~', '\x1b[0m' ), # reset all 39 | ( '^U', '\x1b[4m' ), # underline on 40 | ( '^u', '\x1b[24m' ), # underline off 41 | ( '^!', '\x1b[1m' ), # bold on 42 | ( '^.', '\x1b[22m'), # bold off 43 | ( '^s', '\x1b[2J'), # clear screen 44 | ( '^l', '\x1b[2K'), # clear to end of line 45 | ) 46 | 47 | 48 | def strip_caret_codes(text): 49 | """ 50 | Strip out any caret codes from a string. 51 | """ 52 | # temporarily escape out ^^ 53 | text = text.replace('^^', '\x00') 54 | for token, foo in _ANSI_CODES: 55 | text = text.replace(token, '') 56 | return text.replace('\x00', '^') 57 | 58 | 59 | def colorize(text, ansi=True): 60 | """ 61 | If the client wants ansi, replace the tokens with ansi sequences -- 62 | otherwise, simply strip them out. 63 | """ 64 | if ansi: 65 | text = text.replace('^^', '\x00') 66 | for token, code in _ANSI_CODES: 67 | text = text.replace(token, code) 68 | text = text.replace('\x00', '^') 69 | else: 70 | text = strip_caret_codes(text) 71 | return text 72 | 73 | 74 | def word_wrap(text, columns=80, indent=4, padding=2): 75 | """ 76 | Given a block of text, breaks into a list of lines wrapped to 77 | length. 78 | """ 79 | paragraphs = _PARA_BREAK.split(text) 80 | lines = [] 81 | columns -= padding 82 | for para in paragraphs: 83 | if para.isspace(): 84 | continue 85 | line = ' ' * indent 86 | for word in para.split(): 87 | if (len(line) + 1 + len(word)) > columns: 88 | lines.append(line) 89 | line = ' ' * padding 90 | line += word 91 | else: 92 | line += ' ' + word 93 | if not line.isspace(): 94 | lines.append(line) 95 | return lines 96 | -------------------------------------------------------------------------------- /chat_demo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Chat Room Demo for Miniboa. 5 | """ 6 | 7 | import logging 8 | from miniboa import TelnetServer 9 | 10 | IDLE_TIMEOUT = 300 11 | CLIENT_LIST = [] 12 | SERVER_RUN = True 13 | 14 | 15 | def on_connect(client): 16 | """ 17 | Sample on_connect function. 18 | Handles new connections. 19 | """ 20 | logging.info("Opened connection to {}".format(client.addrport())) 21 | broadcast("{} joins the conversation.\n".format(client.addrport())) 22 | CLIENT_LIST.append(client) 23 | client.send("Welcome to the Chat Server, {}.\n".format(client.addrport())) 24 | 25 | 26 | def on_disconnect(client): 27 | """ 28 | Sample on_disconnect function. 29 | Handles lost connections. 30 | """ 31 | logging.info("Lost connection to {}".format(client.addrport())) 32 | CLIENT_LIST.remove(client) 33 | broadcast("{} leaves the conversation.\n".format(client.addrport())) 34 | 35 | 36 | def kick_idle(): 37 | """ 38 | Looks for idle clients and disconnects them by setting active to False. 39 | """ 40 | # Who hasn't been typing? 41 | for client in CLIENT_LIST: 42 | if client.idle() > IDLE_TIMEOUT: 43 | logging.info("Kicking idle lobby client from {}".format(client.addrport())) 44 | client.active = False 45 | 46 | 47 | def process_clients(): 48 | """ 49 | Check each client, if client.cmd_ready == True then there is a line of 50 | input available via client.get_command(). 51 | """ 52 | for client in CLIENT_LIST: 53 | if client.active and client.cmd_ready: 54 | # If the client sends input echo it to the chat room 55 | chat(client) 56 | 57 | 58 | def broadcast(msg): 59 | """ 60 | Send msg to every client. 61 | """ 62 | for client in CLIENT_LIST: 63 | client.send(msg) 64 | 65 | 66 | def chat(client): 67 | """ 68 | Echo whatever client types to everyone. 69 | """ 70 | global SERVER_RUN 71 | msg = client.get_command() 72 | logging.info("{} says '{}'".format(client.addrport(), msg)) 73 | 74 | for guest in CLIENT_LIST: 75 | if guest != client: 76 | guest.send("{} says '{}'\n".format(client.addrport(), msg)) 77 | else: 78 | guest.send("You say '{}'\n".format(msg)) 79 | 80 | cmd = msg.lower() 81 | # bye = disconnect 82 | if cmd == 'bye': 83 | client.active = False 84 | # shutdown == stop the server 85 | elif cmd == 'shutdown': 86 | SERVER_RUN = False 87 | 88 | 89 | if __name__ == '__main__': 90 | 91 | # Simple chat server to demonstrate connection handling via the 92 | # async and telnet modules. 93 | 94 | logging.basicConfig(level=logging.DEBUG) 95 | 96 | # Create a telnet server with a port, address, 97 | # a function to call with new connections 98 | # and one to call with lost connections. 99 | 100 | telnet_server = TelnetServer( 101 | port=7777, 102 | address='', 103 | on_connect=on_connect, 104 | on_disconnect=on_disconnect, 105 | timeout = .05 106 | ) 107 | 108 | logging.info("Listening for connections on port {}. CTRL-C to break.".format(telnet_server.port)) 109 | 110 | # Server Loop 111 | while SERVER_RUN: 112 | telnet_server.poll() # Send, Recv, and look for new connections 113 | kick_idle() # Check for idle clients 114 | process_clients() # Check for client input 115 | 116 | logging.info("Server shutdown.") 117 | -------------------------------------------------------------------------------- /miniboa/async.py: -------------------------------------------------------------------------------- 1 | """ 2 | Handle Asynchronous Telnet Connections. 3 | """ 4 | 5 | import socket 6 | import select 7 | import sys 8 | import logging 9 | 10 | from .telnet import TelnetClient 11 | from .telnet import ConnectionLost 12 | 13 | # Cap sockets to 512 on Windows because winsock can only process 512 at time 14 | # Cap sockets to 1000 on UNIX because you can only have 1024 file descriptors 15 | MAX_CONNECTIONS = 500 if sys.platform == 'win32' else 1000 16 | 17 | #-----------------------------------------------------Dummy Connection Handlers 18 | 19 | def _on_connect(client): 20 | """ 21 | Placeholder new connection handler. 22 | """ 23 | logging.info("++ Opened connection to {}, sending greeting...".format(client.addrport())) 24 | client.send("Greetings from Miniboa-py3!\n") 25 | 26 | def _on_disconnect(client): 27 | """ 28 | Placeholder lost connection handler. 29 | """ 30 | logging.info ("-- Lost connection to %s".format(client.addrport())) 31 | 32 | #-----------------------------------------------------------------Telnet Server 33 | 34 | class TelnetServer(object): 35 | """ 36 | Poll sockets for new connections and sending/receiving data from clients. 37 | """ 38 | def __init__(self, port=7777, address='', on_connect=_on_connect, 39 | on_disconnect=_on_disconnect, max_connections=MAX_CONNECTIONS, 40 | timeout=0.05): 41 | """ 42 | Create a new Telnet Server. 43 | 44 | port -- Port to listen for new connection on. On UNIX-like platforms, 45 | you made need root access to use ports under 1025. 46 | 47 | address -- Address of the LOCAL network interface to listen on. You 48 | can usually leave this blank unless you want to restrict traffic 49 | to a specific network device. This will usually NOT be the same 50 | as the Internet address of your server. 51 | 52 | on_connect -- function to call with new telnet connections 53 | 54 | on_disconnect -- function to call when a client's connection dies, 55 | either through a terminated session or client.active being set 56 | to False. 57 | 58 | max_connections -- maximum simultaneous the server will accept at once 59 | 60 | timeout -- amount of time that Poll() will wait from user input 61 | before returning. Also frees a slice of CPU time. 62 | """ 63 | 64 | self.port = port 65 | self.address = address 66 | self.on_connect = on_connect 67 | self.on_disconnect = on_disconnect 68 | self.max_connections = min(max_connections, MAX_CONNECTIONS) 69 | self.timeout = timeout 70 | 71 | server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 72 | server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 73 | 74 | try: 75 | server_socket.bind((address, port)) 76 | server_socket.listen(5) 77 | except socket.error as err: 78 | logging.critical("Unable to create the server socket: " + str(err)) 79 | raise 80 | 81 | self.server_socket = server_socket 82 | self.server_fileno = server_socket.fileno() 83 | 84 | # Dictionary of active clients, 85 | # key = file descriptor, value = TelnetClient (see miniboa.telnet) 86 | self.clients = {} 87 | 88 | def stop(self): 89 | """ 90 | Disconnects the clients and shuts down the server 91 | """ 92 | for clients in self.client_list(): 93 | clients.sock.close() 94 | self.server_socket.close() 95 | 96 | def client_count(self): 97 | """ 98 | Returns the number of active connections. 99 | """ 100 | return len(self.clients) 101 | 102 | def client_list(self): 103 | """ 104 | Returns a list of connected clients. 105 | """ 106 | return self.clients.values() 107 | 108 | 109 | def poll(self): 110 | """ 111 | Perform a non-blocking scan of recv and send states on the server 112 | and client connection sockets. Process new connection requests, 113 | read incomming data, and send outgoing data. Sends and receives may 114 | be partial. 115 | """ 116 | # Build a list of connections to test for receive data pending 117 | recv_list = [self.server_fileno] # always add the server 118 | 119 | del_list = [] # list of clients to delete after polling 120 | 121 | for client in self.clients.values(): 122 | if client.active: 123 | recv_list.append(client.fileno) 124 | else: 125 | self.on_disconnect(client) 126 | del_list.append(client.fileno) 127 | 128 | # Delete inactive connections from the dictionary 129 | for client in del_list: 130 | del self.clients[client] 131 | 132 | # Build a list of connections that need to send data 133 | send_list = [] 134 | for client in self.clients.values(): 135 | if client.send_pending: 136 | send_list.append(client.fileno) 137 | 138 | # Get active socket file descriptors from select.select() 139 | try: 140 | rlist, slist, elist = select.select(recv_list, send_list, [], 141 | self.timeout) 142 | except select.error as err: 143 | # If we can't even use select(), game over man, game over 144 | logging.critical("SELECT socket error '{}'".format(str(err))) 145 | raise 146 | 147 | # Process socket file descriptors with data to recieve 148 | for sock_fileno in rlist: 149 | 150 | # If it's coming from the server's socket then this is a new connection request. 151 | if sock_fileno == self.server_fileno: 152 | 153 | try: 154 | sock, addr_tup = self.server_socket.accept() 155 | except socket.error as err: 156 | logging.error("ACCEPT socket error '{}:{}'.".format(err[0], err[1])) 157 | continue 158 | 159 | # Check for maximum connections 160 | if self.client_count() >= self.max_connections: 161 | logging.warning("Refusing new connection, maximum already in use.") 162 | sock.close() 163 | continue 164 | 165 | # Create the client instance 166 | new_client = TelnetClient(sock, addr_tup) 167 | 168 | # Add the connection to our dictionary and call handler 169 | self.clients[new_client.fileno] = new_client 170 | self.on_connect(new_client) 171 | 172 | else: 173 | # Call the connection's recieve method 174 | try: 175 | self.clients[sock_fileno].socket_recv() 176 | except ConnectionLost: 177 | self.clients[sock_fileno].deactivate() 178 | 179 | # Process sockets with data to send 180 | for sock_fileno in slist: 181 | # Call the connection's send method 182 | self.clients[sock_fileno].socket_send() 183 | -------------------------------------------------------------------------------- /miniboa/telnet.py: -------------------------------------------------------------------------------- 1 | """ 2 | Manage one Telnet client connected via a TCP/IP socket. 3 | """ 4 | 5 | import socket 6 | import time 7 | import logging 8 | 9 | from .xterm import colorize 10 | from .xterm import word_wrap 11 | 12 | 13 | #---[ Telnet Notes ]----------------------------------------------------------- 14 | # (See RFC 854 for more information) 15 | # 16 | # Negotiating a Local Option 17 | # -------------------------- 18 | # 19 | # Side A begins with: 20 | # 21 | # "IAC WILL/WONT XX" Meaning "I would like to [use|not use] option XX." 22 | # 23 | # Side B replies with either: 24 | # 25 | # "IAC DO XX" Meaning "OK, you may use option XX." 26 | # "IAC DONT XX" Meaning "No, you cannot use option XX." 27 | # 28 | # 29 | # Negotiating a Remote Option 30 | # ---------------------------- 31 | # 32 | # Side A begins with: 33 | # 34 | # "IAC DO/DONT XX" Meaning "I would like YOU to [use|not use] option XX." 35 | # 36 | # Side B replies with either: 37 | # 38 | # "IAC WILL XX" Meaning "I will begin using option XX" 39 | # "IAC WONT XX" Meaning "I will not begin using option XX" 40 | # 41 | # 42 | # The syntax is designed so that if both parties receive simultaneous requests 43 | # for the same option, each will see the other's request as a positive 44 | # acknowledgement of it's own. 45 | # 46 | # If a party receives a request to enter a mode that it is already in, the 47 | # request should not be acknowledged. 48 | 49 | UNKNOWN = -1 50 | 51 | #--[ Telnet Commands ]--------------------------------------------------------- 52 | 53 | SE = chr(240) # End of subnegotiation parameters 54 | NOP = chr(241) # No operation 55 | DATMK = chr(242) # Data stream portion of a sync. 56 | BREAK = chr(243) # NVT Character BRK 57 | IP = chr(244) # Interrupt Process 58 | AO = chr(245) # Abort Output 59 | AYT = chr(246) # Are you there 60 | EC = chr(247) # Erase Character 61 | EL = chr(248) # Erase Line 62 | GA = chr(249) # The Go Ahead Signal 63 | SB = chr(250) # Sub-option to follow 64 | WILL = chr(251) # Will; request or confirm option begin 65 | WONT = chr(252) # Wont; deny option request 66 | DO = chr(253) # Do = Request or confirm remote option 67 | DONT = chr(254) # Don't = Demand or confirm option halt 68 | IAC = chr(255) # Interpret as Command 69 | SEND = chr( 1) # Sub-process negotiation SEND command 70 | IS = chr( 0) # Sub-process negotiation IS command 71 | 72 | #--[ Telnet Options ]---------------------------------------------------------- 73 | 74 | BINARY = chr( 0) # Transmit Binary 75 | ECHO = chr( 1) # Echo characters back to sender 76 | RECON = chr( 2) # Reconnection 77 | SGA = chr( 3) # Suppress Go-Ahead 78 | TTYPE = chr( 24) # Terminal Type 79 | NAWS = chr( 31) # Negotiate About Window Size 80 | LINEMO = chr( 34) # Line Mode 81 | 82 | #-----------------------------------------------------Connection Lost Exception 83 | 84 | class ConnectionLost(Exception): 85 | """ 86 | Custom exception to signal a lost connection to the Telnet Server. 87 | """ 88 | pass 89 | 90 | #-----------------------------------------------------------------Telnet Option 91 | 92 | class TelnetOption(object): 93 | """ 94 | Simple class used to track the status of an extended Telnet option. 95 | """ 96 | def __init__(self): 97 | self.local_option = UNKNOWN # Local state of an option 98 | self.remote_option = UNKNOWN # Remote state of an option 99 | self.reply_pending = False # Are we expecting a reply? 100 | 101 | #------------------------------------------------------------------------Telnet 102 | 103 | class TelnetClient(object): 104 | """ 105 | Represents a client connection via Telnet. 106 | 107 | First argument is the socket discovered by the Telnet Server. 108 | Second argument is the tuple (ip address, port number). 109 | """ 110 | 111 | def __init__(self, sock, addr_tup): 112 | self.protocol = 'telnet' 113 | self.active = True # Turns False when the connection is lost 114 | self.sock = sock # The connection's socket 115 | self.fileno = sock.fileno() # The socket's file descriptor 116 | self.address = addr_tup[0] # The client's remote TCP/IP address 117 | self.port = addr_tup[1] # The client's remote port 118 | self.terminal_type = 'ANSI' # set via request_terminal_type() 119 | self.use_ansi = True 120 | self.columns = 80 121 | self.rows = 24 122 | self.send_pending = False 123 | self.send_buffer = '' 124 | self.recv_buffer = '' 125 | self.bytes_sent = 0 126 | self.bytes_received = 0 127 | self.cmd_ready = False 128 | self.command_list = [] 129 | self.connect_time = time.time() 130 | self.last_input_time = time.time() 131 | 132 | # State variables for interpreting incoming telnet commands 133 | self.telnet_got_iac = False # Are we inside an IAC sequence? 134 | self.telnet_got_cmd = None # Did we get a telnet command? 135 | self.telnet_got_sb = False # Are we inside a subnegotiation? 136 | self.telnet_opt_dict = {} # Mapping for up to 256 TelnetOptions 137 | self.telnet_echo = False # Echo input back to the client? 138 | self.telnet_echo_password = False # Echo back '*' for passwords? 139 | self.telnet_sb_buffer = '' # Buffer for sub-negotiations 140 | 141 | def get_command(self): 142 | """ 143 | Get a line of text that was received from the client. The class's 144 | cmd_ready attribute will be true if lines are available. 145 | """ 146 | cmd = None 147 | count = len(self.command_list) 148 | if count > 0: 149 | cmd = self.command_list.pop(0) 150 | 151 | # If that was the last line, turn off lines_pending 152 | if count == 1: 153 | self.cmd_ready = False 154 | return cmd 155 | 156 | def send(self, text): 157 | """ 158 | Send raw text to the distant end. 159 | """ 160 | if text: 161 | self.send_buffer += text.replace('\n', '\r\n') 162 | self.send_pending = True 163 | 164 | def send_cc(self, text): 165 | """ 166 | Send text with caret codes converted to ansi. 167 | """ 168 | self.send(colorize(text, self.use_ansi)) 169 | 170 | def send_wrapped(self, text): 171 | """ 172 | Send text padded and wrapped to the user's screen width. 173 | """ 174 | lines = word_wrap(text, self.columns) 175 | for line in lines: 176 | self.send_cc(line + '\n') 177 | 178 | def deactivate(self): 179 | """ 180 | Set the client to disconnect on the next server poll. 181 | """ 182 | self.active = False 183 | 184 | def addrport(self): 185 | """ 186 | Return the client's IP address and port number as a string. 187 | """ 188 | return "{}:{}".format(self.address, self.port) 189 | 190 | def idle(self): 191 | """ 192 | Returns the number of seconds that have elasped since the client 193 | last sent us some input. 194 | """ 195 | return time.time() - self.last_input_time 196 | 197 | def duration(self): 198 | """ 199 | Returns the number of seconds the client has been connected. 200 | """ 201 | return time.time() - self.connect_time 202 | 203 | def request_do_sga(self): 204 | """ 205 | Request client to Suppress Go-Ahead. See RFC 858. 206 | """ 207 | self._iac_do(SGA) 208 | self._note_reply_pending(SGA, True) 209 | 210 | def request_will_echo(self): 211 | """ 212 | Tell the client that we would like to echo their text. See RFC 857. 213 | """ 214 | self._iac_will(ECHO) 215 | self._note_reply_pending(ECHO, True) 216 | self.telnet_echo = True 217 | 218 | def request_wont_echo(self): 219 | """ 220 | Tell the client that we would like to stop echoing their text. 221 | See RFC 857. 222 | """ 223 | self._iac_wont(ECHO) 224 | self._note_reply_pending(ECHO, True) 225 | self.telnet_echo = False 226 | 227 | def password_mode_on(self): 228 | """ 229 | Tell client we will echo (but don't) so typed passwords don't show. 230 | """ 231 | self._iac_will(ECHO) 232 | self._note_reply_pending(ECHO, True) 233 | 234 | def password_mode_off(self): 235 | """ 236 | Tell client we are done echoing (we lied) and show typing again. 237 | """ 238 | self._iac_wont(ECHO) 239 | self._note_reply_pending(ECHO, True) 240 | 241 | def request_naws(self): 242 | """ 243 | Request to Negotiate About Window Size. See RFC 1073. 244 | """ 245 | self._iac_do(NAWS) 246 | self._note_reply_pending(NAWS, True) 247 | 248 | def request_terminal_type(self): 249 | """ 250 | Begins the Telnet negotiations to request the terminal type from 251 | the client. See RFC 779. 252 | """ 253 | self._iac_do(TTYPE) 254 | self._note_reply_pending(TTYPE, True) 255 | 256 | def socket_send(self): 257 | """ 258 | Called by TelnetServer when send data is ready. 259 | """ 260 | if len(self.send_buffer): 261 | try: 262 | #convert to ansi before sending 263 | sent = self.sock.send(bytes(self.send_buffer, "cp1252")) 264 | except socket.error as err: 265 | logging.error("SEND error '{}' from {}".format(err, self.addrport())) 266 | self.active = False 267 | return 268 | self.bytes_sent += sent 269 | self.send_buffer = self.send_buffer[sent:] 270 | else: 271 | self.send_pending = False 272 | 273 | def socket_recv(self): 274 | """ 275 | Called by TelnetServer when recv data is ready. 276 | """ 277 | try: 278 | #Encode recieved bytes in ansi 279 | data = str(self.sock.recv(2048), "cp1252") 280 | except socket.error as err: 281 | logging.error("RECIEVE socket error '{}' from {}".format(err, self.addrport())) 282 | raise ConnectionLost() 283 | 284 | # Did they close the connection? 285 | size = len(data) 286 | if size == 0: 287 | logging.debug ("No data recieved, client closed connection") 288 | raise ConnectionLost() 289 | 290 | # Update some trackers 291 | self.last_input_time = time.time() 292 | self.bytes_received += size 293 | 294 | # Test for telnet commands 295 | for byte in data: 296 | self._iac_sniffer(byte) 297 | 298 | # Look for newline characters to get whole lines from the buffer 299 | while True: 300 | mark = self.recv_buffer.find('\n') 301 | if mark == -1: 302 | break 303 | cmd = self.recv_buffer[:mark].strip() 304 | self.command_list.append(cmd) 305 | self.cmd_ready = True 306 | self.recv_buffer = self.recv_buffer[mark+1:] 307 | 308 | def _recv_byte(self, byte): 309 | """ 310 | Non-printable filtering currently disabled because it did not play 311 | well with extended character sets. 312 | """ 313 | # Filter out non-printing characters 314 | #if (byte >= ' ' and byte <= '~') or byte == '\n': 315 | if self.telnet_echo: 316 | self._echo_byte(byte) 317 | self.recv_buffer += byte 318 | 319 | def _echo_byte(self, byte): 320 | """ 321 | Echo a character back to the client and convert LF into CR\LF. 322 | """ 323 | if byte == '\n': 324 | self.send_buffer += '\r' 325 | if self.telnet_echo_password: 326 | self.send_buffer += '*' 327 | else: 328 | self.send_buffer += byte 329 | 330 | def _iac_sniffer(self, byte): 331 | """ 332 | Watches incomming data for Telnet IAC sequences. 333 | Passes the data, if any, with the IAC commands stripped to 334 | _recv_byte(). 335 | """ 336 | # Are we not currently in an IAC sequence coming from the client? 337 | if self.telnet_got_iac is False: 338 | 339 | if byte == IAC: 340 | # Well, we are now 341 | self.telnet_got_iac = True 342 | return 343 | 344 | # Are we currenty in a sub-negotion? 345 | elif self.telnet_got_sb is True: 346 | # Sanity check on length 347 | if len(self.telnet_sb_buffer) < 64: 348 | self.telnet_sb_buffer += byte 349 | else: 350 | self.telnet_got_sb = False 351 | self.telnet_sb_buffer = "" 352 | return 353 | 354 | else: 355 | # Just a normal NVT character 356 | self._recv_byte(byte) 357 | return 358 | 359 | # Byte handling when already in an IAC sequence sent from the client 360 | else: 361 | 362 | # Did we get sent a second IAC? 363 | if byte == IAC and self.telnet_got_sb is True: 364 | # Must be an escaped 255 (IAC + IAC) 365 | self.telnet_sb_buffer += byte 366 | self.telnet_got_iac = False 367 | return 368 | 369 | # Do we already have an IAC + CMD? 370 | elif self.telnet_got_cmd: 371 | # Yes, so handle the option 372 | self._three_byte_cmd(byte) 373 | return 374 | 375 | # We have IAC but no CMD 376 | else: 377 | 378 | # Is this the middle byte of a three-byte command? 379 | if byte == DO: 380 | self.telnet_got_cmd = DO 381 | return 382 | 383 | elif byte == DONT: 384 | self.telnet_got_cmd = DONT 385 | return 386 | 387 | elif byte == WILL: 388 | self.telnet_got_cmd = WILL 389 | return 390 | 391 | elif byte == WONT: 392 | self.telnet_got_cmd = WONT 393 | return 394 | 395 | else: 396 | # Nope, must be a two-byte command 397 | self._two_byte_cmd(byte) 398 | 399 | 400 | def _two_byte_cmd(self, cmd): 401 | """ 402 | Handle incoming Telnet commands that are two bytes long. 403 | """ 404 | logging.debug("Got two byte cmd '{}'".format(ord(cmd))) 405 | 406 | if cmd == SB: 407 | # Begin capturing a sub-negotiation string 408 | self.telnet_got_sb = True 409 | self.telnet_sb_buffer = '' 410 | 411 | elif cmd == SE: 412 | # Stop capturing a sub-negotiation string 413 | self.telnet_got_sb = False 414 | self._sb_decoder() 415 | 416 | elif cmd == NOP: 417 | pass 418 | 419 | elif cmd == DATMK: 420 | pass 421 | 422 | elif cmd == IP: 423 | pass 424 | 425 | elif cmd == AO: 426 | pass 427 | 428 | elif cmd == AYT: 429 | pass 430 | 431 | elif cmd == EC: 432 | pass 433 | 434 | elif cmd == EL: 435 | pass 436 | 437 | elif cmd == GA: 438 | pass 439 | 440 | else: 441 | logging.warning("Send an invalid 2 byte command") 442 | 443 | self.telnet_got_iac = False 444 | self.telnet_got_cmd = None 445 | 446 | def _three_byte_cmd(self, option): 447 | """ 448 | Handle incoming Telnet commmands that are three bytes long. 449 | """ 450 | cmd = self.telnet_got_cmd 451 | logging.debug("Got three byte cmd {}:{}".format(ord(cmd), ord(option))) 452 | 453 | # Incoming DO's and DONT's refer to the status of this end 454 | if cmd == DO: 455 | if option == BINARY or option == SGA or option == ECHO: 456 | 457 | if self._check_reply_pending(option): 458 | self._note_reply_pending(option, False) 459 | self._note_local_option(option, True) 460 | 461 | elif (self._check_local_option(option) is False or 462 | self._check_local_option(option) is UNKNOWN): 463 | self._note_local_option(option, True) 464 | self._iac_will(option) 465 | # Just nod unless setting echo 466 | if option == ECHO: 467 | self.telnet_echo = True 468 | 469 | else: 470 | # All other options = Default to refusing once 471 | if self._check_local_option(option) is UNKNOWN: 472 | self._note_local_option(option, False) 473 | self._iac_wont(option) 474 | 475 | elif cmd == DONT: 476 | if option == BINARY or option == SGA or option == ECHO: 477 | 478 | if self._check_reply_pending(option): 479 | self._note_reply_pending(option, False) 480 | self._note_local_option(option, False) 481 | 482 | elif (self._check_local_option(option) is True or 483 | self._check_local_option(option) is UNKNOWN): 484 | self._note_local_option(option, False) 485 | self._iac_wont(option) 486 | # Just nod unless setting echo 487 | if option == ECHO: 488 | self.telnet_echo = False 489 | else: 490 | # All other options = Default to ignoring 491 | pass 492 | 493 | 494 | # Incoming WILL's and WONT's refer to the status of the client 495 | elif cmd == WILL: 496 | if option == ECHO: 497 | 498 | # Nutjob client offering to echo the server... 499 | if self._check_remote_option(ECHO) is UNKNOWN: 500 | self._note_remote_option(ECHO, False) 501 | # No no, bad client! 502 | self._iac_dont(ECHO) 503 | 504 | elif option == NAWS or option == SGA: 505 | if self._check_reply_pending(option): 506 | self._note_reply_pending(option, False) 507 | self._note_remote_option(option, True) 508 | 509 | elif (self._check_remote_option(option) is False or 510 | self._check_remote_option(option) is UNKNOWN): 511 | self._note_remote_option(option, True) 512 | self._iac_do(option) 513 | # Client should respond with SB (for NAWS) 514 | 515 | elif option == TTYPE: 516 | if self._check_reply_pending(TTYPE): 517 | self._note_reply_pending(TTYPE, False) 518 | self._note_remote_option(TTYPE, True) 519 | # Tell them to send their terminal type 520 | self.send("{}{}{}{}{}{}".format(IAC, SB, TTYPE, SEND, IAC, SE)) 521 | 522 | elif (self._check_remote_option(TTYPE) is False or 523 | self._check_remote_option(TTYPE) is UNKNOWN): 524 | self._note_remote_option(TTYPE, True) 525 | self._iac_do(TTYPE) 526 | 527 | elif cmd == WONT: 528 | if option == ECHO: 529 | 530 | # Client states it wont echo us -- good, they're not supposed to. 531 | if self._check_remote_option(ECHO) is UNKNOWN: 532 | self._note_remote_option(ECHO, False) 533 | self._iac_dont(ECHO) 534 | 535 | elif option == SGA or option == TTYPE: 536 | 537 | if self._check_reply_pending(option): 538 | self._note_reply_pending(option, False) 539 | self._note_remote_option(option, False) 540 | 541 | elif (self._check_remote_option(option) is True or 542 | self._check_remote_option(option) is UNKNOWN): 543 | self._note_remote_option(option, False) 544 | self._iac_dont(option) 545 | 546 | # Should TTYPE be below this? 547 | 548 | else: 549 | # All other options = Default to ignoring 550 | pass 551 | else: 552 | logging.warning("Send an invalid 3 byte command") 553 | 554 | self.telnet_got_iac = False 555 | self.telnet_got_cmd = None 556 | 557 | def _sb_decoder(self): 558 | """ 559 | Figures out what to do with a received sub-negotiation block. 560 | """ 561 | bloc = self.telnet_sb_buffer 562 | if len(bloc) > 2: 563 | 564 | if bloc[0] == TTYPE and bloc[1] == IS: 565 | self.terminal_type = bloc[2:] 566 | logging.debug("Terminal type = '{}'".format(self.terminal_type)) 567 | 568 | if bloc[0] == NAWS: 569 | if len(bloc) != 5: 570 | logging.warning("Bad length on NAWS SB: " + str(len(bloc))) 571 | else: 572 | self.columns = (256 * ord(bloc[1])) + ord(bloc[2]) 573 | self.rows = (256 * ord(bloc[3])) + ord(bloc[4]) 574 | 575 | logging.info("Screen is {} x {}".format(self.columns, self.rows)) 576 | 577 | self.telnet_sb_buffer = '' 578 | 579 | 580 | #---[ State Juggling for Telnet Options ]---------------------------------- 581 | 582 | # Sometimes verbiage is tricky. I use 'note' rather than 'set' here 583 | # because (to me) set infers something happened. 584 | 585 | def _check_local_option(self, option): 586 | """Test the status of local negotiated Telnet options.""" 587 | if not option in self.telnet_opt_dict: 588 | self.telnet_opt_dict[option] = TelnetOption() 589 | return self.telnet_opt_dict[option].local_option 590 | 591 | def _note_local_option(self, option, state): 592 | """Record the status of local negotiated Telnet options.""" 593 | if not option in self.telnet_opt_dict: 594 | self.telnet_opt_dict[option] = TelnetOption() 595 | self.telnet_opt_dict[option].local_option = state 596 | 597 | def _check_remote_option(self, option): 598 | """Test the status of remote negotiated Telnet options.""" 599 | if not option in self.telnet_opt_dict: 600 | self.telnet_opt_dict[option] = TelnetOption() 601 | return self.telnet_opt_dict[option].remote_option 602 | 603 | def _note_remote_option(self, option, state): 604 | """Record the status of local negotiated Telnet options.""" 605 | if not option in self.telnet_opt_dict: 606 | self.telnet_opt_dict[option] = TelnetOption() 607 | self.telnet_opt_dict[option].remote_option = state 608 | 609 | def _check_reply_pending(self, option): 610 | """Test the status of requested Telnet options.""" 611 | if not option in self.telnet_opt_dict: 612 | self.telnet_opt_dict[option] = TelnetOption() 613 | return self.telnet_opt_dict[option].reply_pending 614 | 615 | def _note_reply_pending(self, option, state): 616 | """Record the status of requested Telnet options.""" 617 | if not option in self.telnet_opt_dict: 618 | self.telnet_opt_dict[option] = TelnetOption() 619 | self.telnet_opt_dict[option].reply_pending = state 620 | 621 | 622 | #---[ Telnet Command Shortcuts ]------------------------------------------- 623 | 624 | def _iac_do(self, option): 625 | """Send a Telnet IAC "DO" sequence.""" 626 | self.send("{}{}{}".format(IAC, DO, option)) 627 | 628 | def _iac_dont(self, option): 629 | """Send a Telnet IAC "DONT" sequence.""" 630 | self.send("{}{}{}".format(IAC, DONT, option)) 631 | 632 | def _iac_will(self, option): 633 | """Send a Telnet IAC "WILL" sequence.""" 634 | self.send("{}{}{}".format(IAC, WILL, option)) 635 | 636 | def _iac_wont(self, option): 637 | """Send a Telnet IAC "WONT" sequence.""" 638 | self.send("{}{}{}".format(IAC, WONT, option)) 639 | --------------------------------------------------------------------------------