#! @PERL@ -wT -I@cgibin@ # display and edit a DCC whitelist file # --S-LICENSE-- # $Revision: 1.74 $ # @configure_input@ # This file must protected with an equivalent to httpd.conf lines # in the README file. use strict 'subs'; use POSIX qw(strftime); use common; my($main_whiteclnt); # path to the main whiteclnt file my(@file); # list representation of the file my(%dict); # dictionary of checksums and options my(%def_options); # option settings from main whiteclnt file my($have_entry_form, $form_marked, $cur_pos, $cur_key, $cur_entry, $cur_index); my $form_num = 0; # display the file literally if ($query{literal}) { my($buf); open(WHITECLNT, "< $whiteclnt") or html_whine("open($whiteclnt): $!"); print "Content-type: text/plain\n"; print "Cache-Control: max-age=0, must-revalidate\n\n"; print $buf while (read(WHITECLNT, $buf, 4*1024)); print "\n"; close(WHITECLNT); exit; } # lock, read and parse the whiteclnt file read_whiteclnt(\@file, \%dict); # get option defaults from the main whiteclnt file read_whitedefs(\%def_options); # get current position for the entry editing form $cur_pos = $query{pos}; # find a whitecnt file entry to edit if ($query{key}) { $cur_key = $query{key}; } elsif ($query{auto} && $query{type} && $query{val}) { my @new_entry = ck_new_white_entry("", "ok", $query{type}, $query{val}); $cur_key = $new_entry[0] if (defined($new_entry[1])); } $cur_entry = $dict{$cur_key} if ($cur_key); html_head("DCC Whitelist for $user at $hostname"); common_buttons(); print "return to $list_msg_link${url_ques}msg=$query{msg}\">logged message $query{msg}\n" if ($query{msg}); print <$edit_link${url_ques}literal=yes" TARGET="DCC literal whiteclnt">Literal contents of whitelist. EOF # add new entry if ($query{Add}) { my(@new_entry, $msg, $prev, $cur, $add_pos); @new_entry = ck_new_white_entry($query{comment}, $query{count}, $query{type}, $query{val}); give_up($new_entry[0]) if (!defined($new_entry[1])); # insert into the file instead of appending to the end if we have # a valid position if (defined($cur_pos)) { $add_pos = next_index($cur_pos); } elsif ($cur_key) { ($prev, $cur, $add_pos) = neighbors($cur_key); } $cur_key = $new_entry[0]; $cur_entry = \@new_entry; give_up("entry already present") if ($dict{$cur_key}); # send the new entry to the disk with the rest of the file $whiteclnt_cur_key = $cur_key; $msg = chg_white_entry(\@file, \%dict, $cur_key, \@new_entry, $add_pos); give_up($msg) if ($msg); # re-prime the form with cleaned comment $cur_key = $new_entry[0]; $cur_entry = $dict{$cur_key}; give_up("new entry did not reach file") if (!$cur_entry); finish("whitelist entry added"); } # change current whitelist entry if ($query{Change}) { my(@new_entry, $msg); give_up("no entry selected to change") if (!$cur_key); give_up("entry '$cur_key' has disappeared") if (!$cur_entry || !$$cur_entry[0]); @new_entry = ck_new_white_entry($query{comment}, $query{count}, $query{type}, $query{val}); give_up($new_entry[0]) if (!defined($new_entry[1])); give_up("no changes requested") if ($$cur_entry[1] eq $new_entry[1] && $$cur_entry[2] eq $new_entry[2]); # send the change to the disk with the rest of the file $whiteclnt_cur_key = $cur_key; $msg = chg_white_entry(\@file, \%dict, $cur_key, \@new_entry); give_up($msg) if ($msg); # re-prime the form with cleaned comment $cur_key = $new_entry[0]; $cur_entry = $dict{$cur_key}; give_up("changed entry did not reach file") if (!$cur_entry); finish("whitelist entry changed"); } # delete current entry if ($query{Delete}) { my($prev, $cur, $next, $new_key, $msg); give_up("no entry selected to delete") if (!$cur_key); give_up("entry '$cur_key' has disappeared") if (!$cur_entry || !$$cur_entry[0]); # find a neighbor of the entry to be deleted ($prev, $cur, $next) = neighbors($cur_key); $new_key = ${$file[$prev]}[0] if (defined($prev)); # write everything to the new file except the deleted entry $whiteclnt_cur_key = $cur_key; $msg = chg_white_entry(\@file, \%dict, $cur_key, undef); give_up($msg) if ($msg); # keep the add/change/delete form in place if possible $cur_key = $new_key; undef($cur_entry); delete $query{comment}; delete $query{count}; delete $query{type}; delete $query{val}; $cur_entry = $dict{$cur_key} if ($cur_key); finish("whitelist entry deleted"); } # move the current entry up if ($query{Up}) { up(1); } if ($query{Up5}) { up(5); } sub up { my($delta) = @_; my($prev, $cur, $next, $moved, @new_file, $msg); if ($cur_entry) { # move an existing entry while ($delta) { --$delta; # It is inefficient but easy and clearly correct to repeated # search the array of entries for the target and then build # a new array. ($prev, $cur, $next) = neighbors($cur_key); if (!$prev) { give_up("cannot move above the top") if (!$moved); last; } @new_file = (@file[0 .. $prev-1], $file[$cur], $file[$prev], @file[$cur+1 .. $#file]); @file = @new_file; $moved = 1; } $whiteclnt_cur_key = $cur_key; $msg = write_whiteclnt(@file); give_up($msg) if ($msg); read_whiteclnt(\@file, \%dict); } else { # move a new or proposed entry $cur_pos = prev_index($cur_pos, $delta); } print_form_file(); } # move the current entry down if ($query{Down}) { down(1); } if ($query{Down5}) { down(5); } sub down { my($delta) = @_; my($prev, $cur, $next, @new_file, $msg); if ($cur_entry) { # move an existing entry while ($delta) { --$delta; ($prev, $cur, $next) = neighbors($cur_key); if (!$next) { give_up("cannot move below the bottom") if (!$moved); last; } @new_file = (@file[0 .. $cur-1], $file[$next], $file[$cur], @file[$next+1 .. $#file]); @file = @new_file; $moved = 1; } $whiteclnt_cur_key = $cur_key; $msg = write_whiteclnt(@file); give_up($msg) if ($msg); read_whiteclnt(\@file, \%dict); } elsif (defined($cur_pos)) { # move position of a future entry $cur_pos = next_index($cur_pos, $delta); } print_form_file(); } # undo the previous change if ($query{Undo}) { my $msg = undo_whiteclnt(); give_up($msg) if ($msg); $cur_key = $whiteclnt_cur_key; read_whiteclnt(\@file, \%dict); $cur_key = $whiteclnt_cur_key if ($whiteclnt_cur_key); # put the add/change/delete form back in place if ($cur_key) { $cur_entry = $dict{$cur_key}; } else { undef($cur_entry); delete $query{comment}; delete $query{count}; delete $query{type}; delete $query{val}; } finish("change undone"); } # change new log file mail notifcations my $old_notify = $whiteclnt_notify; if ($query{notify}) { if ($query{notify} =~ /off/) { $whiteclnt_notify =~ s/$whiteclnt_notify_pat$/${1}off$3$4/i; } elsif ($query{notify} =~ /on/) { $whiteclnt_notify =~ s/$whiteclnt_notify_pat$/${1}on$3$4/i; } } if (defined($query{notifybox})) { my $new_box = $query{notifybox}; $new_box =~ s/^\s+(.*)\s*$/$1/; $whiteclnt_notify =~ s/$whiteclnt_notify_pat$/$1$2$3$new_box/i; give_up('The notification mailbox is limited to -, _, letters, and digits') if ($whiteclnt_notify !~ /^$whiteclnt_notify_pat$/); } if ($whiteclnt_notify ne $old_notify) { $whiteclnt_cur_key = ""; my $msg = write_whiteclnt(@file); give_up($msg) if ($msg); read_whiteclnt(\@file, \%dict); } # process requests to change options option_form("dccenable", "On", "dcc-on", "Off", "dcc-off"); option_form("greyfilter", "On", "greylist-on", "Off", "greylist-off");; option_form("greylog", "On", "greylist-log-on", "Off", "greylist-log-off"); option_form("mtafirst", "first", "MTA-first", "last", "MTA-last"); option_form("rep", "On", "DCC-rep-on", "Off", "DCC-rep-off"); option_form("dnsbl1", "On", "dnsbl1-on", "Off", "dnsbl1-off"); option_form("dnsbl2", "On", "dnsbl2-on", "Off", "dnsbl2-off"); option_form("dnsbl3", "On", "dnsbl3-on", "Off", "dnsbl3-off"); option_form("dnsbl4", "On", "dnsbl4-on", "Off", "dnsbl4-off"); option_form("logall", "On", "log-all", "Off", "log-normal"); option_form("logsubdir", "day", "log-subdirectory-day", "hour", "log-subdirectory-hour", "minute", "log-subdirectory-minute"); option_form("discardok", "discard spam", "forced-discard-ok", "delay mail", "no-forced-discard"); # process requests from the HTTP client to change the threshold foreach my $ck (split(/,/, $thold_cks)) { my $nm = "thold-$ck"; foreach my $val ($query{$nm}, $query{"text-$nm"}) { next if (!$val); if ($val =~ /^Default/) { set_option($nm); } elsif (!parse_thold_value($ck, $val)) { give_up("invalid threshold setting $nm='$val'"); } else { set_option($nm, "option threshold $ck,$val\n"); } last; } } # nothing to do? give_up("entry '$cur_key' has disappeared") if (!$query{auto} && $cur_key && (!$cur_entry || !$$cur_entry[0])); print_form_file($query{result} ? "

$query{result}\n" : ""); ############################################################################# # display the whiteclnt file with the option setting form and and quit sub print_form_file { my($result) = @_; # "" or some kind of error message close(WHITECLNT); my $locked = ($whiteclnt_lock =~ /\blocked/) ? " disabled" : ""; # display any error message from the previous action print $result ? $result : "

 \n"; # generate table of forms to control option lines print "

\n\n"; print_form_start("
", "", ""); print "\t
"; undo_form($locked); print "\t
\n"; # two HTML forms for the '#webuser...' line $whiteclnt_notify =~ /$whiteclnt_notify_pat/; my $notify_cur = $2; my $notifybox = $4; my $notify_on_locked = ($notify_cur eq "on") ? " disabled" : $locked; my $notify_off_locked = ($notify_cur eq "off") ? " disabled" : $locked; print_form_start("
", "", "
"); print < $notify_cur
EOF print_form_start("
", "", "
"); print_button("\t", "notify", $notify_on_locked, "on"); print_button("\t", "notify", $notify_off_locked, "off"); print "\t
\n"; table_row_form("dccenable", "DCC", $locked, "dcc-off", "dcc-on"); if ($DCCM_ARGS =~ /-G/ || $DCCIFD_ARGS =~ /-G/ || (defined($GREY_CLIENT_ARGS) && $GREY_CLIENT_ARGS ne "")) { table_row_form("greyfilter", "greylist filter", $locked, "greylist-off", "greylist-on"); table_row_form("greylog", "greylist log", $locked, "greylist-log-off", "greylist-log-on"); } table_row_form("mtafirst", "check MTA blacklist", $locked, "MTA-last", "MTA-first", "last", "first"); table_row_form("rep", "DCC Reputations", $locked, "DCC-rep-off", "DCC-rep-on"); # ask about DNSBLs if they are available my $args = "$DNSBL_ARGS $DCCM_ARGS $DCCIFD_ARGS"; if ($args =~ /-B/) { if (!defined($dict{dnsbl2}) && !defined($dict{dnsbl3}) && !defined($dict{dnsbl4}) && $args !~ /-B\s*set:group=\d+/i) { # only one question if there are no groups table_row_form("dnsbl1", "DNS list checking", $locked); } else { table_row_form("dnsbl1", "DNS list #1 checking", $locked); table_row_form("dnsbl2", "DNS list #2 checking", $locked); table_row_form("dnsbl3", "DNS list #3 checking", $locked) if (defined($dict{dnsbl3})|| defined($dict{dnsbl4}) || $args =~ /-B\s*set:group=[34]/i); table_row_form("dnsbl4", "DNS list #4 checking", $locked) if (defined($dict{dnsbl4}) || $args =~ /-B\s*set:group=4/i); } } table_row_form("logall", "debug logging", $locked, "log-normal", "log-all"); table_row_form("discardok", " also addressed to others", $locked, "no-forced-discard", "forced-discard-ok", "delay mail", "discard spam"); # forms for checksum thresholds foreach my $ck (split(/,/, $thold_cks)) { my($cur_val, $sw_val, $nm, $def_label, $bydef, $dis_field, $dis_def, $dis_never); $nm = "thold-" . $ck; # construct label for the default button from default value $def_label = $def_options{$nm}; $def_label =~ s/.*([^<]+)<.*/Default ($1)/; if (defined($dict{$nm})) { $cur_val = $dict{$nm}[2]; $cur_val =~ s/.*,([-_a-z0-9%]+)\s+$/$1/i; $bydef = ''; $sw_val = $cur_val; } else { $cur_val = $def_options{$nm}; $cur_val =~ s@(.*)(.*)@$1@; $bydef = $2; $sw_val = 'Default'; } $dis_field = $locked; $dis_def = $locked; $dis_never = $locked; $dis_def = " class=selected disabled" if ($sw_val eq "Default"); $dis_never = " class=selected disabled" if ($sw_val eq "Never"); # changing reputation thresholds ought to affect tagging # even if reputation checking is turned off print_form_start("
", "", "
"); print <$ck threshold$bydef
EOF print_form_start("
", "", "
"); print_button("\t", $nm, $dis_def, $def_label); print_button("\t", $nm, $dis_never, "Never"); # "many" makes no sense for either reputation threshold print_button("\t", $nm, $sw_val =~ /^many$/i ? " disabled" : $locked, "MANY") if ($ck !~ /^rep/i); print "\t
\n"; } print "
\n\n


\n

\n"; # display a form for a new entry before the file if we have not # been given a position or an entry to modify print_entry_form($locked, $result) if (!$cur_key && !defined($cur_pos)); print_whiteclnt_file($result, $locked); } # display the common start of forms sub print_form_start { my($before, # HTML before start of form $tag, # tag on action $after # HTML after start of form ) = @_; print $before if ($before); print "

$form_hidden\n"; print $after if ($after); print "\t\n" if ($query{msg}); if ($cur_key) { print "\t\n"; } if ($cur_pos) { print "\t\n"; } } sub undo_form { my($locked) = @_; print "\n"; } # display the entry editing form sub print_entry_form { my($locked, $result) = @_; my($add_str, $new_val, $comment, $comment_rows, $change_ok, $prev, $cur, $next); return if ($have_entry_form); $have_entry_form = 1; # prime the form with the currently selected whiteclnt entry, if any if ($cur_entry) { $comment = $$cur_entry[1]; $query{comment} = html_str_encode($comment); my $value = $$cur_entry[2]; $value =~ s/(\S+)\s+//; $query{count} = $1; ($query{type}, $query{val}) = parse_type_value($value); $change_ok = $locked; } else { # "disabled" does not work with Netscape 4.*, but we have to handle # changes without a valid key, so don't worry about it $change_ok = " disabled"; } # compute a comment if this came from a log file if ($query{auto} && !$cur_entry) { $comment = "\n#"; $comment .= " added from logged message $query{msg}" if ($query{msg}); $comment .= strftime(" %x", localtime); $comment = html_str_encode($comment); $query{count} = "OK"; } else { $comment = $query{comment}; $comment = '' if (!defined($comment)); } $comment =~ s/\h+$//mg; # need a blank on a leading blank line in $comment =~ s/^\n/ \n/; $comment_rows = $comment; $comment_rows =~ s/[^\n]//g; $comment_rows = length($comment_rows); $comment_rows = 2 if ($comment_rows < 2); $comment_rows = 10 if ($comment_rows > 10); if (!$form_marked) { print_form_start("", "#cur_key", "\n
 "); } else { print_form_start("", "#cur_key", "\n
 "); } print " "; print_button("\t", "Add", $locked, "Add"); if (defined($cur_pos)) { $prev = prev_index($cur_pos); $next = next_index($cur_pos); } elsif ($cur_key) { ($prev, $cur, $next) = neighbors($cur_key); } else { undef($prev); undef($next); } if ($query{auto} && !$cur_entry) { print_button("\t", "Change", " disabled", "Change"); print_button("\t", "Delete", " disabled", "Delete"); print "\tfrom $list_msg_link${url_ques}msg=$query{msg}\">logged message $query{msg}"; } else { print_button("\t", "Change", $change_ok, "Change"); print_button("\t", "Delete", $change_ok, "Delete"); undo_form($locked); } print_button("\t", "Up", !defined($prev) ? " disabled" : $locked, "Move Up"); print_button("\t", "Up5", !defined($prev) ? " disabled" : $locked, "Move Up 5"); print_button("\t", "Down", !$next ? " disabled" : $locked, "Move Down"); print_button("\t", "Down5", !$next ? " disabled" : $locked, "Move Down 5"); print <Description $comment
  \n"; print "\t\n"; print "\t\n"; print "
return to $list_msg_link${url_ques}msg=$query{msg}\">logged message $query{msg}\n" if ($query{msg}); print "
\n"; print $result ? $result : " "; print "
\n\n\n"; } # find indeces of previous, current, and next entries # return a list of 3 entries of the preceding, current, and following indeces sub neighbors { my($tgt_key) = @_; my($prev, $cur, $index, $entry); # look for the current entry while tracking predecessors $index = 0; foreach $entry (@file) { next if (!ref($entry)); # ignore deleted lines, options, and include lines next if (!$$entry[0] || !defined($$entry[1]) || $$entry[2] =~ /^option/); # stop at the first entry when there is no current position return ($prev, $cur, $index) if (!$tgt_key); if ($$entry[0] eq $tgt_key) { $cur = $index; last; } $prev = $index; } continue { ++$index; } do { return ($prev, $cur, undef) if ($index >= $#file); $entry = $file[++$index]; } while (!$$entry[0] || !defined($$entry[1]) || $$entry[2] =~ /^option/); return ($prev, $cur, $index); } sub prev_index { my($pos, $delta) = @_; my ($entry); $pos = $#file if (!$pos); while (--$pos >= 0) { $entry = $file[$pos]; # skip deleted entries return $pos if ($$entry[0] && defined($$entry[1]) && $$entry[2] !~ /^option/ && (!$delta || !--$delta)); } return undef; } sub next_index { my($pos, $delta) = @_; my ($entry); $pos = $#file if (!$pos); while (++$pos <= $#file) { $entry = $file[$pos]; # skip deleted entries return $pos if ($$entry[0] && defined($$entry[1]) && $$entry[2] !~ /^option/ && (!$delta || !--$delta)); } return undef; } sub set_option { my($key, $line) = @_; my($msg); # put the new value, if any, into the spare slot created when the file # was read into memory $file[1] = ["", "", $line] if ($line); # delete the old value if any $whiteclnt_cur_key = ""; $msg = chg_white_entry(\@file, \%dict, $key); give_up($msg) if ($msg); } # see if a form for an option was selected and process the result if so # The first arg is the name of the option. It is followed by # (form-value,file-value) pairs sub option_form { my($key, $new_formval, $formval, $fileval); $key = shift @_; $new_formval = $query{$key}; return if (!$new_formval); if ($new_formval =~ /^Default/) { set_option("$key"); return; } while ($#_ > 0) { $formval = shift @_; $fileval = shift @_; if ($new_formval eq $formval) { set_option("$key", "option $fileval\n"); return; } } give_up("invalid setting $key='$new_formval'"); } sub finish { print_form_file("

" . html_str_encode($_[0]) . "\n"); } sub give_up { print_form_file("

" . html_str_encode($_[0]) . "\n"); } # You cannot use real HTML 4 buttons because Microsoft gets them all wrong. # Contrary to the standard, they return all type=submit buttons. # They also return any text label instead of the value, thereby removing # most or all reason to use

$label\n"; print_form_start(" ", "", "
"); print_button("\t", $nm, $dis_def, $def_label); print_button("\t", $nm, $dis_on, $on_label); print_button("\t", $nm, $dis_off, $off_label); print "\t
\n"; } sub print_str { my($lineno, $leader, $str) = @_; while ($str =~ s/(.*\n?)// && $1) { my $line = $1; if ($line =~ /\n/) { ++$lineno; } else { $line .= "\n"; $leader .= "? "; } print $lineno if ($query{debug}); print $leader; print $line; } return $lineno; } sub print_option { my($field, $label, $value) = @_; my($s); $s = ""; if ($query{$field}) { if (defined($value) && $query{$field} =~ /^$value$/i) { $s = " selected" } elsif ($query{$field} =~ /^$label$/i) { $s = " selected"; } } if (defined($value)) { $value = " value=\"$value\""; } else { $value = ""; } print "\t \n"; } # display the current contents of the whiteclnt file # It is represented as an array or list of references to 3-tuples. # The first of the three is the whitelist entry in a canonical form # as a key uniquely identifying the entry. # The second is a comment string of zero or more comment lines. # The third is the DCC whiteclnt entry. # # The canonical form and the whiteclnt line of the first 3-tuple for a file # are null, because it contains the comments, if any, before the file's # preamble of dates when the file has been changed and flags. # The file[1] entry is an empty slot for adding option settings. # The last triple in a file may also lack a whitelist entry. sub print_whiteclnt_file { my($result, $locked) = @_; my($preamble, $str, $url, $entry, $lineno, $in_pre, $leader, $end_select, $tgt_key, $prev_key); $url = $edit_link . $url_ques; $url .= "msg=" . $query{msg} . "&" if ($query{msg}); $url .= "key="; $tgt_key = defined($cur_pos) ? ${$file[$cur_pos]}[0] : $cur_key; # try to find an entry before the current entry to start the display # in the browser's window if ($tgt_key) { my @prev_keys; foreach $entry (@file) { # ignore deleted lines, options, and include lines next if (!$$entry[0] || !defined($$entry[1]) || $$entry[2] =~ /^option/); shift(@prev_keys) if ($#prev_keys >= 2); push(@prev_keys, $$entry[0]); last if ($$entry[0] eq $tgt_key); } $prev_key = shift(@prev_keys); } $lineno = 1; foreach $entry (@file) { # do not list deleted entries next if (!defined($$entry[1])); # no options if not debugging next if ($$entry[2] =~ /^option/ && !$query{debug}); # tell the browser that the form will be soon if ($prev_key && $$entry[0] && $$entry[0] eq $prev_key) { print "
"; $form_marked = 1; } # mark the currently selected entry if ($tgt_key && $$entry[0] && $$entry[0] eq $tgt_key) { print ""; $leader = " ¦\t"; $end_select = 1; } else { $leader = "\t"; $end_select = undef; } if ($query{debug}) { if ($in_pre) { $in_pre = undef; print ""; } print "

" if ($query{debug}); } if (!$in_pre) { $in_pre = 1; print "

";
	}

	# display comment lines
	$str = $$entry[1];
	if (!$preamble) {
	    # Display the preamble parameters after comments in first triple
	    #	but before the ultimate blank line in the comments, if present.
	    $preamble = $whiteclnt_version;
	    $preamble .= $whiteclnt_notify;
	    $preamble .= $whiteclnt_lock;
	    $preamble .= "#webuser cur_key $whiteclnt_cur_key\n"
		if ($whiteclnt_cur_key);
	    $preamble .= $whiteclnt_change_log;
	    $str .= $preamble if ($query{debug});
	}
	$lineno = print_str($lineno, $leader, html_str_encode($str));

	$str = $$entry[2];
	if ($$entry[0] && $$entry[2] !~ /^option/) {
	    # Display an ordinary entry as a link for editing.
	    chomp($str);
	    # Suppress "substitute" noise
	    $str =~ s/^(\S*\s+)substitute\s+/$1/;
	    # use tab for blanks between the type and value
	    $str =~ s/^(\S+)\s+(\S+)\s+/$1\t$2\t/;
	    # make columns
	    $str =~ s/^(\S+\s+\S{1,7})\t/$1\t\t/;
	    $str = $url . url_encode($$entry[0]) . "#cur_key\">"
		    . html_str_encode($str)  . "\n";
	} else {
	    # just display option lines
	    $str = html_str_encode($str);
	}
	$lineno = print_str($lineno, $leader, $str);

	# put the editing form after the selected entry
	if ($end_select) {
	    print "
\n"; $in_pre = undef; print_entry_form($locked, $result); } } print "\n" if ($in_pre); print_entry_form($locked, $result) if (!$have_entry_form); close(WHITECLNT); html_footer(); print "\n\n"; exit; }