#! /usr/bin/python -E
# Authors: Dan Walsh <dwalsh@redhat.com>
# Authors: Josh Cogliati
#
# Copyright (C) 2009,2010  Red Hat
# see file 'COPYING' for use and warranty information
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; version 2 only
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#

import os, sys, socket, random, fcntl, shutil, re, subprocess
import selinux
import signal
from tempfile import mkdtemp
import pwd
import commands 

PROGNAME = "policycoreutils"
HOMEDIR=pwd.getpwuid(os.getuid()).pw_dir

import gettext
gettext.bindtextdomain(PROGNAME, "/usr/share/locale")
gettext.textdomain(PROGNAME)

try:
       gettext.install(PROGNAME,
                       localedir = "/usr/share/locale",
                       unicode=False,
                       codeset = 'utf-8')
except IOError:
       import __builtin__
       __builtin__.__dict__['_'] = unicode

DEFAULT_TYPE = "sandbox_t"
DEFAULT_X_TYPE = "sandbox_x_t"
X_FILES = {}

random.seed(None)

def sighandler(signum, frame):
    signal.signal(signum,  signal.SIG_IGN)
    os.kill(0, signum)
    raise KeyboardInterrupt

def setup_sighandlers():
    signal.signal(signal.SIGHUP,  sighandler)
    signal.signal(signal.SIGQUIT, sighandler)
    signal.signal(signal.SIGTERM, sighandler)

def error_exit(msg):
    sys.stderr.write("%s: " % sys.argv[0])
    sys.stderr.write("%s\n" % msg)
    sys.stderr.flush()
    sys.exit(1)

def copyfile(file, dir, dest):
       import re
       if file.startswith(dir):
              dname = os.path.dirname(file)
              bname = os.path.basename(file)
              if dname == dir:
                     dest = dest + "/" + bname
              else:
                     newdir = re.sub(dir, dest, dname)
                     if not os.path.exists(newdir):
                            os.makedirs(newdir)
                     dest = newdir + "/" + bname

              if os.path.isdir(file):
                     shutil.copytree(file, dest)
              else:
                     shutil.copy2(file, dest)
              X_FILES[file] = (dest, os.path.getmtime(dest))

def savefile(new, orig, X_ind):
       copy = False
       if(X_ind):
              import gtk
              dlg = gtk.MessageDialog(None, 0, gtk.MESSAGE_INFO,
                                      gtk.BUTTONS_YES_NO,
                                      _("Do you want to save changes to '%s' (Y/N): ") % orig)
              dlg.set_title(_("Sandbox Message"))
              dlg.set_position(gtk.WIN_POS_MOUSE)
              dlg.show_all()
              rc = dlg.run()
              dlg.destroy()
              if rc == gtk.RESPONSE_YES:
                     copy = True
       else:
              ans = raw_input(_("Do you want to save changes to '%s' (y/N): ") % orig)
              if(re.match(_("[yY]"),ans)):
                     copy = True
       if(copy):
              shutil.copy2(new,orig)

def reserve(level):
    sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    sock.bind("\0%s" % level)
    fcntl.fcntl(sock.fileno(), fcntl.F_SETFD, fcntl.FD_CLOEXEC)

def gen_mcs():
       while True:
              i1 = random.randrange(0, 1024)
              i2 = random.randrange(0, 1024)
              if i1 == i2:
                     continue
              if i1 > i2:
                     tmp = i1
                     i1 = i2
                     i2 = tmp
                     level = "s0:c%d,c%d" % (i1, i2)
              level = "s0:c%d,c%d" % (i1, i2)
              try:
                     reserve(level)
              except socket.error:
                     continue
              break
       return level

def fullpath(cmd):
       for i in [ "/", "./", "../" ]:
              if cmd.startswith(i):
                     return cmd
       for i in  os.environ["PATH"].split(':'):
              f = "%s/%s" % (i, cmd)
              if os.access(f, os.X_OK):
                     return f
       return cmd

class Sandbox:
    VERSION = "sandbox .1"
    SYSLOG = "/var/log/messages"

    def __init__(self):
        self.__options = None
        self.__cmds = None
        self.__init_files = []
        self.__paths = []
        self.__mount = False
        self.__level = None
        self.__homedir = None
        self.__tmpdir = None

    def __validate_mount(self):
           if self.__options.level:
                  if not self.__options.homedir or not self.__options.tmpdir:
                         self.usage(_("Homedir and tempdir required for level mounts"))

           if not os.path.exists("/usr/sbin/seunshare"):
                  raise ValueError("""
/usr/sbin/seunshare required for sandbox -M, to install you need to execute 
#yum install /usr/sbin/seunshare
""")
           homedir=pwd.getpwuid(os.getuid()).pw_dir
           fd = open("/proc/self/mountinfo", "r")
           recs = fd.readlines()
           fd.close()
           for i in recs:
                  x = i.split() 
                  if x[3] == x[4] and homedir.startswith(x[3]+"/"):
                         return
           raise ValueError(_("""
'%s' is required to be a shared mount point for this tool to run.  
'%s' can be added to the HOMEDIRS variable in /etc/sysconfig/sandbox
 along with a reboot will fix the problem.
""" % ((os.path.dirname(homedir)), os.path.dirname(homedir))))
        
    def __mount_callback(self, option, opt, value, parser):
           self.__mount = True

    def __x_callback(self, option, opt, value, parser):
           self.__mount = True
           setattr(parser.values, option.dest, True)

    def __validdir(self, option, opt, value, parser):
           if not os.path.isdir(value):
                  raise IOError("Directory "+value+" not found")
           self.__mount = True

    def __include(self, option, opt, value, parser):
           rp = os.path.realpath(os.path.expanduser(value))
           if not os.path.exists(rp):
                  raise IOError(value+" not found")

           if rp not in self.__init_files:
                  self.__init_files.append(rp)

    def __includefile(self, option, opt, value, parser):
           fd = open(value, "r")
           for i in fd.readlines():
                  rp = os.path.realpath(os.path.expanduser(i[:-1]))
                  if rp not in self.__init_files and os.path.exists(rp):
                         self.__init_files.append(rp)
           fd.close()

    def __copyfiles(self):
           files = self.__init_files + self.__paths
           homedir=pwd.getpwuid(os.getuid()).pw_dir
           for f in files:
                  copyfile(f, homedir, self.__homedir)
                  copyfile(f, "/tmp", self.__tmpdir)

    def __setup_sandboxrc(self, wm = "/usr/bin/matchbox-window-manager -use_titlebar no"):
           execfile =self.__homedir + "/.sandboxrc"
           fd = open(execfile, "w+") 
           if self.__options.session:
                  fd.write("""#!/bin/sh
#TITLE: /etc/gdm/Xsession
/etc/gdm/Xsession
""")
           else:
                  command = " ".join(self.__paths)
                  fd.write("""#! /bin/sh
#TITLE: %s
/usr/bin/test -r ~/.xmodmap && /usr/bin/xmodmap ~/.xmodmap
%s &
WM_PID=$!
%s
kill -TERM $WM_PID  2> /dev/null
""" % (command, wm, command))
           fd.close()
           os.chmod(execfile, 0700)

    def usage(self, message = ""):
           error_exit("%s\n%s" % (self.__parser.usage, message))

    def __parse_options(self):
        from optparse import OptionParser
        usage = _("""
sandbox [-h] [-[X|M] [-l level ] [-H homedir] [-T tempdir]] [-I includefile ] [-W windowmanager ] [[-i file ] ...] [ -t type ] command

sandbox [-h] [-[X|M] [-l level ] [-H homedir] [-T tempdir]] [-I includefile ] [-W windowmanager ] [[-i file ] ...] [ -t type ] -S
""")
        
        parser = OptionParser(version=self.VERSION, usage=usage)
        parser.disable_interspersed_args()
        parser.add_option("-i", "--include", 
                          action="callback", callback=self.__include, 
                          type="string",
                          help="include file in sandbox")
        parser.add_option("-I", "--includefile",  action="callback", callback=self.__includefile,
                          type="string",
                          help="include contents of file in sandbox")
        parser.add_option("-t", "--type", dest="setype", action="store", default=DEFAULT_TYPE,
                          help="Run sandbox with SELinux type")
        parser.add_option("-M", "--mount", 
                          action="callback", callback=self.__mount_callback, 
                          help="Mount new home and tmp Dir")

        parser.add_option("-S", "--session", action="store_true",  dest="session", 
                          default=False,  help="Run complete desktop session within sandbox")
        parser.add_option("-X", dest="X_ind", 
                          action="callback", callback=self.__x_callback, 
                          default=False,  help="Run X sandbox")

        parser.add_option("-H", "--homedir", 
                          action="callback", callback=self.__validdir,
                          type="string",
                          dest="homedir",  
                          help="Alternate homedir to use for mounting")

        parser.add_option("-T", "--tmpdir", dest="tmpdir",  
                          type="string",
                          action="callback", callback=self.__validdir,
                          help="Alternate tempdir to use for mounting")

        parser.add_option("-W", "--windowmanager", dest="wm",  
                          type="string",
                          default="/usr/bin/matchbox-window-manager -use_titlebar no",
                          help="Alternate window maanger")

        parser.add_option("-l", "--level", dest="level", 
                          help="MCS/MLS Level for the sandbox")

        self.__parser=parser

        self.__options, cmds = parser.parse_args()

        if self.__options.X_ind:
               if DEFAULT_TYPE == self.__options.setype:
                     self.__options.setype = DEFAULT_X_TYPE

        if self.__mount:
               self.__validate_mount()

        if self.__options.session:
               if self.__options.setype in (DEFAULT_TYPE, DEFAULT_X_TYPE):
                      self.__options.setype = selinux.getcon()[1].split(":")[2]
               if not self.__options.homedir or not self.__options.tmpdir:
                      self.usage(_("Homedir and tempdir required for session"))
               if len(cmds) > 0:
                      self.usage(_("Commands not allowed in a session"))
        else:
               if len(cmds) == 0:
                      self.usage(_("Command required"))
               cmds[0] = fullpath(cmds[0])
               if not os.access(cmds[0], os.X_OK):
                      self.usage(_("%s is not an executable") % cmds[0]  )
                      
               self.__cmds = cmds

        for f in cmds:
               rp = os.path.realpath(f)
               if os.path.exists(rp):
                      self.__paths.append(rp)
               else:
                      self.__paths.append(f)
                  
    def __gen_context(self):
           if self.__options.level:
                  level = self.__options.level
           else:
                  level = gen_mcs()

           con = selinux.getcon()[1].split(":")
           self.__execcon = "%s:%s:%s:%s" % (con[0], con[1], self.__options.setype, level)
           self.__filecon = "%s:%s:%s:%s" % (con[0], "object_r", 
                                             "%s_file_t" % self.__options.setype[:-2], 
                                             level)
    def __setup_dir(self):
           if self.__options.level or self.__options.session:
                  return
           sandboxdir = HOMEDIR + "/.sandbox"
           if not os.path.exists(sandboxdir):
                  os.mkdir(sandboxdir)

           import warnings 
           warnings.simplefilter("ignore")
           if self.__options.homedir:
                  chcon =  ("/usr/bin/chcon -R %s %s" % (self.__filecon, self.__options.homedir)).split()
                  rc = os.spawnvp(os.P_WAIT, chcon[0], chcon)
                  self.__homedir = self.__options.homedir
           else:
                  selinux.setfscreatecon(self.__filecon)
                  self.__homedir = mkdtemp(dir=sandboxdir, prefix=".sandbox")

           if self.__options.tmpdir:
                  chcon =  ("/usr/bin/chcon -R %s %s" % (self.__filecon, self.__options.tmpdir)).split()
                  rc = os.spawnvp(os.P_WAIT, chcon[0], chcon)
                  self.__tmpdir = self.__options.homedir
           else:
                  selinux.setfscreatecon(self.__filecon)
                  self.__tmpdir = mkdtemp(dir="/tmp", prefix=".sandbox")
           warnings.resetwarnings()
           selinux.setfscreatecon(None)
           self.__copyfiles()

    def __execute(self):
           try:
                  if self.__options.X_ind:
                         xmodmapfile = self.__homedir + "/.xmodmap"
                         xd = open(xmodmapfile,"w")
                         subprocess.Popen(["/usr/bin/xmodmap","-pke"],stdout=xd).wait()
                         xd.close()

                         self.__setup_sandboxrc(self.__options.wm)
                         
                         cmds =  ("/usr/sbin/seunshare -t %s -h %s -- %s /usr/share/sandbox/sandboxX.sh" % (self.__tmpdir, self.__homedir, self.__execcon)).split()
                         rc = subprocess.Popen(cmds).wait()
                         return rc

                  if self.__mount:
                         cmds =  ("/usr/sbin/seunshare -t %s -h %s -- %s " % (self.__tmpdir, self.__homedir, self.__execcon)).split()+self.__paths
                         rc = subprocess.Popen(cmds).wait()
                         return rc

                  selinux.setexeccon(self.__execcon)
                  rc = subprocess.Popen(self.__cmds).wait()
                  selinux.setexeccon(None)
                  return rc

           finally:
                  for i in self.__paths:
                         if i not in X_FILES:
                                continue
                         (dest, mtime) = X_FILES[i]
                         if os.path.getmtime(dest) > mtime:
                                savefile(dest, i, X_ind)

                  if self.__homedir and not self.__options.homedir: 
                         shutil.rmtree(self.__homedir)
                  if self.__tmpdir and not self.__options.tmpdir:
                         shutil.rmtree(self.__tmpdir)
    def main(self):
        try:
               self.__parse_options()
               self.__gen_context()
               self.__setup_dir()
               return self.__execute()
        except KeyboardInterrupt:
            sys.exit(0)


if __name__ == '__main__':
    setup_sighandlers()
    if selinux.is_selinux_enabled() != 1:
        error_exit("Requires an SELinux enabled system")
    
    try:
           sandbox = Sandbox()
           rc = sandbox.main()
    except OSError, error:
           error_exit(error.args[1])
    except ValueError, error:
           error_exit(error.args[0])
    except KeyError, error:
           error_exit(_("Invalid value %s") % error.args[0])
    except IOError, error:
           error_exit(error)
    except KeyboardInterrupt:
           rc = 0
           
    sys.exit(rc)
