shell bypass 403
package Test::mysqld;
use strict;
use warnings;
use 5.008_001;
use Class::Accessor::Lite;
use Cwd;
use DBI;
use File::Copy::Recursive qw(dircopy);
use File::Temp qw(tempdir);
use POSIX qw(SIGTERM WNOHANG);
use Time::HiRes qw(sleep);
our $VERSION = '1.0013';
our $errstr;
our @SEARCH_PATHS = qw(/usr/local/mysql);
my %Defaults = (
auto_start => 2,
base_dir => undef,
my_cnf => {},
mysqld => undef,
use_mysqld_initialize => undef,
mysql_install_db => undef,
pid => undef,
copy_data_from => undef,
_owner_pid => undef,
);
Class::Accessor::Lite->mk_accessors(keys %Defaults);
sub new {
my $klass = shift;
my $self = bless {
%Defaults,
@_ == 1 ? %{$_[0]} : @_,
_owner_pid => $$,
}, $klass;
$self->my_cnf({
%{$self->my_cnf},
});
if (defined $self->base_dir) {
$self->base_dir(cwd . '/' . $self->base_dir)
if $self->base_dir !~ m|^/|;
} else {
$self->base_dir(
tempdir(
CLEANUP => $ENV{TEST_MYSQLD_PRESERVE} ? undef : 1,
),
);
}
$self->my_cnf->{socket} ||= $self->base_dir . "/tmp/mysql.sock";
$self->my_cnf->{datadir} ||= $self->base_dir . "/var";
$self->my_cnf->{'pid-file'} ||= $self->base_dir . "/tmp/mysqld.pid";
$self->my_cnf->{tmpdir} ||= $self->base_dir . "/tmp";
if (! defined $self->mysqld) {
my $prog = _find_program(qw/mysqld bin libexec sbin/)
or return;
$self->mysqld($prog);
}
if (! defined $self->use_mysqld_initialize) {
$self->use_mysqld_initialize($self->_use_mysqld_initialize);
}
if ($self->auto_start) {
die 'mysqld is already running (' . $self->my_cnf->{'pid-file'} . ')'
if -e $self->my_cnf->{'pid-file'};
$self->setup
if $self->auto_start >= 2;
$self->start;
}
$self;
}
sub DESTROY {
my $self = shift;
$self->stop
if defined $self->pid && $$ == $self->_owner_pid;
}
sub dsn {
my ($self, %args) = @_;
$args{port} ||= $self->my_cnf->{port}
if $self->my_cnf->{port};
if (defined $args{port}) {
$args{host} ||= $self->my_cnf->{'bind-address'} || '127.0.0.1';
$args{user} ||= 'root';
if ($self->_use_unix_socket_auth) {
# TODO: Set a password,
# <https://mariadb.com/kb/en/authentication-from-mariadb-104/>
}
} else {
$args{mysql_socket} ||= $self->my_cnf->{socket};
if (!$self->_use_unix_socket_auth) {
$args{user} ||= 'root';
}
}
$args{dbname} ||= 'test';
return 'DBI:mysql:' . join(';', map { "$_=$args{$_}" } sort keys %args);
}
sub start {
my $self = shift;
return
if defined $self->pid;
$self->spawn;
$self->wait_for_setup;
}
sub spawn {
my $self = shift;
return
if defined $self->pid;
open my $logfh, '>>', $self->base_dir . '/tmp/mysqld.log'
or die 'failed to create log file:' . $self->base_dir
. "/tmp/mysqld.log:$!";
my $pid = fork;
die "fork(2) failed:$!"
unless defined $pid;
if ($pid == 0) {
open STDOUT, '>&', $logfh
or die "dup(2) failed:$!";
open STDERR, '>&', $logfh
or die "dup(2) failed:$!";
exec(
$self->mysqld,
'--defaults-file=' . $self->base_dir . '/etc/my.cnf',
'--user=root',
);
die "failed to launch mysqld:$?";
}
close $logfh;
$self->pid($pid);
}
sub wait_for_setup {
my $self = shift;
return
unless defined $self->pid;
my $pid = $self->pid;
while (! -e $self->my_cnf->{'pid-file'}) {
if (waitpid($pid, WNOHANG) > 0) {
die "*** failed to launch mysqld ***\n" . $self->read_log;
}
sleep 0.1;
}
unless ($self->copy_data_from) { # create 'test' database
my $dbh = DBI->connect($self->dsn(dbname => 'mysql'))
or die $DBI::errstr;
$dbh->do('CREATE DATABASE IF NOT EXISTS test')
or die $dbh->errstr;
}
}
sub stop {
my ($self, $sig) = @_;
return
unless defined $self->pid;
$sig ||= SIGTERM;
$self->send_stop_signal($sig);
$self->wait_for_stop;
}
sub send_stop_signal {
my ($self, $sig) = @_;
return
unless defined $self->pid;
$sig ||= SIGTERM;
kill $sig, $self->pid;
}
sub wait_for_stop {
my $self = shift;
local $?; # waitpid may change this value :/
while (waitpid($self->pid, 0) <= 0) {
}
$self->pid(undef);
# might remain for example when sending SIGKILL
unlink $self->my_cnf->{'pid-file'};
}
sub setup {
my $self = shift;
# (re)create directory structure
mkdir $self->base_dir;
for my $subdir (qw/etc var tmp/) {
mkdir $self->base_dir . "/$subdir";
}
# copy the data before setup db for quick bootstrap.
if ($self->copy_data_from) {
dircopy($self->copy_data_from, $self->my_cnf->{datadir})
or die "could not dircopy @{[$self->copy_data_from]} to " .
"@{[$self->my_cnf->{datadir}]}:$!";
if (!$self->_is_maria && ($self->_mysql_major_version || 0) >= 8) {
my $mysql_db_dir = $self->my_cnf->{datadir} . '/mysql';
if (! -d $mysql_db_dir) {
mkdir $mysql_db_dir or die "failed to mkdir $mysql_db_dir: $!";
}
}
}
# my.cnf
open my $fh, '>', $self->base_dir . '/etc/my.cnf'
or die "failed to create file:" . $self->base_dir . "/etc/my.cnf:$!";
print $fh "[mysqld]\n";
print $fh map {
my $v = $self->my_cnf->{$_};
defined $v && length $v
? "$_=$v" . "\n"
: "$_\n";
} sort keys %{$self->my_cnf};
close $fh;
# mysql_install_db
if (! -d $self->base_dir . '/var/mysql') {
my $cmd = $self->use_mysqld_initialize ? $self->mysqld : do {
if (! defined $self->mysql_install_db) {
my $prog = _find_program(qw/mysql_install_db bin scripts/)
or die 'failed to find mysql_install_db';
$self->mysql_install_db($prog);
}
$self->mysql_install_db;
};
# We should specify --defaults-file option first.
$cmd .= " --defaults-file='" . $self->base_dir . "/etc/my.cnf'";
if ($self->use_mysqld_initialize) {
$cmd .= ' --initialize-insecure';
if ($self->copy_data_from &&
!(!$self->_is_maria && ($self->_mysql_major_version || 0) >= 8)
) {
opendir my $dh, $self->copy_data_from
or die "failed to open copy_data_from directory @{[$self->copy_data_from]}: $!";
while (my $entry = readdir $dh) {
next unless -d $self->copy_data_from . "/$entry";
next if $entry =~ /^\.\.?$/;
$cmd .= " --ignore-db-dir=$entry"
}
}
} else {
# `abs_path` resolves nested symlinks and returns canonical absolute path
my $mysql_base_dir = Cwd::abs_path($self->mysql_install_db);
if ($mysql_base_dir =~ s{/(?:bin|extra|scripts)/mysql_install_db$}{}) {
$cmd .= " --basedir='$mysql_base_dir'";
}
}
$cmd .= " 2>&1";
# The MySQL scripts are in Perl, so clear out all current Perl
# related environment variables before the call
local @ENV{ grep { /^PERL/ } keys %ENV };
open $fh, '-|', $cmd
or die "failed to spawn mysql_install_db:$!";
my $output;
while (my $l = <$fh>) {
$output .= $l;
}
close $fh
or die "*** mysql_install_db failed ***\n% $cmd\n$output\n";
}
}
sub read_log {
my $self = shift;
open my $logfh, '<', $self->base_dir . '/tmp/mysqld.log'
or die "failed to open file:tmp/mysql.log:$!";
do { local $/; <$logfh> };
}
sub _find_program {
my ($prog, @subdirs) = @_;
undef $errstr;
my $path = _get_path_of($prog);
return $path
if $path;
for my $mysql (_get_path_of('mysql'),
map { "$_/bin/mysql" } @SEARCH_PATHS) {
if (-x $mysql) {
for my $subdir (@subdirs) {
$path = $mysql;
if ($path =~ s|/bin/mysql$|/$subdir/$prog|
and -x $path) {
return $path;
}
}
}
}
$errstr = "could not find $prog, please set appropriate PATH";
return;
}
sub _verbose_help {
my $self = shift;
$self->{_verbose_help} ||= `@{[$self->mysqld]} --verbose --help 2>/dev/null`;
}
# Detecting if the mysqld supports `--initialize-insecure` option or not from the
# output of `mysqld --help --verbose`.
# `mysql_install_db` command is obsoleted MySQL 5.7.6 or later and
# `mysqld --initialize-insecure` should be used.
sub _use_mysqld_initialize {
shift->_verbose_help =~ /--initialize-insecure/ms;
}
sub _is_maria {
my $self = shift;
unless (exists $self->{_is_maria}) {
$self->{_is_maria} = $self->_verbose_help =~ /\A.*MariaDB/;
}
$self->{_is_maria};
}
sub _mysql_version {
my $self = shift;
unless (exists $self->{_mysql_version}) {
($self->{_mysql_version})
= $self->_verbose_help =~ /\A.*Ver ([0-9]+\.[0-9]+\.[0-9]+)/;
}
$self->{_mysql_version};
}
# https://mariadb.com/kb/en/authentication-plugin-unix-socket/
sub _use_unix_socket_auth {
my $self = shift;
$self->{_use_unix_socket_auth} = 0;
if ($self->_is_maria && defined $self->_mysql_version()) {
my ($x, $y, $z) = $self->_mysql_version() =~ /([0-9]+)\.([0-9]+)\.([0-9]+)/;
$self->{_use_unix_socket_auth} = ($x > 10 || ($x == 10 && $y > 4)
|| ($x == 10 && $y == 4 && $z >= 3));
}
$self->{_use_unix_socket_auth};
}
sub _mysql_major_version {
my $ver = shift->_mysql_version;
return unless $ver;
+(split /\./, $ver)[0];
}
sub _get_path_of {
my $prog = shift;
my $path = `which $prog 2> /dev/null`;
chomp $path
if $path;
$path = ''
unless -x $path;
$path;
}
sub start_mysqlds {
my $class = shift;
my $number = shift;
my @args = @_;
my @mysqlds = map { Test::mysqld->new(@args, auto_start => 0) } (1..$number);
for my $mysqld (@mysqlds) {
$mysqld->setup;
$mysqld->spawn;
}
for my $mysqld (@mysqlds) {
$mysqld->wait_for_setup;
}
return @mysqlds;
}
sub stop_mysqlds {
my $class = shift;
my @mysqlds = @_;
for my $mysqld (@mysqlds) {
$mysqld->send_stop_signal;
}
for my $mysqld (@mysqlds) {
$mysqld->wait_for_stop;
}
return @mysqlds;
}
"lestrrat-san he";
__END__
=encoding utf-8
=for stopwords DESTROYed mysqld dsn dbname
=head1 NAME
Test::mysqld - mysqld runner for tests
=head1 SYNOPSIS
use DBI;
use Test::mysqld;
use Test::More;
my $mysqld = Test::mysqld->new(
my_cnf => {
'skip-networking' => '', # no TCP socket
}
) or plan skip_all => $Test::mysqld::errstr;
plan tests => XXX;
my $dbh = DBI->connect(
$mysqld->dsn(dbname => 'test'),
);
# start_mysqlds is faster than calling Test::mysqld->new twice
my @mysqlds = Test::mysqld->start_mysqlds(
2,
my_cnf => {
'skip-networking' => '', # no TCP socket
}
) or plan skip_all => $Test::mysqld::errstr;
Test::mysqlds->stop_mysqlds(@mysqlds);
=head1 DESCRIPTION
C<Test::mysqld> automatically setups a mysqld instance in a temporary directory, and destroys it when the perl script exits.
=head1 FUNCTIONS
=head2 new
Create and run a mysqld instance. The instance is terminated when the returned object is being DESTROYed. If required programs (mysql_install_db and mysqld) were not found, the function returns undef and sets appropriate message to $Test::mysqld::errstr.
=head2 base_dir
Returns directory under which the mysqld instance is being created. The property can be set as a parameter of the C<new> function, in which case the directory will not be removed at exit.
=head2 copy_data_from
If specified, uses a copy of the specified directory as the data directory of MySQL. "Mysql" database (which is used to store administrative information) is automatically created if necessary by invoking mysql_install_db.
=head2 my_cnf
A hash containing the list of name=value pairs to be written into my.cnf. The property can be set as a parameter of the C<new> function.
=head2 mysql_install_db
=head2 mysqld
Path to C<mysql_install_db> script or C<mysqld> program bundled to the mysqld distribution. If not set, the program is automatically search by looking up $PATH and other prefixed directories.
=head2 dsn
Builds and returns dsn by using given parameters (if any). Default username depends on a server version and a socket family. Default dbname is 'test'.
=head2 pid
Returns process id of mysqld (or undef if not running).
=head2 start
Starts mysqld.
=head2 stop
Stops mysqld.
=head2 setup
Setups the mysqld instance.
=head2 read_log
Returns the contents of the mysqld log file.
=head2 start_mysqlds
Create and run some mysqld instances, and return a list of C<Test::mysqld>.
=head2 stop_mysqlds
Stop some mysqld instances.
=head1 COPYRIGHT
Copyright (C) 2009 Cybozu Labs, Inc. Written by Kazuho Oku.
=head1 LICENSE
This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
See L<http://www.perl.com/perl/misc/Artistic.html>
=cut