#!/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;