]> 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 68ba987636524e8bba58e4c5e1535f2f4de32e55..24c13ad24d159fcd300f7a64ef9090d99dc53205 100644 (file)
@@ -9,10 +9,10 @@ use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev/;
 use IO::Handle;
 use IO::Socket;
 use Cwd qw/abs_path/;
-use Time::HiRes qw(clock_gettime CLOCK_MONOTONIC);
 STDOUT->autoflush(1);
 STDERR->autoflush(1);
-require PublicInbox::DS;
+use PublicInbox::DS qw(now);
+require PublicInbox::EvCleanup;
 require POSIX;
 require PublicInbox::Listener;
 require PublicInbox::ParentPipe;
@@ -22,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);
@@ -42,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";
 
@@ -55,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) {
@@ -182,7 +232,7 @@ sub worker_quit {
        PublicInbox::DS->SetPostLoopCallback(sub {
                my ($dmap, undef) = @_;
                my $n = 0;
-               my $now = clock_gettime(CLOCK_MONOTONIC);
+               my $now = now();
 
                foreach my $s (values %$dmap) {
                        $s->can('busy') or next;
@@ -194,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];
@@ -461,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
@@ -483,19 +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;
-       PublicInbox::EvCleanup::enable();
        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 ($) {