1 # Copyright (c) 2010-2012 Zmanda, Inc. All Rights Reserved.
3 # This program is free software; you can redistribute it and/or modify it
4 # under the terms of the GNU General Public License version 2 as published
5 # by the Free Software Foundation.
7 # This program is distributed in the hope that it will be useful, but
8 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
9 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12 # You should have received a copy of the GNU General Public License along
13 # with this program; if not, write to the Free Software Foundation, Inc.,
14 # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
16 # Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
17 # Sunnyvale, CA 94085, USA, or: http://www.zmanda.com
19 package Amanda::ClientService;
23 Amanda::ClientService -- support for writing amandad and inetd services
27 package main::Service;
28 use base qw( Amanda::ClientService );
32 if ($self->from_inetd) {
33 $self->check_bsd_security('main');
41 my $svc = main::Service->new();
42 Amanda::MainLoop::run();
46 Note that, despite the name, some client services are actually run on the
47 server - C<amidxtaped> and C<amindexd> in particular.
51 This package is used as a parent class, and is usually subclassed as
52 C<main::Service>. Its constructor takes no arguments, and automatically
53 configures the MainLoop to call the C<start> method:
55 my $svc = main::Service->new();
56 Amanda::MainLoop::start();
58 The object is a blessed hashref. And all of the keys used by this class are
59 private, and begin with an underscore. Subclasses are free to use any key
60 names that do not begin with an underscore.
62 =head2 Invocation Type
64 Some client services can be invoked directly from inetd for backward
65 compatibility. This package will automatically detect this, and act
66 accordingly. Subclasses can determine if they were invoked from inetd with the
67 boolean C<from_inetd> and C<from_amandad> methods. If C<from_amandad> is true,
68 then the authentication specified is available from the C<amandad_auth> method
69 (and may be C<undef>).
72 if $self->from_inetd();
73 print "hi, amandadd w/ ", ($self->amandad_auth() || "none")
74 if $self->from_amandad();
78 This package manages the available data streams as pairs of file descriptors,
79 providing convenience methods to hide some of the complexity of dealing with
80 them. Note that the class does not handle asynchronous reading or writing to
81 these file descriptors, and in fact most of the operations are entirely
82 synchronous, as they are non-blocking. File descriptors for streams are
83 available from the C<rfd> and C<wfd> methods, which both take a stream name:
85 Amanda::Util::full_write($self->wfd('MESG'), $buf, length($buf));
87 Each bidirectional stream has a name. At startup, stdin and stdout constitute
88 the 'main' stream. For amandad invocations, this stream should be used to read
89 the REQ packet and write the PREP and REP packets (see below). For inetd
90 invocations, this is the primary means of communicating with the other end of
93 For amandad invocations, the C<create_streams> method will create named streams
94 back to amandad. Each stream name is paired with direction indicators: C<'r'>
95 for read, C<'w'> for write, or C<'rw'> for both. Any unused file descriptors
96 will be closed. The method will return a C<CONNECT> line suitable for
97 inclusion in a REP packet, without a newline terminator, giving the streams in
98 the order they were specified.
100 push @replines, $self->connect_streams(
101 'DATA' => 'w', 'MESG' => 'rw', 'INDEX' => 'w');
103 For inetd invocations, the C<connection_listen> method will open a (privileged,
104 if C<$priv> is true) listening socket and return the port. You should then
105 write a C<CONNECT $port\n> to the main stream and call C<connection_acecpt> to
106 wait for a connection to the listening port. The latter method calls
107 C<finished_cb> with C<undef> on success or an error message on failure. The
108 C<$timeout> is specified in seconds. On success, the stream named C<$name> is
109 then available for use. Note that this method does not check security on the
110 connection. Also note that this process requires that the Amanda configuration
113 my $port = $self->connection_listen($name, $priv);
114 # send $port to the other side
115 $self->connection_accept($name, $timeout, $finished_cb);
117 To close a stream, use C<close>. This takes an optional second argument which
118 can be used to half-close connections which support it. Note that the
119 underlying file descriptor is closed immediately, without regard to any
120 outstanding asynchronous reads or writes.
122 $self->close('MESG'); # complete close ('rw' is OK too)
123 $self->close('INDEX', 'r'); # close for reading
124 $self->close('DATA', 'w'); # close for writing
128 Invocations from inetd require a BSD-style security check. The
129 C<check_bsd_security> method takes the stream and the authentication string,
130 I<without> the C<"SECURITY "> prefix or trailing newline, and performs the
131 necesary checks. To be clear, this string usually has the form C<"USER root">.
132 The method returns C<undef> if the check succeeds, and an error message if it
135 if ($self->check_bsd_security($stream, $authstr)) { .. }
137 Not that the security check is skipped if the service is being run from an
138 installcheck, since BSD security can't be tested by installchecks.
142 When invoked from amandad, a REQ packet is available on stdin, and amanadad
143 expects a REP packet on stdout. The C<parse_req> method takes the entire REP
144 packet, splits it into lines without trailing whitespace, exracts any OPTIONS
145 line into a hash, and decodes any features in the OPTIONS line. If no OPTIONS
146 appear, or the OPTIONS do not include features, then the C<features> key of the
147 result will be undefined. If there are format errors in the REQ, then the
148 C<errors> key will contain a list of error messages.
150 my $req_info = parse_req($req_str);
151 if (@{$req_info->{'errors'}}) {
152 print join("\n", @{$req_info->{'errors'}}), "\n";
155 print $req_info->{'options'}{'auth'}; # access to options
156 print "yes!" if $req_info->{'features'}->has($fe_whatever);
157 for my $line (@{$req_info->{'lines'}) {
158 print "got REQ line '$line'\n";
161 Note that the general format of OPTION strings is unknown at this time, so this
162 method may change significantly as more client services are added.
171 use Amanda::Util qw( :constants stream_server stream_accept );
172 use Amanda::Debug qw( debug );
173 use Amanda::Constants;
174 use Amanda::MainLoop;
182 _listen_sockets => {},
186 $self->_add_stream('main', 0, 1);
192 return 1 if (!defined $self->{'_argv'}[0] or $self->{'_argv'}[0] eq 'installcheck');
197 return 1 if defined $self->{'_argv'}[0] and $self->{'_argv'}[0] eq 'amandad';
200 sub from_installcheck {
202 return 1 if defined $self->{'_argv'}[0] and $self->{'_argv'}[0] eq 'installcheck';
207 return undef unless $self->from_amandad();
208 return $self->{'_argv'}[1];
211 sub connect_streams {
213 my $connect_line = "CONNECT";
214 my $fd = $Amanda::Constants::DATA_FD_OFFSET;
215 my $handle = $Amanda::Constants::DATA_FD_OFFSET;
218 # NOTE: while $handle and $fd both start counting in the same place, they
219 # are not the same thing! $fd counts real file descriptors - two per
220 # stream. $handle only increases by one for each stream.
222 if (@_ > $Amanda::Constants::DATA_FD_COUNT * 2) {
223 die "too many streams!";
226 die "not using amandad" if $self->from_inetd();
231 my ($wfd, $rfd) = (-1, -1);
233 # lower-numbered fd is for writing
237 push @fds_to_close, $fd;
241 # higher-numbered fd is for reading
245 push @fds_to_close, $fd;
249 $self->_add_stream($name, $rfd, $wfd);
250 $connect_line .= " $name $handle";
254 while ($fd < $Amanda::Constants::DATA_FD_OFFSET
255 + 2 * $Amanda::Constants::DATA_FD_COUNT) {
256 push @fds_to_close, $fd++;
259 # _dont_use_real_fds indicats that we should mock the close operation
260 if (!$self->{'_dont_use_real_fds'}) {
261 for $fd (@fds_to_close) {
262 if (!POSIX::close($fd)) {
263 Amanda::Debug::warning("Error closing fd $fd: $!");
266 # let the stupid OpenBSD threads library know we're using these fd's
267 Amanda::Util::openbsd_fd_inform();
269 $self->{'_would_have_closed_fds'} = \@fds_to_close;
272 return $connect_line;
275 sub connection_listen {
277 my ($name, $priv) = @_;
279 die "not using inetd" unless $self->from_inetd();
280 die "stream $name already exists" if exists $self->{'_streams'}{$name};
281 die "stream $name is already listening" if exists $self->{'_listen_sockets'}{$name};
283 # first open a socket
284 my ($lsock, $port) = stream_server(
285 $AF_INET, $STREAM_BUFSIZE, $STREAM_BUFSIZE, $priv);
286 return 0 if ($lsock < 0);
288 $self->{'_listen_sockets'}{$name} = $lsock;
293 sub connection_accept {
295 my ($name, $timeout, $finished_cb) = @_;
297 my $lsock = $self->{'_listen_sockets'}{$name};
298 die "stream $name is not listening" unless defined $lsock;
300 # set up a fd source *and* a timeout source
301 my ($fd_source, $timeout_source);
304 # accept is a "read" operation on a listening socket
305 $fd_source = Amanda::MainLoop::fd_source($lsock, $Amanda::MainLoop::G_IO_IN);
306 $fd_source->set_callback(sub {
309 $fd_source->remove();
310 $timeout_source->remove();
313 my $datasock = stream_accept($lsock, 1, $STREAM_BUFSIZE, $STREAM_BUFSIZE);
315 $errmsg = "error from stream_accept: $!";
319 POSIX::close($lsock);
320 delete $self->{'_listen_sockets'}{$name};
322 if ($datasock >= 0) {
323 $self->_add_stream($name, $datasock, $datasock);
326 return $finished_cb->($errmsg);
329 $timeout_source = Amanda::MainLoop::timeout_source(1000*$timeout);
330 $timeout_source->set_callback(sub {
333 $fd_source->remove();
334 $timeout_source->remove();
337 POSIX::close($lsock);
338 delete $self->{'_listen_sockets'}{$name};
340 return $finished_cb->("timeout while waiting for incoming TCP connection");
348 return -1 unless exists $self->{'_streams'}{$name};
349 my $fd = $self->{'_streams'}{$name}{'rfd'};
350 return defined($fd)? $fd : -1;
357 return -1 unless exists $self->{'_streams'}{$name};
358 my $fd = $self->{'_streams'}{$name}{'wfd'};
359 return defined($fd)? $fd : -1;
364 my ($name, $dir) = @_;
366 $dir = 'rw' unless defined $dir;
367 my $rfd = $self->rfd($name);
368 my $wfd = $self->wfd($name);
370 # sockets will have the read and write fd's, and are handled differently. If
371 # one end of a socket has been closed, then we can treat it like a regular fd
373 die "stream is already closed?" if ($rfd == -1);
378 } elsif ($dir eq 'r') {
379 # perl doesn't provide a fd-compatible shutdown, but luckily shudown
380 # affects dup'd file descriptors, too! So create a new handle and shut
381 # it down. When the handle is garbage collected, it will be closed,
382 # but that will not affect the original. This will look strange in an
383 # strace, but it works without SWIGging shutdown()
384 shutdown(IO::Handle->new_from_fd(POSIX::dup($rfd), "r"), 0);
386 } elsif ($dir eq 'w') {
387 shutdown(IO::Handle->new_from_fd(POSIX::dup($wfd), "w"), 1);
391 if ($dir =~ /r/ and $rfd != -1) {
395 if ($dir =~ /w/ and $wfd != -1) {
401 if ($rfd == -1 and $wfd == -1) {
402 delete $self->{'_streams'}{$name};
404 $self->{'_streams'}{$name}{'rfd'} = $rfd;
405 $self->{'_streams'}{$name}{'wfd'} = $wfd;
409 sub check_bsd_security {
411 my ($name, $authstr) = @_;
413 # find the open file descriptor
414 my $fd = $self->rfd($name);
415 $fd = $self->wfd($name) if $fd < 0;
416 die "stream '$name' not open" if $fd < 0;
418 # don't invoke check_security if we're run as an installcheck;
419 # installchecks are incompatible with BSD security
420 return undef if $self->from_installcheck();
422 return Amanda::Util::check_security($fd, $authstr);
435 # split into lines, split by '\n', filtering empty lines
436 my @req_lines = grep /.+/, (split(/\n/, $req_str));
437 $rv->{'lines'} = [ @req_lines ];
439 # find and parse the options line
440 my @opt_lines = grep /^OPTIONS /, @req_lines;
441 if (@opt_lines > 1) {
442 push @{$rv->{'errors'}}, "got multiple OPTIONS lines";
443 # (and use the first OPTIONS line anyway)
446 my ($line) = $opt_lines[0] =~ /^OPTIONS (.*)$/;
447 my @opts = grep /.+/, (split(/;/, $line));
448 for my $opt (@opts) {
449 if ($opt =~ /^([^=]+)=(.*)$/) {
450 $rv->{'options'}{$1} = $2;
452 $rv->{'options'}{$opt} = 1;
458 if ($rv->{'options'}{'features'}) {
459 $rv->{'features'} = Amanda::Feature::Set->from_string($rv->{'options'}{'features'});
469 my ($stream, $rfd, $wfd) = @_;
471 $self->{'_streams'}{$stream} = {