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