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