about summary refs log tree commit homepage
path: root/lib/PublicInbox/NetReader.pm
diff options
context:
space:
mode:
Diffstat (limited to 'lib/PublicInbox/NetReader.pm')
-rw-r--r--lib/PublicInbox/NetReader.pm171
1 files changed, 115 insertions, 56 deletions
diff --git a/lib/PublicInbox/NetReader.pm b/lib/PublicInbox/NetReader.pm
index c1af03a3..ec18818b 100644
--- a/lib/PublicInbox/NetReader.pm
+++ b/lib/PublicInbox/NetReader.pm
@@ -3,8 +3,7 @@
 
 # common reader code for IMAP and NNTP (and maybe JMAP)
 package PublicInbox::NetReader;
-use strict;
-use v5.10.1;
+use v5.12;
 use parent qw(Exporter PublicInbox::IPC);
 use PublicInbox::Eml;
 use PublicInbox::Config;
@@ -15,7 +14,7 @@ our @EXPORT = qw(uri_section imap_uri nntp_uri);
 
 sub ndump {
         require Data::Dumper;
-        Data::Dumper->new(\@_)->Useqq(1)->Terse(1)->Dump;
+        Data::Dumper->new([ $_[-1] ])->Useqq(1)->Terse(1)->Dump;
 }
 
 # returns the git config section name, e.g [imap "imaps://user@example.com"]
@@ -41,10 +40,27 @@ EOM
         die "$val not understood (only socks5h:// is supported)\n";
 }
 
+# gives an arrayref suitable for the Mail::IMAPClient Ssl or Starttls arg
+sub mic_tls_opt ($$) {
+        my ($o, $hostname) = @_;
+        require IO::Socket::SSL;
+        $o = {} if !ref($o);
+        $o->{SSL_hostname} //= $hostname;
+        [ map { ($_, $o->{$_}) } keys %$o ];
+}
+
+sub set_ssl_verify_mode ($$) {
+        my ($o, $bool) = @_;
+        require IO::Socket::SSL;
+        $o->{SSL_verify_mode} = $bool ? IO::Socket::SSL::SSL_VERIFY_PEER() :
+                                        IO::Socket::SSL::SSL_VERIFY_NONE();
+}
+
 sub mic_new ($$$$) {
         my ($self, $mic_arg, $sec, $uri) = @_;
         my %mic_arg = (%$mic_arg, Keepalive => 1);
         my $sa = $self->{cfg_opt}->{$sec}->{-proxy_cfg} || $self->{-proxy_cli};
+        my ($mic, $s, $t);
         if ($sa) {
                 # this `require' needed for worker[1..Inf], since socks_args
                 # only got called in worker[0]
@@ -53,16 +69,37 @@ sub mic_new ($$$$) {
                 $opt{SocksDebug} = 1 if $mic_arg{Debug};
                 $opt{ConnectAddr} = delete $mic_arg{Server};
                 $opt{ConnectPort} = delete $mic_arg{Port};
-                my $s = IO::Socket::Socks->new(%opt) or die
-                        "E: <$uri> ".eval('$IO::Socket::Socks::SOCKS_ERROR');
-                if ($mic_arg->{Ssl}) { # for imaps://
-                        require IO::Socket::SSL;
-                        $s = IO::Socket::SSL->start_SSL($s) or die
-                                "E: <$uri> ".(IO::Socket::SSL->errstr // '');
-                }
+                do {
+                        $! = 0;
+                        $s = IO::Socket::Socks->new(%opt);
+                } until ($s || !$!{EINTR} || $self->{quit});
+                return if $self->{quit};
+                $s or die "E: <$uri> ".eval('$IO::Socket::Socks::SOCKS_ERROR');
                 $mic_arg{Socket} = $s;
+                if (my $o = delete $mic_arg{Ssl}) { # for imaps://
+                        $o = mic_tls_opt($o, $opt{ConnectAddr});
+                        do {
+                                $! = 0;
+                                $t = IO::Socket::SSL->start_SSL($s, @$o);
+                        } until ($t || !$!{EINTR} || $self->{quit});
+                        return if $self->{quit};
+                        $t or die "E: <$uri> ".(IO::Socket::SSL->errstr // '');
+                        $mic_arg{Socket} = $t;
+                } elsif ($o = $mic_arg{Starttls}) {
+                        # Mail::IMAPClient will use this:
+                        $mic_arg{Starttls} = mic_tls_opt($o, $opt{ConnectAddr});
+                }
+        } elsif ($mic_arg{Ssl} || $mic_arg{Starttls}) {
+                for my $f (qw(Ssl Starttls)) {
+                        my $o = $mic_arg{$f} or next;
+                        $mic_arg{$f} = mic_tls_opt($o, $mic_arg{Server});
+                }
         }
-        PublicInbox::IMAPClient->new(%mic_arg);
+        do {
+                $! = 0;
+                $mic = PublicInbox::IMAPClient->new(%mic_arg);
+        } until ($mic || !$!{EINTR} || $self->{quit});
+        $mic;
 }
 
 sub auth_anon_cb { '' }; # for Mail::IMAPClient::Authcallback
@@ -72,6 +109,7 @@ sub onion_hint ($$) {
         $uri->host =~ /\.onion\z/i or return "\n";
         my $t = $uri->isa('PublicInbox::URIimap') ? 'imap' : 'nntp';
         my $url = PublicInbox::Config::squote_maybe(uri_section($uri));
+        my $scheme = $uri->scheme;
         my $set_cfg = 'lei config';
         if (!$lei) { # public-inbox-watch
                 my $f = PublicInbox::Config::squote_maybe(
@@ -87,6 +125,10 @@ try configuring a socks5h:// proxy:
         url=$url
         $set_cfg $t.$dq\$url$dq.proxy socks5h://127.0.0.1:9050
 
+git 2.26+ users may instead rely on `*' to match all .onion URLs:
+
+        $set_cfg '$t.$scheme://*.onion.proxy' socks5h://127.0.0.1:9050
+
 ...before retrying your current command
 EOM
 }
@@ -122,9 +164,9 @@ sub mic_for ($$$$) { # mic = Mail::IMAPClient
                 Server => $host,
                 %$common, # may set Starttls, Compress, Debug ....
         };
-        $mic_arg->{Ssl} = 1 if $uri->scheme eq 'imaps';
         require PublicInbox::IMAPClient;
         my $mic = mic_new($self, $mic_arg, $sec, $uri);
+        return if $self->{quit};
         ($mic && $mic->IsConnected) or
                 die "E: <$uri> new: $@".onion_hint($lei, $uri);
 
@@ -175,17 +217,20 @@ sub mic_for ($$$$) { # mic = Mail::IMAPClient
         $mic;
 }
 
-sub nn_new ($$$) {
-        my ($nn_arg, $nntp_cfg, $uri) = @_;
+sub nn_new ($$$$) {
+        my ($self, $nn_arg, $nntp_cfg, $uri) = @_;
         my $nn;
+        my ($Net_NNTP, $new) = qw(Net::NNTP new);
         if (defined $nn_arg->{ProxyAddr}) {
                 require PublicInbox::NetNNTPSocks;
+                ($Net_NNTP, $new) = qw(PublicInbox::NetNNTPSocks new_socks);
                 $nn_arg->{SocksDebug} = 1 if $nn_arg->{Debug};
-                eval { $nn = PublicInbox::NetNNTPSocks->new_socks(%$nn_arg) };
-                die "E: <$uri> $@\n" if $@;
-        } else {
-                $nn = Net::NNTP->new(%$nn_arg) or return;
         }
+        do {
+                $! = 0;
+                $nn = $Net_NNTP->$new(%$nn_arg);
+        } until ($nn || !$!{EINTR} || $self->{quit});
+        $nn // return;
         setsockopt($nn, Socket::SOL_SOCKET(), Socket::SO_KEEPALIVE(), 1);
 
         # default to using STARTTLS if it's available, but allow
@@ -195,19 +240,19 @@ sub nn_new ($$$) {
                                 try_starttls($nn_arg->{Host})) {
                         # soft fail by default
                         $nn->starttls or warn <<"";
-W: <$uri> STARTTLS tried and failed (not requested)
+W: <$uri> STARTTLS tried and failed (not requested): ${\(ndump($nn->message))}
 
                 } elsif ($nntp_cfg->{starttls}) {
                         # hard fail if explicitly configured
                         $nn->starttls or die <<"";
-E: <$uri> STARTTLS requested and failed
+E: <$uri> STARTTLS requested and failed: ${\(ndump($nn->message))}
 
                 }
         } elsif ($nntp_cfg->{starttls}) {
                 $nn->can('starttls') or
                         die "E: <$uri> Net::NNTP too old for STARTTLS\n";
                 $nn->starttls or die <<"";
-E: <$uri> STARTTLS requested and failed
+E: <$uri> STARTTLS requested and failed: ${\(ndump($nn->message))}
 
         }
         $nn;
@@ -242,25 +287,32 @@ sub nn_for ($$$$) { # nn = Net::NNTP
         $nn_arg->{SSL} = 1 if $uri->secure; # snews == nntps
         my $sa = $self->{-proxy_cli};
         %$nn_arg = (%$nn_arg, %$sa) if $sa;
-        my $nn = nn_new($nn_arg, $nntp_cfg, $uri) or
-                die "E: <$uri> new: $@".onion_hint($lei, $uri);
+        my $nn = nn_new($self, $nn_arg, $nntp_cfg, $uri);
+        return if $self->{quit};
+        $nn // die "E: <$uri> new: $@".onion_hint($lei, $uri);
         if ($cred) {
-                $cred->fill($lei) unless defined($p); # may prompt user here
+                $p //= do {
+                        $cred->fill($lei); # may prompt user here
+                        $cred->{password};
+                };
                 if ($nn->authinfo($u, $p)) {
                         push @{$nntp_cfg->{-postconn}}, [ 'authinfo', $u, $p ];
                 } else {
-                        warn "E: <$uri> AUTHINFO $u XXXX failed\n";
+                        warn <<EOM;
+E: <$uri> AUTHINFO $u XXXX: ${\(ndump($nn->message))}
+EOM
                         $nn = undef;
                 }
         }
-
-        if ($nntp_cfg->{compress}) {
+        if ($nn && $nntp_cfg->{compress}) {
                 # https://rt.cpan.org/Ticket/Display.html?id=129967
                 if ($nn->can('compress')) {
                         if ($nn->compress) {
                                 push @{$nntp_cfg->{-postconn}}, [ 'compress' ];
                         } else {
-                                warn "W: <$uri> COMPRESS failed\n";
+                                warn <<EOM;
+W: <$uri> COMPRESS: ${\(ndump($nn->message))}
+EOM
                         }
                 } else {
                         delete $nntp_cfg->{compress};
@@ -304,14 +356,6 @@ sub cfg_intvl ($$$) {
         }
 }
 
-sub cfg_bool ($$$) {
-        my ($cfg, $key, $url) = @_;
-        my $orig = $cfg->urlmatch($key, $url) // return;
-        my $bool = $cfg->git_bool($orig);
-        warn "W: $key=$orig for $url is not boolean\n" unless defined($bool);
-        $bool;
-}
-
 # flesh out common IMAP-specific data structures
 sub imap_common_init ($;$) {
         my ($self, $lei) = @_;
@@ -329,11 +373,12 @@ sub imap_common_init ($;$) {
 
                 # knobs directly for Mail::IMAPClient->new
                 for my $k (qw(Starttls Debug Compress)) {
-                        my $bool = cfg_bool($cfg, "imap.$k", $$uri) // next;
-                        $mic_common->{$sec}->{$k} = $bool;
+                        my $v = $cfg->urlmatch('--bool', "imap.$k", $$uri);
+                        $mic_common->{$sec}->{$k} = $v if defined $v;
                 }
                 my $to = cfg_intvl($cfg, 'imap.timeout', $$uri);
                 $mic_common->{$sec}->{Timeout} = $to if $to;
+                $mic_common->{$sec}->{Ssl} = 1 if $uri->scheme eq 'imaps';
 
                 # knobs we use ourselves:
                 my $sa = socks_args($cfg->urlmatch('imap.Proxy', $$uri));
@@ -343,11 +388,18 @@ sub imap_common_init ($;$) {
                         $self->{cfg_opt}->{$sec}->{$k} = $to;
                 }
                 my $k = 'imap.fetchBatchSize';
-                my $bs = $cfg->urlmatch($k, $$uri) // next;
-                if ($bs =~ /\A([0-9]+)\z/ && $bs > 0) {
-                        $self->{cfg_opt}->{$sec}->{batch_size} = $bs;
-                } else {
-                        warn "$k=$bs is not a positive integer\n";
+                if (defined(my $bs = $cfg->urlmatch($k, $$uri))) {
+                        ($bs =~ /\A([0-9]+)\z/ && $bs > 0) ?
+                                ($self->{cfg_opt}->{$sec}->{batch_size} = $bs) :
+                                warn("$k=$bs is not a positive integer\n");
+                }
+                my $v = $cfg->urlmatch(qw(--bool imap.sslVerify), $$uri);
+                if (defined $v) {
+                        my $cur = $mic_common->{$sec} //= {};
+                        $cur->{Starttls} //= 1 if !$cur->{Ssl};
+                        for my $f (grep { $cur->{$_} } qw(Ssl Starttls)) {
+                                set_ssl_verify_mode($cur->{$f} = {}, $v);
+                        }
                 }
         }
         # make sure we can connect and cache the credentials in memory
@@ -356,8 +408,9 @@ sub imap_common_init ($;$) {
                 my $sec = uri_section($orig_uri);
                 my $uri = PublicInbox::URIimap->new("$sec/");
                 my $mic = $mics->{$sec} //=
-                                mic_for($self, $uri, $mic_common, $lei) //
-                                die "Unable to continue\n";
+                                mic_for($self, $uri, $mic_common, $lei);
+                return if $self->{quit};
+                $mic // die "Unable to continue\n";
                 next unless $self->isa('PublicInbox::NetWriter');
                 next if $self->{-skip_creat};
                 my $dst = $orig_uri->mailbox // next;
@@ -383,7 +436,7 @@ sub nntp_common_init ($;$) {
                 my $args = $nn_common->{$sec} //= {};
 
                 # Debug and Timeout are passed to Net::NNTP->new
-                my $v = cfg_bool($cfg, 'nntp.Debug', $$uri);
+                my $v = $cfg->urlmatch(qw(--bool nntp.Debug), $$uri);
                 $args->{Debug} = $v if defined $v;
                 my $to = cfg_intvl($cfg, 'nntp.Timeout', $$uri);
                 $args->{Timeout} = $to if $to;
@@ -392,9 +445,11 @@ sub nntp_common_init ($;$) {
 
                 # Net::NNTP post-connect commands
                 for my $k (qw(starttls compress)) {
-                        $v = cfg_bool($cfg, "nntp.$k", $$uri) // next;
-                        $self->{cfg_opt}->{$sec}->{$k} = $v;
+                        $v = $cfg->urlmatch('--bool', "nntp.$k", $$uri);
+                        $self->{cfg_opt}->{$sec}->{$k} = $v if defined $v;
                 }
+                $v = $cfg->urlmatch(qw(--bool nntp.sslVerify), $$uri);
+                set_ssl_verify_mode($args, $v) if defined $v;
 
                 # -watch internal option
                 for my $k (qw(pollInterval)) {
@@ -685,7 +740,13 @@ sub mic_get {
         }
         my $mic = mic_new($self, $mic_arg, $sec, $uri);
         $cached //= {}; # invalid placeholder if no cache enabled
-        $mic && $mic->IsConnected ? ($cached->{$sec} = $mic) : undef;
+        if ($mic && $mic->IsConnected) {
+                $cached->{$sec} = $mic;
+        } else {
+                warn 'IMAP LastError: ',$mic->LastError, "\n" if $mic;
+                warn "IMAP errno: $!\n" if $!;
+                undef;
+        }
 }
 
 sub imap_each {
@@ -717,7 +778,7 @@ sub nn_get {
         my $nn_arg = $self->{net_arg}->{$sec} or
                         die "BUG: no Net::NNTP->new arg for $sec";
         my $nntp_cfg = $self->{cfg_opt}->{$sec};
-        $nn = nn_new($nn_arg, $nntp_cfg, $uri) or return;
+        $nn = nn_new($self, $nn_arg, $nntp_cfg, $uri) or return;
         if (my $postconn = $nntp_cfg->{-postconn}) {
                 for my $m_arg (@$postconn) {
                         my ($method, @args) = @$m_arg;
@@ -759,14 +820,12 @@ sub _nntp_fetch_all ($$$) {
         $beg = $num_a if defined($num_a) && $num_a > $beg && $num_a <= $end;
         $end = $num_b if defined($num_b) && $num_b >= $beg && $num_b < $end;
         $end = $beg if defined($num_a) && !defined($num_b);
-        my ($err, $art, $last_art, $kw); # kw stays undef, no keywords in NNTP
-        unless ($self->{quiet}) {
-                warn "# $uri fetching ARTICLE $beg..$end\n";
-        }
+        my ($err, $last_art, $kw); # kw stays undef, no keywords in NNTP
+        warn "# $uri fetching ARTICLE $beg..$end\n" if !$self->{quiet};
         my $n = $self->{max_batch};
-        for ($beg..$end) {
+        for my $art ($beg..$end) {
                 last if $self->{quit};
-                $art = $_;
+                local $0 = "#$art $group $sec";
                 if (--$n < 0) {
                         run_commit_cb($self);
                         $itrk->update_last(0, $last_art) if $itrk;