Imported Upstream version 3.3.3
[debian/amanda] / perl / Amanda / ClientService.pm
1 # Copyright (c) 2010-2012 Zmanda, Inc.  All Rights Reserved.
2 #
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.
7 #
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
11 # for more details.
12 #
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
16 #
17 # Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
18 # Sunnyvale, CA 94085, USA, or: http://www.zmanda.com
19
20 package Amanda::ClientService;
21
22 =head1 NAME
23
24 Amanda::ClientService -- support for writing amandad and inetd services
25
26 =head1 SYNOPSIS
27
28     package main::Service;
29     use base qw( Amanda::ClientService );
30
31     sub start {
32         my $self = shift;
33         if ($self->from_inetd) {
34             $self->check_bsd_security('main');
35         }
36         # ...
37     }
38     #...
39
40     package main;
41     use Amanda::MainLoop;
42     my $svc = main::Service->new();
43     Amanda::MainLoop::run();
44
45 =head1 NOTE
46
47 Note that, despite the name, some client services are actually run on the
48 server - C<amidxtaped> and C<amindexd> in particular.
49
50 =head1 INTERFACE
51
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:
55
56     my $svc = main::Service->new();
57     Amanda::MainLoop::start();
58
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.
62
63 =head2 Invocation Type
64
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>).
71
72     print "hi, inetd"
73         if $self->from_inetd();
74     print "hi, amandadd w/ ", ($self->amandad_auth() || "none")
75         if $self->from_amandad();
76
77 =head2 Streams
78
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:
85
86     Amanda::Util::full_write($self->wfd('MESG'), $buf, length($buf));
87
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
92 the connection.
93
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.
100
101     push @replines, $self->connect_streams(
102             'DATA' => 'w', 'MESG' => 'rw', 'INDEX' => 'w');
103
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
112 be initialized.
113
114     my $port = $self->connection_listen($name, $priv);
115     # send $port to the other side
116     $self->connection_accept($name, $timeout, $finished_cb);
117
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.
122
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
126
127 =head2 Security
128
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
134 fails.
135
136     if ($self->check_bsd_security($stream, $authstr)) { .. }
137
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.
140
141 =head2 REQ packets
142
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.
150
151     my $req_info = parse_req($req_str);
152     if (@{$req_info->{'errors'}}) {
153         print join("\n", @{$req_info->{'errors'}}), "\n";
154         exit;
155     }
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";
160     }
161
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.
164
165 =cut
166
167 use strict;
168 use warnings;
169 use Data::Dumper;
170 use IO::Handle;
171
172 use Amanda::Util qw( :constants stream_server stream_accept );
173 use Amanda::Debug qw( debug );
174 use Amanda::Constants;
175 use Amanda::MainLoop;
176 use Amanda::Feature;
177
178 sub new {
179     my $class = shift;
180
181     my $self = bless {
182         _streams => {},
183         _listen_sockets => {},
184         _argv => [ @ARGV ],
185     }, $class;
186
187     $self->_add_stream('main', 0, 1);
188     return $self;
189 }
190
191 sub from_inetd {
192     my $self = shift;
193     return 1 if (!defined $self->{'_argv'}[0] or $self->{'_argv'}[0] eq 'installcheck');
194 }
195
196 sub from_amandad {
197     my $self = shift;
198     return 1 if defined $self->{'_argv'}[0] and $self->{'_argv'}[0] eq 'amandad';
199 }
200
201 sub from_installcheck {
202     my $self = shift;
203     return 1 if defined $self->{'_argv'}[0] and $self->{'_argv'}[0] eq 'installcheck';
204 }
205
206 sub amandad_auth {
207     my $self = shift;
208     return undef unless $self->from_amandad();
209     return $self->{'_argv'}[1];
210 }
211
212 sub connect_streams {
213     my $self = shift;
214     my $connect_line = "CONNECT";
215     my $fd = $Amanda::Constants::DATA_FD_OFFSET;
216     my $handle = $Amanda::Constants::DATA_FD_OFFSET;
217     my @fds_to_close;
218
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.
222
223     if (@_ > $Amanda::Constants::DATA_FD_COUNT * 2) {
224         die "too many streams!";
225     }
226
227     die "not using amandad" if $self->from_inetd();
228
229     while (@_) {
230         my $name = shift;
231         my $dirs = shift;
232         my ($wfd, $rfd) = (-1, -1);
233
234         # lower-numbered fd is for writing
235         if ($dirs =~ /w/) {
236             $wfd = $fd;
237         } else {
238             push @fds_to_close, $fd;
239         }
240         $fd++;
241
242         # higher-numbered fd is for reading
243         if ($dirs =~ /r/) {
244             $rfd = $fd;
245         } else {
246             push @fds_to_close, $fd;
247         }
248         $fd++;
249
250         $self->_add_stream($name, $rfd, $wfd);
251         $connect_line .= " $name $handle";
252         $handle++;
253     }
254
255     while ($fd < $Amanda::Constants::DATA_FD_OFFSET
256                  + 2 * $Amanda::Constants::DATA_FD_COUNT) {
257         push @fds_to_close, $fd++;
258     }
259
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: $!");
265             }
266         }
267         # let the stupid OpenBSD threads library know we're using these fd's
268         Amanda::Util::openbsd_fd_inform();
269     } else {
270         $self->{'_would_have_closed_fds'} = \@fds_to_close;
271     }
272
273     return $connect_line;
274 }
275
276 sub connection_listen {
277     my $self = shift;
278     my ($name, $priv) = @_;
279
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};
283
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);
288
289     $self->{'_listen_sockets'}{$name} = $lsock;
290     return $port;
291 }
292
293
294 sub connection_accept {
295     my $self = shift;
296     my ($name, $timeout, $finished_cb) = @_;
297
298     my $lsock = $self->{'_listen_sockets'}{$name};
299     die "stream $name is not listening" unless defined $lsock;
300
301     # set up a fd source *and* a timeout source
302     my ($fd_source, $timeout_source);
303     my $fired = 0;
304
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 {
308         return if $fired;
309         $fired = 1;
310         $fd_source->remove();
311         $timeout_source->remove();
312
313         my $errmsg;
314         my $datasock = stream_accept($lsock, 1, $STREAM_BUFSIZE, $STREAM_BUFSIZE);
315         if ($datasock < 0) {
316             $errmsg = "error from stream_accept: $!";
317         }
318
319         # clean up
320         POSIX::close($lsock);
321         delete $self->{'_listen_sockets'}{$name};
322
323         if ($datasock >= 0) {
324             $self->_add_stream($name, $datasock, $datasock);
325         }
326
327         return $finished_cb->($errmsg);
328     });
329
330     $timeout_source = Amanda::MainLoop::timeout_source(1000*$timeout);
331     $timeout_source->set_callback(sub {
332         return if $fired;
333         $fired = 1;
334         $fd_source->remove();
335         $timeout_source->remove();
336
337         # clean up
338         POSIX::close($lsock);
339         delete $self->{'_listen_sockets'}{$name};
340
341         return $finished_cb->("timeout while waiting for incoming TCP connection");
342     });
343 }
344
345 sub rfd {
346     my $self = shift;
347     my ($name) = @_;
348
349     return -1 unless exists $self->{'_streams'}{$name};
350     my $fd = $self->{'_streams'}{$name}{'rfd'};
351     return defined($fd)? $fd : -1;
352 }
353
354 sub wfd {
355     my $self = shift;
356     my ($name) = @_;
357
358     return -1 unless exists $self->{'_streams'}{$name};
359     my $fd = $self->{'_streams'}{$name}{'wfd'};
360     return defined($fd)? $fd : -1;
361 }
362
363 sub close {
364     my $self = shift;
365     my ($name, $dir) = @_;
366
367     $dir = 'rw' unless defined $dir;
368     my $rfd = $self->rfd($name);
369     my $wfd = $self->wfd($name);
370
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
373     if ($rfd == $wfd) {
374         die "stream is already closed?" if ($rfd == -1);
375
376         if ($dir eq 'rw') {
377             POSIX::close($rfd);
378             $rfd = $wfd = -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);
386             $rfd = -1;
387         } elsif ($dir eq 'w') {
388             shutdown(IO::Handle->new_from_fd(POSIX::dup($wfd), "w"), 1);
389             $wfd = -1;
390         }
391     } else {
392         if ($dir =~ /r/ and $rfd != -1) {
393             POSIX::close($rfd);
394             $rfd = -1;
395         }
396         if ($dir =~ /w/ and $wfd != -1) {
397             POSIX::close($wfd);
398             $wfd = -1;
399         }
400     }
401
402     if ($rfd == -1 and $wfd == -1) {
403         delete $self->{'_streams'}{$name};
404     } else {
405         $self->{'_streams'}{$name}{'rfd'} = $rfd;
406         $self->{'_streams'}{$name}{'wfd'} = $wfd;
407     }
408 }
409
410 sub check_bsd_security {
411     my $self = shift;
412     my ($name, $authstr) = @_;
413
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;
418
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();
422
423     return Amanda::Util::check_security($fd, $authstr);
424 }
425
426 sub parse_req {
427     my $self = shift;
428     my ($req_str) = @_;
429     my $rv = {
430         lines => [],
431         options => {},
432         features => undef,
433         errors => [],
434     };
435
436     # split into lines, split by '\n', filtering empty lines
437     my @req_lines = grep /.+/, (split(/\n/, $req_str));
438     $rv->{'lines'} = [ @req_lines ];
439
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)
445     }
446     if (@opt_lines) {
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;
452             } else {
453                 $rv->{'options'}{$opt} = 1;
454             }
455         }
456     }
457
458     # features
459     if ($rv->{'options'}{'features'}) {
460         $rv->{'features'} = Amanda::Feature::Set->from_string($rv->{'options'}{'features'});
461     }
462
463     return $rv;
464 }
465
466 # private methods
467
468 sub _add_stream {
469     my $self = shift;
470     my ($stream, $rfd, $wfd) = @_;
471
472     $self->{'_streams'}{$stream} = {
473         rfd => $rfd,
474         wfd => $wfd,
475     };
476 }
477
478 1;