#!/usr/bin/perl ### suPHPfix # # AUTHOR: Scott Sullivan (ssullivan@liquidweb.com) # # GENERAL: # See the POD (perldoc /path/to/suphpfix.pl) or --help ### use JSON; use LWP::UserAgent; use Encode; use Term::ANSIColor; use Fcntl ':mode'; use File::Find::Object; use File::Find::Object::Rule; use POSIX qw(strftime); use Linux::Ext2::FileAttributes; use strict; my $version = '2.2.1'; my @contact_email = ('ssullivan@liquidweb.com,','mshooltz@liquidweb.com,','twichert@liquidweb.com'); my $apache_user = 'nobody'; # This is used to get the GID for group owner for accnts public_html (--prep only) sub help { print color 'red'; print "*** Options: \n\n"; print color 'green'; print "+++ Fix common problems when converting to suPHP\n"; print color 'reset'; print color 'yellow'; print "--prep all "; print color 'blue'; print "==> "; print color 'reset'; print " This will chown all cPanel users files/directories in their public_html's to cPanelUser.cPanelUser. It will also remove group and world write from files/directories. In addition, any php directive in .htaccess such as php_flag will be commented out (unless htscanner is present). \n"; print color 'yellow'; print "--prep cPanelUser "; print color 'blue'; print "==> "; print color 'reset'; print "Same as '--prep all' but only applies fixes to the specified cPanel account.\n"; print color 'green'; print "\n+++ Saving states\n"; print color 'reset'; print color 'yellow'; print "--save-state all "; print color 'blue'; print "==> "; print color 'reset'; print "This saves the current permission/ownership settings (for later restores) for all cPanel accounts files/directories under their public_html's.\n"; print color 'yellow'; print "--save-state cPanelUser"; print color 'blue'; print " ==> "; print color 'reset'; print "Same as '--save-state all' but only for the specified cPanel account.\n"; print color 'green'; print "\n+++ Restoring states\n"; print color 'reset'; print color 'yellow'; print "--restore-state all "; print color 'blue'; print "==> "; print color 'reset'; print "This restores all saved cPanel accounts permission/ownership settings at the time --save-state all was last ran. It also uncomments any php directives in accounts .htaccess file(s).\n"; print color 'yellow'; print "--restore-state all dryrun"; print color 'blue'; print " ==> "; print color 'reset'; print "Same as '--restore-state all' but just outputs proposed chmods/chowns for all accounts without actually changing anything.\n"; print color 'yellow'; print "--restore-state cPanelUser"; print color 'blue'; print " ==> "; print color 'reset'; print "This restores only the specified cPanel accounts permissions/ownership settings at the time --save-state was last ran for the user. It also uncomments any php directives in the accounts .htaccess file(s).\n"; print color 'yellow'; print "--restore-state cPanelUser dryrun"; print color 'blue'; print " ==> "; print color 'reset'; print "Same as '--restore-state cPanelUser' but just outputs proposed chmods/chowns for the user without actually changing anything.\n"; print color 'red'; print "\n*** General: \n\n"; print color 'reset'; print color 'yellow'; print "* "; print color 'reset'; print "suPHPfix uses /root/datastore_suphpfix/ to store JSON data. These JSON files allow suPHPfix to store what permissions/ownerships each backed up cPanel user had at the time of the last --save-state. There is one JSON file for each cPanel account. In this directory suPHPfix uses backedupUserList.all to store what cPanel users were backed up with --save-state all. \n\n"; } sub preChecks { if ( $< != 0 ) { print color 'red'; print "ERROR: must be root to run this.\n"; print color 'reset'; exit(1); } system("which chkconfig &> /dev/null"); if ( $? != 0 ) { print color 'red'; print "ERROR: chkconfig not found in path! \n"; exception_msg(); print color 'reset'; exit(1); } system("unalias du &> /dev/null"); system("which du &> /dev/null"); if ( $? != 0 ) { print color 'red'; print "ERROR: du not found in path! \n"; exception_msg(); print color 'reset'; exit(1); } system("unalias cut &> /dev/null"); system("which cut &> /dev/null"); if ( $? != 0 ) { print color 'red'; print "ERROR: cut not found in path! \n"; exception_msg(); print color 'reset'; exit(1); } system("unalias find &> /dev/null"); system("which find &> /dev/null"); if ( $? != 0 ) { print color 'red'; print "ERROR: find not found in path! \n"; exception_msg(); print color 'reset'; exit(1); } system("unalias chmod &> /dev/null"); system("which chmod &> /dev/null"); if ( $? != 0 ) { print color 'red'; print "ERROR: chmod not found in path! \n"; exception_msg(); print color 'reset'; exit(1); } system("unalias chown &> /dev/null"); system("which chown &> /dev/null"); if ( $? != 0 ) { print color 'red'; print "ERROR: chown not found in path! \n"; exception_msg(); print color 'reset'; exit(1); } } sub exception_msg { print color 'red'; print "An unexpected error has occurred please email: @contact_email \n"; print color 'reset'; } sub chkSysUser { my $user = $_[0]; my @passwdArray; open (FILE, '/etc/passwd') or die "ERROR: Cannot open /etc/passwd $! \n"; while () { push(@passwdArray, $_); } close(FILE); my @passwdCheck = grep(/^$user:/,@passwdArray); if ( scalar(@passwdCheck) != 1 ) { print color 'red'; print "ERROR: Unable to find $user in /etc/passwd (or there are multiple lines that start with $user). Since this means we cannot get numeric UID/GID, failing.\n"; print color 'reset'; exception_msg(); exit(1); } } sub chkSysGrp { my $group = $_[0]; my @groupArray; open (FILE, '/etc/group') or die "ERROR: Cannot open /etc/group $! \n"; while () { push(@groupArray, $_); } close(FILE); my @groupCheck = grep(/^$group:/,@groupArray); if ( scalar(@groupCheck) != 1 ) { print color 'red'; print "ERROR: Unable to find $group in /etc/group (or there are multiple lines that start with $group). Since this means we cannot get numeric UID/GID, failing.\n"; print color 'reset'; exception_msg(); exit(1); } } sub checkLogSize { print "Checking size of logs...\n"; ## 2097152kb = 2gb my $twoGB = '2097152'; my $suphpLogFile = '/usr/local/apache/logs/suphp_log'; my $apacheErrorLog = '/usr/local/apache/logs/error_log'; my $suexecLog = '/usr/local/apache/logs/suexec_log'; my $accessLog = '/usr/local/apache/logs/access_log'; my $suphpLogSize; my $apacheErrorLogSize = `du $apacheErrorLog | cut -f 1`; my $suexecLogSize = `du $suexecLog | cut -f 1`; my $accessLogSize = `du $accessLog | cut -f 1`; if ( ! -e $apacheErrorLog ) { print color 'red'; print "ERROR: $apacheErrorLog does not exist!\n"; exception_msg(); print color 'reset'; exit(1); } if ( ! -e $accessLog ) { print color 'red'; print "ERROR: $accessLog does not exist!\n"; exception_msg(); print color 'reset'; exit(1); } if ( -e $suphpLogFile ) { $suphpLogSize = `du $suphpLogFile | cut -f 1`; } if ( $suphpLogSize >= $twoGB ) { print color 'yellow'; print "WARNING: suPHP error log ($suphpLogFile) is greater than or equal to 2GB. You should clear this log file to prevent problems serving webpages.\n"; print color 'reset'; } if ( $apacheErrorLogSize >= $twoGB ) { print color 'yellow'; print "WARNING: suexec log ($suexecLog) is greater than or equal to 2GB. You should clear this log file to prevent problems serving webpages.\n"; print color 'reset'; } if ( $accessLogSize >= $twoGB ) { print color 'yellow'; print "WARNING: Access log ($accessLog) is greater than or equal to 2GB. You should clear this log file to prevent problems serving webpages.\n"; print color 'reset'; } print color 'green'; print "All tasks complete.\n"; print color 'reset'; } sub cleanUp { print "\nCleaning up... \n"; my @path; push (@path, '/tmp'); my $ffo = File::Find::Object->new({ followlink => '0', }, @path); while ( my $file = $ffo->next() ) { chmod oct('1777'), $file; } chmod oct('0700'), '/tmp/screens/S-root'; chmod oct('0755'), '/tmp/screens'; } sub getAuth { my $silent = $_[0]; if ( $silent ne 'yes' ) { print "Generating hash..."; } system("QUERY_STRING=\\\"regen=1\\\" /usr/local/cpanel/whostmgr/bin/whostmgr ./setrhash &> /dev/null"); my $hashfile = '/root/.accesshash'; if (! -e $hashfile) { print color 'red'; print "ERROR: Failed to automatically generate hash! Please try logging into WHM and click `Setup Remote Access Key` and then re-run this script. \n"; exception_msg(); print color 'reset'; exit(1); } my $hash = `cat /root/.accesshash`; if ( $silent ne 'yes' ) { print color 'green'; print "success!\n"; print color 'reset'; } $hash =~ s/\n//g; my $auth = "WHM root:" . $hash; return $auth; } sub call { my $url = $_[0]; my $params = $_[1] || {}; my $auth = getAuth('yes'); my $json = new JSON; my $ua = LWP::UserAgent->new; $ua->agent("suPHPfix"); $ua->env_proxy(); my $request = HTTP::Request->new(POST => $url); $request->header( Authorization => $auth ); $request->content_type('application/jsonrequest'); $request->content("$params"); my $response = $ua->request($request); my $rawResponse = $response->content; my $result = encode("UTF-8", $rawResponse); my $decoded; eval { $decoded = $json->allow_nonref->utf8->relaxed->decode($result); }; if ($@) { print color 'red'; print "ERROR: Didn't receive a valid JSON response from cPanel API.\n"; print "Got Error: $@ "; exception_msg(); print color 'reset'; exit(1); } return $decoded; } sub do_prep { die "FATAL: wrong args passed to do_prep()!\n" if (scalar(@_) != 1); my $param = $_[0]; my $response = call("http://127.0.0.1:2086/json-api/listaccts"); my $totalAccnts="0"; for my $each( @{$response->{acct}} ) { $totalAccnts++; }; print "Checking for htscanner..."; my $htscannerChk = `php -i |grep -i htscanner |grep htscanner.config_file|cut -f 1 -d "="`; my $htscannerReturnChk; if (!$htscannerChk) { print color 'yellow'; print "not found! \n"; print color 'reset'; $htscannerReturnChk = "NotInstalled"; } else { print color 'green'; print "found! \n"; print color 'reset'; $htscannerReturnChk = "Installed"; } ### Single user if ( $param ne "all" ) { if ($totalAccnts >= 2) { my ($validUser, $singleUserDocRoot, $singleUserDomain, $home_dir); for my $userCnt( @{$response->{acct}} ) { if ( $userCnt->{user} eq $param ) { $validUser = "true"; $singleUserDomain = $userCnt->{domain}; my $docRoot_details = call("http://127.0.0.1:2086/json-api/domainuserdata?domain=$singleUserDomain"); $singleUserDocRoot = $docRoot_details->{userdata}->{documentroot}; $home_dir = $docRoot_details->{userdata}->{homedir}; } } if ( $validUser ne "true" ) { print color 'red'; print "ERROR: $param not found to be a valid user. \n"; print color 'reset'; exit(1); } elsif ( $validUser eq "true" ) { runCmds("$param","$singleUserDomain","$singleUserDocRoot","$htscannerReturnChk","$home_dir"); cleanUp(); checkLogSize(); } } elsif ( $totalAccnts == 1 ) { print color 'yellow'; print "NOTICE: You only have one account, no need to specify user. Please run with '--prep all'.\n"; print color 'reset'; exit(1); } else { print color 'yellow'; print "NOTICE: You do not appear to have any accounts setup yet.\n"; print color 'reset'; exit(1); } } ### All users elsif ( $totalAccnts == 1 ) { print "Discovered one account.\n"; my ($domain, $docRoot_details, $user); for my $users( @{$response->{acct}} ) { $user = $users->{user}; $domain = $users->{domain}; $docRoot_details = call("http://127.0.0.1:2086/json-api/domainuserdata?domain=$domain"); }; my $docRoot = $docRoot_details->{userdata}->{documentroot}; my $home_dir = $docRoot_details->{userdata}->{homedir}; runCmds("$user","$domain","$docRoot","$htscannerReturnChk","$home_dir"); cleanUp(); checkLogSize(); } elsif ( $totalAccnts >= 2 ) { print "Discovered multiple accounts.\n"; my ($domain,$user,$docRoot,$home_dir); my $count="0"; for my $userCnt( @{$response->{acct}} ) { $user = $userCnt->{user}; $domain = $userCnt->{domain}; my $docRoot_details = call("http://127.0.0.1:2086/json-api/domainuserdata?domain=$domain"); $docRoot = $docRoot_details->{userdata}->{documentroot}; $home_dir = $docRoot_details->{userdata}->{homedir}; runCmds("$user","$domain","$docRoot","$htscannerReturnChk","$home_dir"); $count++; print color 'green'; print "Completed: $count / $totalAccnts \n"; print color 'reset'; }; cleanUp(); checkLogSize(); } else { print color 'yellow'; print "NOTICE: You do not appear to have any accounts setup yet.\n"; print color 'reset'; exit(1); } } sub runCmds { die "FATAL: wrong args passed to runCmds()!\n" if (scalar(@_) != 5); my $user = $_[0]; my $domain = $_[1]; my $docRoot = $_[2]; my $htscannerCheck = $_[3]; my $home_dir = $_[4]; if (! -d $docRoot ) { print color 'red'; print "ERROR: The discovered document root ($docRoot) for $user doesn't exist!\n"; exception_msg(); print color 'reset'; exit(1); } elsif ( $docRoot !~ m/^\/home/ ) { print color 'red'; print "ERROR: The discovered document root ($docRoot) for $user doesn't start with /home*\n"; exception_msg(); print color 'reset'; exit(1); } elsif ( $home_dir !~ m/^\/home/ ) { print color 'red'; print "ERROR: The discovered home dir ($home_dir) for $user doesn't start with /home*\n"; exception_msg(); print color 'reset'; exit(1); } print "---------------------------------", "\n"; print color 'yellow'; print "Working with: "; print color 'reset'; print $domain, "\n"; print color 'yellow'; print "Discovered document root: "; print color 'reset'; print $docRoot, "\n"; print "++ Removing group and world write in ", $docRoot, " ...\n"; system("find $docRoot -perm +022 -exec chmod go-w {} \\\;"); print color 'green'; print "* Task complete.\n"; print color 'reset'; print "++ Checking directory permissions in ", $docRoot, " ...\n"; my @dirs = File::Find::Object::Rule->directory->in($docRoot); for my $dir ( @dirs ) { chmod oct('0755'), $dir; } chmod oct('0750'), $docRoot; print color 'green'; print "* Task complete.\n"; print color 'reset'; print "++ Setting ownerships to ", $user, ":", $user, " in ", $docRoot, " ...\n"; chkSysUser($user); chkSysGrp($user); my ($uname, $upass, $uuid, $ugid, $uquota, $ucomment, $ugcos, $udir, $ushell, $uexpire) = getpwnam($user); my ($gname, $gpasswd, $ggid, $gmembers) = getgrnam($user); system("chown -R $user:$user $docRoot"); ## Switched back to system chown here, as File::Find::Object was not playing nice with symlinks chown($uuid, $ggid, $home_dir); chkSysGrp($apache_user); my ($apname, $appasswd, $apgid, $apmembers) = getgrnam($apache_user); chown($uuid, $apgid, $docRoot); print color 'green'; print "* Task complete.\n"; print color 'reset'; print "++ Ensuring files are readable in $docRoot ... \n"; system("find $docRoot -type f -exec chmod ugo+r {} \\\;"); print color 'green'; print "* Task complete.\n"; print color 'reset'; if ($htscannerCheck eq "Installed") { print color 'yellow'; print "NOTICE: htscanner found, keeping php tweaks. \n"; print color 'reset'; } elsif ($htscannerCheck eq "NotInstalled") { print "++ Commenting out php tweaks from .htaccess ...", "\n"; system("find $docRoot -name .htaccess -exec sed -i 's/^php_/#php_/g' {} \\\;"); print color 'green'; print "* Task complete.", "\n"; print color 'reset'; } print "Done with ", $domain, "!", "\n"; print "------------------------------", "\n"; } sub savePermOwner { die "savePermOwner() needs four params. Param1: array ref of docRoot contents. Param2: cPanelUser. Param3: docRoot \n" if (scalar(@_) != 3); my $aref = $_[0]; my $cpUser = $_[1]; my $docRoot = $_[2]; my %docRoot_Contents = ( acct => { cpanelUser => [ ], file => [ ], perm => [ ], owner => [ ], group => [ ], }, ); my $counter = 0; for my $file( @{$aref} ) { if ((my $dev,my $ino,my $mode,my $nlink,my $uid,my $gid,my $rdev,my $size,my $atime,my $mtime,my $ctime,my $blksize,my $blocks) = lstat($file)) { my $user = getpwuid($uid); my $group = getgrgid($gid); my $permissions = sprintf "%04o", S_IMODE($mode); push ( @{$docRoot_Contents{'acct'}{'cpanelUser'}}, $cpUser); push ( @{$docRoot_Contents{'acct'}{'file'}}, $file); push ( @{$docRoot_Contents{'acct'}{'perm'}}, $permissions); push ( @{$docRoot_Contents{'acct'}{'owner'}}, $user); push ( @{$docRoot_Contents{'acct'}{'group'}}, $group); $counter++; } } my $json = new JSON; my $docRoot_ContentsRef = \%docRoot_Contents; my $json_text = JSON->new->utf8(1)->pretty(0)->allow_nonref->encode($docRoot_ContentsRef); my $saveDir = '/root/datastore_suphpfix'; if (! -d "$saveDir") { print color 'yellow'; print "[ ",POSIX::strftime("%m/%d/%Y %H:%M:%S ", localtime) ,"] " , "NOTICE: $saveDir not found, creating it... \n"; print color 'reset'; mkdir $saveDir or die $!; } print color 'green'; print "[ ",POSIX::strftime("%m/%d/%Y %H:%M:%S ", localtime) ,"] " , "++ Recorded $counter file/dir permissions/ownerships for $cpUser\n"; print color 'reset'; my $json_datastore = $saveDir . '/datastore_suphpfix.' . $cpUser .'.JSON'; print color 'red'; open FILE, ">$json_datastore" or die "ERROR: Could not open $json_datastore $!"; print color 'reset'; print FILE $json_text , "\n"; close(FILE); } sub findContents { die "findFiles() requires cpanel user as arg1 and wwwRoot as arg2. \n" if (scalar(@_) != 2); my $cpUser = $_[0]; my $path = $_[1]; my $saveDir = '/root/datastore_suphpfix'; if (! -d "$saveDir") { print color 'yellow'; print "[ ",POSIX::strftime("%m/%d/%Y %H:%M:%S ", localtime) ,"] " , "NOTICE: $saveDir not found, creating it... \n"; print color 'reset'; mkdir $saveDir or die $!; } if ( $path eq "" || !defined($path) ) { print color 'red'; print "ERROR: Unable to locate file under account $cpUser !\n"; exception_msg(); print color 'reset'; exit(1); } if ( ! -e "$path") { print color 'red'; print "ERROR: File $path doesn't exist for account $cpUser !\n"; exception_msg(); print color 'reset'; exit(1); } print "[ ",POSIX::strftime("%m/%d/%Y %H:%M:%S ", localtime) ,"] " , "Recording the contents of $path ... \n"; my (@docRoot_contents, @path); push ( @path, $path ); my $ffo = File::Find::Object->new({ followlink => '0', }, @path); while ( my $file = $ffo->next() ) { push ( @docRoot_contents , $file ); } my $docRoot_contentsRef = \@docRoot_contents; savePermOwner($docRoot_contentsRef,$cpUser,$path); } sub getCPuserDetails { die "getCPuserDetails() needs one param. Param1: cPanel user or 'all'.\n" if (scalar(@_) != 1); my $arg = $_[0]; my $accntList = call("http://127.0.0.1:2086/json-api/listaccts"); my @cpUsersArray; if ( $arg eq 'all' ) { my $backedUpUsersSave = '/root/datastore_suphpfix/backedupUserList.all'; unlink($backedUpUsersSave); my $accountCounter = 0; for my $Cnt( @{$accntList->{acct}} ) { $accountCounter++; } if ( $accountCounter == 0 ) { print color 'yellow'; print "NOTICE: You do not appear to have any accounts setup yet. \n"; print color 'reset'; exit(1); } my $saveStateProgCount = 1; for my $userCnt( @{$accntList->{acct}} ) { my $cpUser = $userCnt->{user}; my $domain = $userCnt->{domain}; print color 'yellow'; print "[ ",POSIX::strftime("%m/%d/%Y %H:%M:%S ", localtime) ,"] " , "Saving state for $cpUser ($saveStateProgCount/$accountCounter)\n"; print color 'reset'; my $docRoot_details = call("http://127.0.0.1:2086/json-api/domainuserdata?domain=$domain"); my $docRoot = $docRoot_details->{userdata}->{documentroot}; print "[ ",POSIX::strftime("%m/%d/%Y %H:%M:%S ", localtime) ,"] " , "$cpUser document root is: $docRoot \n"; findContents("$cpUser","$docRoot"); push(@cpUsersArray, $cpUser); print color 'red'; open BAKUSERS, ">>$backedUpUsersSave" or die "ERROR: Could not open $!\n"; print color 'reset'; print BAKUSERS "$cpUser\n"; close(BAKUSERS); $saveStateProgCount++; } my $endCount = "0"; for my $user ( @cpUsersArray ) { $endCount++; }; print color 'bold green'; print "[ ",POSIX::strftime("%m/%d/%Y %H:%M:%S ", localtime) ,"] " , "Completed. Saved state for $endCount accounts. \n"; print color 'reset'; } else { ## Verify passed arg is valid cpUser my $isValid; for my $userCnt( @{$accntList->{acct}} ) { if ( $userCnt->{user} eq $arg ) { my $cpUser = $userCnt->{user}; my $domain = $userCnt->{domain}; print color 'yellow'; print "[ ",POSIX::strftime("%m/%d/%Y %H:%M:%S ", localtime) ,"] " , "Saving state for $cpUser\n"; print color 'reset'; $isValid = "true"; my $docRoot_details = call("http://127.0.0.1:2086/json-api/domainuserdata?domain=$domain"); my $docRoot = $docRoot_details->{userdata}->{documentroot}; print "[ ",POSIX::strftime("%m/%d/%Y %H:%M:%S ", localtime) ,"] " , "$cpUser document root is: $docRoot \n"; findContents("$cpUser","$docRoot"); print color 'bold green'; print "[ ",POSIX::strftime("%m/%d/%Y %H:%M:%S ", localtime) ,"] " , "Completed. Saved state for 1 account.\n"; print color 'reset'; } } if ( $isValid ne "true" ) { print color 'red'; print "ERROR: $arg is not a valid cPanel user.\n"; print color 'reset'; exit(1); } } } sub parseAndRestore { die "parseAndRestore() requires at least one argument. Arg1 (required) RestoreType (values: all or singleAccnt). Arg2 (required only if arg1 is singleAccnt) specific cPanel user to restore. Arg3 (optional) dryrun\n" if (scalar(@_) < 1); my $restoreType = $_[0]; my $accntToRestore = $_[1]; my $passedArg3 = $_[2]; my $cpUser=$accntToRestore; my $jsonFile; my $json = new JSON; if ( $restoreType eq 'singleAccnt' ) { my $fileStoreLocation = "/root/datastore_suphpfix/datastore_suphpfix.$accntToRestore.JSON"; if ( ! -e $fileStoreLocation ) { print color 'red'; print "[ ",POSIX::strftime("%m/%d/%Y %H:%M:%S ", localtime) ,"] " , "ERROR: Datastore file ($fileStoreLocation) for $accntToRestore doesn't exist. Please take another snapshot or restore the datastore files from a previous snapshot.\n"; print color 'reset'; exit(1); } if ( -z $fileStoreLocation ) { print color 'red'; print "[ ",POSIX::strftime("%m/%d/%Y %H:%M:%S ", localtime) ,"] " , "ERROR: Datastore file ($fileStoreLocation) for $accntToRestore exists but is empty. Please take another snapshot or restore the datastore files from a previous snapshot.\n"; print color 'reset'; exit(1); } $jsonFile = `cat $fileStoreLocation`; my $jsonFileToParse; eval { $jsonFileToParse = $json->allow_nonref->utf8->relaxed->decode($jsonFile); }; if ($@) { print color 'red'; print "[ ",POSIX::strftime("%m/%d/%Y %H:%M:%S ", localtime) ,"] " , "ERROR: Datastore file ($fileStoreLocation) is not valid JSON. Please take another snapshot or restore the datastore files from a previous snapshot. \n"; print color 'reset'; exit(1); }; my (@fileDocRootArray,@fileArray,@cpuserFileArray,@permFileArray,@ownerFileArray,@groupFileArray); for my $docRoot( @{$jsonFileToParse->{acct}->{docRoot}} ) { push(@fileDocRootArray, $docRoot); }; for my $cpuser( @{$jsonFileToParse->{acct}->{cpanelUser}} ) { push(@cpuserFileArray, $cpuser); }; for my $file( @{$jsonFileToParse->{acct}->{file}} ) { push(@fileArray, $file); }; for my $perm( @{$jsonFileToParse->{acct}->{perm}} ) { push(@permFileArray, $perm); }; for my $owner( @{$jsonFileToParse->{acct}->{owner}} ) { push(@ownerFileArray, $owner); }; for my $group( @{$jsonFileToParse->{acct}->{group}} ) { push(@groupFileArray, $group); }; print color 'yellow'; print "[ ",POSIX::strftime("%m/%d/%Y %H:%M:%S ", localtime) ,"] " , "Reverting suPHP fixes for $accntToRestore\n"; print color 'reset'; print "[ ",POSIX::strftime("%m/%d/%Y %H:%M:%S ", localtime) ,"] " , "Reverting file/dir permissions/ownerships for $accntToRestore ...\n"; my $fileSlot = 0; for my $cpuser( @{$jsonFileToParse->{acct}->{cpanelUser}} ) { if ( $passedArg3 eq 'dryrun' ) { print color 'yellow'; print "-----------------------------------------------\n"; print color 'reset'; print color 'green'; print "Proposed chmod: "; print color 'red'; print "chmod @permFileArray[$fileSlot] @fileArray[$fileSlot] "; print color 'reset'; print "\n"; print color 'green'; print "Proposed chown: "; print color 'red'; print "chown @ownerFileArray[$fileSlot]:@groupFileArray[$fileSlot] @fileArray[$fileSlot]"; print color 'reset'; print "\n"; print color 'yellow'; print "-----------------------------------------------\n"; print color 'reset'; } else { if ( -e @fileArray[$fileSlot] ) { if ( is_immutable(@fileArray[$fileSlot]) ) { print color 'yellow'; print "[ ",POSIX::strftime("%m/%d/%Y %H:%M:%S ", localtime) ,"] " , "WARNING: @fileArray[$fileSlot] is immutable, skipping...\n"; print color 'reset'; } else { chmod oct(@permFileArray[$fileSlot]), @fileArray[$fileSlot]; chkSysUser(@ownerFileArray[$fileSlot]); chkSysGrp(@ownerFileArray[$fileSlot]); my ($uname, $upass, $uuid, $ugid, $uquota, $ucomment, $ugcos, $udir, $ushell, $uexpire) = getpwnam(@ownerFileArray[$fileSlot]); my ($gname, $gpasswd, $ggid, $gmembers) = getgrnam(@groupFileArray[$fileSlot]); chown($uuid, $ggid, @fileArray[$fileSlot]); } } else { print color 'yellow'; print "[ ",POSIX::strftime("%m/%d/%Y %H:%M:%S ", localtime) ,"] " , "WARNING: @fileArray[$fileSlot] no longer exists, skipping.\n"; print color 'reset'; } } $fileSlot++; }; print color 'green'; print "[ ",POSIX::strftime("%m/%d/%Y %H:%M:%S ", localtime) ,"] " , "* Task complete.\n"; print color 'reset'; print "[ ",POSIX::strftime("%m/%d/%Y %H:%M:%S ", localtime) ,"] " , "Uncommenting php tweaks for $accntToRestore ...\n"; system("find @fileDocRootArray[0] -name .htaccess -exec sed -i 's/^#php_/php_/g' {} \\\;"); print color 'green'; print "[ ",POSIX::strftime("%m/%d/%Y %H:%M:%S ", localtime) ,"] " , "* Task complete.\n"; print color 'reset'; print color 'bold green'; print "[ ",POSIX::strftime("%m/%d/%Y %H:%M:%S ", localtime) ,"] " , "suPHP fixes reverted for $accntToRestore!\n"; print color 'reset'; } elsif ( $restoreType eq 'all' ) { open FD, "/root/datastore_suphpfix/backedupUserList.all" or die "ERROR: Could not open datastore file (/root/datastore_suphpfix/backedupUserList.all): $! . For more details see --help\n"; my @backedUpCPusers = ; close FD; my $restoreCounter = 0; for my $user ( @backedUpCPusers ) { chomp($user); $cpUser=$user; print color 'yellow'; print "[ ",POSIX::strftime("%m/%d/%Y %H:%M:%S ", localtime) ,"] " , "Reverting suPHP fixes for $cpUser ... \n"; print color 'reset'; ### Let's make sure the cPanel account to be restored still exists on the server. my $accntList = call("http://127.0.0.1:2086/json-api/listaccts"); my $validUser; for my $userCheck( @{$accntList->{acct}} ) { if ( $userCheck->{user} eq $user ) { $validUser = "true"; } }; if ( $validUser ne "true" ) { print color 'red'; print "[ ",POSIX::strftime("%m/%d/%Y %H:%M:%S ", localtime) ,"] " , "ERROR: $user is no longer a valid cPanel user on this system. You can either take another snapshot of all accounts, or you can restore snapshots for backed up cPanel accounts that are still on the server individually with --restore-state cPanelUser. For more details, see --help \n"; print color 'reset'; exit(1); } my $fileStoreLocation = "/root/datastore_suphpfix/datastore_suphpfix.$user.JSON"; if ( ! -e $fileStoreLocation ) { print color 'red'; print "[ ",POSIX::strftime("%m/%d/%Y %H:%M:%S ", localtime) ,"] " , "ERROR: Datastore file ($fileStoreLocation) for $user doesn't exist. Please take another snapshot or restore the datastore files from a previous snapshot.\n"; print color 'reset'; exit(1); } if ( -z $fileStoreLocation ) { print color 'red'; print "[ ",POSIX::strftime("%m/%d/%Y %H:%M:%S ", localtime) ,"] " , "ERROR: Datastore file ($fileStoreLocation) for $user exists but is empty. Please take another snapshot or restore the datastore files from a previous snapshot.\n"; print color 'reset'; exit(1); } $jsonFile = `cat $fileStoreLocation`; my $jsonFileToParse; eval { $jsonFileToParse = $json->allow_nonref->utf8->relaxed->decode($jsonFile); }; if ($@) { print color 'red'; print "[ ",POSIX::strftime("%m/%d/%Y %H:%M:%S ", localtime) ,"] " , "ERROR: Datastore file ($fileStoreLocation) is not valid JSON. Please take another snapshot or restore the datastore files from a previous snapshot. \n"; print color 'reset'; exit(1); }; my (@fileArray,@fileDocRootArray,@fileInodeArray,@cpuserFileArray,@permFileArray,@ownerFileArray,@groupFileArray); for my $cpuser( @{$jsonFileToParse->{acct}->{cpanelUser}} ) { push(@cpuserFileArray, $cpuser); }; for my $docRoot( @{$jsonFileToParse->{acct}->{docRoot}} ) { push(@fileDocRootArray, $docRoot); }; for my $file( @{$jsonFileToParse->{acct}->{file}} ) { push(@fileArray, $file); }; for my $perm( @{$jsonFileToParse->{acct}->{perm}} ) { push(@permFileArray, $perm); }; for my $owner( @{$jsonFileToParse->{acct}->{owner}} ) { push(@ownerFileArray, $owner); }; for my $group( @{$jsonFileToParse->{acct}->{group}} ) { push(@groupFileArray, $group); }; print "[ ",POSIX::strftime("%m/%d/%Y %H:%M:%S ", localtime) ,"] " , "Reverting file/dir permissions/ownerships for $user ... \n"; my $fileSlot = 0; for my $cpuser( @{$jsonFileToParse->{acct}->{cpanelUser}} ) { if ( $passedArg3 eq 'dryrun' ) { print color 'yellow'; print "-----------------------------------------------\n"; print color 'reset'; print color 'green'; print "Proposed chmod: "; print color 'red'; print "chmod @permFileArray[$fileSlot] @fileArray[$fileSlot] "; print color 'reset'; print "\n"; print color 'green'; print "Proposed chown: "; print color 'red'; print "chown @ownerFileArray[$fileSlot]:@groupFileArray[$fileSlot] @fileArray[$fileSlot]"; print color 'reset'; print "\n"; print color 'yellow'; print "-----------------------------------------------\n"; print color 'reset'; } else { if ( -e @fileArray[$fileSlot] ) { if ( is_immutable(@fileArray[$fileSlot]) ) { print color 'yellow'; print "[ ",POSIX::strftime("%m/%d/%Y %H:%M:%S ", localtime) ,"] " , "WARNING: @fileArray[$fileSlot] is immutable, skipping...\n"; print color 'reset'; } else { chmod oct(@permFileArray[$fileSlot]), @fileArray[$fileSlot]; chkSysUser(@ownerFileArray[$fileSlot]); chkSysGrp(@ownerFileArray[$fileSlot]); my ($uname, $upass, $uuid, $ugid, $uquota, $ucomment, $ugcos, $udir, $ushell, $uexpire) = getpwnam(@ownerFileArray[$fileSlot]); my ($gname, $gpasswd, $ggid, $gmembers) = getgrnam(@groupFileArray[$fileSlot]); chown($uuid, $ggid, @fileArray[$fileSlot]); } } else { print color 'yellow'; print "[ ",POSIX::strftime("%m/%d/%Y %H:%M:%S ", localtime) ,"] " , "WARNING: @fileArray[$fileSlot] no longer exists, skipping.\n"; print color 'reset'; } } $fileSlot++; }; print color 'green'; print "[ ",POSIX::strftime("%m/%d/%Y %H:%M:%S ", localtime) ,"] " , "* Task complete.\n"; print color 'reset'; print "[ ",POSIX::strftime("%m/%d/%Y %H:%M:%S ", localtime) ,"] " , "Uncommenting php tweaks for $user ...\n"; system("find @fileDocRootArray[0] -name .htaccess -exec sed -i 's/^#php_/php_/g' {} \\\;"); print color 'green'; print "[ ",POSIX::strftime("%m/%d/%Y %H:%M:%S ", localtime) ,"] " , "* Task complete.\n"; print color 'reset'; $restoreCounter++; print color 'bold green'; print "[ ",POSIX::strftime("%m/%d/%Y %H:%M:%S ", localtime) ,"] ","suPHP fixes reverted for $user! {Completed: $restoreCounter/", scalar(@backedUpCPusers), "}\n"; print color 'reset'; }; print color 'green'; print "[ ",POSIX::strftime("%m/%d/%Y %H:%M:%S ", localtime) ,"] " , "suPHP fixes reverted for $restoreCounter accounts. \n"; print color 'reset'; } } sub restorePermsOwners { die "restorePermsOwners() requires one argument. Arg1: cpanelUser or 'all' Optional arg2: dryrun\n" if (scalar(@_) < 1); my $passedArg = $_[0]; my $passedArg2 = $_[1]; if ( $passedArg eq "all" ) { if ( $passedArg2 eq "dryrun" ) { parseAndRestore("all","no","$passedArg2"); } else { parseAndRestore("all"); } } else { #Verify if passedArg is a valid cpanel user my $accntList = call("http://127.0.0.1:2086/json-api/listaccts"); my (@cpUsersArray,$isValid); for my $userCnt( @{$accntList->{acct}} ) { if ( $userCnt->{user} eq $passedArg ) { $isValid = 'true'; if ( $passedArg2 eq 'dryrun' ) { parseAndRestore("singleAccnt","$passedArg","$passedArg2"); } else { parseAndRestore("singleAccnt","$passedArg"); } } } if ( $isValid ne "true" ) { print color 'red'; print "ERROR: $passedArg is not a valid cPanel user.\n"; print color 'reset'; exit(1); } } } ####################################### ####################################### preChecks(); print "-suPHPfix $version\n"; if ( $ARGV[0] eq '--prep' ) { if ( scalar(@ARGV) > 2 ) { print "Too many arguments pased. For help, see --help.\n"; exit(1); } if ( $ARGV[1] eq "all" ) { do_prep("all"); } elsif ( defined($ARGV[1]) ) { do_prep("$ARGV[1]"); } else { print "Please give a valid argument to --prep. For help, see --help.\n"; } } elsif ( $ARGV[0] eq '--save-state' ) { if ( scalar(@ARGV) > 2 ) { print "Too many arguments passed. For help, see --help.\n"; exit(1); } if ( defined($ARGV[1]) ) { if ( $ARGV[1] eq 'all' ) { getCPuserDetails("all"); } else { getCPuserDetails("$ARGV[1]"); } } elsif ( !defined($ARGV[1]) ) { print "Please pass a valid option to --save-state . For help, see --help \n"; exit(1); } } elsif ( $ARGV[0] eq '--restore-state' ) { if ( scalar(@ARGV) > 3 ) { print "Too many arguments passed. For help, see --help.\n"; exit(1); } if ( defined($ARGV[1]) ) { if ( $ARGV[1] eq 'all' ) { if ( defined($ARGV[2]) ) { if ( $ARGV[2] eq 'dryrun' ) { print color 'yellow'; print "\nRunning restore state in dryrun mode. No changes will be made!\n\n"; print color 'reset'; sleep(3); restorePermsOwners("all","dryrun"); } else { print "Unrecognized arguments. For help, see --help \n"; } } else { restorePermsOwners("all"); } } else { if ( $ARGV[2] eq 'dryrun' ) { print color 'yellow'; print "\nRunning restore state in dryrun mode. No changes will be made!\n\n"; print color 'reset'; sleep(3); restorePermsOwners("$ARGV[1]","dryrun"); } else { restorePermsOwners("$ARGV[1]"); } } } elsif ( !defined($ARGV[1]) ) { print "Please pass a valid option to --restore-state . For help, see --help \n"; exit(1); } } elsif ( $ARGV[0] eq '--help' ) { help(); } elsif ( $ARGV[0] eq "" ) { print color 'yellow'; print "NOTICE: suphpfix requires an option. For help, see --help\n"; print color 'reset'; } else { print "Option '$ARGV[0]' not recognized. For help, see --help \n"; } =head1 TITLE suphpfix.pl - suPHP migration assistance for cPanel servers =head1 SUMMARY Fixes common suPHP/FCGI/CGI (with suexec enabled) permission/ownership issues on cPanel servers. For specifics, see the USAGE section. This script grabs account information such as cPanel usernames, and document roots from the cPanel JSON API. /root/datastore_suphpfix/ is used to store JSON data for saving states. Restore states read these JSON files to perform restores. =head1 USAGE +++ Fix common problems when converting to suPHP +++ --prep all ==> This will chown all cPanel users files/directories in their public_html's to cPanelUser.cPanelUser. It will also remove group and world write from files/directories. In addition, any php directive in .htaccess such as php_flag will be commented out (unless htscanner is present). --prep cPanelUser ==> Same as '--prep all' but only applies fixes to the specified cPanel account. +++ Saving states +++ --save-state all ==> This saves the current permission/ownership settings (for later restores) for all cPanel accounts files/directories under their public_html's. --save-state cPanelUser ==> Same as '--save-state all' but only for the specified cPanel account. +++ Restoring states +++ --restore-state all ==> This restores all saved cPanel accounts permission/ownership settings at the time --save-state all was last ran. It also uncomments any php directives in accounts .htaccess file(s). --restore-state all dryrun ==> Same as '--restore-state all' but just outputs proposed chmods/chowns for all accounts without actually changing anything. --restore-state cPanelUser ==> This restores only the specified cPanel accounts permissions/ownership settings at the time --save-state was last ran for the user. It also uncomments any php directives in the accounts .htaccess file(s). --restore-state cPanelUser dryrun ==> Same as '--restore-state cPanelUser' but just outputs proposed chmods/chowns for the user without actually changing anything. =head1 AUTHOR Scott Sullivan (ssullivan@liquidweb.com) =cut