3 ### Adverbial modifier conferring AUTHINFO GENERIC support on NNTP clients
5 ### (c) 2016 Mark Wooding
8 ###----- Licensing notice ---------------------------------------------------
10 ### This program is free software; you can redistribute it and/or modify
11 ### it under the terms of the GNU General Public License as published by
12 ### the Free Software Foundation; either version 2 of the License, or
13 ### (at your option) any later version.
15 ### This program is distributed in the hope that it will be useful,
16 ### but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 ### GNU General Public License for more details.
20 ### You should have received a copy of the GNU General Public License
21 ### along with this program; if not, write to the Free Software Foundation,
22 ### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
26 ## split parsing and resolution of addresses
30 my $VERSION = "0.1.0~unfinished";
34 ###--------------------------------------------------------------------------
37 ## Included batteries.
40 use Getopt
::Long
qw(:config gnu_compat bundling
41 require_order no_getopt_compat
);
42 use POSIX
qw(:errno_h
:fcntl_h
:sys_wait_h
);
43 use Socket
qw(/^[AP]F_/ /^SOCK_/ /^sockaddr_/
44 getaddrinfo
/^AI_/ /^EAI_/
48 ## External batteries.
51 ###--------------------------------------------------------------------------
52 ### Configuration variables.
54 ## The global configuration.
59 ## The per-server configuration.
61 my %SPARAM = map { $_ => 1 }
62 "local", "nntpauth", "remote", "sshbind", "via";
64 ## Various facts we might discover.
65 my $HOME = $ENV{"HOME"};
66 (my $PROG = $0) =~ s
:^.*/::;
72 ## Other bits of useful state.
78 my $CLIENTKID = undef;
80 ###--------------------------------------------------------------------------
87 print STDERR
"$PROG: $msg\n";
109 print STDERR
"$PROG: ;; $msg\n" if $VERBOSE;
120 defined $HOME or fail
"no home directory set";
124 sub ensure_dir_exists
($$) {
125 my ($dir, $mode) = @_;
126 mkdir $dir, $mode or $! == EEXIST
or
127 sysfail
"failed to create directory `$dir': $!";
135 unless (opendir $d, $f) {
136 moan
"failed to open directory `$d': $!";
140 defined (my $b = readdir $d) or last ENTRY
;
141 next ENTRY
if grep { $b eq $_ } ".", "..";
145 rmdir $f or $! == ENOENT
or moan
"failed to zap directory `$f': $!";
147 unlink $f or $! == ENOENT
or moan
"failed to zap file thing `$f': $!";
151 sub set_cloexec
($) {
153 my $f = fcntl $fh, F_GETFD
, 0 or sysfail
"failed to get per-fd flags: $!";
154 fcntl $fh, F_SETFD
, $f | FD_CLOEXEC
or
155 sysfail
"failed to set close-on-exec: $!";
160 my $l = new File
::FcntlLock
;
161 $l->lock($f, F_GETLK
) or sysfail
"couldn't read locking for `$f': $!";
162 return $l->l_type != F_UNLCK
;
168 if (defined $kid && !$kid) { $INKIDP = 1; }
173 sub sequence
() { return $SEQ++; }
175 ###--------------------------------------------------------------------------
176 ### Setting up the configuration.
178 sub set_global_param
($$) {
179 my ($param, $value) = @_;
180 exists $C{$param} or fail
"unknown global parameter `$param'";
184 sub notice_server
($$) {
185 my ($server, $where) = @_;
186 inform
"found server `$server' $where";
190 sub set_server_param
($$$) {
191 my ($server, $param, $value) = @_;
192 $S{$server} or bad
"unknown server `$param'";
193 $SPARAM{$param} or bad
"unknown server parameter `$param'";
194 $S{$server}{$param} = $value;
197 sub chew_cli_server_configs
(\@
) {
202 last ARG
unless @
$args;
203 my $arg = shift @
$args;
204 if ($arg eq "+") { last ARG
; }
205 elsif ($arg =~ /^\+/) {
206 $server = substr $arg, 1;
207 notice_server
$server, "on command line";
209 elsif (!defined $server or $arg !~ /^([^=]+)=(.*)$/)
210 { unshift @
$args, $arg; last ARG
; }
211 else { set_server_param
$server, $1, $2; }
215 sub parse_config_file
() {
217 ## If we already know what we're doing then forbid a configuration file as
220 return unless defined $CONF;
221 fail
"servers defined on command-line; won't read config file too";
224 ## Search about to find a suitable configuration file.
227 ($ENV{"XDG_CONFIG_HOME"} // ensure_home
. "/.config",
228 split /:/, $ENV{"XDG_CONFIG_DIRS"} // "/etc/xdg");
229 inform
"searching for a configuration file with tag `$TAG'...";
230 PATH
: for my $dir (@confpath) {
231 for my $base ($TAG, "\@default") {
232 my $f = "$dir/with-authinfo-kludge/$base.conf";
233 if (open $cf, "<", $f) {
234 inform
" found `$f'; search over";
235 $CONF = $f; last PATH
;
236 } elsif ($! != ENOENT
) {
237 bad
"couldn't open `$f' for reading: $!";
239 inform
" `$f' not found; search continues";
244 ## If we still don't have a configuration file then synthesize one from the
245 ## `$NNTPSERVER' variable.
247 my $server = $ENV{"NNTPSERVER"};
248 defined $server or fail
"no `NNTPSERVER' defined in the environment";
249 inform
"no config file found; synthesizing default";
250 notice_server
$server, "in environment";
254 ## Work through the configuration file setting up servers.
255 my $set_param = \
&set_global_param
;
257 next if /^\s*([#;]|$)/;
258 if (/^\s*\[(.+)\]\s*$/) {
260 if ($head eq "\@GLOBAL") { $set_param = \
&set_global_param
; }
262 notice_server
$head, "in config file";
263 $set_param = sub { set_server_param
$head, $_[0], $_[1]; };
265 } elsif (/^([^=]+)=(.*)$/) { $set_param->(trim
$1, trim
$2); }
266 else { bad
"$CONF:$.: couldn't parse configuration file line"; }
268 (!$cf->error and close $cf)
269 or sysfail
"error reading configuration file `$CONF': $!";
272 sub format_value
($);
273 sub format_value
($) {
275 if (!defined $value) { return "<undef>"; }
276 elsif (my $r = ref $value) {
278 return "[" . join(", ", map { format_value
$_ } @
$value) . "]";
279 } elsif ($r eq "HASH") {
281 join(", ", map { format_value
$_ . " => " .
282 format_value
$value->{$_} } sort keys %$value) .
287 } else { return "`$value'"; }
290 sub inform_param
($$) {
291 my ($param, $value) = @_;
292 inform
" $param = " . format_value
$value;
295 sub dump_configuration
() {
296 inform
"Global parameters...";
297 for my $p (sort keys %C) { inform_param
$p, $C{$p}; }
299 for my $s (sort keys %S) {
300 inform
"Server `$s' parameters...";
301 for my $p (sort keys %{$S{$s}}) { inform_param
$p, $S{$s}{$p}; }
305 ###--------------------------------------------------------------------------
306 ### Managing the runtime directory.
308 ### Truly told, this bit is probably the trickiest part of the program.
310 ## How long we allow for a new server directory to be set up.
315 ## Maybe we've done all of this already.
316 defined $RUNDIR and return;
318 ## Find a suitable place to put things.
320 inform
"searching for a suitable runtime directory...";
322 ## Maybe the user's configured a directory explicitly. (Maybe we still
323 ## have to arrange for this to exist.)
324 if (defined ($RUNDIR = $C{"rundir"})) {
325 inform
"using runtime directory from configuration";
329 ## First attempt: use `$XDG_RUNTIME_DIR'.
330 if (defined (my $runhome = $ENV{"XDG_RUNTIME_DIR"})) {
331 inform
"setting runtime directory from `XDG_RUNTIME_DIR'";
332 $RUNDIR = "$runhome/with-authinfo-kludge";
336 ## Second attempt: let's use /tmp, or whatever `$TMPDIR' is set.
337 my $tmpdir = $ENV{"TMPDIR"} // "/tmp";
338 inform
"investigating putting runtime directory under tmpdir `$tmpdir'";
339 my $dir = "$tmpdir/with-authinfo-kludge-$>";
341 if (!$st && $! == ENOENT
) {
342 mkdir $dir, 0700 or sysfail
"failed to create directory `$dir': $!";
344 inform
"created `$dir'";
346 if (!-d
$st) { inform
"alas, `$dir' isn't a directory"; }
347 elsif ($st->uid != $>) { inform
"alas, we don't own `$dir'"; }
348 elsif ($st->mode & 0077) { inform
"alas, `$dir' has liberal perms"; }
350 inform
"accepting `$dir' as runtime directory";
355 ## Third attempt: we'll use the XDG cache directory.
356 my $cachehome = $ENV{"XDG_CACHE_HOME"} // ensure_home
. "/.cache";
357 ensure_dir_exists
$cachehome, 0777;
359 $RUNDIR = "$cachehome/with-authinfo-kludge.$host";
360 inform
"last ditch: using `$RUNDIR' as runtime directory";
363 ## Make the runtime directory if it doesn't exist. Be paranoid here; users
364 ## can override if they really want. (Note that noip(1) is untweakably
365 ## picky about its socket directories, so this is less generous than it
367 ensure_dir_exists
$RUNDIR, 0700;
368 for my $d ("junk", "new") { ensure_dir_exists
"$RUNDIR/$d", 0777; }
371 sub junk_rundir_thing
($$) {
373 inform
"junking $what `$f'";
375 ## Find a name to rename it to under the `junk' directory. Anyone can put
376 ## things in the `junk' directory, and anyone is allowed to delete them;
377 ## the only tricky bit is making sure the names don't collide.
380 my $r = int rand 1000000;
381 $junk = "$RUNDIR/junk/j.$r";
383 ## It'll be OK if this fails because someone else has junked the file (in
384 ## which case we end happy), or if the target exists (in which case we
385 ## pick another and try again).
386 if (rename $f, $junk or ($! == ENOENT
&& !-e
$f)) { last NAME
; }
387 elsif ($! != EEXIST
) { sysfail
"couldn't rename `$f' to `$junk': $!"; }
393 sub clean_up_rundir
() {
394 inform
"cleaning up stale things from runtime directory";
396 ## Work through the things in the directory, making sure they're meant to
398 opendir my $dh, $RUNDIR or
399 sysfail
"failed to open directory `$RUNDIR': $!";
401 defined (my $base = readdir $dh) or last ENTRY
;
402 next ENTRY
if grep { $base eq $_ } ".", "..";
403 my $f = "$RUNDIR/$base";
405 ## If this thing isn't a directory then it shouldn't be there. Maybe a
406 ## later version of us put it there.
408 inform
"found unexpected thing `$f' in runtime directory";
412 ## Maybe it's a standard thing that's meant to be here. We'll clean
414 next ENTRY
if grep { $base eq $_ } "junk", "new";
416 ## If the name doesn't have a `.' in it, then it's some other special
417 ## thing which we don't understand.
418 if ($base !~ /^s.*\.\d+/) {
419 inform
"found unexpected special directory `$f' in runtime directory";
423 ## Otherwise, it's a session directory. If its lockfile isn't locked
424 ## then it's fair game.
426 if (open my $fh, "<", $lk) {
427 my $ownedp = lockedp
$fh;
428 close $fh or sysfail
"couldn't close file, what's up with that?: $!";
429 if (!$ownedp) { junk_rundir_thing
$f, "stale session dir"; }
430 } elsif ($! == ENOENT
) {
431 junk_rundir_thing
$f, "session dir without `lock' file";
433 moan
"couldn't open `$lk' (found in runtime dir) for reading: $!";
434 inform
"leaving `$f' alone";
439 ## Work through the things in the `new' directory.
440 my $thresh = time - $BIRTHTIME;
441 my $newdir = "$RUNDIR/new";
442 opendir $dh, $newdir or
443 sysfail
"failed to open directory `$newdir': $!";
445 defined (my $base = readdir $dh) or last NEW
;
446 next NEW
if grep { $base eq $_ } ".", "..";
447 my $f = "$newdir/$base";
449 inform
"found unexepected nondirectory thing `$f' in nursery";
452 if ($base !~ /^n\.(\d+)\./) {
453 inform
"found directory with unexpected name `$f' in nursery";
457 $stamp >= $thresh or junk_rundir_thing
$f, "stillborn session directory";
461 ## Work through the things in the `junk' directory. Anyone can put things
462 ## in the `junk' directory, and anyone is allowed to delete them.
463 ## Therefore we can just zap everything in here. The `zap' function is
464 ## (somewhat) careful not to screw up if someone else is also zapping the
466 my $junkdir = "$RUNDIR/junk";
467 opendir $dh, $junkdir or
468 sysfail
"failed to open directory `$junkdir': $!";
470 defined (my $base = readdir $dh) or last NEW
;
471 next NEW
if grep { $base eq $_ } ".", "..";
472 my $f = "$junkdir/$base";
478 sub make_session_dir
() {
479 inform
"making session directory for `$TAG'";
481 ## Make a new directory in the nursery. Only the creator of a nursery
482 ## directory is allowed to put things in it.
483 my $newdir = "$RUNDIR/new";
487 my $r = int rand 1000000;
488 $n = "$newdir/n.$now.$$.$r";
489 if (mkdir $n, 0777) { last NAME
; }
490 elsif ($! != EEXIST
) { sysfail
"failed to create `$n': $!"; }
493 ## Create the lockfile, and take out a lock.
494 open my $fh, ">", "$n/lock";
496 my $l = File
::FcntlLock
->new(l_type
=> F_WRLCK
,
497 l_whence
=> SEEK_SET
,
500 $l->lock($fh, F_SETLK
) or sysfail
"failed to lock `$n/lock: $!";
502 ## Rename the directory into its proper place. We have already cleaned out
503 ## stale directories, and the target name has our PID in it, so it can't
504 ## exist any more unless something unfortunate has happened.
505 $SESSDIR = "$RUNDIR/s.$TAG.$$";
506 rename $n, $SESSDIR or sysfail
"failed to rename `$n' to `$SESSDIR': $!";
508 ## Create some necessary things.
509 ensure_dir_exists
"$SESSDIR/noip-client", 0700;
513 zap junk_rundir_thing
$SESSDIR, "cleanup on exit"
514 if !$INKIDP && defined $SESSDIR;
517 ###--------------------------------------------------------------------------
518 ### Setting up a session.
520 sub parse_address
($;$) {
521 my ($addr, $defport) = @_;
522 inform
"parsing address `$addr'...";
525 if ($addr =~ /^\[([^]]*)\]:(\d+)$/ || $addr =~ /^([^:]+):(\d+)$/)
526 { $host = $1; $port = $2; }
527 elsif (defined $defport) { $host = $addr; $port = $defport; }
528 else { fail
"invalid address `$addr': missing port number"; }
529 inform
" host = `$host'; port = $port";
530 return ($host, $port);
533 sub format_address
($$) {
534 my ($host, $port) = @_;
535 $host =~ /:/ and $host = "[$host]";
536 return "$host:$port";
539 sub canonify_address
($;$) {
540 my ($addr, $defport) = @_;
541 my ($host, $port) = parse_address
$addr, $defport;
542 return format_address
$host, $port;
545 sub resolve_parsed_address
($$) {
546 my ($host, $port) = @_;
547 inform
"resolving host `$host', port $port";
549 my ($err, @a) = getaddrinfo
$host, $port, { flags
=> AI_NUMERICSERV
};
550 $err and fail
"failed to resolve `$host': $err";
555 ($err, $host, $port) =
556 getnameinfo
$a->{addr
}, NI_NUMERICHOST
| NI_NUMERICSERV
;
557 $err and sysfail
"unexpectedly failed to convert addr to text: $err";
558 inform
" resolved to $host $port";
559 my $r = format_address
$host, $port;
560 unless ($seen{$r}) { push @res, $r; $seen{$r} = 1; }
566 sub resolve_address
($;$) {
567 my ($addr, $defport) = @_;
568 my ($host, $port) = parse_address
$addr, $defport;
569 return resolve_parsed_address
$host, $port;
572 sub fix_server_config
($) {
576 ## Keep the name. This is useful for diagnostics, but it's also important
577 ## for finding the right socket directory if we're doing SSH forwarding.
578 $s->{"_name"} = $server;
580 ## Sort out the various addresses.
582 ($host, $port) = parse_address
($s->{"local"} // $server, 119);
583 $s->{"local"} = format_address
$host, $port;
584 $s->{"_laddrs"} = [resolve_parsed_address
$host, $port];
585 $s->{"remote"} = canonify_address
($s->{"remote"} // $server, 119);
586 ($host, $port) = parse_address
($s->{"sshbind"} // "127.1.0.1", 1119);
587 $s->{"sshbind"} = format_address
$host, $port;
588 $s->{"_sshaddrs"} = [resolve_parsed_address
$host, $port];
590 ## Initialize other settings.
591 $s->{"_proxy_noip"} = undef;
592 $s->{"_proxy_sockdir"} = undef;
593 $s->{"_proxy_server"} = defined $s->{"via"} ?
594 $s->{"sshbind"} : $s->{"remote"};
595 $s->{"_proxy_server"} =~ s/:119$//;
596 $s->{"_proxy_server"} =~ s/^\[(.*)\]$/$1/;
597 $s->{"_sshkid"} = undef;
598 $s->{"_ssh_master"} = undef;
601 sub hack_noip_envvar
($$) {
602 my ($var, $val) = @_;
603 inform
" hack env for noip: $var = `$val'";
607 sub hack_noip_env
($$) {
608 my ($vars, $dir) = @_;
611 hack_noip_envvar
"LD_PRELOAD",
613 (exists $ENV{"LD_PRELOAD"} ?
":" . $ENV{"LD_PRELOAD"} : "");
614 for my $k (keys %ENV) { delete $ENV{$k} if $k =~ /^NOIP_/; }
615 hack_noip_envvar
"NOIP_CONFIG", "$RUNDIR/noip.conf.notexist";
616 hack_noip_envvar
"NOIP_SOCKETDIR", $dir;
617 hack_noip_envvar
"NOIP_DEBUG", $VERBOSE;
618 for my $acl ("REALBIND", "REALCONNECT") {
619 hack_noip_envvar
"NOIP_$acl",
620 join ",", @
{$vars->{$acl} // []}, "+any";
624 sub server_listen
($) {
628 ## Set up the listening sockets for this server's addresses.
629 inform
"set up sockets for `$server'";
630 for my $a (@
{$s->{"_laddrs"}}) {
631 socket my $sk, PF_UNIX
, SOCK_STREAM
, 0
632 or sysfail
"failed to make Unix-domain socket: $!";
634 my $sa = "$SESSDIR/noip-client/$a";
635 bind $sk, sockaddr_un
$sa
636 or sysfail
"failed to bind Unix-domain socket to `$sa': $!";
637 listen $sk, 5 or sysfail
"failed to listen on Unix-domain socket: $!";
638 $SERVMAP{fileno $sk} = [$s, $a, $sk];
639 inform
" listening on $a";
640 push @
{$CLIENT_NOIP{"REALCONNECT"}}, "-$a";
643 ## If we're forwarding via SSH then set that up too.
644 if (defined (my $via = $s->{"via"})) {
645 inform
"set up SSH tunnel to `$server' via $via...";
647 my $sockdir = "$SESSDIR/noip-ssh.$server";
648 ensure_dir_exists
$sockdir, 0700;
649 my $sshbind = $s->{"sshbind"};
650 my $remote = $s->{"remote"};
651 for my $a (@
{$s->{"_sshaddrs"}}) {
652 push @
{$ssh_noip{"REALBIND"}}, "-$a";
653 inform
" listening on $a";
654 push @
{$s->{"_proxy_noip"}{"REALCONNECT"}}, "-$a";
656 $s->{"_proxy_sockdir"} = $sockdir;
658 ## This is quite awful. The `-L' option sets up the tunnel that we
659 ## actually wanted. The `-v' makes SSH spew stuff to stdout, which might
660 ## be useful if you're debugging. The `-S' has two effects: firstly, it
661 ## detaches OpenSSH from any other control master things which might be
662 ## going on, because they tend to interfere with forwarding (and,
663 ## besides, the existing master won't be under the same noip
664 ## configuration); and, secondly, it causes OpenSSH to make a socket in a
665 ## place we know, so we can tell when it's actually ready. The `cat'
666 ## will keep the tunnel open until we close our end, which we don't do
668 inform
" starting SSH tunnel";
669 my @sshargs = ("ssh", "-L$sshbind:$remote");
670 $VERBOSE and push @sshargs, "-v";
671 my $master = "$SESSDIR/ssh-master." . sequence
;
672 push @sshargs, "-S$master", "-M";
673 $s->{"_ssh_master"} = $master;
674 push @sshargs, $via, "cat";
675 pipe my $rfd, my $wfd or sysfail
"failed to create pipe: $!";
677 defined (my $kid = myfork
) or sysfail
"failed to fork: $!";
679 open STDIN
, "<&", $rfd or sysfail
"failed to dup pipe to stdin: $!";
680 open STDOUT
, ">", "/dev/null"
681 or sysfail
"failed to redirect stdout to /dev/null: $!";
682 hack_noip_env \
%ssh_noip, $sockdir;
683 exec @sshargs or sysfail
"failed to exec SSH: $!";
686 $s->{"_sshkid"} = $kid;
687 $s->{"_ssh_pipe"} = $wfd;
688 $KIDMAP{$kid} = [$s, "SSH tunnel"];
692 sub wait_for_ssh
() {
693 inform
"waiting for SSH tunnels to start...";
700 KID
: for my $kid (keys %KIDMAP) {
701 my ($s, $what) = @
{$KIDMAP{$kid}};
702 next KID
unless $kid == $s->{"_sshkid"};
703 if (-S
$s->{"_ssh_master"}) {
704 inform
" found socket from `$s->{_name}'";
706 inform
" no socket yet from `$s->{_name}'";
711 inform
" all present and correct!";
715 inform
" bored now; giving up";
718 inform
"waiting ${delay}s for stuff to happen...";
719 select undef, undef, undef, $delay;
726 defined (my $kid = waitpid -1, WNOHANG
)
727 or sysfail
"failed to reap child: $!";
728 last KID
if $kid <= 0;
731 $how = "exited successfully";
733 } elsif ($?
& 0xff) {
735 $how = "killed by signal $sig";
736 $how .= " (core dumped)" if $?
& 0x80;
740 $how = "exited with status $rc";
742 if ($kid == $CLIENTKID) {
743 inform
"client kid $how; shutting down";
745 } elsif (exists $KIDMAP{$kid}) {
746 my ($s, $what) = @
{$KIDMAP{$kid}};
747 inform
"$what for server `$s->{_name}' collapsed ($how)";
748 delete $KIDMAP{$kid};
750 inform
"unrecognized child $kid $how";
758 inform
"starting client";
759 defined (my $kid = myfork
) or sysfail
"failed to fork: $!";
761 hack_noip_env \
%CLIENT_NOIP, "$SESSDIR/noip-client";
763 exec @args or sysfail
"failed to exec `$prog': $!";
770 for my $fd (keys %SERVMAP) { vec($rfd_in, $fd, 1) = 1; }
772 my ($n, $t) = select my $rfd_out = $rfd_in, undef, undef, undef;
773 $n >= 0 || $! == EINTR
or sysfail
"select failed: $!";
774 FD
: for my $fd (keys %SERVMAP) {
775 next unless vec $rfd_out, $fd, 1;
776 my ($s, $a, $sk) = @
{$SERVMAP{$fd}};
778 unless (accept $nsk, $sk) {
779 moan
"failed to accept new connection: $!";
783 inform
"incoming connection `$s->{_name}' to $a; starting proxy...";
784 defined (my $kid = myfork
) or sysfail
"failed to fork: $!";
786 $ENV{"NNTPAUTH"} = $s->{"nntpauth"} if exists $s->{"nntpauth"};
787 hack_noip_env
$s->{"_proxy_noip"}, $s->{"_proxy_sockdir"};
788 open STDIN
, "<&", $nsk
789 or sysfail
"failed to dup socket to kid stdin: $!";
790 open STDOUT
, ">&", $nsk
791 or sysfail
"failed to dup socket to kid stdin: $!";
792 inform
"running proxy to `$s->{_proxy_server}'";
793 exec "authinfo-kludge", $s->{"_proxy_server"}
794 or sysfail
"failed to exec `authinfo-kludge': $!";
796 $KIDMAP{$kid} = [$s, "proxy"];
801 ###--------------------------------------------------------------------------
806 print $fh "$PROG, version $VERSION\n";
812 usage: $PROG [-v] [-d DIR] [-f CONF] [-t TAG]
813 [ [+SERVER] [PARAM=VALUE ...] ...] [+]
824 Command-line options:
825 -h, --help Show this help text.
826 -d, --rundir=DIR Use DIR to store runtime state.
827 -f, --config=FILE Read configuration from FILE.
828 -t, --tag=TAG Use TAG to identify this session.
829 -v, --verbose Emit running commentary to stderr.
831 Server parameter summary:
832 local=ADDRESS Listen on ADDRESS for client connections.
833 nntpauth=AUTH-METHOD Set authentication method and arguments.
834 remote=ADDRESS Connect to server at ADDRESS.
835 sshbind=ADDRESS Use ADDRESS for local SSH tunnel endpoint.
836 via=SSH-HOST Use SSH to connect to remote server.
838 See the manual page for full details.
844 "h|help" => sub { help
; exit 0; },
845 "version" => sub { version
*STDOUT
; exit 0; },
846 "d|rundir=s" => \
$RUNDIR,
847 "f|config=s" => \
$CONF,
849 "v|verbose" => \
$VERBOSE
851 chew_cli_server_configs
@ARGV;
853 (my $cmd = $ARGV[0]) =~ s
:^.*/::;
858 if ($BAD) { usage
*STDERR
; exit 1; }
860 for my $server (keys %S) { fix_server_config
$server; }
861 dump_configuration
if $VERBOSE;
865 for my $server (keys %S) { server_listen
$server; }
873 ###----- That's all, folks --------------------------------------------------