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