diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7ebe0732..9282241d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,4 +32,5 @@ jobs: perl-version: ${{ matrix.perl }} - run: cpanm --installdeps -n -f . - run: cpanm --installdeps -n -f Mail::SPF Mail::DMARC GeoIP2 ClamAV::Client Redis - - run: prove -lv t \ No newline at end of file + - run: prove -lv t + diff --git a/CREDITS b/CREDITS index 62ed71eb..d904e8a9 100644 --- a/CREDITS +++ b/CREDITS @@ -8,7 +8,7 @@ improvements for me to ever catch up on here. Matt Sergeant : Clamav plugin. Patch for the dnsbl plugin to give us all the dns results. Resident SpamAssassin guru. PPerl. smtp-forward plugin. Documentation (yay!). Lots of fixes and -tweaks. Apache module. Event based high performance experiment. +tweaks. Event based high performance experiment. Devin Carraway : Patch to not accept half mails if the connection gets dropped at the wrong moment. Support and enable diff --git a/Changes b/Changes index 34db1315..38e2680f 100644 --- a/Changes +++ b/Changes @@ -1,4 +1,10 @@ + Forkserver is THE run model + + Remove tcpserver run mode + + Remove Apache::Qpsmtpd support + 1.00 Feb 16, 2023 Use readable file test for certificate files (#304) diff --git a/MANIFEST b/MANIFEST index a38bb014..18649ad4 100644 --- a/MANIFEST +++ b/MANIFEST @@ -34,14 +34,12 @@ docs/development.md docs/hooks.md docs/logging.md docs/writing.md -lib/Apache/Qpsmtpd.pm lib/Qpsmtpd.pm lib/Qpsmtpd/Address.pm lib/Qpsmtpd/Auth.pm lib/Qpsmtpd/Base.pm lib/Qpsmtpd/Command.pm lib/Qpsmtpd/Config.pm -lib/Qpsmtpd/ConfigServer.pm lib/Qpsmtpd/Connection.pm lib/Qpsmtpd/Constants.pm lib/Qpsmtpd/DB.pm @@ -53,9 +51,7 @@ lib/Qpsmtpd/Postfix.pm lib/Qpsmtpd/Postfix/Constants.pm lib/Qpsmtpd/Postfix/pf2qp.pl lib/Qpsmtpd/SMTP.pm -lib/Qpsmtpd/SMTP/Prefork.pm lib/Qpsmtpd/TcpServer.pm -lib/Qpsmtpd/TcpServer/Prefork.pm lib/Qpsmtpd/Transaction.pm LICENSE log/log2sql @@ -105,7 +101,6 @@ plugins/karma plugins/karma_tool plugins/loadcheck plugins/logging/adaptive -plugins/logging/apache plugins/logging/connection_id plugins/logging/devnull plugins/logging/file @@ -150,12 +145,9 @@ plugins/virus/sophie plugins/virus/uvscan plugins/whitelist qpsmtpd -qpsmtpd-forkserver -qpsmtpd-prefork README.md README.plugins.md -run.forkserver -run.tcpserver +run STATUS t/addresses.t t/auth.t diff --git a/META.yml b/META.yml index 03e0248d..ff455fca 100644 --- a/META.yml +++ b/META.yml @@ -21,10 +21,12 @@ requires: CDB_File: 0 Data::Dumper: 0 Date::Parse: 0 + DB_File: 0 File::NFSLock: 0 File::Tail: 0 File::Temp: 0 - GeoIP2: 0 + File::NFSLock: 0 + GeoIP2: 2 IO::Socket::SSL: 0 MIME::Base64: 0 Mail::DKIM: 0 diff --git a/Makefile.PL b/Makefile.PL index 1662631e..44d209d4 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -39,7 +39,7 @@ WriteMakefile( }, ABSTRACT => 'Flexible smtpd daemon written in Perl', AUTHOR => 'Ask Bjoern Hansen ', - EXE_FILES => [qw(qpsmtpd qpsmtpd-forkserver qpsmtpd-prefork)], + EXE_FILES => [qw(qpsmtpd)], clean => { FILES => [ '*.bak', 't/tmp' ], }, ); diff --git a/README.plugins.md b/README.plugins.md index 330dab7f..78e72f2e 100644 --- a/README.plugins.md +++ b/README.plugins.md @@ -25,8 +25,7 @@ subdirectory, the directory must also be given, like the may be given in the `config/plugin_dirs` config file, one directory per line, these will be searched first before using the builtin fallback of `plugins/` relative to the qpsmtpd root directory. It may be -necessary, that the `config/plugin_dirs` must be used (if you're using -`Apache::Qpsmtpd`, for example). +necessary, that the `config/plugin_dirs` must be used. Some plugins may be configured by passing arguments in the `plugins` config file. diff --git a/config.sample/logging b/config.sample/logging index 578467a3..32977d28 100644 --- a/config.sample/logging +++ b/config.sample/logging @@ -10,9 +10,6 @@ logging/warn 6 #logging/adaptive [accept minlevel] [reject maxlevel] [prefix char] #logging/adaptive 4 6 -# send logs to apache (useful if running qpsmtpd under apache) -#logging/apache - # send logs to the great bit bucket #logging/devnull diff --git a/lib/Apache/Qpsmtpd.pm b/lib/Apache/Qpsmtpd.pm deleted file mode 100644 index bd0729c0..00000000 --- a/lib/Apache/Qpsmtpd.pm +++ /dev/null @@ -1,253 +0,0 @@ -package Apache::Qpsmtpd; - -use 5.006001; -use strict; -use warnings FATAL => 'all'; - -use Apache2::ServerUtil (); -use Apache2::Connection (); -use Apache2::Const -compile => qw(OK MODE_GETLINE); -use APR::Const -compile => qw(SO_NONBLOCK EOF SUCCESS); -use APR::Error (); -use APR::Brigade (); -use APR::Bucket (); -use APR::Socket (); -use Apache2::Filter (); -use ModPerl::Util (); - -our $VERSION = '0.02'; - -sub handler { - my Apache2::Connection $c = shift; - $c->client_socket->opt_set(APR::Const::SO_NONBLOCK => 0); - - die "\$ENV{QPSMTPD_CONFIG} must be given" unless $ENV{QPSMTPD_CONFIG}; - - my $qpsmtpd = Qpsmtpd::Apache->new(); - $qpsmtpd->start_connection( - ip => $c->remote_ip, - host => $c->remote_host, - info => undef, - conn => $c, - ); - - $qpsmtpd->run($c); - $qpsmtpd->run_hooks("post-connection"); - $qpsmtpd->connection->reset; - - return Apache2::Const::OK; -} - -package Qpsmtpd::Apache; - -use Qpsmtpd::Constants; -use base qw(Qpsmtpd::SMTP); - -my %cdir_memo; - -sub config_dir { - my ($self, $config) = @_; - if (exists $cdir_memo{$config}) { - return $cdir_memo{$config}; - } - - if (uc($ENV{QPSMTPD_CONFIG}) eq 'USE-VIRTUAL-DOMAINS') { - my $cdir = $self->{conn}->base_server->dir_config("qpsmtpd.config_dir"); - $cdir =~ /^(.*)$/; # detaint - my $configdir = $1 if -e "$1/$config"; - $cdir_memo{$config} = $configdir; - } - else { - shift; - $cdir_memo{$config} = $self->SUPER::config_dir(@_); - } - return $cdir_memo{$config}; -} - -sub start_connection { - my $self = shift; - my %opts = @_; - - $self->{conn} = $opts{conn}; - $self->{conn} - ->client_socket->timeout_set($self->config('timeout') * 1_000_000); - $self->{bb_in} = - APR::Brigade->new($self->{conn}->pool, $self->{conn}->bucket_alloc); - $self->{bb_out} = - APR::Brigade->new($self->{conn}->pool, $self->{conn}->bucket_alloc); - - my $remote_host = $opts{host} || ($opts{ip} ? "[$opts{ip}]" : "[noip!]"); - my $remote_info = $opts{info} ? "$opts{info}\@$remote_host" : $remote_host; - my $remote_ip = $opts{ip}; - - $self->log(LOGNOTICE, "Connection from $remote_info [$remote_ip]"); - - $self->SUPER::connection->start( - remote_info => $remote_info, - remote_ip => $remote_ip, - remote_host => $remote_host, - local_ip => $opts{conn}->local_ip, - @_ - ); -} - -sub config { - my $self = shift; - my ($param, $type) = @_; - if (!$type) { - my $opt = $self->{conn}->base_server->dir_config("qpsmtpd.$param"); - return $opt if defined($opt); - } - return $self->SUPER::config(@_); -} - -sub run { - my $self = shift; - - # should be somewhere in Qpsmtpd.pm and not here... - $self->load_plugins; - - my $rc = $self->start_conversation; - return if $rc != DONE; - - # this should really be the loop and read_input should just - # get one line; I think - $self->read_input(); -} - -sub getline { - my $self = shift; - my $c = $self->{conn} || die "Cannot getline without a conn"; - - return if $c->aborted; - - my $bb = $self->{bb_in}; - - while (1) { - my $rc = - $c->input_filters->get_brigade($bb, Apache2::Const::MODE_GETLINE); - return if $rc == APR::Const::EOF; - die APR::Error::strerror($rc) unless $rc == APR::Const::SUCCESS; - - next unless $bb->flatten(my $data); - - $bb->cleanup; - return $data; - } - - return ''; -} - -sub read_input { - my $self = shift; - my $c = $self->{conn}; - - while (defined(my $data = $self->getline)) { - $data =~ s/\r?\n$//s; # advanced chomp - $self->connection->notes('original_string', $data); - $self->log(LOGDEBUG, "dispatching $data"); - defined $self->dispatch(split / +/, $data, 2) - or $self->respond(502, "command unrecognized: '$data'"); - last if $self->{_quitting}; - } -} - -sub respond { - my ($self, $code, @messages) = @_; - my $c = $self->{conn}; - while (my $msg = shift @messages) { - my $bb = $self->{bb_out}; - my $line = $code . (@messages ? "-" : " ") . $msg; - $self->log(LOGDEBUG, $line); - my $bucket = APR::Bucket->new(($c->bucket_alloc), "$line\r\n"); - $bb->insert_tail($bucket); - $c->output_filters->fflush($bb); - - # $bucket->remove; - $bb->cleanup; - } - return 1; -} - -sub disconnect { - my $self = shift; - $self->SUPER::disconnect(@_); - $self->{_quitting} = 1; - $self->{conn}->client_socket->close(); -} - -1; - -__END__ - -=head1 NAME - -Apache::Qpsmtpd - a mod_perl-2 connection handler for qpsmtpd - -=head1 SYNOPSIS - - Listen 0.0.0.0:25 smtp - AcceptFilter smtp none - ## "smtp" and the AcceptFilter are required for Linux, FreeBSD - ## with apache >= 2.1.5, for others it doesn't hurt. See also - ## http://httpd.apache.org/docs/2.2/mod/core.html#acceptfilter - ## and http://httpd.apache.org/docs/2.2/mod/mpm_common.html#listen - - LoadModule perl_module modules/mod_perl.so - - - use lib qw( /path/to/qpsmtpd/lib ); - use Apache::Qpsmtpd; - $ENV{QPSMTPD_CONFIG} = "/path/to/qpsmtpd/config"; - - - - PerlModule Apache::Qpsmtpd - PerlProcessConnectionHandler Apache::Qpsmtpd - # can specify this in config/plugin_dirs if you wish: - PerlSetVar qpsmtpd.plugin_dirs /path/to/qpsmtpd/plugins - PerlSetVar qpsmtpd.loglevel 4 - - -Using multiple instances of Qpsmtpd on the same server is also -possible by setting: - - $ENV{QPSMTPD_CONFIG} = "USE-VIRTUAL-DOMAINS"; - -Then in the VirtualHost of each config define the configuration -directory: - - PerlSetVar qpsmtpd.config_dir /path/to/qpsmtpd/config - -Several different configurations can be running on the same -server. - -=head1 DESCRIPTION - -This module implements a mod_perl/apache 2.0 connection handler -that turns Apache into an SMTP server using Qpsmtpd. - -It also allows you to set single-valued config options (such -as I, as seen above) using C in F. - -This module should be considered beta software as it is not yet -widely tested. However it is currently the fastest way to run -Qpsmtpd, so if performance is important to you then consider this -module. - -=head1 BUGS - -Probably a few. Make sure you test your plugins carefully. - -The Apache scoreboard (/server-status/) mostly works and shows -connections, but could do with some enhancements specific to SMTP. - -=head1 AUTHOR - -Matt Sergeant, - -Some credit goes to for Apache::SMTP which gave -me the inspiration to do this. added the virtual -host support. - -=cut diff --git a/lib/Qpsmtpd/Base.pm b/lib/Qpsmtpd/Base.pm index 5d3e8faa..4a2da064 100644 --- a/lib/Qpsmtpd/Base.pm +++ b/lib/Qpsmtpd/Base.pm @@ -59,26 +59,6 @@ sub get_resolver { return $self->{_resolver}; } -sub get_async_resolver { - my ( $self, %args ) = @_; - return $self->{_async_resolver} if $self->{_async_resolver}; - - my $async_res; - eval 'use Net::DNS::Async'; - if ($@) { - warn "could not load Net::DNS::Async, is it installed?"; - return; - } - - my $res = Net::DNS::Resolver->new(dnsrch => 0); - $res->tcp_timeout(0); # Net::DNS::Async handles its own timeouts - $res->tcp_timeout(0); - - $self->{_async_resolver} = Net::DNS::Async->new( %args ); - $self->{_async_resolver}{Resolver} = $res; - return $self->{_async_resolver}; -} - sub resolve_a { my ($self, $name) = @_; my $q = $self->get_resolver->query($name, 'A') or return; diff --git a/lib/Qpsmtpd/ConfigServer.pm b/lib/Qpsmtpd/ConfigServer.pm deleted file mode 100644 index f2127fe2..00000000 --- a/lib/Qpsmtpd/ConfigServer.pm +++ /dev/null @@ -1,303 +0,0 @@ -package Qpsmtpd::ConfigServer; - -use base ('Danga::Client'); -use Qpsmtpd::Constants; - -use strict; - -use fields qw( - _auth - _commands - _config_cache - _connection - _transaction - _test_mode - _extras - other_fds - ); - -my $PROMPT = "Enter command: "; - -sub new { - my Qpsmtpd::ConfigServer $self = shift; - - $self = fields::new($self) unless ref $self; - $self->SUPER::new(@_); - $self->write($PROMPT); - return $self; -} - -sub max_idle_time { 3600 } # one hour - -sub process_line { - my $self = shift; - my $line = shift || return; - if ($::DEBUG > 1) { print "$$:" . ($self + 0) . "C($self->{mode}): $line"; } - local $SIG{ALRM} = sub { - my ($pkg, $file, $line) = caller(); - die "ALARM: $pkg, $file, $line"; - }; - my $prev = alarm(2); # must process a command in < 2 seconds - my $resp = eval { $self->_process_line($line) }; - alarm($prev); - if ($@) { - print STDERR "Error: $@\n"; - } - return $resp || ''; -} - -sub respond { - my $self = shift; - my (@messages) = @_; - while (my $msg = shift @messages) { - $self->write("$msg\r\n"); - } - return; -} - -sub fault { - my $self = shift; - my ($msg) = shift || "program fault - command not performed"; - print STDERR "$0 [$$]: $msg\n"; - print STDERR $name, "[$$]: Last system error: $!" - ." (Likely irelevant--debug the crashed plugin to ensure it handles \$! properly)"; - $self->respond("Error - " . $msg); - return $PROMPT; -} - -sub _process_line { - my $self = shift; - my $line = shift; - - $line =~ s/\r?\n//; - my ($cmd, @params) = split(/ +/, $line); - my $meth = "cmd_" . lc($cmd); - if (my $lookup = $self->can($meth)) { - my $resp = eval { $lookup->($self, @params); }; - if ($@) { - my $error = $@; - chomp($error); - Qpsmtpd->log(LOGERROR, "Command Error: $error"); - return $self->fault("command '$cmd' failed unexpectedly"); - } - return "$resp\n$PROMPT"; - } - else { - # No such method - i.e. unrecognized command - return $self->fault("command '$cmd' unrecognised"); - } -} - -my %helptext = ( - help => "HELP [CMD] - Get help on all commands or a specific command", - status => "STATUS - Returns status information about current connections", - list => -"LIST [LIMIT] - List the connections, specify limit or negative limit to shrink list", - kill => -"KILL (\$IP | \$REF) - Disconnect all connections from \$IP or connection reference \$REF", - pause => "PAUSE - Stop accepting new connections", - continue => "CONTINUE - Resume accepting connections", - reload => "RELOAD - Reload all plugins and config", - quit => "QUIT - Exit the config server", -); - -sub cmd_help { - my $self = shift; - my ($subcmd) = @_; - - $subcmd ||= 'help'; - $subcmd = lc($subcmd); - - if ($subcmd eq 'help') { - my $txt = join("\n", - map { substr($_, 0, index($_, "-")) } - sort values(%helptext)); - return "Available Commands:\n\n$txt\n"; - } - my $txt = $helptext{$subcmd} - || "Unrecognised help option. Try 'help' for a full list."; - return "$txt\n"; -} - -sub cmd_quit { - my $self = shift; - $self->close; -} - -sub cmd_shutdown { - exit; -} - -sub cmd_pause { - my $self = shift; - - my $other_fds = $self->OtherFds; - - $self->{other_fds} = {%$other_fds}; - %$other_fds = (); - return "PAUSED"; -} - -sub cmd_continue { - my $self = shift; - - my $other_fds = $self->{other_fds}; - - $self->OtherFds(%$other_fds); - %$other_fds = (); - return "UNPAUSED"; -} - -sub cmd_status { - my $self = shift; - - # Status should show: - # - Total time running - # - Total number of mails received - # - Total number of mails rejected (5xx) - # - Total number of mails tempfailed (5xx) - # - Avg number of mails/minute - # - Number of current connections - # - Number of outstanding DNS queries - - my $output = "Current Status as of " . gmtime() . " GMT\n\n"; - - if (defined &Qpsmtpd::Plugin::stats::get_stats) { - - # Stats plugin is loaded - $output .= Qpsmtpd::Plugin::stats->get_stats; - } - - my $descriptors = Danga::Socket->DescriptorMap; - - my $current_connections = 0; - my $current_dns = 0; - foreach my $fd (keys %$descriptors) { - my $pob = $descriptors->{$fd}; - if ($pob->isa("Qpsmtpd::PollServer")) { - $current_connections++; - } - elsif ($pob->isa("ParaDNS::Resolver")) { - $current_dns = $pob->pending; - } - } - - $output .= "Curr Connections: $current_connections / $::MAXconn\n" - . "Curr DNS Queries: $current_dns"; - - return $output; -} - -sub cmd_list { - my $self = shift; - my ($count) = @_; - - my $descriptors = Danga::Socket->DescriptorMap; - - my $list = - "Current" - . ($count ? (($count > 0) ? " Oldest $count" : " Newest " . -$count) : "") - . " Connections: \n\n"; - my @all; - foreach my $fd (keys %$descriptors) { - my $pob = $descriptors->{$fd}; - if ($pob->isa("Qpsmtpd::PollServer")) { - next unless $pob->connection->remote_ip; # haven't even started yet - push @all, - [ - $pob + 0, $pob->connection->remote_ip, - $pob->connection->remote_host, $pob->uptime - ]; - } - } - - @all = sort { $a->[3] <=> $b->[3] } @all; - if ($count) { - if ($count > 0) { - @all = @all[$#all - ($count - 1) .. $#all]; - } - else { - @all = @all[0 .. (abs($count) - 1)]; - } - } - foreach my $item (@all) { - $list .= sprintf("%x : %s [%s] Connected %0.2fs\n", - map { defined() ? $_ : '' } @$item); - } - - return $list; -} - -sub cmd_kill { - my $self = shift; - my ($match) = @_; - - return "SYNTAX: KILL (\$IP | \$REF)\n" unless $match; - - my $descriptors = Danga::Socket->DescriptorMap; - - my $killed = 0; - my $is_ip = (index($match, '.') >= 0); - foreach my $fd (keys %$descriptors) { - my $pob = $descriptors->{$fd}; - if ($pob->isa("Qpsmtpd::PollServer")) { - if ($is_ip) { - next - unless $pob->connection->remote_ip; # haven't even started yet - if ($pob->connection->remote_ip eq $match) { - $pob->write( -"550 Your connection has been killed by an administrator\r\n"); - $pob->disconnect; - $killed++; - } - } - else { - # match by ID - if ($pob + 0 == hex($match)) { - $pob->write( -"550 Your connection has been killed by an administrator\r\n"); - $pob->disconnect; - $killed++; - } - } - } - } - - return "Killed $killed connection" . ($killed > 1 ? "s" : "") . "\n"; -} - -sub cmd_dump { - my $self = shift; - my ($ref) = @_; - - return "SYNTAX: DUMP \$REF\n" unless $ref; - require Data::Dumper; - $Data::Dumper::Indent = 1; - - my $descriptors = Danga::Socket->DescriptorMap; - foreach my $fd (keys %$descriptors) { - my $pob = $descriptors->{$fd}; - if ($pob->isa("Qpsmtpd::PollServer")) { - if ($pob + 0 == hex($ref)) { - return Data::Dumper::Dumper($pob); - } - } - } - - return "Unable to find the connection: $ref. Try the LIST command\n"; -} - -1; -__END__ - -=head1 NAME - -Qpsmtpd::ConfigServer - a configuration server for qpsmtpd - -=head1 DESCRIPTION - -When qpsmtpd runs in multiplex mode it also provides a config server that you -can connect to. This allows you to view current connection statistics and other -gumph that you probably don't care about. - -=cut diff --git a/lib/Qpsmtpd/Connection.pm b/lib/Qpsmtpd/Connection.pm index 0efa829f..3b72c8b8 100644 --- a/lib/Qpsmtpd/Connection.pm +++ b/lib/Qpsmtpd/Connection.pm @@ -45,8 +45,6 @@ sub clone { $new->{_notes} = $self->{_notes} if defined $self->{_notes}; # reset the old connection object like it's done at the end of a connection - # to prevent leaks (like prefork/tls problem with the old SSL file handle - # still around) $self->reset unless $args{no_reset}; # should we generate a new id here? diff --git a/lib/Qpsmtpd/SMTP/Prefork.pm b/lib/Qpsmtpd/SMTP/Prefork.pm deleted file mode 100644 index 20b05b78..00000000 --- a/lib/Qpsmtpd/SMTP/Prefork.pm +++ /dev/null @@ -1,31 +0,0 @@ -package Qpsmtpd::SMTP::Prefork; -use Qpsmtpd::SMTP; -use Qpsmtpd::Constants; -@ISA = qw(Qpsmtpd::SMTP); - -sub dispatch { - my $self = shift; - my ($cmd) = lc shift; - - $self->{_counter}++; - - if ($cmd !~ /^(\w{1,12})$/ or !exists $self->{_commands}->{$1}) { - $self->run_hooks("unrecognized_command", $cmd, @_); - return 1; - } - $cmd = $1; - - if (1 or $self->{_commands}->{$cmd} and $self->can($cmd)) { - my ($result) = eval { $self->$cmd(@_) }; - if ($@ =~ /^disconnect_tcpserver/) { - die "disconnect_tcpserver"; - } - elsif ($@) { - $self->log(LOGERROR, "XX: $@") if $@; - } - return $result if defined $result; - return $self->fault("command '$cmd' failed unexpectedly"); - } - - return; -} diff --git a/lib/Qpsmtpd/TcpServer.pm b/lib/Qpsmtpd/TcpServer.pm index 81b7b7ae..ff157658 100644 --- a/lib/Qpsmtpd/TcpServer.pm +++ b/lib/Qpsmtpd/TcpServer.pm @@ -212,4 +212,4 @@ sub check_socket() { return 0; } -1; +1; \ No newline at end of file diff --git a/lib/Qpsmtpd/TcpServer/Prefork.pm b/lib/Qpsmtpd/TcpServer/Prefork.pm deleted file mode 100644 index 46214f96..00000000 --- a/lib/Qpsmtpd/TcpServer/Prefork.pm +++ /dev/null @@ -1,83 +0,0 @@ -package Qpsmtpd::TcpServer::Prefork; -use strict; - -use lib 'lib'; -use Qpsmtpd::Constants; - -use parent 'Qpsmtpd::SMTP::Prefork'; -use parent 'Qpsmtpd::TcpServer'; - -my $first_0; - -sub start_connection { - my $self = shift; - - #reset info - $self->{_connection} = Qpsmtpd::Connection->new(); #reset connection - $self->reset_transaction; - $self->SUPER::start_connection(@_); -} - -sub read_input { - my $self = shift; - - my $timeout = $self->config('timeoutsmtpd') # qmail smtpd control file - || $self->config('timeout') # qpsmtpd control file - || 1200; # default value - - alarm $timeout; - eval { - while () { - alarm 0; - $_ =~ s/\r?\n$//s; # advanced chomp - $self->log(LOGINFO, "dispatching $_"); - $self->connection->notes('original_string', $_); - defined $self->dispatch(split / +/, $_, 2) - or $self->respond(502, "command unrecognized: '$_'"); - alarm $timeout; - } - unless ($self->connection->notes('disconnected')) { - $self->reset_transaction; - $self->run_hooks('disconnect'); - $self->connection->notes(disconnected => 1); - } - }; - if ($@ =~ /^disconnect_tcpserver/) { - die "disconnect_tcpserver"; - } - else { - $self->run_hooks("post-connection"); - $self->connection->reset; - die "died while reading from STDIN (probably broken sender) - $@"; - } - alarm(0); -} - -sub respond { - my ($self, $code, @messages) = @_; - - if (!$self->check_socket()) { - $self->log(LOGERROR, - "Lost connection to client, cannot send response."); - return 0; - } - - while (my $msg = shift @messages) { - my $line = $code . (@messages ? "-" : " ") . $msg; - $self->log(LOGINFO, $line); - print "$line\r\n" - or ($self->log(LOGERROR, "Could not print [$line]: $!"), return 0); - } - return 1; -} - -sub disconnect { - my $self = shift; - $self->log(LOGINFO, "click, disconnecting"); - $self->SUPER::disconnect(@_); - $self->run_hooks("post-connection"); - $self->connection->reset; - die "disconnect_tcpserver"; -} - -1; diff --git a/packaging/rpm/files/README.selinux b/packaging/rpm/files/README.selinux deleted file mode 100644 index 39c015f1..00000000 --- a/packaging/rpm/files/README.selinux +++ /dev/null @@ -1,10 +0,0 @@ -If you run qpsmtpd-apache on a box with SELinux enabled, you'll need to -allow apache to listen to your SMTP port, typically port 25. - -The following command allows apache to listen on port 25: - - semanage port -m -t http_port_t -p tcp 25 - -Use the -d option to remove this permission: - - semanage port -d -t http_port_t -p tcp 25 diff --git a/packaging/rpm/files/qpsmtpd-forkserver.rc b/packaging/rpm/files/qpsmtpd-forkserver.rc index 14775e44..acf0e14b 100755 --- a/packaging/rpm/files/qpsmtpd-forkserver.rc +++ b/packaging/rpm/files/qpsmtpd-forkserver.rc @@ -1,26 +1,26 @@ #! /bin/bash # -# qpsmtpd-forkserver Start/Stop the qpsmtpd forking server +# qpsmtpd Start/Stop the qpsmtpd forking server # # chkconfig: 2345 90 60 # description: qpsmtpd is a flexible smtpd daemon written in Perl. \ # Apart from the core SMTP features, all functionality is \ # implemented in small "extension plugins" using the easy \ # to use object oriented plugin API. -# processname: qpsmtpd-forkserver +# processname: qpsmtpd # config: /etc/qpsmtpd -# pidfile: /var/run/qpsmtpd-forkserver.pid +# pidfile: /var/run/qpsmtpd.pid # Source function library. . /etc/init.d/functions -. /etc/sysconfig/qpsmtpd-forkserver +. /etc/sysconfig/qpsmtpd RETVAL=0 # See how we were called. -prog="qpsmtpd-forkserver" +prog="qpsmtpd" start() { # cleanup environment a bit. @@ -109,7 +109,7 @@ case "$1" in reload ;; status) - qpstatus qpsmtpd-forkserver + qpstatus qpsmtpd ;; condrestart) [ -f /var/lock/subsys/$prog ] && restart || : diff --git a/packaging/rpm/files/qpsmtpd-forkserver.sysconfig b/packaging/rpm/files/qpsmtpd-forkserver.sysconfig index d7a7f7c1..1da56c89 100644 --- a/packaging/rpm/files/qpsmtpd-forkserver.sysconfig +++ b/packaging/rpm/files/qpsmtpd-forkserver.sysconfig @@ -1,3 +1,3 @@ -QPSMTPD_OPTIONS="-p 25 -l 127.0.0.1 --pid-file /var/run/qpsmtpd-forkserver.pid" +QPSMTPD_OPTIONS="-p 25 -l 127.0.0.1 --pid-file /var/run/qpsmtpd.pid" export QPSMTPD_CONFIG=/etc/qpsmtpd export HOME=~smtpd diff --git a/packaging/rpm/files/qpsmtpd.conf b/packaging/rpm/files/qpsmtpd.conf deleted file mode 100644 index b46ead79..00000000 --- a/packaging/rpm/files/qpsmtpd.conf +++ /dev/null @@ -1,16 +0,0 @@ -Listen 0.0.0.0:25 smtp -AcceptFilter smtp none -## "smtp" and the AcceptFilter are required for Linux, FreeBSD -## with apache >= 2.1.5, for others it doesn't hurt. See also -## http://httpd.apache.org/docs/2.2/mod/core.html#acceptfilter -## and http://httpd.apache.org/docs/2.2/mod/mpm_common.html#listen - - - use Apache::Qpsmtpd; - $ENV{QPSMTPD_CONFIG} = "/etc/qpsmtpd"; - - - - PerlModule Apache::Qpsmtpd - PerlProcessConnectionHandler Apache::Qpsmtpd - diff --git a/packaging/rpm/qpsmtpd.spec.in b/packaging/rpm/qpsmtpd.spec.in index 60045a2b..e28f2fc9 100644 --- a/packaging/rpm/qpsmtpd.spec.in +++ b/packaging/rpm/qpsmtpd.spec.in @@ -6,7 +6,7 @@ Name: %{_package} Version: %{_version} Release: %{_release} -Summary: qpsmtpd + qpsmtpd-apache +Summary: qpsmtpd License: MIT Group: System Environment/Daemons URL: http://smtpd.develooper.com/ @@ -33,21 +33,11 @@ plugins" using the easy to use object oriented plugin API. qpsmtpd was originally written as a drop-in qmail-smtpd replacement, but now it also includes a smtp forward and a postfix "backend". -%package apache -Requires: perl(mod_perl2) -Summary: mod_perl-2 connection handler for qpsmtpd -Group: System Environment/Daemons - %package xinetd Summary: xinetd support for qpsmtpd Group: System Environment/Daemons Requires: xinetd -%description apache - -This module implements a mod_perl/apache 2.0 connection handler -that turns Apache into an SMTP server using Qpsmtpd. - %description xinetd This package contains the xinetd startup files for qpsmptd. @@ -92,10 +82,6 @@ mkdir -p ${RPM_BUILD_ROOT}%{_sysconfdir}/xinetd.d cp %{SOURCE3} ${RPM_BUILD_ROOT}%{_sysconfdir}/xinetd.d/smtp mkdir -p ${RPM_BUILD_ROOT}%{_sbindir} cp %{SOURCE4} ${RPM_BUILD_ROOT}%{_sbindir}/in.qpsmtpd -mkdir -p ${RPM_BUILD_ROOT}%{_sysconfdir}/httpd/conf.d -cp %{SOURCE5} ${RPM_BUILD_ROOT}%{_sysconfdir}/httpd/conf.d -mkdir -p $RPM_BUILD_ROOT%{_docdir}/%{name}-apache-%{version} -cp %{SOURCE6} $RPM_BUILD_ROOT%{_docdir}/%{name}-apache-%{version} [ -x /usr/lib/rpm/brp-compress ] && /usr/lib/rpm/brp-compress @@ -106,26 +92,12 @@ find ${RPM_BUILD_ROOT}%{_prefix} -type f -print | \ grep -v packaging | \ grep -v README.selinux | \ grep -v in\\.qpsmtpd | \ - grep -v /Apache | \ - grep -v /Danga | \ grep -v Qpsmtpd/PollServer.pm > %{name}-%{version}-%{release}-filelist if [ "$(cat %{name}-%{version}-%{release}-filelist)X" = "X" ] ; then echo "ERROR: EMPTY FILE LIST" exit -1 fi -find ${RPM_BUILD_ROOT}%{_prefix} -type f -print | \ - sed "s@^$RPM_BUILD_ROOT@@g" | \ - grep -v [Aa]sync | \ - grep -v packaging | \ - grep -v README.selinux | \ - grep -v /Danga | \ - grep -v Qpsmtpd/PollServer.pm | cat - %{name}-%{version}-%{release}-filelist | sort | uniq -u > %{name}-%{version}-%{release}-apache-filelist -if [ "$(cat %{name}-%{version}-%{release}-apache-filelist)X" = "X" ] ; then - echo "ERROR: EMPTY FILE LIST" - exit -1 -fi - %files -f %{name}-%{version}-%{release}-filelist %defattr(-,root,root) %doc CREDITS Changes LICENSE README.md README.plugins.md STATUS @@ -135,11 +107,6 @@ fi %attr(2750,qpsmtpd,clamav) %dir %{_localstatedir}/spool/qpsmtpd %attr(0750,smtpd,smtpd) %dir %{_localstatedir}/log/qpsmtpd -%files apache -f %{name}-%{version}-%{release}-apache-filelist -%defattr(-,root,root) -%config(noreplace) %{_sysconfdir}/httpd/conf.d/* -%doc %{_docdir}/%{name}-apache-%{version}/README.selinux - %files xinetd %defattr(-,root,root) %config(noreplace) %{_sysconfdir}/xinetd.d/smtp @@ -170,7 +137,6 @@ newer version in plugins/logging/file * Sun Jul 12 2009 0.82-0.1 - Update to latest release - don't add qpsmtpd to start-up by default -- add apache config file to qpsmtpd-apache package - remove all patches - use rpm macros for dirs - use a filelist for main package instead of a long list of files @@ -193,10 +159,6 @@ newer version in plugins/logging/file * Wed Mar 12 2008 0.43-0.3 - Makefile.PL now updated in svn, so remove hack -* Wed Mar 12 2008 0.43-0.2 -- Added qpsmtpd-prefork to qpsmtpd RPM, inc. hack to work round - deficiency in Makefile.PL - * Mon Mar 10 2008 0.43-0.1 - Updated to work with Makefile to build from svn @@ -316,7 +278,6 @@ newer version in plugins/logging/file * Sun Jun 12 2005 - avoid installing patch backup files -- split Apache::Qpsmtpd into separate package to avoid dependency hell. - fixed URL - changed group to Daemons. - Fixed installation for newer versions of ExtUtils::MakeMaker diff --git a/plugins/earlytalker b/plugins/earlytalker index 14c5d7f3..cf02c30d 100644 --- a/plugins/earlytalker +++ b/plugins/earlytalker @@ -114,16 +114,9 @@ sub register { } # /end compat - if ($qp->{conn} && $qp->{conn}->isa('Apache2::Connection')) { - require APR::Const; - APR::Const->import(qw(POLLIN SUCCESS)); - $self->register_hook('connect', 'apr_connect_handler'); - $self->register_hook('data', 'apr_data_handler'); - } - else { - $self->register_hook('connect', 'connect_handler'); - $self->register_hook('data', 'data_handler'); - } + $self->register_hook('connect', 'connect_handler'); + $self->register_hook('data', 'data_handler'); + if ($self->{_args}{'defer-reject'}) { $self->register_hook('mail', 'mail_handler') }; diff --git a/plugins/hosts_allow b/plugins/hosts_allow index 499c656c..e7643ca5 100644 --- a/plugins/hosts_allow +++ b/plugins/hosts_allow @@ -13,7 +13,7 @@ file I. The plugin takes no config/plugin arguments. -This plugin only works with the forkserver and prefork deployment models. It +This plugin only works with the forkserver deployment model. It does not work with the tcpserver deployment model. See SEE ALSO below. =head1 CONFIG diff --git a/plugins/ident/p0f b/plugins/ident/p0f index 25c05f20..522a90ac 100644 --- a/plugins/ident/p0f +++ b/plugins/ident/p0f @@ -142,7 +142,7 @@ p0f v3 requires only the remote IP. p0f v2 requires four pieces of information to look up the p0f fingerprint: local_ip, local_port, remote_ip, and remote_port. TcpServer.pm has been has been updated to provide that information when running under djb's -tcpserver. The forkserver and prefork models will likely require +tcpserver. The forkserver model will likely require some additional changes to make sure these fields are populated. =head1 ACKNOWLEDGEMENTS diff --git a/plugins/logging/apache b/plugins/logging/apache deleted file mode 100644 index b6099227..00000000 --- a/plugins/logging/apache +++ /dev/null @@ -1,113 +0,0 @@ -#!perl -w - -=head1 NAME - -logging/apache - logging plugin for qpsmtpd which logs to the apache error log - -=cut - -# more POD at the end - -use strict; -use warnings FATAL => 'all'; -use Apache2::Log; -use Apache2::RequestUtil (); - -use Qpsmtpd::Constants; - -sub register { - my ($self, $qp) = @_; - - die "Not running under Apache::Qpsmtpd" - unless ($qp->{conn} && $qp->{conn}->isa('Apache2::Connection')); - - my $rr = Apache2::RequestRec->new($self->qp->{conn}); - $self->{_log} = $rr->log - if $rr; - - $self->log(LOGINFO, 'Initializing logging::apache plugin'); -} - -sub hook_logging { - my ($self, $transaction, $trace, $hook, $plugin, @log) = @_; - - # Don't log your own log entries! If this is the only logging plugin - # then these lines will not be logged at all. You can safely comment - # out this line and it will not cause an infinite loop. - return DECLINED if defined $plugin and $plugin eq $self->plugin_name; - - unless ($self->{_log}) { - my $rr = Apache2::RequestRec->new($self->qp->{conn}); - unless ($rr) { - warn "no Apache2::RequestRec?... logmsg was: ", join(" ", @log); - return DECLINED; - } - $self->{_log} = $rr->log; - } - - # luckily apache uses the same log levels as qpsmtpd... - ($trace = lc Qpsmtpd::Constants::log_level($trace)) =~ s/^log//; - $trace = 'emerg' # ... well, nearly... - if $trace eq 'radar'; - - my $log = $self->{_log}; - unless ($log->can($trace)) { # ... but you never know if it changes - $log->emerg("Can't log with level '$trace', logmsg was: ", - join(" ", @log)); - return DECLINED; - } - - $log->$trace( - join( - " ", - $$ - . ( - defined $plugin ? " $plugin plugin:" - : defined $hook ? " running plugin ($hook):" - : "" - ), - @log - ) - ); # no \n at the end! - - return DECLINED; -} - -=head1 DESCRIPTION - -The logging/apache plugin uses the apache logging mechanism to write its -messages to the apache error log. - -=head1 INSTALL AND CONFIG - -Place this plugin in the plugin/logging directory beneath the standard -qpsmtpd installation. Edit the config/logging file and add a line like -this: - - logging/apache - -To change what is shown in the logs, change the I directive in -the virtual host config for Qpsmtpd and maybe change the I log -file: - - - PerlSetVar QpsmtpdDir /path/to/qpsmtpd - PerlModule Apache::Qpsmtpd - PerlProcessConnectionHandler Apache::Qpsmtpd - LogLevel debug - ErrorLog /var/log/apache2/qpsmtpd.log - - -=head1 AUTHOR - -Hanno Hecker - -=head1 COPYRIGHT AND LICENSE - -Copyright (c) 2007 Hanno Hecker - -This plugin is licensed under the same terms as the qpsmtpd package itself. -Please see the LICENSE file included with qpsmtpd for details. - -=cut - diff --git a/qpsmtpd b/qpsmtpd index 9a3d75b0..ef151a6b 100755 --- a/qpsmtpd +++ b/qpsmtpd @@ -1,36 +1,375 @@ #!/usr/bin/perl -Tw +use strict; # Copyright (c) 2001-2010 Ask Bjoern Hansen. See the LICENSE file for details. -# The "command dispatch" system was taken from colobus - http://trainedmonkey.com/colobus/ -# -# this is designed to be run under tcpserver (http://cr.yp.to/ucspi-tcp.html) -# or inetd if you're into that sort of thing +# The "command dispatch" system is taken from colobus - http://trainedmonkey.com/colobus/ # # For more information see http://smtpd.github.io/qpsmtpd/ # use lib 'lib'; +use Qpsmtpd::Constants; use Qpsmtpd::TcpServer; -use strict; +use IO::Socket; +use IO::Select; +use Socket; +use Getopt::Long qw(:config no_ignore_case); +use POSIX qw(:sys_wait_h :errno_h :signal_h); $| = 1; +my $has_ipv6 = Qpsmtpd::TcpServer::has_ipv6; + +# Configuration +my $MAXCONN = 15; # max simultaneous connections +my @PORT; # port number(s) +my @LOCALADDR; # ip address(es) to bind to +my $MAXCONNIP = 5; # max simultaneous connections from one IP +my $PID_FILE = ''; +my $DETACH; # daemonize on startup +my $NORDNS; + +my $USER = (getpwuid $>)[0]; # user to suid to +$USER = 'smtpd' if $USER eq 'root'; + +sub usage { + print <<"EOT"; +usage: qpsmtpd-forkserver [ options ] + -l, --listen-address addr : listen on specific address(es); can be specified + multiple times for multiple bindings. IPv6 + addresses must be inside square brackets [], and + don't need to be zero padded. + Default is [::] (if has_ipv6) or 0.0.0.0 (if not) + -p, --port P : listen on a specific port; default 2525; can be + specified multiple times for multiple bindings. + -c, --limit-connections N : limit concurrent connections to N; default 15 + -u, --user U : run as a particular user (default '$USER') + -m, --max-from-ip M : limit connections from a single IP; default 5 + --pid-file P : print main servers PID to file P + -d, --detach : detach from controlling terminal (daemonize) + -H, --no-rdns : don't perform reverse DNS lookups +EOT + exit 0; +} + +GetOptions( + 'h|help' => \&usage, + 'l|listen-address=s' => \@LOCALADDR, + 'c|limit-connections=i' => \$MAXCONN, + 'm|max-from-ip=i' => \$MAXCONNIP, + 'p|port=s' => \@PORT, + 'u|user=s' => \$USER, + 'pid-file=s' => \$PID_FILE, + 'd|detach' => \$DETACH, + 'H|no-rdns' => \$NORDNS, + ) + || &usage; + +# detaint the commandline +if ($has_ipv6) { + @LOCALADDR = ('[::]') if !@LOCALADDR; +} +else { + @LOCALADDR = ('0.0.0.0') if !@LOCALADDR; +} +@PORT = 2525 if !@PORT; + +my @LISTENADDR; +for (0 .. $#LOCALADDR) { + if ($LOCALADDR[$_] !~ /^(\[.*\]|[\d\w\-.]+)(?::(\d+))?$/) { + &usage; + } + if (defined $2) { + push @LISTENADDR, {'addr' => $1, 'port' => $2}; + next; + } + + my $addr = $1; + for (0 .. $#PORT) { + if ($PORT[$_] !~ /^(\d+)$/) { + &usage; + } + push @LISTENADDR, {'addr' => $addr, 'port' => $1}; + } +} + +if ($USER !~ /^([\w\-]+)$/) { &usage; } +$USER = $1; +if ($MAXCONN !~ /^(\d+)$/) { &usage; } +$MAXCONN = $1; + delete $ENV{ENV}; $ENV{PATH} = '/bin:/usr/bin:/var/qmail/bin'; +my %childstatus = (); + +sub REAPER { + while (defined(my $chld = waitpid(-1, WNOHANG))) { + last unless $chld > 0; + ::log(LOGINFO, "cleaning up after $chld"); + delete $childstatus{$chld}; + } +} + +sub HUNTSMAN { + $SIG{CHLD} = 'DEFAULT'; + kill 'INT' => keys %childstatus; + if ($PID_FILE && -e $PID_FILE) { + unlink $PID_FILE or ::log(LOGERROR, "unlink: $PID_FILE: $!"); + } + exit(0); +} + +$SIG{INT} = \&HUNTSMAN; +$SIG{TERM} = \&HUNTSMAN; + +my $select = new IO::Select; +my $server; + +# establish SERVER socket(s), bind and listen. +for my $listen_addr (@LISTENADDR) { + my @Socket_opts = ( + LocalPort => $listen_addr->{'port'}, + LocalAddr => $listen_addr->{'addr'}, + Proto => 'tcp', + Reuse => 1, + Blocking => 0, + Listen => SOMAXCONN + ); + + if ($has_ipv6) { + $server = IO::Socket::INET6->new(@Socket_opts) + or die +"Creating TCP socket $listen_addr->{'addr'}:$listen_addr->{'port'}: $!\n"; + } + else { + $server = IO::Socket::INET->new(@Socket_opts) + or die +"Creating TCP socket $listen_addr->{'addr'}:$listen_addr->{'port'}: $!\n"; + } + IO::Handle::blocking($server, 0); + $select->add($server); +} + +if ($PID_FILE) { + if ($PID_FILE =~ m#^(/[\w\d/\-.]+)$#) { $PID_FILE = $1 } + else { &usage } + if (-e $PID_FILE) { + open PID, "+<$PID_FILE" + or die "open pid_file: $!\n"; + my $running_pid = || ''; + chomp $running_pid; + if ($running_pid =~ /(\d+)/) { + $running_pid = $1; + if (kill 0, $running_pid) { + die "Found an already running qpsmtpd with pid $running_pid.\n"; + } + } + seek PID, 0, 0 + or die "Could not seek back to beginning of $PID_FILE: $!\n"; + truncate PID, 0 + or die "Could not truncate $PID_FILE at 0: $!"; + } + else { + open PID, ">$PID_FILE" + or die "open pid_file: $!\n"; + } +} + +# Load plugins here my $qpsmtpd = Qpsmtpd::TcpServer->new(); -$qpsmtpd->load_plugins(); -$qpsmtpd->start_connection(); -$qpsmtpd->run(\*STDIN); # pass the "socket" like -prefork/-forkserver -$qpsmtpd->run_hooks("post-connection"); -$qpsmtpd->connection->reset; -# needed for Qpsmtpd::TcpServer::check_socket(): -# emulate IO::Socket::connected on STDIN. STDIN was used instead of STDOUT -# because the other code also calls getpeername(STDIN). -sub IO::Handle::connected { return getpeername(shift) } +# Drop privileges +my (undef, undef, $quid, $qgid) = getpwnam $USER + or die "unable to determine uid/gid for $USER\n"; +my $groups = "$qgid $qgid"; +while (my ($name, $passwd, $gid, $members) = getgrent()) { + my @m = split / /, $members; + if (grep { $_ eq $USER } @m) { + $groups .= " $gid"; + } +} +endgrent; +$) = $groups; +POSIX::setgid($qgid) or die "unable to change gid: $!\n"; +POSIX::setuid($quid) or die "unable to change uid: $!\n"; +$> = $quid; -__END__ +$qpsmtpd->load_plugins; + +foreach my $addr (@LISTENADDR) { + ::log(LOGINFO, "Listening on $addr->{addr}:$addr->{port}"); +} +::log(LOGINFO, + 'Running as user ' + . (getpwuid($>) || $>) + . ', group ' + . (getgrgid($)) || $)) + ); + +if ($DETACH) { + open STDIN, '/dev/null' or die "/dev/null: $!"; + open STDOUT, '>/dev/null' or die "/dev/null: $!"; + open STDERR, '>&STDOUT' or die "open(stderr): $!"; + defined(my $pid = fork) or die "fork: $!"; + exit 0 if $pid; + POSIX::setsid or die "setsid: $!"; +} + +if ($PID_FILE) { + print PID $$, "\n"; + close PID; +} +# Populate class cached variables +$qpsmtpd->spool_dir; +$qpsmtpd->size_threshold; +$SIG{HUP} = sub { + $qpsmtpd = Qpsmtpd::TcpServer->new('restart' => 1); + $qpsmtpd->load_plugins; + $qpsmtpd->spool_dir; + $qpsmtpd->size_threshold; +}; +while (1) { + REAPER(); + my $running = scalar keys %childstatus; + if ($running >= $MAXCONN) { + ::log(LOGINFO, + "Too many connections: $running >= $MAXCONN. Waiting one second." + ); + sleep 1; + next; + } + my @ready = $select->can_read(1); + next if !@ready; + while (my $server = shift @ready) { + my ($client, $hisaddr) = $server->accept; + + next if !$hisaddr; + IO::Handle::blocking($client, 1); + + # get local/remote hostname, port and ip address + my ($port, $iaddr, $lport, $laddr, $nto_iaddr, $nto_laddr) = + $qpsmtpd->lrpip($server, $client, $hisaddr); + + my ($rc, @msg) = + $qpsmtpd->run_hooks( + 'pre-connection', + remote_ip => $nto_iaddr, + remote_port => $port, + local_ip => $nto_laddr, + local_port => $lport, + max_conn_ip => $MAXCONNIP, + child_addrs => [values %childstatus], + ); + if ($rc == DENYSOFT || $rc == DENYSOFT_DISCONNECT) { + unless ($msg[0]) { + @msg = ("Sorry, try again later"); + } + &respond_client($client, 451, @msg); + close $client; + next; + } + if ($rc == DENY || $rc == DENY_DISCONNECT) { + unless ($msg[0]) { + @msg = ("Sorry, service not available for you"); + } + &respond_client($client, 550, @msg); + close $client; + next; + } + + my $pid = safe_fork(); + if ($pid) { + + # parent + $childstatus{$pid} = $iaddr; # add to table + $running++; + close $client; + next; + } + + # child + + close $_ for $select->handles; + + $SIG{$_} = 'DEFAULT' for keys %SIG; + $SIG{ALRM} = sub { + print $client "421 Connection Timed Out\n"; + ::log(LOGINFO, "Connection Timed Out"); + exit; + }; + + # set enviroment variables + ($ENV{TCPLOCALIP}, $ENV{TCPREMOTEIP}, $ENV{TCPREMOTEHOST}) = + $qpsmtpd->tcpenv($nto_laddr, $nto_iaddr); + + # don't do this! + #$0 = "qpsmtpd-forkserver: $ENV{TCPREMOTEIP} / $ENV{TCPREMOTEHOST}"; + + ::log(LOGINFO, +"Accepted connection $running/$MAXCONN from $ENV{TCPREMOTEIP} / $ENV{TCPREMOTEHOST}" + ); + + # dup to STDIN/STDOUT + POSIX::dup2(fileno($client), 0); + POSIX::dup2(fileno($client), 1); + + $qpsmtpd->start_connection( + local_ip => $ENV{TCPLOCALIP}, + local_port => $lport, + remote_ip => $ENV{TCPREMOTEIP}, + remote_port => $port, + ); + $qpsmtpd->run($client); + $qpsmtpd->run_hooks('post-connection'); + $qpsmtpd->connection->reset; + close $client; + exit; # child + } +} + +sub log { + my ($level, $message) = @_; + $qpsmtpd->log($level, $message); +} + +sub respond_client { + my ($client, $code, @message) = @_; + $client->autoflush(1); + while (my $msg = shift @message) { + my $line = $code . (@message ? "-" : " ") . $msg; + ::log(LOGDEBUG, $line); + print $client "$line\r\n" + or (::log(LOGERROR, "Could not print [$line]: $!"), return 0); + } + return 1; +} + +### routine to protect process during fork +sub safe_fork { + + ### block signal for fork + my $sigset = POSIX::SigSet->new(SIGINT); + POSIX::sigprocmask(SIG_BLOCK, $sigset) + or die "Can't block SIGINT for fork: [$!]\n"; + + ### fork off a child + my $pid = fork; + unless (defined $pid) { + die "Couldn't fork: [$!]\n"; + } + + ### make SIGINT kill us as it did before + $SIG{INT} = 'DEFAULT'; + + ### put back to normal + POSIX::sigprocmask(SIG_UNBLOCK, $sigset) + or die "Can't unblock SIGINT for fork: [$!]\n"; + + return $pid; +} + +__END__ 1; diff --git a/qpsmtpd-forkserver b/qpsmtpd-forkserver deleted file mode 100755 index ef151a6b..00000000 --- a/qpsmtpd-forkserver +++ /dev/null @@ -1,375 +0,0 @@ -#!/usr/bin/perl -Tw -use strict; -# Copyright (c) 2001-2010 Ask Bjoern Hansen. See the LICENSE file for details. -# The "command dispatch" system is taken from colobus - http://trainedmonkey.com/colobus/ -# -# For more information see http://smtpd.github.io/qpsmtpd/ -# - -use lib 'lib'; -use Qpsmtpd::Constants; -use Qpsmtpd::TcpServer; -use IO::Socket; -use IO::Select; -use Socket; -use Getopt::Long qw(:config no_ignore_case); -use POSIX qw(:sys_wait_h :errno_h :signal_h); -$| = 1; - -my $has_ipv6 = Qpsmtpd::TcpServer::has_ipv6; - -# Configuration -my $MAXCONN = 15; # max simultaneous connections -my @PORT; # port number(s) -my @LOCALADDR; # ip address(es) to bind to -my $MAXCONNIP = 5; # max simultaneous connections from one IP -my $PID_FILE = ''; -my $DETACH; # daemonize on startup -my $NORDNS; - -my $USER = (getpwuid $>)[0]; # user to suid to -$USER = 'smtpd' if $USER eq 'root'; - -sub usage { - print <<"EOT"; -usage: qpsmtpd-forkserver [ options ] - -l, --listen-address addr : listen on specific address(es); can be specified - multiple times for multiple bindings. IPv6 - addresses must be inside square brackets [], and - don't need to be zero padded. - Default is [::] (if has_ipv6) or 0.0.0.0 (if not) - -p, --port P : listen on a specific port; default 2525; can be - specified multiple times for multiple bindings. - -c, --limit-connections N : limit concurrent connections to N; default 15 - -u, --user U : run as a particular user (default '$USER') - -m, --max-from-ip M : limit connections from a single IP; default 5 - --pid-file P : print main servers PID to file P - -d, --detach : detach from controlling terminal (daemonize) - -H, --no-rdns : don't perform reverse DNS lookups -EOT - exit 0; -} - -GetOptions( - 'h|help' => \&usage, - 'l|listen-address=s' => \@LOCALADDR, - 'c|limit-connections=i' => \$MAXCONN, - 'm|max-from-ip=i' => \$MAXCONNIP, - 'p|port=s' => \@PORT, - 'u|user=s' => \$USER, - 'pid-file=s' => \$PID_FILE, - 'd|detach' => \$DETACH, - 'H|no-rdns' => \$NORDNS, - ) - || &usage; - -# detaint the commandline -if ($has_ipv6) { - @LOCALADDR = ('[::]') if !@LOCALADDR; -} -else { - @LOCALADDR = ('0.0.0.0') if !@LOCALADDR; -} -@PORT = 2525 if !@PORT; - -my @LISTENADDR; -for (0 .. $#LOCALADDR) { - if ($LOCALADDR[$_] !~ /^(\[.*\]|[\d\w\-.]+)(?::(\d+))?$/) { - &usage; - } - if (defined $2) { - push @LISTENADDR, {'addr' => $1, 'port' => $2}; - next; - } - - my $addr = $1; - for (0 .. $#PORT) { - if ($PORT[$_] !~ /^(\d+)$/) { - &usage; - } - push @LISTENADDR, {'addr' => $addr, 'port' => $1}; - } -} - -if ($USER !~ /^([\w\-]+)$/) { &usage; } -$USER = $1; -if ($MAXCONN !~ /^(\d+)$/) { &usage; } -$MAXCONN = $1; - -delete $ENV{ENV}; -$ENV{PATH} = '/bin:/usr/bin:/var/qmail/bin'; - -my %childstatus = (); - -sub REAPER { - while (defined(my $chld = waitpid(-1, WNOHANG))) { - last unless $chld > 0; - ::log(LOGINFO, "cleaning up after $chld"); - delete $childstatus{$chld}; - } -} - -sub HUNTSMAN { - $SIG{CHLD} = 'DEFAULT'; - kill 'INT' => keys %childstatus; - if ($PID_FILE && -e $PID_FILE) { - unlink $PID_FILE or ::log(LOGERROR, "unlink: $PID_FILE: $!"); - } - exit(0); -} - -$SIG{INT} = \&HUNTSMAN; -$SIG{TERM} = \&HUNTSMAN; - -my $select = new IO::Select; -my $server; - -# establish SERVER socket(s), bind and listen. -for my $listen_addr (@LISTENADDR) { - my @Socket_opts = ( - LocalPort => $listen_addr->{'port'}, - LocalAddr => $listen_addr->{'addr'}, - Proto => 'tcp', - Reuse => 1, - Blocking => 0, - Listen => SOMAXCONN - ); - - if ($has_ipv6) { - $server = IO::Socket::INET6->new(@Socket_opts) - or die -"Creating TCP socket $listen_addr->{'addr'}:$listen_addr->{'port'}: $!\n"; - } - else { - $server = IO::Socket::INET->new(@Socket_opts) - or die -"Creating TCP socket $listen_addr->{'addr'}:$listen_addr->{'port'}: $!\n"; - } - IO::Handle::blocking($server, 0); - $select->add($server); -} - -if ($PID_FILE) { - if ($PID_FILE =~ m#^(/[\w\d/\-.]+)$#) { $PID_FILE = $1 } - else { &usage } - if (-e $PID_FILE) { - open PID, "+<$PID_FILE" - or die "open pid_file: $!\n"; - my $running_pid = || ''; - chomp $running_pid; - if ($running_pid =~ /(\d+)/) { - $running_pid = $1; - if (kill 0, $running_pid) { - die "Found an already running qpsmtpd with pid $running_pid.\n"; - } - } - seek PID, 0, 0 - or die "Could not seek back to beginning of $PID_FILE: $!\n"; - truncate PID, 0 - or die "Could not truncate $PID_FILE at 0: $!"; - } - else { - open PID, ">$PID_FILE" - or die "open pid_file: $!\n"; - } -} - -# Load plugins here -my $qpsmtpd = Qpsmtpd::TcpServer->new(); - -# Drop privileges -my (undef, undef, $quid, $qgid) = getpwnam $USER - or die "unable to determine uid/gid for $USER\n"; -my $groups = "$qgid $qgid"; -while (my ($name, $passwd, $gid, $members) = getgrent()) { - my @m = split / /, $members; - if (grep { $_ eq $USER } @m) { - $groups .= " $gid"; - } -} -endgrent; -$) = $groups; -POSIX::setgid($qgid) or die "unable to change gid: $!\n"; -POSIX::setuid($quid) or die "unable to change uid: $!\n"; -$> = $quid; - -$qpsmtpd->load_plugins; - -foreach my $addr (@LISTENADDR) { - ::log(LOGINFO, "Listening on $addr->{addr}:$addr->{port}"); -} -::log(LOGINFO, - 'Running as user ' - . (getpwuid($>) || $>) - . ', group ' - . (getgrgid($)) || $)) - ); - -if ($DETACH) { - open STDIN, '/dev/null' or die "/dev/null: $!"; - open STDOUT, '>/dev/null' or die "/dev/null: $!"; - open STDERR, '>&STDOUT' or die "open(stderr): $!"; - defined(my $pid = fork) or die "fork: $!"; - exit 0 if $pid; - POSIX::setsid or die "setsid: $!"; -} - -if ($PID_FILE) { - print PID $$, "\n"; - close PID; -} - -# Populate class cached variables -$qpsmtpd->spool_dir; -$qpsmtpd->size_threshold; - -$SIG{HUP} = sub { - $qpsmtpd = Qpsmtpd::TcpServer->new('restart' => 1); - $qpsmtpd->load_plugins; - $qpsmtpd->spool_dir; - $qpsmtpd->size_threshold; -}; - -while (1) { - REAPER(); - my $running = scalar keys %childstatus; - if ($running >= $MAXCONN) { - ::log(LOGINFO, - "Too many connections: $running >= $MAXCONN. Waiting one second." - ); - sleep 1; - next; - } - my @ready = $select->can_read(1); - next if !@ready; - while (my $server = shift @ready) { - my ($client, $hisaddr) = $server->accept; - - next if !$hisaddr; - IO::Handle::blocking($client, 1); - - # get local/remote hostname, port and ip address - my ($port, $iaddr, $lport, $laddr, $nto_iaddr, $nto_laddr) = - $qpsmtpd->lrpip($server, $client, $hisaddr); - - my ($rc, @msg) = - $qpsmtpd->run_hooks( - 'pre-connection', - remote_ip => $nto_iaddr, - remote_port => $port, - local_ip => $nto_laddr, - local_port => $lport, - max_conn_ip => $MAXCONNIP, - child_addrs => [values %childstatus], - ); - if ($rc == DENYSOFT || $rc == DENYSOFT_DISCONNECT) { - unless ($msg[0]) { - @msg = ("Sorry, try again later"); - } - &respond_client($client, 451, @msg); - close $client; - next; - } - if ($rc == DENY || $rc == DENY_DISCONNECT) { - unless ($msg[0]) { - @msg = ("Sorry, service not available for you"); - } - &respond_client($client, 550, @msg); - close $client; - next; - } - - my $pid = safe_fork(); - if ($pid) { - - # parent - $childstatus{$pid} = $iaddr; # add to table - $running++; - close $client; - next; - } - - # child - - close $_ for $select->handles; - - $SIG{$_} = 'DEFAULT' for keys %SIG; - $SIG{ALRM} = sub { - print $client "421 Connection Timed Out\n"; - ::log(LOGINFO, "Connection Timed Out"); - exit; - }; - - # set enviroment variables - ($ENV{TCPLOCALIP}, $ENV{TCPREMOTEIP}, $ENV{TCPREMOTEHOST}) = - $qpsmtpd->tcpenv($nto_laddr, $nto_iaddr); - - # don't do this! - #$0 = "qpsmtpd-forkserver: $ENV{TCPREMOTEIP} / $ENV{TCPREMOTEHOST}"; - - ::log(LOGINFO, -"Accepted connection $running/$MAXCONN from $ENV{TCPREMOTEIP} / $ENV{TCPREMOTEHOST}" - ); - - # dup to STDIN/STDOUT - POSIX::dup2(fileno($client), 0); - POSIX::dup2(fileno($client), 1); - - $qpsmtpd->start_connection( - local_ip => $ENV{TCPLOCALIP}, - local_port => $lport, - remote_ip => $ENV{TCPREMOTEIP}, - remote_port => $port, - ); - $qpsmtpd->run($client); - $qpsmtpd->run_hooks('post-connection'); - $qpsmtpd->connection->reset; - close $client; - exit; # child - } -} - -sub log { - my ($level, $message) = @_; - $qpsmtpd->log($level, $message); -} - -sub respond_client { - my ($client, $code, @message) = @_; - $client->autoflush(1); - while (my $msg = shift @message) { - my $line = $code . (@message ? "-" : " ") . $msg; - ::log(LOGDEBUG, $line); - print $client "$line\r\n" - or (::log(LOGERROR, "Could not print [$line]: $!"), return 0); - } - return 1; -} - -### routine to protect process during fork -sub safe_fork { - - ### block signal for fork - my $sigset = POSIX::SigSet->new(SIGINT); - POSIX::sigprocmask(SIG_BLOCK, $sigset) - or die "Can't block SIGINT for fork: [$!]\n"; - - ### fork off a child - my $pid = fork; - unless (defined $pid) { - die "Couldn't fork: [$!]\n"; - } - - ### make SIGINT kill us as it did before - $SIG{INT} = 'DEFAULT'; - - ### put back to normal - POSIX::sigprocmask(SIG_UNBLOCK, $sigset) - or die "Can't unblock SIGINT for fork: [$!]\n"; - - return $pid; -} - -__END__ - -1; diff --git a/qpsmtpd-prefork b/qpsmtpd-prefork deleted file mode 100755 index f05dfd25..00000000 --- a/qpsmtpd-prefork +++ /dev/null @@ -1,762 +0,0 @@ -#!/usr/bin/perl -Tw -# High performance pre-forking qpsmtpd daemon, Copyright (C) 2006 SoftScan -# http://www.softscan.co.uk -# -# Based on qpsmtpd-forkserver Copyright (C) 2001 Ask Bjoern Hansen -# See the LICENSE file for details. -# -# For more information see http://smtpd.github.io/qpsmtpd/ - -# safety guards -use strict; - -BEGIN { - # secure shell - $ENV{'PATH'} = '/bin:/usr/bin'; - delete @ENV{qw(IFS CDPATH ENV BASH_ENV)}; -} - -# includes -use IO::Socket; -use IO::Select; -use POSIX; -use IPC::Shareable(':all'); -use lib 'lib'; -use Qpsmtpd::TcpServer::Prefork; -use Qpsmtpd::Constants; -use Getopt::Long; - -use Config; -defined $Config{sig_name} || die "No signals?"; - -my $has_ipv6 = Qpsmtpd::TcpServer::has_ipv6; - -#use Time::HiRes qw(gettimeofday tv_interval); - -#get available signals -my %sig_num; -my $i = 0; -foreach my $sig_name (split(/\s/, $Config{sig_name})) { - $sig_num{$sig_name} = $i++; -} - -# version -my $VERSION = "1.0"; - -# qpsmtpd instances -my ($qpsmtpd); - -# cmd's needed by IPC -my $ipcrm = '/usr/bin/ipcrm'; -my $ipcs = '/usr/bin/ipcs'; -my $xargs = '/usr/bin/xargs'; - -# vars we need -my $chld_shmem; # shared mem to keep track of children (and their connections) -my %children; -my $chld_pool; -my $chld_busy; -my @children_term; # terminated children, their death pending processing - # by the main loop -my $select = new IO::Select; # socket(s) - -# default settings -my $pid_file; -my $d_port = 25; -my @d_addr; # default applied after getopt call - -my $debug = 0; -my $max_children = 15; # max number of child processes to spawn -my $idle_children = 5; # number of idle child processes to spawn -my $maxconnip = 10; -my $child_lifetime = 100; # number of times a child may be reused -my $loop_sleep = 15; # seconds main_loop sleeps before checking children -my $re_nice = 5; # substracted from parents current nice level -my $d_start = 0; -my $quiet = 0; -my $status = 0; -my $signal = ''; -my $pretty = 0; -my $detach = 0; -my $user; - -# help text -sub usage { - print <<"EOT"; -Usage: qpsmtpd-prefork [ options ] ---quiet : Be quiet (even errors are suppressed) ---version : Show version information ---debug : Enable debug output ---listen-address addr: Listen for connections on the address 'addr' (either - an IP address or ip:port pair). Listens on all - interfaces by default; may be specified multiple - times. ---port int : TCP port daemon should listen on (default: $d_port) ---max-from-ip int : Limit number of connections from single IP (default: $maxconnip, 0 to disable) ---children int : Max number of children that can be spawned (default: $max_children) ---idle-children int : Number of idle children to spawn (default: $idle_children, 0 to disable) ---pretty-child : Change child process name (default: 0) ---user username : User the daemon should run as ---pid-file path : Path to pid file ---renice-parent int : Subtract value from parent process nice level (default: $re_nice) ---detach : detach from controlling terminal (daemonize) ---help : This message -EOT - exit 0; -} - -# get arguments -GetOptions( - 'quiet' => \$quiet, - 'version' => sub { print "Qpsmtpd Daemon - version $VERSION\n"; exit 0; }, - 'debug' => \$debug, - 'interface|listen-address=s' => \@d_addr, - 'port=i' => \$d_port, - 'max-from-ip=i' => \$maxconnip, - 'children=i' => \$max_children, - 'idle-children=i' => \$idle_children, - 'pretty-child' => \$pretty, - 'user=s' => \$user, - 'renice-parent=i' => \$re_nice, - 'detach' => \$detach, - 'pid-file=s' => \$pid_file, - 'help' => \&usage, - ) - || &usage; - -if ($user && $user =~ /^([\w\-]+)$/) { $user = $1 } -else { &usage } - -if (@d_addr) { - for my $i (0 .. $#d_addr) { - if ($d_addr[$i] !~ /^(\[.*\]|[\d\w\-.]+)(?::(\d+))?$/) { - print STDERR "Malformed listen address '$d_addr[$i]'\n"; - &usage; - } - $d_addr[$i] = {'addr' => $1, 'port' => $2 || $d_port}; - } -} -else { - @d_addr = ({addr => $has_ipv6 ? "[::]" : "0.0.0.0", port => $d_port}); -} - -# set max from ip to max number of children if option is set to disabled -$maxconnip = $max_children if ($maxconnip == 0); - -#to fix limit counter error in plugin -$maxconnip++; - -#ensure that idle_children matches value given to max_children -$idle_children = $max_children - if (!$idle_children || $idle_children > $max_children || $idle_children < -1); -$chld_pool = $idle_children; - -if ($pid_file) { - if ($pid_file =~ m#^(/[\w\d/\-.]+)$#) { $pid_file = $1 } - else { &usage } - if (-e $pid_file) { - open PID, "+<$pid_file" - or die "open pid_file: $!\n"; - my $running_pid = || ''; - chomp $running_pid; - if ($running_pid =~ /(\d+)/) { - $running_pid = $1; - die "Found an already running qpsmtpd with pid $running_pid.\n" - if (kill 0, $running_pid); - } - seek PID, 0, 0 - or die "Could not seek back to beginning of $pid_file: $!\n"; - truncate PID, 0 - or die "Could not truncate $pid_file at 0: $!"; - } - else { - open PID, ">$pid_file" - or die "open pid_file: $!\n"; - } -} - -run(); - -#start daemon -sub run { - - # get UUID/GUID - my ($quid, $qgid, $groups); - if ($user) { - (undef, undef, $quid, $qgid) = getpwnam $user - or die "unable to determine uid/gid for $user\n"; - $groups = "$qgid $qgid"; - while (my ($name, $passwd, $gid, $members) = getgrent()) { - my @m = split(/ /, $members); - if (grep { $_ eq $user } @m) { - $groups .= " $gid"; - } - } - endgrent; - } - - for my $addr (@d_addr) { - my @Socket_opts = ( - LocalPort => $addr->{port}, - LocalAddr => $addr->{addr}, - Proto => 'tcp', - Listen => SOMAXCONN, - Reuse => 1, - ); - - # create new socket (used by clients to communicate with daemon) - my $s; - if ($has_ipv6) { - $s = IO::Socket::INET6->new(@Socket_opts); - } - else { - $s = IO::Socket::INET->new(@Socket_opts); - } - die "FATAL: Failed to open socket on $addr->{addr}:$addr->{port} ($@)" - . "\nIt may be necessary to wait 20 secs before starting daemon" - . " again." - unless $s; - $select->add($s); - } - - info( "qpsmtpd-prefork daemon, version: $VERSION, staring on host: " - . join(', ', map { "$_->{addr}:$_->{port}" } @d_addr) - . " (user: $user [$<])"); - - # reset priority - my $old_nice = getpriority(0, 0); - my $new_nice = $old_nice - $re_nice; - if ($new_nice < 20 && $new_nice > -20) { - setpriority(0, 0, $1) if ($new_nice =~ /(\-?\d+)/); - info("parent daemon nice level: $1"); - } - else { - die "FATAL: new nice level: $new_nice is not between -19 and 19 " - . "(old level = $old_nice, renice value = $re_nice)"; - } - - if ($user) { - - # change UUID/UGID - $) = $groups; - POSIX::setgid($qgid) or die "unable to change gid: $!\n"; - POSIX::setuid($quid) or die "unable to change uid: $!\n"; - $> = $quid; - die "FATAL: failed to setuid to user: $user, uid: $quid\n" - if ($> != $quid and $> != ($quid - 2**32)); - } - - # setup shared memory - $chld_shmem = shmem($d_port . "qpsmtpd", 1); - untie $chld_shmem; - - # Interrupt handler - $SIG{INT} = $SIG{TERM} = sub { - - # terminate daemon (and children) - my $sig = shift; - - # prevent another signal and disable reaper - $SIG{$sig} = $SIG{CHLD} = $SIG{HUP} = 'IGNORE'; - - # a notice, before the sleep below - info("shutting down"); - - # close socket(s) - $_->close for $select->handles; - - # send signal to process group - kill -$sig_num{$sig} => $$; - - # cleanup - IPC::Shareable->clean_up; - unlink($pid_file) if $pid_file; - - info("shutdown of daemon"); - exit; - }; - - # Hup handler - $SIG{HUP} = sub { - - # reload qpmstpd plugins - $qpsmtpd = qpsmtpd_instance('restart' => 1); # reload plugins... - kill 'HUP' => keys %children; - info("reload daemon requested"); - }; - - # setup qpsmtpd_instance - $qpsmtpd = qpsmtpd_instance(); - - if ($detach) { - open STDIN, '/dev/null' or die "/dev/null: $!"; - open STDOUT, '>/dev/null' or die "/dev/null: $!"; - open STDERR, '>&STDOUT' or die "open(stderr): $!"; - defined(my $pid = fork) or die "fork: $!"; - exit 0 if $pid; - } - POSIX::setsid or die "setsid: $!"; - - if ($pid_file) { - print PID $$, "\n"; - close PID; - } - - # child reaper - $SIG{CHLD} = \&reaper; - spawn_children(); - main_loop(); - exit; -} - -# initialize children (only done at daemon startup) -sub spawn_children { - - # block signals while new children are being spawned - my $sigset = block_signal(SIGCHLD); - for (1 .. $chld_pool) { - new_child(); - } - - # reset block signals - unblock_signal($sigset); -} - -# cleanup after child dies -sub reaper { - my $stiff; - while (($stiff = waitpid(-1, &WNOHANG)) > 0) { - my $res = WEXITSTATUS($?); - info("child terminated, pid: $stiff (status $?, res: $res)"); - delete $children{$stiff}; # delete pid from children - # add pid to array so it later can be removed from shared memory - push @children_term, $stiff; - } - - $SIG{CHLD} = \&reaper; -} - -#main_loop: main loop. Either processes children that have exited or -# periodically scans the shared memory for children that are not longer -# alive. Spawns new children when necessary. -#arg0: void -#ret0: void -sub main_loop { - my $created_children = $idle_children; - while (1) { - - # if there is no child death to process, then sleep EXPR seconds - # or until signal (i.e. child death) is received - sleep $loop_sleep / ($created_children * 2 + 1) unless @children_term; - - # block CHLD signals to avoid race - my $sigset = block_signal(SIGCHLD); - - # get number of busy children - if (@children_term) { - - # remove dead children info from shared memory - $chld_busy = shmem_opt(undef, \@children_term, undef, undef); - @children_term = (); - } - else { - # just check the shared memory - $chld_busy = shmem_opt(undef, undef, undef, undef, 1); - } - - # calculate children in pool (if valid busy children number) - if (defined($chld_busy)) { - info("busy children: $chld_busy"); - $chld_pool = $chld_busy + $idle_children; - - # ensure pool limit is max_children - $chld_pool = $max_children if ($chld_pool > $max_children); - info( "children pool: $chld_pool, spawned: " - . scalar(keys %children) - . ", busy: $chld_busy"); - } - else { - - # reset shared memory - warn("unable to access shared memory - resetting it"); - IPC::Shareable->clean_up; - my $shmem = shmem($d_port . "qpsmtpd", 1); - untie $shmem; - } - - # spawn children - $created_children = $chld_pool - keys %children; - $created_children = 0 if $created_children < 0; - new_child() for 1 .. $created_children; - - # unblock signals - unblock_signal($sigset); - } -} - -# block_signal: block signals -# arg0..n: int with signal(s) to block -# ret0: ref str with sigset (used to later unblock signal) -sub block_signal { - my @signal = @_; #arg0..n - - my ($sigset, $blockset); - - $sigset = POSIX::SigSet->new(); - $blockset = POSIX::SigSet->new(@signal); - sigprocmask(SIG_BLOCK, $blockset, $sigset) - or die "Could not block @signal signals: $!\n"; - - return $sigset; -} - -# unblock_signal: unblock/reset and receive pending signals -# arg0: ref str with sigset -# ret0: void -sub unblock_signal { - my $sigset = shift; # arg0 - sigprocmask(SIG_SETMASK, $sigset) - or die "Could not restore signals: $!\n"; -} - -# new_child: initialize new child -# arg0: void -# ret0: void -sub new_child { - - # daemonize away from the parent process - my $pid; - die "Cannot fork child: $!\n" unless defined($pid = fork); - if ($pid) { - - # in parent - $children{$pid} = 1; - info("new child, pid: $pid"); - return; - } - - # in child - - # reset priority - setpriority 0, 0, getpriority(0, 0) + $re_nice; - - # reset signals - my $sigset = POSIX::SigSet->new(); - my $blockset = POSIX::SigSet->new(SIGCHLD); - sigprocmask(SIG_UNBLOCK, $blockset, $sigset) - or die "Could not unblock SIGCHLD signal: $!\n"; - $SIG{CHLD} = $SIG{INT} = $SIG{TERM} = $SIG{ALRM} = 'DEFAULT'; - - # child should exit if it receives HUP signal (note: blocked while child - # is busy, but restored once done) - $SIG{HUP} = sub { - info("signal HUP received, going to exit"); - exit; - }; - - # continue to accept connections until "old age" is reached - for (my $i = 0 ; $i < $child_lifetime ; $i++) { - - # accept a connection - if ($pretty) { - $ENV{PROCESS} = $0 if not defined $ENV{PROCESS}; # 1st time only - $0 = 'qpsmtpd child'; # set pretty child name in process listing - } - my @ready = $select->can_read(); - next unless @ready; - my $socket = $ready[0]; - my ($client, $iinfo) = $socket->accept() - or die - "failed to create new object - $!"; # wait here until client connects - info("connect from: " . $client->peerhost . ":" . $client->peerport); - - # clear a previously running instance by creating a new instance - $qpsmtpd = qpsmtpd_instance(); - - # set STDIN/STDOUT and autoflush - # ... no longer use POSIX::dup2: it failes after a few - # million connections - close(STDIN); - open(STDIN, "+<&" . fileno($client)) - or die "unable to duplicate filehandle to STDIN - $!"; - - close(STDOUT); - open(STDOUT, "+>&" . fileno($client)) - or die "unable to duplicate filehandle to STDOUT - $!"; - select(STDOUT); - $| = 1; - - # connection recieved, block signals - my $sigset = block_signal(SIGHUP); - - # start a session if connection looks valid - qpsmtpd_session($socket, $client, $iinfo, $qpsmtpd) if ($iinfo); - - # close connection and cleanup - $client->shutdown(2); - - # unset block and receive pending signals - unblock_signal($sigset); - } - exit; # this child has reached its end-of-life -} - -# respond to client -# arg0: ref to socket object (client) -# arg1: int with SMTP reply code -# arg2: arr with message -# ret0: int 0|1 (0 = failure, 1 = success) -sub respond_client { - my ($client, $code, @message) = @_; - $client->autoflush(1); - while (my $msg = shift @message) { - my $line = $code . (@message ? "-" : " ") . $msg; - info("reply to client: <$line>"); - print $client "$line\r\n" - or (info("Could not print [$line]: $!"), return 0); - } - return 1; -} - -# qpsmtpd_instance: setup qpsmtpd instance -# arg0: void -# ret0: ref to qpsmtpd_instance -sub qpsmtpd_instance { - my %args = @_; - my $qpsmtpd = Qpsmtpd::TcpServer::Prefork->new(%args); - $qpsmtpd->spool_dir; - $qpsmtpd->size_threshold; - - return $qpsmtpd; -} - -# shmem: tie to shared memory hash -# arg0: str with glue -# arg1: int 0|1 (0 = don't create shmem, 1 = create shmem) -# ret0: ref to shared hash -sub shmem { - my $glue = shift; #arg0 - my $create = shift || 0; #arg1 - - my %options = ( - create => $create, - exclusive => 0, - mode => 0640, - destroy => 0, - ); - - my %shmem_hash; - eval { - tie %shmem_hash, 'IPC::Shareable', $glue, {%options} - || die "unable to tie to shared memory - $!"; - }; - if ($@) { - info("$@"); - return; - } - - return \%shmem_hash; -} - -# shmem_opt: connect to shared memory and perform options -# arg0: ref to hash where shared memory should be copied to -# arg1: ref to arr with pid(s) to delete -# arg2: int with pid to add (key) -# arg3: str with packed iaddr to add (value) -# arg4: int 0|1 check and cleanup shared memory (0 = no, 1 = yes - default 0) -# ret0: int with number of busy children (undef if error) -sub shmem_opt { - my $ref_shmem = shift; #arg0 - my $ref_pid_del = shift; #arg1 - my $pid_add_key = shift; #arg2 - my $pid_add_value = shift; #arg3 - my $check = shift || 0; #arg4 - - # check arguments - if ( (defined($pid_add_key) && !defined($pid_add_value)) - || (!defined($pid_add_key) && defined($pid_add_value))) - { - return; - } - - my ($chld_shmem, $chld_busy); - eval { - $chld_shmem = - &shmem($d_port . "qpsmtpd", 0); #connect to shared memory hash - - if (tied %{$chld_shmem}) { - - # lock shared memory - eval { - # ensure that hung shared memory is noticed - local $SIG{ALRM} = sub { - die "locking timed out\n"; - }; - alarm 15; - - (tied %{$chld_shmem})->shlock(LOCK_EX); - - alarm 0; - }; - die $@ if $@; - - # delete - if ($ref_pid_del) { - foreach my $pid_del (@{$ref_pid_del}) { - delete $$chld_shmem{$pid_del}; - } - } - - # add - $$chld_shmem{$pid_add_key} = $pid_add_value if ($pid_add_key); - - # copy - %{$ref_shmem} = %{$chld_shmem} if ($ref_shmem); - - # check - if ($check) { - - # loop through pid list and delete orphaned processes - foreach my $pid (keys %{$chld_shmem}) { - if (!kill 0, $pid) { - delete $$chld_shmem{$pid}; - warn("orphaned child, pid: $pid removed from memory"); - } - } - } - - # number of busy children - $chld_busy = scalar(keys %{$chld_shmem}); - - # unlock shared memory - (tied %{$chld_shmem})->shunlock; - - # untie from shared memory - untie $chld_shmem || die "unable to untie from shared memory"; - } - else { - die "failed to connect to shared memory"; - } - }; - - # check for error - if ($@) { - undef($chld_busy); - warn("$@"); - } - - return $chld_busy; -} - -# info: write info -# arg0: str with debug text -sub info { - my $text = shift; #arg0 - return if (!$debug); - - my ($sec, $min, $hour, $mday, $mon, $year) = localtime(time); - my $nowtime = sprintf "%02d/%02d/%02d %02d:%02d:%02d", $mday, $mon + 1, - $year + 1900, $hour, $min, $sec; - - chomp($text); - print STDERR "$nowtime:$$: $text\n"; -} - -# start qpmstpd session -# arg0: ref to socket object -# arg1: ref to socket object -# arg2: ref to qpsmtpd instance -# ret0: void -sub qpsmtpd_session { - my $socket = shift; #arg0 - my $client = shift; #arg1 - my $iinfo = shift; #arg2 - my $qpsmtpd = shift; #arg3 - - # get local/remote hostname, port and ip address - my ($port, $iaddr, $lport, $laddr, $nto_iaddr, $nto_laddr) = - $qpsmtpd->lrpip($socket, $client, $iinfo); - - # get current connected ip addresses (from shared memory) - my %children; - shmem_opt(\%children, undef, $$, $iaddr); - - my ($rc, @msg) = - $qpsmtpd->run_hooks( - "pre-connection", - remote_ip => $nto_iaddr, - remote_port => $port, - local_ip => $nto_laddr, - local_port => $lport, - max_conn_ip => $maxconnip, - child_addrs => [values %children], - ); - if ( $rc == DENYSOFT - || $rc == DENYSOFT_DISCONNECT - || $rc == DENY - || $rc == DENY_DISCONNECT) - { - #smtp return code to reply client with (seed with soft deny) - my $rc_reply = 451; - unless ($msg[0]) { - if ($rc == DENYSOFT || $rc == DENYSOFT_DISCONNECT) { - @msg = ("Sorry, try again later"); - } - else { - @msg = ("Sorry, service not available to you"); - $rc_reply = 550; - } - } - respond_client($client, $rc_reply, @msg); - - # remove pid from shared memory - shmem_opt(undef, [$$], undef, undef); - - # retur so child can be reused - return; - } - - # all children should have different seeds, to prevent conflicts - srand(time ^ ($$ + ($$ << 15))); - - # ALRM handler - $SIG{ALRM} = sub { - print $client "421 Connection Timed Out\n"; - info("Connection Timed Out"); - - # child terminates - exit; - }; - - # set enviroment variables - ($ENV{TCPLOCALIP}, $ENV{TCPREMOTEIP}, $ENV{TCPREMOTEHOST}) = - $qpsmtpd->tcpenv($nto_laddr, $nto_iaddr); - - # run qpmsptd functions - $SIG{__DIE__} = 'DEFAULT'; - eval { - $qpsmtpd->start_connection( - local_ip => $ENV{TCPLOCALIP}, - local_port => $lport, - remote_ip => $ENV{TCPREMOTEIP}, - remote_port => $client->peerport, - ); - $qpsmtpd->run($client); - $qpsmtpd->run_hooks("post-connection"); - $qpsmtpd->connection->reset; - }; - if ($@ !~ /^(disconnect_tcpserver|died while reading from STDIN)/) { - warn("$@"); - } - - # child is now idle again - info("disconnect from: $nto_iaddr:$port"); - - # remove pid from shared memory - unless (defined(shmem_opt(undef, [$$], undef, undef))) { - - # exit because parent is down or shared memory is corrupted - info("parent seems to be down, going to exit"); - exit 1; - } -} diff --git a/run.forkserver b/run similarity index 100% rename from run.forkserver rename to run diff --git a/run.tcpserver b/run.tcpserver deleted file mode 100755 index d5b4c995..00000000 --- a/run.tcpserver +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/sh -# -QPUSER=smtpd -# limit qpsmtpd to 300MB memory -MAXRAM=300000000 -BIN=/usr/local/bin -PERL=/usr/bin/perl - -IP=`head -1 config/IP` -PORT=25 - -LANG=C -QMAILDUID=`id -u $QPUSER` -NOFILESGID=`id -g $QPUSER` - -# See also: http://wiki.qpsmtpd.org/deploy:start - -# -exec $BIN/softlimit -m $MAXRAM \ - $BIN/tcpserver -c 10 -v -R -p \ - -u $QMAILDUID -g $NOFILESGID $IP $PORT \ - ./qpsmtpd 2>&1 -# - diff --git a/t/qpsmtpd-base.t b/t/qpsmtpd-base.t index 20f5148e..2d443024 100644 --- a/t/qpsmtpd-base.t +++ b/t/qpsmtpd-base.t @@ -17,7 +17,6 @@ __tildeexp(); __is_localhost(); __is_valid_ip(); __get_resolver(); -__get_async_resolver(); __resolve_a(); __resolve_aaaa(); __resolve_mx(); @@ -63,14 +62,6 @@ sub __get_resolver { } -sub __get_async_resolver { - eval 'use Net::DNS::Async'; - return if ($@); - my $res = $base->get_async_resolver() or return; - isa_ok( $res, 'Net::DNS::Async', "resolver object, $res"); - isa_ok( $res->{Resolver}, 'Net::DNS::Resolver', "resolver object, $res"); -} - sub __resolve_a { my @r = $base->resolve_a('simerson.net'); ok(@r, "resolve_a: " . join(',', @r)); diff --git a/xt/01-syntax.t b/xt/01-syntax.t index 842b9715..50d687b9 100644 --- a/xt/01-syntax.t +++ b/xt/01-syntax.t @@ -31,10 +31,6 @@ sub test_syntax { ok(0 == 0, "skipping $f, I couldn't load w/o $1"); return; } - if ($r =~ /^Base class package "Danga::Socket" is empty/) { - ok(0 == 0, "skipping $f, Danga::Socket not available."); - return; - } print "ec: $exit_code, r: $r\n"; }