esky/sudo/sudo_unix.py
# Copyright (c) 2009-2010, Cloud Matrix Pty. Ltd.
# All rights reserved; available under the terms of the BSD License.
"""
esky.sudo.sudo_unix: unix platform-specific functionality for esky.sudo
"""
import os
import sys
import errno
import subprocess
import tempfile
from esky.sudo import sudo_base as base
import esky.slaveproc
def has_root():
"""Check whether the use current has root access."""
return (os.geteuid() == 0)
def can_get_root():
"""Check whether the usee may be able to get root access.
This is currently always True on unix-like platforms, since we have no
sensible way of peering inside the sudoers file.
"""
return True
class KillablePopen(subprocess.Popen):
"""Popen that's guaranteed killable, even on python2.5."""
if not hasattr(subprocess.Popen,"terminate"):
def terminate(self):
import signal
os.kill(self.pid,signal.SIGTERM)
class SecureStringPipe(base.SecureStringPipe):
"""A two-way pipe for securely communicating with a sudo subprocess.
On unix this is implemented as a pair of fifos. It would be more secure
to use anonymous pipes, but they're not reliably inherited through sudo
wrappers such as gksudo.
Unfortunately this leaves the pipes wide open to hijacking by other
processes running as the same user. Security depends on secrecy of the
message-hashing token, which we pass to the slave in its env vars.
"""
def __init__(self,token=None,data=None):
super(SecureStringPipe,self).__init__(token)
self.rfd = None
self.wfd = None
if data is None:
self.tdir = tempfile.mkdtemp()
self.rnm = os.path.join(self.tdir,"master")
self.wnm = os.path.join(self.tdir,"slave")
os.mkfifo(self.rnm,0600)
os.mkfifo(self.wnm,0600)
else:
self.tdir,self.rnm,self.wnm = data
def __del__(self):
try:
self.close()
except Exception:
pass
def connect(self):
return SecureStringPipe(self.token,(self.tdir,self.wnm,self.rnm))
def _read(self,size):
return os.read(self.rfd,size)
def _write(self,data):
return os.write(self.wfd,data)
def _open(self):
if self.rnm.endswith("master"):
self.rfd = os.open(self.rnm,os.O_RDONLY)
self.wfd = os.open(self.wnm,os.O_WRONLY)
else:
self.wfd = os.open(self.wnm,os.O_WRONLY)
self.rfd = os.open(self.rnm,os.O_RDONLY)
os.unlink(self.wnm)
def _recover(self):
try:
os.close(os.open(self.rnm,os.O_WRONLY))
except EnvironmentError:
pass
try:
os.close(os.open(self.wnm,os.O_RDONLY))
except EnvironmentError:
pass
def close(self):
if self.rfd is not None:
os.close(self.rfd)
os.close(self.wfd)
self.rfd = None
self.wfd = None
if os.path.isfile(self.wnm):
os.unlink(self.wnm)
try:
if not os.listdir(self.tdir):
os.rmdir(self.tdir)
except EnvironmentError, e:
if e.errno != errno.ENOENT:
raise
super(SecureStringPipe,self).close()
def find_exe(name,*args):
path = os.environ.get("PATH","/bin:/usr/bin").split(":")
if getattr(sys,"frozen",False):
path.append(os.path.dirname(sys.executable))
for dir in path:
exe = os.path.join(dir,name)
if os.path.exists(exe):
return [exe] + list(args)
return None
def spawn_sudo(proxy):
"""Spawn the sudo slave process, returning proc and a pipe to message it."""
rnul = open(os.devnull,"r")
wnul = open(os.devnull,"w")
pipe = SecureStringPipe()
c_pipe = pipe.connect()
if not getattr(sys,"frozen",False):
exe = [sys.executable,"-c","import esky; esky.run_startup_hooks()"]
elif os.path.basename(sys.executable).lower() in ("python","pythonw"):
exe = [sys.executable,"-c","import esky; esky.run_startup_hooks()"]
else:
if not esky._startup_hooks_were_run:
raise OSError(None,"unable to sudo: startup hooks not run")
exe = [sys.executable]
args = ["--esky-spawn-sudo"]
args.append(base.b64pickle(proxy))
# Look for a variety of sudo-like programs
sudo = None
display_name = "%s update" % (proxy.name,)
if "DISPLAY" in os.environ:
sudo = find_exe("gksudo","-k","-D",display_name,"--")
if sudo is None:
sudo = find_exe("kdesudo")
if sudo is None:
sudo = find_exe("cocoasudo","--prompt='%s'" % (display_name,))
if sudo is None:
sudo = find_exe("sudo")
if sudo is None:
sudo = []
# Make it a slave process so it dies if we die
exe = sudo + exe + esky.slaveproc.get_slave_process_args() + args
# Pass the pipe in environment vars, they seem to be harder to snoop.
env = os.environ.copy()
env["ESKY_SUDO_PIPE"] = base.b64pickle(c_pipe)
# Spawn the subprocess
kwds = dict(stdin=rnul,stdout=wnul,stderr=wnul,close_fds=True,env=env)
proc = KillablePopen(exe,**kwds)
return (proc,pipe)
def run_startup_hooks():
if len(sys.argv) > 1 and sys.argv[1] == "--esky-spawn-sudo":
proxy = base.b64unpickle(sys.argv[2])
pipe = base.b64unpickle(os.environ["ESKY_SUDO_PIPE"])
proxy.run(pipe)
sys.exit(0)