From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.2 (2018-09-13) on dcvr.yhbt.net X-Spam-Level: X-Spam-ASN: X-Spam-Status: No, score=-3.3 required=3.0 tests=ALL_TRUSTED,AWL,BAYES_00, NORMAL_HTTP_TO_IP,NUMERIC_HTTP_ADDR,T_SCC_BODY_TEXT_LINE shortcircuit=no autolearn=ham autolearn_force=no version=3.4.2 Received: from localhost (dcvr.yhbt.net [127.0.0.1]) by dcvr.yhbt.net (Postfix) with ESMTP id DEDF91F4D8 for ; Thu, 5 May 2022 10:55:37 +0000 (UTC) From: Eric Wong To: meta@public-inbox.org Subject: [PATCH] public-inbox-netd: a multi-protocol superserver Date: Thu, 5 May 2022 10:55:37 +0000 Message-Id: <20220505105537.20576-1-e@80x24.org> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit List-Id: Since we'll be adding POP3 as our 4th network protocol; asking admins to run yet another daemon on top of existing -httpd, -nntpd, -imapd is a burden and a waste of system resources given the amount of code and file handles shared. The goal of public-inbox-netd is to be able to replace all existing read-only daemons with a single process to save memory and reduce administrative overhead; hopefully encouraging more users to self-host their own mirrors. It's barely-tested at the moment. Eventually, multiple PI_CONFIG and HOME directories will be supported, as are per-listener .psgi config files. --- Documentation/public-inbox-netd.pod | 81 +++++++++++++++++++++++++++ MANIFEST | 3 + lib/PublicInbox/Daemon.pm | 78 +++++++++++++++++++------- lib/PublicInbox/HTTPD.pm | 48 +++++++++++++--- lib/PublicInbox/IMAPD.pm | 5 +- lib/PublicInbox/NNTPD.pm | 5 +- script/public-inbox-httpd | 51 ++--------------- script/public-inbox-imapd | 12 +--- script/public-inbox-netd | 6 ++ script/public-inbox-nntpd | 15 ++--- t/netd.t | 85 +++++++++++++++++++++++++++++ 11 files changed, 290 insertions(+), 99 deletions(-) create mode 100644 Documentation/public-inbox-netd.pod create mode 100755 script/public-inbox-netd create mode 100644 t/netd.t diff --git a/Documentation/public-inbox-netd.pod b/Documentation/public-inbox-netd.pod new file mode 100644 index 00000000..d6520608 --- /dev/null +++ b/Documentation/public-inbox-netd.pod @@ -0,0 +1,81 @@ +=head1 NAME + +public-inbox-netd - read-only network daemon for sharing public-inboxes + +=head1 SYNOPSIS + + public-inbox-netd [OPTIONS] + +=head1 DESCRIPTION + +public-inbox-netd provides a read-only HTTP/IMAP/NNTP/POP3 daemon for +public-inbox. It uses options and environment variables common +to all L implementations. + +The default configuration will never require write access +to the directory where the public-inbox is stored, so it +may be run as a different user than the user running +L, L, or +L. + +=head1 OPTIONS + +See common options in L. + +=over + +=item -l PROTO://ADDRESS/?cert=/path/to/cert,key=/path/to/key + +=item --listen PROTO://ADDRESS/?cert=/path/to/cert,key=/path/to/key + +In addition to the normal C<-l>/C<--listen> switch described in +L, the protocol prefix (e.g. C or +C) may be specified to force a given protocol. + +=item --cert /path/to/cert + +The default TLS certificate for optional TLS support +if the C option is not given with C<--listen>. + +=item --key /path/to/key + +The default private TLS certificate key for optional TLS support +if the C option is not given with C<--listen>. The private +key may concatenated into the path used by C<--cert>, in which case this +option is not needed. + +=back + +=head1 CONFIGURATION + +These configuration knobs should be used in the +L. + +=over 8 + +=item publicinbox..newsgroup + +=item publicinbox.nntpserver + +=back + +See L for documentation on them. + +=head1 CONTACT + +Feedback welcome via plain-text mail to L + +The mail archives are hosted at L, and +L, +L + +=head1 COPYRIGHT + +Copyright all contributors L + +License: AGPL-3.0+ L + +=head1 SEE ALSO + +L, L, L, +L diff --git a/MANIFEST b/MANIFEST index c22d943b..46979908 100644 --- a/MANIFEST +++ b/MANIFEST @@ -80,6 +80,7 @@ Documentation/public-inbox-index.pod Documentation/public-inbox-init.pod Documentation/public-inbox-learn.pod Documentation/public-inbox-mda.pod +Documentation/public-inbox-netd.pod Documentation/public-inbox-nntpd.pod Documentation/public-inbox-overview.pod Documentation/public-inbox-purge.pod @@ -363,6 +364,7 @@ script/public-inbox-index script/public-inbox-init script/public-inbox-learn script/public-inbox-mda +script/public-inbox-netd script/public-inbox-nntpd script/public-inbox-purge script/public-inbox-watch @@ -502,6 +504,7 @@ t/msgmap.t t/msgtime.t t/multi-mid.t t/net_reader-imap.t +t/netd.t t/nntp.t t/nntpd-tls.t t/nntpd-v2.t diff --git a/lib/PublicInbox/Daemon.pm b/lib/PublicInbox/Daemon.pm index 50523586..13cce1ec 100644 --- a/lib/PublicInbox/Daemon.pm +++ b/lib/PublicInbox/Daemon.pm @@ -1,4 +1,4 @@ -# Copyright (C) 2015-2021 all contributors +# Copyright (C) all contributors # License: AGPL-3.0+ # # Contains common daemon code for the httpd, imapd, and nntpd servers @@ -75,18 +75,34 @@ sub accept_tls_opt ($) { { SSL_server => 1, SSL_startHandshake => 0, SSL_reuse_ctx => $ctx }; } -sub daemon_prepare ($) { - my ($default_listen) = @_; +sub load_mod ($) { + my ($scheme) = @_; + my $modc = "PublicInbox::\U$1"; + my $mod = $modc.'D'; + eval "require $mod"; # IMAPD|HTTPD|NNTPD|POP3D + die $@ if $@; + my %xn = map { $_ => $mod->can($_) } qw(refresh post_accept); + $xn{tlsd} = $mod->new if $mod->can('refresh_groups'); #!HTTPD + my $tlsd = $xn{tlsd}; + $xn{refresh} //= sub { $tlsd->refresh_groups(@_) }; + $xn{post_accept} //= sub { $modc->new($_[0], $tlsd) }; + $xn{af_default} = 'httpready' if $modc eq 'PublicInbox::HTTP'; + \%xn; +} + +sub daemon_prepare ($$) { + my ($default_listen, $xnetd) = @_; my $listener_names = {}; # sockname => IO::Handle $oldset = PublicInbox::DS::block_signals(); @CMD = ($0, @ARGV); my ($prog) = ($CMD[0] =~ m!([^/]+)\z!g); + my $dh = defined($default_listen) ? " (default: $default_listen)" : ''; my $help = <{$l} = load_mod($1); next if $listener_names->{$l}; # already inherited my (%o, $sock_pkg); @@ -585,14 +605,25 @@ sub defer_accept ($$) { } } -sub daemon_loop ($$$$) { - my ($refresh, $post_accept, $tlsd, $af_default) = @_; +sub daemon_loop ($) { + my ($xnetd) = @_; + my $refresh = sub { + my ($sig) = @_; + for my $xn (values %$xnetd) { + eval { $xn->{refresh}->($sig) }; + warn "refresh $@\n" if $@; + } + }; my %post_accept; while (my ($k, $v) = each %tls_opt) { - if ($k =~ s!\A(?:https|imaps|nntps)://!!) { - $post_accept{$k} = tls_start_cb($v, $post_accept); - } elsif ($tlsd) { # STARTTLS, $k eq '' is OK - $tlsd->{accept_tls} = $v; + my $l = $k; + $l =~ s!\A([^:]+)://!!; + my $scheme = $1; + my $xn = $xnetd->{$l} // $xnetd->{''}; + if ($scheme =~ s!\A(?:https|imaps|nntps)!!) { + $post_accept{$l} = tls_start_cb($v, $xn->{post_accept}); + } elsif ($xn->{tlsd}) { # STARTTLS, $k eq '' is OK + $xn->{tlsd}->{accept_tls} = $v; } } my $sig = { @@ -620,22 +651,29 @@ sub daemon_loop ($$$$) { $uid = $gid = undef; reopen_logs(); @listeners = map {; - my $tls_cb = $post_accept{sockname($_)}; + my $l = sockname($_); + my $tls_cb = $post_accept{$l}; + my $xn = $xnetd->{$l} // $xnetd->{''}; # NNTPS, HTTPS, HTTP, IMAPS and POP3S are client-first traffic # IMAP, NNTP and POP3 are server-first - defer_accept($_, $tls_cb ? 'dataready' : $af_default); + defer_accept($_, $tls_cb ? 'dataready' : $xn->{af_default}); # this calls epoll_create: - PublicInbox::Listener->new($_, $tls_cb || $post_accept) + PublicInbox::Listener->new($_, $tls_cb || $xn->{post_accept}) } @listeners; PublicInbox::DS::event_loop($sig, $oldset); } -sub run ($$$;$) { - my ($default, $refresh, $post_accept, $tlsd) = @_; - daemon_prepare($default); - my $af_default = $default =~ /:8080\z/ ? 'httpready' : undef; +sub run { + my ($default_listen) = @_; + my $xnetd = {}; + if ($default_listen) { + $default_listen =~ /\A(http|imap|nntp|pop3)/ or + die "BUG: $default_listen"; + $xnetd->{''} = load_mod($1); + } + daemon_prepare($default_listen, $xnetd); my $for_destroy = daemonize(); # localize GCF2C for tests: @@ -643,7 +681,7 @@ sub run ($$$;$) { local $PublicInbox::Git::async_warn = 1; local $SIG{__WARN__} = PublicInbox::Eml::warn_ignore_cb(); - daemon_loop($refresh, $post_accept, $tlsd, $af_default); + daemon_loop($xnetd); PublicInbox::DS->Reset; # ->DESTROY runs when $for_destroy goes out-of-scope } diff --git a/lib/PublicInbox/HTTPD.pm b/lib/PublicInbox/HTTPD.pm index 02f424c6..715e4538 100644 --- a/lib/PublicInbox/HTTPD.pm +++ b/lib/PublicInbox/HTTPD.pm @@ -1,14 +1,15 @@ -# Copyright (C) 2016-2021 all contributors +# Copyright (C) all contributors # License: AGPL-3.0+ # wraps a listen socket for HTTP and links it to the PSGI app in # public-inbox-httpd package PublicInbox::HTTPD; +use v5.10.1; use strict; -use warnings; -use Plack::Util; +use Plack::Util (); +use Plack::Builder; +use PublicInbox::HTTP; use PublicInbox::HTTPD::Async; -use PublicInbox::Daemon; sub pi_httpd_async { PublicInbox::HTTPD::Async->new(@_) } @@ -41,10 +42,41 @@ sub new { # detect when to use async paths for slow blobs 'pi-httpd.async' => \&pi_httpd_async ); - bless { - app => $app, - env => \%env - }, $class; + bless { app => $app, env => \%env }, $class; +} + +my %httpds; # per-listen-FD mapping for HTTPD->{env}->{SERVER_} +my $default_app; # ugh... + +sub refresh { + if (@main::ARGV) { + eval { $default_app = Plack::Util::load_psgi(@ARGV) }; + if ($@) { + die $@, +"$0 runs in /, command-line paths must be absolute\n"; + } + } else { + require PublicInbox::WWW; + my $www = PublicInbox::WWW->new; + $www->preload; + $default_app = builder { + eval { enable 'ReverseProxy' }; + $@ and warn <call(@_) }; + }; + } + %httpds = (); # invalidate cache +} + +sub post_accept { # Listener->{post_accept} + my ($client, $addr, $srv) = @_; # $_[3] - tls_wrap (unused) + my $httpd = $httpds{fileno($srv)} //= + __PACKAGE__->new($srv, $default_app, $client); + PublicInbox::HTTP->new($client, $addr, $httpd), } 1; diff --git a/lib/PublicInbox/IMAPD.pm b/lib/PublicInbox/IMAPD.pm index 6aa3d12f..661d6537 100644 --- a/lib/PublicInbox/IMAPD.pm +++ b/lib/PublicInbox/IMAPD.pm @@ -1,14 +1,15 @@ -# Copyright (C) 2020-2021 all contributors +# Copyright (C) all contributors # License: AGPL-3.0+ # represents an IMAPD (currently a singleton), # see script/public-inbox-imapd for how it is used package PublicInbox::IMAPD; use strict; +use v5.10.1; use PublicInbox::Config; use PublicInbox::ConfigIter; use PublicInbox::InboxIdle; -use PublicInbox::IMAP; +use PublicInbox::IMAPdeflate; # loads PublicInbox::IMAP use PublicInbox::DummyInbox; my $dummy = bless { uidvalidity => 0 }, 'PublicInbox::DummyInbox'; diff --git a/lib/PublicInbox/NNTPD.pm b/lib/PublicInbox/NNTPD.pm index 1e4ddd18..0350830b 100644 --- a/lib/PublicInbox/NNTPD.pm +++ b/lib/PublicInbox/NNTPD.pm @@ -1,14 +1,15 @@ -# Copyright (C) 2016-2021 all contributors +# Copyright (C) all contributors # License: AGPL-3.0+ # represents an NNTPD (currently a singleton), # see script/public-inbox-nntpd for how it is used package PublicInbox::NNTPD; use strict; -use warnings; +use v5.10.1; use Sys::Hostname; use PublicInbox::Config; use PublicInbox::InboxIdle; +use PublicInbox::NNTPdeflate; # loads PublicInbox::NNTP sub new { my ($class) = @_; diff --git a/script/public-inbox-httpd b/script/public-inbox-httpd index a4dd8099..caceae20 100755 --- a/script/public-inbox-httpd +++ b/script/public-inbox-httpd @@ -1,51 +1,8 @@ -#!/usr/bin/perl -w -# Copyright (C) 2016-2021 all contributors +#!perl -w +# Copyright (C) all contributors # License: AGPL-3.0+ # # Standalone HTTP server for public-inbox. -use strict; -use v5.10.1; +use v5.12; use PublicInbox::Daemon; -BEGIN { - for (qw(Plack::Builder Plack::Util)) { - eval("require $_") or die "E: Plack is required for $0\n"; - } - Plack::Builder->import; - require PublicInbox::HTTP; - require PublicInbox::HTTPD; -} - -my %httpds; # per-listen-FD mapping for HTTPD->{env}->{SERVER_} -my $app; -my $refresh = sub { - if (@ARGV) { - eval { $app = Plack::Util::load_psgi(@ARGV) }; - if ($@) { - die $@, -"$0 runs in /, command-line paths must be absolute\n"; - } - } else { - require PublicInbox::WWW; - my $www = PublicInbox::WWW->new; - $www->preload; - $app = builder { - eval { enable 'ReverseProxy' }; - $@ and warn -"Plack::Middleware::ReverseProxy missing,\n", -"URL generation for redirects may be wrong if behind a reverse proxy\n"; - - enable 'Head'; - sub { $www->call(@_) }; - }; - } - %httpds = (); # invalidate cache -}; - -PublicInbox::Daemon::run('0.0.0.0:8080', $refresh, - sub ($$$) { # Listener->{post_accept} - my ($client, $addr, $srv, $tls_wrap) = @_; - my $fd = fileno($srv); - my $h = $httpds{$fd} //= - PublicInbox::HTTPD->new($srv, $app, $client); - PublicInbox::HTTP->new($client, $addr, $h), - }); +PublicInbox::Daemon::run('http://0.0.0.0:8080'); diff --git a/script/public-inbox-imapd b/script/public-inbox-imapd index 6b755938..0c96cdbb 100755 --- a/script/public-inbox-imapd +++ b/script/public-inbox-imapd @@ -1,14 +1,8 @@ #!perl -w -# Copyright (C) 2020-2021 all contributors +# Copyright (C) all contributors # License: AGPL-3.0+ # # Standalone read-only IMAP server for public-inbox. -use strict; +use v5.12; use PublicInbox::Daemon; -use PublicInbox::IMAPdeflate; # loads PublicInbox::IMAP -use PublicInbox::IMAPD; -my $imapd = PublicInbox::IMAPD->new; -PublicInbox::Daemon::run('0.0.0.0:143', - sub { $imapd->refresh_groups(@_) }, # refresh - sub ($$$) { PublicInbox::IMAP->new($_[0], $imapd) }, # post_accept - $imapd); +PublicInbox::Daemon::run('imap://0.0.0.0:143'); diff --git a/script/public-inbox-netd b/script/public-inbox-netd new file mode 100755 index 00000000..e8b1ca69 --- /dev/null +++ b/script/public-inbox-netd @@ -0,0 +1,6 @@ +#!/usr/bin/perl -w +# Copyright (C) all contributors +# License: AGPL-3.0+ +use v5.12; +use PublicInbox::Daemon; +PublicInbox::Daemon::run(); diff --git a/script/public-inbox-nntpd b/script/public-inbox-nntpd index 9fb0a8d9..aca27383 100755 --- a/script/public-inbox-nntpd +++ b/script/public-inbox-nntpd @@ -1,15 +1,8 @@ -#!/usr/bin/perl -w -# Copyright (C) 2015-2021 all contributors +#!perl -w +# Copyright (C) all contributors # License: AGPL-3.0+ # # Standalone NNTP server for public-inbox. -use strict; -use warnings; +use v5.12; use PublicInbox::Daemon; -use PublicInbox::NNTPdeflate; # loads PublicInbox::NNTP -use PublicInbox::NNTPD; -my $nntpd = PublicInbox::NNTPD->new; -PublicInbox::Daemon::run('0.0.0.0:119', - sub { $nntpd->refresh_groups }, # refresh - sub ($$$) { PublicInbox::NNTP->new($_[0], $nntpd) }, # post_accept - $nntpd); +PublicInbox::Daemon::run('nntp://0.0.0.0:119'); diff --git a/t/netd.t b/t/netd.t new file mode 100644 index 00000000..abdde124 --- /dev/null +++ b/t/netd.t @@ -0,0 +1,85 @@ +#!perl -w +# Copyright (C) all contributors +# License: AGPL-3.0+ +use v5.12; +use Socket qw(IPPROTO_TCP SOL_SOCKET); +use PublicInbox::TestCommon; +# IO::Poll and Net::NNTP are part of the standard library, but +# distros may split them off... +require_mods(qw(-imapd IO::Socket::SSL Mail::IMAPClient IO::Poll Net::NNTP)); +my $imap_client = 'Mail::IMAPClient'; +$imap_client->can('starttls') or + plan skip_all => 'Mail::IMAPClient does not support TLS'; +Net::NNTP->can('starttls') or + plan skip_all => 'Net::NNTP does not support TLS'; +my $cert = 'certs/server-cert.pem'; +my $key = 'certs/server-key.pem'; +unless (-r $key && -r $cert) { + plan skip_all => + "certs/ missing for $0, run $^X ./create-certs.perl in certs/"; +} +use_ok 'PublicInbox::TLS'; +use_ok 'IO::Socket::SSL'; +require_git('2.6'); + +my ($tmpdir, $for_destroy) = tmpdir(); +my $err = "$tmpdir/stderr.log"; +my $out = "$tmpdir/stdout.log"; +my $pi_config; +my $group = 'test-netd'; +my $addr = $group . '@example.com'; + +# ensure we have free, low-numbered contiguous FDs from 3.. FD inheritance +my @pad_pipes; +for (1..3) { + pipe(my ($r, $w)) or xbail "pipe: $!"; + push @pad_pipes, $r, $w; +}; +my %srv = map { $_ => tcp_server() } qw(imap nntp imaps nntps); +my $ibx = create_inbox 'netd', version => 2, + -primary_address => $addr, indexlevel => 'basic', sub { + my ($im, $ibx) = @_; + $im->add(eml_load('t/data/0001.patch')) or BAIL_OUT '->add'; + $pi_config = "$ibx->{inboxdir}/pi_config"; + open my $fh, '>', $pi_config or BAIL_OUT "open: $!"; + print $fh <{inboxdir} + address = $addr + indexlevel = basic + newsgroup = $group +EOF + close $fh or BAIL_OUT "close: $!\n"; +}; +$pi_config //= "$ibx->{inboxdir}/pi_config"; +my @args = ("--cert=$cert", "--key=$key"); +my $rdr = {}; +my $fd = 3; +while (my ($k, $v) = each %srv) { + push @args, "-l$k://".tcp_host_port($v); + $rdr->{$fd++} = $v; +} +my $cmd = [ '-netd', '-W0', @args, "--stdout=$out", "--stderr=$err" ]; +my $env = { PI_CONFIG => $pi_config }; +my $td = start_script($cmd, $env, $rdr); +@pad_pipes = (); +undef $rdr; +my %o = ( + SSL_hostname => 'server.local', + SSL_verifycn_name => 'server.local', + SSL_verify_mode => SSL_VERIFY_PEER(), + SSL_ca_file => 'certs/test-ca.pem', +); +{ + my $c = tcp_connect($srv{imap}); + my $msg = <$c>; + like($msg, qr/IMAP4rev1/, 'connected to IMAP'); +} +{ + my $c = tcp_connect($srv{nntp}); + my $msg = <$c>; + like($msg, qr/^201 .*? ready - post via email/, 'connected to NNTP'); +} + +# TODO: more tests +done_testing;