2
0

negtelnetserver.py 11 KB

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