App-CamelPKI
view release on metacpan or search on metacpan
lib/App/CamelPKI/SysV/Apache.pm view on Meta::CPAN
which only serves for Camel-PKI's self-tests (unit and integration).
The value of I<test_php_directory> is persisted to disk, so that it need
not be reset at each construction. It only takes effect the next time
the server is restarted with L</start>.
=head2 has_camel_pki()
=head2 has_camel_pki($boolean)
Gets or sets the "has App-PKI" flag, which defaults to true.
Instances of I<App::CamelPKI::SysV::Apache> that have I<has_camel_pki()> set
to false do not contain the Camel-PKI application. Again, this is only
useful for tests.
The value of I<has_camel_pki> is persisted to disk, so that it need not
be reset at each construction. It only takes effect the next time the
server is restarted with L</start>.
=cut
{
my %defaults =
(https_port =>
(IO::Socket::INET->new(LocalPort => 443, ReuseAddr => 1) ?
443 : 3443),
test_php_directory => undef,
has_camel_pki => 1);
foreach my $persistent_field (keys %defaults) {
my $getsetter = sub {
my ($self, @set) = @_;
if (@set) {
($self->{$persistent_field}) = @set;
$self->_write_config_file(); # Persist
}
unless (exists($self->{$persistent_field})) {
$self->{$persistent_field} = $defaults{$persistent_field};
}
return $self->{$persistent_field};
};
no strict "refs"; *{$persistent_field} = $getsetter;
}
}
=head2 set_keys(-certificate => $cert, -key => $key,
-certification_chain => \@chain)
Installs key material that will allow this Apache daemon to
authenticate itself to its HTTP/S clients ($cert and $key, which must
be instances of L<App::CamelPKI::Certificate> and L<App::CamelPKI::PrivateKey>
respectively), and also to verify the identity of HTTP/S clients that
themselves use a certificate (@chain, which is a list of instances of
L<App::CamelPKI::Certificate>; see also L</update_crl>). If $cert is a
self-signed certificate, C<-certification_chain> and its parameter
\@chain may be omitted.
=cut
sub set_keys {
throw App::CamelPKI::Error::Internal("WRONG_NUMBER_ARGS")
unless (@_ % 2);
my ($self, %keys) = @_;
while(my ($k, $v) = each %keys) {
if ($k eq "-certificate") {
write_file($self->_certificate_filename, $v->serialize());
} elsif ($k eq "-key") {
write_file($self->_key_filename, $v->serialize());
} elsif ($k eq "-certification_chain") {
write_file($self->_ca_bundle_filename,
join("", map { $_->serialize } @$v));
} else {
throw App::CamelPKI::Error::Internal
("INCORRECT_ARGS",
-details => "Unknown named option $k");
}
}
}
=head2 is_operational()
Returns true if and only if the ad-hoc cryptographic material has been
added to this Web server using L</set_keys>.
=cut
# The above POD is ambiguous on purpose: ->is_operational may someday
# return true even if there is no CA chain available.
sub is_operational {
my ($self) = @_;
-r $self->_key_filename && -r $self->_certificate_filename &&
-r $self->_ca_bundle_filename;
}
=head2 certificate()
Returns the Web server's SSL certificate, as an instance of
L<App::CamelPKI::Certificate>.
=cut
sub certificate {
App::CamelPKI::Certificate->load(shift->_certificate_filename);
}
=head2 update_crl($crl)
Given $crl, an instance of L<App::CamelPKI::CRL>, verifies the signature
thereof and stores it into this Apache server if and only if it
matches one of the CAs previously installed using L</set_keys>'
C<-certificate_chain> named option, B<and> $crl is older than any CRL
previously added with I<update_crl()>. If these security checks are
successful and Apache is already running, it will be restarted so as
to take the new CRL into account immediately.
Note that a Web server works perfectly without a CRL, and therefore
calling I<update_crl> is optional. However, remember that CRLs have
expiration dates: once a CRL has been installed using this method, one
should plan for a suitable mechanism (e.g. a crontab entry) that will
download updated CRLs on a regular basis and submit them using
I<update_crl()>.
=cut
sub update_crl { "UNIMPLEMENTED" }
=head2 start(%opts)
Starts the daemon synchronously, meaning that I<start> will only
return control to its caller after ensuring that the Apache process
wrote its PID file and bound to its TCP port. I<start()> is
idempotent, and terminates immediately if the serveur is already up.
An L<App::CamelPKI::Error/App::CamelPKI::Error::OtherProcess> exception will be
thrown if the server doesn't answer within L</async_timeout> seconds.
An L<App::CamelPKI::Error/App::CamelPKI::Error::User> exception will be thrown
if one attempts to I<start()> the server before providing it with its
certificate and key with L</set_keys>.
Available named options are:
=over
=item I<< -strace => $strace_logfile >>
Starts Apache under the C<strace> debug command, storing all results
into $strace_logfile.
=item I<< -X => 1 >>
Starts Apache with the C<-X> option, which causes it to launch only
one worker and to not detach from the terminal.
=item I<< -gdb => 1 >>
=item I<< -gdb => $tty >>
Starts Apache under the GNU debugger attached to tty $tty (or the
current tty, if the value 1 is specified). Incompatible with
I<-strace>. If this option is specified, I<start()> will not time out
after L</async_timeout> seconds, but will instead wait an unlimited
amount of time for the server to come up.
=item I<< -exec => 1 >>
Don't fork a subprocess, use the C<exec> system call instead (see
L<perlfunc/exec>) to run Apache directly (or more usefully, some
combination of Apache and a debugger, according to the above named
options). The current UNIX process will turn into Apache, and the
I<start> method will therefore never return.
=back
=cut
sub start {
throw App::CamelPKI::Error::Internal("WRONG_NUMBER_ARGS")
unless (@_ % 2);
my ($self, %opts) = @_;
throw App::CamelPKI::Error::OtherProcess("Apache is wedged")
if ($self->is_wedged);
return if $self->is_started;
$self->_write_config_file();
my (@debugprecmd, @dashX);
my $timeout = 1;
if (defined(my $stracefile = delete $opts{-strace})) {
@debugprecmd = ("strace", -o => $stracefile,
qw(-f -s 2000));
} elsif (my $tty = delete $opts{-gdb}) {
@debugprecmd = ("gdb", ( ($tty eq "1") ? () : ("-tty=$tty") ),
"--args");
$timeout = 0;
}
if (delete $opts{-X}) { @dashX = qw(-X); }
my @fullcmdline =
(@debugprecmd,
$self->_apache_bin, @dashX, -f => $self->_config_filename);
if ($opts{-exec}) {
exec(@fullcmdline) or
throw App::CamelPKI::Error::OtherProcess("cannot exec() Apache",
-cmdline => \@fullcmdline);
} else {
# Double fork(), so we don't have to bother with zombies :
fork_and_do { fork_and_do {
exec @fullcmdline;
} };
}
if ($timeout) {
$self->_wait_for(sub { $self->is_started })
or throw App::CamelPKI::Error::OtherProcess("Cannot start Apache");
} else {
while(! $self->is_started) { sleep(1); }
};
return;
}
=head2 stop()
Stops the daemon synchronously, meaning that I<stop> will only return
control to its caller after ensuring that the Apache process whose PID
is in the PID file is terminated, and the TCP port is closed. Like
L</start>, this method is idempotent and returns immediately if the
server was already down.
An exception of class L<App::CamelPKI::Error/App::CamelPKI::Error::OtherProcess>
will be thrown if the server still hasn't stopped after
L</async_timeout> seconds.
Note that the "started" or "stopped" state is persisted to the
filesystem using the usual UNIX PID file mechanism; therefore it is
not necessary to use the same Perl object (or even the same process)
to L</start> and I<stop()> a given server.
=cut
sub stop {
my ($self) = @_;
throw App::CamelPKI::Error::OtherProcess("Apache is wedged")
if ($self->is_wedged);
return # Not wedged and not started means stopped
if ! defined(my $pid = $self->_process_ready);
kill TERM => $pid;
$self->_wait_for(sub { $self->is_stopped })
or throw App::CamelPKI::Error::OtherProcess("Cannot stop Apache");
return;
}
=head2 is_started()
Returns true iff the PID file currently contains the PID of a live
Apache process, B<and> one can connect to the TCP port.
=cut
sub is_started {
my ($self) = @_;
$self->_process_ready && $self->_port_ready;
}
=head2 is_stopped()
Returns true iff the PID file (if it exists at all) contains something
that is not the PID of a live Apache process, B<and> the TCP port is
closed.
=cut
sub is_stopped {
my ($self) = @_;
(! $self->_process_ready) && (! $self->_port_ready);
}
=head2 is_wedged()
Returns true iff neither L</is_stopped>, nor L</is_started> are true
(e.g. if the TCP port is taken, but not by us). One cannot call
L</start> or L</stop> against an instance of I<App::CamelPKI::SysV::Apache>
that I<is_wedged()> (L<App::CamelPKI::Error/App::CamelPKI::Error::OtherProcess>
exceptions would be thrown). More generally, neither can one call any
method that act upon other processes such as L</update_crl>. The
systems administrator therefore needs to take manual corrective action
to get out of this state.
=cut
sub is_wedged {
my ($self) = @_;
$self->_process_ready xor $self->_port_ready;
}
sub _has_mod_apsx_support{
my ($mod_name) = @_;
my @mods = `apxs2 -q LIBEXECDIR | xargs ls | sed 's/\.so//'`;
foreach (@mods){
$_ =~ s/\n//g;
return 1 if ($_ =~ /$mod_name/);
}
return 0;
}
=head2 is_installed_and_has_perl_support()
Returns true if Apache id installed and has perl support as a static or shared module,
false otherwise.
=cut
sub is_installed_and_has_perl_support {
use App::Info::HTTPD::Apache;
my $apache = App::Info::HTTPD::Apache->new;
return $apache->mod_perl if $apache->mod_perl;
#We are giving a last chance for ubuntu as App::Info::HTTPD::Apache
# doesn't seems to detect reallay good modules
return _has_mod_apsx_support("mod_perl");
}
=head2 is_installed_and_has_php_support()
Returns true if Apache id installed and has php support as a static or shared module,
false otherwise.
=cut
sub is_installed_and_has_php_support {
use App::Info::HTTPD::Apache;
my $apache = App::Info::HTTPD::Apache->new;
eval {
foreach ($apache->static_mods){
return 1 if ($_ =~ /libphp5/);
}
foreach ($apache->shared_mods){
return 1 if ($_ =~ /libphp5/);
}
};
lib/App/CamelPKI/SysV/Apache.pm view on Meta::CPAN
sub is_running_under {
# We test not only that a version-discriminating subroutine
# exists, but also that it is implemented in C (as an XSUB), hence
# the magic with the B package. Thus we don't get fooled with
# ::bootstrap getting defined as a side effect of Apache::DB
# loading, or something.
my @discriminating_symbols =
(
2 => "ModPerl::Util::exit",
1.99 => "ModPerl::Const::compile",
1 => "Apache::ModuleConfig::bootstrap",
);
require B;
while(my ($version, $discrimsymbol) =
splice(@discriminating_symbols, 0, 2)) {
no strict "refs"; my $ref = \&{$discrimsymbol};
next if (!defined $ref); # Does not seem to ever
# happen in Perl, but you never know.
my $bref = B::svref_2object($ref);
next if (! defined $bref);
next if (! $bref->XSUB());
return $version;
}
return undef;
}
=head2 async_timeout()
=head2 async_timeout($timeout)
Gets or sets the maximum time (in seconds) that L</start> and L</stop>
will wait for the Apache server to come up (resp. down). The default
value is 20 seconds; it does B<not> get persisted, and therefore must
be set by caller code after each L</load>.
=cut
sub async_timeout {
my ($self, @set) = @_;
($self->{async_timeout}) = @set if (@set);
return ( $self->{async_timeout} ||= 120);
}
=head2 tail_error_logfile()
Returns the amount of text that was appended to the error log file
since the object was created since the previous call to
I<tail_eror_logfile()> (or barring that, to L</load>). Returns undef
if the log file does not exist (yet).
=cut
sub tail_error_logfile {
my ($self) = @_;
my $log = new IO::File($self->_error_log_filename);
$self->{offset} = 0, return if (! defined $log);
my $retval;
if (defined wantarray) {
$log->seek($self->{offset}, SEEK_CUR)
or throw App::CamelPKI::Error::IO
("cannot seek", -IOfile => $self->_error_log_filename);
$retval = join('', $log->getlines);
} else { # Notamment à la construction
$log->seek(0, SEEK_END)
or throw App::CamelPKI::Error::IO
("cannot seek to end of file",
-IOfile => $self->_error_log_filename);
}
$self->{offset} = $log->tell();
return $retval;
}
=head1 TODO
In B<App::CamelPKI::Apache>'s current implementation, only Apache 2 for
Ubuntu Edgy is supported. However, the encapsulation of the class
makes it easy to support other environments, without changing anything
in the rest of Camel-PKI.
=begin internals
=head1 INTERNAL METHODS
=head2 _apache_bin()
Returns the path to Apache's binary.
=cut
sub _apache_bin { "/usr/sbin/apache2" }
=head2 _has_module_built_in($module_shortname)
Returns true if apache has $module_shortname (e.g. "mime") built-in.
=cut
use File::Slurp;
sub _has_module_built_in {
my ($class, $modulename) = @_;
my $bin = read_file($class->_apache_bin);
if ($bin =~ m/mod_$modulename/) {
return 1;
}
return 0;
}
=head2 _pid_filename()
Returns the path to this Apache daemon's PID file.
=cut
sub _pid_filename { catfile(shift->{homedir}, "apache.pid") }
=head2 _config_filename()
Returns the path to this daemon's configuration file (which is
automagically generated by L</_write_config_file>).
=cut
sub _config_filename { catfile(shift->{homedir}, "httpd-DO-NOT-EDIT.conf") }
lib/App/CamelPKI/SysV/Apache.pm view on Meta::CPAN
CATALYST_STUFF
write_file($self->_config_filename, <<"CONFIG");
$banner
$loadmodules
ServerName Camel-PKI
ServerRoot $homedir
Listen $port
#### Files
PidFile $pidfile
SSLCertificateFile $certfile
SSLCertificateKeyFile $keyfile
SSLCACertificateFile $ca_bundle
ErrorLog $error_log
LockFile $homedir/httpd.lock
ScoreBoardFile $homedir/httpd.scoreboard
#### SSL configuration
SSLEngine on
# Dissect SSL connection info into \$r->subprocess_env:
SSLOptions +StdEnvVars +ExportCertData
#Minor Bug in Firefox 3.0 :
SSLProtocol all -TLSv1
# Ask for a certificate every time, but do not barf if none is given:
SSLVerifyClient optional
# Work around some of the rampant OpenSSL braindamage:
SSLVerifyDepth 250
# Loads all digests into OpenSSL by side effect, thereby allowing mod_ssl
# to grok SHA-256 certificates (which it normally doesn't):
PerlModule Crypt::OpenSSL::CA
$phpstuff
$perlswitches
$catalyststuff
$modmime
CONFIG
}
=head2 _try_and_parse_config_file()
Retrieves persistent information from the configuration file
(e.g. port number and PHP directory) and populates this object's
fields accordingly. Returns true if the parsing was successful.
Returns false if there was no configuration file to parse. Throws an
exception in any other case.
=cut
sub _try_and_parse_config_file {
my ($self) = @_;
return if ! -f $self->_config_filename();
my $configtext = read_file($self->_config_filename());
(($self->{https_port}) = $configtext =~ m/Listen (\d+)/ )
or throw App::CamelPKI::Error::State
("Configuration file was tampered with",
-config_file => $self->_config_filename());
($self->{test_php_directory}) =
$configtext =~ m|Alias /t/php "(.*)"|; # Optional
$self->{has_camel_pki} =
($configtext =~ m/PerlModule App::CamelPKI/) ? 1 : 0;
return 1;
}
require My::Tests::Below unless caller;
1;
__END__
=head1 TEST SUITE
=cut
use Test::More qw(no_plan);
use Test::Group;
use Fatal qw(mkdir);
use File::Slurp;
use File::Spec::Functions qw(catdir catfile);
use LWP::Simple ();
use Crypt::OpenSSL::CA;
use App::CamelPKI::Error;
use App::CamelPKI::Time;
use App::CamelPKI::Certificate;
use App::CamelPKI::PrivateKey;
use App::CamelPKI::CRL;
use App::CamelPKI::Sys qw(fork_and_do);
use App::CamelPKI::Test qw(%test_entity_certs %test_keys_plaintext
%test_rootca_certs);
=head2 Prologue
According to L</SYNOPSIS>, the Apache server needs a home directory, a
certificate, a private key, a certification chain and a CRL.
=cut
my $rootcacert = App::CamelPKI::Certificate->parse
($test_rootca_certs{rsa1024});
my $rootcakey = App::CamelPKI::PrivateKey->parse
($test_keys_plaintext{rsa1024});
my $crl = new Crypt::OpenSSL::CA::X509_CRL();
$crl->set_issuer_DN($rootcacert->get_subject_DN);
$crl->set_extension("crlNumber", "0x01", -critical => 1);
my $now = App::CamelPKI::Time->now;
$crl->set_lastUpdate($now->advance_days(-1));
$crl->set_nextUpdate($now->advance_days(+1));
$crl = App::CamelPKI::CRL->parse
($crl->sign($rootcakey->as_crypt_openssl_ca_privatekey, "sha256"));
ok(! $crl->is_member($rootcacert), "test CRL OK");
=head3 fresh_directory()
Returns a fresh, empty directory for tests.
=cut
( run in 0.471 second using v1.01-cache-2.11-cpan-39bf76dae61 )