Server IP : 195.201.23.43 / Your IP : 3.141.40.192 Web Server : Apache System : Linux webserver2.vercom.be 5.4.0-192-generic #212-Ubuntu SMP Fri Jul 5 09:47:39 UTC 2024 x86_64 User : kdecoratie ( 1041) PHP Version : 7.1.33-63+ubuntu20.04.1+deb.sury.org+1 Disable Function : pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals, MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : ON Directory : /usr/share/webmin/virtual-server/ |
Upload File : |
use feature 'state'; use Fcntl ':mode'; # Functions for accessing files and running commands as a domain owner # has_domain_user(&domain) # Returns 1 if some domain has a Unix user sub has_domain_user { my ($d) = @_; if ($d->{'parent'}) { $d = &get_domain($d->{'parent'}); } return 0 if (!$d->{'unix'}); my @uinfo = getpwnam($d->{'user'}); return scalar(@uinfo) ? 1 : 0; } # switch_to_domain_user(&domain|&user) # Changes the current UID and GID to that of the domain's unix user, or # a mailbox user sub switch_to_domain_user { my ($d_or_user) = @_; if ($d_or_user->{'id'}) { # Virtualmin domain given my $d = $d_or_user; if ($d->{'parent'}) { $d = &get_domain($d->{'parent'}); } return 0 if (!$d->{'unix'}); # Doesn't have a user &switch_to_unix_user([ $d->{'user'}, undef, $d->{'uid'}, $d->{'ugid'} ]); $ENV{'USER'} = $ENV{'LOGNAME'} = $d->{'user'}; $ENV{'HOME'} = $d->{'home'}; return 1; } else { # Mailbox user given my $user = $d_or_user; return 0 if (!$user->{'uid'}); &switch_to_unix_user([ $user->{'user'}, undef, $user->{'uid'}, $user->{'gid'} ]); $ENV{'USER'} = $ENV{'LOGNAME'} = $user->{'user'}; $ENV{'HOME'} = $user->{'home'}; return 1; } } # run_as_domain_user(&domain, command, background, [never-su]) # Runs some command as the owner of a virtual server, and returns the output sub run_as_domain_user { local ($d, $cmd, $bg, $nosu) = @_; if ($d->{'parent'}) { $d = &get_domain($d->{'parent'}); } # Set a reasonable environment for the command local %OLDENV = %ENV; $ENV{'HOME'} = $uinfo[7]; $ENV{'USER'} = $uinfo[0]; $ENV{'LOGNAME'} = $uinfo[0]; &foreign_require("proc"); local @uinfo = getpwnam($d->{'user'}); local @rv; if (($uinfo[8] =~ /\/(sh|bash|tcsh|csh)$/ || $gconfig{'os_type'} =~ /-linux$/) && !$nosu) { # Usable shell .. use su local $cmd = &command_as_user($d->{'user'}, 0, $cmd); if ($bg) { # No status available &system_logged("$cmd &"); @rv = ( undef, 0 ); } else { local $out = &backquote_logged($cmd); @rv = ( $out, $? ); } } else { # Need to run ourselves local $temp = &transname(); open(TEMP, ">$temp"); &proc::safe_process_exec_logged($cmd, $d->{'uid'}, $d->{'ugid'},\*TEMP); local $ex = $?; local $out; close(TEMP); local $_; open(TEMP, "<".$temp); while(<TEMP>) { $out .= $_; } close(TEMP); unlink($temp); @rv = ( $out, $ex ); } # Clean up the environment $ENV{'HOME'} = $OLDENV{'HOME'}; $ENV{'USER'} = $OLDENV{'USER'}; $ENV{'LOGNAME'} = $OLDENV{'LOGNAME'}; return wantarray ? @rv : $rv[0]; } # make_dir_as_domain_user(&domain, dir, permissions, recursive?) # Creates a directory, with mkdir run as the domain owner. Returns 1 on success # or 0 on failure. sub make_dir_as_domain_user { my ($d, $dir, $perms, $recur) = @_; return 1 if (&is_readonly_mode()); local $cmd = "mkdir ".($recur ? "-p " : "").quotemeta($dir)." 2>&1";; if ($perms) { $cmd .= " && chmod ".sprintf("%o", $perms & 07777)." ". quotemeta($dir)." 2>&1";; } local ($out, $ex) = &run_as_domain_user($d, $cmd); return $ex ? 0 : 1; } # unlink_file_as_domain_user(&domain, file, ...) # Deletes some files or directories, as the domain owner sub unlink_file_as_domain_user { my ($d, @files) = @_; return 1 if (&is_readonly_mode()); while(@files) { my @del; if (@files > 100) { @del = @files[0..99]; @files = @files[100..$#files]; } else { @del = @files; @files = ( ); } local $cmd = "rm -rf ".join(" ", map { quotemeta($_) } @del)." 2>&1"; local ($out, $ex) = &run_as_domain_user($d, $cmd); return 0 if ($ex); } return 1; } # unlink_logged_as_domain_user(&domain, file, ...) # Like unlink_file_as_domain_user, but locks the file to log the change sub unlink_logged_as_domain_user { my ($d, @files) = @_; my %locked; foreach my $f (@file) { if (!&test_lock($f)) { &lock_file($f); $locked{$f} = 1; } } my $rv = &unlink_file_as_domain_user($d, @files); foreach my $f (@files) { if ($locked{$f}) { &unlock_file($f); } } return $rv; } # symlink_file_as_domain_user(&domain, src, dest) # Creates a symbolic link, using ln -s run as the domain owner sub symlink_file_as_domain_user { my ($d, $src, $dest) = @_; return 1 if (&is_readonly_mode()); local $cmd = "ln -s ".quotemeta($src)." ".quotemeta($dest)." 2>&1"; local ($out, $ex) = &run_as_domain_user($d, $cmd); return $ex ? 0 : 1; } # symlink_logged_as_domain_user(&domain, src, dest) sub symlink_logged_as_domain_user { my ($d, $src, $dest) = @_; &lock_file($dest); my $rv = &symlink_file_as_domain_user($d, $src, $dest); &unlock_file($dest); return $rv; } # link_file_as_domain_user(&domain, src, dest) # Creates a hard link, using ln run as the domain owner sub link_file_as_domain_user { my ($d, $src, $dest) = @_; return 1 if (&is_readonly_mode()); local $cmd = "ln ".quotemeta($src)." ".quotemeta($dest)." 2>&1"; local ($out, $ex) = &run_as_domain_user($d, $cmd); return $ex ? 0 : 1; } # open_tempfile_as_domain_user(&domain, handle, file, [no-error], # [no-tempfile], [safe?]) # Like the Webmin open_tempfile function, but in a sub-process that runs as # the domain owner. sub open_tempfile_as_domain_user { my ($d, $fh, $file, $noerror, $notemp, $safe) = @_; $fh = (caller(0))[0]."::".$fh; my $realfile = $file; $realfile =~ s/^[> ]*//; while(-l $realfile) { # Open the link target instead $realfile = &resolve_links($realfile); } if (-d $realfile) { if ($noerror) { return 0; } else { &error("Cannot write to directory $realfile"); } } if (&is_readonly_mode() && $file =~ />/ && !$safe) { # Read-only mode .. veto all writes return open($fh, ">$null_file"); } # Get the temp file now, before forking my $tempfile; if ($file =~ /^>\s*(([a-zA-Z]:)?\/.*)$/ && !$notemp) { $tempfile = &open_tempfile($realfile); } # Create pipes for sending in data and reading back error my ($writein, $writeout) = ($fh, "writeout".(++$main::open_tempfile_count)); my ($readin, $readout) = ("readin".(++$main::open_tempfile_count), "readout".(++$main::open_tempfile_count)); pipe($writeout, $writein); pipe($readout, $readin); # Fork the process we will use for writing my $pid = fork(); if ($pid < 0) { if ($noerror) { return 0; } else { &error("Failed to fork sub-process for writing : $!"); } } if (!$pid) { # Close file handles untie(*STDIN); untie(*STDOUT); untie(*STDERR); close(STDIN); close(STDOUT); close(STDERR); close($writein); close($readout); my $oldsel = select($readin); $| = 1; select($oldsel); # Open the temp file and start writing &switch_to_domain_user($d); if ($file =~ /^>\s*(([a-zA-Z]:)?\/.*)$/ && !$notemp) { # Writing to a file, via a tempfile my $ex = open(FILE, ">$tempfile"); if (!$ex) { print $readin "Failed to open $tempfile : $!\n"; exit(1); } } elsif ($file =~ /^>\s*(([a-zA-Z]:)?\/.*)$/ && $notemp) { # Writing directly my $ex = open(FILE, ">$realfile"); if (!$ex) { print $readin "Failed to open $realfile : $!\n"; exit(1); } } elsif ($file =~ /^>>\s*(([a-zA-Z]:)?\/.*)$/) { # Appending to a file my $ex = open(FILE, ">>$realfile"); if (!$ex) { print $readin "Failed to open $realfile : $!\n"; exit(1); } } else { print $readin "Unknown file mode $file\n"; exit(1); } print $readin "OK\n"; # Signal OK $SIG{'PIPE'} = 'ignore'; # Write errors detected by print while(<$writeout>) { my $rv = (print FILE $_); if (!$rv) { print $readin "Write to $realfile failed : $!\n"; exit(2); } } my $ex = close(FILE); if ($ex) { exit(0); } else { print $readin "Close of $realfile failed : $!\n"; exit(3); } } close($writeout); close($readin); # Check if the file was opened OK my $oldsel = select($readout); $| = 1; select($oldsel); my $err = <$readout>; chop($err); if ($err ne 'OK') { waitpid($pid, 0); if ($noerror) { return 0; } else { &error($err || "Unknown error in sub-process"); } } $main::open_temphandles{$fh} = $realfile; $main::open_tempfile_as_domain_user_pid{$fh} = $pid; $main::open_tempfile_readout{$fh} = $readout; $main::open_tempfile_noerror{$fh} = $noerror; return 1; } # close_tempfile_as_domain_user(&domain, fh) # Like close_tempfile, but does the final write as the domain owner sub close_tempfile_as_domain_user { my ($d, $fh) = @_; $fh = (caller(0))[0]."::".$fh; my $pid = $main::open_tempfile_as_domain_user_pid{$fh}; my $readout = $main::open_tempfile_readout{$fh}; my $realfile = $main::open_temphandles{$fh}; my $tempfile = $main::open_tempfiles{$realfile}; my ($rv, $err); if ($pid) { # Writing was done in a sub-process .. wait for it to exit close($fh); waitpid($pid, 0); my $ex = $?; $err = <$readout>; close($readout); # Rename over temp file if needed if ($tempfile && !$ex) { my @st = stat($realfile); &rename_as_domain_user($d, $tempfile, $realfile); if (@st) { &set_permissions_as_domain_user($d, $st[2], $realfile); } } $rv = !$ex; } else { # Just close the file $rv = close($fh); } delete($main::open_tempfile_as_domain_user_pid{$fh}); delete($main::open_tempfile_readout{$fh}); delete($main::open_temphandles{$fh}); return $rv; } # open_readfile_as_domain_user(&domain, handle, file) # Open a file for reading, using a sub-process run as the domain owner sub open_readfile_as_domain_user { my ($d, $fh, $file) = @_; my ($readin, $readout) = ("readin".(++$main::open_tempfile_count), $fh); pipe($readout, $readin); my $pid = fork(); if ($pid < 0) { return 0; } if (!$pid) { # Close file handles untie(*STDIN); untie(*STDOUT); untie(*STDERR); close(STDIN); close(STDOUT); close(STDERR); close($readout); my $oldsel = select($readin); $| = 1; select($oldsel); # Open the file and start reading &switch_to_domain_user($d); my $ok = open(FILE, "<".$file); if (!$ok) { print $readin "Failed to open $file : $!\n"; exit(1); } print $readin "OK\n"; # Signal OK while(<FILE>) { print $readin $_; } close(FILE); exit(0); } close($readin); my $oldsel = select($readout); $| = 1; select($oldsel); my $err = <$readout>; chop($err); if ($err ne 'OK') { waitpid($pid, 0); return 0; } $main::open_readfile_as_domain_user_pid{$fh} = $pid; return 1; } # close_readfile_as_domain_user(&domain, handle) # Close a file opened by open_readfile_as_domain_user sub close_readfile_as_domain_user { my ($d, $fh) = @_; my $pid = $main::open_readfile_as_domain_user_pid{$fh}; if ($pid) { close($fh); kill('KILL', $pid); waitpid($pid, 0); } delete($main::open_readfile_as_domain_user_pid{$fh}); return 1; } # read_file_lines_as_domain_user(&domain, file, [readonly]) # Like Webmin's read_file_lines function, but opens the file as a domain owner sub read_file_lines_as_domain_user { my ($d, $file, $ro) = @_; if (!$file) { my ($package, $filename, $line) = caller; &error("Missing file to read at ${package}::${filename} line $line\n"); } if (!$main::file_cache{$file}) { my (@lines, $eol); local $_; &open_readfile_as_domain_user($d, READFILE, $file); while(<READFILE>) { if (!$eol) { $eol = /\r\n$/ ? "\r\n" : "\n"; } tr/\r\n//d; push(@lines, $_); } &close_readfile_as_domain_user($d, READFILE); $main::file_cache{$file} = \@lines; $main::file_cache_noflush{$file} = $ro; $main::file_cache_eol{$file} = $eol || "\n"; } else { # Make read-write if currently readonly if (!$ro) { $main::file_cache_noflush{$file} = 0; } } return $main::file_cache{$file}; } # flush_file_lines_as_domain_user(&domain, file, eol) # Write out a file read into memory by read_file_lines_as_domain_user sub flush_file_lines_as_domain_user { my ($d, $file, $eol) = @_; my ($package, $filename, $line) = caller; if (!$file) { &error("Missing file to flush at ${package}::${filename} line $line"); } if (!$main::file_cache{$file}) { &error("File $file was not opened by read_file_lines_as_domain_user ". "at ${package}::${filename} line $line"); } $eol ||= $main::file_cache_eol{$file} || "\n"; if (!$main::file_cache_noflush{$file}) { &open_tempfile_as_domain_user($d, FLUSHFILE, ">$file"); foreach my $line (@{$main::file_cache{$file}}) { (print FLUSHFILE $line,$eol) || &error(&text("efilewrite", $file, $!)); } &close_tempfile_as_domain_user($d, FLUSHFILE); } delete($main::file_cache{$file}); delete($main::file_cache_noflush{$file}); } # rename_as_domain_user(&domain, oldfile, newfile) # Rename a file, using mv run as the domain owner sub rename_as_domain_user { my ($d, $oldfile, $newfile) = @_; return 1 if (&is_readonly_mode()); my $cmd = "mv -f ".quotemeta($oldfile)." ".quotemeta($newfile)." 2>&1"; my ($out, $ex) = &run_as_domain_user($d, $cmd); return $ex ? 0 : 1; } # set_permissions_as_domain_user(&domain, perms, file, ...) # Set permissions on some file, using chmod run as the domain owner sub set_permissions_as_domain_user { my ($d, $perms, @files) = @_; return 1 if (&is_readonly_mode()); my $cmd = "chmod ".sprintf("%o", $perms & 07777)." ". join(" ", map { quotemeta($_) } @files)." 2>&1"; my ($out, $ex) = &run_as_domain_user($d, $cmd); return $ex ? 0 : 1; } # execute_as_domain_user(&domain, &code) # Run some code reference in a sub-process, as the domain's user. If the # function fails (due to calling error), this process will exit too. sub execute_as_domain_user { my ($d, $code) = @_; my $pid = fork(); if (!$pid) { &switch_to_domain_user($d); &$code(); exit(0); } elsif ($pid < 0) { &error("Fork for execute_as_domain_user failed : $!"); } else { waitpid($pid, 0); if ($?) { exit($? / 256); } } } # control_path_permissions(action, dir/file, [under_dir]) # Can set and get given file permissions for each path parts # # Example: # call: control_path_permissions('set', '/root/dir1/dir2/dir3/file1.txt', '/root/dir1'); # stores: ['/root/dir1/dir2/dir3/file1.txt' => # { # '/root/dir1/dir2/dir3/file1.txt' => oct # 644 # '/root/dir1/dir2/dir3' => oct # 755 # '/root/dir1/dir2' => oct # 755 # '/root/dir1' => oct # 755 # }] sub control_path_permissions { my ($action, $path, $under) = @_; state %paths; # Store and fix initial path my $ipath = $path; $ipath =~ s/[\/]+$//g; # Return existing if ($action eq 'get') { return $paths{$ipath}; } elsif ($action eq 'set') { # Controls which path parts are affected when called, # e.g. passing /home/user/public_html/script will not # consider anything below given directory my $under_dir = !ref($under) && -d $under ? $under : ref($under) ? $under->{'home'} || "/" : "/"; # Sanity check for passed path to # make sure that a directory ends # always with trailing slash if (-d $path && $path !~ /\/$/) { $path .= "/"; } my $xpath = $path; my %cpaths; # In case initial param refers to a file, always add it # manually, as next iteration only works on directories $cpaths{$path} = sprintf("%04o", (stat($path))[2] & 07777) if (!-d $path && -r $path); # Build a hash storing `file => currperms` records map { $xpath =~ s/(.*)(\/.*)/$1/; if (&is_under_directory($under_dir, $xpath)) { $cpaths{$xpath} = sprintf("%04o", (stat($xpath))[2] & 07777) if ($xpath && -e $xpath); } } split('/', $path); # Return a list of current permissions for passed path $paths{$ipath} = \%cpaths; return $paths{$ipath}; } } # set_filepath_permissions_as_domain_user(&domain, file, [perms], [under]) # Set given file and each path parts certain # permissions (to make file and path writable) sub set_filepath_permissions_as_domain_user { my ($d, $file, $perms, $under) = @_; $perms ||= 0755; my $fileparts = &control_path_permissions('set', $file, $under || $d->{'home'}); my @files = keys %{$fileparts}; my $done_num = &set_permissions_as_domain_user($d, $perms, @files) if (@files); return $done_num; } # remove_write_permissions_for_group(dir, [&excludes]) # Removes write permissions for group from all files and directories under # given directory, except those that match excludes given in array reference sub remove_write_permissions_for_group { my ($dir, $excludes) = @_; opendir(my $dh, $dir) || die("Cannot open directory $dir: $!"); my @entries = readdir($dh); closedir($dh); ENTRY: foreach my $entry (@entries) { next if ($entry eq '.' || $entry eq '..'); my $path = "$dir/$entry"; if ($excludes) { foreach my $exclude (@$excludes) { next ENTRY if ($path =~ /\Q$exclude\E/); } } my $mode = (stat($path))[2]; if ($mode & S_IWGRP) { $mode &= ~S_IWGRP; chmod($mode, $path) || warn("Failed to change permissions for $path: $!"); } &remove_write_permissions_for_group($path, $excludes) if (-d $path); } } # restore_filepath_permissions_as_domain_user(&domain, file, [under]) # Restores given file and each path parts initial permissions sub restore_filepath_permissions_as_domain_user { my ($d, $file, $under) = @_; my $fileparts = &control_path_permissions('get', $file, $under || $d->{'home'}); my $done_num; foreach my $ifile (keys %{$fileparts}) { if (&set_permissions_as_domain_user($d, oct($fileparts->{$ifile}), $ifile)) { $done_num++; } } return $done_num; } # write_as_domain_user(&domain, &code) # Runs some code with the effective UID and GID set to that of the domain user, # so that file IO is locked down. Sets it back afterwards. sub write_as_domain_user { my ($d, $code) = @_; if ($d->{'parent'}) { $d = &get_domain($d->{'parent'}); } if ($d->{'unix'}) { my $gid = $d->{'ugid'} || $d->{'gid'}; $) = $gid." ".join(" ", $gid, &other_groups($d->{'user'})); $> = $d->{'uid'}; } my @rv; eval { local $main::error_must_die = 1; @rv = &$code(); }; my $err = $@; if ($d->{'unix'}) { $) = 0; $> = 0; } if ($err) { $err =~ s/\s+at\s+(\/\S+)\s+line\s+(\d+)\.?//; &error($err); } return wantarray ? @rv : $rv[0]; } # write_as_mailbox_user(&user, &code) # Runs some code with the effective UID and GID set to that of a mailbox user, # so that file IO is locked down. Sets it back afterwards. sub write_as_mailbox_user { my ($user, $code) = @_; $) = $user->{'gid'}." ".join(" ", $user->{'gid'}, &other_groups($user->{'user'})); $> = $user->{'uid'}; my @rv; eval { local $main::error_must_die = 1; @rv = &$code(); }; my $err = $@; $) = 0; $> = 0; if ($err) { $err =~ s/\s+at\s+(\/\S+)\s+line\s+(\d+)\.?//; &error($err); } return wantarray ? @rv : $rv[0]; } # copy_source_dest_as_domain_user(&domain, source, dest) # Copy a file or directory, with commands run as a domain owner sub copy_source_dest_as_domain_user { my ($d, $src, $dst) = @_; return (1, undef) if (&is_readonly_mode()); my $ok = 1; my $err; if (-d $src) { # A directory .. need to copy with tar command my @st = stat($src); &unlink_file_as_domain_user($d, $dst); &make_dir_as_domain_user($d, $dst, $st[2]); my ($out, $ex) = &run_as_domain_user($d, "(cd ".quotemeta($src)." && ". &make_tar_command("cf", "-", "."). " | (cd ".quotemeta($dst)." && ". &make_tar_command("xf", "-").")) 2>&1"); if ($ex) { $ok = 0; $err = $out; } } else { # Can just copy with cp my ($out, $ex) = &run_as_domain_user($d, "cp -p ".quotemeta($src)." ".quotemeta($dst)." 2>&1"); if ($ex && $out !~ /failed to preserve ownership/i) { $ok = 0; $err = $out; } } return wantarray ? ($ok, $err) : $ok; } # copy_write_as_domain_user(&domain, source, dest) # Copy a file, with only the writing done as the domain user sub copy_write_as_domain_user { my ($d, $src, $dst) = @_; return (1, undef) if (&is_readonly_mode()); my ($ok, $err); $ok = 1; if (!open(SOURCEFILE, "<".$src)) { $ok = 0; $err = $!; } else { if (!&open_tempfile_as_domain_user($d, DESTFILE, ">$dst", 1, 1)) { $ok = 0; $err = $!; } else { eval { local $main::error_must_die = 1; my $bs = &get_buffer_size(); my $buf; while(read(SOURCEFILE, $buf, $bs) > 0) { &print_tempfile(DESTFILE, $buf); } }; if ($@) { $ok = 0; $err = $@; } close(SOURCEFILE); &close_tempfile_as_domain_user($d, DESTFILE); } } return wantarray ? ($ok, $err) : $ok; } # copy_write_remote_as_domain_user(&domain, &server, srcfile, dstfile) # Read a file as root, and write it with domain user permissions. The file # can be local, or on a remote Webmin system. sub copy_write_remote_as_domain_user { my ($d, $r, $src, $dst) = @_; if ($r->{'id'} == 0) { ©_write_as_domain_user($d, $src, $dst); } else { &write_as_domain_user($d, sub { &remote_read($r, $dst, $src); }); } } # safe_domain_file(&domain, file) # Returns 1 if some file is safe for a given domain to manage. # Currently just prevents symlinks sub safe_domain_file { my ($d, $file) = @_; my $realfile = &resolve_links($file); return $realfile eq $file || &same_file($realfile, $file); } # read_file_contents_as_domain_user(&domain, file) # Returns the full contents of some file, read as the domain owner sub read_file_contents_as_domain_user { my ($d, $file) = @_; &open_readfile_as_domain_user($d, FILE, $file) || return undef; local $/ = undef; my $rv = <FILE>; &close_readfile_as_domain_user($d, FILE); return $rv; } # readable_by_domain_user(&domain, file) # Uses -r to check for readability, but as the domain user sub readable_by_domain_user { my ($d, $file) = @_; my $rv = &write_as_domain_user($d, sub { -r $file }); return $rv; } 1;Private