server.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. """
  2. Telnet server.
  3. Example usage::
  4. class MyTelnetApplication(TelnetApplication):
  5. def client_connected(self, telnet_connection):
  6. # Set CLI with simple prompt.
  7. telnet_connection.set_application(
  8. telnet_connection.create_prompt_application(...))
  9. def handle_command(self, telnet_connection, document):
  10. # When the client enters a command, just reply.
  11. telnet_connection.send('You said: %r\n\n' % document.text)
  12. ...
  13. a = MyTelnetApplication()
  14. TelnetServer(application=a, host='127.0.0.1', port=23).run()
  15. """
  16. from __future__ import unicode_literals
  17. import socket
  18. import select
  19. import threading
  20. import os
  21. import fcntl
  22. from six import int2byte, text_type, binary_type
  23. from codecs import getincrementaldecoder
  24. from prompt_toolkit.enums import DEFAULT_BUFFER
  25. from prompt_toolkit.eventloop.base import EventLoop
  26. from prompt_toolkit.interface import CommandLineInterface, Application
  27. from prompt_toolkit.layout.screen import Size
  28. from prompt_toolkit.shortcuts import create_prompt_application
  29. from prompt_toolkit.terminal.vt100_input import InputStream
  30. from prompt_toolkit.terminal.vt100_output import Vt100_Output
  31. from .log import logger
  32. from .protocol import IAC, DO, LINEMODE, SB, MODE, SE, WILL, ECHO, NAWS, SUPPRESS_GO_AHEAD
  33. from .protocol import TelnetProtocolParser
  34. from .application import TelnetApplication
  35. __all__ = (
  36. 'TelnetServer',
  37. )
  38. def _initialize_telnet(connection):
  39. logger.info('Initializing telnet connection')
  40. # Iac Do Linemode
  41. connection.send(IAC + DO + LINEMODE)
  42. # Suppress Go Ahead. (This seems important for Putty to do correct echoing.)
  43. # This will allow bi-directional operation.
  44. connection.send(IAC + WILL + SUPPRESS_GO_AHEAD)
  45. # Iac sb
  46. connection.send(IAC + SB + LINEMODE + MODE + int2byte(0) + IAC + SE)
  47. # IAC Will Echo
  48. connection.send(IAC + WILL + ECHO)
  49. # Negotiate window size
  50. connection.send(IAC + DO + NAWS)
  51. class _ConnectionStdout(object):
  52. """
  53. Wrapper around socket which provides `write` and `flush` methods for the
  54. Vt100_Output output.
  55. """
  56. def __init__(self, connection, encoding):
  57. self._encoding = encoding
  58. self._connection = connection
  59. self._buffer = []
  60. def write(self, data):
  61. assert isinstance(data, text_type)
  62. self._buffer.append(data.encode(self._encoding))
  63. self.flush()
  64. def flush(self):
  65. try:
  66. self._connection.send(b''.join(self._buffer))
  67. except socket.error as e:
  68. logger.error("Couldn't send data over socket: %s" % e)
  69. self._buffer = []
  70. class TelnetConnection(object):
  71. """
  72. Class that represents one Telnet connection.
  73. """
  74. def __init__(self, conn, addr, application, server, encoding):
  75. assert isinstance(addr, tuple) # (addr, port) tuple
  76. assert isinstance(application, TelnetApplication)
  77. assert isinstance(server, TelnetServer)
  78. assert isinstance(encoding, text_type) # e.g. 'utf-8'
  79. self.conn = conn
  80. self.addr = addr
  81. self.application = application
  82. self.closed = False
  83. self.handling_command = True
  84. self.server = server
  85. self.encoding = encoding
  86. self.callback = None # Function that handles the CLI result.
  87. # Create "Output" object.
  88. self.size = Size(rows=40, columns=79)
  89. # Initialize.
  90. _initialize_telnet(conn)
  91. # Create output.
  92. def get_size():
  93. return self.size
  94. self.stdout = _ConnectionStdout(conn, encoding=encoding)
  95. self.vt100_output = Vt100_Output(self.stdout, get_size, write_binary=False)
  96. # Create an eventloop (adaptor) for the CommandLineInterface.
  97. self.eventloop = _TelnetEventLoopInterface(server)
  98. # Set default CommandLineInterface.
  99. self.set_application(create_prompt_application())
  100. # Call client_connected
  101. application.client_connected(self)
  102. # Draw for the first time.
  103. self.handling_command = False
  104. self.cli._redraw()
  105. def set_application(self, app, callback=None):
  106. """
  107. Set ``CommandLineInterface`` instance for this connection.
  108. (This can be replaced any time.)
  109. :param cli: CommandLineInterface instance.
  110. :param callback: Callable that takes the result of the CLI.
  111. """
  112. assert isinstance(app, Application)
  113. assert callback is None or callable(callback)
  114. self.cli = CommandLineInterface(
  115. application=app,
  116. eventloop=self.eventloop,
  117. output=self.vt100_output)
  118. self.callback = callback
  119. # Create a parser, and parser callbacks.
  120. cb = self.cli.create_eventloop_callbacks()
  121. inputstream = InputStream(cb.feed_key)
  122. # Input decoder for stdin. (Required when working with multibyte
  123. # characters, like chinese input.)
  124. stdin_decoder_cls = getincrementaldecoder(self.encoding)
  125. stdin_decoder = [stdin_decoder_cls()] # nonlocal
  126. # Tell the CLI that it's running. We don't start it through the run()
  127. # call, but will still want _redraw() to work.
  128. self.cli._is_running = True
  129. def data_received(data):
  130. """ TelnetProtocolParser 'data_received' callback """
  131. assert isinstance(data, binary_type)
  132. try:
  133. result = stdin_decoder[0].decode(data)
  134. inputstream.feed(result)
  135. except UnicodeDecodeError:
  136. stdin_decoder[0] = stdin_decoder_cls()
  137. return ''
  138. def size_received(rows, columns):
  139. """ TelnetProtocolParser 'size_received' callback """
  140. self.size = Size(rows=rows, columns=columns)
  141. cb.terminal_size_changed()
  142. self.parser = TelnetProtocolParser(data_received, size_received)
  143. def feed(self, data):
  144. """
  145. Handler for incoming data. (Called by TelnetServer.)
  146. """
  147. assert isinstance(data, binary_type)
  148. self.parser.feed(data)
  149. # Render again.
  150. self.cli._redraw()
  151. # When a return value has been set (enter was pressed), handle command.
  152. if self.cli.is_returning:
  153. try:
  154. return_value = self.cli.return_value()
  155. except (EOFError, KeyboardInterrupt) as e:
  156. # Control-D or Control-C was pressed.
  157. logger.info('%s, closing connection.', type(e).__name__)
  158. self.close()
  159. return
  160. # Handle CLI command
  161. self._handle_command(return_value)
  162. def _handle_command(self, command):
  163. """
  164. Handle command. This will run in a separate thread, in order not
  165. to block the event loop.
  166. """
  167. logger.info('Handle command %r', command)
  168. def in_executor():
  169. self.handling_command = True
  170. try:
  171. if self.callback is not None:
  172. self.callback(self, command)
  173. finally:
  174. self.server.call_from_executor(done)
  175. def done():
  176. self.handling_command = False
  177. # Reset state and draw again. (If the connection is still open --
  178. # the application could have called TelnetConnection.close()
  179. if not self.closed:
  180. self.cli.reset()
  181. self.cli.buffers[DEFAULT_BUFFER].reset()
  182. self.cli.renderer.request_absolute_cursor_position()
  183. self.vt100_output.flush()
  184. self.cli._redraw()
  185. self.server.run_in_executor(in_executor)
  186. def erase_screen(self):
  187. """
  188. Erase output screen.
  189. """
  190. self.vt100_output.erase_screen()
  191. self.vt100_output.cursor_goto(0, 0)
  192. self.vt100_output.flush()
  193. def send(self, data):
  194. """
  195. Send text to the client.
  196. """
  197. assert isinstance(data, text_type)
  198. # When data is send back to the client, we should replace the line
  199. # endings. (We didn't allocate a real pseudo terminal, and the telnet
  200. # connection is raw, so we are responsible for inserting \r.)
  201. self.stdout.write(data.replace('\n', '\r\n'))
  202. self.stdout.flush()
  203. def close(self):
  204. """
  205. Close the connection.
  206. """
  207. self.application.client_leaving(self)
  208. self.conn.close()
  209. self.closed = True
  210. class _TelnetEventLoopInterface(EventLoop):
  211. """
  212. Eventloop object to be assigned to `CommandLineInterface`.
  213. """
  214. def __init__(self, server):
  215. self._server = server
  216. def close(self):
  217. " Ignore. "
  218. def stop(self):
  219. " Ignore. "
  220. def run_in_executor(self, callback):
  221. self._server.run_in_executor(callback)
  222. def call_from_executor(self, callback, _max_postpone_until=None):
  223. self._server.call_from_executor(callback)
  224. def add_reader(self, fd, callback):
  225. raise NotImplementedError
  226. def remove_reader(self, fd):
  227. raise NotImplementedError
  228. class TelnetServer(object):
  229. """
  230. Telnet server implementation.
  231. """
  232. def __init__(self, host='127.0.0.1', port=23, application=None, encoding='utf-8'):
  233. assert isinstance(host, text_type)
  234. assert isinstance(port, int)
  235. assert isinstance(application, TelnetApplication)
  236. assert isinstance(encoding, text_type)
  237. self.host = host
  238. self.port = port
  239. self.application = application
  240. self.encoding = encoding
  241. self.connections = set()
  242. self._calls_from_executor = []
  243. # Create a pipe for inter thread communication.
  244. self._schedule_pipe = os.pipe()
  245. fcntl.fcntl(self._schedule_pipe[0], fcntl.F_SETFL, os.O_NONBLOCK)
  246. @classmethod
  247. def create_socket(cls, host, port):
  248. # Create and bind socket
  249. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  250. s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  251. s.bind((host, port))
  252. s.listen(4)
  253. return s
  254. def run_in_executor(self, callback):
  255. threading.Thread(target=callback).start()
  256. def call_from_executor(self, callback):
  257. self._calls_from_executor.append(callback)
  258. if self._schedule_pipe:
  259. os.write(self._schedule_pipe[1], b'x')
  260. def _process_callbacks(self):
  261. """
  262. Process callbacks from `call_from_executor` in eventloop.
  263. """
  264. # Flush all the pipe content.
  265. os.read(self._schedule_pipe[0], 1024)
  266. # Process calls from executor.
  267. calls_from_executor, self._calls_from_executor = self._calls_from_executor, []
  268. for c in calls_from_executor:
  269. c()
  270. def run(self):
  271. """
  272. Run the eventloop for the telnet server.
  273. """
  274. listen_socket = self.create_socket(self.host, self.port)
  275. logger.info('Listening for telnet connections on %s port %r', self.host, self.port)
  276. try:
  277. while True:
  278. # Removed closed connections.
  279. self.connections = set([c for c in self.connections if not c.closed])
  280. # Ignore connections handling commands.
  281. connections = set([c for c in self.connections if not c.handling_command])
  282. # Wait for next event.
  283. read_list = (
  284. [listen_socket, self._schedule_pipe[0]] +
  285. [c.conn for c in connections])
  286. read, _, _ = select.select(read_list, [], [])
  287. for s in read:
  288. # When the socket itself is ready, accept a new connection.
  289. if s == listen_socket:
  290. self._accept(listen_socket)
  291. # If we receive something on our "call_from_executor" pipe, process
  292. # these callbacks in a thread safe way.
  293. elif s == self._schedule_pipe[0]:
  294. self._process_callbacks()
  295. # Handle incoming data on socket.
  296. else:
  297. self._handle_incoming_data(s)
  298. finally:
  299. listen_socket.close()
  300. def _accept(self, listen_socket):
  301. """
  302. Accept new incoming connection.
  303. """
  304. conn, addr = listen_socket.accept()
  305. connection = TelnetConnection(conn, addr, self.application, self, encoding=self.encoding)
  306. self.connections.add(connection)
  307. logger.info('New connection %r %r', *addr)
  308. def _handle_incoming_data(self, conn):
  309. """
  310. Handle incoming data on socket.
  311. """
  312. connection = [c for c in self.connections if c.conn == conn][0]
  313. data = conn.recv(1024)
  314. if data:
  315. connection.feed(data)
  316. else:
  317. self.connections.remove(connection)