PageRenderTime 74ms CodeModel.GetById 11ms app.highlight 58ms RepoModel.GetById 0ms app.codeStats 1ms

/neatx/lib/app/nxserver.py

http://neatx.googlecode.com/
Python | 736 lines | 698 code | 14 blank | 24 comment | 6 complexity | 71daf0b715a77dc3c7f2510267ff1d04 MD5 | raw file
  1#
  2#
  3
  4# Copyright (C) 2007 Google Inc.
  5#
  6# This program is free software; you can redistribute it and/or modify
  7# it under the terms of the GNU General Public License as published by
  8# the Free Software Foundation; either version 2 of the License, or
  9# (at your option) any later version.
 10#
 11# This program is distributed in the hope that it will be useful, but
 12# WITHOUT ANY WARRANTY; without even the implied warranty of
 13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 14# General Public License for more details.
 15#
 16# You should have received a copy of the GNU General Public License
 17# along with this program; if not, write to the Free Software
 18# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 19# 02110-1301, USA.
 20
 21
 22"""nxserver program for accepting nx connections.
 23
 24"""
 25
 26
 27import logging
 28import optparse
 29import socket
 30import subprocess
 31import sys
 32
 33from neatx import cli
 34from neatx import constants
 35from neatx import errors
 36from neatx import node
 37from neatx import protocol
 38from neatx import session
 39from neatx import utils
 40
 41
 42PROGRAM = "nxserver"
 43
 44NX_PROMPT_PARAMETERS = "Parameters: "
 45
 46_SESSION_START_TIMEOUT = 30
 47_SESSION_RESTORE_TIMEOUT = 60
 48
 49# TODO: Determine how the commercial NX version gets the depth from nxagent
 50DEFAULT_DEPTH = 24
 51
 52LISTSESSION_COLUMNS = [
 53    ("Display", 7, lambda sess: sess.display),
 54    ("Type", 16, lambda sess: sess.type),
 55    ("Session ID", 32, lambda sess: sess.id),
 56    ("Options", 8, lambda sess: FormatOptions(sess)),
 57    ("Depth", -5, lambda sess: DEFAULT_DEPTH),
 58    ("Screen", 14, lambda sess: FormatGeometry(sess)),
 59    ("Status", 11, lambda sess: FormatStatus(sess)),
 60    ("Session Name", 30, lambda sess: sess.name),
 61    ]
 62"""
 63Column definitions for "listsession" command.
 64
 65See L{utils.FormatTable} for more details.
 66
 67"""
 68
 69
 70def FormatOptions(sess):
 71  """Format session options for "listsessions" command.
 72
 73  """
 74  flags = []
 75  unset = "-"
 76
 77  # Fullscreen
 78  if sess.fullscreen:
 79    flags.append("F")
 80  else:
 81    flags.append(unset)
 82
 83  # Render
 84  if sess.screeninfo and "render" in sess.screeninfo:
 85    flags.append("R")
 86  else:
 87    flags.append(unset)
 88
 89  # Non-rootless (Desktop?)
 90  if sess.virtualdesktop:
 91    flags.append("D")
 92  else:
 93    flags.append(unset)
 94
 95  # Unknown
 96  flags.append(unset)
 97  flags.append(unset)
 98  flags.append("P")
 99  flags.append("S")
100  flags.append("A")
101
102  return "".join(flags)
103
104
105def FormatGeometry(sess):
106  if not sess.geometry:
107    return "-"
108
109  pos = sess.geometry.find("+")
110  if pos == -1:
111    return sess.geometry
112
113  return sess.geometry[:pos]
114
115
116def ConvertStatusForClient(status):
117  """Convert status for client.
118
119  The client doesn't know about the "terminating" and "suspending" statuses.
120
121  @type status: str
122  @param status: Server-side session status
123  @rtype: str
124  @return: Client-side session status
125
126  """
127  if status == constants.SESS_STATE_TERMINATING:
128    return constants.SESS_STATE_TERMINATED
129
130  if status == constants.SESS_STATE_SUSPENDING:
131    return constants.SESS_STATE_SUSPENDED
132
133  return status
134
135
136def FormatStatus(sess):
137  """Format session status for session list.
138
139  """
140  return ConvertStatusForClient(sess.state).capitalize()
141
142
143def _GetSessionCache(sess):
144  sesstype = sess.type
145  if sesstype.startswith(constants.SESS_TYPE_UNIX_PREFIX):
146    return sesstype
147  return constants.SESS_TYPE_UNIX_PREFIX + sesstype
148
149
150def GetClientSessionInfo(sess):
151  """Get session information for the client
152
153  This is used for starting/resuming a session.
154
155  """
156  # "702 Proxy IP: 1.2.3.4" is not used because we don't support unencrypted
157  # sessions anyway.
158  return [
159    (700, "Session id: %s" % sess.full_id),
160    (705, "Session display: %s" % sess.display),
161    (703, "Session type: %s" % sess.type),
162    (701, "Proxy cookie: %s" % sess.cookie),
163    (706, "Agent cookie: %s" % sess.cookie),
164    (704, "Session cache: %s" % _GetSessionCache(sess)),
165    (728, "Session caption: %s" % sess.windowname),
166    (707, "SSL tunneling: %s" % protocol.FormatNxBoolean(sess.ssl)),
167    (708, "Subscription: %s" % sess.subscription),
168    ]
169
170
171class ServerCommandHandler(object):
172  def __init__(self, server, ctx):
173    self._server = server
174    self._ctx = ctx
175
176  def __call__(self, cmdline):
177    """Parses and handles a command sent by the client.
178
179    @type cmdline: str
180    @param cmdline: Unparsed command
181
182    """
183    (cmd, args) = protocol.SplitCommand(cmdline)
184
185    # Confirm command
186    # TODO: Move confirmation code to protocol.py and use it from
187    # nxserver_login.py, too.
188    self._SendConfirmation(cmdline, cmd, args)
189
190    if cmd in (protocol.NX_CMD_LOGIN,
191               protocol.NX_CMD_HELLO,
192               protocol.NX_CMD_SET):
193      raise protocol.NxNotAfterLogin(cmd)
194
195    try:
196      if cmd == protocol.NX_CMD_BYE:
197        return self._Bye()
198
199      elif cmd == protocol.NX_CMD_LISTSESSION:
200        return self._ListSession(args)
201
202      elif cmd == protocol.NX_CMD_STARTSESSION:
203        return self._StartSession(args)
204
205      elif cmd == protocol.NX_CMD_ATTACHSESSION:
206        return self._AttachSession(args)
207
208      elif cmd == protocol.NX_CMD_RESTORESESSION:
209        return self._RestoreSession(args)
210
211    except errors.SessionParameterError, err:
212      logging.exception("Session parameter error")
213      raise protocol.NxProtocolError(500, err.args[0], fatal=True)
214
215    raise protocol.NxUndefinedCommand(cmd)
216
217  def _SendConfirmation(self, cmdline, cmd, args):
218    """Sends a command confirmation to the client.
219
220    """
221    server = self._server
222
223    if cmd == protocol.NX_CMD_STARTSESSION:
224      self._server.WriteLine("Start session with: " + args)
225      return
226
227    # The "set" command uses a different confirmation in the commercial version
228    # (as implemented in nxserver-login), but it shouldn't be used after login
229    # anyway.
230
231    server.WriteLine(cmdline.lstrip().capitalize())
232
233  def _Bye(self):
234    raise protocol.NxQuitServer()
235
236  def _ListSession(self, args):
237    """Handle the listsession NX command.
238
239    "listsession" requests a table of session information for the current
240    user. It requires parameters be specified.
241
242    The following parameters have been seen:
243
244      - C{--geometry="1920x1200x24+render"}:
245        This seems to specify the desired geometry.
246      - C{--status="suspended,running"}:
247        This seems to specify the desired type.
248      - C{--type="unix-gnome"}:
249        This seems to constrain the list to sessions in the given states.
250      - C{--user="someone"}:
251        This seems to be ignored. No matter what is specified, the user given at
252        login is used.
253
254    @type args: string
255    @param args: Parameters
256
257    """
258    ctx = self._ctx
259    server = self._server
260    mgr = ctx.session_mgr
261
262    # Parse parameters
263    parsed_params = dict(protocol.ParseParameters(self._GetParameters(args)))
264
265    # TODO: Accepted parameters
266
267    # Ignore --user, as per commercial implementation
268    # TODO: Check sessions from all users if type=shadow? This is problematic
269    # due to file system access permissions.
270    find_users = [self._ctx.username]
271
272    find_types = None
273    want_shadow = False
274
275    # Ignoring --user, as per commercial implementation
276
277    if "type" in parsed_params:
278      types = parsed_params["type"].split(",")
279
280      # If the type is shadow do the settings to get running sessions
281      if types[0] == constants.SESS_TYPE_SHADOW:
282        want_shadow = True
283      else:
284        find_types = types
285
286    if want_shadow:
287      find_states = constants.SESS_STATE_RUNNING
288    elif "status" in parsed_params:
289      find_states = parsed_params["status"].split(",")
290    else:
291      find_states = None
292
293    sessions = self._ListSessionInner(find_types, find_states)
294
295    server.Write(127, "Session list of user '%s':" % ctx.username)
296    for line in utils.FormatTable(sessions, LISTSESSION_COLUMNS):
297      server.WriteLine(line)
298    server.WriteLine("")
299    server.Write(148, ("Server capacity: not reached for user: %s" %
300                       ctx.username))
301
302  def _ListSessionInner(self, find_types, find_states):
303    """Returns a list of sessions filtered by parameters specified.
304
305    @type find_types: list
306    @param find_types: List of wanted session types
307    @type find_states: list
308    @param find_states: List of wanted (client) session states
309
310    """
311    ctx = self._ctx
312    mgr = ctx.session_mgr
313
314    logging.debug("Looking for sessions with types=%r, state=%r",
315                  find_types, find_states)
316
317    def _Filter(sess):
318      if find_states and ConvertStatusForClient(sess.state) not in find_states:
319        return False
320
321      if find_types and sess.type not in find_types:
322        return False
323
324      return True
325
326    return mgr.FindSessionsWithFilter(ctx.username, _Filter)
327
328  def _StartSession(self, args):
329    """Handle the startsession NX command.
330
331    "startsession" seems to request a new session be started. It requires
332    parameters be specified.
333
334    The following parameters have been seen:
335
336      - C{--backingstore="1"}
337      - C{--cache="16M"}
338      - C{--client="linux"}
339      - C{--composite="1"}
340      - C{--encryption="1"}
341      - C{--fullscreen="0"}
342      - C{--geometry="3840x1150"}
343      - C{--images="64M"}
344      - C{--keyboard="pc102/gb"}
345      - C{--link="lan"}
346      - C{--media="0"}
347      - C{--rootless="0"}
348      - C{--screeninfo="3840x1150x24+render"}
349      - C{--session="localtest"}
350      - C{--shmem="1"}
351      - C{--shpix="1"}
352      - C{--strict="0"}
353      - C{--type="unix-gnome"}
354      - C{--virtualdesktop="1"}
355
356    Experiments with this command by directly invoking nxserver have not
357    worked, as it refuses to create a session saying the unencrypted sessions
358    are not supported. This is independent of whether the --encryption option
359    has been set, so probably is related to the fact the nxserver has not been
360    launched by sshd.
361
362    @type args: string
363    @param args: Parameters
364
365    """
366    ctx = self._ctx
367    mgr = ctx.session_mgr
368    server = self._server
369
370    # Parse parameters
371    params = self._GetParameters(args)
372    parsed_params = dict(protocol.ParseParameters(params))
373
374    # Parameters will be checked in nxnode
375
376    sessid = mgr.CreateSessionID()
377    logging.info("Starting new session %r", sessid)
378
379    # Start nxnode daemon
380    node.StartNodeDaemon(ctx.username, sessid)
381
382    # Connect to daemon and tell it to start our session
383    nodeclient = self._GetNodeClient(sessid, True)
384    try:
385      logging.debug("Sending startsession command")
386      nodeclient.StartSession(parsed_params)
387    finally:
388      nodeclient.Close()
389
390    # Wait for session
391    self._ConnectToSession(sessid, _SESSION_START_TIMEOUT)
392
393  def _AttachSession(self, args):
394    """Handle the attachsession NX command.
395
396    "attachsession" seems to request a new shadow session be started. It
397    requires parameters be specified.
398
399    The following parameters have been seen:
400      - C{--backingstore="1"}
401      - C{--cache="16M"}
402      - C{--client="linux"}
403      - C{--composite="1"}
404      - C{--encryption="1"}
405      - C{--geometry="3840x1150"}
406      - C{--images="64M"}
407      - C{--keyboard="pc102/gb"}
408      - C{--link="lan"}
409      - C{--media="0"}
410      - C{--screeninfo="3840x1150x24+render"}
411      - C{--session="localtest"}
412      - C{--shmem="1"}
413      - C{--shpix="1"}
414      - C{--strict="0"}
415      - C{--type="shadow"}
416
417    @type args: string
418    @param args: Parameters
419
420    """
421    ctx = self._ctx
422    server = self._server
423    mgr = self._ctx.session_mgr
424
425    # Parse parameters
426    params = self._GetParameters(args)
427    parsed_params = dict(protocol.ParseParameters(params))
428
429    # Parameters will be checked in nxnode
430
431    try:
432      shadowid = parsed_params["id"]
433    except KeyError:
434      raise protocol.NxProtocolError(500, ("Shadow session requested, "
435                                           "but no session specified"))
436
437    logging.info("Preparing to shadow session %r", shadowid)
438
439    # Connect to daemon and ask for shadow cookie
440    shadownodeclient = self._GetNodeClient(shadowid, False)
441    try:
442      logging.debug("Requesting shadow cookie from session %r", shadowid)
443      shadowcookie = shadownodeclient.GetShadowCookie(None)
444    finally:
445      shadownodeclient.Close()
446
447    logging.debug("Got shadow cookie %r", shadowcookie)
448
449    sessid = mgr.CreateSessionID()
450    logging.info("Starting new session %r", sessid)
451
452    # Start nxnode daemon
453    node.StartNodeDaemon(ctx.username, sessid)
454
455    # Connect to daemon and tell it to shadow our session
456    nodeclient = self._GetNodeClient(sessid, True)
457    try:
458      logging.debug("Sending attachsession command")
459      nodeclient.AttachSession(parsed_params, shadowcookie)
460    finally:
461      nodeclient.Close()
462
463    # Wait for session
464    self._ConnectToSession(sessid, _SESSION_START_TIMEOUT)
465
466  def _RestoreSession(self, args):
467    """Handle the restoresession NX command.
468
469    "restoresession" requests an existing session be resumed. It requires
470    parameters be specified.
471
472    The following parameters have been seen, from which at least the session id
473    must be specified:
474
475      - C{--backingstore="1"}
476      - C{--cache="16M"}
477      - C{--client="linux"}
478      - C{--composite="1"}
479      - C{--encryption="1"}
480      - C{--geometry="3840x1150"}
481      - C{--id="A28EBF5AAC354E9EEAFEEB867980C543"}
482      - C{--images="64M"}
483      - C{--keyboard="pc102/gb"}
484      - C{--link="lan"}
485      - C{--media="0"}
486      - C{--rootless="1"}
487      - C{--screeninfo="3840x1150x24+render"}
488      - C{--session="localtest"}
489      - C{--shmem="1"}
490      - C{--shpix="1"}
491      - C{--strict="0"}
492      - C{--type="unix-gnome"}
493      - C{--virtualdesktop="0"}
494
495    @type args: string
496    @param args: Parameters
497
498    """
499    ctx = self._ctx
500    server = self._server
501    mgr = ctx.session_mgr
502
503    # Parse parameters
504    params = self._GetParameters(args)
505    parsed_params = dict(protocol.ParseParameters(params))
506
507    # Parameters will be checked in nxnode
508
509    try:
510      sessid = parsed_params["id"]
511    except KeyError:
512      raise protocol.NxProtocolError(500, ("Restore session requested, "
513                                           "but no session specified"))
514
515    logging.info("Restoring session %r", sessid)
516
517    # Try to find session
518    sess = mgr.LoadSessionForUser(sessid, ctx.username)
519    if sess is None:
520      raise protocol.NxProtocolError(500, "Failed to load session")
521
522    sessid = sess.id
523
524    logging.info("Found session %r in session database", sessid)
525
526    # Connect to daemon and tell it to restore our session
527    nodeclient = self._GetNodeClient(sessid, False)
528    try:
529      logging.debug("Sending restoresession command")
530      nodeclient.RestoreSession(parsed_params)
531    finally:
532      nodeclient.Close()
533
534    # Already running sessions take a bit longer to restart
535    self._ConnectToSession(sessid, _SESSION_RESTORE_TIMEOUT)
536
537  def _GetParameters(self, args):
538    """Returns parameters or, if none were given, query client for them.
539
540    @type args: str
541    @param args: Command arguments (can be empty)
542
543    """
544    server = self._server
545
546    # Ask for parameters if none have been given
547    if args:
548      return args
549
550    server.Write(106, NX_PROMPT_PARAMETERS, newline=False)
551    try:
552      return server.ReadLine()
553    finally:
554      server.WriteLine("")
555
556  def _WriteSessionInfo(self, sess):
557    """Writes session information required by client.
558
559    @type sess: L{session.NxSession}
560    @param sess: Session object
561
562    """
563    for code, message in GetClientSessionInfo(sess):
564      self._server.Write(code, message=message)
565
566  def _WaitForSessionReady(self, sessid, timeout):
567    """Waits for a session to become ready for connecting.
568
569    @type sessid: str
570    @param sessid: Session ID
571    @type timeout: int or float
572    @param timeout: Timeout in seconds
573
574    """
575    mgr = self._ctx.session_mgr
576    server = self._server
577
578    def _CheckForSessionReady():
579      sess = mgr.LoadSession(sessid)
580      if sess:
581        if sess.state == constants.SESS_STATE_WAITING:
582          return sess
583
584        elif sess.state in (constants.SESS_STATE_TERMINATING,
585                            constants.SESS_STATE_TERMINATED):
586          logging.error("Session %r has status %r", sess.id, sess.state)
587          server.Write(500, message=("Error: Session %r has status %r, "
588                                     "aborting") % (sess.id, sess.state))
589          raise protocol.NxQuitServer()
590
591      raise utils.RetryAgain()
592
593    logging.info("Waiting for session %r to achieve waiting status",
594                 sessid)
595
596    try:
597      return utils.Retry(_CheckForSessionReady, 0.1, 1.5, 1.0, timeout)
598    except utils.RetryTimeout:
599      logging.error(("Session %s has not achieved waiting status "
600                     "within %s seconds"), sessid, timeout)
601      server.Write(500, "Session didn't become ready in time")
602      raise protocol.NxQuitServer()
603
604  def _ConnectToSession(self, sessid, timeout):
605    """Waits for a session to become ready and stores the port.
606
607    @type sessid: str
608    @param sessid: Session ID
609    @type timeout: int or float
610    @param timeout: Timeout in seconds
611
612    """
613    server = self._server
614
615    # TODO: Instead of polling for the session, the daemon could only return
616    # once the session is ready.
617
618    # Wait for session to become ready
619    sess = self._WaitForSessionReady(sessid, timeout)
620
621    # Send session details to client
622    self._WriteSessionInfo(sess)
623    server.Write(710, "Session status: running")
624
625    # Store session port for use by netcat
626    self._ctx.nxagent_port = sess.port
627
628  def _GetNodeClient(self, sessid, retry):
629    """Starts the nxnode RPC client for a session.
630
631    @type sessid: str
632    @param sessid: Session ID
633    @type retry: bool
634    @param retry: Whether to retry connecting several times
635    @rtype: L{node.NodeClient}
636    @return: Node client object
637
638    """
639    ctx = self._ctx
640    mgr = ctx.session_mgr
641
642    # Connect to nxnode
643    nodeclient = node.NodeClient(mgr.GetSessionNodeSocket(sessid))
644
645    logging.debug("Connecting to nxnode")
646    nodeclient.Connect(retry)
647
648    return nodeclient
649
650
651class NxServerContext(object):
652  def __init__(self):
653    self.username = None
654    self.session_mgr = None
655    self.nxagent_port = None
656
657
658class NxServer(protocol.NxServerBase):
659  def __init__(self, ctx):
660    protocol.NxServerBase.__init__(self, sys.stdin, sys.stdout,
661                                   ServerCommandHandler(self, ctx))
662    self._ctx = ctx
663
664  def SendBanner(self):
665    """Send banner to peer.
666
667    """
668    # TODO: Hostname in configuration?
669    hostname = socket.getfqdn().lower()
670    username = self._ctx.username
671
672    self.Write(103, message="Welcome to: %s user: %s" % (hostname, username))
673
674
675class NxServerProgram(cli.GenericProgram):
676  def BuildOptions(self):
677    options = cli.GenericProgram.BuildOptions(self)
678    options.extend([
679      optparse.make_option("--proto", type="int", dest="proto"),
680      ])
681    return options
682
683  def Run(self):
684    if len(self.args) != 1:
685      raise errors.GenericError("Username missing")
686
687    (username, ) = self.args
688
689    logging.info("Starting nxserver for user %s", username)
690
691    ctx = NxServerContext()
692    ctx.username = username
693    ctx.session_mgr = session.NxSessionManager()
694
695    try:
696      NxServer(ctx).Start()
697    finally:
698      sys.stdout.flush()
699
700    if ctx.nxagent_port is None:
701      logging.debug("No nxagent port, not starting netcat")
702    else:
703      self._RunNetcat("localhost", ctx.nxagent_port)
704
705  def _RunNetcat(self, host, port):
706    """Starts netcat and returns only after it's done.
707
708    @type host: str
709    @param host: Hostname
710    @type port: int
711    @param port: Port
712
713    """
714    logging.info("Starting netcat (%s:%s)", host, port)
715
716    stderr_logger = utils.LogFunctionWithPrefix(logging.error,
717                                                "netcat stderr: ")
718
719    args = [self.cfg.netcat, "--", host, str(port)]
720
721    process = subprocess.Popen(args, shell=False, close_fds=True,
722                               stdin=None, stdout=None, stderr=subprocess.PIPE)
723
724    for line in process.stderr:
725      stderr_logger(line.rstrip())
726
727    (exitcode, signum) = utils.GetExitcodeSignal(process.wait())
728    if exitcode == 0 and signum is None:
729      logging.debug("Netcat exited cleanly")
730    else:
731      logging.error("Netcat failed (code=%s, signal=%s)", exitcode, signum)
732
733
734def Main():
735  logsetup = utils.LoggingSetup(PROGRAM)
736  NxServerProgram(logsetup).Main()