X-Git-Url: http://www.git.stargrave.org/?a=blobdiff_plain;f=lib%2FPublicInbox%2FNetReader.pm;h=2b74af4142155a76a632369ebfed81b240c50f4b;hb=840c1c80ccd3000a8ed888851a86a096e514002c;hp=08166415cfcb0c6519bd6dd749934a81d54b98a3;hpb=ef507953617741565d18c86b87973469206d5632;p=public-inbox.git diff --git a/lib/PublicInbox/NetReader.pm b/lib/PublicInbox/NetReader.pm index 08166415..2b74af41 100644 --- a/lib/PublicInbox/NetReader.pm +++ b/lib/PublicInbox/NetReader.pm @@ -35,6 +35,7 @@ sub socks_args ($) { eval { require IO::Socket::Socks } or die < $h, ProxyPort => $p }; } die "$val not understood (only socks5h:// is supported)\n"; @@ -42,23 +43,67 @@ EOM sub mic_new ($$$$) { my ($self, $mic_arg, $sec, $uri) = @_; - my %socks; - my $sa = $self->{imap_opt}->{$sec}->{-proxy_cfg} || $self->{-proxy_cli}; + my %mic_arg = (%$mic_arg, Keepalive => 1); + my $sa = $self->{cfg_opt}->{$sec}->{-proxy_cfg} || $self->{-proxy_cli}; if ($sa) { - my %opt = %$sa; - $opt{ConnectAddr} = delete $mic_arg->{Server}; - $opt{ConnectPort} = delete $mic_arg->{Port}; - $socks{Socket} = IO::Socket::Socks->new(%opt) or die - "E: <$$uri> ".eval('$IO::Socket::Socks::SOCKS_ERROR'); + # this `require' needed for worker[1..Inf], since socks_args + # only got called in worker[0] + require IO::Socket::Socks; + my %opt = (%$sa, Keepalive => 1); + $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 // ''); + } + $mic_arg{Socket} = $s; } - PublicInbox::IMAPClient->new(%$mic_arg, %socks); + PublicInbox::IMAPClient->new(%mic_arg); } sub auth_anon_cb { '' }; # for Mail::IMAPClient::Authcallback +sub onion_hint ($$) { + my ($lei, $uri) = @_; + $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 $set_cfg = 'lei config'; + if (!$lei) { # public-inbox-watch + my $f = PublicInbox::Config::squote_maybe( + $ENV{PI_CONFIG} || '~/.public-inbox/config'); + $set_cfg = "git config -f $f"; + } + my $dq = substr($url, 0, 1) eq "'" ? '"' : ''; + < "$uri", @@ -68,24 +113,26 @@ sub mic_for ($$$$) { # mic = Mail::IMAPClient password => $uri->password, }, 'PublicInbox::GitCredential'; my $sec = uri_section($uri); - my $common = $mic_args->{$sec} // {}; + my $common = $mic_common->{$sec} // {}; # IMAPClient and Net::Netrc both mishandles `0', so we pass `127.0.0.1' my $host = $cred->{host}; $host = '127.0.0.1' if $host eq '0'; my $mic_arg = { Port => $uri->port, Server => $host, - Ssl => $uri->scheme eq 'imaps', - Keepalive => 1, # SO_KEEPALIVE %$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) or - die "E: <$uri> new: $@\n"; + my $mic = mic_new($self, $mic_arg, $sec, $uri); + ($mic && $mic->IsConnected) or + die "E: <$uri> new: $@".onion_hint($lei, $uri); + # default to using STARTTLS if it's available, but allow # it to be disabled since I usually connect to localhost if (!$mic_arg->{Ssl} && !defined($mic_arg->{Starttls}) && $mic->has_capability('STARTTLS') && + try_starttls($host) && $mic->can('starttls')) { $mic->starttls or die "E: <$uri> STARTTLS: $@\n"; } @@ -96,7 +143,7 @@ sub mic_for ($$$$) { # mic = Mail::IMAPClient $cred = undef; } if ($cred) { - my $p = $cred->{password} // $cred->check_netrc; + my $p = $cred->{password} // $cred->check_netrc($lei); $cred->fill($lei) unless defined($p); # may prompt user here $mic->User($mic_arg->{User} = $cred->{username}); $mic->Password($mic_arg->{Password} = $cred->{password}); @@ -108,7 +155,7 @@ sub mic_for ($$$$) { # mic = Mail::IMAPClient my $err; if ($mic->login && $mic->IsAuthenticated) { # success! keep IMAPClient->new arg in case we get disconnected - $self->{mic_arg}->{$sec} = $mic_arg; + $self->{net_arg}->{$sec} = $mic_arg; if ($cred) { $uri->user($cred->{username}) if !defined($uri->user); } elsif ($mic_arg->{Authmechanism} eq 'ANONYMOUS') { @@ -128,42 +175,35 @@ sub mic_for ($$$$) { # mic = Mail::IMAPClient $mic; } -# Net::NNTP doesn't support CAPABILITIES, yet -sub try_starttls ($) { - my ($host) = @_; - return if $host =~ /\.onion\z/s; - return if $host =~ /\A127\.[0-9]+\.[0-9]+\.[0-9]+\z/s; - return if $host eq '::1'; - 1; -} - sub nn_new ($$$) { - my ($nn_arg, $nntp_opt, $uri) = @_; + my ($nn_arg, $nntp_cfg, $uri) = @_; my $nn; if (defined $nn_arg->{ProxyAddr}) { require PublicInbox::NetNNTPSocks; + $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 die "E: <$uri> new: $!\n"; + $nn = Net::NNTP->new(%$nn_arg) or return; } + setsockopt($nn, Socket::SOL_SOCKET(), Socket::SO_KEEPALIVE(), 1); # default to using STARTTLS if it's available, but allow # it to be disabled for localhost/VPN users if (!$nn_arg->{SSL} && $nn->can('starttls')) { - if (!defined($nntp_opt->{starttls}) && + if (!defined($nntp_cfg->{starttls}) && try_starttls($nn_arg->{Host})) { # soft fail by default $nn->starttls or warn <<""; W: <$uri> STARTTLS tried and failed (not requested) - } elsif ($nntp_opt->{starttls}) { + } elsif ($nntp_cfg->{starttls}) { # hard fail if explicitly configured $nn->starttls or die <<""; E: <$uri> STARTTLS requested and failed } - } elsif ($nntp_opt->{starttls}) { + } elsif ($nntp_cfg->{starttls}) { $nn->can('starttls') or die "E: <$uri> Net::NNTP too old for STARTTLS\n"; $nn->starttls or die <<""; @@ -174,9 +214,9 @@ E: <$uri> STARTTLS requested and failed } sub nn_for ($$$$) { # nn = Net::NNTP - my ($self, $uri, $nn_args, $lei) = @_; + my ($self, $uri, $nn_common, $lei) = @_; my $sec = uri_section($uri); - my $nntp_opt = $self->{nntp_opt}->{$sec} //= {}; + my $nntp_cfg = $self->{cfg_opt}->{$sec} //= {}; my $host = $uri->host; # Net::NNTP and Net::Netrc both mishandle `0', so we pass `127.0.0.1' $host = '127.0.0.1' if $host eq '0'; @@ -191,38 +231,39 @@ sub nn_for ($$$$) { # nn = Net::NNTP }, 'PublicInbox::GitCredential'; ($u, $p) = split(/:/, $ui, 2); ($cred->{username}, $cred->{password}) = ($u, $p); - $p //= $cred->check_netrc; + $p //= $cred->check_netrc($lei); } - my $common = $nn_args->{$sec} // {}; + my $common = $nn_common->{$sec} // {}; my $nn_arg = { Port => $uri->port, Host => $host, - SSL => $uri->secure, # snews == nntps %$common, # may Debug .... }; + $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_opt, $uri); + my $nn = nn_new($nn_arg, $nntp_cfg, $uri) or + die "E: <$uri> new: $@".onion_hint($lei, $uri); if ($cred) { $cred->fill($lei) unless defined($p); # may prompt user here if ($nn->authinfo($u, $p)) { - push @{$nntp_opt->{-postconn}}, [ 'authinfo', $u, $p ]; + push @{$nntp_cfg->{-postconn}}, [ 'authinfo', $u, $p ]; } else { warn "E: <$uri> AUTHINFO $u XXXX failed\n"; $nn = undef; } } - if ($nntp_opt->{compress}) { + if ($nntp_cfg->{compress}) { # https://rt.cpan.org/Ticket/Display.html?id=129967 if ($nn->can('compress')) { if ($nn->compress) { - push @{$nntp_opt->{-postconn}}, [ 'compress' ]; + push @{$nntp_cfg->{-postconn}}, [ 'compress' ]; } else { warn "W: <$uri> COMPRESS failed\n"; } } else { - delete $nntp_opt->{compress}; + delete $nntp_cfg->{compress}; warn <<""; W: <$uri> COMPRESS not supported by Net::NNTP W: see https://rt.cpan.org/Ticket/Display.html?id=129967 for updates @@ -230,7 +271,7 @@ W: see https://rt.cpan.org/Ticket/Display.html?id=129967 for updates } } - $self->{nn_arg}->{$sec} = $nn_arg; + $self->{net_arg}->{$sec} = $nn_arg; $cred->run($nn ? 'approve' : 'reject') if $cred && $cred->{filled}; $nn; } @@ -282,37 +323,40 @@ sub imap_common_init ($;$) { die "DBD::SQLite is required for IMAP\n:$@\n"; require PublicInbox::URIimap; my $cfg = $self->{pi_cfg} // $lei->_lei_cfg; - my $mic_args = {}; # scheme://authority => Mail:IMAPClient arg + my $mic_common = {}; # scheme://authority => Mail:IMAPClient arg for my $uri (@{$self->{imap_order}}) { my $sec = uri_section($uri); + + # knobs directly for Mail::IMAPClient->new for my $k (qw(Starttls Debug Compress)) { my $bool = cfg_bool($cfg, "imap.$k", $$uri) // next; - $mic_args->{$sec}->{$k} = $bool; + $mic_common->{$sec}->{$k} = $bool; } my $to = cfg_intvl($cfg, 'imap.timeout', $$uri); - $mic_args->{$sec}->{Timeout} = $to if $to; + $mic_common->{$sec}->{Timeout} = $to if $to; + + # knobs we use ourselves: my $sa = socks_args($cfg->urlmatch('imap.Proxy', $$uri)); - $self->{imap_opt}->{$sec}->{-proxy_cfg} = $sa if $sa; + $self->{cfg_opt}->{$sec}->{-proxy_cfg} = $sa if $sa; for my $k (qw(pollInterval idleInterval)) { $to = cfg_intvl($cfg, "imap.$k", $$uri) // next; - $self->{imap_opt}->{$sec}->{$k} = $to; + $self->{cfg_opt}->{$sec}->{$k} = $to; } my $k = 'imap.fetchBatchSize'; my $bs = $cfg->urlmatch($k, $$uri) // next; - if ($bs =~ /\A([0-9]+)\z/) { - $self->{imap_opt}->{$sec}->{batch_size} = $bs; + if ($bs =~ /\A([0-9]+)\z/ && $bs > 0) { + $self->{cfg_opt}->{$sec}->{batch_size} = $bs; } else { - warn "$k=$bs is not an integer\n"; + warn "$k=$bs is not a positive integer\n"; } } # make sure we can connect and cache the credentials in memory - $self->{mic_arg} = {}; # schema://authority => IMAPClient->new args my $mics = {}; # schema://authority => IMAPClient obj for my $orig_uri (@{$self->{imap_order}}) { my $sec = uri_section($orig_uri); my $uri = PublicInbox::URIimap->new("$sec/"); my $mic = $mics->{$sec} //= - mic_for($self, $uri, $mic_args, $lei) // + mic_for($self, $uri, $mic_common, $lei) // die "Unable to continue\n"; next unless $self->isa('PublicInbox::NetWriter'); my $dst = $orig_uri->mailbox // next; @@ -332,10 +376,10 @@ sub nntp_common_init ($;$) { ($lei || eval { require PublicInbox::IMAPTracker }) or die "DBD::SQLite is required for NNTP\n:$@\n"; my $cfg = $self->{pi_cfg} // $lei->_lei_cfg; - my $nn_args = {}; # scheme://authority => Net::NNTP->new arg + my $nn_common = {}; # scheme://authority => Net::NNTP->new arg for my $uri (@{$self->{nntp_order}}) { my $sec = uri_section($uri); - my $args = $nn_args->{$sec} //= {}; + my $args = $nn_common->{$sec} //= {}; # Debug and Timeout are passed to Net::NNTP->new my $v = cfg_bool($cfg, 'nntp.Debug', $$uri); @@ -348,21 +392,20 @@ sub nntp_common_init ($;$) { # Net::NNTP post-connect commands for my $k (qw(starttls compress)) { $v = cfg_bool($cfg, "nntp.$k", $$uri) // next; - $self->{nntp_opt}->{$sec}->{$k} = $v; + $self->{cfg_opt}->{$sec}->{$k} = $v; } # -watch internal option for my $k (qw(pollInterval)) { $to = cfg_intvl($cfg, "nntp.$k", $$uri) // next; - $self->{nntp_opt}->{$sec}->{$k} = $to; + $self->{cfg_opt}->{$sec}->{$k} = $to; } } # make sure we can connect and cache the credentials in memory - $self->{nn_arg} = {}; # schema://authority => Net::NNTP->new args my %nn; # schema://authority => Net::NNTP object for my $uri (@{$self->{nntp_order}}) { my $sec = uri_section($uri); - $nn{$sec} //= nn_for($self, $uri, $nn_args, $lei); + $nn{$sec} //= nn_for($self, $uri, $nn_common, $lei); } \%nn; # for optional {nn_cached} } @@ -462,7 +505,7 @@ sub each_old_flags ($$$$) { my ($self, $mic, $uri, $l_uid) = @_; $l_uid ||= 1; my $sec = uri_section($uri); - my $bs = ($self->{imap_opt}->{$sec}->{batch_size} // 1) * 10000; + my $bs = ($self->{cfg_opt}->{$sec}->{batch_size} // 1) * 10000; my ($eml_cb, @args) = @{$self->{eml_each}}; $self->{quiet} or warn "# $uri syncing flags 1:$l_uid\n"; for (my $n = 1; $n <= $l_uid; $n += $bs) { @@ -554,7 +597,7 @@ EOF my $m = $mod ? " [(UID % $mod) == $shard]" : ''; warn "# $uri fetching UID $l_uid:$r_uid$m\n"; } - my $bs = $self->{imap_opt}->{$sec}->{batch_size} // 1; + my $bs = $self->{cfg_opt}->{$sec}->{batch_size} // 1; my $req = $mic->imap4rev1 ? 'BODY.PEEK[]' : 'RFC822.PEEK'; my $key = $req; $key =~ s/\.PEEK//; @@ -622,7 +665,7 @@ sub mic_get { return $mic if $mic && $mic->IsConnected; delete $cached->{$sec}; } - my $mic_arg = $self->{mic_arg}->{$sec} or + my $mic_arg = $self->{net_arg}->{$sec} or die "BUG: no Mail::IMAPClient->new arg for $sec"; if (defined(my $cb_name = $mic_arg->{Authcallback})) { if (ref($cb_name) ne 'CODE') { @@ -660,11 +703,11 @@ sub nn_get { my $cached = $self->{nn_cached} // {}; my $nn; $nn = delete($cached->{$sec}) and return $nn; - my $nn_arg = $self->{nn_arg}->{$sec} or + my $nn_arg = $self->{net_arg}->{$sec} or die "BUG: no Net::NNTP->new arg for $sec"; - my $nntp_opt = $self->{nntp_opt}->{$sec}; - $nn = nn_new($nn_arg, $nntp_opt, $uri) or return; - if (my $postconn = $nntp_opt->{-postconn}) { + my $nntp_cfg = $self->{cfg_opt}->{$sec}; + $nn = nn_new($nn_arg, $nntp_cfg, $uri) or return; + if (my $postconn = $nntp_cfg->{-postconn}) { for my $m_arg (@$postconn) { my ($method, @args) = @$m_arg; $nn->$method(@args) and next; @@ -684,21 +727,27 @@ sub _nntp_fetch_all ($$$) { my $msg = ndump($nn->message); return "E: GROUP $group <$sec> $msg"; } - + (defined($num_a) && defined($num_b) && $num_a > $num_b) and + return "E: $uri: backwards range: $num_a > $num_b"; + if (defined($num_a)) { # no article numbers in mail_sync.sqlite3 + $uri = $uri->clone; + $uri->group($group); + } # IMAPTracker is also used for tracking NNTP, UID == article number # LIST.ACTIVE can get the equivalent of UIDVALIDITY, but that's # expensive. So we assume newsgroups don't change: my ($itrk, $l_art) = itrk_last($self, $uri); - # allow users to specify articles to refetch - # cf. https://tools.ietf.org/id/draft-gilman-news-url-01.txt - # nntp://example.com/inbox.foo/$num_a-$num_b - $beg = $num_a if defined($num_a) && $num_a < $beg; - $end = $num_b if defined($num_b) && $num_b < $end; - if (defined $l_art) { + if (defined($l_art) && !defined($num_a)) { return if $l_art >= $end; # nothing to do $beg = $l_art + 1; } + # allow users to specify articles to refetch + # cf. https://tools.ietf.org/id/draft-gilman-news-url-01.txt + # nntp://example.com/inbox.foo/$num_a-$num_b + $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";