]> Sergey Matveev's repositories - public-inbox.git/blobdiff - lib/PublicInbox/Daemon.pm
nntp: NNTPS and NNTP+STARTTLS working
[public-inbox.git] / lib / PublicInbox / Daemon.pm
index 9125584b3b12b334a078eb1c9aec1ff4aa5f27af..24c13ad24d159fcd300f7a64ef9090d99dc53205 100644 (file)
@@ -1,5 +1,5 @@
-# Copyright (C) 2015 all contributors <meta@public-inbox.org>
-# License: AGPLv3 or later (https://www.gnu.org/licenses/agpl-3.0.txt)
+# Copyright (C) 2015-2018 all contributors <meta@public-inbox.org>
+# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
 # contains common daemon code for the nntpd and httpd servers.
 # This may be used for read-only IMAP server if we decide to implement it.
 package PublicInbox::Daemon;
@@ -11,7 +11,8 @@ use IO::Socket;
 use Cwd qw/abs_path/;
 STDOUT->autoflush(1);
 STDERR->autoflush(1);
-require Danga::Socket;
+use PublicInbox::DS qw(now);
+require PublicInbox::EvCleanup;
 require POSIX;
 require PublicInbox::Listener;
 require PublicInbox::ParentPipe;
@@ -21,12 +22,48 @@ my (@cfg_listen, $stdout, $stderr, $group, $user, $pid_file, $daemonize);
 my $worker_processes = 1;
 my @listeners;
 my %pids;
-my %listener_names;
+my %listener_names; # sockname => IO::Handle
+my %tls_opt; # scheme://sockname => args for IO::Socket::SSL->start_SSL
 my $reexec_pid;
 my $cleanup;
 my ($uid, $gid);
+my ($default_cert, $default_key);
 END { $cleanup->() if $cleanup };
 
+sub tls_listen ($$$) {
+       my ($scheme, $sockname, $opt_str) = @_;
+       # opt_str: opt1=val1,opt2=val2 (opt may repeat for multi-value)
+       require PublicInbox::TLS;
+       my $o = {};
+       # allow ',' as delimiter since '&' is shell-unfriendly
+       foreach (split(/[,&]/, $opt_str)) {
+               my ($k, $v) = split(/=/, $_, 2);
+               push @{$o->{$k} ||= []}, $v;
+       }
+
+       # key may be a part of cert.  At least
+       # p5-io-socket-ssl/example/ssl_server.pl has this fallback:
+       $o->{cert} //= [ $default_cert ];
+       $o->{key} //= defined($default_key) ? [ $default_key ] : $o->{cert};
+       my %ctx_opt = (SSL_server => 1);
+       # parse out hostname:/path/to/ mappings:
+       foreach my $k (qw(cert key)) {
+               my $x = $ctx_opt{'SSL_'.$k.'_file'} = {};
+               foreach my $path (@{$o->{$k}}) {
+                       my $host = '';
+                       $path =~ s/\A([^:]+):// and $host = $1;
+                       $x->{$host} = $path;
+               }
+       }
+       my $ctx = IO::Socket::SSL::SSL_Context->new(%ctx_opt) or
+               die 'SSL_Context->new: '.PublicInbox::TLS::err();
+       $tls_opt{"$scheme://$sockname"} = {
+               SSL_server => 1,
+               SSL_startHandshake => 0,
+               SSL_reuse_ctx => $ctx
+       };
+}
+
 sub daemon_prepare ($) {
        my ($default_listen) = @_;
        @CMD = ($0, @ARGV);
@@ -41,6 +78,8 @@ sub daemon_prepare ($) {
                'u|user=s' => \$user,
                'g|group=s' => \$group,
                'D|daemonize' => \$daemonize,
+               'cert=s' => \$default_cert,
+               'key=s' => \$default_key,
        );
        GetOptions(%opts) or die "bad command-line args\n";
 
@@ -54,6 +93,18 @@ sub daemon_prepare ($) {
        push @cfg_listen, $default_listen unless (@listeners || @cfg_listen);
 
        foreach my $l (@cfg_listen) {
+               my $orig = $l;
+               my $scheme = '';
+               $l =~ s!\A([^:]+)://!! and $scheme = $1;
+               if ($l =~ s!/?\?(.+)\z!!) {
+                       tls_listen($scheme, $l, $1);
+               } elsif (defined($default_cert)) {
+                       tls_listen($scheme, $l, '');
+               } elsif ($scheme =~ /\A(?:nntps|https)\z/) {
+                       die "$orig specified w/o cert=\n";
+               }
+               # TODO: use scheme to load either NNTP.pm or HTTP.pm
+
                next if $listener_names{$l}; # already inherited
                my (%o, $sock_pkg);
                if (index($l, '/') == 0) {
@@ -102,16 +153,18 @@ sub check_absolute ($$) {
 }
 
 sub daemonize () {
-       foreach my $i (0..$#ARGV) {
-               my $arg = $ARGV[$i];
-               next unless -e $arg;
-               $ARGV[$i] = abs_path($arg);
-       }
-       check_absolute('stdout', $stdout);
-       check_absolute('stderr', $stderr);
-       check_absolute('pid-file', $pid_file);
+       if ($daemonize) {
+               foreach my $i (0..$#ARGV) {
+                       my $arg = $ARGV[$i];
+                       next unless -e $arg;
+                       $ARGV[$i] = abs_path($arg);
+               }
+               check_absolute('stdout', $stdout);
+               check_absolute('stderr', $stderr);
+               check_absolute('pid-file', $pid_file);
 
-       chdir '/' or die "chdir failed: $!";
+               chdir '/' or die "chdir failed: $!";
+       }
 
        return unless (defined $pid_file || defined $group || defined $user
                        || $daemonize);
@@ -169,20 +222,21 @@ sub worker_quit {
        # killing again terminates immediately:
        exit unless @listeners;
 
-       $_->close foreach @listeners; # call Danga::Socket::close
+       $_->close foreach @listeners; # call PublicInbox::DS::close
        @listeners = ();
        $reason->close if ref($reason) eq 'PublicInbox::ParentPipe';
 
        my $proc_name;
        my $warn = 0;
        # drop idle connections and try to quit gracefully
-       Danga::Socket->SetPostLoopCallback(sub {
+       PublicInbox::DS->SetPostLoopCallback(sub {
                my ($dmap, undef) = @_;
                my $n = 0;
+               my $now = now();
 
                foreach my $s (values %$dmap) {
                        $s->can('busy') or next;
-                       if ($s->busy) {
+                       if ($s->busy($now)) {
                                ++$n;
                        } else {
                                # close as much as possible, early as possible
@@ -190,9 +244,9 @@ sub worker_quit {
                        }
                }
                if ($n) {
-                       if (($warn + 5) < time) {
+                       if (($warn + 5) < now()) {
                                warn "$$ quitting, $n client(s) left\n";
-                               $warn = time;
+                               $warn = now();
                        }
                        unless (defined $proc_name) {
                                $proc_name = (split(/\s+/, $0))[0];
@@ -230,6 +284,28 @@ sub sockname ($) {
        "$host:$port";
 }
 
+sub unpack_ipv6 ($) {
+       my ($addr) = @_;
+       my ($port, $host);
+
+       # Socket.pm in Perl 5.14+ supports IPv6:
+       eval {
+               ($port, $host) = Socket::unpack_sockaddr_in6($addr);
+               $host = Socket::inet_ntop(Socket::AF_INET6(), $host);
+       };
+
+       if ($@) {
+               # Perl 5.12 or earlier?  SpamAssassin and Net::Server use
+               # Socket6, so it may be installed on our system, already
+               # (otherwise die here):
+               require Socket6;
+
+               ($port, $host) = Socket6::unpack_sockaddr_in6($addr);
+               $host = Socket6::inet_ntop(Socket6::AF_INET6(), $host);
+       }
+       ($host, $port);
+}
+
 sub host_with_port ($) {
        my ($addr) = @_;
        my ($port, $host);
@@ -237,9 +313,7 @@ sub host_with_port ($) {
        # this eval will die on Unix sockets:
        eval {
                if (length($addr) >= 28) {
-                       require Socket6;
-                       ($port, $host) = Socket6::unpack_sockaddr_in6($addr);
-                       $host = Socket6::inet_ntop(Socket6::AF_INET6(), $host);
+                       ($host, $port) = unpack_ipv6($addr);
                        $host = "[$host]";
                } else {
                        ($port, $host) = Socket::sockaddr_in($addr);
@@ -352,6 +426,11 @@ sub unlink_pid_file_safe_ish ($$) {
 sub master_loop {
        pipe(my ($p0, $p1)) or die "failed to create parent-pipe: $!";
        pipe(my ($r, $w)) or die "failed to create self-pipe: $!";
+
+       if ($^O eq 'linux') { # 1031: F_SETPIPE_SZ = 1031
+               fcntl($_, 1031, 4096) for ($w, $p1);
+       }
+
        IO::Handle::blocking($w, 0);
        my $set_workers = $worker_processes;
        my @caught;
@@ -432,8 +511,26 @@ sub master_loop {
        exit # never gets here, just for documentation
 }
 
-sub daemon_loop ($$) {
-       my ($refresh, $post_accept) = @_;
+sub tls_start_cb ($$) {
+       my ($opt, $orig_post_accept) = @_;
+       sub {
+               my ($io, $addr, $srv) = @_;
+               my $ssl = IO::Socket::SSL->start_SSL($io, %$opt);
+               $orig_post_accept->($ssl, $addr, $srv);
+       }
+}
+
+sub daemon_loop ($$$) {
+       my ($refresh, $post_accept, $nntpd) = @_;
+       PublicInbox::EvCleanup::enable(); # early for $refresh
+       my %post_accept;
+       while (my ($k, $v) = each %tls_opt) {
+               if ($k =~ s!\A(?:nntps|https)://!!) {
+                       $post_accept{$k} = tls_start_cb($v, $post_accept);
+               } elsif ($nntpd) { # STARTTLS, $k eq '' is OK
+                       $nntpd->{accept_tls} = $v;
+               }
+       }
        my $parent_pipe;
        if ($worker_processes > 0) {
                $refresh->(); # preload by default
@@ -454,18 +551,19 @@ sub daemon_loop ($$) {
        $SIG{$_} = 'IGNORE' for qw(USR2 TTIN TTOU WINCH);
        # this calls epoll_create:
        @listeners = map {
-               PublicInbox::Listener->new($_, $post_accept)
+               PublicInbox::Listener->new($_,
+                               $post_accept{sockname($_)} || $post_accept)
        } @listeners;
-       Danga::Socket->EventLoop;
+       PublicInbox::DS->EventLoop;
        $parent_pipe = undef;
 }
 
 
-sub run ($$$) {
-       my ($default, $refresh, $post_accept) = @_;
+sub run ($$$;$) {
+       my ($default, $refresh, $post_accept, $nntpd) = @_;
        daemon_prepare($default);
        daemonize();
-       daemon_loop($refresh, $post_accept);
+       daemon_loop($refresh, $post_accept, $nntpd);
 }
 
 sub do_chown ($) {