about summary refs log tree commit homepage
path: root/lib
diff options
context:
space:
mode:
authorEric Wong <e@80x24.org>2019-06-24 02:52:38 +0000
committerEric Wong <e@80x24.org>2019-06-24 05:26:26 +0000
commitb70cf61f0c1f70621b88fe6420083a576d47f19f (patch)
tree88b4cd6fb40e983d7b9a536456067e42b7d890ba /lib
parent93fc0336d39ba3ef07b479877e64371f07c86eab (diff)
downloadpublic-inbox-b70cf61f0c1f70621b88fe6420083a576d47f19f.tar.gz
It kinda, barely works, and I'm most happy I got it working
without any modifications to the main NNTP::event_step callback
thanks to the DS->write(CODE) support we inherited from
Danga::Socket.
Diffstat (limited to 'lib')
-rw-r--r--lib/PublicInbox/DS.pm28
-rw-r--r--lib/PublicInbox/Daemon.pm82
-rw-r--r--lib/PublicInbox/NNTP.pm27
-rw-r--r--lib/PublicInbox/NNTPD.pm1
-rw-r--r--lib/PublicInbox/TLS.pm24
5 files changed, 150 insertions, 12 deletions
diff --git a/lib/PublicInbox/DS.pm b/lib/PublicInbox/DS.pm
index 1a1ef7d3..044b991c 100644
--- a/lib/PublicInbox/DS.pm
+++ b/lib/PublicInbox/DS.pm
@@ -465,7 +465,11 @@ next_buf:
             }
         } else { #($ref eq 'CODE') {
             shift @$wbuf;
+            my $before = scalar(@$wbuf);
             $bref->($self);
+
+            # bref may be enqueueing more CODE to call (see accept_tls_step)
+            return 0 if (scalar(@$wbuf) > $before);
         }
     } # while @$wbuf
 
@@ -479,7 +483,14 @@ sub do_read ($$$$) {
     return ($r == 0 ? $self->close : $r) if defined $r;
     # common for clients to break connections without warning,
     # would be too noisy to log here:
-    $! == EAGAIN ? $self->watch_in1 : $self->close;
+    if (ref($self) eq 'IO::Socket::SSL') {
+        my $ev = PublicInbox::TLS::epollbit() or return $self->close;
+        watch($self, $ev | EPOLLONESHOT);
+    } elsif ($! == EAGAIN) {
+        watch($self, EPOLLIN | EPOLLONESHOT);
+    } else {
+        $self->close;
+    }
 }
 
 # drop the socket if we hit unrecoverable errors on our system which
@@ -566,7 +577,7 @@ sub msg_more ($$) {
     my $self = $_[0];
     my $sock = $self->{sock} or return 1;
 
-    if (MSG_MORE && !$self->{wbuf}) {
+    if (MSG_MORE && !$self->{wbuf} && ref($sock) ne 'IO::Socket::SSL') {
         my $n = send($sock, $_[1], MSG_MORE);
         if (defined $n) {
             my $nlen = bytes::length($_[1]) - $n;
@@ -597,6 +608,19 @@ sub watch ($$) {
 
 sub watch_in1 ($) { watch($_[0], EPOLLIN | EPOLLONESHOT) }
 
+# return true if complete, false if incomplete (or failure)
+sub accept_tls_step ($) {
+    my ($self) = @_;
+    my $sock = $self->{sock} or return;
+    return 1 if $sock->accept_SSL;
+    return $self->close if $! != EAGAIN;
+    if (my $ev = PublicInbox::TLS::epollbit()) {
+        unshift @{$self->{wbuf} ||= []}, \&accept_tls_step;
+        return watch($self, $ev | EPOLLONESHOT);
+    }
+    drop($self, 'BUG? EAGAIN but '.PublicInbox::TLS::err());
+}
+
 package PublicInbox::DS::Timer;
 # [$abs_float_firetime, $coderef];
 sub cancel {
diff --git a/lib/PublicInbox/Daemon.pm b/lib/PublicInbox/Daemon.pm
index b8d6b572..24c13ad2 100644
--- a/lib/PublicInbox/Daemon.pm
+++ b/lib/PublicInbox/Daemon.pm
@@ -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) {
@@ -461,9 +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
@@ -484,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;
         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 ($) {
diff --git a/lib/PublicInbox/NNTP.pm b/lib/PublicInbox/NNTP.pm
index a18641d3..659e44d5 100644
--- a/lib/PublicInbox/NNTP.pm
+++ b/lib/PublicInbox/NNTP.pm
@@ -1,4 +1,4 @@
-# Copyright (C) 2015-2018 all contributors <meta@public-inbox.org>
+# Copyright (C) 2015-2019 all contributors <meta@public-inbox.org>
 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
 #
 # Each instance of this represents a NNTP client socket
@@ -98,11 +98,19 @@ sub expire_old () {
 sub new ($$$) {
         my ($class, $sock, $nntpd) = @_;
         my $self = fields::new($class);
-        $self->SUPER::new($sock, EPOLLOUT | EPOLLONESHOT);
+        my $ev = EPOLLOUT | EPOLLONESHOT;
+        my $wbuf = [];
+        if (ref($sock) eq 'IO::Socket::SSL' && !$sock->accept_SSL) {
+                $ev = PublicInbox::TLS::epollbit() or return $sock->close;
+                $ev |= EPOLLONESHOT;
+                $wbuf->[0] = \&PublicInbox::DS::accept_tls_step;
+        }
+        $self->SUPER::new($sock, $ev);
         $self->{nntpd} = $nntpd;
         my $greet = "201 $nntpd->{servername} ready - post via email\r\n";
         open my $fh, '<:scalar',  \$greet or die "open :scalar: $!";
-        $self->{wbuf} = [ $fh ];
+        push @$wbuf, $fh;
+        $self->{wbuf} = $wbuf;
         $self->{rbuf} = '';
         update_idle_time($self);
         $expt ||= PublicInbox::EvCleanup::later(*expire_old);
@@ -900,6 +908,19 @@ sub cmd_xover ($;$) {
         });
 }
 
+sub cmd_starttls ($) {
+        my ($self) = @_;
+        my $sock = $self->{sock} or return;
+        # RFC 4642 2.2.1
+        (ref($sock) eq 'IO::Socket::SSL') and return '502 Command unavailable';
+        my $opt = $self->{nntpd}->{accept_tls} or
+                return '580 can not initiate TLS negotiation';
+        res($self, '382 Continue with TLS negotiation');
+        $self->{sock} = IO::Socket::SSL->start_SSL($sock, %$opt);
+        requeue($self) if PublicInbox::DS::accept_tls_step($self);
+        undef;
+}
+
 sub cmd_xpath ($$) {
         my ($self, $mid) = @_;
         return r501 unless $mid =~ /\A<(.+)>\z/;
diff --git a/lib/PublicInbox/NNTPD.pm b/lib/PublicInbox/NNTPD.pm
index 32848d7c..6d9ffd5f 100644
--- a/lib/PublicInbox/NNTPD.pm
+++ b/lib/PublicInbox/NNTPD.pm
@@ -25,6 +25,7 @@ sub new {
                 out => \*STDOUT,
                 grouplist => [],
                 servername => $name,
+                # accept_tls => { SSL_server => 1, ..., SSL_reuse_ctx => ... }
         }, $class;
 }
 
diff --git a/lib/PublicInbox/TLS.pm b/lib/PublicInbox/TLS.pm
new file mode 100644
index 00000000..576c11d7
--- /dev/null
+++ b/lib/PublicInbox/TLS.pm
@@ -0,0 +1,24 @@
+# Copyright (C) 2019 all contributors <meta@public-inbox.org>
+# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
+
+# IO::Socket::SSL support code
+package PublicInbox::TLS;
+use strict;
+use IO::Socket::SSL;
+require Carp;
+use Errno qw(EAGAIN);
+use PublicInbox::Syscall qw(EPOLLIN EPOLLOUT);
+
+sub err () { $SSL_ERROR }
+
+# returns the EPOLL event bit which matches the existing SSL error
+sub epollbit () {
+        if ($! == EAGAIN) {
+                return EPOLLIN if $SSL_ERROR == SSL_WANT_READ;
+                return EPOLLOUT if $SSL_ERROR == SSL_WANT_WRITE;
+                die "unexpected SSL error: $SSL_ERROR";
+        }
+        0;
+}
+
+1;