/neatx/lib/protocol.py

http://neatx.googlecode.com/ · Python · 353 lines · 157 code · 71 blank · 125 comment · 27 complexity · 95aa3cf2f0946e55fc28d5ce3da1b523 MD5 · raw file

  1. #
  2. #
  3. # Copyright (C) 2009 Google Inc.
  4. #
  5. # This program is free software; you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation; either version 2 of the License, or
  8. # (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful, but
  11. # WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  13. # General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with this program; if not, write to the Free Software
  17. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
  18. # 02110-1301, USA.
  19. """NX protocol utilities
  20. """
  21. import logging
  22. import re
  23. import urllib
  24. from neatx import utils
  25. NX_PROMPT = "NX>"
  26. NX_EOL = "\n"
  27. NX_EOL_CHARS = NX_EOL
  28. NX_CMD_HELLO = "hello"
  29. NX_CMD_BYE = "bye"
  30. NX_CMD_LOGIN = "login"
  31. NX_CMD_LISTSESSION = "listsession"
  32. NX_CMD_STARTSESSION = "startsession"
  33. NX_CMD_ATTACHSESSION = "attachsession"
  34. NX_CMD_RESTORESESSION = "restoresession"
  35. NX_CMD_TERMINATE = "terminate"
  36. NX_CMD_SET = "set"
  37. NX_CMD_QUIT = "quit"
  38. NX_FALSE = "0"
  39. NX_TRUE = "1"
  40. class NxQuitServer(Exception):
  41. pass
  42. class NxQuietQuitServer(NxQuitServer):
  43. pass
  44. class NxProtocolError(Exception):
  45. def __init__(self, code, message, fatal=False):
  46. self.code = code
  47. self.msg = message
  48. self.fatal = fatal
  49. class NxUndefinedCommand(NxProtocolError):
  50. def __init__(self, command):
  51. NxProtocolError.__init__(self, 503,
  52. "Error: undefined command: '%s'" % command)
  53. class NxNotBeforeLogin(NxProtocolError):
  54. def __init__(self, command):
  55. message = ("Error: the command '%s' cannot be called before to login" %
  56. command)
  57. NxProtocolError.__init__(self, 554, message)
  58. class NxNotAfterLogin(NxProtocolError):
  59. def __init__(self, command):
  60. message = "Error: the command '%s' cannot be called after login" % command
  61. NxProtocolError.__init__(self, 554, message)
  62. class NxUnsupportedProtocol(NxProtocolError):
  63. def __init__(self):
  64. # Had to set code to 500 instead of 552, otherwise client ignores
  65. # this error.
  66. NxProtocolError.__init__(self, 500,
  67. ("Protocol you requested is not supported, "
  68. "please upgrade your client to latest version"),
  69. True)
  70. class NxUnencryptedSessionsNotAllowed(NxProtocolError):
  71. def __init__(self, x):
  72. message = "ERROR: Unencrypted sessions are not allowed on this server"
  73. NxProtocolError.__init__(self, 594, message)
  74. class NxParameterParsingError(NxProtocolError):
  75. def __init__(self, params):
  76. message = (("Error: Parsing parameters: string \"%s\" has "
  77. "invalid format") % params)
  78. NxProtocolError.__init__(self, 597, message)
  79. class NxServerBase(object):
  80. """Base class for NX protocol servers.
  81. """
  82. def __init__(self, input, output, handler):
  83. """Instance initialization.
  84. @type input: file
  85. @param input: Input file handle
  86. @type output: file
  87. @param output: Output file handle
  88. @type handler: callable
  89. @param handler: Called for received lines
  90. """
  91. assert callable(handler)
  92. self._input = input
  93. self._output = output
  94. self._handler = handler
  95. def Start(self):
  96. """Start responding to requests.
  97. """
  98. self.SendBanner()
  99. while True:
  100. self.Write(105)
  101. try:
  102. try:
  103. line = self.ReadLine()
  104. # Ignore empty lines
  105. if line.strip():
  106. self._HandleLine(line)
  107. except NxProtocolError, err:
  108. self.Write(err.code, message=err.msg)
  109. if err.fatal:
  110. raise NxQuitServer()
  111. except NxQuietQuitServer:
  112. break
  113. except NxQuitServer:
  114. self.Write(999, "Bye.")
  115. break
  116. def _HandleLine(self, line):
  117. try:
  118. self._handler(line)
  119. except (SystemExit, KeyboardInterrupt, NxProtocolError, NxQuitServer):
  120. raise
  121. except Exception:
  122. logging.exception("Error while handling line %r", line)
  123. raise NxProtocolError(500, "Internal error", fatal=True)
  124. def _Write(self, data):
  125. """Write to output after logging.
  126. """
  127. logging.debug(">>> %r", data)
  128. try:
  129. self._output.write(data)
  130. finally:
  131. self._output.flush()
  132. def Write(self, code, message=None, newline=None):
  133. """Write prompt to output.
  134. @type code: int
  135. @param code: Status code
  136. @type message: str
  137. @param message: Message text
  138. @type newline: bool
  139. @param newline: Whether to add newline
  140. Note: The "newline" parameter is a tri-state variable. If there's a
  141. message, print newline by default (e.g. "NX> 500 something\\n"). If there's
  142. no message, don't print newline (e.g. "NX> 105 "). This logic can be
  143. overridden by explictly setting the "newline" parameter to a non-None
  144. value.
  145. """
  146. assert code >= 0 and code <= 999
  147. # Build prompt
  148. prompt = "%s %s " % (NX_PROMPT, code)
  149. if message:
  150. prompt += "%s" % message
  151. if (newline is None and message) or (newline is not None and newline):
  152. prompt += NX_EOL
  153. self._Write(prompt)
  154. def WriteLine(self, line):
  155. """Write line to output.
  156. One newline char is automatically added.
  157. """
  158. self._Write(line + NX_EOL)
  159. def ReadLine(self, hide=False):
  160. """Reads line from input.
  161. @type hide: bool
  162. @param hide: Whether to hide line read from log output
  163. """
  164. # TODO: Timeout (poll, etc.)
  165. line = self._input.readline()
  166. if hide:
  167. logging.debug("<<< [hidden]")
  168. else:
  169. logging.debug("<<< %r", line)
  170. # Has the client closed the connection?
  171. if not line:
  172. raise NxQuitServer()
  173. return line.rstrip(NX_EOL_CHARS)
  174. def WithoutTerminalEcho(self, fn, *args, **kwargs):
  175. """Calls function with ECHO flag disabled.
  176. @type fn: callable
  177. @param fn: Called function
  178. """
  179. return utils.WithoutTerminalEcho(self._input, fn, *args, **kwargs)
  180. def SendBanner(self):
  181. """Send banner to peer.
  182. Can be overriden by subclass.
  183. """
  184. def SplitCommand(command):
  185. """Split line into command and arguments on first whitespace.
  186. """
  187. parts = command.split(None, 1)
  188. # Empty lines should've been filtered out earlier
  189. assert parts
  190. if len(parts) == 1:
  191. args = ""
  192. else:
  193. args = parts[1]
  194. return (parts[0].lower(), args)
  195. def ParseParameters(params, _logging=logging):
  196. """Parse parameters sent by client.
  197. @type params: string
  198. @param params: Parameter string
  199. """
  200. param_re = re.compile((r"^\s*--(?P<name>[a-z][a-z0-9_-]*)="
  201. "\"(?P<value>[^\"]*)\"\\s*"), re.I)
  202. result = []
  203. work = params.strip()
  204. while work:
  205. m = param_re.search(work)
  206. if not m:
  207. _logging.warning("Failed to parse parameter string %r", params)
  208. raise NxParameterParsingError(params)
  209. result.append((m.group("name"), m.group("value")))
  210. work = work[m.end():]
  211. assert not work
  212. return result
  213. def UnquoteParameterValue(value):
  214. """Unquotes parameter value.
  215. @type value: str
  216. @param value: Quoted value
  217. @rtype: str
  218. @return: Unquoted value
  219. """
  220. return urllib.unquote(value)
  221. def ParseNxBoolean(value):
  222. """Parses a boolean parameter value.
  223. @type value: str
  224. @param value: Value
  225. @rtype: bool
  226. @return: Whether parameter evaluates to true
  227. """
  228. return value == NX_TRUE
  229. def FormatNxBoolean(value):
  230. """Format boolean value for nxagent.
  231. @type value: bool
  232. @param value: Value
  233. @rtype: str
  234. """
  235. if value:
  236. return NX_TRUE
  237. return NX_FALSE
  238. def ParseNxSize(value):
  239. """Parses a size unit parameter value.
  240. @type value: str
  241. @param value: Value
  242. @rtype: int
  243. @return: Size in Mebibytes
  244. """
  245. return int(value.rstrip("M"))
  246. def FormatNxSize(value):
  247. """Format size value.
  248. @type value: int
  249. @param value: Value in Mebibytes
  250. @rtype: str
  251. """
  252. return "%dM" % value