1# This file is part of Xpra.
2# Copyright (C) 2013-2020 Antoine Martin <antoine@xpra.org>
3# Copyright (C) 2008 Nathaniel Smith <njs@pobox.com>
4# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
5# later version. See the file COPYING for details.
6
7import os
8import sys
9import time
10from time import monotonic
11from multiprocessing import Queue as MQueue, freeze_support #@UnresolvedImport
12from gi.repository import GLib
13
14from xpra.util import (
15    LOGIN_TIMEOUT, AUTHENTICATION_ERROR, SESSION_NOT_FOUND, SERVER_ERROR,
16    repr_ellipsized, print_nested_dict, csv, envfloat, envbool, envint, typedict,
17    )
18from xpra.os_util import (
19    get_username_for_uid, get_groups, get_home_for_uid, bytestostr,
20    getuid, getgid, WIN32, POSIX, OSX,
21    umask_context, get_group_id,
22    )
23from xpra.net.socket_util import SOCKET_DIR_MODE, SOCKET_DIR_GROUP
24from xpra.server.server_core import ServerCore
25from xpra.server.control_command import ArgsControlCommand, ControlError
26from xpra.child_reaper import getChildReaper
27from xpra.scripts.parsing import parse_bool
28from xpra.scripts.config import make_defaults_struct, PROXY_START_OVERRIDABLE_OPTIONS, OPTION_TYPES
29from xpra.scripts.main import parse_display_name, connect_to, start_server_subprocess
30from xpra.make_thread import start_thread
31from xpra.log import Logger
32
33log = Logger("proxy")
34authlog = Logger("proxy", "auth")
35
36freeze_support()
37
38
39PROXY_SOCKET_TIMEOUT = envfloat("XPRA_PROXY_SOCKET_TIMEOUT", "0.1")
40PROXY_WS_TIMEOUT = envfloat("XPRA_PROXY_WS_TIMEOUT", "1.0")
41assert PROXY_SOCKET_TIMEOUT>0, "invalid proxy socket timeout"
42CAN_STOP_PROXY = envbool("XPRA_CAN_STOP_PROXY", getuid()!=0 or WIN32)
43STOP_PROXY_SOCKET_TYPES = os.environ.get("XPRA_STOP_PROXY_SOCKET_TYPES", "unix-domain,named-pipe").split(",")
44STOP_PROXY_AUTH_SOCKET_TYPES = os.environ.get("XPRA_STOP_PROXY_AUTH_SOCKET_TYPES", "unix-domain").split(",")
45#something (a thread lock?) doesn't allow us to use multiprocessing on MS Windows:
46PROXY_INSTANCE_THREADED = envbool("XPRA_PROXY_INSTANCE_THREADED", WIN32)
47PROXY_CLEANUP_GRACE_PERIOD = envfloat("XPRA_PROXY_CLEANUP_GRACE_PERIOD", "0.5")
48
49MAX_CONCURRENT_CONNECTIONS = envint("XPRA_PROXY_MAX_CONCURRENT_CONNECTIONS", 200)
50if WIN32:
51    #DEFAULT_ENV_WHITELIST = "ALLUSERSPROFILE,APPDATA,COMMONPROGRAMFILES,COMMONPROGRAMFILES(X86),COMMONPROGRAMW6432,COMPUTERNAME,COMSPEC,FP_NO_HOST_CHECK,LOCALAPPDATA,NUMBER_OF_PROCESSORS,OS,PATH,PATHEXT,PROCESSOR_ARCHITECTURE,PROCESSOR_ARCHITECTURE,PROCESSOR_IDENTIFIER,PROCESSOR_LEVEL,PROCESSOR_REVISION,PROGRAMDATA,PROGRAMFILES,PROGRAMFILES(X86),PROGRAMW6432,PSMODULEPATH,PUBLIC,SYSTEMDRIVE,SYSTEMROOT,TEMP,TMP,USERDOMAIN,WORKGROUP,USERNAME,USERPROFILE,WINDIR,XPRA_REDIRECT_OUTPUT,XPRA_LOG_FILENAME,XPRA_ALL_DEBUG"
52    DEFAULT_ENV_WHITELIST = "*"
53else:
54    DEFAULT_ENV_WHITELIST = "LANG,HOSTNAME,PWD,TERM,SHELL,SHLVL,PATH,USER,HOME"
55ENV_WHITELIST = os.environ.get("XPRA_PROXY_ENV_WHITELIST", DEFAULT_ENV_WHITELIST).split(",")
56
57
58def get_socktype(proto):
59    try:
60        return proto._conn.socktype
61    except AttributeError:
62        return "unknown"
63
64
65class ProxyServer(ServerCore):
66    """
67        This is the proxy server you can launch with "xpra proxy",
68        once authenticated, it will dispatch the connection
69        to the session found using the authenticator's
70        get_sessions() function.
71    """
72
73    def __init__(self):
74        log("ProxyServer.__init__()")
75        super().__init__()
76        self._max_connections = MAX_CONCURRENT_CONNECTIONS
77        self._start_sessions = False
78        self.session_type = "proxy"
79        self.main_loop = None
80        #proxy servers may have to connect to remote servers,
81        #or even start them, so allow more time before timing out:
82        self._accept_timeout += 10
83        self.pings = 0
84        self.video_encoders = ()
85        self._start_sessions = False
86        #keep track of the proxy process instances
87        #the display they're on and the message queue we can
88        # use to communicate with them
89        self.instances = {}
90        #connections used exclusively for requests:
91        self._requests = set()
92        self.idle_add = GLib.idle_add
93        self.timeout_add = GLib.timeout_add
94        self.source_remove = GLib.source_remove
95        self._socket_timeout = PROXY_SOCKET_TIMEOUT
96        self._ws_timeout = PROXY_WS_TIMEOUT
97
98    def init(self, opts):
99        log("ProxyServer.init(%s)", opts)
100        self.pings = int(opts.pings)
101        self.video_encoders = opts.proxy_video_encoders
102        self._start_sessions = opts.proxy_start_sessions
103        super().init(opts)
104        #ensure we cache the platform info before intercepting SIGCHLD
105        #as this will cause a fork and SIGCHLD to be emitted:
106        from xpra.version_util import get_platform_info
107        get_platform_info()
108        self.child_reaper = getChildReaper()
109        self.create_system_dir(opts.system_proxy_socket)
110
111    def create_system_dir(self, sps):
112        if not POSIX or OSX or not sps:
113            return
114        xpra_group_id = get_group_id(SOCKET_DIR_GROUP)
115        if sps.startswith("/run/xpra") or sps.startswith("/var/run/xpra"):
116            #create the directory and verify its permissions
117            #which should have been set correctly by tmpfiles.d,
118            #but may have been set wrong if created by systemd's socket activation instead
119            d = sps.split("/xpra")[0]+"/xpra"
120            try:
121                if os.path.exists(d):
122                    stat = os.stat(d)
123                    mode = stat.st_mode
124                    if (mode & SOCKET_DIR_MODE)!=SOCKET_DIR_MODE:
125                        log.warn("Warning: invalid permissions on '%s' : %s", d, oct(mode))
126                        mode = mode | SOCKET_DIR_MODE
127                        log.warn(" changing to %s", oct(mode))
128                        os.chmod(d, mode)
129                    if xpra_group_id>=0 and stat.st_gid!=xpra_group_id:
130                        import grp
131                        group = grp.getgrgid(stat.st_gid)[0]
132                        log.warn("Warning: invalid group on '%s': %s", d, group)
133                        log.warn(" changing to '%s'", SOCKET_DIR_GROUP)
134                        os.lchown(d, stat.st_uid, xpra_group_id)
135                else:
136                    log.info("creating '%s' with permissions %s and group '%s'",
137                             d, oct(SOCKET_DIR_MODE), SOCKET_DIR_GROUP)
138                    with umask_context(0):
139                        os.mkdir(d, SOCKET_DIR_MODE)
140                    stat = os.stat(d)
141                    if xpra_group_id>=0 and stat.st_gid!=xpra_group_id:
142                        os.lchown(d, stat.st_uid, xpra_group_id)
143                mode = os.stat(d).st_mode
144                log("%s permissions: %s", d, oct(mode))
145            except OSError as e:
146                log("create_system_dir()", exc_info=True)
147                log.error("Error: failed to create or change the permissions on '%s':", d)
148                log.error(" %s", e)
149
150    def init_control_commands(self):
151        super().init_control_commands()
152        self.control_commands["stop"] = ArgsControlCommand("stop", "stops the proxy instance on the given display",
153                                                           self.handle_stop_command, min_args=1, max_args=1)
154
155
156    def install_signal_handlers(self, callback):
157        from xpra.gtk_common.gobject_compat import register_os_signals, register_SIGUSR_signals
158        register_os_signals(callback, "Proxy Server")
159        register_SIGUSR_signals("Proxy Server")
160
161
162    def make_dbus_server(self):
163        from xpra.server.proxy.proxy_dbus_server import Proxy_DBUS_Server
164        return Proxy_DBUS_Server(self)
165
166
167    def init_packet_handlers(self):
168        super().init_packet_handlers()
169        #add shutdown handler
170        self._default_packet_handlers["shutdown-server"] = self._process_proxy_shutdown_server
171
172    def _process_proxy_shutdown_server(self, proto, _packet):
173        assert proto in self._requests
174        self.clean_quit(False)
175
176
177    def print_screen_info(self):
178        #no screen, we just use a virtual display number
179        pass
180
181    def get_server_mode(self):
182        return "proxy"
183
184    def init_aliases(self):
185        """
186        It is a lot less confusing if proxy servers don't use packet aliases at all.
187        So we override the aliases initialization and skip it.
188        """
189
190    def do_run(self):
191        self.main_loop = GLib.MainLoop()
192        self.main_loop.run()
193
194    def handle_stop_command(self, *args):
195        display = args[0]
196        log("stop command: will try to find proxy process for display %s", display)
197        for instance, v in dict(self.instances).items():
198            _, disp, _ = v
199            if disp==display:
200                log.info("stop command: found matching process %s with pid %i for display %s",
201                         instance, instance.pid, display)
202                self.stop_proxy(instance)
203                return "stopped proxy instance for display %s" % display
204        raise ControlError("no proxy found for display %s" % display)
205
206    def stop_all_proxies(self, force=False):
207        instances = self.instances
208        log("stop_all_proxies() will stop proxy instances: %s", instances)
209        for instance in tuple(instances.keys()):
210            self.stop_proxy(instance, force)
211        log("stop_all_proxies() done")
212
213    def stop_proxy(self, instance, force=False):
214        v = self.instances.get(instance)
215        if not v:
216            log.error("Error: proxy instance not found for %s", instance)
217            return
218        log("stop_proxy(%s) is_alive=%s", instance, instance.is_alive())
219        if not instance.is_alive() and not force:
220            return
221        isprocess, _, mq = v
222        log("stop_proxy(%s) %s", instance, v)
223        #different ways of stopping for process vs threaded implementations:
224        if isprocess:
225            #send message:
226            if force:
227                instance.terminate()
228            else:
229                mq.put_nowait("stop")
230                try:
231                    mq.close()
232                except Exception as e:
233                    log("%s() %s", mq.close, e)
234        else:
235            #direct method call:
236            instance.stop(None, "proxy server request")
237
238
239    def cleanup(self):
240        self.stop_all_proxies()
241        super().cleanup()
242        start = monotonic()
243        live = True
244        log("cleanup() proxy instances: %s", self.instances)
245        while monotonic()-start<PROXY_CLEANUP_GRACE_PERIOD and live:
246            live = tuple(x for x in tuple(self.instances.keys()) if x.is_alive())
247            if live:
248                log("cleanup() still %i proxies alive: %s", len(live), live)
249                time.sleep(0.1)
250        if live:
251            self.stop_all_proxies(True)
252        log("cleanup() frames remaining:")
253        from xpra.util import dump_all_frames
254        dump_all_frames(log)
255
256
257    def do_quit(self):
258        self.main_loop.quit()
259        #from now on, we can't rely on the main loop:
260        from xpra.os_util import register_SIGUSR_signals
261        register_SIGUSR_signals()
262
263    def log_closing_message(self):
264        log.info("Proxy Server process ended")
265
266
267    def verify_connection_accepted(self, protocol):
268        #if we start a proxy, the protocol will be closed
269        #(a new one is created in the proxy process)
270        if not protocol.is_closed():
271            self.send_disconnect(protocol, LOGIN_TIMEOUT)
272
273    def hello_oked(self, proto, packet, c, auth_caps):
274        if super().hello_oked(proto, packet, c, auth_caps):
275            #already handled in superclass
276            return
277        self.accept_client(proto, c)
278        generic_request = c.strget("request")
279        def is_req(mode):
280            return generic_request==mode or c.boolget("%s_request" % mode)
281        for x in ("screenshot", "event", "print", "exit"):
282            if is_req(x):
283                self.send_disconnect(proto, "error: invalid request, '%s' is not supported by the proxy server" % x)
284                return
285        if is_req("stop"):
286            #global kill switch:
287            if not CAN_STOP_PROXY:
288                msg = "cannot stop proxy server"
289                log.warn("Warning: %s", msg)
290                self.send_disconnect(proto, msg)
291                return
292            #verify socket type (only local connections by default):
293            socktype = get_socktype(proto)
294            if socktype not in STOP_PROXY_SOCKET_TYPES:
295                msg = "cannot stop proxy server from a '%s' connection" % socktype
296                log.warn("Warning: %s", msg)
297                log.warn(" only from: %s", csv(STOP_PROXY_SOCKET_TYPES))
298                self.send_disconnect(proto, msg)
299                return
300            #connection must be authenticated:
301            if socktype in STOP_PROXY_AUTH_SOCKET_TYPES and not proto.authenticators:
302                msg = "cannot stop proxy server from unauthenticated connections"
303                log.warn("Warning: %s", msg)
304                self.send_disconnect(proto, msg)
305                return
306            self._requests.add(proto)
307            #send a hello back and the client should then send its "shutdown-server" packet
308            capabilities = self.make_hello()
309            proto.send_now(("hello", capabilities))
310            def force_exit_request_client():
311                try:
312                    self._requests.remove(proto)
313                except KeyError:
314                    pass
315                if not proto.is_closed():
316                    self.send_disconnect(proto, "timeout")
317            self.timeout_add(10*1000, force_exit_request_client)
318            return
319        self.proxy_auth(proto, c, auth_caps)
320
321    def proxy_auth(self, client_proto, c, auth_caps):
322        def disconnect(reason, *extras):
323            log("disconnect(%s, %s)", reason, extras)
324            self.send_disconnect(client_proto, reason, *extras)
325
326        #find the target server session:
327        if not client_proto.authenticators:
328            log.error("Error: the proxy server requires an authentication mode,")
329            try:
330                log.error(" client connection '%s' does not specify one", get_socktype(client_proto))
331            except AttributeError:
332                pass
333            log.error(" use 'none' to disable authentication")
334            disconnect(SESSION_NOT_FOUND, "no sessions found")
335            return
336        sessions = None
337        authenticator = None
338        for authenticator in client_proto.authenticators:
339            try:
340                auth_sessions = authenticator.get_sessions()
341                authlog("proxy_auth %s.get_sessions()=%s", authenticator, auth_sessions)
342                if auth_sessions:
343                    sessions = auth_sessions
344                    break
345            except Exception as e:
346                authlog("failed to get the list of sessions from %s", authenticator, exc_info=True)
347                authlog.error("Error: failed to get the list of sessions using '%s' authenticator", authenticator)
348                authlog.error(" %s", e)
349                disconnect(AUTHENTICATION_ERROR, "cannot access sessions")
350                return
351        authlog("proxy_auth(%s, {..}, %s) found sessions: %s", client_proto, auth_caps, sessions)
352        if sessions is None:
353            disconnect(SESSION_NOT_FOUND, "no sessions found")
354            return
355        self.proxy_session(client_proto, c, auth_caps, sessions)
356
357    def proxy_session(self, client_proto, c, auth_caps, sessions):
358        def disconnect(reason, *extras):
359            log("disconnect(%s, %s)", reason, extras)
360            self.send_disconnect(client_proto, reason, *extras)
361        uid, gid, displays, env_options, session_options = sessions
362        if POSIX:
363            if getuid()==0:
364                if uid==0 or gid==0:
365                    log.error("Error: proxy instances cannot run as root")
366                    log.error(" use a different uid and gid (ie: nobody)")
367                    disconnect(AUTHENTICATION_ERROR, "cannot run proxy instances as root")
368                    return
369            else:
370                uid = getuid()
371                gid = getgid()
372            username = get_username_for_uid(uid)
373            password = None
374            groups = get_groups(username)
375            log("username(%i)=%s, groups=%s", uid, username, groups)
376        else:
377            #the auth module recorded the username we authenticate against
378            assert client_proto.authenticators
379            for authenticator in client_proto.authenticators:
380                username = getattr(authenticator, "username", "")
381                password = authenticator.get_password()
382                if username:
383                    break
384        #ensure we don't loop back to the proxy:
385        proxy_virtual_display = os.environ.get("DISPLAY")
386        if proxy_virtual_display in displays:
387            displays.remove(proxy_virtual_display)
388        #remove proxy instance virtual displays:
389        displays = [x for x in displays if not x.startswith(":proxy-")]
390        #log("unused options: %s, %s", env_options, session_options)
391        proc = None
392        socket_path = None
393        display = None
394        sns = typedict(c.dictget("start-new-session", {}))
395        authlog("proxy_session: displays=%s, start_sessions=%s, start-new-session=%s",
396                displays, self._start_sessions, sns)
397        if not displays or sns:
398            if not self._start_sessions:
399                disconnect(SESSION_NOT_FOUND, "no displays found")
400                return
401            try:
402                proc, socket_path, display = self.start_new_session(username, password, uid, gid, sns, displays)
403                log("start_new_session%s=%s", (username, "..", uid, gid, sns, displays), (proc, socket_path, display))
404            except Exception as e:
405                log("start_server_subprocess failed", exc_info=True)
406                log.error("Error: failed to start server subprocess:")
407                log.error(" %s", e)
408                disconnect(SERVER_ERROR, "failed to start a new session")
409                return
410        if display is None:
411            display = c.strget("display")
412            authlog("proxy_session: proxy-virtual-display=%s (ignored), user specified display=%s, found displays=%s",
413                    proxy_virtual_display, display, displays)
414            if display==proxy_virtual_display:
415                disconnect(SESSION_NOT_FOUND, "invalid display: proxy display")
416                return
417            if display:
418                if display not in displays:
419                    if ":%s" % display in displays:
420                        display = ":%s" % display
421                    else:
422                        disconnect(SESSION_NOT_FOUND, "display '%s' not found" % display)
423                        return
424            else:
425                if len(displays)!=1:
426                    disconnect(SESSION_NOT_FOUND,
427                               "please specify a display, more than one is available: %s" % csv(displays))
428                    return
429                display = displays[0]
430
431        connect = c.boolget("connect", True)
432        #ConnectTestXpraClient doesn't want to connect to the real session either:
433        ctr = c.strget("connect_test_request")
434        log("connect=%s, connect_test_request=%s", connect, ctr)
435        if not connect or ctr:
436            log("proxy_session: not connecting to the session")
437            hello = {"display" : display}
438            if socket_path:
439                hello["socket-path"] = socket_path
440            #echo mode if present:
441            mode = sns.strget("mode")
442            if mode:
443                hello["mode"] = mode
444            client_proto.send_now(("hello", hello))
445            return
446
447        def stop_server_subprocess():
448            log("stop_server_subprocess() proc=%s", proc)
449            if proc and proc.poll() is None:
450                proc.terminate()
451
452        log("start_proxy(%s, {..}, %s) using server display at: %s", client_proto, auth_caps, display)
453        def parse_error(*args):
454            stop_server_subprocess()
455            disconnect(SESSION_NOT_FOUND, "invalid display string")
456            log.warn("Error: parsing failed for display string '%s':", display)
457            for arg in args:
458                log.warn(" %s", arg)
459            raise Exception("parse error on %s: %s" % (display, args))
460        opts = make_defaults_struct(username=username, uid=uid, gid=gid)
461        opts.username = username
462        disp_desc = parse_display_name(parse_error, opts, display)
463        if uid or gid:
464            disp_desc["uid"] = uid
465            disp_desc["gid"] = gid
466        log("display description(%s) = %s", display, disp_desc)
467        try:
468            server_conn = connect_to(disp_desc, opts)
469        except Exception as e:
470            log("cannot connect", exc_info=True)
471            log.error("Error: cannot start proxy connection:")
472            for x in str(e).splitlines():
473                log.error(" %s", x)
474            log.error(" connection definition:")
475            print_nested_dict(disp_desc, prefix=" ", lchar="*", pad=20, print_fn=log.error)
476            disconnect(SESSION_NOT_FOUND, "failed to connect to display")
477            stop_server_subprocess()
478            return
479        log("server connection=%s", server_conn)
480
481        cipher = cipher_mode = None
482        encryption_key = None
483        if auth_caps:
484            cipher = auth_caps.get("cipher")
485            if cipher:
486                from xpra.net.crypto import DEFAULT_MODE
487                cipher_mode = auth_caps.get("cipher.mode", DEFAULT_MODE)
488                encryption_key = self.get_encryption_key(client_proto.authenticators, client_proto.keyfile)
489
490        use_thread = PROXY_INSTANCE_THREADED
491        if not use_thread:
492            client_socktype = get_socktype(client_proto)
493            server_socktype = disp_desc["type"]
494            if client_socktype in ("ssl", "wss", "ssh"):
495                log.info("using threaded mode for %s client connection", client_socktype)
496                use_thread = True
497            elif server_socktype in ("ssl", "wss", "ssh"):
498                log.info("using threaded mode for %s server connection", server_socktype)
499                use_thread = True
500        if use_thread:
501            if env_options:
502                log.warn("environment options are ignored in threaded mode")
503            from xpra.server.proxy.proxy_instance_thread import ProxyInstanceThread
504            pit = ProxyInstanceThread(session_options, self.video_encoders, self.pings,
505                                      client_proto, server_conn,
506                                      disp_desc, cipher, cipher_mode, encryption_key, c)
507            pit.stopped = self.reap
508            pit.run()
509            self.instances[pit] = (False, display, None)
510            return
511
512        #this may block, so run it in a thread:
513        def start_proxy_process():
514            log("start_proxy_process()")
515            message_queue = MQueue()
516            client_conn = None
517            try:
518                #no other packets should be arriving until the proxy instance responds to the initial hello packet
519                def unexpected_packet(packet):
520                    if packet:
521                        log.warn("Warning: received an unexpected packet")
522                        log.warn(" from the proxy connection %s:", client_proto)
523                        log.warn(" %s", repr_ellipsized(packet))
524                        client_proto.close()
525                client_conn = client_proto.steal_connection(unexpected_packet)
526                client_state = client_proto.save_state()
527                log("start_proxy_process(..) client connection=%s", client_conn)
528                log("start_proxy_process(..) client state=%s", client_state)
529
530                ioe = client_proto.wait_for_io_threads_exit(5+self._socket_timeout)
531                if not ioe:
532                    log.error("Error: some network IO threads have failed to terminate")
533                    client_proto.close()
534                    return
535                client_conn.set_active(True)
536                from xpra.server.proxy.proxy_instance_process import ProxyInstanceProcess
537                process = ProxyInstanceProcess(uid, gid, env_options, session_options, self._socket_dir,
538                                               self.video_encoders, self.pings,
539                                               client_conn, disp_desc, client_state,
540                                               cipher, cipher_mode, encryption_key, server_conn, c, message_queue)
541                log("starting %s from pid=%s", process, os.getpid())
542                self.instances[process] = (True, display, message_queue)
543                process.start()
544                log("ProxyInstanceProcess started")
545                popen = process._popen
546                assert popen
547                #when this process dies, run reap to update our list of proxy instances:
548                self.child_reaper.add_process(popen, "xpra-proxy-%s" % display,
549                                              "xpra-proxy-instance", True, True, self.reap)
550            except Exception as e:
551                log("start_proxy_process() failed", exc_info=True)
552                log.error("Error starting proxy instance process:")
553                log.error(" %s", e)
554                message_queue.put("error: %s" % e)
555                message_queue.put("stop")
556            finally:
557                #now we can close our handle on the connection:
558                log("handover complete: closing connection from proxy server")
559                if client_conn:
560                    client_conn.close()
561                server_conn.close()
562                log("sending socket-handover-complete")
563                message_queue.put("socket-handover-complete")
564        start_thread(start_proxy_process, "start_proxy(%s)" % client_proto)
565
566    def start_new_session(self, username, _password, uid, gid, new_session_dict=None, displays=()):
567        log("start_new_session%s", (username, "..", uid, gid, new_session_dict, displays))
568        sns = typedict(new_session_dict or {})
569        mode = sns.strget("mode", "start")
570        assert mode in ("start", "start-desktop", "shadow"), "invalid start-new-session mode '%s'" % mode
571        display = sns.strget("display")
572        if display in displays:
573            raise Exception("display %s is already active!" % display)
574        log("starting new server subprocess: mode=%s, display=%s", mode, display)
575        args = []
576        if display:
577            args = [display]
578        #allow the client to override some options:
579        opts = make_defaults_struct(username=username, uid=uid, gid=gid)
580        for k,v in sns.items():
581            k = bytestostr(k)
582            if k in ("mode", "display"):
583                continue    #those special attributes have been consumed already
584            if k not in PROXY_START_OVERRIDABLE_OPTIONS:
585                log.warn("Warning: ignoring invalid start override")
586                log.warn(" %s=%s", k, v)
587                continue
588            try:
589                vt = OPTION_TYPES[k]
590                if vt==str:
591                    v = bytestostr(v)
592                elif vt==bool:
593                    v = parse_bool(k, v)
594                elif vt==int:
595                    v = int(v)
596                elif vt==list:
597                    v = list(bytestostr(x) for x in v)
598            except ValueError:
599                log("start_new_session: override option %s", k, exc_info=True)
600                log.warn("Warning: ignoring invalid value %s for %s (%s)", v, k, vt)
601                continue
602            if v is not None:
603                fn = k.replace("-", "_")
604                curr = getattr(opts, fn, None)
605                if curr!=v:
606                    log("start override: %24s=%-24s (default=%s)", k, v, curr)
607                    setattr(opts, fn, v)
608                else:
609                    log("start override: %24s=%-24s (unchanged)", k, v)
610            else:
611                log("start override: %24s=%-24s (invalid, unchanged)", k, v)
612        opts.attach = False
613        opts.start_via_proxy = False
614        env = self.get_proxy_env()
615        cwd = None
616        if uid>0:
617            cwd = get_home_for_uid(uid) or None
618            if not cwd or not os.path.exists(cwd):
619                import tempfile
620                cwd = tempfile.gettempdir()
621        log("starting new server subprocess: options=%s", opts)
622        log("env=%s", env)
623        log("args=%s", args)
624        log("cwd=%s", cwd)
625        proc, socket_path, display = start_server_subprocess(sys.argv[0], args,
626                                                             mode, opts, username, uid, gid, env, cwd)
627        if proc:
628            self.child_reaper.add_process(proc, "server-%s" % (display or socket_path), "xpra %s" % mode, True, True)
629        log("start_new_session(..) pid=%s, socket_path=%s, display=%s, ", proc.pid, socket_path, display)
630        return proc, socket_path, display
631
632    def get_proxy_env(self):
633        env = dict((k,v) for k,v in os.environ.items() if k in ENV_WHITELIST or "*" in ENV_WHITELIST)
634        #env var to add to environment of subprocess:
635        extra_env_str = os.environ.get("XPRA_PROXY_START_ENV", "")
636        if extra_env_str:
637            extra_env = {}
638            for e in extra_env_str.split(os.path.pathsep):  #ie: "A=1:B=2"
639                parts = e.split("=", 1)
640                if len(parts)==2:
641                    extra_env[parts[0]]= parts[1]
642            log("extra_env(%s)=%s", extra_env_str, extra_env)
643            env.update(extra_env)
644        return env
645
646
647    def reap(self, *args):
648        log("reap%s", args)
649        dead = []
650        for instance in tuple(self.instances.keys()):
651            #instance is a process
652            if not instance.is_alive():
653                dead.append(instance)
654        log("reap%s dead processes: %s", args, dead or None)
655        for p in dead:
656            del self.instances[p]
657
658
659    def get_info(self, proto, *_args):
660        authenticated = proto and proto.authenticators
661        if not authenticated:
662            info = super().get_server_info()
663        else:
664            #only show more info if we have authenticated
665            #as the user running the proxy server process:
666            info = super().get_info(proto)
667            sessions = ()
668            for authenticator in proto.authenticators:
669                auth_sessions = authenticator.get_sessions()
670                if auth_sessions:
671                    sessions = auth_sessions
672                    break
673            if sessions:
674                uid, gid = sessions[:2]
675                if not POSIX or (uid==getuid() and gid==getgid()):
676                    self.reap()
677                    i = 0
678                    instances = dict(self.instances)
679                    instances_info = {}
680                    for proxy_instance, v in instances.items():
681                        isprocess, d, _ = v
682                        iinfo = {
683                            "display"    : d,
684                            "live"       : proxy_instance.is_alive(),
685                            }
686                        if isprocess:
687                            iinfo.update({
688                                "pid"        : proxy_instance.pid,
689                                })
690                        else:
691                            iinfo.update(proxy_instance.get_info())
692                        instances_info[i] = iinfo
693                        i += 1
694                    info["instances"] = instances_info
695                    info["proxies"] = len(instances)
696        info.setdefault("server", {})["type"] = "Python/GLib/proxy"
697        return info
698