2 # -*- coding: utf-8 -*-
4 # Project ___| | | | _ \| |
6 # | (__| |_| | _ <| |___
7 # \___|\___/|_| \_\_____|
9 # Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
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.
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.
19 # This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
20 # KIND, either express or implied.
22 # SPDX-License-Identifier: curl
24 """ A telnet server which negotiates"""
26 from __future__ import (absolute_import, division, print_function,
36 from util import ClosingFileHandler
38 if sys.version_info.major >= 3:
41 import SocketServer as socketserver
43 log = logging.getLogger(__name__)
48 # The strings that indicate the test framework is checking our aliveness
49 VERIFIED_REQ = "verifiedserver"
50 VERIFIED_RSP = "WE ROOLZ: {pid}"
53 def telnetserver(options):
55 Starts up a TCP server with a telnet handler and serves DICT requests
60 # see tests/server/util.c function write_pidfile
63 with open(options.pidfile, "w") as f:
66 local_bind = (HOST, options.port)
67 log.info("Listening on %s", local_bind)
69 # Need to set the allow_reuse on the class, not on the instance.
70 socketserver.TCPServer.allow_reuse_address = True
71 with socketserver.TCPServer(local_bind, NegotiatingTelnetHandler) as server:
72 server.serve_forever()
73 # leaving `with` calls server.close() automatically
74 return ScriptRC.SUCCESS
77 class NegotiatingTelnetHandler(socketserver.BaseRequestHandler):
78 """Handler class for Telnet connections.
83 Negotiates options before reading data.
85 neg = Negotiator(self.request)
88 # Send some initial negotiations.
89 neg.send_do("NEW_ENVIRON")
90 neg.send_will("NEW_ENVIRON")
94 # Get the data passed through the negotiator
95 data = neg.recv(4*1024)
96 log.debug("Incoming data: %r", data)
98 if VERIFIED_REQ.encode('utf-8') in data:
99 log.debug("Received verification request from test framework")
101 # see tests/server/util.c function write_pidfile
104 response = VERIFIED_RSP.format(pid=pid)
105 response_data = response.encode('utf-8')
107 log.debug("Received normal request - echoing back")
108 response_data = data.decode('utf-8').strip().encode('utf-8')
111 log.debug("Sending %r", response_data)
112 self.request.sendall(response_data)
114 # put some effort into making a clean socket shutdown
115 # that does not give the client ECONNRESET
116 self.request.settimeout(0.1)
117 self.request.recv(4*1024)
118 self.request.shutdown(socket.SHUT_RDWR)
121 log.exception("IOError hit during request")
124 class Negotiator(object):
132 def __init__(self, tcp):
134 self.state = self.NO_NEG
136 def recv(self, bytes):
138 Read bytes from TCP, handling negotiation sequences
140 :param bytes: Number of bytes to read
141 :return: a buffer of bytes
145 # If we keep receiving negotiation sequences, we won't fill the buffer.
146 # Keep looping while we can, and until we have something to give back
148 while len(buffer) == 0:
149 data = self.tcp.recv(bytes)
151 # TCP failed to give us any data. Break out.
154 for byte_int in bytearray(data):
155 if self.state == self.NO_NEG:
156 self.no_neg(byte_int, buffer)
157 elif self.state == self.START_NEG:
158 self.start_neg(byte_int)
159 elif self.state in [self.WILL, self.WONT, self.DO, self.DONT]:
160 self.handle_option(byte_int)
162 # Received an unexpected byte. Stop negotiations
163 log.error("Unexpected byte %s in state %s",
166 self.state = self.NO_NEG
170 def no_neg(self, byte_int, buffer):
171 # Not negotiating anything thus far. Check to see if we
173 if byte_int == NegTokens.IAC:
175 log.debug("Starting negotiation (IAC)")
176 self.state = self.START_NEG
178 # Just append the incoming byte to the buffer
179 buffer.append(byte_int)
181 def start_neg(self, byte_int):
183 log.debug("In negotiation (%s)",
184 NegTokens.from_val(byte_int))
186 if byte_int == NegTokens.WILL:
187 # Client is confirming they are willing to do an option
188 log.debug("Client is willing")
189 self.state = self.WILL
190 elif byte_int == NegTokens.WONT:
191 # Client is confirming they are unwilling to do an
193 log.debug("Client is unwilling")
194 self.state = self.WONT
195 elif byte_int == NegTokens.DO:
196 # Client is indicating they can do an option
197 log.debug("Client can do")
199 elif byte_int == NegTokens.DONT:
200 # Client is indicating they can't do an option
201 log.debug("Client can't do")
202 self.state = self.DONT
204 # Received an unexpected byte. Stop negotiations
205 log.error("Unexpected byte %s in state %s",
208 self.state = self.NO_NEG
210 def handle_option(self, byte_int):
211 if byte_int in [NegOptions.BINARY,
213 NegOptions.SUPPRESS_GO_AHEAD,
215 NegOptions.NEW_ENVIRON]:
216 log.debug("Option: %s", NegOptions.from_val(byte_int))
218 # No further negotiation of this option needed. Reset the state.
219 self.state = self.NO_NEG
222 # Received an unexpected byte. Stop negotiations
223 log.error("Unexpected byte %s in state %s",
226 self.state = self.NO_NEG
228 def send_message(self, message_ints):
229 self.tcp.sendall(bytearray(message_ints))
231 def send_iac(self, arr):
232 message = [NegTokens.IAC]
234 self.send_message(message)
236 def send_do(self, option_str):
237 log.debug("Sending DO %s", option_str)
238 self.send_iac([NegTokens.DO, NegOptions.to_val(option_str)])
240 def send_dont(self, option_str):
241 log.debug("Sending DONT %s", option_str)
242 self.send_iac([NegTokens.DONT, NegOptions.to_val(option_str)])
244 def send_will(self, option_str):
245 log.debug("Sending WILL %s", option_str)
246 self.send_iac([NegTokens.WILL, NegOptions.to_val(option_str)])
248 def send_wont(self, option_str):
249 log.debug("Sending WONT %s", option_str)
250 self.send_iac([NegTokens.WONT, NegOptions.to_val(option_str)])
253 class NegBase(object):
255 def to_val(cls, name):
256 return getattr(cls, name)
259 def from_val(cls, val):
260 for k in cls.__dict__.keys():
261 if getattr(cls, k) == val:
267 class NegTokens(NegBase):
268 # The start of a negotiation sequence
270 # Confirm willingness to negotiate
272 # Confirm unwillingness to negotiate
274 # Indicate willingness to negotiate
276 # Indicate unwillingness to negotiate
279 # The start of sub-negotiation options.
281 # The end of sub-negotiation options.
285 class NegOptions(NegBase):
286 # Binary Transmission
289 SUPPRESS_GO_AHEAD = 3
290 # NAWS - width and height of client
292 # NEW-ENVIRON - environment variables on client
299 parser = argparse.ArgumentParser()
301 parser.add_argument("--port", action="store", default=9019,
302 type=int, help="port to listen on")
303 parser.add_argument("--verbose", action="store", type=int, default=0,
304 help="verbose output")
305 parser.add_argument("--pidfile", action="store",
306 help="file name for the PID")
307 parser.add_argument("--logfile", action="store",
308 help="file name for the log")
309 parser.add_argument("--srcdir", action="store", help="test directory")
310 parser.add_argument("--id", action="store", help="server ID")
311 parser.add_argument("--ipv4", action="store_true", default=0,
314 return parser.parse_args()
317 def setup_logging(options):
319 Set up logging from the command line options
321 root_logger = logging.getLogger()
324 formatter = logging.Formatter("%(asctime)s %(levelname)-5.5s "
325 "[{ident}] %(message)s"
326 .format(ident=IDENT))
328 # Write out to a logfile
330 handler = ClosingFileHandler(options.logfile)
331 handler.setFormatter(formatter)
332 handler.setLevel(logging.DEBUG)
333 root_logger.addHandler(handler)
335 # The logfile wasn't specified. Add a stdout logger.
339 # Add a stdout logger as well in verbose mode
340 root_logger.setLevel(logging.DEBUG)
343 root_logger.setLevel(logging.INFO)
346 stdout_handler = logging.StreamHandler(sys.stdout)
347 stdout_handler.setFormatter(formatter)
348 stdout_handler.setLevel(logging.DEBUG)
349 root_logger.addHandler(stdout_handler)
352 class ScriptRC(object):
353 """Enum for script return codes"""
359 class ScriptException(Exception):
363 if __name__ == '__main__':
364 # Get the options from the user.
365 options = get_options()
367 # Setup logging using the user options
368 setup_logging(options)
372 rc = telnetserver(options)
373 except Exception as e:
375 rc = ScriptRC.EXCEPTION
377 if options.pidfile and os.path.isfile(options.pidfile):
378 os.unlink(options.pidfile)
380 log.info("Returning %d", rc)