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