/neatx/lib/app/nxserver.py

http://neatx.googlecode.com/ · Python · 736 lines · 444 code · 90 blank · 202 comment · 32 complexity · 71daf0b715a77dc3c7f2510267ff1d04 MD5 · raw file

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