#!/usr/bin/perl
# save_rule.cgi
# Save a new or existing rule

require './nftables-lib.pl';    ## no critic
use strict;
use warnings;
our (%in, %text);
ReadParse();
error_setup($text{'save_err'});
assert_acl('rules');
assert_acl('raw') if ($in{'edit_direct'});
my @tables = get_nftables_save();
my $table = $tables[$in{'table'}];
$table || error($text{'move_notable'});
assert_table_acl($table);

foreach my $sfield (qw(saddr_set daddr_set sport_set dport_set)) {
	if ($in{$sfield}) {
		$table->{'sets'}->{$in{$sfield}} ||
		    error(text('save_set_missing', $in{$sfield}));
		}
	}
foreach my $check (
	['saddr_set', 'addr', $text{'edit_saddr'}],
	['daddr_set', 'addr', $text{'edit_daddr'}],
	['sport_set', 'port', $text{'edit_sport'}],
	['dport_set', 'port', $text{'edit_dport'}],
    )
{
	my ($sfield, $want, $label) = @$check;
	next if (!$in{$sfield});
	my $set = $table->{'sets'}->{$in{$sfield}};
	my $kind = set_type_kind($set->{'type'});
	if (!$kind || $kind ne $want) {
		my $type = $set->{'type'} || $text{'set_type_select'};
		error(text('save_set_type', $in{$sfield}, $type, $label));
		}
	}

# join_multi_value(string)
# Joins multi-select form values into an nftables comma list
sub join_multi_value
{
my ($v) = @_;
return if (!defined($v) || $v eq '');
my @vals = split(/\0/, $v);
@vals = grep { defined($_) && $_ ne '' } @vals;
return if (!@vals);
return join(",", @vals);
}

if ($in{'delete'}) {
	# Delete the rule
	my $rule = $table->{'rules'}->[$in{'idx'}];
	splice(@{$table->{'rules'}}, $in{'idx'}, 1);
	webmin_log("delete", "rule", $rule ? $rule->{'text'} : undef);
	}
else {
	my $rule = {};
	if ($in{'new'}) {
		$rule->{'chain'} = $in{'chain'};
		$rule->{'index'} = scalar(@{$table->{'rules'}});
		}
	else {
		$rule = $table->{'rules'}->[$in{'idx'}];
		}

	if ($in{'edit_direct'}) {
		my $raw = $in{'raw_rule'};
		$raw =~ s/\r//g if (defined($raw));
		$raw =~ s/^\s+// if (defined($raw));
		$raw =~ s/\s+$// if (defined($raw));
		error($text{'save_raw_empty'}) if (!defined($raw) || $raw eq '');
		error($text{'save_raw_multiline'}) if ($raw =~ /[\r\n]/);
		$rule->{'text'} = $raw;
		}
	else {
		$rule->{'comment'} = $in{'comment'};
		my $action = $in{'action'} || 'accept';
		$rule->{'action'} = undef;
		$rule->{'jump'} = undef;
		$rule->{'goto'} = undef;
		$rule->{'nat_addr'} = undef;
		$rule->{'nat_port'} = undef;
		$rule->{'nat_family'} = undef;
		if ($action eq 'jump') {
			$rule->{'jump'} = $in{'jump'};
			}
		elsif ($action eq 'goto') {
			$rule->{'goto'} = $in{'goto'};
			}
		else {
			$rule->{'action'} = $action;
			}
		if ($action eq 'redirect') {
			my $nat_port = $in{'nat_port'};
			$nat_port =~ s/^\s+// if (defined($nat_port));
			$nat_port =~ s/\s+$// if (defined($nat_port));
			$rule->{'nat_port'} =
			    (defined($nat_port) && $nat_port ne '') ? $nat_port : undef;
			}
		elsif ($action eq 'dnat') {
			my $nat_addr = $in{'nat_addr'};
			my $nat_port = $in{'nat_port'};
			$nat_addr =~ s/^\s+// if (defined($nat_addr));
			$nat_addr =~ s/\s+$// if (defined($nat_addr));
			$nat_port =~ s/^\s+// if (defined($nat_port));
			$nat_port =~ s/\s+$// if (defined($nat_port));
			$rule->{'nat_addr'} =
			    (defined($nat_addr) && $nat_addr ne '') ? $nat_addr : undef;
			$rule->{'nat_port'} =
			    (defined($nat_port) && $nat_port ne '') ? $nat_port : undef;
			$rule->{'nat_family'} =
			    $rule->{'nat_addr'} &&
			    (($table->{'family'} || '') eq 'inet') ?
			    guess_addr_family($rule->{'nat_addr'}) : undef;
			}

		my $saddr = $in{'saddr'};
		my $daddr = $in{'daddr'};
		$saddr = '@'.$in{'saddr_set'} if ($in{'saddr_set'});
		$daddr = '@'.$in{'daddr_set'} if ($in{'daddr_set'});
		$rule->{'saddr'} = (defined($saddr) && $saddr ne '') ? $saddr : undef;
		$rule->{'daddr'} = (defined($daddr) && $daddr ne '') ? $daddr : undef;
		$rule->{'saddr_family'} =
		    $rule->{'saddr'} ? guess_addr_family($rule->{'saddr'}) : undef;
		$rule->{'daddr_family'} =
		    $rule->{'daddr'} ? guess_addr_family($rule->{'daddr'}) : undef;

		if ($rule->{'saddr'} && $rule->{'saddr'} =~ /^\@(\S+)/) {
			my $fam = set_type_family($table->{'sets'}->{$1}->{'type'});
			$rule->{'saddr_family'} = $fam if ($fam);
			}
		if ($rule->{'daddr'} && $rule->{'daddr'} =~ /^\@(\S+)/) {
			my $fam = set_type_family($table->{'sets'}->{$1}->{'type'});
			$rule->{'daddr_family'} = $fam if ($fam);
			}

		my $proto = $in{'proto'};
		$proto = undef if (defined($proto) && $proto eq '');
		my $sport = $in{'sport'};
		my $dport = $in{'dport'};
		$sport = '@'.$in{'sport_set'} if ($in{'sport_set'});
		$dport = '@'.$in{'dport_set'} if ($in{'dport_set'});
		$rule->{'sport'} = (defined($sport) && $sport ne '') ? $sport : undef;
		$rule->{'dport'} = (defined($dport) && $dport ne '') ? $dport : undef;

		if (!$proto && ($rule->{'sport'} || $rule->{'dport'})) {
			$proto = 'tcp';
			}
		$rule->{'l4proto'} = undef;
		$rule->{'l4proto_family'} = undef;
		$rule->{'proto'} = undef;
		$rule->{'sport_proto'} = undef;
		if ($proto && ($proto eq 'tcp' || $proto eq 'udp')) {
			$rule->{'proto'} = $proto
			    if ($rule->{'sport'} || $rule->{'dport'});
			$rule->{'sport_proto'} = $proto if ($rule->{'sport'});
			}
		elsif ($proto && $proto !~ /^(tcp|udp)$/) {
			$rule->{'sport'} = undef;
			$rule->{'dport'} = undef;
			}
		if ($proto) {
			if (($proto eq 'tcp' || $proto eq 'udp') &&
				($rule->{'sport'} || $rule->{'dport'}))
			{
				# L4 proto implied by port match
				}
			else {
				$rule->{'l4proto'} = $proto;
				$rule->{'l4proto_family'} = 'meta';
				}
			}

		my $icmp_type = $in{'icmp_type'};
		$rule->{'icmp_type'} = undef;
		$rule->{'icmpv6_type'} = undef;
		if ($proto && $proto eq 'icmp') {
			$rule->{'icmp_type'} = $icmp_type
			    if (defined($icmp_type) && $icmp_type ne '');
			}
		elsif ($proto && $proto eq 'icmpv6') {
			$rule->{'icmpv6_type'} = $icmp_type
			    if (defined($icmp_type) && $icmp_type ne '');
			}
		elsif (!$proto && defined($icmp_type) && $icmp_type ne '') {
			$rule->{'icmp_type'} = $icmp_type;
			$rule->{'l4proto'} = 'icmp';
			$rule->{'l4proto_family'} = 'meta';
			}

		my $ct_state = join_multi_value($in{'ct_state'});
		my $tcp_flags = join_multi_value($in{'tcp_flags'});
		$rule->{'ct_state'} = defined($ct_state) ? $ct_state : undef;
		$rule->{'tcp_flags'} = defined($tcp_flags) ? $tcp_flags : undef;
		$rule->{'tcp_flags_mask'} =
		    (defined($in{'tcp_flags_mask'}) && $in{'tcp_flags_mask'} ne '')
		    ? $in{'tcp_flags_mask'}
		    : undef;
		$rule->{'limit_rate'} =
		    (defined($in{'limit_rate'}) && $in{'limit_rate'} ne '')
		    ? $in{'limit_rate'}
		    : undef;
		$rule->{'limit_burst'} =
		    (defined($in{'limit_burst'}) && $in{'limit_burst'} ne '')
		    ? $in{'limit_burst'}
		    : undef;

		my $log_enabled = $in{'log'} || $in{'log_prefix'} || $in{'log_level'};
		$rule->{'log'} = $log_enabled ? 1 : undef;
		$rule->{'log_prefix'} =
		    $log_enabled &&
		    defined($in{'log_prefix'}) &&
		    $in{'log_prefix'} ne '' ? $in{'log_prefix'} : undef;
		$rule->{'log_level'} =
		    $log_enabled &&
		    defined($in{'log_level'}) &&
		    $in{'log_level'} ne '' ? $in{'log_level'} : undef;
		$rule->{'counter'} = $in{'counter'} ? 1 : undef;

		my $iif = $in{'iif'};
		my $oif = $in{'oif'};
		$iif = $in{'iif_other'} if (defined($iif) && $iif eq 'other');
		$oif = $in{'oif_other'} if (defined($oif) && $oif eq 'other');
		$rule->{'iif'} = (defined($iif) && $iif ne '') ? $iif : undef;
		$rule->{'oif'} = (defined($oif) && $oif ne '') ? $oif : undef;

		$rule->{'text'} = format_rule_text($rule);
		}

	if ($in{'new'}) {
		push(@{$table->{'rules'}}, $rule);
		}

	if ($in{'edit_direct'}) {
		my $cmd = get_nft_command();
		if ($cmd) {
			my $tmp = tempname();
			open_tempfile(my $fh, ">$tmp");
			print_tempfile($fh, dump_nftables_save(@tables));
			close_tempfile($fh);
			my $out = backquote_logged("$cmd -c -f $tmp 2>&1");
			unlink_file($tmp);
			error(text('save_invalid_rule', "<pre>$out</pre>")) if ($?);
			}
		}

	webmin_log("save", $in{'new'} ? "create" : "modify", $rule->{'text'});
	}
my $err = save_table_configuration($table, @tables);
error(text('save_failed', $err)) if ($err);
redirect("index.cgi?table=$in{'table'}");
