Files
gniza4cp/whm/gniza-whm/lib/GnizaWHM/Runner.pm
shuki 1459bd1b8b Initial commit: gniza backup & disaster recovery CLI + WHM plugin
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>
2026-03-04 02:39:39 +02:00

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;