mirror of
https://github.com/ovh/the-bastion.git
synced 2025-01-08 00:12:10 +08:00
a1812e34bb
Fixes #363
137 lines
5 KiB
Perl
137 lines
5 KiB
Perl
package OVH::Bastion::Helper;
|
|
|
|
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
|
|
use common::sense;
|
|
|
|
use Fcntl qw{ :flock :mode };
|
|
use Time::HiRes qw{ usleep };
|
|
|
|
use File::Basename;
|
|
use lib dirname(__FILE__) . '/../../../../lib/perl';
|
|
use OVH::Bastion;
|
|
use OVH::Result;
|
|
|
|
# We handle our importer's '$self' var, this is by design.
|
|
use Exporter 'import';
|
|
our $self; ## no critic (ProhibitPackageVars)
|
|
our @EXPORT = qw( $self HEXIT ); ## no critic (ProhibitAutomaticExportation)
|
|
|
|
# HEXIT aka "helper exit", used by helper scripts found in helpers/
|
|
# Can be used in several ways:
|
|
# With an R object: HEXIT(R('OK', value => {}, msg => "okey"))
|
|
# Or with 1 value, that will be taken as the R->err: HEXIT('OK')
|
|
# Or with 2 values, that will be taken as err, msg: HEXIT('ERR_UNKNOWN', 'Unexpected error')
|
|
# With more values, they'll be used as constructor for an R object
|
|
sub HEXIT { ## no critic (ArgUnpacking)
|
|
my $R;
|
|
|
|
if (@_ == 1) {
|
|
$R = ref $_[0] eq 'OVH::Result' ? $_[0] : R($_[0]);
|
|
}
|
|
elsif (@_ == 2) {
|
|
my $err = shift || 'OK';
|
|
my $msg = shift;
|
|
$R = R($err, msg => $msg);
|
|
}
|
|
else {
|
|
$R = R(@_);
|
|
}
|
|
OVH::Bastion::json_output($R, force_default => 1);
|
|
exit 0;
|
|
}
|
|
|
|
# Used after Getopt::Long::GetOptions() in each helper, to ensure there are no unparsed/spurious args
|
|
sub check_spurious_args {
|
|
if (@ARGV) {
|
|
local $" = ", ";
|
|
warn_syslog("Spurious arguments on command line: @ARGV");
|
|
HEXIT('ERR_BAD_OPTIONS', msg => "Spurious arguments on command line: @ARGV");
|
|
}
|
|
}
|
|
|
|
#
|
|
# This code has to be ran for all helpers before they attempt to do anything useful,
|
|
# and as we're only use'd by helpers, we include it here directly on top-level.
|
|
#
|
|
|
|
$| = 1;
|
|
|
|
# Don't let helpers be interrupted too easily
|
|
$SIG{'HUP'} = 'IGNORE'; # continue even when attached terminal is closed (we're called with setsid on supported systems anyway)
|
|
$SIG{'PIPE'} = 'IGNORE'; # continue even if osh_info gets a SIGPIPE because there's no longer a terminal
|
|
|
|
# Ensure the PATH is not tainted, and has sane values
|
|
$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:/usr/pkg/bin';
|
|
|
|
# Build $self from SUDO_USER, as helpers are always run under sudo
|
|
($self) = $ENV{'SUDO_USER'} =~ m{^([a-zA-Z0-9._-]+)$};
|
|
if (not defined $self) {
|
|
if ($< == 0) {
|
|
$self = 'root';
|
|
}
|
|
else {
|
|
HEXIT('ERR_SUDO_NEEDED', msg => 'This command must be run under sudo');
|
|
}
|
|
}
|
|
|
|
sub get_lock_fh {
|
|
my $fh;
|
|
my $lockdir = "/tmp/bastion.lock";
|
|
my $lockfile = "$lockdir/lock";
|
|
|
|
# to avoid symlink attacks, we first create a subdir only accessible by root
|
|
unlink $lockdir; # will silently fail if doesn't exist or is not a file
|
|
mkdir $lockdir; # will silently fail if we lost the race
|
|
chown 0, 0, $lockdir;
|
|
chmod 0700, $lockdir;
|
|
|
|
# now, check if we do have a directory, or if we lost the race
|
|
if (!-d $lockdir) {
|
|
warn_syslog("Couldn't create $lockdir: are we being raced against?");
|
|
return R('ERR_CANNOT_LOCK', msg => "Couldn't create lock file, please retry");
|
|
}
|
|
# here, $lockdir is guaranteed to be a directory, check its perms
|
|
my @perms = stat($lockdir);
|
|
if ($perms[4] != 0 || $perms[5] != 0 || S_IMODE($perms[2]) != oct(700)) {
|
|
warn_syslog("The $lockdir directory has invalid perms: are we being raced against? mode="
|
|
. sprintf("%04o", S_IMODE($perms[2])));
|
|
return R('ERR_CANNOT_LOCK', msg => "Couldn't create lock file, please retry");
|
|
}
|
|
|
|
# here, $lockdir is guaranteed to be owned only by us. but rogue files
|
|
# might have appeared in it after the mkdir and before the chown/chmod,
|
|
# so check for the lockfile existence. if it does exist, it must be a normal
|
|
# file and not a symlink or any other file type. Note that we don't have
|
|
# a TOCTTOU problem here because no rogue user can no longer create files
|
|
# in $lockdir, as we checked just above.
|
|
if (-l $lockfile || -e !-f $lockfile) {
|
|
warn_syslog("The $lockfile file exists but is not a file, unlinking it and bailing out");
|
|
unlink($lockfile);
|
|
# don't give too much info to the caller
|
|
return R('ERR_CANNOT_LOCK', msg => "Couldn't create lock file, please retry");
|
|
}
|
|
|
|
if (!open($fh, '>>', $lockfile)) {
|
|
return R('ERR_CANNOT_LOCK', msg => "Couldn't create lock file, please retry");
|
|
}
|
|
return R('OK', value => $fh);
|
|
}
|
|
|
|
sub acquire_lock {
|
|
my $fh = shift;
|
|
return R('ERR_INVALID_PARAMETER', msg => "Invalid filehandle") if !$fh;
|
|
|
|
# try to lock for at most 60 seconds
|
|
my $limit = time() + 60;
|
|
my $locked;
|
|
my $first = 1;
|
|
while (!($locked = flock($fh, LOCK_EX | LOCK_NB)) && time() < $limit) {
|
|
usleep(rand(200_000) + 100_000); # sleep for 100-300ms
|
|
OVH::Bastion::osh_info("Acquiring lock, this may take a few seconds...") if $first;
|
|
$first = 0;
|
|
}
|
|
return R('OK') if $locked;
|
|
return R('KO_LOCK_FAILED', msg => "Couldn't acquire lock in a reasonable amount of time, please retry later");
|
|
}
|
|
|
|
1;
|