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