#/usr/local/bin/perl

# o  it will directly insert a new user into the hash password file
#    (/etc/passwd.{pag,dir}) if these files exist.
#
# o  it adds the quota by calling the quota system call from perl
#
# o  the next uid is taken by doing a 'tail -1 /etc/passwd', so your
#    passwd file should be sorted by uid (at least the last entry
#    should be the highest).
#
#We have used it to add over 2000 students, and has performed quite well.
#
#It has the following options:
#
# usage:  add_user [-options ...] username
#
#where options include:                                    Defaults -
#    -dir      dir             parent directory            least used disk
#    -full     "full name"     full name of new user       username
#    -group    group           group of new user           $def_group
#    -password password        password of new user        username (don't use!)
#    -quota    quota           disk quota in kbytes        $def_quota
#    -shell    login shell     login shell of new user     $def_shell
#
#All options can be abbreviated up to one letter.

$| = 1;
$exit_status=0;
$working_dir = "/etc";
@user_dirs = ("/u");
$def_group = "users";
$def_gid   = 500;
$def_shell = "/bin/csh";
$def_quota = 2000;

$max_uid = 32000;

chdir($working_dir) || die "can't change to $working_dir\n";

# parse command options

while ( $_ = $ARGV[0]) {
       shift;
       last if /^--$/;
       if    (/^-d/)           { $nu_parent = &get_option("-dir");     }
       elsif (/^-f/)           { $nu_full   = &get_option("-full");    }
       elsif (/^-g/)           { $nu_group  = &get_option("-full");    }
       elsif (/^-h/)           { &do_help; }
       elsif (/^-p/)           { $nu_passwd  = &get_option("-pass");    }
       elsif (/^-q/)           { $nu_quota  = &get_option("-quota");    }
       elsif (/^-s/)           { $nu_shell  = &get_option("-shell");    }
       elsif (/^[a-zA-Z]*/)    {
         &cleanup("can't specify more then one user!",7) if ($nu_user ne "");
         $nu_user = $_;
       }
       else                    { &usage("unknown argument: $_");          }

}

&usage("must specify one user") if ( $nu_user eq "" );

&usage("must specify a passwd") if ( $nu_passwd eq "");

if ("$nu_full" eq "")   { $nu_full = "$nu_user"; }
if ("$nu_parent" eq "") {
   $lowest_links=32768;
   foreach (@user_dirs) {
       $links = (stat($_))[3];
       if ($links < $lowest_links) { $lowest_links=$links; $nu_parent=$_; }
   }
}

&cleanup("$nu_parent directory not found!",5) if (! -d $nu_parent);

if ($nu_group eq "")   {
       $nu_group = "$def_group";
       $nu_gid   = $def_gid;
} else {
       ($t,$t,$nu_gid) = getgrnam($nu_group);
       &cleanup("unknown group: $nu_group",4) if ($nu_gid eq '');
}

if ($nu_shell eq "")   { $nu_shell = "$def_shell"; }
if ($nu_quota eq "")   { $nu_quota = "$def_quota"; }

&catch_signals;
&passwd_lock || &cleanup("couldn't lock passwd file!",1);

($name)=getpwnam($nu_user);

&cleanup("user $nu_user already in passwd file.",3) if ($name ne "");

print "adduser: Adding $nu_user, quota=$nu_quota group=$nu_group\n";

$nu_uid = &next_uid;

if ($nu_uid eq "" || $nu_uid<100 || $nu_uid> $max_uid) {
 &cleanup("next uid error. uid $nu_uid is invalid",6);
}

$nu_home_dir = "$nu_parent/$nu_user";

$nu_encrypted_passwd = &encrypt_passwd($nu_user,$nu_passwd);


(
open(PASSWD,">>passwd") &&
(print PASSWD "$nu_user:$nu_encrypted_passwd:$nu_uid:$nu_gid:$nu_full:$nu_home_dir:$nu_shell\n") &&
close(PASSWD)
) || &cleanup("error creating new passwd file!",10);


if ( -f passwd.dir && -f passwd.pag ) {
    dbmopen(%DBM_PASSWD,"passwd",0644);
    $buf = &pack_passwd_dbm($nu_user,$nu_encrypted_passwd,$nu_uid,$nu_gid,
            $nu_full,$nu_home_dir,$nu_shell);
    $DBM_PASSWD{$nu_user}=$buf;
    $DBM_PASSWD{pack("i",$nu_uid)}=$buf;
    dbmclose(%DBM_PASSWD);
}

&cancel_passwd_lock;

#set the quota here!
if (!&set_new_quota($nu_uid,$nu_quota)) {
  print "adduser: warning: error setting quota!\n";
}


(
 mkdir("$nu_home_dir",0711) &&
 chown($nu_uid,$nu_gid,"$nu_home_dir")
) || &cleanup("error creating home directory $nu_home_dir",9);

chdir("$nu_home_dir") || &cleanup("error changing to directory $nu_home_dir",9);

(
mkdir("bin",0711) &&
chown($nu_uid,$nu_gid,"bin")
) || &cleanup("error creating bin directory $nu_home_dir/bin",9);

system "cp /admin/skel/.profile /admin/skel/.cshrc /admin/skel/.login .";
chmod(0711,".profile",".cshrc",".login");
chown($nu_uid,$nu_gid,".profile",".cshrc",".login");
chmod(0755,".forward");

chdir($working_dir) || die "can't change to $working_dir\n";

exit 0;


sub usage {
 local($mess) = @_;

 print "adduser: $mess\n\n";

 print <<"_EOF_";
usage:  add_user [-options ...] username

where options include:                                    Defaults -
   -dir      dir             parent directory            least used disk
   -full     "full name"     full name of new user       username
   -group    group           group of new user           $def_group
   -password password        password of new user        username
   -quota    quota           disk quota in kbytes        $def_quota
   -shell    login shell     login shell of new user     $def_shell

All options can be abbreviated up to one letter.

Possible exit codes:
          0 - normal, success           1 - password file is busy
          2 - interrupted               3 - user already in passwd file
          4 - bad group specified       5 - bad parent directory
          6 - error getting new uid     7 - bad arguements (usage)
          8 - error from remote system  9 - error creating user files
         10 - error creating new passwd
_EOF_

exit 7;
}

sub get_option {
       &usage("missing argument for $_[0]") if ($#ARGV==-1) ;
       $result = $ARGV[0];
       shift @ARGV;
       return $result;
}

sub cancel_passwd_lock {
  if (!$passwd_file_locked) {
       return 0;
  }
  else {
       unlink 'ptmp';
       $passwd_file_locked=0;
       return 1;
  }
}

sub finish_passwd_lock {
  if (!$passwd_file_locked) {
       return 0;
  } else {
       close(PASSWD);
       close(PTMP);
       chmod 0644,'ptmp';
       rename('passwd','passwd.old');
       rename('ptmp','passwd') || die "can't install new passwd file: $!\n";
       $passwd_file_locked=0;
  }
}

sub passwd_lock {
 local($retry)=0;

 $the_ptmp = "ptmp.$$";

 open(PTMP,">$the_ptmp") || die"can't create tmp passwd file: $the_ptmp\n";
 close(PTMP);

 if (!link("$the_ptmp",'ptmp') ) {
    print "passwd file busy.";
    while (!link("$the_ptmp",'ptmp')) {
          if ($retry++ == 24) {
              printf "giving up!\n";
              unlink("$the_ptmp");
              $passwd_file_locked=0;
              return 0;
          }
          sleep(5);
          print ".";
    }
    printf "locked!\n";
 }

 $passwd_file_locked=1;
 unlink("$the_ptmp");
 open(PTMP,">ptmp") || die "can't copy passwd file\n";
 open(PASSWD,"passwd") || die "can't open passwd file\n";
 return 1;

}

sub encrypt_passwd {
 local($user,$pass)=@_;
 local($nslat,$week,$now,$pert1,$pert2);
 local(@salt_set)=('a'..'z','A'..'Z','0'..'9','.','/');
 $now=time;
 ($pert1,$per2) = unpack("C2",$user);
 $week = $now / (60*60*24*7) + $pert1 + $pert2;
 $nsalt = $salt_set[$week % 64] . $salt_set[$now %64];
 return crypt($pass,$nsalt);
}

sub next_uid {
       local(*FILE);
       open (FILE,'tail -1 /etc/passwd|') ||
               die "Can't get last used uid: $?";
       local($name,$pass,$uid)=split(':',<FILE>);
       close(FILE);
       return $uid+1;
}

sub catch_signals {
 $SIG{'INT'} = 'SIGNAL_CLEANUP';
 $SIG{'HUP'} = 'SIGNAL_CLEANUP';
 $SIG{'QUIT'} = 'SIGNAL_CLEANUP';
 $SIG{'PIPE'} = 'SIGNAL_CLEANUP';
 $SIG{'ALRM'} = 'SIGNAL_CLEANUP';
}

sub cleanup {
 local($message,$exit_status) = @_;
 &cancel_passwd_lock;
 unlink("$the_ptmp") if (defined ($the_ptmp));
 print "adduser: $message\n";
 exit $exit_status;
}

sub SIGNAL_CLEANUP {
 &cancel_passwd_lock;
 unlink("$the_ptmp") if (defined ($the_ptmp));
 print "\nadduser: interrupted!\n";
 exit 2;
}

sub unpack_passwd_dbm {
local($buf) = $_[0];
local($i,$l,$name,$passwd,$uid,$gid,$quota,$comment,$gecos,$dir,$shell);

$name   = substr($buf,$i,$l=index($buf,"\0",$i));       $i += $l+1;
$passwd = substr($buf,$i,$l=index($buf,"\0",$i)-$i);    $i += $l+1;
($uid,$gid,$quota)=unpack("i i i",substr($buf,$i,12));  $i += 12;
$comment= substr($buf,$i,$l=index($buf,"\0",$i)-$i);    $i += $l+1;
$gecos  = substr($buf,$i,$l=index($buf,"\0",$i)-$i);    $i += $l+1;
$dir    = substr($buf,$i,$l=index($buf,"\0",$i)-$i);    $i += $l+1;
$shell  = substr($buf,$i,$l=index($buf,"\0",$i)-$i);    $i += $l+1;
return ($name,$passwd,$uid,$gid,$gecos,$dir,$shell);
}


sub pack_passwd_dbm {

local($name,$passwd,$uid,$gid,$gecos,$dir,$shell) = @_;
local($i,$l,$quota,$comment,$buf);

$buf = $name . "\0" . $passwd . "\0" . pack("iii",$uid,$gid,0)  .
      "\0" . $gecos . "\0" . $dir . "\0" . $shell . "\0";

return $buf;
}
#
#sub set_new_quota { #user,dir,bs
#  local ($SYS_quota)=149;
#  local ($Q_SETDLIM)=1;
#  local ($uid,$dir,$bs) = @_;
#  local ($dev)   = stat($dir);
#  local ($dqblk) = pack("LLLSSSCC",($bs+1000)*2,($bs*2),0,0,0,0,3,3);
#  local ($stat,$buf);
#  $stat=syscall($SYS_quota,$Q_SETDLIM,$uid,$dev,$dqblk);
#  return $stat==0;
#}
#
#
# this is all i had to change, besides the default home dir and stuff

sub set_new_quota { #user,blocks
 local ($SYS_quota)=148;
 local ($Q_SETDLIM)=3;
 local ($uid,$bs) = @_ ;
 local ($dir) = "/dev/dsk/6s0\0";
 local ($dqblk)  = pack("LLLLLLLL",10000,$bs,0,1000,200,0,0,0);
local ($stat,$buf);
$stat=syscall($SYS_quota,$Q_SETDLIM,$dir,$uid+0,$dqblk);
return $stat==0;
}


#Roland J. Schemers III              |            Networking Systems
#Systems Programmer                  |            168 Pine Hall   (415)-723-6740
#Distributed Computing Group         |            Stanford, CA 94305-4122
#Stanford University                 |            [email protected]