1 # Copyright (c) 2010-2012 Zmanda, Inc. All Rights Reserved.
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful, but
9 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
10 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
13 # You should have received a copy of the GNU General Public License along
14 # with this program; if not, write to the Free Software Foundation, Inc.,
15 # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17 # Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
18 # Sunnyvale, CA 94085, USA, or: http://www.zmanda.com
20 package Amanda::ClientService;
24 Amanda::ClientService -- support for writing amandad and inetd services
28 package main::Service;
29 use base qw( Amanda::ClientService );
33 if ($self->from_inetd) {
34 $self->check_bsd_security('main');
42 my $svc = main::Service->new();
43 Amanda::MainLoop::run();
47 Note that, despite the name, some client services are actually run on the
48 server - C<amidxtaped> and C<amindexd> in particular.
52 This package is used as a parent class, and is usually subclassed as
53 C<main::Service>. Its constructor takes no arguments, and automatically
54 configures the MainLoop to call the C<start> method:
56 my $svc = main::Service->new();
57 Amanda::MainLoop::start();
59 The object is a blessed hashref. And all of the keys used by this class are
60 private, and begin with an underscore. Subclasses are free to use any key
61 names that do not begin with an underscore.
63 =head2 Invocation Type
65 Some client services can be invoked directly from inetd for backward
66 compatibility. This package will automatically detect this, and act
67 accordingly. Subclasses can determine if they were invoked from inetd with the
68 boolean C<from_inetd> and C<from_amandad> methods. If C<from_amandad> is true,
69 then the authentication specified is available from the C<amandad_auth> method
70 (and may be C<undef>).
73 if $self->from_inetd();
74 print "hi, amandadd w/ ", ($self->amandad_auth() || "none")
75 if $self->from_amandad();
79 This package manages the available data streams as pairs of file descriptors,
80 providing convenience methods to hide some of the complexity of dealing with
81 them. Note that the class does not handle asynchronous reading or writing to
82 these file descriptors, and in fact most of the operations are entirely
83 synchronous, as they are non-blocking. File descriptors for streams are
84 available from the C<rfd> and C<wfd> methods, which both take a stream name:
86 Amanda::Util::full_write($self->wfd('MESG'), $buf, length($buf));
88 Each bidirectional stream has a name. At startup, stdin and stdout constitute
89 the 'main' stream. For amandad invocations, this stream should be used to read
90 the REQ packet and write the PREP and REP packets (see below). For inetd
91 invocations, this is the primary means of communicating with the other end of
94 For amandad invocations, the C<create_streams> method will create named streams
95 back to amandad. Each stream name is paired with direction indicators: C<'r'>
96 for read, C<'w'> for write, or C<'rw'> for both. Any unused file descriptors
97 will be closed. The method will return a C<CONNECT> line suitable for
98 inclusion in a REP packet, without a newline terminator, giving the streams in
99 the order they were specified.
101 push @replines, $self->connect_streams(
102 'DATA' => 'w', 'MESG' => 'rw', 'INDEX' => 'w');
104 For inetd invocations, the C<connection_listen> method will open a (privileged,
105 if C<$priv> is true) listening socket and return the port. You should then
106 write a C<CONNECT $port\n> to the main stream and call C<connection_acecpt> to
107 wait for a connection to the listening port. The latter method calls
108 C<finished_cb> with C<undef> on success or an error message on failure. The
109 C<$timeout> is specified in seconds. On success, the stream named C<$name> is
110 then available for use. Note that this method does not check security on the
111 connection. Also note that this process requires that the Amanda configuration
114 my $port = $self->connection_listen($name, $priv);
115 # send $port to the other side
116 $self->connection_accept($name, $timeout, $finished_cb);
118 To close a stream, use C<close>. This takes an optional second argument which
119 can be used to half-close connections which support it. Note that the
120 underlying file descriptor is closed immediately, without regard to any
121 outstanding asynchronous reads or writes.
123 $self->close('MESG'); # complete close ('rw' is OK too)
124 $self->close('INDEX', 'r'); # close for reading
125 $self->close('DATA', 'w'); # close for writing
129 Invocations from inetd require a BSD-style security check. The
130 C<check_bsd_security> method takes the stream and the authentication string,
131 I<without> the C<"SECURITY "> prefix or trailing newline, and performs the
132 necesary checks. To be clear, this string usually has the form C<"USER root">.
133 The method returns C<undef> if the check succeeds, and an error message if it
136 if ($self->check_bsd_security($stream, $authstr)) { .. }
138 Not that the security check is skipped if the service is being run from an
139 installcheck, since BSD security can't be tested by installchecks.
143 When invoked from amandad, a REQ packet is available on stdin, and amanadad
144 expects a REP packet on stdout. The C<parse_req> method takes the entire REP
145 packet, splits it into lines without trailing whitespace, exracts any OPTIONS
146 line into a hash, and decodes any features in the OPTIONS line. If no OPTIONS
147 appear, or the OPTIONS do not include features, then the C<features> key of the
148 result will be undefined. If there are format errors in the REQ, then the
149 C<errors> key will contain a list of error messages.
151 my $req_info = parse_req($req_str);
152 if (@{$req_info->{'errors'}}) {
153 print join("\n", @{$req_info->{'errors'}}), "\n";
156 print $req_info->{'options'}{'auth'}; # access to options
157 print "yes!" if $req_info->{'features'}->has($fe_whatever);
158 for my $line (@{$req_info->{'lines'}) {
159 print "got REQ line '$line'\n";
162 Note that the general format of OPTION strings is unknown at this time, so this
163 method may change significantly as more client services are added.
172 use Amanda::Util qw( :constants stream_server stream_accept );
173 use Amanda::Debug qw( debug );
174 use Amanda::Constants;
175 use Amanda::MainLoop;
183 _listen_sockets => {},
187 $self->_add_stream('main', 0, 1);
193 return 1 if (!defined $self->{'_argv'}[0] or $self->{'_argv'}[0] eq 'installcheck');
198 return 1 if defined $self->{'_argv'}[0] and $self->{'_argv'}[0] eq 'amandad';
201 sub from_installcheck {
203 return 1 if defined $self->{'_argv'}[0] and $self->{'_argv'}[0] eq 'installcheck';
208 return undef unless $self->from_amandad();
209 return $self->{'_argv'}[1];
212 sub connect_streams {
214 my $connect_line = "CONNECT";
215 my $fd = $Amanda::Constants::DATA_FD_OFFSET;
216 my $handle = $Amanda::Constants::DATA_FD_OFFSET;
219 # NOTE: while $handle and $fd both start counting in the same place, they
220 # are not the same thing! $fd counts real file descriptors - two per
221 # stream. $handle only increases by one for each stream.
223 if (@_ > $Amanda::Constants::DATA_FD_COUNT * 2) {
224 die "too many streams!";
227 die "not using amandad" if $self->from_inetd();
232 my ($wfd, $rfd) = (-1, -1);
234 # lower-numbered fd is for writing
238 push @fds_to_close, $fd;
242 # higher-numbered fd is for reading
246 push @fds_to_close, $fd;
250 $self->_add_stream($name, $rfd, $wfd);
251 $connect_line .= " $name $handle";
255 while ($fd < $Amanda::Constants::DATA_FD_OFFSET
256 + 2 * $Amanda::Constants::DATA_FD_COUNT) {
257 push @fds_to_close, $fd++;
260 # _dont_use_real_fds indicats that we should mock the close operation
261 if (!$self->{'_dont_use_real_fds'}) {
262 for $fd (@fds_to_close) {
263 if (!POSIX::close($fd)) {
264 Amanda::Debug::warning("Error closing fd $fd: $!");
267 # let the stupid OpenBSD threads library know we're using these fd's
268 Amanda::Util::openbsd_fd_inform();
270 $self->{'_would_have_closed_fds'} = \@fds_to_close;
273 return $connect_line;
276 sub connection_listen {
278 my ($name, $priv) = @_;
280 die "not using inetd" unless $self->from_inetd();
281 die "stream $name already exists" if exists $self->{'_streams'}{$name};
282 die "stream $name is already listening" if exists $self->{'_listen_sockets'}{$name};
284 # first open a socket
285 my ($lsock, $port) = stream_server(
286 $AF_INET, $STREAM_BUFSIZE, $STREAM_BUFSIZE, $priv);
287 return 0 if ($lsock < 0);
289 $self->{'_listen_sockets'}{$name} = $lsock;
294 sub connection_accept {
296 my ($name, $timeout, $finished_cb) = @_;
298 my $lsock = $self->{'_listen_sockets'}{$name};
299 die "stream $name is not listening" unless defined $lsock;
301 # set up a fd source *and* a timeout source
302 my ($fd_source, $timeout_source);
305 # accept is a "read" operation on a listening socket
306 $fd_source = Amanda::MainLoop::fd_source($lsock, $Amanda::MainLoop::G_IO_IN);
307 $fd_source->set_callback(sub {
310 $fd_source->remove();
311 $timeout_source->remove();
314 my $datasock = stream_accept($lsock, 1, $STREAM_BUFSIZE, $STREAM_BUFSIZE);
316 $errmsg = "error from stream_accept: $!";
320 POSIX::close($lsock);
321 delete $self->{'_listen_sockets'}{$name};
323 if ($datasock >= 0) {
324 $self->_add_stream($name, $datasock, $datasock);
327 return $finished_cb->($errmsg);
330 $timeout_source = Amanda::MainLoop::timeout_source(1000*$timeout);
331 $timeout_source->set_callback(sub {
334 $fd_source->remove();
335 $timeout_source->remove();
338 POSIX::close($lsock);
339 delete $self->{'_listen_sockets'}{$name};
341 return $finished_cb->("timeout while waiting for incoming TCP connection");
349 return -1 unless exists $self->{'_streams'}{$name};
350 my $fd = $self->{'_streams'}{$name}{'rfd'};
351 return defined($fd)? $fd : -1;
358 return -1 unless exists $self->{'_streams'}{$name};
359 my $fd = $self->{'_streams'}{$name}{'wfd'};
360 return defined($fd)? $fd : -1;
365 my ($name, $dir) = @_;
367 $dir = 'rw' unless defined $dir;
368 my $rfd = $self->rfd($name);
369 my $wfd = $self->wfd($name);
371 # sockets will have the read and write fd's, and are handled differently. If
372 # one end of a socket has been closed, then we can treat it like a regular fd
374 die "stream is already closed?" if ($rfd == -1);
379 } elsif ($dir eq 'r') {
380 # perl doesn't provide a fd-compatible shutdown, but luckily shudown
381 # affects dup'd file descriptors, too! So create a new handle and shut
382 # it down. When the handle is garbage collected, it will be closed,
383 # but that will not affect the original. This will look strange in an
384 # strace, but it works without SWIGging shutdown()
385 shutdown(IO::Handle->new_from_fd(POSIX::dup($rfd), "r"), 0);
387 } elsif ($dir eq 'w') {
388 shutdown(IO::Handle->new_from_fd(POSIX::dup($wfd), "w"), 1);
392 if ($dir =~ /r/ and $rfd != -1) {
396 if ($dir =~ /w/ and $wfd != -1) {
402 if ($rfd == -1 and $wfd == -1) {
403 delete $self->{'_streams'}{$name};
405 $self->{'_streams'}{$name}{'rfd'} = $rfd;
406 $self->{'_streams'}{$name}{'wfd'} = $wfd;
410 sub check_bsd_security {
412 my ($name, $authstr) = @_;
414 # find the open file descriptor
415 my $fd = $self->rfd($name);
416 $fd = $self->wfd($name) if $fd < 0;
417 die "stream '$name' not open" if $fd < 0;
419 # don't invoke check_security if we're run as an installcheck;
420 # installchecks are incompatible with BSD security
421 return undef if $self->from_installcheck();
423 return Amanda::Util::check_security($fd, $authstr);
436 # split into lines, split by '\n', filtering empty lines
437 my @req_lines = grep /.+/, (split(/\n/, $req_str));
438 $rv->{'lines'} = [ @req_lines ];
440 # find and parse the options line
441 my @opt_lines = grep /^OPTIONS /, @req_lines;
442 if (@opt_lines > 1) {
443 push @{$rv->{'errors'}}, "got multiple OPTIONS lines";
444 # (and use the first OPTIONS line anyway)
447 my ($line) = $opt_lines[0] =~ /^OPTIONS (.*)$/;
448 my @opts = grep /.+/, (split(/;/, $line));
449 for my $opt (@opts) {
450 if ($opt =~ /^([^=]+)=(.*)$/) {
451 $rv->{'options'}{$1} = $2;
453 $rv->{'options'}{$opt} = 1;
459 if ($rv->{'options'}{'features'}) {
460 $rv->{'features'} = Amanda::Feature::Set->from_string($rv->{'options'}{'features'});
470 my ($stream, $rfd, $wfd) = @_;
472 $self->{'_streams'}{$stream} = {