1#! /usr/bin/perl
2
3# Copyright (c) 2007 Riccardo Murri <riccardo.murri@gmail.com>
4#
5# License Information:
6# This program is free software; you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation; either version 2 of the License, or
9# (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, write to the Free Software
18# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
19#
20
21use warnings;
22use strict;
23use Getopt::Long;
24use File::Basename;
25use File::Copy;
26use File::Temp qw/ tempfile /;
27use IO::File;
28
29# Do I/O to the data file in binary mode (so it
30# wouldn't complain about invalid UTF-8 characters).
31use bytes;
32
33File::Temp->safe_level( File::Temp::HIGH );
34
35my %opts = ();
36my $outfile;
37my $verbose;
38my $base = basename($0);
39
40my $cfgfile  = "@myconffile@";
41my $yule     = "@sbindir@/@install_name@";
42
43$cfgfile  =~ s/^REQ_FROM_SERVER//;
44
45sub usage() {
46    print <<__END_OF_TEXT__
47Usage:
48  $base { -a | --add } [options] HOSTNAME [PASSWORD]
49    Add client HOSTNAME to configuration file. If PASSWORD is
50    omitted, it is read from stdin.  If HOSTNAME already exists
51    in the configuration file, an error is given.
52
53  $base { -d | --delete } [options] HOSTNAME
54    Remove client HOSTNAME from configuration file.
55
56  $base { -l | --list } [options]
57    List clients in the yule configuration file.
58
59  $base { -r | --replace } [options] HOSTNAME [PASSWORD]
60    Replace password of existing client HOSTNAME in configuration file.
61    If PASSWORD is omitted, it is read from stdin.  If HOSTNAME does not
62    already exist in the configuration file, an error is given.
63
64  $base { -u | --update } [options] HOSTNAME [PASSWORD]
65    Add client HOSTNAME to config file or replace its password with a new one.
66    If PASSWORD is omitted, it is read from stdin.
67
68Options:
69  -c CFGFILE    --cfgfile CFGFILE
70    Select an alternate configuration file. (default: $cfgfile)
71
72  -o OUTFILE    --output OUTFILE
73    Write modified configuration to OUTFILE.  If this option is
74    omitted, $base will rename the original configuration file
75    to '$cfgfile.BAK' and overwrite it with the modified content.
76
77  -Y YULECMD    --yule YULECMD
78    Use command YULECMD to generate the client key from the password.
79    (default: $yule)
80
81  -v            --verbose
82    Verbose output.
83
84__END_OF_TEXT__
85;
86    return;
87}
88
89
90## subroutines
91
92sub read_clients ($) {
93    my $cfgfile = shift || '-';
94    my %clients;
95
96    open INPUT, "<$cfgfile"
97	or die ("Cannot read configuration file '$cfgfile'. Aborting");
98
99    my $section;
100    while (<INPUT>) {
101	# skip comment and blank lines
102	next if m{^\s*#};
103        next if m{^\s*$};
104
105	# match section headers
106	$section = $1 if m{^\s*\[([a-z0-9 ]+)\]}i;
107
108	# ok, list matching lines
109	if ($section =~ m/Clients/) {
110	    if (m{^\s*Client=}i) {
111		chomp;
112		s{^\s*Client=}{}i;
113		my ($client, $key) = split /@/,$_,2;
114
115		$clients{lc($client)} = $key;
116	    }
117	}
118    }
119
120    close INPUT;
121    return \%clients;
122}
123
124
125sub write_clients ($$$) {
126    my $cfgfile_in = shift || '-';
127    my $cfgfile_out = shift || $cfgfile_in;
128    my $clients = shift;
129
130    my @lines;
131    my $in_clients_section;
132
133    # copy-pass input file
134    my $section = '';
135    open INPUT, "<$cfgfile_in"
136	or die ("Cannot read configuration file '$cfgfile_in'. Aborting");
137    while (<INPUT>) {
138	# match section headers
139	if (m{^\s*\[([a-z0-9 ]+)\]}i) {
140	    if ($in_clients_section and ($section ne $1)) {
141		# exiting [Clients] section, output remaining ones
142		foreach my $hostname (keys %{$clients}) {
143		    push @lines,
144		        'Client=' . $hostname . '@'
145			. $clients->{lc($hostname)} . "\n";
146		    delete $clients->{lc($hostname)};
147		}
148	    }
149	    # update section title
150	    $section = $1;
151	    if ($section =~ m/Clients/i) {
152		$in_clients_section = 1;
153	    } else {
154		$in_clients_section = 0;
155	    }
156	}
157
158	# process entries in [Clients] section
159	if ($in_clients_section) {
160	    if (m{^\s*Client=}i) {
161		my ($hostname, undef) = split /@/,$_,2;
162		$hostname =~ s{^\s*Client=}{}i;
163		if (defined($clients->{lc($hostname)})) {
164		    # output (possibly) modified key
165		    $_ = 'Client=' . $hostname . '@' . $clients->{lc($hostname)} . "\n";
166		    delete $clients->{lc($hostname)};
167		}
168		else {
169		    # client deleted, skip this line from output
170		    $_ = '';
171		}
172	    }
173	}
174
175	# copy input to output
176	push @lines, $_;
177    }
178    close INPUT;
179
180    # if end-of-file reached within [Clients] section, output remaining ones
181    if ($in_clients_section) {
182	foreach my $hostname (keys %{$clients}) {
183	    push @lines, 'Client=' . $hostname . '@'
184		. $clients->{lc($hostname)} . "\n";
185	}
186    }
187
188    # if necessary, replace input file with output file
189    if ($cfgfile_in eq $cfgfile_out) {
190	copy($cfgfile_in, $cfgfile_in . '.BAK')
191	    or die("Cannot backup config file '$cfgfile_in'. Aborting");
192    }
193    open OUTPUT, ">$cfgfile_out"
194	or die ("Cannot write to file '$cfgfile_out'. Aborting");
195    # overwrite config file line by line
196    foreach my $line (@lines) { print OUTPUT $line; }
197    close OUTPUT;
198}
199
200
201sub new_client_key ($) {
202    my $password = shift;
203    my $yulecmd = shift || $yule;
204
205    my (undef, $key) = split /@/, `$yulecmd -P $password`, 2;
206    chomp $key;
207    return $key;
208}
209
210
211## main
212
213Getopt::Long::Configure ("posix_default");
214Getopt::Long::Configure ("bundling");
215# Getopt::Long::Configure ("debug");
216
217GetOptions (\%opts,
218	    'Y|yule=s',
219	    'a|add',
220	    'c|cfgfile=s',
221	    'd|delete',
222	    'h|help',
223	    'l|list',
224	    'o|output=s',
225	    'r|replace',
226	    'u|update',
227	    'v|verbose',
228	    );
229
230if (defined ($opts{'h'})) {
231    usage();
232    exit;
233}
234
235if (defined($opts{'c'})) {
236    $cfgfile = $opts{'c'};
237    $outfile = $cfgfile unless defined($outfile);
238}
239if (defined($opts{'Y'})) {
240    $yule = $opts{'Y'};
241}
242if (defined($opts{'v'})) {
243    $verbose = 1;
244}
245if (defined($opts{'o'})) {
246    $outfile = $opts{'o'};
247}
248
249if (defined($opts{'l'})) {
250    # list contents
251    my $clients = read_clients($cfgfile);
252
253    foreach my $client (keys %{$clients}) {
254	print "$client";
255	print " ${$clients}{$client}" if $verbose;
256	print "\n";
257    }
258}
259elsif (defined($opts{'a'})
260       or defined($opts{'u'})
261       or defined($opts{'r'})) {
262    # add HOSTNAME
263    my $hostname = $ARGV[0]
264	or die("Actions --add/--replace/--update require at least argument HOSTNAME. Aborting");
265
266    my $password;
267    if (defined($ARGV[1])) {
268	$password = uc($ARGV[1]);
269    } else {
270	$password = uc(<STDIN>);
271	# remove leading and trailing space
272	$password =~ s{\s*}{}g;
273    }
274    # sanity check
275    die ("Argument PASSWORD must be a 16-digit hexadecimal string. Aborting")
276	unless ($password =~ m/[[:xdigit:]]{16}/);
277
278    my $add = defined($opts{'a'});
279    my $replace = defined($opts{'r'});
280
281    my $clients = read_clients($cfgfile);
282    die ("Client '$hostname' already present in config file - cannot add. Aborting")
283	if ($add and defined(${$clients}{$hostname}));
284    die ("Client '$hostname' not already present in config file - cannot replace. Aborting")
285	if ($replace and not defined(${$clients}{$hostname}));
286
287    $clients->{$hostname} = new_client_key($password)
288      or die ("Cannot get key for the given password. Aborting");
289    write_clients($cfgfile, $outfile, $clients);
290}
291elsif (defined($opts{'d'})) {
292    # remove HOSTNAME
293    my $hostname = $ARGV[0]
294	or die("Action --delete requires one argument HOSTNAME. Aborting");
295
296    my $clients = read_clients($cfgfile);
297    delete ${$clients}{$hostname};
298    write_clients($cfgfile, $outfile, $clients);
299}
300else {
301    usage();
302    die ("You must specify one of --list, --add or --remove options. Aborting");
303}
304