1#!/usr/bin/env python 2# -*- coding: utf-8 -*- 3# 4""" A telnet server which negotiates""" 5 6from __future__ import (absolute_import, division, print_function, 7 unicode_literals) 8import argparse 9import os 10import sys 11import logging 12try: # Python 2 13 import SocketServer as socketserver 14except ImportError: # Python 3 15 import socketserver 16 17 18log = logging.getLogger(__name__) 19HOST = "localhost" 20IDENT = "NTEL" 21 22 23# The strings that indicate the test framework is checking our aliveness 24VERIFIED_REQ = "verifiedserver" 25VERIFIED_RSP = "WE ROOLZ: {pid}" 26 27 28def telnetserver(options): 29 """ 30 Starts up a TCP server with a telnet handler and serves DICT requests 31 forever. 32 """ 33 if options.pidfile: 34 pid = os.getpid() 35 with open(options.pidfile, "w") as f: 36 f.write(str(pid)) 37 38 local_bind = (HOST, options.port) 39 log.info("Listening on %s", local_bind) 40 41 # Need to set the allow_reuse on the class, not on the instance. 42 socketserver.TCPServer.allow_reuse_address = True 43 server = socketserver.TCPServer(local_bind, NegotiatingTelnetHandler) 44 server.serve_forever() 45 46 return ScriptRC.SUCCESS 47 48 49class NegotiatingTelnetHandler(socketserver.BaseRequestHandler): 50 """Handler class for Telnet connections. 51 52 """ 53 def handle(self): 54 """ 55 Negotiates options before reading data. 56 """ 57 neg = Negotiator(self.request) 58 59 try: 60 # Send some initial negotiations. 61 neg.send_do("NEW_ENVIRON") 62 neg.send_will("NEW_ENVIRON") 63 neg.send_dont("NAWS") 64 neg.send_wont("NAWS") 65 66 # Get the data passed through the negotiator 67 data = neg.recv(1024) 68 log.debug("Incoming data: %r", data) 69 70 if VERIFIED_REQ.encode('ascii') in data: 71 log.debug("Received verification request from test framework") 72 response = VERIFIED_RSP.format(pid=os.getpid()) 73 response_data = response.encode('ascii') 74 else: 75 log.debug("Received normal request - echoing back") 76 response_data = data.strip() 77 78 if response_data: 79 log.debug("Sending %r", response_data) 80 self.request.sendall(response_data) 81 82 except IOError: 83 log.exception("IOError hit during request") 84 85 86class Negotiator(object): 87 NO_NEG = 0 88 START_NEG = 1 89 WILL = 2 90 WONT = 3 91 DO = 4 92 DONT = 5 93 94 def __init__(self, tcp): 95 self.tcp = tcp 96 self.state = self.NO_NEG 97 98 def recv(self, bytes): 99 """ 100 Read bytes from TCP, handling negotiation sequences 101 102 :param bytes: Number of bytes to read 103 :return: a buffer of bytes 104 """ 105 buffer = bytearray() 106 107 # If we keep receiving negotiation sequences, we won't fill the buffer. 108 # Keep looping while we can, and until we have something to give back 109 # to the caller. 110 while len(buffer) == 0: 111 data = self.tcp.recv(bytes) 112 if not data: 113 # TCP failed to give us any data. Break out. 114 break 115 116 for byte_int in bytearray(data): 117 if self.state == self.NO_NEG: 118 self.no_neg(byte_int, buffer) 119 elif self.state == self.START_NEG: 120 self.start_neg(byte_int) 121 elif self.state in [self.WILL, self.WONT, self.DO, self.DONT]: 122 self.handle_option(byte_int) 123 else: 124 # Received an unexpected byte. Stop negotiations 125 log.error("Unexpected byte %s in state %s", 126 byte_int, 127 self.state) 128 self.state = self.NO_NEG 129 130 return buffer 131 132 def no_neg(self, byte_int, buffer): 133 # Not negotiating anything thus far. Check to see if we 134 # should. 135 if byte_int == NegTokens.IAC: 136 # Start negotiation 137 log.debug("Starting negotiation (IAC)") 138 self.state = self.START_NEG 139 else: 140 # Just append the incoming byte to the buffer 141 buffer.append(byte_int) 142 143 def start_neg(self, byte_int): 144 # In a negotiation. 145 log.debug("In negotiation (%s)", 146 NegTokens.from_val(byte_int)) 147 148 if byte_int == NegTokens.WILL: 149 # Client is confirming they are willing to do an option 150 log.debug("Client is willing") 151 self.state = self.WILL 152 elif byte_int == NegTokens.WONT: 153 # Client is confirming they are unwilling to do an 154 # option 155 log.debug("Client is unwilling") 156 self.state = self.WONT 157 elif byte_int == NegTokens.DO: 158 # Client is indicating they can do an option 159 log.debug("Client can do") 160 self.state = self.DO 161 elif byte_int == NegTokens.DONT: 162 # Client is indicating they can't do an option 163 log.debug("Client can't do") 164 self.state = self.DONT 165 else: 166 # Received an unexpected byte. Stop negotiations 167 log.error("Unexpected byte %s in state %s", 168 byte_int, 169 self.state) 170 self.state = self.NO_NEG 171 172 def handle_option(self, byte_int): 173 if byte_int in [NegOptions.BINARY, 174 NegOptions.CHARSET, 175 NegOptions.SUPPRESS_GO_AHEAD, 176 NegOptions.NAWS, 177 NegOptions.NEW_ENVIRON]: 178 log.debug("Option: %s", NegOptions.from_val(byte_int)) 179 180 # No further negotiation of this option needed. Reset the state. 181 self.state = self.NO_NEG 182 183 else: 184 # Received an unexpected byte. Stop negotiations 185 log.error("Unexpected byte %s in state %s", 186 byte_int, 187 self.state) 188 self.state = self.NO_NEG 189 190 def send_message(self, message_ints): 191 self.tcp.sendall(bytearray(message_ints)) 192 193 def send_iac(self, arr): 194 message = [NegTokens.IAC] 195 message.extend(arr) 196 self.send_message(message) 197 198 def send_do(self, option_str): 199 log.debug("Sending DO %s", option_str) 200 self.send_iac([NegTokens.DO, NegOptions.to_val(option_str)]) 201 202 def send_dont(self, option_str): 203 log.debug("Sending DONT %s", option_str) 204 self.send_iac([NegTokens.DONT, NegOptions.to_val(option_str)]) 205 206 def send_will(self, option_str): 207 log.debug("Sending WILL %s", option_str) 208 self.send_iac([NegTokens.WILL, NegOptions.to_val(option_str)]) 209 210 def send_wont(self, option_str): 211 log.debug("Sending WONT %s", option_str) 212 self.send_iac([NegTokens.WONT, NegOptions.to_val(option_str)]) 213 214 215class NegBase(object): 216 @classmethod 217 def to_val(cls, name): 218 return getattr(cls, name) 219 220 @classmethod 221 def from_val(cls, val): 222 for k in cls.__dict__.keys(): 223 if getattr(cls, k) == val: 224 return k 225 226 return "<unknown>" 227 228 229class NegTokens(NegBase): 230 # The start of a negotiation sequence 231 IAC = 255 232 # Confirm willingness to negotiate 233 WILL = 251 234 # Confirm unwillingness to negotiate 235 WONT = 252 236 # Indicate willingness to negotiate 237 DO = 253 238 # Indicate unwillingness to negotiate 239 DONT = 254 240 241 # The start of sub-negotiation options. 242 SB = 250 243 # The end of sub-negotiation options. 244 SE = 240 245 246 247class NegOptions(NegBase): 248 # Binary Transmission 249 BINARY = 0 250 # Suppress Go Ahead 251 SUPPRESS_GO_AHEAD = 3 252 # NAWS - width and height of client 253 NAWS = 31 254 # NEW-ENVIRON - environment variables on client 255 NEW_ENVIRON = 39 256 # Charset option 257 CHARSET = 42 258 259 260def get_options(): 261 parser = argparse.ArgumentParser() 262 263 parser.add_argument("--port", action="store", default=9019, 264 type=int, help="port to listen on") 265 parser.add_argument("--verbose", action="store", type=int, default=0, 266 help="verbose output") 267 parser.add_argument("--pidfile", action="store", 268 help="file name for the PID") 269 parser.add_argument("--logfile", action="store", 270 help="file name for the log") 271 parser.add_argument("--srcdir", action="store", help="test directory") 272 parser.add_argument("--id", action="store", help="server ID") 273 parser.add_argument("--ipv4", action="store_true", default=0, 274 help="IPv4 flag") 275 276 return parser.parse_args() 277 278 279def setup_logging(options): 280 """ 281 Set up logging from the command line options 282 """ 283 root_logger = logging.getLogger() 284 add_stdout = False 285 286 formatter = logging.Formatter("%(asctime)s %(levelname)-5.5s " 287 "[{ident}] %(message)s" 288 .format(ident=IDENT)) 289 290 # Write out to a logfile 291 if options.logfile: 292 handler = logging.FileHandler(options.logfile, mode="w") 293 handler.setFormatter(formatter) 294 handler.setLevel(logging.DEBUG) 295 root_logger.addHandler(handler) 296 else: 297 # The logfile wasn't specified. Add a stdout logger. 298 add_stdout = True 299 300 if options.verbose: 301 # Add a stdout logger as well in verbose mode 302 root_logger.setLevel(logging.DEBUG) 303 add_stdout = True 304 else: 305 root_logger.setLevel(logging.INFO) 306 307 if add_stdout: 308 stdout_handler = logging.StreamHandler(sys.stdout) 309 stdout_handler.setFormatter(formatter) 310 stdout_handler.setLevel(logging.DEBUG) 311 root_logger.addHandler(stdout_handler) 312 313 314class ScriptRC(object): 315 """Enum for script return codes""" 316 SUCCESS = 0 317 FAILURE = 1 318 EXCEPTION = 2 319 320 321class ScriptException(Exception): 322 pass 323 324 325if __name__ == '__main__': 326 # Get the options from the user. 327 options = get_options() 328 329 # Setup logging using the user options 330 setup_logging(options) 331 332 # Run main script. 333 try: 334 rc = telnetserver(options) 335 except Exception as e: 336 log.exception(e) 337 rc = ScriptRC.EXCEPTION 338 339 log.info("Returning %d", rc) 340 sys.exit(rc) 341