# ipsec-lib.pl
# Common functions for managing the freeswan config file
# XXX check for new errors in log after applying
# XXX option to download connection as .conf file, and upload an existing
#     .conf file for addition

BEGIN { push(@INC, ".."); };
use WebminCore;
&init_config();

# get_config([file])
# Returns an array of configured connections
sub get_config
{
local $file = $_[0] || $config{'file'};
local (@rv, $sect);
local $lnum = 0;
local $fh = "CONF".$get_config_fh++;
open($fh, "<".$file);
while(<$fh>) {
	s/\r|\n//g;
	s/#.*$//;
	if (/^\s*([^= ]+)\s*=\s*"([^"]*)"/ ||
	    /^\s*([^= ]+)\s*=\s*'([^"]*)'/ ||
	    /^\s*([^= ]+)\s*=\s*(\S+)/) {
		# Directive within a section
		if ($sect) {
			if ($sect->{'values'}->{lc($1)}) {
				$sect->{'values'}->{lc($1)} .= "\0".$2;
				}
			else {
				$sect->{'values'}->{lc($1)} = $2;
				}
			$sect->{'eline'} = $lnum;
			}
		}
	elsif (/^\s*include\s+(\S+)/) {
		# Including possibly multiple files
		local $inc = $1;
		if ($inc !~ /^\//) {
			$file =~ /^(.*)\//;
			$inc = "$1/$inc";
			}
		local $g;
		foreach $g (glob($inc)) {
			local @inc = &get_config($g);
			map { $_->{'index'} += scalar(@rv) } @inc;
			push(@rv, @inc);
			}
		}
	elsif (/^\s*(\S+)\s+(\S+)/) {
		# Start of a section
		$sect = { 'name' => $1,
			  'value' => $2,
			  'line' => $lnum,
			  'eline' => $lnum,
			  'file' => $file,
			  'index' => scalar(@rv),
			  'values' => { } };
		push(@rv, $sect);
		}
	$lnum++;
	}
close($fh);
return @rv;
}

# create_conn(&conn)
# Add a new connection to the config file
sub create_conn
{
local $lref = &read_file_lines($_[0]->{'file'} || $config{'file'});
push(@$lref, "", &conn_lines($_[0]));
&flush_file_lines();
}

# modify_conn(&conn)
sub modify_conn
{
local $lref = &read_file_lines($_[0]->{'file'});
splice(@$lref, $_[0]->{'line'}, $_[0]->{'eline'} - $_[0]->{'line'} + 1,
       &conn_lines($_[0]));
&flush_file_lines();
}

# delete_conn(&conn)
sub delete_conn
{
local $lref = &read_file_lines($_[0]->{'file'});
splice(@$lref, $_[0]->{'line'}, $_[0]->{'eline'} - $_[0]->{'line'} + 1);
&flush_file_lines();
}

# swap_conns(&conn1, &conn2)
# Swaps two connections in the config file
sub swap_conns
{
local ($first, $second) = @_;
if ($first->{'line'} > $second->{'line'}) {
	($first, $second) = ($second, $first);
	}
local $lref1 = &read_file_lines($first->{'file'});
local $lref2 = &read_file_lines($second->{'file'});
splice(@$lref2, $second->{'line'}, $second->{'eline'} - $second->{'line'} + 1,
       @$lref1[$first->{'line'} .. $first->{'eline'}]);
splice(@$lref2, $first->{'line'}, $first->{'eline'} - $first->{'line'} + 1,
       @$lref1[$second->{'line'} .. $second->{'eline'}]);
&flush_file_lines();
}

# conn_lines(&conn)
sub conn_lines
{
local @rv;
push(@rv, $_[0]->{'name'}." ".$_[0]->{'value'});
foreach $o (sort { $a cmp $b } keys %{$_[0]->{'values'}}) {
	local $v = $_[0]->{'values'}->{$o};
	local $vv;
	foreach $vv (split(/\0/, $v)) {
		if ($vv =~ /\s|=/) {
			push(@rv, "\t".$o."=\"".$vv."\"");
			}
		else {
			push(@rv, "\t".$o."=".$vv);
			}
		}
	}
return @rv;
}

# is_ipsec_running()
sub is_ipsec_running
{
local $out = `$config{'ipsec'} auto --status 2>&1`;
return $? || $out =~ /not running/i ? 0 : 1;
}

# get_public_key()
# Returns this system's public key
sub get_public_key
{
local $out = `$config{'ipsec'} showhostkey --file '$config{'secrets'}' --left 2>&1`;
if ($out =~ /leftrsasigkey=(\S+)/) {
	return $1;
	}
return undef;
}

# get_public_key_dns()
# Returns the flags, protocol, algorithm and key data for the public key,
# suitable for creating a DNS KEY record
sub get_public_key_dns
{
local $out = `$config{'ipsec'} showhostkey --file '$config{'secrets'}' 2>&1`;
if ($out =~ /KEY\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)/) {
	return ($1, $2, $3, $4);
	}
else {
	# Try with new --key argument
	$out = `$config{'ipsec'} showhostkey --key --file '$config{'secrets'}' 2>&1`;
	if ($out =~ /KEY\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)/) {
		return ($1, $2, $3, $4);
		}
	}
return ();
}

# list_policies()
# Returns a list of all policy files
sub list_policies
{
local ($f, @rv);
opendir(DIR, $config{'policies_dir'});
while($f = readdir(DIR)) {
	push(@rv, $f) if ($f !~ /^\./ && $f !~ /\.rpmsave$/);
	}
closedir(DIR);
return @rv;
}

# read_policy(name)
sub read_policy
{
local @rv;
open(FILE, "<$config{'policies_dir'}/$_[0]");
while(<FILE>) {
	push(@rv, "$1/$2") if (/^\s*([0-9\.]+)\/(\d+)/);
	}
close(FILE);
return @rv;
}

# write_policy(name, &nets)
sub write_policy
{
local $lref = &read_file_lines("$config{'policies_dir'}/$_[0]");
local $l = 0;
foreach $p (@{$_[1]}) {
	while($l < @$lref && $lref->[$l] !~ /^\s*([0-9\.]+)\/(\d+)/) {
		$l++;
		}
	if ($l < @$lref) {
		# Found line to replace
		$lref->[$l] = $p;
		}
	else {
		# Add at end
		push(@$lref, $p);
		}
	$l++;
	}
while($l < @$lref) {
	if ($lref->[$l] =~ /^\s*([0-9\.]+)\/(\d+)/) {
		splice(@$lref, $l, 1);
		}
	else { $l++; }
	}
&flush_file_lines();
}

# wrap_lines(text, width)
# Given a multi-line string, return an array of lines wrapped to
# the given width
sub wrap_lines
{
local $rest = $_[0];
local @rv;
while(length($rest) > $_[1]) {
	push(@rv, substr($rest, 0, $_[1]));
	$rest = substr($rest, $_[1]);
	}
push(@rv, $rest) if ($rest ne '');
return @rv;
}

# before_start()
# Work out which log file IPsec messages go to, and record the size
sub before_start
{
@ipsec_logfiles = ( $config{'logfile'} );
if (&foreign_check("syslog")) {
	# Find all syslog logfiles
	&foreign_require("syslog", "syslog-lib.pl");
	local $conf = &syslog::get_config();
	foreach $c (@$conf) {
		push(@ipsec_logfiles, $c->{'file'}) if ($c->{'file'} &&
							-f $c->{'file'});
		}
	}
@ipsec_logfiles = &unique(@ipsec_logfiles);
@ipsec_logfile_sizes = map { local @st = stat($_); $st[7] } @ipsec_logfiles;
}

# after_start()
# Check any new IPsec-related messages in the log for errors
sub after_start
{
# Give the server a chance to start
sleep(5);

# Look for new error messages
local $i;
for($i=0; $i<@ipsec_logfiles; $i++) {
	open(LOG, "<".$ipsec_logfiles[$i]) || next;
	seek(LOG, $ipsec_logfile_sizes[$i], 0);
	while(<LOG>) {
		s/\r|\n//g;
		if (/ipsec/i && /error/i) {
			s/^(\S+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\S+)\s+//;
			push(@errs, $_);
			}
		}
	close(LOG);
	}

# Fail if there were any
if (@errs) {
	&error(&text('start_elog', "<p><tt>".join("<br>", @errs)."</tt><br>"));
	}
}

# get_ipsec_version(&out)
sub get_ipsec_version
{
local $out = `$config{'ipsec'} --version 2>&1`;
${$_[0]} = $out;
return $out =~ /(FreeS\/WAN|Openswan|StrongSWAN|Libreswan)\s+([^ \n\(]+)/i ? ($2,$1) : (undef);
}

# got_secret()
# Returns 1 if a valid secret key file exists, 0 if not
sub got_secret
{
local $gotkey;
open(SEC, "<".$config{'secrets'}) || return 0;
while(<SEC>) {
	s/\r|\n//g;
	s/#.*$//;
	if (/Modulus:\s*(\S+)/) {
		$gotkey = 1;
		}
	}
close(SEC);
return $gotkey;
}

# expand_conf(&config)
sub expand_conf
{
my $conf = shift;
for my $n (0..scalar(@$conf)-1) {
	$conn = @$conf[$n];
	foreach my $key (keys(%{$conn->{'values'}})) {
		$expanded{$conn->{'value'}}->{$key} = $conn->{'values'}->{$key};
		}
	}
# now go through and expand alsos
foreach my $k (keys(%expanded)) {
	$conn = \%{$expanded{$k}};
	# XXX - only supporing a single level of redirection
	#     - this should be moved into a function that could be called
	#       recursively
	if ($$conn{'also'}) {
		foreach my $also (split(/\000/, $$conn{'also'})) {
			foreach my $i (keys(%{$expanded{$also}})) {
				$$conn{$i} = $expanded{$also}{$i};
				}
			}
		# there is only one also key
		next;
		}
	}
return %expanded;
}

# restart_ipsec()
# Apply the current configuration, and return an error message on failure or
# undef on success
sub restart_ipsec
{
local $cmd = $config{'restart_cmd'} ||
       "($config{'stop_cmd'} && $config{'start_cmd'})";
&before_start();
local $out = &backquote_logged("$cmd 2>&1");
if ($?) {
	return "<pre>$out</pre>";
	}
&after_start();
return undef;
}

# list_secrets()
# Returns a list of IPsec secret keys
sub list_secrets
{
if (!scalar(@list_secrets_cache)) {
	local (@lines);
	local $lnum = 0;
	open(SEC, "<".$config{'secrets'});
	while(<SEC>) {
		s/\r|\n//g;
		s/^\s*#.*$//;
		if (/^(\S.*)$/) {
			push(@lines, { 'value' => $1,
				       'line' => $lnum,
				       'eline' => $lnum });
			}
		elsif (/^\s+(.*)/ && @lines) {
			$lines[$#rv]->{'value'} .= "\n".$1;
			$lines[$#rv]->{'eline'} = $lnum;
			}
		$lnum++;
		}
	close(SEC);

	# Turn joined lines into secrets
	local $l;
	foreach $l (@lines) {
		$l->{'value'} =~ /^([^:]*)\s*:\s+(\S+)\s+((.|\n)*)$/ || next;
		local $sec = { 'type' => $2,
			       'name' => $1,
			       'value' => $3,
			       'line' => $l->{'line'},
			       'eline' => $l->{'eline'},
			       'idx' => scalar(@list_secrets_cache),
			      };
		$sec->{'name'} =~ s/\n/ /g;
		$sec->{'name'} =~ s/\s+$//;
		push(@list_secrets_cache, $sec);
		}
	}
return @list_secrets_cache;
}

# delete_secret(&sec)
# Removes one secret from the file
sub delete_secret
{
local $lref = &read_file_lines($config{'secrets'});
local $lines = $_[0]->{'eline'} - $_[0]->{'line'} + 1;
splice(@$lref, $_[0]->{'line'}, $lines);
&flush_file_lines();
local $s;
splice(@list_secrets_cache, $_[0]->{'idx'}, 1);
foreach $s (@list_secrets_cache) {
	if ($s->{'line'} > $_[0]->{'line'}) {
		$s->{'line'} -= $lines;
		$s->{'eline'} -= $lines;
		}
	if ($s->{'idx'} > $_[0]->{'idx'}) {
		$s->{'idx'}--;
		}
	}
}

# create_secret(&sec)
# Add one secret to the file
sub create_secret
{
&list_secrets();	# force cache init
local $lref = &read_file_lines($config{'secrets'});
$_[0]->{'line'} = scalar(@$lref);
local @lines = &secret_lines($_[0]);
push(@$lref, @lines);
&flush_file_lines();
$_[0]->{'eline'} = scalar(@$lref)-1;
$_[0]->{'idx'} = scalar(@list_secrets_cache);
push(@list_secrets_cache, $_[0]);
}

# modify_secret(&sec)
# Update one secret in the file
sub modify_secret
{
local $lref = &read_file_lines($config{'secrets'});
local @newlines = &secret_lines($_[0]);
local $oldlines = $_[0]->{'eline'} - $_[0]->{'line'} + 1;
splice(@$lref, $_[0]->{'line'}, $oldlines, @newlines);
&flush_file_lines();
local $s;
foreach $s (@list_secrets_cache) {
	if ($s ne $_[0] && $s->{'line'} > $_[0]->{'line'}) {
		$s->{'line'} += @newlines - $oldlines;
		$s->{'eline'} += @newlines - $oldlines;
		}
	}
$_[0]->{'eline'} += @newlines - $oldlines;
}

sub secret_lines
{
local $str = $_[0]->{'name'} ? $_[0]->{'name'}." : " : ": ";
$str .= uc($_[0]->{'type'});
$str .= " ".$_[0]->{'value'};
return split(/\n/, $str);
}

@rsa_attribs = ( "Modulus", "PublicExponent", "PrivateExponent",
		 "Prime1", "Prime2", "Exponent1", "Exponent2", "Coefficient" );

1;

