Full-featured cPanel backup tool with SSH, S3, and Google Drive support. Includes WHM plugin with Tailwind/DaisyUI UI, multi-remote management, decoupled schedules, and account restore workflows. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
128 lines
4.6 KiB
Perl
128 lines
4.6 KiB
Perl
package GnizaWHM::Runner;
|
|
# Pattern-based command runner for gniza CLI.
|
|
# Each allowed command has a regex per argument position for safe execution.
|
|
|
|
use strict;
|
|
use warnings;
|
|
use IPC::Open3;
|
|
use Symbol 'gensym';
|
|
|
|
my $GNIZA_BIN = '/usr/local/bin/gniza';
|
|
|
|
# Allowed command patterns.
|
|
# Each entry: [ subcommand, arg_patterns... ]
|
|
# arg_patterns are regexes applied to each positional argument.
|
|
my @ALLOWED = (
|
|
# restore subcommands
|
|
{ cmd => 'restore', subcmd => 'account', args => [qr/^[a-z][a-z0-9_-]*$/] },
|
|
{ cmd => 'restore', subcmd => 'files', args => [qr/^[a-z][a-z0-9_-]*$/] },
|
|
{ cmd => 'restore', subcmd => 'database', args => [qr/^[a-z][a-z0-9_-]*$/] },
|
|
{ cmd => 'restore', subcmd => 'database', args => [qr/^[a-z][a-z0-9_-]*$/, qr/^[a-zA-Z0-9_]+$/] },
|
|
{ cmd => 'restore', subcmd => 'mailbox', args => [qr/^[a-z][a-z0-9_-]*$/] },
|
|
{ cmd => 'restore', subcmd => 'mailbox', args => [qr/^[a-z][a-z0-9_-]*$/, qr/^[a-zA-Z0-9._+-]+\@[a-zA-Z0-9._-]+$/] },
|
|
{ cmd => 'restore', subcmd => 'cron', args => [qr/^[a-z][a-z0-9_-]*$/] },
|
|
{ cmd => 'restore', subcmd => 'dbusers', args => [qr/^[a-z][a-z0-9_-]*$/] },
|
|
{ cmd => 'restore', subcmd => 'dbusers', args => [qr/^[a-z][a-z0-9_-]*$/, qr/^[a-zA-Z0-9_]+$/] },
|
|
{ cmd => 'restore', subcmd => 'cpconfig', args => [qr/^[a-z][a-z0-9_-]*$/] },
|
|
{ cmd => 'restore', subcmd => 'domains', args => [qr/^[a-z][a-z0-9_-]*$/] },
|
|
{ cmd => 'restore', subcmd => 'domains', args => [qr/^[a-z][a-z0-9_-]*$/, qr/^[a-zA-Z0-9._-]+$/] },
|
|
{ cmd => 'restore', subcmd => 'ssl', args => [qr/^[a-z][a-z0-9_-]*$/] },
|
|
{ cmd => 'restore', subcmd => 'ssl', args => [qr/^[a-z][a-z0-9_-]*$/, qr/^[a-zA-Z0-9._-]+$/] },
|
|
{ cmd => 'restore', subcmd => 'list-databases', args => [qr/^[a-z][a-z0-9_-]*$/] },
|
|
{ cmd => 'restore', subcmd => 'list-mailboxes', args => [qr/^[a-z][a-z0-9_-]*$/] },
|
|
{ cmd => 'restore', subcmd => 'list-files', args => [qr/^[a-z][a-z0-9_-]*$/] },
|
|
{ cmd => 'restore', subcmd => 'list-dbusers', args => [qr/^[a-z][a-z0-9_-]*$/] },
|
|
{ cmd => 'restore', subcmd => 'list-cron', args => [qr/^[a-z][a-z0-9_-]*$/] },
|
|
{ cmd => 'restore', subcmd => 'list-dns', args => [qr/^[a-z][a-z0-9_-]*$/] },
|
|
{ cmd => 'restore', subcmd => 'list-ssl', args => [qr/^[a-z][a-z0-9_-]*$/] },
|
|
# list
|
|
{ cmd => 'list', subcmd => undef, args => [] },
|
|
);
|
|
|
|
# Named option patterns (--key=value).
|
|
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_.\/@ -]+$/,
|
|
account => qr/^[a-z][a-z0-9_-]*$/,
|
|
);
|
|
|
|
# run($cmd, $subcmd, \@args, \%opts)
|
|
# Returns ($success, $stdout, $stderr).
|
|
sub run {
|
|
my ($cmd, $subcmd, $args, $opts) = @_;
|
|
$args //= [];
|
|
$opts //= {};
|
|
|
|
# Find matching allowed pattern
|
|
my $matched;
|
|
for my $pattern (@ALLOWED) {
|
|
next unless $pattern->{cmd} eq $cmd;
|
|
if (defined $pattern->{subcmd}) {
|
|
next unless defined $subcmd && $subcmd eq $pattern->{subcmd};
|
|
} else {
|
|
next if defined $subcmd;
|
|
}
|
|
# Check arg count
|
|
next unless scalar(@$args) == scalar(@{$pattern->{args}});
|
|
# Validate each arg
|
|
my $ok = 1;
|
|
for my $i (0 .. $#$args) {
|
|
unless ($args->[$i] =~ $pattern->{args}[$i]) {
|
|
$ok = 0;
|
|
last;
|
|
}
|
|
}
|
|
if ($ok) {
|
|
$matched = $pattern;
|
|
last;
|
|
}
|
|
}
|
|
|
|
unless ($matched) {
|
|
my $desc = "gniza $cmd" . (defined $subcmd ? " $subcmd" : "") . " " . join(" ", @$args);
|
|
return (0, '', "Command not allowed: $desc");
|
|
}
|
|
|
|
# Validate options
|
|
for my $key (keys %$opts) {
|
|
my $pat = $OPT_PATTERNS{$key};
|
|
unless ($pat) {
|
|
return (0, '', "Unknown option: --$key");
|
|
}
|
|
unless ($opts->{$key} =~ $pat) {
|
|
return (0, '', "Invalid value for --$key: $opts->{$key}");
|
|
}
|
|
}
|
|
|
|
# Build command
|
|
my @exec_args = ($cmd);
|
|
push @exec_args, $subcmd if defined $subcmd;
|
|
push @exec_args, @$args;
|
|
for my $key (sort keys %$opts) {
|
|
push @exec_args, "--$key=$opts->{$key}";
|
|
}
|
|
|
|
return _exec(@exec_args);
|
|
}
|
|
|
|
sub _exec {
|
|
my (@args) = @_;
|
|
|
|
my $err = gensym;
|
|
my $pid = open3(my $in, my $out, $err, $GNIZA_BIN, @args);
|
|
close $in;
|
|
|
|
my $stdout = do { local $/; <$out> } // '';
|
|
my $stderr = do { local $/; <$err> } // '';
|
|
close $out;
|
|
close $err;
|
|
|
|
waitpid($pid, 0);
|
|
my $exit_code = $? >> 8;
|
|
|
|
return ($exit_code == 0, $stdout, $stderr);
|
|
}
|
|
|
|
1;
|