]> git.refcnt.org Git - distdns.git/blob - client.pl
Mention all copyright years
[distdns.git] / client.pl
1 #!/usr/bin/perl
2 #
3 # Copyright (c) 2013, 2015 Michel Ketterle, Steven Schubiger
4 #
5 # This file is part of distdns.
6 #
7 # distdns is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # distdns is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with distdns. If not, see <http://www.gnu.org/licenses/>.
19
20 use strict;
21 use warnings;
22 use constant false => 0;
23
24 BEGIN
25 {
26 my @modules = (
27 [ 'Config::Tiny', [ ] ],
28 [ 'Digest::MD5', [ qw(md5_hex) ] ],
29 [ 'Fcntl', [ qw(:flock) ] ],
30 [ 'File::Spec::Functions', [ qw(catfile rel2abs) ] ],
31 [ 'FindBin', [ qw($Bin) ] ],
32 [ 'Getopt::Long', [ qw(:config no_auto_abbrev no_ignore_case) ] ],
33 [ 'JSON', [ qw(decode_json) ] ],
34 [ 'LWP::UserAgent', [ ] ],
35 [ 'POSIX', [ qw(strftime) ] ],
36 [ 'Sys::Hostname', [ qw(hostname) ] ],
37 [ 'Tie::File', [ ] ],
38 );
39 my (@missing, @import);
40 foreach my $module (@modules) {
41 unless (eval "require $module->[0]; 1") {
42 push @missing, $module->[0];
43 next;
44 }
45 unless (eval { $module->[0]->import(@{$module->[1]}); 1 }) {
46 push @import, $module->[0];
47 }
48 }
49 if (@missing || @import) {
50 warn <<"EOT";
51 Modules missing: @missing
52 Import failures: @import
53 EOT
54 exit 1;
55 }
56 }
57
58 my $VERSION = '0.07';
59
60 my $conf_file = catfile($Bin, 'client.conf');
61
62 sub _die { die "$0: [client] $_[0]" }
63
64 sub usage
65 {
66 warn "$_[0]\n" if defined $_[0];
67 print <<"USAGE";
68 Usage: $0 [options]
69 -d, --debug server debugging
70 -h, --help this help screen
71 -i, --init initialize session data
72 -l, --list list remote entries
73 USAGE
74 exit;
75 }
76
77 my %opts;
78 GetOptions(\%opts, qw(d|debug h|help i|init l|list)) or usage();
79 usage() if $opts{h};
80
81 usage('Cannot combine --init and --list') if $opts{i} && $opts{l};
82
83 my $config = Config::Tiny->new;
84 $config = Config::Tiny->read($conf_file);
85
86 my $get_config_opts = sub
87 {
88 my ($section, $options) = @_;
89
90 _die "Section '$section' missing in $conf_file\n" unless exists $config->{$section};
91
92 my %options;
93 @options{@$options} = @{$config->{$section}}{@$options};
94
95 foreach my $option (@$options) {
96 _die "Option '$option' not set in $conf_file\n" unless defined $options{$option} && length $options{$option};
97 }
98
99 return @options{@$options};
100 };
101
102 my ($hosts_file, $session_file) = map rel2abs($_, $Bin), $get_config_opts->('path', [ qw(hosts_file session_file) ]);
103
104 my ($server_url) = $get_config_opts->('url', [ qw(server_url) ]);
105 my ($netz, $name) = $get_config_opts->('data', [ qw(netz name) ]);
106
107 my $save_session = sub
108 {
109 my ($session) = @_;
110
111 open(my $fh, '>', $session_file) or _die "Cannot open $session_file for writing: $!\n";
112 print {$fh} "$session\n";
113 close($fh);
114 };
115
116 my $get_session = sub
117 {
118 open(my $fh, '<', $session_file) or _die "Cannot open $session_file for reading: $!\nPerhaps try running --init\n";
119 chomp(my $session = <$fh>);
120 close($fh);
121
122 return $session;
123 };
124
125 my $session = $opts{i} ? substr(md5_hex(md5_hex(time() . {} . rand() . $$)), 0, 32) : $get_session->();
126
127 my %params = (
128 netz => $netz,
129 pc => hostname(),
130 name => $name,
131 debug => $opts{d} || false,
132 init => $opts{i} || false,
133 list => $opts{l} || false,
134 session => $session,
135 );
136
137 my $ua = LWP::UserAgent->new;
138
139 my $response = $ua->post($server_url, \%params);
140
141 if ($response->is_success) {
142 my $data;
143
144 eval {
145 $data = decode_json($response->decoded_content);
146 } or exit;
147
148 die "$0: [server] $data->{error}" if defined $data->{error};
149
150 if ($opts{i}) {
151 $save_session->($session);
152 }
153 elsif ($opts{l}) {
154 format STDOUT_TOP =
155 IP Name PC Netz Aktualisiert
156 ====================================================================================================
157 .
158 foreach my $entry (sort { $a->{netz} cmp $b->{netz} } @{$data->{entries}}) {
159 my $updated = strftime '%Y-%m-%d %H:%M:%S', localtime $entry->{time};
160 format STDOUT =
161 @<<<<<<<<<<<<<< @<<<<<<<<<<<<<< @<<<<<<<<<<<<<<<<<<< @<<<<<<<<<<<<<< @<<<<<<<<<<<<<<<<<<
162 @$entry{qw(ip name pc netz)}, $updated
163 .
164 write;
165 }
166 }
167 else {
168 my %list;
169 foreach my $entry (@{$data->{entries}}) {
170 my $host = "$entry->{ip}\t" . join '.', @$entry{qw(name pc netz)};
171 push @{$list{$entry->{netz}}}, $host;
172 }
173
174 my $o = tie my @hosts, 'Tie::File', $hosts_file or _die "Cannot tie $hosts_file: $!\n";
175 $o->flock(LOCK_EX);
176
177 foreach my $network (keys %list) {
178 my %indexes;
179 for (my $i = 0; $i < @hosts; $i++) {
180 if ($hosts[$i] =~ /^\#$network\#$/i) {
181 $indexes{start} = $i;
182 }
183 elsif (exists $indexes{start} && $hosts[$i] =~ /^\#\/$network\#$/i) {
184 $indexes{end} = $i;
185 my $count = ($indexes{end} - $indexes{start} > 1)
186 ? $indexes{end} - $indexes{start} - 1
187 : 0;
188 splice @hosts, $indexes{start} + 1, $count, @{$list{$network}};
189 last;
190 }
191 }
192 }
193
194 undef $o;
195 untie @hosts;
196 }
197 }
198 else {
199 warn $response->status_line, "\n";
200 }