# # # Copyright (C) 2009 Google Inc. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301, USA. """NX protocol utilities """ import logging import re import urllib from neatx import utils NX_PROMPT = "NX>" NX_EOL = "\n" NX_EOL_CHARS = NX_EOL NX_CMD_HELLO = "hello" NX_CMD_BYE = "bye" NX_CMD_LOGIN = "login" NX_CMD_LISTSESSION = "listsession" NX_CMD_STARTSESSION = "startsession" NX_CMD_ATTACHSESSION = "attachsession" NX_CMD_RESTORESESSION = "restoresession" NX_CMD_TERMINATE = "terminate" NX_CMD_SET = "set" NX_CMD_QUIT = "quit" NX_FALSE = "0" NX_TRUE = "1" class NxQuitServer(Exception): pass class NxQuietQuitServer(NxQuitServer): pass class NxProtocolError(Exception): def __init__(self, code, message, fatal=False): self.code = code self.msg = message self.fatal = fatal class NxUndefinedCommand(NxProtocolError): def __init__(self, command): NxProtocolError.__init__(self, 503, "Error: undefined command: '%s'" % command) class NxNotBeforeLogin(NxProtocolError): def __init__(self, command): message = ("Error: the command '%s' cannot be called before to login" % command) NxProtocolError.__init__(self, 554, message) class NxNotAfterLogin(NxProtocolError): def __init__(self, command): message = "Error: the command '%s' cannot be called after login" % command NxProtocolError.__init__(self, 554, message) class NxUnsupportedProtocol(NxProtocolError): def __init__(self): # Had to set code to 500 instead of 552, otherwise client ignores # this error. NxProtocolError.__init__(self, 500, ("Protocol you requested is not supported, " "please upgrade your client to latest version"), True) class NxUnencryptedSessionsNotAllowed(NxProtocolError): def __init__(self, x): message = "ERROR: Unencrypted sessions are not allowed on this server" NxProtocolError.__init__(self, 594, message) class NxParameterParsingError(NxProtocolError): def __init__(self, params): message = (("Error: Parsing parameters: string \"%s\" has " "invalid format") % params) NxProtocolError.__init__(self, 597, message) class NxServerBase(object): """Base class for NX protocol servers. """ def __init__(self, input, output, handler): """Instance initialization. @type input: file @param input: Input file handle @type output: file @param output: Output file handle @type handler: callable @param handler: Called for received lines """ assert callable(handler) self._input = input self._output = output self._handler = handler def Start(self): """Start responding to requests. """ self.SendBanner() while True: self.Write(105) try: try: line = self.ReadLine() # Ignore empty lines if line.strip(): self._HandleLine(line) except NxProtocolError, err: self.Write(err.code, message=err.msg) if err.fatal: raise NxQuitServer() except NxQuietQuitServer: break except NxQuitServer: self.Write(999, "Bye.") break def _HandleLine(self, line): try: self._handler(line) except (SystemExit, KeyboardInterrupt, NxProtocolError, NxQuitServer): raise except Exception: logging.exception("Error while handling line %r", line) raise NxProtocolError(500, "Internal error", fatal=True) def _Write(self, data): """Write to output after logging. """ logging.debug(">>> %r", data) try: self._output.write(data) finally: self._output.flush() def Write(self, code, message=None, newline=None): """Write prompt to output. @type code: int @param code: Status code @type message: str @param message: Message text @type newline: bool @param newline: Whether to add newline Note: The "newline" parameter is a tri-state variable. If there's a message, print newline by default (e.g. "NX> 500 something\\n"). If there's no message, don't print newline (e.g. "NX> 105 "). This logic can be overridden by explictly setting the "newline" parameter to a non-None value. """ assert code >= 0 and code <= 999 # Build prompt prompt = "%s %s " % (NX_PROMPT, code) if message: prompt += "%s" % message if (newline is None and message) or (newline is not None and newline): prompt += NX_EOL self._Write(prompt) def WriteLine(self, line): """Write line to output. One newline char is automatically added. """ self._Write(line + NX_EOL) def ReadLine(self, hide=False): """Reads line from input. @type hide: bool @param hide: Whether to hide line read from log output """ # TODO: Timeout (poll, etc.) line = self._input.readline() if hide: logging.debug("<<< [hidden]") else: logging.debug("<<< %r", line) # Has the client closed the connection? if not line: raise NxQuitServer() return line.rstrip(NX_EOL_CHARS) def WithoutTerminalEcho(self, fn, *args, **kwargs): """Calls function with ECHO flag disabled. @type fn: callable @param fn: Called function """ return utils.WithoutTerminalEcho(self._input, fn, *args, **kwargs) def SendBanner(self): """Send banner to peer. Can be overriden by subclass. """ def SplitCommand(command): """Split line into command and arguments on first whitespace. """ parts = command.split(None, 1) # Empty lines should've been filtered out earlier assert parts if len(parts) == 1: args = "" else: args = parts[1] return (parts[0].lower(), args) def ParseParameters(params, _logging=logging): """Parse parameters sent by client. @type params: string @param params: Parameter string """ param_re = re.compile((r"^\s*--(?P[a-z][a-z0-9_-]*)=" "\"(?P[^\"]*)\"\\s*"), re.I) result = [] work = params.strip() while work: m = param_re.search(work) if not m: _logging.warning("Failed to parse parameter string %r", params) raise NxParameterParsingError(params) result.append((m.group("name"), m.group("value"))) work = work[m.end():] assert not work return result def UnquoteParameterValue(value): """Unquotes parameter value. @type value: str @param value: Quoted value @rtype: str @return: Unquoted value """ return urllib.unquote(value) def ParseNxBoolean(value): """Parses a boolean parameter value. @type value: str @param value: Value @rtype: bool @return: Whether parameter evaluates to true """ return value == NX_TRUE def FormatNxBoolean(value): """Format boolean value for nxagent. @type value: bool @param value: Value @rtype: str """ if value: return NX_TRUE return NX_FALSE def ParseNxSize(value): """Parses a size unit parameter value. @type value: str @param value: Value @rtype: int @return: Size in Mebibytes """ return int(value.rstrip("M")) def FormatNxSize(value): """Format size value. @type value: int @param value: Value in Mebibytes @rtype: str """ return "%dM" % value