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