Handle hostnames with upper-case letters
[webmin.git] / ipsec / ipsec-lib.pl
1 # ipsec-lib.pl
2 # Common functions for managing the freeswan config file
3 # XXX check for new errors in log after applying
4 # XXX option to download connection as .conf file, and upload an existing
5 #     .conf file for addition
6
7 BEGIN { push(@INC, ".."); };
8 use WebminCore;
9 &init_config();
10
11 # get_config([file])
12 # Returns an array of configured connections
13 sub get_config
14 {
15 local $file = $_[0] || $config{'file'};
16 local (@rv, $sect);
17 local $lnum = 0;
18 local $fh = "CONF".$get_config_fh++;
19 open($fh, $file);
20 while(<$fh>) {
21         s/\r|\n//g;
22         s/#.*$//;
23         if (/^\s*([^= ]+)\s*=\s*"([^"]*)"/ ||
24             /^\s*([^= ]+)\s*=\s*'([^"]*)'/ ||
25             /^\s*([^= ]+)\s*=\s*(\S+)/) {
26                 # Directive within a section
27                 if ($sect) {
28                         if ($sect->{'values'}->{lc($1)}) {
29                                 $sect->{'values'}->{lc($1)} .= "\0".$2;
30                                 }
31                         else {
32                                 $sect->{'values'}->{lc($1)} = $2;
33                                 }
34                         $sect->{'eline'} = $lnum;
35                         }
36                 }
37         elsif (/^\s*include\s+(\S+)/) {
38                 # Including possibly multiple files
39                 local $inc = $1;
40                 if ($inc !~ /^\//) {
41                         $file =~ /^(.*)\//;
42                         $inc = "$1/$inc";
43                         }
44                 local $g;
45                 foreach $g (glob($inc)) {
46                         local @inc = &get_config($g);
47                         map { $_->{'index'} += scalar(@rv) } @inc;
48                         push(@rv, @inc);
49                         }
50                 }
51         elsif (/^\s*(\S+)\s+(\S+)/) {
52                 # Start of a section
53                 $sect = { 'name' => $1,
54                           'value' => $2,
55                           'line' => $lnum,
56                           'eline' => $lnum,
57                           'file' => $file,
58                           'index' => scalar(@rv),
59                           'values' => { } };
60                 push(@rv, $sect);
61                 }
62         $lnum++;
63         }
64 close($fh);
65 return @rv;
66 }
67
68 # create_conn(&conn)
69 # Add a new connection to the config file
70 sub create_conn
71 {
72 local $lref = &read_file_lines($_[0]->{'file'} || $config{'file'});
73 push(@$lref, "", &conn_lines($_[0]));
74 &flush_file_lines();
75 }
76
77 # modify_conn(&conn)
78 sub modify_conn
79 {
80 local $lref = &read_file_lines($_[0]->{'file'});
81 splice(@$lref, $_[0]->{'line'}, $_[0]->{'eline'} - $_[0]->{'line'} + 1,
82        &conn_lines($_[0]));
83 &flush_file_lines();
84 }
85
86 # delete_conn(&conn)
87 sub delete_conn
88 {
89 local $lref = &read_file_lines($_[0]->{'file'});
90 splice(@$lref, $_[0]->{'line'}, $_[0]->{'eline'} - $_[0]->{'line'} + 1);
91 &flush_file_lines();
92 }
93
94 # swap_conns(&conn1, &conn2)
95 # Swaps two connections in the config file
96 sub swap_conns
97 {
98 local ($first, $second) = @_;
99 if ($first->{'line'} > $second->{'line'}) {
100         ($first, $second) = ($second, $first);
101         }
102 local $lref1 = &read_file_lines($first->{'file'});
103 local $lref2 = &read_file_lines($second->{'file'});
104 splice(@$lref2, $second->{'line'}, $second->{'eline'} - $second->{'line'} + 1,
105        @$lref1[$first->{'line'} .. $first->{'eline'}]);
106 splice(@$lref2, $first->{'line'}, $first->{'eline'} - $first->{'line'} + 1,
107        @$lref1[$second->{'line'} .. $second->{'eline'}]);
108 &flush_file_lines();
109 }
110
111 # conn_lines(&conn)
112 sub conn_lines
113 {
114 local @rv;
115 push(@rv, $_[0]->{'name'}." ".$_[0]->{'value'});
116 foreach $o (sort { $a cmp $b } keys %{$_[0]->{'values'}}) {
117         local $v = $_[0]->{'values'}->{$o};
118         local $vv;
119         foreach $vv (split(/\0/, $v)) {
120                 if ($vv =~ /\s|=/) {
121                         push(@rv, "\t".$o."=\"".$vv."\"");
122                         }
123                 else {
124                         push(@rv, "\t".$o."=".$vv);
125                         }
126                 }
127         }
128 return @rv;
129 }
130
131 # is_ipsec_running()
132 sub is_ipsec_running
133 {
134 local $out = `$config{'ipsec'} auto --status 2>&1`;
135 return $? || $out =~ /not running/i ? 0 : 1;
136 }
137
138 # get_public_key()
139 # Returns this system's public key
140 sub get_public_key
141 {
142 local $out = `$config{'ipsec'} showhostkey --file '$config{'secrets'}' --left 2>&1`;
143 if ($out =~ /leftrsasigkey=(\S+)/) {
144         return $1;
145         }
146 return undef;
147 }
148
149 # get_public_key_dns()
150 # Returns the flags, protocol, algorithm and key data for the public key,
151 # suitable for creating a DNS KEY record
152 sub get_public_key_dns
153 {
154 local $out = `$config{'ipsec'} showhostkey --file '$config{'secrets'}' 2>&1`;
155 if ($out =~ /KEY\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)/) {
156         return ($1, $2, $3, $4);
157         }
158 else {
159         # Try with new --key argument
160         $out = `$config{'ipsec'} showhostkey --key --file '$config{'secrets'}' 2>&1`;
161         if ($out =~ /KEY\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)/) {
162                 return ($1, $2, $3, $4);
163                 }
164         }
165 return ();
166 }
167
168 # list_policies()
169 # Returns a list of all policy files
170 sub list_policies
171 {
172 local ($f, @rv);
173 opendir(DIR, $config{'policies_dir'});
174 while($f = readdir(DIR)) {
175         push(@rv, $f) if ($f !~ /^\./ && $f !~ /\.rpmsave$/);
176         }
177 closedir(DIR);
178 return @rv;
179 }
180
181 # read_policy(name)
182 sub read_policy
183 {
184 local @rv;
185 open(FILE, "$config{'policies_dir'}/$_[0]");
186 while(<FILE>) {
187         push(@rv, "$1/$2") if (/^\s*([0-9\.]+)\/(\d+)/);
188         }
189 close(FILE);
190 return @rv;
191 }
192
193 # write_policy(name, &nets)
194 sub write_policy
195 {
196 local $lref = &read_file_lines("$config{'policies_dir'}/$_[0]");
197 local $l = 0;
198 foreach $p (@{$_[1]}) {
199         while($l < @$lref && $lref->[$l] !~ /^\s*([0-9\.]+)\/(\d+)/) {
200                 $l++;
201                 }
202         if ($l < @$lref) {
203                 # Found line to replace
204                 $lref->[$l] = $p;
205                 }
206         else {
207                 # Add at end
208                 push(@$lref, $p);
209                 }
210         $l++;
211         }
212 while($l < @$lref) {
213         if ($lref->[$l] =~ /^\s*([0-9\.]+)\/(\d+)/) {
214                 splice(@$lref, $l, 1);
215                 }
216         else { $l++; }
217         }
218 &flush_file_lines();
219 }
220
221 # wrap_lines(text, width)
222 # Given a multi-line string, return an array of lines wrapped to
223 # the given width
224 sub wrap_lines
225 {
226 local $rest = $_[0];
227 local @rv;
228 while(length($rest) > $_[1]) {
229         push(@rv, substr($rest, 0, $_[1]));
230         $rest = substr($rest, $_[1]);
231         }
232 push(@rv, $rest) if ($rest ne '');
233 return @rv;
234 }
235
236 # before_start()
237 # Work out which log file IPsec messages go to, and record the size
238 sub before_start
239 {
240 @ipsec_logfiles = ( $config{'logfile'} );
241 if (&foreign_check("syslog")) {
242         # Find all syslog logfiles
243         &foreign_require("syslog", "syslog-lib.pl");
244         local $conf = &syslog::get_config();
245         foreach $c (@$conf) {
246                 push(@ipsec_logfiles, $c->{'file'}) if ($c->{'file'} &&
247                                                         -f $c->{'file'});
248                 }
249         }
250 @ipsec_logfiles = &unique(@ipsec_logfiles);
251 @ipsec_logfile_sizes = map { local @st = stat($_); $st[7] } @ipsec_logfiles;
252 }
253
254 # after_start()
255 # Check any new IPsec-related messages in the log for errors
256 sub after_start
257 {
258 # Give the server a chance to start
259 sleep(5);
260
261 # Look for new error messages
262 local $i;
263 for($i=0; $i<@ipsec_logfiles; $i++) {
264         open(LOG, $ipsec_logfiles[$i]) || next;
265         seek(LOG, $ipsec_logfile_sizes[$i], 0);
266         while(<LOG>) {
267                 s/\r|\n//g;
268                 if (/ipsec/i && /error/i) {
269                         s/^(\S+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\S+)\s+//;
270                         push(@errs, $_);
271                         }
272                 }
273         close(LOG);
274         }
275
276 # Fail if there were any
277 if (@errs) {
278         &error(&text('start_elog', "<p><tt>".join("<br>", @errs)."</tt><br>"));
279         }
280 }
281
282 # get_ipsec_version(&out)
283 sub get_ipsec_version
284 {
285 local $out = `$config{'ipsec'} --version 2>&1`;
286 ${$_[0]} = $out;
287 return $out =~ /(FreeS\/WAN|Openswan|StrongSWAN)\s+([^ \n\(]+)/i ? ($2,$1) : (undef);
288 }
289
290 # got_secret()
291 # Returns 1 if a valid secret key file exists, 0 if not
292 sub got_secret
293 {
294 local $gotkey;
295 open(SEC, $config{'secrets'}) || return 0;
296 while(<SEC>) {
297         s/\r|\n//g;
298         s/#.*$//;
299         if (/Modulus:\s*(\S+)/) {
300                 $gotkey = 1;
301                 }
302         }
303 close(SEC);
304 return $gotkey;
305 }
306
307 # expand_conf(&config)
308 sub expand_conf
309 {
310 my $conf = shift;
311 for my $n (0..scalar(@$conf)-1) {
312         $conn = @$conf[$n];
313         foreach my $key (keys(%{$conn->{'values'}})) {
314                 $expanded{$conn->{'value'}}->{$key} = $conn->{'values'}->{$key};
315                 }
316         }
317 # now go through and expand alsos
318 foreach my $k (keys(%expanded)) {
319         $conn = \%{$expanded{$k}};
320         # XXX - only supporing a single level of redirection
321         #     - this should be moved into a function that could be called
322         #       recursively
323         if ($$conn{'also'}) {
324                 foreach my $also (split(/\000/, $$conn{'also'})) {
325                         foreach my $i (keys(%{$expanded{$also}})) {
326                                 $$conn{$i} = $expanded{$also}{$i};
327                                 }
328                         }
329                 # there is only one also key
330                 next;
331                 }
332         }
333 return %expanded;
334 }
335
336 # restart_ipsec()
337 # Apply the current configuration, and return an error message on failure or
338 # undef on success
339 sub restart_ipsec
340 {
341 local $cmd = $config{'restart_cmd'} ||
342        "($config{'stop_cmd'} && $config{'start_cmd'})";
343 &before_start();
344 local $out = &backquote_logged("$cmd 2>&1");
345 if ($?) {
346         return "<pre>$out</pre>";
347         }
348 &after_start();
349 return undef;
350 }
351
352 # list_secrets()
353 # Returns a list of IPsec secret keys
354 sub list_secrets
355 {
356 if (!scalar(@list_secrets_cache)) {
357         local (@lines);
358         local $lnum = 0;
359         open(SEC, $config{'secrets'});
360         while(<SEC>) {
361                 s/\r|\n//g;
362                 s/^\s*#.*$//;
363                 if (/^(\S.*)$/) {
364                         push(@lines, { 'value' => $1,
365                                        'line' => $lnum,
366                                        'eline' => $lnum });
367                         }
368                 elsif (/^\s+(.*)/ && @lines) {
369                         $lines[$#rv]->{'value'} .= "\n".$1;
370                         $lines[$#rv]->{'eline'} = $lnum;
371                         }
372                 $lnum++;
373                 }
374         close(SEC);
375
376         # Turn joined lines into secrets
377         local $l;
378         foreach $l (@lines) {
379                 $l->{'value'} =~ /^([^:]*)\s*:\s+(\S+)\s+((.|\n)*)$/ || next;
380                 local $sec = { 'type' => $2,
381                                'name' => $1,
382                                'value' => $3,
383                                'line' => $l->{'line'},
384                                'eline' => $l->{'eline'},
385                                'idx' => scalar(@list_secrets_cache),
386                               };
387                 $sec->{'name'} =~ s/\n/ /g;
388                 $sec->{'name'} =~ s/\s+$//;
389                 push(@list_secrets_cache, $sec);
390                 }
391         }
392 return @list_secrets_cache;
393 }
394
395 # delete_secret(&sec)
396 # Removes one secret from the file
397 sub delete_secret
398 {
399 local $lref = &read_file_lines($config{'secrets'});
400 local $lines = $_[0]->{'eline'} - $_[0]->{'line'} + 1;
401 splice(@$lref, $_[0]->{'line'}, $lines);
402 &flush_file_lines();
403 local $s;
404 splice(@list_secrets_cache, $_[0]->{'idx'}, 1);
405 foreach $s (@list_secrets_cache) {
406         if ($s->{'line'} > $_[0]->{'line'}) {
407                 $s->{'line'} -= $lines;
408                 $s->{'eline'} -= $lines;
409                 }
410         if ($s->{'idx'} > $_[0]->{'idx'}) {
411                 $s->{'idx'}--;
412                 }
413         }
414 }
415
416 # create_secret(&sec)
417 # Add one secret to the file
418 sub create_secret
419 {
420 &list_secrets();        # force cache init
421 local $lref = &read_file_lines($config{'secrets'});
422 $_[0]->{'line'} = scalar(@$lref);
423 local @lines = &secret_lines($_[0]);
424 push(@$lref, @lines);
425 &flush_file_lines();
426 $_[0]->{'eline'} = scalar(@$lref)-1;
427 $_[0]->{'idx'} = scalar(@list_secrets_cache);
428 push(@list_secrets_cache, $_[0]);
429 }
430
431 # modify_secret(&sec)
432 # Update one secret in the file
433 sub modify_secret
434 {
435 local $lref = &read_file_lines($config{'secrets'});
436 local @newlines = &secret_lines($_[0]);
437 local $oldlines = $_[0]->{'eline'} - $_[0]->{'line'} + 1;
438 splice(@$lref, $_[0]->{'line'}, $oldlines, @newlines);
439 &flush_file_lines();
440 local $s;
441 foreach $s (@list_secrets_cache) {
442         if ($s ne $_[0] && $s->{'line'} > $_[0]->{'line'}) {
443                 $s->{'line'} += @newlines - $oldlines;
444                 $s->{'eline'} += @newlines - $oldlines;
445                 }
446         }
447 $_[0]->{'eline'} += @newlines - $oldlines;
448 }
449
450 sub secret_lines
451 {
452 local $str = $_[0]->{'name'} ? $_[0]->{'name'}." : " : ": ";
453 $str .= uc($_[0]->{'type'});
454 $str .= " ".$_[0]->{'value'};
455 return split(/\n/, $str);
456 }
457
458 @rsa_attribs = ( "Modulus", "PublicExponent", "PrivateExponent",
459                  "Prime1", "Prime2", "Exponent1", "Exponent2", "Coefficient" );
460
461 1;
462