protocol.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. """
  2. Parser for the Telnet protocol. (Not a complete implementation of the telnet
  3. specification, but sufficient for a command line interface.)
  4. Inspired by `Twisted.conch.telnet`.
  5. """
  6. from __future__ import annotations
  7. import struct
  8. from typing import Callable, Generator
  9. from .log import logger
  10. __all__ = [
  11. "TelnetProtocolParser",
  12. ]
  13. def int2byte(number: int) -> bytes:
  14. return bytes((number,))
  15. # Telnet constants.
  16. NOP = int2byte(0)
  17. SGA = int2byte(3)
  18. IAC = int2byte(255)
  19. DO = int2byte(253)
  20. DONT = int2byte(254)
  21. LINEMODE = int2byte(34)
  22. SB = int2byte(250)
  23. WILL = int2byte(251)
  24. WONT = int2byte(252)
  25. MODE = int2byte(1)
  26. SE = int2byte(240)
  27. ECHO = int2byte(1)
  28. NAWS = int2byte(31)
  29. LINEMODE = int2byte(34)
  30. SUPPRESS_GO_AHEAD = int2byte(3)
  31. TTYPE = int2byte(24)
  32. SEND = int2byte(1)
  33. IS = int2byte(0)
  34. DM = int2byte(242)
  35. BRK = int2byte(243)
  36. IP = int2byte(244)
  37. AO = int2byte(245)
  38. AYT = int2byte(246)
  39. EC = int2byte(247)
  40. EL = int2byte(248)
  41. GA = int2byte(249)
  42. class TelnetProtocolParser:
  43. """
  44. Parser for the Telnet protocol.
  45. Usage::
  46. def data_received(data):
  47. print(data)
  48. def size_received(rows, columns):
  49. print(rows, columns)
  50. p = TelnetProtocolParser(data_received, size_received)
  51. p.feed(binary_data)
  52. """
  53. def __init__(
  54. self,
  55. data_received_callback: Callable[[bytes], None],
  56. size_received_callback: Callable[[int, int], None],
  57. ttype_received_callback: Callable[[str], None],
  58. ) -> None:
  59. self.data_received_callback = data_received_callback
  60. self.size_received_callback = size_received_callback
  61. self.ttype_received_callback = ttype_received_callback
  62. self._parser = self._parse_coroutine()
  63. self._parser.send(None) # type: ignore
  64. def received_data(self, data: bytes) -> None:
  65. self.data_received_callback(data)
  66. def do_received(self, data: bytes) -> None:
  67. """Received telnet DO command."""
  68. logger.info("DO %r", data)
  69. def dont_received(self, data: bytes) -> None:
  70. """Received telnet DONT command."""
  71. logger.info("DONT %r", data)
  72. def will_received(self, data: bytes) -> None:
  73. """Received telnet WILL command."""
  74. logger.info("WILL %r", data)
  75. def wont_received(self, data: bytes) -> None:
  76. """Received telnet WONT command."""
  77. logger.info("WONT %r", data)
  78. def command_received(self, command: bytes, data: bytes) -> None:
  79. if command == DO:
  80. self.do_received(data)
  81. elif command == DONT:
  82. self.dont_received(data)
  83. elif command == WILL:
  84. self.will_received(data)
  85. elif command == WONT:
  86. self.wont_received(data)
  87. else:
  88. logger.info("command received %r %r", command, data)
  89. def naws(self, data: bytes) -> None:
  90. """
  91. Received NAWS. (Window dimensions.)
  92. """
  93. if len(data) == 4:
  94. # NOTE: the first parameter of struct.unpack should be
  95. # a 'str' object. Both on Py2/py3. This crashes on OSX
  96. # otherwise.
  97. columns, rows = struct.unpack("!HH", data)
  98. self.size_received_callback(rows, columns)
  99. else:
  100. logger.warning("Wrong number of NAWS bytes")
  101. def ttype(self, data: bytes) -> None:
  102. """
  103. Received terminal type.
  104. """
  105. subcmd, data = data[0:1], data[1:]
  106. if subcmd == IS:
  107. ttype = data.decode("ascii")
  108. self.ttype_received_callback(ttype)
  109. else:
  110. logger.warning("Received a non-IS terminal type Subnegotiation")
  111. def negotiate(self, data: bytes) -> None:
  112. """
  113. Got negotiate data.
  114. """
  115. command, payload = data[0:1], data[1:]
  116. if command == NAWS:
  117. self.naws(payload)
  118. elif command == TTYPE:
  119. self.ttype(payload)
  120. else:
  121. logger.info("Negotiate (%r got bytes)", len(data))
  122. def _parse_coroutine(self) -> Generator[None, bytes, None]:
  123. """
  124. Parser state machine.
  125. Every 'yield' expression returns the next byte.
  126. """
  127. while True:
  128. d = yield
  129. if d == int2byte(0):
  130. pass # NOP
  131. # Go to state escaped.
  132. elif d == IAC:
  133. d2 = yield
  134. if d2 == IAC:
  135. self.received_data(d2)
  136. # Handle simple commands.
  137. elif d2 in (NOP, DM, BRK, IP, AO, AYT, EC, EL, GA):
  138. self.command_received(d2, b"")
  139. # Handle IAC-[DO/DONT/WILL/WONT] commands.
  140. elif d2 in (DO, DONT, WILL, WONT):
  141. d3 = yield
  142. self.command_received(d2, d3)
  143. # Subnegotiation
  144. elif d2 == SB:
  145. # Consume everything until next IAC-SE
  146. data = []
  147. while True:
  148. d3 = yield
  149. if d3 == IAC:
  150. d4 = yield
  151. if d4 == SE:
  152. break
  153. else:
  154. data.append(d4)
  155. else:
  156. data.append(d3)
  157. self.negotiate(b"".join(data))
  158. else:
  159. self.received_data(d)
  160. def feed(self, data: bytes) -> None:
  161. """
  162. Feed data to the parser.
  163. """
  164. for b in data:
  165. self._parser.send(int2byte(b))