# -*- perl -*-
#
# Net::Server
# ABSTRACT: Extensible Perl internet server
#
# Copyright (C) 2001-2017
#
# Paul Seamons <paul@seamons.com>
#
# Rob Brown <bbb@cpan,org>
#
# This package may be distributed under the terms of either the
# GNU General Public License
# or the
# Perl Artistic License
#
# All rights reserved.
#
################################################################
package Net::Server;
use strict;
use Socket qw(AF_INET AF_UNIX SOCK_DGRAM SOCK_STREAM);
use IO::Socket ();
use IO::Select ();
use POSIX ();
use Net::Server::Proto ();
use Net::Server::Daemonize qw(check_pid_file create_pid_file safe_fork
get_uid get_gid set_uid set_gid);
our $VERSION = '2.009';
sub new {
my $class = shift || die "Missing class";
my $args = @_ == 1 ? shift : {@_};
return bless {server => {%$args}}, $class;
}
sub net_server_type { __PACKAGE__ }
sub get_property { $_[0]->{'server'}->{$_[1]} }
sub set_property { $_[0]->{'server'}->{$_[1]} = $_[2] }
sub run {
my $self = ref($_[0]) ? shift() : shift->new; # pass package or object
$self->{'server'}->{'_run_args'} = [@_ == 1 ? %{$_[0]} : @_];
$self->_initialize; # configure all parameters
$self->post_configure; # verification of passed parameters
$self->post_configure_hook; # user customizable hook
$self->pre_bind; # finalize ports to be bound
$self->bind; # connect to port(s), setup selection handle for multi port
$self->post_bind_hook; # user customizable hook
$self->post_bind; # allow for chrooting, becoming a different user and group
$self->pre_loop_hook; # user customizable hook
$self->loop; # repeat accept/process cycle
$self->server_close; # close the server and release the port
}
sub run_client_connection {
my $self = shift;
my $c = $self->{'server'}->{'client'};
$self->post_accept($c); # prepare client for processing
$self->get_client_info($c); # determines information about peer and local
$self->post_accept_hook($c); # user customizable hook
my $ok = $self->allow_deny($c) && $self->allow_deny_hook($c); # do allow/deny check on client info
if ($ok) {
$self->process_request($c); # This is where the core functionality of a Net::Server should be.
} else {
$self->request_denied_hook($c); # user customizable hook
}
$self->post_process_request_hook($ok); # user customizable hook
$self->post_process_request; # clean up client connection, etc
$self->post_client_connection_hook; # one last hook
}
###----------------------------------------------------------------###
sub _initialize {
my $self = shift;
my $prop = $self->{'server'} ||= {};
$self->commandline($self->_get_commandline) if ! eval { $self->commandline }; # save for a HUP
$self->configure_hook; # user customizable hook
$self->configure; # allow for reading of commandline, program, and configuration file parameters
my @defaults = %{ $self->default_values || {} }; # allow yet another way to pass defaults
$self->process_args(\@defaults) if @defaults;
}
sub commandline {
my $self = shift;
$self->{'server'}->{'commandline'} = ref($_[0]) ? shift : \@_ if @_;
return $self->{'server'}->{'commandline'} || die "commandline was not set during initialization";
}
sub _get_commandline {
my $self = shift;
my $script = $0;
$script = $ENV{'PWD'} .'/'. $script if $script =~ m|^[^/]+/| && $ENV{'PWD'}; # add absolute to relative - avoid Cwd
$script =~ /^(.+)$/; # untaint for later use in hup
return [$1, @ARGV]
}
sub configure_hook {}
sub configure {
my $self = shift;
my $prop = $self->{'server'};
my $template = ($_[0] && ref($_[0])) ? shift : undef;
$self->process_args(\@ARGV, $template) if @ARGV; # command line
$self->process_args($prop->{'_run_args'}, $template) if $prop->{'_run_args'}; # passed to run
if ($prop->{'conf_file'}) {
$self->process_args($self->_read_conf($prop->{'conf_file'}), $template);
} else {
my $def = $self->default_values || {};
$self->process_args($self->_read_conf($def->{'conf_file'}), $template) if $def->{'conf_file'};
}
}
sub default_values { {} }
sub post_configure {
my $self = shift;
my $prop = $self->{'server'};
$prop->{'log_level'} = 2 if ! defined($prop->{'log_level'}) || $prop->{'log_level'} !~ /^\d+$/;
$prop->{'log_level'} = 4 if $prop->{'log_level'} > 4;
$self->initialize_logging;
if ($prop->{'pid_file'}) { # see if a daemon is already running
if (! eval{ check_pid_file($prop->{'pid_file'}) }) {
warn $@ if !$ENV{'BOUND_SOCKETS'};
$self->fatal(my $e = $@);
}
}
if (! $prop->{'_is_inet'}) { # completetly daemonize by closing STDIN, STDOUT (should be done before fork)
if ($prop->{'setsid'} || length($prop->{'log_file'})) {
open(STDIN, '<', '/dev/null') || die "Cannot read /dev/null [$!]";
open(STDOUT, '>', '/dev/null') || die "Cannot write /dev/null [$!]";
}
}
if (!$ENV{'BOUND_SOCKETS'}) { # don't need to redo this if hup'ing
if ($prop->{'setsid'} || $prop->{'background'}) {
my $pid = eval { safe_fork() };
$self->fatal(my $e = $@) if ! defined $pid;
exit(0) if $pid;
$self->log(2, "Process Backgrounded");
}
POSIX::setsid() if $prop->{'setsid'}; # completely remove myself from parent process
}
if (length($prop->{'log_file'})
&& !$prop->{'log_function'}) {
open STDERR, '>&_SERVER_LOG' || die "Cannot open STDERR to _SERVER_LOG [$!]";
} elsif ($prop->{'setsid'}) { # completely daemonize by closing STDERR (should be done after fork)
open STDERR, '>&STDOUT' || die "Cannot open STDERR to STDOUT [$!]";
}
# allow for a pid file (must be done after backgrounding and chrooting)
# Remove of this pid may fail after a chroot to another location... however it doesn't interfere either.
if ($prop->{'pid_file'}) {
if (eval { create_pid_file($prop->{'pid_file'}) }) {
$prop->{'pid_file_unlink'} = 1;
} else {
$self->fatal(my $e = $@);
}
}
# make sure that allow and deny look like array refs
$prop->{$_} = [] for grep {! ref $prop->{$_}} qw(allow deny cidr_allow cidr_deny);
}
sub initialize_logging {
my $self = shift;
my $prop = $self->{'server'};
if (! defined($prop->{'log_file'})) {
$prop->{'log_file'} = ''; # log to STDERR
return;
}
# pluggable logging
if ($prop->{'log_file'} =~ /^([a-zA-Z]\w*(?:::[a-zA-Z]\w*)*)$/) {
my $pkg = "Net::Server::Log::$prop->{'log_file'}";
(my $file = "$pkg.pm") =~ s|::|/|g;
if (eval { require $file }) {
$prop->{'log_function'} = $pkg->initialize($self);
$prop->{'log_class'} = $pkg;
return;
} elsif ($file =~ /::/ || grep {-e "$_/$file"} @INC) {
$self->fatal("Unable to load log module $pkg from file $file: $@");
}
}
# regular file based logging
die "Unsecure filename \"$prop->{'log_file'}\"" if $prop->{'log_file'} !~ m|^([\:\w\.\-/\\]+)$|;
$prop->{'log_file'} = $1; # open a logging file
open(_SERVER_LOG, ">>", $prop->{'log_file'})
|| die "Couldn't open log file \"$prop->{'log_file'}\" [$!].";
_SERVER_LOG->autoflush(1);
push @{ $prop->{'chown_files'} }, $prop->{'log_file'};
}
sub post_configure_hook {}
sub _server_type { ref($_[0]) }
sub pre_bind { # make sure we have good port parameters
my $self = shift;
my $prop = $self->{'server'};
my $super = $self->net_server_type;
my $type = $self->_server_type;
if ($self->isa('Net::Server::MultiType')) {
my $base = delete($prop->{'_recursive_multitype'}) || Net::Server::MultiType->net_server_type;
$super = "$super -> MultiType -> $base";
}
$type .= " (type $super)" if $type ne $super;
$self->log(2, $self->log_time ." $type starting! pid($$)");
$prop->{'sock'} = [grep {$_} map { $self->proto_object($_) } @{ $self->prepared_ports }];
$self->fatal("No valid socket parameters found") if ! @{ $prop->{'sock'} };
}
sub prepared_ports {
my $self = shift;
my $prop = $self->{'server'};
my ($ports, $hosts, $protos, $ipvs) = @$prop{qw(port host proto ipv)};
$ports ||= $prop->{'ports'};
if (!defined($ports) || (ref($ports) && !@$ports)) {
$ports = $self->default_port;
if (!defined($ports) || (ref($ports) && !@$ports)) {
$ports = default_port();
$self->log(2, "Port Not Defined. Defaulting to '$ports'");
}
}
my %bound;
my $bind = $prop->{'_bind'} = [];
for my $_port (ref($ports) ? @$ports : $ports) {
my $_host = ref($hosts) ? $hosts->[ @$bind >= @$hosts ? -1 : $#$bind + 1] : $hosts; # if ports are greater than hosts - augment with the last host
my $_proto = ref($protos) ? $protos->[@$bind >= @$protos ? -1 : $#$bind + 1] : $protos;
my $_ipv = ref($ipvs) ? $ipvs->[ @$bind >= @$ipvs ? -1 : $#$bind + 1] : $ipvs;
foreach my $info ($self->port_info($_port, $_host, $_proto, $_ipv)) {
my ($port, $host, $proto, $ipv) = @$info{qw(port host proto ipv)}; # use cleaned values
if ($port ne "0" && $bound{"$host\e$port\e$proto\e$ipv"}++) {
$self->log(2, "Duplicate configuration (\U$proto\E) on [$host]:$port with IPv$ipv) - skipping");
next;
}
push @$bind, $info;
}
}
return $bind;
}
sub port_info {
my ($self, $port, $host, $proto, $ipv) = @_;
return Net::Server::Proto->parse_info($port, $host, $proto, $ipv, $self);
}
sub proto_object {
my ($self, $info) = @_;
return Net::Server::Proto->object($info, $self);
}
sub bind { # bind to the port (This should serve all but INET)
my $self = shift;
my $prop = $self->{'server'};
if (exists $ENV{'BOUND_SOCKETS'}) {
$self->restart_open_hook;
$self->log(2, "Binding open file descriptors");
my %map;
foreach my $info (split /\s*;\s*/, $ENV{'BOUND_SOCKETS'}) {
my ($fd, $host, $port, $proto, $ipv, $orig) = split /\|/, $info;
$orig = $port if ! defined $orig; # allow for things like service ports or port 0
$fd = ($fd =~ /^(\d+)$/) ? $1 : $self->fatal("Bad file descriptor");
$map{"$host|$orig|$proto|$ipv"}->{$fd} = $port;
}
foreach my $sock (@{ $prop->{'sock'} }) {
$sock->log_connect($self);
if (my $ref = $map{$sock->hup_string}) {
my ($fd, $port) = each %$ref;
$sock->reconnect($fd, $self, $port);
delete $ref->{$fd};
delete $map{$sock->hup_string} if ! keys %$ref;
} else {
$self->log(2, "Added new port configuration");
$sock->connect($self);
}
}
foreach my $str (keys %map) {
foreach my $fd (keys %{ $map{$str} }) {
$self->log(2, "Closing un-mapped port ($str) on fd $fd");
POSIX::close($fd);
}
}
delete $ENV{'BOUND_SOCKETS'};
$self->{'hup_waitpid'} = 1;
} else { # connect to fresh ports
foreach my $sock (@{ $prop->{'sock'} }) {
$sock->log_connect($self);
$sock->connect($self);
}
}
if (@{ $prop->{'sock'} } > 1 || $prop->{'multi_port'}) {
$prop->{'multi_port'} = 1;
$prop->{'select'} = IO::Select->new; # if more than one socket we'll need to select on it
$prop->{'select'}->add($_) for @{ $prop->{'sock'} };
} else {
$prop->{'multi_port'} = undef;
$prop->{'select'} = undef;
}
}
sub post_bind_hook {}
sub post_bind { # secure the process and background it
my $self = shift;
my $prop = $self->{'server'};
if (! defined $prop->{'group'}) {
$self->log(1, "Group Not Defined. Defaulting to EGID '$)'");
$prop->{'group'} = $);
} elsif ($prop->{'group'} =~ /^([\w-]+(?: [\w-]+)*)$/) {
$prop->{'group'} = eval { get_gid($1) };
$self->fatal(my $e = $@) if $@;
} else {
$self->fatal("Invalid group \"$prop->{'group'}\"");
}
if (! defined $prop->{'user'}) {
$self->log(1, "User Not Defined. Defaulting to EUID '$>'");
$prop->{'user'} = $>;
} elsif ($prop->{'user'} =~ /^([\w-]+)$/) {
$prop->{'user'} = eval { get_uid($1) };
$self->fatal(my $e = $@) if $@;
} else {
$self->fatal("Invalid user \"$prop->{'user'}\"");
}
# chown any files or sockets that we need to
if ($prop->{'group'} ne $) || $prop->{'user'} ne $>) {
my @chown_files;
push @chown_files, map {$_->NS_port} grep {$_->NS_proto =~ /^UNIX/} @{ $prop->{'sock'} };
push @chown_files, $prop->{'pid_file'} if $prop->{'pid_file_unlink'};
push @chown_files, $prop->{'lock_file'} if $prop->{'lock_file_unlink'};
push @chown_files, @{ $prop->{'chown_files'} || [] };
my $uid = $prop->{'user'};
my $gid = (split /\ /, $prop->{'group'})[0];
foreach my $file (@chown_files){
chown($uid, $gid, $file) || $self->fatal("Couldn't chown \"$file\" [$!]");
}
}
if ($prop->{'chroot'}) {
$self->fatal("Specified chroot \"$prop->{'chroot'}\" doesn't exist.") if ! -d $prop->{'chroot'};
$self->log(2, "Chrooting to $prop->{'chroot'}");
chroot($prop->{'chroot'}) || $self->fatal("Couldn't chroot to \"$prop->{'chroot'}\": $!");
}
# drop privileges
eval {
if ($prop->{'group'} ne $)) {
$self->log(2, "Setting gid to \"$prop->{'group'}\"");
set_gid($prop->{'group'} );
}
if ($prop->{'user'} ne $>) {
$self->log(2, "Setting uid to \"$prop->{'user'}\"");
set_uid($prop->{'user'});
}
};
if ($@) {
if ($> == 0) {
$self->fatal(my $e = $@);
} elsif ($< == 0) {
$self->log(2, "NOTICE: Effective UID changed, but Real UID is 0: $@");
} else {
$self->log(2, my $e = $@);
}
}
$prop->{'requests'} = 0; # record number of request
$SIG{'INT'} = $SIG{'TERM'} = $SIG{'QUIT'} = sub { $self->server_close; };
$SIG{'PIPE'} = 'IGNORE'; # most cases, a closed pipe will take care of itself
$SIG{'CHLD'} = \&sig_chld; # catch children (mainly for Fork and PreFork but works for any chld)
$SIG{'HUP'} = sub { $self->sig_hup };
}
sub sig_chld {
1 while waitpid(-1, POSIX::WNOHANG()) > 0;
$SIG{'CHLD'} = \&sig_chld;
}
sub pre_loop_hook {}
sub loop {
my $self = shift;
while ($self->accept) {
$self->run_client_connection;
last if $self->done;
}
}
sub accept {
my $self = shift;
my $prop = $self->{'server'};
my $sock = undef;
my $retries = 30;
while ($retries--) {
if ($prop->{'multi_port'}) { # with more than one port, use select to get the next one
return 0 if $prop->{'_HUP'};
$sock = $self->accept_multi_port || next; # keep trying for the rest of retries
return 0 if $prop->{'_HUP'};
if ($self->can_read_hook($sock)) {
$retries++;
next;
}
} else {
$sock = $prop->{'sock'}->[0]; # single port is bound - just accept
}
$self->fatal("Received a bad sock!") if ! defined $sock;
if (SOCK_DGRAM == $sock->getsockopt(Socket::SOL_SOCKET(), Socket::SO_TYPE())) { # receive a udp packet
$prop->{'client'} = $sock;
$prop->{'udp_true'} = 1;
$prop->{'udp_peer'} = $sock->recv($prop->{'udp_data'}, $sock->NS_recv_len, $sock->NS_recv_flags);
} else { # blocking accept per proto
delete $prop->{'udp_true'};
$prop->{'client'} = $sock->accept();
}
return 0 if $prop->{'_HUP'};
return 1 if $prop->{'client'};
$self->log(2,"Accept failed with $retries tries left: $!");
sleep(1);
}
$self->log(1,"Ran out of accept retries!");
return undef;
}
sub accept_multi_port {
my @waiting = shift->{'server'}->{'select'}->can_read();
return undef if ! @waiting;
return $waiting[rand @waiting];
}
sub can_read_hook {}
sub post_accept {
my $self = shift;
my $prop = $self->{'server'};
my $client = shift || $prop->{'client'};
$prop->{'requests'}++;
return if $prop->{'udp_true'}; # no need to do STDIN/STDOUT in UDP
if (!$client) {
$self->log(1,"Client socket information could not be determined!");
return;
}
$client->post_accept() if $client->can("post_accept");
if (! $prop->{'no_client_stdout'}) {
close STDIN; # duplicate some handles and flush them
close STDOUT;
if ($prop->{'tie_client_stdout'} || ($client->can('tie_stdout') && $client->tie_stdout)) {
open STDIN, '<', '/dev/null' or die "Couldn't open STDIN to the client socket: $!";
open STDOUT, '>', '/dev/null' or die "Couldn't open STDOUT to the client socket: $!";
tie *STDOUT, 'Net::Server::TiedHandle', $client, $prop->{'tied_stdout_callback'} or die "Couldn't tie STDOUT: $!";
tie *STDIN, 'Net::Server::TiedHandle', $client, $prop->{'tied_stdin_callback'} or die "Couldn't tie STDIN: $!";
} elsif (defined(my $fileno = fileno $prop->{'client'})) {
open STDIN, '<&', $fileno or die "Couldn't open STDIN to the client socket: $!";
open STDOUT, '>&', $fileno or die "Couldn't open STDOUT to the client socket: $!";
} else {
*STDIN = \*{ $client };
*STDOUT = \*{ $client };
}
STDIN->autoflush(1);
STDOUT->autoflush(1);
select STDOUT;
}
}
sub get_client_info {
my $self = shift;
my $prop = $self->{'server'};
my $client = shift || $prop->{'client'};
if ($client->NS_proto =~ /^UNIX/) {
delete @$prop{qw(sockaddr sockport peeraddr peerport peerhost)};
$self->log(3, $self->log_time." CONNECT ".$client->NS_proto." Socket: \"".$client->NS_port."\"") if $prop->{'log_level'} && 3 <= $prop->{'log_level'};
return;
}
if (my $sockname = $client->sockname) {
$prop->{'sockaddr'} = $client->sockhost;
$prop->{'sockport'} = $client->sockport;
} else {
@{ $prop }{qw(sockaddr sockhost sockport)} = ($ENV{'REMOTE_HOST'} || '0.0.0.0', 'inet.test', 0); # commandline
}
my $addr;
if ($prop->{'udp_true'}) {
if ($client->sockdomain == AF_INET) {
($prop->{'peerport'}, $addr) = Socket::sockaddr_in($prop->{'udp_peer'});
$prop->{'peeraddr'} = Socket::inet_ntoa($addr);
} else {
($prop->{'peerport'}, $addr) = Socket6::sockaddr_in6($prop->{'udp_peer'});
$prop->{'peeraddr'} = Socket6->can('inet_ntop')
? Socket6::inet_ntop($client->sockdomain, $addr)
: Socket::inet_ntoa($addr);
}
} elsif ($prop->{'peername'} = $client->peername) {
$addr = $client->peeraddr;
$prop->{'peeraddr'} = $client->peerhost;
$prop->{'peerport'} = $client->peerport;
} else {
@{ $prop }{qw(peeraddr peerhost peerport)} = ('0.0.0.0', 'inet.test', 0); # commandline
}
if ($addr && defined $prop->{'reverse_lookups'}) {
if ($INC{'Socket6.pm'} && Socket6->can('getnameinfo')) {
my @res = Socket6::getnameinfo($addr, 0);
$prop->{'peerhost'} = $res[0] if @res > 1;
}else{
$prop->{'peerhost'} = gethostbyaddr($addr, AF_INET);
}
}
$self->log(3, $self->log_time
." CONNECT ".$client->NS_proto
." Peer: \"[$prop->{'peeraddr'}]:$prop->{'peerport'}\""
." Local: \"[$prop->{'sockaddr'}]:$prop->{'sockport'}\"") if $prop->{'log_level'} && 3 <= $prop->{'log_level'};
}
sub post_accept_hook {}
sub allow_deny {
my $self = shift;
my $prop = $self->{'server'};
my $sock = shift || $prop->{'client'};
# unix sockets are immune to this check
return 1 if $sock && $sock->NS_proto =~ /^UNIX/;
# if no allow or deny parameters are set, allow all
return 1 if ! @{ $prop->{'allow'} }
&& ! @{ $prop->{'deny'} }
&& ! @{ $prop->{'cidr_allow'} }
&& ! @{ $prop->{'cidr_deny'} };
# work around Net::CIDR::cidrlookup() croaking,
# if first parameter is an IPv4 address in IPv6 notation.
my $peeraddr = ($prop->{'peeraddr'} =~ /^\s*::ffff:([0-9.]+\s*)$/) ? $1 : $prop->{'peeraddr'};
# if the addr or host matches a deny, reject it immediately
foreach (@{ $prop->{'deny'} }) {
return 0 if $prop->{'reverse_lookups'}
&& defined($prop->{'peerhost'}) && $prop->{'peerhost'} =~ /^$_$/;
return 0 if $peeraddr =~ /^$_$/;
}
if (@{ $prop->{'cidr_deny'} }) {
require Net::CIDR;
return 0 if Net::CIDR::cidrlookup($peeraddr, @{ $prop->{'cidr_deny'} });
}
# if the addr or host isn't blocked yet, allow it if it is allowed
foreach (@{ $prop->{'allow'} }) {
return 1 if $prop->{'reverse_lookups'}
&& defined($prop->{'peerhost'}) && $prop->{'peerhost'} =~ /^$_$/;
return 1 if $peeraddr =~ /^$_$/;
}
if (@{ $prop->{'cidr_allow'} }) {
require Net::CIDR;
return 1 if Net::CIDR::cidrlookup($peeraddr, @{ $prop->{'cidr_allow'} });
}
return 0;
}
sub allow_deny_hook { 1 } # false to deny request
sub request_denied_hook {}
sub process_request { # sample echo server - override for full functionality
my $self = shift;
my $prop = $self->{'server'};
if ($prop->{'udp_true'}) { # udp echo server
my $client = shift || $prop->{'client'};
if ($prop->{'udp_data'} =~ /dump/) {
require Data::Dumper;
return $client->send(Data::Dumper::Dumper($self), 0);
}
return $client->send("You said \"$prop->{'udp_data'}\"", 0);
}
print "Welcome to \"".ref($self)."\" ($$)\015\012";
my $previous_alarm = alarm 30;
eval {
local $SIG{'ALRM'} = sub { die "Timed Out!\n" };
while (<STDIN>) {
s/[\r\n]+$//;
print ref($self),":$$: You said \"$_\"\015\012";
$self->log(5, $_); # very verbose log
if (/get\s+(\w+)/) { print "$1: $self->{'server'}->{$1}\015\012" }
elsif (/dump/) { require Data::Dumper; print Data::Dumper::Dumper($self) }
elsif (/quit/) { last }
elsif (/exit/) { $self->server_close }
alarm 30; # another 30
}
alarm($previous_alarm);
};
alarm 0;
print "Timed Out.\015\012" if $@ eq "Timed Out!\n";
}
sub post_process_request_hook {}
sub post_client_connection_hook {}
sub post_process_request {
my $self = shift;
$self->close_client_stdout;
}
sub close_client_stdout {
my $self = shift;
my $prop = $self->{'server'};
return if $prop->{'udp_true'};
if (! $prop->{'no_client_stdout'}) {
my $t = tied *STDOUT; if ($t) { undef $t; untie *STDOUT };
$t = tied *STDIN; if ($t) { undef $t; untie *STDIN };
open(STDIN, '<', '/dev/null') || die "Cannot read /dev/null [$!]";
open(STDOUT, '>', '/dev/null') || die "Cannot write /dev/null [$!]";
}
$prop->{'client'}->close;
}
sub done {
my $self = shift;
$self->{'server'}->{'done'} = shift if @_;
return $self->{'server'}->{'done'};
}
sub pre_fork_hook {}
sub child_init_hook {}
sub child_finish_hook {}
sub run_dequeue { # fork off a child process to handle dequeuing
my $self = shift;
$self->pre_fork_hook('dequeue');
my $pid = fork;
$self->fatal("Bad fork [$!]") if ! defined $pid;
if (!$pid) { # child
$SIG{'INT'} = $SIG{'TERM'} = $SIG{'QUIT'} = $SIG{'HUP'} = sub {
$self->child_finish_hook('dequeue');
exit;
};
$SIG{'PIPE'} = $SIG{'TTIN'} = $SIG{'TTOU'} = 'DEFAULT';
$self->child_init_hook('dequeue');
$self->dequeue();
$self->child_finish_hook('dequeue');
exit;
}
$self->log(4, "Running dequeue child $pid");
$self->{'server'}->{'children'}->{$pid}->{'status'} = 'dequeue'
if $self->{'server'}->{'children'};
}
sub default_port { 20203 }
sub dequeue {}
sub pre_server_close_hook {}
sub server_close {
my ($self, $exit_val) = @_;
my $prop = $self->{'server'};
$SIG{'INT'} = 'DEFAULT';
### if this is a child process, signal the parent and close
### normally the child shouldn't, but if they do...
### otherwise the parent continues with the shutdown
### this is safe for non standard forked child processes
### as they will not have server_close as a handler
if (defined($prop->{'ppid'})
&& $prop->{'ppid'} != $$
&& ! defined($prop->{'no_close_by_child'})) {
$self->close_parent;
exit;
}
$self->pre_server_close_hook;
$self->log(2, $self->log_time . " Server closing!");
if ($prop->{'kind_quit'} && $prop->{'children'}) {
$self->log(3, "Attempting a slow shutdown");
$prop->{$_} = 0 for qw(min_servers max_servers);
$self->hup_children; # send children signal to finish up
while (1) {
Net::Server::SIG::check_sigs();
$self->coordinate_children if $self->can('coordinate_children');
last if !keys %{$self->{'server'}->{'children'}};
sleep 1;
}
}
if ($prop->{'_HUP'} && $prop->{'leave_children_open_on_hup'}) {
$self->hup_children;
} else {
$self->close_children() if $prop->{'children'};
$self->post_child_cleanup_hook;
}
if (defined($prop->{'lock_file'})
&& -e $prop->{'lock_file'}
&& defined($prop->{'lock_file_unlink'})) {
unlink($prop->{'lock_file'}) || $self->log(1, "Couldn't unlink \"$prop->{'lock_file'}\" [$!]");
}
if (defined($prop->{'pid_file'})
&& -e $prop->{'pid_file'}
&& !$prop->{'_HUP'}
&& defined($prop->{'pid_file_unlink'})) {
unlink($prop->{'pid_file'}) || $self->log(1, "Couldn't unlink \"$prop->{'pid_file'}\" [$!]");
}
if ($prop->{'_HUP'}) {
$self->restart_close_hook();
$self->hup_server; # execs at the end
}
$self->shutdown_sockets;
return $self if $prop->{'no_exit_on_close'};
$self->server_exit($exit_val);
}
sub server_exit {
my ($self, $exit_val) = @_;
exit($exit_val || 0);
}
sub shutdown_sockets {
my $self = shift;
my $prop = $self->{'server'};
foreach my $sock (@{ $prop->{'sock'} }) { # unlink remaining socket files (if any)
$sock->shutdown(2);
unlink $sock->NS_port if $sock->NS_proto =~ /^UNIX/;
}
$prop->{'sock'} = []; # delete the sock objects
return 1;
}
### Allow children to send INT signal to parent (or use another method)
### This method is only used by forking servers
sub close_parent {
my $self = shift;
my $prop = $self->{'server'};
die "Missing parent pid (ppid)" if ! $prop->{'ppid'};
kill 2, $prop->{'ppid'};
}
### SIG INT the children
### This method is only used by forking servers (ie Fork, PreFork)
sub close_children {
my $self = shift;
my $prop = $self->{'server'};
return unless $prop->{'children'} && scalar keys %{ $prop->{'children'} };
foreach my $pid (keys %{ $prop->{'children'} }) {
$self->log(4, "Kill TERM pid $pid");
if (kill(15, $pid) || ! kill(0, $pid)) { # if it is killable, kill it
$self->delete_child($pid);
}
}
1 while waitpid(-1, POSIX::WNOHANG()) > 0;
}
sub is_prefork { 0 }
sub hup_children {
my $self = shift;
my $prop = $self->{'server'};
return unless defined $prop->{'children'} && scalar keys %{ $prop->{'children'} };
return if ! $self->is_prefork;
$self->log(2, "Sending children hup signal");
for my $pid (keys %{ $prop->{'children'} }) {
$self->log(4, "Kill HUP pid $pid");
kill(1, $pid) or $self->log(2, "Failed to kill pid $pid: $!");
}
}
sub post_child_cleanup_hook {}
### handle sig hup
### this will prepare the server for a restart via exec
sub sig_hup {
my $self = shift;
my $prop = $self->{'server'};
$self->log(2, "Received a SIG HUP");
my $i = 0;
my @fd;
$prop->{'_HUP'} = [];
foreach my $sock (@{ $prop->{'sock'} }) {
my $fd = POSIX::dup($sock->fileno) || $self->fatal("Cannot duplicate the socket [$!]");
# hold on to the socket copy until exec;
# just temporary: any socket domain will do,
# forked process will decide to use IO::Socket::INET6 if necessary
$prop->{'_HUP'}->[$i] = IO::Socket::INET->new;
$prop->{'_HUP'}->[$i]->fdopen($fd, 'w') || $self->fatal("Cannot open to file descriptor [$!]");
# turn off the FD_CLOEXEC bit to allow reuse on exec
require Fcntl;
$prop->{'_HUP'}->[$i]->fcntl(Fcntl::F_SETFD(), my $flags = "");
push @fd, $fd .'|'. $sock->hup_string; # save file-descriptor and host|port|proto|ipv
$sock->close();
$i++;
}
delete $prop->{'select'}; # remove any blocking obstacle
$ENV{'BOUND_SOCKETS'} = join "; ", @fd;
if ($prop->{'leave_children_open_on_hup'} && scalar keys %{ $prop->{'children'} }) {
$ENV{'HUP_CHILDREN'} = join "\n", map {"$_\t$prop->{'children'}->{$_}->{'status'}"} sort keys %{ $prop->{'children'} };
}
}
sub hup_server {
my $self = shift;
$self->log(0, $self->log_time()." Re-exec server during HUP");
delete @ENV{$self->hup_delete_env_keys};
exec @{ $self->commandline };
}
sub hup_delete_env_keys { return qw(PATH) }
sub restart_open_hook {} # this hook occurs if a server has been HUP'ed it occurs just before opening to the fileno's
sub restart_close_hook {} # this hook occurs if a server has been HUP'ed it occurs just before exec'ing the server
###----------------------------------------------------------###
sub fatal {
my ($self, $error) = @_;
my ($package, $file, $line) = caller;
$self->fatal_hook($error, $package, $file, $line);
$self->log(0, $self->log_time ." $error\n at line $line in file $file");
$self->server_close(1);
}
sub fatal_hook {}
###----------------------------------------------------------###
sub log {
my ($self, $level, $msg, @therest) = @_;
my $prop = $self->{'server'};
return if ! $prop->{'log_level'};
return if $level =~ /^\d+$/ && $level > $prop->{'log_level'};
$msg = sprintf($msg, @therest) if @therest; # if multiple arguments are passed, assume that the first is a format string
if ($prop->{'log_function'}) {
return if eval { $prop->{'log_function'}->($level, $msg); 1 };
my $err = $@;
if ($prop->{'log_class'} && $prop->{'log_class'}->can('handle_error')) {
$prop->{'log_class'}->handle_log_error($self, $err, [$level, $msg]);
} else {
$self->handle_log_error($err, [$level, $msg]);
}
}
return if $level !~ /^\d+$/;
$self->write_to_log_hook($level, $msg);
}
sub handle_log_error { my ($self, $error) = @_; die $error }
sub handle_syslog_error { &handle_log_error }
sub write_to_log_hook {
my ($self, $level, $msg) = @_;
my $prop = $self->{'server'};
chomp $msg;
$msg =~ s/([^\n\ -\~])/sprintf("%%%02X",ord($1))/eg;
if ($prop->{'log_file'}) {
print _SERVER_LOG $msg, "\n";
} elsif ($prop->{'setsid'}) {
# do nothing ?
} else {
my $old = select STDERR;
print $msg. "\n";
select $old;
}
}
sub log_time {
my ($sec,$min,$hour,$day,$mon,$year) = localtime;
return sprintf "%04d/%02d/%02d-%02d:%02d:%02d", $year + 1900, $mon + 1, $day, $hour, $min, $sec;
}
###----------------------------------------------------------###
sub options {
my $self = shift;
my $ref = shift || {};
my $prop = $self->{'server'};
foreach (qw(port host proto ipv allow deny cidr_allow cidr_deny)) {
if (! defined $prop->{$_}) {
$prop->{$_} = [];
} elsif (! ref $prop->{$_}) {
$prop->{$_} = [$prop->{$_}]; # nicely turn us into an arrayref if we aren't one already
}
$ref->{$_} = $prop->{$_};
}
foreach (qw(conf_file
user group chroot log_level
log_file pid_file background setsid
listen reverse_lookups
no_close_by_child
no_client_stdout tie_client_stdout tied_stdout_callback tied_stdin_callback
leave_children_open_on_hup
)) {
$ref->{$_} = \$prop->{$_};
}
return $ref;
}
### routine for parsing commandline, module, and conf file
### method has the benefit of leaving unused arguments in @ARGV
sub process_args {
my ($self, $args, $template) = @_;
$self->options($template = {}) if ! $template || ! ref $template;
if (!$_[2] && !scalar(keys %$template) && !$self->{'server'}->{'_no_options'}++) {
warn "Configuration options were empty - skipping any commandline, config file, or run argument parsing.\n";
}
# we want subsequent calls to not overwrite or add to previously set values so that command line arguments win
my %previously_set;
foreach (my $i = 0; $i < @$args; $i++) {
if ($args->[$i] =~ /^(?:--)?(\w+)(?:[=\ ](\S+))?$/
&& exists $template->{$1}) {
my ($key, $val) = ($1, $2);
splice @$args, $i, 1;
if (! defined $val) {
if ($i > $#$args
|| ($args->[$i] && $args->[$i] =~ /^--\w+/)) {
$val = 1; # allow for options such as --setsid
} else {
$val = splice @$args, $i, 1;
$val = $val->[0] if ref($val) eq 'ARRAY' && @$val == 1 && ref($template->{$key}) ne 'ARRAY';
}
}
$i--;
$val =~ s/%([A-F0-9])/chr(hex $1)/eig if ! ref $val;
if (ref $template->{$key} eq 'ARRAY') {
if (! defined $previously_set{$key}) {
$previously_set{$key} = scalar @{ $template->{$key} };
}
next if $previously_set{$key};
push @{ $template->{$key} }, ref($val) eq 'ARRAY' ? @$val : $val;
} else {
if (! defined $previously_set{$key}) {
$previously_set{$key} = defined(${ $template->{$key} }) ? 1 : 0;
}
next if $previously_set{$key};
die "Found multiple values on the configuration item \"$key\" which expects only one value" if ref($val) eq 'ARRAY';
${ $template->{$key} } = $val;
}
}
}
}
sub _read_conf {
my ($self, $file) = @_;
my @args;
$file = ($file =~ m|^([\w\.\-\/\\\:]+)$|) ? $1 : $self->fatal("Unsecure filename \"$file\"");
open my $fh, '<', $file or do {
$self->fatal("Couldn't open conf \"$file\" [$!]") if $ENV{'BOUND_SOCKETS'};
warn "Couldn't open conf \"$file\" [$!]\n";
};
while (defined(my $line = <$fh>)) {
push @args, $1, $2 if $line =~ m/^\s* ((?:--)?\w+) (?:\s*[=:]\s*|\s+) (\S+)/x;
}
close $fh;
return \@args;
}
###----------------------------------------------------------------###
sub other_child_died_hook {}
sub delete_child_hook {}
sub delete_child {
my ($self, $pid) = @_;
my $prop = $self->{'server'};
return $self->other_child_died_hook($pid) if ! exists $prop->{'children'}->{$pid};
# prefork server check to clear child communication
if ($prop->{'child_communication'}) {
if ($prop->{'children'}->{$pid}->{'sock'}) {
$prop->{'child_select'}->remove($prop->{'children'}->{$pid}->{'sock'});
$prop->{'children'}->{$pid}->{'sock'}->close;
}
}
$self->delete_child_hook($pid); # user customizable hook
delete $prop->{'children'}->{$pid};
}
# send signal to all children - used by forking servers
sub sig_pass {
my ($self, $sig) = @_;
foreach my $chld (keys %{ $self->{'server'}->{'children'} }) {
$self->log(4, "signaling $chld with $sig" );
kill($sig, $chld) || $self->log(1, "child $chld not signaled with $sig");
}
}
# register sigs to allow passthrough to children
sub register_sig_pass {
my $self = shift;
my $ref = $self->{'server'}->{'sig_passthrough'} || [];
$ref = [$ref] if ! ref $ref;
$self->fatal('invalid sig_passthrough') if ref $ref ne 'ARRAY';
return if ! @$ref;
$self->log(4, "sig_passthrough option found");
require Net::Server::SIG;
foreach my $sig (map {split /\s*,\s*/, $_} @$ref) {
my $code = Net::Server::SIG::sig_is_registered($sig);
if ($code) {
$self->log(2, "Installing passthrough for $sig even though it is already registered.");
} else {
$code = ref($SIG{$sig}) eq 'CODE' ? $SIG{$sig} : undef;
}
Net::Server::SIG::register_sig($sig => sub { $self->sig_pass($sig); $code->($sig) if $code; });
$self->log(2, "Installed passthrough for $sig");
}
}
###----------------------------------------------------------------###
package Net::Server::TiedHandle;
sub TIEHANDLE { my $pkg = shift; return bless [@_], $pkg }
sub READLINE { my $s = shift; $s->[1] ? $s->[1]->($s->[0], 'getline', @_) : $s->[0]->getline }
sub SAY { my $s = shift; $s->[1] ? $s->[1]->($s->[0], 'say', @_) : $s->[0]->say(@_) }
sub PRINT { my $s = shift; $s->[1] ? $s->[1]->($s->[0], 'print', @_) : $s->[0]->print(@_) }
sub PRINTF { my $s = shift; $s->[1] ? $s->[1]->($s->[0], 'printf', @_) : $s->[0]->printf(@_) }
sub READ { my $s = shift; $s->[1] ? $s->[1]->($s->[0], 'read', @_) : $s->[0]->read(@_) }
sub WRITE { my $s = shift; $s->[1] ? $s->[1]->($s->[0], 'write', @_) : $s->[0]->write(@_) }
sub SYSREAD { my $s = shift; $s->[1] ? $s->[1]->($s->[0], 'sysread', @_) : $s->[0]->sysread(@_) }
sub SYSWRITE { my $s = shift; $s->[1] ? $s->[1]->($s->[0], 'syswrite', @_) : $s->[0]->syswrite(@_) }
sub SEEK { my $s = shift; $s->[1] ? $s->[1]->($s->[0], 'seek', @_) : $s->[0]->seek(@_) }
sub BINMODE {}
sub FILENO {}
sub CLOSE { my $s = shift; $s->[1] ? $s->[1]->($s->[0], 'close', @_) : $s->[0]->close(@_) }
1;
### The documentation is in Net/Server.pod