__PACKAGE__->run() was called before my variables were declared, so $MAIN_CONFIG and $REMOTES_DIR were undef when action methods ran. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
414 lines
15 KiB
Perl
414 lines
15 KiB
Perl
#!/usr/local/cpanel/3rdparty/bin/perl
|
|
package Cpanel::AdminBin::Script::Call::Gniza::Restore;
|
|
|
|
use strict;
|
|
use warnings;
|
|
use parent 'Cpanel::AdminBin::Script::Call';
|
|
use IPC::Open3;
|
|
use Symbol 'gensym';
|
|
|
|
my $GNIZA_BIN = '/usr/local/bin/gniza';
|
|
my $MAIN_CONFIG = '/etc/gniza/gniza.conf';
|
|
my $REMOTES_DIR = '/etc/gniza/remotes.d';
|
|
|
|
# Argument validation patterns (mirrors GnizaWHM::Runner)
|
|
my %OPT_PATTERNS = (
|
|
remote => qr/^[a-zA-Z0-9_,-]+$/,
|
|
timestamp => qr/^\d{4}-\d{2}-\d{2}T\d{6}$/,
|
|
path => qr/^(?!.*\.\.)[a-zA-Z0-9_.\/@ -]+$/,
|
|
exclude => qr/^[a-zA-Z0-9_.,\/@ *?\[\]-]+$/,
|
|
);
|
|
|
|
my $ACCOUNT_RE = qr/^[a-z][a-z0-9_-]*$/;
|
|
my $REMOTE_RE = qr/^[a-zA-Z0-9_-]+$/;
|
|
my $DBNAME_RE = qr/^[a-zA-Z0-9_]+$/;
|
|
my $EMAIL_RE = qr/^[a-zA-Z0-9._+-]+\@[a-zA-Z0-9._-]+$/;
|
|
my $DOMAIN_RE = qr/^[a-zA-Z0-9._-]+$/;
|
|
my $TS_RE = qr/^\d{4}-\d{2}-\d{2}T\d{6}$/;
|
|
|
|
# ── Allowed remotes for user restore ──────────────────────────
|
|
|
|
sub _get_allowed_remotes {
|
|
my $setting = '';
|
|
if (open my $fh, '<', $MAIN_CONFIG) {
|
|
while (my $line = <$fh>) {
|
|
if ($line =~ /^USER_RESTORE_REMOTES=(?:"([^"]*)"|'([^']*)'|(\S*))$/) {
|
|
$setting = defined $1 ? $1 : (defined $2 ? $2 : ($3 // ''));
|
|
}
|
|
}
|
|
close $fh;
|
|
}
|
|
# Default to "all" if not set
|
|
$setting = 'all' if !defined $setting || $setting eq '';
|
|
|
|
return $setting;
|
|
}
|
|
|
|
sub _list_all_remotes {
|
|
my @remotes;
|
|
if (-d $REMOTES_DIR && opendir my $dh, $REMOTES_DIR) {
|
|
while (my $entry = readdir $dh) {
|
|
if ($entry =~ /^([a-zA-Z0-9_-]+)\.conf$/) {
|
|
push @remotes, $1;
|
|
}
|
|
}
|
|
closedir $dh;
|
|
}
|
|
return sort @remotes;
|
|
}
|
|
|
|
sub _is_remote_allowed {
|
|
my ($remote) = @_;
|
|
my $setting = _get_allowed_remotes();
|
|
return 0 if $setting eq ''; # disabled
|
|
|
|
if ($setting eq 'all') {
|
|
# Check it actually exists
|
|
return -f "$REMOTES_DIR/$remote.conf" ? 1 : 0;
|
|
}
|
|
|
|
my %allowed = map { $_ => 1 } split /,/, $setting;
|
|
return $allowed{$remote} ? 1 : 0;
|
|
}
|
|
|
|
sub _get_filtered_remotes {
|
|
my $setting = _get_allowed_remotes();
|
|
return () if $setting eq '';
|
|
|
|
my @all = _list_all_remotes();
|
|
return @all if $setting eq 'all';
|
|
|
|
my %allowed = map { $_ => 1 } split /,/, $setting;
|
|
return grep { $allowed{$_} } @all;
|
|
}
|
|
|
|
# ── Command execution ─────────────────────────────────────────
|
|
|
|
sub _run_gniza {
|
|
my (@args) = @_;
|
|
|
|
my $err_fh = gensym;
|
|
my ($in, $out);
|
|
my $pid = eval { open3($in, $out, $err_fh, $GNIZA_BIN, @args) };
|
|
unless ($pid) {
|
|
return (0, '', "Failed to execute gniza: $@");
|
|
}
|
|
close $in if $in;
|
|
|
|
my $stdout = do { local $/; <$out> } // '';
|
|
my $stderr = do { local $/; <$err_fh> } // '';
|
|
close $out;
|
|
close $err_fh;
|
|
|
|
waitpid($pid, 0);
|
|
my $exit_code = $? >> 8;
|
|
|
|
return ($exit_code == 0, $stdout, $stderr);
|
|
}
|
|
|
|
# ── Action dispatch ───────────────────────────────────────────
|
|
|
|
sub _actions {
|
|
return qw(
|
|
LIST_ALLOWED_REMOTES
|
|
LIST_SNAPSHOTS
|
|
LIST_DATABASES
|
|
LIST_MAILBOXES
|
|
LIST_FILES
|
|
LIST_DBUSERS
|
|
LIST_CRON
|
|
LIST_DNS
|
|
LIST_SSL
|
|
RESTORE_ACCOUNT
|
|
RESTORE_FILES
|
|
RESTORE_DATABASE
|
|
RESTORE_MAILBOX
|
|
RESTORE_CRON
|
|
RESTORE_DBUSERS
|
|
RESTORE_DOMAINS
|
|
RESTORE_SSL
|
|
);
|
|
}
|
|
|
|
sub LIST_ALLOWED_REMOTES {
|
|
my ($self) = @_;
|
|
my @remotes = _get_filtered_remotes();
|
|
return join("\n", @remotes);
|
|
}
|
|
|
|
sub LIST_SNAPSHOTS {
|
|
my ($self, $remote) = @_;
|
|
my $user = $ENV{'REMOTE_USER'} // '';
|
|
|
|
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
|
|
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
|
|
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
|
|
|
|
my ($ok, $stdout, $stderr) = _run_gniza('list', "--remote=$remote", "--account=$user");
|
|
return $ok ? $stdout : "ERROR: $stderr";
|
|
}
|
|
|
|
sub LIST_DATABASES {
|
|
my ($self, $remote, $timestamp) = @_;
|
|
my $user = $ENV{'REMOTE_USER'} // '';
|
|
|
|
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
|
|
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
|
|
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
|
|
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
|
|
|
|
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'list-databases', $user,
|
|
"--remote=$remote", "--timestamp=$timestamp");
|
|
return $ok ? $stdout : "ERROR: $stderr";
|
|
}
|
|
|
|
sub LIST_MAILBOXES {
|
|
my ($self, $remote, $timestamp) = @_;
|
|
my $user = $ENV{'REMOTE_USER'} // '';
|
|
|
|
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
|
|
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
|
|
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
|
|
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
|
|
|
|
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'list-mailboxes', $user,
|
|
"--remote=$remote", "--timestamp=$timestamp");
|
|
return $ok ? $stdout : "ERROR: $stderr";
|
|
}
|
|
|
|
sub LIST_FILES {
|
|
my ($self, $remote, $timestamp, $path) = @_;
|
|
my $user = $ENV{'REMOTE_USER'} // '';
|
|
|
|
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
|
|
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
|
|
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
|
|
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
|
|
|
|
my @opts = ("--remote=$remote", "--timestamp=$timestamp");
|
|
if (defined $path && $path ne '') {
|
|
return "ERROR: Invalid path" unless $path =~ $OPT_PATTERNS{path};
|
|
push @opts, "--path=$path";
|
|
}
|
|
|
|
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'list-files', $user, @opts);
|
|
return $ok ? $stdout : "ERROR: $stderr";
|
|
}
|
|
|
|
sub LIST_DBUSERS {
|
|
my ($self, $remote, $timestamp) = @_;
|
|
my $user = $ENV{'REMOTE_USER'} // '';
|
|
|
|
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
|
|
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
|
|
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
|
|
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
|
|
|
|
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'list-dbusers', $user,
|
|
"--remote=$remote", "--timestamp=$timestamp");
|
|
return $ok ? $stdout : "ERROR: $stderr";
|
|
}
|
|
|
|
sub LIST_CRON {
|
|
my ($self, $remote, $timestamp) = @_;
|
|
my $user = $ENV{'REMOTE_USER'} // '';
|
|
|
|
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
|
|
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
|
|
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
|
|
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
|
|
|
|
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'list-cron', $user,
|
|
"--remote=$remote", "--timestamp=$timestamp");
|
|
return $ok ? $stdout : "ERROR: $stderr";
|
|
}
|
|
|
|
sub LIST_DNS {
|
|
my ($self, $remote, $timestamp) = @_;
|
|
my $user = $ENV{'REMOTE_USER'} // '';
|
|
|
|
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
|
|
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
|
|
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
|
|
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
|
|
|
|
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'list-dns', $user,
|
|
"--remote=$remote", "--timestamp=$timestamp");
|
|
return $ok ? $stdout : "ERROR: $stderr";
|
|
}
|
|
|
|
sub LIST_SSL {
|
|
my ($self, $remote, $timestamp) = @_;
|
|
my $user = $ENV{'REMOTE_USER'} // '';
|
|
|
|
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
|
|
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
|
|
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
|
|
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
|
|
|
|
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'list-ssl', $user,
|
|
"--remote=$remote", "--timestamp=$timestamp");
|
|
return $ok ? $stdout : "ERROR: $stderr";
|
|
}
|
|
|
|
sub RESTORE_ACCOUNT {
|
|
my ($self, $remote, $timestamp, $exclude) = @_;
|
|
my $user = $ENV{'REMOTE_USER'} // '';
|
|
|
|
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
|
|
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
|
|
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
|
|
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
|
|
|
|
my @opts = ("--remote=$remote", "--timestamp=$timestamp");
|
|
# NOTE: --terminate is NEVER passed for user restore
|
|
if (defined $exclude && $exclude ne '') {
|
|
return "ERROR: Invalid exclude" unless $exclude =~ $OPT_PATTERNS{exclude};
|
|
push @opts, "--exclude=$exclude";
|
|
}
|
|
|
|
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'account', $user, @opts);
|
|
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
|
|
}
|
|
|
|
sub RESTORE_FILES {
|
|
my ($self, $remote, $timestamp, $path, $exclude) = @_;
|
|
my $user = $ENV{'REMOTE_USER'} // '';
|
|
|
|
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
|
|
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
|
|
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
|
|
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
|
|
|
|
my @opts = ("--remote=$remote", "--timestamp=$timestamp");
|
|
if (defined $path && $path ne '') {
|
|
return "ERROR: Invalid path" unless $path =~ $OPT_PATTERNS{path};
|
|
push @opts, "--path=$path";
|
|
}
|
|
if (defined $exclude && $exclude ne '') {
|
|
return "ERROR: Invalid exclude" unless $exclude =~ $OPT_PATTERNS{exclude};
|
|
push @opts, "--exclude=$exclude";
|
|
}
|
|
|
|
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'files', $user, @opts);
|
|
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
|
|
}
|
|
|
|
sub RESTORE_DATABASE {
|
|
my ($self, $remote, $timestamp, $dbname) = @_;
|
|
my $user = $ENV{'REMOTE_USER'} // '';
|
|
|
|
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
|
|
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
|
|
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
|
|
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
|
|
|
|
my @args = ($user);
|
|
if (defined $dbname && $dbname ne '') {
|
|
return "ERROR: Invalid database name" unless $dbname =~ $DBNAME_RE;
|
|
push @args, $dbname;
|
|
}
|
|
|
|
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'database', @args,
|
|
"--remote=$remote", "--timestamp=$timestamp");
|
|
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
|
|
}
|
|
|
|
sub RESTORE_MAILBOX {
|
|
my ($self, $remote, $timestamp, $email) = @_;
|
|
my $user = $ENV{'REMOTE_USER'} // '';
|
|
|
|
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
|
|
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
|
|
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
|
|
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
|
|
|
|
my @args = ($user);
|
|
if (defined $email && $email ne '') {
|
|
return "ERROR: Invalid email" unless $email =~ $EMAIL_RE;
|
|
push @args, $email;
|
|
}
|
|
|
|
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'mailbox', @args,
|
|
"--remote=$remote", "--timestamp=$timestamp");
|
|
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
|
|
}
|
|
|
|
sub RESTORE_CRON {
|
|
my ($self, $remote, $timestamp) = @_;
|
|
my $user = $ENV{'REMOTE_USER'} // '';
|
|
|
|
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
|
|
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
|
|
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
|
|
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
|
|
|
|
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'cron', $user,
|
|
"--remote=$remote", "--timestamp=$timestamp");
|
|
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
|
|
}
|
|
|
|
sub RESTORE_DBUSERS {
|
|
my ($self, $remote, $timestamp, $dbuser) = @_;
|
|
my $user = $ENV{'REMOTE_USER'} // '';
|
|
|
|
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
|
|
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
|
|
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
|
|
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
|
|
|
|
my @args = ($user);
|
|
if (defined $dbuser && $dbuser ne '') {
|
|
return "ERROR: Invalid database user" unless $dbuser =~ $DBNAME_RE;
|
|
push @args, $dbuser;
|
|
}
|
|
|
|
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'dbusers', @args,
|
|
"--remote=$remote", "--timestamp=$timestamp");
|
|
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
|
|
}
|
|
|
|
sub RESTORE_DOMAINS {
|
|
my ($self, $remote, $timestamp, $domain) = @_;
|
|
my $user = $ENV{'REMOTE_USER'} // '';
|
|
|
|
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
|
|
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
|
|
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
|
|
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
|
|
|
|
my @args = ($user);
|
|
if (defined $domain && $domain ne '') {
|
|
return "ERROR: Invalid domain" unless $domain =~ $DOMAIN_RE;
|
|
push @args, $domain;
|
|
}
|
|
|
|
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'domains', @args,
|
|
"--remote=$remote", "--timestamp=$timestamp");
|
|
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
|
|
}
|
|
|
|
sub RESTORE_SSL {
|
|
my ($self, $remote, $timestamp, $domain) = @_;
|
|
my $user = $ENV{'REMOTE_USER'} // '';
|
|
|
|
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
|
|
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
|
|
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
|
|
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
|
|
|
|
my @args = ($user);
|
|
if (defined $domain && $domain ne '') {
|
|
return "ERROR: Invalid domain" unless $domain =~ $DOMAIN_RE;
|
|
push @args, $domain;
|
|
}
|
|
|
|
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'ssl', @args,
|
|
"--remote=$remote", "--timestamp=$timestamp");
|
|
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
|
|
}
|
|
|
|
__PACKAGE__->run() if !caller;
|
|
|
|
1;
|