/neatx/lib/app/nxserver.py
http://neatx.googlecode.com/ · Python · 736 lines · 444 code · 90 blank · 202 comment · 32 complexity · 71daf0b715a77dc3c7f2510267ff1d04 MD5 · raw file
- #
- #
- # Copyright (C) 2007 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.
- """nxserver program for accepting nx connections.
- """
- import logging
- import optparse
- import socket
- import subprocess
- import sys
- from neatx import cli
- from neatx import constants
- from neatx import errors
- from neatx import node
- from neatx import protocol
- from neatx import session
- from neatx import utils
- PROGRAM = "nxserver"
- NX_PROMPT_PARAMETERS = "Parameters: "
- _SESSION_START_TIMEOUT = 30
- _SESSION_RESTORE_TIMEOUT = 60
- # TODO: Determine how the commercial NX version gets the depth from nxagent
- DEFAULT_DEPTH = 24
- LISTSESSION_COLUMNS = [
- ("Display", 7, lambda sess: sess.display),
- ("Type", 16, lambda sess: sess.type),
- ("Session ID", 32, lambda sess: sess.id),
- ("Options", 8, lambda sess: FormatOptions(sess)),
- ("Depth", -5, lambda sess: DEFAULT_DEPTH),
- ("Screen", 14, lambda sess: FormatGeometry(sess)),
- ("Status", 11, lambda sess: FormatStatus(sess)),
- ("Session Name", 30, lambda sess: sess.name),
- ]
- """
- Column definitions for "listsession" command.
- See L{utils.FormatTable} for more details.
- """
- def FormatOptions(sess):
- """Format session options for "listsessions" command.
- """
- flags = []
- unset = "-"
- # Fullscreen
- if sess.fullscreen:
- flags.append("F")
- else:
- flags.append(unset)
- # Render
- if sess.screeninfo and "render" in sess.screeninfo:
- flags.append("R")
- else:
- flags.append(unset)
- # Non-rootless (Desktop?)
- if sess.virtualdesktop:
- flags.append("D")
- else:
- flags.append(unset)
- # Unknown
- flags.append(unset)
- flags.append(unset)
- flags.append("P")
- flags.append("S")
- flags.append("A")
- return "".join(flags)
- def FormatGeometry(sess):
- if not sess.geometry:
- return "-"
- pos = sess.geometry.find("+")
- if pos == -1:
- return sess.geometry
- return sess.geometry[:pos]
- def ConvertStatusForClient(status):
- """Convert status for client.
- The client doesn't know about the "terminating" and "suspending" statuses.
- @type status: str
- @param status: Server-side session status
- @rtype: str
- @return: Client-side session status
- """
- if status == constants.SESS_STATE_TERMINATING:
- return constants.SESS_STATE_TERMINATED
- if status == constants.SESS_STATE_SUSPENDING:
- return constants.SESS_STATE_SUSPENDED
- return status
- def FormatStatus(sess):
- """Format session status for session list.
- """
- return ConvertStatusForClient(sess.state).capitalize()
- def _GetSessionCache(sess):
- sesstype = sess.type
- if sesstype.startswith(constants.SESS_TYPE_UNIX_PREFIX):
- return sesstype
- return constants.SESS_TYPE_UNIX_PREFIX + sesstype
- def GetClientSessionInfo(sess):
- """Get session information for the client
- This is used for starting/resuming a session.
- """
- # "702 Proxy IP: 1.2.3.4" is not used because we don't support unencrypted
- # sessions anyway.
- return [
- (700, "Session id: %s" % sess.full_id),
- (705, "Session display: %s" % sess.display),
- (703, "Session type: %s" % sess.type),
- (701, "Proxy cookie: %s" % sess.cookie),
- (706, "Agent cookie: %s" % sess.cookie),
- (704, "Session cache: %s" % _GetSessionCache(sess)),
- (728, "Session caption: %s" % sess.windowname),
- (707, "SSL tunneling: %s" % protocol.FormatNxBoolean(sess.ssl)),
- (708, "Subscription: %s" % sess.subscription),
- ]
- class ServerCommandHandler(object):
- def __init__(self, server, ctx):
- self._server = server
- self._ctx = ctx
- def __call__(self, cmdline):
- """Parses and handles a command sent by the client.
- @type cmdline: str
- @param cmdline: Unparsed command
- """
- (cmd, args) = protocol.SplitCommand(cmdline)
- # Confirm command
- # TODO: Move confirmation code to protocol.py and use it from
- # nxserver_login.py, too.
- self._SendConfirmation(cmdline, cmd, args)
- if cmd in (protocol.NX_CMD_LOGIN,
- protocol.NX_CMD_HELLO,
- protocol.NX_CMD_SET):
- raise protocol.NxNotAfterLogin(cmd)
- try:
- if cmd == protocol.NX_CMD_BYE:
- return self._Bye()
- elif cmd == protocol.NX_CMD_LISTSESSION:
- return self._ListSession(args)
- elif cmd == protocol.NX_CMD_STARTSESSION:
- return self._StartSession(args)
- elif cmd == protocol.NX_CMD_ATTACHSESSION:
- return self._AttachSession(args)
- elif cmd == protocol.NX_CMD_RESTORESESSION:
- return self._RestoreSession(args)
- except errors.SessionParameterError, err:
- logging.exception("Session parameter error")
- raise protocol.NxProtocolError(500, err.args[0], fatal=True)
- raise protocol.NxUndefinedCommand(cmd)
- def _SendConfirmation(self, cmdline, cmd, args):
- """Sends a command confirmation to the client.
- """
- server = self._server
- if cmd == protocol.NX_CMD_STARTSESSION:
- self._server.WriteLine("Start session with: " + args)
- return
- # The "set" command uses a different confirmation in the commercial version
- # (as implemented in nxserver-login), but it shouldn't be used after login
- # anyway.
- server.WriteLine(cmdline.lstrip().capitalize())
- def _Bye(self):
- raise protocol.NxQuitServer()
- def _ListSession(self, args):
- """Handle the listsession NX command.
- "listsession" requests a table of session information for the current
- user. It requires parameters be specified.
- The following parameters have been seen:
- - C{--geometry="1920x1200x24+render"}:
- This seems to specify the desired geometry.
- - C{--status="suspended,running"}:
- This seems to specify the desired type.
- - C{--type="unix-gnome"}:
- This seems to constrain the list to sessions in the given states.
- - C{--user="someone"}:
- This seems to be ignored. No matter what is specified, the user given at
- login is used.
- @type args: string
- @param args: Parameters
- """
- ctx = self._ctx
- server = self._server
- mgr = ctx.session_mgr
- # Parse parameters
- parsed_params = dict(protocol.ParseParameters(self._GetParameters(args)))
- # TODO: Accepted parameters
- # Ignore --user, as per commercial implementation
- # TODO: Check sessions from all users if type=shadow? This is problematic
- # due to file system access permissions.
- find_users = [self._ctx.username]
- find_types = None
- want_shadow = False
- # Ignoring --user, as per commercial implementation
- if "type" in parsed_params:
- types = parsed_params["type"].split(",")
- # If the type is shadow do the settings to get running sessions
- if types[0] == constants.SESS_TYPE_SHADOW:
- want_shadow = True
- else:
- find_types = types
- if want_shadow:
- find_states = constants.SESS_STATE_RUNNING
- elif "status" in parsed_params:
- find_states = parsed_params["status"].split(",")
- else:
- find_states = None
- sessions = self._ListSessionInner(find_types, find_states)
- server.Write(127, "Session list of user '%s':" % ctx.username)
- for line in utils.FormatTable(sessions, LISTSESSION_COLUMNS):
- server.WriteLine(line)
- server.WriteLine("")
- server.Write(148, ("Server capacity: not reached for user: %s" %
- ctx.username))
- def _ListSessionInner(self, find_types, find_states):
- """Returns a list of sessions filtered by parameters specified.
- @type find_types: list
- @param find_types: List of wanted session types
- @type find_states: list
- @param find_states: List of wanted (client) session states
- """
- ctx = self._ctx
- mgr = ctx.session_mgr
- logging.debug("Looking for sessions with types=%r, state=%r",
- find_types, find_states)
- def _Filter(sess):
- if find_states and ConvertStatusForClient(sess.state) not in find_states:
- return False
- if find_types and sess.type not in find_types:
- return False
- return True
- return mgr.FindSessionsWithFilter(ctx.username, _Filter)
- def _StartSession(self, args):
- """Handle the startsession NX command.
- "startsession" seems to request a new session be started. It requires
- parameters be specified.
- The following parameters have been seen:
- - C{--backingstore="1"}
- - C{--cache="16M"}
- - C{--client="linux"}
- - C{--composite="1"}
- - C{--encryption="1"}
- - C{--fullscreen="0"}
- - C{--geometry="3840x1150"}
- - C{--images="64M"}
- - C{--keyboard="pc102/gb"}
- - C{--link="lan"}
- - C{--media="0"}
- - C{--rootless="0"}
- - C{--screeninfo="3840x1150x24+render"}
- - C{--session="localtest"}
- - C{--shmem="1"}
- - C{--shpix="1"}
- - C{--strict="0"}
- - C{--type="unix-gnome"}
- - C{--virtualdesktop="1"}
- Experiments with this command by directly invoking nxserver have not
- worked, as it refuses to create a session saying the unencrypted sessions
- are not supported. This is independent of whether the --encryption option
- has been set, so probably is related to the fact the nxserver has not been
- launched by sshd.
- @type args: string
- @param args: Parameters
- """
- ctx = self._ctx
- mgr = ctx.session_mgr
- server = self._server
- # Parse parameters
- params = self._GetParameters(args)
- parsed_params = dict(protocol.ParseParameters(params))
- # Parameters will be checked in nxnode
- sessid = mgr.CreateSessionID()
- logging.info("Starting new session %r", sessid)
- # Start nxnode daemon
- node.StartNodeDaemon(ctx.username, sessid)
- # Connect to daemon and tell it to start our session
- nodeclient = self._GetNodeClient(sessid, True)
- try:
- logging.debug("Sending startsession command")
- nodeclient.StartSession(parsed_params)
- finally:
- nodeclient.Close()
- # Wait for session
- self._ConnectToSession(sessid, _SESSION_START_TIMEOUT)
- def _AttachSession(self, args):
- """Handle the attachsession NX command.
- "attachsession" seems to request a new shadow session be started. It
- requires parameters be specified.
- The following parameters have been seen:
- - C{--backingstore="1"}
- - C{--cache="16M"}
- - C{--client="linux"}
- - C{--composite="1"}
- - C{--encryption="1"}
- - C{--geometry="3840x1150"}
- - C{--images="64M"}
- - C{--keyboard="pc102/gb"}
- - C{--link="lan"}
- - C{--media="0"}
- - C{--screeninfo="3840x1150x24+render"}
- - C{--session="localtest"}
- - C{--shmem="1"}
- - C{--shpix="1"}
- - C{--strict="0"}
- - C{--type="shadow"}
- @type args: string
- @param args: Parameters
- """
- ctx = self._ctx
- server = self._server
- mgr = self._ctx.session_mgr
- # Parse parameters
- params = self._GetParameters(args)
- parsed_params = dict(protocol.ParseParameters(params))
- # Parameters will be checked in nxnode
- try:
- shadowid = parsed_params["id"]
- except KeyError:
- raise protocol.NxProtocolError(500, ("Shadow session requested, "
- "but no session specified"))
- logging.info("Preparing to shadow session %r", shadowid)
- # Connect to daemon and ask for shadow cookie
- shadownodeclient = self._GetNodeClient(shadowid, False)
- try:
- logging.debug("Requesting shadow cookie from session %r", shadowid)
- shadowcookie = shadownodeclient.GetShadowCookie(None)
- finally:
- shadownodeclient.Close()
- logging.debug("Got shadow cookie %r", shadowcookie)
- sessid = mgr.CreateSessionID()
- logging.info("Starting new session %r", sessid)
- # Start nxnode daemon
- node.StartNodeDaemon(ctx.username, sessid)
- # Connect to daemon and tell it to shadow our session
- nodeclient = self._GetNodeClient(sessid, True)
- try:
- logging.debug("Sending attachsession command")
- nodeclient.AttachSession(parsed_params, shadowcookie)
- finally:
- nodeclient.Close()
- # Wait for session
- self._ConnectToSession(sessid, _SESSION_START_TIMEOUT)
- def _RestoreSession(self, args):
- """Handle the restoresession NX command.
- "restoresession" requests an existing session be resumed. It requires
- parameters be specified.
- The following parameters have been seen, from which at least the session id
- must be specified:
- - C{--backingstore="1"}
- - C{--cache="16M"}
- - C{--client="linux"}
- - C{--composite="1"}
- - C{--encryption="1"}
- - C{--geometry="3840x1150"}
- - C{--id="A28EBF5AAC354E9EEAFEEB867980C543"}
- - C{--images="64M"}
- - C{--keyboard="pc102/gb"}
- - C{--link="lan"}
- - C{--media="0"}
- - C{--rootless="1"}
- - C{--screeninfo="3840x1150x24+render"}
- - C{--session="localtest"}
- - C{--shmem="1"}
- - C{--shpix="1"}
- - C{--strict="0"}
- - C{--type="unix-gnome"}
- - C{--virtualdesktop="0"}
- @type args: string
- @param args: Parameters
- """
- ctx = self._ctx
- server = self._server
- mgr = ctx.session_mgr
- # Parse parameters
- params = self._GetParameters(args)
- parsed_params = dict(protocol.ParseParameters(params))
- # Parameters will be checked in nxnode
- try:
- sessid = parsed_params["id"]
- except KeyError:
- raise protocol.NxProtocolError(500, ("Restore session requested, "
- "but no session specified"))
- logging.info("Restoring session %r", sessid)
- # Try to find session
- sess = mgr.LoadSessionForUser(sessid, ctx.username)
- if sess is None:
- raise protocol.NxProtocolError(500, "Failed to load session")
- sessid = sess.id
- logging.info("Found session %r in session database", sessid)
- # Connect to daemon and tell it to restore our session
- nodeclient = self._GetNodeClient(sessid, False)
- try:
- logging.debug("Sending restoresession command")
- nodeclient.RestoreSession(parsed_params)
- finally:
- nodeclient.Close()
- # Already running sessions take a bit longer to restart
- self._ConnectToSession(sessid, _SESSION_RESTORE_TIMEOUT)
- def _GetParameters(self, args):
- """Returns parameters or, if none were given, query client for them.
- @type args: str
- @param args: Command arguments (can be empty)
- """
- server = self._server
- # Ask for parameters if none have been given
- if args:
- return args
- server.Write(106, NX_PROMPT_PARAMETERS, newline=False)
- try:
- return server.ReadLine()
- finally:
- server.WriteLine("")
- def _WriteSessionInfo(self, sess):
- """Writes session information required by client.
- @type sess: L{session.NxSession}
- @param sess: Session object
- """
- for code, message in GetClientSessionInfo(sess):
- self._server.Write(code, message=message)
- def _WaitForSessionReady(self, sessid, timeout):
- """Waits for a session to become ready for connecting.
- @type sessid: str
- @param sessid: Session ID
- @type timeout: int or float
- @param timeout: Timeout in seconds
- """
- mgr = self._ctx.session_mgr
- server = self._server
- def _CheckForSessionReady():
- sess = mgr.LoadSession(sessid)
- if sess:
- if sess.state == constants.SESS_STATE_WAITING:
- return sess
- elif sess.state in (constants.SESS_STATE_TERMINATING,
- constants.SESS_STATE_TERMINATED):
- logging.error("Session %r has status %r", sess.id, sess.state)
- server.Write(500, message=("Error: Session %r has status %r, "
- "aborting") % (sess.id, sess.state))
- raise protocol.NxQuitServer()
- raise utils.RetryAgain()
- logging.info("Waiting for session %r to achieve waiting status",
- sessid)
- try:
- return utils.Retry(_CheckForSessionReady, 0.1, 1.5, 1.0, timeout)
- except utils.RetryTimeout:
- logging.error(("Session %s has not achieved waiting status "
- "within %s seconds"), sessid, timeout)
- server.Write(500, "Session didn't become ready in time")
- raise protocol.NxQuitServer()
- def _ConnectToSession(self, sessid, timeout):
- """Waits for a session to become ready and stores the port.
- @type sessid: str
- @param sessid: Session ID
- @type timeout: int or float
- @param timeout: Timeout in seconds
- """
- server = self._server
- # TODO: Instead of polling for the session, the daemon could only return
- # once the session is ready.
- # Wait for session to become ready
- sess = self._WaitForSessionReady(sessid, timeout)
- # Send session details to client
- self._WriteSessionInfo(sess)
- server.Write(710, "Session status: running")
- # Store session port for use by netcat
- self._ctx.nxagent_port = sess.port
- def _GetNodeClient(self, sessid, retry):
- """Starts the nxnode RPC client for a session.
- @type sessid: str
- @param sessid: Session ID
- @type retry: bool
- @param retry: Whether to retry connecting several times
- @rtype: L{node.NodeClient}
- @return: Node client object
- """
- ctx = self._ctx
- mgr = ctx.session_mgr
- # Connect to nxnode
- nodeclient = node.NodeClient(mgr.GetSessionNodeSocket(sessid))
- logging.debug("Connecting to nxnode")
- nodeclient.Connect(retry)
- return nodeclient
- class NxServerContext(object):
- def __init__(self):
- self.username = None
- self.session_mgr = None
- self.nxagent_port = None
- class NxServer(protocol.NxServerBase):
- def __init__(self, ctx):
- protocol.NxServerBase.__init__(self, sys.stdin, sys.stdout,
- ServerCommandHandler(self, ctx))
- self._ctx = ctx
- def SendBanner(self):
- """Send banner to peer.
- """
- # TODO: Hostname in configuration?
- hostname = socket.getfqdn().lower()
- username = self._ctx.username
- self.Write(103, message="Welcome to: %s user: %s" % (hostname, username))
- class NxServerProgram(cli.GenericProgram):
- def BuildOptions(self):
- options = cli.GenericProgram.BuildOptions(self)
- options.extend([
- optparse.make_option("--proto", type="int", dest="proto"),
- ])
- return options
- def Run(self):
- if len(self.args) != 1:
- raise errors.GenericError("Username missing")
- (username, ) = self.args
- logging.info("Starting nxserver for user %s", username)
- ctx = NxServerContext()
- ctx.username = username
- ctx.session_mgr = session.NxSessionManager()
- try:
- NxServer(ctx).Start()
- finally:
- sys.stdout.flush()
- if ctx.nxagent_port is None:
- logging.debug("No nxagent port, not starting netcat")
- else:
- self._RunNetcat("localhost", ctx.nxagent_port)
- def _RunNetcat(self, host, port):
- """Starts netcat and returns only after it's done.
- @type host: str
- @param host: Hostname
- @type port: int
- @param port: Port
- """
- logging.info("Starting netcat (%s:%s)", host, port)
- stderr_logger = utils.LogFunctionWithPrefix(logging.error,
- "netcat stderr: ")
- args = [self.cfg.netcat, "--", host, str(port)]
- process = subprocess.Popen(args, shell=False, close_fds=True,
- stdin=None, stdout=None, stderr=subprocess.PIPE)
- for line in process.stderr:
- stderr_logger(line.rstrip())
- (exitcode, signum) = utils.GetExitcodeSignal(process.wait())
- if exitcode == 0 and signum is None:
- logging.debug("Netcat exited cleanly")
- else:
- logging.error("Netcat failed (code=%s, signal=%s)", exitcode, signum)
- def Main():
- logsetup = utils.LoggingSetup(PROGRAM)
- NxServerProgram(logsetup).Main()