Skip to content

Commit

Permalink
add uid and gid support on the command line: the server can be starte…
Browse files Browse the repository at this point in the history
…d as root and bind to low ports, then we change uid / gid

git-svn-id: https://xpra.org/svn/Xpra/trunk@15810 3bb7dfac-3a0b-4e04-842a-767bc560f471
  • Loading branch information
totaam committed May 11, 2017
1 parent 95c7e15 commit 02946ec
Show file tree
Hide file tree
Showing 9 changed files with 185 additions and 136 deletions.
7 changes: 7 additions & 0 deletions src/man/xpra.1
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ xpra \- viewer for remote, persistent X applications
\fBxpra\fP \fBshadow\fP [\fI:DISPLAY\fP] | \fIssh:[USER@]HOST[:DISPLAY]\fP
[\fB\-\-env\fP=\fIKEY=VALUE\fP]\fB .\|.\|.\fP
[\fB\-\-exit\-with\-children\fP]
[\fB\-\-uid\fP=\fIUID\fP]
[\fB\-\-gid\fP=\fIGID\fP]
[\fB\-\-daemon\fP=\fIyes\fP|\fIno\fP]
[\fB\-\-pidfile\fP=\fIFILENAME\fP]
[\fB\-\-readonly\fP=\fIyes\fP|\fIno\fP]
Expand Down Expand Up @@ -672,6 +674,11 @@ i.e. 'daemonizes', and redirects its output to a log file. This
prevents that behavior (useful mostly for debugging).
.TP

\fB\-\-uid\fP=\fIUID\fP and \fB\-\-gid\fP=\fIGID\fP
When launching the server as root, these options can be used
to drop privileges to the given UID / GID.
.TP

\fB\-\-pidfile\fP=\fIFILENAME\fP
Writes the server process ID to this file on startup.
If the file has not been replaced,
Expand Down
52 changes: 52 additions & 0 deletions src/xpra/os_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,15 @@ def get_username_for_uid(uid):
pass
return ""

def get_home_for_uid(uid):
if os.name=="posix":
from pwd import getpwuid
try:
return getpwuid(uid).pw_dir
except KeyError:
pass
return ""

def get_groups(username):
if os.name=="posix":
import grp #@UnresolvedImport
Expand Down Expand Up @@ -506,6 +515,49 @@ def get_ssh_port():
return 22


def setuidgid(uid, gid):
if os.name!="posix":
return
from xpra.log import Logger
log = Logger("server")
if os.getuid()!=uid or os.getgid()!=gid:
#find the username for the given uid:
from pwd import getpwuid
try:
username = getpwuid(uid).pw_name
except KeyError:
raise Exception("uid %i not found" % uid)
#set the groups:
if hasattr(os, "initgroups"): # python >= 2.7
os.initgroups(username, gid)
else:
import grp #@UnresolvedImport
groups = [gr.gr_gid for gr in grp.getgrall() if (username in gr.gr_mem)]
os.setgroups(groups)
#change uid and gid:
try:
if os.getgid()!=gid:
os.setgid(gid)
except OSError as e:
log.error("Error: cannot change gid to %i:", gid)
if os.getgid()==0:
#don't run as root!
raise
log.error(" %s", e)
log.error(" continuing with gid=%i", os.getgid())
try:
if os.getuid()!=uid:
os.setuid(uid)
except OSError as e:
log.error("Error: cannot change uid to %i:", uid)
if os.getuid()==0:
#don't run as root!
raise
log.error(" %s", e)
log.error(" continuing with gid=%i", os.getuid())
log("new uid=%s, gid=%s", os.getuid(), os.getgid())


def main():
from xpra.log import Logger
log = Logger("util")
Expand Down
8 changes: 7 additions & 1 deletion src/xpra/scripts/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,8 @@ def may_create_user_config(xpra_conf_filename=DEFAULT_XPRA_CONF_FILENAME):
"server-idle-timeout" : int,
"sync-xvfb" : int,
"pixel-depth" : int,
"uid" : int,
"gid" : int,
#float options:
"auto-refresh-delay": float,
#boolean options:
Expand Down Expand Up @@ -599,7 +601,7 @@ def may_create_user_config(xpra_conf_filename=DEFAULT_XPRA_CONF_FILENAME):

#keep track of the options added since v1,
#so we can generate command lines that work with older supported versions:
OPTIONS_ADDED_SINCE_V1 = ["attach", "open-files", "pixel-depth", ""]
OPTIONS_ADDED_SINCE_V1 = ["attach", "open-files", "pixel-depth", "uid", "gid"]

CLIENT_OPTIONS = ["title", "username", "password", "session-name",
"dock-icon", "tray-icon", "window-icon",
Expand Down Expand Up @@ -750,6 +752,8 @@ def addtrailingslash(v):
if sys.version_info<(2, 7, 9):
ssl_protocol = "SSLv23"

from xpra.os_util import getuid, getgid

GLOBAL_DEFAULTS = {
"encoding" : "",
"title" : "@title@ on @client-machine@",
Expand Down Expand Up @@ -839,6 +843,8 @@ def addtrailingslash(v):
"server-idle-timeout" : 0,
"sync-xvfb" : 0,
"pixel-depth" : 24,
"uid" : getuid(),
"gid" : getgid(),
"auto-refresh-delay": 0.15,
"daemon" : CAN_DAEMONIZE,
"attach" : None,
Expand Down
79 changes: 18 additions & 61 deletions src/xpra/scripts/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from xpra.platform.features import LOCAL_SERVERS_SUPPORTED, SHADOW_SUPPORTED, CAN_DAEMONIZE
from xpra.util import csv, envbool, envint, DEFAULT_PORT
from xpra.exit_codes import EXIT_SSL_FAILURE, EXIT_SSH_FAILURE
from xpra.os_util import getuid, getgid, monotonic_time, setsid, WIN32, OSX
from xpra.os_util import getuid, getgid, monotonic_time, setsid, get_username_for_uid, WIN32, OSX
from xpra.scripts.config import OPTION_TYPES, CLIENT_OPTIONS, NON_COMMAND_LINE_OPTIONS, START_COMMAND_OPTIONS, BIND_OPTIONS, OPTIONS_ADDED_SINCE_V1, \
InitException, InitInfo, InitExit, \
fixup_debug_option, fixup_options, dict_to_validated_config, \
Expand Down Expand Up @@ -358,6 +358,18 @@ def ignore(defaults):
"html" : ""})
legacy_bool_parse("daemon")
legacy_bool_parse("attach")
if os.name=="posix" and os.getuid()==0:
group.add_option("--uid", action="store",
dest="uid", default=defaults.uid,
help="The user id to change to when the server is started by root. Default: %s." % defaults.uid)
group.add_option("--gid", action="store",
dest="gid", default=defaults.gid,
help="The group id to change to when the server is started by root. Default: %s." % defaults.gid)
else:
ignore({
"uid" : defaults.uid,
"gid" : defaults.gid,
})
if (supports_server or supports_shadow) and CAN_DAEMONIZE:
group.add_option("--daemon", action="store", metavar="yes|no",
dest="daemon", default=defaults.daemon,
Expand Down Expand Up @@ -2348,57 +2360,8 @@ def run_glcheck(opts):
return 0


def setuidgid(uid, gid):
if os.name!="posix":
return
from xpra.log import Logger
log = Logger("server")
if os.getuid()!=uid or os.getgid()!=gid:
#find the username for the given uid:
from pwd import getpwuid
try:
username = getpwuid(uid).pw_name
except KeyError:
raise Exception("uid %i not found" % uid)
#set the groups:
if hasattr(os, "initgroups"): # python >= 2.7
os.initgroups(username, gid)
else:
import grp #@UnresolvedImport
groups = [gr.gr_gid for gr in grp.getgrall() if (username in gr.gr_mem)]
os.setgroups(groups)
#change uid and gid:
try:
if os.getgid()!=gid:
os.setgid(gid)
except OSError as e:
log.error("Error: cannot change gid to %i:", gid)
if os.getgid()==0:
#don't run as root!
raise
log.error(" %s", e)
log.error(" continuing with gid=%i", os.getgid())
try:
if os.getuid()!=uid:
os.setuid(uid)
except OSError as e:
log.error("Error: cannot change uid to %i:", uid)
if os.getuid()==0:
#don't run as root!
raise
log.error(" %s", e)
log.error(" continuing with gid=%i", os.getuid())
log("new uid=%s, gid=%s", os.getuid(), os.getgid())


def start_server_subprocess(script_file, args, mode, opts, uid=getuid(), gid=getgid()):
username = ""
home = ""
if os.name=="posix":
import pwd
e = pwd.getpwuid(uid)
username = e.pw_name
home = e.pw_dir
username = get_username_for_uid(uid)
dotxpra = DotXpra(opts.socket_dir, opts.socket_dirs, username, uid=uid, gid=gid)
#we must use a subprocess to avoid messing things up - yuk
assert mode in ("start", "start-desktop", "shadow")
Expand Down Expand Up @@ -2469,17 +2432,11 @@ def start_server_subprocess(script_file, args, mode, opts, uid=getuid(), gid=get
else:
def preexec():
setsid()
if uid!=0 or gid!=0:
setuidgid(uid, gid)
cmd.append("--systemd-run=no")
server_env = os.environ.copy()
if username:
server_env.update({
"USER" : username,
"USERNAME" : username,
"HOME" : home or os.path.join("/home", username),
})
proc = Popen(cmd, shell=False, close_fds=True, env=server_env, preexec_fn=preexec)
if os.name=="posix" and getuid()==0 and (uid!=0 or gid!=0):
cmd.append("--uid=%i" % uid)
cmd.append("--gid=%i" % gid)
proc = Popen(cmd, shell=False, close_fds=True, preexec_fn=setsid)
socket_path = identify_new_socket(proc, dotxpra, existing_sockets, matching_display, new_server_uuid, display_name, uid)
return proc, socket_path

Expand Down
82 changes: 28 additions & 54 deletions src/xpra/scripts/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,11 @@

from xpra.scripts.main import warn, no_gtk, validate_encryption
from xpra.scripts.config import InitException, TRUE_OPTIONS, FALSE_OPTIONS
from xpra.os_util import SIGNAMES, close_fds, get_ssh_port, WIN32, OSX
from xpra.util import envint, envbool
from xpra.os_util import SIGNAMES, close_fds, get_ssh_port, get_username_for_uid, get_home_for_uid, getuid, getgid, setuidgid, WIN32, OSX
from xpra.util import envbool
from xpra.platform.dotxpra import DotXpra


#what timeout value to use on the socket probe attempt:
WAIT_PROBE_TIMEOUT = envint("XPRA_WAIT_PROBE_TIMEOUT", 6)


_cleanups = []
def run_cleanups():
global _cleanups
Expand Down Expand Up @@ -161,48 +157,6 @@ def display_name_check(display_name):
pass


#warn just once:
MDNS_WARNING = False
def mdns_publish(display_name, mode, listen_on, text_dict={}):
global MDNS_WARNING
if MDNS_WARNING is True:
return
PREFER_PYBONJOUR = envbool("XPRA_PREFER_PYBONJOUR", False) or WIN32 or OSX
try:
from xpra.net import mdns
assert mdns
if PREFER_PYBONJOUR:
from xpra.net.mdns.pybonjour_publisher import BonjourPublishers as MDNSPublishers, get_interface_index
else:
from xpra.net.mdns.avahi_publisher import AvahiPublishers as MDNSPublishers, get_interface_index
except ImportError as e:
MDNS_WARNING = True
from xpra.log import Logger
log = Logger("mdns")
log("mdns import failure", exc_info=True)
log.warn("Warning: failed to load the mdns %s publisher:", ["avahi", "pybonjour"][PREFER_PYBONJOUR])
log.warn(" %s", e)
log.warn(" either fix your installation or use the 'mdns=no' option")
return
d = text_dict.copy()
d["mode"] = mode
#ensure we don't have duplicate interfaces:
f_listen_on = {}
for host, port in listen_on:
f_listen_on[get_interface_index(host)] = (host, port)
try:
name = socket.gethostname()
except:
name = "Xpra"
if display_name and not (OSX or WIN32):
name += " %s" % display_name
if mode!="tcp":
name += " (%s)" % mode
ap = MDNSPublishers(f_listen_on.values(), name, text_dict=d)
_when_ready.append(ap.start)
_cleanups.append(ap.stop)


def sanitize_env():
def unsetenv(*varnames):
for x in varnames:
Expand Down Expand Up @@ -415,12 +369,23 @@ def run_server(error_cb, opts, mode, xpra_file, extra_args, desktop_display=None
from xpra.server.server_util import xpra_runner_shell_script, write_runner_shell_scripts, pam_open, write_pidfile, find_log_dir
script = xpra_runner_shell_script(xpra_file, cwd, opts.socket_dir)

uid = int(opts.uid)
gid = int(opts.gid)
username = get_username_for_uid(uid)
home = get_home_for_uid(uid)
def fchown(fd):
if os.name=="posix" and uid!=getuid() or gid!=getgid():
try:
os.fchown(fd, uid, gid)
except:
pass

if start_vfb or opts.daemon:
#we will probably need a log dir
#either for the vfb, or for our own log file
log_dir = opts.log_dir or ""
if not log_dir or log_dir.lower()=="auto":
log_dir = find_log_dir()
log_dir = find_log_dir(username, uid=uid, gid=gid)
if not log_dir:
raise InitException("cannot find or create a logging directory")
#expose the log-dir as "XPRA_LOG_DIR",
Expand All @@ -439,6 +404,7 @@ def run_server(error_cb, opts, mode, xpra_file, extra_args, desktop_display=None
# so log_filename0 may point to a temporary file which we will rename later
log_filename0 = select_log_file(log_dir, opts.log_file, display_name)
logfd = open_log_file(log_filename0)
fchown(logfd)
assert logfd > 2
stdout, stderr = daemonize(logfd)
try:
Expand All @@ -450,12 +416,12 @@ def run_server(error_cb, opts, mode, xpra_file, extra_args, desktop_display=None
pass

if opts.pidfile:
write_pidfile(opts.pidfile)
write_pidfile(opts.pidfile, withfd=fchown)

if os.name=="posix":
# Write out a shell-script so that we can start our proxy in a clean
# environment:
write_runner_shell_scripts(script)
write_runner_shell_scripts(script, withfd=fchown)

from xpra.log import Logger
log = Logger("server")
Expand Down Expand Up @@ -559,7 +525,7 @@ def run_server(error_cb, opts, mode, xpra_file, extra_args, desktop_display=None
assert not proxying
pixel_depth = validate_pixel_depth(opts.pixel_depth)
try:
xvfb, display_name, xauth_data = start_Xvfb(opts.xvfb, pixel_depth, display_name, cwd)
xvfb, display_name, xauth_data = start_Xvfb(opts.xvfb, pixel_depth, display_name, cwd, uid, gid)
except OSError as e:
log.error("Error starting Xvfb:")
log.error(" %s", e)
Expand Down Expand Up @@ -614,6 +580,14 @@ def noerr(fn, *args):
#xvfb problem: exit now
return 1

if os.name=="posix" and getuid()==0 and uid!=0:
setuidgid(uid, gid)
os.environ.update({
"HOME" : home,
"USER" : username,
"LOGNAME" : username,
})

display = None
if not proxying:
no_gtk()
Expand All @@ -635,7 +609,7 @@ def noerr(fn, *args):
#we always need at least one valid socket dir
from xpra.platform.paths import get_socket_dirs
opts.socket_dirs = get_socket_dirs()
local_sockets = setup_local_sockets(opts.bind, opts.socket_dir, opts.socket_dirs, display_name, clobber, opts.mmap_group, opts.socket_permissions)
local_sockets = setup_local_sockets(opts.bind, opts.socket_dir, opts.socket_dirs, display_name, clobber, opts.mmap_group, opts.socket_permissions, username, uid, gid)
for socket, cleanup_socket in local_sockets:
#ie: ("unix-domain", sock, sockpath), cleanup_socket
sockets.append(socket)
Expand Down Expand Up @@ -727,6 +701,7 @@ def kill_dbus():
if opts.mdns:
from xpra.os_util import strtobytes
from xpra.platform.info import get_username
from xpra.server.socket_util import mdns_publish
mdns_info = {
"display" : display_name,
"username" : get_username(),
Expand All @@ -736,7 +711,6 @@ def kill_dbus():
}
if opts.session_name:
mdns_info["session"] = opts.session_name
#reduce
for mode, listen_on in mdns_recs:
mdns_publish(display_name, mode, listen_on, mdns_info)

Expand Down
Loading

0 comments on commit 02946ec

Please sign in to comment.