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