• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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