1# Copyright (c) 2010-2013 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 94086, USA, or: http://www.zmanda.com
19
20use Test::More tests => 30;
21use strict;
22use warnings;
23
24use Data::Dumper;
25
26use lib "@amperldir@";
27use Amanda::ClientService;
28use Amanda::Constants;
29use Amanda::Util;
30use Amanda::Debug;
31use Amanda::Config qw( :init );
32use Amanda::MainLoop;
33use Socket;
34
35config_init(0, undef);
36Amanda::Debug::dbopen('installcheck');
37
38# test connect_streams
39{
40    # that these tests assume that DATA_FD_OFFSET and DATA_FD_COUNT have these values
41    is($Amanda::Constants::DATA_FD_OFFSET, 50, "DATA_FD_OFFSET is what I think it is");
42    is($Amanda::Constants::DATA_FD_COUNT, 3, "DATA_FD_COUNT is what I think it is");
43
44    sub test_connect_streams {
45	my ($args, $exp_line, $exp_closed_fds, $exp_streams, $msg) = @_;
46
47	my $cs = Amanda::ClientService->new();
48	$cs->{'_dont_use_real_fds'} = 1;
49	$cs->{'_argv'} = [ 'amandad' ];
50	my $got_line = $cs->connect_streams(@$args);
51
52	is($got_line, $exp_line, "$msg (CONNECT line)");
53
54	is_deeply([ sort @{$cs->{'_would_have_closed_fds'}} ],
55		  [ sort @$exp_closed_fds ],
56		  "$msg (closed fds)");
57
58	# get the named streams and their fd's
59	my %streams;
60	while (@$args) {
61	    my $name = shift @$args;
62	    my $dirs = shift @$args;
63	    $streams{$name} = [ $cs->rfd($name), $cs->wfd($name) ];
64	}
65
66	is_deeply(\%streams, $exp_streams, "$msg (streams)");
67    }
68
69    test_connect_streams(
70	[ 'DATA' => 'rw' ],
71	'CONNECT DATA 50',
72	[ 52, 53, 54, 55 ],
73	{ 'DATA' => [ 51, 50 ] },
74	"simple read/write DATA stream");
75
76    test_connect_streams(
77	[ 'DATA' => 'r' ],
78	'CONNECT DATA 50',
79	[ 50, 52, 53, 54, 55 ],
80	{ 'DATA' => [ 51, -1 ] },
81	"read-only stream");
82
83    test_connect_streams(
84	[ 'DATA' => 'w' ],
85	'CONNECT DATA 50',
86	[ 51, 52, 53, 54, 55 ],
87	{ 'DATA' => [ -1, 50 ] },
88	"write-only stream");
89
90    test_connect_streams(
91	[ 'DATA' => 'rw', 'RD' => 'r', 'WR' => 'w' ],
92	'CONNECT DATA 50 RD 51 WR 52',
93	[ 52, 55 ],
94	{ 'DATA' => [ 51, 50 ],
95	  'RD' => [ 53, -1 ],
96	  'WR' => [ -1, 54 ] },
97	"three streams");
98}
99
100# test from_inetd and friends
101{
102    my $cs;
103
104    $cs = Amanda::ClientService->new();
105    $cs->{'_argv'} = [];
106    ok($cs->from_inetd, "no argv[0] interpreted as a run from inetd");
107
108    $cs = Amanda::ClientService->new();
109    $cs->{'_argv'} = [ 'installcheck' ];
110    ok($cs->from_inetd, "argv[0] = 'installcheck' also interpreted as a run from inetd");
111
112    $cs = Amanda::ClientService->new();
113    $cs->{'_argv'} = [ 'amandad' ];
114    ok(!$cs->from_inetd, "argv[0] = 'amandad' interpreted as a run from amandad");
115
116    $cs = Amanda::ClientService->new();
117    $cs->{'_argv'} = [ 'amandad', 'bsdgre' ];
118    is($cs->amandad_auth, "bsdgre",
119	"argv[1] = 'bsdgre' interpreted as auth");
120
121    $cs = Amanda::ClientService->new();
122    $cs->{'_argv'} = [ 'amandad' ];
123    is($cs->amandad_auth, undef,
124	"amandad_auth interpreted as undef if missing");
125}
126
127# test add_connection and half-close operations
128sub test_connections {
129    my ($finished_cb) = @_;
130
131    my $port;
132    my $cs = Amanda::ClientService->new();
133    $cs->{'_argv'} = [ ];
134
135    my $steps = define_steps
136	cb_ref => \$finished_cb;
137
138    step listen => sub {
139	$port = $cs->connection_listen('FOO', 0);
140
141	$steps->{'fork'}->();
142    };
143
144    step fork => sub {
145	# fork off a child to connect to and write to that port
146	if (fork() == 0) {
147	    socket(my $foo, PF_INET, SOCK_STREAM, getprotobyname('tcp'))
148		or die "error creating connect socket: $!";
149	    connect($foo, sockaddr_in($port, inet_aton("127.0.0.1")))
150		or die "error connecting: $!";
151	    my $info = <$foo>;
152	    print $foo "GOT[$info]";
153	    close($foo);
154	    exit(0);
155	} else {
156	    $steps->{'accept'}->();
157	}
158    };
159
160    step accept => sub {
161	$cs->connection_accept('FOO', 90, $steps->{'accept_finished'});
162    };
163
164    step accept_finished => sub {
165	# write a message to the fd and read back the result; this is
166	# synchronous
167	my $msg = "HELLO WORLD";
168	Amanda::Util::full_write($cs->wfd('FOO'), $msg, length($msg))
169	    or die "full write: $!";
170	$cs->close('FOO', 'w');
171	is($cs->wfd('FOO'), -1,
172	    "FOO is closed for writing");
173
174	my $input = Amanda::Util::full_read($cs->rfd('FOO'), 1024);
175	$cs->close('FOO', 'r');
176	is_deeply([ keys %{$cs->{'_streams'}} ], [ 'main' ],
177	    "FOO stream is deleted when completely closed");
178
179	is($input, "GOT[HELLO WORLD]",
180	    "both directions of the FOO stream work");
181
182	$finished_cb->();
183    };
184}
185test_connections(\&Amanda::MainLoop::quit);
186Amanda::MainLoop::run();
187
188# check rfd and wfd
189{
190    my $cs = Amanda::ClientService->new();
191    is($cs->rfd('main'), 0,
192	"main rfd is stdin");
193    is($cs->wfd('main'), 1,
194	"main wfd is stdout");
195    is($cs->wfd('none'), -1,
196	"wfd returns -1 for invalid stream");
197    is($cs->rfd('none'), -1,
198	"rfd returns -1 for invalid stream");
199}
200
201# check check_bsd_security
202{
203    # note that we can't completely test this, because BSD security entails checking
204    # DNS and privileged ports, neither of which are controllable from the installcheck
205    # environment.  However, we can at least call the method.
206
207    my $cs = Amanda::ClientService->new();
208    $cs->{'_argv'} = [ 'installcheck' ]; # basically neuters check_bsd_security
209
210    ok(!$cs->check_bsd_security('main', "USER bart"),
211	"check_bsd_security returns undef");
212}
213
214# check parse_req
215{
216    my $cs = Amanda::ClientService->new();
217    my $req_str;
218
219    # is_deeply doesn't like objects very much
220    sub strip_features {
221	my ($x) = @_;
222	#use Data::Dumper;
223	#print Dumper($x);
224	return $x unless defined $x->{'features'};
225	$x->{'features'} = "featureset";
226	return $x;
227    }
228
229    $req_str = <<ENDREQ;
230OPTIONS auth=passport;features=f0039;
231FOO
232ENDREQ
233    is_deeply(strip_features($cs->parse_req($req_str)), {
234	lines => [ 'OPTIONS auth=passport;features=f0039;', 'FOO' ],
235	options => {
236	    auth => 'passport',
237	    features => 'f0039',
238	},
239	errors => [],
240	features => "featureset",
241    }, "parse_req parses a request properly");
242
243    $req_str = <<ENDREQ;
244OPTIONS auth=bsd;no-features;yes=no;
245ENDREQ
246    is_deeply(strip_features($cs->parse_req($req_str)), {
247	lines => [ 'OPTIONS auth=bsd;no-features;yes=no;' ],
248	options => {
249	    auth => 'bsd',
250	    yes => 'no',
251	    'no-features' => 1,
252	},
253	errors => [],
254	features => undef,
255    }, "parse_req parses a request with boolean options");
256
257    $req_str = <<ENDREQ;
258OPTIONS turn=left;
259OPTIONS turn=right;
260ENDREQ
261    is_deeply(strip_features($cs->parse_req($req_str)), {
262	lines => [ 'OPTIONS turn=left;', 'OPTIONS turn=right;' ],
263	options => {
264	    turn => 'left',
265	},
266	errors => [ 'got multiple OPTIONS lines' ],
267	features => undef,
268    }, "parse_req detects multiple OPTIONS lines as an error");
269}
270