negtelnetserver.py 12 KB

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