// $Id: chcontext.c,v 1.26 2006/01/22 19:31:53 ensc Exp $

// Copyright (C) 2003,2004 Enrico Scholz <enrico.scholz@informatik.tu-chemnitz.de>
// based on chcontext.cc by Jacques Gelinas
//  
// 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; either version 2, or (at your option)
// any later version.
//  
// 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.

/*
	chcontext is a wrapper to user the new_s_context system call. It
	does little more than mapping command line option to the system call
	arguments.
*/
#ifdef HAVE_CONFIG_H
#  include <config.h>
#endif

#include "util.h"
#include "vserver.h"
#include "internal.h"
#include "lib_internal/jail.h"

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <getopt.h>
#include <assert.h>
#include <fcntl.h>
#include <libgen.h>
#include <signal.h>

#define ENSC_WRAPPERS_PREFIX	"chcontext: "
#define ENSC_WRAPPERS_VSERVER	1
#define ENSC_WRAPPERS_UNISTD	1
#define ENSC_WRAPPERS_FCNTL	1
#include <wrappers.h>

#define CMD_HELP	0x1000
#define CMD_VERSION	0x1001
#define CMD_CAP		0x4000
#define CMD_CTX		0x4001
#define CMD_DISCONNECT	0x4002
#define CMD_DOMAINNAME	0x4003
#define CMD_FLAG	0x4004
#define CMD_HOSTNAME	0x4005
#define CMD_SECURE	0x4006
#define CMD_SILENT	0x4007

int wrapper_exit_code	= 255;

struct option const
CMDLINE_OPTIONS[] = {
  { "help",     no_argument,  0, CMD_HELP },
  { "version",  no_argument,  0, CMD_VERSION },
  { "cap",        required_argument,  0, CMD_CAP },
  { "ctx",        required_argument,  0, CMD_CTX },
  { "xid",        required_argument,  0, CMD_CTX },
  { "disconnect", no_argument,        0, CMD_DISCONNECT },
  { "domainname", required_argument,  0, CMD_DOMAINNAME },
  { "flag",       required_argument,  0, CMD_FLAG },
  { "hostname",   required_argument,  0, CMD_HOSTNAME },
  { "secure",     no_argument,        0, CMD_SECURE },
  { "silent",     no_argument,        0, CMD_SILENT },
  { 0,0,0,0 }
};

struct Arguments {
    size_t		nbctx;
    xid_t		ctxs[16];
    bool		do_disconnect;
    bool		do_silent;
    unsigned int	flags;
    uint32_t		remove_caps;
    uint32_t		add_caps;
    char const *	hostname;
    char const *	domainname;
};

static struct Arguments const *		global_args = 0;

static void
showHelp(int fd, char const *cmd, int res)
{
  VSERVER_DECLARE_CMD(cmd);

#if !defined(VC_ENABLE_API_COMPAT) && !defined(VC_ENABLE_API_LEGACY)
  WRITE_MSG(1, "ERROR: tools were built without legacy API support; chcontext will not work!\n\n");
#endif
  
  WRITE_MSG(fd, "Usage: ");
  WRITE_STR(fd, cmd);
  WRITE_MSG(fd,
	    " [--cap [!]<cap_name>] [--secure] [--xid <num>] [--disconnect]\n"
	    "       [--domainname <name>] [--hostname <name>] [--flag <flags>+]\n"
	    "       [--silent] [--] command arguments ...\n"
	    "\n"
	    "chcontext allocate a new security context and executes\n"
	    "a command in that context.\n"
	    "By default, a new/unused context is allocated\n"
	    "\n"
	    "--cap CAP_NAME\n"
	    "    Add a capability from the command. This option may be\n"
	    "    repeated several time.\n"
	    "    See /usr/include/linux/capability.h\n"
	    "    In general, this option is used with the --secure option\n"
	    "    --secure removes most critical capabilities and --cap\n"
	    "    adds specific ones.\n"
	    "\n"

	    "--cap !CAP_NAME\n"
	    "    Remove a capability from the command. This option may be\n"
	    "    repeated several time.\n"
	    "    See /usr/include/linux/capability.h\n"
	    "\n"
	    "--xid num\n"
	    "    Select the context. On root in context 0 is allowed to\n"
	    "    select a specific context.\n"
	    "    Context number 1 is special. It can see all processes\n"
	    "    in any contexts, but can't kill them though.\n"
	    "    Option --xid may be repeated several times to specify up to 16 contexts.\n"

	    "--disconnect\n"
	    "    Start the command in background and make the process\n"
	    "    a child of process 1.\n"

	    "--domainname new_domainname\n"
	    "    Set the domainname (NIS) in the new security context.\n"
	    "    Use \"none\" to unset the domain name.\n"

	    "--flag\n"
	    "    Set one flag in the new or current security context. The following\n"
	    "    flags are supported. The option may be used several time.\n"
	    "\n"
	    "        fakeinit: The new process will believe it is process number 1.\n"
	    "                  Useful to run a real /sbin/init in a vserver.\n"
	    "        lock:     The new process is trapped and can't use chcontext anymore.\n"
	    "        sched:    The new process and its children will share a common \n"
	    "                  execution priority.\n"
	    "        nproc:    Limit the number of process in the vserver according to\n"
	    "                  ulimit setting. Normally, ulimit is a per user thing.\n"
	    "                  With this flag, it becomes a per vserver thing.\n"
	    "        private:  No one can join this security context once created.\n"
	    "        ulimit:   Apply the current ulimit to the whole context\n"

	    "--hostname new_hostname\n"
	    "    Set the hostname in the new security context\n"
	    "    This is need because if you create a less privileged\n"
	    "    security context, it may be unable to change its hostname\n"

	    "--secure\n"
	    "    Remove all the capabilities to make a virtual server trustable\n"

	    "--silent\n"
	    "    Do not print the allocated context number.\n"
	    "\n"
	    "Please report bugs to " PACKAGE_BUGREPORT "\n");

  exit(res);
}

static void
showVersion()
{
  WRITE_MSG(1,
	    "chcontext-compat " VERSION " -- allocates/enters a security context\n"
	    "This program is part of " PACKAGE_STRING "\n\n"
	    "Copyright (C) 2003,2004 Enrico Scholz\n"
	    VERSION_COPYRIGHT_DISCLAIMER);
  exit(0);
}

#if defined(VC_ENABLE_API_COMPAT) || defined(VC_ENABLE_API_LEGACY)

static inline void
setCap(char const *str, uint32_t *add_caps, uint32_t *remove_caps)
{
  uint32_t	*cap;
  int		bit;
  
  if (str[0] != '!')
    cap = add_caps;
  else {
    cap = remove_caps;
    str++;
  }
	
  bit = vc_text2cap(str);
	
  if (bit!=-1) *cap |= (1<<bit);
  else {
    WRITE_MSG(2, "Unknown capability '");
    WRITE_STR(2, str);
    WRITE_MSG(2, "'\n");
    exit(wrapper_exit_code);
  }
}

static inline void
setFlags(char const *str, uint32_t *flags)
{
  struct vc_err_listparser	err;
  
  *flags = vc_list2cflag_compat(str, 0, &err);

  if (err.ptr!=0) {
    WRITE_MSG(2, "Unknown flag '");
    Vwrite(2, err.ptr, err.len);
    WRITE_MSG(2, "'\n");
    exit(wrapper_exit_code);
  }
}

static inline ALWAYSINLINE void
setHostname(char const *name)
{
  if (name == NULL) return;
  
  if (sethostname(name, strlen(name))==-1) {
    perror("chcontext: sethostname()");
    exit(255);
  }
  if (!global_args->do_silent) {
    WRITE_MSG(1, "Host name is now ");
    WRITE_STR(1, name);
    WRITE_MSG(1, "\n");
  }
}

static inline ALWAYSINLINE void
setDomainname(char const *name)
{
  if (name == NULL) return;
  
  if (setdomainname(name, strlen(name))==-1) {
    perror("chcontext: setdomainname()");
    exit(255);
  }
  if (!global_args->do_silent) {
    WRITE_MSG(1, "Domain name is now ");
    WRITE_STR(1, name);
    WRITE_MSG(1, "\n");
  }
}

static inline ALWAYSINLINE void
tellContext(xid_t ctx)
{
  char		buf[sizeof(xid_t)*3+2];
  size_t	l;

  if (global_args->do_silent) return;

  l = utilvserver_fmt_long(buf,ctx);

  WRITE_MSG(1, "New security context is ");
  Vwrite(1, buf, l);
  WRITE_MSG(1, "\n");
}

#include "context-sync.hc"

#endif


int main (int argc, char *argv[])
{
  struct Arguments args = {
    .nbctx         = 0,
    .do_disconnect = false,
    .do_silent     = false,
    .flags         = 0,
    .remove_caps   = 0,
    .add_caps      = 0,
    .hostname      = 0,
    .domainname    = 0
  };

#if defined(VC_ENABLE_API_COMPAT) || defined(VC_ENABLE_API_LEGACY)
  xid_t		newctx;
  int		xflags;
  int		p[2][2];
  pid_t		pid;
#endif
  
  global_args = &args;
  signal(SIGCHLD, SIG_DFL);

  while (1) {
    int		c = getopt_long(argc, argv, "+", CMDLINE_OPTIONS, 0);
    if (c==-1) break;

    switch (c) {
      case CMD_HELP		:  showHelp(1, argv[0], 0);
      case CMD_VERSION		:  showVersion();
      case CMD_DISCONNECT	:  args.do_disconnect = true;   break;
      case CMD_SILENT		:  args.do_silent     = true;   break;
      case CMD_DOMAINNAME	:  args.domainname    = optarg; break;
      case CMD_HOSTNAME		:  args.hostname      = optarg; break;
	
#if defined(VC_ENABLE_API_COMPAT) || defined(VC_ENABLE_API_LEGACY)
      case CMD_CAP		:
	setCap(optarg, &args.add_caps, &args.remove_caps);
	break;
      case CMD_SECURE		:
	args.remove_caps |= vc_get_insecurebcaps();
	break;
      case CMD_FLAG		:
	setFlags(optarg, &args.flags);
	break;
      case CMD_CTX		:
	if (args.nbctx>0)
	  WRITE_MSG(2, "WARNING: More than one ctx not supported by this version\n");
	if (args.nbctx>=DIM_OF(args.ctxs)) {
	  WRITE_MSG(2, "Too many contexts given\n");
	  exit(255);
	}
	args.ctxs[args.nbctx++] = Evc_xidopt2xid(optarg, true);
	break;
#else
      case CMD_CAP		:
      case CMD_SECURE		:
      case CMD_FLAG		:
      case CMD_CTX		:  break;
#endif	
	  
      default		:
	WRITE_MSG(2, "Try '");
	WRITE_STR(2, argv[0]);
	WRITE_MSG(2, " --help\" for more information.\n");
	return 255;
	break;
    }
  }

#if defined(VC_ENABLE_API_COMPAT) || defined(VC_ENABLE_API_LEGACY)
  if (optind>=argc) {
    WRITE_MSG(2, "No command given; use '--help' for more information.\n");
    exit(255);
  }
  
  if (args.domainname && strcmp(args.domainname, "none")==0)
    args.domainname = "";
    
  if (args.nbctx == 0)
    args.ctxs[args.nbctx++] = VC_DYNAMIC_XID;
    
  xflags      = args.flags & S_CTX_INFO_INIT;
  args.flags &= ~S_CTX_INFO_INIT;

  pid = initSync(p, args.do_disconnect);
  if (pid==0) {
    doSyncStage0(p, args.do_disconnect);
    
    newctx            = Evc_new_s_context(args.ctxs[0],0,args.flags);
    args.remove_caps &= (~args.add_caps);
    setHostname(args.hostname);
    setDomainname(args.domainname);

    if (args.remove_caps!=0 || xflags!=0)
      Evc_new_s_context (VC_SAMECTX,args.remove_caps,xflags);
    tellContext(args.ctxs[0]==VC_DYNAMIC_XID ? newctx : args.ctxs[0]);

    doSyncStage1(p, args.do_disconnect);
    execvp (argv[optind],argv+optind);
    doSyncStage2(p, args.do_disconnect);

    PERROR_Q("chcontext: execvp", argv[optind]);
    exit(255);
  }

  waitOnSync(pid, p, args.ctxs[0]!=VC_DYNAMIC_XID);
  return EXIT_SUCCESS;
#else
  WRITE_MSG(2, "chcontext: tools were built without legacy API support; can not continue\n");
  return EXIT_FAILURE;
#endif
}

#if defined(VC_ENABLE_API_COMPAT) || defined(VC_ENABLE_API_LEGACY)

#ifdef ENSC_TESTSUITE
#define FLAG_TEST(STR,EXP) \
  {			   \
    uint32_t	flag=0;	   \
    setFlags(STR, &flag);  \
    assert(flag==(EXP));   \
  }

#define CAP_TEST(STR,EXP_ADD,EXP_DEL)		\
  {						\
    uint32_t	add=0,del=0;			\
    setCap(STR, &add, &del);			\
    assert(add==(EXP_ADD));			\
    assert(del==(EXP_DEL));			\
  }

void
test()
{
  FLAG_TEST("lock",       1);
  FLAG_TEST("lock,sched", 3);

  CAP_TEST("CHOWN",      1, 0);
  CAP_TEST("CAP_CHOWN",  1, 0);
  CAP_TEST("!CHOWN",     0, 1);
  CAP_TEST("!CAP_CHOWN", 0, 1);
}
#endif

#else
void test() {}
#endif
